tool-execution-logs.test.ts
1 import { mkdtempSync, rmSync } from 'node:fs' 2 import { tmpdir } from 'node:os' 3 import path from 'node:path' 4 5 import { afterEach, beforeEach, describe, expect, it } from 'vitest' 6 7 import { ensureSession } from '@/server/storage/chat-store' 8 import { getAppSettings, updateAppSettings } from '@/server/storage/app-settings' 9 import { 10 listToolExecutionLogs, 11 pruneToolExecutionLogsOlderThan, 12 recordToolExecutionLog, 13 } from '@/server/storage/tool-execution-logs' 14 15 const originalEnv = { ...process.env } 16 17 let tempDataDir = '' 18 19 describe('tool execution log storage', () => { 20 beforeEach(() => { 21 tempDataDir = mkdtempSync(path.join(tmpdir(), 'helper-tool-logs-')) 22 resetDbConnection() 23 process.env = { 24 ...originalEnv, 25 APP_DATA_DIR: tempDataDir, 26 } 27 }) 28 29 afterEach(() => { 30 resetDbConnection() 31 rmSync(tempDataDir, { recursive: true, force: true }) 32 process.env = originalEnv 33 }) 34 35 it('records and lists tool executions scoped to a session', () => { 36 ensureSession('session-a') 37 38 recordToolExecutionLog({ 39 sessionId: 'session-a', 40 provider: 'openai-compatible', 41 model: 'minimax-m2', 42 toolName: 'web_fetch', 43 toolCallId: 'call_1', 44 source: 'textual', 45 status: 'ok', 46 preview: 'Fetched https://example.com', 47 arguments: { url: 'https://example.com' }, 48 output: { ok: true, tool: 'web_fetch', url: 'https://example.com' }, 49 }) 50 51 const entries = listToolExecutionLogs({ sessionId: 'session-a', limit: 10 }) 52 expect(entries).toHaveLength(1) 53 expect(entries[0]).toMatchObject({ 54 sessionId: 'session-a', 55 toolName: 'web_fetch', 56 source: 'textual', 57 status: 'ok', 58 toolCallId: 'call_1', 59 }) 60 expect(entries[0]?.arguments?.url).toBe('https://example.com') 61 }) 62 63 it('does not mix entries across sessions', () => { 64 ensureSession('session-a') 65 ensureSession('session-b') 66 67 recordToolExecutionLog({ 68 sessionId: 'session-a', 69 provider: 'openai-compatible', 70 model: 'gpt-5-mini', 71 toolName: 'get_current_time', 72 source: 'function', 73 status: 'ok', 74 preview: 'Local date/time', 75 arguments: {}, 76 output: { ok: true }, 77 }) 78 recordToolExecutionLog({ 79 sessionId: 'session-b', 80 provider: 'openai-compatible', 81 model: 'gpt-5-mini', 82 toolName: 'bash', 83 source: 'function', 84 status: 'error', 85 preview: 'bash disabled', 86 arguments: { command: 'pwd' }, 87 output: { ok: false, error: 'disabled' }, 88 }) 89 90 const sessionAEntries = listToolExecutionLogs({ 91 sessionId: 'session-a', 92 limit: 10, 93 }) 94 expect(sessionAEntries).toHaveLength(1) 95 expect(sessionAEntries[0]?.toolName).toBe('get_current_time') 96 }) 97 98 it('persists retention settings and prunes old entries', () => { 99 ensureSession('session-retention') 100 updateAppSettings({ toolCallLogRetentionDays: 7 }) 101 expect(getAppSettings().toolCallLogRetentionDays).toBe(7) 102 103 const now = Date.now() 104 recordToolExecutionLog({ 105 sessionId: 'session-retention', 106 provider: 'openai-compatible', 107 model: 'minimax-m2', 108 toolName: 'web_search', 109 source: 'function', 110 status: 'ok', 111 preview: 'new', 112 arguments: { query: 'today' }, 113 output: { ok: true }, 114 startedAt: new Date(now).toISOString(), 115 completedAt: new Date(now).toISOString(), 116 }) 117 recordToolExecutionLog({ 118 sessionId: 'session-retention', 119 provider: 'openai-compatible', 120 model: 'minimax-m2', 121 toolName: 'web_search', 122 source: 'function', 123 status: 'ok', 124 preview: 'old', 125 arguments: { query: 'old' }, 126 output: { ok: true }, 127 startedAt: new Date(now - 10 * 24 * 60 * 60 * 1000).toISOString(), 128 completedAt: new Date(now - 10 * 24 * 60 * 60 * 1000).toISOString(), 129 }) 130 131 const pruned = pruneToolExecutionLogsOlderThan(7) 132 expect(pruned).toBe(1) 133 134 const entries = listToolExecutionLogs({ 135 sessionId: 'session-retention', 136 limit: 10, 137 }) 138 expect(entries).toHaveLength(1) 139 expect(entries[0]?.preview).toBe('new') 140 }) 141 }) 142 143 function resetDbConnection(): void { 144 const globalWithDb = globalThis as typeof globalThis & { 145 __helperDb__?: { close: () => void } 146 } 147 globalWithDb.__helperDb__?.close() 148 globalWithDb.__helperDb__ = undefined 149 }