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,369 @@
|
|
|
1
|
+
// GSD-2 + Integration regression suite for workspace collapse (feat/workspace-collapse)
|
|
2
|
+
|
|
3
|
+
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import {
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
realpathSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { execFileSync } from "node:child_process";
|
|
16
|
+
|
|
17
|
+
import { createWorkspace, scopeMilestone } from "../../workspace.ts";
|
|
18
|
+
import {
|
|
19
|
+
gsdRoot,
|
|
20
|
+
clearPathCache,
|
|
21
|
+
_clearGsdRootCache,
|
|
22
|
+
} from "../../paths.ts";
|
|
23
|
+
import {
|
|
24
|
+
loadWriteGateSnapshot,
|
|
25
|
+
markDepthVerified,
|
|
26
|
+
clearDiscussionFlowState,
|
|
27
|
+
} from "../../bootstrap/write-gate.ts";
|
|
28
|
+
import {
|
|
29
|
+
teardownAutoWorktree,
|
|
30
|
+
_resetAutoWorktreeOriginalBaseForTests,
|
|
31
|
+
} from "../../auto-worktree.ts";
|
|
32
|
+
import {
|
|
33
|
+
openDatabaseByWorkspace,
|
|
34
|
+
closeAllDatabases,
|
|
35
|
+
_getDbCache,
|
|
36
|
+
} from "../../gsd-db.ts";
|
|
37
|
+
|
|
38
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
function makeProjectDir(): string {
|
|
41
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-collapse-int-")));
|
|
42
|
+
mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
|
|
43
|
+
return dir;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function git(args: string[], cwd: string): void {
|
|
47
|
+
execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function makeGitRepo(): string {
|
|
51
|
+
const dir = makeProjectDir();
|
|
52
|
+
git(["init"], dir);
|
|
53
|
+
git(["config", "user.email", "test@gsd.test"], dir);
|
|
54
|
+
git(["config", "user.name", "GSD Test"], dir);
|
|
55
|
+
writeFileSync(join(dir, "README.md"), "# test\n");
|
|
56
|
+
git(["add", "README.md"], dir);
|
|
57
|
+
git(["commit", "-m", "init"], dir);
|
|
58
|
+
git(["branch", "-M", "main"], dir);
|
|
59
|
+
return dir;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Test 1: Writer/validator path agreement under cwd-drift ─────────────────
|
|
63
|
+
|
|
64
|
+
describe("workspace-collapse integration: Test 1 — cwd-drift path agreement", () => {
|
|
65
|
+
let projectDir: string;
|
|
66
|
+
let otherDir: string;
|
|
67
|
+
const savedCwd = process.cwd();
|
|
68
|
+
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
projectDir = makeProjectDir();
|
|
71
|
+
otherDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-cwd-drift-other-")));
|
|
72
|
+
_clearGsdRootCache();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterEach(() => {
|
|
76
|
+
process.chdir(savedCwd);
|
|
77
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
78
|
+
rmSync(otherDir, { recursive: true, force: true });
|
|
79
|
+
_clearGsdRootCache();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("contextFile() returns same absolute path before and after cwd change", () => {
|
|
83
|
+
const worktreeDir = join(projectDir, ".gsd", "worktrees", "M001");
|
|
84
|
+
mkdirSync(worktreeDir, { recursive: true });
|
|
85
|
+
|
|
86
|
+
const ws = createWorkspace(projectDir);
|
|
87
|
+
const scope = scopeMilestone(ws, "M001");
|
|
88
|
+
|
|
89
|
+
// Record the path the "writer" would use
|
|
90
|
+
const writerPath = scope.contextFile();
|
|
91
|
+
assert.ok(writerPath.startsWith(projectDir), "writer path is under projectDir");
|
|
92
|
+
|
|
93
|
+
// Simulate cwd drift
|
|
94
|
+
process.chdir(otherDir);
|
|
95
|
+
assert.notEqual(process.cwd(), projectDir, "cwd has drifted away from projectDir");
|
|
96
|
+
|
|
97
|
+
// The "validator" recomputes via the same scope
|
|
98
|
+
const validatorPath = scope.contextFile();
|
|
99
|
+
|
|
100
|
+
assert.equal(
|
|
101
|
+
validatorPath,
|
|
102
|
+
writerPath,
|
|
103
|
+
"contextFile() must return the same absolute path regardless of cwd drift",
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("scopeMilestone paths are stable across cwd changes (roadmap, state, db)", () => {
|
|
108
|
+
const ws = createWorkspace(projectDir);
|
|
109
|
+
const scope = scopeMilestone(ws, "M001");
|
|
110
|
+
|
|
111
|
+
const before = {
|
|
112
|
+
roadmap: scope.roadmapFile(),
|
|
113
|
+
state: scope.stateFile(),
|
|
114
|
+
db: scope.dbPath(),
|
|
115
|
+
milestoneDir: scope.milestoneDir(),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
process.chdir(otherDir);
|
|
119
|
+
|
|
120
|
+
assert.equal(scope.roadmapFile(), before.roadmap, "roadmapFile() stable after cwd drift");
|
|
121
|
+
assert.equal(scope.stateFile(), before.state, "stateFile() stable after cwd drift");
|
|
122
|
+
assert.equal(scope.dbPath(), before.db, "dbPath() stable after cwd drift");
|
|
123
|
+
assert.equal(scope.milestoneDir(), before.milestoneDir, "milestoneDir() stable after cwd drift");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ─── Test 2: Abort path leaves no stale state ────────────────────────────────
|
|
128
|
+
|
|
129
|
+
describe("workspace-collapse integration: Test 2 — abort teardown clears stale state", () => {
|
|
130
|
+
let repoDir: string;
|
|
131
|
+
const savedCwd = process.cwd();
|
|
132
|
+
|
|
133
|
+
beforeEach(() => {
|
|
134
|
+
repoDir = makeGitRepo();
|
|
135
|
+
_resetAutoWorktreeOriginalBaseForTests();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
afterEach(() => {
|
|
139
|
+
process.chdir(savedCwd);
|
|
140
|
+
_resetAutoWorktreeOriginalBaseForTests();
|
|
141
|
+
rmSync(repoDir, { recursive: true, force: true });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("STATE.md and M001-META.json are removed by teardownAutoWorktree", () => {
|
|
145
|
+
// Phase C pt 2: auto.lock no longer exists as a file — it migrated
|
|
146
|
+
// to the workers + unit_dispatches tables.
|
|
147
|
+
const gsdDir = join(repoDir, ".gsd");
|
|
148
|
+
const milestonesDir = join(gsdDir, "milestones", "M001");
|
|
149
|
+
mkdirSync(milestonesDir, { recursive: true });
|
|
150
|
+
|
|
151
|
+
const stateMd = join(gsdDir, "STATE.md");
|
|
152
|
+
const metaJson = join(milestonesDir, "M001-META.json");
|
|
153
|
+
|
|
154
|
+
writeFileSync(stateMd, "# State\nactive\n");
|
|
155
|
+
writeFileSync(metaJson, JSON.stringify({ milestoneId: "M001" }));
|
|
156
|
+
|
|
157
|
+
assert.ok(existsSync(stateMd), "STATE.md exists before teardown");
|
|
158
|
+
assert.ok(existsSync(metaJson), "M001-META.json exists before teardown");
|
|
159
|
+
|
|
160
|
+
// teardownAutoWorktree clears state files before the git step; git removal
|
|
161
|
+
// may fail in a minimal test repo — that is acceptable.
|
|
162
|
+
try {
|
|
163
|
+
teardownAutoWorktree(repoDir, "M001");
|
|
164
|
+
} catch {
|
|
165
|
+
// git worktree removal may fail when no worktree was created — non-fatal for this assertion
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
assert.ok(!existsSync(stateMd), "STATE.md removed by teardownAutoWorktree (regression: A5)");
|
|
169
|
+
assert.ok(!existsSync(metaJson), "M001-META.json removed by teardownAutoWorktree (regression: A5)");
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ─── Test 3: Cwd drift between persist and load of write-gate state ──────────
|
|
174
|
+
|
|
175
|
+
describe("workspace-collapse integration: Test 3 — write-gate snapshot survives cwd drift", () => {
|
|
176
|
+
let projectDir: string;
|
|
177
|
+
let otherDir: string;
|
|
178
|
+
const savedCwd = process.cwd();
|
|
179
|
+
|
|
180
|
+
beforeEach(() => {
|
|
181
|
+
projectDir = makeProjectDir();
|
|
182
|
+
otherDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-wg-other-")));
|
|
183
|
+
// Start with a clean write-gate state for projectDir
|
|
184
|
+
clearDiscussionFlowState(projectDir);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
afterEach(() => {
|
|
188
|
+
process.chdir(savedCwd);
|
|
189
|
+
clearDiscussionFlowState(projectDir);
|
|
190
|
+
try { clearDiscussionFlowState(otherDir); } catch { /* best-effort */ }
|
|
191
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
192
|
+
rmSync(otherDir, { recursive: true, force: true });
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("loadWriteGateSnapshot returns persisted state after cwd drift", () => {
|
|
196
|
+
// Persist a snapshot: mark M001 depth-verified for projectDir
|
|
197
|
+
markDepthVerified("M001", projectDir);
|
|
198
|
+
|
|
199
|
+
// Drift cwd away from projectDir
|
|
200
|
+
process.chdir(otherDir);
|
|
201
|
+
assert.notEqual(process.cwd(), projectDir, "cwd has drifted");
|
|
202
|
+
|
|
203
|
+
// Load the snapshot using the explicit basePath — must not be affected by cwd
|
|
204
|
+
const snapshot = loadWriteGateSnapshot(projectDir);
|
|
205
|
+
|
|
206
|
+
assert.ok(
|
|
207
|
+
snapshot.verifiedDepthMilestones.includes("M001"),
|
|
208
|
+
"snapshot loaded from projectDir includes M001 despite cwd drift",
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("loadWriteGateSnapshot from different basePath does not bleed state", () => {
|
|
213
|
+
markDepthVerified("M001", projectDir);
|
|
214
|
+
|
|
215
|
+
process.chdir(otherDir);
|
|
216
|
+
|
|
217
|
+
// otherDir has no persisted state — should return empty snapshot
|
|
218
|
+
const snapshot = loadWriteGateSnapshot(otherDir);
|
|
219
|
+
|
|
220
|
+
assert.ok(
|
|
221
|
+
!snapshot.verifiedDepthMilestones.includes("M001"),
|
|
222
|
+
"otherDir snapshot must not bleed M001 state from projectDir",
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// ─── Test 4: Sibling worktrees share DB connection ───────────────────────────
|
|
228
|
+
|
|
229
|
+
describe("workspace-collapse integration: Test 4 — sibling worktrees share DB connection", () => {
|
|
230
|
+
let projectDir: string;
|
|
231
|
+
|
|
232
|
+
beforeEach(() => {
|
|
233
|
+
projectDir = makeProjectDir();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
afterEach(() => {
|
|
237
|
+
closeAllDatabases();
|
|
238
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("ws1 and ws2 (sibling worktrees) have same identityKey", () => {
|
|
242
|
+
const wt1 = join(projectDir, ".gsd", "worktrees", "M001");
|
|
243
|
+
const wt2 = join(projectDir, ".gsd", "worktrees", "M002");
|
|
244
|
+
mkdirSync(wt1, { recursive: true });
|
|
245
|
+
mkdirSync(wt2, { recursive: true });
|
|
246
|
+
|
|
247
|
+
const ws1 = createWorkspace(wt1);
|
|
248
|
+
const ws2 = createWorkspace(wt2);
|
|
249
|
+
|
|
250
|
+
assert.equal(
|
|
251
|
+
ws1.identityKey,
|
|
252
|
+
ws2.identityKey,
|
|
253
|
+
"sibling worktrees M001 and M002 must share the same identityKey",
|
|
254
|
+
);
|
|
255
|
+
assert.equal(
|
|
256
|
+
ws1.identityKey,
|
|
257
|
+
realpathSync(projectDir),
|
|
258
|
+
"identityKey is the realpath of the project root",
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("openDatabaseByWorkspace for sibling worktrees resolves to the same DB path", () => {
|
|
263
|
+
const wt1 = join(projectDir, ".gsd", "worktrees", "M001");
|
|
264
|
+
const wt2 = join(projectDir, ".gsd", "worktrees", "M002");
|
|
265
|
+
mkdirSync(wt1, { recursive: true });
|
|
266
|
+
mkdirSync(wt2, { recursive: true });
|
|
267
|
+
|
|
268
|
+
const ws1 = createWorkspace(wt1);
|
|
269
|
+
const ws2 = createWorkspace(wt2);
|
|
270
|
+
|
|
271
|
+
const ok1 = openDatabaseByWorkspace(ws1);
|
|
272
|
+
assert.ok(ok1, "openDatabaseByWorkspace(ws1) must succeed");
|
|
273
|
+
const cacheAfterWs1 = _getDbCache();
|
|
274
|
+
const entry1 = cacheAfterWs1.get(ws1.identityKey);
|
|
275
|
+
assert.ok(entry1, "cache entry for ws1.identityKey must exist");
|
|
276
|
+
const dbPath1 = entry1.dbPath;
|
|
277
|
+
|
|
278
|
+
const ok2 = openDatabaseByWorkspace(ws2);
|
|
279
|
+
assert.ok(ok2, "openDatabaseByWorkspace(ws2) must succeed");
|
|
280
|
+
const cacheAfterWs2 = _getDbCache();
|
|
281
|
+
const entry2 = cacheAfterWs2.get(ws2.identityKey);
|
|
282
|
+
assert.ok(entry2, "cache entry for ws2.identityKey must exist");
|
|
283
|
+
const dbPath2 = entry2.dbPath;
|
|
284
|
+
|
|
285
|
+
assert.equal(
|
|
286
|
+
dbPath1,
|
|
287
|
+
dbPath2,
|
|
288
|
+
"sibling worktrees must resolve to the same DB path (shared WAL)",
|
|
289
|
+
);
|
|
290
|
+
assert.equal(
|
|
291
|
+
cacheAfterWs2.size,
|
|
292
|
+
1,
|
|
293
|
+
"only one cache entry for project + two sibling worktrees",
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// ─── Test 5: gsdRootCache normalization survives trailing-slash inputs ────────
|
|
299
|
+
|
|
300
|
+
describe("workspace-collapse integration: Test 5 — gsdRootCache normalization deduplicates trailing-slash inputs", () => {
|
|
301
|
+
let projectDir: string;
|
|
302
|
+
let fakeHome: string;
|
|
303
|
+
let savedHome: string | undefined;
|
|
304
|
+
let savedUserProfile: string | undefined;
|
|
305
|
+
let savedGsdHome: string | undefined;
|
|
306
|
+
|
|
307
|
+
beforeEach(() => {
|
|
308
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-cache-int-")));
|
|
309
|
+
mkdirSync(join(projectDir, ".gsd"), { recursive: true });
|
|
310
|
+
|
|
311
|
+
fakeHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-cache-int-home-")));
|
|
312
|
+
|
|
313
|
+
savedHome = process.env.HOME;
|
|
314
|
+
savedUserProfile = process.env.USERPROFILE;
|
|
315
|
+
savedGsdHome = process.env.GSD_HOME;
|
|
316
|
+
|
|
317
|
+
// Prevent ~/.gsd interference
|
|
318
|
+
process.env.HOME = fakeHome;
|
|
319
|
+
process.env.USERPROFILE = fakeHome;
|
|
320
|
+
process.env.GSD_HOME = join(fakeHome, ".gsd");
|
|
321
|
+
|
|
322
|
+
clearPathCache();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
afterEach(() => {
|
|
326
|
+
if (savedHome === undefined) delete process.env.HOME;
|
|
327
|
+
else process.env.HOME = savedHome;
|
|
328
|
+
if (savedUserProfile === undefined) delete process.env.USERPROFILE;
|
|
329
|
+
else process.env.USERPROFILE = savedUserProfile;
|
|
330
|
+
if (savedGsdHome === undefined) delete process.env.GSD_HOME;
|
|
331
|
+
else process.env.GSD_HOME = savedGsdHome;
|
|
332
|
+
|
|
333
|
+
clearPathCache();
|
|
334
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
335
|
+
rmSync(fakeHome, { recursive: true, force: true });
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("gsdRoot('/path/to/project') and gsdRoot('/path/to/project/') return identical paths", () => {
|
|
339
|
+
const withoutSlash = gsdRoot(projectDir);
|
|
340
|
+
const withSlash = gsdRoot(projectDir + "/");
|
|
341
|
+
|
|
342
|
+
assert.equal(
|
|
343
|
+
withoutSlash,
|
|
344
|
+
withSlash,
|
|
345
|
+
"gsdRoot must return identical paths for inputs with and without trailing slash",
|
|
346
|
+
);
|
|
347
|
+
assert.equal(
|
|
348
|
+
withoutSlash,
|
|
349
|
+
join(projectDir, ".gsd"),
|
|
350
|
+
"both calls must resolve to projectDir/.gsd",
|
|
351
|
+
);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("both calls after clearPathCache() return identical paths (no duplicate cache entries)", () => {
|
|
355
|
+
// Start clean
|
|
356
|
+
clearPathCache();
|
|
357
|
+
|
|
358
|
+
const r1 = gsdRoot(projectDir);
|
|
359
|
+
const r2 = gsdRoot(projectDir + "/");
|
|
360
|
+
|
|
361
|
+
assert.equal(r1, r2, "r1 and r2 must be the same string after normalization");
|
|
362
|
+
// The cache normalizes both inputs to the same key — no duplicate entries.
|
|
363
|
+
// We can't inspect the cache size directly, but the behavioral proof is
|
|
364
|
+
// that a second call after clearPathCache re-probes and still matches.
|
|
365
|
+
clearPathCache();
|
|
366
|
+
const r3 = gsdRoot(projectDir + "/");
|
|
367
|
+
assert.equal(r3, r1, "re-probe after clearPathCache must produce the same result");
|
|
368
|
+
});
|
|
369
|
+
});
|
|
@@ -6,6 +6,21 @@ import { tmpdir } from "node:os";
|
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
|
|
8
8
|
import { assessInterruptedSession } from "../interrupted-session.ts";
|
|
9
|
+
import {
|
|
10
|
+
openDatabase,
|
|
11
|
+
closeDatabase,
|
|
12
|
+
insertMilestone,
|
|
13
|
+
_getAdapter,
|
|
14
|
+
} from "../gsd-db.ts";
|
|
15
|
+
import { registerAutoWorker } from "../db/auto-workers.ts";
|
|
16
|
+
import { claimMilestoneLease } from "../db/milestone-leases.ts";
|
|
17
|
+
import { recordDispatchClaim } from "../db/unit-dispatches.ts";
|
|
18
|
+
import { setRuntimeKv } from "../db/runtime-kv.ts";
|
|
19
|
+
import {
|
|
20
|
+
PAUSED_SESSION_KV_KEY,
|
|
21
|
+
type PausedSessionMetadata,
|
|
22
|
+
} from "../interrupted-session.ts";
|
|
23
|
+
import { normalizeRealPath } from "../paths.ts";
|
|
9
24
|
|
|
10
25
|
function makeTmpBase(): string {
|
|
11
26
|
const base = join(tmpdir(), `gsd-auto-interrupted-${randomUUID()}`);
|
|
@@ -14,9 +29,61 @@ function makeTmpBase(): string {
|
|
|
14
29
|
}
|
|
15
30
|
|
|
16
31
|
function cleanup(base: string): void {
|
|
32
|
+
try { closeDatabase(); } catch { /* */ }
|
|
17
33
|
try { rmSync(base, { recursive: true, force: true }); } catch { /* */ }
|
|
18
34
|
}
|
|
19
35
|
|
|
36
|
+
function openFixtureDb(base: string): void {
|
|
37
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function expireWorker(workerId: string): void {
|
|
41
|
+
const db = _getAdapter()!;
|
|
42
|
+
db.prepare(
|
|
43
|
+
`UPDATE workers SET last_heartbeat_at = '1970-01-01T00:00:00.000Z' WHERE worker_id = :worker_id`,
|
|
44
|
+
).run({ ":worker_id": workerId });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function writeLock(base: string, unitType: string, unitId: string): void {
|
|
48
|
+
openFixtureDb(base);
|
|
49
|
+
insertMilestone({
|
|
50
|
+
id: "M001",
|
|
51
|
+
title: "Test Milestone",
|
|
52
|
+
status: unitType === "complete-slice" ? "complete" : "active",
|
|
53
|
+
});
|
|
54
|
+
const workerId = registerAutoWorker({ projectRootRealpath: normalizeRealPath(base) });
|
|
55
|
+
const lease = claimMilestoneLease(workerId, "M001");
|
|
56
|
+
assert.equal(lease.ok, true);
|
|
57
|
+
if (lease.ok) {
|
|
58
|
+
const [, sliceId = null, taskId = null] = unitId.split("/");
|
|
59
|
+
const claimed = recordDispatchClaim({
|
|
60
|
+
traceId: `trace-${randomUUID().slice(0, 8)}`,
|
|
61
|
+
workerId,
|
|
62
|
+
milestoneLeaseToken: lease.token,
|
|
63
|
+
milestoneId: "M001",
|
|
64
|
+
sliceId,
|
|
65
|
+
taskId,
|
|
66
|
+
unitType,
|
|
67
|
+
unitId,
|
|
68
|
+
});
|
|
69
|
+
assert.equal(claimed.ok, true);
|
|
70
|
+
}
|
|
71
|
+
_getAdapter()!
|
|
72
|
+
.prepare(`UPDATE workers SET pid = 99999 WHERE worker_id = :worker_id`)
|
|
73
|
+
.run({ ":worker_id": workerId });
|
|
74
|
+
expireWorker(workerId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function writePausedSession(base: string, milestoneId = "M001", stepMode = false): void {
|
|
78
|
+
openFixtureDb(base);
|
|
79
|
+
const meta: PausedSessionMetadata = {
|
|
80
|
+
milestoneId,
|
|
81
|
+
originalBasePath: base,
|
|
82
|
+
stepMode,
|
|
83
|
+
};
|
|
84
|
+
setRuntimeKv("global", "", PAUSED_SESSION_KV_KEY, meta);
|
|
85
|
+
}
|
|
86
|
+
|
|
20
87
|
function writeRoadmap(base: string, checked = false): void {
|
|
21
88
|
const milestoneDir = join(base, ".gsd", "milestones", "M001");
|
|
22
89
|
mkdirSync(join(milestoneDir, "slices", "S01", "tasks"), { recursive: true });
|
|
@@ -51,42 +118,22 @@ function writeRoadmap(base: string, checked = false): void {
|
|
|
51
118
|
function writeCompleteArtifacts(base: string): void {
|
|
52
119
|
const milestoneDir = join(base, ".gsd", "milestones", "M001");
|
|
53
120
|
const sliceDir = join(milestoneDir, "slices", "S01");
|
|
121
|
+
const tasksDir = join(sliceDir, "tasks");
|
|
54
122
|
mkdirSync(sliceDir, { recursive: true });
|
|
123
|
+
mkdirSync(tasksDir, { recursive: true });
|
|
124
|
+
writeFileSync(join(sliceDir, "S01-PLAN.md"), "# S01: Test Slice\n\n## Tasks\n- [x] **T01: Do thing** `est:10m`\n", "utf-8");
|
|
125
|
+
writeFileSync(join(tasksDir, "T01-SUMMARY.md"), "# Task Summary\nDone.\n", "utf-8");
|
|
55
126
|
writeFileSync(join(sliceDir, "S01-SUMMARY.md"), "# Summary\nDone.\n", "utf-8");
|
|
56
127
|
writeFileSync(join(sliceDir, "S01-UAT.md"), "# UAT\nPassed.\n", "utf-8");
|
|
57
128
|
writeFileSync(join(milestoneDir, "M001-SUMMARY.md"), "# Milestone Summary\nDone.\n", "utf-8");
|
|
58
129
|
}
|
|
59
130
|
|
|
60
|
-
function writeLock(base: string, unitType: string, unitId: string): void {
|
|
61
|
-
writeFileSync(
|
|
62
|
-
join(base, ".gsd", "auto.lock"),
|
|
63
|
-
JSON.stringify({
|
|
64
|
-
pid: 999999999,
|
|
65
|
-
startedAt: new Date().toISOString(),
|
|
66
|
-
unitType,
|
|
67
|
-
unitId,
|
|
68
|
-
unitStartedAt: new Date().toISOString(),
|
|
69
|
-
}, null, 2),
|
|
70
|
-
"utf-8",
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function writePausedSession(base: string, milestoneId = "M001", stepMode = false): void {
|
|
75
|
-
const runtimeDir = join(base, ".gsd", "runtime");
|
|
76
|
-
mkdirSync(runtimeDir, { recursive: true });
|
|
77
|
-
writeFileSync(
|
|
78
|
-
join(runtimeDir, "paused-session.json"),
|
|
79
|
-
JSON.stringify({ milestoneId, originalBasePath: base, stepMode }, null, 2),
|
|
80
|
-
"utf-8",
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
131
|
test("direct /gsd auto stale complete repo yields stale classification with no recovery payload", async () => {
|
|
85
132
|
const base = makeTmpBase();
|
|
86
133
|
try {
|
|
87
134
|
writeRoadmap(base, true);
|
|
88
135
|
writeCompleteArtifacts(base);
|
|
89
|
-
writeLock(base, "
|
|
136
|
+
writeLock(base, "complete-slice", "M001/S01");
|
|
90
137
|
|
|
91
138
|
const assessment = await assessInterruptedSession(base);
|
|
92
139
|
assert.equal(assessment.classification, "stale");
|
|
@@ -6,6 +6,21 @@ import { tmpdir } from "node:os";
|
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
|
|
8
8
|
import { assessInterruptedSession } from "../interrupted-session.ts";
|
|
9
|
+
import {
|
|
10
|
+
openDatabase,
|
|
11
|
+
closeDatabase,
|
|
12
|
+
insertMilestone,
|
|
13
|
+
_getAdapter,
|
|
14
|
+
} from "../gsd-db.ts";
|
|
15
|
+
import { registerAutoWorker } from "../db/auto-workers.ts";
|
|
16
|
+
import { claimMilestoneLease } from "../db/milestone-leases.ts";
|
|
17
|
+
import { recordDispatchClaim } from "../db/unit-dispatches.ts";
|
|
18
|
+
import { setRuntimeKv } from "../db/runtime-kv.ts";
|
|
19
|
+
import {
|
|
20
|
+
PAUSED_SESSION_KV_KEY,
|
|
21
|
+
type PausedSessionMetadata,
|
|
22
|
+
} from "../interrupted-session.ts";
|
|
23
|
+
import { normalizeRealPath } from "../paths.ts";
|
|
9
24
|
|
|
10
25
|
function makeTmpBase(): string {
|
|
11
26
|
const base = join(tmpdir(), `gsd-smart-entry-${randomUUID()}`);
|
|
@@ -14,9 +29,61 @@ function makeTmpBase(): string {
|
|
|
14
29
|
}
|
|
15
30
|
|
|
16
31
|
function cleanup(base: string): void {
|
|
32
|
+
try { closeDatabase(); } catch { /* */ }
|
|
17
33
|
try { rmSync(base, { recursive: true, force: true }); } catch { /* */ }
|
|
18
34
|
}
|
|
19
35
|
|
|
36
|
+
function openFixtureDb(base: string): void {
|
|
37
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function expireWorker(workerId: string): void {
|
|
41
|
+
const db = _getAdapter()!;
|
|
42
|
+
db.prepare(
|
|
43
|
+
`UPDATE workers SET last_heartbeat_at = '1970-01-01T00:00:00.000Z' WHERE worker_id = :worker_id`,
|
|
44
|
+
).run({ ":worker_id": workerId });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function writePausedSession(base: string, milestoneId = "M001", stepMode = false): void {
|
|
48
|
+
openFixtureDb(base);
|
|
49
|
+
const meta: PausedSessionMetadata = {
|
|
50
|
+
milestoneId,
|
|
51
|
+
originalBasePath: base,
|
|
52
|
+
stepMode,
|
|
53
|
+
};
|
|
54
|
+
setRuntimeKv("global", "", PAUSED_SESSION_KV_KEY, meta);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function writeLock(base: string, unitType: string, unitId: string): void {
|
|
58
|
+
openFixtureDb(base);
|
|
59
|
+
insertMilestone({
|
|
60
|
+
id: "M001",
|
|
61
|
+
title: "Test Milestone",
|
|
62
|
+
status: unitType === "complete-slice" ? "complete" : "active",
|
|
63
|
+
});
|
|
64
|
+
const workerId = registerAutoWorker({ projectRootRealpath: normalizeRealPath(base) });
|
|
65
|
+
const lease = claimMilestoneLease(workerId, "M001");
|
|
66
|
+
assert.equal(lease.ok, true);
|
|
67
|
+
if (lease.ok) {
|
|
68
|
+
const [, sliceId = null, taskId = null] = unitId.split("/");
|
|
69
|
+
const claimed = recordDispatchClaim({
|
|
70
|
+
traceId: `trace-${randomUUID().slice(0, 8)}`,
|
|
71
|
+
workerId,
|
|
72
|
+
milestoneLeaseToken: lease.token,
|
|
73
|
+
milestoneId: "M001",
|
|
74
|
+
sliceId,
|
|
75
|
+
taskId,
|
|
76
|
+
unitType,
|
|
77
|
+
unitId,
|
|
78
|
+
});
|
|
79
|
+
assert.equal(claimed.ok, true);
|
|
80
|
+
}
|
|
81
|
+
_getAdapter()!
|
|
82
|
+
.prepare(`UPDATE workers SET pid = 99999 WHERE worker_id = :worker_id`)
|
|
83
|
+
.run({ ":worker_id": workerId });
|
|
84
|
+
expireWorker(workerId);
|
|
85
|
+
}
|
|
86
|
+
|
|
20
87
|
function writeRoadmap(base: string, checked = false): void {
|
|
21
88
|
const milestoneDir = join(base, ".gsd", "milestones", "M001");
|
|
22
89
|
mkdirSync(join(milestoneDir, "slices", "S01", "tasks"), { recursive: true });
|
|
@@ -51,42 +118,22 @@ function writeRoadmap(base: string, checked = false): void {
|
|
|
51
118
|
function writeCompleteArtifacts(base: string): void {
|
|
52
119
|
const milestoneDir = join(base, ".gsd", "milestones", "M001");
|
|
53
120
|
const sliceDir = join(milestoneDir, "slices", "S01");
|
|
121
|
+
const tasksDir = join(sliceDir, "tasks");
|
|
54
122
|
mkdirSync(sliceDir, { recursive: true });
|
|
123
|
+
mkdirSync(tasksDir, { recursive: true });
|
|
124
|
+
writeFileSync(join(sliceDir, "S01-PLAN.md"), "# S01: Test Slice\n\n## Tasks\n- [x] **T01: Do thing** `est:10m`\n", "utf-8");
|
|
125
|
+
writeFileSync(join(tasksDir, "T01-SUMMARY.md"), "# Task Summary\nDone.\n", "utf-8");
|
|
55
126
|
writeFileSync(join(sliceDir, "S01-SUMMARY.md"), "# Summary\nDone.\n", "utf-8");
|
|
56
127
|
writeFileSync(join(sliceDir, "S01-UAT.md"), "# UAT\nPassed.\n", "utf-8");
|
|
57
128
|
writeFileSync(join(milestoneDir, "M001-SUMMARY.md"), "# Milestone Summary\nDone.\n", "utf-8");
|
|
58
129
|
}
|
|
59
130
|
|
|
60
|
-
function writePausedSession(base: string, milestoneId = "M001", stepMode = false): void {
|
|
61
|
-
const runtimeDir = join(base, ".gsd", "runtime");
|
|
62
|
-
mkdirSync(runtimeDir, { recursive: true });
|
|
63
|
-
writeFileSync(
|
|
64
|
-
join(runtimeDir, "paused-session.json"),
|
|
65
|
-
JSON.stringify({ milestoneId, originalBasePath: base, stepMode }, null, 2),
|
|
66
|
-
"utf-8",
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function writeLock(base: string, unitType: string, unitId: string): void {
|
|
71
|
-
writeFileSync(
|
|
72
|
-
join(base, ".gsd", "auto.lock"),
|
|
73
|
-
JSON.stringify({
|
|
74
|
-
pid: 999999999,
|
|
75
|
-
startedAt: new Date().toISOString(),
|
|
76
|
-
unitType,
|
|
77
|
-
unitId,
|
|
78
|
-
unitStartedAt: new Date().toISOString(),
|
|
79
|
-
}, null, 2),
|
|
80
|
-
"utf-8",
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
131
|
test("guided-flow stale complete scenario classifies as stale so the resume prompt can be suppressed", async () => {
|
|
85
132
|
const base = makeTmpBase();
|
|
86
133
|
try {
|
|
87
134
|
writeRoadmap(base, true);
|
|
88
135
|
writeCompleteArtifacts(base);
|
|
89
|
-
writeLock(base, "
|
|
136
|
+
writeLock(base, "complete-slice", "M001/S01");
|
|
90
137
|
|
|
91
138
|
const assessment = await assessInterruptedSession(base);
|
|
92
139
|
assert.equal(assessment.classification, "stale");
|
|
@@ -40,23 +40,26 @@ describe("stuck detection persistence (#3704)", () => {
|
|
|
40
40
|
assert.match(loopSource, /function saveStuckState/);
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
+
// Phase C: API changed from (basePath) to (session) — recentUnits is
|
|
44
|
+
// now reconstructed from unit_dispatches and stuckRecoveryAttempts
|
|
45
|
+
// persists in runtime_kv (worker scope).
|
|
43
46
|
test("loopState initialized from persisted state", () => {
|
|
44
|
-
assert.match(loopSource, /loadStuckState\(s
|
|
47
|
+
assert.match(loopSource, /loadStuckState\(s\)/);
|
|
45
48
|
});
|
|
46
49
|
|
|
47
50
|
test("stuck state saved after each iteration", () => {
|
|
48
|
-
assert.match(loopSource, /saveStuckState\(s
|
|
51
|
+
assert.match(loopSource, /saveStuckState\(s,\s*loopState\)/);
|
|
49
52
|
});
|
|
50
53
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
// Phase C: stuck-state.json file IO deleted; persistence moved to
|
|
55
|
+
// unit_dispatches (recentUnits) + runtime_kv (stuckRecoveryAttempts).
|
|
56
|
+
// The stuck-state-via-db.test.ts suite covers the round-trip.
|
|
54
57
|
|
|
55
58
|
test("saveStuckState called in standard dev path as well as custom engine path (#4382)", () => {
|
|
56
59
|
// Count all call-sites of saveStuckState (excluding the function definition itself).
|
|
57
60
|
// After the fix, both the custom-engine path and the standard dev path must each
|
|
58
61
|
// call saveStuckState so stuckRecoveryAttempts survives session restarts.
|
|
59
|
-
const callMatches = loopSource.match(/saveStuckState\(s
|
|
62
|
+
const callMatches = loopSource.match(/saveStuckState\(s,\s*loopState\)/g) ?? [];
|
|
60
63
|
assert.ok(
|
|
61
64
|
callMatches.length >= 2,
|
|
62
65
|
`saveStuckState must be called in both the custom-engine path and the standard dev path ` +
|