315 lines
9.8 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|