import { describe, it, expect, afterEach } from 'vitest'; import { loadConfigSafe, redactSecrets, getConfigPath, getDataDir } from './shared.js'; import { writeFileSync, mkdirSync, rmSync } from 'fs'; import { join } from 'path'; import { tmpdir } from 'os'; describe('CLI shared utilities', () => { const testDir = join(tmpdir(), 'flynn-test-cli-shared'); afterEach(() => { try { rmSync(testDir, { recursive: true }); } catch {} }); describe('getConfigPath', () => { it('returns FLYNN_CONFIG env var if set', () => { const original = process.env.FLYNN_CONFIG; process.env.FLYNN_CONFIG = '/custom/path.yaml'; expect(getConfigPath()).toBe('/custom/path.yaml'); if (original !== undefined) { process.env.FLYNN_CONFIG = original; } else { delete process.env.FLYNN_CONFIG; } }); it('returns default path if env var not set', () => { const original = process.env.FLYNN_CONFIG; delete process.env.FLYNN_CONFIG; const path = getConfigPath(); expect(path).toContain('.config/flynn/config.yaml'); if (original !== undefined) { process.env.FLYNN_CONFIG = original; } }); }); describe('loadConfigSafe', () => { it('returns config on success', () => { mkdirSync(testDir, { recursive: true }); const configPath = join(testDir, 'config.yaml'); writeFileSync(configPath, ` telegram: bot_token: "test-token" allowed_chat_ids: [123] models: default: provider: anthropic model: claude-sonnet `); const result = loadConfigSafe(configPath); expect(result.config).toBeDefined(); expect(result.error).toBeUndefined(); const config = result.config; if (!config) { throw new Error('Expected loaded config'); } expect(config.telegram?.bot_token).toBe('test-token'); }); it('loads env vars from FLYNN_ENV_FILE before parsing config', () => { const prevEnvFile = process.env.FLYNN_ENV_FILE; const prevToken = process.env.TEST_BOT_TOKEN; delete process.env.TEST_BOT_TOKEN; mkdirSync(testDir, { recursive: true }); const envPath = join(testDir, 'cloud.env'); const configPath = join(testDir, 'config.yaml'); writeFileSync(envPath, 'TEST_BOT_TOKEN=test-token\n'); process.env.FLYNN_ENV_FILE = envPath; writeFileSync(configPath, ` telegram: bot_token: \${TEST_BOT_TOKEN} allowed_chat_ids: [123] models: default: provider: anthropic model: claude-sonnet `); const result = loadConfigSafe(configPath); expect(result.config).toBeDefined(); expect(result.error).toBeUndefined(); const config = result.config; if (!config) { throw new Error('Expected loaded config'); } expect(config.telegram?.bot_token).toBe('test-token'); if (prevEnvFile !== undefined) { process.env.FLYNN_ENV_FILE = prevEnvFile; } else { delete process.env.FLYNN_ENV_FILE; } if (prevToken !== undefined) { process.env.TEST_BOT_TOKEN = prevToken; } else { delete process.env.TEST_BOT_TOKEN; } }); it('returns error when file not found', () => { const result = loadConfigSafe('/nonexistent/config.yaml'); expect(result.config).toBeUndefined(); expect(result.error).toBeDefined(); }); it('returns error on invalid YAML', () => { mkdirSync(testDir, { recursive: true }); const configPath = join(testDir, 'bad.yaml'); writeFileSync(configPath, '{{{{invalid yaml'); const result = loadConfigSafe(configPath); expect(result.config).toBeUndefined(); expect(result.error).toBeDefined(); }); }); describe('redactSecrets', () => { it('redacts bot_token', () => { const config = { telegram: { bot_token: 'secret-token-123', allowed_chat_ids: [123] }, models: { default: { provider: 'anthropic', model: 'claude' } }, }; const redacted = redactSecrets(config); const telegram = redacted.telegram as Record; expect(telegram.bot_token).toBe('***'); expect(telegram.allowed_chat_ids).toEqual([123]); }); it('redacts api_key in models', () => { const config = { telegram: { bot_token: 'token', allowed_chat_ids: [123] }, models: { default: { provider: 'anthropic', model: 'claude', api_key: 'sk-secret' }, }, }; const redacted = redactSecrets(config); const models = redacted.models as Record>; expect(models.default.api_key).toBe('***'); }); it('redacts access_token (matrix)', () => { const config = { matrix: { homeserver_url: 'https://matrix.example.org', access_token: 'mx-secret' }, models: { default: { provider: 'anthropic', model: 'claude' } }, }; const redacted = redactSecrets(config); const matrix = redacted.matrix as Record; expect(matrix.access_token).toBe('***'); }); it('redacts app_password (teams)', () => { const config = { teams: { app_id: 'id', app_password: 'teams-secret' }, models: { default: { provider: 'anthropic', model: 'claude' } }, }; const redacted = redactSecrets(config); const teams = redacted.teams as Record; expect(teams.app_password).toBe('***'); }); }); describe('getDataDir', () => { it('returns path under home directory', () => { const dir = getDataDir(); expect(dir).toContain('.local/share/flynn'); }); }); });