gsd-pi 2.31.2 → 2.32.0-dev.1e39869
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/cli.js +5 -5
- package/dist/resources/extensions/gsd/auto-constants.ts +6 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +20 -26
- package/dist/resources/extensions/gsd/auto-direct-dispatch.ts +1 -6
- package/dist/resources/extensions/gsd/auto-dispatch.ts +4 -8
- package/dist/resources/extensions/gsd/auto-post-unit.ts +27 -32
- package/dist/resources/extensions/gsd/auto-prompts.ts +38 -34
- package/dist/resources/extensions/gsd/auto-start.ts +8 -6
- package/dist/resources/extensions/gsd/auto.ts +54 -33
- package/dist/resources/extensions/gsd/commands-workflow-templates.ts +3 -5
- package/dist/resources/extensions/gsd/commands.ts +19 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +28 -0
- package/dist/resources/extensions/gsd/doctor-environment.ts +497 -0
- package/dist/resources/extensions/gsd/doctor-providers.ts +343 -0
- package/dist/resources/extensions/gsd/doctor-types.ts +14 -1
- package/dist/resources/extensions/gsd/doctor.ts +6 -0
- package/dist/resources/extensions/gsd/git-service.ts +9 -0
- package/dist/resources/extensions/gsd/guided-flow-queue.ts +1 -8
- package/dist/resources/extensions/gsd/health-widget.ts +167 -0
- package/dist/resources/extensions/gsd/index.ts +6 -0
- package/dist/resources/extensions/gsd/preferences-types.ts +8 -0
- package/dist/resources/extensions/gsd/preferences-validation.ts +3 -10
- package/dist/resources/extensions/gsd/progress-score.ts +273 -0
- package/dist/resources/extensions/gsd/prompts/run-uat.md +1 -42
- package/dist/resources/extensions/gsd/quick.ts +3 -5
- package/dist/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +127 -0
- package/dist/resources/extensions/gsd/tests/doctor-environment.test.ts +314 -0
- package/dist/resources/extensions/gsd/tests/doctor-providers.test.ts +298 -0
- package/dist/resources/extensions/gsd/tests/export-html-enhancements.test.ts +3 -0
- package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +7 -3
- package/dist/resources/extensions/gsd/tests/progress-score.test.ts +206 -0
- package/dist/resources/extensions/gsd/tests/run-uat.test.ts +56 -7
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +12 -0
- package/dist/resources/extensions/gsd/visualizer-data.ts +60 -2
- package/dist/resources/extensions/gsd/visualizer-views.ts +54 -0
- package/dist/worktree-cli.d.ts +42 -6
- package/dist/worktree-cli.js +88 -48
- package/package.json +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto-constants.ts +6 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +20 -26
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -6
- package/src/resources/extensions/gsd/auto-dispatch.ts +4 -8
- package/src/resources/extensions/gsd/auto-post-unit.ts +27 -32
- package/src/resources/extensions/gsd/auto-prompts.ts +38 -34
- package/src/resources/extensions/gsd/auto-start.ts +8 -6
- package/src/resources/extensions/gsd/auto.ts +54 -33
- package/src/resources/extensions/gsd/commands-workflow-templates.ts +3 -5
- package/src/resources/extensions/gsd/commands.ts +19 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +28 -0
- package/src/resources/extensions/gsd/doctor-environment.ts +497 -0
- package/src/resources/extensions/gsd/doctor-providers.ts +343 -0
- package/src/resources/extensions/gsd/doctor-types.ts +14 -1
- package/src/resources/extensions/gsd/doctor.ts +6 -0
- package/src/resources/extensions/gsd/git-service.ts +9 -0
- package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -8
- package/src/resources/extensions/gsd/health-widget.ts +167 -0
- package/src/resources/extensions/gsd/index.ts +6 -0
- package/src/resources/extensions/gsd/preferences-types.ts +8 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +3 -10
- package/src/resources/extensions/gsd/progress-score.ts +273 -0
- package/src/resources/extensions/gsd/prompts/run-uat.md +1 -42
- package/src/resources/extensions/gsd/quick.ts +3 -5
- package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +127 -0
- package/src/resources/extensions/gsd/tests/doctor-environment.test.ts +314 -0
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +298 -0
- package/src/resources/extensions/gsd/tests/export-html-enhancements.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +7 -3
- package/src/resources/extensions/gsd/tests/progress-score.test.ts +206 -0
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +56 -7
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +12 -0
- package/src/resources/extensions/gsd/visualizer-data.ts +60 -2
- package/src/resources/extensions/gsd/visualizer-views.ts +54 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* doctor-environment.test.ts — Tests for environment health checks (#1221).
|
|
3
|
+
*
|
|
4
|
+
* Tests:
|
|
5
|
+
* - Node version detection
|
|
6
|
+
* - Dependencies installed check
|
|
7
|
+
* - Env file detection
|
|
8
|
+
* - Port conflict detection
|
|
9
|
+
* - Disk space check
|
|
10
|
+
* - Docker detection
|
|
11
|
+
* - Project tool detection
|
|
12
|
+
* - Doctor issue conversion
|
|
13
|
+
* - Report formatting
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
17
|
+
import { join, dirname } from "node:path";
|
|
18
|
+
import { tmpdir } from "node:os";
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
runEnvironmentChecks,
|
|
22
|
+
runFullEnvironmentChecks,
|
|
23
|
+
environmentResultsToDoctorIssues,
|
|
24
|
+
formatEnvironmentReport,
|
|
25
|
+
checkEnvironmentHealth,
|
|
26
|
+
type EnvironmentCheckResult,
|
|
27
|
+
} from "../doctor-environment.ts";
|
|
28
|
+
import { createTestContext } from "./test-helpers.ts";
|
|
29
|
+
|
|
30
|
+
const { assertEq, assertTrue, assertMatch, report } = createTestContext();
|
|
31
|
+
|
|
32
|
+
function createProjectDir(files: Record<string, string> = {}): string {
|
|
33
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-env-test-"));
|
|
34
|
+
for (const [name, content] of Object.entries(files)) {
|
|
35
|
+
const filePath = join(dir, name);
|
|
36
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
37
|
+
writeFileSync(filePath, content);
|
|
38
|
+
}
|
|
39
|
+
return dir;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function main(): Promise<void> {
|
|
43
|
+
const cleanups: string[] = [];
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// ── Node Version Check ─────────────────────────────────────────────
|
|
47
|
+
console.log("\n=== env: no package.json returns empty ===");
|
|
48
|
+
{
|
|
49
|
+
const dir = createProjectDir();
|
|
50
|
+
cleanups.push(dir);
|
|
51
|
+
const results = runEnvironmentChecks(dir);
|
|
52
|
+
// No package.json → no node checks
|
|
53
|
+
const nodeCheck = results.find(r => r.name === "node_version");
|
|
54
|
+
assertEq(nodeCheck, undefined, "no node version check without package.json");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log("\n=== env: package.json without engines returns no node check ===");
|
|
58
|
+
{
|
|
59
|
+
const dir = createProjectDir({
|
|
60
|
+
"package.json": JSON.stringify({ name: "test", version: "1.0.0" }),
|
|
61
|
+
});
|
|
62
|
+
cleanups.push(dir);
|
|
63
|
+
const results = runEnvironmentChecks(dir);
|
|
64
|
+
const nodeCheck = results.find(r => r.name === "node_version");
|
|
65
|
+
assertEq(nodeCheck, undefined, "no node version check without engines field");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log("\n=== env: package.json with engines returns node check ===");
|
|
69
|
+
{
|
|
70
|
+
const dir = createProjectDir({
|
|
71
|
+
"package.json": JSON.stringify({
|
|
72
|
+
name: "test",
|
|
73
|
+
version: "1.0.0",
|
|
74
|
+
engines: { node: ">=18.0.0" },
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
cleanups.push(dir);
|
|
78
|
+
const results = runEnvironmentChecks(dir);
|
|
79
|
+
const nodeCheck = results.find(r => r.name === "node_version");
|
|
80
|
+
assertTrue(nodeCheck !== undefined, "node version check runs with engines field");
|
|
81
|
+
// Current node should be >= 18 in CI
|
|
82
|
+
assertEq(nodeCheck!.status, "ok", "node version meets requirement");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Dependencies Check ─────────────────────────────────────────────
|
|
86
|
+
console.log("\n=== env: missing node_modules detected ===");
|
|
87
|
+
{
|
|
88
|
+
const dir = createProjectDir({
|
|
89
|
+
"package.json": JSON.stringify({ name: "test" }),
|
|
90
|
+
});
|
|
91
|
+
cleanups.push(dir);
|
|
92
|
+
const results = runEnvironmentChecks(dir);
|
|
93
|
+
const depsCheck = results.find(r => r.name === "dependencies");
|
|
94
|
+
assertTrue(depsCheck !== undefined, "dependencies check runs");
|
|
95
|
+
assertEq(depsCheck!.status, "error", "missing node_modules is an error");
|
|
96
|
+
assertTrue(depsCheck!.message.includes("node_modules missing"), "reports missing node_modules");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log("\n=== env: existing node_modules detected ===");
|
|
100
|
+
{
|
|
101
|
+
const dir = createProjectDir({
|
|
102
|
+
"package.json": JSON.stringify({ name: "test" }),
|
|
103
|
+
});
|
|
104
|
+
mkdirSync(join(dir, "node_modules"), { recursive: true });
|
|
105
|
+
cleanups.push(dir);
|
|
106
|
+
const results = runEnvironmentChecks(dir);
|
|
107
|
+
const depsCheck = results.find(r => r.name === "dependencies");
|
|
108
|
+
assertTrue(depsCheck !== undefined, "dependencies check runs");
|
|
109
|
+
assertEq(depsCheck!.status, "ok", "existing node_modules is ok");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Env File Check ─────────────────────────────────────────────────
|
|
113
|
+
console.log("\n=== env: .env.example without .env detected ===");
|
|
114
|
+
{
|
|
115
|
+
const dir = createProjectDir({
|
|
116
|
+
".env.example": "DB_URL=xxx\nAPI_KEY=xxx\n",
|
|
117
|
+
});
|
|
118
|
+
cleanups.push(dir);
|
|
119
|
+
const results = runEnvironmentChecks(dir);
|
|
120
|
+
const envCheck = results.find(r => r.name === "env_file");
|
|
121
|
+
assertTrue(envCheck !== undefined, "env file check runs");
|
|
122
|
+
assertEq(envCheck!.status, "warning", "missing .env is a warning");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log("\n=== env: .env.example with .env is ok ===");
|
|
126
|
+
{
|
|
127
|
+
const dir = createProjectDir({
|
|
128
|
+
".env.example": "DB_URL=xxx\n",
|
|
129
|
+
".env": "DB_URL=postgres://localhost/test\n",
|
|
130
|
+
});
|
|
131
|
+
cleanups.push(dir);
|
|
132
|
+
const results = runEnvironmentChecks(dir);
|
|
133
|
+
const envCheck = results.find(r => r.name === "env_file");
|
|
134
|
+
assertTrue(envCheck !== undefined, "env file check runs");
|
|
135
|
+
assertEq(envCheck!.status, "ok", "present .env is ok");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log("\n=== env: .env.example with .env.local is ok ===");
|
|
139
|
+
{
|
|
140
|
+
const dir = createProjectDir({
|
|
141
|
+
".env.example": "DB_URL=xxx\n",
|
|
142
|
+
".env.local": "DB_URL=postgres://localhost/test\n",
|
|
143
|
+
});
|
|
144
|
+
cleanups.push(dir);
|
|
145
|
+
const results = runEnvironmentChecks(dir);
|
|
146
|
+
const envCheck = results.find(r => r.name === "env_file");
|
|
147
|
+
assertTrue(envCheck !== undefined, "env file check runs");
|
|
148
|
+
assertEq(envCheck!.status, "ok", ".env.local counts as present");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Disk Space Check ───────────────────────────────────────────────
|
|
152
|
+
console.log("\n=== env: disk space check returns result ===");
|
|
153
|
+
if (process.platform !== "win32") {
|
|
154
|
+
const dir = createProjectDir();
|
|
155
|
+
cleanups.push(dir);
|
|
156
|
+
const results = runEnvironmentChecks(dir);
|
|
157
|
+
const diskCheck = results.find(r => r.name === "disk_space");
|
|
158
|
+
assertTrue(diskCheck !== undefined, "disk space check runs on unix");
|
|
159
|
+
// Should be ok on dev machines with reasonable disk
|
|
160
|
+
assertTrue(diskCheck!.status === "ok" || diskCheck!.status === "warning", "disk check returns valid status");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── Project Tools Check ────────────────────────────────────────────
|
|
164
|
+
console.log("\n=== env: detects missing python when pyproject.toml exists ===");
|
|
165
|
+
{
|
|
166
|
+
const dir = createProjectDir({
|
|
167
|
+
"package.json": JSON.stringify({ name: "test" }),
|
|
168
|
+
"pyproject.toml": "[build-system]\nrequires = ['setuptools']\n",
|
|
169
|
+
});
|
|
170
|
+
mkdirSync(join(dir, "node_modules"), { recursive: true });
|
|
171
|
+
cleanups.push(dir);
|
|
172
|
+
const results = runEnvironmentChecks(dir);
|
|
173
|
+
const pythonCheck = results.find(r => r.name === "python");
|
|
174
|
+
// Python is likely installed on CI/dev machines, so just verify the check runs
|
|
175
|
+
// without error — the result depends on the system
|
|
176
|
+
assertTrue(true, "python check runs without error");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log("\n=== env: detects Cargo.toml ===");
|
|
180
|
+
{
|
|
181
|
+
const dir = createProjectDir({
|
|
182
|
+
"package.json": JSON.stringify({ name: "test" }),
|
|
183
|
+
"Cargo.toml": "[package]\nname = 'test'\n",
|
|
184
|
+
});
|
|
185
|
+
mkdirSync(join(dir, "node_modules"), { recursive: true });
|
|
186
|
+
cleanups.push(dir);
|
|
187
|
+
const results = runEnvironmentChecks(dir);
|
|
188
|
+
// Just verify it runs without error
|
|
189
|
+
assertTrue(true, "cargo check runs without error");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Docker Check ───────────────────────────────────────────────────
|
|
193
|
+
console.log("\n=== env: no docker check without Dockerfile ===");
|
|
194
|
+
{
|
|
195
|
+
const dir = createProjectDir({
|
|
196
|
+
"package.json": JSON.stringify({ name: "test" }),
|
|
197
|
+
});
|
|
198
|
+
mkdirSync(join(dir, "node_modules"), { recursive: true });
|
|
199
|
+
cleanups.push(dir);
|
|
200
|
+
const results = runEnvironmentChecks(dir);
|
|
201
|
+
const dockerCheck = results.find(r => r.name === "docker");
|
|
202
|
+
assertEq(dockerCheck, undefined, "no docker check without Dockerfile");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.log("\n=== env: docker check with Dockerfile ===");
|
|
206
|
+
{
|
|
207
|
+
const dir = createProjectDir({
|
|
208
|
+
"package.json": JSON.stringify({ name: "test" }),
|
|
209
|
+
"Dockerfile": "FROM node:22\n",
|
|
210
|
+
});
|
|
211
|
+
mkdirSync(join(dir, "node_modules"), { recursive: true });
|
|
212
|
+
cleanups.push(dir);
|
|
213
|
+
const results = runEnvironmentChecks(dir);
|
|
214
|
+
const dockerCheck = results.find(r => r.name === "docker");
|
|
215
|
+
// Docker may or may not be installed on the test machine
|
|
216
|
+
assertTrue(dockerCheck !== undefined, "docker check runs when Dockerfile present");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Doctor Issue Conversion ────────────────────────────────────────
|
|
220
|
+
console.log("\n=== env: converts results to doctor issues ===");
|
|
221
|
+
{
|
|
222
|
+
const results: EnvironmentCheckResult[] = [
|
|
223
|
+
{ name: "node_version", status: "ok", message: "Node.js v22.0.0" },
|
|
224
|
+
{ name: "dependencies", status: "error", message: "node_modules missing" },
|
|
225
|
+
{ name: "env_file", status: "warning", message: ".env missing", detail: "Copy .env.example" },
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
const issues = environmentResultsToDoctorIssues(results);
|
|
229
|
+
assertEq(issues.length, 2, "only non-ok results converted");
|
|
230
|
+
assertEq(issues[0]!.severity, "error", "error severity preserved");
|
|
231
|
+
assertEq(issues[0]!.code, "env_dependencies", "code prefixed with env_");
|
|
232
|
+
assertEq(issues[1]!.severity, "warning", "warning severity preserved");
|
|
233
|
+
assertTrue(issues[1]!.message.includes("Copy .env.example"), "detail included in message");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── checkEnvironmentHealth integration ──────────────────────────────
|
|
237
|
+
console.log("\n=== env: checkEnvironmentHealth adds issues to array ===");
|
|
238
|
+
{
|
|
239
|
+
const dir = createProjectDir({
|
|
240
|
+
"package.json": JSON.stringify({ name: "test" }),
|
|
241
|
+
});
|
|
242
|
+
cleanups.push(dir);
|
|
243
|
+
|
|
244
|
+
const issues: any[] = [];
|
|
245
|
+
await checkEnvironmentHealth(dir, issues);
|
|
246
|
+
// Should have at least the missing node_modules issue
|
|
247
|
+
assertTrue(issues.some(i => i.code === "env_dependencies"), "environment issues added to array");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── Report Formatting ──────────────────────────────────────────────
|
|
251
|
+
console.log("\n=== env: formatEnvironmentReport ===");
|
|
252
|
+
{
|
|
253
|
+
const results: EnvironmentCheckResult[] = [
|
|
254
|
+
{ name: "node_version", status: "ok", message: "Node.js v22.0.0" },
|
|
255
|
+
{ name: "dependencies", status: "error", message: "node_modules missing", detail: "Run npm install" },
|
|
256
|
+
{ name: "disk_space", status: "ok", message: "50.2GB free" },
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
const report = formatEnvironmentReport(results);
|
|
260
|
+
assertTrue(report.includes("Environment Health:"), "has header");
|
|
261
|
+
assertTrue(report.includes("Node.js v22.0.0"), "includes ok result");
|
|
262
|
+
assertTrue(report.includes("node_modules missing"), "includes error result");
|
|
263
|
+
assertTrue(report.includes("Run npm install"), "includes detail for errors");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
console.log("\n=== env: formatEnvironmentReport empty ===");
|
|
267
|
+
{
|
|
268
|
+
const report = formatEnvironmentReport([]);
|
|
269
|
+
assertEq(report, "No environment checks applicable.", "empty report message");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── Full environment checks include git remote ─────────────────────
|
|
273
|
+
console.log("\n=== env: runFullEnvironmentChecks includes git remote ===");
|
|
274
|
+
{
|
|
275
|
+
// runFullEnvironmentChecks adds git remote check
|
|
276
|
+
// We can't easily test this without a real git repo, but verify it doesn't throw
|
|
277
|
+
const dir = createProjectDir();
|
|
278
|
+
cleanups.push(dir);
|
|
279
|
+
const results = runFullEnvironmentChecks(dir);
|
|
280
|
+
// No git repo → no remote check, but should not throw
|
|
281
|
+
assertTrue(true, "runFullEnvironmentChecks does not throw on non-git dir");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── Port Detection from package.json ───────────────────────────────
|
|
285
|
+
console.log("\n=== env: port detection from scripts ===");
|
|
286
|
+
if (process.platform !== "win32") {
|
|
287
|
+
const dir = createProjectDir({
|
|
288
|
+
"package.json": JSON.stringify({
|
|
289
|
+
name: "test",
|
|
290
|
+
scripts: {
|
|
291
|
+
dev: "next dev --port 3456",
|
|
292
|
+
start: "node server.js",
|
|
293
|
+
},
|
|
294
|
+
}),
|
|
295
|
+
});
|
|
296
|
+
mkdirSync(join(dir, "node_modules"), { recursive: true });
|
|
297
|
+
cleanups.push(dir);
|
|
298
|
+
const results = runEnvironmentChecks(dir);
|
|
299
|
+
// Port 3456 is unlikely to be in use, so no conflicts expected
|
|
300
|
+
const portConflicts = results.filter(r => r.name === "port_conflict");
|
|
301
|
+
// Just verify it ran without error
|
|
302
|
+
assertTrue(true, "port check with script-detected ports runs without error");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
} finally {
|
|
306
|
+
for (const dir of cleanups) {
|
|
307
|
+
try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
report();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
main();
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* doctor-providers.test.ts — Tests for provider & integration health checks.
|
|
3
|
+
*
|
|
4
|
+
* Tests:
|
|
5
|
+
* - LLM provider key detection from env vars
|
|
6
|
+
* - LLM provider key detection from auth.json
|
|
7
|
+
* - Missing required provider → error status
|
|
8
|
+
* - Backed-off credentials → warning status
|
|
9
|
+
* - Remote questions channel check (configured vs missing token)
|
|
10
|
+
* - Optional provider unconfigured status
|
|
11
|
+
* - formatProviderReport output
|
|
12
|
+
* - summariseProviderIssues compaction
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import test from "node:test";
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, realpathSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
runProviderChecks,
|
|
23
|
+
formatProviderReport,
|
|
24
|
+
summariseProviderIssues,
|
|
25
|
+
type ProviderCheckResult,
|
|
26
|
+
} from "../doctor-providers.ts";
|
|
27
|
+
|
|
28
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
function withEnv(vars: Record<string, string | undefined>, fn: () => void): void {
|
|
31
|
+
const saved: Record<string, string | undefined> = {};
|
|
32
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
33
|
+
saved[k] = process.env[k];
|
|
34
|
+
if (v === undefined) {
|
|
35
|
+
delete process.env[k];
|
|
36
|
+
} else {
|
|
37
|
+
process.env[k] = v;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
fn();
|
|
42
|
+
} finally {
|
|
43
|
+
for (const [k, v] of Object.entries(saved)) {
|
|
44
|
+
if (v === undefined) delete process.env[k];
|
|
45
|
+
else process.env[k] = v;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── formatProviderReport ─────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
test("formatProviderReport returns fallback for empty results", () => {
|
|
53
|
+
const out = formatProviderReport([]);
|
|
54
|
+
assert.equal(out, "No provider checks run.");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("formatProviderReport shows ok icon for ok status", () => {
|
|
58
|
+
const results: ProviderCheckResult[] = [{
|
|
59
|
+
name: "anthropic",
|
|
60
|
+
label: "Anthropic (Claude)",
|
|
61
|
+
category: "llm",
|
|
62
|
+
status: "ok",
|
|
63
|
+
message: "Anthropic (Claude) — key present (env)",
|
|
64
|
+
required: true,
|
|
65
|
+
}];
|
|
66
|
+
const out = formatProviderReport(results);
|
|
67
|
+
assert.ok(out.includes("✓"), "should include checkmark for ok");
|
|
68
|
+
assert.ok(out.includes("Anthropic"), "should include provider name");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("formatProviderReport shows error icon and detail for error status", () => {
|
|
72
|
+
const results: ProviderCheckResult[] = [{
|
|
73
|
+
name: "anthropic",
|
|
74
|
+
label: "Anthropic (Claude)",
|
|
75
|
+
category: "llm",
|
|
76
|
+
status: "error",
|
|
77
|
+
message: "Anthropic (Claude) — no API key found",
|
|
78
|
+
detail: "Set ANTHROPIC_API_KEY or run /gsd keys",
|
|
79
|
+
required: true,
|
|
80
|
+
}];
|
|
81
|
+
const out = formatProviderReport(results);
|
|
82
|
+
assert.ok(out.includes("✗"), "should include cross for error");
|
|
83
|
+
assert.ok(out.includes("ANTHROPIC_API_KEY"), "should include detail");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("formatProviderReport shows warning icon for warning status", () => {
|
|
87
|
+
const results: ProviderCheckResult[] = [{
|
|
88
|
+
name: "slack_bot",
|
|
89
|
+
label: "Slack Bot",
|
|
90
|
+
category: "remote",
|
|
91
|
+
status: "warning",
|
|
92
|
+
message: "Slack Bot — channel configured but token not found",
|
|
93
|
+
required: true,
|
|
94
|
+
}];
|
|
95
|
+
const out = formatProviderReport(results);
|
|
96
|
+
assert.ok(out.includes("⚠"), "should include warning icon");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("formatProviderReport groups by category", () => {
|
|
100
|
+
const results: ProviderCheckResult[] = [
|
|
101
|
+
{ name: "anthropic", label: "Anthropic", category: "llm", status: "ok", message: "ok", required: true },
|
|
102
|
+
{ name: "brave", label: "Brave Search", category: "search", status: "unconfigured", message: "not configured", required: false },
|
|
103
|
+
];
|
|
104
|
+
const out = formatProviderReport(results);
|
|
105
|
+
assert.ok(out.includes("LLM Providers"), "should have LLM section");
|
|
106
|
+
assert.ok(out.includes("Search"), "should have Search section");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("formatProviderReport omits detail for ok status", () => {
|
|
110
|
+
const results: ProviderCheckResult[] = [{
|
|
111
|
+
name: "openai",
|
|
112
|
+
label: "OpenAI",
|
|
113
|
+
category: "llm",
|
|
114
|
+
status: "ok",
|
|
115
|
+
message: "OpenAI — key present (env)",
|
|
116
|
+
detail: "should not appear",
|
|
117
|
+
required: true,
|
|
118
|
+
}];
|
|
119
|
+
const out = formatProviderReport(results);
|
|
120
|
+
assert.ok(!out.includes("should not appear"), "detail should not show for ok");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ─── summariseProviderIssues ──────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
test("summariseProviderIssues returns null when no required issues", () => {
|
|
126
|
+
const results: ProviderCheckResult[] = [
|
|
127
|
+
{ name: "anthropic", label: "Anthropic", category: "llm", status: "ok", message: "ok", required: true },
|
|
128
|
+
{ name: "brave", label: "Brave", category: "search", status: "unconfigured", message: "not configured", required: false },
|
|
129
|
+
];
|
|
130
|
+
assert.equal(summariseProviderIssues(results), null);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("summariseProviderIssues returns error summary for missing required key", () => {
|
|
134
|
+
const results: ProviderCheckResult[] = [{
|
|
135
|
+
name: "anthropic",
|
|
136
|
+
label: "Anthropic (Claude)",
|
|
137
|
+
category: "llm",
|
|
138
|
+
status: "error",
|
|
139
|
+
message: "no key",
|
|
140
|
+
required: true,
|
|
141
|
+
}];
|
|
142
|
+
const summary = summariseProviderIssues(results);
|
|
143
|
+
assert.ok(summary !== null, "should return a summary");
|
|
144
|
+
assert.ok(summary!.includes("Anthropic"), "should name the provider");
|
|
145
|
+
assert.ok(summary!.includes("✗"), "should use error icon");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("summariseProviderIssues returns warning for backed-off required provider", () => {
|
|
149
|
+
const results: ProviderCheckResult[] = [{
|
|
150
|
+
name: "anthropic",
|
|
151
|
+
label: "Anthropic (Claude)",
|
|
152
|
+
category: "llm",
|
|
153
|
+
status: "warning",
|
|
154
|
+
message: "backed off",
|
|
155
|
+
required: true,
|
|
156
|
+
}];
|
|
157
|
+
const summary = summariseProviderIssues(results);
|
|
158
|
+
assert.ok(summary !== null, "should return summary");
|
|
159
|
+
assert.ok(summary!.includes("⚠"), "should use warning icon");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("summariseProviderIssues appends count when multiple issues", () => {
|
|
163
|
+
const results: ProviderCheckResult[] = [
|
|
164
|
+
{ name: "anthropic", label: "Anthropic", category: "llm", status: "error", message: "err", required: true },
|
|
165
|
+
{ name: "openai", label: "OpenAI", category: "llm", status: "error", message: "err", required: true },
|
|
166
|
+
{ name: "google", label: "Google", category: "llm", status: "error", message: "err", required: true },
|
|
167
|
+
];
|
|
168
|
+
const summary = summariseProviderIssues(results);
|
|
169
|
+
assert.ok(summary!.includes("+2 more"), "should show overflow count");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("summariseProviderIssues ignores unconfigured optional providers", () => {
|
|
173
|
+
const results: ProviderCheckResult[] = [
|
|
174
|
+
{ name: "anthropic", label: "Anthropic", category: "llm", status: "ok", message: "ok", required: true },
|
|
175
|
+
{ name: "brave", label: "Brave", category: "search", status: "unconfigured", message: "nc", required: false },
|
|
176
|
+
{ name: "tavily", label: "Tavily", category: "search", status: "unconfigured", message: "nc", required: false },
|
|
177
|
+
];
|
|
178
|
+
assert.equal(summariseProviderIssues(results), null, "optional missing providers should not raise issue");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// ─── runProviderChecks — env var detection ────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
test("runProviderChecks detects Anthropic key from ANTHROPIC_API_KEY env var", () => {
|
|
184
|
+
// Isolate from real HOME so loadEffectiveGSDPreferences returns null (default → anthropic)
|
|
185
|
+
// and auth.json lookups hit an empty directory.
|
|
186
|
+
const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-env-test-")));
|
|
187
|
+
withEnv({ ANTHROPIC_API_KEY: "sk-ant-test-key", HOME: tmpHome }, () => {
|
|
188
|
+
try {
|
|
189
|
+
const results = runProviderChecks();
|
|
190
|
+
const anthropic = results.find(r => r.name === "anthropic");
|
|
191
|
+
assert.ok(anthropic, "anthropic result should exist");
|
|
192
|
+
assert.equal(anthropic!.status, "ok", "should be ok when env var set");
|
|
193
|
+
assert.ok(anthropic!.message.includes("env"), "should report env source");
|
|
194
|
+
} finally {
|
|
195
|
+
rmSync(tmpHome, { recursive: true, force: true });
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("runProviderChecks returns error for Anthropic when no key present", () => {
|
|
201
|
+
const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-test-")));
|
|
202
|
+
withEnv({ ANTHROPIC_API_KEY: undefined, HOME: tmpHome }, () => {
|
|
203
|
+
try {
|
|
204
|
+
const results = runProviderChecks();
|
|
205
|
+
const anthropic = results.find(r => r.name === "anthropic");
|
|
206
|
+
assert.ok(anthropic, "anthropic should be present (default required)");
|
|
207
|
+
assert.equal(anthropic!.status, "error", "should be error when no key");
|
|
208
|
+
} finally {
|
|
209
|
+
rmSync(tmpHome, { recursive: true, force: true });
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("runProviderChecks optional providers have required=false", () => {
|
|
215
|
+
const results = runProviderChecks();
|
|
216
|
+
const optional = results.filter(r => ["brave", "tavily", "jina", "context7"].includes(r.name));
|
|
217
|
+
for (const r of optional) {
|
|
218
|
+
assert.equal(r.required, false, `${r.name} should not be required`);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("runProviderChecks optional providers show unconfigured when no key", () => {
|
|
223
|
+
withEnv(
|
|
224
|
+
{ BRAVE_API_KEY: undefined, TAVILY_API_KEY: undefined, JINA_API_KEY: undefined, CONTEXT7_API_KEY: undefined },
|
|
225
|
+
() => {
|
|
226
|
+
const origHome = process.env.HOME;
|
|
227
|
+
process.env.HOME = mkdtempSync(join(tmpdir(), "gsd-providers-test-"));
|
|
228
|
+
try {
|
|
229
|
+
const results = runProviderChecks();
|
|
230
|
+
const brave = results.find(r => r.name === "brave");
|
|
231
|
+
assert.ok(brave, "brave should be present");
|
|
232
|
+
assert.equal(brave!.status, "unconfigured", "should be unconfigured");
|
|
233
|
+
} finally {
|
|
234
|
+
rmSync(process.env.HOME!, { recursive: true, force: true });
|
|
235
|
+
process.env.HOME = origHome;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("runProviderChecks optional providers show ok when key set", () => {
|
|
242
|
+
withEnv({ BRAVE_API_KEY: "test-brave-key" }, () => {
|
|
243
|
+
const results = runProviderChecks();
|
|
244
|
+
const brave = results.find(r => r.name === "brave");
|
|
245
|
+
assert.ok(brave, "brave should be present");
|
|
246
|
+
assert.equal(brave!.status, "ok", "should be ok when env var set");
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// ─── runProviderChecks — auth.json detection ─────────────────────────────────
|
|
251
|
+
|
|
252
|
+
test("runProviderChecks detects key from auth.json", () => {
|
|
253
|
+
withEnv({ ANTHROPIC_API_KEY: undefined }, () => {
|
|
254
|
+
const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-test-")));
|
|
255
|
+
const agentDir = join(tmpHome, ".gsd", "agent");
|
|
256
|
+
mkdirSync(agentDir, { recursive: true });
|
|
257
|
+
|
|
258
|
+
// AuthStorage persists credentials with provider ID as the top-level key:
|
|
259
|
+
// { "anthropic": { "type": "api_key", "key": "..." } }
|
|
260
|
+
const authData = {
|
|
261
|
+
anthropic: { type: "api_key", key: "sk-ant-from-auth-json" },
|
|
262
|
+
};
|
|
263
|
+
writeFileSync(join(agentDir, "auth.json"), JSON.stringify(authData));
|
|
264
|
+
|
|
265
|
+
withEnv({ HOME: tmpHome }, () => {
|
|
266
|
+
const results = runProviderChecks();
|
|
267
|
+
const anthropic = results.find(r => r.name === "anthropic");
|
|
268
|
+
assert.ok(anthropic, "anthropic should be present");
|
|
269
|
+
assert.equal(anthropic!.status, "ok", "should be ok with auth.json key");
|
|
270
|
+
assert.ok(anthropic!.message.includes("auth.json"), "should report auth.json source");
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
rmSync(tmpHome, { recursive: true, force: true });
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("runProviderChecks ignores empty placeholder keys in auth.json", () => {
|
|
278
|
+
withEnv({ ANTHROPIC_API_KEY: undefined }, () => {
|
|
279
|
+
const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-test-")));
|
|
280
|
+
const agentDir = join(tmpHome, ".gsd", "agent");
|
|
281
|
+
mkdirSync(agentDir, { recursive: true });
|
|
282
|
+
|
|
283
|
+
// Empty key — what onboarding writes when user skips
|
|
284
|
+
const authData = {
|
|
285
|
+
anthropic: { type: "api_key", key: "" },
|
|
286
|
+
};
|
|
287
|
+
writeFileSync(join(agentDir, "auth.json"), JSON.stringify(authData));
|
|
288
|
+
|
|
289
|
+
withEnv({ HOME: tmpHome }, () => {
|
|
290
|
+
const results = runProviderChecks();
|
|
291
|
+
const anthropic = results.find(r => r.name === "anthropic");
|
|
292
|
+
assert.ok(anthropic, "anthropic should be present");
|
|
293
|
+
assert.equal(anthropic!.status, "error", "empty placeholder key should count as not configured");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
rmSync(tmpHome, { recursive: true, force: true });
|
|
297
|
+
});
|
|
298
|
+
});
|
|
@@ -119,6 +119,9 @@ function mockData(overrides: Partial<VisualizerData> = {}): VisualizerData {
|
|
|
119
119
|
toolCalls: 40,
|
|
120
120
|
assistantMessages: 20,
|
|
121
121
|
userMessages: 12,
|
|
122
|
+
providers: [],
|
|
123
|
+
skillSummary: { total: 0, warningCount: 0, criticalCount: 0, topIssue: null },
|
|
124
|
+
environmentIssues: [],
|
|
122
125
|
},
|
|
123
126
|
discussion: [],
|
|
124
127
|
stats: { missingCount: 0, missingSlices: [], updatedCount: 0, updatedSlices: [], recentEntries: [] },
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import test from "node:test";
|
|
9
9
|
import assert from "node:assert/strict";
|
|
10
|
-
import { mkdtempSync, rmSync, existsSync, readdirSync, readFileSync } from "node:fs";
|
|
10
|
+
import { mkdtempSync, rmSync, existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
13
|
|
|
@@ -23,7 +23,11 @@ function createCtx(entries: unknown[]) {
|
|
|
23
23
|
|
|
24
24
|
test("clearActivityLogState resets dedup state so identical saves write again", () => {
|
|
25
25
|
clearActivityLogState();
|
|
26
|
-
|
|
26
|
+
// Pre-resolve baseDir so gsdRoot() returns a stable key across calls.
|
|
27
|
+
// On macOS, /tmp is a symlink to /private/tmp — without realpathSync, the
|
|
28
|
+
// key changes between the first save (dir doesn't exist, realpathSync throws)
|
|
29
|
+
// and subsequent saves (dir exists, realpathSync resolves to /private/tmp/...).
|
|
30
|
+
const baseDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-memleak-test-")));
|
|
27
31
|
try {
|
|
28
32
|
const entries = [{ role: "assistant", content: "test entry" }];
|
|
29
33
|
const ctx = createCtx(entries);
|
|
@@ -53,7 +57,7 @@ test("clearActivityLogState resets dedup state so identical saves write again",
|
|
|
53
57
|
|
|
54
58
|
test("saveActivityLog writes valid JSONL via streaming", () => {
|
|
55
59
|
clearActivityLogState();
|
|
56
|
-
const baseDir = mkdtempSync(join(tmpdir(), "gsd-memleak-jsonl-"));
|
|
60
|
+
const baseDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-memleak-jsonl-")));
|
|
57
61
|
try {
|
|
58
62
|
const entries = [
|
|
59
63
|
{ type: "message", message: { role: "user", content: "hello" } },
|