/** * Tests for loadGlobalConfig default values when config.yaml is missing */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { vi } from 'vitest'; // Mock the home directory to use a temp directory const testHomeDir = join(tmpdir(), `takt-gc-test-${Date.now()}`); vi.mock('node:os', async () => { const actual = await vi.importActual('node:os'); return { ...actual, homedir: () => testHomeDir, }; }); // Import after mocks are set up const { loadGlobalConfig, saveGlobalConfig, invalidateGlobalConfigCache, loadGlobalMigratedProjectLocalFallback, } = await import('../infra/config/global/globalConfig.js'); const { getGlobalConfigPath } = await import('../infra/config/paths.js'); describe('loadGlobalConfig', () => { beforeEach(() => { invalidateGlobalConfigCache(); mkdirSync(testHomeDir, { recursive: true }); }); afterEach(() => { if (existsSync(testHomeDir)) { rmSync(testHomeDir, { recursive: true }); } }); it('should return default values when config.yaml does not exist', () => { const config = loadGlobalConfig(); expect(config.language).toBe('en'); expect(config.provider).toBe('claude'); expect(config.model).toBeUndefined(); }); it('should not expose migrated project-local fields from global config', () => { const config = loadGlobalConfig() as Record; expect(config).not.toHaveProperty('logLevel'); expect(config).not.toHaveProperty('pipeline'); expect(config).not.toHaveProperty('personaProviders'); expect(config).not.toHaveProperty('branchNameStrategy'); expect(config).not.toHaveProperty('minimalOutput'); expect(config).not.toHaveProperty('concurrency'); expect(config).not.toHaveProperty('taskPollIntervalMs'); expect(config).not.toHaveProperty('interactivePreviewMovements'); expect(config).not.toHaveProperty('verbose'); }); it('should accept migrated project-local keys in global config.yaml for resolver fallback', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), [ 'language: en', 'log_level: debug', 'pipeline:', ' default_branch_prefix: "global/"', 'persona_providers:', ' coder:', ' provider: codex', 'branch_name_strategy: ai', 'minimal_output: true', 'concurrency: 3', 'task_poll_interval_ms: 1000', 'interactive_preview_movements: 2', 'verbose: true', ].join('\n'), 'utf-8', ); expect(() => loadGlobalConfig()).not.toThrow(); const config = loadGlobalConfig() as Record; expect(config).not.toHaveProperty('logLevel'); expect(config).not.toHaveProperty('pipeline'); expect(config).not.toHaveProperty('personaProviders'); expect(config).not.toHaveProperty('branchNameStrategy'); expect(config).not.toHaveProperty('minimalOutput'); expect(config).not.toHaveProperty('concurrency'); expect(config).not.toHaveProperty('taskPollIntervalMs'); expect(config).not.toHaveProperty('interactivePreviewMovements'); expect(config).not.toHaveProperty('verbose'); }); it('should not persist migrated project-local keys when saving global config', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync(getGlobalConfigPath(), 'language: en\n', 'utf-8'); const config = loadGlobalConfig() as Record; config.logLevel = 'debug'; config.pipeline = { defaultBranchPrefix: 'global/' }; config.personaProviders = { coder: { provider: 'codex' } }; config.branchNameStrategy = 'ai'; config.minimalOutput = true; config.concurrency = 4; config.taskPollIntervalMs = 1200; config.interactivePreviewMovements = 1; config.verbose = true; saveGlobalConfig(config as Parameters[0]); const raw = readFileSync(getGlobalConfigPath(), 'utf-8'); expect(raw).not.toContain('log_level:'); expect(raw).not.toContain('pipeline:'); expect(raw).not.toContain('persona_providers:'); expect(raw).not.toContain('branch_name_strategy:'); expect(raw).not.toContain('minimal_output:'); expect(raw).not.toContain('concurrency:'); expect(raw).not.toContain('task_poll_interval_ms:'); expect(raw).not.toContain('interactive_preview_movements:'); expect(raw).not.toContain('verbose:'); }); it('should return the same cached object on subsequent calls', () => { const config1 = loadGlobalConfig(); const config2 = loadGlobalConfig(); expect(config1).toBe(config2); }); it('should return a fresh object after cache invalidation', () => { const config1 = loadGlobalConfig(); invalidateGlobalConfigCache(); const config2 = loadGlobalConfig(); expect(config1).not.toBe(config2); expect(config1).toEqual(config2); }); it('should load from config.yaml when it exists', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), 'language: ja\nprovider: codex\n', 'utf-8', ); const config = loadGlobalConfig(); expect(config.language).toBe('ja'); expect(config.provider).toBe('codex'); expect((config as Record).logLevel).toBeUndefined(); }); it('should load provider block from config.yaml and normalize model/providerOptions', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), [ 'provider:', ' type: codex', ' model: gpt-5.3', ' network_access: true', ].join('\n'), 'utf-8', ); const config = loadGlobalConfig(); expect(config.provider).toBe('codex'); expect(config.model).toBe('gpt-5.3'); expect(config.providerOptions).toEqual({ codex: { networkAccess: true }, }); }); it('should preserve provider_options when saveGlobalConfig is called with loaded config', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), [ 'language: en', 'provider: claude', 'provider_options:', ' codex:', ' network_access: true', ' opencode:', ' network_access: false', ' claude:', ' sandbox:', ' allow_unsandboxed_commands: true', ' excluded_commands:', ' - git push', ].join('\n'), 'utf-8', ); const loaded = loadGlobalConfig(); saveGlobalConfig(loaded); invalidateGlobalConfigCache(); const reloaded = loadGlobalConfig(); expect(reloaded.providerOptions).toEqual({ codex: { networkAccess: true }, opencode: { networkAccess: false }, claude: { sandbox: { allowUnsandboxedCommands: true, excludedCommands: ['git push'], }, }, }); const raw = readFileSync(getGlobalConfigPath(), 'utf-8'); expect(raw).toContain('provider_options:'); }); it('should round-trip copilot global fields', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), [ 'language: en', 'copilot_cli_path: /tmp/copilot', 'copilot_github_token: ghp_test_token', ].join('\n'), 'utf-8', ); const loaded = loadGlobalConfig(); expect(loaded.copilotCliPath).toBe('/tmp/copilot'); expect(loaded.copilotGithubToken).toBe('ghp_test_token'); saveGlobalConfig(loaded); invalidateGlobalConfigCache(); const reloaded = loadGlobalConfig(); expect(reloaded.copilotCliPath).toBe('/tmp/copilot'); expect(reloaded.copilotGithubToken).toBe('ghp_test_token'); }); it('should apply env override for nested provider_options key', () => { const original = process.env.TAKT_PROVIDER_OPTIONS_CLAUDE_SANDBOX_ALLOW_UNSANDBOXED_COMMANDS; try { process.env.TAKT_PROVIDER_OPTIONS_CLAUDE_SANDBOX_ALLOW_UNSANDBOXED_COMMANDS = 'true'; invalidateGlobalConfigCache(); const config = loadGlobalConfig(); expect(config.providerOptions?.claude?.sandbox?.allowUnsandboxedCommands).toBe(true); } finally { if (original === undefined) { delete process.env.TAKT_PROVIDER_OPTIONS_CLAUDE_SANDBOX_ALLOW_UNSANDBOXED_COMMANDS; } else { process.env.TAKT_PROVIDER_OPTIONS_CLAUDE_SANDBOX_ALLOW_UNSANDBOXED_COMMANDS = original; } } }); it('should accept pipeline in global config for migrated fallback', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), [ 'language: en', 'pipeline:', ' default_branch_prefix: "feat/"', ' commit_message_template: "fix: {title} (#{issue})"', ].join('\n'), 'utf-8', ); expect(() => loadGlobalConfig()).not.toThrow(); const config = loadGlobalConfig() as Record; expect(config).not.toHaveProperty('pipeline'); }); it('should save and reload pipeline config', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); // Create minimal config first writeFileSync(getGlobalConfigPath(), 'language: en\n', 'utf-8'); const config = loadGlobalConfig(); (config as Record).pipeline = { defaultBranchPrefix: 'takt/', commitMessageTemplate: 'feat: {title} (#{issue})', }; saveGlobalConfig(config); invalidateGlobalConfigCache(); const reloaded = loadGlobalConfig(); expect((reloaded as Record).pipeline).toBeUndefined(); }); it('should load auto_pr config from config.yaml', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), 'language: en\nauto_pr: true\n', 'utf-8', ); const config = loadGlobalConfig(); expect(config.autoPr).toBe(true); }); it('should save and reload auto_pr config', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); // Create minimal config first writeFileSync(getGlobalConfigPath(), 'language: en\n', 'utf-8'); const config = loadGlobalConfig(); config.autoPr = true; saveGlobalConfig(config); invalidateGlobalConfigCache(); const reloaded = loadGlobalConfig(); expect(reloaded.autoPr).toBe(true); }); it('should save auto_pr: false when explicitly set', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync(getGlobalConfigPath(), 'language: en\n', 'utf-8'); const config = loadGlobalConfig(); config.autoPr = false; saveGlobalConfig(config); invalidateGlobalConfigCache(); const reloaded = loadGlobalConfig(); expect(reloaded.autoPr).toBe(false); }); it('should read from cache without hitting disk on second call', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), 'language: ja\nprovider: codex\n', 'utf-8', ); const config1 = loadGlobalConfig(); expect(config1.language).toBe('ja'); // Overwrite file on disk - cached result should still be returned writeFileSync( getGlobalConfigPath(), 'language: en\nprovider: claude\n', 'utf-8', ); const config2 = loadGlobalConfig(); expect(config2.language).toBe('ja'); expect(config2).toBe(config1); // After invalidation, the new file content is read invalidateGlobalConfigCache(); const config3 = loadGlobalConfig(); expect(config3.language).toBe('en'); expect(config3).not.toBe(config1); }); it('should load prevent_sleep config from config.yaml', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), 'language: en\nprevent_sleep: true\n', 'utf-8', ); const config = loadGlobalConfig(); expect(config.preventSleep).toBe(true); }); it('should save and reload prevent_sleep config', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync(getGlobalConfigPath(), 'language: en\n', 'utf-8'); const config = loadGlobalConfig(); config.preventSleep = true; saveGlobalConfig(config); invalidateGlobalConfigCache(); const reloaded = loadGlobalConfig(); expect(reloaded.preventSleep).toBe(true); }); it('should save prevent_sleep: false when explicitly set', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync(getGlobalConfigPath(), 'language: en\n', 'utf-8'); const config = loadGlobalConfig(); config.preventSleep = false; saveGlobalConfig(config); invalidateGlobalConfigCache(); const reloaded = loadGlobalConfig(); expect(reloaded.preventSleep).toBe(false); }); it('should have undefined preventSleep by default', () => { const config = loadGlobalConfig(); expect(config.preventSleep).toBeUndefined(); }); it('should load notification_sound config from config.yaml', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), 'language: en\nnotification_sound: false\n', 'utf-8', ); const config = loadGlobalConfig(); expect(config.notificationSound).toBe(false); }); it('should save and reload notification_sound config', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync(getGlobalConfigPath(), 'language: en\n', 'utf-8'); const config = loadGlobalConfig(); config.notificationSound = true; saveGlobalConfig(config); invalidateGlobalConfigCache(); const reloaded = loadGlobalConfig(); expect(reloaded.notificationSound).toBe(true); }); it('should save notification_sound: false when explicitly set', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync(getGlobalConfigPath(), 'language: en\n', 'utf-8'); const config = loadGlobalConfig(); config.notificationSound = false; saveGlobalConfig(config); invalidateGlobalConfigCache(); const reloaded = loadGlobalConfig(); expect(reloaded.notificationSound).toBe(false); }); it('should have undefined notificationSound by default', () => { const config = loadGlobalConfig(); expect(config.notificationSound).toBeUndefined(); }); it('should load notification_sound_events config from config.yaml', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), [ 'language: en', 'notification_sound_events:', ' iteration_limit: false', ' piece_complete: true', ' piece_abort: true', ' run_complete: true', ' run_abort: false', ].join('\n'), 'utf-8', ); const config = loadGlobalConfig(); expect(config.notificationSoundEvents).toEqual({ iterationLimit: false, pieceComplete: true, pieceAbort: true, runComplete: true, runAbort: false, }); }); it('should load logging config from config.yaml', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), [ 'language: en', 'logging:', ' provider_events: false', ' usage_events: true', ].join('\n'), 'utf-8', ); const config = loadGlobalConfig(); expect(config.logging).toEqual({ providerEvents: false, usageEvents: true, }); }); it('should load full logging config with all fields', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), [ 'language: en', 'logging:', ' level: debug', ' trace: true', ' debug: true', ' provider_events: true', ' usage_events: false', ].join('\n'), 'utf-8', ); const config = loadGlobalConfig(); expect(config.logging).toEqual({ level: 'debug', trace: true, debug: true, providerEvents: true, usageEvents: false, }); }); it('should save and reload logging config', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync(getGlobalConfigPath(), 'language: en\n', 'utf-8'); const config = loadGlobalConfig(); config.logging = { level: 'warn', trace: false, debug: true, providerEvents: false, usageEvents: true, }; saveGlobalConfig(config); invalidateGlobalConfigCache(); const reloaded = loadGlobalConfig(); expect(reloaded.logging).toEqual({ level: 'warn', trace: false, debug: true, providerEvents: false, usageEvents: true, }); }); it('should save partial logging config (only provider_events)', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync(getGlobalConfigPath(), 'language: en\n', 'utf-8'); const config = loadGlobalConfig(); config.logging = { providerEvents: true, }; saveGlobalConfig(config); invalidateGlobalConfigCache(); const reloaded = loadGlobalConfig(); expect(reloaded.logging).toEqual({ providerEvents: true, }); }); it('should save partial logging config (only usage_events)', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync(getGlobalConfigPath(), 'language: en\n', 'utf-8'); const config = loadGlobalConfig(); config.logging = { usageEvents: true, }; saveGlobalConfig(config); invalidateGlobalConfigCache(); const reloaded = loadGlobalConfig(); expect(reloaded.logging).toEqual({ usageEvents: true, }); }); it('should save and reload notification_sound_events config', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync(getGlobalConfigPath(), 'language: en\n', 'utf-8'); const config = loadGlobalConfig(); config.notificationSoundEvents = { iterationLimit: false, pieceComplete: true, pieceAbort: false, runComplete: true, runAbort: true, }; saveGlobalConfig(config); invalidateGlobalConfigCache(); const reloaded = loadGlobalConfig(); expect(reloaded.notificationSoundEvents).toEqual({ iterationLimit: false, pieceComplete: true, pieceAbort: false, runComplete: true, runAbort: true, }); }); it('should accept interactive_preview_movements in global config for migrated fallback', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), 'language: en\ninteractive_preview_movements: 5\n', 'utf-8', ); expect(() => loadGlobalConfig()).not.toThrow(); const config = loadGlobalConfig() as Record; expect(config).not.toHaveProperty('interactivePreviewMovements'); }); it('should save and reload interactive_preview_movements config', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync(getGlobalConfigPath(), 'language: en\n', 'utf-8'); const config = loadGlobalConfig(); (config as Record).interactivePreviewMovements = 7; saveGlobalConfig(config); invalidateGlobalConfigCache(); const reloaded = loadGlobalConfig(); expect((reloaded as Record).interactivePreviewMovements).toBeUndefined(); }); it('should default interactive_preview_movements to 3', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync(getGlobalConfigPath(), 'language: en\n', 'utf-8'); const config = loadGlobalConfig(); expect((config as Record).interactivePreviewMovements).toBeUndefined(); }); it('should accept interactive_preview_movements=0 in global config for migrated fallback', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), 'language: en\ninteractive_preview_movements: 0\n', 'utf-8', ); expect(() => loadGlobalConfig()).not.toThrow(); const config = loadGlobalConfig() as Record; expect(config).not.toHaveProperty('interactivePreviewMovements'); }); describe('persona_providers', () => { it('should fail fast when persona_providers provider/model alias combination is invalid', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), 'language: en\npersona_providers:\n coder:\n provider: codex\n model: opus\n', 'utf-8', ); expect(() => loadGlobalConfig()).toThrow(); }); it('should fail fast when persona provider block includes provider options', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), [ 'language: en', 'persona_providers:', ' coder:', ' type: codex', ' network_access: true', ].join('\n'), 'utf-8', ); expect(() => loadGlobalConfig()).toThrow(); }); }); describe('runtime', () => { it('should load runtime.prepare from config.yaml', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), [ 'language: en', 'runtime:', ' prepare:', ' - gradle', ' - node', ].join('\n'), 'utf-8', ); const config = loadGlobalConfig(); expect(config.runtime).toEqual({ prepare: ['gradle', 'node'] }); }); it('should save and reload runtime.prepare', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync(getGlobalConfigPath(), 'language: en\n', 'utf-8'); const config = loadGlobalConfig(); config.runtime = { prepare: ['gradle', 'node'] }; saveGlobalConfig(config); invalidateGlobalConfigCache(); const reloaded = loadGlobalConfig(); expect(reloaded.runtime).toEqual({ prepare: ['gradle', 'node'] }); }); }); describe('provider/model compatibility validation', () => { it('should throw when provider block uses claude with network_access', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), [ 'provider:', ' type: claude', ' network_access: true', ].join('\n'), 'utf-8', ); expect(() => loadGlobalConfig()).toThrow(/network_access/); }); it('should throw when provider block uses codex with sandbox', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), [ 'provider:', ' type: codex', ' sandbox:', ' allow_unsandboxed_commands: true', ].join('\n'), 'utf-8', ); expect(() => loadGlobalConfig()).toThrow(/sandbox/); }); it('should throw when provider is codex but model is a Claude alias (opus)', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), 'provider: codex\nmodel: opus\n', 'utf-8', ); expect(() => loadGlobalConfig()).toThrow(/model 'opus' is a Claude model alias but provider is 'codex'/); }); it('should throw when provider is codex but model is sonnet', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), 'provider: codex\nmodel: sonnet\n', 'utf-8', ); expect(() => loadGlobalConfig()).toThrow(/model 'sonnet' is a Claude model alias but provider is 'codex'/); }); it('should throw when provider is codex but model is haiku', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), 'provider: codex\nmodel: haiku\n', 'utf-8', ); expect(() => loadGlobalConfig()).toThrow(/model 'haiku' is a Claude model alias but provider is 'codex'/); }); it('should not throw when provider is codex with a compatible model', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), 'provider: codex\nmodel: gpt-4o\n', 'utf-8', ); expect(() => loadGlobalConfig()).not.toThrow(); }); it('should not throw when provider is claude with Claude models', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), 'provider: claude\nmodel: opus\n', 'utf-8', ); expect(() => loadGlobalConfig()).not.toThrow(); }); it('should not throw when provider is codex without a model', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), 'provider: codex\n', 'utf-8', ); expect(() => loadGlobalConfig()).not.toThrow(); }); it('should throw when provider is opencode but model is a Claude alias (opus)', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), 'provider: opencode\nmodel: opus\n', 'utf-8', ); expect(() => loadGlobalConfig()).toThrow(/model 'opus' is a Claude model alias but provider is 'opencode'/); }); it('should throw when provider is opencode but model is sonnet', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), 'provider: opencode\nmodel: sonnet\n', 'utf-8', ); expect(() => loadGlobalConfig()).toThrow(/model 'sonnet' is a Claude model alias but provider is 'opencode'/); }); it('should throw when provider is opencode but model is haiku', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), 'provider: opencode\nmodel: haiku\n', 'utf-8', ); expect(() => loadGlobalConfig()).toThrow(/model 'haiku' is a Claude model alias but provider is 'opencode'/); }); it('should not throw when provider is opencode with a compatible model', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), 'provider: opencode\nmodel: opencode/big-pickle\n', 'utf-8', ); expect(() => loadGlobalConfig()).not.toThrow(); }); it('should throw when provider is opencode without a model', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), 'provider: opencode\n', 'utf-8', ); expect(() => loadGlobalConfig()).toThrow(/provider 'opencode' requires model in 'provider\/model' format/i); }); it('should throw when provider is opencode and model is not provider/model format', () => { const taktDir = join(testHomeDir, '.takt'); mkdirSync(taktDir, { recursive: true }); writeFileSync( getGlobalConfigPath(), 'provider: opencode\nmodel: big-pickle\n', 'utf-8', ); expect(() => loadGlobalConfig()).toThrow(/must be in 'provider\/model' format/i); }); }); });