import { describe, expect, test } from "bun:test"; import { checkFragment, report, worstLevel, type Facts } from "../src/commands/doctor.ts"; function facts(overrides: Partial = {}): Facts { return { sl0ppijVersion: "sl0ppij 0.45.0+sl0p1", piVersion: "0.84.1", configDir: "/home/u/.config/sl0ppij", configFile: true, plugins: [ { name: "pane-finder.wasm", present: true }, { name: "zjstatus.wasm", present: true }, ], registry: { dev: "/home/u/dev" }, liveSessions: ["dev"], roots: [{ session: "dev", root: "/home/u/dev", exists: true, projects: 3 }], fragments: [], externalTools: [ { name: "python3", present: true, neededBy: "pir" }, { name: "fzf", present: true, neededBy: "pir" }, ], ...overrides, }; } describe("report", () => { test("a healthy install has nothing to say", () => { expect(worstLevel(report(facts()))).toBe("ok"); }); test("a missing sl0ppij is fatal, a missing pi is not", () => { expect(worstLevel(report(facts({ sl0ppijVersion: undefined })))).toBe("fail"); expect(worstLevel(report(facts({ piVersion: undefined })))).toBe("warn"); }); test("missing plugins warn, because the shipped layout needs them", () => { const checks = report( facts({ plugins: [{ name: "zjstatus.wasm", present: false }] }), ); const check = checks.find((c) => c.label.includes("zjstatus")); expect(check?.level).toBe("warn"); }); // The known packaging hole: pir ships without its runtime deps. test("warns about pir's undeclared dependencies", () => { const checks = report( facts({ externalTools: [ { name: "python3", present: false, neededBy: "pir" }, { name: "fzf", present: true, neededBy: "pir" }, ], }), ); expect(checks.find((c) => c.label === "python3")?.level).toBe("warn"); expect(checks.find((c) => c.label === "fzf")).toBeUndefined(); // present: silent }); // A missing root is the unmounted-filesystem case, which must never read as // "delete everything". test("a missing root warns and suggests mounting before removing", () => { const checks = report( facts({ roots: [{ session: "dev", root: "/mnt/dev", exists: false, projects: 0 }] }), ); const check = checks.find((c) => c.label === "root dev"); expect(check?.level).toBe("warn"); expect(check?.fix).toContain("mount"); }); test("reports whether each root's session is running", () => { const running = report(facts()).find((c) => c.label === "root dev"); expect(running?.detail).toContain("session live"); const stopped = report(facts({ liveSessions: [] })).find((c) => c.label === "root dev"); expect(stopped?.detail).toContain("not running"); }); test("surfaces a broken fragment", () => { const checks = report(facts({ fragments: [{ relPath: "alpha", issue: "unbalanced braces" }] })); expect(checks.find((c) => c.label === "layout alpha")?.level).toBe("warn"); }); }); describe("checkFragment", () => { test("accepts a plain fragment", () => { expect(checkFragment('pane purpose="agent:main"')).toBeUndefined(); }); test("accepts nested panes", () => { expect(checkFragment('pane split_direction="vertical" {\n pane\n pane\n}')).toBeUndefined(); }); test("treats a comments-only file as empty", () => { expect(checkFragment("// nothing here\n")).toContain("empty"); }); test("catches unbalanced braces", () => { expect(checkFragment("pane {\n pane\n")).toContain("unbalanced"); }); // The two shapes people will actually get wrong, because every zellij example // online is a whole layout. test("catches a whole layout pasted in as a fragment", () => { expect(checkFragment("layout {\n tab {\n pane\n }\n}")).toContain("pane nodes only"); }); test("catches a tab node, which sl0ppi owns", () => { expect(checkFragment('tab name="x" {\n pane\n}')).toContain("identity"); }); test("ignores braces inside comments", () => { expect(checkFragment('pane\n// pane split_direction="v" {')).toBeUndefined(); }); });