clawspec 1.0.15 → 1.0.19

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.
@@ -0,0 +1,142 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { mkdtemp, mkdir } from "node:fs/promises";
6
+ import { runDoctorCommand } from "../src/orchestrator/service.ts";
7
+ import { writeJsonFile, readJsonFile } from "../src/utils/fs.ts";
8
+
9
+ test("doctor reports no issues when config file does not exist", async () => {
10
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-doctor-"));
11
+ const configPath = path.join(tempRoot, ".acpx", "config.json");
12
+
13
+ const result = await runDoctorCommand(configPath);
14
+
15
+ assert.equal(result.isError, undefined);
16
+ assert.match(result.text ?? "", /No issues found/);
17
+ });
18
+
19
+ test("doctor reports no issues when agents is empty", async () => {
20
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-doctor-"));
21
+ const configDir = path.join(tempRoot, ".acpx");
22
+ await mkdir(configDir, { recursive: true });
23
+ const configPath = path.join(configDir, "config.json");
24
+
25
+ await writeJsonFile(configPath, {
26
+ defaultAgent: "codex",
27
+ agents: {},
28
+ });
29
+
30
+ const result = await runDoctorCommand(configPath);
31
+
32
+ assert.equal(result.isError, undefined);
33
+ assert.match(result.text ?? "", /No issues found/);
34
+ });
35
+
36
+ test("doctor detects custom agent entries", async () => {
37
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-doctor-"));
38
+ const configDir = path.join(tempRoot, ".acpx");
39
+ await mkdir(configDir, { recursive: true });
40
+ const configPath = path.join(configDir, "config.json");
41
+
42
+ await writeJsonFile(configPath, {
43
+ defaultAgent: "codex",
44
+ agents: {
45
+ codex: { command: "/usr/local/bin/codex" },
46
+ },
47
+ });
48
+
49
+ const result = await runDoctorCommand(configPath);
50
+
51
+ assert.equal(result.isError, undefined);
52
+ assert.match(result.text ?? "", /custom agent entries/);
53
+ assert.match(result.text ?? "", /`codex`/);
54
+ assert.match(result.text ?? "", /doctor fix/);
55
+ });
56
+
57
+ test("doctor reports no issues when config file is empty", async () => {
58
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-doctor-"));
59
+ const configDir = path.join(tempRoot, ".acpx");
60
+ await mkdir(configDir, { recursive: true });
61
+ const configPath = path.join(configDir, "config.json");
62
+
63
+ const { writeUtf8 } = await import("../src/utils/fs.ts");
64
+ await writeUtf8(configPath, "");
65
+
66
+ const result = await runDoctorCommand(configPath);
67
+
68
+ assert.equal(result.isError, undefined);
69
+ assert.match(result.text ?? "", /No issues found/);
70
+ });
71
+
72
+ test("doctor detects invalid JSON", async () => {
73
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-doctor-"));
74
+ const configDir = path.join(tempRoot, ".acpx");
75
+ await mkdir(configDir, { recursive: true });
76
+ const configPath = path.join(configDir, "config.json");
77
+
78
+ const { writeUtf8 } = await import("../src/utils/fs.ts");
79
+ await writeUtf8(configPath, '{ "agents": { broken }');
80
+
81
+ const result = await runDoctorCommand(configPath);
82
+
83
+ assert.equal(result.isError, undefined);
84
+ assert.match(result.text ?? "", /invalid JSON/);
85
+ });
86
+
87
+ test("doctor fix clears custom agent entries", async () => {
88
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-doctor-"));
89
+ const configDir = path.join(tempRoot, ".acpx");
90
+ await mkdir(configDir, { recursive: true });
91
+ const configPath = path.join(configDir, "config.json");
92
+
93
+ await writeJsonFile(configPath, {
94
+ defaultAgent: "codex",
95
+ authPolicy: "skip",
96
+ agents: {
97
+ codex: { command: "/usr/local/bin/codex" },
98
+ },
99
+ });
100
+
101
+ const result = await runDoctorCommand(configPath, "fix");
102
+
103
+ assert.equal(result.isError, undefined);
104
+ assert.match(result.text ?? "", /Doctor Fix Applied/);
105
+
106
+ const updated = await readJsonFile<Record<string, unknown>>(configPath, {});
107
+ assert.deepEqual(updated.agents, {});
108
+ assert.equal(updated.defaultAgent, "codex");
109
+ assert.equal(updated.authPolicy, "skip");
110
+ });
111
+
112
+ test("doctor fix reports nothing to fix when agents is already empty", async () => {
113
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-doctor-"));
114
+ const configDir = path.join(tempRoot, ".acpx");
115
+ await mkdir(configDir, { recursive: true });
116
+ const configPath = path.join(configDir, "config.json");
117
+
118
+ await writeJsonFile(configPath, {
119
+ defaultAgent: "codex",
120
+ agents: {},
121
+ });
122
+
123
+ const result = await runDoctorCommand(configPath, "fix");
124
+
125
+ assert.equal(result.isError, undefined);
126
+ assert.match(result.text ?? "", /Nothing to fix/);
127
+ });
128
+
129
+ test("doctor fix reports error when config has invalid JSON", async () => {
130
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-doctor-"));
131
+ const configDir = path.join(tempRoot, ".acpx");
132
+ await mkdir(configDir, { recursive: true });
133
+ const configPath = path.join(configDir, "config.json");
134
+
135
+ const { writeUtf8 } = await import("../src/utils/fs.ts");
136
+ await writeUtf8(configPath, '{ broken json');
137
+
138
+ const result = await runDoctorCommand(configPath, "fix");
139
+
140
+ assert.equal(result.isError, true);
141
+ assert.match(result.text ?? "", /Cannot auto-fix/);
142
+ });
@@ -55,13 +55,17 @@ export function createLogger() {
55
55
  };
56
56
  }
57
57
 
58
- export async function waitFor(check: () => Promise<boolean>, timeoutMs = 4_000): Promise<void> {
58
+ export async function waitFor(
59
+ check: () => Promise<boolean>,
60
+ timeoutMs = 4_000,
61
+ pollIntervalMs = 250,
62
+ ): Promise<void> {
59
63
  const startedAt = Date.now();
60
64
  while (Date.now() - startedAt < timeoutMs) {
61
65
  if (await check()) {
62
66
  return;
63
67
  }
64
- await new Promise((resolve) => setTimeout(resolve, 25));
68
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
65
69
  }
66
70
  throw new Error("Timed out waiting for test condition.");
67
71
  }
@@ -1,15 +1,18 @@
1
- import test from "node:test";
1
+ import test, { type TestContext } from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { mkdtemp, mkdir } from "node:fs/promises";
6
- import { pathExists, readUtf8, writeJsonFile, writeUtf8 } from "../src/utils/fs.ts";
6
+ import { pathExists, readJsonFile, readUtf8, writeJsonFile, writeUtf8 } from "../src/utils/fs.ts";
7
7
  import { getRepoStatePaths } from "../src/utils/paths.ts";
8
8
  import { RollbackStore } from "../src/rollback/store.ts";
9
9
  import { ProjectStateStore } from "../src/state/store.ts";
10
10
  import { WatcherManager } from "../src/watchers/manager.ts";
11
11
  import { createLogger, waitFor } from "./helpers/harness.ts";
12
12
 
13
+ const TEST_WATCHER_POLL_INTERVAL_MS = 250;
14
+ const TEST_WAIT_TIMEOUT_MS = 15_000;
15
+
13
16
  function createWorkOpenSpec(changeDir: string, tasksPath: string) {
14
17
  return {
15
18
  instructionsApply: async (cwd: string, cn: string) => {
@@ -39,7 +42,25 @@ function createWorkOpenSpec(changeDir: string, tasksPath: string) {
39
42
  } as any;
40
43
  }
41
44
 
42
- test("recovery from orphaned state (armed, no execution field)", async () => {
45
+ function registerManagerCleanup(t: TestContext, manager: WatcherManager): () => Promise<void> {
46
+ let stopped = false;
47
+ const stop = async () => {
48
+ if (stopped) {
49
+ return;
50
+ }
51
+ stopped = true;
52
+ await manager.stop();
53
+ };
54
+ t.after(stop);
55
+ return stop;
56
+ }
57
+
58
+ async function waitForProjectStatus(repoPath: string, status: "done", timeoutMs = TEST_WAIT_TIMEOUT_MS): Promise<void> {
59
+ const stateFile = getRepoStatePaths(repoPath, "archives").stateFile;
60
+ await waitFor(async () => (await readJsonFile<any>(stateFile, null))?.status === status, timeoutMs);
61
+ }
62
+
63
+ test("recovery from orphaned state (armed, no execution field)", async (t) => {
43
64
  const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-recovery-orphan-"));
44
65
  const workspacePath = path.join(tempRoot, "workspace");
45
66
  const repoPath = path.join(workspacePath, "demo-app");
@@ -105,12 +126,13 @@ test("recovery from orphaned state (armed, no execution field)", async () => {
105
126
  logger: createLogger(),
106
127
  notifier: { send: async (_: string, text: string) => { notifierMessages.push(text); } } as any,
107
128
  acpClient: fakeAcpClient as any,
108
- pollIntervalMs: 25,
129
+ pollIntervalMs: TEST_WATCHER_POLL_INTERVAL_MS,
109
130
  });
131
+ const stopManager = registerManagerCleanup(t, manager);
110
132
 
111
133
  await manager.start();
112
- await waitFor(async () => (await stateStore.getActiveProject(channelKey))?.status === "done");
113
- await manager.stop();
134
+ await waitForProjectStatus(repoPath, "done");
135
+ await stopManager();
114
136
 
115
137
  const project = await stateStore.getActiveProject(channelKey);
116
138
  assert.equal(project?.status, "done");
@@ -120,7 +142,7 @@ test("recovery from orphaned state (armed, no execution field)", async () => {
120
142
  assert.equal(await pathExists(path.join(clawspecDir, "state.json.12345.999999.tmp")), false);
121
143
  });
122
144
 
123
- test("recovery from mid-crash running state", async () => {
145
+ test("recovery from mid-crash running state", async (t) => {
124
146
  const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-recovery-midcrash-"));
125
147
  const workspacePath = path.join(tempRoot, "workspace");
126
148
  const repoPath = path.join(workspacePath, "demo-app");
@@ -178,19 +200,20 @@ test("recovery from mid-crash running state", async () => {
178
200
  logger: createLogger(),
179
201
  notifier: { send: async (_: string, text: string) => { notifierMessages.push(text); } } as any,
180
202
  acpClient: fakeAcpClient as any,
181
- pollIntervalMs: 25,
203
+ pollIntervalMs: TEST_WATCHER_POLL_INTERVAL_MS,
182
204
  });
205
+ const stopManager = registerManagerCleanup(t, manager);
183
206
 
184
207
  await manager.start();
185
- await waitFor(async () => (await stateStore.getActiveProject(channelKey))?.status === "done");
186
- await manager.stop();
208
+ await waitForProjectStatus(repoPath, "done");
209
+ await stopManager();
187
210
 
188
211
  const project = await stateStore.getActiveProject(channelKey);
189
212
  assert.equal(project?.status, "done");
190
213
  assert.equal(notifierMessages.some((m) => m.includes("Gateway restarted")), true);
191
214
  });
192
215
 
193
- test("startup recovery adopts a live implementation session instead of spawning a new worker", async () => {
216
+ test("startup recovery adopts a live implementation session instead of spawning a new worker", async (t) => {
194
217
  const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-recovery-adopt-live-"));
195
218
  const workspacePath = path.join(tempRoot, "workspace");
196
219
  const repoPath = path.join(workspacePath, "demo-app");
@@ -274,8 +297,9 @@ test("startup recovery adopts a live implementation session instead of spawning
274
297
  logger: createLogger(),
275
298
  notifier: { send: async (_: string, text: string) => { notifierMessages.push(text); } } as any,
276
299
  acpClient: fakeAcpClient as any,
277
- pollIntervalMs: 25,
300
+ pollIntervalMs: TEST_WATCHER_POLL_INTERVAL_MS,
278
301
  });
302
+ const stopManager = registerManagerCleanup(t, manager);
279
303
 
280
304
  setTimeout(() => {
281
305
  void (async () => {
@@ -308,8 +332,8 @@ test("startup recovery adopts a live implementation session instead of spawning
308
332
  }, 120);
309
333
 
310
334
  await manager.start();
311
- await waitFor(async () => (await stateStore.getActiveProject(channelKey))?.status === "done");
312
- await manager.stop();
335
+ await waitForProjectStatus(repoPath, "done");
336
+ await stopManager();
313
337
 
314
338
  const project = await stateStore.getActiveProject(channelKey);
315
339
  assert.equal(project?.status, "done");
@@ -328,7 +352,7 @@ test("startup recovery adopts a live implementation session instead of spawning
328
352
  assert.equal(notifierMessages.some((m) => m.includes("Watcher active. Starting codex worker")), false);
329
353
  });
330
354
 
331
- test("recovery from recoverable blocked implementation state", async () => {
355
+ test("recovery from recoverable blocked implementation state", async (t) => {
332
356
  const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-recovery-blocked-"));
333
357
  const workspacePath = path.join(tempRoot, "workspace");
334
358
  const repoPath = path.join(workspacePath, "demo-app");
@@ -421,12 +445,13 @@ test("recovery from recoverable blocked implementation state", async () => {
421
445
  logger: createLogger(),
422
446
  notifier: { send: async (_: string, text: string) => { notifierMessages.push(text); } } as any,
423
447
  acpClient: fakeAcpClient as any,
424
- pollIntervalMs: 25,
448
+ pollIntervalMs: TEST_WATCHER_POLL_INTERVAL_MS,
425
449
  });
450
+ const stopManager = registerManagerCleanup(t, manager);
426
451
 
427
452
  await manager.start();
428
- await waitFor(async () => (await stateStore.getActiveProject(channelKey))?.status === "done");
429
- await manager.stop();
453
+ await waitForProjectStatus(repoPath, "done");
454
+ await stopManager();
430
455
 
431
456
  const project = await stateStore.getActiveProject(channelKey);
432
457
  assert.equal(project?.status, "done");
@@ -435,7 +460,7 @@ test("recovery from recoverable blocked implementation state", async () => {
435
460
  assert.equal(notifierMessages.some((m) => m.includes("All tasks complete")), true);
436
461
  });
437
462
 
438
- test("rearm never drops execution field (batch mode)", async () => {
463
+ test("rearm never drops execution field (batch mode)", async (t) => {
439
464
  const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-rearm-safe-"));
440
465
  const workspacePath = path.join(tempRoot, "workspace");
441
466
  const repoPath = path.join(workspacePath, "demo-app");
@@ -497,13 +522,14 @@ test("rearm never drops execution field (batch mode)", async () => {
497
522
  logger: createLogger(),
498
523
  notifier: { send: async () => undefined } as any,
499
524
  acpClient: fakeAcpClient as any,
500
- pollIntervalMs: 25,
525
+ pollIntervalMs: TEST_WATCHER_POLL_INTERVAL_MS,
501
526
  });
527
+ const stopManager = registerManagerCleanup(t, manager);
502
528
 
503
529
  await manager.start();
504
530
  await manager.wake(channelKey);
505
- await waitFor(async () => (await stateStore.getActiveProject(channelKey))?.status === "done");
506
- await manager.stop();
531
+ await waitForProjectStatus(repoPath, "done");
532
+ await stopManager();
507
533
 
508
534
  const project = await stateStore.getActiveProject(channelKey);
509
535
  assert.equal(project?.status, "done");
@@ -511,7 +537,7 @@ test("rearm never drops execution field (batch mode)", async () => {
511
537
  assert.equal(turnCount, 1);
512
538
  });
513
539
 
514
- test("startup recovery does not spawn a worker for visible chat planning", async () => {
540
+ test("startup recovery does not spawn a worker for visible chat planning", async (t) => {
515
541
  const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-recovery-visible-plan-"));
516
542
  const workspacePath = path.join(tempRoot, "workspace");
517
543
  const repoPath = path.join(workspacePath, "demo-app");
@@ -561,12 +587,13 @@ test("startup recovery does not spawn a worker for visible chat planning", async
561
587
  logger: createLogger(),
562
588
  notifier: { send: async (_: string, text: string) => { notifierMessages.push(text); } } as any,
563
589
  acpClient: fakeAcpClient as any,
564
- pollIntervalMs: 25,
590
+ pollIntervalMs: TEST_WATCHER_POLL_INTERVAL_MS,
565
591
  });
592
+ const stopManager = registerManagerCleanup(t, manager);
566
593
 
567
594
  await manager.start();
568
595
  await new Promise((resolve) => setTimeout(resolve, 150));
569
- await manager.stop();
596
+ await stopManager();
570
597
 
571
598
  const project = await stateStore.getActiveProject(channelKey);
572
599
  assert.equal(runCount, 0);
@@ -1,15 +1,18 @@
1
- import test from "node:test";
1
+ import test, { type TestContext } from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { mkdtemp, mkdir } from "node:fs/promises";
6
- import { pathExists, writeJsonFile, writeUtf8 } from "../src/utils/fs.ts";
6
+ import { pathExists, readJsonFile, writeJsonFile, writeUtf8 } from "../src/utils/fs.ts";
7
7
  import { PlanningJournalStore } from "../src/planning/journal.ts";
8
8
  import { getRepoStatePaths } from "../src/utils/paths.ts";
9
9
  import { ProjectStateStore } from "../src/state/store.ts";
10
10
  import { WatcherManager } from "../src/watchers/manager.ts";
11
11
  import { createLogger, waitFor } from "./helpers/harness.ts";
12
12
 
13
+ const TEST_WATCHER_POLL_INTERVAL_MS = 1_000;
14
+ const TEST_WAIT_TIMEOUT_MS = 35_000;
15
+
13
16
  function createPlanningOpenSpec(changeDir: string, proposalPath: string) {
14
17
  return {
15
18
  status: async (cwd: string, changeName: string) => ({
@@ -71,7 +74,29 @@ function createPlanningOpenSpec(changeDir: string, proposalPath: string) {
71
74
  } as any;
72
75
  }
73
76
 
74
- test("watcher planning flow completes", async () => {
77
+ function registerManagerCleanup(t: TestContext, manager: WatcherManager): () => Promise<void> {
78
+ let stopped = false;
79
+ const stop = async () => {
80
+ if (stopped) {
81
+ return;
82
+ }
83
+ stopped = true;
84
+ await manager.stop();
85
+ };
86
+ t.after(stop);
87
+ return stop;
88
+ }
89
+
90
+ async function waitForProjectStatus(
91
+ repoPath: string,
92
+ status: "ready" | "done" | "blocked" | "planning" | "armed" | "running",
93
+ timeoutMs = TEST_WAIT_TIMEOUT_MS,
94
+ ): Promise<void> {
95
+ const stateFile = getRepoStatePaths(repoPath, "archives").stateFile;
96
+ await waitFor(async () => (await readJsonFile<any>(stateFile, null))?.status === status, timeoutMs);
97
+ }
98
+
99
+ test("watcher planning flow completes", async (t) => {
75
100
  const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-watcher-plan-"));
76
101
  const workspacePath = path.join(tempRoot, "workspace");
77
102
  const repoPath = path.join(workspacePath, "demo-app");
@@ -113,8 +138,9 @@ test("watcher planning flow completes", async () => {
113
138
  logger: createLogger(),
114
139
  notifier: { send: async (_: string, text: string) => { notifierMessages.push(text); } } as any,
115
140
  acpClient: fakeAcpClient as any,
116
- pollIntervalMs: 25,
141
+ pollIntervalMs: TEST_WATCHER_POLL_INTERVAL_MS,
117
142
  });
143
+ const stopManager = registerManagerCleanup(t, manager);
118
144
 
119
145
  const channelKey = "discord:watch-plan:default:main";
120
146
  await stateStore.createProject(channelKey);
@@ -143,8 +169,8 @@ test("watcher planning flow completes", async () => {
143
169
 
144
170
  await manager.start();
145
171
  await manager.wake(channelKey);
146
- await waitFor(async () => (await stateStore.getActiveProject(channelKey))?.status === "ready");
147
- await manager.stop();
172
+ await waitForProjectStatus(repoPath, "ready");
173
+ await stopManager();
148
174
 
149
175
  const project = await stateStore.getActiveProject(channelKey);
150
176
  assert.equal(project?.status, "ready");
@@ -155,7 +181,7 @@ test("watcher planning flow completes", async () => {
155
181
  assert.equal(notifierMessages.some((m) => m.includes("Planning ready") && m.includes("Next: run `cs-work` to start implementation")), true);
156
182
  });
157
183
 
158
- test("planning fallback normalizes artifact path", async () => {
184
+ test("planning fallback normalizes artifact path", async (t) => {
159
185
  const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-watcher-plan-fallback-"));
160
186
  const workspacePath = path.join(tempRoot, "workspace");
161
187
  const repoPath = path.join(workspacePath, "demo-app");
@@ -215,8 +241,9 @@ test("planning fallback normalizes artifact path", async () => {
215
241
  cancelSession: async () => undefined,
216
242
  closeSession: async () => undefined,
217
243
  } as any,
218
- pollIntervalMs: 25,
244
+ pollIntervalMs: TEST_WATCHER_POLL_INTERVAL_MS,
219
245
  });
246
+ const stopManager = registerManagerCleanup(t, manager);
220
247
 
221
248
  const channelKey = "discord:watch-plan-fallback:default:main";
222
249
  await stateStore.createProject(channelKey);
@@ -230,14 +257,14 @@ test("planning fallback normalizes artifact path", async () => {
230
257
 
231
258
  await manager.start();
232
259
  await manager.wake(channelKey);
233
- await waitFor(async () => (await stateStore.getActiveProject(channelKey))?.status === "ready");
234
- await manager.stop();
260
+ await waitForProjectStatus(repoPath, "ready");
261
+ await stopManager();
235
262
 
236
263
  const project = await stateStore.getActiveProject(channelKey);
237
264
  assert.deepEqual(project?.lastExecution?.changedFiles, ["openspec/changes/watch-plan-fallback/proposal.md"]);
238
265
  });
239
266
 
240
- test("watcher force-refreshes planning artifacts when journal is dirty and OpenSpec reports all artifacts done", async () => {
267
+ test("watcher force-refreshes planning artifacts when journal is dirty and OpenSpec reports all artifacts done", async (t) => {
241
268
  const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-watcher-plan-forced-"));
242
269
  const workspacePath = path.join(tempRoot, "workspace");
243
270
  const repoPath = path.join(workspacePath, "demo-app");
@@ -358,8 +385,9 @@ test("watcher force-refreshes planning artifacts when journal is dirty and OpenS
358
385
  logger: createLogger(),
359
386
  notifier: { send: async (_: string, text: string) => { notifierMessages.push(text); } } as any,
360
387
  acpClient: fakeAcpClient as any,
361
- pollIntervalMs: 25,
388
+ pollIntervalMs: TEST_WATCHER_POLL_INTERVAL_MS,
362
389
  });
390
+ const stopManager = registerManagerCleanup(t, manager);
363
391
 
364
392
  const channelKey = "discord:watch-plan-forced:default:main";
365
393
  await stateStore.createProject(channelKey);
@@ -398,8 +426,8 @@ test("watcher force-refreshes planning artifacts when journal is dirty and OpenS
398
426
 
399
427
  await manager.start();
400
428
  await manager.wake(channelKey);
401
- await waitFor(async () => (await stateStore.getActiveProject(channelKey))?.status === "ready");
402
- await manager.stop();
429
+ await waitForProjectStatus(repoPath, "ready");
430
+ await stopManager();
403
431
 
404
432
  const project = await stateStore.getActiveProject(channelKey);
405
433
  assert.deepEqual(runOrder, ["proposal", "specs", "design", "tasks"]);
@@ -412,7 +440,7 @@ test("watcher force-refreshes planning artifacts when journal is dirty and OpenS
412
440
  );
413
441
  });
414
442
 
415
- test("watcher restarts planning worker after ACP runtime exit", async () => {
443
+ test("watcher restarts planning worker after ACP runtime exit", async (t) => {
416
444
  const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-watcher-plan-restart-"));
417
445
  const workspacePath = path.join(tempRoot, "workspace");
418
446
  const repoPath = path.join(workspacePath, "demo-app");
@@ -460,8 +488,9 @@ test("watcher restarts planning worker after ACP runtime exit", async () => {
460
488
  logger: createLogger(),
461
489
  notifier: { send: async (_: string, text: string) => { notifierMessages.push(text); } } as any,
462
490
  acpClient: fakeAcpClient as any,
463
- pollIntervalMs: 25,
491
+ pollIntervalMs: TEST_WATCHER_POLL_INTERVAL_MS,
464
492
  });
493
+ const stopManager = registerManagerCleanup(t, manager);
465
494
 
466
495
  const channelKey = "discord:watch-plan-restart:default:main";
467
496
  await stateStore.createProject(channelKey);
@@ -490,8 +519,8 @@ test("watcher restarts planning worker after ACP runtime exit", async () => {
490
519
 
491
520
  await manager.start();
492
521
  await manager.wake(channelKey);
493
- await waitFor(async () => (await stateStore.getActiveProject(channelKey))?.status === "ready", 8_000);
494
- await manager.stop();
522
+ await waitForProjectStatus(repoPath, "ready");
523
+ await stopManager();
495
524
 
496
525
  const project = await stateStore.getActiveProject(channelKey);
497
526
  assert.equal(project?.status, "ready");