970 lines
30 KiB
TypeScript
970 lines
30 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 — server', () => {
|
|
const minimalConfig = {
|
|
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
|
|
models: { default: { provider: 'anthropic', model: 'claude-3' } },
|
|
};
|
|
|
|
it('defaults max_request_body_bytes', () => {
|
|
const result = configSchema.parse(minimalConfig);
|
|
expect(result.server.max_request_body_bytes).toBe(1_048_576);
|
|
});
|
|
|
|
it('accepts custom max_request_body_bytes', () => {
|
|
const result = configSchema.parse({
|
|
...minimalConfig,
|
|
server: { max_request_body_bytes: 2048 },
|
|
});
|
|
expect(result.server.max_request_body_bytes).toBe(2048);
|
|
});
|
|
|
|
it('defaults ws_rate_limit settings', () => {
|
|
const result = configSchema.parse(minimalConfig);
|
|
expect(result.server.ws_rate_limit.enabled).toBe(true);
|
|
expect(result.server.ws_rate_limit.capacity).toBe(30);
|
|
expect(result.server.ws_rate_limit.refill_per_sec).toBe(15);
|
|
expect(result.server.ws_rate_limit.max_violations).toBe(8);
|
|
expect(result.server.ws_rate_limit.violation_window_ms).toBe(10_000);
|
|
});
|
|
|
|
it('accepts custom ws_rate_limit settings', () => {
|
|
const result = configSchema.parse({
|
|
...minimalConfig,
|
|
server: {
|
|
ws_rate_limit: {
|
|
enabled: true,
|
|
capacity: 5,
|
|
refill_per_sec: 2,
|
|
max_violations: 3,
|
|
violation_window_ms: 2000,
|
|
},
|
|
},
|
|
});
|
|
expect(result.server.ws_rate_limit.capacity).toBe(5);
|
|
expect(result.server.ws_rate_limit.refill_per_sec).toBe(2);
|
|
expect(result.server.ws_rate_limit.max_violations).toBe(3);
|
|
expect(result.server.ws_rate_limit.violation_window_ms).toBe(2000);
|
|
});
|
|
|
|
it('defaults queue settings', () => {
|
|
const result = configSchema.parse(minimalConfig);
|
|
expect(result.server.queue.mode).toBe('collect');
|
|
expect(result.server.queue.cap).toBe(50);
|
|
expect(result.server.queue.overflow).toBe('drop_old');
|
|
expect(result.server.queue.debounce_ms).toBe(0);
|
|
expect(result.server.queue.summarize_overflow).toBe(true);
|
|
expect(result.server.queue.overrides.channels).toEqual({});
|
|
expect(result.server.queue.overrides.sessions).toEqual({});
|
|
});
|
|
|
|
it('accepts custom queue settings', () => {
|
|
const result = configSchema.parse({
|
|
...minimalConfig,
|
|
server: {
|
|
queue: {
|
|
mode: 'steer_backlog',
|
|
cap: 10,
|
|
overflow: 'drop_new',
|
|
debounce_ms: 250,
|
|
summarize_overflow: false,
|
|
},
|
|
},
|
|
});
|
|
expect(result.server.queue.mode).toBe('steer_backlog');
|
|
expect(result.server.queue.cap).toBe(10);
|
|
expect(result.server.queue.overflow).toBe('drop_new');
|
|
expect(result.server.queue.debounce_ms).toBe(250);
|
|
expect(result.server.queue.summarize_overflow).toBe(false);
|
|
});
|
|
|
|
it('accepts queue override settings', () => {
|
|
const result = configSchema.parse({
|
|
...minimalConfig,
|
|
server: {
|
|
queue: {
|
|
overrides: {
|
|
channels: {
|
|
ws: { mode: 'collect', cap: 5 },
|
|
},
|
|
sessions: {
|
|
'ws:vip-user': { mode: 'interrupt', overflow: 'drop_new', debounce_ms: 1000, summarize_overflow: false },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
expect(result.server.queue.overrides.channels.ws.mode).toBe('collect');
|
|
expect(result.server.queue.overrides.channels.ws.cap).toBe(5);
|
|
expect(result.server.queue.overrides.sessions['ws:vip-user'].mode).toBe('interrupt');
|
|
expect(result.server.queue.overrides.sessions['ws:vip-user'].overflow).toBe('drop_new');
|
|
expect(result.server.queue.overrides.sessions['ws:vip-user'].debounce_ms).toBe(1000);
|
|
expect(result.server.queue.overrides.sessions['ws:vip-user'].summarize_overflow).toBe(false);
|
|
});
|
|
|
|
it('defaults discovery settings', () => {
|
|
const result = configSchema.parse(minimalConfig);
|
|
expect(result.server.discovery.enabled).toBe(false);
|
|
expect(result.server.discovery.service_name).toBe('flynn-gateway');
|
|
expect(result.server.discovery.service_type).toBe('_flynn._tcp');
|
|
expect(result.server.discovery.txt).toEqual({});
|
|
});
|
|
|
|
it('defaults node policy settings', () => {
|
|
const result = configSchema.parse(minimalConfig);
|
|
expect(result.server.nodes.enabled).toBe(false);
|
|
expect(result.server.nodes.allowed_roles).toEqual(['companion']);
|
|
expect(result.server.nodes.feature_gates).toEqual({});
|
|
expect(result.server.nodes.location.enabled).toBe(false);
|
|
});
|
|
|
|
it('accepts custom node policy settings', () => {
|
|
const result = configSchema.parse({
|
|
...minimalConfig,
|
|
server: {
|
|
nodes: {
|
|
enabled: true,
|
|
allowed_roles: ['companion', 'observer'],
|
|
feature_gates: {
|
|
'ui.canvas': true,
|
|
'fs.sync': false,
|
|
},
|
|
location: {
|
|
enabled: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
expect(result.server.nodes.enabled).toBe(true);
|
|
expect(result.server.nodes.allowed_roles).toEqual(['companion', 'observer']);
|
|
expect(result.server.nodes.feature_gates['ui.canvas']).toBe(true);
|
|
expect(result.server.nodes.feature_gates['fs.sync']).toBe(false);
|
|
expect(result.server.nodes.location.enabled).toBe(true);
|
|
});
|
|
|
|
it('accepts custom discovery settings', () => {
|
|
const result = configSchema.parse({
|
|
...minimalConfig,
|
|
server: {
|
|
discovery: {
|
|
enabled: true,
|
|
service_name: 'flynn-dev',
|
|
service_type: '_custom._tcp',
|
|
txt: {
|
|
env: 'dev',
|
|
},
|
|
},
|
|
},
|
|
});
|
|
expect(result.server.discovery.enabled).toBe(true);
|
|
expect(result.server.discovery.service_name).toBe('flynn-dev');
|
|
expect(result.server.discovery.service_type).toBe('_custom._tcp');
|
|
expect(result.server.discovery.txt).toEqual({ env: 'dev' });
|
|
});
|
|
});
|
|
|
|
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 — models auth_mode', () => {
|
|
const minimalConfig = {
|
|
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
|
|
models: { default: { provider: 'anthropic', model: 'claude-3' } },
|
|
};
|
|
|
|
it('accepts auth_mode values per tier', () => {
|
|
const result = configSchema.parse({
|
|
...minimalConfig,
|
|
models: {
|
|
default: { provider: 'openai', model: 'gpt-4o', auth_mode: 'api_key' },
|
|
fast: { provider: 'openai', model: 'gpt-4o-mini', auth_mode: 'oauth' },
|
|
},
|
|
});
|
|
|
|
expect(result.models.default.auth_mode).toBe('api_key');
|
|
expect(result.models.fast?.auth_mode).toBe('oauth');
|
|
});
|
|
|
|
it('rejects invalid auth_mode values', () => {
|
|
expect(() => {
|
|
configSchema.parse({
|
|
...minimalConfig,
|
|
models: {
|
|
default: { provider: 'openai', model: 'gpt-4o', auth_mode: 'bogus' },
|
|
},
|
|
});
|
|
}).toThrow(/auth_mode/i);
|
|
});
|
|
|
|
it('accepts vercel provider id', () => {
|
|
const result = configSchema.parse({
|
|
...minimalConfig,
|
|
models: {
|
|
default: { provider: 'vercel', model: 'openai/gpt-4.1', endpoint: 'https://ai-gateway.vercel.sh/v1', api_key: 'test-key' },
|
|
},
|
|
});
|
|
expect(result.models.default.provider).toBe('vercel');
|
|
});
|
|
|
|
it('accepts minimax and moonshot provider ids', () => {
|
|
const minimax = configSchema.parse({
|
|
...minimalConfig,
|
|
models: {
|
|
default: { provider: 'minimax', model: 'MiniMax-M1', api_key: 'test-key' },
|
|
},
|
|
});
|
|
expect(minimax.models.default.provider).toBe('minimax');
|
|
|
|
const moonshot = configSchema.parse({
|
|
...minimalConfig,
|
|
models: {
|
|
default: { provider: 'moonshot', model: 'moonshot-v1-8k', api_key: 'test-key' },
|
|
},
|
|
});
|
|
expect(moonshot.models.default.provider).toBe('moonshot');
|
|
});
|
|
});
|
|
|
|
describe('configSchema — matrix', () => {
|
|
const minimalConfig = {
|
|
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
|
|
models: { default: { provider: 'anthropic', model: 'claude-3' } },
|
|
};
|
|
|
|
it('accepts matrix config and defaults sync_timeout_ms', () => {
|
|
const result = configSchema.parse({
|
|
...minimalConfig,
|
|
matrix: {
|
|
homeserver_url: 'https://matrix.example.org',
|
|
access_token: 'syt_test_token',
|
|
allowed_room_ids: ['!room1:example.org'],
|
|
require_mention: true,
|
|
},
|
|
});
|
|
|
|
expect(result.matrix).toBeDefined();
|
|
if (!result.matrix) {
|
|
throw new Error('Expected matrix config');
|
|
}
|
|
expect(result.matrix.homeserver_url).toBe('https://matrix.example.org');
|
|
expect(result.matrix.access_token).toBe('syt_test_token');
|
|
expect(result.matrix.sync_timeout_ms).toBe(30000);
|
|
});
|
|
|
|
it('matrix config is optional', () => {
|
|
const result = configSchema.parse(minimalConfig);
|
|
expect(result.matrix).toBeUndefined();
|
|
});
|
|
|
|
it('rejects invalid homeserver_url', () => {
|
|
expect(() => {
|
|
configSchema.parse({
|
|
...minimalConfig,
|
|
matrix: {
|
|
homeserver_url: 'not-a-url',
|
|
access_token: 'token',
|
|
},
|
|
});
|
|
}).toThrow(/homeserver/i);
|
|
});
|
|
});
|
|
|
|
describe('configSchema — signal', () => {
|
|
const minimalConfig = {
|
|
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
|
|
models: { default: { provider: 'anthropic', model: 'claude-3' } },
|
|
};
|
|
|
|
it('accepts signal config and defaults polling fields', () => {
|
|
const result = configSchema.parse({
|
|
...minimalConfig,
|
|
signal: {
|
|
account: '+15551234567',
|
|
},
|
|
});
|
|
|
|
expect(result.signal).toBeDefined();
|
|
if (!result.signal) {
|
|
throw new Error('Expected signal config');
|
|
}
|
|
expect(result.signal.account).toBe('+15551234567');
|
|
expect(result.signal.signal_cli_path).toBe('signal-cli');
|
|
expect(result.signal.poll_interval_ms).toBe(5000);
|
|
expect(result.signal.send_timeout_ms).toBe(15000);
|
|
expect(result.signal.require_mention).toBe(true);
|
|
});
|
|
|
|
it('signal config is optional', () => {
|
|
const result = configSchema.parse(minimalConfig);
|
|
expect(result.signal).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('configSchema — audio talk mode', () => {
|
|
const minimalConfig = {
|
|
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
|
|
models: { default: { provider: 'anthropic', model: 'claude-3' } },
|
|
};
|
|
|
|
it('defaults talk_mode fields', () => {
|
|
const result = configSchema.parse(minimalConfig);
|
|
expect(result.audio.talk_mode.enabled).toBe(false);
|
|
expect(result.audio.talk_mode.wake_phrase).toBe('hey flynn');
|
|
expect(result.audio.talk_mode.timeout_ms).toBe(120000);
|
|
expect(result.audio.talk_mode.allow_manual_toggle).toBe(true);
|
|
});
|
|
|
|
it('accepts custom talk_mode settings', () => {
|
|
const result = configSchema.parse({
|
|
...minimalConfig,
|
|
audio: {
|
|
talk_mode: {
|
|
enabled: true,
|
|
wake_phrase: 'ok flynn',
|
|
timeout_ms: 300000,
|
|
allow_manual_toggle: false,
|
|
},
|
|
},
|
|
});
|
|
expect(result.audio.talk_mode.enabled).toBe(true);
|
|
expect(result.audio.talk_mode.wake_phrase).toBe('ok flynn');
|
|
expect(result.audio.talk_mode.timeout_ms).toBe(300000);
|
|
expect(result.audio.talk_mode.allow_manual_toggle).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('configSchema — mattermost', () => {
|
|
const minimalConfig = {
|
|
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
|
|
models: { default: { provider: 'anthropic', model: 'claude-3' } },
|
|
};
|
|
|
|
it('accepts mattermost config and defaults optional fields', () => {
|
|
const result = configSchema.parse({
|
|
...minimalConfig,
|
|
mattermost: {
|
|
server_url: 'https://mattermost.example.com',
|
|
bot_token: 'mm-token',
|
|
},
|
|
});
|
|
|
|
expect(result.mattermost).toBeDefined();
|
|
if (!result.mattermost) {
|
|
throw new Error('Expected mattermost config');
|
|
}
|
|
expect(result.mattermost.server_url).toBe('https://mattermost.example.com');
|
|
expect(result.mattermost.allowed_channel_ids).toEqual([]);
|
|
expect(result.mattermost.require_mention).toBe(true);
|
|
expect(result.mattermost.mention_name).toBe('flynn');
|
|
expect(result.mattermost.poll_interval_ms).toBe(3000);
|
|
});
|
|
|
|
it('mattermost config is optional', () => {
|
|
const result = configSchema.parse(minimalConfig);
|
|
expect(result.mattermost).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('configSchema — teams', () => {
|
|
const minimalConfig = {
|
|
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
|
|
models: { default: { provider: 'anthropic', model: 'claude-3' } },
|
|
};
|
|
|
|
it('accepts teams config and defaults allowlist/mention fields', () => {
|
|
const result = configSchema.parse({
|
|
...minimalConfig,
|
|
teams: {
|
|
app_id: 'app-id',
|
|
app_password: 'app-password',
|
|
},
|
|
});
|
|
|
|
expect(result.teams).toBeDefined();
|
|
if (!result.teams) {
|
|
throw new Error('Expected teams config');
|
|
}
|
|
expect(result.teams.app_id).toBe('app-id');
|
|
expect(result.teams.allowed_conversation_ids).toEqual([]);
|
|
expect(result.teams.require_mention).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('configSchema — google_chat', () => {
|
|
const minimalConfig = {
|
|
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
|
|
models: { default: { provider: 'anthropic', model: 'claude-3' } },
|
|
};
|
|
|
|
it('accepts google_chat config and defaults filters', () => {
|
|
const result = configSchema.parse({
|
|
...minimalConfig,
|
|
google_chat: {
|
|
service_account_key_file: '/tmp/gchat-service-account.json',
|
|
},
|
|
});
|
|
|
|
expect(result.google_chat).toBeDefined();
|
|
if (!result.google_chat) {
|
|
throw new Error('Expected google_chat config');
|
|
}
|
|
expect(result.google_chat.service_account_key_file).toBe('/tmp/gchat-service-account.json');
|
|
expect(result.google_chat.allowed_space_names).toEqual([]);
|
|
expect(result.google_chat.require_mention).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('configSchema — bluebubbles', () => {
|
|
const minimalConfig = {
|
|
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
|
|
models: { default: { provider: 'anthropic', model: 'claude-3' } },
|
|
};
|
|
|
|
it('accepts bluebubbles config and defaults optional fields', () => {
|
|
const result = configSchema.parse({
|
|
...minimalConfig,
|
|
bluebubbles: {
|
|
endpoint: 'http://localhost:1234',
|
|
api_key: 'bb-key',
|
|
},
|
|
});
|
|
|
|
expect(result.bluebubbles).toBeDefined();
|
|
if (!result.bluebubbles) {
|
|
throw new Error('Expected bluebubbles config');
|
|
}
|
|
expect(result.bluebubbles.allowed_chat_guids).toEqual([]);
|
|
expect(result.bluebubbles.require_mention).toBe(true);
|
|
expect(result.bluebubbles.mention_name).toBe('flynn');
|
|
});
|
|
});
|
|
|
|
describe('configSchema — whatsapp', () => {
|
|
const minimalConfig = {
|
|
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
|
|
models: { default: { provider: 'anthropic', model: 'claude-3' } },
|
|
};
|
|
|
|
it('defaults whatsapp no_sandbox to false', () => {
|
|
const result = configSchema.parse({
|
|
...minimalConfig,
|
|
whatsapp: {},
|
|
});
|
|
expect(result.whatsapp?.no_sandbox).toBe(false);
|
|
});
|
|
|
|
it('accepts whatsapp no_sandbox override', () => {
|
|
const result = configSchema.parse({
|
|
...minimalConfig,
|
|
whatsapp: { no_sandbox: true },
|
|
});
|
|
expect(result.whatsapp?.no_sandbox).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('configSchema — skills watcher', () => {
|
|
const minimalConfig = {
|
|
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
|
|
models: { default: { provider: 'anthropic', model: 'claude-3' } },
|
|
};
|
|
|
|
it('defaults skills watcher settings', () => {
|
|
const result = configSchema.parse(minimalConfig);
|
|
expect(result.skills.load.watch).toBe(false);
|
|
expect(result.skills.load.watch_debounce_ms).toBe(250);
|
|
expect(result.skills.installation_execution).toBe('disabled');
|
|
expect(result.skills.allow_shell_runner).toBe(false);
|
|
expect(result.skills.shell_runner_allowlist).toEqual([]);
|
|
expect(result.skills.shell_runner_governance.owner).toBeUndefined();
|
|
expect(result.skills.shell_runner_governance.review_cadence_days).toBe(7);
|
|
expect(result.skills.shell_runner_governance.promotion_min_success_rate).toBe(0.9);
|
|
});
|
|
|
|
it('accepts explicit installation execution policy', () => {
|
|
const enabled = configSchema.parse({
|
|
...minimalConfig,
|
|
skills: {
|
|
installation_execution: 'enabled',
|
|
allow_shell_runner: true,
|
|
shell_runner_allowlist: ['npm install*'],
|
|
shell_runner_governance: {
|
|
owner: 'skills-team',
|
|
review_cadence_days: 14,
|
|
promotion_min_success_rate: 0.95,
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(enabled.skills.installation_execution).toBe('enabled');
|
|
expect(enabled.skills.allow_shell_runner).toBe(true);
|
|
expect(enabled.skills.shell_runner_allowlist).toEqual(['npm install*']);
|
|
expect(enabled.skills.shell_runner_governance.owner).toBe('skills-team');
|
|
expect(enabled.skills.shell_runner_governance.review_cadence_days).toBe(14);
|
|
expect(enabled.skills.shell_runner_governance.promotion_min_success_rate).toBe(0.95);
|
|
});
|
|
|
|
it('accepts explicit watcher settings', () => {
|
|
const result = configSchema.parse({
|
|
...minimalConfig,
|
|
skills: {
|
|
load: {
|
|
watch: true,
|
|
watch_debounce_ms: 500,
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(result.skills.load.watch).toBe(true);
|
|
expect(result.skills.load.watch_debounce_ms).toBe(500);
|
|
});
|
|
});
|
|
|
|
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.delivery_mode).toBe('shared_session');
|
|
expect(result.automation.cron).toEqual([]);
|
|
});
|
|
|
|
it('accepts isolated automation delivery mode', () => {
|
|
const result = configSchema.parse({
|
|
...baseConfig,
|
|
automation: {
|
|
delivery_mode: 'isolated_job',
|
|
},
|
|
});
|
|
expect(result.automation.delivery_mode).toBe('isolated_job');
|
|
});
|
|
|
|
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');
|
|
});
|
|
});
|
|
|
|
describe('configSchema — intents', () => {
|
|
const minimalConfig = {
|
|
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
|
|
models: { default: { provider: 'anthropic', model: 'claude-3' } },
|
|
};
|
|
|
|
it('defaults intents to disabled with no rules', () => {
|
|
const result = configSchema.parse(minimalConfig);
|
|
expect(result.intents.enabled).toBe(false);
|
|
expect(result.intents.rules).toEqual([]);
|
|
});
|
|
|
|
it('accepts intent rule config', () => {
|
|
const result = configSchema.parse({
|
|
...minimalConfig,
|
|
intents: {
|
|
enabled: true,
|
|
match_threshold: 0.6,
|
|
rules: [
|
|
{
|
|
name: 'deploy-rule',
|
|
patterns: ['deploy *'],
|
|
target: { type: 'agent', name: 'coder' },
|
|
priority: 5,
|
|
enabled: true,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
expect(result.intents.enabled).toBe(true);
|
|
expect(result.intents.rules[0].target.type).toBe('agent');
|
|
expect(result.intents.rules[0].target.name).toBe('coder');
|
|
});
|
|
});
|
|
|
|
describe('configSchema — routing_policy', () => {
|
|
const minimalConfig = {
|
|
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
|
|
models: { default: { provider: 'anthropic', model: 'claude-3' } },
|
|
};
|
|
|
|
it('defaults routing_policy values', () => {
|
|
const result = configSchema.parse(minimalConfig);
|
|
expect(result.routing_policy.enabled).toBe(false);
|
|
expect(result.routing_policy.fast_path_threshold).toBe(0.85);
|
|
expect(result.routing_policy.llm_threshold).toBe(0.5);
|
|
expect(result.routing_policy.default_path).toBe('llm');
|
|
});
|
|
});
|
|
|
|
describe('configSchema — history_index', () => {
|
|
const minimalConfig = {
|
|
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
|
|
models: { default: { provider: 'anthropic', model: 'claude-3' } },
|
|
};
|
|
|
|
it('defaults history indexing config', () => {
|
|
const result = configSchema.parse(minimalConfig);
|
|
expect(result.history_index.enabled).toBe(false);
|
|
expect(result.history_index.max_keywords).toBe(8);
|
|
expect(result.history_index.search_limit).toBe(10);
|
|
});
|
|
});
|
|
|
|
describe('configSchema — memory injection strategy', () => {
|
|
const minimalConfig = {
|
|
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
|
|
models: { default: { provider: 'anthropic', model: 'claude-3' } },
|
|
};
|
|
|
|
it('defaults memory injection settings', () => {
|
|
const result = configSchema.parse(minimalConfig);
|
|
expect(result.memory.injection_strategy).toBe('all');
|
|
expect(result.memory.max_injection_tokens).toBe(2000);
|
|
expect(result.memory.qmd.enabled).toBe(false);
|
|
expect(result.memory.qmd.top_k).toBe(8);
|
|
expect(result.memory.qmd.min_score).toBe(0.15);
|
|
});
|
|
|
|
it('accepts adaptive memory injection settings', () => {
|
|
const result = configSchema.parse({
|
|
...minimalConfig,
|
|
memory: {
|
|
injection_strategy: 'adaptive',
|
|
max_injection_tokens: 1200,
|
|
},
|
|
});
|
|
expect(result.memory.injection_strategy).toBe('adaptive');
|
|
expect(result.memory.max_injection_tokens).toBe(1200);
|
|
});
|
|
|
|
it('accepts qmd backend settings', () => {
|
|
const result = configSchema.parse({
|
|
...minimalConfig,
|
|
memory: {
|
|
qmd: {
|
|
enabled: true,
|
|
top_k: 12,
|
|
min_score: 0.2,
|
|
},
|
|
},
|
|
});
|
|
expect(result.memory.qmd.enabled).toBe(true);
|
|
expect(result.memory.qmd.top_k).toBe(12);
|
|
expect(result.memory.qmd.min_score).toBe(0.2);
|
|
});
|
|
});
|
|
|
|
describe('configSchema — compaction importance threshold', () => {
|
|
const minimalConfig = {
|
|
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
|
|
models: { default: { provider: 'anthropic', model: 'claude-3' } },
|
|
};
|
|
|
|
it('defaults compaction importance threshold to disabled behavior', () => {
|
|
const result = configSchema.parse(minimalConfig);
|
|
expect(result.compaction.importance_threshold).toBe(1);
|
|
});
|
|
|
|
it('accepts a custom importance threshold', () => {
|
|
const result = configSchema.parse({
|
|
...minimalConfig,
|
|
compaction: {
|
|
importance_threshold: 0.5,
|
|
},
|
|
});
|
|
expect(result.compaction.importance_threshold).toBe(0.5);
|
|
});
|
|
});
|
|
|
|
describe('configSchema — prompt context level', () => {
|
|
const minimalConfig = {
|
|
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
|
|
models: { default: { provider: 'anthropic', model: 'claude-3' } },
|
|
};
|
|
|
|
it('defaults prompt.context_level to normal', () => {
|
|
const result = configSchema.parse(minimalConfig);
|
|
expect(result.prompt.context_level).toBe('normal');
|
|
});
|
|
|
|
it('accepts valid context levels', () => {
|
|
const result = configSchema.parse({
|
|
...minimalConfig,
|
|
prompt: {
|
|
context_level: 'debug',
|
|
},
|
|
});
|
|
|
|
expect(result.prompt.context_level).toBe('debug');
|
|
});
|
|
|
|
it('rejects invalid context levels', () => {
|
|
expect(() => configSchema.parse({
|
|
...minimalConfig,
|
|
prompt: {
|
|
context_level: 'verbose',
|
|
},
|
|
})).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('configSchema — agents truthfulness/autonomy', () => {
|
|
const minimalConfig = {
|
|
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
|
|
models: { default: { provider: 'anthropic', model: 'claude-3' } },
|
|
};
|
|
|
|
it('defaults to standard truthfulness and autonomy', () => {
|
|
const result = configSchema.parse(minimalConfig);
|
|
expect(result.agents.truthfulness_mode).toBe('standard');
|
|
expect(result.agents.autonomy_level).toBe('standard');
|
|
});
|
|
|
|
it('accepts explicit truthfulness and autonomy modes', () => {
|
|
const result = configSchema.parse({
|
|
...minimalConfig,
|
|
agents: {
|
|
truthfulness_mode: 'strict',
|
|
autonomy_level: 'conservative',
|
|
},
|
|
});
|
|
|
|
expect(result.agents.truthfulness_mode).toBe('strict');
|
|
expect(result.agents.autonomy_level).toBe('conservative');
|
|
});
|
|
|
|
it('rejects invalid truthfulness_mode', () => {
|
|
expect(() => configSchema.parse({
|
|
...minimalConfig,
|
|
agents: {
|
|
truthfulness_mode: 'always',
|
|
},
|
|
})).toThrow();
|
|
});
|
|
|
|
it('rejects invalid autonomy_level', () => {
|
|
expect(() => configSchema.parse({
|
|
...minimalConfig,
|
|
agents: {
|
|
autonomy_level: 'manual',
|
|
},
|
|
})).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('configSchema — skills registry source', () => {
|
|
const minimalConfig = {
|
|
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
|
|
models: { default: { provider: 'anthropic', model: 'claude-3' } },
|
|
};
|
|
|
|
it('accepts skills.registry_source when provided', () => {
|
|
const result = configSchema.parse({
|
|
...minimalConfig,
|
|
skills: {
|
|
registry_source: 'https://registry.example/catalog.json',
|
|
},
|
|
});
|
|
|
|
expect(result.skills.registry_source).toBe('https://registry.example/catalog.json');
|
|
});
|
|
});
|