// E2E更新時は docs/testing/e2e.md も更新すること import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { stringify as stringifyYaml } from 'yaml'; import { createIsolatedEnv, type IsolatedEnv } from '../helpers/isolated-env'; import { runTakt } from '../helpers/takt-runner'; interface FakePackageOptions { taktDir: string; owner: string; repo: string; description?: string; ref?: string; commit?: string; } function createFakePackage(opts: FakePackageOptions): string { const { taktDir, owner, repo } = opts; const ref = opts.ref ?? 'v1.0.0'; const commit = opts.commit ?? 'abc1234def5678'; const packageDir = join(taktDir, 'repertoire', `@${owner}`, repo); mkdirSync(packageDir, { recursive: true }); const manifest: Record = { path: '.' }; if (opts.description) manifest.description = opts.description; writeFileSync(join(packageDir, 'takt-repertoire.yaml'), stringifyYaml(manifest)); const lock = { source: `github:${owner}/${repo}`, ref, commit, imported_at: new Date().toISOString(), }; writeFileSync(join(packageDir, '.takt-repertoire-lock.yaml'), stringifyYaml(lock)); return packageDir; } // --------------------------------------------------------------------------- // E2E: takt repertoire add — 正常系 // --------------------------------------------------------------------------- describe('E2E: takt repertoire add (正常系)', () => { it.todo('should install standard package and verify directory structure'); it.todo('should generate .takt-repertoire-lock.yaml with source, ref, commit, imported_at'); it.todo('should install subdir-type package and copy only path-specified files'); it.todo('should install facets-only package without creating pieces/ directory'); it.todo('should populate lock file commit field with the specified commit SHA when installing by SHA'); it.todo('should display pre-install summary with package name, faceted count, and pieces list'); it.todo('should display warning symbol when package contains piece with edit: true'); it.todo('should abort installation when user answers N to confirmation prompt'); }); // --------------------------------------------------------------------------- // E2E: takt repertoire add — 上書きシナリオ // --------------------------------------------------------------------------- describe('E2E: takt repertoire add (上書きシナリオ)', () => { it.todo('should display already-installed warning on second add'); it.todo('should atomically update package when user answers y to overwrite prompt'); it.todo('should keep existing package when user answers N to overwrite prompt'); it.todo('should clean up leftover .tmp/ directory from previous failed installation'); it.todo('should clean up leftover .bak/ directory from previous failed installation'); }); // --------------------------------------------------------------------------- // E2E: takt repertoire add — バリデーション・エラー系 // --------------------------------------------------------------------------- describe('E2E: takt repertoire add (バリデーション・エラー系)', () => { it.todo('should fail with error when repository has no takt-repertoire.yaml'); it.todo('should reject takt-repertoire.yaml with absolute path in path field (/foo)'); it.todo('should reject takt-repertoire.yaml with path traversal via ".." segments'); it.todo('should reject package with neither facets/ nor pieces/ directory'); it.todo('should reject takt-repertoire.yaml with min_version "1.0" (missing patch segment)'); it.todo('should reject takt-repertoire.yaml with min_version "v1.0.0" (v prefix)'); it.todo('should reject takt-repertoire.yaml with min_version "1.0.0-alpha" (pre-release suffix)'); it.todo('should fail with version mismatch message when min_version exceeds current takt version'); }); // --------------------------------------------------------------------------- // E2E: takt repertoire remove (mock) // --------------------------------------------------------------------------- describe('E2E: takt repertoire remove (mock)', () => { let isolatedEnv: IsolatedEnv; beforeEach(() => { isolatedEnv = createIsolatedEnv(); }); afterEach(() => { try { isolatedEnv.cleanup(); } catch { // best-effort } }); it('should remove installed package and delete empty owner directory when user answers y', () => { const scope = '@testowner/single-fixture'; createFakePackage({ taktDir: isolatedEnv.taktDir, owner: 'testowner', repo: 'single-fixture' }); const result = runTakt({ args: ['repertoire', 'remove', scope], cwd: process.cwd(), env: isolatedEnv.env, input: 'y\n', timeout: 30_000, }); expect(result.exitCode).toBe(0); const packageDir = join(isolatedEnv.taktDir, 'repertoire', '@testowner', 'single-fixture'); const ownerDir = join(isolatedEnv.taktDir, 'repertoire', '@testowner'); expect(existsSync(packageDir)).toBe(false); expect(existsSync(ownerDir)).toBe(false); }, 240_000); it('should keep owner directory when other packages remain under same scope', () => { createFakePackage({ taktDir: isolatedEnv.taktDir, owner: 'testowner', repo: 'fixture-a' }); createFakePackage({ taktDir: isolatedEnv.taktDir, owner: 'testowner', repo: 'fixture-b' }); const result = runTakt({ args: ['repertoire', 'remove', '@testowner/fixture-a'], cwd: process.cwd(), env: isolatedEnv.env, input: 'y\n', timeout: 30_000, }); expect(result.exitCode).toBe(0); const removedDir = join(isolatedEnv.taktDir, 'repertoire', '@testowner', 'fixture-a'); const ownerDir = join(isolatedEnv.taktDir, 'repertoire', '@testowner'); const remainingDir = join(isolatedEnv.taktDir, 'repertoire', '@testowner', 'fixture-b'); expect(existsSync(removedDir)).toBe(false); expect(existsSync(ownerDir)).toBe(true); expect(existsSync(remainingDir)).toBe(true); }, 240_000); it('should display reference warning before deletion but still proceed when user answers y', () => { const scope = '@testowner/ref-fixture'; createFakePackage({ taktDir: isolatedEnv.taktDir, owner: 'testowner', repo: 'ref-fixture' }); const piecesDir = join(isolatedEnv.taktDir, 'pieces'); mkdirSync(piecesDir, { recursive: true }); writeFileSync(join(piecesDir, 'ref-piece.yaml'), `from: ${scope}\nname: example\n`); const result = runTakt({ args: ['repertoire', 'remove', scope], cwd: process.cwd(), env: isolatedEnv.env, input: 'y\n', timeout: 30_000, }); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('⚠ 以下のファイルが'); expect(result.stdout).toContain('を参照しています'); const packageDir = join(isolatedEnv.taktDir, 'repertoire', '@testowner', 'ref-fixture'); expect(existsSync(packageDir)).toBe(false); }, 240_000); it('should not modify reference files during package removal', () => { const scope = '@testowner/ref-fixture2'; createFakePackage({ taktDir: isolatedEnv.taktDir, owner: 'testowner', repo: 'ref-fixture2' }); const piecesDir = join(isolatedEnv.taktDir, 'pieces'); mkdirSync(piecesDir, { recursive: true }); const refFilePath = join(piecesDir, 'ref-piece2.yaml'); const originalContent = `from: ${scope}\nname: example\n`; writeFileSync(refFilePath, originalContent); const removeResult = runTakt({ args: ['repertoire', 'remove', scope], cwd: process.cwd(), env: isolatedEnv.env, input: 'y\n', timeout: 30_000, }); expect(removeResult.exitCode).toBe(0); const afterContent = readFileSync(refFilePath, 'utf-8'); expect(afterContent).toBe(originalContent); }, 240_000); it('should keep package directory when user answers N to removal prompt', () => { const scope = '@testowner/keep-fixture'; createFakePackage({ taktDir: isolatedEnv.taktDir, owner: 'testowner', repo: 'keep-fixture' }); const result = runTakt({ args: ['repertoire', 'remove', scope], cwd: process.cwd(), env: isolatedEnv.env, input: 'n\n', timeout: 30_000, }); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('キャンセルしました'); const packageDir = join(isolatedEnv.taktDir, 'repertoire', '@testowner', 'keep-fixture'); expect(existsSync(packageDir)).toBe(true); }, 240_000); }); // --------------------------------------------------------------------------- // E2E: takt repertoire list (mock) // --------------------------------------------------------------------------- describe('E2E: takt repertoire list (mock)', () => { let isolatedEnv: IsolatedEnv; beforeEach(() => { isolatedEnv = createIsolatedEnv(); }); afterEach(() => { try { isolatedEnv.cleanup(); } catch { // best-effort } }); it('should list installed package with name, description, ref, and abbreviated commit', () => { createFakePackage({ taktDir: isolatedEnv.taktDir, owner: 'testowner', repo: 'list-fixture', description: 'My test package', ref: 'v2.0.0', commit: 'abcdef1234567', }); const result = runTakt({ args: ['repertoire', 'list'], cwd: process.cwd(), env: isolatedEnv.env, timeout: 30_000, }); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('📦 インストール済みパッケージ:'); expect(result.stdout).toContain('@testowner/list-fixture'); expect(result.stdout).toContain('My test package'); expect(result.stdout).toContain('v2.0.0'); expect(result.stdout).toContain('abcdef1'); }, 240_000); it('should display empty-state message when no packages are installed', () => { const result = runTakt({ args: ['repertoire', 'list'], cwd: process.cwd(), env: isolatedEnv.env, timeout: 30_000, }); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('インストール済みパッケージはありません'); }, 240_000); it('should list all installed packages when multiple packages exist', () => { createFakePackage({ taktDir: isolatedEnv.taktDir, owner: 'ownerA', repo: 'pkg-alpha', description: 'Alpha package', }); createFakePackage({ taktDir: isolatedEnv.taktDir, owner: 'ownerB', repo: 'pkg-beta', description: 'Beta package', }); const result = runTakt({ args: ['repertoire', 'list'], cwd: process.cwd(), env: isolatedEnv.env, timeout: 30_000, }); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('@ownerA/pkg-alpha'); expect(result.stdout).toContain('@ownerB/pkg-beta'); }, 240_000); });