aboutsummaryrefslogtreecommitdiffstats
path: root/cli/test/cli.test.ts
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-12 22:24:55 +0200
committeruser <user@clank>2026-08-12 22:24:55 +0200
commit2cd247828d863cdc779c7178c988152835096ca2 (patch)
tree867b63f58057e81ac8ab4d242e73b733b8372c9e /cli/test/cli.test.ts
parentdocs(cli): record the fork gap the design turned up (diff)
downloadsl0ppi-2cd247828d863cdc779c7178c988152835096ca2.tar.gz
sl0ppi-2cd247828d863cdc779c7178c988152835096ca2.tar.xz
sl0ppi-2cd247828d863cdc779c7178c988152835096ca2.zip
cli: up, ls, open, close
Slice 2 of docs/cli-design.md. The commands read the world (discovery + list-tabs), compute a plan, then apply it -- never mutating while walking, which is how `up` stays idempotent. sl0ppij.ts process wrapper; every piece of state comes back out of list-tabs/list-panes, so there is nothing to desync layout.ts composes the tab node around a project's fragment root.ts root resolution, session naming, the root registry workspace.ts up / ls / open / close index.ts dispatch + the fail-open wrapper Four things this turned up that reading the code would not have: - `attach --create-background` fails with "There is no active session!" when the caller is itself inside a session -- which sl0ppi almost always is, being run from the user's daily-driver terminal. Session creation now runs with the SL0PPIJ*/ZELLIJ* markers stripped: we are creating a new detached session, not nesting. - falling open by passing our own argv to sl0ppij just produces "unrecognized subcommand: up" and still leaves the user without a terminal. Our verbs and flags are stripped first, so what is handed over is something sl0ppij actually understands. - attaching panics in the client when stdin is not a tty, so `up` from a script or CI step died. It now reports the session is ready and exits. - a start_suspended pane reports `is_held`, not `exited`. Conflating them made `ls` claim every agent was running when none had started. Plugin panes (the status bar, the link handler) also inflated pane counts and are now excluded as chrome. Index conventions were verified against a live session rather than inferred, because guessing closes the wrong tab: list-tabs `position` and `close-tab --tab-id` are 0-based, `go-to-tab` is 1-based, and closing renumbers every tab after it, so batched closes run in descending position order. Verified end to end against a real session (three projects, one shipping a custom fragment): up creates the session with all tabs and their purposes; up again reports "up to date" with tabs unchanged; ls reports panes and agent:suspended correctly; open creates-and-focuses and resolves unique prefixes; close removes a tab; deleting a project auto-closes its tab; and with the root itself missing, nothing is closed at all. 64 tests green.
Diffstat (limited to 'cli/test/cli.test.ts')
-rw-r--r--cli/test/cli.test.ts95
1 files changed, 95 insertions, 0 deletions
diff --git a/cli/test/cli.test.ts b/cli/test/cli.test.ts
new file mode 100644
index 0000000..464ade4
--- /dev/null
+++ b/cli/test/cli.test.ts
@@ -0,0 +1,95 @@
+import { describe, expect, test } from "bun:test";
+import { parseArgs, passthroughArgs } from "../src/index.ts";
+import { agentStateOf } from "../src/commands/workspace.ts";
+import { withoutSessionEnv } from "../src/core/sl0ppij.ts";
+
+describe("parseArgs", () => {
+ test("defaults to up", () => {
+ expect(parseArgs([]).command).toBe("up");
+ expect(parseArgs(["--root", "/x"]).command).toBe("up");
+ });
+
+ test("recognises the verbs", () => {
+ for (const verb of ["up", "ls", "open", "close", "help"]) {
+ expect(parseArgs([verb]).command).toBe(verb);
+ }
+ });
+
+ test("keeps the project name as an argument", () => {
+ const args = parseArgs(["open", "myproj", "--root", "/x"]);
+ expect(args.command).toBe("open");
+ expect(args.rest).toEqual(["myproj"]);
+ expect(args.rootFlag).toBe("/x");
+ });
+
+ test("accepts both flag spellings", () => {
+ expect(parseArgs(["--root=/x"]).rootFlag).toBe("/x");
+ expect(parseArgs(["--depth=3"]).depth).toBe(3);
+ expect(parseArgs(["--depth", "2"]).depth).toBe(2);
+ expect(parseArgs(["-n"]).dryRun).toBe(true);
+ });
+
+ test("a bad depth falls back to 1 rather than NaN", () => {
+ expect(parseArgs(["--depth", "banana"]).depth).toBe(1);
+ });
+});
+
+// Falling open is only useful if what we hand over is something sl0ppij
+// understands. Passing `up` through just yields "unrecognized subcommand" and
+// still leaves the user without a terminal.
+describe("passthroughArgs", () => {
+ test("strips our verbs and flags", () => {
+ expect(passthroughArgs(["up", "--root", "/x"])).toEqual([]);
+ expect(passthroughArgs(["ls", "--depth=2"])).toEqual([]);
+ expect(passthroughArgs(["open", "proj", "-n"])).toEqual(["proj"]);
+ });
+
+ test("keeps arguments sl0ppij would understand", () => {
+ expect(passthroughArgs(["attach", "mysession"])).toEqual(["attach", "mysession"]);
+ expect(passthroughArgs(["--root", "/x", "list-sessions"])).toEqual(["list-sessions"]);
+ });
+
+ test("only the leading verb is dropped", () => {
+ // `sl0ppij action ... close` style words further along must survive
+ expect(passthroughArgs(["action", "close-tab"])).toEqual(["action", "close-tab"]);
+ });
+});
+
+describe("agentStateOf", () => {
+ // A start_suspended pane reports is_held, not exited: the command is staged
+ // but has never run. Conflating the two made `ls` claim twelve agents were
+ // running when none were.
+ test("held means suspended", () => {
+ expect(agentStateOf({ is_held: true })).toBe("suspended");
+ });
+ test("exited means exited", () => {
+ expect(agentStateOf({ exited: true })).toBe("exited");
+ });
+ test("neither means running", () => {
+ expect(agentStateOf({})).toBe("running");
+ });
+ test("absent means none", () => {
+ expect(agentStateOf(undefined)).toBe("none");
+ });
+});
+
+describe("withoutSessionEnv", () => {
+ // `attach --create-background` fails with "There is no active session!" when
+ // the caller is inside a session, and sl0ppi is nearly always run from one.
+ test("drops both spellings of the session markers", () => {
+ const cleaned = withoutSessionEnv({
+ ZELLIJ: "0",
+ ZELLIJ_SESSION_NAME: "inventive-drum",
+ SL0PPIJ: "0",
+ SL0PPIJ_SESSION_NAME: "dev",
+ SL0PPIJ_PANE_ID: "3",
+ PATH: "/usr/bin",
+ });
+ expect(cleaned.ZELLIJ).toBeUndefined();
+ expect(cleaned.ZELLIJ_SESSION_NAME).toBeUndefined();
+ expect(cleaned.SL0PPIJ).toBeUndefined();
+ expect(cleaned.SL0PPIJ_SESSION_NAME).toBeUndefined();
+ expect(cleaned.SL0PPIJ_PANE_ID).toBeUndefined();
+ expect(cleaned.PATH).toBe("/usr/bin");
+ });
+});