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.
@@ -3,17 +3,32 @@ 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, describeWorkerStartupTimeout, shouldAbortWorkerStartup } 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 = 60_000;
15
+
13
16
  function hasMessage(messages: string[], ...parts: string[]): boolean {
14
17
  return messages.some((message) => parts.every((part) => message.includes(part)));
15
18
  }
16
19
 
20
+ async function readProjectState(repoPath: string) {
21
+ return await readJsonFile<any>(getRepoStatePaths(repoPath, "archives").stateFile, null);
22
+ }
23
+
24
+ async function waitForProjectState(
25
+ repoPath: string,
26
+ predicate: (project: any) => boolean,
27
+ timeoutMs = TEST_WAIT_TIMEOUT_MS,
28
+ ): Promise<void> {
29
+ await waitFor(async () => predicate(await readProjectState(repoPath)), timeoutMs);
30
+ }
31
+
17
32
  test("queue owner unavailable is treated as a non-fatal startup state", () => {
18
33
  const status = {
19
34
  summary: "status=dead acpxRecordId=session-1",
@@ -129,7 +144,7 @@ test("watcher work flow completes", async (t) => {
129
144
  logger: createLogger(),
130
145
  notifier: { send: async (_: string, text: string) => { notifierMessages.push(text); } } as any,
131
146
  acpClient: fakeAcpClient as any,
132
- pollIntervalMs: 25,
147
+ pollIntervalMs: TEST_WATCHER_POLL_INTERVAL_MS,
133
148
  });
134
149
  t.after(async () => {
135
150
  await manager.stop();
@@ -166,9 +181,11 @@ test("watcher work flow completes", async (t) => {
166
181
 
167
182
  await manager.start();
168
183
  await manager.wake(channelKey);
169
- await waitFor(async () =>
170
- (await stateStore.getActiveProject(channelKey))?.status === "done"
171
- && hasMessage(notifierMessages, "demo-app-watch-work", "All tasks complete", "/clawspec archive"),
184
+ await waitForProjectState(
185
+ repoPath,
186
+ (project) =>
187
+ project?.status === "done"
188
+ && hasMessage(notifierMessages, "demo-app-watch-work", "All tasks complete", "/clawspec archive"),
172
189
  );
173
190
 
174
191
  const project = await stateStore.getActiveProject(channelKey);
@@ -325,7 +342,7 @@ test("worker progress events keep running state in sync before execution finishe
325
342
  logger: createLogger(),
326
343
  notifier: { send: async (_: string, text: string) => { notifierMessages.push(text); } } as any,
327
344
  acpClient: fakeAcpClient as any,
328
- pollIntervalMs: 25,
345
+ pollIntervalMs: TEST_WATCHER_POLL_INTERVAL_MS,
329
346
  });
330
347
  t.after(async () => {
331
348
  await manager.stop();
@@ -362,20 +379,17 @@ test("worker progress events keep running state in sync before execution finishe
362
379
 
363
380
  await manager.start();
364
381
  await manager.wake(channelKey);
365
- await waitFor(async () => {
366
- const project = await stateStore.getActiveProject(channelKey);
367
- return project?.status === "running"
382
+ await waitForProjectState(repoPath, (project) =>
383
+ project?.status === "running"
368
384
  && project.taskCounts?.complete === 1
369
385
  && project.taskCounts?.remaining === 1
370
386
  && project.currentTask === "1.2 Add multipart parsing"
371
387
  && project.latestSummary?.includes("Start 1.2") === true
372
- && project.execution?.currentTaskId === "1.2";
373
- });
388
+ && project.execution?.currentTaskId === "1.2",
389
+ );
374
390
 
375
391
  releaseFinalStep();
376
- await waitFor(async () =>
377
- (await stateStore.getActiveProject(channelKey))?.status === "done",
378
- );
392
+ await waitForProjectState(repoPath, (project) => project?.status === "done");
379
393
 
380
394
  assert.equal(hasMessage(notifierMessages, "demo-app-watch-work-progress-sync", "1/2", "Done 1.1"), true);
381
395
  assert.equal(hasMessage(notifierMessages, "demo-app-watch-work-progress-sync", "2/2", "Start 1.2"), true);
@@ -483,7 +497,7 @@ test("watcher restarts implementation worker after ACP runtime exit", async (t)
483
497
  logger: createLogger(),
484
498
  notifier: { send: async (_: string, text: string) => { notifierMessages.push(text); } } as any,
485
499
  acpClient: fakeAcpClient as any,
486
- pollIntervalMs: 25,
500
+ pollIntervalMs: TEST_WATCHER_POLL_INTERVAL_MS,
487
501
  });
488
502
  t.after(async () => {
489
503
  await manager.stop();
@@ -520,10 +534,11 @@ test("watcher restarts implementation worker after ACP runtime exit", async (t)
520
534
 
521
535
  await manager.start();
522
536
  await manager.wake(channelKey);
523
- await waitFor(async () =>
524
- (await stateStore.getActiveProject(channelKey))?.status === "done"
525
- && hasMessage(notifierMessages, "demo-app-watch-work-restart", "All tasks complete", "/clawspec archive"),
526
- 8_000,
537
+ await waitForProjectState(
538
+ repoPath,
539
+ (project) =>
540
+ project?.status === "done"
541
+ && hasMessage(notifierMessages, "demo-app-watch-work-restart", "All tasks complete", "/clawspec archive"),
527
542
  );
528
543
 
529
544
  const project = await stateStore.getActiveProject(channelKey);
@@ -651,7 +666,7 @@ test("watcher restarts a dead ACP session after progress stalls", async (t) => {
651
666
  logger: createLogger(),
652
667
  notifier: { send: async (_: string, text: string) => { notifierMessages.push(text); } } as any,
653
668
  acpClient: fakeAcpClient as any,
654
- pollIntervalMs: 25,
669
+ pollIntervalMs: TEST_WATCHER_POLL_INTERVAL_MS,
655
670
  });
656
671
  t.after(async () => {
657
672
  await manager.stop();
@@ -688,9 +703,11 @@ test("watcher restarts a dead ACP session after progress stalls", async (t) => {
688
703
 
689
704
  await manager.start();
690
705
  await manager.wake(channelKey);
691
- await waitFor(async () =>
692
- (await stateStore.getActiveProject(channelKey))?.status === "done",
693
- 8_000,
706
+ await waitForProjectState(
707
+ repoPath,
708
+ (project) =>
709
+ project?.status === "done"
710
+ && hasMessage(notifierMessages, "demo-app-watch-work-dead-session", "All tasks complete", "/clawspec archive"),
694
711
  );
695
712
 
696
713
  const project = await stateStore.getActiveProject(channelKey);
@@ -798,7 +815,7 @@ test("watcher restarts a dead ACP session that dies before first progress", asyn
798
815
  logger: createLogger(),
799
816
  notifier: { send: async (_: string, text: string) => { notifierMessages.push(text); } } as any,
800
817
  acpClient: fakeAcpClient as any,
801
- pollIntervalMs: 25,
818
+ pollIntervalMs: TEST_WATCHER_POLL_INTERVAL_MS,
802
819
  });
803
820
  t.after(async () => {
804
821
  await manager.stop();
@@ -835,9 +852,11 @@ test("watcher restarts a dead ACP session that dies before first progress", asyn
835
852
 
836
853
  await manager.start();
837
854
  await manager.wake(channelKey);
838
- await waitFor(async () =>
839
- (await stateStore.getActiveProject(channelKey))?.status === "done",
840
- 8_000,
855
+ await waitForProjectState(
856
+ repoPath,
857
+ (project) =>
858
+ project?.status === "done"
859
+ && hasMessage(notifierMessages, "demo-app-watch-work-dead-startup", "All tasks complete", "/clawspec archive"),
841
860
  );
842
861
 
843
862
  const project = await stateStore.getActiveProject(channelKey);
@@ -958,7 +977,7 @@ test("status-only ACP heartbeats do not keep a dead session alive", async (t) =>
958
977
  logger: createLogger(),
959
978
  notifier: { send: async (_: string, text: string) => { notifierMessages.push(text); } } as any,
960
979
  acpClient: fakeAcpClient as any,
961
- pollIntervalMs: 25,
980
+ pollIntervalMs: TEST_WATCHER_POLL_INTERVAL_MS,
962
981
  });
963
982
  t.after(async () => {
964
983
  await manager.stop();
@@ -995,9 +1014,11 @@ test("status-only ACP heartbeats do not keep a dead session alive", async (t) =>
995
1014
 
996
1015
  await manager.start();
997
1016
  await manager.wake(channelKey);
998
- await waitFor(async () =>
999
- (await stateStore.getActiveProject(channelKey))?.status === "done",
1000
- 8_000,
1017
+ await waitForProjectState(
1018
+ repoPath,
1019
+ (project) =>
1020
+ project?.status === "done"
1021
+ && hasMessage(notifierMessages, "demo-app-watch-work-status-heartbeats", "All tasks complete"),
1001
1022
  );
1002
1023
 
1003
1024
  const project = await stateStore.getActiveProject(channelKey);
@@ -1124,7 +1145,7 @@ test("dead ACP session that ignores abort is restarted without hanging the watch
1124
1145
  logger: createLogger(),
1125
1146
  notifier: { send: async (_: string, text: string) => { notifierMessages.push(text); } } as any,
1126
1147
  acpClient: fakeAcpClient as any,
1127
- pollIntervalMs: 25,
1148
+ pollIntervalMs: TEST_WATCHER_POLL_INTERVAL_MS,
1128
1149
  });
1129
1150
  t.after(async () => {
1130
1151
  if (heartbeatTimer) {
@@ -1165,9 +1186,11 @@ test("dead ACP session that ignores abort is restarted without hanging the watch
1165
1186
 
1166
1187
  await manager.start();
1167
1188
  await manager.wake(channelKey);
1168
- await waitFor(async () =>
1169
- (await stateStore.getActiveProject(channelKey))?.status === "done",
1170
- 8_000,
1189
+ await waitForProjectState(
1190
+ repoPath,
1191
+ (project) =>
1192
+ project?.status === "done"
1193
+ && hasMessage(notifierMessages, "demo-app-watch-work-hung-dead-session", "All tasks complete"),
1171
1194
  );
1172
1195
 
1173
1196
  const project = await stateStore.getActiveProject(channelKey);
@@ -1205,7 +1228,7 @@ test("manager stop closes active worker sessions and rearms project recovery sta
1205
1228
  closedSessions.push({ sessionKey, reason });
1206
1229
  },
1207
1230
  } as any,
1208
- pollIntervalMs: 25,
1231
+ pollIntervalMs: TEST_WATCHER_POLL_INTERVAL_MS,
1209
1232
  });
1210
1233
 
1211
1234
  const channelKey = "discord:watch-stop:default:main";
@@ -1309,7 +1332,7 @@ test("watcher stops retrying after 10 ACP restart attempts", async (t) => {
1309
1332
  logger: createLogger(),
1310
1333
  notifier: { send: async (_: string, text: string) => { notifierMessages.push(text); } } as any,
1311
1334
  acpClient: fakeAcpClient as any,
1312
- pollIntervalMs: 25,
1335
+ pollIntervalMs: TEST_WATCHER_POLL_INTERVAL_MS,
1313
1336
  });
1314
1337
  t.after(async () => {
1315
1338
  await manager.stop();
@@ -1347,9 +1370,11 @@ test("watcher stops retrying after 10 ACP restart attempts", async (t) => {
1347
1370
  }));
1348
1371
 
1349
1372
  await manager.wake(channelKey);
1350
- await waitFor(async () =>
1351
- (await stateStore.getActiveProject(channelKey))?.status === "blocked",
1352
- 8_000,
1373
+ await waitForProjectState(
1374
+ repoPath,
1375
+ (project) =>
1376
+ project?.status === "blocked"
1377
+ && hasMessage(notifierMessages, "demo-app-watch-work-restart-cap", "Blocked after 10 ACP restart attempts"),
1353
1378
  );
1354
1379
 
1355
1380
  const project = await stateStore.getActiveProject(channelKey);
@@ -1415,7 +1440,7 @@ test("watcher blocked message includes ACPX setup guidance when backend stays un
1415
1440
  logger: createLogger(),
1416
1441
  notifier: { send: async (_: string, text: string) => { notifierMessages.push(text); } } as any,
1417
1442
  acpClient: fakeAcpClient as any,
1418
- pollIntervalMs: 25,
1443
+ pollIntervalMs: TEST_WATCHER_POLL_INTERVAL_MS,
1419
1444
  });
1420
1445
  t.after(async () => {
1421
1446
  await manager.stop();
@@ -1453,9 +1478,11 @@ test("watcher blocked message includes ACPX setup guidance when backend stays un
1453
1478
  }));
1454
1479
 
1455
1480
  await manager.wake(channelKey);
1456
- await waitFor(async () =>
1457
- (await stateStore.getActiveProject(channelKey))?.status === "blocked",
1458
- 8_000,
1481
+ await waitForProjectState(
1482
+ repoPath,
1483
+ (project) =>
1484
+ project?.status === "blocked"
1485
+ && hasMessage(notifierMessages, "demo-app-watch-work-backend-blocked", "Blocked: ACPX backend unavailable"),
1459
1486
  );
1460
1487
 
1461
1488
  const project = await stateStore.getActiveProject(channelKey);
@@ -1545,7 +1572,7 @@ test("watcher retries when ACP runtime backend is temporarily unavailable", asyn
1545
1572
  logger: createLogger(),
1546
1573
  notifier: { send: async (_: string, text: string) => { notifierMessages.push(text); } } as any,
1547
1574
  acpClient: fakeAcpClient as any,
1548
- pollIntervalMs: 25,
1575
+ pollIntervalMs: TEST_WATCHER_POLL_INTERVAL_MS,
1549
1576
  });
1550
1577
  t.after(async () => {
1551
1578
  await manager.stop();
@@ -1581,9 +1608,11 @@ test("watcher retries when ACP runtime backend is temporarily unavailable", asyn
1581
1608
  }));
1582
1609
 
1583
1610
  await manager.wake(channelKey);
1584
- await waitFor(async () =>
1585
- (await stateStore.getActiveProject(channelKey))?.status === "done",
1586
- 8_000,
1611
+ await waitForProjectState(
1612
+ repoPath,
1613
+ (project) =>
1614
+ project?.status === "done"
1615
+ && hasMessage(notifierMessages, "demo-app-watch-work-backend-unavailable", "All tasks complete"),
1587
1616
  );
1588
1617
 
1589
1618
  const project = await stateStore.getActiveProject(channelKey);
@@ -1689,7 +1718,7 @@ test("watcher finalizes when terminal result exists before ACP turn exits", asyn
1689
1718
  logger: createLogger(),
1690
1719
  notifier: { send: async (_: string, text: string) => { notifierMessages.push(text); } } as any,
1691
1720
  acpClient: fakeAcpClient as any,
1692
- pollIntervalMs: 25,
1721
+ pollIntervalMs: TEST_WATCHER_POLL_INTERVAL_MS,
1693
1722
  });
1694
1723
  t.after(async () => {
1695
1724
  await manager.stop();
@@ -1726,10 +1755,11 @@ test("watcher finalizes when terminal result exists before ACP turn exits", asyn
1726
1755
 
1727
1756
  await manager.start();
1728
1757
  await manager.wake(channelKey);
1729
- await waitFor(async () =>
1730
- (await stateStore.getActiveProject(channelKey))?.status === "done"
1731
- && hasMessage(notifierMessages, "demo-app-watch-work-terminal", "All tasks complete"),
1732
- 8_000,
1758
+ await waitForProjectState(
1759
+ repoPath,
1760
+ (project) =>
1761
+ project?.status === "done"
1762
+ && hasMessage(notifierMessages, "demo-app-watch-work-terminal", "All tasks complete"),
1733
1763
  );
1734
1764
 
1735
1765
  const project = await stateStore.getActiveProject(channelKey);
@@ -93,5 +93,5 @@ test("implementation prompt instructs the worker to use the helper", async () =>
93
93
  });
94
94
 
95
95
  assert.match(prompt, /Use the worker IO helper instead of editing .*worker-progress\.jsonl directly\./);
96
- assert.match(prompt, /worker_io\.mjs" event --kind <status\|task_start\|task_done\|blocked>/);
96
+ assert.match(prompt, /worker_io\.mjs['"] event --kind <status\|task_start\|task_done\|blocked>/);
97
97
  });
@@ -1,14 +0,0 @@
1
- import { appendFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import path from "node:path";
4
-
5
- const LOG_FILE = path.join(homedir(), ".openclaw", "clawspec-debug.log");
6
-
7
- export function debugLog(message: string): void {
8
- try {
9
- const timestamp = new Date().toISOString();
10
- appendFileSync(LOG_FILE, `${timestamp} ${message}\n`);
11
- } catch {
12
- // ignore
13
- }
14
- }