import { describe, it, expect, vi } from 'vitest'; import { formatPrompt, parseCommand } from './minimal.js'; import type { ModelConfig } from '../../config/schema.js'; import type { ManagedSession } from '../../session/index.js'; import type { ModelClient } from '../../models/types.js'; import type { ModelRouter } from '../../models/router.js'; import type { NativeAgent } from '../../backends/native/agent.js'; import { MinimalTui } from './minimal.js'; type TuiRouterStub = Pick & Partial & Partial & { getLocalProviderName: () => string | undefined; setLocalClient: ReturnType; }; function asSession(value: unknown): ManagedSession { return value as ManagedSession; } function asRouter(value: unknown): ModelClient & ModelRouter { return value as ModelClient & ModelRouter; } function asAgent(value: unknown): NativeAgent { return value as NativeAgent; } function minimalTuiPrivates(value: MinimalTui): { handleBackendCommand: (provider: string) => Promise; handleModelCommand: (tier: string, providerModel?: string) => void; handleEscapeAction: () => boolean; prompt: (text: string) => Promise; rl: { once: (event: string, cb: () => void) => void; removeListener: (event: string, cb: () => void) => void; question: (text: string, cb: (answer: string) => void) => void; write: (data: string | null, key?: { ctrl?: boolean; name?: string }) => void; }; activePromptCancel: (() => void) | null; activeOperationCancel: (() => void) | null; } { return value as unknown as { handleBackendCommand: (provider: string) => Promise; handleModelCommand: (tier: string, providerModel?: string) => void; handleEscapeAction: () => boolean; prompt: (text: string) => Promise; rl: { once: (event: string, cb: () => void) => void; removeListener: (event: string, cb: () => void) => void; question: (text: string, cb: (answer: string) => void) => void; write: (data: string | null, key?: { ctrl?: boolean; name?: string }) => void; }; activePromptCancel: (() => void) | null; activeOperationCancel: (() => void) | null; }; } describe('formatPrompt', () => { it('formats default prompt', () => { const prompt = formatPrompt('default'); expect(prompt).toContain('flynn>'); }); it('formats thinking prompt', () => { const prompt = formatPrompt('thinking'); expect(prompt).toContain('flynn...'); }); }); describe('parseCommand (re-exported)', () => { it('parses /quit command', () => { const result = parseCommand('/quit'); expect(result).toEqual({ type: 'quit' }); }); it('parses /model command', () => { const result = parseCommand('/model local'); expect(result).toEqual({ type: 'model', name: 'local' }); }); it('parses regular message', () => { const result = parseCommand('Hello, Flynn!'); expect(result).toEqual({ type: 'message', content: 'Hello, Flynn!' }); }); it('returns null for empty input', () => { const result = parseCommand(''); expect(result).toBeNull(); }); }); describe('MinimalTui backend command', () => { it('switches local backend when provider is configured', async () => { const mockSession = { id: 'test', getHistory: () => [], addMessage: vi.fn(), clear: vi.fn(), replaceHistory: vi.fn(), }; const mockRouter: TuiRouterStub = { getTier: () => 'default' as const, getAvailableTiers: () => ['default', 'local'], setTier: vi.fn(() => true), getLabel: (tier: string) => tier, getLocalProviderName: () => 'ollama', setLocalClient: vi.fn(), chat: vi.fn(), getClient: vi.fn(), }; const localProviders: Record = { llamacpp: { provider: 'llamacpp', model: '', endpoint: 'http://localhost:8080', }, }; const tui = new MinimalTui({ session: asSession(mockSession), modelClient: asRouter(mockRouter), modelRouter: asRouter(mockRouter), systemPrompt: 'test', localProviders, }); // Access private method for testing await minimalTuiPrivates(tui).handleBackendCommand('llamacpp'); expect(mockRouter.setLocalClient).toHaveBeenCalled(); }); it('syncs agent tier when /model command switches tier', () => { const mockSession = { id: 'test', getHistory: () => [], addMessage: vi.fn(), clear: vi.fn(), replaceHistory: vi.fn(), }; const mockRouter: TuiRouterStub = { getTier: () => 'default' as const, getAvailableTiers: () => ['default', 'local'], setTier: vi.fn(() => true), getLabel: (tier: string) => tier, getLocalProviderName: () => 'ollama', setLocalClient: vi.fn(), chat: vi.fn(), getClient: vi.fn(), }; const mockAgent = { setModelTier: vi.fn(), getModelTier: vi.fn(() => 'default'), process: vi.fn(), }; const tui = new MinimalTui({ session: asSession(mockSession), modelClient: asRouter(mockRouter), modelRouter: asRouter(mockRouter), agent: asAgent(mockAgent), systemPrompt: 'test', }); // Call private handleModelCommand to switch to local minimalTuiPrivates(tui).handleModelCommand('local'); expect(mockRouter.setTier).toHaveBeenCalledWith('local'); expect(mockAgent.setModelTier).toHaveBeenCalledWith('local'); }); it('reuses configured provider credentials for /model ', () => { const prevOpenRouterKey = process.env.OPENROUTER_API_KEY; delete process.env.OPENROUTER_API_KEY; try { const mockSession = { id: 'test', getHistory: () => [], addMessage: vi.fn(), clear: vi.fn(), replaceHistory: vi.fn(), }; const mockRouter: TuiRouterStub = { getTier: () => 'default' as const, getAvailableTiers: () => ['default', 'local'], setTier: vi.fn(() => true), getLabel: (tier: string) => tier, getLocalProviderName: () => 'ollama', setLocalClient: vi.fn(), setClient: vi.fn(), setTierStrict: vi.fn(), chat: vi.fn(), getClient: vi.fn(), }; const tui = new MinimalTui({ session: asSession(mockSession), modelClient: asRouter(mockRouter), modelRouter: asRouter(mockRouter), systemPrompt: 'test', modelProviderConfigs: { openrouter: { provider: 'openrouter', model: 'seed-model', api_key: 'test-key', endpoint: 'https://openrouter.ai/api/v1', }, }, }); minimalTuiPrivates(tui).handleModelCommand('default', 'openrouter/deepseek/deepseek-chat'); expect(mockRouter.setClient).toHaveBeenCalledOnce(); expect(mockRouter.setTierStrict).toHaveBeenCalledWith('default', true); expect(mockRouter.setTier).toHaveBeenCalledWith('default'); } finally { if (prevOpenRouterKey) { process.env.OPENROUTER_API_KEY = prevOpenRouterKey; } else { delete process.env.OPENROUTER_API_KEY; } } }); it('switches active tier and syncs agent for /model ', () => { const prevOpenRouterKey = process.env.OPENROUTER_API_KEY; delete process.env.OPENROUTER_API_KEY; try { const mockSession = { id: 'test', getHistory: () => [], addMessage: vi.fn(), clear: vi.fn(), replaceHistory: vi.fn(), }; const mockRouter: TuiRouterStub = { getTier: () => 'fast' as const, getAvailableTiers: () => ['default', 'fast', 'local'], setTier: vi.fn(() => true), getLabel: (tier: string) => tier, getLocalProviderName: () => 'ollama', setLocalClient: vi.fn(), setClient: vi.fn(), setTierStrict: vi.fn(), chat: vi.fn(), getClient: vi.fn(), }; const mockAgent = { setModelTier: vi.fn(), getModelTier: vi.fn(() => 'fast'), process: vi.fn(), }; const tui = new MinimalTui({ session: asSession(mockSession), modelClient: asRouter(mockRouter), modelRouter: asRouter(mockRouter), agent: asAgent(mockAgent), systemPrompt: 'test', modelProviderConfigs: { openrouter: { provider: 'openrouter', model: 'seed-model', api_key: 'test-key', endpoint: 'https://openrouter.ai/api/v1', }, }, }); minimalTuiPrivates(tui).handleModelCommand('default', 'openrouter/deepseek/deepseek-chat'); expect(mockRouter.setTier).toHaveBeenCalledWith('default'); expect(mockAgent.setModelTier).toHaveBeenCalledWith('default'); } finally { if (prevOpenRouterKey) { process.env.OPENROUTER_API_KEY = prevOpenRouterKey; } else { delete process.env.OPENROUTER_API_KEY; } } }); }); describe('MinimalTui prompt cancellation', () => { it('cancels an active prompt without closing the TUI', async () => { const mockSession = { id: 'test', getHistory: () => [], addMessage: vi.fn(), clear: vi.fn(), replaceHistory: vi.fn(), }; const tui = new MinimalTui({ session: asSession(mockSession), modelClient: asRouter({}), systemPrompt: 'test', }); let onAnswer: ((answer: string) => void) | undefined; const write = vi.fn((_: string | null, key?: { ctrl?: boolean; name?: string }) => { if (key?.name === 'return') { onAnswer?.(''); } }); minimalTuiPrivates(tui).rl = { once: vi.fn(), removeListener: vi.fn(), question: vi.fn((_text: string, cb: (answer: string) => void) => { onAnswer = cb; }), write, }; const promptPromise = minimalTuiPrivates(tui).prompt('Confirm? '); expect(minimalTuiPrivates(tui).activePromptCancel).toBeTypeOf('function'); minimalTuiPrivates(tui).activePromptCancel?.(); await expect(promptPromise).resolves.toBe(''); expect(write).toHaveBeenCalledWith(null, { ctrl: true, name: 'u' }); expect(write).toHaveBeenCalledWith(null, { name: 'return' }); expect(minimalTuiPrivates(tui).activePromptCancel).toBeNull(); }); it('uses Esc to cancel active running operation', () => { const mockSession = { id: 'test', getHistory: () => [], addMessage: vi.fn(), clear: vi.fn(), replaceHistory: vi.fn(), }; const tui = new MinimalTui({ session: asSession(mockSession), modelClient: asRouter({}), systemPrompt: 'test', }); const cancelRunningOperation = vi.fn(); minimalTuiPrivates(tui).activeOperationCancel = cancelRunningOperation; expect(minimalTuiPrivates(tui).handleEscapeAction()).toBe(true); expect(cancelRunningOperation).toHaveBeenCalledOnce(); }); });