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,226 @@
|
|
|
1
|
+
// GSD-2 + gsd-db workspace-scoped connection cache tests
|
|
2
|
+
|
|
3
|
+
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import {
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
realpathSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
|
|
14
|
+
import { createWorkspace, scopeMilestone } from "../workspace.ts";
|
|
15
|
+
import {
|
|
16
|
+
openDatabaseByWorkspace,
|
|
17
|
+
openDatabaseByScope,
|
|
18
|
+
closeDatabaseByWorkspace,
|
|
19
|
+
closeAllDatabases,
|
|
20
|
+
_getDbCache,
|
|
21
|
+
_getAdapter,
|
|
22
|
+
} from "../gsd-db.ts";
|
|
23
|
+
|
|
24
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a minimal project directory with the artifacts that make
|
|
28
|
+
* createWorkspace() resolve it as a proper project root (not a bare temp dir).
|
|
29
|
+
* Returns the realpath-normalised absolute path.
|
|
30
|
+
*/
|
|
31
|
+
function makeProjectDir(): string {
|
|
32
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-db-ws-scope-")));
|
|
33
|
+
// hasGsdBootstrapArtifacts checks for .gsd/milestones or .gsd/PREFERENCES.md
|
|
34
|
+
mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
|
|
35
|
+
return dir;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a worktree path inside a project's .gsd/worktrees/<MID>/ layout.
|
|
40
|
+
* createWorkspace() will detect the /.gsd/worktrees/ segment and resolve the
|
|
41
|
+
* project root back to `projectDir`.
|
|
42
|
+
*/
|
|
43
|
+
function makeWorktreeDir(projectDir: string, mid: string): string {
|
|
44
|
+
const worktreeDir = join(projectDir, ".gsd", "worktrees", mid);
|
|
45
|
+
mkdirSync(worktreeDir, { recursive: true });
|
|
46
|
+
return worktreeDir;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── Suite: same realpath → same identityKey → same DB instance ──────────────
|
|
50
|
+
|
|
51
|
+
describe("openDatabaseByWorkspace: same project reuses connection", () => {
|
|
52
|
+
let projectDir: string;
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
projectDir = makeProjectDir();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
closeAllDatabases();
|
|
60
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("two createWorkspace calls with the same path share identityKey", () => {
|
|
64
|
+
const ws1 = createWorkspace(projectDir);
|
|
65
|
+
const ws2 = createWorkspace(projectDir);
|
|
66
|
+
assert.equal(ws1.identityKey, ws2.identityKey);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("openDatabaseByWorkspace returns the same DB adapter for the same project", () => {
|
|
70
|
+
const ws1 = createWorkspace(projectDir);
|
|
71
|
+
const ws2 = createWorkspace(projectDir);
|
|
72
|
+
|
|
73
|
+
const ok1 = openDatabaseByWorkspace(ws1);
|
|
74
|
+
assert.ok(ok1, "first open should succeed");
|
|
75
|
+
const adapter1 = _getAdapter();
|
|
76
|
+
|
|
77
|
+
const ok2 = openDatabaseByWorkspace(ws2);
|
|
78
|
+
assert.ok(ok2, "second open should succeed");
|
|
79
|
+
const adapter2 = _getAdapter();
|
|
80
|
+
|
|
81
|
+
assert.equal(adapter1, adapter2, "same project → same DB adapter instance");
|
|
82
|
+
assert.equal(_getDbCache().size, 1, "only one cache entry for same project");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ─── Suite: different projects → different DB instances ──────────────────────
|
|
87
|
+
|
|
88
|
+
describe("openDatabaseByWorkspace: different projects get separate connections", () => {
|
|
89
|
+
let projectA: string;
|
|
90
|
+
let projectB: string;
|
|
91
|
+
|
|
92
|
+
beforeEach(() => {
|
|
93
|
+
projectA = makeProjectDir();
|
|
94
|
+
projectB = makeProjectDir();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
afterEach(() => {
|
|
98
|
+
closeAllDatabases();
|
|
99
|
+
rmSync(projectA, { recursive: true, force: true });
|
|
100
|
+
rmSync(projectB, { recursive: true, force: true });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("two different projects produce different identityKeys", () => {
|
|
104
|
+
const wsA = createWorkspace(projectA);
|
|
105
|
+
const wsB = createWorkspace(projectB);
|
|
106
|
+
assert.notEqual(wsA.identityKey, wsB.identityKey);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("opening two different projects stores two cache entries", () => {
|
|
110
|
+
const wsA = createWorkspace(projectA);
|
|
111
|
+
const wsB = createWorkspace(projectB);
|
|
112
|
+
|
|
113
|
+
openDatabaseByWorkspace(wsA);
|
|
114
|
+
const adapterAfterA = _getAdapter();
|
|
115
|
+
|
|
116
|
+
openDatabaseByWorkspace(wsB);
|
|
117
|
+
const adapterAfterB = _getAdapter();
|
|
118
|
+
|
|
119
|
+
assert.notEqual(adapterAfterA, adapterAfterB, "different projects → different adapter instances");
|
|
120
|
+
assert.equal(_getDbCache().size, 2, "two cache entries for two distinct projects");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ─── Suite: sibling worktrees share the same DB instance ─────────────────────
|
|
125
|
+
|
|
126
|
+
describe("openDatabaseByWorkspace: sibling worktrees share DB connection", () => {
|
|
127
|
+
let projectDir: string;
|
|
128
|
+
|
|
129
|
+
beforeEach(() => {
|
|
130
|
+
projectDir = makeProjectDir();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
afterEach(() => {
|
|
134
|
+
closeAllDatabases();
|
|
135
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("worktree path resolves to same identityKey as project root", () => {
|
|
139
|
+
const worktreeDir = makeWorktreeDir(projectDir, "M001");
|
|
140
|
+
const wsProject = createWorkspace(projectDir);
|
|
141
|
+
const wsWorktree = createWorkspace(worktreeDir);
|
|
142
|
+
assert.equal(
|
|
143
|
+
wsProject.identityKey,
|
|
144
|
+
wsWorktree.identityKey,
|
|
145
|
+
"project root and sibling worktree share identityKey",
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("opening via project path and via worktree path yields the same DB adapter", () => {
|
|
150
|
+
const worktreeDir = makeWorktreeDir(projectDir, "M001");
|
|
151
|
+
const wsProject = createWorkspace(projectDir);
|
|
152
|
+
const wsWorktree = createWorkspace(worktreeDir);
|
|
153
|
+
|
|
154
|
+
openDatabaseByWorkspace(wsProject);
|
|
155
|
+
const adapterProject = _getAdapter();
|
|
156
|
+
|
|
157
|
+
openDatabaseByWorkspace(wsWorktree);
|
|
158
|
+
const adapterWorktree = _getAdapter();
|
|
159
|
+
|
|
160
|
+
assert.equal(
|
|
161
|
+
adapterProject,
|
|
162
|
+
adapterWorktree,
|
|
163
|
+
"sibling worktree reuses the same DB adapter as the project root",
|
|
164
|
+
);
|
|
165
|
+
assert.equal(_getDbCache().size, 1, "only one cache entry for project + sibling worktree");
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ─── Suite: closing removes only the targeted cache entry ─────────────────────
|
|
170
|
+
|
|
171
|
+
describe("closeDatabaseByWorkspace: removes only the targeted cache entry", () => {
|
|
172
|
+
let projectA: string;
|
|
173
|
+
let projectB: string;
|
|
174
|
+
|
|
175
|
+
beforeEach(() => {
|
|
176
|
+
projectA = makeProjectDir();
|
|
177
|
+
projectB = makeProjectDir();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
afterEach(() => {
|
|
181
|
+
closeAllDatabases();
|
|
182
|
+
rmSync(projectA, { recursive: true, force: true });
|
|
183
|
+
rmSync(projectB, { recursive: true, force: true });
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("closing workspace A removes only A from the cache", () => {
|
|
187
|
+
const wsA = createWorkspace(projectA);
|
|
188
|
+
const wsB = createWorkspace(projectB);
|
|
189
|
+
|
|
190
|
+
openDatabaseByWorkspace(wsA);
|
|
191
|
+
openDatabaseByWorkspace(wsB);
|
|
192
|
+
assert.equal(_getDbCache().size, 2, "precondition: two cache entries");
|
|
193
|
+
|
|
194
|
+
closeDatabaseByWorkspace(wsA);
|
|
195
|
+
|
|
196
|
+
assert.equal(_getDbCache().size, 1, "one entry remains after closing A");
|
|
197
|
+
assert.ok(!_getDbCache().has(wsA.identityKey), "A's entry is gone");
|
|
198
|
+
assert.ok(_getDbCache().has(wsB.identityKey), "B's entry is still present");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("closing the active workspace via closeDatabaseByWorkspace nulls currentDb", () => {
|
|
202
|
+
const wsA = createWorkspace(projectA);
|
|
203
|
+
|
|
204
|
+
openDatabaseByWorkspace(wsA);
|
|
205
|
+
assert.ok(_getAdapter() !== null, "precondition: adapter is open");
|
|
206
|
+
|
|
207
|
+
// Make wsA the active connection explicitly.
|
|
208
|
+
openDatabaseByWorkspace(wsA);
|
|
209
|
+
closeDatabaseByWorkspace(wsA);
|
|
210
|
+
|
|
211
|
+
// After closing the active connection, the global adapter should be null.
|
|
212
|
+
assert.equal(_getAdapter(), null, "currentDb should be null after closing active workspace");
|
|
213
|
+
assert.equal(_getDbCache().size, 0, "cache should be empty after closing sole entry");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("openDatabaseByScope delegates to workspace correctly", () => {
|
|
217
|
+
const ws = createWorkspace(projectA);
|
|
218
|
+
const scope = scopeMilestone(ws, "M001");
|
|
219
|
+
|
|
220
|
+
const ok = openDatabaseByScope(scope);
|
|
221
|
+
assert.ok(ok, "openDatabaseByScope should succeed");
|
|
222
|
+
assert.ok(_getDbCache().has(ws.identityKey), "cache entry exists after openDatabaseByScope");
|
|
223
|
+
|
|
224
|
+
closeDatabaseByWorkspace(ws);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// GSD-2 + gsd-root-canonical: gsdRoot() result is realpath-canonicalized before caching
|
|
2
|
+
|
|
3
|
+
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import {
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
realpathSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
symlinkSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
15
|
+
|
|
16
|
+
import { gsdRoot, _clearGsdRootCache } from "../paths.ts";
|
|
17
|
+
|
|
18
|
+
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
describe("gsdRoot: returns realpath-canonicalized result", () => {
|
|
21
|
+
let projectDir: string;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-root-canon-")));
|
|
25
|
+
mkdirSync(join(projectDir, ".gsd"), { recursive: true });
|
|
26
|
+
_clearGsdRootCache();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
_clearGsdRootCache();
|
|
31
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("gsdRoot from a canonical project path returns a realpath-canonicalized result", () => {
|
|
35
|
+
const result = gsdRoot(projectDir);
|
|
36
|
+
const canonical = realpathSync(join(projectDir, ".gsd"));
|
|
37
|
+
assert.equal(result, canonical, "gsdRoot must return the realpath of the .gsd directory");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("gsdRoot via a symlinked project path returns the realpath-canonicalized .gsd", (t) => {
|
|
41
|
+
// Create a symlink pointing to projectDir
|
|
42
|
+
const linkPath = join(tmpdir(), `gsd-root-link-${randomUUID()}`);
|
|
43
|
+
symlinkSync(projectDir, linkPath);
|
|
44
|
+
t.after(() => {
|
|
45
|
+
try { rmSync(linkPath); } catch { /* ignore */ }
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
_clearGsdRootCache();
|
|
49
|
+
|
|
50
|
+
const result = gsdRoot(linkPath);
|
|
51
|
+
// The canonical .gsd is under the realpath of projectDir, not the symlink
|
|
52
|
+
const canonicalGsd = realpathSync(join(projectDir, ".gsd"));
|
|
53
|
+
|
|
54
|
+
assert.equal(
|
|
55
|
+
result,
|
|
56
|
+
canonicalGsd,
|
|
57
|
+
`gsdRoot via symlink ("${linkPath}") must return the realpath'd .gsd ("${canonicalGsd}"), not a symlink-based path`,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Also verify that the result does NOT contain the symlink in its path
|
|
61
|
+
assert.ok(
|
|
62
|
+
!result.startsWith(linkPath),
|
|
63
|
+
`gsdRoot result must not start with the symlink path "${linkPath}"`,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* GSD2 — regression
|
|
3
|
-
* GSD home (~/.gsd) as a project .gsd directory when basePath resolves to
|
|
4
|
-
* $HOME. Paths under ~/.gsd/projects/<hash>/ remain valid.
|
|
2
|
+
* GSD2 — regression tests for #5187 and git-root anchor guard:
|
|
5
3
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* #5187: gsdRoot() must refuse to use the global GSD home (~/.gsd) as a
|
|
5
|
+
* project .gsd directory when basePath resolves to $HOME. Paths under
|
|
6
|
+
* ~/.gsd/projects/<hash>/ remain valid.
|
|
7
|
+
*
|
|
8
|
+
* git-root anchor guard: when $HOME is itself a git repo and ~/.gsd exists,
|
|
9
|
+
* gsdRoot() must NOT return ~/.gsd for a subdir basePath like ~/projects/foo.
|
|
10
|
+
* It should fall through to step 4 (creation fallback) instead.
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
13
|
import { describe, test, beforeEach, afterEach } from 'node:test';
|
|
@@ -12,6 +15,7 @@ import assert from 'node:assert/strict';
|
|
|
12
15
|
import { mkdtempSync, mkdirSync, rmSync, realpathSync } from 'node:fs';
|
|
13
16
|
import { tmpdir } from 'node:os';
|
|
14
17
|
import { join } from 'node:path';
|
|
18
|
+
import { spawnSync } from 'node:child_process';
|
|
15
19
|
|
|
16
20
|
import { gsdRoot, _clearGsdRootCache } from '../paths.ts';
|
|
17
21
|
|
|
@@ -84,3 +88,62 @@ describe('gsdRoot() refuses ~/.gsd as project state when basePath is $HOME (#518
|
|
|
84
88
|
}
|
|
85
89
|
});
|
|
86
90
|
});
|
|
91
|
+
|
|
92
|
+
describe('git-root anchor guard: subdir basePath must not resolve to ~/.gsd', () => {
|
|
93
|
+
let fakeHome: string;
|
|
94
|
+
let subDir: string;
|
|
95
|
+
let savedHome: string | undefined;
|
|
96
|
+
let savedUserProfile: string | undefined;
|
|
97
|
+
let savedGsdHome: string | undefined;
|
|
98
|
+
|
|
99
|
+
beforeEach(() => {
|
|
100
|
+
// Create a tmpdir that will act as both $HOME and a git repo root.
|
|
101
|
+
fakeHome = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-anchor-guard-')));
|
|
102
|
+
// Init a bare-minimum git repo so git rev-parse --show-toplevel returns fakeHome.
|
|
103
|
+
spawnSync('git', ['init', fakeHome], { encoding: 'utf-8' });
|
|
104
|
+
// Create ~/.gsd (the global home that must NOT be used for project subdirs).
|
|
105
|
+
mkdirSync(join(fakeHome, '.gsd'), { recursive: true });
|
|
106
|
+
// Create a subdir inside the git repo — this is the project basePath.
|
|
107
|
+
subDir = join(fakeHome, 'projects', 'foo');
|
|
108
|
+
mkdirSync(subDir, { recursive: true });
|
|
109
|
+
|
|
110
|
+
savedHome = process.env.HOME;
|
|
111
|
+
savedUserProfile = process.env.USERPROFILE;
|
|
112
|
+
savedGsdHome = process.env.GSD_HOME;
|
|
113
|
+
|
|
114
|
+
process.env.HOME = fakeHome;
|
|
115
|
+
process.env.USERPROFILE = fakeHome;
|
|
116
|
+
delete process.env.GSD_HOME;
|
|
117
|
+
|
|
118
|
+
_clearGsdRootCache();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
afterEach(() => {
|
|
122
|
+
if (savedHome === undefined) delete process.env.HOME;
|
|
123
|
+
else process.env.HOME = savedHome;
|
|
124
|
+
if (savedUserProfile === undefined) delete process.env.USERPROFILE;
|
|
125
|
+
else process.env.USERPROFILE = savedUserProfile;
|
|
126
|
+
if (savedGsdHome === undefined) delete process.env.GSD_HOME;
|
|
127
|
+
else process.env.GSD_HOME = savedGsdHome;
|
|
128
|
+
|
|
129
|
+
_clearGsdRootCache();
|
|
130
|
+
rmSync(fakeHome, { recursive: true, force: true });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('does NOT return ~/.gsd when $HOME is a git repo and basePath is a subdir', () => {
|
|
134
|
+
// fakeHome IS the git root AND $HOME, so git rev-parse returns fakeHome,
|
|
135
|
+
// and ~/.gsd (fakeHome/.gsd) exists. The guard must skip that candidate
|
|
136
|
+
// and fall through to the creation fallback: subDir/.gsd.
|
|
137
|
+
const result = gsdRoot(subDir);
|
|
138
|
+
assert.notEqual(
|
|
139
|
+
result,
|
|
140
|
+
join(fakeHome, '.gsd'),
|
|
141
|
+
'gsdRoot must not return ~/.gsd for a subdir basePath',
|
|
142
|
+
);
|
|
143
|
+
assert.equal(
|
|
144
|
+
result,
|
|
145
|
+
join(subDir, '.gsd'),
|
|
146
|
+
'gsdRoot should fall through to the creation fallback for a subdir',
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -86,8 +86,8 @@ describe("guided-flow → auto-prompts consolidation (#5183)", () => {
|
|
|
86
86
|
assert.ok(prompt.includes(TID), "must mention task id");
|
|
87
87
|
assert.ok(prompt.includes(T_TITLE), "must mention task title");
|
|
88
88
|
assert.ok(
|
|
89
|
-
prompt.includes("
|
|
90
|
-
"must instruct calling the canonical
|
|
89
|
+
prompt.includes("gsd_task_complete"),
|
|
90
|
+
"must instruct calling the canonical gsd_task_complete tool",
|
|
91
91
|
);
|
|
92
92
|
assert.ok(
|
|
93
93
|
prompt.includes(base),
|
|
@@ -110,8 +110,8 @@ describe("guided-flow → auto-prompts consolidation (#5183)", () => {
|
|
|
110
110
|
assert.ok(prompt.includes(SID), "must mention slice id");
|
|
111
111
|
assert.ok(prompt.includes(S_TITLE), "must mention slice title");
|
|
112
112
|
assert.ok(
|
|
113
|
-
prompt.includes("
|
|
114
|
-
"must instruct calling
|
|
113
|
+
prompt.includes("gsd_slice_complete"),
|
|
114
|
+
"must instruct calling gsd_slice_complete (was in guided-complete-slice.md)",
|
|
115
115
|
);
|
|
116
116
|
assert.ok(
|
|
117
117
|
prompt.includes(base),
|
|
@@ -160,7 +160,7 @@ describe("auto-worktree lifecycle", () => {
|
|
|
160
160
|
assert.ok(realWtPath.startsWith(storage), "git registered the symlink-resolved worktree path");
|
|
161
161
|
|
|
162
162
|
_resetAutoWorktreeOriginalBaseForTests();
|
|
163
|
-
process.chdir(
|
|
163
|
+
process.chdir(realWtPath);
|
|
164
164
|
|
|
165
165
|
assert.ok(isInAutoWorktree(tempDir), "structural detection works without module originalBase");
|
|
166
166
|
const resolved = getAutoWorktreePath(realWtPath, "M001");
|
|
@@ -169,7 +169,7 @@ describe("auto-worktree lifecycle", () => {
|
|
|
169
169
|
assert.equal(existsSync(join(realWtPath, ".gsd", "worktrees", "M001")), false);
|
|
170
170
|
|
|
171
171
|
enterAutoWorktree(tempDir, "M001");
|
|
172
|
-
process.chdir(
|
|
172
|
+
process.chdir(realWtPath);
|
|
173
173
|
assert.deepStrictEqual(
|
|
174
174
|
getActiveAutoWorktreeContext(),
|
|
175
175
|
{
|
|
@@ -282,7 +282,7 @@ describe("auto-worktree lifecycle", () => {
|
|
|
282
282
|
teardownAutoWorktree(tempDir, "M010");
|
|
283
283
|
});
|
|
284
284
|
|
|
285
|
-
test("#778: reconcile plan checkboxes
|
|
285
|
+
test("#778: re-attach does not reconcile plan checkboxes into a worktree-local .gsd projection", async () => {
|
|
286
286
|
tempDir = createTempRepo();
|
|
287
287
|
const msDir = join(tempDir, ".gsd", "milestones", "M003");
|
|
288
288
|
mkdirSync(msDir, { recursive: true });
|
|
@@ -322,23 +322,31 @@ describe("auto-worktree lifecycle", () => {
|
|
|
322
322
|
"# S01 Plan\n- [x] **T01:** task one\n- [x] **T02:** task two\n- [ ] **T03:** task three\n",
|
|
323
323
|
);
|
|
324
324
|
|
|
325
|
-
//
|
|
325
|
+
// Re-attaching the worktree should not reconcile the branch copy from the
|
|
326
|
+
// project-root plan. The project root stays canonical; the worktree keeps
|
|
327
|
+
// the milestone branch's tracked file contents until a git operation
|
|
328
|
+
// changes them.
|
|
326
329
|
const wtPath = createAutoWorktree(tempDir, "M004");
|
|
327
330
|
|
|
328
331
|
try {
|
|
329
332
|
const wtPlanPath = join(wtPath, planRelPath);
|
|
330
|
-
assert.ok(existsSync(wtPlanPath), "plan file
|
|
333
|
+
assert.ok(existsSync(wtPlanPath), "tracked plan file remains present in the worktree branch");
|
|
331
334
|
|
|
332
335
|
const wtPlan = read(wtPlanPath, "utf-8");
|
|
333
|
-
assert.ok(wtPlan.includes("- [
|
|
334
|
-
assert.ok(wtPlan.includes("- [x] **T01:"), "T01
|
|
335
|
-
assert.ok(wtPlan.includes("- [ ] **T03:"), "
|
|
336
|
+
assert.ok(wtPlan.includes("- [ ] **T02:"), "worktree branch should retain its unreconciled T02 [ ] state");
|
|
337
|
+
assert.ok(wtPlan.includes("- [x] **T01:"), "worktree branch should retain T01 [x]");
|
|
338
|
+
assert.ok(wtPlan.includes("- [ ] **T03:"), "worktree branch should retain T03 [ ]");
|
|
339
|
+
|
|
340
|
+
const rootPlan = read(join(tempDir, planRelPath), "utf-8");
|
|
341
|
+
assert.ok(rootPlan.includes("- [x] **T02:"), "canonical root plan retains the newer T02 [x] state");
|
|
342
|
+
assert.ok(rootPlan.includes("- [x] **T01:"), "canonical root plan retains T01 [x]");
|
|
343
|
+
assert.ok(rootPlan.includes("- [ ] **T03:"), "canonical root plan retains T03 [ ]");
|
|
336
344
|
} finally {
|
|
337
345
|
teardownAutoWorktree(tempDir, "M004");
|
|
338
346
|
}
|
|
339
347
|
});
|
|
340
348
|
|
|
341
|
-
test("#2791: mcp.json copied into worktree
|
|
349
|
+
test("#2791: mcp.json is not copied into worktree on creation after copyPlanningArtifacts removal", () => {
|
|
342
350
|
tempDir = createTempRepo();
|
|
343
351
|
const msDir = join(tempDir, ".gsd", "milestones", "M003");
|
|
344
352
|
mkdirSync(msDir, { recursive: true });
|
|
@@ -347,7 +355,8 @@ describe("auto-worktree lifecycle", () => {
|
|
|
347
355
|
run("git commit -m \"add milestone\"", tempDir);
|
|
348
356
|
|
|
349
357
|
// Create mcp.json in .gsd/ AFTER the commit (untracked, like real usage).
|
|
350
|
-
// copyPlanningArtifacts
|
|
358
|
+
// Phase C removed copyPlanningArtifacts, so creation should not seed a
|
|
359
|
+
// second worktree-local copy.
|
|
351
360
|
writeFileSync(
|
|
352
361
|
join(tempDir, ".gsd", "mcp.json"),
|
|
353
362
|
JSON.stringify({ servers: { test: { command: "echo" } } }),
|
|
@@ -356,9 +365,10 @@ describe("auto-worktree lifecycle", () => {
|
|
|
356
365
|
const wtPath = createAutoWorktree(tempDir, "M003");
|
|
357
366
|
|
|
358
367
|
try {
|
|
359
|
-
assert.
|
|
368
|
+
assert.equal(
|
|
360
369
|
existsSync(join(wtPath, ".gsd", "mcp.json")),
|
|
361
|
-
|
|
370
|
+
false,
|
|
371
|
+
"mcp.json should not be copied into worktree .gsd/ on creation",
|
|
362
372
|
);
|
|
363
373
|
} finally {
|
|
364
374
|
teardownAutoWorktree(tempDir, "M003");
|
|
@@ -240,17 +240,31 @@ describe('doctor-proactive', async () => {
|
|
|
240
240
|
cleanups.push(dir);
|
|
241
241
|
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
242
242
|
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
})
|
|
243
|
+
// Phase C pt 2: stale lock state lives in the workers table now.
|
|
244
|
+
// Open the DB, insert a fake stale worker row directly (PID 9999999
|
|
245
|
+
// is functionally guaranteed dead), then close — the doctor will
|
|
246
|
+
// re-open via its own path.
|
|
247
|
+
const { openDatabase, _getAdapter } = await import("../../gsd-db.ts");
|
|
248
|
+
const { randomUUID } = await import("node:crypto");
|
|
249
|
+
openDatabase(join(dir, ".gsd", "gsd.db"));
|
|
250
|
+
const db = _getAdapter()!;
|
|
251
|
+
db.prepare(
|
|
252
|
+
`INSERT INTO workers (worker_id, host, pid, started_at, version, last_heartbeat_at, status, project_root_realpath)
|
|
253
|
+
VALUES (:w, 'test-host', 9999999, '2026-03-10T00:00:00Z', 'test', '1970-01-01T00:00:00.000Z', 'active', :root)`,
|
|
254
|
+
).run({ ":w": `test-fake-${randomUUID().slice(0, 8)}`, ":root": dir });
|
|
255
|
+
const { closeDatabase } = await import("../../gsd-db.ts");
|
|
256
|
+
closeDatabase();
|
|
249
257
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
258
|
+
try {
|
|
259
|
+
const result = await preDispatchHealthGate(dir);
|
|
260
|
+
assert.ok(result.proceed, "gate passes after auto-clearing stale lock");
|
|
261
|
+
assert.ok(
|
|
262
|
+
result.fixesApplied.some(f => f.includes("cleared stale") || f.includes("cleared stale auto.lock")),
|
|
263
|
+
`reports lock cleared (got: ${result.fixesApplied.join(", ")})`,
|
|
264
|
+
);
|
|
265
|
+
} finally {
|
|
266
|
+
closeDatabase();
|
|
267
|
+
}
|
|
254
268
|
});
|
|
255
269
|
|
|
256
270
|
test('health gate: corrupt merge state auto-healed', async () => {
|
|
@@ -60,20 +60,24 @@ describe('doctor-runtime', async () => {
|
|
|
60
60
|
|
|
61
61
|
try {
|
|
62
62
|
// ─── Test 1: Stale crash lock detection & fix ─────────────────────
|
|
63
|
-
test('stale_crash_lock', async () => {
|
|
63
|
+
test('stale_crash_lock', async (t) => {
|
|
64
64
|
const dir = createMinimalProject();
|
|
65
65
|
cleanups.push(dir);
|
|
66
66
|
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
// Phase C pt 2: stale lock state lives in the workers table now.
|
|
68
|
+
// Insert a fake stale worker row directly (PID 9999999 is dead).
|
|
69
|
+
const { openDatabase, _getAdapter } = await import("../../gsd-db.ts");
|
|
70
|
+
const gsdDb = await import("../../gsd-db.ts");
|
|
71
|
+
t.after(() => { gsdDb.closeDatabase(); });
|
|
72
|
+
const { randomUUID } = await import("node:crypto");
|
|
73
|
+
openDatabase(join(dir, ".gsd", "gsd.db"));
|
|
74
|
+
const db = _getAdapter()!;
|
|
75
|
+
db.prepare(
|
|
76
|
+
`INSERT INTO workers (worker_id, host, pid, started_at, version, last_heartbeat_at, status, project_root_realpath)
|
|
77
|
+
VALUES (:w, 'test-host', 9999999, '2026-03-10T00:00:00Z', 'test', '1970-01-01T00:00:00.000Z', 'active', :root)`,
|
|
78
|
+
).run({ ":w": `test-fake-${randomUUID().slice(0, 8)}`, ":root": dir });
|
|
79
|
+
// Leave DB open — runGSDDoctor's readCrashLock relies on the
|
|
80
|
+
// currently-open DB connection (it does not open one of its own).
|
|
77
81
|
|
|
78
82
|
const detect = await runGSDDoctor(dir);
|
|
79
83
|
const lockIssues = detect.issues.filter(i => i.code === "stale_crash_lock");
|
|
@@ -82,8 +86,15 @@ describe('doctor-runtime', async () => {
|
|
|
82
86
|
assert.ok(lockIssues[0]?.fixable === true, "stale lock is fixable");
|
|
83
87
|
|
|
84
88
|
const fixed = await runGSDDoctor(dir, { fix: true });
|
|
85
|
-
assert.ok(
|
|
86
|
-
|
|
89
|
+
assert.ok(
|
|
90
|
+
fixed.fixesApplied.some(f => f.includes("cleared stale")),
|
|
91
|
+
`fix clears stale lock (got: ${fixed.fixesApplied.join(", ")})`,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Close DB so subsequent tests in this file (which expect a clean
|
|
95
|
+
// state) don't see this test's connection lingering.
|
|
96
|
+
const { closeDatabase } = await import("../../gsd-db.ts");
|
|
97
|
+
closeDatabase();
|
|
87
98
|
});
|
|
88
99
|
|
|
89
100
|
// ─── Test 2: No false positive for missing lock ───────────────────
|
|
@@ -417,18 +428,19 @@ node_modules/
|
|
|
417
428
|
const dir = createMinimalProject();
|
|
418
429
|
cleanups.push(dir);
|
|
419
430
|
|
|
420
|
-
// Create lock dir +
|
|
431
|
+
// Create lock dir + insert a live worker row (PID 1 = init/launchd —
|
|
432
|
+
// always alive, never our own PID). Phase C pt 2: worker liveness
|
|
433
|
+
// lives in the workers table. last_heartbeat_at = now → not stale.
|
|
421
434
|
const lockDir = join(dir, ".gsd.lock");
|
|
422
435
|
mkdirSync(lockDir, { recursive: true });
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
};
|
|
431
|
-
writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify(liveLockData, null, 2));
|
|
436
|
+
const { openDatabase, _getAdapter } = await import("../../gsd-db.ts");
|
|
437
|
+
const { randomUUID } = await import("node:crypto");
|
|
438
|
+
openDatabase(join(dir, ".gsd", "gsd.db"));
|
|
439
|
+
const db = _getAdapter()!;
|
|
440
|
+
db.prepare(
|
|
441
|
+
`INSERT INTO workers (worker_id, host, pid, started_at, version, last_heartbeat_at, status, project_root_realpath)
|
|
442
|
+
VALUES (:w, 'test-host', 1, :now, 'test', :now, 'active', :root)`,
|
|
443
|
+
).run({ ":w": `test-fake-${randomUUID().slice(0, 8)}`, ":now": new Date().toISOString(), ":root": dir });
|
|
432
444
|
|
|
433
445
|
const detect = await runGSDDoctor(dir);
|
|
434
446
|
const strandedIssues = detect.issues.filter(i => i.code === "stranded_lock_directory");
|