gsd-pi 2.78.1-dev.e9d88a536 → 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 +14 -7
- package/dist/resources/extensions/gsd/auto/session.js +36 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +49 -4
- package/dist/resources/extensions/gsd/auto-post-unit.js +26 -12
- package/dist/resources/extensions/gsd/auto-worktree.js +185 -201
- package/dist/resources/extensions/gsd/auto.js +139 -49
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +26 -20
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
- 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/db-writer.js +96 -16
- package/dist/resources/extensions/gsd/delegation-policy.js +155 -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 +355 -3
- package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
- package/dist/resources/extensions/gsd/guided-flow.js +116 -26
- package/dist/resources/extensions/gsd/interrupted-session.js +18 -15
- package/dist/resources/extensions/gsd/metrics.js +287 -1
- package/dist/resources/extensions/gsd/paths.js +79 -8
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -4
- package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -3
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
- package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
- package/dist/resources/extensions/gsd/state.js +21 -6
- package/dist/resources/extensions/gsd/templates/project.md +10 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
- package/dist/resources/extensions/gsd/workspace.js +59 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +79 -2
- package/dist/resources/extensions/gsd/write-intercept.js +3 -3
- 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 +14 -14
- 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/required-server-files.json +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.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 +14 -14
- 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/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/README.md +2 -11
- package/packages/mcp-server/dist/remote-questions.d.ts +27 -0
- package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
- package/packages/mcp-server/dist/remote-questions.js +28 -0
- package/packages/mcp-server/dist/remote-questions.js.map +1 -1
- package/packages/mcp-server/dist/server.d.ts +28 -0
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +94 -4
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/mcp-server.test.ts +226 -0
- package/packages/mcp-server/src/remote-questions.test.ts +103 -0
- package/packages/mcp-server/src/remote-questions.ts +35 -0
- package/packages/mcp-server/src/server.ts +129 -6
- package/packages/mcp-server/src/workflow-tools.ts +1 -1
- package/packages/mcp-server/tsconfig.tsbuildinfo +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 +15 -7
- package/src/resources/extensions/gsd/auto/session.ts +40 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +63 -4
- package/src/resources/extensions/gsd/auto-post-unit.ts +27 -12
- package/src/resources/extensions/gsd/auto-worktree.ts +218 -225
- package/src/resources/extensions/gsd/auto.ts +166 -43
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +26 -21
- package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +103 -0
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -55
- 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/db-writer.ts +113 -17
- package/src/resources/extensions/gsd/delegation-policy.ts +197 -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 +354 -3
- package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
- package/src/resources/extensions/gsd/guided-flow.ts +152 -26
- package/src/resources/extensions/gsd/interrupted-session.ts +19 -12
- package/src/resources/extensions/gsd/metrics.ts +321 -1
- package/src/resources/extensions/gsd/paths.ts +67 -8
- package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -4
- package/src/resources/extensions/gsd/prompts/execute-task.md +3 -3
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
- package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
- package/src/resources/extensions/gsd/state.ts +44 -6
- package/src/resources/extensions/gsd/templates/project.md +10 -0
- package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +14 -14
- 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-session-scope.test.ts +331 -0
- package/src/resources/extensions/gsd/tests/auto-workers.test.ts +105 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -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/db-writer-path-containment.test.ts +152 -0
- package/src/resources/extensions/gsd/tests/db-writer-root-artifact.test.ts +221 -0
- package/src/resources/extensions/gsd/tests/db-writer-scope.test.ts +230 -0
- package/src/resources/extensions/gsd/tests/delegation-policy.test.ts +151 -0
- package/src/resources/extensions/gsd/tests/detect-stuck-respects-retry.test.ts +173 -0
- package/src/resources/extensions/gsd/tests/dispatch-backgroundable-annotation.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +3 -23
- package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +193 -0
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +246 -0
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +218 -0
- package/src/resources/extensions/gsd/tests/gsd-db-failed-open-restore.test.ts +117 -0
- package/src/resources/extensions/gsd/tests/gsd-db-workspace-scope.test.ts +226 -0
- package/src/resources/extensions/gsd/tests/gsd-root-canonical.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/gsd-root-home-guard.test.ts +68 -5
- package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +4 -4
- 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 +369 -0
- 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/metrics-atomic-merge.test.ts +222 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-hardening.test.ts +400 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-not-acquired.test.ts +141 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-retry-sleep.test.ts +287 -0
- package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +149 -0
- package/src/resources/extensions/gsd/tests/metrics-scope.test.ts +378 -0
- package/src/resources/extensions/gsd/tests/milestone-leases.test.ts +152 -0
- package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +329 -0
- package/src/resources/extensions/gsd/tests/parallel-milestone-isolation.test.ts +106 -0
- package/src/resources/extensions/gsd/tests/path-cache-decoupled.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/path-normalization-unified.test.ts +175 -0
- package/src/resources/extensions/gsd/tests/paths-cache.test.ts +170 -0
- package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +119 -0
- package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +120 -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/prompt-contracts.test.ts +150 -7
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +138 -16
- package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -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 +434 -0
- package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +162 -0
- package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +186 -0
- package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +1 -1
- 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/validator-scope-parity.test.ts +239 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +9 -15
- package/src/resources/extensions/gsd/tests/workspace.test.ts +196 -0
- package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +94 -71
- package/src/resources/extensions/gsd/tests/write-intercept.test.ts +1 -1
- package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
- package/src/resources/extensions/gsd/workspace.ts +95 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +78 -2
- package/src/resources/extensions/gsd/write-intercept.ts +3 -3
- 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/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// gsd-2 + Command queue tests (Phase B coordination — IPC inbox + broadcast NULL semantics)
|
|
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 { openDatabase, closeDatabase } from "../gsd-db.ts";
|
|
10
|
+
import {
|
|
11
|
+
enqueueCommand,
|
|
12
|
+
claimNextCommand,
|
|
13
|
+
completeCommand,
|
|
14
|
+
getCommand,
|
|
15
|
+
} from "../db/command-queue.ts";
|
|
16
|
+
|
|
17
|
+
function makeBase(): string {
|
|
18
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-cmd-q-"));
|
|
19
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
20
|
+
return base;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function cleanup(base: string): void {
|
|
24
|
+
try { closeDatabase(); } catch { /* noop */ }
|
|
25
|
+
try { rmSync(base, { recursive: true, force: true }); } catch { /* noop */ }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test("enqueue + claim + complete round-trip for targeted command", (t) => {
|
|
29
|
+
const base = makeBase();
|
|
30
|
+
t.after(() => cleanup(base));
|
|
31
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
32
|
+
|
|
33
|
+
const id = enqueueCommand({
|
|
34
|
+
targetWorker: "worker-A",
|
|
35
|
+
command: "cancel",
|
|
36
|
+
args: { reason: "user-request" },
|
|
37
|
+
});
|
|
38
|
+
assert.ok(id > 0);
|
|
39
|
+
|
|
40
|
+
const claimed = claimNextCommand("worker-A");
|
|
41
|
+
assert.ok(claimed);
|
|
42
|
+
assert.equal(claimed!.id, id);
|
|
43
|
+
assert.equal(claimed!.command, "cancel");
|
|
44
|
+
assert.equal(claimed!.claimed_by, "worker-A");
|
|
45
|
+
assert.ok(claimed!.claimed_at);
|
|
46
|
+
|
|
47
|
+
completeCommand(id, "worker-A", { acknowledged: true });
|
|
48
|
+
const final = getCommand(id);
|
|
49
|
+
assert.ok(final!.completed_at);
|
|
50
|
+
assert.equal(final!.result_json, JSON.stringify({ acknowledged: true }));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("targeted command is invisible to other workers", (t) => {
|
|
54
|
+
const base = makeBase();
|
|
55
|
+
t.after(() => cleanup(base));
|
|
56
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
57
|
+
|
|
58
|
+
enqueueCommand({ targetWorker: "worker-A", command: "for-A" });
|
|
59
|
+
const wrong = claimNextCommand("worker-B");
|
|
60
|
+
assert.equal(wrong, null, "worker-B sees nothing for worker-A");
|
|
61
|
+
|
|
62
|
+
const right = claimNextCommand("worker-A");
|
|
63
|
+
assert.ok(right);
|
|
64
|
+
assert.equal(right!.command, "for-A");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("broadcast command (target=null) is visible to ANY worker, claimed exactly once", (t) => {
|
|
68
|
+
const base = makeBase();
|
|
69
|
+
t.after(() => cleanup(base));
|
|
70
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
71
|
+
|
|
72
|
+
enqueueCommand({ targetWorker: null, command: "broadcast-cancel" });
|
|
73
|
+
|
|
74
|
+
const a = claimNextCommand("worker-A");
|
|
75
|
+
assert.ok(a, "first poller wins");
|
|
76
|
+
assert.equal(a!.command, "broadcast-cancel");
|
|
77
|
+
|
|
78
|
+
// Second poller (different worker) sees nothing — broadcast is single-delivery
|
|
79
|
+
const b = claimNextCommand("worker-B");
|
|
80
|
+
assert.equal(b, null);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("oldest-first ordering across mixed targeted + broadcast queue", (t) => {
|
|
84
|
+
const base = makeBase();
|
|
85
|
+
t.after(() => cleanup(base));
|
|
86
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
87
|
+
|
|
88
|
+
enqueueCommand({ targetWorker: null, command: "first" });
|
|
89
|
+
enqueueCommand({ targetWorker: "worker-A", command: "second" });
|
|
90
|
+
enqueueCommand({ targetWorker: null, command: "third" });
|
|
91
|
+
|
|
92
|
+
const c1 = claimNextCommand("worker-A")!;
|
|
93
|
+
const c2 = claimNextCommand("worker-A")!;
|
|
94
|
+
const c3 = claimNextCommand("worker-A")!;
|
|
95
|
+
assert.equal(c1.command, "first");
|
|
96
|
+
assert.equal(c2.command, "second");
|
|
97
|
+
assert.equal(c3.command, "third");
|
|
98
|
+
assert.equal(claimNextCommand("worker-A"), null);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("completeCommand is idempotent — second call does not overwrite", (t) => {
|
|
102
|
+
const base = makeBase();
|
|
103
|
+
t.after(() => cleanup(base));
|
|
104
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
105
|
+
|
|
106
|
+
const id = enqueueCommand({ targetWorker: "w", command: "x" });
|
|
107
|
+
claimNextCommand("w");
|
|
108
|
+
completeCommand(id, "w", { result: 1 });
|
|
109
|
+
completeCommand(id, "w", { result: 2 }); // second call should no-op
|
|
110
|
+
const row = getCommand(id)!;
|
|
111
|
+
assert.equal(row.result_json, JSON.stringify({ result: 1 }));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("completed commands cannot be reclaimed or completed by a different worker", (t) => {
|
|
115
|
+
const base = makeBase();
|
|
116
|
+
t.after(() => cleanup(base));
|
|
117
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
118
|
+
|
|
119
|
+
const id = enqueueCommand({ targetWorker: "worker-A", command: "x" });
|
|
120
|
+
const claimed = claimNextCommand("worker-A");
|
|
121
|
+
assert.ok(claimed);
|
|
122
|
+
|
|
123
|
+
completeCommand(id, "worker-A", { result: 1 });
|
|
124
|
+
completeCommand(id, "worker-B", { result: 2 });
|
|
125
|
+
|
|
126
|
+
assert.equal(claimNextCommand("worker-A"), null);
|
|
127
|
+
const row = getCommand(id)!;
|
|
128
|
+
assert.equal(row.result_json, JSON.stringify({ result: 1 }));
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("completeCommand does not complete an unclaimed command", (t) => {
|
|
132
|
+
const base = makeBase();
|
|
133
|
+
t.after(() => cleanup(base));
|
|
134
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
135
|
+
|
|
136
|
+
const id = enqueueCommand({ targetWorker: "w", command: "x" });
|
|
137
|
+
completeCommand(id, "w", { result: 1 });
|
|
138
|
+
const row = getCommand(id)!;
|
|
139
|
+
assert.equal(row.completed_at, null);
|
|
140
|
+
assert.equal(row.result_json, null);
|
|
141
|
+
});
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// gsd-2 + Crash recovery via DB (Phase C pt 2 — auto.lock migration)
|
|
2
|
+
//
|
|
3
|
+
// auto.lock file IO is gone. readCrashLock now synthesizes a LockData
|
|
4
|
+
// from the workers + unit_dispatches + runtime_kv tables. These tests
|
|
5
|
+
// verify the synthesis end-to-end: register a worker, simulate it going
|
|
6
|
+
// stale (heartbeat lapsed), and confirm readCrashLock returns the
|
|
7
|
+
// correct LockData with PID, started_at, unit details, and session
|
|
8
|
+
// file derived from the DB.
|
|
9
|
+
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
openDatabase,
|
|
18
|
+
closeDatabase,
|
|
19
|
+
insertMilestone,
|
|
20
|
+
_getAdapter,
|
|
21
|
+
} from "../gsd-db.ts";
|
|
22
|
+
import { registerAutoWorker } from "../db/auto-workers.ts";
|
|
23
|
+
import { claimMilestoneLease } from "../db/milestone-leases.ts";
|
|
24
|
+
import { recordDispatchClaim } from "../db/unit-dispatches.ts";
|
|
25
|
+
import { setRuntimeKv, getRuntimeKv } from "../db/runtime-kv.ts";
|
|
26
|
+
import {
|
|
27
|
+
writeLock,
|
|
28
|
+
readCrashLock,
|
|
29
|
+
clearLock,
|
|
30
|
+
isLockProcessAlive,
|
|
31
|
+
} from "../crash-recovery.ts";
|
|
32
|
+
import { normalizeRealPath } from "../paths.ts";
|
|
33
|
+
|
|
34
|
+
function makeBase(): string {
|
|
35
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-crash-recovery-"));
|
|
36
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
37
|
+
return base;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function cleanup(base: string): void {
|
|
41
|
+
try { closeDatabase(); } catch { /* noop */ }
|
|
42
|
+
try { rmSync(base, { recursive: true, force: true }); } catch { /* noop */ }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Force a worker's last_heartbeat_at into the past so the stale-detector picks it up. */
|
|
46
|
+
function expireWorker(workerId: string): void {
|
|
47
|
+
const db = _getAdapter()!;
|
|
48
|
+
db.prepare(
|
|
49
|
+
`UPDATE workers SET last_heartbeat_at = '1970-01-01T00:00:00.000Z' WHERE worker_id = :w`,
|
|
50
|
+
).run({ ":w": workerId });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function setWorkerPid(workerId: string, pid: number): void {
|
|
54
|
+
const db = _getAdapter()!;
|
|
55
|
+
db.prepare(
|
|
56
|
+
`UPDATE workers SET pid = :pid WHERE worker_id = :w`,
|
|
57
|
+
).run({ ":pid": pid, ":w": workerId });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
test("readCrashLock returns null when no workers exist", (t) => {
|
|
61
|
+
const base = makeBase();
|
|
62
|
+
t.after(() => cleanup(base));
|
|
63
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
64
|
+
assert.equal(readCrashLock(base), null);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("readCrashLock returns null when only fresh (un-expired) workers exist", (t) => {
|
|
68
|
+
const base = makeBase();
|
|
69
|
+
t.after(() => cleanup(base));
|
|
70
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
71
|
+
registerAutoWorker({ projectRootRealpath: normalizeRealPath(base) });
|
|
72
|
+
// Heartbeat is fresh — not stale yet.
|
|
73
|
+
assert.equal(readCrashLock(base), null);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("readCrashLock ignores a stale heartbeat when the worker PID is still alive", (t) => {
|
|
77
|
+
const base = makeBase();
|
|
78
|
+
t.after(() => cleanup(base));
|
|
79
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
80
|
+
const projectRoot = normalizeRealPath(base);
|
|
81
|
+
const workerId = registerAutoWorker({ projectRootRealpath: projectRoot });
|
|
82
|
+
expireWorker(workerId);
|
|
83
|
+
|
|
84
|
+
assert.equal(readCrashLock(base), null);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("readCrashLock synthesizes LockData from a stale dead worker (no dispatches yet)", (t) => {
|
|
88
|
+
const base = makeBase();
|
|
89
|
+
t.after(() => cleanup(base));
|
|
90
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
91
|
+
const projectRoot = normalizeRealPath(base);
|
|
92
|
+
const workerId = registerAutoWorker({ projectRootRealpath: projectRoot });
|
|
93
|
+
setWorkerPid(workerId, 99999);
|
|
94
|
+
expireWorker(workerId);
|
|
95
|
+
|
|
96
|
+
const lock = readCrashLock(base);
|
|
97
|
+
assert.ok(lock, "stale worker surfaced as a crash lock");
|
|
98
|
+
assert.equal(lock!.pid, 99999);
|
|
99
|
+
// Bootstrap default — no dispatches recorded
|
|
100
|
+
assert.equal(lock!.unitType, "starting");
|
|
101
|
+
assert.equal(lock!.unitId, "bootstrap");
|
|
102
|
+
assert.ok(lock!.startedAt, "startedAt populated from workers.started_at");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("readCrashLock includes the most recent dispatch as unitType/unitId", (t) => {
|
|
106
|
+
const base = makeBase();
|
|
107
|
+
t.after(() => cleanup(base));
|
|
108
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
109
|
+
insertMilestone({ id: "M001", title: "T", status: "active" });
|
|
110
|
+
const projectRoot = normalizeRealPath(base);
|
|
111
|
+
const workerId = registerAutoWorker({ projectRootRealpath: projectRoot });
|
|
112
|
+
const lease = claimMilestoneLease(workerId, "M001");
|
|
113
|
+
assert.equal(lease.ok, true);
|
|
114
|
+
if (!lease.ok) return;
|
|
115
|
+
recordDispatchClaim({
|
|
116
|
+
traceId: "t1", workerId, milestoneLeaseToken: lease.token,
|
|
117
|
+
milestoneId: "M001", unitType: "plan-slice", unitId: "M001/S01",
|
|
118
|
+
});
|
|
119
|
+
setWorkerPid(workerId, 99999);
|
|
120
|
+
expireWorker(workerId);
|
|
121
|
+
|
|
122
|
+
const lock = readCrashLock(base);
|
|
123
|
+
assert.ok(lock);
|
|
124
|
+
assert.equal(lock!.unitType, "plan-slice");
|
|
125
|
+
assert.equal(lock!.unitId, "M001/S01");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("readCrashLock surfaces sessionFile from runtime_kv", (t) => {
|
|
129
|
+
const base = makeBase();
|
|
130
|
+
t.after(() => cleanup(base));
|
|
131
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
132
|
+
const projectRoot = normalizeRealPath(base);
|
|
133
|
+
const workerId = registerAutoWorker({ projectRootRealpath: projectRoot });
|
|
134
|
+
setRuntimeKv("worker", workerId, "session_file", "/tmp/pi-session-abc.jsonl");
|
|
135
|
+
setWorkerPid(workerId, 99999);
|
|
136
|
+
expireWorker(workerId);
|
|
137
|
+
|
|
138
|
+
const lock = readCrashLock(base);
|
|
139
|
+
assert.ok(lock);
|
|
140
|
+
assert.equal(lock!.sessionFile, "/tmp/pi-session-abc.jsonl");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("isLockProcessAlive returns true for the current process", () => {
|
|
144
|
+
const lock = {
|
|
145
|
+
pid: process.pid,
|
|
146
|
+
startedAt: new Date().toISOString(),
|
|
147
|
+
unitType: "starting",
|
|
148
|
+
unitId: "bootstrap",
|
|
149
|
+
unitStartedAt: new Date().toISOString(),
|
|
150
|
+
};
|
|
151
|
+
assert.equal(isLockProcessAlive(lock), true);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("isLockProcessAlive returns false for a dead PID", () => {
|
|
155
|
+
// PID 99999 is essentially guaranteed dead on a fresh test box.
|
|
156
|
+
const lock = {
|
|
157
|
+
pid: 99999,
|
|
158
|
+
startedAt: new Date().toISOString(),
|
|
159
|
+
unitType: "starting",
|
|
160
|
+
unitId: "bootstrap",
|
|
161
|
+
unitStartedAt: new Date().toISOString(),
|
|
162
|
+
};
|
|
163
|
+
assert.equal(isLockProcessAlive(lock), false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("writeLock stores the session_file in runtime_kv (worker scope)", (t) => {
|
|
167
|
+
const base = makeBase();
|
|
168
|
+
t.after(() => cleanup(base));
|
|
169
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
170
|
+
const projectRoot = normalizeRealPath(base);
|
|
171
|
+
const workerId = registerAutoWorker({ projectRootRealpath: projectRoot });
|
|
172
|
+
|
|
173
|
+
writeLock(base, "plan-slice", "M001/S01", "/tmp/session-xyz.jsonl");
|
|
174
|
+
|
|
175
|
+
// Verify the value was written for the live worker.
|
|
176
|
+
const stored = getRuntimeKv<string>("worker", workerId, "session_file");
|
|
177
|
+
assert.equal(stored, "/tmp/session-xyz.jsonl");
|
|
178
|
+
|
|
179
|
+
// Confirm a stale read picks it up via readCrashLock.
|
|
180
|
+
setWorkerPid(workerId, 99999);
|
|
181
|
+
expireWorker(workerId);
|
|
182
|
+
const lock = readCrashLock(base);
|
|
183
|
+
assert.ok(lock);
|
|
184
|
+
assert.equal(lock!.sessionFile, "/tmp/session-xyz.jsonl");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("clearLock removes the session_file row for the active worker", (t) => {
|
|
188
|
+
const base = makeBase();
|
|
189
|
+
t.after(() => cleanup(base));
|
|
190
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
191
|
+
const projectRoot = normalizeRealPath(base);
|
|
192
|
+
const workerId = registerAutoWorker({ projectRootRealpath: projectRoot });
|
|
193
|
+
|
|
194
|
+
writeLock(base, "plan-slice", "M001/S01", "/tmp/session-xyz.jsonl");
|
|
195
|
+
assert.equal(getRuntimeKv("worker", workerId, "session_file"), "/tmp/session-xyz.jsonl");
|
|
196
|
+
|
|
197
|
+
// clearLock operates on the active worker (this process) — must run
|
|
198
|
+
// BEFORE expiring the heartbeat, mirroring stopAuto's order: clearLock
|
|
199
|
+
// → markWorkerStopping → done.
|
|
200
|
+
clearLock(base);
|
|
201
|
+
assert.equal(getRuntimeKv("worker", workerId, "session_file"), null,
|
|
202
|
+
"session_file row deleted by clearLock");
|
|
203
|
+
});
|
|
@@ -18,39 +18,96 @@ import {
|
|
|
18
18
|
hasResumableDerivedState,
|
|
19
19
|
isBootstrapCrashLock,
|
|
20
20
|
readPausedSessionMetadata,
|
|
21
|
+
PAUSED_SESSION_KV_KEY,
|
|
21
22
|
} from "../interrupted-session.ts";
|
|
22
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
openDatabase,
|
|
25
|
+
closeDatabase,
|
|
26
|
+
insertMilestone,
|
|
27
|
+
_getAdapter,
|
|
28
|
+
} from "../gsd-db.ts";
|
|
29
|
+
import { registerAutoWorker } from "../db/auto-workers.ts";
|
|
30
|
+
import { claimMilestoneLease } from "../db/milestone-leases.ts";
|
|
31
|
+
import { recordDispatchClaim } from "../db/unit-dispatches.ts";
|
|
32
|
+
import { insertSlice, insertTask } from "../gsd-db.ts";
|
|
33
|
+
import { setRuntimeKv } from "../db/runtime-kv.ts";
|
|
34
|
+
import { normalizeRealPath } from "../paths.ts";
|
|
23
35
|
import type { GSDState } from "../types.ts";
|
|
24
36
|
import { _synthesizePausedSessionRecoveryForTest } from "../auto.ts";
|
|
25
37
|
|
|
26
38
|
function makeTmpBase(): string {
|
|
27
39
|
const base = join(tmpdir(), `gsd-test-${randomUUID()}`);
|
|
28
40
|
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
41
|
+
// Phase C pt 2: lock and paused-session live in the DB now. Open it
|
|
42
|
+
// for every test base so the helpers below can write through.
|
|
43
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
29
44
|
return base;
|
|
30
45
|
}
|
|
31
46
|
|
|
32
47
|
function cleanup(base: string): void {
|
|
48
|
+
try { closeDatabase(); } catch { /* */ }
|
|
33
49
|
try { rmSync(base, { recursive: true, force: true }); } catch { /* */ }
|
|
34
50
|
}
|
|
35
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Phase C pt 2 fixture: insert a stale worker row + dispatch + session_file
|
|
54
|
+
* directly via SQL so it appears as a crashed PEER process, not as the
|
|
55
|
+
* current test process. assessInterruptedSession filters out
|
|
56
|
+
* `rawLock.pid === process.pid` to avoid classifying its own process as
|
|
57
|
+
* a previous crash; using PID 999999999 (functionally guaranteed dead)
|
|
58
|
+
* bypasses that guard exactly the way the old file-based writeTestLock
|
|
59
|
+
* did with the same PID.
|
|
60
|
+
*/
|
|
36
61
|
function writeTestLock(
|
|
37
62
|
base: string,
|
|
38
63
|
unitType: string,
|
|
39
64
|
unitId: string,
|
|
40
65
|
sessionFile?: string,
|
|
41
66
|
): void {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
67
|
+
const projectRoot = normalizeRealPath(base);
|
|
68
|
+
const workerId = `test-fake-${randomUUID().slice(0, 8)}`;
|
|
69
|
+
const fakePid = 999999999;
|
|
70
|
+
const stalePast = "1970-01-01T00:00:00.000Z";
|
|
71
|
+
const db = _getAdapter()!;
|
|
72
|
+
db.prepare(
|
|
73
|
+
`INSERT INTO workers (
|
|
74
|
+
worker_id, host, pid, started_at, version,
|
|
75
|
+
last_heartbeat_at, status, project_root_realpath
|
|
76
|
+
) VALUES (
|
|
77
|
+
:w, 'test-host', :pid, :started_at, 'test',
|
|
78
|
+
:stale, 'active', :project_root
|
|
79
|
+
)`,
|
|
80
|
+
).run({
|
|
81
|
+
":w": workerId,
|
|
82
|
+
":pid": fakePid,
|
|
83
|
+
":started_at": new Date().toISOString(),
|
|
84
|
+
":stale": stalePast,
|
|
85
|
+
":project_root": projectRoot,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Ensure milestones referenced by the unitId exist so the dispatch
|
|
89
|
+
// FK is satisfied. Parse "M###/S##" or "M###" or "starting" / etc.
|
|
90
|
+
const midMatch = unitId.match(/^(M\d+)/);
|
|
91
|
+
if (midMatch && unitType !== "starting") {
|
|
92
|
+
const mid = midMatch[1];
|
|
93
|
+
try { insertMilestone({ id: mid, title: `Test ${mid}`, status: "active" }); }
|
|
94
|
+
catch { /* may already exist */ }
|
|
95
|
+
try {
|
|
96
|
+
const lease = claimMilestoneLease(workerId, mid);
|
|
97
|
+
recordDispatchClaim({
|
|
98
|
+
traceId: randomUUID(),
|
|
99
|
+
workerId,
|
|
100
|
+
milestoneLeaseToken: lease.ok ? lease.token : 0,
|
|
101
|
+
milestoneId: mid,
|
|
102
|
+
unitType,
|
|
103
|
+
unitId,
|
|
104
|
+
});
|
|
105
|
+
} catch { /* ignore — best-effort */ }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (sessionFile) {
|
|
109
|
+
setRuntimeKv("worker", workerId, "session_file", sessionFile);
|
|
110
|
+
}
|
|
54
111
|
}
|
|
55
112
|
|
|
56
113
|
function writeRoadmap(base: string, checked = false): void {
|
|
@@ -82,6 +139,25 @@ function writeRoadmap(base: string, checked = false): void {
|
|
|
82
139
|
].join("\n"),
|
|
83
140
|
"utf-8",
|
|
84
141
|
);
|
|
142
|
+
// Phase C pt 2: makeTmpBase() opens the DB so writeTestLock can write
|
|
143
|
+
// the workers row. deriveState then goes DB-first; mirror the markdown
|
|
144
|
+
// fixture into the DB so the assessment sees the same milestone state.
|
|
145
|
+
// Use direct upsert SQL so calling writeRoadmap twice (e.g. once for
|
|
146
|
+
// base + once for a paused worktree) actually flips the status.
|
|
147
|
+
const status = checked ? "complete" : "active";
|
|
148
|
+
const adapter = _getAdapter();
|
|
149
|
+
if (adapter) {
|
|
150
|
+
adapter.prepare(
|
|
151
|
+
`INSERT INTO milestones (id, title, status, created_at)
|
|
152
|
+
VALUES (:id, :title, :status, :now)
|
|
153
|
+
ON CONFLICT(id) DO UPDATE SET status = excluded.status, title = excluded.title`,
|
|
154
|
+
).run({ ":id": "M001", ":title": "Test Milestone", ":status": status, ":now": new Date().toISOString() });
|
|
155
|
+
adapter.prepare(
|
|
156
|
+
`INSERT INTO slices (milestone_id, id, title, status, created_at)
|
|
157
|
+
VALUES (:mid, :sid, :title, :status, :now)
|
|
158
|
+
ON CONFLICT(milestone_id, id) DO UPDATE SET status = excluded.status, title = excluded.title`,
|
|
159
|
+
).run({ ":mid": "M001", ":sid": "S01", ":title": "Test slice", ":status": status, ":now": new Date().toISOString() });
|
|
160
|
+
}
|
|
85
161
|
}
|
|
86
162
|
|
|
87
163
|
function writeCompleteSliceArtifacts(base: string): void {
|
|
@@ -105,13 +181,16 @@ function writePausedSession(
|
|
|
105
181
|
unitType?: string,
|
|
106
182
|
unitId?: string,
|
|
107
183
|
): void {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
184
|
+
// Phase C pt 2: paused-session.json migrated to runtime_kv
|
|
185
|
+
// (global scope, key PAUSED_SESSION_KV_KEY).
|
|
186
|
+
setRuntimeKv("global", "", PAUSED_SESSION_KV_KEY, {
|
|
187
|
+
milestoneId,
|
|
188
|
+
originalBasePath: base,
|
|
189
|
+
stepMode,
|
|
190
|
+
worktreePath,
|
|
191
|
+
unitType,
|
|
192
|
+
unitId,
|
|
193
|
+
});
|
|
115
194
|
}
|
|
116
195
|
|
|
117
196
|
function writeActivityLog(base: string, entries: Record<string, unknown>[]): void {
|
|
@@ -231,14 +310,12 @@ test("readPausedSessionMetadata preserves unitType and unitId through round-trip
|
|
|
231
310
|
test("readPausedSessionMetadata handles legacy metadata without unitType/unitId", () => {
|
|
232
311
|
const base = makeTmpBase();
|
|
233
312
|
try {
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
"utf-8",
|
|
241
|
-
);
|
|
313
|
+
// Phase C pt 2: write directly to runtime_kv (simulates older payload
|
|
314
|
+
// missing the now-canonical unitType/unitId fields).
|
|
315
|
+
setRuntimeKv("global", "", PAUSED_SESSION_KV_KEY, {
|
|
316
|
+
milestoneId: "M001",
|
|
317
|
+
originalBasePath: base,
|
|
318
|
+
});
|
|
242
319
|
const meta = readPausedSessionMetadata(base);
|
|
243
320
|
assert.equal(meta?.milestoneId, "M001");
|
|
244
321
|
assert.equal(meta?.unitType, undefined);
|
|
@@ -251,23 +328,23 @@ test("readPausedSessionMetadata handles legacy metadata without unitType/unitId"
|
|
|
251
328
|
test("readPausedSessionMetadata drops stale discuss-milestone pseudo PROJECT metadata", () => {
|
|
252
329
|
const base = makeTmpBase();
|
|
253
330
|
try {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
originalBasePath: base,
|
|
262
|
-
unitType: "discuss-milestone",
|
|
263
|
-
unitId: "PROJECT",
|
|
264
|
-
}, null, 2),
|
|
265
|
-
"utf-8",
|
|
266
|
-
);
|
|
331
|
+
// Phase C pt 2: write directly to runtime_kv (the file location is gone)
|
|
332
|
+
setRuntimeKv("global", "", PAUSED_SESSION_KV_KEY, {
|
|
333
|
+
milestoneId: null,
|
|
334
|
+
originalBasePath: base,
|
|
335
|
+
unitType: "discuss-milestone",
|
|
336
|
+
unitId: "PROJECT",
|
|
337
|
+
});
|
|
267
338
|
|
|
268
339
|
const meta = readPausedSessionMetadata(base);
|
|
269
340
|
assert.equal(meta, null);
|
|
270
|
-
|
|
341
|
+
// Confirm the row was deleted by readPausedSessionMetadata's
|
|
342
|
+
// isStalePseudoMilestonePause branch.
|
|
343
|
+
const adapter = _getAdapter()!;
|
|
344
|
+
const row = adapter.prepare(
|
|
345
|
+
`SELECT 1 FROM runtime_kv WHERE scope = 'global' AND scope_id = '' AND key = :k`,
|
|
346
|
+
).get({ ":k": PAUSED_SESSION_KV_KEY });
|
|
347
|
+
assert.equal(row, undefined);
|
|
271
348
|
} finally {
|
|
272
349
|
cleanup(base);
|
|
273
350
|
}
|
|
@@ -276,23 +353,20 @@ test("readPausedSessionMetadata drops stale discuss-milestone pseudo PROJECT met
|
|
|
276
353
|
test("readPausedSessionMetadata drops stale deep setup pseudo-unit metadata", () => {
|
|
277
354
|
const base = makeTmpBase();
|
|
278
355
|
try {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
milestoneId: "WORKFLOW-PREFS",
|
|
286
|
-
originalBasePath: base,
|
|
287
|
-
unitType: "workflow-preferences",
|
|
288
|
-
unitId: "WORKFLOW-PREFS",
|
|
289
|
-
}, null, 2),
|
|
290
|
-
"utf-8",
|
|
291
|
-
);
|
|
356
|
+
setRuntimeKv("global", "", PAUSED_SESSION_KV_KEY, {
|
|
357
|
+
milestoneId: "WORKFLOW-PREFS",
|
|
358
|
+
originalBasePath: base,
|
|
359
|
+
unitType: "workflow-preferences",
|
|
360
|
+
unitId: "WORKFLOW-PREFS",
|
|
361
|
+
});
|
|
292
362
|
|
|
293
363
|
const meta = readPausedSessionMetadata(base);
|
|
294
364
|
assert.equal(meta, null);
|
|
295
|
-
|
|
365
|
+
const adapter = _getAdapter()!;
|
|
366
|
+
const row = adapter.prepare(
|
|
367
|
+
`SELECT 1 FROM runtime_kv WHERE scope = 'global' AND scope_id = '' AND key = :k`,
|
|
368
|
+
).get({ ":k": PAUSED_SESSION_KV_KEY });
|
|
369
|
+
assert.equal(row, undefined);
|
|
296
370
|
} finally {
|
|
297
371
|
cleanup(base);
|
|
298
372
|
}
|
|
@@ -504,10 +578,29 @@ test("assessInterruptedSession treats bootstrap crash as stale without paused me
|
|
|
504
578
|
// ─── writeLock / readCrashLock ────────────────────────────────────────────
|
|
505
579
|
|
|
506
580
|
test("writeLock creates lock file and readCrashLock reads it", (t) => {
|
|
581
|
+
// Phase C pt 2: lock state is reconstructed from workers + unit_dispatches
|
|
582
|
+
// + runtime_kv. The fresh worker is not stale yet — we register, dispatch,
|
|
583
|
+
// write the session_file, then expire the heartbeat to simulate a crash.
|
|
507
584
|
const base = makeTmpBase();
|
|
508
585
|
t.after(() => cleanup(base));
|
|
509
586
|
|
|
587
|
+
insertMilestone({ id: "M001", title: "Test", status: "active" });
|
|
588
|
+
const projectRoot = normalizeRealPath(base);
|
|
589
|
+
const workerId = registerAutoWorker({ projectRootRealpath: projectRoot });
|
|
590
|
+
const lease = claimMilestoneLease(workerId, "M001");
|
|
591
|
+
assert.equal(lease.ok, true);
|
|
592
|
+
if (!lease.ok) return;
|
|
593
|
+
recordDispatchClaim({
|
|
594
|
+
traceId: "t1", workerId, milestoneLeaseToken: lease.token,
|
|
595
|
+
milestoneId: "M001", unitType: "execute-task", unitId: "M001/S01/T01",
|
|
596
|
+
});
|
|
510
597
|
writeLock(base, "execute-task", "M001/S01/T01", "/tmp/session.jsonl");
|
|
598
|
+
|
|
599
|
+
// Force stale so readCrashLock surfaces it.
|
|
600
|
+
_getAdapter()!.prepare(
|
|
601
|
+
`UPDATE workers SET last_heartbeat_at = '1970-01-01T00:00:00.000Z' WHERE worker_id = :w`,
|
|
602
|
+
).run({ ":w": workerId });
|
|
603
|
+
|
|
511
604
|
const lock = readCrashLock(base);
|
|
512
605
|
assert.ok(lock, "lock should exist");
|
|
513
606
|
assert.equal(lock!.unitType, "execute-task");
|
|
@@ -527,13 +620,30 @@ test("readCrashLock returns null when no lock exists", (t) => {
|
|
|
527
620
|
// ─── clearLock ────────────────────────────────────────────────────────────
|
|
528
621
|
|
|
529
622
|
test("clearLock removes existing lock file", (t) => {
|
|
623
|
+
// Phase C pt 2: clearLock now drops the session_file runtime_kv row
|
|
624
|
+
// for the LIVE worker (not the stale one). The "lock state" itself
|
|
625
|
+
// (pid, unitType, etc.) lives in workers + unit_dispatches; those are
|
|
626
|
+
// managed by markWorkerStopping (called from stopAuto, not here).
|
|
530
627
|
const base = makeTmpBase();
|
|
531
628
|
t.after(() => cleanup(base));
|
|
532
629
|
|
|
533
|
-
|
|
534
|
-
|
|
630
|
+
const projectRoot = normalizeRealPath(base);
|
|
631
|
+
const workerId = registerAutoWorker({ projectRootRealpath: projectRoot });
|
|
632
|
+
|
|
633
|
+
writeLock(base, "plan-slice", "M001/S01", "/tmp/session.jsonl");
|
|
634
|
+
// Confirm the session_file row landed for the live worker.
|
|
635
|
+
const adapter = _getAdapter()!;
|
|
636
|
+
const before = adapter.prepare(
|
|
637
|
+
`SELECT 1 FROM runtime_kv WHERE scope = 'worker' AND scope_id = :w AND key = 'session_file'`,
|
|
638
|
+
).get({ ":w": workerId });
|
|
639
|
+
assert.ok(before, "session_file row exists before clear");
|
|
640
|
+
|
|
535
641
|
clearLock(base);
|
|
536
|
-
|
|
642
|
+
|
|
643
|
+
const after = adapter.prepare(
|
|
644
|
+
`SELECT 1 FROM runtime_kv WHERE scope = 'worker' AND scope_id = :w AND key = 'session_file'`,
|
|
645
|
+
).get({ ":w": workerId });
|
|
646
|
+
assert.equal(after, undefined, "session_file row gone after clearLock");
|
|
537
647
|
});
|
|
538
648
|
|
|
539
649
|
test("clearLock is safe when no lock exists", (t) => {
|