aboutsummaryrefslogtreecommitdiffstats
path: root/cli/test/doctor.test.ts
blob: 40eef79fea92ae3adb369facbeba7d5730dd49cc (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import { describe, expect, test } from "bun:test";
import { checkFragment, report, worstLevel, type Facts } from "../src/commands/doctor.ts";

function facts(overrides: Partial<Facts> = {}): 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();
  });
});