* takt: refactor-logging-config * fix: resolve merge conflicts * chore: trigger CI --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
138 lines
4.4 KiB
TypeScript
138 lines
4.4 KiB
TypeScript
/**
|
|
* Global config tests.
|
|
*
|
|
* Tests global config loading and saving with piece_overrides,
|
|
* including empty array round-trip behavior.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import type { PersistedGlobalConfig } from '../core/models/persisted-global-config.js';
|
|
|
|
// Mock the getGlobalConfigPath to use a test directory
|
|
let testConfigPath: string;
|
|
vi.mock('../infra/config/paths.js', () => ({
|
|
getGlobalConfigPath: () => testConfigPath,
|
|
getGlobalTaktDir: () => join(testConfigPath, '..'),
|
|
getProjectTaktDir: vi.fn(),
|
|
getProjectCwd: vi.fn(),
|
|
}));
|
|
|
|
import { GlobalConfigManager } from '../infra/config/global/globalConfigCore.js';
|
|
|
|
describe('globalConfig', () => {
|
|
let testDir: string;
|
|
|
|
beforeEach(() => {
|
|
testDir = mkdtempSync(join(tmpdir(), 'takt-test-global-config-'));
|
|
mkdirSync(testDir, { recursive: true });
|
|
testConfigPath = join(testDir, 'config.yaml');
|
|
GlobalConfigManager.resetInstance();
|
|
});
|
|
|
|
afterEach(() => {
|
|
GlobalConfigManager.resetInstance();
|
|
if (testDir) {
|
|
rmSync(testDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe('piece_overrides empty array round-trip', () => {
|
|
it('should preserve empty quality_gates array in save/load cycle', () => {
|
|
// Write config with empty quality_gates array
|
|
const configContent = `
|
|
piece_overrides:
|
|
quality_gates: []
|
|
`;
|
|
writeFileSync(testConfigPath, configContent, 'utf-8');
|
|
|
|
// Load config
|
|
const manager = GlobalConfigManager.getInstance();
|
|
const loaded = manager.load();
|
|
expect(loaded.pieceOverrides?.qualityGates).toEqual([]);
|
|
|
|
// Save config
|
|
manager.save(loaded);
|
|
|
|
// Reset and reload to verify empty array is preserved
|
|
GlobalConfigManager.resetInstance();
|
|
const reloadedManager = GlobalConfigManager.getInstance();
|
|
const reloaded = reloadedManager.load();
|
|
expect(reloaded.pieceOverrides?.qualityGates).toEqual([]);
|
|
});
|
|
|
|
it('should preserve empty quality_gates in movements', () => {
|
|
const configContent = `
|
|
piece_overrides:
|
|
movements:
|
|
implement:
|
|
quality_gates: []
|
|
`;
|
|
writeFileSync(testConfigPath, configContent, 'utf-8');
|
|
|
|
const manager = GlobalConfigManager.getInstance();
|
|
const loaded = manager.load();
|
|
expect(loaded.pieceOverrides?.movements?.implement?.qualityGates).toEqual([]);
|
|
|
|
manager.save(loaded);
|
|
|
|
GlobalConfigManager.resetInstance();
|
|
const reloadedManager = GlobalConfigManager.getInstance();
|
|
const reloaded = reloadedManager.load();
|
|
expect(reloaded.pieceOverrides?.movements?.implement?.qualityGates).toEqual([]);
|
|
});
|
|
|
|
it('should distinguish undefined from empty array', () => {
|
|
// Test with undefined (not specified)
|
|
writeFileSync(testConfigPath, 'piece_overrides: {}\n', 'utf-8');
|
|
|
|
const manager1 = GlobalConfigManager.getInstance();
|
|
const loaded1 = manager1.load();
|
|
expect(loaded1.pieceOverrides?.qualityGates).toBeUndefined();
|
|
|
|
// Test with empty array (explicitly disabled)
|
|
GlobalConfigManager.resetInstance();
|
|
writeFileSync(testConfigPath, 'piece_overrides:\n quality_gates: []\n', 'utf-8');
|
|
|
|
const manager2 = GlobalConfigManager.getInstance();
|
|
const loaded2 = manager2.load();
|
|
expect(loaded2.pieceOverrides?.qualityGates).toEqual([]);
|
|
});
|
|
|
|
it('should preserve non-empty quality_gates array', () => {
|
|
const config: PersistedGlobalConfig = {
|
|
pieceOverrides: {
|
|
qualityGates: ['Test 1', 'Test 2'],
|
|
},
|
|
};
|
|
|
|
const manager = GlobalConfigManager.getInstance();
|
|
manager.save(config);
|
|
|
|
GlobalConfigManager.resetInstance();
|
|
const reloadedManager = GlobalConfigManager.getInstance();
|
|
const reloaded = reloadedManager.load();
|
|
|
|
expect(reloaded.pieceOverrides?.qualityGates).toEqual(['Test 1', 'Test 2']);
|
|
});
|
|
});
|
|
|
|
describe('security hardening', () => {
|
|
it('should reject forbidden keys that can cause prototype pollution', () => {
|
|
const configContent = `
|
|
logging:
|
|
level: info
|
|
__proto__:
|
|
polluted: true
|
|
`;
|
|
writeFileSync(testConfigPath, configContent, 'utf-8');
|
|
|
|
const manager = GlobalConfigManager.getInstance();
|
|
expect(() => manager.load()).toThrow(/forbidden key "__proto__"/i);
|
|
expect(({} as Record<string, unknown>)['polluted']).toBeUndefined();
|
|
});
|
|
});
|
|
});
|