gsd-pi 2.31.2-dev.c8d7e03 → 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/resources/extensions/gsd/auto-start.ts +4 -2
- 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/health-widget.ts +167 -0
- package/dist/resources/extensions/gsd/index.ts +6 -0
- package/dist/resources/extensions/gsd/progress-score.ts +273 -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/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/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-start.ts +4 -2
- 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/health-widget.ts +167 -0
- package/src/resources/extensions/gsd/index.ts +6 -0
- package/src/resources/extensions/gsd/progress-score.ts +273 -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/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,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" } },
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* progress-score.test.ts — Tests for progress score / traffic light (#1221).
|
|
3
|
+
*
|
|
4
|
+
* Tests:
|
|
5
|
+
* - Score computation from health signals
|
|
6
|
+
* - Signal evaluation (trend, error streak, recent errors)
|
|
7
|
+
* - Context-aware scoring (retry counts, unit progress)
|
|
8
|
+
* - Formatting (single-line, detailed report)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
recordHealthSnapshot,
|
|
13
|
+
resetProactiveHealing,
|
|
14
|
+
} from "../doctor-proactive.ts";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
computeProgressScore,
|
|
18
|
+
computeProgressScoreWithContext,
|
|
19
|
+
formatProgressLine,
|
|
20
|
+
formatProgressReport,
|
|
21
|
+
} from "../progress-score.ts";
|
|
22
|
+
|
|
23
|
+
import { createTestContext } from "./test-helpers.ts";
|
|
24
|
+
|
|
25
|
+
const { assertEq, assertTrue, assertMatch, report } = createTestContext();
|
|
26
|
+
|
|
27
|
+
async function main(): Promise<void> {
|
|
28
|
+
try {
|
|
29
|
+
// ── Base Score: No Data ─────────────────────────────────────────────
|
|
30
|
+
console.log("\n=== progress: green with no data ===");
|
|
31
|
+
{
|
|
32
|
+
resetProactiveHealing();
|
|
33
|
+
const score = computeProgressScore();
|
|
34
|
+
assertEq(score.level, "green", "green when no data available");
|
|
35
|
+
assertTrue(score.summary.includes("Progressing well"), "summary says progressing");
|
|
36
|
+
assertTrue(score.signals.length > 0, "has signals");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Green: Clean Health Data ────────────────────────────────────────
|
|
40
|
+
console.log("\n=== progress: green with clean health ===");
|
|
41
|
+
{
|
|
42
|
+
resetProactiveHealing();
|
|
43
|
+
for (let i = 0; i < 5; i++) {
|
|
44
|
+
recordHealthSnapshot(0, 0, 0);
|
|
45
|
+
}
|
|
46
|
+
const score = computeProgressScore();
|
|
47
|
+
assertEq(score.level, "green", "green with all clean snapshots");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Yellow: Some Warnings ──────────────────────────────────────────
|
|
51
|
+
console.log("\n=== progress: yellow with error streak ===");
|
|
52
|
+
{
|
|
53
|
+
resetProactiveHealing();
|
|
54
|
+
recordHealthSnapshot(1, 2, 0);
|
|
55
|
+
recordHealthSnapshot(1, 1, 0);
|
|
56
|
+
const score = computeProgressScore();
|
|
57
|
+
assertEq(score.level, "yellow", "yellow with consecutive errors");
|
|
58
|
+
assertTrue(score.summary.includes("Struggling"), "summary says struggling");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Red: Degrading Health ──────────────────────────────────────────
|
|
62
|
+
console.log("\n=== progress: red with degrading trend ===");
|
|
63
|
+
{
|
|
64
|
+
resetProactiveHealing();
|
|
65
|
+
// 5 older clean snapshots
|
|
66
|
+
for (let i = 0; i < 5; i++) {
|
|
67
|
+
recordHealthSnapshot(0, 0, 0);
|
|
68
|
+
}
|
|
69
|
+
// 5 recent error snapshots — triggers degrading trend
|
|
70
|
+
for (let i = 0; i < 5; i++) {
|
|
71
|
+
recordHealthSnapshot(3, 5, 0);
|
|
72
|
+
}
|
|
73
|
+
const score = computeProgressScore();
|
|
74
|
+
assertEq(score.level, "red", "red with degrading trend and persistent errors");
|
|
75
|
+
assertTrue(score.summary.includes("Stuck"), "summary says stuck");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Red: High Error Streak ─────────────────────────────────────────
|
|
79
|
+
console.log("\n=== progress: red with high error streak ===");
|
|
80
|
+
{
|
|
81
|
+
resetProactiveHealing();
|
|
82
|
+
for (let i = 0; i < 4; i++) {
|
|
83
|
+
recordHealthSnapshot(2, 0, 0);
|
|
84
|
+
}
|
|
85
|
+
const score = computeProgressScore();
|
|
86
|
+
assertEq(score.level, "red", "red with 4 consecutive error units");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Context-Aware Scoring ──────────────────────────────────────────
|
|
90
|
+
console.log("\n=== progress: context with retries ===");
|
|
91
|
+
{
|
|
92
|
+
resetProactiveHealing();
|
|
93
|
+
for (let i = 0; i < 3; i++) {
|
|
94
|
+
recordHealthSnapshot(0, 0, 0);
|
|
95
|
+
}
|
|
96
|
+
const score = computeProgressScoreWithContext({
|
|
97
|
+
currentUnitId: "M001/S01/T03",
|
|
98
|
+
completedUnits: 2,
|
|
99
|
+
totalUnits: 5,
|
|
100
|
+
retryCount: 0,
|
|
101
|
+
maxRetries: 5,
|
|
102
|
+
});
|
|
103
|
+
assertEq(score.level, "green", "green with no retries");
|
|
104
|
+
assertTrue(score.summary.includes("M001/S01/T03"), "summary includes unit ID");
|
|
105
|
+
assertTrue(score.summary.includes("2 of 5"), "summary includes progress");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log("\n=== progress: context with high retry count ===");
|
|
109
|
+
{
|
|
110
|
+
resetProactiveHealing();
|
|
111
|
+
for (let i = 0; i < 3; i++) {
|
|
112
|
+
recordHealthSnapshot(0, 0, 0);
|
|
113
|
+
}
|
|
114
|
+
const score = computeProgressScoreWithContext({
|
|
115
|
+
currentUnitId: "M001/S01/T03",
|
|
116
|
+
retryCount: 4,
|
|
117
|
+
maxRetries: 5,
|
|
118
|
+
});
|
|
119
|
+
assertEq(score.level, "red", "red with high retry count");
|
|
120
|
+
assertTrue(score.summary.includes("looping"), "summary mentions looping");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log("\n=== progress: context with moderate retries ===");
|
|
124
|
+
{
|
|
125
|
+
resetProactiveHealing();
|
|
126
|
+
for (let i = 0; i < 3; i++) {
|
|
127
|
+
recordHealthSnapshot(0, 0, 0);
|
|
128
|
+
}
|
|
129
|
+
const score = computeProgressScoreWithContext({
|
|
130
|
+
currentUnitId: "M001/S01/T03",
|
|
131
|
+
retryCount: 1,
|
|
132
|
+
maxRetries: 5,
|
|
133
|
+
});
|
|
134
|
+
assertEq(score.level, "yellow", "yellow with 1 retry");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── Formatting ─────────────────────────────────────────────────────
|
|
138
|
+
console.log("\n=== progress: formatProgressLine ===");
|
|
139
|
+
{
|
|
140
|
+
resetProactiveHealing();
|
|
141
|
+
const score = computeProgressScore();
|
|
142
|
+
const line = formatProgressLine(score);
|
|
143
|
+
assertTrue(line.includes("Progressing well"), "line includes summary");
|
|
144
|
+
// Should start with green circle emoji
|
|
145
|
+
assertTrue(line.startsWith("\uD83D\uDFE2"), "starts with green circle");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
console.log("\n=== progress: formatProgressLine yellow ===");
|
|
149
|
+
{
|
|
150
|
+
resetProactiveHealing();
|
|
151
|
+
recordHealthSnapshot(1, 0, 0);
|
|
152
|
+
recordHealthSnapshot(1, 0, 0);
|
|
153
|
+
const score = computeProgressScore();
|
|
154
|
+
const line = formatProgressLine(score);
|
|
155
|
+
assertTrue(line.startsWith("\uD83D\uDFE1"), "starts with yellow circle");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.log("\n=== progress: formatProgressReport ===");
|
|
159
|
+
{
|
|
160
|
+
resetProactiveHealing();
|
|
161
|
+
recordHealthSnapshot(0, 1, 0);
|
|
162
|
+
const score = computeProgressScore();
|
|
163
|
+
const detailed = formatProgressReport(score);
|
|
164
|
+
assertTrue(detailed.includes("Signals:"), "report has signals section");
|
|
165
|
+
assertTrue(detailed.includes("health_trend"), "report includes trend signal");
|
|
166
|
+
assertTrue(detailed.includes("error_streak"), "report includes streak signal");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Signal Details ─────────────────────────────────────────────────
|
|
170
|
+
console.log("\n=== progress: signal names are consistent ===");
|
|
171
|
+
{
|
|
172
|
+
resetProactiveHealing();
|
|
173
|
+
recordHealthSnapshot(0, 0, 0);
|
|
174
|
+
const score = computeProgressScore();
|
|
175
|
+
const names = score.signals.map(s => s.name);
|
|
176
|
+
assertTrue(names.includes("health_trend"), "has health_trend signal");
|
|
177
|
+
assertTrue(names.includes("error_streak"), "has error_streak signal");
|
|
178
|
+
assertTrue(names.includes("recent_errors"), "has recent_errors signal");
|
|
179
|
+
assertTrue(names.includes("artifact_production"), "has artifact_production signal");
|
|
180
|
+
assertTrue(names.includes("dispatch_velocity"), "has dispatch_velocity signal");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log("\n=== progress: all signals have valid levels ===");
|
|
184
|
+
{
|
|
185
|
+
resetProactiveHealing();
|
|
186
|
+
for (let i = 0; i < 5; i++) {
|
|
187
|
+
recordHealthSnapshot(1, 1, 1);
|
|
188
|
+
}
|
|
189
|
+
const score = computeProgressScore();
|
|
190
|
+
for (const signal of score.signals) {
|
|
191
|
+
assertTrue(
|
|
192
|
+
signal.level === "green" || signal.level === "yellow" || signal.level === "red",
|
|
193
|
+
`signal ${signal.name} has valid level: ${signal.level}`,
|
|
194
|
+
);
|
|
195
|
+
assertTrue(signal.detail.length > 0, `signal ${signal.name} has non-empty detail`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
} finally {
|
|
200
|
+
resetProactiveHealing();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
report();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
main();
|
|
@@ -60,6 +60,9 @@ function makeVisualizerData(overrides: Partial<VisualizerData> = {}): Visualizer
|
|
|
60
60
|
toolCalls: 0,
|
|
61
61
|
assistantMessages: 0,
|
|
62
62
|
userMessages: 0,
|
|
63
|
+
providers: [],
|
|
64
|
+
skillSummary: { total: 0, warningCount: 0, criticalCount: 0, topIssue: null },
|
|
65
|
+
environmentIssues: [],
|
|
63
66
|
},
|
|
64
67
|
discussion: [],
|
|
65
68
|
stats: {
|
|
@@ -503,6 +506,9 @@ console.log("\n=== renderAgentView ===");
|
|
|
503
506
|
truncationRate: 15.5, continueHereRate: 5.0,
|
|
504
507
|
tierBreakdown: [], tierSavingsLine: "",
|
|
505
508
|
toolCalls: 20, assistantMessages: 15, userMessages: 8,
|
|
509
|
+
providers: [],
|
|
510
|
+
skillSummary: { total: 0, warningCount: 0, criticalCount: 0, topIssue: null },
|
|
511
|
+
environmentIssues: [],
|
|
506
512
|
},
|
|
507
513
|
captures: { entries: [], pendingCount: 3, totalCount: 5 },
|
|
508
514
|
});
|
|
@@ -669,6 +675,9 @@ console.log("\n=== renderHealthView ===");
|
|
|
669
675
|
toolCalls: 50,
|
|
670
676
|
assistantMessages: 30,
|
|
671
677
|
userMessages: 15,
|
|
678
|
+
providers: [],
|
|
679
|
+
skillSummary: { total: 0, warningCount: 0, criticalCount: 0, topIssue: null },
|
|
680
|
+
environmentIssues: [],
|
|
672
681
|
},
|
|
673
682
|
});
|
|
674
683
|
|
|
@@ -693,6 +702,9 @@ console.log("\n=== renderHealthView ===");
|
|
|
693
702
|
truncationRate: 0, continueHereRate: 0,
|
|
694
703
|
tierBreakdown: [], tierSavingsLine: "",
|
|
695
704
|
toolCalls: 0, assistantMessages: 0, userMessages: 0,
|
|
705
|
+
providers: [],
|
|
706
|
+
skillSummary: { total: 0, warningCount: 0, criticalCount: 0, topIssue: null },
|
|
707
|
+
environmentIssues: [],
|
|
696
708
|
},
|
|
697
709
|
});
|
|
698
710
|
|