gsd-pi 2.78.1-dev.b0759e59b → 2.78.1-dev.e9d88a536
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/dist/headless-recover.d.ts +23 -0
- package/dist/headless-recover.js +93 -0
- package/dist/headless.js +9 -0
- package/dist/help-text.js +1 -0
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/browser-tools/tools/intent.js +8 -1
- package/dist/resources/extensions/gsd/auto-dispatch.js +4 -56
- package/dist/resources/extensions/gsd/auto-post-unit.js +7 -27
- package/dist/resources/extensions/gsd/auto-start.js +1 -8
- package/dist/resources/extensions/gsd/auto-worktree.js +59 -176
- package/dist/resources/extensions/gsd/auto.js +24 -6
- package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +9 -77
- package/dist/resources/extensions/gsd/commands-codebase.js +2 -2
- package/dist/resources/extensions/gsd/commands-handlers.js +5 -5
- package/dist/resources/extensions/gsd/commands-logs.js +2 -2
- package/dist/resources/extensions/gsd/commands-scan.js +2 -2
- package/dist/resources/extensions/gsd/commands-ship.js +2 -2
- package/dist/resources/extensions/gsd/commands-workflow-templates.js +5 -5
- package/dist/resources/extensions/gsd/db-writer.js +16 -85
- package/dist/resources/extensions/gsd/dispatch-guard.js +6 -10
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +2 -2
- package/dist/resources/extensions/gsd/gsd-db.js +74 -8
- package/dist/resources/extensions/gsd/guided-flow.js +31 -8
- package/dist/resources/extensions/gsd/markdown-renderer.js +14 -51
- package/dist/resources/extensions/gsd/parallel-merge.js +14 -13
- package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +5 -2
- package/dist/resources/extensions/gsd/paths.js +35 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +6 -0
- package/dist/resources/extensions/gsd/queue-order.js +6 -1
- package/dist/resources/extensions/gsd/rethink.js +2 -2
- package/dist/resources/extensions/gsd/state.js +91 -372
- package/dist/resources/extensions/gsd/tools/complete-milestone.js +6 -5
- package/dist/resources/extensions/gsd/tools/complete-slice.js +7 -12
- package/dist/resources/extensions/gsd/tools/complete-task.js +19 -31
- package/dist/resources/extensions/gsd/tools/validate-milestone.js +7 -5
- package/dist/resources/extensions/gsd/workflow-manifest.js +2 -1
- package/dist/resources/extensions/gsd/workflow-mcp-auto-prep.js +3 -21
- package/dist/resources/extensions/gsd/workflow-reconcile.js +3 -3
- package/dist/resources/extensions/gsd/worktree-command.js +4 -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 +12 -12
- 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/api/boot/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +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 +12 -12
- package/dist/web/standalone/.next/server/chunks/6336.js +1 -0
- package/dist/web/standalone/.next/server/chunks/6897.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/mcp-server/dist/workflow-tools.d.ts +6 -0
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +56 -2
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/parse-workflow-args.test.ts +80 -0
- package/packages/mcp-server/src/workflow-tools.ts +61 -2
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/browser-tools/tools/intent.ts +13 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +4 -60
- package/src/resources/extensions/gsd/auto-post-unit.ts +7 -26
- package/src/resources/extensions/gsd/auto-start.ts +1 -8
- package/src/resources/extensions/gsd/auto-worktree.ts +61 -204
- package/src/resources/extensions/gsd/auto.ts +23 -6
- package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +9 -84
- package/src/resources/extensions/gsd/commands-codebase.ts +2 -2
- package/src/resources/extensions/gsd/commands-handlers.ts +5 -5
- package/src/resources/extensions/gsd/commands-logs.ts +2 -2
- package/src/resources/extensions/gsd/commands-scan.ts +2 -2
- package/src/resources/extensions/gsd/commands-ship.ts +2 -2
- package/src/resources/extensions/gsd/commands-workflow-templates.ts +5 -5
- package/src/resources/extensions/gsd/db-writer.ts +16 -83
- package/src/resources/extensions/gsd/dispatch-guard.ts +6 -11
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +2 -2
- package/src/resources/extensions/gsd/gsd-db.ts +85 -8
- package/src/resources/extensions/gsd/guided-flow.ts +35 -8
- package/src/resources/extensions/gsd/markdown-renderer.ts +13 -64
- package/src/resources/extensions/gsd/parallel-merge.ts +14 -13
- package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +5 -2
- package/src/resources/extensions/gsd/paths.ts +55 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +6 -0
- package/src/resources/extensions/gsd/queue-order.ts +6 -1
- package/src/resources/extensions/gsd/rethink.ts +2 -2
- package/src/resources/extensions/gsd/state.ts +91 -389
- package/src/resources/extensions/gsd/tests/artifact-corruption-2630.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/auto-paused-session-validation.test.ts +6 -0
- package/src/resources/extensions/gsd/tests/auto-remediate-slice-status.test.ts +21 -34
- package/src/resources/extensions/gsd/tests/complete-task-rollback-evidence.test.ts +6 -7
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +8 -6
- package/src/resources/extensions/gsd/tests/completed-at-reconcile.test.ts +12 -27
- package/src/resources/extensions/gsd/tests/completed-units-metrics-sync.test.ts +18 -5
- package/src/resources/extensions/gsd/tests/db-path-worktree-symlink.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/db-writer.test.ts +14 -16
- package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +6 -5
- package/src/resources/extensions/gsd/tests/derive-state-db-disk-reconcile.test.ts +10 -38
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +136 -56
- package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +119 -61
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +4 -0
- package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +6 -20
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +4 -5
- package/src/resources/extensions/gsd/tests/dispatcher-stuck-planning.test.ts +14 -15
- package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +11 -16
- package/src/resources/extensions/gsd/tests/escalation.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/gsdroot-worktree-detection.test.ts +15 -36
- package/src/resources/extensions/gsd/tests/handler-worktree-write-isolation.test.ts +57 -0
- package/src/resources/extensions/gsd/tests/integration/parallel-merge.test.ts +15 -15
- package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +15 -5
- package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +14 -8
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/park-milestone.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/progressive-planning.test.ts +25 -16
- package/src/resources/extensions/gsd/tests/projection-regression.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +184 -0
- package/src/resources/extensions/gsd/tests/register-hooks-compaction-checkpoint.test.ts +6 -1
- package/src/resources/extensions/gsd/tests/replan-slice.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/resolve-ts.mjs +4 -0
- package/src/resources/extensions/gsd/tests/rogue-file-detection.test.ts +3 -4
- package/src/resources/extensions/gsd/tests/slice-disk-reconcile.test.ts +10 -56
- package/src/resources/extensions/gsd/tests/stale-slice-rows.test.ts +15 -16
- package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/state-machine-full-walkthrough.test.ts +23 -27
- package/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts +13 -14
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/sync-worktree-skip-current.test.ts +10 -33
- package/src/resources/extensions/gsd/tests/validate-milestone-write-order.test.ts +7 -8
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +9 -15
- package/src/resources/extensions/gsd/tests/workflow-logger-wiring.test.ts +12 -7
- package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +24 -1
- package/src/resources/extensions/gsd/tests/worktree-db-same-file.test.ts +13 -0
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +65 -71
- package/src/resources/extensions/gsd/tests/worktree-sync-tasks.test.ts +26 -151
- package/src/resources/extensions/gsd/tools/complete-milestone.ts +7 -5
- package/src/resources/extensions/gsd/tools/complete-slice.ts +7 -14
- package/src/resources/extensions/gsd/tools/complete-task.ts +19 -34
- package/src/resources/extensions/gsd/tools/validate-milestone.ts +7 -5
- package/src/resources/extensions/gsd/workflow-manifest.ts +4 -1
- package/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts +2 -18
- package/src/resources/extensions/gsd/workflow-reconcile.ts +3 -3
- package/src/resources/extensions/gsd/worktree-command.ts +4 -3
- package/dist/web/standalone/.next/server/chunks/8527.js +0 -1
- /package/dist/web/standalone/.next/static/{rk1EN3FQTE6Z1yalkW_GE → oZGTPvJBQX_IDKKnuV8Bt}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{rk1EN3FQTE6Z1yalkW_GE → oZGTPvJBQX_IDKKnuV8Bt}/_ssgManifest.js +0 -0
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
maybeHandleEmptyIntentTurn,
|
|
22
22
|
resetEmptyTurnCounter,
|
|
23
23
|
} from "../guided-flow.ts";
|
|
24
|
+
import { drainLogs } from "../workflow-logger.ts";
|
|
25
|
+
import { resolveMilestoneFile, clearPathCache } from "../paths.ts";
|
|
24
26
|
|
|
25
27
|
// ─── Test harness ──────────────────────────────────────────────────────────
|
|
26
28
|
|
|
@@ -177,6 +179,79 @@ describe("#4573 maybeHandleReadyPhraseWithoutFiles", () => {
|
|
|
177
179
|
}
|
|
178
180
|
});
|
|
179
181
|
|
|
182
|
+
test("stale path cache from a prior listing → fresh writes are detected (regression)", () => {
|
|
183
|
+
// Repro the live binary failure where:
|
|
184
|
+
// 1. paths.ts cached dir listings were populated when M001/ was empty
|
|
185
|
+
// (or the milestone dir didn't yet exist).
|
|
186
|
+
// 2. The LLM then wrote M001-CONTEXT.md and M001-ROADMAP.md via the
|
|
187
|
+
// standard Write tool — which has no awareness of paths.ts caches.
|
|
188
|
+
// 3. maybeHandleReadyPhraseWithoutFiles called resolveMilestoneFile,
|
|
189
|
+
// which read the stale cache and reported the artifacts missing,
|
|
190
|
+
// firing a false rejection nudge until MAX_READY_REJECTS aborted
|
|
191
|
+
// the auto-start with `LLM signaled "ready" 3 times without
|
|
192
|
+
// writing files`.
|
|
193
|
+
//
|
|
194
|
+
// The fix busts the path cache at the top of the validator before
|
|
195
|
+
// re-resolving. This test fails pre-fix (handled === true) because the
|
|
196
|
+
// cache returns the empty listing it captured in step (a).
|
|
197
|
+
const base = mkBase();
|
|
198
|
+
try {
|
|
199
|
+
const mDir = join(base, ".gsd", "milestones", "M001");
|
|
200
|
+
|
|
201
|
+
// (a) Prime the cache with a listing that DOES NOT include M001's
|
|
202
|
+
// CONTEXT/ROADMAP files. mkBase() has already created the M001
|
|
203
|
+
// directory but nothing inside it yet — so this readdir caches an
|
|
204
|
+
// empty entry list keyed by the M001 dir path.
|
|
205
|
+
clearPathCache();
|
|
206
|
+
assert.equal(
|
|
207
|
+
resolveMilestoneFile(base, "M001", "CONTEXT"),
|
|
208
|
+
null,
|
|
209
|
+
"precondition: resolver must report missing before files are written",
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// (b) Write the artifacts directly to disk (simulates the LLM Write
|
|
213
|
+
// tool — no clearPathCache() call between the write and the
|
|
214
|
+
// validator).
|
|
215
|
+
writeFileSync(join(mDir, "M001-CONTEXT.md"), "# ctx");
|
|
216
|
+
writeFileSync(join(mDir, "M001-ROADMAP.md"), "# roadmap");
|
|
217
|
+
|
|
218
|
+
// (c) Sanity: the cache is still stale. Without the fix, the
|
|
219
|
+
// validator would still see the empty cached listing.
|
|
220
|
+
assert.equal(
|
|
221
|
+
resolveMilestoneFile(base, "M001", "CONTEXT"),
|
|
222
|
+
null,
|
|
223
|
+
"stale cache still reports missing pre-clearPathCache",
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// (d) Run the validator. With the fix it busts the cache before
|
|
227
|
+
// resolving and returns false (no nudge). Without the fix it
|
|
228
|
+
// fires the nudge.
|
|
229
|
+
const cap = mkCapture();
|
|
230
|
+
setPendingAutoStart(base, {
|
|
231
|
+
basePath: base,
|
|
232
|
+
milestoneId: "M001",
|
|
233
|
+
ctx: mkCtx(cap),
|
|
234
|
+
pi: mkPi(cap),
|
|
235
|
+
});
|
|
236
|
+
const handled = maybeHandleReadyPhraseWithoutFiles({
|
|
237
|
+
messages: [assistantMsg("Milestone M001 ready.")],
|
|
238
|
+
});
|
|
239
|
+
assert.equal(
|
|
240
|
+
handled,
|
|
241
|
+
false,
|
|
242
|
+
"fresh writes must not trigger the rejection nudge — cache must be busted before resolution",
|
|
243
|
+
);
|
|
244
|
+
assert.equal(cap.messages.length, 0, "no nudge sent");
|
|
245
|
+
assert.equal(
|
|
246
|
+
cap.notifies.length,
|
|
247
|
+
0,
|
|
248
|
+
"no rejection notify when files exist on disk",
|
|
249
|
+
);
|
|
250
|
+
} finally {
|
|
251
|
+
clearPendingAutoStart();
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
180
255
|
test("legacy unprefixed files present → no nudge", () => {
|
|
181
256
|
const base = mkBase();
|
|
182
257
|
try {
|
|
@@ -219,6 +294,81 @@ describe("#4573 maybeHandleReadyPhraseWithoutFiles", () => {
|
|
|
219
294
|
}
|
|
220
295
|
});
|
|
221
296
|
|
|
297
|
+
test("nudge fires → diagnostic warning logged with basePath, mDir, canonical-path existsSync results", () => {
|
|
298
|
+
// Diagnostic logging added so we can tell, in real failures, whether
|
|
299
|
+
// resolveMilestoneFile is reporting files missing that actually exist on
|
|
300
|
+
// disk (basePath/symlink mismatch, stale cache despite the
|
|
301
|
+
// agent-end-recovery flush, legacy descriptor dir, etc.).
|
|
302
|
+
const base = mkBase();
|
|
303
|
+
try {
|
|
304
|
+
drainLogs(); // discard prior test noise
|
|
305
|
+
const cap = mkCapture();
|
|
306
|
+
setPendingAutoStart(base, {
|
|
307
|
+
basePath: base,
|
|
308
|
+
milestoneId: "M001",
|
|
309
|
+
ctx: mkCtx(cap),
|
|
310
|
+
pi: mkPi(cap),
|
|
311
|
+
});
|
|
312
|
+
const handled = maybeHandleReadyPhraseWithoutFiles({
|
|
313
|
+
messages: [assistantMsg("Milestone M001 ready.")],
|
|
314
|
+
});
|
|
315
|
+
assert.equal(handled, true);
|
|
316
|
+
|
|
317
|
+
const logs = drainLogs();
|
|
318
|
+
const diag = logs.find(
|
|
319
|
+
(e) => e.component === "guided" && /ready-phrase-reject diagnostic/.test(e.message),
|
|
320
|
+
);
|
|
321
|
+
assert.ok(diag, "expected diagnostic warning to be logged when nudge fires");
|
|
322
|
+
assert.match(diag!.message, /mid=M001/);
|
|
323
|
+
assert.match(diag!.message, new RegExp(`basePath=${base.replace(/[/\\]/g, "[/\\\\]")}`));
|
|
324
|
+
assert.match(diag!.message, /mDir=/);
|
|
325
|
+
assert.match(diag!.message, /ctx-exists=false/);
|
|
326
|
+
assert.match(diag!.message, /roadmap-exists=false/);
|
|
327
|
+
} finally {
|
|
328
|
+
clearPendingAutoStart();
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("diagnostic logs ctx-exists=true when file is on disk but cached resolver missed it", () => {
|
|
333
|
+
// Simulates the test123 #5xxx scenario: file exists on disk, cached
|
|
334
|
+
// resolver claims it doesn't. We drop a file with a non-canonical path
|
|
335
|
+
// (forces the legacy-descriptor pattern miss) so resolveMilestoneFile
|
|
336
|
+
// returns null but existsSync on the canonical path returns true.
|
|
337
|
+
//
|
|
338
|
+
// Note: the canonical path probe in the diagnostic uses the literal
|
|
339
|
+
// `${milestoneId}-CONTEXT.md` filename. If a file is at that path,
|
|
340
|
+
// existsSync will see it regardless of resolver behavior.
|
|
341
|
+
const base = mkBase();
|
|
342
|
+
try {
|
|
343
|
+
drainLogs();
|
|
344
|
+
// Write the canonical file directly — both resolver AND existsSync
|
|
345
|
+
// would normally see it. To prove the diagnostic captures the
|
|
346
|
+
// existsSync result independently, we cover the basic case here.
|
|
347
|
+
const cap = mkCapture();
|
|
348
|
+
setPendingAutoStart(base, {
|
|
349
|
+
basePath: base,
|
|
350
|
+
milestoneId: "M001",
|
|
351
|
+
ctx: mkCtx(cap),
|
|
352
|
+
pi: mkPi(cap),
|
|
353
|
+
});
|
|
354
|
+
// No files written — both probes should report false.
|
|
355
|
+
maybeHandleReadyPhraseWithoutFiles({
|
|
356
|
+
messages: [assistantMsg("Milestone M001 ready.")],
|
|
357
|
+
});
|
|
358
|
+
const logs = drainLogs();
|
|
359
|
+
const diag = logs.find(
|
|
360
|
+
(e) => e.component === "guided" && /ready-phrase-reject diagnostic/.test(e.message),
|
|
361
|
+
);
|
|
362
|
+
assert.ok(diag, "diagnostic logged");
|
|
363
|
+
// mDir resolves because mkBase creates the directory
|
|
364
|
+
assert.match(diag!.message, /mDir=.+M001/);
|
|
365
|
+
assert.match(diag!.message, /canonical-ctx=.+M001-CONTEXT\.md/);
|
|
366
|
+
assert.match(diag!.message, /canonical-roadmap=.+M001-ROADMAP\.md/);
|
|
367
|
+
} finally {
|
|
368
|
+
clearPendingAutoStart();
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
222
372
|
test("fresh entry after give-up resets counter", () => {
|
|
223
373
|
const base = mkBase();
|
|
224
374
|
try {
|
|
@@ -344,6 +494,40 @@ describe("#4573 maybeHandleEmptyIntentTurn", () => {
|
|
|
344
494
|
}
|
|
345
495
|
});
|
|
346
496
|
|
|
497
|
+
test("single-line approval prompt with mid-line `?` and conditional intent → treated as user-handoff (regression: #5187 follow-up)", () => {
|
|
498
|
+
// Regression for the discuss-milestone case where the LLM presented a
|
|
499
|
+
// depth summary and ended with: "Did I capture that correctly? If so,
|
|
500
|
+
// say yes and I'll write requirements and the roadmap preview."
|
|
501
|
+
// The previous heuristic only checked for lines *ending* in `?`, so
|
|
502
|
+
// this single-line paragraph (terminating in `.`) bypassed the
|
|
503
|
+
// user-handoff guard, then COMMIT_INTENT_RE matched "I'll write" and
|
|
504
|
+
// the nudge auto-replied while the user was meant to approve.
|
|
505
|
+
const base = mkBase();
|
|
506
|
+
try {
|
|
507
|
+
const cap = mkCapture();
|
|
508
|
+
setPendingAutoStart(base, {
|
|
509
|
+
basePath: base,
|
|
510
|
+
milestoneId: "M001",
|
|
511
|
+
ctx: mkCtx(cap),
|
|
512
|
+
pi: mkPi(cap),
|
|
513
|
+
});
|
|
514
|
+
const handled = maybeHandleEmptyIntentTurn(
|
|
515
|
+
{
|
|
516
|
+
messages: [
|
|
517
|
+
assistantMsg(
|
|
518
|
+
"Did I capture that correctly? If so, say yes and I'll write requirements and the roadmap preview.",
|
|
519
|
+
),
|
|
520
|
+
],
|
|
521
|
+
},
|
|
522
|
+
false,
|
|
523
|
+
);
|
|
524
|
+
assert.equal(handled, false, "any sentence-terminating ? must defer to the user");
|
|
525
|
+
assert.equal(cap.messages.length, 0);
|
|
526
|
+
} finally {
|
|
527
|
+
clearPendingAutoStart();
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
|
|
347
531
|
test('"Let me make sure" meta phrase → not flagged as commit intent (regression)', () => {
|
|
348
532
|
const base = mkBase();
|
|
349
533
|
try {
|
|
@@ -6,7 +6,7 @@ import { tmpdir } from "node:os";
|
|
|
6
6
|
|
|
7
7
|
import { registerHooks } from "../bootstrap/register-hooks.ts";
|
|
8
8
|
import { parseContinue } from "../files.ts";
|
|
9
|
-
import { closeDatabase } from "../gsd-db.ts";
|
|
9
|
+
import { closeDatabase, insertMilestone, insertSlice, openDatabase } from "../gsd-db.ts";
|
|
10
10
|
import { deriveState, invalidateStateCache } from "../state.ts";
|
|
11
11
|
|
|
12
12
|
function createPlanningFixtureBase(): string {
|
|
@@ -39,6 +39,11 @@ function createPlanningFixtureBase(): string {
|
|
|
39
39
|
`,
|
|
40
40
|
);
|
|
41
41
|
|
|
42
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
43
|
+
insertMilestone({ id: "M001", title: "Test Milestone", status: "active" });
|
|
44
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Test Slice", status: "active", risk: "low", depends: [] });
|
|
45
|
+
closeDatabase();
|
|
46
|
+
|
|
42
47
|
return base;
|
|
43
48
|
}
|
|
44
49
|
|
|
@@ -8,6 +8,9 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
8
|
import { parseSummary } from '../files.ts';
|
|
9
9
|
import { deriveState } from '../state.ts';
|
|
10
10
|
|
|
11
|
+
// This suite exercises the explicit legacy markdown derivation path.
|
|
12
|
+
process.env.GSD_ALLOW_MARKDOWN_DERIVE_FALLBACK = '1';
|
|
13
|
+
|
|
11
14
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
15
|
const worktreePromptsDir = join(__dirname, '..', 'prompts');
|
|
13
16
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { register } from 'node:module';
|
|
2
2
|
import { pathToFileURL } from 'node:url';
|
|
3
3
|
|
|
4
|
+
// Source legacy state tests exercise markdown derivation through deriveState().
|
|
5
|
+
// Production/runtime keeps this fallback disabled unless explicitly requested.
|
|
6
|
+
process.env.GSD_ALLOW_MARKDOWN_DERIVE_FALLBACK ??= '1';
|
|
7
|
+
|
|
4
8
|
// Register hook to redirect imports to the dist directory
|
|
5
9
|
register(new URL('./dist-redirect.mjs', import.meta.url), pathToFileURL('./'));
|
|
@@ -149,7 +149,7 @@ test("rogue detection: DB not available → returns empty array (graceful degrad
|
|
|
149
149
|
}
|
|
150
150
|
});
|
|
151
151
|
|
|
152
|
-
test("rogue detection: slice summary on disk, no DB
|
|
152
|
+
test("rogue detection: slice summary on disk, no DB completion → detected as rogue without DB import", () => {
|
|
153
153
|
const basePath = createTmpBase();
|
|
154
154
|
const dbPath = join(basePath, ".gsd", "gsd.db");
|
|
155
155
|
mkdirSync(join(basePath, ".gsd"), { recursive: true });
|
|
@@ -160,10 +160,9 @@ test("rogue detection: slice summary on disk, no DB row → auto-remediated (not
|
|
|
160
160
|
const summaryPath = createSliceSummaryOnDisk(basePath, "M001", "S01");
|
|
161
161
|
assert.ok(existsSync(summaryPath), "Slice summary file should exist on disk");
|
|
162
162
|
|
|
163
|
-
// Fix #3633: stale slice DB status is auto-remediated via updateSliceStatus()
|
|
164
|
-
// instead of being reported as rogue, so rogues array should be empty.
|
|
165
163
|
const rogues = detectRogueFileWrites("complete-slice", "M001/S01", basePath);
|
|
166
|
-
assert.equal(rogues.length,
|
|
164
|
+
assert.equal(rogues.length, 1, "Should report stale disk summary instead of mutating DB");
|
|
165
|
+
assert.equal(rogues[0]?.path, summaryPath);
|
|
167
166
|
} finally {
|
|
168
167
|
closeDatabase();
|
|
169
168
|
rmSync(basePath, { recursive: true, force: true });
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* slice-disk-reconcile.test.ts — #2533
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* blocks. deriveStateFromDb must reconcile disk slices into the DB, just as
|
|
7
|
-
* it already does for milestones (#2416).
|
|
4
|
+
* DB-authoritative state: slices that exist only in ROADMAP.md are projections
|
|
5
|
+
* and must not be imported into the SQLite database by deriveStateFromDb().
|
|
8
6
|
*
|
|
9
7
|
* Scenario: M001 has a ROADMAP with S01-S04. S01 and S02 have SUMMARY files
|
|
10
8
|
* (complete on disk). S03 depends on S01. Only S04 is in the DB (depends on
|
|
@@ -70,7 +68,7 @@ const ROADMAP_CONTENT = `# M001: Test Milestone
|
|
|
70
68
|
`;
|
|
71
69
|
|
|
72
70
|
async function testMissingSlicesCauseBlock(): Promise<void> {
|
|
73
|
-
console.log("\n--- Test: missing DB slices
|
|
71
|
+
console.log("\n--- Test: missing DB slices are not imported from ROADMAP.md ---");
|
|
74
72
|
|
|
75
73
|
const base = createFixtureBase();
|
|
76
74
|
const dbPath = join(base, ".gsd", "gsd.db");
|
|
@@ -96,51 +94,11 @@ async function testMissingSlicesCauseBlock(): Promise<void> {
|
|
|
96
94
|
invalidateStateCache();
|
|
97
95
|
const state = await deriveStateFromDb(base);
|
|
98
96
|
|
|
99
|
-
// After the fix, slices S01-S03 should be reconciled into DB
|
|
100
97
|
const dbSlices = getMilestoneSlices("M001");
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
// S01 and S02 should be marked complete (have SUMMARY files)
|
|
107
|
-
const s01 = dbSlices.find(s => s.id === "S01");
|
|
108
|
-
assertTrue(s01 !== undefined, "S01 should exist in DB after reconciliation");
|
|
109
|
-
if (s01) {
|
|
110
|
-
assertEq(s01.status, "complete", "S01 should be 'complete' (has SUMMARY on disk)");
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const s02 = dbSlices.find(s => s.id === "S02");
|
|
114
|
-
assertTrue(s02 !== undefined, "S02 should exist in DB after reconciliation");
|
|
115
|
-
if (s02) {
|
|
116
|
-
assertEq(s02.status, "complete", "S02 should be 'complete' (has SUMMARY on disk)");
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// S03 should be pending (no SUMMARY)
|
|
120
|
-
const s03 = dbSlices.find(s => s.id === "S03");
|
|
121
|
-
assertTrue(s03 !== undefined, "S03 should exist in DB after reconciliation");
|
|
122
|
-
if (s03) {
|
|
123
|
-
assertEq(s03.status, "pending", "S03 should be 'pending' (no SUMMARY on disk)");
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// The state should NOT be blocked — S03 should be eligible (S01 dep satisfied)
|
|
127
|
-
assertTrue(
|
|
128
|
-
state.phase !== "blocked",
|
|
129
|
-
`Phase should not be 'blocked' after reconciliation, got '${state.phase}'`,
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
// Active slice should be S03 (S01 dep met, S03 is first incomplete with satisfied deps)
|
|
133
|
-
assertTrue(
|
|
134
|
-
state.activeSlice !== null,
|
|
135
|
-
"There should be an active slice after reconciliation",
|
|
136
|
-
);
|
|
137
|
-
if (state.activeSlice) {
|
|
138
|
-
assertEq(
|
|
139
|
-
state.activeSlice.id,
|
|
140
|
-
"S03",
|
|
141
|
-
"Active slice should be S03 (its dependency S01 is complete) (#2533)",
|
|
142
|
-
);
|
|
143
|
-
}
|
|
98
|
+
assertEq(dbSlices.length, 1, `Only DB slice S04 should remain, got ${dbSlices.length}`);
|
|
99
|
+
assertEq(dbSlices[0]?.id, "S04", "Disk-only slices are not inserted");
|
|
100
|
+
assertEq(state.phase, "blocked", "DB-only S04 remains blocked on missing DB dependency S03");
|
|
101
|
+
assertEq(state.activeSlice, null, "No active slice is inferred from roadmap-only rows");
|
|
144
102
|
} finally {
|
|
145
103
|
closeDatabase();
|
|
146
104
|
cleanup(base);
|
|
@@ -148,7 +106,7 @@ async function testMissingSlicesCauseBlock(): Promise<void> {
|
|
|
148
106
|
}
|
|
149
107
|
|
|
150
108
|
async function testSliceReconciliationIdempotent(): Promise<void> {
|
|
151
|
-
console.log("\n--- Test:
|
|
109
|
+
console.log("\n--- Test: disk-only slices remain absent on repeated derives ---");
|
|
152
110
|
|
|
153
111
|
const base = createFixtureBase();
|
|
154
112
|
const dbPath = join(base, ".gsd", "gsd.db");
|
|
@@ -178,11 +136,7 @@ async function testSliceReconciliationIdempotent(): Promise<void> {
|
|
|
178
136
|
assertEq(s01.status, "complete", "S01 status should remain 'complete' (not overwritten)");
|
|
179
137
|
}
|
|
180
138
|
|
|
181
|
-
|
|
182
|
-
assertTrue(
|
|
183
|
-
dbSlices.length === 4,
|
|
184
|
-
`Should have 4 slices after reconciliation (existing + new), got ${dbSlices.length}`,
|
|
185
|
-
);
|
|
139
|
+
assertEq(dbSlices.length, 1, `Only existing DB slice should remain, got ${dbSlices.length}`);
|
|
186
140
|
} finally {
|
|
187
141
|
closeDatabase();
|
|
188
142
|
cleanup(base);
|
|
@@ -218,7 +172,7 @@ async function testNoRoadmapSkipsReconciliation(): Promise<void> {
|
|
|
218
172
|
}
|
|
219
173
|
|
|
220
174
|
async function main(): Promise<void> {
|
|
221
|
-
console.log("\n===
|
|
175
|
+
console.log("\n=== deriveStateFromDb does not reconcile disk slices ===");
|
|
222
176
|
|
|
223
177
|
await testMissingSlicesCauseBlock();
|
|
224
178
|
await testSliceReconciliationIdempotent();
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* stale-slice-rows.test.ts
|
|
2
|
+
* stale-slice-rows.test.ts
|
|
3
3
|
*
|
|
4
|
-
* Verify that state.ts
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* doneSliceIds from stale DB rows and downstream slices stay blocked.
|
|
4
|
+
* Verify that state.ts no longer treats slice SUMMARY.md projections as
|
|
5
|
+
* authority for DB slice status. Slice rows must be updated through DB-backed
|
|
6
|
+
* completion/import APIs.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
import { describe, test } from "node:test";
|
|
@@ -16,26 +15,26 @@ import { fileURLToPath } from "node:url";
|
|
|
16
15
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
16
|
const sourceFile = join(__dirname, "..", "state.ts");
|
|
18
17
|
|
|
19
|
-
describe("stale slice row
|
|
18
|
+
describe("stale slice row DB-authoritative boundary", () => {
|
|
20
19
|
const source = readFileSync(sourceFile, "utf-8");
|
|
21
20
|
|
|
22
|
-
test("
|
|
23
|
-
assert.
|
|
21
|
+
test("does not import updateSliceStatus into state derivation", () => {
|
|
22
|
+
assert.doesNotMatch(source, /import\s*\{[^}]*updateSliceStatus[^}]*\}\s*from/);
|
|
24
23
|
});
|
|
25
24
|
|
|
26
|
-
test("
|
|
27
|
-
assert.
|
|
25
|
+
test("does not scan DB slice rows for disk SUMMARY reconciliation", () => {
|
|
26
|
+
assert.doesNotMatch(source, /dbSlice/);
|
|
28
27
|
});
|
|
29
28
|
|
|
30
|
-
test("
|
|
31
|
-
assert.
|
|
29
|
+
test("does not resolve slice SUMMARY to mutate DB state", () => {
|
|
30
|
+
assert.doesNotMatch(source, /resolveSliceFile\(basePath,\s*mid,\s*dbSlice\.id,\s*["']SUMMARY["']\)/);
|
|
32
31
|
});
|
|
33
32
|
|
|
34
|
-
test("
|
|
35
|
-
assert.
|
|
33
|
+
test("does not call updateSliceStatus from state derivation", () => {
|
|
34
|
+
assert.doesNotMatch(source, /updateSliceStatus\(/);
|
|
36
35
|
});
|
|
37
36
|
|
|
38
|
-
test("
|
|
39
|
-
assert.match(source,
|
|
37
|
+
test("documents markdown projections as non-authoritative", () => {
|
|
38
|
+
assert.match(source, /Markdown files are projections only/);
|
|
40
39
|
});
|
|
41
40
|
});
|
|
@@ -858,11 +858,11 @@ describe("state-machine-full-walkthrough", () => {
|
|
|
858
858
|
});
|
|
859
859
|
|
|
860
860
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
861
|
-
//
|
|
861
|
+
// DB-AUTHORITATIVE DERIVATION
|
|
862
862
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
863
863
|
|
|
864
|
-
describe("
|
|
865
|
-
test("DB: task with SUMMARY on disk but DB says pending →
|
|
864
|
+
describe("DB-authoritative derivation", () => {
|
|
865
|
+
test("DB: task with SUMMARY on disk but DB says pending → DB remains authoritative", async () => {
|
|
866
866
|
const base = createFixtureBase();
|
|
867
867
|
const dbPath = join(base, ".gsd", "gsd.db");
|
|
868
868
|
openDatabase(dbPath);
|
|
@@ -875,19 +875,19 @@ describe("state-machine-full-walkthrough", () => {
|
|
|
875
875
|
writeRoadmap(base, "M001", standardRoadmap());
|
|
876
876
|
writePlan(base, "M001", "S01", standardPlan());
|
|
877
877
|
|
|
878
|
-
// Write SUMMARY files on disk for both tasks
|
|
878
|
+
// Write SUMMARY files on disk for both tasks. These are projections and
|
|
879
|
+
// must not complete pending DB tasks during runtime derivation.
|
|
879
880
|
writeTaskSummary(base, "M001", "S01", "T01");
|
|
880
881
|
writeTaskSummary(base, "M001", "S01", "T02");
|
|
881
882
|
|
|
882
883
|
invalidateStateCache();
|
|
883
884
|
const state = await deriveStateFromDb(base);
|
|
884
885
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
assert.equal(state.phase, "summarizing", "reconciliation should advance past pending tasks");
|
|
886
|
+
assert.equal(state.phase, "executing", "disk SUMMARY projections must not complete DB tasks");
|
|
887
|
+
assert.equal(state.activeTask?.id, "T01", "first pending DB task remains active");
|
|
888
888
|
});
|
|
889
889
|
|
|
890
|
-
test("empty DB with disk milestones → disk-to-DB sync
|
|
890
|
+
test("empty DB with disk milestones → no runtime disk-to-DB sync", async () => {
|
|
891
891
|
const base = createFixtureBase();
|
|
892
892
|
writeContext(base, "M001", "# M001: Test\n\nContext.");
|
|
893
893
|
|
|
@@ -899,11 +899,10 @@ describe("state-machine-full-walkthrough", () => {
|
|
|
899
899
|
invalidateStateCache();
|
|
900
900
|
const state = await deriveState(base);
|
|
901
901
|
|
|
902
|
-
//
|
|
902
|
+
// Runtime derivation must not import disk milestones into the DB.
|
|
903
903
|
const after = getAllMilestones();
|
|
904
|
-
assert.
|
|
905
|
-
assert.equal(
|
|
906
|
-
assert.ok(state.activeMilestone !== null);
|
|
904
|
+
assert.equal(after.length, 0, "DB should remain empty without explicit migration");
|
|
905
|
+
assert.equal(state.activeMilestone, null, "disk milestone is ignored while DB is authoritative");
|
|
907
906
|
});
|
|
908
907
|
|
|
909
908
|
test("ghost milestone (empty dir) → NOT in registry", async () => {
|
|
@@ -1063,7 +1062,7 @@ describe("state-machine-full-walkthrough", () => {
|
|
|
1063
1062
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1064
1063
|
|
|
1065
1064
|
describe("Recovery: DB has slice but no task rows (partial migration)", () => {
|
|
1066
|
-
test("DB tasks empty but PLAN on disk has tasks →
|
|
1065
|
+
test("DB tasks empty but PLAN on disk has tasks → stays planning", async () => {
|
|
1067
1066
|
const base = createFixtureBase();
|
|
1068
1067
|
const dbPath = join(base, ".gsd", "gsd.db");
|
|
1069
1068
|
openDatabase(dbPath);
|
|
@@ -1078,15 +1077,13 @@ describe("state-machine-full-walkthrough", () => {
|
|
|
1078
1077
|
invalidateStateCache();
|
|
1079
1078
|
const state = await deriveStateFromDb(base);
|
|
1080
1079
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
assert.equal(state.phase, "executing",
|
|
1084
|
-
"reconciled plan-file tasks → executing (not stuck in planning)");
|
|
1080
|
+
assert.equal(state.phase, "planning",
|
|
1081
|
+
"PLAN.md projection must not import DB tasks during runtime derivation");
|
|
1085
1082
|
});
|
|
1086
1083
|
});
|
|
1087
1084
|
|
|
1088
1085
|
describe("Failure: partial SUMMARY reconciliation", () => {
|
|
1089
|
-
test("only one task has SUMMARY, other still pending → executing
|
|
1086
|
+
test("only one task has SUMMARY, other still pending → executing first DB-pending task", async () => {
|
|
1090
1087
|
const base = createFixtureBase();
|
|
1091
1088
|
const dbPath = join(base, ".gsd", "gsd.db");
|
|
1092
1089
|
openDatabase(dbPath);
|
|
@@ -1104,9 +1101,8 @@ describe("state-machine-full-walkthrough", () => {
|
|
|
1104
1101
|
invalidateStateCache();
|
|
1105
1102
|
const state = await deriveStateFromDb(base);
|
|
1106
1103
|
|
|
1107
|
-
// T01 reconciled to complete, T02 still pending → executing T02
|
|
1108
1104
|
assert.equal(state.phase, "executing");
|
|
1109
|
-
assert.equal(state.activeTask?.id, "
|
|
1105
|
+
assert.equal(state.activeTask?.id, "T01", "disk SUMMARY must not advance past pending DB task");
|
|
1110
1106
|
});
|
|
1111
1107
|
});
|
|
1112
1108
|
|
|
@@ -1255,7 +1251,7 @@ describe("state-machine-full-walkthrough", () => {
|
|
|
1255
1251
|
});
|
|
1256
1252
|
|
|
1257
1253
|
describe("Failure: missing task plan files in DB path", () => {
|
|
1258
|
-
test("DB has tasks but no T##-PLAN.md files →
|
|
1254
|
+
test("DB has tasks but no T##-PLAN.md files → executing phase", async () => {
|
|
1259
1255
|
const base = createFixtureBase();
|
|
1260
1256
|
const dbPath = join(base, ".gsd", "gsd.db");
|
|
1261
1257
|
openDatabase(dbPath);
|
|
@@ -1273,8 +1269,8 @@ describe("state-machine-full-walkthrough", () => {
|
|
|
1273
1269
|
invalidateStateCache();
|
|
1274
1270
|
const state = await deriveStateFromDb(base);
|
|
1275
1271
|
|
|
1276
|
-
assert.equal(state.phase, "
|
|
1277
|
-
"
|
|
1272
|
+
assert.equal(state.phase, "executing",
|
|
1273
|
+
"DB tasks are authoritative even when task plan projections are missing");
|
|
1278
1274
|
});
|
|
1279
1275
|
});
|
|
1280
1276
|
|
|
@@ -1591,7 +1587,7 @@ describe("state-machine-full-walkthrough", () => {
|
|
|
1591
1587
|
});
|
|
1592
1588
|
|
|
1593
1589
|
describe("Failure: multiple reconciliation in single derivation", () => {
|
|
1594
|
-
test("DB has 3 stale tasks, all with SUMMARY on disk →
|
|
1590
|
+
test("DB has 3 stale tasks, all with SUMMARY on disk → first DB-pending task remains active", async () => {
|
|
1595
1591
|
const base = createFixtureBase();
|
|
1596
1592
|
const dbPath = join(base, ".gsd", "gsd.db");
|
|
1597
1593
|
openDatabase(dbPath);
|
|
@@ -1641,9 +1637,9 @@ describe("state-machine-full-walkthrough", () => {
|
|
|
1641
1637
|
invalidateStateCache();
|
|
1642
1638
|
const state = await deriveStateFromDb(base);
|
|
1643
1639
|
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1640
|
+
assert.equal(state.phase, "executing",
|
|
1641
|
+
"disk SUMMARY projections must not reconcile DB task state");
|
|
1642
|
+
assert.equal(state.activeTask?.id, "T01", "first non-closed DB task remains active");
|
|
1647
1643
|
});
|
|
1648
1644
|
});
|
|
1649
1645
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// GSD Extension - Steer Worktree Path Resolution Test
|
|
2
|
-
//
|
|
3
|
-
//
|
|
2
|
+
// Worktrees share the canonical project .gsd state root. /gsd steer writes
|
|
3
|
+
// overrides to that canonical root even when invoked with a worktree path.
|
|
4
4
|
|
|
5
5
|
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
6
6
|
import assert from "node:assert/strict";
|
|
@@ -27,32 +27,31 @@ describe("steer worktree path resolution (#3476)", () => {
|
|
|
27
27
|
rmSync(projectRoot, { recursive: true, force: true });
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
test("appendOverride writes to
|
|
30
|
+
test("appendOverride writes to canonical project .gsd/ when worktree path is used", async () => {
|
|
31
31
|
await appendOverride(worktreePath, "Use Postgres instead of SQLite", "M001/S01/T01");
|
|
32
32
|
|
|
33
|
-
// Override should be in the
|
|
33
|
+
// Override should be in the canonical project .gsd/
|
|
34
34
|
const wtOverrides = join(worktreePath, ".gsd", "OVERRIDES.md");
|
|
35
|
-
|
|
35
|
+
const rootOverrides = join(projectRoot, ".gsd", "OVERRIDES.md");
|
|
36
|
+
assert.ok(!existsSync(wtOverrides), "no override file in worktree-local .gsd/");
|
|
37
|
+
assert.ok(existsSync(rootOverrides), "override file exists in project root .gsd/");
|
|
36
38
|
|
|
37
|
-
const content = readFileSync(
|
|
39
|
+
const content = readFileSync(rootOverrides, "utf-8");
|
|
38
40
|
assert.ok(content.includes("Use Postgres instead of SQLite"), "override content is correct");
|
|
39
|
-
|
|
40
|
-
// Override should NOT be in the project root .gsd/
|
|
41
|
-
const rootOverrides = join(projectRoot, ".gsd", "OVERRIDES.md");
|
|
42
|
-
assert.ok(!existsSync(rootOverrides), "no override file in project root .gsd/");
|
|
43
41
|
});
|
|
44
42
|
|
|
45
|
-
test("loadActiveOverrides reads
|
|
43
|
+
test("loadActiveOverrides reads canonical project .gsd/ when worktree path is used", async () => {
|
|
46
44
|
await appendOverride(worktreePath, "Switch to JWT auth", "M001/S02/T01");
|
|
47
45
|
|
|
48
|
-
// Loading from worktree
|
|
46
|
+
// Loading from worktree resolves to the canonical project state root.
|
|
49
47
|
const wtOverrides = await loadActiveOverrides(worktreePath);
|
|
50
48
|
assert.equal(wtOverrides.length, 1, "one active override in worktree");
|
|
51
49
|
assert.equal(wtOverrides[0].change, "Switch to JWT auth");
|
|
52
50
|
|
|
53
|
-
// Loading from project root
|
|
51
|
+
// Loading from project root sees the same canonical override.
|
|
54
52
|
const rootOverrides = await loadActiveOverrides(projectRoot);
|
|
55
|
-
assert.equal(rootOverrides.length,
|
|
53
|
+
assert.equal(rootOverrides.length, 1, "same override visible from project root");
|
|
54
|
+
assert.equal(rootOverrides[0].change, "Switch to JWT auth");
|
|
56
55
|
});
|
|
57
56
|
|
|
58
57
|
test("appendOverride falls back to project root when no worktree exists", async () => {
|