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> = {}): 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> = {}): Session { return { id: 'test-session', projectId: 'test-project', projectPath: '/test/path', createdAt: Date.now(), hasSubagents: false, messageCount: 0, ...overrides, };} function createMockMetrics(overrides: Partial<SessionMetrics> = {}): SessionMetrics { return { durationMs: 0, totalTokens: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, messageCount: 0, ...overrides, };} function createMockDetail(overrides: Partial<SessionDetail> = {}): 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[] = [Lines 1–500 of 1509. Load more
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> = {}): 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> = {}): Session { return { id: 'test-session', projectId: 'test-project', projectPath: '/test/path', createdAt: Date.now(), hasSubagents: false, messageCount: 0, ...overrides, }; } function createMockMetrics(overrides: Partial<SessionMetrics> = {}): SessionMetrics { return { durationMs: 0, totalTokens: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, messageCount: 0, ...overrides, }; } function createMockDetail(overrides: Partial<SessionDetail> = {}): 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[] = [