gsd-pi 2.78.1-dev.d8826a445 → 2.78.1-dev.eccf86e27
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/README.md +5 -7
- package/dist/help-text.js +1 -1
- package/dist/resource-loader.js +6 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/detect-stuck.js +41 -5
- package/dist/resources/extensions/gsd/auto/loop.js +235 -36
- package/dist/resources/extensions/gsd/auto/phases.js +7 -5
- package/dist/resources/extensions/gsd/auto/session.js +33 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +46 -2
- package/dist/resources/extensions/gsd/auto-post-unit.js +19 -11
- package/dist/resources/extensions/gsd/auto-worktree.js +26 -187
- package/dist/resources/extensions/gsd/auto.js +79 -50
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +9 -4
- package/dist/resources/extensions/gsd/crash-recovery.js +160 -47
- package/dist/resources/extensions/gsd/db/auto-workers.js +227 -0
- package/dist/resources/extensions/gsd/db/command-queue.js +105 -0
- package/dist/resources/extensions/gsd/db/milestone-leases.js +210 -0
- package/dist/resources/extensions/gsd/db/runtime-kv.js +91 -0
- package/dist/resources/extensions/gsd/db/unit-dispatches.js +322 -0
- package/dist/resources/extensions/gsd/docs/COORDINATION.md +42 -0
- package/dist/resources/extensions/gsd/doctor-proactive.js +4 -0
- package/dist/resources/extensions/gsd/doctor-runtime-checks.js +22 -6
- package/dist/resources/extensions/gsd/doctor.js +12 -2
- package/dist/resources/extensions/gsd/gsd-db.js +161 -3
- package/dist/resources/extensions/gsd/guided-flow.js +6 -2
- package/dist/resources/extensions/gsd/interrupted-session.js +18 -15
- package/dist/resources/extensions/gsd/state.js +21 -6
- package/dist/resources/extensions/gsd/worktree-resolver.js +64 -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 +12 -12
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- 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.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- 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 +1 -1
- 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 +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- 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 +1 -1
- 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 +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +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/package.json +1 -1
- package/src/resources/extensions/gsd/auto/detect-stuck.ts +37 -5
- package/src/resources/extensions/gsd/auto/loop.ts +263 -41
- package/src/resources/extensions/gsd/auto/phases.ts +7 -5
- package/src/resources/extensions/gsd/auto/session.ts +36 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +53 -2
- package/src/resources/extensions/gsd/auto-post-unit.ts +19 -11
- package/src/resources/extensions/gsd/auto-worktree.ts +26 -211
- package/src/resources/extensions/gsd/auto.ts +89 -44
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +9 -4
- package/src/resources/extensions/gsd/crash-recovery.ts +177 -43
- package/src/resources/extensions/gsd/db/auto-workers.ts +273 -0
- package/src/resources/extensions/gsd/db/command-queue.ts +149 -0
- package/src/resources/extensions/gsd/db/milestone-leases.ts +274 -0
- package/src/resources/extensions/gsd/db/runtime-kv.ts +127 -0
- package/src/resources/extensions/gsd/db/unit-dispatches.ts +446 -0
- package/src/resources/extensions/gsd/docs/COORDINATION.md +42 -0
- package/src/resources/extensions/gsd/doctor-proactive.ts +4 -0
- package/src/resources/extensions/gsd/doctor-runtime-checks.ts +24 -6
- package/src/resources/extensions/gsd/doctor.ts +10 -2
- package/src/resources/extensions/gsd/gsd-db.ts +170 -3
- package/src/resources/extensions/gsd/guided-flow.ts +6 -2
- package/src/resources/extensions/gsd/interrupted-session.ts +19 -12
- package/src/resources/extensions/gsd/state.ts +44 -6
- package/src/resources/extensions/gsd/tests/auto-loop-no-copy-artifacts.test.ts +72 -0
- package/src/resources/extensions/gsd/tests/auto-loop-symlink-worktree.test.ts +190 -0
- package/src/resources/extensions/gsd/tests/auto-workers.test.ts +105 -0
- package/src/resources/extensions/gsd/tests/command-queue.test.ts +141 -0
- package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +203 -0
- package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +169 -59
- package/src/resources/extensions/gsd/tests/detect-stuck-respects-retry.test.ts +173 -0
- package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +22 -12
- package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +24 -10
- package/src/resources/extensions/gsd/tests/integration/doctor-runtime.test.ts +35 -23
- package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +3 -5
- package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +72 -25
- package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +72 -25
- package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +9 -6
- package/src/resources/extensions/gsd/tests/milestone-leases.test.ts +152 -0
- package/src/resources/extensions/gsd/tests/parallel-milestone-isolation.test.ts +106 -0
- package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +119 -0
- package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +3 -17
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +110 -0
- package/src/resources/extensions/gsd/tests/runtime-kv.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/skipped-validation-completion.test.ts +133 -28
- package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +17 -0
- package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +134 -0
- package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +7 -26
- package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +4 -8
- package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +247 -0
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +41 -1
- package/src/resources/extensions/gsd/tests/workspace.test.ts +15 -9
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +31 -23
- package/src/resources/extensions/gsd/worktree-resolver.ts +62 -0
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +0 -213
- package/src/resources/extensions/gsd/tests/auto-stale-lock-self-kill.test.ts +0 -87
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +0 -159
- /package/dist/web/standalone/.next/static/{AT5qi39nKXkdmQIOIoh0f → Y5UeGFkXTYM9WIQOWHkot}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{AT5qi39nKXkdmQIOIoh0f → Y5UeGFkXTYM9WIQOWHkot}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// gsd-2 + Stuck-state DB-migration regression (Phase C)
|
|
2
|
+
//
|
|
3
|
+
// stuck-state.json file IO has been deleted. The auto-loop now reconstructs
|
|
4
|
+
// recentUnits from unit_dispatches (Phase B ledger) and persists
|
|
5
|
+
// stuckRecoveryAttempts in runtime_kv (stable project scope, soft state).
|
|
6
|
+
//
|
|
7
|
+
// This test verifies the round-trip via the db modules directly: write
|
|
8
|
+
// dispatch rows + a runtime_kv counter, then confirm the same data shape
|
|
9
|
+
// the loop expects is returned.
|
|
10
|
+
|
|
11
|
+
import test from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
openDatabase,
|
|
19
|
+
closeDatabase,
|
|
20
|
+
insertMilestone,
|
|
21
|
+
} from "../gsd-db.ts";
|
|
22
|
+
import { registerAutoWorker } from "../db/auto-workers.ts";
|
|
23
|
+
import { claimMilestoneLease } from "../db/milestone-leases.ts";
|
|
24
|
+
import {
|
|
25
|
+
recordDispatchClaim,
|
|
26
|
+
getRecentUnitKeysForWorker,
|
|
27
|
+
} from "../db/unit-dispatches.ts";
|
|
28
|
+
import { setRuntimeKv, getRuntimeKv } from "../db/runtime-kv.ts";
|
|
29
|
+
|
|
30
|
+
function makeBase(): string {
|
|
31
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-stuck-state-db-"));
|
|
32
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
33
|
+
return base;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function cleanup(base: string): void {
|
|
37
|
+
try { closeDatabase(); } catch { /* noop */ }
|
|
38
|
+
try { rmSync(base, { recursive: true, force: true }); } catch { /* noop */ }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test("getRecentUnitKeysForWorker reconstructs the recentUnits sliding window", (t) => {
|
|
42
|
+
const base = makeBase();
|
|
43
|
+
t.after(() => cleanup(base));
|
|
44
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
45
|
+
insertMilestone({ id: "M001", title: "T", status: "active" });
|
|
46
|
+
const worker = registerAutoWorker({ projectRootRealpath: base });
|
|
47
|
+
const lease = claimMilestoneLease(worker, "M001");
|
|
48
|
+
assert.equal(lease.ok, true);
|
|
49
|
+
if (!lease.ok) return;
|
|
50
|
+
|
|
51
|
+
// Record three dispatches in chronological order. Each must transition
|
|
52
|
+
// out of 'claimed' before the next one with the same unit_id can claim
|
|
53
|
+
// (partial unique index). We use distinct unit IDs so all three coexist.
|
|
54
|
+
const claims: number[] = [];
|
|
55
|
+
for (const id of ["U1", "U2", "U3"]) {
|
|
56
|
+
const c = recordDispatchClaim({
|
|
57
|
+
traceId: id, workerId: worker, milestoneLeaseToken: lease.token,
|
|
58
|
+
milestoneId: "M001", unitType: "plan-slice", unitId: id,
|
|
59
|
+
});
|
|
60
|
+
assert.equal(c.ok, true);
|
|
61
|
+
if (c.ok) claims.push(c.dispatchId);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The loader should return them oldest-first to match the in-memory
|
|
65
|
+
// window semantics that detect-stuck.ts expects.
|
|
66
|
+
const window = getRecentUnitKeysForWorker(worker, 20);
|
|
67
|
+
assert.deepEqual(window.map(w => w.key), ["U1", "U2", "U3"]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("getRecentUnitKeysForWorker honors the limit parameter", (t) => {
|
|
71
|
+
const base = makeBase();
|
|
72
|
+
t.after(() => cleanup(base));
|
|
73
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
74
|
+
insertMilestone({ id: "M001", title: "T", status: "active" });
|
|
75
|
+
const worker = registerAutoWorker({ projectRootRealpath: base });
|
|
76
|
+
const lease = claimMilestoneLease(worker, "M001");
|
|
77
|
+
assert.equal(lease.ok, true);
|
|
78
|
+
if (!lease.ok) return;
|
|
79
|
+
|
|
80
|
+
for (let i = 0; i < 25; i++) {
|
|
81
|
+
const c = recordDispatchClaim({
|
|
82
|
+
traceId: `t${i}`, workerId: worker, milestoneLeaseToken: lease.token,
|
|
83
|
+
milestoneId: "M001", unitType: "plan-slice", unitId: `U${i}`,
|
|
84
|
+
});
|
|
85
|
+
assert.equal(c.ok, true);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const win20 = getRecentUnitKeysForWorker(worker, 20);
|
|
89
|
+
assert.equal(win20.length, 20);
|
|
90
|
+
// Most recent 20 are U5..U24 (chronological), oldest-first → U5..U24.
|
|
91
|
+
assert.equal(win20[0].key, "U5");
|
|
92
|
+
assert.equal(win20[19].key, "U24");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("stuckRecoveryAttempts round-trips via runtime_kv (stable project scope)", (t) => {
|
|
96
|
+
const base = makeBase();
|
|
97
|
+
t.after(() => cleanup(base));
|
|
98
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
99
|
+
registerAutoWorker({ projectRootRealpath: base });
|
|
100
|
+
|
|
101
|
+
setRuntimeKv("global", base, "stuck_recovery_attempts", 3);
|
|
102
|
+
assert.equal(getRuntimeKv<number>("global", base, "stuck_recovery_attempts"), 3);
|
|
103
|
+
setRuntimeKv("global", base, "stuck_recovery_attempts", 7);
|
|
104
|
+
assert.equal(getRuntimeKv<number>("global", base, "stuck_recovery_attempts"), 7);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("getRecentUnitKeysForWorker filters by worker_id (no cross-worker bleed)", (t) => {
|
|
108
|
+
const base = makeBase();
|
|
109
|
+
t.after(() => cleanup(base));
|
|
110
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
111
|
+
insertMilestone({ id: "M001", title: "T", status: "active" });
|
|
112
|
+
insertMilestone({ id: "M002", title: "U", status: "active" });
|
|
113
|
+
const w1 = registerAutoWorker({ projectRootRealpath: base });
|
|
114
|
+
const w2 = registerAutoWorker({ projectRootRealpath: base });
|
|
115
|
+
const lease1 = claimMilestoneLease(w1, "M001");
|
|
116
|
+
const lease2 = claimMilestoneLease(w2, "M002");
|
|
117
|
+
assert.equal(lease1.ok, true);
|
|
118
|
+
assert.equal(lease2.ok, true);
|
|
119
|
+
if (!lease1.ok || !lease2.ok) return;
|
|
120
|
+
|
|
121
|
+
recordDispatchClaim({
|
|
122
|
+
traceId: "ta", workerId: w1, milestoneLeaseToken: lease1.token,
|
|
123
|
+
milestoneId: "M001", unitType: "plan-slice", unitId: "for-w1",
|
|
124
|
+
});
|
|
125
|
+
recordDispatchClaim({
|
|
126
|
+
traceId: "tb", workerId: w2, milestoneLeaseToken: lease2.token,
|
|
127
|
+
milestoneId: "M002", unitType: "plan-slice", unitId: "for-w2",
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const w1Window = getRecentUnitKeysForWorker(w1, 20);
|
|
131
|
+
const w2Window = getRecentUnitKeysForWorker(w2, 20);
|
|
132
|
+
assert.deepEqual(w1Window.map(w => w.key), ["for-w1"]);
|
|
133
|
+
assert.deepEqual(w2Window.map(w => w.key), ["for-w2"]);
|
|
134
|
+
});
|
|
@@ -21,8 +21,10 @@ import {
|
|
|
21
21
|
syncStateToProjectRootByScope,
|
|
22
22
|
syncGsdStateToWorktree,
|
|
23
23
|
syncGsdStateToWorktreeByScope,
|
|
24
|
-
reconcilePlanCheckboxesByScope,
|
|
25
24
|
} from "../auto-worktree.ts";
|
|
25
|
+
// Phase C: reconcilePlanCheckboxesByScope was deleted along with the
|
|
26
|
+
// underlying reconcilePlanCheckboxes (auto-worktree.ts). Worktrees no
|
|
27
|
+
// longer maintain a parallel .gsd/ projection that needs reconciliation.
|
|
26
28
|
|
|
27
29
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
28
30
|
|
|
@@ -124,19 +126,8 @@ describe("ByScope variants: mismatched-workspace identity assertion", () => {
|
|
|
124
126
|
);
|
|
125
127
|
});
|
|
126
128
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
mkdirSync(join(tmpB, ".gsd"), { recursive: true });
|
|
130
|
-
const wsA = createWorkspace(tmpA);
|
|
131
|
-
const wsB = createWorkspace(tmpB);
|
|
132
|
-
const scopeA = scopeMilestone(wsA, MID);
|
|
133
|
-
const scopeB = scopeMilestone(wsB, MID);
|
|
134
|
-
|
|
135
|
-
assert.throws(
|
|
136
|
-
() => reconcilePlanCheckboxesByScope(scopeA, scopeB),
|
|
137
|
-
/scope identity mismatch/,
|
|
138
|
-
);
|
|
139
|
-
});
|
|
129
|
+
// Phase C: reconcilePlanCheckboxesByScope identity-mismatch test
|
|
130
|
+
// removed along with the deleted function.
|
|
140
131
|
});
|
|
141
132
|
|
|
142
133
|
// ─── Suite: same-milestone, same-workspace path identity ────────────────────
|
|
@@ -425,18 +416,8 @@ describe("ByScope variants: milestoneId mismatch throws for milestone-aware wrap
|
|
|
425
416
|
);
|
|
426
417
|
});
|
|
427
418
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
const rootWs = createWorkspace(projectDir);
|
|
431
|
-
const worktreeWs = createWorkspace(worktreeDir);
|
|
432
|
-
const rootScope = scopeMilestone(rootWs, "M001-abc123");
|
|
433
|
-
const worktreeScope = scopeMilestone(worktreeWs, "M002-def456");
|
|
434
|
-
|
|
435
|
-
assert.throws(
|
|
436
|
-
() => reconcilePlanCheckboxesByScope(rootScope, worktreeScope),
|
|
437
|
-
/milestoneId mismatch/,
|
|
438
|
-
);
|
|
439
|
-
});
|
|
419
|
+
// Phase C: reconcilePlanCheckboxesByScope milestoneId-mismatch test
|
|
420
|
+
// removed along with the deleted function.
|
|
440
421
|
|
|
441
422
|
test("syncGsdStateToWorktreeByScope does NOT throw when milestoneIds differ (workspace-only wrapper)", () => {
|
|
442
423
|
const { projectDir, worktreeDir } = makeProjectAndWorktree(tmp);
|
|
@@ -53,24 +53,21 @@ describe("teardownAutoWorktree cleanup parity", () => {
|
|
|
53
53
|
_resetAutoWorktreeOriginalBaseForTests();
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
test("STATE.md
|
|
56
|
+
test("STATE.md and M001-META.json are removed after abort teardown", () => {
|
|
57
|
+
// Phase C pt 2: auto.lock no longer exists as a file — it migrated
|
|
58
|
+
// to the workers + unit_dispatches tables. clearProjectRootStateFiles
|
|
59
|
+
// still removes STATE.md and {MID}-META.json on teardown.
|
|
57
60
|
const gsdDir = join(repoDir, ".gsd");
|
|
58
61
|
const milestonesDir = join(gsdDir, "milestones", "M001");
|
|
59
62
|
mkdirSync(milestonesDir, { recursive: true });
|
|
60
63
|
|
|
61
64
|
const stateMd = join(gsdDir, "STATE.md");
|
|
62
|
-
const autoLock = join(gsdDir, "auto.lock");
|
|
63
65
|
const metaJson = join(milestonesDir, "M001-META.json");
|
|
64
66
|
|
|
65
67
|
writeFileSync(stateMd, "# State\nactive\n");
|
|
66
|
-
writeFileSync(
|
|
67
|
-
autoLock,
|
|
68
|
-
JSON.stringify({ pid: process.pid, unitType: "plan-milestone", unitId: "M001" }),
|
|
69
|
-
);
|
|
70
68
|
writeFileSync(metaJson, JSON.stringify({ milestoneId: "M001" }));
|
|
71
69
|
|
|
72
70
|
assert.ok(existsSync(stateMd), "STATE.md exists before teardown");
|
|
73
|
-
assert.ok(existsSync(autoLock), "auto.lock exists before teardown");
|
|
74
71
|
assert.ok(existsSync(metaJson), "M001-META.json exists before teardown");
|
|
75
72
|
|
|
76
73
|
// teardownAutoWorktree may throw when git worktree removal fails
|
|
@@ -83,7 +80,6 @@ describe("teardownAutoWorktree cleanup parity", () => {
|
|
|
83
80
|
}
|
|
84
81
|
|
|
85
82
|
assert.ok(!existsSync(stateMd), "STATE.md removed by teardownAutoWorktree");
|
|
86
|
-
assert.ok(!existsSync(autoLock), "auto.lock removed by teardownAutoWorktree");
|
|
87
83
|
assert.ok(!existsSync(metaJson), "M001-META.json removed by teardownAutoWorktree");
|
|
88
84
|
});
|
|
89
85
|
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// gsd-2 + Unit dispatch ledger tests (Phase B coordination — partial unique index, retry metadata)
|
|
2
|
+
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
openDatabase,
|
|
11
|
+
closeDatabase,
|
|
12
|
+
insertMilestone,
|
|
13
|
+
insertSlice,
|
|
14
|
+
} from "../gsd-db.ts";
|
|
15
|
+
import { registerAutoWorker } from "../db/auto-workers.ts";
|
|
16
|
+
import { claimMilestoneLease } from "../db/milestone-leases.ts";
|
|
17
|
+
import {
|
|
18
|
+
recordDispatchClaim,
|
|
19
|
+
markRunning,
|
|
20
|
+
markCompleted,
|
|
21
|
+
markFailed,
|
|
22
|
+
markStuck,
|
|
23
|
+
markCanceled,
|
|
24
|
+
getRecentForUnit,
|
|
25
|
+
getLatestForUnit,
|
|
26
|
+
} from "../db/unit-dispatches.ts";
|
|
27
|
+
|
|
28
|
+
function makeBase(): string {
|
|
29
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-dispatches-"));
|
|
30
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
31
|
+
return base;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function cleanup(base: string): void {
|
|
35
|
+
try { closeDatabase(); } catch { /* noop */ }
|
|
36
|
+
try { rmSync(base, { recursive: true, force: true }); } catch { /* noop */ }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function setup(base: string): { workerId: string; leaseToken: number } {
|
|
40
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
41
|
+
insertMilestone({ id: "M001", title: "Test", status: "active" });
|
|
42
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice" });
|
|
43
|
+
const workerId = registerAutoWorker({ projectRootRealpath: base });
|
|
44
|
+
const lease = claimMilestoneLease(workerId, "M001");
|
|
45
|
+
assert.equal(lease.ok, true);
|
|
46
|
+
if (!lease.ok) throw new Error("expected test lease");
|
|
47
|
+
return { workerId, leaseToken: lease.token };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
test("recordDispatchClaim creates a claimed row", (t) => {
|
|
51
|
+
const base = makeBase();
|
|
52
|
+
t.after(() => cleanup(base));
|
|
53
|
+
const { workerId, leaseToken } = setup(base);
|
|
54
|
+
|
|
55
|
+
const claim = recordDispatchClaim({
|
|
56
|
+
traceId: "trace-1",
|
|
57
|
+
turnId: "turn-1",
|
|
58
|
+
workerId,
|
|
59
|
+
milestoneLeaseToken: leaseToken,
|
|
60
|
+
milestoneId: "M001",
|
|
61
|
+
sliceId: "S01",
|
|
62
|
+
unitType: "plan-slice",
|
|
63
|
+
unitId: "M001/S01",
|
|
64
|
+
});
|
|
65
|
+
assert.equal(claim.ok, true);
|
|
66
|
+
if (claim.ok) {
|
|
67
|
+
const row = getLatestForUnit("M001/S01");
|
|
68
|
+
assert.ok(row);
|
|
69
|
+
assert.equal(row!.id, claim.dispatchId);
|
|
70
|
+
assert.equal(row!.status, "claimed");
|
|
71
|
+
assert.equal(row!.worker_id, workerId);
|
|
72
|
+
assert.equal(row!.attempt_n, 1);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("partial unique index rejects double-claim of the same active unit", (t) => {
|
|
77
|
+
const base = makeBase();
|
|
78
|
+
t.after(() => cleanup(base));
|
|
79
|
+
const { workerId, leaseToken } = setup(base);
|
|
80
|
+
|
|
81
|
+
const first = recordDispatchClaim({
|
|
82
|
+
traceId: "t-a",
|
|
83
|
+
workerId,
|
|
84
|
+
milestoneLeaseToken: leaseToken,
|
|
85
|
+
milestoneId: "M001",
|
|
86
|
+
unitType: "plan-slice",
|
|
87
|
+
unitId: "M001/S01",
|
|
88
|
+
});
|
|
89
|
+
assert.equal(first.ok, true);
|
|
90
|
+
|
|
91
|
+
// Second worker tries to claim the same unit while first is still claimed
|
|
92
|
+
const second = recordDispatchClaim({
|
|
93
|
+
traceId: "t-b",
|
|
94
|
+
workerId,
|
|
95
|
+
milestoneLeaseToken: leaseToken,
|
|
96
|
+
milestoneId: "M001",
|
|
97
|
+
unitType: "plan-slice",
|
|
98
|
+
unitId: "M001/S01",
|
|
99
|
+
});
|
|
100
|
+
assert.equal(second.ok, false);
|
|
101
|
+
if (!second.ok) {
|
|
102
|
+
assert.equal(second.error, "already_active");
|
|
103
|
+
assert.equal(second.existingWorker, workerId);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("after markCompleted, a fresh claim for the same unit succeeds", (t) => {
|
|
108
|
+
const base = makeBase();
|
|
109
|
+
t.after(() => cleanup(base));
|
|
110
|
+
const { workerId, leaseToken } = setup(base);
|
|
111
|
+
|
|
112
|
+
const first = recordDispatchClaim({
|
|
113
|
+
traceId: "t-1",
|
|
114
|
+
workerId,
|
|
115
|
+
milestoneLeaseToken: leaseToken,
|
|
116
|
+
milestoneId: "M001",
|
|
117
|
+
unitType: "plan-slice",
|
|
118
|
+
unitId: "M001/S01",
|
|
119
|
+
});
|
|
120
|
+
assert.equal(first.ok, true);
|
|
121
|
+
if (!first.ok) return;
|
|
122
|
+
markRunning(first.dispatchId);
|
|
123
|
+
markCompleted(first.dispatchId);
|
|
124
|
+
|
|
125
|
+
// Re-dispatch
|
|
126
|
+
const second = recordDispatchClaim({
|
|
127
|
+
traceId: "t-2",
|
|
128
|
+
workerId,
|
|
129
|
+
milestoneLeaseToken: leaseToken,
|
|
130
|
+
milestoneId: "M001",
|
|
131
|
+
unitType: "plan-slice",
|
|
132
|
+
unitId: "M001/S01",
|
|
133
|
+
attemptN: 2,
|
|
134
|
+
});
|
|
135
|
+
assert.equal(second.ok, true);
|
|
136
|
+
if (second.ok) {
|
|
137
|
+
const recent = getRecentForUnit("M001/S01", 5);
|
|
138
|
+
assert.equal(recent.length, 2);
|
|
139
|
+
assert.equal(recent[0].status, "claimed");
|
|
140
|
+
assert.equal(recent[0].attempt_n, 2);
|
|
141
|
+
assert.equal(recent[1].status, "completed");
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("markFailed records error_summary and retry metadata", (t) => {
|
|
146
|
+
const base = makeBase();
|
|
147
|
+
t.after(() => cleanup(base));
|
|
148
|
+
const { workerId, leaseToken } = setup(base);
|
|
149
|
+
|
|
150
|
+
const claim = recordDispatchClaim({
|
|
151
|
+
traceId: "t-1",
|
|
152
|
+
workerId,
|
|
153
|
+
milestoneLeaseToken: leaseToken,
|
|
154
|
+
milestoneId: "M001",
|
|
155
|
+
unitType: "plan-slice",
|
|
156
|
+
unitId: "M001/S01",
|
|
157
|
+
});
|
|
158
|
+
assert.equal(claim.ok, true);
|
|
159
|
+
if (!claim.ok) return;
|
|
160
|
+
markRunning(claim.dispatchId);
|
|
161
|
+
markFailed(claim.dispatchId, {
|
|
162
|
+
errorSummary: "boom",
|
|
163
|
+
errorCode: "test-fail",
|
|
164
|
+
retryAfterMs: 5000,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const row = getLatestForUnit("M001/S01")!;
|
|
168
|
+
assert.equal(row.status, "failed");
|
|
169
|
+
assert.equal(row.error_summary, "boom");
|
|
170
|
+
assert.equal(row.last_error_code, "test-fail");
|
|
171
|
+
assert.equal(row.retry_after_ms, 5000);
|
|
172
|
+
assert.ok(row.next_run_at, "next_run_at scheduled");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("markStuck and markCanceled set their respective statuses", (t) => {
|
|
176
|
+
const base = makeBase();
|
|
177
|
+
t.after(() => cleanup(base));
|
|
178
|
+
const { workerId, leaseToken } = setup(base);
|
|
179
|
+
|
|
180
|
+
const a = recordDispatchClaim({
|
|
181
|
+
traceId: "ta", workerId, milestoneLeaseToken: leaseToken,
|
|
182
|
+
milestoneId: "M001", unitType: "plan-slice", unitId: "M001/S01",
|
|
183
|
+
});
|
|
184
|
+
assert.equal(a.ok, true);
|
|
185
|
+
if (!a.ok) return;
|
|
186
|
+
markStuck(a.dispatchId, "test-stuck");
|
|
187
|
+
assert.equal(getLatestForUnit("M001/S01")!.status, "stuck");
|
|
188
|
+
|
|
189
|
+
const b = recordDispatchClaim({
|
|
190
|
+
traceId: "tb", workerId, milestoneLeaseToken: leaseToken,
|
|
191
|
+
milestoneId: "M001", unitType: "run-task", unitId: "M001/S01/T01",
|
|
192
|
+
});
|
|
193
|
+
assert.equal(b.ok, true);
|
|
194
|
+
if (!b.ok) return;
|
|
195
|
+
markCanceled(b.dispatchId, "user-cancel");
|
|
196
|
+
assert.equal(getLatestForUnit("M001/S01/T01")!.status, "canceled");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("terminal transitions do not overwrite an already terminal dispatch", (t) => {
|
|
200
|
+
const base = makeBase();
|
|
201
|
+
t.after(() => cleanup(base));
|
|
202
|
+
const { workerId, leaseToken } = setup(base);
|
|
203
|
+
|
|
204
|
+
const claim = recordDispatchClaim({
|
|
205
|
+
traceId: "t-terminal",
|
|
206
|
+
workerId,
|
|
207
|
+
milestoneLeaseToken: leaseToken,
|
|
208
|
+
milestoneId: "M001",
|
|
209
|
+
unitType: "plan-slice",
|
|
210
|
+
unitId: "M001/S09",
|
|
211
|
+
});
|
|
212
|
+
assert.equal(claim.ok, true);
|
|
213
|
+
if (!claim.ok) return;
|
|
214
|
+
|
|
215
|
+
markRunning(claim.dispatchId);
|
|
216
|
+
markCompleted(claim.dispatchId, { exitReason: "done" });
|
|
217
|
+
markFailed(claim.dispatchId, { errorSummary: "late-failure" });
|
|
218
|
+
markStuck(claim.dispatchId, "late-stuck");
|
|
219
|
+
|
|
220
|
+
const row = getLatestForUnit("M001/S09")!;
|
|
221
|
+
assert.equal(row.status, "completed");
|
|
222
|
+
assert.equal(row.exit_reason, "done");
|
|
223
|
+
assert.equal(row.error_summary, null);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("recordDispatchClaim rejects claims for missing leases before insert", (t) => {
|
|
227
|
+
const base = makeBase();
|
|
228
|
+
t.after(() => cleanup(base));
|
|
229
|
+
setup(base);
|
|
230
|
+
|
|
231
|
+
const claim = recordDispatchClaim({
|
|
232
|
+
traceId: "t-stale-lease",
|
|
233
|
+
workerId: "missing-worker",
|
|
234
|
+
milestoneLeaseToken: 1,
|
|
235
|
+
milestoneId: "M001",
|
|
236
|
+
unitType: "plan-slice",
|
|
237
|
+
unitId: "M001/S01",
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
assert.deepEqual(claim, {
|
|
241
|
+
ok: false,
|
|
242
|
+
error: "stale_lease",
|
|
243
|
+
milestoneId: "M001",
|
|
244
|
+
workerId: "missing-worker",
|
|
245
|
+
milestoneLeaseToken: 1,
|
|
246
|
+
});
|
|
247
|
+
});
|
|
@@ -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 { deriveState, isValidationTerminal } from "../state.ts";
|
|
8
|
+
import { deriveState, invalidateStateCache, isValidationTerminal } from "../state.ts";
|
|
9
9
|
import { resolveExpectedArtifactPath, diagnoseExpectedArtifact } from "../auto-artifact-paths.ts";
|
|
10
10
|
import { verifyExpectedArtifact, buildLoopRemediationSteps } from "../auto-recovery.ts";
|
|
11
11
|
import { resolveDispatch, type DispatchContext } from "../auto-dispatch.ts";
|
|
@@ -24,6 +24,7 @@ function makeTmpBase(): string {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function cleanup(base: string): void {
|
|
27
|
+
invalidateStateCache();
|
|
27
28
|
clearPathCache();
|
|
28
29
|
clearParseCache();
|
|
29
30
|
closeDatabase();
|
|
@@ -394,6 +395,45 @@ test("dispatch rule skips when skip_milestone_validation preference is set", asy
|
|
|
394
395
|
}
|
|
395
396
|
});
|
|
396
397
|
|
|
398
|
+
test("skip write immediately advances deriveState out of validating-milestone", async () => {
|
|
399
|
+
const base = makeTmpBase();
|
|
400
|
+
try {
|
|
401
|
+
openTestDb(base);
|
|
402
|
+
insertMilestone({ id: "M001", title: "Test", status: "active" } as any);
|
|
403
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice 1", status: "complete" } as any);
|
|
404
|
+
|
|
405
|
+
writeContext(base, "M001");
|
|
406
|
+
writeRoadmap(base, "M001", ALL_DONE_ROADMAP);
|
|
407
|
+
writeSliceSummary(base, "M001", "S01", "# S01 Summary\nDone.");
|
|
408
|
+
|
|
409
|
+
invalidateStateCache();
|
|
410
|
+
clearPathCache();
|
|
411
|
+
clearParseCache();
|
|
412
|
+
|
|
413
|
+
const before = await deriveState(base);
|
|
414
|
+
assert.equal(before.phase, "validating-milestone", "precondition: missing VALIDATION keeps phase in validation");
|
|
415
|
+
|
|
416
|
+
const ctx: DispatchContext = {
|
|
417
|
+
basePath: base,
|
|
418
|
+
mid: "M001",
|
|
419
|
+
midTitle: "Test",
|
|
420
|
+
state: before,
|
|
421
|
+
prefs: { phases: { skip_milestone_validation: true } },
|
|
422
|
+
};
|
|
423
|
+
const result = await resolveDispatch(ctx);
|
|
424
|
+
assert.equal(result.action, "skip");
|
|
425
|
+
|
|
426
|
+
const after = await deriveState(base);
|
|
427
|
+
assert.equal(
|
|
428
|
+
after.phase,
|
|
429
|
+
"completing-milestone",
|
|
430
|
+
"post-skip deriveState should see the new VALIDATION file without manual cache invalidation",
|
|
431
|
+
);
|
|
432
|
+
} finally {
|
|
433
|
+
cleanup(base);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
397
437
|
test("dispatch rule ignores failure-path SUMMARY projection when DB milestone is not complete (#4658 superseded)", async () => {
|
|
398
438
|
const state: GSDState = {
|
|
399
439
|
activeMilestone: { id: "M001", title: "Test" },
|
|
@@ -60,11 +60,12 @@ describe("createWorkspace", () => {
|
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
test("follows symlinks — identityKey matches realpath of target", (t) => {
|
|
63
|
-
const
|
|
64
|
-
|
|
63
|
+
const linkParent = mkdtempSync(join(tmpdir(), "gsd-ws-link-"));
|
|
64
|
+
const linkPath = join(linkParent, "project");
|
|
65
65
|
t.after(() => {
|
|
66
|
-
|
|
66
|
+
rmSync(linkParent, { recursive: true, force: true });
|
|
67
67
|
});
|
|
68
|
+
symlinkSync(projectDir, linkPath, "junction");
|
|
68
69
|
|
|
69
70
|
const ws = createWorkspace(linkPath);
|
|
70
71
|
assert.equal(ws.identityKey, realpathSync(projectDir));
|
|
@@ -146,18 +147,23 @@ describe("scopeMilestone path methods", () => {
|
|
|
146
147
|
});
|
|
147
148
|
|
|
148
149
|
describe("createWorkspace: contract.projectGsd is realpath-canonicalized when basePath is a symlink", () => {
|
|
149
|
-
let projectDir
|
|
150
|
-
let
|
|
150
|
+
let projectDir = "";
|
|
151
|
+
let linkParent = "";
|
|
152
|
+
let linkPath = "";
|
|
151
153
|
|
|
152
154
|
beforeEach(() => {
|
|
153
155
|
projectDir = makeProjectDir();
|
|
154
|
-
|
|
155
|
-
|
|
156
|
+
linkParent = mkdtempSync(join(tmpdir(), "gsd-ws-symlink-"));
|
|
157
|
+
linkPath = join(linkParent, "project");
|
|
158
|
+
symlinkSync(projectDir, linkPath, "junction");
|
|
156
159
|
});
|
|
157
160
|
|
|
158
161
|
afterEach(() => {
|
|
159
|
-
|
|
160
|
-
rmSync(projectDir, { recursive: true, force: true });
|
|
162
|
+
if (linkParent) rmSync(linkParent, { recursive: true, force: true });
|
|
163
|
+
if (projectDir) rmSync(projectDir, { recursive: true, force: true });
|
|
164
|
+
linkParent = "";
|
|
165
|
+
linkPath = "";
|
|
166
|
+
projectDir = "";
|
|
161
167
|
});
|
|
162
168
|
|
|
163
169
|
test("contract.projectGsd matches realpath of projectRoot when workspace is created via symlink", () => {
|
|
@@ -299,31 +299,39 @@ test('write-gate: deep root PROJECT/REQUIREMENTS final saves require verified ap
|
|
|
299
299
|
});
|
|
300
300
|
|
|
301
301
|
test('write-gate: reopening a gate revokes its previous verified approval', () => {
|
|
302
|
-
|
|
302
|
+
const base = join(tmpdir(), `gsd-write-gate-reopen-${randomUUID()}`);
|
|
303
|
+
mkdirSync(base, { recursive: true });
|
|
303
304
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
shouldBlockRootArtifactSaveInSnapshot(
|
|
307
|
-
loadWriteGateSnapshot(process.cwd()),
|
|
308
|
-
'PROJECT',
|
|
309
|
-
{ requireVerifiedApproval: true },
|
|
310
|
-
).block,
|
|
311
|
-
false,
|
|
312
|
-
'precondition: verified approval unlocks the final project artifact',
|
|
313
|
-
);
|
|
314
|
-
|
|
315
|
-
setPendingGate('depth_verification_project_confirm', process.cwd());
|
|
316
|
-
clearPendingGate(process.cwd());
|
|
305
|
+
try {
|
|
306
|
+
clearDiscussionFlowState(base);
|
|
317
307
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
308
|
+
markApprovalGateVerified('depth_verification_project_confirm', base);
|
|
309
|
+
assert.strictEqual(
|
|
310
|
+
shouldBlockRootArtifactSaveInSnapshot(
|
|
311
|
+
loadWriteGateSnapshot(base),
|
|
312
|
+
'PROJECT',
|
|
313
|
+
{ requireVerifiedApproval: true },
|
|
314
|
+
).block,
|
|
315
|
+
false,
|
|
316
|
+
'precondition: verified approval unlocks the final project artifact',
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
setPendingGate('depth_verification_project_confirm', base);
|
|
320
|
+
clearPendingGate(base);
|
|
321
|
+
|
|
322
|
+
assert.strictEqual(
|
|
323
|
+
shouldBlockRootArtifactSaveInSnapshot(
|
|
324
|
+
loadWriteGateSnapshot(base),
|
|
325
|
+
'PROJECT',
|
|
326
|
+
{ requireVerifiedApproval: true },
|
|
327
|
+
).block,
|
|
328
|
+
true,
|
|
329
|
+
'a re-asked gate must require a fresh approval',
|
|
330
|
+
);
|
|
331
|
+
} finally {
|
|
332
|
+
clearDiscussionFlowState(base);
|
|
333
|
+
rmSync(base, { recursive: true, force: true });
|
|
334
|
+
}
|
|
327
335
|
});
|
|
328
336
|
|
|
329
337
|
// ═══════════════════════════════════════════════════════════════════════
|