takt/src/__tests__/eject-facet.test.ts
nrs 3167f038a4
github-issue-135-beesunofuaset (#145)
* planner と architect-planner を統合し、knowledge で設計知識を補完する構成に変更

plan → architect → implement の3ステップを plan → implement の2ステップに統合。
planner ペルソナに構造設計・モジュール設計の知識を追加し、plan ステップに
knowledge: architecture を付与することで architect ステップを不要にした。
prompt-log-viewer ツールを追加。

* takt: github-issue-135-beesunofuaset
2026-02-08 17:54:45 +09:00

129 lines
4.4 KiB
TypeScript

/**
* Tests for ejectFacet function.
*
* Covers:
* - Normal copy from builtin to project layer
* - Normal copy from builtin to global layer (--global)
* - Skip when facet already exists at destination
* - Error and listing when facet not found in builtins
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { existsSync, readFileSync, mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
// vi.hoisted runs before vi.mock hoisting — safe for shared state
const mocks = vi.hoisted(() => {
let builtinDir = '';
let projectFacetDir = '';
let globalFacetDir = '';
return {
get builtinDir() { return builtinDir; },
set builtinDir(v: string) { builtinDir = v; },
get projectFacetDir() { return projectFacetDir; },
set projectFacetDir(v: string) { projectFacetDir = v; },
get globalFacetDir() { return globalFacetDir; },
set globalFacetDir(v: string) { globalFacetDir = v; },
ui: {
header: vi.fn(),
success: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
blankLine: vi.fn(),
},
};
});
vi.mock('../infra/config/index.js', () => ({
getLanguage: () => 'en' as const,
getBuiltinFacetDir: () => mocks.builtinDir,
getProjectFacetDir: () => mocks.projectFacetDir,
getGlobalFacetDir: () => mocks.globalFacetDir,
getGlobalPiecesDir: vi.fn(),
getProjectPiecesDir: vi.fn(),
getBuiltinPiecesDir: vi.fn(),
}));
vi.mock('../shared/ui/index.js', () => mocks.ui);
import { ejectFacet } from '../features/config/ejectBuiltin.js';
function createTestDirs() {
const baseDir = mkdtempSync(join(tmpdir(), 'takt-eject-facet-test-'));
const builtinDir = join(baseDir, 'builtins', 'personas');
const projectDir = join(baseDir, 'project');
const globalDir = join(baseDir, 'global');
mkdirSync(builtinDir, { recursive: true });
mkdirSync(projectDir, { recursive: true });
mkdirSync(globalDir, { recursive: true });
writeFileSync(join(builtinDir, 'coder.md'), '# Coder Persona\nYou are a coder.');
writeFileSync(join(builtinDir, 'planner.md'), '# Planner Persona\nYou are a planner.');
return {
baseDir,
builtinDir,
projectDir,
globalDir,
cleanup: () => rmSync(baseDir, { recursive: true, force: true }),
};
}
describe('ejectFacet', () => {
let dirs: ReturnType<typeof createTestDirs>;
beforeEach(() => {
dirs = createTestDirs();
mocks.builtinDir = dirs.builtinDir;
mocks.projectFacetDir = join(dirs.projectDir, '.takt', 'personas');
mocks.globalFacetDir = join(dirs.globalDir, 'personas');
Object.values(mocks.ui).forEach((fn) => fn.mockClear());
});
afterEach(() => {
dirs.cleanup();
});
it('should copy builtin facet to project .takt/{type}/', async () => {
await ejectFacet('personas', 'coder', { projectDir: dirs.projectDir });
const destPath = join(dirs.projectDir, '.takt', 'personas', 'coder.md');
expect(existsSync(destPath)).toBe(true);
expect(readFileSync(destPath, 'utf-8')).toBe('# Coder Persona\nYou are a coder.');
expect(mocks.ui.success).toHaveBeenCalled();
});
it('should copy builtin facet to global ~/.takt/{type}/ with --global', async () => {
await ejectFacet('personas', 'coder', { global: true, projectDir: dirs.projectDir });
const destPath = join(dirs.globalDir, 'personas', 'coder.md');
expect(existsSync(destPath)).toBe(true);
expect(readFileSync(destPath, 'utf-8')).toBe('# Coder Persona\nYou are a coder.');
expect(mocks.ui.success).toHaveBeenCalled();
});
it('should skip if facet already exists at destination', async () => {
const destDir = join(dirs.projectDir, '.takt', 'personas');
mkdirSync(destDir, { recursive: true });
writeFileSync(join(destDir, 'coder.md'), 'Custom coder content');
await ejectFacet('personas', 'coder', { projectDir: dirs.projectDir });
// File should NOT be overwritten
expect(readFileSync(join(destDir, 'coder.md'), 'utf-8')).toBe('Custom coder content');
expect(mocks.ui.warn).toHaveBeenCalledWith(expect.stringContaining('Already exists'));
});
it('should show error and list available facets when not found', async () => {
await ejectFacet('personas', 'nonexistent', { projectDir: dirs.projectDir });
expect(mocks.ui.error).toHaveBeenCalledWith(expect.stringContaining('not found'));
expect(mocks.ui.info).toHaveBeenCalledWith(expect.stringContaining('Available'));
});
});