Path: blob/main/extensions/copilot/src/extension/chronicle/node/test/sessionReindexer.spec.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { describe, expect, it, vi } from 'vitest';6import type { IChatDebugFileLoggerService, IDebugLogEntry } from '../../../../platform/chat/common/chatDebugFileLoggerService';7import type { ISessionStore, SessionRow, TurnRow, FileRow, RefRow } from '../../../../platform/chronicle/common/sessionStore';8import { CancellationTokenSource } from '../../../../util/vs/base/common/cancellation';9import { reindexSessions } from '../sessionReindexer';1011// ── Helpers ──────────────────────────────────────────────────────────────────1213function makeEntry(overrides: Partial<IDebugLogEntry>): IDebugLogEntry {14return {15ts: Date.now(),16dur: 0,17sid: 'session-1',18type: 'generic',19name: '',20spanId: 'span-1',21status: 'ok',22attrs: {},23...overrides,24};25}2627interface MockSessionStore extends ISessionStore {28upsertedSessions: SessionRow[];29insertedTurns: TurnRow[];30insertedFiles: FileRow[];31insertedRefs: RefRow[];32existingSessions: Set<string>;33}3435function createMockStore(): MockSessionStore {36const mock: MockSessionStore = {37_serviceBrand: undefined as any,38upsertedSessions: [] as SessionRow[],39insertedTurns: [] as TurnRow[],40insertedFiles: [] as FileRow[],41insertedRefs: [] as RefRow[],42existingSessions: new Set<string>(),4344getPath: () => '/tmp/test.db',45upsertSession: (s: SessionRow) => mock.upsertedSessions.push(s),46insertTurn: (t: TurnRow) => mock.insertedTurns.push(t),47insertCheckpoint: () => { },48insertFile: (f: FileRow) => mock.insertedFiles.push(f),49insertRef: (r: RefRow) => mock.insertedRefs.push(r),50indexWorkspaceArtifact: () => { },51search: () => [],52getSession: (id: string) => mock.existingSessions.has(id) ? { id } as SessionRow : undefined,53getTurns: () => [],54getFiles: () => [],55getRefs: () => [],56getMaxTurnIndex: () => -1,57getStats: () => ({ sessions: 0, turns: 0, checkpoints: 0, files: 0, refs: 0 }),58executeReadOnly: () => [],59executeReadOnlyFallback: () => [],60runInTransaction: (fn: () => void) => fn(),61close: () => { },62};63return mock;64}6566function createMockDebugLogService(67sessionIds: string[],68entriesMap: Map<string, IDebugLogEntry[]>,69): IChatDebugFileLoggerService {70return {71_serviceBrand: undefined as any,72listSessionIds: async () => sessionIds,73streamEntries: async (sessionId: string, onEntry: (entry: IDebugLogEntry) => void) => {74const entries = entriesMap.get(sessionId) ?? [];75for (const entry of entries) {76onEntry(entry);77}78},79// Stubs for unused methods80startSession: async () => { },81startChildSession: () => { },82registerSpanSession: () => { },83endSession: async () => { },84flush: async () => { },85getLogPath: () => undefined,86getSessionDir: () => undefined,87getActiveSessionIds: () => [],88isDebugLogUri: () => false,89getSessionDirForResource: () => undefined,90setModelSnapshot: () => { },91debugLogsDir: undefined,92onDidEmitEntry: undefined as any,93readEntries: async () => [],94readTailEntries: async () => [],95} as any;96}9798// ── Tests ────────────────────────────────────────────────────────────────────99100describe('reindexSessions', () => {101it('processes a session with user + assistant turns', async () => {102const store = createMockStore();103const entries = new Map<string, IDebugLogEntry[]>();104entries.set('session-1', [105makeEntry({ type: 'session_start', name: 'session_start', sid: 'session-1', attrs: { cwd: '/workspace' } }),106makeEntry({ type: 'user_message', name: 'user_message', sid: 'session-1', attrs: { content: 'Fix the bug' } }),107makeEntry({ type: 'agent_response', name: 'agent_response', sid: 'session-1', attrs: { response: JSON.stringify([{ role: 'assistant', parts: [{ type: 'text', content: 'I fixed the bug by changing X' }] }]) } }),108makeEntry({ type: 'user_message', name: 'user_message', sid: 'session-1', attrs: { content: 'Now add tests' } }),109makeEntry({ type: 'agent_response', name: 'agent_response', sid: 'session-1', attrs: { response: JSON.stringify([{ role: 'assistant', parts: [{ type: 'text', content: 'Added tests for X' }] }]) } }),110]);111112const debugLog = createMockDebugLogService(['session-1'], entries);113const cts = new CancellationTokenSource();114const progress = vi.fn();115116const result = await reindexSessions(store, debugLog, progress, cts.token);117118expect(result).toEqual({ processed: 1, skipped: 0, cancelled: false });119expect(store.upsertedSessions).toHaveLength(1);120expect(store.upsertedSessions[0].cwd).toBe('/workspace');121expect(store.insertedTurns).toHaveLength(2);122expect(store.insertedTurns[0].user_message).toBe('Fix the bug');123expect(store.insertedTurns[0].assistant_response).toBe('I fixed the bug by changing X');124expect(store.insertedTurns[1].user_message).toBe('Now add tests');125expect(store.insertedTurns[1].assistant_response).toBe('Added tests for X');126});127128it('extracts file paths from tool_call events', async () => {129const store = createMockStore();130const entries = new Map<string, IDebugLogEntry[]>();131entries.set('session-1', [132makeEntry({ type: 'tool_call', name: 'read_file', sid: 'session-1', attrs: { args: JSON.stringify({ filePath: '/src/foo.ts', startLine: 1, endLine: 10 }) } }),133makeEntry({ type: 'tool_call', name: 'create_file', sid: 'session-1', attrs: { args: JSON.stringify({ filePath: '/src/bar.ts', content: '// new' }) } }),134]);135136const debugLog = createMockDebugLogService(['session-1'], entries);137const cts = new CancellationTokenSource();138139await reindexSessions(store, debugLog, vi.fn(), cts.token);140141expect(store.insertedFiles).toHaveLength(2);142expect(store.insertedFiles[0].file_path).toBe('/src/foo.ts');143expect(store.insertedFiles[1].file_path).toBe('/src/bar.ts');144});145146it('extracts refs from GitHub MCP tool calls', async () => {147const store = createMockStore();148const entries = new Map<string, IDebugLogEntry[]>();149entries.set('session-1', [150makeEntry({151type: 'tool_call',152name: 'mcp_github_pull_request_read',153sid: 'session-1',154attrs: { args: JSON.stringify({ owner: 'microsoft', repo: 'vscode', pullNumber: 42 }) },155}),156]);157158const debugLog = createMockDebugLogService(['session-1'], entries);159const cts = new CancellationTokenSource();160161await reindexSessions(store, debugLog, vi.fn(), cts.token);162163expect(store.insertedRefs).toHaveLength(1);164expect(store.insertedRefs[0]).toEqual(expect.objectContaining({ ref_type: 'pr', ref_value: '42' }));165expect(store.upsertedSessions[0].repository).toBe('microsoft/vscode');166});167168it('extracts refs from terminal tool calls', async () => {169const store = createMockStore();170const entries = new Map<string, IDebugLogEntry[]>();171entries.set('session-1', [172makeEntry({173type: 'tool_call',174name: 'run_in_terminal',175sid: 'session-1',176attrs: {177args: JSON.stringify({ command: 'gh pr create --title "Fix" --body "desc"' }),178result: 'https://github.com/microsoft/vscode/pull/123',179},180}),181]);182183const debugLog = createMockDebugLogService(['session-1'], entries);184const cts = new CancellationTokenSource();185186await reindexSessions(store, debugLog, vi.fn(), cts.token);187188expect(store.insertedRefs).toHaveLength(1);189expect(store.insertedRefs[0]).toEqual(expect.objectContaining({ ref_type: 'pr', ref_value: '123' }));190});191192it('skips already-indexed sessions unless force=true', async () => {193const store = createMockStore();194store.existingSessions.add('session-1');195const entries = new Map<string, IDebugLogEntry[]>();196entries.set('session-1', [197makeEntry({ type: 'user_message', name: 'user_message', sid: 'session-1', attrs: { content: 'hello' } }),198]);199200const debugLog = createMockDebugLogService(['session-1'], entries);201const cts = new CancellationTokenSource();202203// Default: skip204const result = await reindexSessions(store, debugLog, vi.fn(), cts.token);205expect(result).toEqual({ processed: 0, skipped: 1, cancelled: false });206expect(store.insertedTurns).toHaveLength(0);207208// Force: process209const result2 = await reindexSessions(store, debugLog, vi.fn(), cts.token, true);210expect(result2.processed).toBe(1);211});212213it('respects cancellation token', async () => {214const store = createMockStore();215const entries = new Map<string, IDebugLogEntry[]>();216entries.set('session-1', [makeEntry({ type: 'session_start', name: 'session_start', sid: 'session-1' })]);217entries.set('session-2', [makeEntry({ type: 'session_start', name: 'session_start', sid: 'session-2' })]);218219const debugLog = createMockDebugLogService(['session-1', 'session-2'], entries);220const cts = new CancellationTokenSource();221222// Cancel immediately223cts.cancel();224225const result = await reindexSessions(store, debugLog, vi.fn(), cts.token);226expect(result.cancelled).toBe(true);227expect(result.processed).toBe(0);228});229230it('skips corrupt sessions and continues', async () => {231const store = createMockStore();232const entries = new Map<string, IDebugLogEntry[]>();233entries.set('session-good', [234makeEntry({ type: 'user_message', name: 'user_message', sid: 'session-good', attrs: { content: 'hello' } }),235makeEntry({ type: 'agent_response', name: 'agent_response', sid: 'session-good', attrs: { response: JSON.stringify([{ role: 'assistant', parts: [{ type: 'text', content: 'hi' }] }]) } }),236]);237238// Create a debug log service where session-bad throws239const debugLog = createMockDebugLogService(['session-bad', 'session-good'], entries);240const originalStream = debugLog.streamEntries.bind(debugLog);241(debugLog as any).streamEntries = async (sessionId: string, onEntry: any) => {242if (sessionId === 'session-bad') {243throw new Error('corrupt file');244}245return originalStream(sessionId, onEntry);246};247248const cts = new CancellationTokenSource();249const result = await reindexSessions(store, debugLog, vi.fn(), cts.token);250251expect(result.processed).toBe(1);252expect(result.skipped).toBe(1);253expect(store.insertedTurns).toHaveLength(1);254});255256it('truncates long user messages and assistant responses', async () => {257const store = createMockStore();258const longUserMsg = 'a'.repeat(200);259const longAssistantMsg = 'b'.repeat(2000);260const entries = new Map<string, IDebugLogEntry[]>();261entries.set('session-1', [262makeEntry({ type: 'user_message', name: 'user_message', sid: 'session-1', attrs: { content: longUserMsg } }),263makeEntry({ type: 'agent_response', name: 'agent_response', sid: 'session-1', attrs: { response: JSON.stringify([{ role: 'assistant', parts: [{ type: 'text', content: longAssistantMsg }] }]) } }),264]);265266const debugLog = createMockDebugLogService(['session-1'], entries);267const cts = new CancellationTokenSource();268269await reindexSessions(store, debugLog, vi.fn(), cts.token);270271expect(store.insertedTurns[0].user_message!.length).toBeLessThanOrEqual(100);272expect(store.insertedTurns[0].assistant_response!.length).toBeLessThanOrEqual(1000);273});274275it('handles sessions with no session_start event', async () => {276const store = createMockStore();277const entries = new Map<string, IDebugLogEntry[]>();278entries.set('session-1', [279makeEntry({ type: 'user_message', name: 'user_message', sid: 'session-1', attrs: { content: 'hello' } }),280makeEntry({ type: 'agent_response', name: 'agent_response', sid: 'session-1', attrs: { response: JSON.stringify([{ role: 'assistant', parts: [{ type: 'text', content: 'hi' }] }]) } }),281]);282283const debugLog = createMockDebugLogService(['session-1'], entries);284const cts = new CancellationTokenSource();285286await reindexSessions(store, debugLog, vi.fn(), cts.token);287288expect(store.upsertedSessions).toHaveLength(1);289expect(store.upsertedSessions[0].id).toBe('session-1');290expect(store.upsertedSessions[0].host_type).toBe('vscode');291});292293it('handles trailing user message without assistant response', async () => {294const store = createMockStore();295const entries = new Map<string, IDebugLogEntry[]>();296entries.set('session-1', [297makeEntry({ type: 'user_message', name: 'user_message', sid: 'session-1', attrs: { content: 'hello' } }),298]);299300const debugLog = createMockDebugLogService(['session-1'], entries);301const cts = new CancellationTokenSource();302303await reindexSessions(store, debugLog, vi.fn(), cts.token);304305expect(store.insertedTurns).toHaveLength(1);306expect(store.insertedTurns[0].user_message).toBe('hello');307expect(store.insertedTurns[0].assistant_response).toBeUndefined();308});309310it('reports progress for each session', async () => {311const store = createMockStore();312const entries = new Map<string, IDebugLogEntry[]>();313entries.set('s1', [makeEntry({ type: 'session_start', name: 'session_start', sid: 's1' })]);314entries.set('s2', [makeEntry({ type: 'session_start', name: 'session_start', sid: 's2' })]);315316const debugLog = createMockDebugLogService(['s1', 's2'], entries);317const cts = new CancellationTokenSource();318const progress = vi.fn();319320await reindexSessions(store, debugLog, progress, cts.token);321322expect(progress).toHaveBeenCalledTimes(2);323});324325it('sets summary from first user message', async () => {326const store = createMockStore();327const entries = new Map<string, IDebugLogEntry[]>();328entries.set('session-1', [329makeEntry({ type: 'user_message', name: 'user_message', sid: 'session-1', attrs: { content: 'Implement a login page' } }),330makeEntry({ type: 'agent_response', name: 'agent_response', sid: 'session-1', attrs: { response: JSON.stringify([{ role: 'assistant', parts: [{ type: 'text', content: 'Done' }] }]) } }),331]);332333const debugLog = createMockDebugLogService(['session-1'], entries);334const cts = new CancellationTokenSource();335336await reindexSessions(store, debugLog, vi.fn(), cts.token);337338expect(store.upsertedSessions[0].summary).toBe('Implement a login page');339});340341it('returns empty result for no sessions', async () => {342const store = createMockStore();343const debugLog = createMockDebugLogService([], new Map());344const cts = new CancellationTokenSource();345346const result = await reindexSessions(store, debugLog, vi.fn(), cts.token);347348expect(result).toEqual({ processed: 0, skipped: 0, cancelled: false });349});350});351352353