From 41bde30adc58d28303ffdb6ea817e94d50c1fe8a Mon Sep 17 00:00:00 2001 From: nrslib <38722970+nrslib@users.noreply.github.com> Date: Thu, 12 Feb 2026 09:40:37 +0900 Subject: [PATCH] =?UTF-8?q?UnitTest=E3=81=A8E2ETest=E3=82=92=E3=83=AA?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=AF=E3=82=BF=E3=83=AA=E3=83=B3=E3=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- e2e/helpers/session-log.ts | 26 ++++++++++ e2e/helpers/test-repo.ts | 28 +++++++++- e2e/specs/cli-catalog.e2e.ts | 23 +------- e2e/specs/cli-clear.e2e.ts | 23 +------- e2e/specs/cli-config.e2e.ts | 23 ++------ e2e/specs/cli-export-cc.e2e.ts | 22 ++------ e2e/specs/cli-help.e2e.ts | 23 +------- e2e/specs/cli-prompt.e2e.ts | 23 +------- e2e/specs/cli-reset-categories.e2e.ts | 23 ++------ e2e/specs/cli-switch.e2e.ts | 23 +------- e2e/specs/cycle-detection.e2e.ts | 45 ++-------------- e2e/specs/eject.e2e.ts | 32 ++---------- e2e/specs/error-handling.e2e.ts | 23 +------- e2e/specs/model-override.e2e.ts | 24 ++------- e2e/specs/multi-step-sequential.e2e.ts | 45 ++-------------- e2e/specs/piece-error-handling.e2e.ts | 23 +------- e2e/specs/pipeline-local-repo.e2e.ts | 20 +------ e2e/specs/provider-error.e2e.ts | 23 +------- e2e/specs/quiet-mode.e2e.ts | 23 +------- e2e/specs/report-file-output.e2e.ts | 23 ++------ e2e/specs/run-multiple-tasks.e2e.ts | 23 ++------ e2e/specs/session-log.e2e.ts | 45 ++-------------- e2e/specs/task-content-file.e2e.ts | 23 ++------ e2e/specs/task-status-persistence.e2e.ts | 23 ++------ src/__tests__/blocked-handler.test.ts | 12 +---- src/__tests__/engine-test-helpers.ts | 9 ++-- src/__tests__/escape.test.ts | 52 +++++-------------- src/__tests__/instruction-helpers.test.ts | 39 +++----------- src/__tests__/it-error-recovery.test.ts | 5 +- src/__tests__/it-instruction-builder.test.ts | 7 +-- src/__tests__/it-piece-execution.test.ts | 5 +- src/__tests__/it-rule-evaluation.test.ts | 5 +- .../it-three-phase-execution.test.ts | 5 +- src/__tests__/judgment-strategies.test.ts | 12 +---- src/__tests__/rule-evaluator.test.ts | 13 +---- src/__tests__/rule-utils.test.ts | 13 +---- src/__tests__/test-helpers.ts | 36 +++++++++++++ 37 files changed, 186 insertions(+), 659 deletions(-) create mode 100644 e2e/helpers/session-log.ts create mode 100644 src/__tests__/test-helpers.ts diff --git a/e2e/helpers/session-log.ts b/e2e/helpers/session-log.ts new file mode 100644 index 0000000..4d458f1 --- /dev/null +++ b/e2e/helpers/session-log.ts @@ -0,0 +1,26 @@ +import { readdirSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +/** + * Read session NDJSON log records from a piece execution run. + * Finds the first .jsonl file whose first record is `piece_start`. + */ +export function readSessionRecords(repoPath: string): Array> { + const runsDir = join(repoPath, '.takt', 'runs'); + const runDirs = readdirSync(runsDir).sort(); + + for (const runDir of runDirs) { + const logsDir = join(runsDir, runDir, 'logs'); + const logFiles = readdirSync(logsDir).filter((file) => file.endsWith('.jsonl')); + for (const file of logFiles) { + const content = readFileSync(join(logsDir, file), 'utf-8').trim(); + if (!content) continue; + const records = content.split('\n').map((line) => JSON.parse(line) as Record); + if (records[0]?.type === 'piece_start') { + return records; + } + } + } + + throw new Error('Session NDJSON log not found'); +} diff --git a/e2e/helpers/test-repo.ts b/e2e/helpers/test-repo.ts index 8c57f4e..04c1c90 100644 --- a/e2e/helpers/test-repo.ts +++ b/e2e/helpers/test-repo.ts @@ -1,9 +1,13 @@ -import { rmSync } from 'node:fs'; -import { mkdtempSync } from 'node:fs'; +import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { execFileSync } from 'node:child_process'; +export interface LocalRepo { + path: string; + cleanup: () => void; +} + export interface TestRepo { path: string; repoName: string; @@ -11,6 +15,26 @@ export interface TestRepo { cleanup: () => void; } +/** + * Create a local git repository in a temporary directory. + * Use this for tests that don't need a remote (GitHub) repository. + */ +export function createLocalRepo(): LocalRepo { + const repoPath = mkdtempSync(join(tmpdir(), 'takt-e2e-')); + 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: () => { + rmSync(repoPath, { recursive: true, force: true }); + }, + }; +} + export interface CreateTestRepoOptions { /** Skip creating a test branch (stay on default branch). Use for pipeline tests. */ skipBranch?: boolean; diff --git a/e2e/specs/cli-catalog.e2e.ts b/e2e/specs/cli-catalog.e2e.ts index 881cde1..074a104 100644 --- a/e2e/specs/cli-catalog.e2e.ts +++ b/e2e/specs/cli-catalog.e2e.ts @@ -1,31 +1,12 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { mkdtempSync, writeFileSync, 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-catalog-')); - 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 */ } - }, - }; -} +import { createLocalRepo, type LocalRepo } from '../helpers/test-repo'; // E2E更新時は docs/testing/e2e.md も更新すること describe('E2E: Catalog command (takt catalog)', () => { let isolatedEnv: IsolatedEnv; - let repo: { path: string; cleanup: () => void }; + let repo: LocalRepo; beforeEach(() => { isolatedEnv = createIsolatedEnv(); diff --git a/e2e/specs/cli-clear.e2e.ts b/e2e/specs/cli-clear.e2e.ts index 81ccad7..09b27ab 100644 --- a/e2e/specs/cli-clear.e2e.ts +++ b/e2e/specs/cli-clear.e2e.ts @@ -1,31 +1,12 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { mkdtempSync, writeFileSync, 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-clear-')); - 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 */ } - }, - }; -} +import { createLocalRepo, type LocalRepo } from '../helpers/test-repo'; // E2E更新時は docs/testing/e2e.md も更新すること describe('E2E: Clear sessions command (takt clear)', () => { let isolatedEnv: IsolatedEnv; - let repo: { path: string; cleanup: () => void }; + let repo: LocalRepo; beforeEach(() => { isolatedEnv = createIsolatedEnv(); diff --git a/e2e/specs/cli-config.e2e.ts b/e2e/specs/cli-config.e2e.ts index e51cfc4..19a6433 100644 --- a/e2e/specs/cli-config.e2e.ts +++ b/e2e/specs/cli-config.e2e.ts @@ -1,31 +1,14 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs'; +import { readFileSync } 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 */ } - }, - }; -} +import { createLocalRepo, type LocalRepo } from '../helpers/test-repo'; // E2E更新時は docs/testing/e2e.md も更新すること describe('E2E: Config command (takt config)', () => { let isolatedEnv: IsolatedEnv; - let repo: { path: string; cleanup: () => void }; + let repo: LocalRepo; beforeEach(() => { isolatedEnv = createIsolatedEnv(); diff --git a/e2e/specs/cli-export-cc.e2e.ts b/e2e/specs/cli-export-cc.e2e.ts index b1d771c..7181106 100644 --- a/e2e/specs/cli-export-cc.e2e.ts +++ b/e2e/specs/cli-export-cc.e2e.ts @@ -1,31 +1,15 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { mkdtempSync, writeFileSync, existsSync, readdirSync, rmSync } from 'node:fs'; +import { mkdtempSync, 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 */ } - }, - }; -} +import { createLocalRepo, type LocalRepo } from '../helpers/test-repo'; // E2E更新時は docs/testing/e2e.md も更新すること describe('E2E: Export-cc command (takt export-cc)', () => { let isolatedEnv: IsolatedEnv; - let repo: { path: string; cleanup: () => void }; + let repo: LocalRepo; let fakeHome: string; beforeEach(() => { diff --git a/e2e/specs/cli-help.e2e.ts b/e2e/specs/cli-help.e2e.ts index c375f23..6d0a03e 100644 --- a/e2e/specs/cli-help.e2e.ts +++ b/e2e/specs/cli-help.e2e.ts @@ -1,31 +1,12 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { mkdtempSync, writeFileSync, 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-help-')); - 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 */ } - }, - }; -} +import { createLocalRepo, type LocalRepo } from '../helpers/test-repo'; // E2E更新時は docs/testing/e2e.md も更新すること describe('E2E: Help command (takt --help)', () => { let isolatedEnv: IsolatedEnv; - let repo: { path: string; cleanup: () => void }; + let repo: LocalRepo; beforeEach(() => { isolatedEnv = createIsolatedEnv(); diff --git a/e2e/specs/cli-prompt.e2e.ts b/e2e/specs/cli-prompt.e2e.ts index 47b78fe..205498d 100644 --- a/e2e/specs/cli-prompt.e2e.ts +++ b/e2e/specs/cli-prompt.e2e.ts @@ -1,36 +1,17 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { mkdtempSync, writeFileSync, 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'; +import { createLocalRepo, type LocalRepo } from '../helpers/test-repo'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -function createLocalRepo(): { path: string; cleanup: () => void } { - const repoPath = mkdtempSync(join(tmpdir(), 'takt-e2e-prompt-')); - 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: Prompt preview command (takt prompt)', () => { let isolatedEnv: IsolatedEnv; - let repo: { path: string; cleanup: () => void }; + let repo: LocalRepo; beforeEach(() => { isolatedEnv = createIsolatedEnv(); diff --git a/e2e/specs/cli-reset-categories.e2e.ts b/e2e/specs/cli-reset-categories.e2e.ts index f53131e..701fac4 100644 --- a/e2e/specs/cli-reset-categories.e2e.ts +++ b/e2e/specs/cli-reset-categories.e2e.ts @@ -1,31 +1,14 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { mkdtempSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs'; +import { readFileSync, existsSync } 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 */ } - }, - }; -} +import { createLocalRepo, type LocalRepo } from '../helpers/test-repo'; // E2E更新時は docs/testing/e2e.md も更新すること describe('E2E: Reset categories command (takt reset categories)', () => { let isolatedEnv: IsolatedEnv; - let repo: { path: string; cleanup: () => void }; + let repo: LocalRepo; beforeEach(() => { isolatedEnv = createIsolatedEnv(); diff --git a/e2e/specs/cli-switch.e2e.ts b/e2e/specs/cli-switch.e2e.ts index f9d05e8..2efa979 100644 --- a/e2e/specs/cli-switch.e2e.ts +++ b/e2e/specs/cli-switch.e2e.ts @@ -1,31 +1,12 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { mkdtempSync, writeFileSync, 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-switch-')); - 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 */ } - }, - }; -} +import { createLocalRepo, type LocalRepo } from '../helpers/test-repo'; // E2E更新時は docs/testing/e2e.md も更新すること describe('E2E: Switch piece command (takt switch)', () => { let isolatedEnv: IsolatedEnv; - let repo: { path: string; cleanup: () => void }; + let repo: LocalRepo; beforeEach(() => { isolatedEnv = createIsolatedEnv(); diff --git a/e2e/specs/cycle-detection.e2e.ts b/e2e/specs/cycle-detection.e2e.ts index b45f466..4c3503a 100644 --- a/e2e/specs/cycle-detection.e2e.ts +++ b/e2e/specs/cycle-detection.e2e.ts @@ -1,59 +1,22 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { resolve, dirname, join } from 'node:path'; +import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { mkdtempSync, writeFileSync, rmSync, readdirSync, readFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { execFileSync } from 'node:child_process'; import { createIsolatedEnv, updateIsolatedConfig, type IsolatedEnv, } from '../helpers/isolated-env'; import { runTakt } from '../helpers/takt-runner'; +import { createLocalRepo, type LocalRepo } from '../helpers/test-repo'; +import { readSessionRecords } from '../helpers/session-log'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -function createLocalRepo(): { path: string; cleanup: () => void } { - const repoPath = mkdtempSync(join(tmpdir(), 'takt-e2e-cycle-detect-')); - 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 */ } - }, - }; -} - -function readSessionRecords(repoPath: string): Array> { - const runsDir = join(repoPath, '.takt', 'runs'); - const runDirs = readdirSync(runsDir).sort(); - - for (const runDir of runDirs) { - const logsDir = join(runsDir, runDir, 'logs'); - const logFiles = readdirSync(logsDir).filter((file) => file.endsWith('.jsonl')); - for (const file of logFiles) { - const content = readFileSync(join(logsDir, file), 'utf-8').trim(); - if (!content) continue; - const records = content.split('\n').map((line) => JSON.parse(line) as Record); - if (records[0]?.type === 'piece_start') { - return records; - } - } - } - - throw new Error('Session NDJSON log not found'); -} - // E2E更新時は docs/testing/e2e.md も更新すること describe('E2E: Cycle detection via loop_monitors (mock)', () => { let isolatedEnv: IsolatedEnv; - let repo: { path: string; cleanup: () => void }; + let repo: LocalRepo; beforeEach(() => { isolatedEnv = createIsolatedEnv(); diff --git a/e2e/specs/eject.e2e.ts b/e2e/specs/eject.e2e.ts index 6ced7f3..bbb1628 100644 --- a/e2e/specs/eject.e2e.ts +++ b/e2e/specs/eject.e2e.ts @@ -1,40 +1,14 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { existsSync, readFileSync, mkdirSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs'; +import { existsSync, readFileSync } 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'; - -/** - * Create a minimal local git repository for eject tests. - * No GitHub access needed — just a local git init. - */ -function createLocalRepo(): { path: string; cleanup: () => void } { - const repoPath = mkdtempSync(join(tmpdir(), 'takt-eject-e2e-')); - 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' }); - // Create initial commit so branch exists - 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 - } - }, - }; -} +import { createLocalRepo, type LocalRepo } from '../helpers/test-repo'; // E2E更新時は docs/testing/e2e.md も更新すること describe('E2E: Eject builtin pieces (takt eject)', () => { let isolatedEnv: IsolatedEnv; - let repo: { path: string; cleanup: () => void }; + let repo: LocalRepo; beforeEach(() => { isolatedEnv = createIsolatedEnv(); diff --git a/e2e/specs/error-handling.e2e.ts b/e2e/specs/error-handling.e2e.ts index 9c6cb0d..92dadb7 100644 --- a/e2e/specs/error-handling.e2e.ts +++ b/e2e/specs/error-handling.e2e.ts @@ -1,36 +1,17 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { mkdtempSync, writeFileSync, 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'; +import { createLocalRepo, type LocalRepo } from '../helpers/test-repo'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -function createLocalRepo(): { path: string; cleanup: () => void } { - const repoPath = mkdtempSync(join(tmpdir(), 'takt-e2e-error-')); - 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: Error handling edge cases (mock)', () => { let isolatedEnv: IsolatedEnv; - let repo: { path: string; cleanup: () => void }; + let repo: LocalRepo; beforeEach(() => { isolatedEnv = createIsolatedEnv(); diff --git a/e2e/specs/model-override.e2e.ts b/e2e/specs/model-override.e2e.ts index d48e3b3..89a842b 100644 --- a/e2e/specs/model-override.e2e.ts +++ b/e2e/specs/model-override.e2e.ts @@ -1,35 +1,17 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { resolve, dirname, join } from 'node:path'; +import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'; -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'; +import { createLocalRepo, type LocalRepo } from '../helpers/test-repo'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -function createLocalRepo(): { path: string; cleanup: () => void } { - const repoPath = mkdtempSync(join(tmpdir(), 'takt-e2e-model-override-')); - 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: --model option override (mock)', () => { let isolatedEnv: IsolatedEnv; - let repo: { path: string; cleanup: () => void }; + let repo: LocalRepo; beforeEach(() => { isolatedEnv = createIsolatedEnv(); diff --git a/e2e/specs/multi-step-sequential.e2e.ts b/e2e/specs/multi-step-sequential.e2e.ts index fa7e6a3..e5f063e 100644 --- a/e2e/specs/multi-step-sequential.e2e.ts +++ b/e2e/specs/multi-step-sequential.e2e.ts @@ -1,55 +1,18 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { resolve, dirname, join } from 'node:path'; +import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { mkdtempSync, writeFileSync, rmSync, readdirSync, readFileSync } from 'node:fs'; -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'; +import { createLocalRepo, type LocalRepo } from '../helpers/test-repo'; +import { readSessionRecords } from '../helpers/session-log'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -function createLocalRepo(): { path: string; cleanup: () => void } { - const repoPath = mkdtempSync(join(tmpdir(), 'takt-e2e-sequential-step-')); - 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 */ } - }, - }; -} - -function readSessionRecords(repoPath: string): Array> { - const runsDir = join(repoPath, '.takt', 'runs'); - const runDirs = readdirSync(runsDir).sort(); - - for (const runDir of runDirs) { - const logsDir = join(runsDir, runDir, 'logs'); - const logFiles = readdirSync(logsDir).filter((file) => file.endsWith('.jsonl')); - for (const file of logFiles) { - const content = readFileSync(join(logsDir, file), 'utf-8').trim(); - if (!content) continue; - const records = content.split('\n').map((line) => JSON.parse(line) as Record); - if (records[0]?.type === 'piece_start') { - return records; - } - } - } - - throw new Error('Session NDJSON log not found'); -} - // E2E更新時は docs/testing/e2e.md も更新すること describe('E2E: Sequential multi-step session log transitions (mock)', () => { let isolatedEnv: IsolatedEnv; - let repo: { path: string; cleanup: () => void }; + let repo: LocalRepo; beforeEach(() => { isolatedEnv = createIsolatedEnv(); diff --git a/e2e/specs/piece-error-handling.e2e.ts b/e2e/specs/piece-error-handling.e2e.ts index 5badea4..0cfae86 100644 --- a/e2e/specs/piece-error-handling.e2e.ts +++ b/e2e/specs/piece-error-handling.e2e.ts @@ -1,36 +1,17 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { mkdtempSync, writeFileSync, 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'; +import { createLocalRepo, type LocalRepo } from '../helpers/test-repo'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -function createLocalRepo(): { path: string; cleanup: () => void } { - const repoPath = mkdtempSync(join(tmpdir(), 'takt-e2e-piece-err-')); - 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: Piece error handling (mock)', () => { let isolatedEnv: IsolatedEnv; - let repo: { path: string; cleanup: () => void }; + let repo: LocalRepo; beforeEach(() => { isolatedEnv = createIsolatedEnv(); diff --git a/e2e/specs/pipeline-local-repo.e2e.ts b/e2e/specs/pipeline-local-repo.e2e.ts index 695dbd7..12a6141 100644 --- a/e2e/specs/pipeline-local-repo.e2e.ts +++ b/e2e/specs/pipeline-local-repo.e2e.ts @@ -3,29 +3,13 @@ import { resolve, dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'; 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'; +import { createLocalRepo, type LocalRepo } from '../helpers/test-repo'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -function createLocalRepo(): { path: string; cleanup: () => void } { - const repoPath = mkdtempSync(join(tmpdir(), 'takt-e2e-pipeline-local-')); - 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 */ } - }, - }; -} - function createNonGitDir(): { path: string; cleanup: () => void } { const dirPath = mkdtempSync(join(tmpdir(), 'takt-e2e-pipeline-nongit-')); writeFileSync(join(dirPath, 'README.md'), '# non-git\n'); @@ -40,7 +24,7 @@ function createNonGitDir(): { path: string; cleanup: () => void } { // E2E更新時は docs/testing/e2e.md も更新すること describe('E2E: Pipeline --skip-git on local/non-git directories (mock)', () => { let isolatedEnv: IsolatedEnv; - let repo: { path: string; cleanup: () => void }; + let repo: LocalRepo; beforeEach(() => { isolatedEnv = createIsolatedEnv(); diff --git a/e2e/specs/provider-error.e2e.ts b/e2e/specs/provider-error.e2e.ts index e2e6978..511a72e 100644 --- a/e2e/specs/provider-error.e2e.ts +++ b/e2e/specs/provider-error.e2e.ts @@ -1,40 +1,21 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'; -import { join } from 'node:path'; -import { tmpdir } from 'node:os'; -import { execFileSync } from 'node:child_process'; import { createIsolatedEnv, updateIsolatedConfig, type IsolatedEnv, } from '../helpers/isolated-env'; import { runTakt } from '../helpers/takt-runner'; +import { createLocalRepo, type LocalRepo } from '../helpers/test-repo'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -function createLocalRepo(): { path: string; cleanup: () => void } { - const repoPath = mkdtempSync(join(tmpdir(), 'takt-e2e-provider-')); - 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: Provider error handling (mock)', () => { let isolatedEnv: IsolatedEnv; - let repo: { path: string; cleanup: () => void }; + let repo: LocalRepo; beforeEach(() => { isolatedEnv = createIsolatedEnv(); diff --git a/e2e/specs/quiet-mode.e2e.ts b/e2e/specs/quiet-mode.e2e.ts index 085fb04..5475fbe 100644 --- a/e2e/specs/quiet-mode.e2e.ts +++ b/e2e/specs/quiet-mode.e2e.ts @@ -1,36 +1,17 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { mkdtempSync, writeFileSync, 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'; +import { createLocalRepo, type LocalRepo } from '../helpers/test-repo'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -function createLocalRepo(): { path: string; cleanup: () => void } { - const repoPath = mkdtempSync(join(tmpdir(), 'takt-e2e-quiet-')); - 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: Quiet mode (--quiet)', () => { let isolatedEnv: IsolatedEnv; - let repo: { path: string; cleanup: () => void }; + let repo: LocalRepo; beforeEach(() => { isolatedEnv = createIsolatedEnv(); diff --git a/e2e/specs/report-file-output.e2e.ts b/e2e/specs/report-file-output.e2e.ts index 8fbec0c..e570713 100644 --- a/e2e/specs/report-file-output.e2e.ts +++ b/e2e/specs/report-file-output.e2e.ts @@ -1,39 +1,22 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { resolve, dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { mkdtempSync, writeFileSync, rmSync, existsSync, readdirSync, readFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { execFileSync } from 'node:child_process'; +import { existsSync, readdirSync, readFileSync } from 'node:fs'; import { createIsolatedEnv, updateIsolatedConfig, type IsolatedEnv, } from '../helpers/isolated-env'; import { runTakt } from '../helpers/takt-runner'; +import { createLocalRepo, type LocalRepo } from '../helpers/test-repo'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -function createLocalRepo(): { path: string; cleanup: () => void } { - const repoPath = mkdtempSync(join(tmpdir(), 'takt-e2e-report-file-')); - 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: Report file output (mock)', () => { let isolatedEnv: IsolatedEnv; - let repo: { path: string; cleanup: () => void }; + let repo: LocalRepo; beforeEach(() => { isolatedEnv = createIsolatedEnv(); diff --git a/e2e/specs/run-multiple-tasks.e2e.ts b/e2e/specs/run-multiple-tasks.e2e.ts index 518db71..ce1de94 100644 --- a/e2e/specs/run-multiple-tasks.e2e.ts +++ b/e2e/specs/run-multiple-tasks.e2e.ts @@ -1,40 +1,23 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; +import { mkdirSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; -import { tmpdir } from 'node:os'; -import { execFileSync } from 'node:child_process'; import { createIsolatedEnv, updateIsolatedConfig, type IsolatedEnv, } from '../helpers/isolated-env'; import { runTakt } from '../helpers/takt-runner'; +import { createLocalRepo, type LocalRepo } from '../helpers/test-repo'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -function createLocalRepo(): { path: string; cleanup: () => void } { - const repoPath = mkdtempSync(join(tmpdir(), 'takt-e2e-run-multi-')); - 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: Run multiple tasks (takt run)', () => { let isolatedEnv: IsolatedEnv; - let repo: { path: string; cleanup: () => void }; + let repo: LocalRepo; const piecePath = resolve(__dirname, '../fixtures/pieces/mock-single-step.yaml'); diff --git a/e2e/specs/session-log.e2e.ts b/e2e/specs/session-log.e2e.ts index 1b1280c..a65cdc2 100644 --- a/e2e/specs/session-log.e2e.ts +++ b/e2e/specs/session-log.e2e.ts @@ -1,55 +1,18 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { resolve, dirname, join } from 'node:path'; +import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { mkdtempSync, writeFileSync, rmSync, readdirSync, readFileSync } from 'node:fs'; -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'; +import { createLocalRepo, type LocalRepo } from '../helpers/test-repo'; +import { readSessionRecords } from '../helpers/session-log'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -function createLocalRepo(): { path: string; cleanup: () => void } { - const repoPath = mkdtempSync(join(tmpdir(), 'takt-e2e-session-log-')); - 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 */ } - }, - }; -} - -function readSessionRecords(repoPath: string): Array> { - const runsDir = join(repoPath, '.takt', 'runs'); - const runDirs = readdirSync(runsDir).sort(); - - for (const runDir of runDirs) { - const logsDir = join(runsDir, runDir, 'logs'); - const logFiles = readdirSync(logsDir).filter((file) => file.endsWith('.jsonl')); - for (const file of logFiles) { - const content = readFileSync(join(logsDir, file), 'utf-8').trim(); - if (!content) continue; - const records = content.split('\n').map((line) => JSON.parse(line) as Record); - if (records[0]?.type === 'piece_start') { - return records; - } - } - } - - throw new Error('Session NDJSON log not found'); -} - // E2E更新時は docs/testing/e2e.md も更新すること describe('E2E: Session NDJSON log output (mock)', () => { let isolatedEnv: IsolatedEnv; - let repo: { path: string; cleanup: () => void }; + let repo: LocalRepo; beforeEach(() => { isolatedEnv = createIsolatedEnv(); diff --git a/e2e/specs/task-content-file.e2e.ts b/e2e/specs/task-content-file.e2e.ts index 4e79acb..ae36721 100644 --- a/e2e/specs/task-content-file.e2e.ts +++ b/e2e/specs/task-content-file.e2e.ts @@ -1,40 +1,23 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; +import { mkdirSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; -import { tmpdir } from 'node:os'; -import { execFileSync } from 'node:child_process'; import { createIsolatedEnv, updateIsolatedConfig, type IsolatedEnv, } from '../helpers/isolated-env'; import { runTakt } from '../helpers/takt-runner'; +import { createLocalRepo, type LocalRepo } from '../helpers/test-repo'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -function createLocalRepo(): { path: string; cleanup: () => void } { - const repoPath = mkdtempSync(join(tmpdir(), 'takt-e2e-contentfile-')); - 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: Task content_file reference (mock)', () => { let isolatedEnv: IsolatedEnv; - let repo: { path: string; cleanup: () => void }; + let repo: LocalRepo; const piecePath = resolve(__dirname, '../fixtures/pieces/mock-single-step.yaml'); diff --git a/e2e/specs/task-status-persistence.e2e.ts b/e2e/specs/task-status-persistence.e2e.ts index 161f2c0..632b905 100644 --- a/e2e/specs/task-status-persistence.e2e.ts +++ b/e2e/specs/task-status-persistence.e2e.ts @@ -1,32 +1,15 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { resolve, dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { execFileSync } from 'node:child_process'; +import { mkdirSync, writeFileSync, readFileSync } from 'node:fs'; import { parse as parseYaml } from 'yaml'; import { createIsolatedEnv, updateIsolatedConfig, type IsolatedEnv } from '../helpers/isolated-env'; import { runTakt } from '../helpers/takt-runner'; +import { createLocalRepo, type LocalRepo } from '../helpers/test-repo'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -function createLocalRepo(): { path: string; cleanup: () => void } { - const repoPath = mkdtempSync(join(tmpdir(), 'takt-e2e-task-status-')); - 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 */ } - }, - }; -} - function writeSinglePendingTask(repoPath: string, piecePath: string): void { const now = new Date().toISOString(); mkdirSync(join(repoPath, '.takt'), { recursive: true }); @@ -49,7 +32,7 @@ function writeSinglePendingTask(repoPath: string, piecePath: string): void { // E2E更新時は docs/testing/e2e.md も更新すること describe('E2E: Task status persistence in tasks.yaml (mock)', () => { let isolatedEnv: IsolatedEnv; - let repo: { path: string; cleanup: () => void }; + let repo: LocalRepo; beforeEach(() => { isolatedEnv = createIsolatedEnv(); diff --git a/src/__tests__/blocked-handler.test.ts b/src/__tests__/blocked-handler.test.ts index 215b139..05a5081 100644 --- a/src/__tests__/blocked-handler.test.ts +++ b/src/__tests__/blocked-handler.test.ts @@ -6,17 +6,9 @@ import { describe, it, expect, vi } from 'vitest'; import { handleBlocked } from '../core/piece/engine/blocked-handler.js'; -import type { PieceMovement, AgentResponse } from '../core/models/types.js'; +import type { AgentResponse } from '../core/models/types.js'; import type { PieceEngineOptions } from '../core/piece/types.js'; - -function makeMovement(): PieceMovement { - return { - name: 'test-movement', - personaDisplayName: 'tester', - instructionTemplate: '', - passPreviousResponse: false, - }; -} +import { makeMovement } from './test-helpers.js'; function makeResponse(content: string): AgentResponse { return { diff --git a/src/__tests__/engine-test-helpers.ts b/src/__tests__/engine-test-helpers.ts index d8c893f..5ac943b 100644 --- a/src/__tests__/engine-test-helpers.ts +++ b/src/__tests__/engine-test-helpers.ts @@ -10,7 +10,8 @@ import { mkdirSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { randomUUID } from 'node:crypto'; -import type { PieceConfig, PieceMovement, AgentResponse, PieceRule } from '../core/models/index.js'; +import type { PieceConfig, PieceMovement, AgentResponse } from '../core/models/index.js'; +import { makeRule } from './test-helpers.js'; // --- Mock imports (consumers must call vi.mock before importing this) --- @@ -22,6 +23,8 @@ import { generateReportDir } from '../shared/utils/index.js'; // --- Factory functions --- +export { makeRule }; + export function makeResponse(overrides: Partial = {}): AgentResponse { return { persona: 'test-agent', @@ -33,10 +36,6 @@ export function makeResponse(overrides: Partial = {}): AgentRespo }; } -export function makeRule(condition: string, next: string, extra: Partial = {}): PieceRule { - return { condition, next, ...extra }; -} - export function makeMovement(name: string, overrides: Partial = {}): PieceMovement { return { name, diff --git a/src/__tests__/escape.test.ts b/src/__tests__/escape.test.ts index 081c643..8c97a04 100644 --- a/src/__tests__/escape.test.ts +++ b/src/__tests__/escape.test.ts @@ -9,31 +9,7 @@ import { escapeTemplateChars, replaceTemplatePlaceholders, } from '../core/piece/instruction/escape.js'; -import type { PieceMovement } from '../core/models/types.js'; -import type { InstructionContext } from '../core/piece/instruction/instruction-context.js'; - -function makeMovement(overrides: Partial = {}): PieceMovement { - return { - name: 'test-movement', - personaDisplayName: 'tester', - instructionTemplate: '', - passPreviousResponse: false, - ...overrides, - }; -} - -function makeContext(overrides: Partial = {}): InstructionContext { - return { - task: 'test task', - iteration: 1, - maxMovements: 10, - movementIteration: 1, - cwd: '/tmp/test', - projectCwd: '/tmp/project', - userInputs: [], - ...overrides, - }; -} +import { makeMovement, makeInstructionContext } from './test-helpers.js'; describe('escapeTemplateChars', () => { it('should replace curly braces with full-width equivalents', () => { @@ -62,7 +38,7 @@ describe('escapeTemplateChars', () => { describe('replaceTemplatePlaceholders', () => { it('should replace {task} placeholder', () => { const step = makeMovement(); - const ctx = makeContext({ task: 'implement feature X' }); + const ctx = makeInstructionContext({ task: 'implement feature X' }); const template = 'Your task is: {task}'; const result = replaceTemplatePlaceholders(template, step, ctx); @@ -71,7 +47,7 @@ describe('replaceTemplatePlaceholders', () => { it('should escape braces in task content', () => { const step = makeMovement(); - const ctx = makeContext({ task: 'fix {bug} in code' }); + const ctx = makeInstructionContext({ task: 'fix {bug} in code' }); const template = '{task}'; const result = replaceTemplatePlaceholders(template, step, ctx); @@ -80,7 +56,7 @@ describe('replaceTemplatePlaceholders', () => { it('should replace {iteration} and {max_movements}', () => { const step = makeMovement(); - const ctx = makeContext({ iteration: 3, maxMovements: 20 }); + const ctx = makeInstructionContext({ iteration: 3, maxMovements: 20 }); const template = 'Iteration {iteration}/{max_movements}'; const result = replaceTemplatePlaceholders(template, step, ctx); @@ -89,7 +65,7 @@ describe('replaceTemplatePlaceholders', () => { it('should replace {movement_iteration}', () => { const step = makeMovement(); - const ctx = makeContext({ movementIteration: 5 }); + const ctx = makeInstructionContext({ movementIteration: 5 }); const template = 'Movement run #{movement_iteration}'; const result = replaceTemplatePlaceholders(template, step, ctx); @@ -98,7 +74,7 @@ describe('replaceTemplatePlaceholders', () => { it('should replace {previous_response} when passPreviousResponse is true', () => { const step = makeMovement({ passPreviousResponse: true }); - const ctx = makeContext({ + const ctx = makeInstructionContext({ previousOutput: { persona: 'coder', status: 'done', @@ -114,7 +90,7 @@ describe('replaceTemplatePlaceholders', () => { it('should prefer preprocessed previous response text when provided', () => { const step = makeMovement({ passPreviousResponse: true }); - const ctx = makeContext({ + const ctx = makeInstructionContext({ previousOutput: { persona: 'coder', status: 'done', @@ -131,7 +107,7 @@ describe('replaceTemplatePlaceholders', () => { it('should replace {previous_response} with empty string when no previous output', () => { const step = makeMovement({ passPreviousResponse: true }); - const ctx = makeContext(); + const ctx = makeInstructionContext(); const template = 'Previous: {previous_response}'; const result = replaceTemplatePlaceholders(template, step, ctx); @@ -140,7 +116,7 @@ describe('replaceTemplatePlaceholders', () => { it('should not replace {previous_response} when passPreviousResponse is false', () => { const step = makeMovement({ passPreviousResponse: false }); - const ctx = makeContext({ + const ctx = makeInstructionContext({ previousOutput: { persona: 'coder', status: 'done', @@ -156,7 +132,7 @@ describe('replaceTemplatePlaceholders', () => { it('should replace {user_inputs} with joined inputs', () => { const step = makeMovement(); - const ctx = makeContext({ userInputs: ['input 1', 'input 2', 'input 3'] }); + const ctx = makeInstructionContext({ userInputs: ['input 1', 'input 2', 'input 3'] }); const template = 'Inputs: {user_inputs}'; const result = replaceTemplatePlaceholders(template, step, ctx); @@ -165,7 +141,7 @@ describe('replaceTemplatePlaceholders', () => { it('should replace {report_dir} with report directory', () => { const step = makeMovement(); - const ctx = makeContext({ reportDir: '/tmp/reports/run-1' }); + const ctx = makeInstructionContext({ reportDir: '/tmp/reports/run-1' }); const template = 'Reports: {report_dir}'; const result = replaceTemplatePlaceholders(template, step, ctx); @@ -174,7 +150,7 @@ describe('replaceTemplatePlaceholders', () => { it('should replace {report:filename} with full path', () => { const step = makeMovement(); - const ctx = makeContext({ reportDir: '/tmp/reports' }); + const ctx = makeInstructionContext({ reportDir: '/tmp/reports' }); const template = 'Read {report:review.md} and {report:plan.md}'; const result = replaceTemplatePlaceholders(template, step, ctx); @@ -183,7 +159,7 @@ describe('replaceTemplatePlaceholders', () => { it('should handle template with multiple different placeholders', () => { const step = makeMovement(); - const ctx = makeContext({ + const ctx = makeInstructionContext({ task: 'test task', iteration: 2, maxMovements: 5, @@ -198,7 +174,7 @@ describe('replaceTemplatePlaceholders', () => { it('should leave unreplaced placeholders when reportDir is undefined', () => { const step = makeMovement(); - const ctx = makeContext({ reportDir: undefined }); + const ctx = makeInstructionContext({ reportDir: undefined }); const template = 'Dir: {report_dir} File: {report:test.md}'; const result = replaceTemplatePlaceholders(template, step, ctx); diff --git a/src/__tests__/instruction-helpers.test.ts b/src/__tests__/instruction-helpers.test.ts index ee75ba9..dcb6c50 100644 --- a/src/__tests__/instruction-helpers.test.ts +++ b/src/__tests__/instruction-helpers.test.ts @@ -10,31 +10,8 @@ import { renderReportContext, renderReportOutputInstruction, } from '../core/piece/instruction/InstructionBuilder.js'; -import type { PieceMovement, OutputContractEntry } from '../core/models/types.js'; -import type { InstructionContext } from '../core/piece/instruction/instruction-context.js'; - -function makeMovement(overrides: Partial = {}): PieceMovement { - return { - name: 'test-movement', - personaDisplayName: 'tester', - instructionTemplate: '', - passPreviousResponse: false, - ...overrides, - }; -} - -function makeContext(overrides: Partial = {}): InstructionContext { - return { - task: 'test task', - iteration: 1, - maxMovements: 10, - movementIteration: 1, - cwd: '/tmp/test', - projectCwd: '/tmp/project', - userInputs: [], - ...overrides, - }; -} +import type { OutputContractEntry } from '../core/models/types.js'; +import { makeMovement, makeInstructionContext } from './test-helpers.js'; describe('isOutputContractItem', () => { it('should return true for OutputContractItem (has name)', () => { @@ -84,19 +61,19 @@ describe('renderReportContext', () => { describe('renderReportOutputInstruction', () => { it('should return empty string when no output contracts', () => { const step = makeMovement(); - const ctx = makeContext({ reportDir: '/tmp/reports' }); + const ctx = makeInstructionContext({ reportDir: '/tmp/reports' }); expect(renderReportOutputInstruction(step, ctx, 'en')).toBe(''); }); it('should return empty string when no reportDir', () => { const step = makeMovement({ outputContracts: [{ name: 'report.md' }] }); - const ctx = makeContext(); + const ctx = makeInstructionContext(); expect(renderReportOutputInstruction(step, ctx, 'en')).toBe(''); }); it('should render English single-file instruction', () => { const step = makeMovement({ outputContracts: [{ name: 'report.md' }] }); - const ctx = makeContext({ reportDir: '/tmp/reports', movementIteration: 2 }); + const ctx = makeInstructionContext({ reportDir: '/tmp/reports', movementIteration: 2 }); const result = renderReportOutputInstruction(step, ctx, 'en'); expect(result).toContain('Report output'); @@ -108,7 +85,7 @@ describe('renderReportOutputInstruction', () => { const step = makeMovement({ outputContracts: [{ name: 'plan.md' }, { name: 'review.md' }], }); - const ctx = makeContext({ reportDir: '/tmp/reports' }); + const ctx = makeInstructionContext({ reportDir: '/tmp/reports' }); const result = renderReportOutputInstruction(step, ctx, 'en'); expect(result).toContain('Report Files'); @@ -116,7 +93,7 @@ describe('renderReportOutputInstruction', () => { it('should render Japanese single-file instruction', () => { const step = makeMovement({ outputContracts: [{ name: 'report.md' }] }); - const ctx = makeContext({ reportDir: '/tmp/reports', movementIteration: 1 }); + const ctx = makeInstructionContext({ reportDir: '/tmp/reports', movementIteration: 1 }); const result = renderReportOutputInstruction(step, ctx, 'ja'); expect(result).toContain('レポート出力'); @@ -128,7 +105,7 @@ describe('renderReportOutputInstruction', () => { const step = makeMovement({ outputContracts: [{ name: 'plan.md' }, { name: 'review.md' }], }); - const ctx = makeContext({ reportDir: '/tmp/reports' }); + const ctx = makeInstructionContext({ reportDir: '/tmp/reports' }); const result = renderReportOutputInstruction(step, ctx, 'ja'); expect(result).toContain('Report Files'); diff --git a/src/__tests__/it-error-recovery.test.ts b/src/__tests__/it-error-recovery.test.ts index 0635fc0..5df4423 100644 --- a/src/__tests__/it-error-recovery.test.ts +++ b/src/__tests__/it-error-recovery.test.ts @@ -15,6 +15,7 @@ import { tmpdir } from 'node:os'; import { setMockScenario, resetScenario } from '../infra/mock/index.js'; import type { PieceConfig, PieceMovement, PieceRule } from '../core/models/index.js'; import { detectRuleIndex } from '../infra/claude/index.js'; +import { makeRule } from './test-helpers.js'; import { callAiJudge } from '../agents/ai-judge.js'; // --- Mocks --- @@ -56,10 +57,6 @@ import { PieceEngine } from '../core/piece/index.js'; // --- Test helpers --- -function makeRule(condition: string, next: string): PieceRule { - return { condition, next }; -} - function makeMovement(name: string, agentPath: string, rules: PieceRule[]): PieceMovement { return { name, diff --git a/src/__tests__/it-instruction-builder.test.ts b/src/__tests__/it-instruction-builder.test.ts index 3753b10..dac6b25 100644 --- a/src/__tests__/it-instruction-builder.test.ts +++ b/src/__tests__/it-instruction-builder.test.ts @@ -8,7 +8,8 @@ */ import { describe, it, expect, vi } from 'vitest'; -import type { PieceMovement, PieceRule, AgentResponse } from '../core/models/index.js'; +import type { PieceMovement, AgentResponse } from '../core/models/index.js'; +import { makeRule } from './test-helpers.js'; vi.mock('../infra/config/global/globalConfig.js', () => ({ loadGlobalConfig: vi.fn().mockReturnValue({}), @@ -34,10 +35,6 @@ function buildStatusJudgmentInstruction(movement: PieceMovement, ctx: StatusJudg // --- Test helpers --- -function makeRule(condition: string, next: string, extra?: Partial): PieceRule { - return { condition, next, ...extra }; -} - function makeMovement(overrides: Partial = {}): PieceMovement { return { name: 'test-step', diff --git a/src/__tests__/it-piece-execution.test.ts b/src/__tests__/it-piece-execution.test.ts index 3539bb7..dd40656 100644 --- a/src/__tests__/it-piece-execution.test.ts +++ b/src/__tests__/it-piece-execution.test.ts @@ -16,6 +16,7 @@ import { tmpdir } from 'node:os'; import { setMockScenario, resetScenario } from '../infra/mock/index.js'; import type { PieceConfig, PieceMovement, PieceRule } from '../core/models/index.js'; import { detectRuleIndex } from '../infra/claude/index.js'; +import { makeRule } from './test-helpers.js'; import { callAiJudge } from '../agents/ai-judge.js'; // --- Mocks (minimal — only infrastructure, not core logic) --- @@ -59,10 +60,6 @@ import { PieceEngine } from '../core/piece/index.js'; // --- Test helpers --- -function makeRule(condition: string, next: string): PieceRule { - return { condition, next }; -} - function makeMovement(name: string, agentPath: string, rules: PieceRule[]): PieceMovement { return { name, diff --git a/src/__tests__/it-rule-evaluation.test.ts b/src/__tests__/it-rule-evaluation.test.ts index 5bd728f..0facf7d 100644 --- a/src/__tests__/it-rule-evaluation.test.ts +++ b/src/__tests__/it-rule-evaluation.test.ts @@ -16,6 +16,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import type { PieceMovement, PieceState, PieceRule, AgentResponse } from '../core/models/index.js'; +import { makeRule } from './test-helpers.js'; // --- Mocks --- @@ -39,10 +40,6 @@ import type { RuleMatch, RuleEvaluatorContext } from '../core/piece/index.js'; // --- Test helpers --- -function makeRule(condition: string, next: string, extra?: Partial): PieceRule { - return { condition, next, ...extra }; -} - function makeMovement( name: string, rules: PieceRule[], diff --git a/src/__tests__/it-three-phase-execution.test.ts b/src/__tests__/it-three-phase-execution.test.ts index a7e250a..9a9bf98 100644 --- a/src/__tests__/it-three-phase-execution.test.ts +++ b/src/__tests__/it-three-phase-execution.test.ts @@ -16,6 +16,7 @@ import { tmpdir } from 'node:os'; import { setMockScenario, resetScenario } from '../infra/mock/index.js'; import type { PieceConfig, PieceMovement, PieceRule } from '../core/models/index.js'; import { detectRuleIndex } from '../infra/claude/index.js'; +import { makeRule } from './test-helpers.js'; import { callAiJudge } from '../agents/ai-judge.js'; // --- Mocks --- @@ -61,10 +62,6 @@ import { PieceEngine } from '../core/piece/index.js'; // --- Test helpers --- -function makeRule(condition: string, next: string): PieceRule { - return { condition, next }; -} - function createTestEnv(): { dir: string; agentPath: string } { const dir = mkdtempSync(join(tmpdir(), 'takt-it-3ph-')); mkdirSync(join(dir, '.takt', 'reports', 'test-report-dir'), { recursive: true }); diff --git a/src/__tests__/judgment-strategies.test.ts b/src/__tests__/judgment-strategies.test.ts index 927c1e9..18d72b9 100644 --- a/src/__tests__/judgment-strategies.test.ts +++ b/src/__tests__/judgment-strategies.test.ts @@ -15,17 +15,7 @@ import { JudgmentStrategyFactory, type JudgmentContext, } from '../core/piece/judgment/FallbackStrategy.js'; -import type { PieceMovement } from '../core/models/types.js'; - -function makeMovement(overrides: Partial = {}): PieceMovement { - return { - name: 'test-movement', - personaDisplayName: 'tester', - instructionTemplate: '', - passPreviousResponse: false, - ...overrides, - }; -} +import { makeMovement } from './test-helpers.js'; function makeContext(overrides: Partial = {}): JudgmentContext { return { diff --git a/src/__tests__/rule-evaluator.test.ts b/src/__tests__/rule-evaluator.test.ts index 7cfd834..444e8d0 100644 --- a/src/__tests__/rule-evaluator.test.ts +++ b/src/__tests__/rule-evaluator.test.ts @@ -6,17 +6,8 @@ import { describe, it, expect, vi } from 'vitest'; import { RuleEvaluator, type RuleEvaluatorContext } from '../core/piece/evaluation/RuleEvaluator.js'; -import type { PieceMovement, PieceState } from '../core/models/types.js'; - -function makeMovement(overrides: Partial = {}): PieceMovement { - return { - name: 'test-movement', - personaDisplayName: 'tester', - instructionTemplate: '', - passPreviousResponse: false, - ...overrides, - }; -} +import type { PieceState } from '../core/models/types.js'; +import { makeMovement } from './test-helpers.js'; function makeState(): PieceState { return { diff --git a/src/__tests__/rule-utils.test.ts b/src/__tests__/rule-utils.test.ts index b690377..80e184b 100644 --- a/src/__tests__/rule-utils.test.ts +++ b/src/__tests__/rule-utils.test.ts @@ -12,17 +12,8 @@ import { getAutoSelectedTag, getReportFiles, } from '../core/piece/evaluation/rule-utils.js'; -import type { PieceMovement, OutputContractEntry } from '../core/models/types.js'; - -function makeMovement(overrides: Partial = {}): PieceMovement { - return { - name: 'test-movement', - personaDisplayName: 'tester', - instructionTemplate: '', - passPreviousResponse: false, - ...overrides, - }; -} +import type { OutputContractEntry } from '../core/models/types.js'; +import { makeMovement } from './test-helpers.js'; describe('hasTagBasedRules', () => { it('should return false when movement has no rules', () => { diff --git a/src/__tests__/test-helpers.ts b/src/__tests__/test-helpers.ts new file mode 100644 index 0000000..e6118df --- /dev/null +++ b/src/__tests__/test-helpers.ts @@ -0,0 +1,36 @@ +/** + * Shared helpers for unit tests and integration tests. + * + * Unlike engine-test-helpers.ts, this file has no mock dependencies and + * can be safely imported from any test file without requiring vi.mock() setup. + */ + +import type { PieceMovement, PieceRule } from '../core/models/types.js'; +import type { InstructionContext } from '../core/piece/instruction/instruction-context.js'; + +export function makeRule(condition: string, next: string, extra: Partial = {}): PieceRule { + return { condition, next, ...extra }; +} + +export function makeMovement(overrides: Partial = {}): PieceMovement { + return { + name: 'test-movement', + personaDisplayName: 'tester', + instructionTemplate: '', + passPreviousResponse: false, + ...overrides, + }; +} + +export function makeInstructionContext(overrides: Partial = {}): InstructionContext { + return { + task: 'test task', + iteration: 1, + maxMovements: 10, + movementIteration: 1, + cwd: '/tmp/test', + projectCwd: '/tmp/project', + userInputs: [], + ...overrides, + }; +}