Files
flynn/src/frontends/tui/minimal.test.ts
T

333 lines
10 KiB
TypeScript

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<ModelRouter, 'getTier' | 'getAvailableTiers' | 'setTier' | 'getLabel'> &
Partial<ModelRouter> &
Partial<ModelClient> & {
getLocalProviderName: () => string | undefined;
setLocalClient: ReturnType<typeof vi.fn>;
};
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<void>;
handleModelCommand: (tier: string, providerModel?: string) => void;
prompt: (text: string) => Promise<string>;
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;
} {
return value as unknown as {
handleBackendCommand: (provider: string) => Promise<void>;
handleModelCommand: (tier: string, providerModel?: string) => void;
prompt: (text: string) => Promise<string>;
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;
};
}
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<string, ModelConfig> = {
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 <tier> <provider/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 <tier> <provider/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();
});
});