diff --git a/src/resources/extensions/sf/milestone-quality.ts b/src/resources/extensions/sf/milestone-quality.ts index d87e1195d..dda5006d8 100644 --- a/src/resources/extensions/sf/milestone-quality.ts +++ b/src/resources/extensions/sf/milestone-quality.ts @@ -44,7 +44,6 @@ function normalizeVisionMeetingRoute( if (!firstLine) return undefined; const cleaned = firstLine.toLowerCase().replace(/[`*_~]/g, " "); const match = cleaned.match(/\b(planning|researching|discussing)\b/); - console.log(`[sf:debug] normalizeVisionMeetingRoute: firstLine="${firstLine}" match="${match?.[1]}"`); return match?.[1] as VisionMeetingRoute | undefined; } @@ -159,12 +158,6 @@ export function inspectMilestoneRoadmapMarkdown( issues.push("missing vision alignment meeting"); return { issues }; } - - const recommendedRoute = normalizeVisionMeetingRoute(extractSubsection( - meetingSection, - "Recommended Route", - )); - console.log(`[sf:debug] recommendedRoute for current meeting: "${recommendedRoute}"`); const meeting: Partial = { trigger: extractSubsection(meetingSection, "Trigger"), pm: extractSubsection(meetingSection, "Product Manager"), @@ -179,14 +172,13 @@ export function inspectMilestoneRoadmapMarkdown( moderator: extractSubsection(meetingSection, "Moderator"), weightedSynthesis: extractSubsection(meetingSection, "Weighted Synthesis"), confidenceByArea: extractSubsection(meetingSection, "Confidence By Area"), - recommendedRoute, + recommendedRoute: normalizeVisionMeetingRoute( + extractSubsection(meetingSection, "Recommended Route"), + ), }; const blockingIssue = getVisionAlignmentBlockingIssue(meeting); - if (blockingIssue) { - console.log(`[sf:debug] blockingIssue: ${blockingIssue}`); - issues.push(blockingIssue); - } + if (blockingIssue) issues.push(blockingIssue); return { issues }; } diff --git a/src/resources/extensions/sf/parsers-legacy.ts b/src/resources/extensions/sf/parsers-legacy.ts index 055324d34..d0c2fb69b 100644 --- a/src/resources/extensions/sf/parsers-legacy.ts +++ b/src/resources/extensions/sf/parsers-legacy.ts @@ -76,9 +76,10 @@ export function parseRoadmap(content: string): Roadmap { function _parseRoadmapImpl(content: string): Roadmap { const stopTimer = debugTime("parse-roadmap"); - // Try native parser first for better performance + // Try native parser first for better performance. Fall back to legacy if + // native finds zero slices (e.g. table-style roadmaps not yet supported). const nativeResult = nativeParseRoadmap(content); - if (nativeResult) { + if (nativeResult && nativeResult.slices.length > 0) { stopTimer({ native: true, slices: nativeResult.slices.length, diff --git a/src/resources/extensions/sf/tests/visualizer-views.test.ts b/src/resources/extensions/sf/tests/visualizer-views.test.ts index 1087e2a4b..a12061d87 100644 --- a/src/resources/extensions/sf/tests/visualizer-views.test.ts +++ b/src/resources/extensions/sf/tests/visualizer-views.test.ts @@ -1,6 +1,7 @@ // Tests for SF visualizer view renderers. // Tests the pure view functions with mock data — no file I/O. +import { describe, it } from "vitest"; import assert from "node:assert/strict"; import type { VisualizerData } from "../visualizer-data.js"; import { @@ -81,1191 +82,1165 @@ function makeVisualizerData( }; } -// ─── renderProgressView ───────────────────────────────────────────────────── +// ═══════════════════════════════════════════════════════════════════════════ +// Tests +// ═══════════════════════════════════════════════════════════════════════════ -console.log("\n=== renderProgressView ==="); - -{ - const data = makeVisualizerData({ - milestones: [ - { - id: "M001", - title: "First Milestone", - status: "active", - dependsOn: [], - slices: [ +describe("SF visualizer view renderers", () => { + describe("renderProgressView", () => { + it("renders progress with milestones, tasks, and stats", () => { + const data = makeVisualizerData({ + milestones: [ { - id: "S01", - title: "Core Types", - done: true, - active: false, - risk: "low", - depends: [], - tasks: [], - }, - { - id: "S02", - title: "State Engine", - done: false, - active: true, - risk: "high", - depends: ["S01"], - tasks: [ + id: "M001", + title: "First Milestone", + status: "active", + dependsOn: [], + slices: [ { - id: "T01", - title: "Dispatch Loop", + id: "S01", + title: "Core Types", + done: true, + active: false, + risk: "low", + depends: [], + tasks: [], + }, + { + id: "S02", + title: "State Engine", done: false, active: true, - estimate: "30m", + risk: "high", + depends: ["S01"], + tasks: [ + { + id: "T01", + title: "Dispatch Loop", + done: false, + active: true, + estimate: "30m", + }, + { id: "T02", title: "Session Mgmt", done: true, active: false }, + ], + }, + { + id: "S03", + title: "Dashboard", + done: false, + active: false, + risk: "medium", + depends: ["S02"], + tasks: [], }, - { id: "T02", title: "Session Mgmt", done: true, active: false }, ], }, { - id: "S03", - title: "Dashboard", - done: false, - active: false, - risk: "medium", - depends: ["S02"], - tasks: [], + id: "M002", + title: "Plugin Arch", + status: "pending", + dependsOn: ["M001"], + slices: [], }, ], - }, - { - id: "M002", - title: "Plugin Arch", - status: "pending", - dependsOn: ["M001"], - slices: [], - }, - ], - sliceVerifications: [ - { - milestoneId: "M001", - sliceId: "S01", - verificationResult: "passed", - blockerDiscovered: false, - keyDecisions: [], - patternsEstablished: [], - provides: ["core-types"], - requires: [], - }, - ], - stats: { - missingCount: 2, - missingSlices: [ - { milestoneId: "M001", sliceId: "S02", title: "State Engine" }, - { milestoneId: "M001", sliceId: "S03", title: "Dashboard" }, - ], - updatedCount: 1, - updatedSlices: [ - { - milestoneId: "M001", - sliceId: "S01", - title: "Core Types", - completedAt: "2026-03-15T14:30:00Z", - }, - ], - recentEntries: [ - { - milestoneId: "M001", - sliceId: "S01", - title: "Core Types Infrastructure", - oneLiner: "Core structures assembled", - filesModified: [], - completedAt: "2026-03-15T14:30:00Z", - }, - ], - }, - }); - - const lines = renderProgressView(data, mockTheme, 80); - assert.ok(lines.length > 0, "progress view produces output"); - assert.ok( - lines.some((l) => l.includes("M001")), - "shows milestone M001", - ); - assert.ok( - lines.some((l) => l.includes("S01")), - "shows slice S01", - ); - assert.ok( - lines.some((l) => l.includes("T01")), - "shows task T01 for active slice", - ); - assert.ok( - lines.some((l) => l.includes("M002")), - "shows milestone M002", - ); - assert.ok( - lines.some((l) => l.includes("depends on M001")), - "shows dependency note", - ); - assert.ok( - lines.some((l) => l.includes("30m")), - "shows task estimate", - ); - assert.ok( - lines.some((l) => l.includes("Feature Snapshot")), - "shows stats header", - ); - assert.ok( - lines.some((l) => l.includes("Missing slices")), - "shows missing slices count", - ); - assert.ok( - lines.some((l) => l.includes("State Engine")), - "shows missing slice preview", - ); - assert.ok( - lines.some((l) => l.includes("Updated (last 7 days)")), - "shows updated count", - ); - assert.ok( - lines.some((l) => l.includes("Recent completions")), - "shows recent completions section", - ); - assert.ok( - lines.some((l) => l.includes("Core structures assembled")), - "shows recent one-liner entry", - ); -} - -{ - const data = makeVisualizerData({ - discussion: [ - { - milestoneId: "M001", - title: "First Milestone", - state: "discussed", - hasContext: true, - hasDraft: false, - lastUpdated: "2026-03-15T14:30:00Z", - }, - { - milestoneId: "M002", - title: "Plugin Arch", - state: "draft", - hasContext: false, - hasDraft: true, - lastUpdated: "2026-03-16T09:00:00Z", - }, - { - milestoneId: "M003", - title: "Next Batch", - state: "undiscussed", - hasContext: false, - hasDraft: false, - lastUpdated: null, - }, - ], - }); - - const lines = renderProgressView(data, mockTheme, 80); - assert.ok( - lines.some((l) => l.includes("Discussion Status")), - "shows discussion section", - ); - assert.ok( - lines.some((l) => l.includes("Discussed: 1")), - "counts discussed milestones", - ); - assert.ok( - lines.some((l) => l.includes("Draft")), - "shows draft badge", - ); - assert.ok( - lines.some((l) => l.includes("Pending")), - "shows pending badge", - ); -} - -// Verification badges -{ - const data = makeVisualizerData({ - milestones: [ - { - id: "M001", - title: "Test", - status: "active", - dependsOn: [], - slices: [ + sliceVerifications: [ { - id: "S01", - title: "Done Slice", - done: true, - active: false, - risk: "low", - depends: [], - tasks: [], + milestoneId: "M001", + sliceId: "S01", + verificationResult: "passed", + blockerDiscovered: false, + keyDecisions: [], + patternsEstablished: [], + provides: ["core-types"], + requires: [], }, ], - }, - ], - sliceVerifications: [ - { - milestoneId: "M001", - sliceId: "S01", - verificationResult: "passed", - blockerDiscovered: true, - keyDecisions: [], - patternsEstablished: [], - provides: [], - requires: [], - }, - ], - }); - - const lines = renderProgressView(data, mockTheme, 80); - // The verification badge should show check mark and warning - assert.ok( - lines.some((l) => l.includes("S01")), - "shows slice with verification", - ); -} - -{ - const data = makeVisualizerData({ milestones: [] }); - const lines = renderProgressView(data, mockTheme, 80); - assert.ok( - lines.some((l) => l.includes("Feature Snapshot")), - "shows stats snapshot even when no milestones", - ); - assert.ok( - lines.some((l) => l.includes("Missing slices")), - "reports missing slices count", - ); -} - -// ─── Risk Heatmap ─────────────────────────────────────────────────────────── - -console.log("\n=== Risk Heatmap ==="); - -{ - const data = makeVisualizerData({ - milestones: [ - { - id: "M001", - title: "First", - status: "active", - dependsOn: [], - slices: [ - { - id: "S01", - title: "A", - done: true, - active: false, - risk: "low", - depends: [], - tasks: [], - }, - { - id: "S02", - title: "B", - done: false, - active: true, - risk: "high", - depends: [], - tasks: [], - }, - { - id: "S03", - title: "C", - done: false, - active: false, - risk: "medium", - depends: [], - tasks: [], - }, - { - id: "S04", - title: "D", - done: false, - active: false, - risk: "high", - depends: [], - tasks: [], - }, - ], - }, - ], - }); - - const lines = renderProgressView(data, mockTheme, 80); - assert.ok( - lines.some((l) => l.includes("Risk Heatmap")), - "heatmap header present", - ); - assert.ok( - lines.some((l) => l.includes("1 low, 1 med, 2 high")), - "risk summary counts", - ); - assert.ok( - lines.some((l) => l.includes("1 high-risk not started")), - "high-risk not started warning", - ); -} - -// ─── Search/Filter ────────────────────────────────────────────────────────── - -console.log("\n=== Search/Filter ==="); - -{ - const data = makeVisualizerData({ - milestones: [ - { - id: "M001", - title: "Auth", - status: "active", - dependsOn: [], - slices: [ - { - id: "S01", - title: "JWT", - done: false, - active: false, - risk: "low", - depends: [], - tasks: [], - }, - { - id: "S02", - title: "OAuth", - done: false, - active: false, - risk: "high", - depends: [], - tasks: [], - }, - ], - }, - { - id: "M002", - title: "Dashboard", - status: "pending", - dependsOn: ["M001"], - slices: [], - }, - ], - }); - - const filtered = renderProgressView(data, mockTheme, 80, { - text: "auth", - field: "all", - }); - assert.ok( - filtered.some((l) => l.includes("M001")), - "filter shows matching milestone", - ); - assert.ok( - filtered.some((l) => l.includes("Filter (all): auth")), - "filter indicator present", - ); - - const riskFiltered = renderProgressView(data, mockTheme, 80, { - text: "high", - field: "risk", - }); - assert.ok( - riskFiltered.some((l) => l.includes("M001")), - "risk filter shows milestone with high-risk slice", - ); -} - -// ─── renderDepsView ───────────────────────────────────────────────────────── - -console.log("\n=== renderDepsView ==="); - -{ - const data = makeVisualizerData({ - milestones: [ - { - id: "M001", - title: "First", - status: "active", - dependsOn: [], - slices: [ - { - id: "S01", - title: "A", - done: false, - active: true, - risk: "low", - depends: [], - tasks: [], - }, - { - id: "S02", - title: "B", - done: false, - active: false, - risk: "low", - depends: ["S01"], - tasks: [], - }, - ], - }, - { - id: "M002", - title: "Second", - status: "pending", - dependsOn: ["M001"], - slices: [], - }, - ], - criticalPath: { - milestonePath: ["M001", "M002"], - slicePath: ["S01", "S02"], - milestoneSlack: new Map([ - ["M001", 0], - ["M002", 0], - ]), - sliceSlack: new Map([ - ["S01", 0], - ["S02", 0], - ]), - }, - sliceVerifications: [ - { - milestoneId: "M001", - sliceId: "S01", - verificationResult: "passed", - blockerDiscovered: false, - keyDecisions: [], - patternsEstablished: [], - provides: ["api-types"], - requires: [], - }, - ], - }); - - const lines = renderDepsView(data, mockTheme, 80); - assert.ok(lines.length > 0, "deps view produces output"); - assert.ok( - lines.some((l) => l.includes("M001") && l.includes("M002")), - "shows milestone dep edge", - ); - assert.ok( - lines.some((l) => l.includes("S01") && l.includes("S02")), - "shows slice dep edge", - ); - assert.ok( - lines.some((l) => l.includes("Critical Path")), - "shows critical path section", - ); - assert.ok( - lines.some((l) => l.includes("[CRITICAL]")), - "shows CRITICAL badge", - ); - assert.ok( - lines.some((l) => l.includes("Data Flow")), - "shows data flow section", - ); - assert.ok( - lines.some((l) => l.includes("api-types")), - "shows provides artifact", - ); -} - -{ - const data = makeVisualizerData({ - milestones: [ - { - id: "M001", - title: "Only", - status: "active", - dependsOn: [], - slices: [], - }, - ], - }); - - const lines = renderDepsView(data, mockTheme, 80); - assert.ok( - lines.some((l) => l.includes("No milestone dependencies")), - "shows no-deps message", - ); -} - -// ─── renderMetricsView ────────────────────────────────────────────────────── - -console.log("\n=== renderMetricsView ==="); - -{ - const data = makeVisualizerData({ - totals: { - units: 5, - tokens: { - input: 1000, - output: 500, - cacheRead: 200, - cacheWrite: 100, - total: 1800, - }, - cost: 2.5, - duration: 60000, - toolCalls: 15, - assistantMessages: 10, - userMessages: 5, - totalTruncationSections: 0, - continueHereFiredCount: 0, - apiRequests: 5, - }, - byPhase: [ - { - phase: "execution", - units: 3, - tokens: { - input: 600, - output: 300, - cacheRead: 100, - cacheWrite: 50, - total: 1050, - }, - cost: 1.5, - duration: 40000, - }, - ], - byModel: [ - { - model: "claude-opus-4-6", - units: 5, - tokens: { - input: 1000, - output: 500, - cacheRead: 200, - cacheWrite: 100, - total: 1800, - }, - cost: 2.5, - }, - ], - byTier: [ - { - tier: "standard", - units: 3, - tokens: { - input: 600, - output: 300, - cacheRead: 100, - cacheWrite: 50, - total: 1050, - }, - cost: 1.5, - downgraded: 0, - }, - { - tier: "light", - units: 2, - tokens: { - input: 400, - output: 200, - cacheRead: 100, - cacheWrite: 50, - total: 750, - }, - cost: 1.0, - downgraded: 1, - }, - ], - tierSavingsLine: "Dynamic routing: 1/5 units downgraded (20%), cost: $1.00", - bySlice: [ - { - sliceId: "M001/S01", - units: 3, - tokens: { - input: 600, - output: 300, - cacheRead: 100, - cacheWrite: 50, - total: 1050, - }, - cost: 1.5, - duration: 40000, - }, - { - sliceId: "M001/S02", - units: 2, - tokens: { - input: 400, - output: 200, - cacheRead: 100, - cacheWrite: 50, - total: 750, - }, - cost: 1.0, - duration: 20000, - }, - ], - remainingSliceCount: 3, - }); - - const lines = renderMetricsView(data, mockTheme, 80); - assert.ok(lines.length > 0, "metrics view produces output"); - assert.ok( - lines.some((l) => l.includes("$2.50")), - "shows total cost", - ); - assert.ok( - lines.some((l) => l.includes("execution")), - "shows phase name", - ); - assert.ok( - lines.some((l) => l.includes("claude-opus-4-6")), - "shows model name", - ); - assert.ok( - lines.some((l) => l.includes("By Tier")), - "shows tier breakdown section", - ); - assert.ok( - lines.some((l) => l.includes("standard")), - "shows tier name", - ); - assert.ok( - lines.some((l) => l.includes("Dynamic routing")), - "shows tier savings line", - ); - assert.ok( - lines.some((l) => l.includes("Tools: 15")), - "shows tool call count", - ); - assert.ok( - lines.some((l) => l.includes("10") && l.includes("sent")), - "shows message counts", - ); -} - -{ - const data = makeVisualizerData({ totals: null }); - const lines = renderMetricsView(data, mockTheme, 80); - assert.ok( - lines.some((l) => l.includes("No metrics data")), - "shows no-data message", - ); -} - -// ─── renderTimelineView ───────────────────────────────────────────────────── - -console.log("\n=== renderTimelineView ==="); - -{ - const now = Date.now(); - const data = makeVisualizerData({ - units: [ - { - type: "execute-task", - id: "M001/S01/T01", - model: "claude-opus-4-6", - startedAt: now - 120000, - finishedAt: now - 60000, - tokens: { - input: 500, - output: 200, - cacheRead: 100, - cacheWrite: 50, - total: 850, - }, - cost: 0.42, - toolCalls: 5, - assistantMessages: 3, - userMessages: 1, - tier: "standard", - }, - ], - }); - - const listLines = renderTimelineView(data, mockTheme, 80); - assert.ok(listLines.length >= 1, "list view produces lines"); - assert.ok( - listLines.some((l) => l.includes("execute-task")), - "shows unit type", - ); - assert.ok( - listLines.some((l) => l.includes("[standard]")), - "shows tier in timeline", - ); - assert.ok( - listLines.some((l) => l.includes("opus-4-6")), - "shows shortened model", - ); -} - -{ - const data = makeVisualizerData({ units: [] }); - const lines = renderTimelineView(data, mockTheme, 80); - assert.ok( - lines.some((l) => l.includes("No execution history")), - "shows empty message", - ); -} - -// ─── renderAgentView ──────────────────────────────────────────────────────── - -console.log("\n=== renderAgentView ==="); - -{ - const now = Date.now(); - const data = makeVisualizerData({ - agentActivity: { - currentUnit: { - type: "execute-task", - id: "M001/S02/T03", - startedAt: now - 60000, - }, - elapsed: 60000, - completedUnits: 8, - totalSlices: 15, - completionRate: 2.4, - active: true, - sessionCost: 1.23, - sessionTokens: 45200, - }, - units: [ - { - type: "execute-task", - id: "M001/S01/T01", - model: "claude-opus-4-6", - startedAt: now - 300000, - finishedAt: now - 240000, - tokens: { - input: 500, - output: 200, - cacheRead: 100, - cacheWrite: 50, - total: 850, - }, - cost: 0.12, - toolCalls: 5, - assistantMessages: 3, - userMessages: 1, - }, - ], - health: { - budgetCeiling: 10, - tokenProfile: "standard", - truncationRate: 15.5, - continueHereRate: 5.0, - tierBreakdown: [], - tierSavingsLine: "", - toolCalls: 20, - assistantMessages: 15, - userMessages: 8, - providers: [], - skillSummary: { - total: 0, - warningCount: 0, - criticalCount: 0, - topIssue: null, - }, - environmentIssues: [], - }, - captures: { entries: [], pendingCount: 3, totalCount: 5 }, - }); - - const lines = renderAgentView(data, mockTheme, 80); - assert.ok(lines.length > 0, "agent view produces output"); - assert.ok( - lines.some((l) => l.includes("ACTIVE")), - "shows active status", - ); - assert.ok( - lines.some((l) => l.includes("Pressure")), - "shows pressure section", - ); - assert.ok( - lines.some((l) => l.includes("15.5%")), - "shows truncation rate", - ); - assert.ok( - lines.some((l) => l.includes("Pending captures: 3")), - "shows pending captures", - ); -} - -{ - const data = makeVisualizerData({ agentActivity: null }); - const lines = renderAgentView(data, mockTheme, 80); - assert.ok( - lines.some((l) => l.includes("No agent activity")), - "shows no-activity message", - ); -} - -// ─── renderChangelogView ──────────────────────────────────────────────────── - -console.log("\n=== renderChangelogView ==="); - -{ - const data = makeVisualizerData({ - changelog: { - entries: [ - { - milestoneId: "M001", - sliceId: "S01", - title: "Core Authentication Setup", - oneLiner: "Added JWT-based auth with refresh token rotation", - filesModified: [ + stats: { + missingCount: 2, + missingSlices: [ + { milestoneId: "M001", sliceId: "S02", title: "State Engine" }, + { milestoneId: "M001", sliceId: "S03", title: "Dashboard" }, + ], + updatedCount: 1, + updatedSlices: [ { - path: "src/auth/jwt.ts", - description: "JWT token generation and validation", + milestoneId: "M001", + sliceId: "S01", + title: "Core Types", + completedAt: "2026-03-15T14:30:00Z", + }, + ], + recentEntries: [ + { + milestoneId: "M001", + sliceId: "S01", + title: "Core Types Infrastructure", + oneLiner: "Core structures assembled", + filesModified: [], + completedAt: "2026-03-15T14:30:00Z", }, ], - completedAt: "2026-03-15T14:30:00Z", }, - ], - }, - sliceVerifications: [ - { - milestoneId: "M001", - sliceId: "S01", - verificationResult: "passed", - blockerDiscovered: false, - keyDecisions: ["Use RS256 for JWT signing"], - patternsEstablished: ["Repository pattern for data access"], - provides: [], - requires: [], - }, - ], - }); + }); - const lines = renderChangelogView(data, mockTheme, 80); - assert.ok( - lines.some((l) => l.includes("M001/S01")), - "shows slice reference", - ); - assert.ok( - lines.some((l) => l.includes("Decisions:")), - "shows decisions section", - ); - assert.ok( - lines.some((l) => l.includes("RS256")), - "shows decision content", - ); - assert.ok( - lines.some((l) => l.includes("Patterns:")), - "shows patterns section", - ); - assert.ok( - lines.some((l) => l.includes("Repository pattern")), - "shows pattern content", - ); -} + const lines = renderProgressView(data, mockTheme, 80); + assert.ok(lines.length > 0, "progress view produces output"); + assert.ok( + lines.some((l) => l.includes("M001")), + "shows milestone M001", + ); + assert.ok( + lines.some((l) => l.includes("S01")), + "shows slice S01", + ); + assert.ok( + lines.some((l) => l.includes("T01")), + "shows task T01 for active slice", + ); + assert.ok( + lines.some((l) => l.includes("M002")), + "shows milestone M002", + ); + assert.ok( + lines.some((l) => l.includes("depends on M001")), + "shows dependency note", + ); + assert.ok( + lines.some((l) => l.includes("30m")), + "shows task estimate", + ); + assert.ok( + lines.some((l) => l.includes("Feature Snapshot")), + "shows stats header", + ); + assert.ok( + lines.some((l) => l.includes("Missing slices")), + "shows missing slices count", + ); + assert.ok( + lines.some((l) => l.includes("State Engine")), + "shows missing slice preview", + ); + assert.ok( + lines.some((l) => l.includes("Updated (last 7 days)")), + "shows updated count", + ); + assert.ok( + lines.some((l) => l.includes("Recent completions")), + "shows recent completions section", + ); + assert.ok( + lines.some((l) => l.includes("Core structures assembled")), + "shows recent one-liner entry", + ); + }); -{ - const data = makeVisualizerData({ changelog: { entries: [] } }); - const lines = renderChangelogView(data, mockTheme, 80); - assert.ok( - lines.some((l) => l.includes("No completed slices")), - "shows empty state", - ); -} - -// ─── renderExportView ─────────────────────────────────────────────────────── - -console.log("\n=== renderExportView ==="); - -{ - const data = makeVisualizerData(); - const lines = renderExportView(data, mockTheme, 80); - assert.ok( - lines.some((l) => l.includes("Export Options")), - "shows export header", - ); - assert.ok( - lines.some((l) => l.includes("[m]")), - "shows markdown option", - ); - assert.ok( - lines.some((l) => l.includes("[j]")), - "shows json option", - ); - assert.ok( - lines.some((l) => l.includes("[s]")), - "shows snapshot option", - ); -} - -// ─── renderKnowledgeView ──────────────────────────────────────────────────── - -console.log("\n=== renderKnowledgeView ==="); - -{ - const data = makeVisualizerData({ - knowledge: { - exists: true, - rules: [ - { id: "K001", scope: "global", content: "Always use transactions" }, - ], - patterns: [{ id: "P001", content: "Repository pattern for DB access" }], - lessons: [{ id: "L001", content: "Cache invalidation needs TTL" }], - }, - }); - - const lines = renderKnowledgeView(data, mockTheme, 80); - assert.ok( - lines.some((l) => l.includes("Rules")), - "shows rules section", - ); - assert.ok( - lines.some((l) => l.includes("K001")), - "shows rule ID", - ); - assert.ok( - lines.some((l) => l.includes("Always use transactions")), - "shows rule content", - ); - assert.ok( - lines.some((l) => l.includes("Patterns")), - "shows patterns section", - ); - assert.ok( - lines.some((l) => l.includes("P001")), - "shows pattern ID", - ); - assert.ok( - lines.some((l) => l.includes("Lessons Learned")), - "shows lessons section", - ); - assert.ok( - lines.some((l) => l.includes("L001")), - "shows lesson ID", - ); -} - -{ - const data = makeVisualizerData({ - knowledge: { exists: false, rules: [], patterns: [], lessons: [] }, - }); - const lines = renderKnowledgeView(data, mockTheme, 80); - assert.ok( - lines.some((l) => l.includes("No KNOWLEDGE.md found")), - "shows no-knowledge message", - ); -} - -// ─── renderCapturesView ───────────────────────────────────────────────────── - -console.log("\n=== renderCapturesView ==="); - -{ - const data = makeVisualizerData({ - captures: { - entries: [ - { - id: "CAP-abc123", - text: "Need to add error handling", - timestamp: "2026-03-15T10:00:00Z", - status: "pending", - classification: "inject", - }, - { - id: "CAP-def456", - text: "Consider caching layer", - timestamp: "2026-03-15T11:00:00Z", - status: "triaged", - classification: "defer", - }, - { - id: "CAP-ghi789", - text: "Fixed typo in config", - timestamp: "2026-03-15T12:00:00Z", - status: "resolved", - classification: "quick-task", - }, - ], - pendingCount: 1, - totalCount: 3, - }, - }); - - const lines = renderCapturesView(data, mockTheme, 80); - assert.ok( - lines.some((l) => l.includes("3") && l.includes("total")), - "shows total count", - ); - assert.ok( - lines.some((l) => l.includes("1") && l.includes("pending")), - "shows pending count", - ); - assert.ok( - lines.some((l) => l.includes("CAP-abc123")), - "shows capture ID", - ); - assert.ok( - lines.some((l) => l.includes("(inject)")), - "shows classification badge", - ); - assert.ok( - lines.some((l) => l.includes("[pending]")), - "shows status badge", - ); -} - -{ - const data = makeVisualizerData({ - captures: { entries: [], pendingCount: 0, totalCount: 0 }, - }); - const lines = renderCapturesView(data, mockTheme, 80); - assert.ok( - lines.some((l) => l.includes("No captures recorded")), - "shows empty state", - ); -} - -// ─── renderHealthView ─────────────────────────────────────────────────────── - -console.log("\n=== renderHealthView ==="); - -{ - const data = makeVisualizerData({ - totals: { - units: 10, - tokens: { - input: 5000, - output: 2000, - cacheRead: 1000, - cacheWrite: 500, - total: 8500, - }, - cost: 5.0, - duration: 120000, - toolCalls: 50, - assistantMessages: 30, - userMessages: 15, - totalTruncationSections: 3, - continueHereFiredCount: 1, - apiRequests: 30, - }, - health: { - budgetCeiling: 20.0, - tokenProfile: "standard", - truncationRate: 30.0, - continueHereRate: 10.0, - tierBreakdown: [ - { - tier: "standard", - units: 7, - tokens: { - input: 3500, - output: 1400, - cacheRead: 700, - cacheWrite: 350, - total: 5950, + it("renders discussion status", () => { + const data = makeVisualizerData({ + discussion: [ + { + milestoneId: "M001", + title: "First Milestone", + state: "discussed", + hasContext: true, + hasDraft: false, + lastUpdated: "2026-03-15T14:30:00Z", }, - cost: 3.5, - downgraded: 0, - }, - { - tier: "light", - units: 3, - tokens: { - input: 1500, - output: 600, - cacheRead: 300, - cacheWrite: 150, - total: 2550, + { + milestoneId: "M002", + title: "Plugin Arch", + state: "draft", + hasContext: false, + hasDraft: true, + lastUpdated: "2026-03-16T09:00:00Z", }, - cost: 1.5, - downgraded: 2, + { + milestoneId: "M003", + title: "Next Batch", + state: "undiscussed", + hasContext: false, + hasDraft: false, + lastUpdated: null, + }, + ], + }); + + const lines = renderProgressView(data, mockTheme, 80); + assert.ok( + lines.some((l) => l.includes("Discussion Status")), + "shows discussion section", + ); + assert.ok( + lines.some((l) => l.includes("Discussed: 1")), + "counts discussed milestones", + ); + assert.ok( + lines.some((l) => l.includes("Draft")), + "shows draft badge", + ); + assert.ok( + lines.some((l) => l.includes("Pending")), + "shows pending badge", + ); + }); + + it("renders verification badges", () => { + const data = makeVisualizerData({ + milestones: [ + { + id: "M001", + title: "Test", + status: "active", + dependsOn: [], + slices: [ + { + id: "S01", + title: "Done Slice", + done: true, + active: false, + risk: "low", + depends: [], + tasks: [], + }, + ], + }, + ], + sliceVerifications: [ + { + milestoneId: "M001", + sliceId: "S01", + verificationResult: "passed", + blockerDiscovered: true, + keyDecisions: [], + patternsEstablished: [], + provides: [], + requires: [], + }, + ], + }); + + const lines = renderProgressView(data, mockTheme, 80); + assert.ok( + lines.some((l) => l.includes("S01")), + "shows slice with verification", + ); + }); + + it("renders stats snapshot even when no milestones", () => { + const data = makeVisualizerData({ milestones: [] }); + const lines = renderProgressView(data, mockTheme, 80); + assert.ok( + lines.some((l) => l.includes("Feature Snapshot")), + "shows stats snapshot even when no milestones", + ); + assert.ok( + lines.some((l) => l.includes("Missing slices")), + "reports missing slices count", + ); + }); + + it("renders risk heatmap", () => { + const data = makeVisualizerData({ + milestones: [ + { + id: "M001", + title: "First", + status: "active", + dependsOn: [], + slices: [ + { + id: "S01", + title: "A", + done: true, + active: false, + risk: "low", + depends: [], + tasks: [], + }, + { + id: "S02", + title: "B", + done: false, + active: true, + risk: "high", + depends: [], + tasks: [], + }, + { + id: "S03", + title: "C", + done: false, + active: false, + risk: "medium", + depends: [], + tasks: [], + }, + { + id: "S04", + title: "D", + done: false, + active: false, + risk: "high", + depends: [], + tasks: [], + }, + ], + }, + ], + }); + + const lines = renderProgressView(data, mockTheme, 80); + assert.ok( + lines.some((l) => l.includes("Risk Heatmap")), + "heatmap header present", + ); + assert.ok( + lines.some((l) => l.includes("1 low, 1 med, 2 high")), + "risk summary counts", + ); + assert.ok( + lines.some((l) => l.includes("1 high-risk not started")), + "high-risk not started warning", + ); + }); + + it("supports text and risk filtering", () => { + const data = makeVisualizerData({ + milestones: [ + { + id: "M001", + title: "Auth", + status: "active", + dependsOn: [], + slices: [ + { + id: "S01", + title: "JWT", + done: false, + active: false, + risk: "low", + depends: [], + tasks: [], + }, + { + id: "S02", + title: "OAuth", + done: false, + active: false, + risk: "high", + depends: [], + tasks: [], + }, + ], + }, + { + id: "M002", + title: "Dashboard", + status: "pending", + dependsOn: ["M001"], + slices: [], + }, + ], + }); + + const filtered = renderProgressView(data, mockTheme, 80, { + text: "auth", + field: "all", + }); + assert.ok( + filtered.some((l) => l.includes("M001")), + "filter shows matching milestone", + ); + assert.ok( + filtered.some((l) => l.includes("Filter (all): auth")), + "filter indicator present", + ); + + const riskFiltered = renderProgressView(data, mockTheme, 80, { + text: "high", + field: "risk", + }); + assert.ok( + riskFiltered.some((l) => l.includes("M001")), + "risk filter shows milestone with high-risk slice", + ); + }); + }); + + describe("renderDepsView", () => { + it("renders dependencies and critical path", () => { + const data = makeVisualizerData({ + milestones: [ + { + id: "M001", + title: "First", + status: "active", + dependsOn: [], + slices: [ + { + id: "S01", + title: "A", + done: false, + active: true, + risk: "low", + depends: [], + tasks: [], + }, + { + id: "S02", + title: "B", + done: false, + active: false, + risk: "low", + depends: ["S01"], + tasks: [], + }, + ], + }, + { + id: "M002", + title: "Second", + status: "pending", + dependsOn: ["M001"], + slices: [], + }, + ], + criticalPath: { + milestonePath: ["M001", "M002"], + slicePath: ["S01", "S02"], + milestoneSlack: new Map([ + ["M001", 0], + ["M002", 0], + ]), + sliceSlack: new Map([ + ["S01", 0], + ["S02", 0], + ]), }, - ], - tierSavingsLine: - "Dynamic routing: 2/10 units downgraded (20%), cost: $1.50", - toolCalls: 50, - assistantMessages: 30, - userMessages: 15, - providers: [], - skillSummary: { - total: 0, - warningCount: 0, - criticalCount: 0, - topIssue: null, - }, - environmentIssues: [], - }, + sliceVerifications: [ + { + milestoneId: "M001", + sliceId: "S01", + verificationResult: "passed", + blockerDiscovered: false, + keyDecisions: [], + patternsEstablished: [], + provides: ["api-types"], + requires: [], + }, + ], + }); + + const lines = renderDepsView(data, mockTheme, 80); + assert.ok(lines.length > 0, "deps view produces output"); + assert.ok( + lines.some((l) => l.includes("M001") && l.includes("M002")), + "shows milestone dep edge", + ); + assert.ok( + lines.some((l) => l.includes("S01") && l.includes("S02")), + "shows slice dep edge", + ); + assert.ok( + lines.some((l) => l.includes("Critical Path")), + "shows critical path section", + ); + assert.ok( + lines.some((l) => l.includes("[CRITICAL]")), + "shows CRITICAL badge", + ); + assert.ok( + lines.some((l) => l.includes("Data Flow")), + "shows data flow section", + ); + assert.ok( + lines.some((l) => l.includes("api-types")), + "shows provides artifact", + ); + }); + + it("shows no-deps message when empty", () => { + const data = makeVisualizerData({ + milestones: [ + { + id: "M001", + title: "Only", + status: "active", + dependsOn: [], + slices: [], + }, + ], + }); + + const lines = renderDepsView(data, mockTheme, 80); + assert.ok( + lines.some((l) => l.includes("No milestone dependencies")), + "shows no-deps message", + ); + }); }); - const lines = renderHealthView(data, mockTheme, 80); - assert.ok( - lines.some((l) => l.includes("Budget")), - "shows budget section", - ); - assert.ok( - lines.some((l) => l.includes("Ceiling")), - "shows budget ceiling", - ); - assert.ok( - lines.some((l) => l.includes("$20.00")), - "shows ceiling amount", - ); - assert.ok( - lines.some((l) => l.includes("Pressure")), - "shows pressure section", - ); - assert.ok( - lines.some((l) => l.includes("30.0%")), - "shows truncation rate", - ); - assert.ok( - lines.some((l) => l.includes("Routing")), - "shows routing section", - ); - assert.ok( - lines.some((l) => l.includes("standard")), - "shows tier name", - ); - assert.ok( - lines.some((l) => l.includes("2 downgraded")), - "shows downgraded count", - ); - assert.ok( - lines.some((l) => l.includes("Dynamic routing")), - "shows savings line", - ); - assert.ok( - lines.some((l) => l.includes("Session")), - "shows session section", - ); - assert.ok( - lines.some((l) => l.includes("Tool calls: 50")), - "shows tool calls", - ); -} + describe("renderMetricsView", () => { + it("renders metrics with data", () => { + const data = makeVisualizerData({ + totals: { + units: 5, + tokens: { + input: 1000, + output: 500, + cacheRead: 200, + cacheWrite: 100, + total: 1800, + }, + cost: 2.5, + duration: 60000, + toolCalls: 15, + assistantMessages: 10, + userMessages: 5, + totalTruncationSections: 0, + continueHereFiredCount: 0, + apiRequests: 5, + }, + byPhase: [ + { + phase: "execution", + units: 3, + tokens: { + input: 600, + output: 300, + cacheRead: 100, + cacheWrite: 50, + total: 1050, + }, + cost: 1.5, + duration: 40000, + }, + ], + byModel: [ + { + model: "claude-opus-4-6", + units: 5, + tokens: { + input: 1000, + output: 500, + cacheRead: 200, + cacheWrite: 100, + total: 1800, + }, + cost: 2.5, + }, + ], + byTier: [ + { + tier: "standard", + units: 3, + tokens: { + input: 600, + output: 300, + cacheRead: 100, + cacheWrite: 50, + total: 1050, + }, + cost: 1.5, + downgraded: 0, + }, + { + tier: "light", + units: 2, + tokens: { + input: 400, + output: 200, + cacheRead: 100, + cacheWrite: 50, + total: 750, + }, + cost: 1.0, + downgraded: 1, + }, + ], + tierSavingsLine: "Dynamic routing: 1/5 units downgraded (20%), cost: $1.00", + bySlice: [ + { + sliceId: "M001/S01", + units: 3, + tokens: { + input: 600, + output: 300, + cacheRead: 100, + cacheWrite: 50, + total: 1050, + }, + cost: 1.5, + duration: 40000, + }, + { + sliceId: "M001/S02", + units: 2, + tokens: { + input: 400, + output: 200, + cacheRead: 100, + cacheWrite: 50, + total: 750, + }, + cost: 1.0, + duration: 20000, + }, + ], + remainingSliceCount: 3, + }); -{ - const data = makeVisualizerData({ - health: { - budgetCeiling: undefined, - tokenProfile: "compact", - truncationRate: 0, - continueHereRate: 0, - tierBreakdown: [], - tierSavingsLine: "", - toolCalls: 0, - assistantMessages: 0, - userMessages: 0, - providers: [], - skillSummary: { - total: 0, - warningCount: 0, - criticalCount: 0, - topIssue: null, - }, - environmentIssues: [], - }, + const lines = renderMetricsView(data, mockTheme, 80); + assert.ok(lines.length > 0, "metrics view produces output"); + assert.ok( + lines.some((l) => l.includes("$2.50")), + "shows total cost", + ); + assert.ok( + lines.some((l) => l.includes("execution")), + "shows phase name", + ); + assert.ok( + lines.some((l) => l.includes("claude-opus-4-6")), + "shows model name", + ); + assert.ok( + lines.some((l) => l.includes("By Tier")), + "shows tier breakdown section", + ); + assert.ok( + lines.some((l) => l.includes("standard")), + "shows tier name", + ); + assert.ok( + lines.some((l) => l.includes("Dynamic routing")), + "shows tier savings line", + ); + assert.ok( + lines.some((l) => l.includes("Tools: 15")), + "shows tool call count", + ); + assert.ok( + lines.some((l) => l.includes("10") && l.includes("sent")), + "shows message counts", + ); + }); + + it("shows no-data message when totals is null", () => { + const data = makeVisualizerData({ totals: null }); + const lines = renderMetricsView(data, mockTheme, 80); + assert.ok( + lines.some((l) => l.includes("No metrics data")), + "shows no-data message", + ); + }); }); - const lines = renderHealthView(data, mockTheme, 80); - assert.ok( - lines.some((l) => l.includes("No budget ceiling set")), - "shows no-ceiling message", - ); - assert.ok( - lines.some((l) => l.includes("compact")), - "shows token profile", - ); -} + describe("renderTimelineView", () => { + it("renders timeline with units", () => { + const now = Date.now(); + const data = makeVisualizerData({ + units: [ + { + type: "execute-task", + id: "M001/S01/T01", + model: "claude-opus-4-6", + startedAt: now - 120000, + finishedAt: now - 60000, + tokens: { + input: 500, + output: 200, + cacheRead: 100, + cacheWrite: 50, + total: 850, + }, + cost: 0.42, + toolCalls: 5, + assistantMessages: 3, + userMessages: 1, + tier: "standard", + }, + ], + }); -// ─── Report ───────────────────────────────────────────────────────────────── + const listLines = renderTimelineView(data, mockTheme, 80); + assert.ok(listLines.length >= 1, "list view produces lines"); + assert.ok( + listLines.some((l) => l.includes("execute-task")), + "shows unit type", + ); + assert.ok( + listLines.some((l) => l.includes("[standard]")), + "shows tier in timeline", + ); + assert.ok( + listLines.some((l) => l.includes("opus-4-6")), + "shows shortened model", + ); + }); + + it("shows empty message when no units", () => { + const data = makeVisualizerData({ units: [] }); + const lines = renderTimelineView(data, mockTheme, 80); + assert.ok( + lines.some((l) => l.includes("No execution history")), + "shows empty message", + ); + }); + }); + + describe("renderAgentView", () => { + it("renders agent activity", () => { + const now = Date.now(); + const data = makeVisualizerData({ + agentActivity: { + currentUnit: { + type: "execute-task", + id: "M001/S02/T03", + startedAt: now - 60000, + }, + elapsed: 60000, + completedUnits: 8, + totalSlices: 15, + completionRate: 2.4, + active: true, + sessionCost: 1.23, + sessionTokens: 45200, + }, + units: [ + { + type: "execute-task", + id: "M001/S01/T01", + model: "claude-opus-4-6", + startedAt: now - 300000, + finishedAt: now - 240000, + tokens: { + input: 500, + output: 200, + cacheRead: 100, + cacheWrite: 50, + total: 850, + }, + cost: 0.12, + toolCalls: 5, + assistantMessages: 3, + userMessages: 1, + }, + ], + health: { + budgetCeiling: 10, + tokenProfile: "standard", + truncationRate: 15.5, + continueHereRate: 5.0, + tierBreakdown: [], + tierSavingsLine: "", + toolCalls: 20, + assistantMessages: 15, + userMessages: 8, + providers: [], + skillSummary: { + total: 0, + warningCount: 0, + criticalCount: 0, + topIssue: null, + }, + environmentIssues: [], + }, + captures: { entries: [], pendingCount: 3, totalCount: 5 }, + }); + + const lines = renderAgentView(data, mockTheme, 80); + assert.ok(lines.length > 0, "agent view produces output"); + assert.ok( + lines.some((l) => l.includes("ACTIVE")), + "shows active status", + ); + assert.ok( + lines.some((l) => l.includes("Pressure")), + "shows pressure section", + ); + assert.ok( + lines.some((l) => l.includes("15.5%")), + "shows truncation rate", + ); + assert.ok( + lines.some((l) => l.includes("Pending captures: 3")), + "shows pending captures", + ); + }); + + it("shows no-activity message when agentActivity is null", () => { + const data = makeVisualizerData({ agentActivity: null }); + const lines = renderAgentView(data, mockTheme, 80); + assert.ok( + lines.some((l) => l.includes("No agent activity")), + "shows no-activity message", + ); + }); + }); + + describe("renderChangelogView", () => { + it("renders changelog with entries", () => { + const data = makeVisualizerData({ + changelog: { + entries: [ + { + milestoneId: "M001", + sliceId: "S01", + title: "Core Authentication Setup", + oneLiner: "Added JWT-based auth with refresh token rotation", + filesModified: [ + { + path: "src/auth/jwt.ts", + description: "JWT token generation and validation", + }, + ], + completedAt: "2026-03-15T14:30:00Z", + }, + ], + }, + sliceVerifications: [ + { + milestoneId: "M001", + sliceId: "S01", + verificationResult: "passed", + blockerDiscovered: false, + keyDecisions: ["Use RS256 for JWT signing"], + patternsEstablished: ["Repository pattern for data access"], + provides: [], + requires: [], + }, + ], + }); + + const lines = renderChangelogView(data, mockTheme, 80); + assert.ok( + lines.some((l) => l.includes("M001/S01")), + "shows slice reference", + ); + assert.ok( + lines.some((l) => l.includes("Decisions:")), + "shows decisions section", + ); + assert.ok( + lines.some((l) => l.includes("RS256")), + "shows decision content", + ); + assert.ok( + lines.some((l) => l.includes("Patterns:")), + "shows patterns section", + ); + assert.ok( + lines.some((l) => l.includes("Repository pattern")), + "shows pattern content", + ); + }); + + it("shows empty state when no entries", () => { + const data = makeVisualizerData({ changelog: { entries: [] } }); + const lines = renderChangelogView(data, mockTheme, 80); + assert.ok( + lines.some((l) => l.includes("No completed slices")), + "shows empty state", + ); + }); + }); + + describe("renderExportView", () => { + it("renders export options", () => { + const data = makeVisualizerData(); + const lines = renderExportView(data, mockTheme, 80); + assert.ok( + lines.some((l) => l.includes("Export Options")), + "shows export header", + ); + assert.ok( + lines.some((l) => l.includes("[m]")), + "shows markdown option", + ); + assert.ok( + lines.some((l) => l.includes("[j]")), + "shows json option", + ); + assert.ok( + lines.some((l) => l.includes("[s]")), + "shows snapshot option", + ); + }); + }); + + describe("renderKnowledgeView", () => { + it("renders knowledge sections", () => { + const data = makeVisualizerData({ + knowledge: { + exists: true, + rules: [ + { id: "K001", scope: "global", content: "Always use transactions" }, + ], + patterns: [{ id: "P001", content: "Repository pattern for DB access" }], + lessons: [{ id: "L001", content: "Cache invalidation needs TTL" }], + }, + }); + + const lines = renderKnowledgeView(data, mockTheme, 80); + assert.ok( + lines.some((l) => l.includes("Rules")), + "shows rules section", + ); + assert.ok( + lines.some((l) => l.includes("K001")), + "shows rule ID", + ); + assert.ok( + lines.some((l) => l.includes("Always use transactions")), + "shows rule content", + ); + assert.ok( + lines.some((l) => l.includes("Patterns")), + "shows patterns section", + ); + assert.ok( + lines.some((l) => l.includes("P001")), + "shows pattern ID", + ); + assert.ok( + lines.some((l) => l.includes("Lessons Learned")), + "shows lessons section", + ); + assert.ok( + lines.some((l) => l.includes("L001")), + "shows lesson ID", + ); + }); + + it("shows no-knowledge message when knowledge does not exist", () => { + const data = makeVisualizerData({ + knowledge: { exists: false, rules: [], patterns: [], lessons: [] }, + }); + const lines = renderKnowledgeView(data, mockTheme, 80); + assert.ok( + lines.some((l) => l.includes("No KNOWLEDGE.md found")), + "shows no-knowledge message", + ); + }); + }); + + describe("renderCapturesView", () => { + it("renders captures with entries", () => { + const data = makeVisualizerData({ + captures: { + entries: [ + { + id: "CAP-abc123", + text: "Need to add error handling", + timestamp: "2026-03-15T10:00:00Z", + status: "pending", + classification: "inject", + }, + { + id: "CAP-def456", + text: "Consider caching layer", + timestamp: "2026-03-15T11:00:00Z", + status: "triaged", + classification: "defer", + }, + { + id: "CAP-ghi789", + text: "Fixed typo in config", + timestamp: "2026-03-15T12:00:00Z", + status: "resolved", + classification: "quick-task", + }, + ], + pendingCount: 1, + totalCount: 3, + }, + }); + + const lines = renderCapturesView(data, mockTheme, 80); + assert.ok( + lines.some((l) => l.includes("3") && l.includes("total")), + "shows total count", + ); + assert.ok( + lines.some((l) => l.includes("1") && l.includes("pending")), + "shows pending count", + ); + assert.ok( + lines.some((l) => l.includes("CAP-abc123")), + "shows capture ID", + ); + assert.ok( + lines.some((l) => l.includes("(inject)")), + "shows classification badge", + ); + assert.ok( + lines.some((l) => l.includes("[pending]")), + "shows status badge", + ); + }); + + it("shows empty state when no captures", () => { + const data = makeVisualizerData({ + captures: { entries: [], pendingCount: 0, totalCount: 0 }, + }); + const lines = renderCapturesView(data, mockTheme, 80); + assert.ok( + lines.some((l) => l.includes("No captures recorded")), + "shows empty state", + ); + }); + }); + + describe("renderHealthView", () => { + it("renders health with data", () => { + const data = makeVisualizerData({ + totals: { + units: 10, + tokens: { + input: 5000, + output: 2000, + cacheRead: 1000, + cacheWrite: 500, + total: 8500, + }, + cost: 5.0, + duration: 120000, + toolCalls: 50, + assistantMessages: 30, + userMessages: 15, + totalTruncationSections: 3, + continueHereFiredCount: 1, + apiRequests: 30, + }, + health: { + budgetCeiling: 20.0, + tokenProfile: "standard", + truncationRate: 30.0, + continueHereRate: 10.0, + tierBreakdown: [ + { + tier: "standard", + units: 7, + tokens: { + input: 3500, + output: 1400, + cacheRead: 700, + cacheWrite: 350, + total: 5950, + }, + cost: 3.5, + downgraded: 0, + }, + { + tier: "light", + units: 3, + tokens: { + input: 1500, + output: 600, + cacheRead: 300, + cacheWrite: 150, + total: 2550, + }, + cost: 1.5, + downgraded: 2, + }, + ], + tierSavingsLine: + "Dynamic routing: 2/10 units downgraded (20%), cost: $1.50", + toolCalls: 50, + assistantMessages: 30, + userMessages: 15, + providers: [], + skillSummary: { + total: 0, + warningCount: 0, + criticalCount: 0, + topIssue: null, + }, + environmentIssues: [], + }, + }); + + const lines = renderHealthView(data, mockTheme, 80); + assert.ok( + lines.some((l) => l.includes("Budget")), + "shows budget section", + ); + assert.ok( + lines.some((l) => l.includes("Ceiling")), + "shows budget ceiling", + ); + assert.ok( + lines.some((l) => l.includes("$20.00")), + "shows ceiling amount", + ); + assert.ok( + lines.some((l) => l.includes("Pressure")), + "shows pressure section", + ); + assert.ok( + lines.some((l) => l.includes("30.0%")), + "shows truncation rate", + ); + assert.ok( + lines.some((l) => l.includes("Routing")), + "shows routing section", + ); + assert.ok( + lines.some((l) => l.includes("standard")), + "shows tier name", + ); + assert.ok( + lines.some((l) => l.includes("2 downgraded")), + "shows downgraded count", + ); + assert.ok( + lines.some((l) => l.includes("Dynamic routing")), + "shows savings line", + ); + assert.ok( + lines.some((l) => l.includes("Session")), + "shows session section", + ); + assert.ok( + lines.some((l) => l.includes("Tool calls: 50")), + "shows tool calls", + ); + }); + + it("shows no-ceiling message and token profile", () => { + const data = makeVisualizerData({ + health: { + budgetCeiling: undefined, + tokenProfile: "compact", + truncationRate: 0, + continueHereRate: 0, + tierBreakdown: [], + tierSavingsLine: "", + toolCalls: 0, + assistantMessages: 0, + userMessages: 0, + providers: [], + skillSummary: { + total: 0, + warningCount: 0, + criticalCount: 0, + topIssue: null, + }, + environmentIssues: [], + }, + }); + + const lines = renderHealthView(data, mockTheme, 80); + assert.ok( + lines.some((l) => l.includes("No budget ceiling set")), + "shows no-ceiling message", + ); + assert.ok( + lines.some((l) => l.includes("compact")), + "shows token profile", + ); + }); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts index 3a1a7d269..e8ad68e6e 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -25,10 +25,7 @@ export default defineConfig({ // runner and must be excluded here too to avoid "No test suite found" errors. exclude: [ // Standalone script-style tests (no describe/test, custom assertEq) - "src/resources/extensions/sf/tests/derive-state-draft.test.ts", - "src/resources/extensions/sf/tests/phases-merge-error-stops-auto.test.ts", - "src/resources/extensions/sf/tests/tool-call-loop-guard.test.ts", - "src/resources/extensions/sf/tests/visualizer-views.test.ts", + // (converted to vitest describe/it style) "src/tests/integration/ci_monitor.test.ts", "src/resources/extensions/vectordrive/tests/manager.test.ts", "src/resources/extensions/voice/tests/linux-ready.test.ts",