aboutsummaryrefslogtreecommitdiffstats
path: root/cli/test
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-12 23:49:19 +0200
committeruser <user@clank>2026-08-12 23:49:19 +0200
commitacf61609ee7b99138251cea879d593d5f1689392 (patch)
tree06f880e8c2e312e1a600c4ee0a63943c29eb902f /cli/test
parentrelease: publish sl0ppi.sl0p.foo, and a script to keep it current (diff)
downloadsl0ppi-acf61609ee7b99138251cea879d593d5f1689392.tar.gz
sl0ppi-acf61609ee7b99138251cea879d593d5f1689392.tar.xz
sl0ppi-acf61609ee7b99138251cea879d593d5f1689392.zip
cli: survive a dead session, and a flaky terminal
Three bugs found on a fresh Debian box, all of which made sl0ppi look broken when the terminal underneath it was the problem. 1. JSON crash on startup. `action list-panes --json` against a session that has died exits 0 with EMPTY stdout, and JSON.parse("") threw "JSON Parse error: Unexpected EOF" at the user. Empty output is now "nothing", and genuinely malformed output is reported with the command name and the offending text. 2. `up` silently resurrected. A dead-but-resurrectable session is not an empty slot: `attach --create-background NAME` restores it from the serialised copy and IGNORES --layout. The result was a root full of projects coming back as plain shells with no agents and no purposes -- and the resurrected background session then exited again within seconds. sl0ppi now refuses to guess and explains the two real options (`up --fresh` to rebuild, `sl0ppij attach` to restore it properly in a terminal). Verified on a clean slate that a fresh `up` produces exactly the right panes: purpose='agent:main' held=true cmd='pi'. 3. The terminal's CLI actions are racy. Measured against a LIVE session on a 2-vCPU box: list-tabs and list-panes each fail ~60% of the time while the session is detached, ~25% with a client attached, either with empty output or "There is no active session!" (the action client consults get_active_session() before it looks at -s). That is a defect in sl0ppij -- filed as a known issue -- but these are idempotent reads, so sl0ppi retries them with backoff and distinguishes "the session is gone" from "the terminal would not answer". `sl0ppi ls` went from failing most runs to 8/8. Also: - session creation returns before the server is registered, so `up` now waits for readiness instead of racing its own next command - UserError: actionable answers ("no project foo", "that session is a saved dead copy") print and exit 1 instead of falling through to sl0ppij, which buried the explanation and started a session nobody asked for - `sl0ppi print <project>` implemented; it was referenced by the init template and the design doc but did not exist
Diffstat (limited to 'cli/test')
-rw-r--r--cli/test/sessions.test.ts39
1 files changed, 39 insertions, 0 deletions
diff --git a/cli/test/sessions.test.ts b/cli/test/sessions.test.ts
new file mode 100644
index 0000000..00e55a2
--- /dev/null
+++ b/cli/test/sessions.test.ts
@@ -0,0 +1,39 @@
+import { describe, expect, test } from "bun:test";
+import { parseJsonList } from "../src/core/sl0ppij.ts";
+
+// The crash a user actually hit: `action list-panes --json` on a session that
+// died underneath us exits 0 with EMPTY stdout, and JSON.parse("") threw
+// "JSON Parse error: Unexpected EOF" at them on startup.
+describe("parseJsonList", () => {
+ test("empty output is nothing, not a crash", () => {
+ expect(parseJsonList("", "list-panes")).toEqual([]);
+ expect(parseJsonList(" \n", "list-panes")).toEqual([]);
+ });
+
+ test("parses a normal listing", () => {
+ expect(parseJsonList('[{"position":0}]', "list-tabs")).toEqual([{ position: 0 }] as never);
+ });
+
+ test("genuinely malformed output names the command and shows the text", () => {
+ expect(() => parseJsonList("There is no active session!", "list-tabs")).toThrow(
+ /list-tabs returned something that is not JSON: There is no active session!/,
+ );
+ });
+
+ test("a non-array payload is treated as nothing rather than crashing later", () => {
+ expect(parseJsonList('{"error":"nope"}', "list-tabs")).toEqual([]);
+ });
+});
+
+import { UserError } from "../src/core/errors.ts";
+
+// Actionable answers must not be replaced by a raw multiplexer. Falling through
+// on "no project called foo" hides the answer AND starts a session nobody asked
+// for.
+describe("UserError", () => {
+ test("is distinguishable from a bug", () => {
+ expect(new UserError("nope") instanceof UserError).toBe(true);
+ expect(new UserError("nope") instanceof Error).toBe(true);
+ expect(new Error("boom") instanceof UserError).toBe(false);
+ });
+});