5つの関心を Persona, Policy, Instruction, Knowledge, Output Contract に統一。 ディレクトリ、YAMLキー、ソースコード、テンプレート、テスト、ドキュメントを全面更新。
221 lines
7.8 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|