/ tests / tool-execution-logs.test.ts
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  }