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,175 @@
|
|
|
1
|
+
// GSD-2 + Unified path normalization tests: normalizeRealPath and tryRealpath parity
|
|
2
|
+
|
|
3
|
+
import { describe, test, before, 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 { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
|
|
16
|
+
import { normalizeRealPath } from "../paths.ts";
|
|
17
|
+
import { createWorkspace } from "../workspace.ts";
|
|
18
|
+
|
|
19
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Detect whether the filesystem hosting `dir` is case-insensitive.
|
|
23
|
+
* Creates a file with a lowercase name, then probes it via an uppercase name.
|
|
24
|
+
*/
|
|
25
|
+
function isCaseInsensitiveFs(dir: string): boolean {
|
|
26
|
+
const lower = join(dir, "ci_probe_lower.txt");
|
|
27
|
+
const upper = join(dir, "CI_PROBE_LOWER.TXT");
|
|
28
|
+
try {
|
|
29
|
+
writeFileSync(lower, "probe");
|
|
30
|
+
return existsSync(upper);
|
|
31
|
+
} finally {
|
|
32
|
+
try { rmSync(lower); } catch { /* ignore */ }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makeProjectDir(): string {
|
|
37
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-pathnorm-")));
|
|
38
|
+
mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
|
|
39
|
+
return dir;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── Suite 1: normalizeRealPath and tryRealpath return identical strings ──────
|
|
43
|
+
|
|
44
|
+
describe("normalizeRealPath and tryRealpath produce identical results", () => {
|
|
45
|
+
let projectDir: string;
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
projectDir = makeProjectDir();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("normalizeRealPath on an existing directory matches realpathSync.native", () => {
|
|
56
|
+
const result = normalizeRealPath(projectDir);
|
|
57
|
+
// realpathSync.native is the canonical form — result must equal it
|
|
58
|
+
const expected = realpathSync.native(projectDir);
|
|
59
|
+
assert.equal(result, expected);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("createWorkspace identityKey equals normalizeRealPath of projectRoot", () => {
|
|
63
|
+
const ws = createWorkspace(projectDir);
|
|
64
|
+
// identityKey is computed via tryRealpath → normalizeRealPath
|
|
65
|
+
assert.equal(ws.identityKey, normalizeRealPath(ws.projectRoot));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("createWorkspace identityKey and normalizeRealPath agree on the same input", () => {
|
|
69
|
+
const ws = createWorkspace(projectDir);
|
|
70
|
+
const direct = normalizeRealPath(projectDir);
|
|
71
|
+
assert.equal(ws.identityKey, direct);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ─── Suite 2: non-existent path fallback ─────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
describe("normalizeRealPath: fallback for non-existent paths", () => {
|
|
78
|
+
test("returns a resolved (absolute) path for a non-existent input", () => {
|
|
79
|
+
const ghost = join(tmpdir(), "gsd-pathnorm-ghost-does-not-exist-" + Date.now());
|
|
80
|
+
const result = normalizeRealPath(ghost);
|
|
81
|
+
// Must be a string, must be absolute, must not throw
|
|
82
|
+
assert.equal(typeof result, "string");
|
|
83
|
+
assert.ok(result.startsWith("/") || /^[A-Za-z]:/.test(result), "result must be absolute");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("normalizeRealPath of a non-existent path is idempotent", () => {
|
|
87
|
+
const ghost = join(tmpdir(), "gsd-pathnorm-ghost2-" + Date.now());
|
|
88
|
+
const first = normalizeRealPath(ghost);
|
|
89
|
+
const second = normalizeRealPath(first);
|
|
90
|
+
assert.equal(first, second);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ─── Suite 3: idempotency on existing paths ───────────────────────────────────
|
|
95
|
+
|
|
96
|
+
describe("normalizeRealPath: idempotency on existing paths", () => {
|
|
97
|
+
let projectDir: string;
|
|
98
|
+
|
|
99
|
+
beforeEach(() => {
|
|
100
|
+
projectDir = makeProjectDir();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
afterEach(() => {
|
|
104
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("normalizeRealPath of a normalizeRealPath result is the same", () => {
|
|
108
|
+
const once = normalizeRealPath(projectDir);
|
|
109
|
+
const twice = normalizeRealPath(once);
|
|
110
|
+
assert.equal(once, twice);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("createWorkspace identityKey is idempotent across two calls with same path", () => {
|
|
114
|
+
const ws1 = createWorkspace(projectDir);
|
|
115
|
+
const ws2 = createWorkspace(projectDir);
|
|
116
|
+
assert.equal(ws1.identityKey, ws2.identityKey);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ─── Suite 4: case-insensitive filesystem (macOS HFS+/APFS) ──────────────────
|
|
121
|
+
|
|
122
|
+
describe("normalizeRealPath: case normalization on case-insensitive volumes", () => {
|
|
123
|
+
let projectDir: string;
|
|
124
|
+
let caseInsensitive: boolean;
|
|
125
|
+
|
|
126
|
+
before(() => {
|
|
127
|
+
// Detect FS case sensitivity once for the suite
|
|
128
|
+
const probe = mkdtempSync(join(tmpdir(), "gsd-ci-probe-"));
|
|
129
|
+
caseInsensitive = isCaseInsensitiveFs(probe);
|
|
130
|
+
rmSync(probe, { recursive: true, force: true });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
beforeEach(() => {
|
|
134
|
+
projectDir = makeProjectDir();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
afterEach(() => {
|
|
138
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("upper and lower case paths resolve to the same canonical string on case-insensitive FS", (t) => {
|
|
142
|
+
if (!caseInsensitive) {
|
|
143
|
+
t.skip("case-sensitive filesystem — case-normalization check not applicable");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const lower = projectDir.toLowerCase();
|
|
148
|
+
const upper = projectDir.toUpperCase();
|
|
149
|
+
|
|
150
|
+
const canonicalFromLower = normalizeRealPath(lower);
|
|
151
|
+
const canonicalFromUpper = normalizeRealPath(upper);
|
|
152
|
+
|
|
153
|
+
assert.equal(
|
|
154
|
+
canonicalFromLower,
|
|
155
|
+
canonicalFromUpper,
|
|
156
|
+
"both case variants must resolve to the same canonical path on case-insensitive FS",
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("createWorkspace identityKey is case-stable on case-insensitive FS", (t) => {
|
|
161
|
+
if (!caseInsensitive) {
|
|
162
|
+
t.skip("case-sensitive filesystem — case-normalization check not applicable");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const wsLower = createWorkspace(projectDir.toLowerCase());
|
|
167
|
+
const wsUpper = createWorkspace(projectDir.toUpperCase());
|
|
168
|
+
|
|
169
|
+
assert.equal(
|
|
170
|
+
wsLower.identityKey,
|
|
171
|
+
wsUpper.identityKey,
|
|
172
|
+
"identityKey must be identical regardless of input case on case-insensitive FS",
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// GSD2 — paths cache normalization and clearPathCache() invalidation tests
|
|
2
|
+
|
|
3
|
+
import { describe, test, beforeEach, afterEach } from 'node:test';
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import { mkdtempSync, mkdirSync, rmSync, renameSync, realpathSync } from 'node:fs';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
import { gsdRoot, clearPathCache, _clearGsdRootCache } from '../paths.ts';
|
|
10
|
+
|
|
11
|
+
describe('gsdRootCache key normalization', () => {
|
|
12
|
+
let projectDir: string;
|
|
13
|
+
let fakeHome: string;
|
|
14
|
+
let savedHome: string | undefined;
|
|
15
|
+
let savedUserProfile: string | undefined;
|
|
16
|
+
let savedGsdHome: string | undefined;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-cache-norm-')));
|
|
20
|
+
mkdirSync(join(projectDir, '.gsd'), { recursive: true });
|
|
21
|
+
|
|
22
|
+
fakeHome = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-cache-home-')));
|
|
23
|
+
|
|
24
|
+
savedHome = process.env.HOME;
|
|
25
|
+
savedUserProfile = process.env.USERPROFILE;
|
|
26
|
+
savedGsdHome = process.env.GSD_HOME;
|
|
27
|
+
|
|
28
|
+
// Point HOME and GSD_HOME at an unrelated temp dir to prevent ~/.gsd interference.
|
|
29
|
+
process.env.HOME = fakeHome;
|
|
30
|
+
process.env.USERPROFILE = fakeHome;
|
|
31
|
+
process.env.GSD_HOME = join(fakeHome, '.gsd');
|
|
32
|
+
|
|
33
|
+
_clearGsdRootCache();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
if (savedHome === undefined) delete process.env.HOME;
|
|
38
|
+
else process.env.HOME = savedHome;
|
|
39
|
+
if (savedUserProfile === undefined) delete process.env.USERPROFILE;
|
|
40
|
+
else process.env.USERPROFILE = savedUserProfile;
|
|
41
|
+
if (savedGsdHome === undefined) delete process.env.GSD_HOME;
|
|
42
|
+
else process.env.GSD_HOME = savedGsdHome;
|
|
43
|
+
|
|
44
|
+
clearPathCache();
|
|
45
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
46
|
+
rmSync(fakeHome, { recursive: true, force: true });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('gsdRoot with trailing slash returns same result as without', () => {
|
|
50
|
+
const withoutSlash = gsdRoot(projectDir);
|
|
51
|
+
_clearGsdRootCache();
|
|
52
|
+
const withSlash = gsdRoot(projectDir + '/');
|
|
53
|
+
|
|
54
|
+
assert.equal(
|
|
55
|
+
withoutSlash,
|
|
56
|
+
withSlash,
|
|
57
|
+
'gsdRoot must return the same path regardless of trailing slash',
|
|
58
|
+
);
|
|
59
|
+
assert.equal(
|
|
60
|
+
withoutSlash,
|
|
61
|
+
join(projectDir, '.gsd'),
|
|
62
|
+
'both calls should resolve to projectDir/.gsd',
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('second call with trailing slash hits cache set by first call without slash', () => {
|
|
67
|
+
// Prime the cache with the no-slash form.
|
|
68
|
+
const first = gsdRoot(projectDir);
|
|
69
|
+
// Now remove .gsd so a fresh probe would return a different path.
|
|
70
|
+
renameSync(join(projectDir, '.gsd'), join(projectDir, '.gsd-hidden'));
|
|
71
|
+
// Call with trailing slash — must hit the normalized cache entry (no re-probe).
|
|
72
|
+
const second = gsdRoot(projectDir + '/');
|
|
73
|
+
// Restore for cleanup.
|
|
74
|
+
renameSync(join(projectDir, '.gsd-hidden'), join(projectDir, '.gsd'));
|
|
75
|
+
|
|
76
|
+
assert.equal(
|
|
77
|
+
second,
|
|
78
|
+
first,
|
|
79
|
+
'trailing-slash call must return cached result from the no-slash call',
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('clearPathCache() does NOT invalidate gsdRootCache (process-lifetime semantics)', () => {
|
|
85
|
+
let projectDir: string;
|
|
86
|
+
let fakeHome: string;
|
|
87
|
+
let savedHome: string | undefined;
|
|
88
|
+
let savedUserProfile: string | undefined;
|
|
89
|
+
let savedGsdHome: string | undefined;
|
|
90
|
+
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-cache-clear-')));
|
|
93
|
+
mkdirSync(join(projectDir, '.gsd'), { recursive: true });
|
|
94
|
+
|
|
95
|
+
fakeHome = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-cache-home2-')));
|
|
96
|
+
|
|
97
|
+
savedHome = process.env.HOME;
|
|
98
|
+
savedUserProfile = process.env.USERPROFILE;
|
|
99
|
+
savedGsdHome = process.env.GSD_HOME;
|
|
100
|
+
|
|
101
|
+
process.env.HOME = fakeHome;
|
|
102
|
+
process.env.USERPROFILE = fakeHome;
|
|
103
|
+
process.env.GSD_HOME = join(fakeHome, '.gsd');
|
|
104
|
+
|
|
105
|
+
_clearGsdRootCache();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
afterEach(() => {
|
|
109
|
+
if (savedHome === undefined) delete process.env.HOME;
|
|
110
|
+
else process.env.HOME = savedHome;
|
|
111
|
+
if (savedUserProfile === undefined) delete process.env.USERPROFILE;
|
|
112
|
+
else process.env.USERPROFILE = savedUserProfile;
|
|
113
|
+
if (savedGsdHome === undefined) delete process.env.GSD_HOME;
|
|
114
|
+
else process.env.GSD_HOME = savedGsdHome;
|
|
115
|
+
|
|
116
|
+
_clearGsdRootCache();
|
|
117
|
+
clearPathCache();
|
|
118
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
119
|
+
rmSync(fakeHome, { recursive: true, force: true });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('clearPathCache() does NOT evict a cached gsdRoot result', (t) => {
|
|
123
|
+
// Prime the cache.
|
|
124
|
+
const firstResult = gsdRoot(projectDir);
|
|
125
|
+
assert.equal(firstResult, join(projectDir, '.gsd'));
|
|
126
|
+
|
|
127
|
+
// Remove .gsd so a fresh probe would return a different (fallback) result.
|
|
128
|
+
renameSync(join(projectDir, '.gsd'), join(projectDir, '.gsd-hidden'));
|
|
129
|
+
t.after(() => {
|
|
130
|
+
try { renameSync(join(projectDir, '.gsd-hidden'), join(projectDir, '.gsd')); } catch { /* ignore */ }
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// clearPathCache() only clears volatile dir caches — gsdRootCache is untouched.
|
|
134
|
+
clearPathCache();
|
|
135
|
+
const afterClearPath = gsdRoot(projectDir);
|
|
136
|
+
assert.equal(
|
|
137
|
+
afterClearPath,
|
|
138
|
+
firstResult,
|
|
139
|
+
'clearPathCache must NOT evict gsdRootCache — result must still be the cached value',
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('_clearGsdRootCache() DOES evict gsdRootCache, causing re-probe', (t) => {
|
|
144
|
+
// Prime the cache.
|
|
145
|
+
const firstResult = gsdRoot(projectDir);
|
|
146
|
+
assert.equal(firstResult, join(projectDir, '.gsd'));
|
|
147
|
+
|
|
148
|
+
// Remove .gsd so a fresh probe returns the creation fallback.
|
|
149
|
+
renameSync(join(projectDir, '.gsd'), join(projectDir, '.gsd-hidden'));
|
|
150
|
+
t.after(() => {
|
|
151
|
+
try { renameSync(join(projectDir, '.gsd-hidden'), join(projectDir, '.gsd')); } catch { /* ignore */ }
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// _clearGsdRootCache() evicts the entry — next call re-probes.
|
|
155
|
+
_clearGsdRootCache();
|
|
156
|
+
const afterClearRoot = gsdRoot(projectDir);
|
|
157
|
+
assert.equal(
|
|
158
|
+
afterClearRoot,
|
|
159
|
+
join(projectDir, '.gsd'),
|
|
160
|
+
'after _clearGsdRootCache, gsdRoot must re-probe and return creation fallback',
|
|
161
|
+
);
|
|
162
|
+
// The two results are equal (same path) but the key point is re-probe occurred;
|
|
163
|
+
// the cached firstResult also happened to equal the fallback path.
|
|
164
|
+
// Verify: if we prime again without removing .gsd, clearing root re-probes to gsd.
|
|
165
|
+
renameSync(join(projectDir, '.gsd-hidden'), join(projectDir, '.gsd'));
|
|
166
|
+
_clearGsdRootCache();
|
|
167
|
+
const reprobe = gsdRoot(projectDir);
|
|
168
|
+
assert.equal(reprobe, join(projectDir, '.gsd'), 're-probe after restore returns .gsd');
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// gsd-2 + Paused-session via runtime_kv (Phase C pt 2 — paused-session.json migration)
|
|
2
|
+
//
|
|
3
|
+
// runtime/paused-session.json is gone. The metadata that the old file
|
|
4
|
+
// stored now lives in runtime_kv (global scope, key PAUSED_SESSION_KV_KEY).
|
|
5
|
+
// readPausedSessionMetadata reads the key; the writer in pauseAuto +
|
|
6
|
+
// the cleanup in stopAuto/startAuto/guided-flow all use the same key.
|
|
7
|
+
//
|
|
8
|
+
// These tests verify the round-trip via the storage layer directly.
|
|
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
|
+
} from "../gsd-db.ts";
|
|
20
|
+
import {
|
|
21
|
+
setRuntimeKv,
|
|
22
|
+
getRuntimeKv,
|
|
23
|
+
deleteRuntimeKv,
|
|
24
|
+
} from "../db/runtime-kv.ts";
|
|
25
|
+
import {
|
|
26
|
+
readPausedSessionMetadata,
|
|
27
|
+
PAUSED_SESSION_KV_KEY,
|
|
28
|
+
type PausedSessionMetadata,
|
|
29
|
+
} from "../interrupted-session.ts";
|
|
30
|
+
|
|
31
|
+
function makeBase(): string {
|
|
32
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-paused-session-"));
|
|
33
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
34
|
+
return base;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function cleanup(base: string): void {
|
|
38
|
+
try { closeDatabase(); } catch { /* noop */ }
|
|
39
|
+
try { rmSync(base, { recursive: true, force: true }); } catch { /* noop */ }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test("readPausedSessionMetadata returns null when no row exists", (t) => {
|
|
43
|
+
const base = makeBase();
|
|
44
|
+
t.after(() => cleanup(base));
|
|
45
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
46
|
+
assert.equal(readPausedSessionMetadata(base), null);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("readPausedSessionMetadata round-trips a real PausedSessionMetadata payload", (t) => {
|
|
50
|
+
const base = makeBase();
|
|
51
|
+
t.after(() => cleanup(base));
|
|
52
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
53
|
+
|
|
54
|
+
const meta: PausedSessionMetadata = {
|
|
55
|
+
milestoneId: "M001",
|
|
56
|
+
worktreePath: "/tmp/wt",
|
|
57
|
+
originalBasePath: base,
|
|
58
|
+
stepMode: false,
|
|
59
|
+
pausedAt: new Date().toISOString(),
|
|
60
|
+
sessionFile: "/tmp/session.jsonl",
|
|
61
|
+
unitType: "plan-slice",
|
|
62
|
+
unitId: "M001/S01",
|
|
63
|
+
activeEngineId: "dev",
|
|
64
|
+
activeRunDir: null,
|
|
65
|
+
autoStartTime: Date.now(),
|
|
66
|
+
milestoneLock: null,
|
|
67
|
+
};
|
|
68
|
+
setRuntimeKv("global", "", PAUSED_SESSION_KV_KEY, meta);
|
|
69
|
+
|
|
70
|
+
const loaded = readPausedSessionMetadata(base);
|
|
71
|
+
assert.ok(loaded);
|
|
72
|
+
assert.equal(loaded!.milestoneId, "M001");
|
|
73
|
+
assert.equal(loaded!.unitType, "plan-slice");
|
|
74
|
+
assert.equal(loaded!.unitId, "M001/S01");
|
|
75
|
+
assert.equal(loaded!.sessionFile, "/tmp/session.jsonl");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("readPausedSessionMetadata auto-deletes stale pseudo-milestone pause rows", (t) => {
|
|
79
|
+
const base = makeBase();
|
|
80
|
+
t.after(() => cleanup(base));
|
|
81
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
82
|
+
|
|
83
|
+
// discuss-milestone with a non-MID-shaped unitId triggers
|
|
84
|
+
// isStalePseudoMilestonePause → returns null + deletes the row.
|
|
85
|
+
const stale: PausedSessionMetadata = {
|
|
86
|
+
milestoneId: "M001",
|
|
87
|
+
unitType: "discuss-milestone",
|
|
88
|
+
unitId: "PROJECT-thing",
|
|
89
|
+
activeEngineId: "dev",
|
|
90
|
+
};
|
|
91
|
+
setRuntimeKv("global", "", PAUSED_SESSION_KV_KEY, stale);
|
|
92
|
+
|
|
93
|
+
assert.equal(readPausedSessionMetadata(base), null);
|
|
94
|
+
assert.equal(getRuntimeKv("global", "", PAUSED_SESSION_KV_KEY), null,
|
|
95
|
+
"stale row was deleted by readPausedSessionMetadata");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("deleteRuntimeKv on PAUSED_SESSION_KV_KEY removes the row idempotently", (t) => {
|
|
99
|
+
const base = makeBase();
|
|
100
|
+
t.after(() => cleanup(base));
|
|
101
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
102
|
+
|
|
103
|
+
setRuntimeKv("global", "", PAUSED_SESSION_KV_KEY, { milestoneId: "M001" });
|
|
104
|
+
deleteRuntimeKv("global", "", PAUSED_SESSION_KV_KEY);
|
|
105
|
+
deleteRuntimeKv("global", "", PAUSED_SESSION_KV_KEY); // idempotent — no throw
|
|
106
|
+
assert.equal(readPausedSessionMetadata(base), null);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("readPausedSessionMetadata returns null when DB is unavailable", () => {
|
|
110
|
+
// No openDatabase call — DB is closed.
|
|
111
|
+
try { closeDatabase(); } catch { /* noop */ }
|
|
112
|
+
// Use a tmpdir-style base; the function should handle DB-unavailable gracefully.
|
|
113
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-paused-no-db-"));
|
|
114
|
+
try {
|
|
115
|
+
assert.equal(readPausedSessionMetadata(base), null);
|
|
116
|
+
} finally {
|
|
117
|
+
rmSync(base, { recursive: true, force: true });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// GSD-2 + Tests for MilestoneScope pinning in pendingAutoStartMap (C1 regression guard)
|
|
2
|
+
|
|
3
|
+
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { mkdtempSync, mkdirSync, rmSync, realpathSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
setPendingAutoStart,
|
|
11
|
+
clearPendingAutoStart,
|
|
12
|
+
_getPendingAutoStart,
|
|
13
|
+
} from "../guided-flow.ts";
|
|
14
|
+
|
|
15
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
function makeProjectDir(): string {
|
|
18
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-pas-scope-")));
|
|
19
|
+
mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
|
|
20
|
+
return dir;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
describe("pendingAutoStart scope pinning (C1)", () => {
|
|
26
|
+
let base: string;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
clearPendingAutoStart();
|
|
30
|
+
base = makeProjectDir();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
clearPendingAutoStart();
|
|
35
|
+
if (base) {
|
|
36
|
+
rmSync(base, { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("setPendingAutoStart stores a scope whose paths derive from the basePath at reservation time", () => {
|
|
41
|
+
setPendingAutoStart(base, { basePath: base, milestoneId: "M001" });
|
|
42
|
+
|
|
43
|
+
const entry = _getPendingAutoStart(base);
|
|
44
|
+
assert.ok(entry, "entry should exist");
|
|
45
|
+
assert.ok(entry.scope, "entry.scope should be set");
|
|
46
|
+
assert.equal(entry.scope.milestoneId, "M001");
|
|
47
|
+
|
|
48
|
+
const expectedContext = join(base, ".gsd", "milestones", "M001", "M001-CONTEXT.md");
|
|
49
|
+
const expectedRoadmap = join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md");
|
|
50
|
+
const expectedState = join(base, ".gsd", "STATE.md");
|
|
51
|
+
|
|
52
|
+
assert.equal(entry.scope.contextFile(), expectedContext);
|
|
53
|
+
assert.equal(entry.scope.roadmapFile(), expectedRoadmap);
|
|
54
|
+
assert.equal(entry.scope.stateFile(), expectedState);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("scope paths are unaffected by process.chdir after reservation", (t) => {
|
|
58
|
+
setPendingAutoStart(base, { basePath: base, milestoneId: "M002" });
|
|
59
|
+
|
|
60
|
+
const entry = _getPendingAutoStart(base);
|
|
61
|
+
assert.ok(entry, "entry should exist");
|
|
62
|
+
|
|
63
|
+
// Capture paths before cwd change
|
|
64
|
+
const ctxBefore = entry.scope.contextFile();
|
|
65
|
+
const roadmapBefore = entry.scope.roadmapFile();
|
|
66
|
+
const stateBefore = entry.scope.stateFile();
|
|
67
|
+
|
|
68
|
+
// Change cwd to a different directory, then check that scope is unchanged
|
|
69
|
+
const originalCwd = process.cwd();
|
|
70
|
+
const altDir = mkdtempSync(join(tmpdir(), "gsd-cwd-alt-"));
|
|
71
|
+
t.after(() => {
|
|
72
|
+
process.chdir(originalCwd);
|
|
73
|
+
rmSync(altDir, { recursive: true, force: true });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
process.chdir(altDir);
|
|
77
|
+
|
|
78
|
+
assert.equal(entry.scope.contextFile(), ctxBefore, "contextFile must not change after cwd drift");
|
|
79
|
+
assert.equal(entry.scope.roadmapFile(), roadmapBefore, "roadmapFile must not change after cwd drift");
|
|
80
|
+
assert.equal(entry.scope.stateFile(), stateBefore, "stateFile must not change after cwd drift");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("scope identityKey matches the realpath of the original basePath even with trailing slash", () => {
|
|
84
|
+
const baseWithSlash = base + "/";
|
|
85
|
+
setPendingAutoStart(base, { basePath: baseWithSlash, milestoneId: "M003" });
|
|
86
|
+
|
|
87
|
+
const entry = _getPendingAutoStart(base);
|
|
88
|
+
assert.ok(entry, "entry should exist");
|
|
89
|
+
|
|
90
|
+
const expectedIdentityKey = realpathSync(base);
|
|
91
|
+
assert.equal(
|
|
92
|
+
entry.scope.workspace.identityKey,
|
|
93
|
+
expectedIdentityKey,
|
|
94
|
+
"identityKey must match realpath of the original (non-canonical) basePath",
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("clearPendingAutoStart removes the entry", () => {
|
|
99
|
+
setPendingAutoStart(base, { basePath: base, milestoneId: "M001" });
|
|
100
|
+
|
|
101
|
+
const before = _getPendingAutoStart(base);
|
|
102
|
+
assert.ok(before, "entry should exist before clear");
|
|
103
|
+
|
|
104
|
+
clearPendingAutoStart(base);
|
|
105
|
+
|
|
106
|
+
const after = _getPendingAutoStart(base);
|
|
107
|
+
assert.equal(after, null, "entry should be null after clearPendingAutoStart(base)");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("_getPendingAutoStart with no basePath argument returns the sole entry", () => {
|
|
111
|
+
setPendingAutoStart(base, { basePath: base, milestoneId: "M001" });
|
|
112
|
+
|
|
113
|
+
// No argument — should return the sole entry
|
|
114
|
+
const entry = _getPendingAutoStart();
|
|
115
|
+
assert.ok(entry, "sole entry should be returned when no basePath given");
|
|
116
|
+
assert.equal(entry.milestoneId, "M001");
|
|
117
|
+
assert.ok(entry.scope, "sole entry must have a scope");
|
|
118
|
+
assert.equal(entry.scope.milestoneId, "M001");
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -11,6 +11,7 @@ import { tmpdir } from "node:os";
|
|
|
11
11
|
|
|
12
12
|
import { DISPATCH_RULES, type DispatchContext } from "../auto-dispatch.ts";
|
|
13
13
|
import {
|
|
14
|
+
_getAdapter,
|
|
14
15
|
openDatabase,
|
|
15
16
|
closeDatabase,
|
|
16
17
|
insertMilestone,
|
|
@@ -241,9 +242,66 @@ test("#4781 phase 2: validate-milestone rule writes pass-through VALIDATION for
|
|
|
241
242
|
const { readFileSync } = await import("node:fs");
|
|
242
243
|
const content = readFileSync(validationPath, "utf-8");
|
|
243
244
|
assert.match(content, /verdict: pass/);
|
|
245
|
+
assert.match(content, /skip_validation: true/);
|
|
244
246
|
assert.match(content, /trivial-scope pipeline variant \(#4781\)/);
|
|
245
247
|
});
|
|
246
248
|
|
|
249
|
+
test("#4781 phase 2: validate-milestone skip path does not persist gates without a real slice", async (t) => {
|
|
250
|
+
const base = makeBase();
|
|
251
|
+
t.after(() => cleanup(base));
|
|
252
|
+
|
|
253
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
254
|
+
insertMilestone({
|
|
255
|
+
id: "M001",
|
|
256
|
+
title: TRIVIAL_INPUT.title,
|
|
257
|
+
status: "active",
|
|
258
|
+
depends_on: [],
|
|
259
|
+
});
|
|
260
|
+
upsertMilestonePlanning("M001", {
|
|
261
|
+
title: TRIVIAL_INPUT.title,
|
|
262
|
+
status: "active",
|
|
263
|
+
vision: TRIVIAL_INPUT.vision,
|
|
264
|
+
successCriteria: TRIVIAL_INPUT.successCriteria,
|
|
265
|
+
keyRisks: [],
|
|
266
|
+
proofStrategy: [],
|
|
267
|
+
verificationContract: "",
|
|
268
|
+
verificationIntegration: "",
|
|
269
|
+
verificationOperational: "",
|
|
270
|
+
verificationUat: "",
|
|
271
|
+
definitionOfDone: [],
|
|
272
|
+
requirementCoverage: "",
|
|
273
|
+
boundaryMapMarkdown: "",
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const { writeFileSync, readFileSync } = await import("node:fs");
|
|
277
|
+
writeFileSync(
|
|
278
|
+
join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
|
|
279
|
+
[
|
|
280
|
+
"# M001",
|
|
281
|
+
"## Slices",
|
|
282
|
+
"",
|
|
283
|
+
"_No slices required for this trivial milestone._",
|
|
284
|
+
].join("\n"),
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
const ctx = makeCtx({ base, mid: "M001", phase: "validating-milestone" });
|
|
288
|
+
const result = await findRule(VALIDATE_RULE).match(ctx);
|
|
289
|
+
|
|
290
|
+
assert.ok(result, "rule must return a result, not null");
|
|
291
|
+
assert.strictEqual(result!.action, "skip", "trivial variant must still skip without slices");
|
|
292
|
+
|
|
293
|
+
const validationPath = join(base, ".gsd", "milestones", "M001", "M001-VALIDATION.md");
|
|
294
|
+
const content = readFileSync(validationPath, "utf-8");
|
|
295
|
+
assert.match(content, /skip_validation: true/);
|
|
296
|
+
|
|
297
|
+
const adapter = _getAdapter();
|
|
298
|
+
assert.ok(adapter, "test database should be open");
|
|
299
|
+
const gateCount = adapter.prepare(
|
|
300
|
+
"SELECT count(*) AS n FROM quality_gates WHERE milestone_id = 'M001'",
|
|
301
|
+
).get() as { n: number };
|
|
302
|
+
assert.equal(gateCount.n, 0, "skip path must not persist milestone gates without a real slice id");
|
|
303
|
+
});
|
|
304
|
+
|
|
247
305
|
test("#4781 phase 2: validate-milestone rule dispatches normally for standard variant", async (t) => {
|
|
248
306
|
const base = makeBase();
|
|
249
307
|
t.after(() => cleanup(base));
|