@calltelemetry/openclaw-linear 0.9.11 → 0.9.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1630,621 +1630,120 @@ describe("checkFilesAndDirs — dispatch state non-Error exception", () => {
1630
1630
  });
1631
1631
 
1632
1632
  // ---------------------------------------------------------------------------
1633
- // checkFilesAndDirs — lock file branches
1633
+ // checkFilesAndDirs — CLAUDE.md and AGENTS.md checks
1634
1634
  // ---------------------------------------------------------------------------
1635
1635
 
1636
- describe("checkFilesAndDirs — lock file branches", () => {
1636
+ describe("checkFilesAndDirs — CLAUDE.md and AGENTS.md", () => {
1637
1637
  let tmpDir: string;
1638
1638
 
1639
1639
  beforeEach(() => {
1640
- tmpDir = mkdtempSync(join(tmpdir(), "doctor-lock-"));
1640
+ tmpDir = mkdtempSync(join(tmpdir(), "doctor-md-"));
1641
+ const { execFileSync } = require("node:child_process");
1642
+ execFileSync("git", ["init"], { cwd: tmpDir, stdio: "ignore" });
1641
1643
  });
1642
1644
 
1643
- it("warns about stale lock file without --fix", async () => {
1644
- const statePath = join(tmpDir, "state.json");
1645
- const lockPath = statePath + ".lock";
1646
- writeFileSync(statePath, '{}');
1647
- writeFileSync(lockPath, "locked");
1648
- // Make the lock file appear stale by backdating its mtime
1649
- const staleTime = Date.now() - 60_000; // 60 seconds old (> 30s LOCK_STALE_MS)
1650
- const { utimesSync } = await import("node:fs");
1651
- utimesSync(lockPath, staleTime / 1000, staleTime / 1000);
1652
-
1653
- vi.mocked(readDispatchState).mockResolvedValueOnce({
1654
- dispatches: { active: {}, completed: {} },
1655
- sessionMap: {},
1656
- processedEvents: [],
1657
- });
1658
-
1659
- const checks = await checkFilesAndDirs({ dispatchStatePath: statePath }, false);
1660
- const lockCheck = checks.find((c) => c.label.includes("Stale lock file"));
1661
- expect(lockCheck?.severity).toBe("warn");
1662
- expect(lockCheck?.fixable).toBe(true);
1645
+ it("passes when CLAUDE.md exists in base repo", async () => {
1646
+ writeFileSync(join(tmpDir, "CLAUDE.md"), "# My Project\n## Tech Stack\n- TypeScript");
1647
+ const checks = await checkFilesAndDirs({ codexBaseRepo: tmpDir });
1648
+ const claudeCheck = checks.find((c) => c.label.includes("CLAUDE.md found"));
1649
+ expect(claudeCheck).toBeDefined();
1650
+ expect(claudeCheck?.severity).toBe("pass");
1663
1651
  });
1664
1652
 
1665
- it("removes stale lock file with --fix", async () => {
1666
- const statePath = join(tmpDir, "state.json");
1667
- const lockPath = statePath + ".lock";
1668
- writeFileSync(statePath, '{}');
1669
- writeFileSync(lockPath, "locked");
1670
- const staleTime = Date.now() - 60_000;
1671
- const { utimesSync } = await import("node:fs");
1672
- utimesSync(lockPath, staleTime / 1000, staleTime / 1000);
1673
-
1674
- vi.mocked(readDispatchState).mockResolvedValueOnce({
1675
- dispatches: { active: {}, completed: {} },
1676
- sessionMap: {},
1677
- processedEvents: [],
1678
- });
1679
-
1680
- const checks = await checkFilesAndDirs({ dispatchStatePath: statePath }, true);
1681
- const lockCheck = checks.find((c) => c.label.includes("Stale lock file removed"));
1682
- expect(lockCheck?.severity).toBe("pass");
1683
- expect(existsSync(lockPath)).toBe(false);
1653
+ it("warns when CLAUDE.md is missing from base repo", async () => {
1654
+ const checks = await checkFilesAndDirs({ codexBaseRepo: tmpDir });
1655
+ const claudeCheck = checks.find((c) => c.label.includes("No CLAUDE.md"));
1656
+ expect(claudeCheck).toBeDefined();
1657
+ expect(claudeCheck?.severity).toBe("warn");
1658
+ expect(claudeCheck?.fix).toContain("CLAUDE.md");
1684
1659
  });
1685
1660
 
1686
- it("warns about active (non-stale) lock file", async () => {
1687
- const statePath = join(tmpDir, "state.json");
1688
- const lockPath = statePath + ".lock";
1689
- writeFileSync(statePath, '{}');
1690
- writeFileSync(lockPath, "locked");
1691
- // Lock file is fresh (just created), so lockAge < LOCK_STALE_MS
1692
-
1693
- vi.mocked(readDispatchState).mockResolvedValueOnce({
1694
- dispatches: { active: {}, completed: {} },
1695
- sessionMap: {},
1696
- processedEvents: [],
1697
- });
1698
-
1699
- const checks = await checkFilesAndDirs({ dispatchStatePath: statePath }, false);
1700
- const lockCheck = checks.find((c) => c.label.includes("Lock file active"));
1701
- expect(lockCheck?.severity).toBe("warn");
1702
- expect(lockCheck?.label).toContain("may be in use");
1661
+ it("passes when AGENTS.md exists in base repo", async () => {
1662
+ writeFileSync(join(tmpDir, "AGENTS.md"), "# Agent Guidelines\n## Code Style");
1663
+ const checks = await checkFilesAndDirs({ codexBaseRepo: tmpDir });
1664
+ const agentsCheck = checks.find((c) => c.label.includes("AGENTS.md found"));
1665
+ expect(agentsCheck).toBeDefined();
1666
+ expect(agentsCheck?.severity).toBe("pass");
1703
1667
  });
1704
- });
1705
-
1706
- // ---------------------------------------------------------------------------
1707
- // checkFilesAndDirs — prompt variable edge cases
1708
- // ---------------------------------------------------------------------------
1709
1668
 
1710
- describe("checkFilesAndDirs prompt variable edge cases", () => {
1711
- it("reports when variable missing from worker.task but present in audit.task", async () => {
1712
- vi.mocked(loadPrompts).mockReturnValueOnce({
1713
- worker: {
1714
- system: "ok",
1715
- task: "Do something", // missing all vars
1716
- },
1717
- audit: {
1718
- system: "ok",
1719
- task: "Audit {{identifier}} {{title}} {{description}} in {{worktreePath}}", // has all vars
1720
- },
1721
- rework: { addendum: "Fix these gaps: {{gaps}}" },
1722
- } as any);
1723
-
1724
- const checks = await checkFilesAndDirs();
1725
- const promptCheck = checks.find((c) => c.label.includes("Prompt issues"));
1726
- expect(promptCheck?.severity).toBe("fail");
1727
- expect(promptCheck?.label).toContain("worker.task missing");
1728
- // Crucially, audit.task should NOT be missing
1729
- expect(promptCheck?.label).not.toContain("audit.task missing");
1669
+ it("warns when AGENTS.md is missing from base repo", async () => {
1670
+ const checks = await checkFilesAndDirs({ codexBaseRepo: tmpDir });
1671
+ const agentsCheck = checks.find((c) => c.label.includes("No AGENTS.md"));
1672
+ expect(agentsCheck).toBeDefined();
1673
+ expect(agentsCheck?.severity).toBe("warn");
1674
+ expect(agentsCheck?.fix).toContain("AGENTS.md");
1730
1675
  });
1731
1676
 
1732
- it("reports loadPrompts throwing non-Error value", async () => {
1733
- vi.mocked(loadPrompts).mockImplementationOnce(() => { throw "raw string error"; });
1677
+ it("passes both when both files exist", async () => {
1678
+ writeFileSync(join(tmpDir, "CLAUDE.md"), "# Project");
1679
+ writeFileSync(join(tmpDir, "AGENTS.md"), "# Guidelines");
1680
+ const checks = await checkFilesAndDirs({ codexBaseRepo: tmpDir });
1681
+ const claudeCheck = checks.find((c) => c.label.includes("CLAUDE.md found"));
1682
+ const agentsCheck = checks.find((c) => c.label.includes("AGENTS.md found"));
1683
+ expect(claudeCheck?.severity).toBe("pass");
1684
+ expect(agentsCheck?.severity).toBe("pass");
1685
+ });
1734
1686
 
1735
- const checks = await checkFilesAndDirs();
1736
- const promptCheck = checks.find((c) => c.label.includes("Failed to load prompts"));
1737
- expect(promptCheck?.severity).toBe("fail");
1738
- expect(promptCheck?.detail).toContain("raw string error");
1687
+ it("shows file size in KB when CLAUDE.md exists", async () => {
1688
+ writeFileSync(join(tmpDir, "CLAUDE.md"), "x".repeat(2048));
1689
+ const checks = await checkFilesAndDirs({ codexBaseRepo: tmpDir });
1690
+ const claudeCheck = checks.find((c) => c.label.includes("CLAUDE.md found"));
1691
+ expect(claudeCheck?.label).toContain("2KB");
1739
1692
  });
1740
1693
  });
1741
1694
 
1742
1695
  // ---------------------------------------------------------------------------
1743
- // checkFilesAndDirs — worktree & base repo edge cases
1696
+ // checkFilesAndDirs — multi-repo path validation
1744
1697
  // ---------------------------------------------------------------------------
1745
1698
 
1746
- describe("checkFilesAndDirs — worktree & base repo edge cases", () => {
1747
- it("reports worktree base dir does not exist", async () => {
1748
- const checks = await checkFilesAndDirs({
1749
- worktreeBaseDir: "/tmp/nonexistent-worktree-dir-" + Date.now(),
1750
- });
1751
- const wtCheck = checks.find((c) => c.label.includes("Worktree base dir does not exist"));
1752
- expect(wtCheck?.severity).toBe("warn");
1753
- expect(wtCheck?.detail).toContain("Will be created on first dispatch");
1699
+ describe("checkFilesAndDirs — multi-repo validation", () => {
1700
+ let tmpDir: string;
1701
+
1702
+ beforeEach(() => {
1703
+ tmpDir = mkdtempSync(join(tmpdir(), "doctor-repos-"));
1704
+ const { execFileSync } = require("node:child_process");
1705
+ execFileSync("git", ["init"], { cwd: tmpDir, stdio: "ignore" });
1754
1706
  });
1755
1707
 
1756
- it("reports base repo does not exist", async () => {
1708
+ it("passes for valid git repo paths", async () => {
1709
+ const repoDir = join(tmpDir, "myrepo");
1710
+ mkdirSync(repoDir);
1711
+ const { execFileSync } = require("node:child_process");
1712
+ execFileSync("git", ["init"], { cwd: repoDir, stdio: "ignore" });
1713
+
1757
1714
  const checks = await checkFilesAndDirs({
1758
- codexBaseRepo: "/tmp/nonexistent-repo-" + Date.now(),
1715
+ codexBaseRepo: tmpDir,
1716
+ repos: { frontend: repoDir },
1759
1717
  });
1760
- const repoCheck = checks.find((c) => c.label.includes("Base repo does not exist"));
1761
- expect(repoCheck?.severity).toBe("fail");
1762
- expect(repoCheck?.fix).toContain("codexBaseRepo");
1718
+ const repoCheck = checks.find((c) => c.label.includes('Repo "frontend"'));
1719
+ expect(repoCheck).toBeDefined();
1720
+ expect(repoCheck?.severity).toBe("pass");
1721
+ expect(repoCheck?.label).toContain("valid git repo");
1763
1722
  });
1764
1723
 
1765
- it("reports base repo exists but is not a git repo", async () => {
1766
- const tmpDir = mkdtempSync(join(tmpdir(), "doctor-nongit-"));
1724
+ it("fails for non-existent repo paths", async () => {
1767
1725
  const checks = await checkFilesAndDirs({
1768
1726
  codexBaseRepo: tmpDir,
1727
+ repos: { backend: "/tmp/nonexistent-repo-path-12345" },
1769
1728
  });
1770
- const repoCheck = checks.find((c) => c.label.includes("Base repo is not a git repo"));
1729
+ const repoCheck = checks.find((c) => c.label.includes('Repo "backend"'));
1730
+ expect(repoCheck).toBeDefined();
1771
1731
  expect(repoCheck?.severity).toBe("fail");
1772
- expect(repoCheck?.fix).toContain("git init");
1732
+ expect(repoCheck?.label).toContain("does not exist");
1773
1733
  });
1774
- });
1775
-
1776
- // ---------------------------------------------------------------------------
1777
- // checkFilesAndDirs — tilde path resolution branches
1778
- // ---------------------------------------------------------------------------
1779
-
1780
- describe("checkFilesAndDirs — tilde path resolution", () => {
1781
- it("resolves ~/... dispatch state path", async () => {
1782
- vi.mocked(readDispatchState).mockRejectedValueOnce(new Error("ENOENT"));
1783
1734
 
1784
- // Providing a path with ~/ triggers the tilde resolution branch
1785
- const checks = await checkFilesAndDirs({
1786
- dispatchStatePath: "~/nonexistent-state-file.json",
1787
- });
1788
- // The file won't exist (tilde-resolved), so we get the "no file yet" message
1789
- const stateCheck = checks.find((c) => c.label.includes("Dispatch state"));
1790
- expect(stateCheck).toBeDefined();
1791
- });
1735
+ it("fails for paths that exist but are not git repos", async () => {
1736
+ // Create a dir outside the git repo so git rev-parse fails
1737
+ const nonGitDir = mkdtempSync(join(tmpdir(), "doctor-nongit-"));
1792
1738
 
1793
- it("resolves ~/... worktree base dir path", async () => {
1794
1739
  const checks = await checkFilesAndDirs({
1795
- worktreeBaseDir: "~/nonexistent-worktree-base",
1796
- });
1797
- const wtCheck = checks.find((c) => c.label.includes("Worktree base dir"));
1798
- expect(wtCheck).toBeDefined();
1799
- });
1800
- });
1801
-
1802
- // ---------------------------------------------------------------------------
1803
- // checkDispatchHealth — orphaned worktree singular
1804
- // ---------------------------------------------------------------------------
1805
-
1806
- describe("checkDispatchHealth — edge cases", () => {
1807
- it("reports single orphaned worktree (singular)", async () => {
1808
- vi.mocked(listWorktrees).mockReturnValueOnce([
1809
- { issueIdentifier: "ORPHAN-1", path: "/tmp/wt1" } as any,
1810
- ]);
1811
-
1812
- const checks = await checkDispatchHealth();
1813
- const orphanCheck = checks.find((c) => c.label.includes("orphaned worktree"));
1814
- expect(orphanCheck?.severity).toBe("warn");
1815
- expect(orphanCheck?.label).toContain("1 orphaned worktree");
1816
- expect(orphanCheck?.label).not.toContain("worktrees"); // singular, not plural
1817
- });
1818
-
1819
- it("prunes multiple old completed dispatches (plural)", async () => {
1820
- vi.mocked(readDispatchState).mockResolvedValueOnce({
1821
- dispatches: {
1822
- active: {},
1823
- completed: {
1824
- "A-1": { completedAt: new Date(Date.now() - 10 * 24 * 3_600_000).toISOString() } as any,
1825
- "A-2": { completedAt: new Date(Date.now() - 10 * 24 * 3_600_000).toISOString() } as any,
1826
- },
1827
- },
1828
- sessionMap: {},
1829
- processedEvents: [],
1740
+ codexBaseRepo: tmpDir,
1741
+ repos: { api: nonGitDir },
1830
1742
  });
1831
- vi.mocked(pruneCompleted).mockResolvedValueOnce(2);
1832
-
1833
- const checks = await checkDispatchHealth(undefined, true);
1834
- const pruneCheck = checks.find((c) => c.label.includes("Pruned"));
1835
- expect(pruneCheck?.severity).toBe("pass");
1836
- expect(pruneCheck?.label).toContain("2 old completed dispatches");
1837
- });
1838
-
1839
- it("reports single stale dispatch (singular)", async () => {
1840
- vi.mocked(listStaleDispatches).mockReturnValueOnce([
1841
- { issueIdentifier: "API-1", status: "working" } as any,
1842
- ]);
1843
-
1844
- const checks = await checkDispatchHealth();
1845
- const staleCheck = checks.find((c) => c.label.includes("stale dispatch"));
1846
- expect(staleCheck?.severity).toBe("warn");
1847
- // Singular: "1 stale dispatch" not "1 stale dispatches"
1848
- expect(staleCheck?.label).toMatch(/1 stale dispatch(?!es)/);
1849
- });
1850
- });
1851
-
1852
- // ---------------------------------------------------------------------------
1853
- // checkConnectivity — webhook self-test with ok but body !== "ok"
1854
- // ---------------------------------------------------------------------------
1855
-
1856
- describe("checkConnectivity — webhook non-ok body", () => {
1857
- it("warns when webhook returns ok status but body is not 'ok'", async () => {
1858
- vi.stubGlobal("fetch", vi.fn(async (url: string) => {
1859
- if (url.includes("localhost")) {
1860
- return { ok: true, text: async () => "pong" };
1861
- }
1862
- throw new Error("unexpected");
1863
- }));
1864
-
1865
- const checks = await checkConnectivity({}, { viewer: { name: "T" } });
1866
- const webhookCheck = checks.find((c) => c.label.includes("Webhook self-test:"));
1867
- // ok is true but body is "pong" not "ok" — the condition is `res.ok && body === "ok"`
1868
- // Since body !== "ok", it falls into the warn branch
1869
- expect(webhookCheck?.severity).toBe("warn");
1870
- });
1871
- });
1872
-
1873
- // ---------------------------------------------------------------------------
1874
- // formatReport — icon function TTY branches
1875
- // ---------------------------------------------------------------------------
1876
-
1877
- describe("formatReport — TTY icon rendering", () => {
1878
- it("renders colored icons when stdout.isTTY is true", () => {
1879
- const origIsTTY = process.stdout.isTTY;
1880
- try {
1881
- Object.defineProperty(process.stdout, "isTTY", { value: true, writable: true, configurable: true });
1882
-
1883
- const report = {
1884
- sections: [{
1885
- name: "Test",
1886
- checks: [
1887
- { label: "pass check", severity: "pass" as const },
1888
- { label: "warn check", severity: "warn" as const },
1889
- { label: "fail check", severity: "fail" as const },
1890
- ],
1891
- }],
1892
- summary: { passed: 1, warnings: 1, errors: 1 },
1893
- };
1894
-
1895
- const output = formatReport(report);
1896
- // TTY output includes ANSI escape codes
1897
- expect(output).toContain("\x1b[32m"); // green for pass
1898
- expect(output).toContain("\x1b[33m"); // yellow for warn
1899
- expect(output).toContain("\x1b[31m"); // red for fail
1900
- } finally {
1901
- Object.defineProperty(process.stdout, "isTTY", { value: origIsTTY, writable: true, configurable: true });
1902
- }
1743
+ const repoCheck = checks.find((c) => c.label.includes('Repo "api"'));
1744
+ expect(repoCheck).toBeDefined();
1745
+ expect(repoCheck?.severity).toBe("fail");
1746
+ expect(repoCheck?.label).toContain("not a git repo");
1903
1747
  });
1904
1748
  });
1905
1749
 
1906
- // ---------------------------------------------------------------------------
1907
- // checkCodingTools — codingTool fallback to "codex" in label
1908
- // ---------------------------------------------------------------------------
1909
-
1910
- describe("checkCodingTools — codingTool null fallback", () => {
1911
- it("shows 'codex' as default when codingTool is undefined but backends exist", () => {
1912
- vi.mocked(loadCodingConfig).mockReturnValueOnce({
1913
- codingTool: undefined,
1914
- backends: { codex: { aliases: ["codex"] } },
1915
- } as any);
1916
-
1917
- const checks = checkCodingTools();
1918
- const configCheck = checks.find((c) => c.label.includes("coding-tools.json loaded"));
1919
- expect(configCheck?.severity).toBe("pass");
1920
- expect(configCheck?.label).toContain("codex"); // falls back to "codex" via ??
1921
- });
1922
- });
1923
-
1924
- // ---------------------------------------------------------------------------
1925
- // checkFilesAndDirs — dispatch state non-Error catch branch
1926
- // ---------------------------------------------------------------------------
1927
-
1928
- describe("checkFilesAndDirs — dispatch state non-Error exception", () => {
1929
- it("handles non-Error thrown during dispatch state read", async () => {
1930
- const tmpDir = mkdtempSync(join(tmpdir(), "doctor-nonError-"));
1931
- const statePath = join(tmpDir, "state.json");
1932
- writeFileSync(statePath, '{}');
1933
- vi.mocked(readDispatchState).mockRejectedValueOnce("raw string dispatch error");
1934
-
1935
- const checks = await checkFilesAndDirs({ dispatchStatePath: statePath });
1936
- const stateCheck = checks.find((c) => c.label.includes("Dispatch state corrupt"));
1937
- expect(stateCheck?.severity).toBe("fail");
1938
- expect(stateCheck?.detail).toContain("raw string dispatch error");
1939
- });
1940
- });
1941
-
1942
- // ---------------------------------------------------------------------------
1943
- // checkFilesAndDirs — lock file branches
1944
- // ---------------------------------------------------------------------------
1945
-
1946
- describe("checkFilesAndDirs — lock file branches", () => {
1947
- let tmpDir: string;
1948
-
1949
- beforeEach(() => {
1950
- tmpDir = mkdtempSync(join(tmpdir(), "doctor-lock-"));
1951
- });
1952
-
1953
- it("warns about stale lock file without --fix", async () => {
1954
- const statePath = join(tmpDir, "state.json");
1955
- const lockPath = statePath + ".lock";
1956
- writeFileSync(statePath, '{}');
1957
- writeFileSync(lockPath, "locked");
1958
- // Make the lock file appear stale by backdating its mtime
1959
- const staleTime = Date.now() - 60_000; // 60 seconds old (> 30s LOCK_STALE_MS)
1960
- const { utimesSync } = await import("node:fs");
1961
- utimesSync(lockPath, staleTime / 1000, staleTime / 1000);
1962
-
1963
- vi.mocked(readDispatchState).mockResolvedValueOnce({
1964
- dispatches: { active: {}, completed: {} },
1965
- sessionMap: {},
1966
- processedEvents: [],
1967
- });
1968
-
1969
- const checks = await checkFilesAndDirs({ dispatchStatePath: statePath }, false);
1970
- const lockCheck = checks.find((c) => c.label.includes("Stale lock file"));
1971
- expect(lockCheck?.severity).toBe("warn");
1972
- expect(lockCheck?.fixable).toBe(true);
1973
- });
1974
-
1975
- it("removes stale lock file with --fix", async () => {
1976
- const statePath = join(tmpDir, "state.json");
1977
- const lockPath = statePath + ".lock";
1978
- writeFileSync(statePath, '{}');
1979
- writeFileSync(lockPath, "locked");
1980
- const staleTime = Date.now() - 60_000;
1981
- const { utimesSync } = await import("node:fs");
1982
- utimesSync(lockPath, staleTime / 1000, staleTime / 1000);
1983
-
1984
- vi.mocked(readDispatchState).mockResolvedValueOnce({
1985
- dispatches: { active: {}, completed: {} },
1986
- sessionMap: {},
1987
- processedEvents: [],
1988
- });
1989
-
1990
- const checks = await checkFilesAndDirs({ dispatchStatePath: statePath }, true);
1991
- const lockCheck = checks.find((c) => c.label.includes("Stale lock file removed"));
1992
- expect(lockCheck?.severity).toBe("pass");
1993
- expect(existsSync(lockPath)).toBe(false);
1994
- });
1995
-
1996
- it("warns about active (non-stale) lock file", async () => {
1997
- const statePath = join(tmpDir, "state.json");
1998
- const lockPath = statePath + ".lock";
1999
- writeFileSync(statePath, '{}');
2000
- writeFileSync(lockPath, "locked");
2001
- // Lock file is fresh (just created), so lockAge < LOCK_STALE_MS
2002
-
2003
- vi.mocked(readDispatchState).mockResolvedValueOnce({
2004
- dispatches: { active: {}, completed: {} },
2005
- sessionMap: {},
2006
- processedEvents: [],
2007
- });
2008
-
2009
- const checks = await checkFilesAndDirs({ dispatchStatePath: statePath }, false);
2010
- const lockCheck = checks.find((c) => c.label.includes("Lock file active"));
2011
- expect(lockCheck?.severity).toBe("warn");
2012
- expect(lockCheck?.label).toContain("may be in use");
2013
- });
2014
- });
2015
-
2016
- // ---------------------------------------------------------------------------
2017
- // checkFilesAndDirs — prompt variable edge cases
2018
- // ---------------------------------------------------------------------------
2019
-
2020
- describe("checkFilesAndDirs — prompt variable edge cases", () => {
2021
- it("reports when variable missing from worker.task but present in audit.task", async () => {
2022
- vi.mocked(loadPrompts).mockReturnValueOnce({
2023
- worker: {
2024
- system: "ok",
2025
- task: "Do something", // missing all vars
2026
- },
2027
- audit: {
2028
- system: "ok",
2029
- task: "Audit {{identifier}} {{title}} {{description}} in {{worktreePath}}", // has all vars
2030
- },
2031
- rework: { addendum: "Fix these gaps: {{gaps}}" },
2032
- } as any);
2033
-
2034
- const checks = await checkFilesAndDirs();
2035
- const promptCheck = checks.find((c) => c.label.includes("Prompt issues"));
2036
- expect(promptCheck?.severity).toBe("fail");
2037
- expect(promptCheck?.label).toContain("worker.task missing");
2038
- // Crucially, audit.task should NOT be missing
2039
- expect(promptCheck?.label).not.toContain("audit.task missing");
2040
- });
2041
-
2042
- it("reports loadPrompts throwing non-Error value", async () => {
2043
- vi.mocked(loadPrompts).mockImplementationOnce(() => { throw "raw string error"; });
2044
-
2045
- const checks = await checkFilesAndDirs();
2046
- const promptCheck = checks.find((c) => c.label.includes("Failed to load prompts"));
2047
- expect(promptCheck?.severity).toBe("fail");
2048
- expect(promptCheck?.detail).toContain("raw string error");
2049
- });
2050
- });
2051
-
2052
- // ---------------------------------------------------------------------------
2053
- // checkFilesAndDirs — worktree & base repo edge cases
2054
- // ---------------------------------------------------------------------------
2055
-
2056
- describe("checkFilesAndDirs — worktree & base repo edge cases", () => {
2057
- it("reports worktree base dir does not exist", async () => {
2058
- const checks = await checkFilesAndDirs({
2059
- worktreeBaseDir: "/tmp/nonexistent-worktree-dir-" + Date.now(),
2060
- });
2061
- const wtCheck = checks.find((c) => c.label.includes("Worktree base dir does not exist"));
2062
- expect(wtCheck?.severity).toBe("warn");
2063
- expect(wtCheck?.detail).toContain("Will be created on first dispatch");
2064
- });
2065
-
2066
- it("reports base repo does not exist", async () => {
2067
- const checks = await checkFilesAndDirs({
2068
- codexBaseRepo: "/tmp/nonexistent-repo-" + Date.now(),
2069
- });
2070
- const repoCheck = checks.find((c) => c.label.includes("Base repo does not exist"));
2071
- expect(repoCheck?.severity).toBe("fail");
2072
- expect(repoCheck?.fix).toContain("codexBaseRepo");
2073
- });
2074
-
2075
- it("reports base repo exists but is not a git repo", async () => {
2076
- const tmpDir = mkdtempSync(join(tmpdir(), "doctor-nongit-"));
2077
- const checks = await checkFilesAndDirs({
2078
- codexBaseRepo: tmpDir,
2079
- });
2080
- const repoCheck = checks.find((c) => c.label.includes("Base repo is not a git repo"));
2081
- expect(repoCheck?.severity).toBe("fail");
2082
- expect(repoCheck?.fix).toContain("git init");
2083
- });
2084
- });
2085
-
2086
- // ---------------------------------------------------------------------------
2087
- // checkFilesAndDirs — tilde path resolution branches
2088
- // ---------------------------------------------------------------------------
2089
-
2090
- describe("checkFilesAndDirs — tilde path resolution", () => {
2091
- it("resolves ~/... dispatch state path", async () => {
2092
- vi.mocked(readDispatchState).mockRejectedValueOnce(new Error("ENOENT"));
2093
-
2094
- // Providing a path with ~/ triggers the tilde resolution branch
2095
- const checks = await checkFilesAndDirs({
2096
- dispatchStatePath: "~/nonexistent-state-file.json",
2097
- });
2098
- // The file won't exist (tilde-resolved), so we get the "no file yet" message
2099
- const stateCheck = checks.find((c) => c.label.includes("Dispatch state"));
2100
- expect(stateCheck).toBeDefined();
2101
- });
2102
-
2103
- it("resolves ~/... worktree base dir path", async () => {
2104
- const checks = await checkFilesAndDirs({
2105
- worktreeBaseDir: "~/nonexistent-worktree-base",
2106
- });
2107
- const wtCheck = checks.find((c) => c.label.includes("Worktree base dir"));
2108
- expect(wtCheck).toBeDefined();
2109
- });
2110
- });
2111
-
2112
- // ---------------------------------------------------------------------------
2113
- // checkDispatchHealth — orphaned worktree singular
2114
- // ---------------------------------------------------------------------------
2115
-
2116
- describe("checkDispatchHealth — edge cases", () => {
2117
- it("reports single orphaned worktree (singular)", async () => {
2118
- vi.mocked(listWorktrees).mockReturnValueOnce([
2119
- { issueIdentifier: "ORPHAN-1", path: "/tmp/wt1" } as any,
2120
- ]);
2121
-
2122
- const checks = await checkDispatchHealth();
2123
- const orphanCheck = checks.find((c) => c.label.includes("orphaned worktree"));
2124
- expect(orphanCheck?.severity).toBe("warn");
2125
- expect(orphanCheck?.label).toContain("1 orphaned worktree");
2126
- expect(orphanCheck?.label).not.toContain("worktrees"); // singular, not plural
2127
- });
2128
-
2129
- it("prunes multiple old completed dispatches (plural)", async () => {
2130
- vi.mocked(readDispatchState).mockResolvedValueOnce({
2131
- dispatches: {
2132
- active: {},
2133
- completed: {
2134
- "A-1": { completedAt: new Date(Date.now() - 10 * 24 * 3_600_000).toISOString() } as any,
2135
- "A-2": { completedAt: new Date(Date.now() - 10 * 24 * 3_600_000).toISOString() } as any,
2136
- },
2137
- },
2138
- sessionMap: {},
2139
- processedEvents: [],
2140
- });
2141
- vi.mocked(pruneCompleted).mockResolvedValueOnce(2);
2142
-
2143
- const checks = await checkDispatchHealth(undefined, true);
2144
- const pruneCheck = checks.find((c) => c.label.includes("Pruned"));
2145
- expect(pruneCheck?.severity).toBe("pass");
2146
- expect(pruneCheck?.label).toContain("2 old completed dispatches");
2147
- });
2148
-
2149
- it("reports single stale dispatch (singular)", async () => {
2150
- vi.mocked(listStaleDispatches).mockReturnValueOnce([
2151
- { issueIdentifier: "API-1", status: "working" } as any,
2152
- ]);
2153
-
2154
- const checks = await checkDispatchHealth();
2155
- const staleCheck = checks.find((c) => c.label.includes("stale dispatch"));
2156
- expect(staleCheck?.severity).toBe("warn");
2157
- // Singular: "1 stale dispatch" not "1 stale dispatches"
2158
- expect(staleCheck?.label).toMatch(/1 stale dispatch(?!es)/);
2159
- });
2160
- });
2161
-
2162
- // ---------------------------------------------------------------------------
2163
- // checkConnectivity — webhook self-test with ok but body !== "ok"
2164
- // ---------------------------------------------------------------------------
2165
-
2166
- describe("checkConnectivity — webhook non-ok body", () => {
2167
- it("warns when webhook returns ok status but body is not 'ok'", async () => {
2168
- vi.stubGlobal("fetch", vi.fn(async (url: string) => {
2169
- if (url.includes("localhost")) {
2170
- return { ok: true, text: async () => "pong" };
2171
- }
2172
- throw new Error("unexpected");
2173
- }));
2174
-
2175
- const checks = await checkConnectivity({}, { viewer: { name: "T" } });
2176
- const webhookCheck = checks.find((c) => c.label.includes("Webhook self-test:"));
2177
- // ok is true but body is "pong" not "ok" — the condition is `res.ok && body === "ok"`
2178
- // Since body !== "ok", it falls into the warn branch
2179
- expect(webhookCheck?.severity).toBe("warn");
2180
- });
2181
- });
2182
-
2183
- // ---------------------------------------------------------------------------
2184
- // formatReport — icon function TTY branches
2185
- // ---------------------------------------------------------------------------
2186
-
2187
- describe("formatReport — TTY icon rendering", () => {
2188
- it("renders colored icons when stdout.isTTY is true", () => {
2189
- const origIsTTY = process.stdout.isTTY;
2190
- try {
2191
- Object.defineProperty(process.stdout, "isTTY", { value: true, writable: true, configurable: true });
2192
-
2193
- const report = {
2194
- sections: [{
2195
- name: "Test",
2196
- checks: [
2197
- { label: "pass check", severity: "pass" as const },
2198
- { label: "warn check", severity: "warn" as const },
2199
- { label: "fail check", severity: "fail" as const },
2200
- ],
2201
- }],
2202
- summary: { passed: 1, warnings: 1, errors: 1 },
2203
- };
2204
-
2205
- const output = formatReport(report);
2206
- // TTY output includes ANSI escape codes
2207
- expect(output).toContain("\x1b[32m"); // green for pass
2208
- expect(output).toContain("\x1b[33m"); // yellow for warn
2209
- expect(output).toContain("\x1b[31m"); // red for fail
2210
- } finally {
2211
- Object.defineProperty(process.stdout, "isTTY", { value: origIsTTY, writable: true, configurable: true });
2212
- }
2213
- });
2214
- });
2215
-
2216
- // ---------------------------------------------------------------------------
2217
- // checkCodingTools — codingTool fallback to "codex" in label
2218
- // ---------------------------------------------------------------------------
2219
-
2220
- describe("checkCodingTools — codingTool null fallback", () => {
2221
- it("shows 'codex' as default when codingTool is undefined but backends exist", () => {
2222
- vi.mocked(loadCodingConfig).mockReturnValueOnce({
2223
- codingTool: undefined,
2224
- backends: { codex: { aliases: ["codex"] } },
2225
- } as any);
2226
-
2227
- const checks = checkCodingTools();
2228
- const configCheck = checks.find((c) => c.label.includes("coding-tools.json loaded"));
2229
- expect(configCheck?.severity).toBe("pass");
2230
- expect(configCheck?.label).toContain("codex"); // falls back to "codex" via ??
2231
- });
2232
- });
2233
-
2234
- // ---------------------------------------------------------------------------
2235
- // checkFilesAndDirs — dispatch state non-Error catch branch
2236
- // ---------------------------------------------------------------------------
2237
-
2238
- describe("checkFilesAndDirs — dispatch state non-Error exception", () => {
2239
- it("handles non-Error thrown during dispatch state read", async () => {
2240
- const tmpDir = mkdtempSync(join(tmpdir(), "doctor-nonError-"));
2241
- const statePath = join(tmpDir, "state.json");
2242
- writeFileSync(statePath, '{}');
2243
- vi.mocked(readDispatchState).mockRejectedValueOnce("raw string dispatch error");
2244
-
2245
- const checks = await checkFilesAndDirs({ dispatchStatePath: statePath });
2246
- const stateCheck = checks.find((c) => c.label.includes("Dispatch state corrupt"));
2247
- expect(stateCheck?.severity).toBe("fail");
2248
- expect(stateCheck?.detail).toContain("raw string dispatch error");
2249
- });
2250
- });