gsd-pi 2.70.1-dev.3591dcf → 2.70.1-dev.6504106
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +129 -30
- package/dist/resources/extensions/get-secrets-from-user.js +17 -1
- package/dist/resources/extensions/gsd/auto-start.js +3 -11
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +4 -0
- package/dist/resources/extensions/gsd/guided-flow.js +12 -10
- package/dist/resources/extensions/gsd/init-wizard.js +3 -11
- package/dist/resources/extensions/gsd/prompts/discuss.md +31 -13
- package/dist/resources/extensions/gsd/state.js +234 -332
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +34 -0
- package/dist/resources/extensions/gsd/workflow-mcp-auto-prep.js +56 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/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 +15 -15
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/2826.dd3dc8bbd3025fa5.js +9 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-6e4d7e9a4f57bed4.js → webpack-b868033a5834586d.js} +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/dist/env-writer.d.ts +39 -0
- package/packages/mcp-server/dist/env-writer.d.ts.map +1 -0
- package/packages/mcp-server/dist/env-writer.js +158 -0
- package/packages/mcp-server/dist/env-writer.js.map +1 -0
- package/packages/mcp-server/dist/server.d.ts +11 -2
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +102 -2
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/src/env-writer.test.ts +280 -0
- package/packages/mcp-server/src/env-writer.ts +183 -0
- package/packages/mcp-server/src/secure-env-collect.test.ts +265 -0
- package/packages/mcp-server/src/server.ts +137 -3
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +310 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts +19 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js +50 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +158 -23
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +6 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +58 -2
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
- package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +370 -0
- package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts +58 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts +2 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +189 -29
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +66 -2
- package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +1 -1
- package/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts +1 -0
- package/packages/pi-tui/dist/components/__tests__/input.test.js +9 -0
- package/packages/pi-tui/dist/components/__tests__/input.test.js.map +1 -1
- package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.d.ts +2 -0
- package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.d.ts.map +1 -0
- package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.js +66 -0
- package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.js.map +1 -0
- package/packages/pi-tui/dist/components/input.d.ts +2 -0
- package/packages/pi-tui/dist/components/input.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/input.js +7 -4
- package/packages/pi-tui/dist/components/input.js.map +1 -1
- package/packages/pi-tui/dist/components/markdown.d.ts +3 -0
- package/packages/pi-tui/dist/components/markdown.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/markdown.js +17 -1
- package/packages/pi-tui/dist/components/markdown.js.map +1 -1
- package/packages/pi-tui/src/components/__tests__/input.test.ts +11 -0
- package/packages/pi-tui/src/components/__tests__/markdown-maxlines.test.ts +75 -0
- package/packages/pi-tui/src/components/input.ts +7 -4
- package/packages/pi-tui/src/components/markdown.ts +22 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +166 -31
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +145 -0
- package/src/resources/extensions/get-secrets-from-user.ts +24 -1
- package/src/resources/extensions/gsd/auto-start.ts +3 -13
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +4 -0
- package/src/resources/extensions/gsd/guided-flow.ts +12 -9
- package/src/resources/extensions/gsd/init-wizard.ts +3 -13
- package/src/resources/extensions/gsd/prompts/discuss.md +31 -13
- package/src/resources/extensions/gsd/state.ts +274 -344
- package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +436 -0
- package/src/resources/extensions/gsd/tests/discuss-incremental-persistence.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/secure-env-collect.test.ts +45 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +76 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +155 -1
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +22 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +60 -25
- package/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts +76 -0
- package/src/resources/extensions/gsd/workflow-mcp.ts +1 -1
- package/dist/web/standalone/.next/static/chunks/2826.821e01b07d92e948.js +0 -9
- /package/dist/web/standalone/.next/static/{KdlODhIktLmeRKpLpHdKb → T3hWbQ-WQBDEIGoaOOfEo}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{KdlODhIktLmeRKpLpHdKb → T3hWbQ-WQBDEIGoaOOfEo}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
// GSD Extension — Tests for extracted deriveStateFromDb helper functions
|
|
2
|
+
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
3
|
+
//
|
|
4
|
+
// Tests the composable helpers extracted from deriveStateFromDb:
|
|
5
|
+
// reconcileDiskToDb, buildCompletenessSet, buildRegistryAndFindActive,
|
|
6
|
+
// handleNoActiveMilestone, resolveSliceDependencies, reconcileSliceTasks,
|
|
7
|
+
// detectBlockers, checkReplanTrigger, checkInterruptedWork
|
|
8
|
+
//
|
|
9
|
+
// Helpers are private — exercised through deriveStateFromDb integration.
|
|
10
|
+
|
|
11
|
+
import { describe, test, beforeEach, afterEach } from 'node:test';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { tmpdir } from 'node:os';
|
|
16
|
+
|
|
17
|
+
import { invalidateStateCache, deriveStateFromDb } from '../state.ts';
|
|
18
|
+
import {
|
|
19
|
+
openDatabase,
|
|
20
|
+
closeDatabase,
|
|
21
|
+
insertMilestone,
|
|
22
|
+
insertSlice,
|
|
23
|
+
insertTask,
|
|
24
|
+
updateTaskStatus,
|
|
25
|
+
} from '../gsd-db.ts';
|
|
26
|
+
|
|
27
|
+
// ─── Fixture Helpers ───────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
function createFixtureBase(): string {
|
|
30
|
+
const base = mkdtempSync(join(tmpdir(), 'gsd-helpers-'));
|
|
31
|
+
mkdirSync(join(base, '.gsd', 'milestones'), { recursive: true });
|
|
32
|
+
return base;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function writeFile(base: string, relativePath: string, content: string): void {
|
|
36
|
+
const full = join(base, '.gsd', relativePath);
|
|
37
|
+
mkdirSync(join(full, '..'), { recursive: true });
|
|
38
|
+
writeFileSync(full, content);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function cleanup(base: string): void {
|
|
42
|
+
rmSync(base, { recursive: true, force: true });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ROADMAP_CONTENT = `# M001: Test Milestone
|
|
46
|
+
|
|
47
|
+
**Vision:** Test helpers.
|
|
48
|
+
|
|
49
|
+
## Slices
|
|
50
|
+
|
|
51
|
+
- [ ] **S01: First Slice** \`risk:low\` \`depends:[]\`
|
|
52
|
+
> After this: Slice done.
|
|
53
|
+
|
|
54
|
+
- [ ] **S02: Second Slice** \`risk:low\` \`depends:[S01]\`
|
|
55
|
+
> After this: All done.
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
const PLAN_CONTENT = `# S01: First Slice
|
|
59
|
+
|
|
60
|
+
**Goal:** Test executing.
|
|
61
|
+
**Demo:** Tests pass.
|
|
62
|
+
|
|
63
|
+
## Tasks
|
|
64
|
+
|
|
65
|
+
- [ ] **T01: First Task** \`est:10m\`
|
|
66
|
+
First task description.
|
|
67
|
+
|
|
68
|
+
- [x] **T02: Done Task** \`est:10m\`
|
|
69
|
+
Already done.
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
73
|
+
// Tests
|
|
74
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
75
|
+
|
|
76
|
+
describe('derive-state-helpers', () => {
|
|
77
|
+
|
|
78
|
+
// ─── handleNoActiveMilestone: all parked ─────────────────────────────
|
|
79
|
+
test('handleNoActiveMilestone: all milestones parked returns pre-planning with unpark hint', async () => {
|
|
80
|
+
const base = createFixtureBase();
|
|
81
|
+
try {
|
|
82
|
+
writeFile(base, 'milestones/M001/M001-CONTEXT.md', '# M001\n\nContext.');
|
|
83
|
+
writeFile(base, 'milestones/M001/M001-PARKED.md', 'Parked.');
|
|
84
|
+
writeFile(base, 'milestones/M002/M002-CONTEXT.md', '# M002\n\nContext.');
|
|
85
|
+
writeFile(base, 'milestones/M002/M002-PARKED.md', 'Also parked.');
|
|
86
|
+
|
|
87
|
+
openDatabase(':memory:');
|
|
88
|
+
insertMilestone({ id: 'M001', title: 'First', status: 'parked' });
|
|
89
|
+
insertMilestone({ id: 'M002', title: 'Second', status: 'parked' });
|
|
90
|
+
|
|
91
|
+
invalidateStateCache();
|
|
92
|
+
const state = await deriveStateFromDb(base);
|
|
93
|
+
|
|
94
|
+
assert.equal(state.phase, 'pre-planning', 'all-parked: phase is pre-planning');
|
|
95
|
+
assert.equal(state.activeMilestone, null, 'all-parked: no active milestone');
|
|
96
|
+
assert.ok(state.nextAction.includes('parked'), 'all-parked: nextAction mentions parked');
|
|
97
|
+
assert.ok(state.nextAction.includes('unpark'), 'all-parked: nextAction hints unpark');
|
|
98
|
+
assert.equal(state.registry.length, 2, 'all-parked: both in registry');
|
|
99
|
+
assert.ok(state.registry.every(e => e.status === 'parked'), 'all-parked: all registry entries parked');
|
|
100
|
+
} finally {
|
|
101
|
+
closeDatabase();
|
|
102
|
+
cleanup(base);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ─── handleNoActiveMilestone: all complete with active requirements ──
|
|
107
|
+
test('handleNoActiveMilestone: all complete with unmapped requirements', async () => {
|
|
108
|
+
const base = createFixtureBase();
|
|
109
|
+
try {
|
|
110
|
+
writeFile(base, 'milestones/M001/M001-SUMMARY.md', '# M001 Summary\n\nDone.');
|
|
111
|
+
writeFile(base, 'REQUIREMENTS.md', `# Requirements\n\n## Active\n\n### R001 — Unmapped\n- Status: active\n- Description: Not mapped.\n`);
|
|
112
|
+
|
|
113
|
+
openDatabase(':memory:');
|
|
114
|
+
insertMilestone({ id: 'M001', title: 'First', status: 'complete' });
|
|
115
|
+
|
|
116
|
+
invalidateStateCache();
|
|
117
|
+
const state = await deriveStateFromDb(base);
|
|
118
|
+
|
|
119
|
+
assert.equal(state.phase, 'complete', 'complete-reqs: phase is complete');
|
|
120
|
+
assert.ok(state.nextAction.includes('1 active requirement'), 'complete-reqs: nextAction notes unmapped reqs');
|
|
121
|
+
assert.equal(state.requirements?.active, 1, 'complete-reqs: requirements.active = 1');
|
|
122
|
+
} finally {
|
|
123
|
+
closeDatabase();
|
|
124
|
+
cleanup(base);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ─── resolveSliceDependencies: GSD_SLICE_LOCK with missing slice ────
|
|
129
|
+
test('resolveSliceDependencies: GSD_SLICE_LOCK pointing to non-existent slice returns blocked', async () => {
|
|
130
|
+
const base = createFixtureBase();
|
|
131
|
+
const origLock = process.env.GSD_SLICE_LOCK;
|
|
132
|
+
try {
|
|
133
|
+
writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
|
|
134
|
+
writeFile(base, 'milestones/M001/slices/S01/S01-PLAN.md', PLAN_CONTENT);
|
|
135
|
+
writeFile(base, 'milestones/M001/slices/S01/tasks/.gitkeep', '');
|
|
136
|
+
writeFile(base, 'milestones/M001/slices/S01/tasks/T01-PLAN.md', '# T01 Plan');
|
|
137
|
+
|
|
138
|
+
openDatabase(':memory:');
|
|
139
|
+
insertMilestone({ id: 'M001', title: 'Test', status: 'active' });
|
|
140
|
+
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'First', status: 'active', risk: 'low', depends: [] });
|
|
141
|
+
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'First Task', status: 'pending' });
|
|
142
|
+
|
|
143
|
+
process.env.GSD_SLICE_LOCK = 'S99';
|
|
144
|
+
|
|
145
|
+
invalidateStateCache();
|
|
146
|
+
const state = await deriveStateFromDb(base);
|
|
147
|
+
|
|
148
|
+
assert.equal(state.phase, 'blocked', 'slice-lock-miss: phase is blocked');
|
|
149
|
+
assert.ok(state.blockers.some(b => b.includes('GSD_SLICE_LOCK=S99')), 'slice-lock-miss: blocker mentions lock');
|
|
150
|
+
} finally {
|
|
151
|
+
if (origLock !== undefined) process.env.GSD_SLICE_LOCK = origLock;
|
|
152
|
+
else delete process.env.GSD_SLICE_LOCK;
|
|
153
|
+
closeDatabase();
|
|
154
|
+
cleanup(base);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// ─── resolveSliceDependencies: GSD_SLICE_LOCK with valid slice ──────
|
|
159
|
+
test('resolveSliceDependencies: GSD_SLICE_LOCK targeting valid slice bypasses deps', async () => {
|
|
160
|
+
const base = createFixtureBase();
|
|
161
|
+
const origLock = process.env.GSD_SLICE_LOCK;
|
|
162
|
+
try {
|
|
163
|
+
writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
|
|
164
|
+
// S02 depends on S01 but we lock to S02 directly
|
|
165
|
+
writeFile(base, 'milestones/M001/slices/S02/S02-PLAN.md', `# S02\n\n**Goal:** Test.\n**Demo:** Pass.\n\n## Tasks\n\n- [ ] **T01: Task** \`est:5m\`\n Do thing.\n`);
|
|
166
|
+
writeFile(base, 'milestones/M001/slices/S02/tasks/.gitkeep', '');
|
|
167
|
+
writeFile(base, 'milestones/M001/slices/S02/tasks/T01-PLAN.md', '# T01 Plan');
|
|
168
|
+
|
|
169
|
+
openDatabase(':memory:');
|
|
170
|
+
insertMilestone({ id: 'M001', title: 'Test', status: 'active' });
|
|
171
|
+
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'First', status: 'pending', risk: 'low', depends: [] });
|
|
172
|
+
insertSlice({ id: 'S02', milestoneId: 'M001', title: 'Second', status: 'pending', risk: 'low', depends: ['S01'] });
|
|
173
|
+
insertTask({ id: 'T01', sliceId: 'S02', milestoneId: 'M001', title: 'Task', status: 'pending' });
|
|
174
|
+
|
|
175
|
+
process.env.GSD_SLICE_LOCK = 'S02';
|
|
176
|
+
|
|
177
|
+
invalidateStateCache();
|
|
178
|
+
const state = await deriveStateFromDb(base);
|
|
179
|
+
|
|
180
|
+
assert.equal(state.activeSlice?.id, 'S02', 'slice-lock-valid: activeSlice is S02 (locked)');
|
|
181
|
+
assert.equal(state.phase, 'executing', 'slice-lock-valid: phase is executing');
|
|
182
|
+
} finally {
|
|
183
|
+
if (origLock !== undefined) process.env.GSD_SLICE_LOCK = origLock;
|
|
184
|
+
else delete process.env.GSD_SLICE_LOCK;
|
|
185
|
+
closeDatabase();
|
|
186
|
+
cleanup(base);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ─── reconcileSliceTasks: plan file imports tasks when DB empty ──────
|
|
191
|
+
test('reconcileSliceTasks: imports tasks from plan file when DB has zero tasks (#3600)', async () => {
|
|
192
|
+
const base = createFixtureBase();
|
|
193
|
+
try {
|
|
194
|
+
writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
|
|
195
|
+
writeFile(base, 'milestones/M001/slices/S01/S01-PLAN.md', PLAN_CONTENT);
|
|
196
|
+
writeFile(base, 'milestones/M001/slices/S01/tasks/.gitkeep', '');
|
|
197
|
+
writeFile(base, 'milestones/M001/slices/S01/tasks/T01-PLAN.md', '# T01 Plan');
|
|
198
|
+
|
|
199
|
+
openDatabase(':memory:');
|
|
200
|
+
insertMilestone({ id: 'M001', title: 'Test', status: 'active' });
|
|
201
|
+
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'First', status: 'active', risk: 'low', depends: [] });
|
|
202
|
+
insertSlice({ id: 'S02', milestoneId: 'M001', title: 'Second', status: 'pending', risk: 'low', depends: ['S01'] });
|
|
203
|
+
// No tasks inserted — reconcileSliceTasks should import from plan file
|
|
204
|
+
|
|
205
|
+
invalidateStateCache();
|
|
206
|
+
const state = await deriveStateFromDb(base);
|
|
207
|
+
|
|
208
|
+
// Plan has T01 (pending) and T02 (done) — reconciliation imports both
|
|
209
|
+
assert.equal(state.phase, 'executing', 'task-reconcile: phase is executing (tasks imported)');
|
|
210
|
+
assert.equal(state.activeTask?.id, 'T01', 'task-reconcile: activeTask is T01');
|
|
211
|
+
assert.equal(state.progress?.tasks?.total, 2, 'task-reconcile: total tasks = 2');
|
|
212
|
+
assert.equal(state.progress?.tasks?.done, 1, 'task-reconcile: done tasks = 1 (T02 was [x])');
|
|
213
|
+
} finally {
|
|
214
|
+
closeDatabase();
|
|
215
|
+
cleanup(base);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// ─── reconcileSliceTasks: stale task reconciled from disk summary ────
|
|
220
|
+
test('reconcileSliceTasks: stale pending task reconciled to complete when disk SUMMARY exists (#2514)', async () => {
|
|
221
|
+
const base = createFixtureBase();
|
|
222
|
+
try {
|
|
223
|
+
writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
|
|
224
|
+
writeFile(base, 'milestones/M001/slices/S01/S01-PLAN.md', PLAN_CONTENT);
|
|
225
|
+
writeFile(base, 'milestones/M001/slices/S01/tasks/.gitkeep', '');
|
|
226
|
+
writeFile(base, 'milestones/M001/slices/S01/tasks/T01-PLAN.md', '# T01 Plan');
|
|
227
|
+
// T01 has a summary on disk but DB still says pending
|
|
228
|
+
writeFile(base, 'milestones/M001/slices/S01/tasks/T01-SUMMARY.md', '# T01 Summary\n\nDone on disk.');
|
|
229
|
+
|
|
230
|
+
openDatabase(':memory:');
|
|
231
|
+
insertMilestone({ id: 'M001', title: 'Test', status: 'active' });
|
|
232
|
+
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'First', status: 'active', risk: 'low', depends: [] });
|
|
233
|
+
insertSlice({ id: 'S02', milestoneId: 'M001', title: 'Second', status: 'pending', risk: 'low', depends: ['S01'] });
|
|
234
|
+
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'First Task', status: 'pending' });
|
|
235
|
+
insertTask({ id: 'T02', sliceId: 'S01', milestoneId: 'M001', title: 'Done Task', status: 'complete' });
|
|
236
|
+
|
|
237
|
+
invalidateStateCache();
|
|
238
|
+
const state = await deriveStateFromDb(base);
|
|
239
|
+
|
|
240
|
+
// T01 should have been reconciled to complete (SUMMARY exists on disk)
|
|
241
|
+
// Both tasks complete → phase should be summarizing
|
|
242
|
+
assert.equal(state.phase, 'summarizing', 'stale-task: phase is summarizing (T01 reconciled)');
|
|
243
|
+
assert.equal(state.activeTask, null, 'stale-task: no active task (all done)');
|
|
244
|
+
assert.equal(state.progress?.tasks?.done, 2, 'stale-task: tasks.done = 2');
|
|
245
|
+
} finally {
|
|
246
|
+
closeDatabase();
|
|
247
|
+
cleanup(base);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// ─── detectBlockers: blocker_discovered triggers replanning ──────────
|
|
252
|
+
test('detectBlockers: task with blocker_discovered triggers replanning-slice', async () => {
|
|
253
|
+
const base = createFixtureBase();
|
|
254
|
+
try {
|
|
255
|
+
writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
|
|
256
|
+
writeFile(base, 'milestones/M001/slices/S01/S01-PLAN.md', PLAN_CONTENT);
|
|
257
|
+
writeFile(base, 'milestones/M001/slices/S01/tasks/.gitkeep', '');
|
|
258
|
+
writeFile(base, 'milestones/M001/slices/S01/tasks/T01-PLAN.md', '# T01 Plan');
|
|
259
|
+
// T02 completed with blocker discovered — written in summary frontmatter
|
|
260
|
+
writeFile(base, 'milestones/M001/slices/S01/tasks/T02-SUMMARY.md',
|
|
261
|
+
'---\nblocker_discovered: true\n---\n\n# T02 Summary\n\nFound a blocker.');
|
|
262
|
+
|
|
263
|
+
openDatabase(':memory:');
|
|
264
|
+
insertMilestone({ id: 'M001', title: 'Test', status: 'active' });
|
|
265
|
+
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'First', status: 'active', risk: 'low', depends: [] });
|
|
266
|
+
insertSlice({ id: 'S02', milestoneId: 'M001', title: 'Second', status: 'pending', risk: 'low', depends: ['S01'] });
|
|
267
|
+
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'First Task', status: 'pending' });
|
|
268
|
+
insertTask({ id: 'T02', sliceId: 'S01', milestoneId: 'M001', title: 'Done Task', status: 'complete' });
|
|
269
|
+
|
|
270
|
+
invalidateStateCache();
|
|
271
|
+
const state = await deriveStateFromDb(base);
|
|
272
|
+
|
|
273
|
+
assert.equal(state.phase, 'replanning-slice', 'blocker: phase is replanning-slice');
|
|
274
|
+
assert.ok(state.blockers.some(b => b.includes('T02')), 'blocker: blockers mention T02');
|
|
275
|
+
} finally {
|
|
276
|
+
closeDatabase();
|
|
277
|
+
cleanup(base);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// ─── checkInterruptedWork: continue.md triggers resume hint ─────────
|
|
282
|
+
test('checkInterruptedWork: continue.md present triggers resume nextAction', async () => {
|
|
283
|
+
const base = createFixtureBase();
|
|
284
|
+
try {
|
|
285
|
+
writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
|
|
286
|
+
writeFile(base, 'milestones/M001/slices/S01/S01-PLAN.md', PLAN_CONTENT);
|
|
287
|
+
writeFile(base, 'milestones/M001/slices/S01/tasks/.gitkeep', '');
|
|
288
|
+
writeFile(base, 'milestones/M001/slices/S01/tasks/T01-PLAN.md', '# T01 Plan');
|
|
289
|
+
writeFile(base, 'milestones/M001/slices/S01/S01-CONTINUE.md', 'Resume from here.');
|
|
290
|
+
|
|
291
|
+
openDatabase(':memory:');
|
|
292
|
+
insertMilestone({ id: 'M001', title: 'Test', status: 'active' });
|
|
293
|
+
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'First', status: 'active', risk: 'low', depends: [] });
|
|
294
|
+
insertSlice({ id: 'S02', milestoneId: 'M001', title: 'Second', status: 'pending', risk: 'low', depends: ['S01'] });
|
|
295
|
+
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'First Task', status: 'pending' });
|
|
296
|
+
insertTask({ id: 'T02', sliceId: 'S01', milestoneId: 'M001', title: 'Done Task', status: 'complete' });
|
|
297
|
+
|
|
298
|
+
invalidateStateCache();
|
|
299
|
+
const state = await deriveStateFromDb(base);
|
|
300
|
+
|
|
301
|
+
assert.equal(state.phase, 'executing', 'continue: phase is still executing');
|
|
302
|
+
assert.ok(state.nextAction.includes('Resume interrupted work'), 'continue: nextAction mentions resume');
|
|
303
|
+
assert.ok(state.nextAction.includes('continue.md'), 'continue: nextAction mentions continue.md');
|
|
304
|
+
} finally {
|
|
305
|
+
closeDatabase();
|
|
306
|
+
cleanup(base);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// ─── buildCompletenessSet: SUMMARY-on-disk marks complete ───────────
|
|
311
|
+
test('buildCompletenessSet: milestone with SUMMARY on disk treated as complete', async () => {
|
|
312
|
+
const base = createFixtureBase();
|
|
313
|
+
try {
|
|
314
|
+
// M001 has summary on disk but DB status is still 'active'
|
|
315
|
+
writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
|
|
316
|
+
writeFile(base, 'milestones/M001/M001-SUMMARY.md', '# M001 Summary\n\nDone.');
|
|
317
|
+
// M002 is the real active milestone
|
|
318
|
+
writeFile(base, 'milestones/M002/M002-CONTEXT.md', '# M002\n\nActive.');
|
|
319
|
+
|
|
320
|
+
openDatabase(':memory:');
|
|
321
|
+
insertMilestone({ id: 'M001', title: 'First', status: 'active' });
|
|
322
|
+
insertMilestone({ id: 'M002', title: 'Second', status: 'active' });
|
|
323
|
+
|
|
324
|
+
invalidateStateCache();
|
|
325
|
+
const state = await deriveStateFromDb(base);
|
|
326
|
+
|
|
327
|
+
// M001 should be complete (summary on disk), M002 should be active
|
|
328
|
+
const m1 = state.registry.find(e => e.id === 'M001');
|
|
329
|
+
assert.equal(m1?.status, 'complete', 'summary-disk: M001 marked complete via disk SUMMARY');
|
|
330
|
+
assert.equal(state.activeMilestone?.id, 'M002', 'summary-disk: M002 is active');
|
|
331
|
+
} finally {
|
|
332
|
+
closeDatabase();
|
|
333
|
+
cleanup(base);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// ─── reconcileDiskToDb: disk slices synced into DB (#2533) ──────────
|
|
338
|
+
test('reconcileDiskToDb: slices in ROADMAP.md but missing from DB are auto-inserted (#2533)', async () => {
|
|
339
|
+
const base = createFixtureBase();
|
|
340
|
+
try {
|
|
341
|
+
writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
|
|
342
|
+
|
|
343
|
+
openDatabase(':memory:');
|
|
344
|
+
insertMilestone({ id: 'M001', title: 'Test', status: 'active' });
|
|
345
|
+
// No slices inserted — reconcileDiskToDb should insert from roadmap
|
|
346
|
+
|
|
347
|
+
invalidateStateCache();
|
|
348
|
+
const state = await deriveStateFromDb(base);
|
|
349
|
+
|
|
350
|
+
// Slices should have been reconciled from roadmap, S01 should be the active slice
|
|
351
|
+
assert.equal(state.activeMilestone?.id, 'M001', 'slice-reconcile: M001 is active');
|
|
352
|
+
assert.equal(state.activeSlice?.id, 'S01', 'slice-reconcile: S01 reconciled and active');
|
|
353
|
+
assert.ok((state.progress?.slices?.total ?? 0) >= 2, 'slice-reconcile: at least 2 slices reconciled');
|
|
354
|
+
} finally {
|
|
355
|
+
closeDatabase();
|
|
356
|
+
cleanup(base);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// ─── Queue order: milestones sorted by custom queue order ───────────
|
|
361
|
+
test('deriveStateFromDb respects custom queue order from QUEUE-ORDER.json', async () => {
|
|
362
|
+
const base = createFixtureBase();
|
|
363
|
+
try {
|
|
364
|
+
// M003 should come first per queue order, M001 second
|
|
365
|
+
const queueOrder = JSON.stringify({ order: ['M003', 'M001', 'M002'], updatedAt: new Date().toISOString() });
|
|
366
|
+
writeFileSync(join(base, '.gsd', 'QUEUE-ORDER.json'), queueOrder);
|
|
367
|
+
writeFile(base, 'milestones/M001/M001-CONTEXT.md', '# M001\n\nContext.');
|
|
368
|
+
writeFile(base, 'milestones/M002/M002-CONTEXT.md', '# M002\n\nContext.');
|
|
369
|
+
writeFile(base, 'milestones/M003/M003-CONTEXT.md', '# M003\n\nContext.');
|
|
370
|
+
|
|
371
|
+
openDatabase(':memory:');
|
|
372
|
+
// Insert in natural order — queue ordering should override
|
|
373
|
+
insertMilestone({ id: 'M001', title: 'First', status: 'active' });
|
|
374
|
+
insertMilestone({ id: 'M002', title: 'Second', status: 'active' });
|
|
375
|
+
insertMilestone({ id: 'M003', title: 'Third', status: 'active' });
|
|
376
|
+
|
|
377
|
+
invalidateStateCache();
|
|
378
|
+
const state = await deriveStateFromDb(base);
|
|
379
|
+
|
|
380
|
+
// M003 should be the active milestone (first in queue)
|
|
381
|
+
assert.equal(state.activeMilestone?.id, 'M003', 'queue-order: M003 is active (first in queue)');
|
|
382
|
+
assert.equal(state.registry[0]?.id, 'M003', 'queue-order: registry[0] is M003');
|
|
383
|
+
} finally {
|
|
384
|
+
closeDatabase();
|
|
385
|
+
cleanup(base);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// ─── handleAllSlicesDone: needs-remediation re-triggers validation ──
|
|
390
|
+
test('handleAllSlicesDone: needs-remediation verdict triggers validating-milestone', async () => {
|
|
391
|
+
const base = createFixtureBase();
|
|
392
|
+
try {
|
|
393
|
+
const doneRoadmap = `# M001: Remediation Test\n\n**Vision:** Test.\n\n## Slices\n\n- [x] **S01: Done** \`risk:low\` \`depends:[]\`\n > Done.\n`;
|
|
394
|
+
writeFile(base, 'milestones/M001/M001-ROADMAP.md', doneRoadmap);
|
|
395
|
+
writeFile(base, 'milestones/M001/M001-VALIDATION.md',
|
|
396
|
+
'---\nverdict: needs-remediation\nremediation_round: 1\n---\n\n# Validation\nNeeds remediation.');
|
|
397
|
+
|
|
398
|
+
openDatabase(':memory:');
|
|
399
|
+
insertMilestone({ id: 'M001', title: 'Remediation Test', status: 'active' });
|
|
400
|
+
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Done', status: 'complete', risk: 'low', depends: [] });
|
|
401
|
+
|
|
402
|
+
invalidateStateCache();
|
|
403
|
+
const state = await deriveStateFromDb(base);
|
|
404
|
+
|
|
405
|
+
assert.equal(state.phase, 'validating-milestone', 'remediation: phase is validating-milestone');
|
|
406
|
+
assert.equal(state.activeMilestone?.id, 'M001', 'remediation: activeMilestone is M001');
|
|
407
|
+
} finally {
|
|
408
|
+
closeDatabase();
|
|
409
|
+
cleanup(base);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// ─── Deferred queued shell: shell milestone deferred, real one promoted ──
|
|
414
|
+
test('buildRegistryAndFindActive: queued shell deferred, later real milestone becomes active (#3470)', async () => {
|
|
415
|
+
const base = createFixtureBase();
|
|
416
|
+
try {
|
|
417
|
+
// M001: queued shell — no content, no slices
|
|
418
|
+
mkdirSync(join(base, '.gsd', 'milestones', 'M001'), { recursive: true });
|
|
419
|
+
// M002: real milestone with context
|
|
420
|
+
writeFile(base, 'milestones/M002/M002-CONTEXT.md', '# M002: Real\n\nActive milestone.');
|
|
421
|
+
|
|
422
|
+
openDatabase(':memory:');
|
|
423
|
+
insertMilestone({ id: 'M001', title: 'Shell', status: 'queued' });
|
|
424
|
+
insertMilestone({ id: 'M002', title: 'Real', status: 'active' });
|
|
425
|
+
|
|
426
|
+
invalidateStateCache();
|
|
427
|
+
const state = await deriveStateFromDb(base);
|
|
428
|
+
|
|
429
|
+
// M002 should be active (M001 queued shell deferred)
|
|
430
|
+
assert.equal(state.activeMilestone?.id, 'M002', 'deferred-shell: M002 is active (shell deferred)');
|
|
431
|
+
} finally {
|
|
432
|
+
closeDatabase();
|
|
433
|
+
cleanup(base);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
});
|
|
@@ -27,10 +27,19 @@ describe("discuss incremental persistence (#2152)", () => {
|
|
|
27
27
|
assert.match(content, /Incremental persistence/, "should have incremental persistence section");
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
test("new-project discuss prompt includes CONTEXT-DRAFT save instruction", () => {
|
|
31
|
+
const content = readFileSync(join(promptsDir, "discuss.md"), "utf-8");
|
|
32
|
+
assert.match(content, /CONTEXT-DRAFT/, "should mention CONTEXT-DRAFT");
|
|
33
|
+
assert.match(content, /Incremental persistence/, "should have incremental persistence section");
|
|
34
|
+
assert.match(content, /gsd_summary_save/, "should use gsd_summary_save tool");
|
|
35
|
+
});
|
|
36
|
+
|
|
30
37
|
test("drafts are saved silently without user notification", () => {
|
|
31
38
|
const milestone = readFileSync(join(promptsDir, "guided-discuss-milestone.md"), "utf-8");
|
|
32
39
|
const slice = readFileSync(join(promptsDir, "guided-discuss-slice.md"), "utf-8");
|
|
40
|
+
const discuss = readFileSync(join(promptsDir, "discuss.md"), "utf-8");
|
|
33
41
|
assert.match(milestone, /Do NOT mention this save to the user/);
|
|
34
42
|
assert.match(slice, /Do NOT mention this to the user/);
|
|
43
|
+
assert.match(discuss, /Do NOT mention this save to the user/);
|
|
35
44
|
});
|
|
36
45
|
});
|
|
@@ -317,3 +317,48 @@ test("secure_env_collect #2997: null from ctx.ui.custom() is still treated as sk
|
|
|
317
317
|
"Key returning null must NOT be in applied list",
|
|
318
318
|
);
|
|
319
319
|
});
|
|
320
|
+
|
|
321
|
+
test("secure_env_collect: falls back to secure input prompt when custom UI is unavailable", async (t) => {
|
|
322
|
+
const { collectSecretsFromManifest } = await loadOrchestrator();
|
|
323
|
+
|
|
324
|
+
const tmp = makeTempDir("sec-input-fallback-test");
|
|
325
|
+
t.after(() => {
|
|
326
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const manifest = makeManifest([
|
|
330
|
+
{ key: "SECRET_FROM_INPUT_FALLBACK", status: "pending", formatHint: "starts with sk-" },
|
|
331
|
+
]);
|
|
332
|
+
await writeManifestFile(tmp, manifest);
|
|
333
|
+
|
|
334
|
+
let callIndex = 0;
|
|
335
|
+
const inputCalls: Array<{ title: string; placeholder?: string; opts?: { secure?: boolean } }> = [];
|
|
336
|
+
const mockCtx = {
|
|
337
|
+
cwd: tmp,
|
|
338
|
+
hasUI: true,
|
|
339
|
+
ui: {
|
|
340
|
+
custom: async (_factory: any) => {
|
|
341
|
+
callIndex++;
|
|
342
|
+
if (callIndex <= 1) return null; // summary screen dismiss
|
|
343
|
+
return undefined; // collect screen unavailable on this surface
|
|
344
|
+
},
|
|
345
|
+
input: async (title: string, placeholder?: string, opts?: { secure?: boolean }) => {
|
|
346
|
+
inputCalls.push({ title, placeholder, opts });
|
|
347
|
+
return " sk-test-fallback-value ";
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const result = await collectSecretsFromManifest(tmp, "M001", mockCtx as any);
|
|
353
|
+
|
|
354
|
+
assert.ok(
|
|
355
|
+
result.applied.includes("SECRET_FROM_INPUT_FALLBACK"),
|
|
356
|
+
"Fallback input should collect and apply the key",
|
|
357
|
+
);
|
|
358
|
+
assert.ok(
|
|
359
|
+
!result.skipped.includes("SECRET_FROM_INPUT_FALLBACK"),
|
|
360
|
+
"Fallback input should not mark the key as skipped",
|
|
361
|
+
);
|
|
362
|
+
assert.equal(inputCalls.length, 1, "Fallback input should be requested once");
|
|
363
|
+
assert.equal(inputCalls[0]?.opts?.secure, true, "Fallback input should request secure entry when supported");
|
|
364
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { prepareWorkflowMcpForProject, shouldAutoPrepareWorkflowMcp } from "../workflow-mcp-auto-prep.ts";
|
|
5
|
+
|
|
6
|
+
test("shouldAutoPrepareWorkflowMcp enables prep for externalCli local transport", () => {
|
|
7
|
+
const result = shouldAutoPrepareWorkflowMcp({
|
|
8
|
+
model: { provider: "claude-code", baseUrl: "local://claude-code" },
|
|
9
|
+
modelRegistry: {
|
|
10
|
+
getProviderAuthMode: () => "externalCli",
|
|
11
|
+
isProviderRequestReady: () => false,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
assert.equal(result, true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("shouldAutoPrepareWorkflowMcp enables prep when claude-code provider is ready", () => {
|
|
19
|
+
const result = shouldAutoPrepareWorkflowMcp({
|
|
20
|
+
model: { provider: "openai", baseUrl: "https://api.openai.com" },
|
|
21
|
+
modelRegistry: {
|
|
22
|
+
getProviderAuthMode: () => "apiKey",
|
|
23
|
+
isProviderRequestReady: (provider: string) => provider === "claude-code",
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
assert.equal(result, true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("shouldAutoPrepareWorkflowMcp enables prep when claude-code provider is registered", () => {
|
|
31
|
+
const result = shouldAutoPrepareWorkflowMcp({
|
|
32
|
+
model: { provider: "openai", baseUrl: "https://api.openai.com" },
|
|
33
|
+
modelRegistry: {
|
|
34
|
+
getProviderAuthMode: (provider: string) => provider === "claude-code" ? "externalCli" : "apiKey",
|
|
35
|
+
isProviderRequestReady: () => false,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
assert.equal(result, true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("shouldAutoPrepareWorkflowMcp stays disabled when neither transport nor provider readiness match", () => {
|
|
43
|
+
const result = shouldAutoPrepareWorkflowMcp({
|
|
44
|
+
model: { provider: "openai", baseUrl: "https://api.openai.com" },
|
|
45
|
+
modelRegistry: {
|
|
46
|
+
getProviderAuthMode: () => "apiKey",
|
|
47
|
+
isProviderRequestReady: () => false,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
assert.equal(result, false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("prepareWorkflowMcpForProject warns with /gsd mcp init guidance when prep fails", () => {
|
|
55
|
+
const notifications: Array<{ message: string; level: "info" | "warning" | "error" | "success" }> = [];
|
|
56
|
+
const result = prepareWorkflowMcpForProject(
|
|
57
|
+
{
|
|
58
|
+
model: { provider: "claude-code", baseUrl: "local://claude-code" },
|
|
59
|
+
modelRegistry: {
|
|
60
|
+
getProviderAuthMode: () => "externalCli",
|
|
61
|
+
isProviderRequestReady: () => true,
|
|
62
|
+
},
|
|
63
|
+
ui: {
|
|
64
|
+
notify: (message: string, level?: "info" | "warning" | "error" | "success") => {
|
|
65
|
+
notifications.push({ message, level: level ?? "info" });
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
"/",
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
assert.equal(result, null);
|
|
73
|
+
assert.equal(notifications.length, 1);
|
|
74
|
+
assert.equal(notifications[0].level, "warning");
|
|
75
|
+
assert.match(notifications[0].message, /Please run \/gsd mcp init \./);
|
|
76
|
+
});
|