gsd-pi 2.82.0-dev.2841a1e44 → 2.82.0-dev.9d5798940
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.
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/partial-builder.js +2 -1
- package/dist/resources/extensions/gsd/auto-dispatch.js +13 -6
- package/dist/resources/extensions/gsd/auto-post-unit.js +69 -8
- package/dist/resources/extensions/gsd/auto-recovery.js +31 -1
- package/dist/resources/extensions/gsd/auto-start.js +7 -3
- package/dist/resources/extensions/gsd/auto-worktree.js +96 -0
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +4 -1
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +13 -1
- package/dist/resources/extensions/gsd/commands/handlers/core.js +17 -1
- package/dist/resources/extensions/gsd/db/unit-dispatches.js +2 -2
- package/dist/resources/extensions/gsd/export-html.js +27 -425
- package/dist/resources/extensions/gsd/milestone-actions.js +11 -4
- package/dist/resources/extensions/gsd/native-git-bridge.js +8 -3
- package/dist/resources/extensions/gsd/state-reconciliation/drift/merge-state.js +6 -1
- package/dist/resources/extensions/gsd/tools/plan-slice.js +2 -1
- package/dist/resources/extensions/gsd/unit-context-manifest.js +7 -8
- package/dist/resources/extensions/gsd/worktree-lifecycle.js +28 -7
- package/dist/resources/extensions/shared/html-shell.js +388 -0
- package/dist/resources/extensions/visual-brief/page-contract.js +2 -0
- package/dist/resources/extensions/visual-brief/prompts.js +29 -0
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
- package/dist/web/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +4 -7
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +4 -7
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +4 -5
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -5
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +4 -7
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -7
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +4 -5
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -5
- package/dist/web/standalone/.next/server/app/page.js +2 -2
- package/dist/web/standalone/.next/server/app/page.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
- package/dist/web/standalone/.next/server/chunks/4266.js +2 -0
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
- package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/app/layout-8c10ec293ae0f1d5.js +1 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-6a95bc41e0f7ec89.js → webpack-9a4db269f9ed63ad.js} +1 -1
- package/dist/web/standalone/.next/static/css/746ee28c929d1880.css +1 -0
- package/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/partial-builder.ts +2 -1
- package/src/resources/extensions/claude-code-cli/tests/partial-builder.test.ts +19 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +14 -6
- package/src/resources/extensions/gsd/auto-post-unit.ts +76 -6
- package/src/resources/extensions/gsd/auto-recovery.ts +29 -0
- package/src/resources/extensions/gsd/auto-start.ts +7 -3
- package/src/resources/extensions/gsd/auto-worktree.ts +104 -0
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +6 -1
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +16 -1
- package/src/resources/extensions/gsd/commands/handlers/core.ts +17 -1
- package/src/resources/extensions/gsd/db/unit-dispatches.ts +3 -3
- package/src/resources/extensions/gsd/export-html.ts +27 -427
- package/src/resources/extensions/gsd/milestone-actions.ts +10 -4
- package/src/resources/extensions/gsd/native-git-bridge.ts +8 -3
- package/src/resources/extensions/gsd/state-reconciliation/drift/merge-state.ts +8 -1
- package/src/resources/extensions/gsd/tests/auto-deterministic-error-classification-4973.test.ts +116 -0
- package/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts +12 -1
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +15 -1
- package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +69 -1
- package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +57 -2
- package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/export-html-enhancements.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/park-db-sync.test.ts +55 -1
- package/src/resources/extensions/gsd/tests/plan-slice.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/post-unit-git-failure.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/remediation-completion-guard.test.ts +46 -2
- package/src/resources/extensions/gsd/tests/session-switch-abort-misclassification.test.ts +10 -0
- package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +65 -7
- package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +64 -12
- package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +31 -0
- package/src/resources/extensions/gsd/tools/plan-slice.ts +2 -0
- package/src/resources/extensions/gsd/unit-context-manifest.ts +12 -9
- package/src/resources/extensions/gsd/worktree-lifecycle.ts +34 -7
- package/src/resources/extensions/shared/html-shell.ts +412 -0
- package/src/resources/extensions/visual-brief/page-contract.ts +2 -0
- package/src/resources/extensions/visual-brief/prompts.ts +37 -1
- package/src/resources/extensions/visual-brief/tests/visual-brief.test.ts +40 -0
- package/dist/web/standalone/.next/server/chunks/5822.js +0 -2
- package/dist/web/standalone/.next/static/chunks/app/layout-a16c7a7ecdf0c2cf.js +0 -1
- package/dist/web/standalone/.next/static/css/0262768ec1b89d34.css +0 -1
- package/dist/web/standalone/.next/static/css/de70bee13400563f.css +0 -1
- package/dist/web/standalone/.next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
- package/dist/web/standalone/.next/static/media/747892c23ea88013-s.woff2 +0 -0
- package/dist/web/standalone/.next/static/media/8d697b304b401681-s.woff2 +0 -0
- package/dist/web/standalone/.next/static/media/93f479601ee12b01-s.p.woff2 +0 -0
- package/dist/web/standalone/.next/static/media/9610d9e46709d722-s.woff2 +0 -0
- package/dist/web/standalone/.next/static/media/ba015fad6dcf6784-s.woff2 +0 -0
- /package/dist/web/standalone/.next/static/{Qgr2B_MRhPxC0z8fwv4vT → BdZQhe8yKl6bdKLiXVEzh}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{Qgr2B_MRhPxC0z8fwv4vT → BdZQhe8yKl6bdKLiXVEzh}/_ssgManifest.js +0 -0
package/src/resources/extensions/gsd/tests/auto-deterministic-error-classification-4973.test.ts
CHANGED
|
@@ -42,6 +42,20 @@ function makeTmpBase(): string {
|
|
|
42
42
|
return base;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function makeBrokenIsolatedWorktree(): string {
|
|
46
|
+
const root = mkdtempSync(join(tmpdir(), `gsd-test-5848-${randomUUID().slice(0, 8)}-`));
|
|
47
|
+
tmpDirs.push(root);
|
|
48
|
+
const base = join(root, ".gsd", "projects", "project-id", "worktrees", "M003");
|
|
49
|
+
mkdirSync(join(base, ".gsd", "milestones", "M003", "slices", "S03"), { recursive: true });
|
|
50
|
+
return base;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function makeBrokenIsolatedWorktreeRevParse(): string {
|
|
54
|
+
const base = makeBrokenIsolatedWorktree();
|
|
55
|
+
mkdirSync(join(base, ".git"));
|
|
56
|
+
return base;
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
function resetAutoState(): void {
|
|
46
60
|
_setAutoActiveForTest(false);
|
|
47
61
|
}
|
|
@@ -231,6 +245,108 @@ describe("Test 5 — postUnitPreVerification short-circuits on deterministic err
|
|
|
231
245
|
});
|
|
232
246
|
});
|
|
233
247
|
|
|
248
|
+
describe("Test 5b — broken isolated worktree short-circuits artifact retry (#5848)", () => {
|
|
249
|
+
test("pauses with worktree integrity failure instead of setting pendingVerificationRetry", async () => {
|
|
250
|
+
const { postUnitPreVerification } = await import("../auto-post-unit.ts");
|
|
251
|
+
|
|
252
|
+
const base = makeBrokenIsolatedWorktree();
|
|
253
|
+
const s = new AutoSession();
|
|
254
|
+
s.active = true;
|
|
255
|
+
s.basePath = base;
|
|
256
|
+
s.currentUnit = { type: "research-slice", id: "M003/S03", startedAt: Date.now() };
|
|
257
|
+
s.verificationRetryCount.set("research-slice:M003/S03", 2);
|
|
258
|
+
|
|
259
|
+
const notifications: string[] = [];
|
|
260
|
+
let pauseCalled = false;
|
|
261
|
+
const pctx = {
|
|
262
|
+
s,
|
|
263
|
+
ctx: {
|
|
264
|
+
ui: {
|
|
265
|
+
notify: (message: string) => {
|
|
266
|
+
notifications.push(message);
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
pi: {},
|
|
271
|
+
buildSnapshotOpts: () => ({}) as any,
|
|
272
|
+
lockBase: () => base,
|
|
273
|
+
stopAuto: async () => {},
|
|
274
|
+
pauseAuto: async () => {
|
|
275
|
+
pauseCalled = true;
|
|
276
|
+
},
|
|
277
|
+
updateProgressWidget: () => {},
|
|
278
|
+
} as any;
|
|
279
|
+
|
|
280
|
+
const result = await postUnitPreVerification(pctx, {
|
|
281
|
+
skipSettleDelay: true,
|
|
282
|
+
skipWorktreeSync: true,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
assert.strictEqual(result, "dispatched", "worktree integrity failure must pause instead of retrying");
|
|
286
|
+
assert.strictEqual(pauseCalled, true, "pauseAuto must be called for a broken isolated worktree");
|
|
287
|
+
assert.strictEqual(s.pendingVerificationRetry, null, "pendingVerificationRetry must NOT be set");
|
|
288
|
+
assert.strictEqual(s.verificationRetryCount.has("research-slice:M003/S03"), false, "stale retry count must be cleared");
|
|
289
|
+
assert.ok(
|
|
290
|
+
notifications.some((message) => message.includes("Worktree integrity failure") && message.includes(".git missing")),
|
|
291
|
+
`expected worktree integrity notification, got: ${notifications.join("\n")}`,
|
|
292
|
+
);
|
|
293
|
+
assert.ok(
|
|
294
|
+
notifications.every((message) => !message.includes("Artifact verification failed")),
|
|
295
|
+
`must not surface artifact retry messaging, got: ${notifications.join("\n")}`,
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("pauses when git rev-parse cannot validate an isolated worktree", async () => {
|
|
300
|
+
const { postUnitPreVerification } = await import("../auto-post-unit.ts");
|
|
301
|
+
|
|
302
|
+
const base = makeBrokenIsolatedWorktreeRevParse();
|
|
303
|
+
const s = new AutoSession();
|
|
304
|
+
s.active = true;
|
|
305
|
+
s.basePath = base;
|
|
306
|
+
s.currentUnit = { type: "research-slice", id: "M003/S03", startedAt: Date.now() };
|
|
307
|
+
s.verificationRetryCount.set("research-slice:M003/S03", 2);
|
|
308
|
+
|
|
309
|
+
const notifications: string[] = [];
|
|
310
|
+
let pauseCalled = false;
|
|
311
|
+
const pctx = {
|
|
312
|
+
s,
|
|
313
|
+
ctx: {
|
|
314
|
+
ui: {
|
|
315
|
+
notify: (message: string) => {
|
|
316
|
+
notifications.push(message);
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
pi: {},
|
|
321
|
+
buildSnapshotOpts: () => ({}) as any,
|
|
322
|
+
lockBase: () => base,
|
|
323
|
+
stopAuto: async () => {},
|
|
324
|
+
pauseAuto: async () => {
|
|
325
|
+
pauseCalled = true;
|
|
326
|
+
},
|
|
327
|
+
updateProgressWidget: () => {},
|
|
328
|
+
} as any;
|
|
329
|
+
|
|
330
|
+
const result = await postUnitPreVerification(pctx, {
|
|
331
|
+
skipSettleDelay: true,
|
|
332
|
+
skipWorktreeSync: true,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
assert.strictEqual(result, "dispatched", "worktree integrity failure must pause instead of retrying");
|
|
336
|
+
assert.strictEqual(pauseCalled, true, "pauseAuto must be called for a broken isolated worktree");
|
|
337
|
+
assert.strictEqual(s.pendingVerificationRetry, null, "pendingVerificationRetry must NOT be set");
|
|
338
|
+
assert.strictEqual(s.verificationRetryCount.has("research-slice:M003/S03"), false, "stale retry count must be cleared");
|
|
339
|
+
assert.ok(
|
|
340
|
+
notifications.some((message) => message.includes("Worktree integrity failure") && message.includes("git rev-parse")),
|
|
341
|
+
`expected git rev-parse worktree integrity notification, got: ${notifications.join("\n")}`,
|
|
342
|
+
);
|
|
343
|
+
assert.ok(
|
|
344
|
+
notifications.every((message) => !message.includes("Artifact verification failed")),
|
|
345
|
+
`must not surface artifact retry messaging, got: ${notifications.join("\n")}`,
|
|
346
|
+
);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
234
350
|
// ─── Test 6: Model-quality failures use standard retry path ──────────────────
|
|
235
351
|
|
|
236
352
|
describe("Test 6 — non-deterministic failures use standard retry; tier escalates once (#4973)", () => {
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
import test from "node:test";
|
|
4
4
|
import assert from "node:assert/strict";
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
buildStepCompleteMessage,
|
|
8
|
+
shouldReturnStepWizardAfterUnit,
|
|
9
|
+
STEP_COMPLETE_FALLBACK_MESSAGE,
|
|
10
|
+
} from "../auto-post-unit.ts";
|
|
7
11
|
import type { GSDState } from "../types.ts";
|
|
8
12
|
|
|
9
13
|
function makeState(overrides: Partial<GSDState>): GSDState {
|
|
@@ -51,3 +55,10 @@ test("STEP_COMPLETE_FALLBACK_MESSAGE: used when deriveState throws, still points
|
|
|
51
55
|
assert.match(STEP_COMPLETE_FALLBACK_MESSAGE, /\/clear/);
|
|
52
56
|
assert.match(STEP_COMPLETE_FALLBACK_MESSAGE, /\/gsd/);
|
|
53
57
|
});
|
|
58
|
+
|
|
59
|
+
test("shouldReturnStepWizardAfterUnit: terminal milestone completion continues to merge-back path", () => {
|
|
60
|
+
assert.equal(shouldReturnStepWizardAfterUnit("complete-milestone", "complete"), false);
|
|
61
|
+
assert.equal(shouldReturnStepWizardAfterUnit("complete-milestone", null), false);
|
|
62
|
+
assert.equal(shouldReturnStepWizardAfterUnit("execute-task", "complete"), false);
|
|
63
|
+
assert.equal(shouldReturnStepWizardAfterUnit("execute-task", "executing"), true);
|
|
64
|
+
});
|
|
@@ -5,7 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
|
|
8
|
-
import { verifyExpectedArtifact, hasImplementationArtifacts, resolveExpectedArtifactPath, diagnoseExpectedArtifact, buildLoopRemediationSteps, writeBlockerPlaceholder, refreshRecoveryDbForArtifact } from "../auto-recovery.ts";
|
|
8
|
+
import { verifyExpectedArtifact, hasImplementationArtifacts, resolveExpectedArtifactPath, diagnoseExpectedArtifact, diagnoseWorktreeIntegrityFailure, buildLoopRemediationSteps, writeBlockerPlaceholder, refreshRecoveryDbForArtifact } from "../auto-recovery.ts";
|
|
9
9
|
import { resolveMilestoneFile } from "../paths.ts";
|
|
10
10
|
import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertGateRow, insertTask, getMilestoneCommitAttributionShas } from "../gsd-db.ts";
|
|
11
11
|
import { clearParseCache } from "../files.ts";
|
|
@@ -141,6 +141,20 @@ test("resolveExpectedArtifactPath returns null for unknown type", () => {
|
|
|
141
141
|
}
|
|
142
142
|
});
|
|
143
143
|
|
|
144
|
+
test("diagnoseWorktreeIntegrityFailure reports missing GSD worktree paths only", () => {
|
|
145
|
+
const missingWorktreePath = join(tmpdir(), `gsd-test-${randomUUID()}`, ".gsd", "worktrees", "M001-S01");
|
|
146
|
+
assert.equal(
|
|
147
|
+
diagnoseWorktreeIntegrityFailure(join(tmpdir(), `gsd-test-${randomUUID()}`)),
|
|
148
|
+
null,
|
|
149
|
+
"non-GSD paths should keep falling through to artifact recovery",
|
|
150
|
+
);
|
|
151
|
+
assert.equal(
|
|
152
|
+
diagnoseWorktreeIntegrityFailure(missingWorktreePath),
|
|
153
|
+
`Worktree integrity failure: ${missingWorktreePath} does not exist. Repair or recreate the worktree before retrying.`,
|
|
154
|
+
"missing GSD worktree paths should fail terminally before artifact retry",
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
144
158
|
test("resolveExpectedArtifactPath returns correct path for all milestone-level types", () => {
|
|
145
159
|
const base = makeTmpBase();
|
|
146
160
|
try {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { describe, test, beforeEach } from "node:test";
|
|
4
4
|
import assert from "node:assert/strict";
|
|
5
|
-
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, realpathSync } from "node:fs";
|
|
5
|
+
import { existsSync, mkdtempSync, mkdirSync, writeFileSync, rmSync, realpathSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import { execFileSync } from "node:child_process";
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
_resetAutoWorktreeOriginalBaseForTests,
|
|
14
14
|
createAutoWorktree,
|
|
15
15
|
enterAutoWorktree,
|
|
16
|
+
mergeMilestoneToMain,
|
|
16
17
|
teardownAutoWorktree,
|
|
17
18
|
} from "../auto-worktree.ts";
|
|
18
19
|
|
|
@@ -173,4 +174,71 @@ describe("auto-worktree workspace registry", () => {
|
|
|
173
174
|
teardownAutoWorktree(dir2, "M020");
|
|
174
175
|
try { process.chdir(savedCwd); } catch { /* ignore */ }
|
|
175
176
|
});
|
|
177
|
+
|
|
178
|
+
test("mergeMilestoneToMain cleans up when milestone branch was already regular-merged", (t) => {
|
|
179
|
+
const tempDir = createTempRepo(t);
|
|
180
|
+
const msDir = join(tempDir, ".gsd", "milestones", "M003");
|
|
181
|
+
mkdirSync(msDir, { recursive: true });
|
|
182
|
+
writeFileSync(join(msDir, "CONTEXT.md"), "# M003 Context\n");
|
|
183
|
+
git(["add", "."], tempDir);
|
|
184
|
+
git(["commit", "-m", "add milestone"], tempDir);
|
|
185
|
+
|
|
186
|
+
createAutoWorktree(tempDir, "M003");
|
|
187
|
+
const wtDir = join(tempDir, ".gsd", "worktrees", "M003");
|
|
188
|
+
writeFileSync(join(wtDir, "feature.txt"), "implemented\n");
|
|
189
|
+
git(["add", "feature.txt"], wtDir);
|
|
190
|
+
git(["commit", "-m", "feat: implement M003"], wtDir);
|
|
191
|
+
|
|
192
|
+
process.chdir(tempDir);
|
|
193
|
+
git(["merge", "--no-ff", "milestone/M003", "-m", "merge M003"], tempDir);
|
|
194
|
+
|
|
195
|
+
process.chdir(wtDir);
|
|
196
|
+
const result = mergeMilestoneToMain(tempDir, "M003", "# M003\n- [x] **S01: Done**\n");
|
|
197
|
+
|
|
198
|
+
assert.equal(result.codeFilesChanged, true);
|
|
199
|
+
assert.equal(result.pushed, false);
|
|
200
|
+
assert.equal(result.prCreated, false);
|
|
201
|
+
assert.equal(existsSync(wtDir), false, "worktree directory is removed");
|
|
202
|
+
assert.throws(
|
|
203
|
+
() => git(["rev-parse", "--verify", "milestone/M003"], tempDir),
|
|
204
|
+
/Command failed/,
|
|
205
|
+
"already-merged milestone branch is deleted",
|
|
206
|
+
);
|
|
207
|
+
try { process.chdir(savedCwd); } catch { /* ignore */ }
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("mergeMilestoneToMain cleans up already-merged milestone after main advances", (t) => {
|
|
211
|
+
const tempDir = createTempRepo(t);
|
|
212
|
+
const msDir = join(tempDir, ".gsd", "milestones", "M004");
|
|
213
|
+
mkdirSync(msDir, { recursive: true });
|
|
214
|
+
writeFileSync(join(msDir, "CONTEXT.md"), "# M004 Context\n");
|
|
215
|
+
git(["add", "."], tempDir);
|
|
216
|
+
git(["commit", "-m", "add milestone"], tempDir);
|
|
217
|
+
|
|
218
|
+
createAutoWorktree(tempDir, "M004");
|
|
219
|
+
const wtDir = join(tempDir, ".gsd", "worktrees", "M004");
|
|
220
|
+
writeFileSync(join(wtDir, "feature.txt"), "implemented\n");
|
|
221
|
+
git(["add", "feature.txt"], wtDir);
|
|
222
|
+
git(["commit", "-m", "feat: implement M004"], wtDir);
|
|
223
|
+
|
|
224
|
+
process.chdir(tempDir);
|
|
225
|
+
git(["merge", "--no-ff", "milestone/M004", "-m", "merge M004"], tempDir);
|
|
226
|
+
writeFileSync(join(tempDir, "hotfix.txt"), "later main work\n");
|
|
227
|
+
git(["add", "hotfix.txt"], tempDir);
|
|
228
|
+
git(["commit", "-m", "fix: advance main"], tempDir);
|
|
229
|
+
|
|
230
|
+
process.chdir(wtDir);
|
|
231
|
+
const result = mergeMilestoneToMain(tempDir, "M004", "# M004\n- [x] **S01: Done**\n");
|
|
232
|
+
|
|
233
|
+
assert.equal(result.codeFilesChanged, true);
|
|
234
|
+
assert.equal(result.pushed, false);
|
|
235
|
+
assert.equal(result.prCreated, false);
|
|
236
|
+
assert.equal(existsSync(wtDir), false, "worktree directory is removed");
|
|
237
|
+
assert.throws(
|
|
238
|
+
() => git(["rev-parse", "--verify", "milestone/M004"], tempDir),
|
|
239
|
+
/Command failed/,
|
|
240
|
+
"already-merged milestone branch is deleted",
|
|
241
|
+
);
|
|
242
|
+
try { process.chdir(savedCwd); } catch { /* ignore */ }
|
|
243
|
+
});
|
|
176
244
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
@@ -10,7 +10,7 @@ import { runDispatch, runPreDispatch } from "../auto/phases.ts";
|
|
|
10
10
|
import { AutoSession } from "../auto/session.ts";
|
|
11
11
|
import { resolveUnitSupervisionTimeouts } from "../auto-timers.ts";
|
|
12
12
|
import { bootstrapAutoSession } from "../auto-start.ts";
|
|
13
|
-
import { postUnitPreVerification } from "../auto-post-unit.ts";
|
|
13
|
+
import { postUnitPostVerification, postUnitPreVerification } from "../auto-post-unit.ts";
|
|
14
14
|
import { resolveDispatch, setResearchProjectPromptBuilderForTest } from "../auto-dispatch.ts";
|
|
15
15
|
import { resolveExpectedArtifactPath, verifyExpectedArtifact, writeBlockerPlaceholder } from "../auto-recovery.ts";
|
|
16
16
|
import { finalizeProjectResearchTimeout } from "../project-research-policy.ts";
|
|
@@ -1587,6 +1587,61 @@ test("deep project setup: discuss-milestone question failure pauses instead of a
|
|
|
1587
1587
|
}
|
|
1588
1588
|
});
|
|
1589
1589
|
|
|
1590
|
+
test("verified task git closeout failure retries and continues auto-mode", async () => {
|
|
1591
|
+
const base = makeBase();
|
|
1592
|
+
try {
|
|
1593
|
+
execFileSync("git", ["init"], { cwd: base, stdio: "ignore" });
|
|
1594
|
+
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: base, stdio: "ignore" });
|
|
1595
|
+
execFileSync("git", ["config", "user.name", "Test User"], { cwd: base, stdio: "ignore" });
|
|
1596
|
+
const hookPath = join(base, ".git", "hooks", "pre-commit");
|
|
1597
|
+
writeFileSync(
|
|
1598
|
+
hookPath,
|
|
1599
|
+
[
|
|
1600
|
+
"#!/bin/sh",
|
|
1601
|
+
"count_file=.git/pre-commit-count",
|
|
1602
|
+
"count=0",
|
|
1603
|
+
"if [ -f \"$count_file\" ]; then count=$(cat \"$count_file\"); fi",
|
|
1604
|
+
"count=$((count + 1))",
|
|
1605
|
+
"printf \"%s\" \"$count\" > \"$count_file\"",
|
|
1606
|
+
"echo blocked by test hook >&2",
|
|
1607
|
+
"exit 1",
|
|
1608
|
+
].join("\n"),
|
|
1609
|
+
);
|
|
1610
|
+
chmodSync(hookPath, 0o755);
|
|
1611
|
+
writeFileSync(join(base, "work.txt"), "changed\n");
|
|
1612
|
+
|
|
1613
|
+
const s = new AutoSession();
|
|
1614
|
+
s.active = true;
|
|
1615
|
+
s.basePath = base;
|
|
1616
|
+
s.originalBasePath = base;
|
|
1617
|
+
s.currentUnit = { type: "execute-task", id: "M001/S01/T01", startedAt: Date.now() };
|
|
1618
|
+
|
|
1619
|
+
let pauseCalled = false;
|
|
1620
|
+
const notifications: Array<{ message: string; severity?: string }> = [];
|
|
1621
|
+
const result = await postUnitPostVerification({
|
|
1622
|
+
s,
|
|
1623
|
+
ctx: { ui: { notify: (message: string, severity?: string) => notifications.push({ message, severity }) } } as any,
|
|
1624
|
+
pi: {} as any,
|
|
1625
|
+
buildSnapshotOpts: () => ({}) as any,
|
|
1626
|
+
lockBase: () => base,
|
|
1627
|
+
stopAuto: async () => {},
|
|
1628
|
+
pauseAuto: async () => { pauseCalled = true; },
|
|
1629
|
+
updateProgressWidget: () => {},
|
|
1630
|
+
});
|
|
1631
|
+
|
|
1632
|
+
assert.equal(result, "continue");
|
|
1633
|
+
assert.equal(pauseCalled, false);
|
|
1634
|
+
assert.equal(s.lastGitActionStatus, "failed");
|
|
1635
|
+
assert.equal(readFileSync(join(base, ".git", "pre-commit-count"), "utf-8"), "3");
|
|
1636
|
+
assert.ok(
|
|
1637
|
+
notifications.some((entry) => entry.severity === "warning" && entry.message.includes("Git commit failed")),
|
|
1638
|
+
"verified task git closeout failure should warn instead of stopping auto-mode",
|
|
1639
|
+
);
|
|
1640
|
+
} finally {
|
|
1641
|
+
rmSync(base, { recursive: true, force: true });
|
|
1642
|
+
}
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1590
1645
|
test("deep project setup: approval wait wins over deterministic write-gate placeholder", async () => {
|
|
1591
1646
|
const base = makeBase();
|
|
1592
1647
|
try {
|
|
@@ -118,3 +118,42 @@ describe("complete phase dispatch guard (#5683)", () => {
|
|
|
118
118
|
assert.equal(result?.reason, "All milestones complete.");
|
|
119
119
|
});
|
|
120
120
|
});
|
|
121
|
+
|
|
122
|
+
describe("complete milestone context recovery guard (#5831)", () => {
|
|
123
|
+
let base = "";
|
|
124
|
+
const executionEntryRule = DISPATCH_RULES.find(
|
|
125
|
+
(candidate) => candidate.name === "execution-entry phase (no context) → discuss-milestone",
|
|
126
|
+
);
|
|
127
|
+
const prePlanningRule = DISPATCH_RULES.find(
|
|
128
|
+
(candidate) => candidate.name === "pre-planning (no context) → discuss-milestone",
|
|
129
|
+
);
|
|
130
|
+
assert.ok(executionEntryRule, "execution-entry missing-context rule should exist");
|
|
131
|
+
assert.ok(prePlanningRule, "pre-planning missing-context rule should exist");
|
|
132
|
+
|
|
133
|
+
afterEach(() => {
|
|
134
|
+
if (base) rmSync(base, { recursive: true, force: true });
|
|
135
|
+
base = "";
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("does not discuss a complete execution-entry milestone with no CONTEXT file", async () => {
|
|
139
|
+
base = makeBase();
|
|
140
|
+
const ctx = buildDispatchCtx(base);
|
|
141
|
+
ctx.state.registry = [{ id: "M001", title: "Milestone One", status: "complete" }];
|
|
142
|
+
ctx.state.phase = "completing-milestone";
|
|
143
|
+
|
|
144
|
+
const result = await executionEntryRule.match(ctx);
|
|
145
|
+
|
|
146
|
+
assert.equal(result, null);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("does not discuss a complete pre-planning milestone with no CONTEXT file", async () => {
|
|
150
|
+
base = makeBase();
|
|
151
|
+
const ctx = buildDispatchCtx(base);
|
|
152
|
+
ctx.state.registry = [{ id: "M001", title: "Milestone One", status: "complete" }];
|
|
153
|
+
ctx.state.phase = "pre-planning";
|
|
154
|
+
|
|
155
|
+
const result = await prePlanningRule.match(ctx);
|
|
156
|
+
|
|
157
|
+
assert.equal(result, null);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -141,6 +141,14 @@ test("Feature 1: executive summary paragraph is rendered", () => {
|
|
|
141
141
|
assert.ok(html.includes("$2.50 spent"), "should contain cost");
|
|
142
142
|
});
|
|
143
143
|
|
|
144
|
+
test("report uses the shared GSD HTML shell", () => {
|
|
145
|
+
const html = generateHtmlReport(mockData(), mockOpts());
|
|
146
|
+
assert.ok(html.includes('<span class="logo">GSD</span>'), "should render shared shell logo");
|
|
147
|
+
assert.ok(html.includes('<span class="kind-chip">Report</span>'), "should render report kind chip");
|
|
148
|
+
assert.ok(html.includes('<nav class="toc" aria-label="Report sections">'), "should render shared shell TOC");
|
|
149
|
+
assert.ok(html.includes('<main>'), "should render content inside shared shell main");
|
|
150
|
+
});
|
|
151
|
+
|
|
144
152
|
test("Feature 1: executive summary includes budget context when set", () => {
|
|
145
153
|
const data = mockData({ health: { ...mockData().health, budgetCeiling: 10.00 } });
|
|
146
154
|
const html = generateHtmlReport(data, mockOpts());
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { test } from "node:test";
|
|
9
9
|
import assert from "node:assert/strict";
|
|
10
|
-
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
10
|
+
import { existsSync, mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
13
|
|
|
@@ -48,6 +48,60 @@ test("parkMilestone updates DB status to 'parked' (#2694)", () => {
|
|
|
48
48
|
}
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
+
test("parkMilestone ignores blocked SUMMARY.md when DB milestone is active (#5828)", () => {
|
|
52
|
+
const base = createBase();
|
|
53
|
+
try {
|
|
54
|
+
openDatabase(":memory:");
|
|
55
|
+
insertMilestone({ id: "M001", title: "Test", status: "active" });
|
|
56
|
+
writeFileSync(
|
|
57
|
+
join(base, ".gsd", "milestones", "M001", "M001-SUMMARY.md"),
|
|
58
|
+
[
|
|
59
|
+
"---",
|
|
60
|
+
"status: closeout_blocked",
|
|
61
|
+
"---",
|
|
62
|
+
"",
|
|
63
|
+
"# M001 Summary",
|
|
64
|
+
"",
|
|
65
|
+
"Completion was not persisted.",
|
|
66
|
+
].join("\n"),
|
|
67
|
+
"utf-8",
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const parked = parkMilestone(base, "M001", "test");
|
|
71
|
+
|
|
72
|
+
assert.ok(parked, "active DB row should allow parking despite a blocked SUMMARY.md");
|
|
73
|
+
assert.ok(
|
|
74
|
+
existsSync(join(base, ".gsd", "milestones", "M001", "M001-PARKED.md")),
|
|
75
|
+
"PARKED.md should be written",
|
|
76
|
+
);
|
|
77
|
+
assert.equal(getMilestone("M001")!.status, "parked", "DB status should be parked");
|
|
78
|
+
} finally {
|
|
79
|
+
closeDatabase();
|
|
80
|
+
rmSync(base, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("parkMilestone refuses DB-complete milestones (#5828)", () => {
|
|
85
|
+
const base = createBase();
|
|
86
|
+
try {
|
|
87
|
+
openDatabase(":memory:");
|
|
88
|
+
insertMilestone({ id: "M001", title: "Test", status: "complete" });
|
|
89
|
+
|
|
90
|
+
const parked = parkMilestone(base, "M001", "test");
|
|
91
|
+
|
|
92
|
+
assert.equal(parked, false, "complete DB row should not be parkable");
|
|
93
|
+
assert.equal(
|
|
94
|
+
existsSync(join(base, ".gsd", "milestones", "M001", "M001-PARKED.md")),
|
|
95
|
+
false,
|
|
96
|
+
"PARKED.md should not be written",
|
|
97
|
+
);
|
|
98
|
+
assert.equal(getMilestone("M001")!.status, "complete", "DB status should remain complete");
|
|
99
|
+
} finally {
|
|
100
|
+
closeDatabase();
|
|
101
|
+
rmSync(base, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
51
105
|
test("unparkMilestone updates DB status to 'active' (#2694)", () => {
|
|
52
106
|
const base = createBase();
|
|
53
107
|
try {
|
|
@@ -125,6 +125,31 @@ test('handlePlanSlice advances DB-derived state out of planning immediately', as
|
|
|
125
125
|
}
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
+
test('handlePlanSlice clears sketch flag so DB-derived state leaves refining', async () => {
|
|
129
|
+
const base = makeTmpBase();
|
|
130
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
insertMilestone({ id: 'M001', title: 'Milestone', status: 'active' });
|
|
134
|
+
insertSlice({ id: 'S02', milestoneId: 'M001', title: 'Planning slice', status: 'pending', demo: 'Rendered plans exist.', isSketch: true });
|
|
135
|
+
|
|
136
|
+
invalidateStateCache();
|
|
137
|
+
const before = await deriveState(base);
|
|
138
|
+
assert.equal(before.phase, 'refining');
|
|
139
|
+
|
|
140
|
+
const result = await handlePlanSlice(validParams(), base);
|
|
141
|
+
assert.ok(!('error' in result), `unexpected error: ${'error' in result ? result.error : ''}`);
|
|
142
|
+
assert.equal(getSlice('M001', 'S02')?.is_sketch, 0, 'planned slice must no longer be treated as a sketch');
|
|
143
|
+
|
|
144
|
+
invalidateStateCache();
|
|
145
|
+
const after = await deriveState(base);
|
|
146
|
+
assert.notEqual(after.phase, 'refining');
|
|
147
|
+
assert.equal(after.progress?.tasks?.total, 2);
|
|
148
|
+
} finally {
|
|
149
|
+
cleanup(base);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
128
153
|
test('handlePlanSlice leaves omitted enrichment fields empty instead of rendering placeholders', async () => {
|
|
129
154
|
const base = makeTmpBase();
|
|
130
155
|
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
@@ -11,7 +11,7 @@ const source = readFileSync(
|
|
|
11
11
|
|
|
12
12
|
test("postUnitPreVerification blocks on git action failure", () => {
|
|
13
13
|
const failureBlock = extractSourceRegion(source, 'if (gitResult.status === "failed")');
|
|
14
|
-
assert.ok(failureBlock.includes('ctx.ui.notify(failureMsg, "error")'));
|
|
14
|
+
assert.ok(failureBlock.includes('ctx.ui.notify(failureMsg, opts?.softFailure ? "warning" : "error")'));
|
|
15
15
|
assert.ok(failureBlock.includes("await pauseAuto(ctx, pi)"));
|
|
16
16
|
assert.ok(failureBlock.includes('return "dispatched"'));
|
|
17
17
|
assert.ok(!failureBlock.includes("git-action-failed-nonblocking"));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Regression
|
|
3
|
-
* block completion when VALIDATION
|
|
2
|
+
* Regression tests for non-passing VALIDATION verdicts: completing-milestone
|
|
3
|
+
* dispatch must block completion when VALIDATION needs remediation or attention.
|
|
4
4
|
*
|
|
5
5
|
* Without this guard, needs-remediation + allSlicesDone causes a loop:
|
|
6
6
|
* complete-milestone dispatched → agent refuses (correct) → no SUMMARY
|
|
@@ -66,6 +66,50 @@ test("completing-milestone blocks when VALIDATION verdict is needs-remediation (
|
|
|
66
66
|
}
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
+
test("completing-milestone blocks when VALIDATION verdict is needs-attention (#5747)", async () => {
|
|
70
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-attention-"));
|
|
71
|
+
mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
writeFileSync(
|
|
75
|
+
join(base, ".gsd", "milestones", "M001", "M001-VALIDATION.md"),
|
|
76
|
+
[
|
|
77
|
+
"---",
|
|
78
|
+
"verdict: needs-attention",
|
|
79
|
+
"remediation_round: 0",
|
|
80
|
+
"---",
|
|
81
|
+
"",
|
|
82
|
+
"# Validation Report",
|
|
83
|
+
"",
|
|
84
|
+
"Acceptance proof is incomplete and needs human attention.",
|
|
85
|
+
].join("\n"),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const ctx = {
|
|
89
|
+
mid: "M001",
|
|
90
|
+
midTitle: "Test Milestone",
|
|
91
|
+
basePath: base,
|
|
92
|
+
state: { phase: "completing-milestone" } as any,
|
|
93
|
+
prefs: {} as any,
|
|
94
|
+
session: undefined,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const result = await completingRule!.match(ctx);
|
|
98
|
+
|
|
99
|
+
assert.ok(result !== null, "rule should match");
|
|
100
|
+
assert.equal(result!.action, "stop", "should return stop action");
|
|
101
|
+
if (result!.action === "stop") {
|
|
102
|
+
assert.equal(result!.level, "warning", "should be warning level (pausable)");
|
|
103
|
+
assert.ok(
|
|
104
|
+
result!.reason.includes("needs-attention"),
|
|
105
|
+
"reason should mention needs-attention",
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
} finally {
|
|
109
|
+
rmSync(base, { recursive: true, force: true });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
69
113
|
test("completing-milestone proceeds normally when VALIDATION verdict is pass (#2675 guard)", async () => {
|
|
70
114
|
const base = mkdtempSync(join(tmpdir(), "gsd-remediation-"));
|
|
71
115
|
mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
|
|
@@ -5,6 +5,7 @@ import test from "node:test";
|
|
|
5
5
|
import assert from "node:assert/strict";
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
|
+
_hasEmptyAgentEndContent,
|
|
8
9
|
_handleSessionSwitchAgentEnd,
|
|
9
10
|
isBareClaudeCodeStreamAbortPlaceholder,
|
|
10
11
|
isClaudeCodeSessionSwitchAbortMessage,
|
|
@@ -188,6 +189,15 @@ test("empty-content aborted during session-switch is silently ignored", () => {
|
|
|
188
189
|
assert.equal(cancelledWith, null);
|
|
189
190
|
});
|
|
190
191
|
|
|
192
|
+
test("missing agent_end content is classified as empty abort content", () => {
|
|
193
|
+
// Providers may omit content entirely for a late aborted agent_end. That is
|
|
194
|
+
// equivalent to empty content and must not pause/cancel the next unit.
|
|
195
|
+
assert.equal(_hasEmptyAgentEndContent(undefined), true);
|
|
196
|
+
assert.equal(_hasEmptyAgentEndContent(null), true);
|
|
197
|
+
assert.equal(_hasEmptyAgentEndContent([]), true);
|
|
198
|
+
assert.equal(_hasEmptyAgentEndContent([{ type: "text", text: "partial" }]), false);
|
|
199
|
+
});
|
|
200
|
+
|
|
191
201
|
test("completed assistant content with aborted stopReason during session-switch is ignored", () => {
|
|
192
202
|
// newSession() can abort the just-finished provider stream while the last
|
|
193
203
|
// assistant message still carries the completed unit summary. That is a
|
|
@@ -23,9 +23,12 @@ import { registerAutoWorker } from "../db/auto-workers.ts";
|
|
|
23
23
|
import { claimMilestoneLease } from "../db/milestone-leases.ts";
|
|
24
24
|
import {
|
|
25
25
|
recordDispatchClaim,
|
|
26
|
+
markCanceled,
|
|
26
27
|
getRecentUnitKeysForWorker,
|
|
28
|
+
getRecentUnitKeysForProjectRoot,
|
|
27
29
|
} from "../db/unit-dispatches.ts";
|
|
28
30
|
import { setRuntimeKv, getRuntimeKv } from "../db/runtime-kv.ts";
|
|
31
|
+
import { detectStuck } from "../auto/detect-stuck.ts";
|
|
29
32
|
|
|
30
33
|
function makeBase(): string {
|
|
31
34
|
const base = mkdtempSync(join(tmpdir(), "gsd-stuck-state-db-"));
|
|
@@ -67,6 +70,42 @@ test("getRecentUnitKeysForWorker reconstructs the recentUnits sliding window", (
|
|
|
67
70
|
assert.deepEqual(window.map(w => w.key), ["U1", "U2", "U3"]);
|
|
68
71
|
});
|
|
69
72
|
|
|
73
|
+
test("getRecentUnitKeysForProjectRoot restores compound keys used by stuck detection", (t) => {
|
|
74
|
+
const base = makeBase();
|
|
75
|
+
t.after(() => cleanup(base));
|
|
76
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
77
|
+
insertMilestone({ id: "M001", title: "T", status: "active" });
|
|
78
|
+
const worker = registerAutoWorker({ projectRootRealpath: base });
|
|
79
|
+
const lease = claimMilestoneLease(worker, "M001");
|
|
80
|
+
assert.equal(lease.ok, true);
|
|
81
|
+
if (!lease.ok) return;
|
|
82
|
+
|
|
83
|
+
for (let i = 0; i < 2; i++) {
|
|
84
|
+
const claim = recordDispatchClaim({
|
|
85
|
+
traceId: `t${i}`,
|
|
86
|
+
workerId: worker,
|
|
87
|
+
milestoneLeaseToken: lease.token,
|
|
88
|
+
milestoneId: "M001",
|
|
89
|
+
sliceId: "S01",
|
|
90
|
+
unitType: "complete-slice",
|
|
91
|
+
unitId: "M001/S01",
|
|
92
|
+
});
|
|
93
|
+
assert.equal(claim.ok, true);
|
|
94
|
+
if (!claim.ok) return;
|
|
95
|
+
markCanceled(claim.dispatchId, "pause");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const window = getRecentUnitKeysForProjectRoot(base, 20);
|
|
99
|
+
assert.deepEqual(window.map(w => w.key), [
|
|
100
|
+
"complete-slice/M001/S01",
|
|
101
|
+
"complete-slice/M001/S01",
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
const result = detectStuck([...window, { key: "complete-slice/M001/S01" }]);
|
|
105
|
+
assert.equal(result?.stuck, true);
|
|
106
|
+
assert.match(result?.reason ?? "", /3 consecutive times/);
|
|
107
|
+
});
|
|
108
|
+
|
|
70
109
|
test("getRecentUnitKeysForWorker honors the limit parameter", (t) => {
|
|
71
110
|
const base = makeBase();
|
|
72
111
|
t.after(() => cleanup(base));
|