takt/src/__tests__/globalConfig-defaults.test.ts
2026-02-07 10:51:04 +09:00

318 lines
9.6 KiB
TypeScript

/**
* Tests for loadGlobalConfig default values when config.yaml is missing
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdirSync, rmSync, writeFileSync, existsSync } 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 } = 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.defaultPiece).toBe('default');
expect(config.logLevel).toBe('info');
expect(config.provider).toBe('claude');
expect(config.model).toBeUndefined();
expect(config.debug).toBeUndefined();
expect(config.pipeline).toBeUndefined();
});
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\nlog_level: debug\n',
'utf-8',
);
const config = loadGlobalConfig();
expect(config.language).toBe('ja');
expect(config.provider).toBe('codex');
expect(config.logLevel).toBe('debug');
});
it('should load pipeline config from config.yaml', () => {
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',
);
const config = loadGlobalConfig();
expect(config.pipeline).toBeDefined();
expect(config.pipeline!.defaultBranchPrefix).toBe('feat/');
expect(config.pipeline!.commitMessageTemplate).toBe('fix: {title} (#{issue})');
expect(config.pipeline!.prBodyTemplate).toBeUndefined();
});
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.pipeline = {
defaultBranchPrefix: 'takt/',
commitMessageTemplate: 'feat: {title} (#{issue})',
};
saveGlobalConfig(config);
invalidateGlobalConfigCache();
const reloaded = loadGlobalConfig();
expect(reloaded.pipeline).toBeDefined();
expect(reloaded.pipeline!.defaultBranchPrefix).toBe('takt/');
expect(reloaded.pipeline!.commitMessageTemplate).toBe('feat: {title} (#{issue})');
});
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();
});
describe('provider/model compatibility validation', () => {
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();
});
});
});