takt/src/__tests__/deploySkill.test.ts
nrslib 2c7bd4834f Faceted Prompting リネーム: stances→policies, report_formats→output_contracts
5つの関心を Persona, Policy, Instruction, Knowledge, Output Contract に統一。
ディレクトリ、YAMLキー、ソースコード、テンプレート、テスト、ドキュメントを全面更新。
2026-02-07 20:04:09 +09:00

221 lines
7.8 KiB
TypeScript

/**
* Tests for deploySkill (export-cc) command
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { mkdtempSync, mkdirSync, writeFileSync, existsSync, readdirSync, rmSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
// Mock home directory to use temp directory
const testHomeDir = mkdtempSync(join(tmpdir(), 'takt-deploy-test-'));
vi.mock('node:os', async () => {
const actual = await vi.importActual('node:os');
return {
...actual,
homedir: () => testHomeDir,
};
});
// Mock confirm to always accept
vi.mock('../shared/prompt/index.js', () => ({
confirm: vi.fn().mockResolvedValue(true),
}));
// Mock UI functions to suppress output
vi.mock('../shared/ui/index.js', () => ({
header: vi.fn(),
success: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
blankLine: vi.fn(),
}));
// Mock getLanguage
vi.mock('../infra/config/index.js', () => ({
getLanguage: vi.fn().mockReturnValue('en'),
}));
// Create fake resources directories
let fakeResourcesDir: string;
vi.mock('../infra/resources/index.js', async () => {
const actual = await vi.importActual('../infra/resources/index.js');
return {
...actual,
getResourcesDir: () => fakeResourcesDir,
getLanguageResourcesDir: (lang: string) => join(fakeResourcesDir, lang),
};
});
// Import after mocks are set up
const { deploySkill } = await import('../features/config/deploySkill.js');
const { warn } = await import('../shared/ui/index.js');
const { confirm } = await import('../shared/prompt/index.js');
describe('deploySkill', () => {
let skillDir: string;
beforeEach(() => {
// Create fake resources directory with skill files
fakeResourcesDir = mkdtempSync(join(tmpdir(), 'takt-resources-'));
// Create skill/ directory with required files
const skillResourcesDir = join(fakeResourcesDir, 'skill');
mkdirSync(skillResourcesDir, { recursive: true });
writeFileSync(join(skillResourcesDir, 'SKILL.md'), '# SKILL');
// Create skill/references/ directory
const refsDir = join(skillResourcesDir, 'references');
mkdirSync(refsDir, { recursive: true });
writeFileSync(join(refsDir, 'engine.md'), '# Engine');
writeFileSync(join(refsDir, 'yaml-schema.md'), '# Schema');
// Create language-specific directories (en/)
const langDir = join(fakeResourcesDir, 'en');
mkdirSync(join(langDir, 'pieces'), { recursive: true });
mkdirSync(join(langDir, 'personas'), { recursive: true });
mkdirSync(join(langDir, 'policies'), { recursive: true });
mkdirSync(join(langDir, 'instructions'), { recursive: true });
mkdirSync(join(langDir, 'knowledge'), { recursive: true });
mkdirSync(join(langDir, 'output-contracts'), { recursive: true });
mkdirSync(join(langDir, 'templates'), { recursive: true });
// Add sample files
writeFileSync(join(langDir, 'pieces', 'default.yaml'), 'name: default');
writeFileSync(join(langDir, 'personas', 'coder.md'), '# Coder');
writeFileSync(join(langDir, 'policies', 'coding.md'), '# Coding');
writeFileSync(join(langDir, 'instructions', 'init.md'), '# Init');
writeFileSync(join(langDir, 'knowledge', 'patterns.md'), '# Patterns');
writeFileSync(join(langDir, 'output-contracts', 'summary.md'), '# Summary');
writeFileSync(join(langDir, 'templates', 'task.md'), '# Task');
// Create target directories
skillDir = join(testHomeDir, '.claude', 'skills', 'takt');
mkdirSync(skillDir, { recursive: true });
// Reset mocks
vi.clearAllMocks();
});
afterEach(() => {
// Clean up
if (existsSync(testHomeDir)) {
rmSync(testHomeDir, { recursive: true, force: true });
}
if (existsSync(fakeResourcesDir)) {
rmSync(fakeResourcesDir, { recursive: true, force: true });
}
// Recreate test home for next test
mkdirSync(testHomeDir, { recursive: true });
});
describe('when skill resources exist', () => {
it('should copy SKILL.md to skill directory', async () => {
await deploySkill();
expect(existsSync(join(skillDir, 'SKILL.md'))).toBe(true);
expect(readFileSync(join(skillDir, 'SKILL.md'), 'utf-8')).toBe('# SKILL');
});
it('should copy references directory', async () => {
await deploySkill();
const refsDir = join(skillDir, 'references');
expect(existsSync(refsDir)).toBe(true);
expect(existsSync(join(refsDir, 'engine.md'))).toBe(true);
expect(existsSync(join(refsDir, 'yaml-schema.md'))).toBe(true);
});
it('should copy all resource directories from language resources', async () => {
await deploySkill();
// Verify each resource directory is copied
expect(existsSync(join(skillDir, 'pieces', 'default.yaml'))).toBe(true);
expect(existsSync(join(skillDir, 'personas', 'coder.md'))).toBe(true);
expect(existsSync(join(skillDir, 'policies', 'coding.md'))).toBe(true);
expect(existsSync(join(skillDir, 'instructions', 'init.md'))).toBe(true);
expect(existsSync(join(skillDir, 'knowledge', 'patterns.md'))).toBe(true);
expect(existsSync(join(skillDir, 'output-contracts', 'summary.md'))).toBe(true);
expect(existsSync(join(skillDir, 'templates', 'task.md'))).toBe(true);
});
});
describe('cleanDir behavior', () => {
it('should remove stale files from previous deployments', async () => {
// Create a stale file in skill directory
const piecesDir = join(skillDir, 'pieces');
mkdirSync(piecesDir, { recursive: true });
writeFileSync(join(piecesDir, 'stale.yaml'), 'name: stale');
await deploySkill();
// Stale file should be removed, new file should exist
expect(existsSync(join(piecesDir, 'stale.yaml'))).toBe(false);
expect(existsSync(join(piecesDir, 'default.yaml'))).toBe(true);
});
it('should clean references directory before copy', async () => {
// Create a stale file in references
const refsDir = join(skillDir, 'references');
mkdirSync(refsDir, { recursive: true });
writeFileSync(join(refsDir, 'old-reference.md'), '# Old');
await deploySkill();
expect(existsSync(join(refsDir, 'old-reference.md'))).toBe(false);
expect(existsSync(join(refsDir, 'engine.md'))).toBe(true);
});
});
describe('when skill resources do not exist', () => {
it('should warn and return early', async () => {
// Remove skill resources directory
rmSync(join(fakeResourcesDir, 'skill'), { recursive: true });
await deploySkill();
expect(warn).toHaveBeenCalledWith('Skill resources not found. Ensure takt is installed correctly.');
});
});
describe('when skill already exists', () => {
it('should ask for confirmation before overwriting', async () => {
// Create existing SKILL.md
writeFileSync(join(skillDir, 'SKILL.md'), '# Old Skill');
await deploySkill();
expect(confirm).toHaveBeenCalledWith(
'既存のスキルファイルをすべて削除し、最新版に置き換えます。続行しますか?',
false,
);
});
it('should cancel when user declines confirmation', async () => {
// Mock confirm to return false
vi.mocked(confirm).mockResolvedValueOnce(false);
// Create existing SKILL.md
writeFileSync(join(skillDir, 'SKILL.md'), '# Old Skill');
await deploySkill();
// File should remain unchanged
expect(readFileSync(join(skillDir, 'SKILL.md'), 'utf-8')).toBe('# Old Skill');
});
});
describe('when language resources directory is empty', () => {
it('should handle missing resource subdirectories gracefully', async () => {
// Remove all resource subdirectories from language dir
const langDir = join(fakeResourcesDir, 'en');
rmSync(langDir, { recursive: true });
mkdirSync(langDir, { recursive: true });
// Should not throw
await expect(deploySkill()).resolves.not.toThrow();
});
});
});