c8c3c74fde
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>
218 lines
6.7 KiB
TypeScript
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');
|
|
});
|
|
});
|