import { describe, it, expect } from 'vitest'; import { analyzeSession } from '@renderer/utils/sessionAnalyzer'; import type { ParsedMessage, Session, SessionDetail, SessionMetrics, Process } from '@shared/types'; // ============================================================================= // Test Helpers // ============================================================================= let msgCounter = 0; function createMockMessage(overrides: Partial = {}): ParsedMessage { msgCounter++; return { uuid: `uuid-${msgCounter}`, parentUuid: `uuid-${msgCounter - 1}`, type: 'assistant', timestamp: new Date('2024-01-01T10:00:00Z'), content: '', isSidechain: false, isMeta: false, toolCalls: [], toolResults: [], ...overrides, }; } function createMockSession(overrides: Partial = {}): Session { return { id: 'test-session', projectId: 'test-project', projectPath: '/test/path', createdAt: Date.now(), hasSubagents: false, messageCount: 0, ...overrides, }; } function createMockMetrics(overrides: Partial = {}): SessionMetrics { return { durationMs: 0, totalTokens: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, messageCount: 0, ...overrides, }; } function createMockDetail(overrides: Partial = {}): SessionDetail { return { session: createMockSession(), messages: [], chunks: [], processes: [], metrics: createMockMetrics(), ...overrides, }; } // ============================================================================= // Tests // ============================================================================= describe('analyzeSession', () => { beforeEach(() => { msgCounter = 0; }); // ------------------------------------------------------------------------- // 1. Empty session // ------------------------------------------------------------------------- describe('empty session', () => { it('returns a zeroed report with correct structure', () => { const report = analyzeSession(createMockDetail()); expect(report.overview.sessionId).toBe('test-session'); expect(report.overview.totalMessages).toBe(0); expect(report.overview.durationSeconds).toBe(0); expect(report.tokenUsage.totals.grandTotal).toBe(0); expect(report.tokenUsage.totals.inputTokens).toBe(0); expect(report.tokenUsage.totals.outputTokens).toBe(0); expect(report.costAnalysis.totalSessionCostUsd).toBe(0); expect(report.toolUsage.totalCalls).toBe(0); expect(report.toolUsage.counts).toEqual({}); expect(report.errors.errors).toHaveLength(0); expect(report.errors.permissionDenials.count).toBe(0); expect(report.frictionSignals.correctionCount).toBe(0); expect(report.frictionSignals.corrections).toHaveLength(0); expect(report.gitActivity.commitCount).toBe(0); expect(report.gitActivity.pushCount).toBe(0); expect(report.idleAnalysis.idleGapCount).toBe(0); expect(report.modelSwitches.count).toBe(0); expect(report.modelSwitches.switches).toHaveLength(0); expect(report.conversationTree.maxDepth).toBe(0); expect(report.conversationTree.totalNodes).toBe(0); expect(report.tokenDensityTimeline.quartiles).toHaveLength(4); expect(report.tokenDensityTimeline.quartiles.every((q) => q.avgTokens === 0)).toBe(true); expect(report.compaction.count).toBe(0); expect(report.compaction.compactSummaryCount).toBe(0); expect(report.gitBranches).toEqual([]); // New sections expect(report.skillsInvoked).toEqual([]); expect(report.bashCommands.total).toBe(0); expect(report.lifecycleTasks).toEqual([]); expect(report.userQuestions).toEqual([]); expect(report.outOfScopeFindings).toEqual([]); expect(report.agentTree.agentCount).toBe(0); expect(report.subagentsList).toEqual([]); }); }); // ------------------------------------------------------------------------- // 2. Basic session with usage data // ------------------------------------------------------------------------- describe('basic session', () => { it('computes overview, token totals, and cost', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'user', isMeta: false, content: 'Hello world', timestamp: new Date('2024-01-01T10:00:00Z'), }), createMockMessage({ type: 'assistant', model: 'claude-sonnet-4-20250514', timestamp: new Date('2024-01-01T10:01:00Z'), content: [{ type: 'text' as const, text: 'Hi there!' }], usage: { input_tokens: 1000, output_tokens: 500, cache_read_input_tokens: 200, cache_creation_input_tokens: 100, }, }), createMockMessage({ type: 'user', isMeta: false, content: 'Follow up', timestamp: new Date('2024-01-01T10:02:00Z'), }), createMockMessage({ type: 'assistant', model: 'claude-sonnet-4-20250514', timestamp: new Date('2024-01-01T10:03:00Z'), content: [{ type: 'text' as const, text: 'Sure thing.' }], usage: { input_tokens: 1500, output_tokens: 300, cache_read_input_tokens: 400, cache_creation_input_tokens: 0, }, }), ]; const report = analyzeSession( createMockDetail({ messages, session: createMockSession({ messageCount: 4 }), }) ); // Overview expect(report.overview.totalMessages).toBe(4); expect(report.overview.durationSeconds).toBe(180); // 3 minutes expect(report.overview.durationHuman).toBe('3:00'); // Token totals expect(report.tokenUsage.totals.inputTokens).toBe(2500); expect(report.tokenUsage.totals.outputTokens).toBe(800); expect(report.tokenUsage.totals.cacheRead).toBe(600); expect(report.tokenUsage.totals.cacheCreation).toBe(100); expect(report.tokenUsage.totals.grandTotal).toBe(4000); // Cost should be positive (sonnet-4 pricing) expect(report.costAnalysis.parentCostUsd).toBeGreaterThan(0); expect(report.costAnalysis.totalSessionCostUsd).toBeGreaterThan(0); // Message types expect(report.messageTypes.user).toBe(2); expect(report.messageTypes.assistant).toBe(2); }); }); // ------------------------------------------------------------------------- // 3. Tool usage // ------------------------------------------------------------------------- describe('tool usage', () => { it('counts tool calls and computes totalCalls', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', model: 'claude-sonnet-4-20250514', toolCalls: [ { id: 'tc-1', name: 'Read', input: { file_path: '/foo.ts' }, isTask: false }, { id: 'tc-2', name: 'Bash', input: { command: 'ls' }, isTask: false }, ], }), createMockMessage({ type: 'user', isMeta: true, content: [ { type: 'tool_result' as const, tool_use_id: 'tc-1', content: 'file contents', is_error: false, }, { type: 'tool_result' as const, tool_use_id: 'tc-2', content: 'output', is_error: false, }, ], toolResults: [ { toolUseId: 'tc-1', content: 'file contents', isError: false }, { toolUseId: 'tc-2', content: 'output', isError: false }, ], }), createMockMessage({ type: 'assistant', model: 'claude-sonnet-4-20250514', toolCalls: [{ id: 'tc-3', name: 'Read', input: { file_path: '/bar.ts' }, isTask: false }], }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.toolUsage.totalCalls).toBe(3); expect(report.toolUsage.counts.Read).toBe(2); expect(report.toolUsage.counts.Bash).toBe(1); }); }); // ------------------------------------------------------------------------- // 4. Error detection // ------------------------------------------------------------------------- describe('error detection', () => { it('collects tool errors from isError results', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', toolCalls: [ { id: 'tc-1', name: 'Read', input: { file_path: '/missing.ts' }, isTask: false }, ], }), createMockMessage({ type: 'user', isMeta: true, content: [], toolResults: [ { toolUseId: 'tc-1', content: 'ENOENT: no such file or directory', isError: true }, ], }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.errors.errors).toHaveLength(1); expect(report.errors.errors[0].tool).toBe('Read'); expect(report.errors.errors[0].error).toContain('ENOENT'); expect(report.errors.errors[0].isPermissionDenial).toBe(false); }); it('detects Bash non-zero exit codes as errors', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', toolCalls: [{ id: 'tc-1', name: 'Bash', input: { command: 'false' }, isTask: false }], }), createMockMessage({ type: 'user', isMeta: true, content: [], toolResults: [ { toolUseId: 'tc-1', content: 'Exit code 1\nCommand failed', isError: false }, ], }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.errors.errors).toHaveLength(1); expect(report.errors.errors[0].tool).toBe('Bash (non-zero exit)'); }); }); // ------------------------------------------------------------------------- // 5. Permission denial // ------------------------------------------------------------------------- describe('permission denial', () => { it('flags errors containing permission keywords', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', toolCalls: [ { id: 'tc-1', name: 'Bash', input: { command: 'rm /root/file' }, isTask: false }, ], }), createMockMessage({ type: 'user', isMeta: true, content: [], toolResults: [{ toolUseId: 'tc-1', content: 'Error: permission denied', isError: true }], }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.errors.permissionDenials.count).toBe(1); expect(report.errors.permissionDenials.denials[0].isPermissionDenial).toBe(true); expect(report.errors.permissionDenials.affectedTools).toContain('Bash'); }); it('detects permission denial in Bash non-zero exit', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', toolCalls: [ { id: 'tc-1', name: 'Bash', input: { command: 'cat /etc/shadow' }, isTask: false }, ], }), createMockMessage({ type: 'user', isMeta: true, content: [], toolResults: [ { toolUseId: 'tc-1', content: 'Exit code 1\ncat: /etc/shadow: Operation not permitted', isError: false, }, ], }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.errors.permissionDenials.count).toBe(1); }); }); // ------------------------------------------------------------------------- // 6. Friction detection // ------------------------------------------------------------------------- describe('friction detection', () => { it('detects friction keywords in user messages', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'user', isMeta: false, content: 'Build the login page', timestamp: new Date('2024-01-01T10:00:00Z'), }), createMockMessage({ type: 'assistant', content: [{ type: 'text' as const, text: 'Done.' }], timestamp: new Date('2024-01-01T10:01:00Z'), }), createMockMessage({ type: 'user', isMeta: false, content: 'No, that is wrong. Use React.', timestamp: new Date('2024-01-01T10:02:00Z'), }), createMockMessage({ type: 'assistant', content: [{ type: 'text' as const, text: 'Updated.' }], timestamp: new Date('2024-01-01T10:03:00Z'), }), createMockMessage({ type: 'user', isMeta: false, content: 'Actually, use Next.js instead', timestamp: new Date('2024-01-01T10:04:00Z'), }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.frictionSignals.correctionCount).toBe(2); expect(report.frictionSignals.corrections).toHaveLength(2); const keywords = report.frictionSignals.corrections.map((c) => c.keyword); // "No," matches 'no,' and "actually" matches 'actually' expect(keywords).toContain('no,'); expect(keywords).toContain('actually'); // Friction rate = 2 corrections / 3 user messages expect(report.frictionSignals.frictionRate).toBeCloseTo(2 / 3, 2); }); it('does not count isMeta user messages as friction', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'user', isMeta: true, content: 'No, wrong, actually this is meta', }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.frictionSignals.correctionCount).toBe(0); }); }); // ------------------------------------------------------------------------- // 7. Git activity // ------------------------------------------------------------------------- describe('git activity', () => { it('detects git commits from Bash tool calls', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', toolCalls: [ { id: 'tc-1', name: 'Bash', input: { command: "git commit -m 'initial commit'" }, isTask: false, }, ], }), createMockMessage({ type: 'assistant', toolCalls: [ { id: 'tc-2', name: 'Bash', input: { command: "git commit -m 'add feature'" }, isTask: false, }, ], }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.gitActivity.commitCount).toBe(2); expect(report.gitActivity.commits).toHaveLength(2); expect(report.gitActivity.commits[0].messagePreview).toContain('initial commit'); expect(report.gitActivity.commits[1].messagePreview).toContain('add feature'); }); it('detects git push and branch creation', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', toolCalls: [ { id: 'tc-1', name: 'Bash', input: { command: 'git checkout -b feat/new-branch' }, isTask: false, }, ], }), createMockMessage({ type: 'assistant', toolCalls: [ { id: 'tc-2', name: 'Bash', input: { command: 'git push -u origin feat/new-branch' }, isTask: false, }, ], }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.gitActivity.pushCount).toBe(1); expect(report.gitActivity.branchCreations).toContain('feat/new-branch'); }); }); // ------------------------------------------------------------------------- // 8. Idle gaps // ------------------------------------------------------------------------- describe('idle gaps', () => { it('detects idle gaps >60s between assistant and next user message', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', timestamp: new Date('2024-01-01T10:00:00Z'), }), // 2 minutes later createMockMessage({ type: 'user', isMeta: false, content: 'back now', timestamp: new Date('2024-01-01T10:02:00Z'), }), createMockMessage({ type: 'assistant', timestamp: new Date('2024-01-01T10:02:30Z'), }), // 30 seconds - no idle gap createMockMessage({ type: 'user', isMeta: false, content: 'quick reply', timestamp: new Date('2024-01-01T10:03:00Z'), }), createMockMessage({ type: 'assistant', timestamp: new Date('2024-01-01T10:03:30Z'), }), // 5 minutes later createMockMessage({ type: 'user', isMeta: false, content: 'took a break', timestamp: new Date('2024-01-01T10:08:30Z'), }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.idleAnalysis.idleGapCount).toBe(2); expect(report.idleAnalysis.totalIdleSeconds).toBeGreaterThan(0); expect(report.idleAnalysis.idlePct).toBeGreaterThan(0); // First gap: 120s, second gap: 300s const gapSeconds = report.idleAnalysis.longestGaps.map((g) => g.gapSeconds); expect(gapSeconds).toContain(120); expect(gapSeconds).toContain(300); }); it('reports zero idle for no gaps', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', timestamp: new Date('2024-01-01T10:00:00Z'), }), createMockMessage({ type: 'user', content: 'quick', timestamp: new Date('2024-01-01T10:00:30Z'), }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.idleAnalysis.idleGapCount).toBe(0); expect(report.idleAnalysis.totalIdleSeconds).toBe(0); }); }); // ------------------------------------------------------------------------- // 9. Model switches // ------------------------------------------------------------------------- describe('model switches', () => { it('detects switches between different model names', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', model: 'claude-sonnet-4-20250514', timestamp: new Date('2024-01-01T10:00:00Z'), }), createMockMessage({ type: 'assistant', model: 'claude-sonnet-4-20250514', timestamp: new Date('2024-01-01T10:01:00Z'), }), createMockMessage({ type: 'assistant', model: 'claude-opus-4-20250514', timestamp: new Date('2024-01-01T10:02:00Z'), }), createMockMessage({ type: 'assistant', model: 'claude-haiku-4-20250514', timestamp: new Date('2024-01-01T10:03:00Z'), }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.modelSwitches.count).toBe(2); expect(report.modelSwitches.switches[0].from).toBe('claude-sonnet-4-20250514'); expect(report.modelSwitches.switches[0].to).toBe('claude-opus-4-20250514'); expect(report.modelSwitches.switches[1].from).toBe('claude-opus-4-20250514'); expect(report.modelSwitches.switches[1].to).toBe('claude-haiku-4-20250514'); expect(report.modelSwitches.modelsUsed).toContain('claude-sonnet-4-20250514'); expect(report.modelSwitches.modelsUsed).toContain('claude-opus-4-20250514'); expect(report.modelSwitches.modelsUsed).toContain('claude-haiku-4-20250514'); }); it('reports zero switches for single model', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', model: 'claude-sonnet-4-20250514', usage: { input_tokens: 100, output_tokens: 50 }, }), createMockMessage({ type: 'assistant', model: 'claude-sonnet-4-20250514', usage: { input_tokens: 100, output_tokens: 50 }, }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.modelSwitches.count).toBe(0); // modelsUsed falls back to modelStats keys when no switches expect(report.modelSwitches.modelsUsed).toHaveLength(1); expect(report.modelSwitches.modelsUsed[0]).toBe('claude-sonnet-4-20250514'); }); }); // ------------------------------------------------------------------------- // 10. Conversation tree // ------------------------------------------------------------------------- describe('conversation tree', () => { it('computes maxDepth from uuid/parentUuid chains', () => { const messages: ParsedMessage[] = [ createMockMessage({ uuid: 'root', parentUuid: null }), createMockMessage({ uuid: 'child-1', parentUuid: 'root' }), createMockMessage({ uuid: 'child-2', parentUuid: 'child-1' }), createMockMessage({ uuid: 'child-3', parentUuid: 'child-2' }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.conversationTree.totalNodes).toBe(4); expect(report.conversationTree.maxDepth).toBe(3); // root(0)->child1(1)->child2(2)->child3(3) }); it('detects branch points (multiple children)', () => { const messages: ParsedMessage[] = [ createMockMessage({ uuid: 'root', parentUuid: null }), createMockMessage({ uuid: 'branch-a', parentUuid: 'root' }), createMockMessage({ uuid: 'branch-b', parentUuid: 'root' }), createMockMessage({ uuid: 'branch-c', parentUuid: 'root' }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.conversationTree.branchPoints).toBe(1); expect(report.conversationTree.branchDetails).toHaveLength(1); expect(report.conversationTree.branchDetails[0].childCount).toBe(3); }); it('counts sidechains', () => { const messages: ParsedMessage[] = [ createMockMessage({ uuid: 'root', parentUuid: null, isSidechain: false }), createMockMessage({ uuid: 'side-1', parentUuid: 'root', isSidechain: true }), createMockMessage({ uuid: 'side-2', parentUuid: 'root', isSidechain: true }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.conversationTree.sidechainCount).toBe(2); }); }); // ------------------------------------------------------------------------- // Additional coverage // ------------------------------------------------------------------------- describe('context consumption assessment', () => { it('assesses healthy context consumption', () => { const report = analyzeSession( createMockDetail({ session: createMockSession({ contextConsumption: 0.3 }), }) ); expect(report.overview.contextAssessment).toBe('healthy'); expect(report.overview.contextConsumptionPct).toBe(30); }); it('assesses critical context consumption', () => { const report = analyzeSession( createMockDetail({ session: createMockSession({ contextConsumption: 0.85 }), }) ); expect(report.overview.contextAssessment).toBe('critical'); }); }); describe('cache economics', () => { it('detects cold start when first assistant has cache creation but no reads', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', model: 'claude-sonnet-4-20250514', usage: { input_tokens: 100, output_tokens: 50, cache_creation_input_tokens: 500, cache_read_input_tokens: 0, }, }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.cacheEconomics.coldStartDetected).toBe(true); }); it('computes cache efficiency percentage', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', model: 'claude-sonnet-4-20250514', usage: { input_tokens: 100, output_tokens: 50, cache_creation_input_tokens: 200, cache_read_input_tokens: 800, }, }), ]; const report = analyzeSession(createMockDetail({ messages })); // efficiency = 800 / (200 + 800) * 100 = 80% expect(report.cacheEconomics.cacheEfficiencyPct).toBe(80); expect(report.cacheEconomics.cacheReadToWriteRatio).toBe(4); }); }); describe('file read redundancy', () => { it('tracks redundant file reads', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', toolCalls: [ { id: 'tc-1', name: 'Read', input: { file_path: '/foo.ts' }, isTask: false }, { id: 'tc-2', name: 'Read', input: { file_path: '/foo.ts' }, isTask: false }, { id: 'tc-3', name: 'Read', input: { file_path: '/foo.ts' }, isTask: false }, { id: 'tc-4', name: 'Read', input: { file_path: '/bar.ts' }, isTask: false }, ], }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.fileReadRedundancy.totalReads).toBe(4); expect(report.fileReadRedundancy.uniqueFiles).toBe(2); expect(report.fileReadRedundancy.redundantFiles['/foo.ts']).toBe(3); expect(report.fileReadRedundancy.redundantFiles['/bar.ts']).toBeUndefined(); // only 1 read, threshold is >2 }); }); describe('prompt quality', () => { it('assesses well_specified when few corrections', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'user', isMeta: false, content: 'Build me a React login component with form validation and error states', }), createMockMessage({ type: 'assistant', content: [{ type: 'text' as const, text: 'Done.' }], }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.promptQuality.assessment).toBe('well_specified'); }); it('assesses underspecified when short prompt and many corrections', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'user', isMeta: false, content: 'Fix the bug', timestamp: new Date('2024-01-01T10:00:00Z'), }), createMockMessage({ type: 'assistant', content: [{ type: 'text' as const, text: 'Done.' }], timestamp: new Date('2024-01-01T10:01:00Z'), }), createMockMessage({ type: 'user', isMeta: false, content: 'No, wrong file', timestamp: new Date('2024-01-01T10:02:00Z'), }), createMockMessage({ type: 'assistant', content: [{ type: 'text' as const, text: 'Updated.' }], timestamp: new Date('2024-01-01T10:03:00Z'), }), createMockMessage({ type: 'user', isMeta: false, content: 'Actually the other module', timestamp: new Date('2024-01-01T10:04:00Z'), }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.promptQuality.assessment).toBe('underspecified'); expect(report.promptQuality.firstMessageLengthChars).toBe('Fix the bug'.length); }); }); describe('subagent metrics from processes', () => { it('computes subagent summary from detail.processes', () => { const processes: Process[] = [ { id: 'agent-1', filePath: '/path/to/agent-1.jsonl', messages: [ createMockMessage({ toolCalls: [ { id: 'tc-1', name: 'Read', input: {}, isTask: false }, { id: 'tc-2', name: 'Edit', input: {}, isTask: false }, ], }), ], startTime: new Date('2024-01-01T10:00:00Z'), endTime: new Date('2024-01-01T10:01:00Z'), durationMs: 60000, metrics: createMockMetrics({ totalTokens: 5000, costUsd: 0.05 }), description: 'Refactor module', subagentType: 'code', isParallel: false, }, ]; const report = analyzeSession(createMockDetail({ processes })); expect(report.subagentMetrics.count).toBe(1); expect(report.subagentMetrics.totalTokens).toBe(5000); expect(report.subagentMetrics.totalToolUseCount).toBe(2); expect(report.subagentMetrics.byAgent[0].description).toBe('Refactor module'); }); }); describe('thinking blocks', () => { it('counts thinking blocks and analyzes signals', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', content: [ { type: 'thinking' as const, thinking: 'Let me think about an alternative approach. Actually, I should reconsider.', signature: 'sig-1', }, { type: 'text' as const, text: 'Here is my response.' }, ], }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.thinkingBlocks.count).toBe(1); expect(report.thinkingBlocks.analyzedCount).toBe(1); expect(report.thinkingBlocks.signalSummary.alternatives).toBe(1); expect(report.thinkingBlocks.signalSummary.direction_change).toBe(1); }); }); describe('working directories', () => { it('tracks working directory changes', () => { const messages: ParsedMessage[] = [ createMockMessage({ cwd: '/project/src' }), createMockMessage({ cwd: '/project/src' }), createMockMessage({ cwd: '/project/test' }), createMockMessage({ cwd: '/project/src' }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.workingDirectories.directoryCount).toBe(2); expect(report.workingDirectories.isMultiDirectory).toBe(true); expect(report.workingDirectories.changeCount).toBe(2); // src->test, test->src }); }); describe('git branches', () => { it('collects unique git branches', () => { const messages: ParsedMessage[] = [ createMockMessage({ gitBranch: 'main' }), createMockMessage({ gitBranch: 'main' }), createMockMessage({ gitBranch: 'feat/new' }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.gitBranches).toContain('main'); expect(report.gitBranches).toContain('feat/new'); expect(report.gitBranches).toHaveLength(2); }); }); describe('test progression', () => { it('detects improving test trajectory', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', toolCalls: [{ id: 'tc-1', name: 'Bash', input: { command: 'pnpm test' }, isTask: false }], }), createMockMessage({ type: 'user', isMeta: true, content: [], toolResults: [{ toolUseId: 'tc-1', content: '3 passed 2 failed', isError: false }], }), createMockMessage({ type: 'assistant', toolCalls: [{ id: 'tc-2', name: 'Bash', input: { command: 'pnpm test' }, isTask: false }], }), createMockMessage({ type: 'user', isMeta: true, content: [], toolResults: [{ toolUseId: 'tc-2', content: '5 passed 0 failed', isError: false }], }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.testProgression.snapshotCount).toBe(2); expect(report.testProgression.trajectory).toBe('improving'); expect(report.testProgression.firstSnapshot?.passed).toBe(3); expect(report.testProgression.lastSnapshot?.passed).toBe(5); }); }); describe('startup overhead', () => { it('counts messages and tokens before first work tool', () => { const messages: ParsedMessage[] = [ // Startup: assistant response with no work tools createMockMessage({ type: 'assistant', model: 'claude-sonnet-4-20250514', usage: { input_tokens: 500, output_tokens: 200 }, toolCalls: [], }), // First work tool createMockMessage({ type: 'assistant', model: 'claude-sonnet-4-20250514', usage: { input_tokens: 1000, output_tokens: 300 }, toolCalls: [{ id: 'tc-1', name: 'Read', input: { file_path: '/foo.ts' }, isTask: false }], }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.startupOverhead.messagesBeforeFirstWork).toBe(1); expect(report.startupOverhead.tokensBeforeFirstWork).toBe(700); // 500 + 200 }); }); describe('thrashing signals', () => { it('detects bash near-duplicates', () => { const makeMsg = (cmd: string, id: string) => createMockMessage({ type: 'assistant', toolCalls: [{ id, name: 'Bash', input: { command: cmd }, isTask: false }], }); const messages: ParsedMessage[] = [ makeMsg('pnpm test src/foo.test.ts', 'tc-1'), makeMsg('pnpm test src/foo.test.ts', 'tc-2'), makeMsg('pnpm test src/foo.test.ts', 'tc-3'), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.thrashingSignals.bashNearDuplicates.length).toBeGreaterThanOrEqual(1); expect(report.thrashingSignals.bashNearDuplicates[0].count).toBe(3); }); it('detects file edit rework', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', toolCalls: [{ id: 'tc-1', name: 'Edit', input: { file_path: '/foo.ts' }, isTask: false }], }), createMockMessage({ type: 'assistant', toolCalls: [{ id: 'tc-2', name: 'Edit', input: { file_path: '/foo.ts' }, isTask: false }], }), createMockMessage({ type: 'assistant', toolCalls: [{ id: 'tc-3', name: 'Edit', input: { file_path: '/foo.ts' }, isTask: false }], }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.thrashingSignals.editReworkFiles).toHaveLength(1); expect(report.thrashingSignals.editReworkFiles[0].filePath).toBe('/foo.ts'); expect(report.thrashingSignals.editReworkFiles[0].editIndices).toHaveLength(3); }); }); describe('skills invoked', () => { it('tracks Skill tool calls', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', toolCalls: [ { id: 'tc-1', name: 'Skill', input: { skill: 'brainstorming', args: '--verbose' }, isTask: false, }, { id: 'tc-2', name: 'Skill', input: { skill: 'writing-plans' }, isTask: false }, ], }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.skillsInvoked).toHaveLength(2); expect(report.skillsInvoked[0].skill).toBe('brainstorming'); expect(report.skillsInvoked[0].argsPreview).toBe('--verbose'); expect(report.skillsInvoked[1].skill).toBe('writing-plans'); }); }); describe('bash commands', () => { it('tracks total, unique, and repeated commands', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', toolCalls: [ { id: 'tc-1', name: 'Bash', input: { command: 'pnpm test' }, isTask: false }, { id: 'tc-2', name: 'Bash', input: { command: 'pnpm test' }, isTask: false }, { id: 'tc-3', name: 'Bash', input: { command: 'git status' }, isTask: false }, ], }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.bashCommands.total).toBe(3); expect(report.bashCommands.unique).toBe(2); expect(report.bashCommands.repeated['pnpm test']).toBe(2); }); }); describe('subagents list', () => { it('tracks Task tool dispatches', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', toolCalls: [ { id: 'tc-1', name: 'Task', input: { description: 'explore auth', subagent_type: 'Explore', run_in_background: true, }, isTask: true, }, ], }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.subagentsList).toHaveLength(1); expect(report.subagentsList[0].description).toBe('explore auth'); expect(report.subagentsList[0].subagentType).toBe('Explore'); expect(report.subagentsList[0].runInBackground).toBe(true); }); }); describe('lifecycle tasks', () => { it('tracks TaskCreate subjects', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', toolCalls: [ { id: 'tc-1', name: 'TaskCreate', input: { subject: 'Add login page' }, isTask: false }, { id: 'tc-2', name: 'TaskCreate', input: { subject: 'Write tests' }, isTask: false }, ], }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.lifecycleTasks).toEqual(['Add login page', 'Write tests']); }); }); describe('user questions', () => { it('tracks AskUserQuestion calls', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', toolCalls: [ { id: 'tc-1', name: 'AskUserQuestion', input: { questions: [ { question: 'Which auth method?', options: [{ label: 'JWT' }, { label: 'OAuth' }], }, ], }, isTask: false, }, ], }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.userQuestions).toHaveLength(1); expect(report.userQuestions[0].question).toBe('Which auth method?'); expect(report.userQuestions[0].options).toEqual(['JWT', 'OAuth']); }); }); describe('out-of-scope findings', () => { it('detects pre-existing and tech debt mentions', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', content: 'This is a pre-existing issue that was there before our changes.', }), createMockMessage({ type: 'assistant', content: 'I noticed some tech debt in the authentication module.', }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.outOfScopeFindings).toHaveLength(2); expect(report.outOfScopeFindings[0].keyword).toBe('pre-existing'); expect(report.outOfScopeFindings[1].keyword).toBe('tech debt'); }); }); describe('compaction', () => { it('tracks compaction count and summary messages', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', isCompactSummary: true }), createMockMessage({ type: 'assistant', isCompactSummary: true }), ]; const session = createMockSession(); session.compactionCount = 2; const report = analyzeSession(createMockDetail({ messages, session })); expect(report.compaction.count).toBe(2); expect(report.compaction.compactSummaryCount).toBe(2); expect(report.compaction.note).toContain('underwent compaction'); }); it('reports no compaction', () => { const report = analyzeSession(createMockDetail({})); expect(report.compaction.count).toBe(0); expect(report.compaction.note).toContain('No compaction'); }); }); // ------------------------------------------------------------------------- // Assessment computations // ------------------------------------------------------------------------- describe('cost assessments', () => { it('computes costPerCommitAssessment when commits exist', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', model: 'claude-sonnet-4-20250514', usage: { input_tokens: 50000, output_tokens: 10000 }, toolCalls: [ { id: 'tc-1', name: 'Bash', input: { command: "git commit -m 'fix'" }, isTask: false, }, ], }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.costAnalysis.costPerCommitAssessment).not.toBeNull(); }); it('returns null assessments when no commits', () => { const report = analyzeSession(createMockDetail()); expect(report.costAnalysis.costPerCommitAssessment).toBeNull(); expect(report.costAnalysis.costPerLineAssessment).toBeNull(); }); it('returns null subagentCostShareAssessment when no cost', () => { const report = analyzeSession(createMockDetail()); expect(report.costAnalysis.subagentCostSharePct).toBeNull(); expect(report.costAnalysis.subagentCostShareAssessment).toBeNull(); }); }); describe('cache assessments', () => { it('computes cache efficiency assessment', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', model: 'claude-sonnet-4-20250514', usage: { input_tokens: 100, output_tokens: 50, cache_creation_input_tokens: 100, cache_read_input_tokens: 9900, }, }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.cacheEconomics.cacheEfficiencyAssessment).toBe('good'); }); it('returns concerning for low cache efficiency', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', model: 'claude-sonnet-4-20250514', usage: { input_tokens: 100, output_tokens: 50, cache_creation_input_tokens: 500, cache_read_input_tokens: 500, }, }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.cacheEconomics.cacheEfficiencyAssessment).toBe('concerning'); }); it('returns null when no cache data', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', model: 'claude-sonnet-4-20250514', usage: { input_tokens: 100, output_tokens: 50, cache_creation_input_tokens: 0, cache_read_input_tokens: 0, }, }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.cacheEconomics.cacheEfficiencyAssessment).toBeNull(); expect(report.cacheEconomics.cacheRatioAssessment).toBeNull(); }); }); describe('tool health assessments', () => { it('computes per-tool assessment', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', toolCalls: [ { id: 'tc-1', name: 'Read', input: { file_path: '/a.ts' }, isTask: false }, { id: 'tc-2', name: 'Read', input: { file_path: '/b.ts' }, isTask: false }, ], }), createMockMessage({ type: 'user', isMeta: true, content: [], toolResults: [ { toolUseId: 'tc-1', content: 'ok', isError: false }, { toolUseId: 'tc-2', content: 'ok', isError: false }, ], }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.toolUsage.successRates.Read.assessment).toBe('healthy'); }); it('computes overall tool health', () => { const report = analyzeSession(createMockDetail()); expect(report.toolUsage.overallToolHealth).toBe('healthy'); }); }); describe('idle assessment', () => { it('returns efficient for low idle', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', timestamp: new Date('2024-01-01T10:00:00Z'), }), createMockMessage({ type: 'user', content: 'quick', timestamp: new Date('2024-01-01T10:00:30Z'), }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.idleAnalysis.idleAssessment).toBe('efficient'); }); it('returns high_idle for mostly idle session', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', timestamp: new Date('2024-01-01T10:00:00Z'), }), createMockMessage({ type: 'user', content: 'back', timestamp: new Date('2024-01-01T11:00:00Z'), }), createMockMessage({ type: 'assistant', timestamp: new Date('2024-01-01T11:00:10Z'), }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.idleAnalysis.idleAssessment).toBe('high_idle'); }); }); describe('thrashing assessment', () => { it('returns none when no signals', () => { const report = analyzeSession(createMockDetail()); expect(report.thrashingSignals.thrashingAssessment).toBe('none'); }); it('returns mild or severe based on signal count', () => { const makeEditMsg = (file: string, id: string) => createMockMessage({ type: 'assistant', toolCalls: [{ id, name: 'Edit', input: { file_path: file }, isTask: false }], }); // 3 edits on one file = 1 signal + 3 repeated bash = 1 signal = mild (2) const messages: ParsedMessage[] = [ makeEditMsg('/foo.ts', 'e1'), makeEditMsg('/foo.ts', 'e2'), makeEditMsg('/foo.ts', 'e3'), ]; const report = analyzeSession(createMockDetail({ messages })); expect(['mild', 'severe']).toContain(report.thrashingSignals.thrashingAssessment); }); }); describe('model switch pattern', () => { it('detects opus_plan_mode', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', model: 'claude-sonnet-4-20250514', timestamp: new Date('2024-01-01T10:00:00Z'), }), createMockMessage({ type: 'assistant', model: 'claude-opus-4-20250514', timestamp: new Date('2024-01-01T10:01:00Z'), }), createMockMessage({ type: 'assistant', model: 'claude-sonnet-4-20250514', timestamp: new Date('2024-01-01T10:02:00Z'), }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.modelSwitches.switchPattern).toBe('opus_plan_mode'); }); it('returns null when no switches', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', model: 'claude-sonnet-4-20250514', usage: { input_tokens: 100, output_tokens: 50 }, }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.modelSwitches.switchPattern).toBeNull(); }); }); describe('startup overhead assessment', () => { it('returns normal for low overhead', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', model: 'claude-sonnet-4-20250514', usage: { input_tokens: 100, output_tokens: 50 }, toolCalls: [{ id: 'tc-1', name: 'Read', input: { file_path: '/f.ts' }, isTask: false }], }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.startupOverhead.overheadAssessment).toBe('normal'); }); it('returns heavy for high overhead', () => { const messages: ParsedMessage[] = [ // Lots of startup tokens, no work tools createMockMessage({ type: 'assistant', model: 'claude-sonnet-4-20250514', usage: { input_tokens: 50000, output_tokens: 10000 }, toolCalls: [], }), // Small work message createMockMessage({ type: 'assistant', model: 'claude-sonnet-4-20250514', usage: { input_tokens: 100, output_tokens: 50 }, toolCalls: [{ id: 'tc-1', name: 'Read', input: { file_path: '/f.ts' }, isTask: false }], }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.startupOverhead.overheadAssessment).toBe('heavy'); }); }); describe('file read redundancy assessment', () => { it('returns normal for low redundancy', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', toolCalls: [ { id: 'tc-1', name: 'Read', input: { file_path: '/a.ts' }, isTask: false }, { id: 'tc-2', name: 'Read', input: { file_path: '/b.ts' }, isTask: false }, ], }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.fileReadRedundancy.redundancyAssessment).toBe('normal'); }); it('returns wasteful for high redundancy', () => { const messages: ParsedMessage[] = [ createMockMessage({ type: 'assistant', toolCalls: [ { id: 'tc-1', name: 'Read', input: { file_path: '/a.ts' }, isTask: false }, { id: 'tc-2', name: 'Read', input: { file_path: '/a.ts' }, isTask: false }, { id: 'tc-3', name: 'Read', input: { file_path: '/a.ts' }, isTask: false }, { id: 'tc-4', name: 'Read', input: { file_path: '/a.ts' }, isTask: false }, ], }), ]; const report = analyzeSession(createMockDetail({ messages })); expect(report.fileReadRedundancy.redundancyAssessment).toBe('wasteful'); }); }); describe('model mismatch in subagents', () => { it('detects mismatch for mechanical tasks on opus', () => { const processes: Process[] = [ { id: 'agent-1', filePath: '/path/to/agent-1.jsonl', messages: [], startTime: new Date('2024-01-01T10:00:00Z'), endTime: new Date('2024-01-01T10:01:00Z'), durationMs: 60000, metrics: createMockMetrics({ totalTokens: 5000, costUsd: 0.05 }), description: 'rename all variables', subagentType: 'code', isParallel: false, }, ]; const report = analyzeSession(createMockDetail({ processes })); // model is 'default (inherits parent)' which doesn't contain 'opus', so no mismatch expect(report.subagentMetrics.byAgent[0].modelMismatch).toBeNull(); }); }); });