@calltelemetry/openclaw-linear 0.9.4 → 0.9.6

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.
@@ -1290,7 +1290,7 @@ describe("buildSummary — additional branches", () => {
1290
1290
  // ---------------------------------------------------------------------------
1291
1291
 
1292
1292
  describe("runDoctor — additional branches", () => {
1293
- it("applies --fix to auth-profiles.json permissions when fixable check exists", async () => {
1293
+ it("applies --fix to auth-profiles.json permissions when fixable check exists", async () => {
1294
1294
  // We need a scenario where checkAuth produces a fixable permissions check
1295
1295
  // The AUTH_PROFILES_PATH is mocked to /tmp/test-auth-profiles.json
1296
1296
  // Write a file there with wrong permissions so statSync succeeds
@@ -1938,3 +1938,313 @@ describe("checkFilesAndDirs — dispatch state non-Error exception", () => {
1938
1938
  expect(stateCheck?.detail).toContain("raw string dispatch error");
1939
1939
  });
1940
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
+ });