Files
waggle-os/packages/core/tests/config.test.ts
Oleg Maslov b20b138fe4 moving
2026-09-02 10:14:22 +02:00

315 lines
9.8 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { WaggleConfig, type ProviderEntry, type TeamServerConfig } from '../src/config.js';
describe('WaggleConfig', () => {
const tempDirs: string[] = [];
function makeTempDir(): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-config-test-'));
tempDirs.push(dir);
return dir;
}
afterEach(() => {
for (const dir of tempDirs) {
fs.rmSync(dir, { recursive: true, force: true });
}
tempDirs.length = 0;
});
it('creates config directory if missing', () => {
const base = makeTempDir();
const configDir = path.join(base, 'nested', '.waggle');
new WaggleConfig(configDir);
expect(fs.existsSync(configDir)).toBe(true);
});
it('returns default config when no file exists', () => {
const configDir = makeTempDir();
const config = new WaggleConfig(configDir);
expect(config.getDefaultModel()).toBe('claude-sonnet-4-6');
expect(config.getProviders()).toEqual({});
expect(config.getMindPath()).toBe(path.join(configDir, 'default.mind'));
});
it('saves and loads provider config', () => {
const configDir = makeTempDir();
const config = new WaggleConfig(configDir);
const provider: ProviderEntry = {
apiKey: 'sk-test-key',
models: ['claude-sonnet-4-6', 'claude-haiku-3'],
baseUrl: 'https://api.anthropic.com',
};
config.setProvider('anthropic', provider);
config.save();
// Load fresh instance from same directory
const config2 = new WaggleConfig(configDir);
const providers = config2.getProviders();
expect(providers['anthropic']).toEqual(provider);
expect(providers['anthropic'].apiKey).toBe('sk-test-key');
expect(providers['anthropic'].models).toHaveLength(2);
});
it('sets and gets default model', () => {
const configDir = makeTempDir();
const config = new WaggleConfig(configDir);
config.setDefaultModel('gpt-4o');
config.save();
const config2 = new WaggleConfig(configDir);
expect(config2.getDefaultModel()).toBe('gpt-4o');
});
it('returns mind file path', () => {
const configDir = makeTempDir();
const config = new WaggleConfig(configDir);
expect(config.getMindPath()).toBe(path.join(configDir, 'default.mind'));
});
it('removes a provider', () => {
const configDir = makeTempDir();
const config = new WaggleConfig(configDir);
config.setProvider('anthropic', { apiKey: 'key1', models: ['m1'] });
config.setProvider('openai', { apiKey: 'key2', models: ['m2'] });
config.removeProvider('anthropic');
config.save();
const config2 = new WaggleConfig(configDir);
const providers = config2.getProviders();
expect(providers['anthropic']).toBeUndefined();
expect(providers['openai']).toBeDefined();
});
it('returns config directory path', () => {
const configDir = makeTempDir();
const config = new WaggleConfig(configDir);
expect(config.getConfigDir()).toBe(configDir);
});
describe('team server config', () => {
it('returns null when no team server configured', () => {
const configDir = makeTempDir();
const config = new WaggleConfig(configDir);
expect(config.getTeamServer()).toBeNull();
expect(config.isTeamConnected()).toBe(false);
});
it('sets and gets team server config', () => {
const configDir = makeTempDir();
const config = new WaggleConfig(configDir);
const teamConfig: TeamServerConfig = {
url: 'https://team.waggle.dev',
token: 'clerk-jwt-token',
userId: 'user-123',
displayName: 'Marko',
};
config.setTeamServer(teamConfig);
config.save();
const config2 = new WaggleConfig(configDir);
const loaded = config2.getTeamServer();
expect(loaded).toEqual(teamConfig);
expect(config2.isTeamConnected()).toBe(true);
});
it('clears team server config', () => {
const configDir = makeTempDir();
const config = new WaggleConfig(configDir);
config.setTeamServer({ url: 'https://team.waggle.dev' });
config.clearTeamServer();
config.save();
const config2 = new WaggleConfig(configDir);
expect(config2.getTeamServer()).toBeNull();
expect(config2.isTeamConnected()).toBe(false);
});
it('persists team server through save/load cycle', () => {
const configDir = makeTempDir();
const config = new WaggleConfig(configDir);
config.setTeamServer({ url: 'https://example.com', userId: 'u1' });
config.save();
const config2 = new WaggleConfig(configDir);
expect(config2.getTeamServer()!.url).toBe('https://example.com');
expect(config2.getTeamServer()!.userId).toBe('u1');
});
});
describe('governed CLI config', () => {
it('normalizes, deduplicates, and persists the CLI allowlist', () => {
const configDir = makeTempDir();
const config = new WaggleConfig(configDir);
config.setCliAllowlist([' node ', 'NODE', '', 'git']);
expect(config.getCliAllowlist()).toEqual(['node', 'git']);
config.save();
const config2 = new WaggleConfig(configDir);
expect(config2.getCliAllowlist()).toEqual(['node', 'git']);
});
});
describe('embedding config', () => {
const envNames = [
'EMBEDDING_PROVIDER',
'EMBEDDING_MODEL',
'OLLAMA_HOST',
'OLLAMA_EMBED_MODEL',
] as const;
const originalEnv = new Map<string, string | undefined>();
beforeEach(() => {
originalEnv.clear();
for (const name of envNames) {
originalEnv.set(name, process.env[name]);
delete process.env[name];
}
});
afterEach(() => {
for (const name of envNames) {
const value = originalEnv.get(name);
if (value === undefined) delete process.env[name];
else process.env[name] = value;
}
});
it.each(['', ' '])('treats blank provider override %j as unset', override => {
process.env.EMBEDDING_PROVIDER = override;
const config = new WaggleConfig(makeTempDir());
expect(config.getEmbeddingConfig().provider).toBe('auto');
config.setEmbeddingProvider('inprocess');
expect(config.getEmbeddingConfig().provider).toBe('inprocess');
});
it('trims a valid provider override and keeps it authoritative', () => {
process.env.EMBEDDING_PROVIDER = ' inprocess ';
const config = new WaggleConfig(makeTempDir());
config.setEmbeddingProvider('ollama');
expect(config.getEmbeddingConfig().provider).toBe('inprocess');
});
it('treats blank embedding model and Ollama overrides as unset', () => {
const configDir = makeTempDir();
fs.writeFileSync(path.join(configDir, 'config.json'), JSON.stringify({
defaultModel: 'claude-sonnet-4-6',
providers: {},
embedding: {
provider: 'auto',
inprocessModel: 'persisted-inprocess-model',
ollamaUrl: 'http://127.0.0.1:11434',
ollamaModel: 'persisted-ollama-model',
},
}));
process.env.EMBEDDING_MODEL = ' ';
process.env.OLLAMA_HOST = '';
process.env.OLLAMA_EMBED_MODEL = ' ';
expect(new WaggleConfig(configDir).getEmbeddingConfig()).toMatchObject({
inprocess: { model: 'persisted-inprocess-model' },
ollama: {
baseUrl: 'http://127.0.0.1:11434',
model: 'persisted-ollama-model',
},
});
});
});
describe('Model Pilot config fields', () => {
let tmpDir: string;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-config-pilot-'));
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it('returns null for fallbackModel when not set', () => {
const config = new WaggleConfig(tmpDir);
expect(config.getFallbackModel()).toBeNull();
});
it('persists fallbackModel', () => {
const config = new WaggleConfig(tmpDir);
config.setFallbackModel('qwen/qwen3.6-plus:free');
config.save();
const config2 = new WaggleConfig(tmpDir);
expect(config2.getFallbackModel()).toBe('qwen/qwen3.6-plus:free');
});
it('returns null for budgetModel when not set', () => {
const config = new WaggleConfig(tmpDir);
expect(config.getBudgetModel()).toBeNull();
});
it('persists budgetModel', () => {
const config = new WaggleConfig(tmpDir);
config.setBudgetModel('deepseek/deepseek-chat-v3-0324:free');
config.save();
const config2 = new WaggleConfig(tmpDir);
expect(config2.getBudgetModel()).toBe('deepseek/deepseek-chat-v3-0324:free');
});
it('returns 0.8 as default budgetThreshold', () => {
const config = new WaggleConfig(tmpDir);
expect(config.getBudgetThreshold()).toBe(0.8);
});
it('persists budgetThreshold', () => {
const config = new WaggleConfig(tmpDir);
config.setBudgetThreshold(0.6);
config.save();
const config2 = new WaggleConfig(tmpDir);
expect(config2.getBudgetThreshold()).toBe(0.6);
});
it('clearFallbackModel removes the field', () => {
const config = new WaggleConfig(tmpDir);
config.setFallbackModel('test-model');
config.save();
config.clearFallbackModel();
config.save();
const config2 = new WaggleConfig(tmpDir);
expect(config2.getFallbackModel()).toBeNull();
});
it('returns 90 as default maxIterations', () => {
const config = new WaggleConfig(tmpDir);
expect(config.getMaxIterations()).toBe(90);
});
it('persists maxIterations', () => {
const config = new WaggleConfig(tmpDir);
config.setMaxIterations(50);
config.save();
const config2 = new WaggleConfig(tmpDir);
expect(config2.getMaxIterations()).toBe(50);
});
});
});