gsd-pi 2.18.0 → 2.19.0

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.
Files changed (73) hide show
  1. package/dist/resources/extensions/gsd/auto-dashboard.ts +14 -2
  2. package/dist/resources/extensions/gsd/auto-prompts.ts +45 -15
  3. package/dist/resources/extensions/gsd/auto.ts +276 -19
  4. package/dist/resources/extensions/gsd/captures.ts +384 -0
  5. package/dist/resources/extensions/gsd/commands.ts +139 -3
  6. package/dist/resources/extensions/gsd/complexity-classifier.ts +322 -0
  7. package/dist/resources/extensions/gsd/dashboard-overlay.ts +10 -0
  8. package/dist/resources/extensions/gsd/metrics.ts +48 -0
  9. package/dist/resources/extensions/gsd/model-cost-table.ts +65 -0
  10. package/dist/resources/extensions/gsd/model-router.ts +256 -0
  11. package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  12. package/dist/resources/extensions/gsd/preferences.ts +73 -0
  13. package/dist/resources/extensions/gsd/prompt-loader.ts +45 -9
  14. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
  15. package/dist/resources/extensions/gsd/prompts/replan-slice.md +8 -0
  16. package/dist/resources/extensions/gsd/prompts/triage-captures.md +62 -0
  17. package/dist/resources/extensions/gsd/tests/captures.test.ts +438 -0
  18. package/dist/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
  19. package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
  20. package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
  21. package/dist/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
  22. package/dist/resources/extensions/gsd/tests/model-router.test.ts +167 -0
  23. package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +227 -1
  24. package/dist/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
  25. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
  26. package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
  27. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
  28. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
  29. package/dist/resources/extensions/gsd/triage-resolution.ts +200 -0
  30. package/dist/resources/extensions/gsd/triage-ui.ts +175 -0
  31. package/dist/resources/extensions/gsd/visualizer-data.ts +154 -0
  32. package/dist/resources/extensions/gsd/visualizer-overlay.ts +193 -0
  33. package/dist/resources/extensions/gsd/visualizer-views.ts +293 -0
  34. package/dist/resources/extensions/remote-questions/discord-adapter.ts +33 -0
  35. package/dist/resources/extensions/remote-questions/format.ts +12 -6
  36. package/dist/resources/extensions/remote-questions/manager.ts +8 -0
  37. package/package.json +1 -1
  38. package/src/resources/extensions/gsd/auto-dashboard.ts +14 -2
  39. package/src/resources/extensions/gsd/auto-prompts.ts +45 -15
  40. package/src/resources/extensions/gsd/auto.ts +276 -19
  41. package/src/resources/extensions/gsd/captures.ts +384 -0
  42. package/src/resources/extensions/gsd/commands.ts +139 -3
  43. package/src/resources/extensions/gsd/complexity-classifier.ts +322 -0
  44. package/src/resources/extensions/gsd/dashboard-overlay.ts +10 -0
  45. package/src/resources/extensions/gsd/metrics.ts +48 -0
  46. package/src/resources/extensions/gsd/model-cost-table.ts +65 -0
  47. package/src/resources/extensions/gsd/model-router.ts +256 -0
  48. package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  49. package/src/resources/extensions/gsd/preferences.ts +73 -0
  50. package/src/resources/extensions/gsd/prompt-loader.ts +45 -9
  51. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
  52. package/src/resources/extensions/gsd/prompts/replan-slice.md +8 -0
  53. package/src/resources/extensions/gsd/prompts/triage-captures.md +62 -0
  54. package/src/resources/extensions/gsd/tests/captures.test.ts +438 -0
  55. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
  56. package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
  57. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
  58. package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
  59. package/src/resources/extensions/gsd/tests/model-router.test.ts +167 -0
  60. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +227 -1
  61. package/src/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
  62. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
  63. package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
  64. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
  65. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
  66. package/src/resources/extensions/gsd/triage-resolution.ts +200 -0
  67. package/src/resources/extensions/gsd/triage-ui.ts +175 -0
  68. package/src/resources/extensions/gsd/visualizer-data.ts +154 -0
  69. package/src/resources/extensions/gsd/visualizer-overlay.ts +193 -0
  70. package/src/resources/extensions/gsd/visualizer-views.ts +293 -0
  71. package/src/resources/extensions/remote-questions/discord-adapter.ts +33 -0
  72. package/src/resources/extensions/remote-questions/format.ts +12 -6
  73. package/src/resources/extensions/remote-questions/manager.ts +8 -0
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Unit tests for GSD Triage Resolution — resolution execution and file overlap detection.
3
+ */
4
+
5
+ import test from "node:test";
6
+ import assert from "node:assert/strict";
7
+ import { mkdirSync, readFileSync, writeFileSync, rmSync, existsSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { tmpdir } from "node:os";
10
+ import { appendCapture, markCaptureResolved, loadAllCaptures } from "../captures.ts";
11
+ // Import only the functions that don't depend on @gsd/pi-coding-agent
12
+ // (triage-ui.ts imports next-action-ui.ts which imports the unavailable package)
13
+ import { executeInject, executeReplan, detectFileOverlap, loadDeferredCaptures, loadReplanCaptures, buildQuickTaskPrompt } from "../triage-resolution.ts";
14
+
15
+ function makeTempDir(prefix: string): string {
16
+ const dir = join(
17
+ tmpdir(),
18
+ `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`,
19
+ );
20
+ mkdirSync(dir, { recursive: true });
21
+ return dir;
22
+ }
23
+
24
+ function setupPlanFile(tmp: string, mid: string, sid: string, content: string): string {
25
+ const planDir = join(tmp, ".gsd", "milestones", mid, "slices", sid);
26
+ mkdirSync(planDir, { recursive: true });
27
+ const planPath = join(planDir, `${sid}-PLAN.md`);
28
+ writeFileSync(planPath, content, "utf-8");
29
+ return planPath;
30
+ }
31
+
32
+ const SAMPLE_PLAN = `# S01: Test Slice
33
+
34
+ **Goal:** Test
35
+ **Demo:** Test
36
+
37
+ ## Must-Haves
38
+
39
+ - Something works
40
+
41
+ ## Tasks
42
+
43
+ - [x] **T01: First task** \`est:1h\`
44
+ - Why: Setup
45
+ - Files: \`src/foo.ts\`, \`src/bar.ts\`
46
+ - Do: Build it
47
+ - Done when: Tests pass
48
+
49
+ - [ ] **T02: Second task** \`est:1h\`
50
+ - Why: Feature
51
+ - Files: \`src/baz.ts\`, \`src/qux.ts\`
52
+ - Do: Build it
53
+ - Done when: Tests pass
54
+
55
+ - [ ] **T03: Third task** \`est:30m\`
56
+ - Why: Polish
57
+ - Files: \`src/qux.ts\`, \`src/config.ts\`
58
+ - Do: Build it
59
+ - Done when: Tests pass
60
+
61
+ ## Files Likely Touched
62
+
63
+ - \`src/foo.ts\`
64
+ - \`src/bar.ts\`
65
+ `;
66
+
67
+ // ─── executeInject ────────────────────────────────────────────────────────────
68
+
69
+ test("resolution: executeInject appends a new task to the plan", () => {
70
+ const tmp = makeTempDir("res-inject");
71
+ try {
72
+ const planPath = setupPlanFile(tmp, "M001", "S01", SAMPLE_PLAN);
73
+ const captureId = appendCapture(tmp, "add retry logic");
74
+ const captures = loadAllCaptures(tmp);
75
+ const capture = captures[0];
76
+
77
+ const newId = executeInject(tmp, "M001", "S01", capture);
78
+
79
+ assert.strictEqual(newId, "T04", "should be T04 (next after T03)");
80
+
81
+ const updated = readFileSync(planPath, "utf-8");
82
+ assert.ok(updated.includes("**T04:"), "should have T04 in plan");
83
+ assert.ok(updated.includes(capture.text), "should include capture text");
84
+ assert.ok(updated.includes("## Files Likely Touched"), "should preserve files section");
85
+
86
+ // T04 should appear before Files Likely Touched
87
+ const t04Pos = updated.indexOf("**T04:");
88
+ const filesPos = updated.indexOf("## Files Likely Touched");
89
+ assert.ok(t04Pos < filesPos, "T04 should be before Files section");
90
+ } finally {
91
+ rmSync(tmp, { recursive: true, force: true });
92
+ }
93
+ });
94
+
95
+ test("resolution: executeInject returns null when plan doesn't exist", () => {
96
+ const tmp = makeTempDir("res-inject-noplan");
97
+ try {
98
+ const captureId = appendCapture(tmp, "some task");
99
+ const captures = loadAllCaptures(tmp);
100
+ const result = executeInject(tmp, "M001", "S01", captures[0]);
101
+ assert.strictEqual(result, null);
102
+ } finally {
103
+ rmSync(tmp, { recursive: true, force: true });
104
+ }
105
+ });
106
+
107
+ // ─── executeReplan ────────────────────────────────────────────────────────────
108
+
109
+ test("resolution: executeReplan writes REPLAN-TRIGGER.md", () => {
110
+ const tmp = makeTempDir("res-replan");
111
+ try {
112
+ setupPlanFile(tmp, "M001", "S01", SAMPLE_PLAN);
113
+ const captureId = appendCapture(tmp, "approach is wrong, need different strategy");
114
+ const captures = loadAllCaptures(tmp);
115
+ const capture = captures[0];
116
+
117
+ const result = executeReplan(tmp, "M001", "S01", capture);
118
+ assert.strictEqual(result, true);
119
+
120
+ const triggerPath = join(
121
+ tmp, ".gsd", "milestones", "M001", "slices", "S01", "S01-REPLAN-TRIGGER.md",
122
+ );
123
+ assert.ok(existsSync(triggerPath), "trigger file should exist");
124
+
125
+ const content = readFileSync(triggerPath, "utf-8");
126
+ assert.ok(content.includes(capture.id), "should include capture ID");
127
+ assert.ok(content.includes(capture.text), "should include capture text");
128
+ assert.ok(content.includes("# Replan Trigger"), "should have header");
129
+ } finally {
130
+ rmSync(tmp, { recursive: true, force: true });
131
+ }
132
+ });
133
+
134
+ // ─── detectFileOverlap ───────────────────────────────────────────────────────
135
+
136
+ test("resolution: detectFileOverlap finds overlapping incomplete tasks", () => {
137
+ const overlaps = detectFileOverlap(["src/qux.ts"], SAMPLE_PLAN);
138
+ assert.deepStrictEqual(overlaps, ["T02", "T03"]);
139
+ });
140
+
141
+ test("resolution: detectFileOverlap ignores completed tasks", () => {
142
+ // T01 is [x] and uses src/foo.ts — should NOT be returned
143
+ const overlaps = detectFileOverlap(["src/foo.ts"], SAMPLE_PLAN);
144
+ assert.deepStrictEqual(overlaps, []);
145
+ });
146
+
147
+ test("resolution: detectFileOverlap returns empty when no overlap", () => {
148
+ const overlaps = detectFileOverlap(["src/unrelated.ts"], SAMPLE_PLAN);
149
+ assert.deepStrictEqual(overlaps, []);
150
+ });
151
+
152
+ test("resolution: detectFileOverlap returns empty for empty affected files", () => {
153
+ assert.deepStrictEqual(detectFileOverlap([], SAMPLE_PLAN), []);
154
+ });
155
+
156
+ test("resolution: detectFileOverlap is case-insensitive", () => {
157
+ const overlaps = detectFileOverlap(["SRC/QUX.TS"], SAMPLE_PLAN);
158
+ assert.deepStrictEqual(overlaps, ["T02", "T03"]);
159
+ });
160
+
161
+ // ─── loadDeferredCaptures / loadReplanCaptures ───────────────────────────────
162
+
163
+ test("resolution: loadDeferredCaptures returns only deferred captures", () => {
164
+ const tmp = makeTempDir("res-deferred");
165
+ try {
166
+ const id1 = appendCapture(tmp, "deferred one");
167
+ const id2 = appendCapture(tmp, "note one");
168
+ const id3 = appendCapture(tmp, "deferred two");
169
+
170
+ markCaptureResolved(tmp, id1, "defer", "deferred to S03", "future work");
171
+ markCaptureResolved(tmp, id2, "note", "acknowledged", "just a note");
172
+ markCaptureResolved(tmp, id3, "defer", "deferred to S04", "later");
173
+
174
+ const deferred = loadDeferredCaptures(tmp);
175
+ assert.strictEqual(deferred.length, 2);
176
+ assert.strictEqual(deferred[0].id, id1);
177
+ assert.strictEqual(deferred[1].id, id3);
178
+ } finally {
179
+ rmSync(tmp, { recursive: true, force: true });
180
+ }
181
+ });
182
+
183
+ test("resolution: loadReplanCaptures returns only replan captures", () => {
184
+ const tmp = makeTempDir("res-replan-load");
185
+ try {
186
+ const id1 = appendCapture(tmp, "needs replan");
187
+ const id2 = appendCapture(tmp, "just a note");
188
+
189
+ markCaptureResolved(tmp, id1, "replan", "replan triggered", "approach changed");
190
+ markCaptureResolved(tmp, id2, "note", "acknowledged", "info only");
191
+
192
+ const replans = loadReplanCaptures(tmp);
193
+ assert.strictEqual(replans.length, 1);
194
+ assert.strictEqual(replans[0].id, id1);
195
+ } finally {
196
+ rmSync(tmp, { recursive: true, force: true });
197
+ }
198
+ });
199
+
200
+ // ─── buildQuickTaskPrompt ────────────────────────────────────────────────────
201
+
202
+ test("resolution: buildQuickTaskPrompt includes capture text and ID", () => {
203
+ const prompt = buildQuickTaskPrompt({
204
+ id: "CAP-abc123",
205
+ text: "add retry logic to OAuth",
206
+ timestamp: "2026-03-15T20:00:00Z",
207
+ status: "resolved",
208
+ classification: "quick-task",
209
+ });
210
+
211
+ assert.ok(prompt.includes("CAP-abc123"), "should include capture ID");
212
+ assert.ok(prompt.includes("add retry logic to OAuth"), "should include capture text");
213
+ assert.ok(prompt.includes("Quick Task"), "should have Quick Task header");
214
+ assert.ok(prompt.includes("Do NOT modify"), "should warn about plan files");
215
+ });
@@ -0,0 +1,198 @@
1
+ // Tests for GSD visualizer data loader.
2
+ // Verifies the VisualizerData interface shape and source-file contracts.
3
+
4
+ import { readFileSync } from "node:fs";
5
+ import { join, dirname } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { createTestContext } from "./test-helpers.ts";
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const { assertTrue, report } = createTestContext();
11
+
12
+ const dataPath = join(__dirname, "..", "visualizer-data.ts");
13
+ const dataSrc = readFileSync(dataPath, "utf-8");
14
+
15
+ console.log("\n=== visualizer-data.ts source contracts ===");
16
+
17
+ // Interface exports
18
+ assertTrue(
19
+ dataSrc.includes("export interface VisualizerData"),
20
+ "exports VisualizerData interface",
21
+ );
22
+
23
+ assertTrue(
24
+ dataSrc.includes("export interface VisualizerMilestone"),
25
+ "exports VisualizerMilestone interface",
26
+ );
27
+
28
+ assertTrue(
29
+ dataSrc.includes("export interface VisualizerSlice"),
30
+ "exports VisualizerSlice interface",
31
+ );
32
+
33
+ assertTrue(
34
+ dataSrc.includes("export interface VisualizerTask"),
35
+ "exports VisualizerTask interface",
36
+ );
37
+
38
+ // Function export
39
+ assertTrue(
40
+ dataSrc.includes("export async function loadVisualizerData"),
41
+ "exports loadVisualizerData function",
42
+ );
43
+
44
+ // Data source usage
45
+ assertTrue(
46
+ dataSrc.includes("deriveState"),
47
+ "uses deriveState for state derivation",
48
+ );
49
+
50
+ assertTrue(
51
+ dataSrc.includes("findMilestoneIds"),
52
+ "uses findMilestoneIds to enumerate milestones",
53
+ );
54
+
55
+ assertTrue(
56
+ dataSrc.includes("parseRoadmap"),
57
+ "uses parseRoadmap for roadmap parsing",
58
+ );
59
+
60
+ assertTrue(
61
+ dataSrc.includes("parsePlan"),
62
+ "uses parsePlan for plan parsing",
63
+ );
64
+
65
+ assertTrue(
66
+ dataSrc.includes("getLedger"),
67
+ "uses getLedger for in-memory metrics",
68
+ );
69
+
70
+ assertTrue(
71
+ dataSrc.includes("loadLedgerFromDisk"),
72
+ "uses loadLedgerFromDisk as fallback",
73
+ );
74
+
75
+ assertTrue(
76
+ dataSrc.includes("getProjectTotals"),
77
+ "uses getProjectTotals for aggregation",
78
+ );
79
+
80
+ assertTrue(
81
+ dataSrc.includes("aggregateByPhase"),
82
+ "uses aggregateByPhase",
83
+ );
84
+
85
+ assertTrue(
86
+ dataSrc.includes("aggregateBySlice"),
87
+ "uses aggregateBySlice",
88
+ );
89
+
90
+ assertTrue(
91
+ dataSrc.includes("aggregateByModel"),
92
+ "uses aggregateByModel",
93
+ );
94
+
95
+ // Interface fields
96
+ assertTrue(
97
+ dataSrc.includes("dependsOn: string[]"),
98
+ "VisualizerMilestone has dependsOn field",
99
+ );
100
+
101
+ assertTrue(
102
+ dataSrc.includes("depends: string[]"),
103
+ "VisualizerSlice has depends field",
104
+ );
105
+
106
+ assertTrue(
107
+ dataSrc.includes("totals: ProjectTotals | null"),
108
+ "VisualizerData has nullable totals",
109
+ );
110
+
111
+ assertTrue(
112
+ dataSrc.includes("units: UnitMetrics[]"),
113
+ "VisualizerData has units array",
114
+ );
115
+
116
+ // Verify overlay source exists and imports data module
117
+ const overlayPath = join(__dirname, "..", "visualizer-overlay.ts");
118
+ const overlaySrc = readFileSync(overlayPath, "utf-8");
119
+
120
+ console.log("\n=== visualizer-overlay.ts source contracts ===");
121
+
122
+ assertTrue(
123
+ overlaySrc.includes("export class GSDVisualizerOverlay"),
124
+ "exports GSDVisualizerOverlay class",
125
+ );
126
+
127
+ assertTrue(
128
+ overlaySrc.includes("loadVisualizerData"),
129
+ "overlay uses loadVisualizerData",
130
+ );
131
+
132
+ assertTrue(
133
+ overlaySrc.includes("renderProgressView"),
134
+ "overlay delegates to renderProgressView",
135
+ );
136
+
137
+ assertTrue(
138
+ overlaySrc.includes("renderDepsView"),
139
+ "overlay delegates to renderDepsView",
140
+ );
141
+
142
+ assertTrue(
143
+ overlaySrc.includes("renderMetricsView"),
144
+ "overlay delegates to renderMetricsView",
145
+ );
146
+
147
+ assertTrue(
148
+ overlaySrc.includes("renderTimelineView"),
149
+ "overlay delegates to renderTimelineView",
150
+ );
151
+
152
+ assertTrue(
153
+ overlaySrc.includes("handleInput"),
154
+ "overlay has handleInput method",
155
+ );
156
+
157
+ assertTrue(
158
+ overlaySrc.includes("dispose"),
159
+ "overlay has dispose method",
160
+ );
161
+
162
+ assertTrue(
163
+ overlaySrc.includes("wrapInBox"),
164
+ "overlay has wrapInBox helper",
165
+ );
166
+
167
+ assertTrue(
168
+ overlaySrc.includes("activeTab"),
169
+ "overlay tracks active tab",
170
+ );
171
+
172
+ assertTrue(
173
+ overlaySrc.includes("scrollOffsets"),
174
+ "overlay tracks per-tab scroll offsets",
175
+ );
176
+
177
+ // Verify commands.ts integration
178
+ const commandsPath = join(__dirname, "..", "commands.ts");
179
+ const commandsSrc = readFileSync(commandsPath, "utf-8");
180
+
181
+ console.log("\n=== commands.ts integration ===");
182
+
183
+ assertTrue(
184
+ commandsSrc.includes('"visualize"'),
185
+ "commands.ts has visualize in subcommands array",
186
+ );
187
+
188
+ assertTrue(
189
+ commandsSrc.includes("GSDVisualizerOverlay"),
190
+ "commands.ts imports GSDVisualizerOverlay",
191
+ );
192
+
193
+ assertTrue(
194
+ commandsSrc.includes("handleVisualize"),
195
+ "commands.ts has handleVisualize handler",
196
+ );
197
+
198
+ report();
@@ -0,0 +1,255 @@
1
+ // Tests for GSD visualizer view renderers.
2
+ // Tests the pure view functions with mock data — no file I/O.
3
+
4
+ import {
5
+ renderProgressView,
6
+ renderDepsView,
7
+ renderMetricsView,
8
+ renderTimelineView,
9
+ } from "../visualizer-views.js";
10
+ import type { VisualizerData } from "../visualizer-data.js";
11
+ import { createTestContext } from "./test-helpers.ts";
12
+
13
+ const { assertEq, assertTrue, report } = createTestContext();
14
+
15
+ // ─── Mock theme ─────────────────────────────────────────────────────────────
16
+
17
+ const mockTheme = {
18
+ fg: (_color: string, text: string) => text,
19
+ bold: (text: string) => text,
20
+ } as any;
21
+
22
+ // ─── Test data factories ────────────────────────────────────────────────────
23
+
24
+ function makeVisualizerData(overrides: Partial<VisualizerData> = {}): VisualizerData {
25
+ return {
26
+ milestones: [],
27
+ phase: "executing",
28
+ totals: null,
29
+ byPhase: [],
30
+ bySlice: [],
31
+ byModel: [],
32
+ units: [],
33
+ ...overrides,
34
+ };
35
+ }
36
+
37
+ // ─── renderProgressView ─────────────────────────────────────────────────────
38
+
39
+ console.log("\n=== renderProgressView ===");
40
+
41
+ {
42
+ const data = makeVisualizerData({
43
+ milestones: [
44
+ {
45
+ id: "M001",
46
+ title: "First Milestone",
47
+ status: "active",
48
+ dependsOn: [],
49
+ slices: [
50
+ {
51
+ id: "S01",
52
+ title: "Core Types",
53
+ done: true,
54
+ active: false,
55
+ risk: "low",
56
+ depends: [],
57
+ tasks: [],
58
+ },
59
+ {
60
+ id: "S02",
61
+ title: "State Engine",
62
+ done: false,
63
+ active: true,
64
+ risk: "high",
65
+ depends: ["S01"],
66
+ tasks: [
67
+ { id: "T01", title: "Dispatch Loop", done: false, active: true },
68
+ { id: "T02", title: "Session Mgmt", done: true, active: false },
69
+ ],
70
+ },
71
+ {
72
+ id: "S03",
73
+ title: "Dashboard",
74
+ done: false,
75
+ active: false,
76
+ risk: "medium",
77
+ depends: ["S02"],
78
+ tasks: [],
79
+ },
80
+ ],
81
+ },
82
+ {
83
+ id: "M002",
84
+ title: "Plugin Arch",
85
+ status: "pending",
86
+ dependsOn: ["M001"],
87
+ slices: [],
88
+ },
89
+ ],
90
+ });
91
+
92
+ const lines = renderProgressView(data, mockTheme, 80);
93
+ assertTrue(lines.length > 0, "progress view produces output");
94
+ assertTrue(lines.some(l => l.includes("M001")), "shows milestone M001");
95
+ assertTrue(lines.some(l => l.includes("S01")), "shows slice S01");
96
+ assertTrue(lines.some(l => l.includes("T01")), "shows task T01 for active slice");
97
+ assertTrue(lines.some(l => l.includes("M002")), "shows milestone M002");
98
+ assertTrue(lines.some(l => l.includes("depends on M001")), "shows dependency note");
99
+ }
100
+
101
+ {
102
+ const data = makeVisualizerData({ milestones: [] });
103
+ const lines = renderProgressView(data, mockTheme, 80);
104
+ assertEq(lines.length, 0, "empty milestones produce no lines");
105
+ }
106
+
107
+ // ─── renderDepsView ─────────────────────────────────────────────────────────
108
+
109
+ console.log("\n=== renderDepsView ===");
110
+
111
+ {
112
+ const data = makeVisualizerData({
113
+ milestones: [
114
+ {
115
+ id: "M001",
116
+ title: "First",
117
+ status: "active",
118
+ dependsOn: [],
119
+ slices: [
120
+ { id: "S01", title: "A", done: false, active: true, risk: "low", depends: [], tasks: [] },
121
+ { id: "S02", title: "B", done: false, active: false, risk: "low", depends: ["S01"], tasks: [] },
122
+ ],
123
+ },
124
+ {
125
+ id: "M002",
126
+ title: "Second",
127
+ status: "pending",
128
+ dependsOn: ["M001"],
129
+ slices: [],
130
+ },
131
+ ],
132
+ });
133
+
134
+ const lines = renderDepsView(data, mockTheme, 80);
135
+ assertTrue(lines.length > 0, "deps view produces output");
136
+ assertTrue(lines.some(l => l.includes("M001") && l.includes("M002")), "shows milestone dep edge");
137
+ assertTrue(lines.some(l => l.includes("S01") && l.includes("S02")), "shows slice dep edge");
138
+ }
139
+
140
+ {
141
+ const data = makeVisualizerData({
142
+ milestones: [
143
+ { id: "M001", title: "Only", status: "active", dependsOn: [], slices: [] },
144
+ ],
145
+ });
146
+
147
+ const lines = renderDepsView(data, mockTheme, 80);
148
+ assertTrue(lines.some(l => l.includes("No milestone dependencies")), "shows no-deps message");
149
+ }
150
+
151
+ // ─── renderMetricsView ──────────────────────────────────────────────────────
152
+
153
+ console.log("\n=== renderMetricsView ===");
154
+
155
+ {
156
+ const data = makeVisualizerData({
157
+ totals: {
158
+ units: 5,
159
+ tokens: { input: 1000, output: 500, cacheRead: 200, cacheWrite: 100, total: 1800 },
160
+ cost: 2.50,
161
+ duration: 60000,
162
+ toolCalls: 15,
163
+ assistantMessages: 10,
164
+ userMessages: 5,
165
+ },
166
+ byPhase: [
167
+ {
168
+ phase: "execution",
169
+ units: 3,
170
+ tokens: { input: 600, output: 300, cacheRead: 100, cacheWrite: 50, total: 1050 },
171
+ cost: 1.50,
172
+ duration: 40000,
173
+ },
174
+ {
175
+ phase: "planning",
176
+ units: 2,
177
+ tokens: { input: 400, output: 200, cacheRead: 100, cacheWrite: 50, total: 750 },
178
+ cost: 1.00,
179
+ duration: 20000,
180
+ },
181
+ ],
182
+ byModel: [
183
+ {
184
+ model: "claude-opus-4-6",
185
+ units: 5,
186
+ tokens: { input: 1000, output: 500, cacheRead: 200, cacheWrite: 100, total: 1800 },
187
+ cost: 2.50,
188
+ },
189
+ ],
190
+ });
191
+
192
+ const lines = renderMetricsView(data, mockTheme, 80);
193
+ assertTrue(lines.length > 0, "metrics view produces output");
194
+ assertTrue(lines.some(l => l.includes("$2.50")), "shows total cost");
195
+ assertTrue(lines.some(l => l.includes("execution")), "shows phase name");
196
+ assertTrue(lines.some(l => l.includes("claude-opus-4-6")), "shows model name");
197
+ }
198
+
199
+ {
200
+ const data = makeVisualizerData({ totals: null });
201
+ const lines = renderMetricsView(data, mockTheme, 80);
202
+ assertTrue(lines.some(l => l.includes("No metrics data")), "shows no-data message");
203
+ }
204
+
205
+ // ─── renderTimelineView ─────────────────────────────────────────────────────
206
+
207
+ console.log("\n=== renderTimelineView ===");
208
+
209
+ {
210
+ const now = Date.now();
211
+ const data = makeVisualizerData({
212
+ units: [
213
+ {
214
+ type: "execute-task",
215
+ id: "M001/S01/T01",
216
+ model: "claude-opus-4-6",
217
+ startedAt: now - 120000,
218
+ finishedAt: now - 60000,
219
+ tokens: { input: 500, output: 200, cacheRead: 100, cacheWrite: 50, total: 850 },
220
+ cost: 0.42,
221
+ toolCalls: 5,
222
+ assistantMessages: 3,
223
+ userMessages: 1,
224
+ },
225
+ {
226
+ type: "plan-slice",
227
+ id: "M001/S02",
228
+ model: "claude-opus-4-6",
229
+ startedAt: now - 60000,
230
+ finishedAt: now - 30000,
231
+ tokens: { input: 300, output: 150, cacheRead: 50, cacheWrite: 25, total: 525 },
232
+ cost: 0.18,
233
+ toolCalls: 2,
234
+ assistantMessages: 2,
235
+ userMessages: 1,
236
+ },
237
+ ],
238
+ });
239
+
240
+ const lines = renderTimelineView(data, mockTheme, 80);
241
+ assertTrue(lines.length >= 2, "timeline view produces lines for each unit");
242
+ assertTrue(lines.some(l => l.includes("execute-task")), "shows unit type");
243
+ assertTrue(lines.some(l => l.includes("M001/S01/T01")), "shows unit id");
244
+ assertTrue(lines.some(l => l.includes("$0.42")), "shows unit cost");
245
+ }
246
+
247
+ {
248
+ const data = makeVisualizerData({ units: [] });
249
+ const lines = renderTimelineView(data, mockTheme, 80);
250
+ assertTrue(lines.some(l => l.includes("No execution history")), "shows empty message");
251
+ }
252
+
253
+ // ─── Report ─────────────────────────────────────────────────────────────────
254
+
255
+ report();