import { describe, expect, it, vi, afterEach } from 'vitest'; import { samplerOptsFromAgent } from '../inference/stream-phase.js'; import { createContentFlusher } from '../inference/content-flusher.js'; import type { Sql } from '../../db.js'; import type { Agent } from '../../types/api.js'; const BASE_AGENT: Agent = { id: 'test-agent', name: 'Test', description: 'test', system_prompt: '', temperature: 0.7, top_p: null, top_k: null, min_p: null, presence_penalty: null, top_n_sigma: null, dry_multiplier: null, dry_base: null, dry_allowed_length: null, dry_penalty_last_n: null, tools: ['view_file'], model: null, source: 'global', max_tool_calls: null, steps: null, llama_extra_args: null, }; describe('samplerOptsFromAgent', () => { it('maps every nullable sampler field to undefined when agent is null', () => { expect(samplerOptsFromAgent(null)).toEqual({ temperature: undefined, top_p: undefined, top_k: undefined, min_p: undefined, presence_penalty: undefined, top_n_sigma: undefined, dry_multiplier: undefined, dry_base: undefined, dry_allowed_length: undefined, dry_penalty_last_n: undefined, }); }); it('strips null sampler fields to undefined but keeps numeric values', () => { const agent: Agent = { ...BASE_AGENT, temperature: 0.5, top_p: 0.9, top_k: null, min_p: 0.05, presence_penalty: null, top_n_sigma: 1, dry_multiplier: null, dry_base: 1.75, dry_allowed_length: null, dry_penalty_last_n: 256, }; expect(samplerOptsFromAgent(agent)).toEqual({ temperature: 0.5, top_p: 0.9, top_k: undefined, min_p: 0.05, presence_penalty: undefined, top_n_sigma: 1, dry_multiplier: undefined, dry_base: 1.75, dry_allowed_length: undefined, dry_penalty_last_n: 256, }); }); it('never includes a tools field (callers add it)', () => { expect('tools' in samplerOptsFromAgent(BASE_AGENT)).toBe(false); }); }); describe('createContentFlusher', () => { afterEach(() => { vi.useRealTimers(); }); // A tagged-template stub matching postgres' sql`...` shape. Records the // interpolated content snapshot (values[0]) of each UPDATE. function makeSqlSpy() { const writes: string[] = []; const sql = ((_strings: TemplateStringsArray, ...values: unknown[]) => { writes.push(values[0] as string); return Promise.resolve([]); }) as unknown as Sql; return { sql, writes }; } it('debounces: many scheduleFlush calls in one window produce one write', async () => { vi.useFakeTimers(); const { sql, writes } = makeSqlSpy(); let content = ''; const flusher = createContentFlusher(sql, 'msg-1', () => content, 500); content = 'a'; flusher.scheduleFlush(); content = 'ab'; flusher.scheduleFlush(); content = 'abc'; flusher.scheduleFlush(); expect(writes).toHaveLength(0); // nothing before the interval elapses vi.advanceTimersByTime(500); await flusher.drain(); expect(writes).toHaveLength(1); // snapshot is read at fire time → latest content, not the value at schedule time expect(writes[0]).toBe('abc'); }); it('arms a fresh timer after a flush fires', async () => { vi.useFakeTimers(); const { sql, writes } = makeSqlSpy(); let content = 'one'; const flusher = createContentFlusher(sql, 'msg-1', () => content, 500); flusher.scheduleFlush(); vi.advanceTimersByTime(500); await Promise.resolve(); content = 'two'; flusher.scheduleFlush(); vi.advanceTimersByTime(500); await flusher.drain(); expect(writes).toEqual(['one', 'two']); }); it('drain cancels a pending timer without performing a final flush', async () => { vi.useFakeTimers(); const { sql, writes } = makeSqlSpy(); let content = 'pending'; const flusher = createContentFlusher(sql, 'msg-1', () => content, 500); flusher.scheduleFlush(); // Drain before the timer fires — the pending flush is cancelled, not forced. await flusher.drain(); vi.advanceTimersByTime(500); await Promise.resolve(); expect(writes).toHaveLength(0); }); });