Files
flynn/src/config/schema.test.ts
T
William Valentin c8c3c74fde feat: add per-tier fallback field to model config schema
Each model tier (fast, default, complex, local) can now specify an
optional fallback provider config that the router will try before
falling through to the global fallback chain.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 12:08:17 -08:00

218 lines
6.7 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { configSchema } from './schema.js';
describe('configSchema — sandbox', () => {
const minimalConfig = {
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
models: { default: { provider: 'anthropic', model: 'claude-3' } },
};
it('defaults sandbox to disabled', () => {
const result = configSchema.parse(minimalConfig);
expect(result.sandbox.enabled).toBe(false);
expect(result.sandbox.image).toBe('node:22-slim');
expect(result.sandbox.network).toBe('none');
expect(result.sandbox.memory_limit).toBe('512m');
expect(result.sandbox.cpu_limit).toBe('1.0');
expect(result.sandbox.timeout_seconds).toBe(300);
});
it('accepts sandbox config', () => {
const result = configSchema.parse({
...minimalConfig,
sandbox: { enabled: true, image: 'ubuntu:24.04', network: 'bridge' },
});
expect(result.sandbox.enabled).toBe(true);
expect(result.sandbox.image).toBe('ubuntu:24.04');
expect(result.sandbox.network).toBe('bridge');
});
});
describe('configSchema — agent_configs', () => {
const minimalConfig = {
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
models: { default: { provider: 'anthropic', model: 'claude-3' } },
};
it('defaults agent_configs to empty', () => {
const result = configSchema.parse(minimalConfig);
expect(result.agent_configs).toEqual({});
});
it('accepts named agent configs', () => {
const result = configSchema.parse({
...minimalConfig,
agent_configs: {
assistant: {
system_prompt: 'You are helpful.',
model_tier: 'default',
tool_profile: 'messaging',
},
coder: {
model_tier: 'complex',
tool_profile: 'coding',
sandbox: true,
},
},
});
expect(result.agent_configs.assistant.system_prompt).toBe('You are helpful.');
expect(result.agent_configs.assistant.tool_profile).toBe('messaging');
expect(result.agent_configs.coder.sandbox).toBe(true);
});
});
describe('configSchema — routing', () => {
const minimalConfig = {
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
models: { default: { provider: 'anthropic', model: 'claude-3' } },
};
it('defaults routing to empty', () => {
const result = configSchema.parse(minimalConfig);
expect(result.routing.default_agent).toBeUndefined();
expect(result.routing.channels).toEqual({});
expect(result.routing.senders).toEqual({});
});
it('accepts routing config', () => {
const result = configSchema.parse({
...minimalConfig,
routing: {
default_agent: 'assistant',
channels: { discord: 'coder' },
senders: { 'telegram:12345': 'coder' },
},
});
expect(result.routing.default_agent).toBe('assistant');
expect(result.routing.channels.discord).toBe('coder');
expect(result.routing.senders['telegram:12345']).toBe('coder');
});
});
describe('configSchema — per-tier fallback', () => {
const minimalConfig = {
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
models: { default: { provider: 'anthropic', model: 'claude-3' } },
};
it('accepts per-tier fallback config', () => {
const result = configSchema.parse({
...minimalConfig,
models: {
default: {
provider: 'anthropic',
model: 'claude-sonnet-4-5-20250929',
fallback: { provider: 'github', model: 'claude-sonnet-4-5-20250929' },
},
fast: {
provider: 'anthropic',
model: 'claude-haiku-4-5-20251001',
fallback: { provider: 'github', model: 'claude-haiku-4-5-20251001' },
},
},
});
expect(result.models.default.fallback?.provider).toBe('github');
expect(result.models.fast?.fallback?.provider).toBe('github');
});
it('works without fallback field (backward compat)', () => {
const result = configSchema.parse(minimalConfig);
expect(result.models.default.fallback).toBeUndefined();
});
it('fallback does not itself accept a nested fallback', () => {
const result = configSchema.parse({
...minimalConfig,
models: {
default: {
provider: 'anthropic',
model: 'claude-3',
fallback: {
provider: 'github',
model: 'claude-3',
fallback: { provider: 'ollama', model: 'llama' },
},
},
},
});
// Zod strips unknown keys from the base schema, so nested fallback is dropped
expect((result.models.default.fallback as Record<string, unknown>)?.fallback).toBeUndefined();
});
});
describe('configSchema automation', () => {
const baseConfig = {
telegram: { bot_token: 'test-token', allowed_chat_ids: [123] },
models: { default: { provider: 'anthropic', model: 'claude-sonnet' } },
};
it('accepts config without automation section', () => {
const result = configSchema.parse(baseConfig);
expect(result.automation).toBeDefined();
expect(result.automation.cron).toEqual([]);
});
it('accepts config with cron jobs', () => {
const result = configSchema.parse({
...baseConfig,
automation: {
cron: [{
name: 'morning-briefing',
schedule: '0 9 * * *',
message: 'Good morning!',
output: { channel: 'telegram', peer: '123' },
}],
},
});
expect(result.automation.cron).toHaveLength(1);
expect(result.automation.cron[0].name).toBe('morning-briefing');
expect(result.automation.cron[0].enabled).toBe(true); // default
});
it('rejects cron job with empty name', () => {
expect(() => configSchema.parse({
...baseConfig,
automation: {
cron: [{
name: '',
schedule: '0 9 * * *',
message: 'test',
output: { channel: 'telegram', peer: '123' },
}],
},
})).toThrow();
});
it('rejects cron job with empty schedule', () => {
expect(() => configSchema.parse({
...baseConfig,
automation: {
cron: [{
name: 'test',
schedule: '',
message: 'test',
output: { channel: 'telegram', peer: '123' },
}],
},
})).toThrow();
});
it('accepts cron job with optional fields', () => {
const result = configSchema.parse({
...baseConfig,
automation: {
cron: [{
name: 'test',
schedule: '0 9 * * *',
message: 'test',
output: { channel: 'telegram', peer: '123' },
enabled: false,
timezone: 'America/New_York',
}],
},
});
expect(result.automation.cron[0].enabled).toBe(false);
expect(result.automation.cron[0].timezone).toBe('America/New_York');
});
});