333 lines
10 KiB
TypeScript
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();
|
|
});
|
|
});
|