/** * 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(); }); }); });