@opengsd/gsd-pi 1.1.1-dev.9bb7453 → 1.1.1-dev.a5a2de8
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-dispatch.js +11 -0
- package/dist/resources/extensions/gsd/auto-prompts.js +4 -0
- package/dist/resources/extensions/gsd/auto-recovery.js +3 -4
- package/dist/resources/extensions/gsd/auto-unit-tool-scope.js +18 -66
- package/dist/resources/extensions/gsd/auto-worktree.js +18 -5
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +16 -10
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +19 -8
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +18 -29
- package/dist/resources/extensions/gsd/closeout-consistency-gate.js +61 -0
- package/dist/resources/extensions/gsd/guided-flow.js +89 -107
- package/dist/resources/extensions/gsd/milestone-closeout.js +3 -1
- package/dist/resources/extensions/gsd/pending-auto-start.js +0 -1
- package/dist/resources/extensions/gsd/prompts/run-uat.md +3 -17
- package/dist/resources/extensions/gsd/recovery-classification.js +20 -0
- package/dist/resources/extensions/gsd/tool-contract.js +5 -0
- package/dist/resources/extensions/gsd/tool-presentation-plan.js +17 -7
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +81 -4
- package/dist/resources/extensions/gsd/unit-tool-contracts.js +169 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +3 -75
- 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/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/chunks/8357.js +1 -1
- 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/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +5 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/package.json +3 -3
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +4 -3
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/dist/harness/agent-harness.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/harness/agent-harness.js +3 -1
- package/packages/pi-agent-core/dist/harness/agent-harness.js.map +1 -1
- package/packages/pi-agent-core/dist/harness/types.d.ts +1 -0
- package/packages/pi-agent-core/dist/harness/types.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/harness/types.js.map +1 -1
- package/packages/pi-agent-core/dist/types.d.ts +3 -1
- package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/types.js.map +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +6 -6
- package/packages/pi-ai/dist/models.generated.js +6 -6
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js +2 -2
- package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit.js +3 -2
- package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/render-utils.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/tools/render-utils.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/render-utils.js +6 -0
- package/packages/pi-coding-agent/dist/core/tools/render-utils.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/write.js +3 -2
- package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +1 -1
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +14 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +4 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +3 -3
- package/src/resources/extensions/gsd/auto-unit-tool-scope.ts +43 -74
- package/src/resources/extensions/gsd/auto-worktree.ts +23 -5
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +16 -10
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +23 -8
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +50 -54
- package/src/resources/extensions/gsd/closeout-consistency-gate.ts +137 -0
- package/src/resources/extensions/gsd/guided-flow.ts +124 -134
- package/src/resources/extensions/gsd/milestone-closeout.ts +3 -1
- package/src/resources/extensions/gsd/pending-auto-start.ts +0 -2
- package/src/resources/extensions/gsd/prompts/run-uat.md +3 -17
- package/src/resources/extensions/gsd/recovery-classification.ts +20 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +10 -2
- package/src/resources/extensions/gsd/tests/auto-start-bootstrap-await-3420.test.ts +4 -1
- package/src/resources/extensions/gsd/tests/auto-warning-noise-regression.test.ts +12 -2
- package/src/resources/extensions/gsd/tests/check-auto-start-pending-gate.test.ts +9 -15
- package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +26 -16
- package/src/resources/extensions/gsd/tests/commands-dispatcher-unmerged-milestone.test.ts +21 -0
- package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +40 -1
- package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +31 -79
- package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +5 -3
- package/src/resources/extensions/gsd/tests/guided-flow-state-rebuild.test.ts +40 -4
- package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/integration/parallel-merge.test.ts +16 -0
- package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/merge-closeout-consistency-gate.test.ts +63 -0
- package/src/resources/extensions/gsd/tests/merge-db-cycle.test.ts +10 -1
- package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +9 -1
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +23 -5
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +44 -0
- package/src/resources/extensions/gsd/tests/run-uat-composer.test.ts +4 -0
- package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +36 -0
- package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +221 -0
- package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +15 -0
- package/src/resources/extensions/gsd/tool-contract.ts +6 -0
- package/src/resources/extensions/gsd/tool-presentation-plan.ts +38 -8
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +100 -5
- package/src/resources/extensions/gsd/unit-tool-contracts.ts +186 -0
- package/src/resources/extensions/gsd/workflow-mcp.ts +3 -75
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +0 -246
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +0 -218
- /package/dist/web/standalone/.next/static/{jBtwT9v1u2lUA3UEOy_ZH → 9y3LeeR2uGr2yRj9RjY3D}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{jBtwT9v1u2lUA3UEOy_ZH → 9y3LeeR2uGr2yRj9RjY3D}/_ssgManifest.js +0 -0
|
@@ -164,17 +164,13 @@ describe("checkAutoStartAfterDiscuss Gate 1a (pending depth-verification gate)",
|
|
|
164
164
|
test("Gate 1a does NOT trip when the pending gate is for a DIFFERENT milestone", () => {
|
|
165
165
|
base = mkBase();
|
|
166
166
|
openDatabase(":memory:");
|
|
167
|
-
// status: "queued" so that Gate 1b downstream of Gate 1a fires its
|
|
168
|
-
// recovery notify ("context file exists but milestone is still queued") —
|
|
169
|
-
// observing that notify proves we advanced past Gate 1a. If Gate 1a
|
|
170
|
-
// wrongly tripped on the M999 gate it would `return false` immediately
|
|
171
|
-
// and Gate 1b would never run, so the notify would be absent.
|
|
172
167
|
insertMilestone({ id: "M001", title: "Pending Gate Test", status: "queued" });
|
|
173
168
|
|
|
174
169
|
cap = mkCapture();
|
|
175
170
|
setPendingAutoStart(base, {
|
|
176
171
|
basePath: base,
|
|
177
172
|
milestoneId: "M001",
|
|
173
|
+
startAuto: false,
|
|
178
174
|
ctx: mkCtx(cap),
|
|
179
175
|
pi: mkPi(cap),
|
|
180
176
|
});
|
|
@@ -182,21 +178,19 @@ describe("checkAutoStartAfterDiscuss Gate 1a (pending depth-verification gate)",
|
|
|
182
178
|
setPendingGate("depth_verification_M999_confirm", base);
|
|
183
179
|
|
|
184
180
|
const result = checkAutoStartAfterDiscuss();
|
|
185
|
-
assert.equal(result,
|
|
181
|
+
assert.equal(result, true, "different milestone gate must not block this handoff");
|
|
186
182
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const gate1bNotify = cap.notifies.find(n =>
|
|
190
|
-
n.level === "warning" && /M001.*context file exists but milestone is still queued/i.test(n.msg)
|
|
183
|
+
const successNotify = cap.notifies.find(n =>
|
|
184
|
+
n.level === "success" && /M001 context captured/i.test(n.msg)
|
|
191
185
|
);
|
|
192
186
|
assert.ok(
|
|
193
|
-
|
|
194
|
-
`expected
|
|
187
|
+
successNotify,
|
|
188
|
+
`expected context-captured success notify about M001; got: ${JSON.stringify(cap.notifies)}`,
|
|
195
189
|
);
|
|
196
190
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
191
|
+
const retryNotify = cap.notifies.find(n => /queued|gsd_plan_milestone/i.test(n.msg));
|
|
192
|
+
assert.equal(retryNotify, undefined, "handoff must not mention queued state or internal plan retry");
|
|
193
|
+
|
|
200
194
|
const m999Notify = cap.notifies.find(n => /M999/i.test(n.msg));
|
|
201
195
|
assert.equal(m999Notify, undefined, "no notify should reference M999 (the pending-gate milestone)");
|
|
202
196
|
});
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
// gsd-pi + Regression tests for checkAutoStartAfterDiscuss
|
|
1
|
+
// gsd-pi + Regression tests for checkAutoStartAfterDiscuss handoff copy (R3b)
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// is absent. Otherwise the user sees "ready" and then /gsd reports
|
|
6
|
-
// "No Active Milestone" because the milestone was never registered.
|
|
3
|
+
// Missing-row repair may accept a context handoff, but "Milestone X ready."
|
|
4
|
+
// is reserved for executable plans with persisted slices in DB mode.
|
|
7
5
|
|
|
8
6
|
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
9
7
|
import assert from "node:assert/strict";
|
|
@@ -21,6 +19,7 @@ import {
|
|
|
21
19
|
openDatabase,
|
|
22
20
|
closeDatabase,
|
|
23
21
|
insertMilestone,
|
|
22
|
+
insertSlice,
|
|
24
23
|
getMilestone,
|
|
25
24
|
} from "../gsd-db.ts";
|
|
26
25
|
import {
|
|
@@ -92,49 +91,60 @@ describe("checkAutoStartAfterDiscuss ready-notify DB guard (R3b)", () => {
|
|
|
92
91
|
}
|
|
93
92
|
});
|
|
94
93
|
|
|
95
|
-
test("
|
|
94
|
+
test("repairs a missing milestone DB row and accepts context-captured handoff", () => {
|
|
96
95
|
base = mkBase();
|
|
97
|
-
// Open a fresh in-memory DB but DO NOT insertMilestone for M001.
|
|
98
96
|
openDatabase(":memory:");
|
|
99
97
|
|
|
100
98
|
cap = mkCapture();
|
|
101
99
|
setPendingAutoStart(base, {
|
|
102
100
|
basePath: base,
|
|
103
101
|
milestoneId: "M001",
|
|
102
|
+
startAuto: false,
|
|
104
103
|
ctx: mkCtx(cap),
|
|
105
104
|
pi: mkPi(cap),
|
|
106
105
|
});
|
|
107
106
|
|
|
108
107
|
const result = checkAutoStartAfterDiscuss();
|
|
109
|
-
assert.equal(result,
|
|
108
|
+
assert.equal(result, true, "missing row with pinned context should repair and accept handoff");
|
|
110
109
|
|
|
111
|
-
// No success "ready" notify
|
|
112
110
|
const successReady = cap.notifies.find(
|
|
113
111
|
(n) => n.level === "success" && /ready\.?$/i.test(n.msg),
|
|
114
112
|
);
|
|
115
113
|
assert.equal(successReady, undefined, "must not announce 'ready' when DB row missing");
|
|
116
114
|
|
|
117
|
-
// When CONTEXT.md is on disk the R3b path recovers: it inserts a placeholder
|
|
118
|
-
// "queued" row (so Gate 1b can retry gsd_plan_milestone) and emits a warning.
|
|
119
115
|
const recovered = getMilestone("M001");
|
|
120
116
|
assert.ok(recovered, "R3b recovery must insert a placeholder 'queued' DB row");
|
|
121
117
|
assert.equal(recovered!.status, "queued", "placeholder row must have status 'queued'");
|
|
122
118
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
119
|
+
assert.equal(
|
|
120
|
+
cap.notifies.some(n => n.level === "warning"),
|
|
121
|
+
false,
|
|
122
|
+
"successful missing-row repair must not warn the user",
|
|
123
|
+
);
|
|
124
|
+
assert.deepEqual(cap.notifies, [
|
|
125
|
+
{
|
|
126
|
+
msg: "Milestone M001 context captured. Continuing the planning pipeline.",
|
|
127
|
+
level: "success",
|
|
128
|
+
},
|
|
129
|
+
]);
|
|
127
130
|
});
|
|
128
131
|
|
|
129
|
-
test("announces 'ready' when DB row
|
|
132
|
+
test("announces 'ready' when DB row has executable slices", () => {
|
|
130
133
|
base = mkBase();
|
|
131
134
|
openDatabase(":memory:");
|
|
132
135
|
insertMilestone({ id: "M001", title: "Ready Guard Test", status: "active" });
|
|
136
|
+
insertSlice({
|
|
137
|
+
id: "S01",
|
|
138
|
+
milestoneId: "M001",
|
|
139
|
+
title: "Executable Slice",
|
|
140
|
+
status: "pending",
|
|
141
|
+
});
|
|
133
142
|
|
|
134
143
|
cap = mkCapture();
|
|
135
144
|
setPendingAutoStart(base, {
|
|
136
145
|
basePath: base,
|
|
137
146
|
milestoneId: "M001",
|
|
147
|
+
startAuto: false,
|
|
138
148
|
ctx: mkCtx(cap),
|
|
139
149
|
pi: mkPi(cap),
|
|
140
150
|
});
|
|
@@ -9,6 +9,7 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
9
9
|
import { handleGSDCommand } from "../commands/dispatcher.ts";
|
|
10
10
|
import {
|
|
11
11
|
closeDatabase,
|
|
12
|
+
insertAssessment,
|
|
12
13
|
insertMilestone,
|
|
13
14
|
insertSlice,
|
|
14
15
|
openDatabase,
|
|
@@ -106,6 +107,13 @@ function seedRegisteredCompletedWorktreeWithoutRoadmap(base: string): void {
|
|
|
106
107
|
title: "Live Text Search",
|
|
107
108
|
status: "complete",
|
|
108
109
|
});
|
|
110
|
+
insertAssessment({
|
|
111
|
+
path: "milestones/M008/M008-VALIDATION.md",
|
|
112
|
+
milestoneId: "M008",
|
|
113
|
+
status: "pass",
|
|
114
|
+
scope: "milestone-validation",
|
|
115
|
+
fullContent: "verdict: pass",
|
|
116
|
+
});
|
|
109
117
|
writeFileSync(
|
|
110
118
|
join(base, ".gsd", "PREFERENCES.md"),
|
|
111
119
|
"---\ngit:\n isolation: worktree\n---\n",
|
|
@@ -124,6 +132,19 @@ function seedRegisteredCompletedWorktree(base: string): void {
|
|
|
124
132
|
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
125
133
|
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
126
134
|
insertMilestone({ id: "M008", title: "Live Text Search", status: "complete" });
|
|
135
|
+
insertSlice({
|
|
136
|
+
id: "S01",
|
|
137
|
+
milestoneId: "M008",
|
|
138
|
+
title: "Live Text Search",
|
|
139
|
+
status: "complete",
|
|
140
|
+
});
|
|
141
|
+
insertAssessment({
|
|
142
|
+
path: "milestones/M008/M008-VALIDATION.md",
|
|
143
|
+
milestoneId: "M008",
|
|
144
|
+
status: "pass",
|
|
145
|
+
scope: "milestone-validation",
|
|
146
|
+
fullContent: "verdict: pass",
|
|
147
|
+
});
|
|
127
148
|
writeWorktreePreferencesAndRoadmap(base);
|
|
128
149
|
|
|
129
150
|
const worktreePath = join(base, ".gsd", "worktrees", "M008");
|
|
@@ -14,7 +14,7 @@ import { execFileSync } from "node:child_process";
|
|
|
14
14
|
|
|
15
15
|
import { DISPATCH_RULES, resolveDispatch, type DispatchContext } from "../auto-dispatch.ts";
|
|
16
16
|
import { AutoSession } from "../auto/session.ts";
|
|
17
|
-
import { closeDatabase, insertMilestone, insertSlice, openDatabase } from "../gsd-db.ts";
|
|
17
|
+
import { closeDatabase, insertAssessment, insertGateRow, insertMilestone, insertSlice, openDatabase } from "../gsd-db.ts";
|
|
18
18
|
|
|
19
19
|
function makeBase(): string {
|
|
20
20
|
const base = mkdtempSync(join(tmpdir(), "gsd-complete-dispatch-"));
|
|
@@ -225,6 +225,14 @@ describe("complete phase dispatch guard (#5683)", () => {
|
|
|
225
225
|
base = makeBase();
|
|
226
226
|
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
227
227
|
insertMilestone({ id: "M001", title: "Milestone One", status: "complete" });
|
|
228
|
+
insertSlice({ milestoneId: "M001", id: "S01", title: "Done", status: "complete" });
|
|
229
|
+
insertAssessment({
|
|
230
|
+
path: "milestones/M001/M001-VALIDATION.md",
|
|
231
|
+
milestoneId: "M001",
|
|
232
|
+
status: "pass",
|
|
233
|
+
scope: "milestone-validation",
|
|
234
|
+
fullContent: "verdict: pass",
|
|
235
|
+
});
|
|
228
236
|
|
|
229
237
|
const ctx = buildDispatchCtx(base);
|
|
230
238
|
ctx.state.phase = "complete";
|
|
@@ -234,6 +242,37 @@ describe("complete phase dispatch guard (#5683)", () => {
|
|
|
234
242
|
assert.equal(result?.action, "stop");
|
|
235
243
|
assert.equal(result?.reason, "All milestones complete.");
|
|
236
244
|
});
|
|
245
|
+
|
|
246
|
+
test("blocks terminal stop when closed milestone still has pending gates", async () => {
|
|
247
|
+
base = makeBase();
|
|
248
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
249
|
+
insertMilestone({ id: "M001", title: "Milestone One", status: "complete" });
|
|
250
|
+
insertSlice({ milestoneId: "M001", id: "S01", title: "Done", status: "complete" });
|
|
251
|
+
insertAssessment({
|
|
252
|
+
path: "milestones/M001/M001-VALIDATION.md",
|
|
253
|
+
milestoneId: "M001",
|
|
254
|
+
status: "pass",
|
|
255
|
+
scope: "milestone-validation",
|
|
256
|
+
fullContent: "verdict: pass",
|
|
257
|
+
});
|
|
258
|
+
insertGateRow({
|
|
259
|
+
milestoneId: "M001",
|
|
260
|
+
sliceId: "S01",
|
|
261
|
+
gateId: "Q3",
|
|
262
|
+
scope: "slice",
|
|
263
|
+
status: "pending",
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const ctx = buildDispatchCtx(base);
|
|
267
|
+
ctx.state.phase = "complete";
|
|
268
|
+
|
|
269
|
+
const result = await rule.match(ctx);
|
|
270
|
+
|
|
271
|
+
assert.equal(result?.action, "stop");
|
|
272
|
+
assert.equal(result?.level, "warning");
|
|
273
|
+
assert.match(result?.reason ?? "", /closeout-consistency-blocked/);
|
|
274
|
+
assert.match(result?.reason ?? "", /quality gate Q3 is still pending/);
|
|
275
|
+
});
|
|
237
276
|
});
|
|
238
277
|
|
|
239
278
|
describe("complete milestone context recovery guard (#5831)", () => {
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* gsd-pi / guided-flow — regression tests for Gate 1b
|
|
2
|
+
* gsd-pi / guided-flow — regression tests for Gate 1b discussion handoff
|
|
3
3
|
*
|
|
4
|
-
* Gate 1b
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* → emit recovery hint directing the LLM to retry gsd_plan_milestone.
|
|
8
|
-
* (b) discuss-incomplete: discuss did not finish, no CONTEXT.md, DB row "queued".
|
|
9
|
-
* → silent block (no recovery hint).
|
|
4
|
+
* Gate 1b treats queued + pinned CONTEXT.md as Discussion Complete, Planning
|
|
5
|
+
* Pending. It must accept the handoff without warning the user or injecting a
|
|
6
|
+
* hidden gsd_plan_milestone retry.
|
|
10
7
|
*/
|
|
11
8
|
|
|
12
9
|
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
@@ -27,8 +24,6 @@ import {
|
|
|
27
24
|
insertMilestone,
|
|
28
25
|
} from "../gsd-db.ts";
|
|
29
26
|
|
|
30
|
-
// ─── Harness ───────────────────────────────────────────────────────────────
|
|
31
|
-
|
|
32
27
|
interface MockCapture {
|
|
33
28
|
notifies: Array<{ msg: string; level: string }>;
|
|
34
29
|
messages: Array<{ payload: any; options: any }>;
|
|
@@ -58,24 +53,26 @@ function mkPi(cap: MockCapture): any {
|
|
|
58
53
|
};
|
|
59
54
|
}
|
|
60
55
|
|
|
61
|
-
/**
|
|
62
|
-
* Create a minimal temp tree with a .gsd/milestones/M001 directory.
|
|
63
|
-
*/
|
|
64
56
|
function mkBase(): string {
|
|
65
57
|
const base = mkdtempSync(join(tmpdir(), "gsd-gate1b-"));
|
|
66
58
|
mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
|
|
67
59
|
return base;
|
|
68
60
|
}
|
|
69
61
|
|
|
70
|
-
|
|
62
|
+
function writeContext(base: string): void {
|
|
63
|
+
writeFileSync(
|
|
64
|
+
join(base, ".gsd", "milestones", "M001", "M001-CONTEXT.md"),
|
|
65
|
+
"# M001: Test Milestone\n\nContext written by discuss phase.\n",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
71
68
|
|
|
72
|
-
describe("Gate 1b
|
|
69
|
+
describe("Gate 1b discussion handoff in checkAutoStartAfterDiscuss", () => {
|
|
73
70
|
let base: string;
|
|
74
71
|
let cap: MockCapture;
|
|
75
72
|
|
|
76
73
|
beforeEach(() => {
|
|
77
74
|
clearPendingAutoStart();
|
|
78
|
-
drainLogs();
|
|
75
|
+
drainLogs();
|
|
79
76
|
});
|
|
80
77
|
|
|
81
78
|
afterEach(() => {
|
|
@@ -86,104 +83,59 @@ describe("Gate 1b orphan discrimination in checkAutoStartAfterDiscuss", () => {
|
|
|
86
83
|
}
|
|
87
84
|
});
|
|
88
85
|
|
|
89
|
-
test("
|
|
86
|
+
test("queued row + CONTEXT.md accepts context-captured handoff without hidden retry", () => {
|
|
90
87
|
base = mkBase();
|
|
91
88
|
openDatabase(":memory:");
|
|
92
|
-
|
|
93
|
-
// DB row exists with status "queued" (plan_milestone was blocked)
|
|
94
89
|
insertMilestone({ id: "M001", title: "Test Milestone", status: "queued" });
|
|
95
|
-
|
|
96
|
-
// CONTEXT.md on disk (discuss phase completed)
|
|
97
|
-
writeFileSync(
|
|
98
|
-
join(base, ".gsd", "milestones", "M001", "M001-CONTEXT.md"),
|
|
99
|
-
"# M001: Test Milestone\n\nContext written by discuss phase.\n",
|
|
100
|
-
);
|
|
90
|
+
writeContext(base);
|
|
101
91
|
|
|
102
92
|
cap = mkCapture();
|
|
103
93
|
setPendingAutoStart(base, {
|
|
104
94
|
basePath: base,
|
|
105
95
|
milestoneId: "M001",
|
|
96
|
+
startAuto: false,
|
|
106
97
|
ctx: mkCtx(cap),
|
|
107
98
|
pi: mkPi(cap),
|
|
108
99
|
});
|
|
109
100
|
|
|
110
101
|
const result = checkAutoStartAfterDiscuss();
|
|
111
102
|
|
|
112
|
-
|
|
113
|
-
assert.equal(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
"exactly one sendMessage call expected for the recovery hint",
|
|
120
|
-
);
|
|
121
|
-
assert.equal(
|
|
122
|
-
cap.messages[0].payload.customType,
|
|
123
|
-
"gsd-plan-milestone-blocked-recovery",
|
|
124
|
-
"recovery message must have customType gsd-plan-milestone-blocked-recovery",
|
|
125
|
-
);
|
|
103
|
+
assert.equal(result, true, "queued + context is a valid planning-pending handoff");
|
|
104
|
+
assert.equal(cap.messages.length, 0, "must not inject a hidden recovery turn");
|
|
105
|
+
assert.equal(cap.notifies.length, 1, "must emit one success notification");
|
|
106
|
+
assert.deepEqual(cap.notifies[0], {
|
|
107
|
+
msg: "Milestone M001 context captured. Continuing the planning pipeline.",
|
|
108
|
+
level: "success",
|
|
109
|
+
});
|
|
126
110
|
assert.equal(
|
|
127
|
-
cap.
|
|
128
|
-
|
|
129
|
-
"
|
|
130
|
-
);
|
|
131
|
-
assert.match(
|
|
132
|
-
cap.messages[0].payload.content,
|
|
133
|
-
/gsd_plan_milestone/,
|
|
134
|
-
"recovery message content must mention gsd_plan_milestone",
|
|
111
|
+
cap.notifies.some(n => /queued|gsd_plan_milestone/i.test(n.msg)),
|
|
112
|
+
false,
|
|
113
|
+
"user-visible copy must not mention queued state or internal plan tool retry",
|
|
135
114
|
);
|
|
136
|
-
|
|
137
|
-
// User must be notified via ctx.ui.notify
|
|
138
|
-
assert.ok(
|
|
139
|
-
cap.notifies.some((n) => n.level === "warning" && /queued/.test(n.msg)),
|
|
140
|
-
"user must be notified with a warning about the queued state",
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
// logWarning must have recorded the Gate 1b event
|
|
144
|
-
const logs = drainLogs();
|
|
145
|
-
const gate1bLog = logs.find(
|
|
146
|
-
(e) => e.component === "guided" && /Gate 1b/.test(e.message),
|
|
147
|
-
);
|
|
148
|
-
assert.ok(gate1bLog, "Gate 1b warning must be logged via logWarning");
|
|
149
115
|
});
|
|
150
116
|
|
|
151
|
-
test("
|
|
117
|
+
test("queued row without CONTEXT.md still waits silently for discussion output", () => {
|
|
152
118
|
base = mkBase();
|
|
153
119
|
openDatabase(":memory:");
|
|
154
|
-
|
|
155
|
-
// DB row exists with status "queued", but NO CONTEXT.md on disk
|
|
156
120
|
insertMilestone({ id: "M001", title: "Test Milestone", status: "queued" });
|
|
157
121
|
|
|
158
|
-
// No CONTEXT.md written — discuss phase is incomplete
|
|
159
122
|
cap = mkCapture();
|
|
160
123
|
setPendingAutoStart(base, {
|
|
161
124
|
basePath: base,
|
|
162
125
|
milestoneId: "M001",
|
|
126
|
+
startAuto: false,
|
|
163
127
|
ctx: mkCtx(cap),
|
|
164
128
|
pi: mkPi(cap),
|
|
165
129
|
});
|
|
166
130
|
|
|
167
|
-
drainLogs();
|
|
131
|
+
drainLogs();
|
|
168
132
|
|
|
169
133
|
const result = checkAutoStartAfterDiscuss();
|
|
170
134
|
|
|
171
|
-
|
|
172
|
-
assert.equal(
|
|
173
|
-
|
|
174
|
-
// No recovery hint — Gate 1 blocks before Gate 1b is reached
|
|
175
|
-
assert.equal(
|
|
176
|
-
cap.messages.length,
|
|
177
|
-
0,
|
|
178
|
-
"no sendMessage calls expected when CONTEXT.md is absent",
|
|
179
|
-
);
|
|
180
|
-
assert.equal(
|
|
181
|
-
cap.notifies.length,
|
|
182
|
-
0,
|
|
183
|
-
"no user notifications expected for discuss-incomplete case",
|
|
184
|
-
);
|
|
135
|
+
assert.equal(result, false, "must keep waiting while discuss has not written context");
|
|
136
|
+
assert.equal(cap.messages.length, 0, "no hidden recovery turn expected");
|
|
137
|
+
assert.equal(cap.notifies.length, 0, "no user notifications expected");
|
|
185
138
|
|
|
186
|
-
// No Gate 1b log entry
|
|
187
139
|
const logs = drainLogs();
|
|
188
140
|
const gate1bLog = logs.find(
|
|
189
141
|
(e) => e.component === "guided" && /Gate 1b/.test(e.message),
|
|
@@ -178,7 +178,7 @@ test("checkAutoStartAfterDiscuss(basePath) selects the matching pending entry wh
|
|
|
178
178
|
}
|
|
179
179
|
});
|
|
180
180
|
|
|
181
|
-
test("checkAutoStartAfterDiscuss can
|
|
181
|
+
test("checkAutoStartAfterDiscuss can accept context handoff without scheduling auto-start", () => {
|
|
182
182
|
const base = mkdtempSync(join(tmpdir(), "gsd-auto-start-headless-owned-"));
|
|
183
183
|
let waitForIdleCalls = 0;
|
|
184
184
|
const notifications: string[] = [];
|
|
@@ -209,9 +209,11 @@ test("checkAutoStartAfterDiscuss can emit ready without scheduling auto-start",
|
|
|
209
209
|
});
|
|
210
210
|
|
|
211
211
|
assert.equal(checkAutoStartAfterDiscuss(base), true);
|
|
212
|
-
assert.deepEqual(notifications, [
|
|
212
|
+
assert.deepEqual(notifications, [
|
|
213
|
+
"Milestone M001 context captured. Continuing the planning pipeline.",
|
|
214
|
+
]);
|
|
213
215
|
assert.equal(waitForIdleCalls, 0, "headless-owned auto start must not schedule guided-flow auto");
|
|
214
|
-
assert.equal(getDiscussionMilestoneId(base), null, "
|
|
216
|
+
assert.equal(getDiscussionMilestoneId(base), null, "accepted handoff should still be cleared");
|
|
215
217
|
} finally {
|
|
216
218
|
clearPendingAutoStart();
|
|
217
219
|
rmSync(base, { recursive: true, force: true });
|
|
@@ -135,7 +135,13 @@ describe("guided-flow STATE.md rebuild (#3475)", () => {
|
|
|
135
135
|
|
|
136
136
|
assert.equal(accepted, true);
|
|
137
137
|
assert.equal(notifications.some(n => n.level === "error" && n.message.includes("no DB row exists")), false);
|
|
138
|
-
assert.ok(
|
|
138
|
+
assert.ok(
|
|
139
|
+
notifications.some(
|
|
140
|
+
n =>
|
|
141
|
+
n.level === "success" &&
|
|
142
|
+
n.message.includes("Milestone M001 context captured. Continuing the planning pipeline."),
|
|
143
|
+
),
|
|
144
|
+
);
|
|
139
145
|
clearPendingAutoStart(base);
|
|
140
146
|
});
|
|
141
147
|
|
|
@@ -157,13 +163,13 @@ describe("guided-flow STATE.md rebuild (#3475)", () => {
|
|
|
157
163
|
};
|
|
158
164
|
setPendingAutoStart(base, { basePath: base, milestoneId: "M001", ctx: ctx as any, pi: {} as any });
|
|
159
165
|
|
|
160
|
-
for (let i = 0; i <
|
|
166
|
+
for (let i = 0; i < 3; i += 1) {
|
|
161
167
|
assert.equal(checkAutoStartAfterDiscuss(), false);
|
|
162
168
|
}
|
|
163
169
|
|
|
164
170
|
assert.equal(
|
|
165
|
-
notifications.filter(n => n.level === "warning"
|
|
166
|
-
|
|
171
|
+
notifications.filter(n => n.level === "warning").length,
|
|
172
|
+
0,
|
|
167
173
|
);
|
|
168
174
|
assert.equal(
|
|
169
175
|
notifications.filter(n => n.level === "error" && n.message.includes("DB row recovery failed")).length,
|
|
@@ -171,4 +177,34 @@ describe("guided-flow STATE.md rebuild (#3475)", () => {
|
|
|
171
177
|
);
|
|
172
178
|
clearPendingAutoStart(base);
|
|
173
179
|
});
|
|
180
|
+
|
|
181
|
+
test("checkAutoStartAfterDiscuss does not double-notify on 4th+ call after recovery limit", () => {
|
|
182
|
+
base = createFixtureBase();
|
|
183
|
+
openDatabase(":memory:");
|
|
184
|
+
const db = _getAdapter();
|
|
185
|
+
assert.ok(db, "database should be open");
|
|
186
|
+
db.exec("DROP TABLE milestones");
|
|
187
|
+
db.exec("CREATE TABLE milestones (id TEXT PRIMARY KEY)");
|
|
188
|
+
|
|
189
|
+
writeFile(base, "milestones/M001/M001-CONTEXT.md", "# M001: Planned\n");
|
|
190
|
+
writeFile(base, "STATE.md", "# GSD State\n\n**Active Milestone:** M001: Planned\n");
|
|
191
|
+
|
|
192
|
+
const notifications: Array<{ message: string; level: string }> = [];
|
|
193
|
+
const ctx = {
|
|
194
|
+
ui: { notify: (message: string, level: string) => notifications.push({ message, level }) },
|
|
195
|
+
waitForIdle: () => new Promise<void>(() => {}),
|
|
196
|
+
};
|
|
197
|
+
setPendingAutoStart(base, { basePath: base, milestoneId: "M001", ctx: ctx as any, pi: {} as any });
|
|
198
|
+
|
|
199
|
+
for (let i = 0; i < 5; i += 1) {
|
|
200
|
+
assert.equal(checkAutoStartAfterDiscuss(), false);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
assert.equal(
|
|
204
|
+
notifications.filter(n => n.level === "error" && n.message.includes("DB row recovery failed")).length,
|
|
205
|
+
1,
|
|
206
|
+
"user must see exactly one error notification even after repeated calls past the recovery limit",
|
|
207
|
+
);
|
|
208
|
+
clearPendingAutoStart(base);
|
|
209
|
+
});
|
|
174
210
|
});
|
package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts
CHANGED
|
@@ -29,6 +29,7 @@ import { nativeMergeSquash } from "../../native-git-bridge.ts";
|
|
|
29
29
|
import { drainLogs, setStderrLoggingEnabled } from "../../workflow-logger.ts";
|
|
30
30
|
import {
|
|
31
31
|
closeDatabase,
|
|
32
|
+
insertAssessment,
|
|
32
33
|
insertMilestone,
|
|
33
34
|
insertSlice,
|
|
34
35
|
insertTask,
|
|
@@ -207,6 +208,13 @@ describe("auto-worktree-milestone-merge", { timeout: 300_000 }, () => {
|
|
|
207
208
|
});
|
|
208
209
|
}
|
|
209
210
|
}
|
|
211
|
+
insertAssessment({
|
|
212
|
+
path: "milestones/M020/M020-VALIDATION.md",
|
|
213
|
+
milestoneId: "M020",
|
|
214
|
+
status: "pass",
|
|
215
|
+
scope: "milestone-validation",
|
|
216
|
+
fullContent: "verdict: pass",
|
|
217
|
+
});
|
|
210
218
|
|
|
211
219
|
const roadmap = makeRoadmap("M020", "Backend foundation", [
|
|
212
220
|
{ id: "S01", title: "Core API" },
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
import {
|
|
42
42
|
openDatabase,
|
|
43
43
|
closeDatabase,
|
|
44
|
+
insertAssessment,
|
|
44
45
|
insertMilestone,
|
|
45
46
|
insertSlice,
|
|
46
47
|
updateMilestoneStatus,
|
|
@@ -347,6 +348,13 @@ test("mergeCompletedMilestone — synthesizes roadmap from DB when projection is
|
|
|
347
348
|
title: "Search Bar",
|
|
348
349
|
status: "complete",
|
|
349
350
|
});
|
|
351
|
+
insertAssessment({
|
|
352
|
+
path: "milestones/M010/M010-VALIDATION.md",
|
|
353
|
+
milestoneId: "M010",
|
|
354
|
+
status: "pass",
|
|
355
|
+
scope: "milestone-validation",
|
|
356
|
+
fullContent: "verdict: pass",
|
|
357
|
+
});
|
|
350
358
|
|
|
351
359
|
createMilestoneBranch(repo, "M010", [
|
|
352
360
|
{ name: "app.js", content: "export const app = true;\n" },
|
|
@@ -671,6 +679,14 @@ function setupCanonicalDbWithWorktree(basePath: string, mid: string): void {
|
|
|
671
679
|
const dbPath = join(basePath, ".gsd", "gsd.db");
|
|
672
680
|
openDatabase(dbPath);
|
|
673
681
|
insertMilestone({ id: mid, title: `Milestone ${mid}`, status: "complete" });
|
|
682
|
+
insertSlice({ id: "S01", milestoneId: mid, title: "Done Slice", status: "complete" });
|
|
683
|
+
insertAssessment({
|
|
684
|
+
path: `milestones/${mid}/${mid}-VALIDATION.md`,
|
|
685
|
+
milestoneId: mid,
|
|
686
|
+
status: "pass",
|
|
687
|
+
scope: "milestone-validation",
|
|
688
|
+
fullContent: "verdict: pass",
|
|
689
|
+
});
|
|
674
690
|
updateMilestoneStatus(mid, "complete", new Date().toISOString());
|
|
675
691
|
closeDatabase();
|
|
676
692
|
}
|
|
@@ -11,6 +11,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
11
11
|
import { extractUatType } from '../../files.ts';
|
|
12
12
|
import { resolveSliceFile } from '../../paths.ts';
|
|
13
13
|
import { buildRunUatPrompt, checkNeedsRunUat } from '../../auto-prompts.ts';
|
|
14
|
+
import { buildRunUatResultPresentation, RUN_UAT_TOOL_PRESENTATION_PLAN_ID } from '../../tool-presentation-plan.ts';
|
|
14
15
|
|
|
15
16
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
17
|
const worktreePromptsDir = join(__dirname, '../..', 'prompts');
|
|
@@ -20,6 +21,8 @@ function loadPromptFromWorktree(name: string, vars: Record<string, string> = {})
|
|
|
20
21
|
let content = readFileSync(path, 'utf-8');
|
|
21
22
|
const effectiveVars = {
|
|
22
23
|
skillActivation: 'If no installed skill clearly matches this unit, skip explicit skill activation and continue with the required workflow.',
|
|
24
|
+
canonicalPresentation: JSON.stringify(buildRunUatResultPresentation(), null, 2),
|
|
25
|
+
toolPresentationPlanId: RUN_UAT_TOOL_PRESENTATION_PLAN_ID,
|
|
23
26
|
...vars,
|
|
24
27
|
};
|
|
25
28
|
for (const [key, value] of Object.entries(effectiveVars)) {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Project/App: gsd-pi
|
|
2
|
+
// File Purpose: Regression tests for DB-backed closeout consistency before merge.
|
|
3
|
+
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { existsSync, mkdtempSync, mkdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
|
|
11
|
+
import { mergeMilestoneToMain } from "../auto-worktree.ts";
|
|
12
|
+
import { closeDatabase, insertMilestone, openDatabase } from "../gsd-db.ts";
|
|
13
|
+
|
|
14
|
+
function git(args: string[], cwd: string): string {
|
|
15
|
+
return execFileSync("git", args, {
|
|
16
|
+
cwd,
|
|
17
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
18
|
+
encoding: "utf-8",
|
|
19
|
+
}).trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createRepo(): string {
|
|
23
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "merge-closeout-gate-")));
|
|
24
|
+
git(["init"], dir);
|
|
25
|
+
git(["config", "user.email", "test@test.com"], dir);
|
|
26
|
+
git(["config", "user.name", "Test"], dir);
|
|
27
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
28
|
+
writeFileSync(join(dir, "README.md"), "# test\n");
|
|
29
|
+
git(["add", "."], dir);
|
|
30
|
+
git(["commit", "-m", "init"], dir);
|
|
31
|
+
git(["branch", "-M", "main"], dir);
|
|
32
|
+
|
|
33
|
+
git(["checkout", "-b", "milestone/M001"], dir);
|
|
34
|
+
writeFileSync(join(dir, "feature.ts"), "export const feature = true;\n");
|
|
35
|
+
git(["add", "feature.ts"], dir);
|
|
36
|
+
git(["commit", "-m", "feat: milestone work"], dir);
|
|
37
|
+
git(["checkout", "main"], dir);
|
|
38
|
+
return dir;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test("mergeMilestoneToMain blocks when project DB closeout is still open", () => {
|
|
42
|
+
const savedCwd = process.cwd();
|
|
43
|
+
const repo = createRepo();
|
|
44
|
+
try {
|
|
45
|
+
assert.equal(openDatabase(join(repo, ".gsd", "gsd.db")), true);
|
|
46
|
+
insertMilestone({ id: "M001", title: "Milestone One", status: "active" });
|
|
47
|
+
|
|
48
|
+
const mainHeadBefore = git(["rev-parse", "main"], repo);
|
|
49
|
+
process.chdir(repo);
|
|
50
|
+
|
|
51
|
+
assert.throws(
|
|
52
|
+
() => mergeMilestoneToMain(repo, "M001", "# M001\n- [x] **S01: Done**\n"),
|
|
53
|
+
/closeout-consistency-blocked/,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
assert.equal(git(["rev-parse", "main"], repo), mainHeadBefore);
|
|
57
|
+
assert.equal(git(["branch", "--show-current"], repo), "main");
|
|
58
|
+
} finally {
|
|
59
|
+
closeDatabase();
|
|
60
|
+
process.chdir(savedCwd);
|
|
61
|
+
if (existsSync(repo)) rmSync(repo, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
});
|