gsd-pi 2.66.0 → 2.66.1-dev.0df32ec
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/claude-cli-check.d.ts +8 -0
- package/dist/claude-cli-check.js +36 -0
- package/dist/cli.js +40 -0
- package/dist/onboarding.js +19 -2
- package/dist/resources/extensions/claude-code-cli/readiness.js +63 -12
- package/dist/resources/extensions/gsd/auto/phases.js +15 -2
- package/dist/resources/extensions/gsd/auto-model-selection.js +12 -3
- package/dist/resources/extensions/gsd/auto-prompts.js +167 -19
- package/dist/resources/extensions/gsd/auto.js +13 -1
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +32 -1
- package/dist/resources/extensions/gsd/bootstrap/provider-error-resume.js +5 -0
- package/dist/resources/extensions/gsd/context-store.js +134 -2
- package/dist/resources/extensions/gsd/preferences.js +6 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +20 -20
- 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/required-server-files.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- 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/page.js +2 -2
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- 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 +3 -3
- 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 +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +4 -4
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
- 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 +3 -3
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/page.js +2 -2
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +20 -20
- package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
- package/dist/web/standalone/.next/server/chunks/7471.js +3 -3
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware.js +2 -2
- package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
- package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/page-0c485498795110d6.js +1 -0
- package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
- package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
- package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts +16 -0
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.js +58 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.test.js +58 -0
- package/packages/pi-coding-agent/dist/core/retry-handler.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +1 -0
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +4 -0
- package/packages/pi-coding-agent/src/core/retry-handler.test.ts +69 -0
- package/packages/pi-coding-agent/src/core/retry-handler.ts +66 -1
- package/packages/pi-coding-agent/src/core/sdk.ts +5 -0
- package/packages/pi-tui/dist/tui.d.ts.map +1 -1
- package/packages/pi-tui/dist/tui.js +1 -3
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/src/tui.ts +1 -3
- package/pkg/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/readiness.ts +67 -12
- package/src/resources/extensions/gsd/auto/phases.ts +20 -2
- package/src/resources/extensions/gsd/auto-model-selection.ts +12 -3
- package/src/resources/extensions/gsd/auto-prompts.ts +190 -19
- package/src/resources/extensions/gsd/auto.ts +12 -1
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +34 -1
- package/src/resources/extensions/gsd/bootstrap/provider-error-resume.ts +6 -0
- package/src/resources/extensions/gsd/context-store.ts +167 -2
- package/src/resources/extensions/gsd/preferences.ts +6 -1
- package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +21 -7
- package/src/resources/extensions/gsd/tests/context-store.test.ts +176 -0
- package/src/resources/extensions/gsd/tests/decision-scope-cascade.test.ts +370 -0
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/measurement.test.ts +531 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +60 -0
- package/dist/web/standalone/.next/static/chunks/app/page-62be3b5fa91e4c8f.js +0 -1
- package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
- /package/dist/web/standalone/.next/static/{Bdk1mnQugYZh7ZxuXUYvc → Zw5aZFHFtOwjJSOsINh1m}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{Bdk1mnQugYZh7ZxuXUYvc → Zw5aZFHFtOwjJSOsINh1m}/_ssgManifest.js +0 -0
|
@@ -143,17 +143,17 @@ test("resolvePreferredModelConfig keeps explicit phase models as the ceiling", (
|
|
|
143
143
|
|
|
144
144
|
// ─── resolveModelId tests ─────────────────────────────────────────────────
|
|
145
145
|
|
|
146
|
-
test("resolveModelId: bare ID resolves to
|
|
146
|
+
test("resolveModelId: bare ID resolves to claude-code when session is claude-code (#3772)", () => {
|
|
147
147
|
const availableModels = [
|
|
148
148
|
{ id: "claude-sonnet-4-6", provider: "anthropic" },
|
|
149
149
|
{ id: "claude-sonnet-4-6", provider: "claude-code" },
|
|
150
150
|
];
|
|
151
151
|
|
|
152
|
-
//
|
|
153
|
-
//
|
|
152
|
+
// When currentProvider is "claude-code" (set by startup migration for subscription
|
|
153
|
+
// users), bare IDs must resolve to claude-code to avoid the third-party block (#3772).
|
|
154
154
|
const result = resolveModelId("claude-sonnet-4-6", availableModels, "claude-code");
|
|
155
155
|
assert.ok(result, "should resolve a model");
|
|
156
|
-
assert.equal(result.provider, "
|
|
156
|
+
assert.equal(result.provider, "claude-code", "bare ID must resolve to claude-code when session provider is claude-code");
|
|
157
157
|
});
|
|
158
158
|
|
|
159
159
|
test("resolveModelId: bare ID still prefers current provider when it is a first-class API provider", () => {
|
|
@@ -227,14 +227,28 @@ test("model change notify in selectAndApplyModel is gated behind verbose flag",
|
|
|
227
227
|
);
|
|
228
228
|
});
|
|
229
229
|
|
|
230
|
-
test("resolveModelId: anthropic wins over claude-code
|
|
230
|
+
test("resolveModelId: anthropic wins over claude-code when session provider is not claude-code", () => {
|
|
231
231
|
const availableModels = [
|
|
232
232
|
{ id: "claude-sonnet-4-6", provider: "claude-code" },
|
|
233
233
|
{ id: "claude-sonnet-4-6", provider: "anthropic" },
|
|
234
234
|
];
|
|
235
235
|
|
|
236
|
-
//
|
|
236
|
+
// When the session is NOT on claude-code, bare IDs should resolve to
|
|
237
|
+
// the canonical anthropic provider (original #2905 behavior preserved).
|
|
238
|
+
const result = resolveModelId("claude-sonnet-4-6", availableModels, undefined);
|
|
239
|
+
assert.ok(result, "should resolve a model");
|
|
240
|
+
assert.equal(result.provider, "anthropic", "anthropic must win when session is not claude-code");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("resolveModelId: claude-code wins when session is claude-code regardless of list order", () => {
|
|
244
|
+
const availableModels = [
|
|
245
|
+
{ id: "claude-sonnet-4-6", provider: "claude-code" },
|
|
246
|
+
{ id: "claude-sonnet-4-6", provider: "anthropic" },
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
// When session provider is claude-code (subscription user migration), it must
|
|
250
|
+
// win regardless of candidate ordering to avoid the third-party block (#3772).
|
|
237
251
|
const result = resolveModelId("claude-sonnet-4-6", availableModels, "claude-code");
|
|
238
252
|
assert.ok(result, "should resolve a model");
|
|
239
|
-
assert.equal(result.provider, "
|
|
253
|
+
assert.equal(result.provider, "claude-code", "claude-code must win when it is the session provider");
|
|
240
254
|
});
|
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
formatRequirementsForPrompt,
|
|
16
16
|
queryArtifact,
|
|
17
17
|
queryProject,
|
|
18
|
+
formatRoadmapExcerpt,
|
|
19
|
+
queryKnowledge,
|
|
18
20
|
} from '../context-store.ts';
|
|
19
21
|
|
|
20
22
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -452,3 +454,177 @@ describe("context-store: queryProject", () => {
|
|
|
452
454
|
assert.strictEqual(content, null, 'queryProject returns null when DB closed');
|
|
453
455
|
});
|
|
454
456
|
});
|
|
457
|
+
|
|
458
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
459
|
+
// context-store: formatRoadmapExcerpt
|
|
460
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
461
|
+
|
|
462
|
+
describe("context-store: formatRoadmapExcerpt", () => {
|
|
463
|
+
// Sample roadmap content matching actual M005-ROADMAP.md format
|
|
464
|
+
const sampleRoadmap = `# M005: Tiered Context Injection
|
|
465
|
+
|
|
466
|
+
## Vision
|
|
467
|
+
Refactor prompt builders to inject relevance-scoped context.
|
|
468
|
+
|
|
469
|
+
## Slice Overview
|
|
470
|
+
| ID | Slice | Risk | Depends | Done | After this |
|
|
471
|
+
|----|-------|------|---------|------|------------|
|
|
472
|
+
| S01 | Scope existing queries | low | — | ✅ | planSlice prompt scoped. |
|
|
473
|
+
| S02 | KNOWLEDGE scoping | medium | S01 | ⬜ | KNOWLEDGE sections filtered. |
|
|
474
|
+
| S03 | Measurement test | low | S02 | ⬜ | 40% reduction confirmed. |
|
|
475
|
+
`;
|
|
476
|
+
|
|
477
|
+
test("S02 with S01 predecessor includes both rows", () => {
|
|
478
|
+
const result = formatRoadmapExcerpt(sampleRoadmap, 'S02', '.gsd/milestones/M005/M005-ROADMAP.md');
|
|
479
|
+
|
|
480
|
+
// Should have header
|
|
481
|
+
assert.match(result, /\| ID \| Slice \| Risk \| Depends \| Done \| After this \|/, 'has header row');
|
|
482
|
+
// Should have separator
|
|
483
|
+
assert.match(result, /\|----\|/, 'has separator row');
|
|
484
|
+
// Should have S01 predecessor
|
|
485
|
+
assert.match(result, /\| S01 \|/, 'has predecessor S01 row');
|
|
486
|
+
// Should have S02 target
|
|
487
|
+
assert.match(result, /\| S02 \|/, 'has target S02 row');
|
|
488
|
+
// Should have reference directive
|
|
489
|
+
assert.match(result, /See full roadmap:.*M005-ROADMAP\.md/, 'has reference directive');
|
|
490
|
+
// Should NOT have S03 (not relevant)
|
|
491
|
+
assert.ok(!result.includes('| S03 |'), 'does not include unrelated S03');
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("S01 with no predecessor includes only target row", () => {
|
|
495
|
+
const result = formatRoadmapExcerpt(sampleRoadmap, 'S01');
|
|
496
|
+
|
|
497
|
+
// Should have header + separator + S01 only
|
|
498
|
+
assert.match(result, /\| ID \| Slice \|/, 'has header row');
|
|
499
|
+
assert.match(result, /\| S01 \|/, 'has target S01 row');
|
|
500
|
+
// Should NOT have S02 or S03
|
|
501
|
+
assert.ok(!result.includes('| S02 |'), 'does not include S02');
|
|
502
|
+
assert.ok(!result.includes('| S03 |'), 'does not include S03');
|
|
503
|
+
// Should have reference
|
|
504
|
+
assert.match(result, /See full roadmap:/, 'has reference directive');
|
|
505
|
+
|
|
506
|
+
// Count rows: header + separator + S01 + blank + directive = 5 lines
|
|
507
|
+
const lines = result.split('\n');
|
|
508
|
+
assert.strictEqual(lines.length, 5, 'correct number of lines (no predecessor)');
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
test("missing slice returns empty string", () => {
|
|
512
|
+
const result = formatRoadmapExcerpt(sampleRoadmap, 'S99');
|
|
513
|
+
|
|
514
|
+
assert.strictEqual(result, '', 'missing slice returns empty string');
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
test("empty input returns empty string", () => {
|
|
518
|
+
assert.strictEqual(formatRoadmapExcerpt('', 'S01'), '', 'empty content returns empty');
|
|
519
|
+
assert.strictEqual(formatRoadmapExcerpt(sampleRoadmap, ''), '', 'empty sliceId returns empty');
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test("handles table with various column formats", () => {
|
|
523
|
+
// Table with different spacing and content
|
|
524
|
+
const variantRoadmap = `# Milestone
|
|
525
|
+
|
|
526
|
+
| ID | Slice | Risk | Depends | Done | After this |
|
|
527
|
+
|:---|:------|:-----|:--------|:-----|:-----------|
|
|
528
|
+
| S01 | First slice title | low | — | ✅ | First complete. |
|
|
529
|
+
| S02 | Second longer slice title here | medium | S01 | ⬜ | Second working. |
|
|
530
|
+
`;
|
|
531
|
+
|
|
532
|
+
const result = formatRoadmapExcerpt(variantRoadmap, 'S02');
|
|
533
|
+
|
|
534
|
+
assert.match(result, /\| S01 \|/, 'has predecessor with different spacing');
|
|
535
|
+
assert.match(result, /\| S02 \|/, 'has target with different spacing');
|
|
536
|
+
assert.match(result, /Second longer slice title/, 'preserves full slice title');
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
test("handles multiple dependencies by using first one", () => {
|
|
540
|
+
const multiDepRoadmap = `| ID | Slice | Risk | Depends | Done | After this |
|
|
541
|
+
|----|-------|------|---------|------|------------|
|
|
542
|
+
| S01 | First | low | — | ✅ | Done. |
|
|
543
|
+
| S02 | Second | low | — | ✅ | Done. |
|
|
544
|
+
| S03 | Third | medium | S01, S02 | ⬜ | Working. |
|
|
545
|
+
`;
|
|
546
|
+
|
|
547
|
+
const result = formatRoadmapExcerpt(multiDepRoadmap, 'S03');
|
|
548
|
+
|
|
549
|
+
// Should include S01 (first dependency) and S03
|
|
550
|
+
assert.match(result, /\| S01 \|/, 'has first dependency S01');
|
|
551
|
+
assert.match(result, /\| S03 \|/, 'has target S03');
|
|
552
|
+
// S02 is also a dependency but we only include the first one
|
|
553
|
+
// (This is intentional to keep excerpts minimal)
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
558
|
+
// context-store: queryKnowledge
|
|
559
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
560
|
+
|
|
561
|
+
describe("context-store: queryKnowledge", () => {
|
|
562
|
+
// Sample KNOWLEDGE.md content
|
|
563
|
+
const sampleKnowledge = `# Project Knowledge
|
|
564
|
+
|
|
565
|
+
## Database Patterns
|
|
566
|
+
SQLite is used with WAL mode for concurrent reads.
|
|
567
|
+
Always use prepared statements.
|
|
568
|
+
|
|
569
|
+
More database details here.
|
|
570
|
+
|
|
571
|
+
## API Design
|
|
572
|
+
REST endpoints follow OpenAPI spec.
|
|
573
|
+
Use versioned paths like /v1/resource.
|
|
574
|
+
|
|
575
|
+
## Testing Guidelines
|
|
576
|
+
Unit tests use node:test.
|
|
577
|
+
Integration tests mock external services.
|
|
578
|
+
`;
|
|
579
|
+
|
|
580
|
+
test("single keyword matches header", async () => {
|
|
581
|
+
const result = await queryKnowledge(sampleKnowledge, ['database']);
|
|
582
|
+
|
|
583
|
+
assert.match(result, /## Database Patterns/, 'includes matching section header');
|
|
584
|
+
assert.match(result, /SQLite is used with WAL mode/, 'includes section content');
|
|
585
|
+
// Should NOT include other sections
|
|
586
|
+
assert.ok(!result.includes('## API Design'), 'does not include non-matching API section');
|
|
587
|
+
assert.ok(!result.includes('## Testing Guidelines'), 'does not include non-matching Testing section');
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
test("multiple keywords match multiple sections", async () => {
|
|
591
|
+
const result = await queryKnowledge(sampleKnowledge, ['database', 'testing']);
|
|
592
|
+
|
|
593
|
+
assert.match(result, /## Database Patterns/, 'includes Database section');
|
|
594
|
+
assert.match(result, /## Testing Guidelines/, 'includes Testing section');
|
|
595
|
+
assert.ok(!result.includes('## API Design'), 'does not include API section');
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test("no matches returns empty string", async () => {
|
|
599
|
+
const result = await queryKnowledge(sampleKnowledge, ['nonexistent']);
|
|
600
|
+
|
|
601
|
+
assert.strictEqual(result, '', 'no matches returns empty string per D020');
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
test("keyword in first paragraph matches", async () => {
|
|
605
|
+
const result = await queryKnowledge(sampleKnowledge, ['sqlite']);
|
|
606
|
+
|
|
607
|
+
// 'sqlite' appears in first paragraph of Database Patterns
|
|
608
|
+
assert.match(result, /## Database Patterns/, 'matches keyword in first paragraph');
|
|
609
|
+
assert.match(result, /SQLite is used/, 'includes the section with matching paragraph');
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
test("case-insensitive matching", async () => {
|
|
613
|
+
const result = await queryKnowledge(sampleKnowledge, ['DATABASE', 'API']);
|
|
614
|
+
|
|
615
|
+
assert.match(result, /## Database Patterns/, 'case-insensitive header match');
|
|
616
|
+
assert.match(result, /## API Design/, 'case-insensitive header match for API');
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
test("empty keywords returns empty string", async () => {
|
|
620
|
+
const result = await queryKnowledge(sampleKnowledge, []);
|
|
621
|
+
|
|
622
|
+
assert.strictEqual(result, '', 'empty keywords returns empty string');
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
test("empty content returns empty string", async () => {
|
|
626
|
+
const result = await queryKnowledge('', ['database']);
|
|
627
|
+
|
|
628
|
+
assert.strictEqual(result, '', 'empty content returns empty string');
|
|
629
|
+
});
|
|
630
|
+
});
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
// decision-scope-cascade: Tests for R005 fallback cascade and scope derivation
|
|
2
|
+
//
|
|
3
|
+
// Validates:
|
|
4
|
+
// (a) inlineDecisionsFromDb cascade: milestone + scope → milestone only → null
|
|
5
|
+
// (b) deriveSliceScope extracts meaningful scope keywords from slice titles
|
|
6
|
+
// (c) deriveSliceScope returns undefined for generic titles
|
|
7
|
+
|
|
8
|
+
import { describe, test, afterEach, beforeEach } from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import {
|
|
11
|
+
openDatabase,
|
|
12
|
+
closeDatabase,
|
|
13
|
+
isDbAvailable,
|
|
14
|
+
insertDecision,
|
|
15
|
+
} from '../gsd-db.ts';
|
|
16
|
+
import {
|
|
17
|
+
queryDecisions,
|
|
18
|
+
formatDecisionsForPrompt,
|
|
19
|
+
} from '../context-store.ts';
|
|
20
|
+
import { deriveSliceScope } from '../auto-prompts.ts';
|
|
21
|
+
|
|
22
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
23
|
+
// deriveSliceScope: Extract meaningful scope from slice titles
|
|
24
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
25
|
+
|
|
26
|
+
describe("deriveSliceScope: keyword extraction", () => {
|
|
27
|
+
test("extracts first meaningful noun from title", () => {
|
|
28
|
+
// "Auth Middleware & Protected Route" → "auth"
|
|
29
|
+
assert.strictEqual(
|
|
30
|
+
deriveSliceScope("Auth Middleware & Protected Route"),
|
|
31
|
+
"auth",
|
|
32
|
+
"extracts 'auth' from auth-related title",
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// "Database & User Model Setup" → "database" (not "setup" which is generic)
|
|
36
|
+
const dbScope = deriveSliceScope("Database & User Model Setup");
|
|
37
|
+
assert.ok(
|
|
38
|
+
dbScope === "database" || dbScope === "user",
|
|
39
|
+
`expected 'database' or 'user', got '${dbScope}'`,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// "API Rate Limiting" → "api"
|
|
43
|
+
assert.strictEqual(
|
|
44
|
+
deriveSliceScope("API Rate Limiting"),
|
|
45
|
+
"api",
|
|
46
|
+
"extracts 'api' from API-related title",
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// "Stripe Payment Integration" → "stripe"
|
|
50
|
+
assert.strictEqual(
|
|
51
|
+
deriveSliceScope("Stripe Payment Integration"),
|
|
52
|
+
"stripe",
|
|
53
|
+
"extracts 'stripe' from payment-related title",
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("returns undefined for generic titles", () => {
|
|
58
|
+
// "Integration Testing" → undefined (both words are generic)
|
|
59
|
+
assert.strictEqual(
|
|
60
|
+
deriveSliceScope("Integration Testing"),
|
|
61
|
+
undefined,
|
|
62
|
+
"returns undefined for generic 'Integration Testing'",
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// "Setup & Configuration" → undefined (all generic)
|
|
66
|
+
assert.strictEqual(
|
|
67
|
+
deriveSliceScope("Setup & Configuration"),
|
|
68
|
+
undefined,
|
|
69
|
+
"returns undefined for generic 'Setup & Configuration'",
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// "Final Review" → undefined
|
|
73
|
+
assert.strictEqual(
|
|
74
|
+
deriveSliceScope("Final Review"),
|
|
75
|
+
undefined,
|
|
76
|
+
"returns undefined for generic 'Final Review'",
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// "Basic Implementation" → undefined
|
|
80
|
+
assert.strictEqual(
|
|
81
|
+
deriveSliceScope("Basic Implementation"),
|
|
82
|
+
undefined,
|
|
83
|
+
"returns undefined for generic 'Basic Implementation'",
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("handles description as additional context", () => {
|
|
88
|
+
// Generic title but specific description
|
|
89
|
+
const scope = deriveSliceScope(
|
|
90
|
+
"Initial Setup",
|
|
91
|
+
"Configure PostgreSQL database connection",
|
|
92
|
+
);
|
|
93
|
+
assert.ok(
|
|
94
|
+
scope === "postgresql" || scope === "database" || scope === "configure",
|
|
95
|
+
`expected meaningful scope from description, got '${scope}'`,
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("handles edge cases", () => {
|
|
100
|
+
// Empty title
|
|
101
|
+
assert.strictEqual(
|
|
102
|
+
deriveSliceScope(""),
|
|
103
|
+
undefined,
|
|
104
|
+
"returns undefined for empty title",
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// Short words only
|
|
108
|
+
assert.strictEqual(
|
|
109
|
+
deriveSliceScope("A B C"),
|
|
110
|
+
undefined,
|
|
111
|
+
"returns undefined for very short words",
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Mixed case and punctuation
|
|
115
|
+
assert.strictEqual(
|
|
116
|
+
deriveSliceScope("OAuth2 + JWT Authentication"),
|
|
117
|
+
"oauth2",
|
|
118
|
+
"handles mixed case and punctuation",
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("filters unit IDs (S01, M001, T03)", () => {
|
|
123
|
+
// "S01: Infrastructure" → undefined (S01 is a unit ID, infrastructure is generic)
|
|
124
|
+
assert.strictEqual(
|
|
125
|
+
deriveSliceScope("S01: Infrastructure"),
|
|
126
|
+
undefined,
|
|
127
|
+
"skips S01 ID and returns undefined for generic 'Infrastructure'",
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// "M001 Setup" → undefined (M001 is a unit ID, setup is generic)
|
|
131
|
+
assert.strictEqual(
|
|
132
|
+
deriveSliceScope("M001 Setup"),
|
|
133
|
+
undefined,
|
|
134
|
+
"skips M001 ID and returns undefined for generic 'Setup'",
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// "T03: Database Migration" → "database" (skips T03, returns meaningful word)
|
|
138
|
+
assert.strictEqual(
|
|
139
|
+
deriveSliceScope("T03: Database Migration"),
|
|
140
|
+
"database",
|
|
141
|
+
"skips T03 ID and returns 'database'",
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// "S02 Auth Flow" → "auth" (skips S02, returns meaningful word)
|
|
145
|
+
assert.strictEqual(
|
|
146
|
+
deriveSliceScope("S02 Auth Flow"),
|
|
147
|
+
"auth",
|
|
148
|
+
"skips S02 ID and returns 'auth'",
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("filters process/activity words", () => {
|
|
153
|
+
// "Integration Testing + Hardening" → undefined (all generic/process words)
|
|
154
|
+
assert.strictEqual(
|
|
155
|
+
deriveSliceScope("Integration Testing + Hardening"),
|
|
156
|
+
undefined,
|
|
157
|
+
"returns undefined for 'Integration Testing + Hardening'",
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// "Validation & Verification" → undefined (both are process words)
|
|
161
|
+
assert.strictEqual(
|
|
162
|
+
deriveSliceScope("Validation & Verification"),
|
|
163
|
+
undefined,
|
|
164
|
+
"returns undefined for 'Validation & Verification'",
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// "Performance Optimization" → "performance" (optimization is generic, performance is domain)
|
|
168
|
+
assert.strictEqual(
|
|
169
|
+
deriveSliceScope("Performance Optimization"),
|
|
170
|
+
"performance",
|
|
171
|
+
"extracts 'performance' before generic 'optimization'",
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// "Security Enhancement" → "security" (enhancement is generic, security is domain)
|
|
175
|
+
assert.strictEqual(
|
|
176
|
+
deriveSliceScope("Security Enhancement"),
|
|
177
|
+
"security",
|
|
178
|
+
"extracts 'security' before generic 'enhancement'",
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// "WebSocket Delivery Pipeline" → "websocket"
|
|
182
|
+
assert.strictEqual(
|
|
183
|
+
deriveSliceScope("WebSocket Delivery Pipeline"),
|
|
184
|
+
"websocket",
|
|
185
|
+
"extracts 'websocket' from delivery pipeline title",
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// "Prisma Schema + Migration" → "prisma"
|
|
189
|
+
assert.strictEqual(
|
|
190
|
+
deriveSliceScope("Prisma Schema + Migration"),
|
|
191
|
+
"prisma",
|
|
192
|
+
"extracts 'prisma' from schema migration title",
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
198
|
+
// inlineDecisionsFromDb cascade: R005 implementation
|
|
199
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
200
|
+
|
|
201
|
+
describe("inlineDecisionsFromDb: cascade fallback (R005)", () => {
|
|
202
|
+
beforeEach(() => {
|
|
203
|
+
openDatabase(':memory:');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
afterEach(() => {
|
|
207
|
+
closeDatabase();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("cascade: scoped query returns scoped decisions when they exist", () => {
|
|
211
|
+
// Insert decisions with different scopes
|
|
212
|
+
insertDecision({
|
|
213
|
+
id: 'D001', when_context: 'M001/S01', scope: 'auth',
|
|
214
|
+
decision: 'use JWT', choice: 'JWT', rationale: 'standard',
|
|
215
|
+
revisable: 'yes', made_by: 'agent', superseded_by: null,
|
|
216
|
+
});
|
|
217
|
+
insertDecision({
|
|
218
|
+
id: 'D002', when_context: 'M001/S02', scope: 'database',
|
|
219
|
+
decision: 'use PostgreSQL', choice: 'PostgreSQL', rationale: 'relational',
|
|
220
|
+
revisable: 'yes', made_by: 'agent', superseded_by: null,
|
|
221
|
+
});
|
|
222
|
+
insertDecision({
|
|
223
|
+
id: 'D003', when_context: 'M001/S01', scope: 'architecture',
|
|
224
|
+
decision: 'use microservices', choice: 'microservices', rationale: 'scalable',
|
|
225
|
+
revisable: 'yes', made_by: 'agent', superseded_by: null,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Query with scope 'auth' should return D001 only
|
|
229
|
+
const authDecisions = queryDecisions({ milestoneId: 'M001', scope: 'auth' });
|
|
230
|
+
assert.strictEqual(authDecisions.length, 1, 'scoped query returns 1 decision');
|
|
231
|
+
assert.strictEqual(authDecisions[0]?.id, 'D001', 'returns D001 for auth scope');
|
|
232
|
+
|
|
233
|
+
// Query with scope 'database' should return D002 only
|
|
234
|
+
const dbDecisions = queryDecisions({ milestoneId: 'M001', scope: 'database' });
|
|
235
|
+
assert.strictEqual(dbDecisions.length, 1, 'scoped query returns 1 decision');
|
|
236
|
+
assert.strictEqual(dbDecisions[0]?.id, 'D002', 'returns D002 for database scope');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("cascade: milestone-only fallback when scoped query returns empty", () => {
|
|
240
|
+
// Insert decisions for M001 with generic scope (e.g. 'architecture')
|
|
241
|
+
insertDecision({
|
|
242
|
+
id: 'D001', when_context: 'M001/S01', scope: 'architecture',
|
|
243
|
+
decision: 'use microservices', choice: 'microservices', rationale: 'scalable',
|
|
244
|
+
revisable: 'yes', made_by: 'agent', superseded_by: null,
|
|
245
|
+
});
|
|
246
|
+
insertDecision({
|
|
247
|
+
id: 'D002', when_context: 'M001/S02', scope: 'performance',
|
|
248
|
+
decision: 'use caching', choice: 'Redis', rationale: 'fast',
|
|
249
|
+
revisable: 'yes', made_by: 'agent', superseded_by: null,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Query with scope 'auth' (no decisions with this scope) should return empty
|
|
253
|
+
const authDecisions = queryDecisions({ milestoneId: 'M001', scope: 'auth' });
|
|
254
|
+
assert.strictEqual(authDecisions.length, 0, 'scoped query for auth returns empty');
|
|
255
|
+
|
|
256
|
+
// Simulate cascade: fallback to milestone-only query
|
|
257
|
+
const milestoneDecisions = queryDecisions({ milestoneId: 'M001' });
|
|
258
|
+
assert.strictEqual(milestoneDecisions.length, 2, 'milestone-only query returns 2 decisions');
|
|
259
|
+
const ids = milestoneDecisions.map(d => d.id).sort();
|
|
260
|
+
assert.deepStrictEqual(ids, ['D001', 'D002'], 'milestone fallback returns all M001 decisions');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("cascade: returns null when both scoped and milestone queries are empty", () => {
|
|
264
|
+
// Insert decisions only for M002
|
|
265
|
+
insertDecision({
|
|
266
|
+
id: 'D001', when_context: 'M002/S01', scope: 'auth',
|
|
267
|
+
decision: 'use OAuth', choice: 'OAuth2', rationale: 'standard',
|
|
268
|
+
revisable: 'yes', made_by: 'agent', superseded_by: null,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Query M001 with scope should return empty (no M001 decisions at all)
|
|
272
|
+
const scopedDecisions = queryDecisions({ milestoneId: 'M001', scope: 'auth' });
|
|
273
|
+
assert.strictEqual(scopedDecisions.length, 0, 'scoped query returns empty');
|
|
274
|
+
|
|
275
|
+
// Fallback to milestone-only should also return empty (no M001 decisions)
|
|
276
|
+
const milestoneDecisions = queryDecisions({ milestoneId: 'M001' });
|
|
277
|
+
assert.strictEqual(milestoneDecisions.length, 0, 'milestone-only query returns empty');
|
|
278
|
+
|
|
279
|
+
// This scenario would result in null from inlineDecisionsFromDb
|
|
280
|
+
// (we can't directly test inlineDecisionsFromDb here without mocking fs)
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("cascade: demonstrates the full cascade behavior", () => {
|
|
284
|
+
// This test demonstrates the cascade logic that inlineDecisionsFromDb implements:
|
|
285
|
+
// 1. First try { milestoneId: 'M001', scope: 'payment' } → empty
|
|
286
|
+
// 2. Then try { milestoneId: 'M001' } → gets D001, D002
|
|
287
|
+
// 3. Return the milestone-level decisions
|
|
288
|
+
|
|
289
|
+
// Setup: decisions exist at milestone level but not for 'payment' scope
|
|
290
|
+
insertDecision({
|
|
291
|
+
id: 'D001', when_context: 'M001/S01', scope: 'architecture',
|
|
292
|
+
decision: 'use REST', choice: 'REST API', rationale: 'standard',
|
|
293
|
+
revisable: 'yes', made_by: 'agent', superseded_by: null,
|
|
294
|
+
});
|
|
295
|
+
insertDecision({
|
|
296
|
+
id: 'D002', when_context: 'M001/S02', scope: 'security',
|
|
297
|
+
decision: 'use HTTPS', choice: 'TLS 1.3', rationale: 'secure',
|
|
298
|
+
revisable: 'yes', made_by: 'agent', superseded_by: null,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Step 1: Query with scope 'payment' (no matches)
|
|
302
|
+
const paymentDecisions = queryDecisions({ milestoneId: 'M001', scope: 'payment' });
|
|
303
|
+
assert.strictEqual(paymentDecisions.length, 0, 'payment scope query returns empty');
|
|
304
|
+
|
|
305
|
+
// Step 2: Since scope was provided but returned empty, cascade to milestone-only
|
|
306
|
+
const milestoneDecisions = queryDecisions({ milestoneId: 'M001' });
|
|
307
|
+
assert.strictEqual(milestoneDecisions.length, 2, 'milestone fallback returns 2 decisions');
|
|
308
|
+
|
|
309
|
+
// Step 3: Format and verify content
|
|
310
|
+
const formatted = formatDecisionsForPrompt(milestoneDecisions);
|
|
311
|
+
assert.match(formatted, /D001/, 'formatted output includes D001');
|
|
312
|
+
assert.match(formatted, /D002/, 'formatted output includes D002');
|
|
313
|
+
assert.match(formatted, /architecture/, 'formatted output includes architecture scope');
|
|
314
|
+
assert.match(formatted, /security/, 'formatted output includes security scope');
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
319
|
+
// Integration: scope derivation feeds into cascade
|
|
320
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
321
|
+
|
|
322
|
+
describe("integration: scope derivation with cascade", () => {
|
|
323
|
+
beforeEach(() => {
|
|
324
|
+
openDatabase(':memory:');
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
afterEach(() => {
|
|
328
|
+
closeDatabase();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("derived scope finds matching decisions when they exist", () => {
|
|
332
|
+
// Insert decisions with 'auth' scope
|
|
333
|
+
insertDecision({
|
|
334
|
+
id: 'D001', when_context: 'M001/S01', scope: 'auth',
|
|
335
|
+
decision: 'use JWT', choice: 'JWT tokens', rationale: 'stateless',
|
|
336
|
+
revisable: 'yes', made_by: 'agent', superseded_by: null,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// Derive scope from slice title
|
|
340
|
+
const derivedScope = deriveSliceScope("Auth Middleware & Protected Routes");
|
|
341
|
+
assert.strictEqual(derivedScope, 'auth', 'derives auth scope from title');
|
|
342
|
+
|
|
343
|
+
// Query with derived scope should find the decision
|
|
344
|
+
const decisions = queryDecisions({ milestoneId: 'M001', scope: derivedScope });
|
|
345
|
+
assert.strictEqual(decisions.length, 1, 'scoped query finds matching decision');
|
|
346
|
+
assert.strictEqual(decisions[0]?.id, 'D001', 'finds the auth decision');
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("generic title triggers milestone-level fallback", () => {
|
|
350
|
+
// Insert decisions with various scopes
|
|
351
|
+
insertDecision({
|
|
352
|
+
id: 'D001', when_context: 'M001/S01', scope: 'architecture',
|
|
353
|
+
decision: 'use monolith', choice: 'monolith', rationale: 'simple',
|
|
354
|
+
revisable: 'yes', made_by: 'agent', superseded_by: null,
|
|
355
|
+
});
|
|
356
|
+
insertDecision({
|
|
357
|
+
id: 'D002', when_context: 'M001/S02', scope: 'tooling',
|
|
358
|
+
decision: 'use TypeScript', choice: 'TypeScript', rationale: 'type safety',
|
|
359
|
+
revisable: 'yes', made_by: 'agent', superseded_by: null,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Derive scope from generic slice title
|
|
363
|
+
const derivedScope = deriveSliceScope("Integration Testing");
|
|
364
|
+
assert.strictEqual(derivedScope, undefined, 'generic title returns undefined scope');
|
|
365
|
+
|
|
366
|
+
// Without a scope, query returns all milestone decisions
|
|
367
|
+
const decisions = queryDecisions({ milestoneId: 'M001', scope: derivedScope });
|
|
368
|
+
assert.strictEqual(decisions.length, 2, 'no scope filter returns all decisions');
|
|
369
|
+
});
|
|
370
|
+
});
|
|
@@ -590,9 +590,9 @@ test("unit-end event contains errorContext when unit is cancelled with structure
|
|
|
590
590
|
resolveAgentEndCancelled({ message: "Hard timeout error: exceeded limit", category: "timeout", isTransient: true });
|
|
591
591
|
|
|
592
592
|
const result = await unitPromise;
|
|
593
|
-
//
|
|
593
|
+
// Transient timeout cancellations pause (recoverable) instead of hard-stopping
|
|
594
594
|
assert.equal(result.action, "break");
|
|
595
|
-
assert.equal((result as any).reason, "session-
|
|
595
|
+
assert.equal((result as any).reason, "session-timeout");
|
|
596
596
|
|
|
597
597
|
// Verify error classification used structured errorContext on the window entry
|
|
598
598
|
const entry = loopState.recentUnits[loopState.recentUnits.length - 1];
|