diff --git a/docs/testing/e2e.md b/docs/testing/e2e.md index 1f91ed7..1df1e8c 100644 --- a/docs/testing/e2e.md +++ b/docs/testing/e2e.md @@ -118,3 +118,27 @@ E2Eテストを追加・変更した場合は、このドキュメントも更 - `takt list --non-interactive --action diff --branch ` で差分統計が出力されることを確認する。 - `takt list --non-interactive --action try --branch ` で変更がステージされることを確認する。 - `takt list --non-interactive --action merge --branch ` でブランチがマージされ削除されることを確認する。 +- Config permission mode(`e2e/specs/cli-config.e2e.ts`) + - 目的: `takt config` でパーミッションモードの切り替えと永続化を確認。 + - LLM: 呼び出さない(LLM不使用の操作のみ) + - 手順(ユーザー行動/コマンド): + - `takt config default` を実行し、`Switched to: default` が出力されることを確認する。 + - `takt config sacrifice-my-pc` を実行し、`Switched to: sacrifice-my-pc` が出力されることを確認する。 + - `takt config sacrifice-my-pc` 実行後、`.takt/config.yaml` に `permissionMode: sacrifice-my-pc` が保存されていることを確認する。 + - `takt config invalid-mode` を実行し、`Invalid mode` が出力されることを確認する。 +- Reset categories(`e2e/specs/cli-reset-categories.e2e.ts`) + - 目的: `takt reset categories` でカテゴリオーバーレイのリセットを確認。 + - LLM: 呼び出さない(LLM不使用の操作のみ) + - 手順(ユーザー行動/コマンド): + - `takt reset categories` を実行する。 + - 出力に `reset` を含むことを確認する。 + - `$TAKT_CONFIG_DIR/preferences/piece-categories.yaml` が存在し `piece_categories: {}` を含むことを確認する。 +- Export Claude Code Skill(`e2e/specs/cli-export-cc.e2e.ts`) + - 目的: `takt export-cc` でClaude Code Skillのデプロイを確認。 + - LLM: 呼び出さない(LLM不使用の操作のみ) + - 手順(ユーザー行動/コマンド): + - `HOME` を一時ディレクトリに設定する。 + - `takt export-cc` を実行する。 + - 出力に `ファイルをデプロイしました` を含むことを確認する。 + - `$HOME/.claude/skills/takt/SKILL.md` が存在することを確認する。 + - `$HOME/.claude/skills/takt/pieces/` および `$HOME/.claude/skills/takt/personas/` ディレクトリが存在し、それぞれ少なくとも1ファイルを含むことを確認する。 diff --git a/e2e/specs/cli-config.e2e.ts b/e2e/specs/cli-config.e2e.ts new file mode 100644 index 0000000..e51cfc4 --- /dev/null +++ b/e2e/specs/cli-config.e2e.ts @@ -0,0 +1,102 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { execFileSync } from 'node:child_process'; +import { createIsolatedEnv, type IsolatedEnv } from '../helpers/isolated-env'; +import { runTakt } from '../helpers/takt-runner'; + +function createLocalRepo(): { path: string; cleanup: () => void } { + const repoPath = mkdtempSync(join(tmpdir(), 'takt-e2e-config-')); + execFileSync('git', ['init'], { cwd: repoPath, stdio: 'pipe' }); + execFileSync('git', ['config', 'user.email', 'test@example.com'], { cwd: repoPath, stdio: 'pipe' }); + execFileSync('git', ['config', 'user.name', 'Test'], { cwd: repoPath, stdio: 'pipe' }); + writeFileSync(join(repoPath, 'README.md'), '# test\n'); + execFileSync('git', ['add', '.'], { cwd: repoPath, stdio: 'pipe' }); + execFileSync('git', ['commit', '-m', 'init'], { cwd: repoPath, stdio: 'pipe' }); + return { + path: repoPath, + cleanup: () => { + try { rmSync(repoPath, { recursive: true, force: true }); } catch { /* best-effort */ } + }, + }; +} + +// E2E更新時は docs/testing/e2e.md も更新すること +describe('E2E: Config command (takt config)', () => { + let isolatedEnv: IsolatedEnv; + let repo: { path: string; cleanup: () => void }; + + beforeEach(() => { + isolatedEnv = createIsolatedEnv(); + repo = createLocalRepo(); + }); + + afterEach(() => { + try { repo.cleanup(); } catch { /* best-effort */ } + try { isolatedEnv.cleanup(); } catch { /* best-effort */ } + }); + + it('should switch to default mode with explicit argument', () => { + // Given: a local repo with isolated env + + // When: running takt config default + const result = runTakt({ + args: ['config', 'default'], + cwd: repo.path, + env: isolatedEnv.env, + }); + + // Then: exits successfully and outputs switched message + expect(result.exitCode).toBe(0); + const output = result.stdout; + expect(output).toMatch(/Switched to: default/); + }); + + it('should switch to sacrifice-my-pc mode with explicit argument', () => { + // Given: a local repo with isolated env + + // When: running takt config sacrifice-my-pc + const result = runTakt({ + args: ['config', 'sacrifice-my-pc'], + cwd: repo.path, + env: isolatedEnv.env, + }); + + // Then: exits successfully and outputs switched message + expect(result.exitCode).toBe(0); + const output = result.stdout; + expect(output).toMatch(/Switched to: sacrifice-my-pc/); + }); + + it('should persist permission mode to project config', () => { + // Given: a local repo with isolated env + + // When: running takt config sacrifice-my-pc + runTakt({ + args: ['config', 'sacrifice-my-pc'], + cwd: repo.path, + env: isolatedEnv.env, + }); + + // Then: .takt/config.yaml contains permissionMode: sacrifice-my-pc + const configPath = join(repo.path, '.takt', 'config.yaml'); + const content = readFileSync(configPath, 'utf-8'); + expect(content).toMatch(/permissionMode:\s*sacrifice-my-pc/); + }); + + it('should report error for invalid mode name', () => { + // Given: a local repo with isolated env + + // When: running takt config with an invalid mode + const result = runTakt({ + args: ['config', 'invalid-mode'], + cwd: repo.path, + env: isolatedEnv.env, + }); + + // Then: output contains invalid mode message + const combined = result.stdout + result.stderr; + expect(combined).toMatch(/Invalid mode/); + }); +}); diff --git a/e2e/specs/cli-export-cc.e2e.ts b/e2e/specs/cli-export-cc.e2e.ts new file mode 100644 index 0000000..b1d771c --- /dev/null +++ b/e2e/specs/cli-export-cc.e2e.ts @@ -0,0 +1,88 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdtempSync, writeFileSync, existsSync, readdirSync, rmSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { execFileSync } from 'node:child_process'; +import { createIsolatedEnv, type IsolatedEnv } from '../helpers/isolated-env'; +import { runTakt } from '../helpers/takt-runner'; + +function createLocalRepo(): { path: string; cleanup: () => void } { + const repoPath = mkdtempSync(join(tmpdir(), 'takt-e2e-export-cc-')); + execFileSync('git', ['init'], { cwd: repoPath, stdio: 'pipe' }); + execFileSync('git', ['config', 'user.email', 'test@example.com'], { cwd: repoPath, stdio: 'pipe' }); + execFileSync('git', ['config', 'user.name', 'Test'], { cwd: repoPath, stdio: 'pipe' }); + writeFileSync(join(repoPath, 'README.md'), '# test\n'); + execFileSync('git', ['add', '.'], { cwd: repoPath, stdio: 'pipe' }); + execFileSync('git', ['commit', '-m', 'init'], { cwd: repoPath, stdio: 'pipe' }); + return { + path: repoPath, + cleanup: () => { + try { rmSync(repoPath, { recursive: true, force: true }); } catch { /* best-effort */ } + }, + }; +} + +// E2E更新時は docs/testing/e2e.md も更新すること +describe('E2E: Export-cc command (takt export-cc)', () => { + let isolatedEnv: IsolatedEnv; + let repo: { path: string; cleanup: () => void }; + let fakeHome: string; + + beforeEach(() => { + isolatedEnv = createIsolatedEnv(); + repo = createLocalRepo(); + fakeHome = mkdtempSync(join(tmpdir(), 'takt-e2e-export-cc-home-')); + }); + + afterEach(() => { + try { repo.cleanup(); } catch { /* best-effort */ } + try { isolatedEnv.cleanup(); } catch { /* best-effort */ } + try { rmSync(fakeHome, { recursive: true, force: true }); } catch { /* best-effort */ } + }); + + it('should deploy skill files to isolated home directory', () => { + // Given: a local repo with isolated env and HOME redirected to fakeHome + const env: NodeJS.ProcessEnv = { ...isolatedEnv.env, HOME: fakeHome }; + + // When: running takt export-cc + const result = runTakt({ + args: ['export-cc'], + cwd: repo.path, + env, + }); + + // Then: exits successfully and outputs deploy message + expect(result.exitCode).toBe(0); + const output = result.stdout; + expect(output).toMatch(/ファイルをデプロイしました/); + + // Then: SKILL.md exists in the skill directory + const skillMdPath = join(fakeHome, '.claude', 'skills', 'takt', 'SKILL.md'); + expect(existsSync(skillMdPath)).toBe(true); + }); + + it('should deploy resource directories', () => { + // Given: a local repo with isolated env and HOME redirected to fakeHome + const env: NodeJS.ProcessEnv = { ...isolatedEnv.env, HOME: fakeHome }; + + // When: running takt export-cc + runTakt({ + args: ['export-cc'], + cwd: repo.path, + env, + }); + + // Then: pieces/ and personas/ directories exist with at least one file each + const skillDir = join(fakeHome, '.claude', 'skills', 'takt'); + + const piecesDir = join(skillDir, 'pieces'); + expect(existsSync(piecesDir)).toBe(true); + const pieceFiles = readdirSync(piecesDir); + expect(pieceFiles.length).toBeGreaterThan(0); + + const personasDir = join(skillDir, 'personas'); + expect(existsSync(personasDir)).toBe(true); + const personaFiles = readdirSync(personasDir); + expect(personaFiles.length).toBeGreaterThan(0); + }); +}); diff --git a/e2e/specs/cli-reset-categories.e2e.ts b/e2e/specs/cli-reset-categories.e2e.ts new file mode 100644 index 0000000..f53131e --- /dev/null +++ b/e2e/specs/cli-reset-categories.e2e.ts @@ -0,0 +1,61 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdtempSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { execFileSync } from 'node:child_process'; +import { createIsolatedEnv, type IsolatedEnv } from '../helpers/isolated-env'; +import { runTakt } from '../helpers/takt-runner'; + +function createLocalRepo(): { path: string; cleanup: () => void } { + const repoPath = mkdtempSync(join(tmpdir(), 'takt-e2e-reset-')); + execFileSync('git', ['init'], { cwd: repoPath, stdio: 'pipe' }); + execFileSync('git', ['config', 'user.email', 'test@example.com'], { cwd: repoPath, stdio: 'pipe' }); + execFileSync('git', ['config', 'user.name', 'Test'], { cwd: repoPath, stdio: 'pipe' }); + writeFileSync(join(repoPath, 'README.md'), '# test\n'); + execFileSync('git', ['add', '.'], { cwd: repoPath, stdio: 'pipe' }); + execFileSync('git', ['commit', '-m', 'init'], { cwd: repoPath, stdio: 'pipe' }); + return { + path: repoPath, + cleanup: () => { + try { rmSync(repoPath, { recursive: true, force: true }); } catch { /* best-effort */ } + }, + }; +} + +// E2E更新時は docs/testing/e2e.md も更新すること +describe('E2E: Reset categories command (takt reset categories)', () => { + let isolatedEnv: IsolatedEnv; + let repo: { path: string; cleanup: () => void }; + + beforeEach(() => { + isolatedEnv = createIsolatedEnv(); + repo = createLocalRepo(); + }); + + afterEach(() => { + try { repo.cleanup(); } catch { /* best-effort */ } + try { isolatedEnv.cleanup(); } catch { /* best-effort */ } + }); + + it('should reset categories and create overlay file', () => { + // Given: a local repo with isolated env + + // When: running takt reset categories + const result = runTakt({ + args: ['reset', 'categories'], + cwd: repo.path, + env: isolatedEnv.env, + }); + + // Then: exits successfully and outputs reset message + expect(result.exitCode).toBe(0); + const output = result.stdout; + expect(output).toMatch(/reset/i); + + // Then: piece-categories.yaml exists with initial content + const categoriesPath = join(isolatedEnv.taktDir, 'preferences', 'piece-categories.yaml'); + expect(existsSync(categoriesPath)).toBe(true); + const content = readFileSync(categoriesPath, 'utf-8'); + expect(content).toContain('piece_categories: {}'); + }); +}); diff --git a/vitest.config.e2e.mock.ts b/vitest.config.e2e.mock.ts index 4817180..12bc9fc 100644 --- a/vitest.config.e2e.mock.ts +++ b/vitest.config.e2e.mock.ts @@ -20,6 +20,9 @@ export default defineConfig({ 'e2e/specs/cli-switch.e2e.ts', 'e2e/specs/cli-help.e2e.ts', 'e2e/specs/cli-clear.e2e.ts', + 'e2e/specs/cli-config.e2e.ts', + 'e2e/specs/cli-reset-categories.e2e.ts', + 'e2e/specs/cli-export-cc.e2e.ts', 'e2e/specs/quiet-mode.e2e.ts', 'e2e/specs/task-content-file.e2e.ts', ],