@opengsd/gsd-pi 1.3.0-dev.65546769 → 1.3.0-dev.eed73bea
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/claude-code-cli/stream-adapter.js +11 -2
- package/dist/resources/extensions/google-cli/stream-adapter.js +82 -15
- package/dist/resources/extensions/gsd/auto/orchestrator.js +12 -3
- package/dist/resources/extensions/gsd/auto-dispatch.js +17 -14
- package/dist/resources/extensions/gsd/auto-prompts.js +43 -12
- package/dist/resources/extensions/gsd/auto-recovery.js +13 -6
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +103 -13
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +6 -1
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +8 -3
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +46 -19
- package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +75 -1
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +1 -1
- package/dist/resources/extensions/gsd/commands-context.js +19 -1
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +16 -10
- package/dist/resources/extensions/gsd/commands-worktree.js +12 -10
- package/dist/resources/extensions/gsd/dashboard-overlay.js +32 -3
- package/dist/resources/extensions/gsd/db/queries.js +60 -0
- package/dist/resources/extensions/gsd/doctor-providers.js +92 -8
- package/dist/resources/extensions/gsd/exec-sandbox.js +45 -9
- package/dist/resources/extensions/gsd/forensics.js +2 -32
- package/dist/resources/extensions/gsd/git-service.js +4 -4
- package/dist/resources/extensions/gsd/guided-flow-queue.js +59 -5
- package/dist/resources/extensions/gsd/health-widget.js +55 -29
- package/dist/resources/extensions/gsd/markdown-renderer.js +6 -2
- package/dist/resources/extensions/gsd/memory-consolidation-scanner.js +44 -21
- package/dist/resources/extensions/gsd/milestone-implementation-evidence.js +26 -20
- package/dist/resources/extensions/gsd/quick.js +45 -2
- package/dist/resources/extensions/gsd/session-forensics.js +11 -1
- package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-render.js +52 -3
- package/dist/resources/extensions/gsd/tools/complete-slice.js +34 -3
- package/dist/resources/extensions/gsd/tools/complete-task.js +78 -16
- package/dist/resources/extensions/gsd/tools/exec-tool.js +7 -2
- package/dist/resources/extensions/gsd/unit-context-composer.js +23 -7
- package/dist/resources/extensions/gsd/unit-registry.js +25 -3
- package/dist/resources/extensions/gsd/unmerged-milestone-guard.js +33 -3
- package/dist/resources/extensions/gsd/validation-block-guard.js +9 -4
- package/dist/resources/extensions/gsd/workspace-git-preflight.js +30 -1
- 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 +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/react-loadable-manifest.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/api/visualizer/route.js +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/middleware-react-loadable-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/.next/static/chunks/{796.e0bdc932325d7e03.js → 796.3976108148518f7d.js} +3 -3
- package/dist/web/standalone/.next/static/chunks/{webpack-f46ea08200a0227e.js → webpack-7c1d97e39be2da11.js} +1 -1
- package/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/dist/workflow.d.ts +1 -0
- package/packages/contracts/dist/workflow.d.ts.map +1 -1
- package/packages/contracts/dist/workflow.js +2 -0
- package/packages/contracts/dist/workflow.js.map +1 -1
- 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/controllers/chat-controller.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +21 -9
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/README.md +1 -1
- package/packages/mcp-server/dist/server.d.ts +1 -1
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +3 -3
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts +13 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +34 -20
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +20 -2
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +80 -0
- package/src/resources/extensions/google-cli/stream-adapter.ts +106 -19
- package/src/resources/extensions/gsd/auto/orchestrator.ts +25 -11
- package/src/resources/extensions/gsd/auto-dispatch.ts +18 -17
- package/src/resources/extensions/gsd/auto-prompts.ts +54 -12
- package/src/resources/extensions/gsd/auto-recovery.ts +13 -6
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +125 -12
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +6 -1
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +9 -3
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +52 -18
- package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +82 -1
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +1 -1
- package/src/resources/extensions/gsd/commands-context.ts +18 -1
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +14 -9
- package/src/resources/extensions/gsd/commands-worktree.ts +12 -10
- package/src/resources/extensions/gsd/dashboard-overlay.ts +32 -3
- package/src/resources/extensions/gsd/db/queries.ts +79 -0
- package/src/resources/extensions/gsd/doctor-providers.ts +103 -9
- package/src/resources/extensions/gsd/exec-sandbox.ts +49 -9
- package/src/resources/extensions/gsd/forensics.ts +2 -33
- package/src/resources/extensions/gsd/git-service.ts +5 -5
- package/src/resources/extensions/gsd/guided-flow-queue.ts +82 -4
- package/src/resources/extensions/gsd/health-widget.ts +69 -32
- package/src/resources/extensions/gsd/markdown-renderer.ts +6 -1
- package/src/resources/extensions/gsd/memory-consolidation-scanner.ts +51 -19
- package/src/resources/extensions/gsd/milestone-implementation-evidence.ts +35 -21
- package/src/resources/extensions/gsd/quick.ts +43 -2
- package/src/resources/extensions/gsd/session-forensics.ts +11 -1
- package/src/resources/extensions/gsd/state-reconciliation/drift/stale-render.ts +76 -8
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +111 -1
- package/src/resources/extensions/gsd/tests/commands-context.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/commands-worktree-clean.test.ts +80 -0
- package/src/resources/extensions/gsd/tests/complete-slice.test.ts +11 -0
- package/src/resources/extensions/gsd/tests/complete-task-rollback-evidence.test.ts +48 -8
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +75 -0
- package/src/resources/extensions/gsd/tests/dashboard-overlay.test.ts +55 -2
- package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +26 -1
- package/src/resources/extensions/gsd/tests/doctor-forensics-db-open-regression.test.ts +70 -2
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/exec-graceful-kill.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/exec-tool.test.ts +45 -1
- package/src/resources/extensions/gsd/tests/forensics-error-filter.test.ts +88 -0
- package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/health-widget.test.ts +268 -3
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +119 -1
- package/src/resources/extensions/gsd/tests/integration/queue-active-milestone-context-budget.test.ts +93 -0
- package/src/resources/extensions/gsd/tests/integration/quick-branch-lifecycle.test.ts +56 -9
- package/src/resources/extensions/gsd/tests/knowledge-cold-start.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/memory-consolidation-scanner.test.ts +78 -0
- package/src/resources/extensions/gsd/tests/orchestrator-logs.test.ts +43 -1
- package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +54 -1
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +195 -1
- package/src/resources/extensions/gsd/tests/read-uat-gate-verdict.test.ts +185 -0
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +76 -0
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +68 -0
- package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +193 -14
- package/src/resources/extensions/gsd/tests/unmerged-milestone-guard.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/validation-block-guard.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/verify-artifact-tightened.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/workspace-git-preflight.test.ts +151 -2
- package/src/resources/extensions/gsd/tools/complete-slice.ts +30 -3
- package/src/resources/extensions/gsd/tools/complete-task.ts +86 -16
- package/src/resources/extensions/gsd/tools/exec-tool.ts +7 -3
- package/src/resources/extensions/gsd/unit-context-composer.ts +33 -7
- package/src/resources/extensions/gsd/unit-registry.ts +25 -3
- package/src/resources/extensions/gsd/unmerged-milestone-guard.ts +41 -5
- package/src/resources/extensions/gsd/validation-block-guard.ts +13 -7
- package/src/resources/extensions/gsd/workspace-git-preflight.ts +31 -0
- /package/dist/web/standalone/.next/static/{BTKtGFF1Y-hvVJEGhBRo9 → SzEuqWX37DR9MEpEuQjP1}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{BTKtGFF1Y-hvVJEGhBRo9 → SzEuqWX37DR9MEpEuQjP1}/_ssgManifest.js +0 -0
|
@@ -8,6 +8,7 @@ import { join } from "node:path";
|
|
|
8
8
|
import { tmpdir } from "node:os";
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
|
+
CONTEXT_MODE_GUIDANCE_BY_UNIT,
|
|
11
12
|
composeContractedUnitContext,
|
|
12
13
|
composeContextModeInstructions,
|
|
13
14
|
composeInlinedContext,
|
|
@@ -141,9 +142,13 @@ test("Context Mode composer: nested output is compact single sentence", () => {
|
|
|
141
142
|
assert.ok(!out.startsWith("## Context Mode"));
|
|
142
143
|
assert.match(out, /^Context Mode \(verification lane\): /);
|
|
143
144
|
assert.strictEqual(out.split(/\n/).length, 1);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
assert.
|
|
145
|
+
// Nested guidance is embedded into tester subagent prompts — it must instruct the tester
|
|
146
|
+
// to run verification and call gsd_save_gate_result, NOT to dispatch further subagents.
|
|
147
|
+
assert.doesNotMatch(out, /`subagent`/, "tester prompts must not be told to dispatch subagents");
|
|
148
|
+
assert.match(out, /`gsd_save_gate_result`/);
|
|
149
|
+
assert.doesNotMatch(out, /`gsd_exec`/);
|
|
150
|
+
assert.doesNotMatch(out, /`gsd_exec_search`/);
|
|
151
|
+
assert.doesNotMatch(out, /`gsd_resume`/);
|
|
147
152
|
assert.ok(out.length < 240, `nested guidance should stay compact, got ${out.length} chars`);
|
|
148
153
|
});
|
|
149
154
|
|
|
@@ -158,6 +163,53 @@ const laneLabelByMode: Record<string, string> = {
|
|
|
158
163
|
triage: "triage",
|
|
159
164
|
};
|
|
160
165
|
|
|
166
|
+
const contextModeGuidanceOverrideExpectedTools: Record<string, readonly string[]> = {
|
|
167
|
+
"discuss-milestone": [
|
|
168
|
+
"ask_user_questions",
|
|
169
|
+
"gsd_summary_save",
|
|
170
|
+
"gsd_decision_save",
|
|
171
|
+
"gsd_requirement_save",
|
|
172
|
+
"gsd_requirement_update",
|
|
173
|
+
"gsd_plan_milestone",
|
|
174
|
+
"gsd_milestone_generate_id",
|
|
175
|
+
],
|
|
176
|
+
"discuss-project": [
|
|
177
|
+
"ask_user_questions",
|
|
178
|
+
"gsd_summary_save",
|
|
179
|
+
"gsd_decision_save",
|
|
180
|
+
"gsd_requirement_save",
|
|
181
|
+
],
|
|
182
|
+
"discuss-requirements": [
|
|
183
|
+
"ask_user_questions",
|
|
184
|
+
"gsd_requirement_save",
|
|
185
|
+
"gsd_summary_save",
|
|
186
|
+
],
|
|
187
|
+
"discuss-slice": [
|
|
188
|
+
"ask_user_questions",
|
|
189
|
+
"gsd_summary_save",
|
|
190
|
+
"gsd_decision_save",
|
|
191
|
+
],
|
|
192
|
+
"replan-slice": [
|
|
193
|
+
"gsd_replan_slice",
|
|
194
|
+
"gsd_decision_save",
|
|
195
|
+
],
|
|
196
|
+
"reassess-roadmap": [
|
|
197
|
+
"gsd_milestone_status",
|
|
198
|
+
"gsd_reassess_roadmap",
|
|
199
|
+
],
|
|
200
|
+
"run-uat": [
|
|
201
|
+
"gsd_uat_exec",
|
|
202
|
+
"gsd_resume",
|
|
203
|
+
],
|
|
204
|
+
// research-project uses scout subagents that write .gsd/research/ files directly;
|
|
205
|
+
// the parent dispatches Task calls and verifies file outputs — no GSD save tools.
|
|
206
|
+
"research-project": [],
|
|
207
|
+
"gate-evaluate": [
|
|
208
|
+
"subagent",
|
|
209
|
+
"gsd_save_gate_result",
|
|
210
|
+
],
|
|
211
|
+
};
|
|
212
|
+
|
|
161
213
|
test("Context Mode composer: every known eligible unit renders its configured lane and required tools", () => {
|
|
162
214
|
for (const unitType of KNOWN_UNIT_TYPES) {
|
|
163
215
|
const manifest = UNIT_MANIFESTS[unitType];
|
|
@@ -170,18 +222,65 @@ test("Context Mode composer: every known eligible unit renders its configured la
|
|
|
170
222
|
assert.ok(out.startsWith("## Context Mode"), `${unitType} should render standalone Context Mode heading`);
|
|
171
223
|
assert.match(out, new RegExp(`Lane: \\*\\*${laneLabelByMode[manifest.contextMode]} lane\\*\\*\\.`, "i"));
|
|
172
224
|
const forbidden = getUnitToolSurfaceContract(unitType)?.forbiddenGsdTools ?? {};
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
assert.doesNotMatch(out, /`gsd_exec`/, `${unitType}
|
|
225
|
+
const overrideExpectedTools = contextModeGuidanceOverrideExpectedTools[unitType];
|
|
226
|
+
if ("gsd_exec" in forbidden || overrideExpectedTools) {
|
|
227
|
+
// Unit overrides are the contract-specific exception to lane defaults.
|
|
228
|
+
// Steering to the lane default here can produce unavailable-tool loops.
|
|
229
|
+
assert.doesNotMatch(out, /`gsd_exec`/, `${unitType} guidance must not steer to gsd_exec`);
|
|
178
230
|
assert.doesNotMatch(out, /`gsd_exec_search`/, `${unitType} guidance must not steer to gsd_exec_search`);
|
|
179
|
-
|
|
231
|
+
for (const toolName of overrideExpectedTools ?? []) {
|
|
232
|
+
assert.match(out, new RegExp(`\`${toolName}\``), `${unitType} guidance should mention ${toolName}`);
|
|
233
|
+
}
|
|
180
234
|
} else {
|
|
181
235
|
assert.match(out, /`gsd_exec`/, `${unitType} should mention gsd_exec`);
|
|
182
236
|
assert.match(out, /`gsd_exec_search`/, `${unitType} should mention gsd_exec_search`);
|
|
183
237
|
}
|
|
184
|
-
|
|
238
|
+
if (!overrideExpectedTools || overrideExpectedTools.includes("gsd_resume")) {
|
|
239
|
+
assert.match(out, /`gsd_resume`/, `${unitType} should mention gsd_resume`);
|
|
240
|
+
} else {
|
|
241
|
+
assert.doesNotMatch(out, /`gsd_resume`/, `${unitType} guidance must not steer to gsd_resume`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("Context Mode composer: discuss interview overrides stay within unit contracts", () => {
|
|
247
|
+
const discussUnits = [
|
|
248
|
+
"discuss-milestone",
|
|
249
|
+
"discuss-project",
|
|
250
|
+
"discuss-requirements",
|
|
251
|
+
"discuss-slice",
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
for (const unitType of discussUnits) {
|
|
255
|
+
const guidance = CONTEXT_MODE_GUIDANCE_BY_UNIT[unitType];
|
|
256
|
+
assert.ok(guidance, `${unitType} should have a Context Mode override`);
|
|
257
|
+
assert.doesNotMatch(guidance, /`gsd_exec`/, `${unitType} guidance must not mention gsd_exec`);
|
|
258
|
+
assert.doesNotMatch(guidance, /`gsd_exec_search`/, `${unitType} guidance must not mention gsd_exec_search`);
|
|
259
|
+
assert.doesNotMatch(guidance, /`gsd_resume`/, `${unitType} guidance must not mention gsd_resume`);
|
|
260
|
+
|
|
261
|
+
const expectedTools = contextModeGuidanceOverrideExpectedTools[unitType] ?? [];
|
|
262
|
+
assert.ok(expectedTools.length > 0, `${unitType} should declare expected override tools`);
|
|
263
|
+
const contract = getUnitToolSurfaceContract(unitType);
|
|
264
|
+
assert.ok(contract, `${unitType} should have a tool contract`);
|
|
265
|
+
const contractTools = new Set([
|
|
266
|
+
...contract.allowedGsdTools,
|
|
267
|
+
...contract.requiredWorkflowTools,
|
|
268
|
+
]);
|
|
269
|
+
|
|
270
|
+
for (const toolName of expectedTools) {
|
|
271
|
+
assert.match(guidance, new RegExp(`\`${toolName}\``), `${unitType} guidance should mention ${toolName}`);
|
|
272
|
+
assert.ok(contractTools.has(toolName as UnitGsdToolName), `${unitType} contract should allow ${toolName}`);
|
|
273
|
+
const scope = shouldBlockAutoUnitToolCall(unitType, toolName);
|
|
274
|
+
assert.equal(scope.block, false, `${unitType} should not hard-block ${toolName}: ${scope.reason ?? ""}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const out = composeContextModeInstructions(unitType, { enabled: true, renderMode: "standalone" });
|
|
278
|
+
if (out) {
|
|
279
|
+
assert.match(out, /interview lane/i);
|
|
280
|
+
assert.doesNotMatch(out, /`gsd_exec`/);
|
|
281
|
+
assert.doesNotMatch(out, /`gsd_exec_search`/);
|
|
282
|
+
assert.doesNotMatch(out, /`gsd_resume`/);
|
|
283
|
+
}
|
|
185
284
|
}
|
|
186
285
|
});
|
|
187
286
|
|
|
@@ -195,8 +294,67 @@ test("Context Mode composer: run-uat guidance steers to gsd_uat_exec in both ren
|
|
|
195
294
|
assert.doesNotMatch(standalone, /`gsd_exec`/);
|
|
196
295
|
});
|
|
197
296
|
|
|
198
|
-
test("Context Mode composer:
|
|
199
|
-
const
|
|
297
|
+
test("Context Mode composer: research-project guidance steers to scout orchestration", () => {
|
|
298
|
+
for (const renderMode of ["nested", "standalone"] as const) {
|
|
299
|
+
const out = composeContextModeInstructions("research-project", { enabled: true, renderMode });
|
|
300
|
+
assert.match(out, /research lane/i);
|
|
301
|
+
assert.match(out, /scout subagents/i);
|
|
302
|
+
assert.match(out, /\.gsd\/research\//);
|
|
303
|
+
assert.match(out, /STACK\.md/);
|
|
304
|
+
assert.match(out, /PITFALLS\.md/);
|
|
305
|
+
assert.doesNotMatch(out, /`gsd_summary_save`/);
|
|
306
|
+
assert.doesNotMatch(out, /`gsd_decision_save`/);
|
|
307
|
+
assert.doesNotMatch(out, /`gsd_exec`/);
|
|
308
|
+
assert.doesNotMatch(out, /`gsd_exec_search`/);
|
|
309
|
+
assert.doesNotMatch(out, /`gsd_resume`/);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const contract = getUnitToolSurfaceContract("research-project");
|
|
313
|
+
assert.deepEqual(contract?.allowedGsdTools, []);
|
|
314
|
+
assert.deepEqual(contract?.requiredWorkflowTools, []);
|
|
315
|
+
for (const toolName of ["gsd_summary_save", "gsd_decision_save"]) {
|
|
316
|
+
const scope = shouldBlockAutoUnitToolCall("research-project", toolName);
|
|
317
|
+
assert.equal(scope.block, true, `research-project should not allow ${toolName}`);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("Context Mode composer: narrow planning guidance steers only to contracted tools", () => {
|
|
322
|
+
const cases = [
|
|
323
|
+
{
|
|
324
|
+
unitType: "replan-slice",
|
|
325
|
+
expectedTools: ["gsd_replan_slice", "gsd_decision_save"],
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
unitType: "reassess-roadmap",
|
|
329
|
+
expectedTools: ["gsd_milestone_status", "gsd_reassess_roadmap"],
|
|
330
|
+
},
|
|
331
|
+
];
|
|
332
|
+
const disallowedTools = ["gsd_exec", "gsd_exec_search", "gsd_resume"];
|
|
333
|
+
|
|
334
|
+
for (const { unitType, expectedTools } of cases) {
|
|
335
|
+
for (const renderMode of ["nested", "standalone"] as const) {
|
|
336
|
+
const out = composeContextModeInstructions(unitType, { enabled: true, renderMode });
|
|
337
|
+
assert.match(out, /planning lane/i, `${unitType} should still render planning lane guidance`);
|
|
338
|
+
for (const toolName of expectedTools) {
|
|
339
|
+
assert.ok(out.includes(`\`${toolName}\``), `${unitType} guidance should mention ${toolName}`);
|
|
340
|
+
}
|
|
341
|
+
for (const toolName of disallowedTools) {
|
|
342
|
+
assert.ok(!out.includes(`\`${toolName}\``), `${unitType} guidance must not mention ${toolName}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("Context Mode composer: lane guidance tools pass unit contracts", () => {
|
|
349
|
+
const affectedUnits = [
|
|
350
|
+
"research-milestone",
|
|
351
|
+
"research-slice",
|
|
352
|
+
"plan-slice",
|
|
353
|
+
"refine-slice",
|
|
354
|
+
"complete-slice",
|
|
355
|
+
"validate-milestone",
|
|
356
|
+
"complete-milestone",
|
|
357
|
+
];
|
|
200
358
|
const contextModeTools: UnitGsdToolName[] = ["gsd_exec", "gsd_exec_search", "gsd_resume"];
|
|
201
359
|
const readOnlyOrientationTools: UnitGsdToolName[] = ["gsd_milestone_status", ...contextModeTools];
|
|
202
360
|
|
|
@@ -207,7 +365,10 @@ test("Context Mode composer: slice planning and research guidance tools pass uni
|
|
|
207
365
|
for (const toolName of contextModeTools) {
|
|
208
366
|
assert.ok(out.includes(`\`${toolName}\``), `${unitType} guidance should mention ${toolName}`);
|
|
209
367
|
}
|
|
210
|
-
|
|
368
|
+
const expectedContractTools = unitType === "research-milestone"
|
|
369
|
+
? contextModeTools
|
|
370
|
+
: readOnlyOrientationTools;
|
|
371
|
+
for (const toolName of expectedContractTools) {
|
|
211
372
|
assert.ok(allowed.has(toolName), `${unitType} contract should allow ${toolName}`);
|
|
212
373
|
const scope = shouldBlockAutoUnitToolCall(unitType, toolName);
|
|
213
374
|
assert.equal(scope.block, false, `${unitType} should not hard-block ${toolName}: ${scope.reason ?? ""}`);
|
|
@@ -357,13 +518,31 @@ test("#4782 phase 2: buildReassessRoadmapPrompt emits composer-shaped context wi
|
|
|
357
518
|
assert.ok(!prompt.includes("Slice Context (from discussion)"));
|
|
358
519
|
});
|
|
359
520
|
|
|
360
|
-
test("execute-task prompt
|
|
521
|
+
test("execute-task prompt omits on-demand slice research when the artifact is absent", async (t) => {
|
|
522
|
+
const base = makeFixtureBase();
|
|
523
|
+
t.after(() => cleanup(base));
|
|
524
|
+
invalidateAllCaches();
|
|
525
|
+
|
|
526
|
+
seed(base, "M001");
|
|
527
|
+
writeArtifacts(base);
|
|
528
|
+
|
|
529
|
+
const prompt = await buildExecuteTaskPrompt("M001", "S01", "First", "T01", "Task", base);
|
|
530
|
+
|
|
531
|
+
assert.doesNotMatch(prompt, /## On-demand Context/);
|
|
532
|
+
assert.doesNotMatch(prompt, /\.gsd\/milestones\/M001\/slices\/S01\/S01-RESEARCH\.md/);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
test("execute-task prompt surfaces on-demand slice research when the artifact exists", async (t) => {
|
|
361
536
|
const base = makeFixtureBase();
|
|
362
537
|
t.after(() => cleanup(base));
|
|
363
538
|
invalidateAllCaches();
|
|
364
539
|
|
|
365
540
|
seed(base, "M001");
|
|
366
541
|
writeArtifacts(base);
|
|
542
|
+
writeFileSync(
|
|
543
|
+
join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-RESEARCH.md"),
|
|
544
|
+
"# S01 Research\n",
|
|
545
|
+
);
|
|
367
546
|
|
|
368
547
|
const prompt = await buildExecuteTaskPrompt("M001", "S01", "First", "T01", "Task", base);
|
|
369
548
|
|
|
@@ -127,6 +127,31 @@ test("formatUnmergedMilestoneBlockMessage includes files, branch, and dirty over
|
|
|
127
127
|
}
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
+
test("findUnmergedCompletedMilestones reports dirty overlap without content fingerprints", async () => {
|
|
131
|
+
const base = makeTempRepo("gsd-unmerged-guard-");
|
|
132
|
+
try {
|
|
133
|
+
const relPath = "build/output.bin";
|
|
134
|
+
const absPath = join(base, relPath);
|
|
135
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
136
|
+
writeFileSync(absPath, "main artifact\n");
|
|
137
|
+
git(base, "add", relPath);
|
|
138
|
+
git(base, "commit", "-m", "test: track build output fixture");
|
|
139
|
+
|
|
140
|
+
seedMilestone(base, "M012");
|
|
141
|
+
commitBranchFile(base, "milestone/M012", relPath, "milestone artifact\n");
|
|
142
|
+
writeFileSync(absPath, "dirty root artifact\n");
|
|
143
|
+
|
|
144
|
+
const [blocker] = await findUnmergedCompletedMilestones(base);
|
|
145
|
+
assert.ok(blocker);
|
|
146
|
+
assert.deepEqual(blocker.files, [relPath]);
|
|
147
|
+
assert.deepEqual(blocker.dirtyOverlap, [{ path: relPath, status: "M" }]);
|
|
148
|
+
assert.equal(Object.hasOwn(blocker.dirtyOverlap[0], "fingerprint"), false);
|
|
149
|
+
} finally {
|
|
150
|
+
closeDatabase();
|
|
151
|
+
cleanup(base);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
130
155
|
test("isUnmergedMilestoneAllowedCommand permits inspection and explicit recovery commands", () => {
|
|
131
156
|
assert.equal(isUnmergedMilestoneAllowedCommand(""), false);
|
|
132
157
|
assert.equal(isUnmergedMilestoneAllowedCommand("auto"), false);
|
|
@@ -3,13 +3,28 @@
|
|
|
3
3
|
|
|
4
4
|
import test from "node:test";
|
|
5
5
|
import assert from "node:assert/strict";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
import { mkdirSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
const _require = createRequire(import.meta.url);
|
|
6
11
|
|
|
7
12
|
import {
|
|
8
13
|
formatValidationBlockedMessage,
|
|
14
|
+
getValidationBlockMessageForBase,
|
|
9
15
|
isValidationBlockAllowedCommand,
|
|
10
16
|
isValidationBlockedState,
|
|
11
17
|
} from "../validation-block-guard.ts";
|
|
18
|
+
import {
|
|
19
|
+
_getAdapter,
|
|
20
|
+
closeDatabase,
|
|
21
|
+
insertMilestone,
|
|
22
|
+
insertSlice,
|
|
23
|
+
openDatabase,
|
|
24
|
+
} from "../gsd-db.ts";
|
|
25
|
+
import { invalidateStateCache } from "../state.ts";
|
|
12
26
|
import type { GSDState } from "../types.ts";
|
|
27
|
+
import { cleanup, makeTempDir } from "./test-utils.ts";
|
|
13
28
|
|
|
14
29
|
function blockedState(): GSDState {
|
|
15
30
|
return {
|
|
@@ -38,6 +53,24 @@ function blockedState(): GSDState {
|
|
|
38
53
|
};
|
|
39
54
|
}
|
|
40
55
|
|
|
56
|
+
function makeBase(): string {
|
|
57
|
+
const base = makeTempDir("gsd-validation-block-");
|
|
58
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
59
|
+
return base;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function openRawSqliteForTest(dbPath: string): { exec(sql: string): void; close(): void } {
|
|
63
|
+
try {
|
|
64
|
+
const mod = _require("node:sqlite") as { DatabaseSync: new (path: string) => { exec(sql: string): void; close(): void } };
|
|
65
|
+
return new mod.DatabaseSync(dbPath);
|
|
66
|
+
} catch {
|
|
67
|
+
type SqliteCtor = new (path: string) => { exec(sql: string): void; close(): void };
|
|
68
|
+
const mod = _require("better-sqlite3") as SqliteCtor | { default: SqliteCtor };
|
|
69
|
+
const DatabaseCtor: SqliteCtor = typeof mod === "function" ? mod : mod.default;
|
|
70
|
+
return new DatabaseCtor(dbPath);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
41
74
|
test("validation block detection only matches validation blockers", () => {
|
|
42
75
|
assert.equal(isValidationBlockedState(blockedState()), true);
|
|
43
76
|
assert.equal(isValidationBlockedState({
|
|
@@ -194,3 +227,49 @@ test("validation block message can guide remediation through dispatch reassess",
|
|
|
194
227
|
assert.match(message, /\/gsd dispatch reassess/);
|
|
195
228
|
assert.doesNotMatch(message, /gsd_reassess_roadmap/);
|
|
196
229
|
});
|
|
230
|
+
|
|
231
|
+
test("validation block guard refreshes from disk and sees external validation blocks", async () => {
|
|
232
|
+
const base = makeBase();
|
|
233
|
+
const dbPath = join(base, ".gsd", "gsd.db");
|
|
234
|
+
const validationPath = join(base, ".gsd", "milestones", "M001", "M001-VALIDATION.md");
|
|
235
|
+
try {
|
|
236
|
+
openDatabase(dbPath);
|
|
237
|
+
insertMilestone({ id: "M001", title: "Active Milestone", status: "active" });
|
|
238
|
+
insertSlice({
|
|
239
|
+
id: "S01",
|
|
240
|
+
milestoneId: "M001",
|
|
241
|
+
title: "Done Slice",
|
|
242
|
+
status: "complete",
|
|
243
|
+
risk: "low",
|
|
244
|
+
depends: [],
|
|
245
|
+
});
|
|
246
|
+
invalidateStateCache();
|
|
247
|
+
|
|
248
|
+
const adapterBefore = _getAdapter();
|
|
249
|
+
assert.ok(adapterBefore);
|
|
250
|
+
|
|
251
|
+
const externalDb = openRawSqliteForTest(dbPath);
|
|
252
|
+
try {
|
|
253
|
+
externalDb.exec(`
|
|
254
|
+
INSERT OR REPLACE INTO assessments (path, milestone_id, slice_id, task_id, status, scope, full_content, created_at)
|
|
255
|
+
VALUES (
|
|
256
|
+
'${validationPath.replace(/'/g, "''")}',
|
|
257
|
+
'M001', NULL, NULL, 'needs-attention', 'milestone-validation',
|
|
258
|
+
'---\nverdict: needs-attention\n---', datetime('now')
|
|
259
|
+
)
|
|
260
|
+
`);
|
|
261
|
+
} finally {
|
|
262
|
+
externalDb.close();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const message = await getValidationBlockMessageForBase(base, "next");
|
|
266
|
+
|
|
267
|
+
assert.ok(message);
|
|
268
|
+
assert.match(message, /cannot run because the active milestone is blocked by validation/);
|
|
269
|
+
assert.notEqual(_getAdapter(), adapterBefore, "guard must refresh stale database handle");
|
|
270
|
+
} finally {
|
|
271
|
+
closeDatabase();
|
|
272
|
+
invalidateStateCache();
|
|
273
|
+
cleanup(base);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
@@ -292,3 +292,69 @@ test("#852: discuss-milestone fails when CONTEXT is in neither worktree nor proj
|
|
|
292
292
|
rmSync(projectRoot, { recursive: true, force: true });
|
|
293
293
|
}
|
|
294
294
|
});
|
|
295
|
+
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
// #870: discuss-milestone verify-fail when the unit runs IN the worktree.
|
|
298
|
+
//
|
|
299
|
+
// The #852 tests above all pass `projectRoot` as the base. But the real call
|
|
300
|
+
// site (auto-post-unit.ts:1726) passes `s.currentUnit.workspaceRoot ?? s.basePath`
|
|
301
|
+
// — i.e. the WORKTREE path when the unit executed in a worktree. In the
|
|
302
|
+
// canonical layout (`<root>/.gsd-worktrees/<MID>/`) resolveCanonicalMilestoneRoot
|
|
303
|
+
// round-trips the worktree path back to itself, so `artifactBase === base` and
|
|
304
|
+
// the worktree→project-root fallback (guarded by `artifactBase !== base`) is
|
|
305
|
+
// skipped. CONTEXT is written to the project root, not projected into the
|
|
306
|
+
// worktree, so verification finds nothing → "existsSync false" → re-dispatch
|
|
307
|
+
// 3× → stuck-loop stop. These tests pin the real call site.
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
test("#870: discuss-milestone falls back to project root when base IS the canonical-layout worktree", () => {
|
|
311
|
+
closeDatabase();
|
|
312
|
+
const projectRoot = mkdtempSync(join(tmpdir(), "gsd-canonical-wt-"));
|
|
313
|
+
try {
|
|
314
|
+
// CONTEXT lives ONLY at the project root (flat-phase layout).
|
|
315
|
+
const phaseDir = join(projectRoot, ".gsd", "phases", "15-m015");
|
|
316
|
+
mkdirSync(phaseDir, { recursive: true });
|
|
317
|
+
writeFileSync(join(phaseDir, "15-CONTEXT.md"), "# M015 context\n");
|
|
318
|
+
|
|
319
|
+
// Canonical-layout worktree: <root>/.gsd-worktrees/<MID>/. Registered
|
|
320
|
+
// with git (.git file) so resolveCanonicalMilestoneRoot treats it as the
|
|
321
|
+
// canonical milestone root — but it has NO phases/ projection.
|
|
322
|
+
const wtRoot = join(projectRoot, ".gsd-worktrees", "M015");
|
|
323
|
+
mkdirSync(join(wtRoot, ".gsd", "milestones", "M015"), { recursive: true });
|
|
324
|
+
writeFileSync(join(wtRoot, ".gsd", "milestones", "M015", "M015-META.json"), '{"branch":"milestone/M015"}');
|
|
325
|
+
writeFileSync(join(wtRoot, ".git"), "gitdir: /fake/path");
|
|
326
|
+
|
|
327
|
+
// Real call site: base = worktree path (workspaceRoot).
|
|
328
|
+
assert.equal(
|
|
329
|
+
verifyExpectedArtifact("discuss-milestone", "M015", wtRoot),
|
|
330
|
+
true,
|
|
331
|
+
"must fall back to project root when base is the canonical-layout worktree",
|
|
332
|
+
);
|
|
333
|
+
} finally {
|
|
334
|
+
closeDatabase();
|
|
335
|
+
rmSync(projectRoot, { recursive: true, force: true });
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("#870: discuss-milestone also falls back when base is the legacy-layout worktree", () => {
|
|
340
|
+
closeDatabase();
|
|
341
|
+
const projectRoot = mkdtempSync(join(tmpdir(), "gsd-legacy-wt-"));
|
|
342
|
+
try {
|
|
343
|
+
const phaseDir = join(projectRoot, ".gsd", "phases", "15-m015");
|
|
344
|
+
mkdirSync(phaseDir, { recursive: true });
|
|
345
|
+
writeFileSync(join(phaseDir, "15-CONTEXT.md"), "# M015 context\n");
|
|
346
|
+
|
|
347
|
+
const wtRoot = join(projectRoot, ".gsd", "worktrees", "M015");
|
|
348
|
+
mkdirSync(join(wtRoot, ".gsd", "milestones", "M015"), { recursive: true });
|
|
349
|
+
writeFileSync(join(wtRoot, ".git"), "gitdir: /fake/path");
|
|
350
|
+
|
|
351
|
+
assert.equal(
|
|
352
|
+
verifyExpectedArtifact("discuss-milestone", "M015", wtRoot),
|
|
353
|
+
true,
|
|
354
|
+
"must fall back to project root when base is the legacy-layout worktree",
|
|
355
|
+
);
|
|
356
|
+
} finally {
|
|
357
|
+
closeDatabase();
|
|
358
|
+
rmSync(projectRoot, { recursive: true, force: true });
|
|
359
|
+
}
|
|
360
|
+
});
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
import test from "node:test";
|
|
5
5
|
import assert from "node:assert/strict";
|
|
6
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { join } from "node:path";
|
|
6
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { delimiter, join } from "node:path";
|
|
8
8
|
|
|
9
|
+
import { GIT_NO_PROMPT_ENV } from "../git-constants.js";
|
|
9
10
|
import { probeGitConflictState } from "../git-conflict-state.js";
|
|
10
11
|
import { ensureWorkspaceGitReadyForPath } from "../workspace-git-preflight.js";
|
|
11
12
|
import { isWorkspaceGitAllowedCommand } from "../workspace-git-guard.js";
|
|
@@ -50,6 +51,38 @@ function seedProductConflict(base: string): void {
|
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
function installCountingGitShim(binDir: string, logPath: string): void {
|
|
55
|
+
const posixShim = join(binDir, "git");
|
|
56
|
+
writeFileSync(
|
|
57
|
+
posixShim,
|
|
58
|
+
[
|
|
59
|
+
"#!/bin/sh",
|
|
60
|
+
'printf "%s\\n" "$*" >> "$GSD_GIT_LOG"',
|
|
61
|
+
'PATH="$GSD_REAL_PATH"',
|
|
62
|
+
"export PATH",
|
|
63
|
+
'exec git "$@"',
|
|
64
|
+
"",
|
|
65
|
+
].join("\n"),
|
|
66
|
+
);
|
|
67
|
+
chmodSync(posixShim, 0o755);
|
|
68
|
+
|
|
69
|
+
writeFileSync(
|
|
70
|
+
join(binDir, "git.cmd"),
|
|
71
|
+
[
|
|
72
|
+
"@echo off",
|
|
73
|
+
'echo %*>>"%GSD_GIT_LOG%"',
|
|
74
|
+
'set "PATH=%GSD_REAL_PATH%"',
|
|
75
|
+
"git %*",
|
|
76
|
+
"",
|
|
77
|
+
].join("\r\n"),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function countGitShimInvocations(logPath: string): number {
|
|
82
|
+
if (!existsSync(logPath)) return 0;
|
|
83
|
+
return readFileSync(logPath, "utf-8").split(/\r?\n/).filter(Boolean).length;
|
|
84
|
+
}
|
|
85
|
+
|
|
53
86
|
test("probeGitConflictState reports clean repo", () => {
|
|
54
87
|
const base = makeTempRepo("gsd-ws-git-clean-");
|
|
55
88
|
try {
|
|
@@ -60,6 +93,122 @@ test("probeGitConflictState reports clean repo", () => {
|
|
|
60
93
|
}
|
|
61
94
|
});
|
|
62
95
|
|
|
96
|
+
test("ensureWorkspaceGitReadyForPath caches clean target probes briefly", async () => {
|
|
97
|
+
const base = makeTempRepo("gsd-ws-git-clean-cache-");
|
|
98
|
+
const binDir = makeTempDir("gsd-ws-git-shim-");
|
|
99
|
+
const logPath = join(binDir, "git.log");
|
|
100
|
+
const originalProcessPath = process.env.PATH;
|
|
101
|
+
const originalEnvPath = GIT_NO_PROMPT_ENV.PATH;
|
|
102
|
+
const originalEnvGitLog = GIT_NO_PROMPT_ENV.GSD_GIT_LOG;
|
|
103
|
+
const originalEnvRealPath = GIT_NO_PROMPT_ENV.GSD_REAL_PATH;
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
installCountingGitShim(binDir, logPath);
|
|
107
|
+
const shimmedPath = `${binDir}${delimiter}${originalProcessPath ?? ""}`;
|
|
108
|
+
process.env.PATH = shimmedPath;
|
|
109
|
+
GIT_NO_PROMPT_ENV.PATH = shimmedPath;
|
|
110
|
+
GIT_NO_PROMPT_ENV.GSD_GIT_LOG = logPath;
|
|
111
|
+
GIT_NO_PROMPT_ENV.GSD_REAL_PATH = originalProcessPath ?? "";
|
|
112
|
+
|
|
113
|
+
const first = await ensureWorkspaceGitReadyForPath(base);
|
|
114
|
+
assert.equal(first.ok, true);
|
|
115
|
+
const firstCount = countGitShimInvocations(logPath);
|
|
116
|
+
assert.equal(firstCount, 3, "first clean probe should run the existing conflict checks");
|
|
117
|
+
|
|
118
|
+
const second = await ensureWorkspaceGitReadyForPath(base);
|
|
119
|
+
assert.equal(second.ok, true);
|
|
120
|
+
assert.equal(
|
|
121
|
+
countGitShimInvocations(logPath),
|
|
122
|
+
firstCount,
|
|
123
|
+
"second clean probe within the cache window must not spawn git again",
|
|
124
|
+
);
|
|
125
|
+
} finally {
|
|
126
|
+
if (originalProcessPath === undefined) delete process.env.PATH;
|
|
127
|
+
else process.env.PATH = originalProcessPath;
|
|
128
|
+
if (originalEnvPath === undefined) delete GIT_NO_PROMPT_ENV.PATH;
|
|
129
|
+
else GIT_NO_PROMPT_ENV.PATH = originalEnvPath;
|
|
130
|
+
if (originalEnvGitLog === undefined) delete GIT_NO_PROMPT_ENV.GSD_GIT_LOG;
|
|
131
|
+
else GIT_NO_PROMPT_ENV.GSD_GIT_LOG = originalEnvGitLog;
|
|
132
|
+
if (originalEnvRealPath === undefined) delete GIT_NO_PROMPT_ENV.GSD_REAL_PATH;
|
|
133
|
+
else GIT_NO_PROMPT_ENV.GSD_REAL_PATH = originalEnvRealPath;
|
|
134
|
+
cleanup(binDir);
|
|
135
|
+
cleanup(base);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("ensureWorkspaceGitReadyForPath detects merge state that appears after a clean probe", async () => {
|
|
140
|
+
const base = makeTempRepo("gsd-ws-git-cache-stale-");
|
|
141
|
+
const binDir = makeTempDir("gsd-ws-git-shim2-");
|
|
142
|
+
const logPath = join(binDir, "git2.log");
|
|
143
|
+
const originalProcessPath = process.env.PATH;
|
|
144
|
+
const originalEnvPath = GIT_NO_PROMPT_ENV.PATH;
|
|
145
|
+
const originalEnvGitLog = GIT_NO_PROMPT_ENV.GSD_GIT_LOG;
|
|
146
|
+
const originalEnvRealPath = GIT_NO_PROMPT_ENV.GSD_REAL_PATH;
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
installCountingGitShim(binDir, logPath);
|
|
150
|
+
const shimmedPath = `${binDir}${delimiter}${originalProcessPath ?? ""}`;
|
|
151
|
+
process.env.PATH = shimmedPath;
|
|
152
|
+
GIT_NO_PROMPT_ENV.PATH = shimmedPath;
|
|
153
|
+
GIT_NO_PROMPT_ENV.GSD_GIT_LOG = logPath;
|
|
154
|
+
GIT_NO_PROMPT_ENV.GSD_REAL_PATH = originalProcessPath ?? "";
|
|
155
|
+
|
|
156
|
+
// First call — repo is clean, cache is populated.
|
|
157
|
+
const first = await ensureWorkspaceGitReadyForPath(base);
|
|
158
|
+
assert.equal(first.ok, true);
|
|
159
|
+
const afterFirstCount = countGitShimInvocations(logPath);
|
|
160
|
+
assert.ok(afterFirstCount > 0, "first probe must have called git");
|
|
161
|
+
|
|
162
|
+
// Introduce MERGE_HEAD to simulate merge state appearing mid-TTL window.
|
|
163
|
+
writeFileSync(join(base, ".git", "MERGE_HEAD"), "0000000000000000000000000000000000000000\n");
|
|
164
|
+
|
|
165
|
+
// Second call — cache should be bypassed because merge state markers are present.
|
|
166
|
+
await ensureWorkspaceGitReadyForPath(base);
|
|
167
|
+
const afterSecondCount = countGitShimInvocations(logPath);
|
|
168
|
+
assert.ok(
|
|
169
|
+
afterSecondCount > afterFirstCount,
|
|
170
|
+
"cache must be invalidated when merge state appears, causing a fresh git probe",
|
|
171
|
+
);
|
|
172
|
+
} finally {
|
|
173
|
+
if (originalProcessPath === undefined) delete process.env.PATH;
|
|
174
|
+
else process.env.PATH = originalProcessPath;
|
|
175
|
+
if (originalEnvPath === undefined) delete GIT_NO_PROMPT_ENV.PATH;
|
|
176
|
+
else GIT_NO_PROMPT_ENV.PATH = originalEnvPath;
|
|
177
|
+
if (originalEnvGitLog === undefined) delete GIT_NO_PROMPT_ENV.GSD_GIT_LOG;
|
|
178
|
+
else GIT_NO_PROMPT_ENV.GSD_GIT_LOG = originalEnvGitLog;
|
|
179
|
+
if (originalEnvRealPath === undefined) delete GIT_NO_PROMPT_ENV.GSD_REAL_PATH;
|
|
180
|
+
else GIT_NO_PROMPT_ENV.GSD_REAL_PATH = originalEnvRealPath;
|
|
181
|
+
cleanup(binDir);
|
|
182
|
+
cleanup(base);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("ensureWorkspaceGitReadyForPath detects conflicts after a non-git folder becomes a repo", async () => {
|
|
187
|
+
const base = makeTempDir("gsd-ws-git-non-repo-cache-");
|
|
188
|
+
try {
|
|
189
|
+
const first = await ensureWorkspaceGitReadyForPath(base);
|
|
190
|
+
assert.equal(first.ok, true);
|
|
191
|
+
|
|
192
|
+
git(base, "init");
|
|
193
|
+
git(base, "config", "user.email", "test@test.com");
|
|
194
|
+
git(base, "config", "user.name", "Test");
|
|
195
|
+
git(base, "config", "core.autocrlf", "false");
|
|
196
|
+
writeFileSync(join(base, "README.md"), "# init\n");
|
|
197
|
+
git(base, "add", "-A");
|
|
198
|
+
git(base, "commit", "-m", "init");
|
|
199
|
+
git(base, "branch", "-M", "main");
|
|
200
|
+
seedProductConflict(base);
|
|
201
|
+
|
|
202
|
+
const second = await ensureWorkspaceGitReadyForPath(base);
|
|
203
|
+
assert.equal(second.ok, false);
|
|
204
|
+
if (second.ok) return;
|
|
205
|
+
assert.equal(second.severity, "product-conflicts");
|
|
206
|
+
assert.ok(second.conflictedPaths.includes("app.ts"));
|
|
207
|
+
} finally {
|
|
208
|
+
cleanup(base);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
63
212
|
test("ensureWorkspaceGitReadyForPath allows fresh non-git project setup folders", async () => {
|
|
64
213
|
const base = makeTempDir("gsd-ws-git-non-repo-");
|
|
65
214
|
try {
|