gsd-pi 2.78.1-dev.b6a389b66 → 2.78.1-dev.d8826a445
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/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/phases.js +7 -2
- package/dist/resources/extensions/gsd/auto/session.js +3 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +3 -2
- package/dist/resources/extensions/gsd/auto-post-unit.js +7 -1
- package/dist/resources/extensions/gsd/auto-worktree.js +185 -40
- package/dist/resources/extensions/gsd/auto.js +62 -1
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +17 -16
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
- 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/gsd-db.js +194 -0
- package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
- package/dist/resources/extensions/gsd/guided-flow.js +117 -25
- 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/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 +15 -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 +10 -10
- 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 +10 -10
- 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/phases.ts +8 -2
- package/src/resources/extensions/gsd/auto/session.ts +4 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +10 -2
- package/src/resources/extensions/gsd/auto-post-unit.ts +8 -1
- package/src/resources/extensions/gsd/auto-worktree.ts +225 -47
- package/src/resources/extensions/gsd/auto.ts +79 -1
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +17 -17
- 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/db-writer.ts +113 -17
- package/src/resources/extensions/gsd/delegation-policy.ts +197 -0
- package/src/resources/extensions/gsd/gsd-db.ts +184 -0
- package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
- package/src/resources/extensions/gsd/guided-flow.ts +154 -25
- 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/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-session-scope.test.ts +331 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
- 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/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/workspace-collapse-integration.test.ts +371 -0
- 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/originalbase-path-comparison.test.ts +329 -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/pending-autostart-scope.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
- package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +74 -0
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +28 -16
- package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +453 -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 +102 -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/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 +190 -0
- package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +67 -52
- 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 +16 -2
- package/src/resources/extensions/gsd/write-intercept.ts +3 -3
- /package/dist/web/standalone/.next/static/{HahrZrc_Xn4wumj0O1Ydp → AT5qi39nKXkdmQIOIoh0f}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{HahrZrc_Xn4wumj0O1Ydp → AT5qi39nKXkdmQIOIoh0f}/_ssgManifest.js +0 -0
|
@@ -21,9 +21,13 @@ Before your first action, print this banner verbatim in chat:
|
|
|
21
21
|
## Pre-flight
|
|
22
22
|
|
|
23
23
|
1. Read `.gsd/PROJECT.md` end-to-end. If it does not exist, STOP and emit: `"PROJECT.md missing — run discuss-project first."`
|
|
24
|
-
2. Extract: Core Value, Anti-goals, Constraints, Milestone Sequence.
|
|
24
|
+
2. Extract: Core Value, Anti-goals, Constraints, Milestone Sequence, and the project shape verdict — read the `## Project Shape` section and look for `**Complexity:**` (verdict is either `simple` or `complex`; default to `complex` if the section is missing or unclear).
|
|
25
25
|
3. Check for existing `.gsd/REQUIREMENTS.md` — if present, this is a refinement pass, not a fresh write. Read existing requirements and treat them as the working set.
|
|
26
26
|
|
|
27
|
+
**Shape-dependent cadence:**
|
|
28
|
+
- **`simple`** — favor a single fast pass: extract requirements directly from PROJECT.md, ask 1–2 plain-text clarifying questions only if a class or status assignment is genuinely ambiguous, then write REQUIREMENTS.md.
|
|
29
|
+
- **`complex`** — full multi-round questioning with structured 3–4-option questions where alternatives matter.
|
|
30
|
+
|
|
27
31
|
---
|
|
28
32
|
|
|
29
33
|
## Interview Protocol
|
|
@@ -51,7 +55,7 @@ Ask **1–3 questions per round**. Each round targets one dimension:
|
|
|
51
55
|
|
|
52
56
|
**Never fabricate or simulate user input.** Wait for actual responses.
|
|
53
57
|
|
|
54
|
-
**If `{{structuredQuestionsAvailable}}` is `true`:** use `ask_user_questions`. Every question object MUST include a stable lowercase `id`. For class assignments, present the allowed classes as multi-select options. For status, present the four statuses as exclusive options. Ask 1–3 questions per call. Wait for each tool result before asking the next round.
|
|
58
|
+
**If `{{structuredQuestionsAvailable}}` is `true`:** use `ask_user_questions`. Every question object MUST include a stable lowercase `id`. For class assignments, present the allowed classes as multi-select options. For status, present the four statuses as exclusive options. In **`complex`** mode, any free-form question MUST present **3 or 4 concrete, researched options** plus a final **"Other — let me discuss"** option grounded in the investigation above. The class-assignment and status questions are exempt — they have fixed enumerations. Ask 1–3 questions per call. Wait for each tool result before asking the next round.
|
|
55
59
|
|
|
56
60
|
**If `{{structuredQuestionsAvailable}}` is `false`:** ask in plain text. Keep each round to 1–3 questions.
|
|
57
61
|
|
|
@@ -8,6 +8,13 @@ Your goal is **not** to center the discussion on tech stack trivia, naming conve
|
|
|
8
8
|
|
|
9
9
|
## Interview Protocol
|
|
10
10
|
|
|
11
|
+
### Read project shape
|
|
12
|
+
|
|
13
|
+
Before your first question round, read `.gsd/PROJECT.md` and look for `## Project Shape` → `**Complexity:**`. The verdict is either **`simple`** or **`complex`** (default to `complex` if the section is missing or unclear).
|
|
14
|
+
|
|
15
|
+
- **`simple`** — favor 1–2 plain-text rounds, write the slice context fast. Skip parallel-research investigation.
|
|
16
|
+
- **`complex`** — full investigation with structured 3–4-option questions.
|
|
17
|
+
|
|
11
18
|
### Before your first question round
|
|
12
19
|
|
|
13
20
|
Do a lightweight targeted investigation so your questions are grounded in reality:
|
|
@@ -24,7 +31,7 @@ Do **not** go deep — just enough that your questions reflect what's actually t
|
|
|
24
31
|
|
|
25
32
|
**Never fabricate or simulate user input.** Never generate fake transcript markers like `[User]`, `[Human]`, or `User:`. Ask one question round, then wait for the user's actual response before continuing.
|
|
26
33
|
|
|
27
|
-
**If `{{structuredQuestionsAvailable}}` is `true`:** Ask **1–3 questions per round** using `ask_user_questions`. **Call `ask_user_questions` exactly once per turn — never make multiple calls with the same or overlapping questions. Wait for the user's response before asking the next round.**
|
|
34
|
+
**If `{{structuredQuestionsAvailable}}` is `true`:** Ask **1–3 questions per round** using `ask_user_questions`. In **`complex`** mode, each multi-choice question MUST present **3 or 4 concrete, researched options** plus a final **"Other — let me discuss"** option; options must be grounded in the investigation above (codebase signals, library docs, prior `.gsd/` artifacts), not generic placeholders. In **`simple`** mode, 2 options is fine. Binary wrap-up gates are exempt from the 3-or-4 rule. **Call `ask_user_questions` exactly once per turn — never make multiple calls with the same or overlapping questions. Wait for the user's response before asking the next round.**
|
|
28
35
|
**If `{{structuredQuestionsAvailable}}` is `false`:** Ask **1–3 questions per round** in plain text. Number them and wait for the user's response before asking the next round.
|
|
29
36
|
Keep each question focused on one of:
|
|
30
37
|
- **UX and user-facing behaviour** — what does the user see, click, trigger, or experience?
|
|
@@ -11,6 +11,16 @@
|
|
|
11
11
|
|
|
12
12
|
{{theOneThingThatMustWorkEvenIfEverythingElseIsCut}}
|
|
13
13
|
|
|
14
|
+
## Project Shape
|
|
15
|
+
|
|
16
|
+
<!-- Drives questioning depth in downstream stages. `simple` → short plain-text
|
|
17
|
+
rounds, fast PROJECT/CONTEXT/REQUIREMENTS writes. `complex` → researched
|
|
18
|
+
3–4-option questions with an "Other — let me discuss" hatch.
|
|
19
|
+
Default to `complex` when uncertain. -->
|
|
20
|
+
|
|
21
|
+
- **Complexity:** {{simple | complex}}
|
|
22
|
+
- **Why:** {{one-line rationale citing the signals that decided it}}
|
|
23
|
+
|
|
14
24
|
## Current State
|
|
15
25
|
|
|
16
26
|
{{whatHasBeenBuiltSoFar — what works, what exists, what's deployed}}
|
|
@@ -35,7 +35,7 @@ import { _setAutoActiveForTest } from '../auto.ts';
|
|
|
35
35
|
// Reset all relevant state before and after each test.
|
|
36
36
|
function resetState(): void {
|
|
37
37
|
_setAutoActiveForTest(false);
|
|
38
|
-
clearDiscussionFlowState();
|
|
38
|
+
clearDiscussionFlowState(process.cwd());
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
describe('auto-discuss-milestone-deadlock-4973', () => {
|
|
@@ -51,7 +51,7 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
|
|
|
51
51
|
_setAutoActiveForTest(true);
|
|
52
52
|
|
|
53
53
|
// Before mark: blocked
|
|
54
|
-
const snapshotBefore = loadWriteGateSnapshot();
|
|
54
|
+
const snapshotBefore = loadWriteGateSnapshot(process.cwd());
|
|
55
55
|
const beforeResult = shouldBlockContextArtifactSaveInSnapshot(
|
|
56
56
|
snapshotBefore,
|
|
57
57
|
'CONTEXT',
|
|
@@ -61,10 +61,10 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
|
|
|
61
61
|
assert.strictEqual(beforeResult.block, true, 'should block before markDepthVerified');
|
|
62
62
|
|
|
63
63
|
// Simulate what the dispatch rule now does in auto-mode
|
|
64
|
-
markDepthVerified('M001');
|
|
64
|
+
markDepthVerified('M001', process.cwd());
|
|
65
65
|
|
|
66
66
|
// After mark: unblocked
|
|
67
|
-
const snapshotAfter = loadWriteGateSnapshot();
|
|
67
|
+
const snapshotAfter = loadWriteGateSnapshot(process.cwd());
|
|
68
68
|
const afterResult = shouldBlockContextArtifactSaveInSnapshot(
|
|
69
69
|
snapshotAfter,
|
|
70
70
|
'CONTEXT',
|
|
@@ -87,7 +87,7 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
|
|
|
87
87
|
assert.strictEqual(beforeResult.block, true, 'write should be blocked before markDepthVerified');
|
|
88
88
|
|
|
89
89
|
// Simulate dispatch rule auto-mark
|
|
90
|
-
markDepthVerified('M001');
|
|
90
|
+
markDepthVerified('M001', process.cwd());
|
|
91
91
|
|
|
92
92
|
// After mark: unblocked
|
|
93
93
|
const afterResult = shouldBlockContextWrite('write', contextPath, 'M001');
|
|
@@ -109,8 +109,8 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
|
|
|
109
109
|
// the dispatch-site call site is safe regardless of prior session state.
|
|
110
110
|
test('Test 3: session_switch ordering — clearDiscussionFlowState clears mark; dispatch-site call re-establishes it', () => {
|
|
111
111
|
// Simulate a mark from a prior session
|
|
112
|
-
markDepthVerified('M001');
|
|
113
|
-
let snapshot = loadWriteGateSnapshot();
|
|
112
|
+
markDepthVerified('M001', process.cwd());
|
|
113
|
+
let snapshot = loadWriteGateSnapshot(process.cwd());
|
|
114
114
|
assert.strictEqual(
|
|
115
115
|
isMilestoneDepthVerifiedInSnapshot(snapshot, 'M001'),
|
|
116
116
|
true,
|
|
@@ -119,8 +119,8 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
|
|
|
119
119
|
|
|
120
120
|
// session_switch fires clearDiscussionFlowState() — this is exactly what
|
|
121
121
|
// register-hooks.ts:106 does
|
|
122
|
-
clearDiscussionFlowState();
|
|
123
|
-
snapshot = loadWriteGateSnapshot();
|
|
122
|
+
clearDiscussionFlowState(process.cwd());
|
|
123
|
+
snapshot = loadWriteGateSnapshot(process.cwd());
|
|
124
124
|
assert.strictEqual(
|
|
125
125
|
isMilestoneDepthVerifiedInSnapshot(snapshot, 'M001'),
|
|
126
126
|
false,
|
|
@@ -130,9 +130,9 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
|
|
|
130
130
|
// Now the dispatch rule fires (after session_switch cleared state)
|
|
131
131
|
// and re-establishes the mark for the new session
|
|
132
132
|
_setAutoActiveForTest(true);
|
|
133
|
-
markDepthVerified('M001'); // this is what the dispatch rule does
|
|
133
|
+
markDepthVerified('M001', process.cwd()); // this is what the dispatch rule does
|
|
134
134
|
|
|
135
|
-
snapshot = loadWriteGateSnapshot();
|
|
135
|
+
snapshot = loadWriteGateSnapshot(process.cwd());
|
|
136
136
|
assert.strictEqual(
|
|
137
137
|
isMilestoneDepthVerifiedInSnapshot(snapshot, 'M001'),
|
|
138
138
|
true,
|
|
@@ -158,7 +158,7 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
|
|
|
158
158
|
|
|
159
159
|
// CONTEXT artifact save is still blocked
|
|
160
160
|
const snapshotResult = shouldBlockContextArtifactSaveInSnapshot(
|
|
161
|
-
loadWriteGateSnapshot(),
|
|
161
|
+
loadWriteGateSnapshot(process.cwd()),
|
|
162
162
|
'CONTEXT',
|
|
163
163
|
'M002',
|
|
164
164
|
null,
|
|
@@ -238,7 +238,7 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
|
|
|
238
238
|
);
|
|
239
239
|
|
|
240
240
|
// ── Deep auto-mode case: the user-facing approval gate must stay closed ──
|
|
241
|
-
clearDiscussionFlowState();
|
|
241
|
+
clearDiscussionFlowState(process.cwd());
|
|
242
242
|
if (existsSync(snapshotFile)) unlinkSync(snapshotFile);
|
|
243
243
|
_setAutoActiveForTest(true);
|
|
244
244
|
const deepCtx = {
|
|
@@ -264,7 +264,7 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
|
|
|
264
264
|
// ── Interactive case: the rule must NOT call markDepthVerified ──
|
|
265
265
|
// clearDiscussionFlowState() only deletes the snapshot at process.cwd(),
|
|
266
266
|
// so we must explicitly remove the snapshot under our tempBase too.
|
|
267
|
-
clearDiscussionFlowState();
|
|
267
|
+
clearDiscussionFlowState(process.cwd());
|
|
268
268
|
if (existsSync(snapshotFile)) unlinkSync(snapshotFile);
|
|
269
269
|
_setAutoActiveForTest(false);
|
|
270
270
|
snap = loadWriteGateSnapshot(tempBase);
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
// GSD-2 + Tests for MilestoneScope threading through AutoSession state (C2)
|
|
2
|
+
//
|
|
3
|
+
// Strategy: construct AutoSession directly + call createWorkspace/scopeMilestone
|
|
4
|
+
// to mirror the rebuildScope() helper in auto.ts — avoids importing the full
|
|
5
|
+
// auto.ts module (too many .js resolved imports).
|
|
6
|
+
|
|
7
|
+
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { mkdtempSync, mkdirSync, rmSync, realpathSync, existsSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
|
|
13
|
+
import { AutoSession } from "../auto/session.ts";
|
|
14
|
+
import { createWorkspace, scopeMilestone } from "../workspace.ts";
|
|
15
|
+
import type { MilestoneScope } from "../workspace.ts";
|
|
16
|
+
|
|
17
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function makeProjectDir(): string {
|
|
20
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-scope-test-")));
|
|
21
|
+
mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
|
|
22
|
+
return dir;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function makeWorktreeDir(projectDir: string, milestoneId: string): string {
|
|
26
|
+
const wt = join(projectDir, ".gsd", "worktrees", milestoneId);
|
|
27
|
+
mkdirSync(wt, { recursive: true });
|
|
28
|
+
return wt;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Mirror the rebuildScope() helper from auto.ts — computes s.scope from the
|
|
33
|
+
* same inputs so tests can verify the behaviour without importing auto.ts.
|
|
34
|
+
*/
|
|
35
|
+
function applyRebuildScope(
|
|
36
|
+
s: AutoSession,
|
|
37
|
+
rawPath: string,
|
|
38
|
+
milestoneId: string | null,
|
|
39
|
+
): void {
|
|
40
|
+
if (!milestoneId) {
|
|
41
|
+
s.scope = null;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const workspace = createWorkspace(rawPath);
|
|
46
|
+
s.scope = scopeMilestone(workspace, milestoneId);
|
|
47
|
+
} catch {
|
|
48
|
+
s.scope = null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
describe("AutoSession.scope — project mode (basePath equals originalBasePath)", () => {
|
|
55
|
+
let s: AutoSession;
|
|
56
|
+
let projectDir: string;
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
projectDir = makeProjectDir();
|
|
60
|
+
s = new AutoSession();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("scope is null when milestoneId is null", () => {
|
|
68
|
+
s.basePath = projectDir;
|
|
69
|
+
s.originalBasePath = projectDir;
|
|
70
|
+
s.currentMilestoneId = null;
|
|
71
|
+
|
|
72
|
+
applyRebuildScope(s, projectDir, null);
|
|
73
|
+
|
|
74
|
+
assert.equal(s.scope, null);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("scope mode is 'project' when basePath equals originalBasePath", () => {
|
|
78
|
+
const mid = "M001";
|
|
79
|
+
s.basePath = projectDir;
|
|
80
|
+
s.originalBasePath = projectDir;
|
|
81
|
+
s.currentMilestoneId = mid;
|
|
82
|
+
|
|
83
|
+
applyRebuildScope(s, projectDir, mid);
|
|
84
|
+
|
|
85
|
+
assert.ok(s.scope, "scope should be set");
|
|
86
|
+
assert.equal(s.scope.workspace.mode, "project");
|
|
87
|
+
assert.equal(s.scope.milestoneId, mid);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("scope projectRoot matches realpath of projectDir", () => {
|
|
91
|
+
const mid = "M001";
|
|
92
|
+
s.basePath = projectDir;
|
|
93
|
+
s.originalBasePath = projectDir;
|
|
94
|
+
s.currentMilestoneId = mid;
|
|
95
|
+
|
|
96
|
+
applyRebuildScope(s, projectDir, mid);
|
|
97
|
+
|
|
98
|
+
assert.ok(s.scope, "scope should be set");
|
|
99
|
+
assert.equal(s.scope.workspace.projectRoot, realpathSync(projectDir));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("scope worktreeRoot is null in project mode", () => {
|
|
103
|
+
const mid = "M001";
|
|
104
|
+
s.basePath = projectDir;
|
|
105
|
+
s.originalBasePath = projectDir;
|
|
106
|
+
s.currentMilestoneId = mid;
|
|
107
|
+
|
|
108
|
+
applyRebuildScope(s, projectDir, mid);
|
|
109
|
+
|
|
110
|
+
assert.ok(s.scope, "scope should be set");
|
|
111
|
+
assert.equal(s.scope.workspace.worktreeRoot, null);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("scope path methods resolve under the .gsd directory", () => {
|
|
115
|
+
const mid = "M002";
|
|
116
|
+
s.basePath = projectDir;
|
|
117
|
+
s.originalBasePath = projectDir;
|
|
118
|
+
s.currentMilestoneId = mid;
|
|
119
|
+
|
|
120
|
+
applyRebuildScope(s, projectDir, mid);
|
|
121
|
+
|
|
122
|
+
assert.ok(s.scope, "scope should be set");
|
|
123
|
+
const gsd = join(projectDir, ".gsd");
|
|
124
|
+
assert.equal(s.scope.contextFile(), join(gsd, "milestones", mid, `${mid}-CONTEXT.md`));
|
|
125
|
+
assert.equal(s.scope.roadmapFile(), join(gsd, "milestones", mid, `${mid}-ROADMAP.md`));
|
|
126
|
+
assert.equal(s.scope.stateFile(), join(gsd, "STATE.md"));
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("AutoSession.scope — worktree mode (basePath differs from originalBasePath)", () => {
|
|
131
|
+
let s: AutoSession;
|
|
132
|
+
let projectDir: string;
|
|
133
|
+
let worktreeDir: string;
|
|
134
|
+
|
|
135
|
+
beforeEach(() => {
|
|
136
|
+
projectDir = makeProjectDir();
|
|
137
|
+
worktreeDir = makeWorktreeDir(projectDir, "M001");
|
|
138
|
+
s = new AutoSession();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
afterEach(() => {
|
|
142
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("scope mode is 'worktree' when basePath is the worktree path", () => {
|
|
146
|
+
const mid = "M001";
|
|
147
|
+
s.basePath = worktreeDir;
|
|
148
|
+
s.originalBasePath = projectDir;
|
|
149
|
+
s.currentMilestoneId = mid;
|
|
150
|
+
|
|
151
|
+
applyRebuildScope(s, worktreeDir, mid);
|
|
152
|
+
|
|
153
|
+
assert.ok(s.scope, "scope should be set");
|
|
154
|
+
assert.equal(s.scope.workspace.mode, "worktree");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("scope worktreeRoot matches realpath of worktreeDir", () => {
|
|
158
|
+
const mid = "M001";
|
|
159
|
+
s.basePath = worktreeDir;
|
|
160
|
+
s.originalBasePath = projectDir;
|
|
161
|
+
s.currentMilestoneId = mid;
|
|
162
|
+
|
|
163
|
+
applyRebuildScope(s, worktreeDir, mid);
|
|
164
|
+
|
|
165
|
+
assert.ok(s.scope, "scope should be set");
|
|
166
|
+
assert.equal(s.scope.workspace.worktreeRoot, realpathSync(worktreeDir));
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("scope projectRoot resolves to project root (not worktree)", () => {
|
|
170
|
+
const mid = "M001";
|
|
171
|
+
s.basePath = worktreeDir;
|
|
172
|
+
s.originalBasePath = projectDir;
|
|
173
|
+
s.currentMilestoneId = mid;
|
|
174
|
+
|
|
175
|
+
applyRebuildScope(s, worktreeDir, mid);
|
|
176
|
+
|
|
177
|
+
assert.ok(s.scope, "scope should be set");
|
|
178
|
+
assert.equal(s.scope.workspace.projectRoot, realpathSync(projectDir));
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("scope milestoneId matches the milestone being tracked", () => {
|
|
182
|
+
const mid = "M001";
|
|
183
|
+
s.basePath = worktreeDir;
|
|
184
|
+
s.originalBasePath = projectDir;
|
|
185
|
+
s.currentMilestoneId = mid;
|
|
186
|
+
|
|
187
|
+
applyRebuildScope(s, worktreeDir, mid);
|
|
188
|
+
|
|
189
|
+
assert.ok(s.scope, "scope should be set");
|
|
190
|
+
assert.equal(s.scope.milestoneId, mid);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("AutoSession.scope — milestoneId change rebuilds scope", () => {
|
|
195
|
+
let s: AutoSession;
|
|
196
|
+
let projectDir: string;
|
|
197
|
+
|
|
198
|
+
beforeEach(() => {
|
|
199
|
+
projectDir = makeProjectDir();
|
|
200
|
+
s = new AutoSession();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
afterEach(() => {
|
|
204
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("scope reflects the new milestoneId after rebuild", () => {
|
|
208
|
+
s.basePath = projectDir;
|
|
209
|
+
s.originalBasePath = projectDir;
|
|
210
|
+
s.currentMilestoneId = "M001";
|
|
211
|
+
applyRebuildScope(s, projectDir, "M001");
|
|
212
|
+
|
|
213
|
+
assert.ok(s.scope, "initial scope should be set");
|
|
214
|
+
assert.equal(s.scope.milestoneId, "M001");
|
|
215
|
+
|
|
216
|
+
// Simulate milestone transition mid-session
|
|
217
|
+
s.currentMilestoneId = "M002";
|
|
218
|
+
applyRebuildScope(s, projectDir, "M002");
|
|
219
|
+
|
|
220
|
+
assert.ok(s.scope, "scope should be set after transition");
|
|
221
|
+
assert.equal(s.scope.milestoneId, "M002");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("scope contextFile changes when milestoneId changes", () => {
|
|
225
|
+
s.basePath = projectDir;
|
|
226
|
+
s.originalBasePath = projectDir;
|
|
227
|
+
|
|
228
|
+
s.currentMilestoneId = "M001";
|
|
229
|
+
applyRebuildScope(s, projectDir, "M001");
|
|
230
|
+
const ctxM001 = s.scope?.contextFile();
|
|
231
|
+
|
|
232
|
+
s.currentMilestoneId = "M002";
|
|
233
|
+
applyRebuildScope(s, projectDir, "M002");
|
|
234
|
+
const ctxM002 = s.scope?.contextFile();
|
|
235
|
+
|
|
236
|
+
assert.ok(ctxM001, "M001 contextFile should be set");
|
|
237
|
+
assert.ok(ctxM002, "M002 contextFile should be set");
|
|
238
|
+
assert.notEqual(ctxM001, ctxM002, "contextFile must differ between milestone IDs");
|
|
239
|
+
assert.ok(ctxM001.includes("M001"), "M001 path should contain M001");
|
|
240
|
+
assert.ok(ctxM002.includes("M002"), "M002 path should contain M002");
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe("AutoSession.scope — resume from persisted state", () => {
|
|
245
|
+
let s: AutoSession;
|
|
246
|
+
let projectDir: string;
|
|
247
|
+
let worktreeDir: string;
|
|
248
|
+
|
|
249
|
+
beforeEach(() => {
|
|
250
|
+
projectDir = makeProjectDir();
|
|
251
|
+
worktreeDir = makeWorktreeDir(projectDir, "M003");
|
|
252
|
+
s = new AutoSession();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
afterEach(() => {
|
|
256
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("resume without worktree: scope mode is project, projectRoot is base", () => {
|
|
260
|
+
// Mirror the paused-session resume path:
|
|
261
|
+
// s.currentMilestoneId = meta.milestoneId
|
|
262
|
+
// s.originalBasePath = meta.originalBasePath || base
|
|
263
|
+
// rawPath = originalBasePath (no worktreePath present)
|
|
264
|
+
const mid = "M003";
|
|
265
|
+
s.currentMilestoneId = mid;
|
|
266
|
+
s.originalBasePath = projectDir;
|
|
267
|
+
s.basePath = projectDir;
|
|
268
|
+
|
|
269
|
+
applyRebuildScope(s, s.originalBasePath, s.currentMilestoneId);
|
|
270
|
+
|
|
271
|
+
assert.ok(s.scope, "scope should be reconstructed");
|
|
272
|
+
assert.equal(s.scope.milestoneId, mid);
|
|
273
|
+
assert.equal(s.scope.workspace.mode, "project");
|
|
274
|
+
assert.equal(s.scope.workspace.projectRoot, realpathSync(projectDir));
|
|
275
|
+
assert.equal(s.scope.workspace.worktreeRoot, null);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("resume with valid worktree path: scope mode is worktree", () => {
|
|
279
|
+
// Mirror the paused-session resume path where worktreePath exists on disk:
|
|
280
|
+
// rawPath = worktreePath (existsSync true)
|
|
281
|
+
const mid = "M003";
|
|
282
|
+
s.currentMilestoneId = mid;
|
|
283
|
+
s.originalBasePath = projectDir;
|
|
284
|
+
s.basePath = worktreeDir;
|
|
285
|
+
|
|
286
|
+
assert.ok(existsSync(worktreeDir), "worktreeDir must exist for this test");
|
|
287
|
+
|
|
288
|
+
applyRebuildScope(s, worktreeDir, s.currentMilestoneId);
|
|
289
|
+
|
|
290
|
+
assert.ok(s.scope, "scope should be reconstructed");
|
|
291
|
+
assert.equal(s.scope.milestoneId, mid);
|
|
292
|
+
assert.equal(s.scope.workspace.mode, "worktree");
|
|
293
|
+
assert.equal(s.scope.workspace.projectRoot, realpathSync(projectDir));
|
|
294
|
+
assert.equal(s.scope.workspace.worktreeRoot, realpathSync(worktreeDir));
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("scope is consistent with direct createWorkspace + scopeMilestone for same inputs", () => {
|
|
298
|
+
const mid = "M003";
|
|
299
|
+
s.currentMilestoneId = mid;
|
|
300
|
+
s.originalBasePath = projectDir;
|
|
301
|
+
s.basePath = projectDir;
|
|
302
|
+
|
|
303
|
+
applyRebuildScope(s, projectDir, mid);
|
|
304
|
+
assert.ok(s.scope, "scope should be set");
|
|
305
|
+
|
|
306
|
+
// Build expected scope via lower-level API to verify equivalence
|
|
307
|
+
const ws = createWorkspace(projectDir);
|
|
308
|
+
const expected = scopeMilestone(ws, mid);
|
|
309
|
+
|
|
310
|
+
assert.equal(s.scope.milestoneId, expected.milestoneId);
|
|
311
|
+
assert.equal(s.scope.contextFile(), expected.contextFile());
|
|
312
|
+
assert.equal(s.scope.roadmapFile(), expected.roadmapFile());
|
|
313
|
+
assert.equal(s.scope.stateFile(), expected.stateFile());
|
|
314
|
+
assert.equal(s.scope.dbPath(), expected.dbPath());
|
|
315
|
+
assert.equal(s.scope.milestoneDir(), expected.milestoneDir());
|
|
316
|
+
assert.equal(s.scope.metaJson(), expected.metaJson());
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("reset() clears scope", () => {
|
|
320
|
+
const mid = "M003";
|
|
321
|
+
s.basePath = projectDir;
|
|
322
|
+
s.originalBasePath = projectDir;
|
|
323
|
+
s.currentMilestoneId = mid;
|
|
324
|
+
|
|
325
|
+
applyRebuildScope(s, projectDir, mid);
|
|
326
|
+
assert.ok(s.scope, "scope should be set before reset");
|
|
327
|
+
|
|
328
|
+
s.reset();
|
|
329
|
+
assert.equal(s.scope, null, "scope must be null after reset()");
|
|
330
|
+
});
|
|
331
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// GSD-2 + Unit tests for the workspace registry that replaced the originalBase singleton
|
|
2
|
+
|
|
3
|
+
import { describe, test, beforeEach } from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, realpathSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
getAutoWorktreeOriginalBase,
|
|
12
|
+
getActiveAutoWorktreeContext,
|
|
13
|
+
_resetAutoWorktreeOriginalBaseForTests,
|
|
14
|
+
createAutoWorktree,
|
|
15
|
+
enterAutoWorktree,
|
|
16
|
+
teardownAutoWorktree,
|
|
17
|
+
} from "../auto-worktree.ts";
|
|
18
|
+
|
|
19
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
// Safe: all inputs below are hardcoded test strings, not user input.
|
|
22
|
+
function git(subArgs: string[], cwd: string): void {
|
|
23
|
+
execFileSync("git", subArgs, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createTempRepo(t: { after: (fn: () => void) => void }): string {
|
|
27
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "awreg-test-")));
|
|
28
|
+
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
29
|
+
git(["init"], dir);
|
|
30
|
+
git(["config", "user.email", "test@test.com"], dir);
|
|
31
|
+
git(["config", "user.name", "Test"], dir);
|
|
32
|
+
writeFileSync(join(dir, "README.md"), "# test\n");
|
|
33
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
34
|
+
git(["add", "."], dir);
|
|
35
|
+
git(["commit", "-m", "init"], dir);
|
|
36
|
+
git(["branch", "-M", "main"], dir);
|
|
37
|
+
return dir;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
describe("auto-worktree workspace registry", () => {
|
|
43
|
+
const savedCwd = process.cwd();
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
_resetAutoWorktreeOriginalBaseForTests();
|
|
47
|
+
process.chdir(savedCwd);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("getAutoWorktreeOriginalBase() is null at baseline", () => {
|
|
51
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), null);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("getActiveAutoWorktreeContext() is null at baseline", () => {
|
|
55
|
+
assert.strictEqual(getActiveAutoWorktreeContext(), null);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("_resetAutoWorktreeOriginalBaseForTests() clears the registry — idempotent", () => {
|
|
59
|
+
_resetAutoWorktreeOriginalBaseForTests();
|
|
60
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), null);
|
|
61
|
+
_resetAutoWorktreeOriginalBaseForTests();
|
|
62
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), null);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("behavioral equivalence: createAutoWorktree populates registry; teardown clears it", (t) => {
|
|
66
|
+
const tempDir = createTempRepo(t);
|
|
67
|
+
const msDir = join(tempDir, ".gsd", "milestones", "M001");
|
|
68
|
+
mkdirSync(msDir, { recursive: true });
|
|
69
|
+
writeFileSync(join(msDir, "CONTEXT.md"), "# M001 Context\n");
|
|
70
|
+
git(["add", "."], tempDir);
|
|
71
|
+
git(["commit", "-m", "add milestone"], tempDir);
|
|
72
|
+
|
|
73
|
+
// Before entering: registry must be empty
|
|
74
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), null,
|
|
75
|
+
"originalBase is null before entering worktree");
|
|
76
|
+
|
|
77
|
+
createAutoWorktree(tempDir, "M001");
|
|
78
|
+
|
|
79
|
+
// After enter: getAutoWorktreeOriginalBase must equal tempDir
|
|
80
|
+
assert.strictEqual(
|
|
81
|
+
getAutoWorktreeOriginalBase(),
|
|
82
|
+
tempDir,
|
|
83
|
+
"getAutoWorktreeOriginalBase() returns projectRoot after createAutoWorktree",
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// getActiveAutoWorktreeContext must return the correct shape
|
|
87
|
+
const ctx = getActiveAutoWorktreeContext();
|
|
88
|
+
assert.ok(ctx !== null, "context is non-null inside worktree");
|
|
89
|
+
assert.strictEqual(ctx.originalBase, tempDir, "context.originalBase matches tempDir");
|
|
90
|
+
assert.strictEqual(ctx.worktreeName, "M001", "context.worktreeName is M001");
|
|
91
|
+
assert.strictEqual(ctx.branch, "milestone/M001", "context.branch is milestone/M001");
|
|
92
|
+
|
|
93
|
+
// Teardown: registry must be cleared
|
|
94
|
+
teardownAutoWorktree(tempDir, "M001");
|
|
95
|
+
|
|
96
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), null,
|
|
97
|
+
"getAutoWorktreeOriginalBase() is null after teardown");
|
|
98
|
+
assert.strictEqual(getActiveAutoWorktreeContext(), null,
|
|
99
|
+
"getActiveAutoWorktreeContext() is null after teardown");
|
|
100
|
+
|
|
101
|
+
try { process.chdir(savedCwd); } catch { /* ignore */ }
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("behavioral equivalence: enterAutoWorktree also populates registry", (t) => {
|
|
105
|
+
const tempDir = createTempRepo(t);
|
|
106
|
+
const msDir = join(tempDir, ".gsd", "milestones", "M002");
|
|
107
|
+
mkdirSync(msDir, { recursive: true });
|
|
108
|
+
writeFileSync(join(msDir, "CONTEXT.md"), "# M002 Context\n");
|
|
109
|
+
git(["add", "."], tempDir);
|
|
110
|
+
git(["commit", "-m", "add milestone"], tempDir);
|
|
111
|
+
|
|
112
|
+
createAutoWorktree(tempDir, "M002");
|
|
113
|
+
|
|
114
|
+
// Simulate leaving the worktree (crash/pause)
|
|
115
|
+
_resetAutoWorktreeOriginalBaseForTests();
|
|
116
|
+
process.chdir(tempDir);
|
|
117
|
+
|
|
118
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), null,
|
|
119
|
+
"registry is empty after manual reset");
|
|
120
|
+
|
|
121
|
+
// Re-enter via enterAutoWorktree
|
|
122
|
+
enterAutoWorktree(tempDir, "M002");
|
|
123
|
+
|
|
124
|
+
assert.strictEqual(
|
|
125
|
+
getAutoWorktreeOriginalBase(),
|
|
126
|
+
tempDir,
|
|
127
|
+
"getAutoWorktreeOriginalBase() returns projectRoot after enterAutoWorktree",
|
|
128
|
+
);
|
|
129
|
+
const ctx = getActiveAutoWorktreeContext();
|
|
130
|
+
assert.ok(ctx !== null, "context is non-null after re-entry");
|
|
131
|
+
assert.strictEqual(ctx.originalBase, tempDir);
|
|
132
|
+
assert.strictEqual(ctx.worktreeName, "M002");
|
|
133
|
+
assert.strictEqual(ctx.branch, "milestone/M002");
|
|
134
|
+
|
|
135
|
+
teardownAutoWorktree(tempDir, "M002");
|
|
136
|
+
try { process.chdir(savedCwd); } catch { /* ignore */ }
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("single-occupancy: entering a new workspace replaces the previous one", (t) => {
|
|
140
|
+
const dir1 = createTempRepo(t);
|
|
141
|
+
const dir2 = createTempRepo(t);
|
|
142
|
+
|
|
143
|
+
// Set up milestone in dir1
|
|
144
|
+
const ms1Dir = join(dir1, ".gsd", "milestones", "M010");
|
|
145
|
+
mkdirSync(ms1Dir, { recursive: true });
|
|
146
|
+
writeFileSync(join(ms1Dir, "CONTEXT.md"), "# M010\n");
|
|
147
|
+
git(["add", "."], dir1);
|
|
148
|
+
git(["commit", "-m", "add milestone"], dir1);
|
|
149
|
+
|
|
150
|
+
// Set up milestone in dir2
|
|
151
|
+
const ms2Dir = join(dir2, ".gsd", "milestones", "M020");
|
|
152
|
+
mkdirSync(ms2Dir, { recursive: true });
|
|
153
|
+
writeFileSync(join(ms2Dir, "CONTEXT.md"), "# M020\n");
|
|
154
|
+
git(["add", "."], dir2);
|
|
155
|
+
git(["commit", "-m", "add milestone"], dir2);
|
|
156
|
+
|
|
157
|
+
// Enter dir1/M010
|
|
158
|
+
createAutoWorktree(dir1, "M010");
|
|
159
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), dir1,
|
|
160
|
+
"registry holds dir1 after entering M010");
|
|
161
|
+
|
|
162
|
+
// Tear down dir1 cleanly
|
|
163
|
+
teardownAutoWorktree(dir1, "M010");
|
|
164
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), null, "registry cleared after M010 teardown");
|
|
165
|
+
|
|
166
|
+
// Enter dir2/M020 — registry should now hold dir2 only
|
|
167
|
+
createAutoWorktree(dir2, "M020");
|
|
168
|
+
assert.strictEqual(getAutoWorktreeOriginalBase(), dir2,
|
|
169
|
+
"registry holds dir2 after entering M020 (single-occupancy preserved)");
|
|
170
|
+
assert.notStrictEqual(getAutoWorktreeOriginalBase(), dir1,
|
|
171
|
+
"dir1 is no longer in the registry");
|
|
172
|
+
|
|
173
|
+
teardownAutoWorktree(dir2, "M020");
|
|
174
|
+
try { process.chdir(savedCwd); } catch { /* ignore */ }
|
|
175
|
+
});
|
|
176
|
+
});
|