gsd-pi 2.41.0-dev.b832948 → 2.41.0-dev.cac69f9

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.
Files changed (43) hide show
  1. package/dist/web/standalone/.next/BUILD_ID +1 -1
  2. package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
  3. package/dist/web/standalone/.next/build-manifest.json +2 -2
  4. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  5. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  6. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  7. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  9. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  10. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  14. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  15. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  16. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  17. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/index.html +1 -1
  22. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
  29. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  30. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  31. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  32. package/package.json +1 -1
  33. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  34. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +4 -0
  35. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  36. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +5 -0
  37. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +100 -118
  38. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -2
  39. package/src/resources/extensions/gsd/tests/captures.test.ts +12 -1
  40. package/src/resources/extensions/gsd/tests/continue-here.test.ts +20 -20
  41. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +195 -105
  42. /package/dist/web/standalone/.next/static/{43Aw72Fdw8k1aAHQOYOXr → EnGUNqHeGbE0tuuUkTJVA}/_buildManifest.js +0 -0
  43. /package/dist/web/standalone/.next/static/{43Aw72Fdw8k1aAHQOYOXr → EnGUNqHeGbE0tuuUkTJVA}/_ssgManifest.js +0 -0
@@ -2321,6 +2321,11 @@ export class InteractiveMode {
2321
2321
  }
2322
2322
 
2323
2323
  private handleCtrlZ(): void {
2324
+ // On Windows, SIGTSTP doesn't exist - Ctrl+Z is not supported
2325
+ if (process.platform === "win32") {
2326
+ return;
2327
+ }
2328
+
2324
2329
  // Ignore SIGINT while suspended so Ctrl+C in the terminal does not
2325
2330
  // kill the backgrounded process. The handler is removed on resume.
2326
2331
  const ignoreSigint = () => {};
@@ -595,7 +595,6 @@ test("autoLoop calls deriveState → resolveDispatch → runUnit in sequence", a
595
595
  ctx.sessionManager = { getSessionFile: () => "/tmp/session.json" };
596
596
  const pi = makeMockPi();
597
597
 
598
- let loopCount = 0;
599
598
  const s = makeLoopSession();
600
599
 
601
600
  const deps = makeMockDeps({
@@ -621,11 +620,8 @@ test("autoLoop calls deriveState → resolveDispatch → runUnit in sequence", a
621
620
  },
622
621
  postUnitPostVerification: async () => {
623
622
  deps.callLog.push("postUnitPostVerification");
624
- loopCount++;
625
- // After first iteration, deactivate to exit the loop
626
- if (loopCount >= 1) {
627
- s.active = false;
628
- }
623
+ // Deactivate after first iteration to exit the loop
624
+ s.active = false;
629
625
  return "continue" as const;
630
626
  },
631
627
  });
@@ -683,7 +679,6 @@ test("crash lock records session file from AFTER newSession, not before (#1710)"
683
679
  };
684
680
  const pi = makeMockPi();
685
681
 
686
- let loopCount = 0;
687
682
  const s = makeLoopSession({
688
683
  cmdCtx: {
689
684
  newSession: () => {
@@ -731,10 +726,8 @@ test("crash lock records session file from AFTER newSession, not before (#1710)"
731
726
  },
732
727
  postUnitPostVerification: async () => {
733
728
  deps.callLog.push("postUnitPostVerification");
734
- loopCount++;
735
- if (loopCount >= 1) {
736
- s.active = false;
737
- }
729
+ // Deactivate after first iteration to exit the loop
730
+ s.active = false;
738
731
  return "continue" as const;
739
732
  },
740
733
  });
@@ -791,6 +784,23 @@ test("autoLoop handles verification retry by continuing loop", async (t) => {
791
784
  let deriveCallCount = 0;
792
785
  const s = makeLoopSession();
793
786
 
787
+ // Pre-queued verification actions: each entry provides a side-effect + return value
788
+ type VerifyAction = { sideEffect?: () => void; response: "retry" | "continue" };
789
+ const verificationActions: VerifyAction[] = [
790
+ {
791
+ sideEffect: () => {
792
+ // Simulate retry — set pendingVerificationRetry on session
793
+ s.pendingVerificationRetry = {
794
+ unitId: "M001/S01/T01",
795
+ failureContext: "test failed: expected X got Y",
796
+ attempt: 1,
797
+ };
798
+ },
799
+ response: "retry",
800
+ },
801
+ { response: "continue" },
802
+ ];
803
+
794
804
  const deps = makeMockDeps({
795
805
  deriveState: async () => {
796
806
  deriveCallCount++;
@@ -805,19 +815,11 @@ test("autoLoop handles verification retry by continuing loop", async (t) => {
805
815
  } as any;
806
816
  },
807
817
  runPostUnitVerification: async () => {
818
+ const action = verificationActions[verifyCallCount] ?? { response: "continue" as const };
808
819
  verifyCallCount++;
809
820
  deps.callLog.push("runPostUnitVerification");
810
- if (verifyCallCount === 1) {
811
- // First call: simulate retry — set pendingVerificationRetry on session
812
- s.pendingVerificationRetry = {
813
- unitId: "M001/S01/T01",
814
- failureContext: "test failed: expected X got Y",
815
- attempt: 1,
816
- };
817
- return "retry" as const;
818
- }
819
- // Second call: pass
820
- return "continue" as const;
821
+ action.sideEffect?.();
822
+ return action.response;
821
823
  },
822
824
  postUnitPostVerification: async () => {
823
825
  deps.callLog.push("postUnitPostVerification");
@@ -894,19 +896,17 @@ test("autoLoop handles dispatch skip action by continuing", async (t) => {
894
896
  const s = makeLoopSession();
895
897
 
896
898
  let dispatchCallCount = 0;
899
+ // Pre-queued dispatch responses: first call returns "skip", second returns "stop"
900
+ const dispatchResponses = [
901
+ { action: "skip" as const },
902
+ { action: "stop" as const, reason: "done", level: "info" as const },
903
+ ];
897
904
  const deps = makeMockDeps({
898
905
  resolveDispatch: async () => {
906
+ const response = dispatchResponses[dispatchCallCount] ?? dispatchResponses[dispatchResponses.length - 1];
899
907
  dispatchCallCount++;
900
908
  deps.callLog.push("resolveDispatch");
901
- if (dispatchCallCount === 1) {
902
- return { action: "skip" as const };
903
- }
904
- // Second time: stop to exit the loop
905
- return {
906
- action: "stop" as const,
907
- reason: "done",
908
- level: "info" as const,
909
- };
909
+ return response;
910
910
  },
911
911
  });
912
912
 
@@ -936,22 +936,26 @@ test("autoLoop drains sidecar queue after postUnitPostVerification enqueues item
936
936
  const s = makeLoopSession();
937
937
 
938
938
  let postVerCallCount = 0;
939
+ const postVerActions: Array<() => void> = [
940
+ () => {
941
+ // First call (main unit): enqueue a sidecar item
942
+ s.sidecarQueue.push({
943
+ kind: "hook" as const,
944
+ unitType: "hook/review",
945
+ unitId: "M001/S01/T01/review",
946
+ prompt: "review the code",
947
+ });
948
+ },
949
+ () => {
950
+ // Second call (sidecar unit completed): deactivate
951
+ s.active = false;
952
+ },
953
+ ];
939
954
  const deps = makeMockDeps({
940
955
  postUnitPostVerification: async () => {
956
+ postVerActions[postVerCallCount]?.();
941
957
  postVerCallCount++;
942
958
  deps.callLog.push("postUnitPostVerification");
943
- if (postVerCallCount === 1) {
944
- // First call (main unit): enqueue a sidecar item
945
- s.sidecarQueue.push({
946
- kind: "hook" as const,
947
- unitType: "hook/review",
948
- unitId: "M001/S01/T01/review",
949
- prompt: "review the code",
950
- });
951
- return "continue" as const;
952
- }
953
- // Second call (sidecar unit completed): done
954
- s.active = false;
955
959
  return "continue" as const;
956
960
  },
957
961
  });
@@ -1119,13 +1123,13 @@ test("startAuto calls selfHealRuntimeRecords before autoLoop (#1727)", () => {
1119
1123
  assert.ok(healIdx > -1, "startAuto must call selfHealRuntimeRecords");
1120
1124
  assert.ok(healIdx < loopIdx, "selfHealRuntimeRecords must be called before autoLoop");
1121
1125
 
1122
- // Verify the second autoLoop call site also has selfHeal before it
1126
+ // Verify the second autoLoop call site also has selfHeal before it (if present)
1123
1127
  const secondLoopIdx = fnBlock.indexOf("autoLoop(", loopIdx + 1);
1124
- if (secondLoopIdx > -1) {
1125
- const secondHealIdx = fnBlock.indexOf("selfHealRuntimeRecords", healIdx + 1);
1126
- assert.ok(secondHealIdx > -1, "second autoLoop call must also have selfHealRuntimeRecords");
1127
- assert.ok(secondHealIdx < secondLoopIdx, "second selfHealRuntimeRecords must precede second autoLoop");
1128
- }
1128
+ const secondHealIdx = fnBlock.indexOf("selfHealRuntimeRecords", healIdx + 1);
1129
+ assert.ok(
1130
+ secondLoopIdx === -1 || (secondHealIdx > -1 && secondHealIdx < secondLoopIdx),
1131
+ "if a second autoLoop call exists, it must also be preceded by selfHealRuntimeRecords",
1132
+ );
1129
1133
  });
1130
1134
 
1131
1135
  test("agent_end handler calls resolveAgentEnd (not handleAgentEnd)", () => {
@@ -1287,25 +1291,29 @@ test("stuck detection: window resets recovery when deriveState returns a differe
1287
1291
  const s = makeLoopSession();
1288
1292
 
1289
1293
  let deriveCallCount = 0;
1294
+ let postVerCallCount = 0;
1290
1295
  let stopCalled = false;
1291
1296
 
1297
+ // First 3 derives return T01, 4th returns T02; dispatch follows the derived task
1298
+ const derivedTaskIds = ["T01", "T01", "T01", "T02"];
1299
+
1292
1300
  const deps = makeMockDeps({
1293
1301
  deriveState: async () => {
1302
+ const taskId = derivedTaskIds[Math.min(deriveCallCount, derivedTaskIds.length - 1)];
1294
1303
  deriveCallCount++;
1295
1304
  deps.callLog.push("deriveState");
1296
1305
  return {
1297
1306
  phase: "executing",
1298
1307
  activeMilestone: { id: "M001", title: "Test", status: "active" },
1299
1308
  activeSlice: { id: "S01", title: "Slice 1" },
1300
- activeTask: { id: deriveCallCount <= 3 ? "T01" : "T02" },
1309
+ activeTask: { id: taskId },
1301
1310
  registry: [{ id: "M001", status: "active" }],
1302
1311
  blockers: [],
1303
1312
  } as any;
1304
1313
  },
1305
1314
  resolveDispatch: async () => {
1315
+ const taskId = derivedTaskIds[Math.min(deriveCallCount - 1, derivedTaskIds.length - 1)];
1306
1316
  deps.callLog.push("resolveDispatch");
1307
- // Return dispatch matching the task from deriveState
1308
- const taskId = deriveCallCount <= 3 ? "T01" : "T02";
1309
1317
  return {
1310
1318
  action: "dispatch" as const,
1311
1319
  unitType: "execute-task",
@@ -1319,11 +1327,11 @@ test("stuck detection: window resets recovery when deriveState returns a differe
1319
1327
  s.active = false;
1320
1328
  },
1321
1329
  postUnitPostVerification: async () => {
1330
+ postVerCallCount++;
1322
1331
  deps.callLog.push("postUnitPostVerification");
1323
- // After 4th iteration (unit changed on iter 4), exit
1324
- if (deriveCallCount >= 4) {
1325
- s.active = false;
1326
- }
1332
+ // Exit on the 4th call (after T02 unit completes)
1333
+ const shouldExit = postVerCallCount >= 4;
1334
+ s.active = !shouldExit;
1327
1335
  return "continue" as const;
1328
1336
  },
1329
1337
  });
@@ -1362,6 +1370,14 @@ test("stuck detection: does not push to window during verification retry", async
1362
1370
  let verifyCallCount = 0;
1363
1371
  let stopReason = "";
1364
1372
 
1373
+ // Pre-queued responses: 3 retries then a continue (exit)
1374
+ const verifyActions: Array<() => "retry" | "continue"> = [
1375
+ () => { s.pendingVerificationRetry = { unitId: "M001/S01/T01", failureContext: "test failed", attempt: 1 }; return "retry"; },
1376
+ () => { s.pendingVerificationRetry = { unitId: "M001/S01/T01", failureContext: "test failed", attempt: 2 }; return "retry"; },
1377
+ () => { s.pendingVerificationRetry = { unitId: "M001/S01/T01", failureContext: "test failed", attempt: 3 }; return "retry"; },
1378
+ () => { s.active = false; return "continue"; },
1379
+ ];
1380
+
1365
1381
  const deps = makeMockDeps({
1366
1382
  deriveState: async () =>
1367
1383
  ({
@@ -1379,20 +1395,10 @@ test("stuck detection: does not push to window during verification retry", async
1379
1395
  prompt: "do the thing",
1380
1396
  }),
1381
1397
  runPostUnitVerification: async () => {
1398
+ const action = verifyActions[verifyCallCount] ?? (() => { s.active = false; return "continue" as const; });
1382
1399
  verifyCallCount++;
1383
1400
  deps.callLog.push("runPostUnitVerification");
1384
- if (verifyCallCount <= 3) {
1385
- // Set pendingVerificationRetry — should prevent stuck counter increment
1386
- s.pendingVerificationRetry = {
1387
- unitId: "M001/S01/T01",
1388
- failureContext: "test failed",
1389
- attempt: verifyCallCount,
1390
- };
1391
- return "retry" as const;
1392
- }
1393
- // After 3 retries, exit gracefully
1394
- s.active = false;
1395
- return "continue" as const;
1401
+ return action();
1396
1402
  },
1397
1403
  stopAuto: async (_ctx?: any, _pi?: any, reason?: string) => {
1398
1404
  deps.callLog.push("stopAuto");
@@ -1544,7 +1550,7 @@ test("autoLoop lifecycle: advances through research → plan → execute → ver
1544
1550
  const dispatchedUnitTypes: string[] = [];
1545
1551
 
1546
1552
  // Phase sequence: each deriveState call returns a different phase.
1547
- // On the 6th call (start of iteration 6), we deactivate to exit.
1553
+ // The 6th entry (index 5) is the terminal "complete" phase that stops the loop.
1548
1554
  const phases = [
1549
1555
  // Call 1: researching → dispatches research-slice
1550
1556
  {
@@ -1576,6 +1582,12 @@ test("autoLoop lifecycle: advances through research → plan → execute → ver
1576
1582
  activeSlice: { id: "S01", title: "Complete Slice" },
1577
1583
  activeTask: null,
1578
1584
  },
1585
+ // Call 6: terminal — deactivate to exit the loop
1586
+ {
1587
+ phase: "complete",
1588
+ activeSlice: null,
1589
+ activeTask: null,
1590
+ },
1579
1591
  ];
1580
1592
 
1581
1593
  const dispatches = [
@@ -1588,46 +1600,26 @@ test("autoLoop lifecycle: advances through research → plan → execute → ver
1588
1600
 
1589
1601
  const deps = makeMockDeps({
1590
1602
  deriveState: async () => {
1603
+ const p = phases[Math.min(deriveCallCount, phases.length - 1)];
1591
1604
  deriveCallCount++;
1592
1605
  deps.callLog.push("deriveState");
1593
1606
 
1594
- if (deriveCallCount > phases.length) {
1595
- // 6th+ call: deactivate to exit the loop
1596
- s.active = false;
1597
- return {
1598
- phase: "complete",
1599
- activeMilestone: { id: "M001", title: "Test", status: "complete" },
1600
- activeSlice: null,
1601
- activeTask: null,
1602
- registry: [{ id: "M001", status: "complete" }],
1603
- blockers: [],
1604
- } as any;
1605
- }
1606
-
1607
- const p = phases[deriveCallCount - 1];
1607
+ const terminalPhases: Record<string, string> = { complete: "complete" };
1608
+ s.active = p.phase !== "complete";
1609
+ const milestoneStatus = terminalPhases[p.phase] ?? "active";
1608
1610
  return {
1609
1611
  phase: p.phase,
1610
- activeMilestone: { id: "M001", title: "Test", status: "active" },
1611
- activeSlice: p.activeSlice,
1612
- activeTask: p.activeTask,
1613
- registry: [{ id: "M001", status: "active" }],
1612
+ activeMilestone: { id: "M001", title: "Test", status: milestoneStatus },
1613
+ activeSlice: p.activeSlice ?? null,
1614
+ activeTask: p.activeTask ?? null,
1615
+ registry: [{ id: "M001", status: milestoneStatus }],
1614
1616
  blockers: [],
1615
1617
  } as any;
1616
1618
  },
1617
1619
  resolveDispatch: async () => {
1620
+ const d = dispatches[Math.min(dispatchCallCount, dispatches.length - 1)];
1618
1621
  dispatchCallCount++;
1619
1622
  deps.callLog.push("resolveDispatch");
1620
-
1621
- if (dispatchCallCount > dispatches.length) {
1622
- // Safety: shouldn't reach here, but stop if it does
1623
- return {
1624
- action: "stop" as const,
1625
- reason: "done",
1626
- level: "info" as const,
1627
- };
1628
- }
1629
-
1630
- const d = dispatches[dispatchCallCount - 1];
1631
1623
  dispatchedUnitTypes.push(d.unitType);
1632
1624
  return {
1633
1625
  action: "dispatch" as const,
@@ -1692,18 +1684,11 @@ test("autoLoop lifecycle: advances through research → plan → execute → ver
1692
1684
  `callLog should have at least 5 resolveDispatch entries (got ${dispatchEntries.length})`,
1693
1685
  );
1694
1686
 
1695
- // Verify interleaving: each resolveDispatch should follow a deriveState
1696
- let dispatchSeen = 0;
1697
- for (const entry of deps.callLog) {
1698
- if (entry === "resolveDispatch") {
1699
- dispatchSeen++;
1700
- }
1701
- if (entry === "deriveState" && dispatchSeen > 0) {
1702
- // A deriveState after a resolveDispatch confirms the loop advanced
1703
- break;
1704
- }
1705
- }
1706
- assert.ok(dispatchSeen > 0, "resolveDispatch should appear in callLog");
1687
+ // Verify interleaving: a deriveState must follow a resolveDispatch (confirms loop advanced)
1688
+ const firstDispatchIdx = deps.callLog.indexOf("resolveDispatch");
1689
+ const firstDeriveAfterDispatch = deps.callLog.indexOf("deriveState", firstDispatchIdx + 1);
1690
+ assert.ok(firstDispatchIdx >= 0, "resolveDispatch should appear in callLog");
1691
+ assert.ok(firstDeriveAfterDispatch > firstDispatchIdx, "deriveState should follow resolveDispatch to confirm loop advanced");
1707
1692
 
1708
1693
  // Assert the exact sequence of dispatched unit types
1709
1694
  assert.deepEqual(
@@ -1776,6 +1761,8 @@ test("autoLoop re-iterates when postUnitPreVerification returns retry (#1571)",
1776
1761
  const s = makeLoopSession();
1777
1762
 
1778
1763
  let preVerifyCallCount = 0;
1764
+ // Pre-queued responses: first call returns "retry", second returns "continue"
1765
+ const preVerifyResponses = ["retry", "continue"] as const;
1779
1766
 
1780
1767
  const deps = makeMockDeps({
1781
1768
  deriveState: async () => {
@@ -1791,11 +1778,7 @@ test("autoLoop re-iterates when postUnitPreVerification returns retry (#1571)",
1791
1778
  },
1792
1779
  postUnitPreVerification: async () => {
1793
1780
  deps.callLog.push("postUnitPreVerification");
1794
- preVerifyCallCount++;
1795
- if (preVerifyCallCount === 1) {
1796
- return "retry" as const;
1797
- }
1798
- return "continue" as const;
1781
+ return preVerifyResponses[preVerifyCallCount++] ?? "continue";
1799
1782
  },
1800
1783
  postUnitPostVerification: async () => {
1801
1784
  deps.callLog.push("postUnitPostVerification");
@@ -1908,9 +1891,8 @@ test("autoLoop rejects execute-task with 0 tool calls as hallucinated (#1833)",
1908
1891
  postUnitPostVerification: async () => {
1909
1892
  deps.callLog.push("postUnitPostVerification");
1910
1893
  iterationCount++;
1911
- if (iterationCount >= 2) {
1912
- s.active = false;
1913
- }
1894
+ // Deactivate after 2nd iteration
1895
+ s.active = iterationCount < 2;
1914
1896
  return "continue" as const;
1915
1897
  },
1916
1898
  });
@@ -101,8 +101,8 @@ test('secrets gate: pending keys exist — gate triggers collection, manifest up
101
101
  const status = await getManifestStatus(tmp, 'M001');
102
102
  assert.notStrictEqual(status, null, 'manifest should exist');
103
103
  assert.ok(status!.pending.length > 0, 'should have pending keys');
104
- assert.deepStrictEqual(status!.pending, ['GSD_GATE_TEST_PEND_A', 'GSD_GATE_TEST_PEND_B']);
105
- assert.deepStrictEqual(status!.existing, ['GSD_GATE_TEST_EXISTING']);
104
+ assert.deepStrictEqual(status!.pending, ['GSD_GATE_TEST_PEND_A', 'GSD_GATE_TEST_PEND_B'], 'pending keys');
105
+ assert.deepStrictEqual(status!.existing, ['GSD_GATE_TEST_EXISTING'], 'existing keys');
106
106
 
107
107
  // (b) Call collectSecretsFromManifest with no-UI context
108
108
  // With hasUI: false, collectOneSecret returns null → pending keys become "skipped"
@@ -119,12 +119,23 @@ test("captures: loadPendingCaptures filters resolved entries", () => {
119
119
  const id1 = appendCapture(tmp, "pending one");
120
120
  appendCapture(tmp, "pending two");
121
121
 
122
- // Resolve the first one
123
122
  markCaptureResolved(tmp, id1, "note", "acknowledged", "just a note");
124
123
 
125
124
  const pending = loadPendingCaptures(tmp);
126
125
  assert.strictEqual(pending.length, 1, "should have 1 pending");
127
126
  assert.strictEqual(pending[0].text, "pending two");
127
+ } finally {
128
+ rmSync(tmp, { recursive: true, force: true });
129
+ }
130
+ });
131
+
132
+ test("captures: loadAllCaptures preserves resolved entries", () => {
133
+ const tmp = makeTempDir("cap-all-resolved");
134
+ try {
135
+ const id1 = appendCapture(tmp, "pending one");
136
+ appendCapture(tmp, "pending two");
137
+
138
+ markCaptureResolved(tmp, id1, "note", "acknowledged", "just a note");
128
139
 
129
140
  const all = loadAllCaptures(tmp);
130
141
  assert.strictEqual(all.length, 2, "all should still have 2");
@@ -72,18 +72,17 @@ describe("continue-here", () => {
72
72
  const budget = computeBudgets(128_000);
73
73
  const threshold = budget.continueThresholdPercent;
74
74
 
75
- // Simulate repeated polls with percent above threshold
76
- let fired = false;
77
- let fireCount = 0;
75
+ // Simulate repeated polls with percent above threshold using a reducer
76
+ // so there is no control flow inside the test body.
78
77
  const usagePercents = [75, 80, 85, 90, 95];
79
-
80
- for (const percent of usagePercents) {
81
- if (fired) continue; // one-shot guard
82
- if (percent >= threshold) {
83
- fired = true;
84
- fireCount++;
85
- }
86
- }
78
+ const { fired, fireCount } = usagePercents.reduce(
79
+ (acc, percent) => {
80
+ if (acc.fired) return acc; // one-shot guard
81
+ if (percent >= threshold) return { fired: true, fireCount: acc.fireCount + 1 };
82
+ return acc;
83
+ },
84
+ { fired: false, fireCount: 0 },
85
+ );
87
86
 
88
87
  assert.equal(fireCount, 1, "must fire exactly once");
89
88
  assert.equal(fired, true);
@@ -97,16 +96,17 @@ describe("continue-here", () => {
97
96
  { name: "1M", contextWindow: 1_000_000 },
98
97
  ];
99
98
 
100
- it("all model sizes produce continueThresholdPercent of 70", () => {
101
- for (const { name, contextWindow } of modelSizes) {
99
+ const thresholdCases: Array<[string, number]> = [
100
+ ["128K", 128_000],
101
+ ["200K", 200_000],
102
+ ["1M", 1_000_000],
103
+ ];
104
+ for (const [name, contextWindow] of thresholdCases) {
105
+ it(`${name} model produces continueThresholdPercent of 70`, () => {
102
106
  const budget = computeBudgets(contextWindow);
103
- assert.equal(
104
- budget.continueThresholdPercent,
105
- 70,
106
- `${name} model should have 70% threshold`,
107
- );
108
- }
109
- });
107
+ assert.equal(budget.continueThresholdPercent, 70, `${name} model should have 70% threshold`);
108
+ });
109
+ }
110
110
 
111
111
  it("larger models produce larger verificationBudgetChars", () => {
112
112
  const budgets = modelSizes.map(({ contextWindow }) => computeBudgets(contextWindow));