Merge remote-tracking branch 'e2e-repo/takt/20260209T2213-add-e2e-tests' into develop
This commit is contained in:
commit
d73643dcd9
5
e2e/fixtures/pieces/broken.yaml
Normal file
5
e2e/fixtures/pieces/broken.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
name: broken
|
||||
this is not valid YAML
|
||||
- indentation: [wrong
|
||||
movements:
|
||||
broken: {{{
|
||||
27
e2e/fixtures/pieces/mock-max-iter.yaml
Normal file
27
e2e/fixtures/pieces/mock-max-iter.yaml
Normal file
@ -0,0 +1,27 @@
|
||||
name: e2e-mock-max-iter
|
||||
description: Piece with max_iterations=2 that loops between two steps
|
||||
|
||||
max_iterations: 2
|
||||
|
||||
initial_movement: step-a
|
||||
|
||||
movements:
|
||||
- name: step-a
|
||||
edit: true
|
||||
persona: ../agents/test-coder.md
|
||||
permission_mode: edit
|
||||
instruction_template: |
|
||||
{task}
|
||||
rules:
|
||||
- condition: Done
|
||||
next: step-b
|
||||
|
||||
- name: step-b
|
||||
edit: true
|
||||
persona: ../agents/test-coder.md
|
||||
permission_mode: edit
|
||||
instruction_template: |
|
||||
Continue the task.
|
||||
rules:
|
||||
- condition: Done
|
||||
next: step-a
|
||||
15
e2e/fixtures/pieces/mock-no-match.yaml
Normal file
15
e2e/fixtures/pieces/mock-no-match.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
name: e2e-mock-no-match
|
||||
description: Piece with a strict rule condition that will not match mock output
|
||||
|
||||
max_iterations: 3
|
||||
|
||||
movements:
|
||||
- name: execute
|
||||
edit: true
|
||||
persona: ../agents/test-coder.md
|
||||
permission_mode: edit
|
||||
instruction_template: |
|
||||
{task}
|
||||
rules:
|
||||
- condition: SpecificMatchThatWillNotOccur
|
||||
next: COMPLETE
|
||||
27
e2e/fixtures/pieces/mock-two-step.yaml
Normal file
27
e2e/fixtures/pieces/mock-two-step.yaml
Normal file
@ -0,0 +1,27 @@
|
||||
name: e2e-mock-two-step
|
||||
description: Two-step sequential piece for E2E testing
|
||||
|
||||
max_iterations: 5
|
||||
|
||||
initial_movement: step-1
|
||||
|
||||
movements:
|
||||
- name: step-1
|
||||
edit: true
|
||||
persona: ../agents/test-coder.md
|
||||
permission_mode: edit
|
||||
instruction_template: |
|
||||
{task}
|
||||
rules:
|
||||
- condition: Done
|
||||
next: step-2
|
||||
|
||||
- name: step-2
|
||||
edit: true
|
||||
persona: ../agents/test-coder.md
|
||||
permission_mode: edit
|
||||
instruction_template: |
|
||||
Continue the task.
|
||||
rules:
|
||||
- condition: Done
|
||||
next: COMPLETE
|
||||
18
e2e/fixtures/scenarios/max-iter-loop.json
Normal file
18
e2e/fixtures/scenarios/max-iter-loop.json
Normal file
@ -0,0 +1,18 @@
|
||||
[
|
||||
{
|
||||
"status": "done",
|
||||
"content": "Step A output."
|
||||
},
|
||||
{
|
||||
"status": "done",
|
||||
"content": "Step B output."
|
||||
},
|
||||
{
|
||||
"status": "done",
|
||||
"content": "Step A output again."
|
||||
},
|
||||
{
|
||||
"status": "done",
|
||||
"content": "Step B output again."
|
||||
}
|
||||
]
|
||||
6
e2e/fixtures/scenarios/no-match.json
Normal file
6
e2e/fixtures/scenarios/no-match.json
Normal file
@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"status": "error",
|
||||
"content": "Simulated failure: API error during execution"
|
||||
}
|
||||
]
|
||||
6
e2e/fixtures/scenarios/one-entry-only.json
Normal file
6
e2e/fixtures/scenarios/one-entry-only.json
Normal file
@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"status": "done",
|
||||
"content": "Only entry in scenario."
|
||||
}
|
||||
]
|
||||
14
e2e/fixtures/scenarios/run-three-tasks.json
Normal file
14
e2e/fixtures/scenarios/run-three-tasks.json
Normal file
@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"status": "done",
|
||||
"content": "Task 1 completed successfully."
|
||||
},
|
||||
{
|
||||
"status": "done",
|
||||
"content": "Task 2 completed successfully."
|
||||
},
|
||||
{
|
||||
"status": "done",
|
||||
"content": "Task 3 completed successfully."
|
||||
}
|
||||
]
|
||||
14
e2e/fixtures/scenarios/run-with-failure.json
Normal file
14
e2e/fixtures/scenarios/run-with-failure.json
Normal file
@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"status": "done",
|
||||
"content": "Task 1 completed successfully."
|
||||
},
|
||||
{
|
||||
"status": "error",
|
||||
"content": "Task 2 encountered an error."
|
||||
},
|
||||
{
|
||||
"status": "done",
|
||||
"content": "Task 3 completed successfully."
|
||||
}
|
||||
]
|
||||
10
e2e/fixtures/scenarios/two-step-done.json
Normal file
10
e2e/fixtures/scenarios/two-step-done.json
Normal file
@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"status": "done",
|
||||
"content": "Step 1 output text completed."
|
||||
},
|
||||
{
|
||||
"status": "done",
|
||||
"content": "Step 2 output text completed."
|
||||
}
|
||||
]
|
||||
85
e2e/specs/cli-catalog.e2e.ts
Normal file
85
e2e/specs/cli-catalog.e2e.ts
Normal file
@ -0,0 +1,85 @@
|
||||
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 */ }
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// E2E更新時は docs/testing/e2e.md も更新すること
|
||||
describe('E2E: Catalog command (takt catalog)', () => {
|
||||
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 list all facet types when no argument given', () => {
|
||||
// Given: a local repo with isolated env
|
||||
|
||||
// When: running takt catalog
|
||||
const result = runTakt({
|
||||
args: ['catalog'],
|
||||
cwd: repo.path,
|
||||
env: isolatedEnv.env,
|
||||
});
|
||||
|
||||
// Then: output contains facet type sections
|
||||
expect(result.exitCode).toBe(0);
|
||||
const output = result.stdout.toLowerCase();
|
||||
expect(output).toMatch(/persona/);
|
||||
});
|
||||
|
||||
it('should list facets for a specific type', () => {
|
||||
// Given: a local repo with isolated env
|
||||
|
||||
// When: running takt catalog personas
|
||||
const result = runTakt({
|
||||
args: ['catalog', 'personas'],
|
||||
cwd: repo.path,
|
||||
env: isolatedEnv.env,
|
||||
});
|
||||
|
||||
// Then: output contains persona names
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toMatch(/coder/i);
|
||||
});
|
||||
|
||||
it('should error for an invalid facet type', () => {
|
||||
// Given: a local repo with isolated env
|
||||
|
||||
// When: running takt catalog with an invalid type
|
||||
const result = runTakt({
|
||||
args: ['catalog', 'invalidtype'],
|
||||
cwd: repo.path,
|
||||
env: isolatedEnv.env,
|
||||
});
|
||||
|
||||
// Then: output contains an error or lists valid types
|
||||
const combined = result.stdout + result.stderr;
|
||||
expect(combined).toMatch(/invalid|not found|valid types|unknown/i);
|
||||
});
|
||||
});
|
||||
55
e2e/specs/cli-clear.e2e.ts
Normal file
55
e2e/specs/cli-clear.e2e.ts
Normal file
@ -0,0 +1,55 @@
|
||||
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 */ }
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// E2E更新時は docs/testing/e2e.md も更新すること
|
||||
describe('E2E: Clear sessions command (takt clear)', () => {
|
||||
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 clear sessions without error', () => {
|
||||
// Given: a local repo with isolated env
|
||||
|
||||
// When: running takt clear
|
||||
const result = runTakt({
|
||||
args: ['clear'],
|
||||
cwd: repo.path,
|
||||
env: isolatedEnv.env,
|
||||
});
|
||||
|
||||
// Then: exits cleanly
|
||||
expect(result.exitCode).toBe(0);
|
||||
const output = result.stdout.toLowerCase();
|
||||
expect(output).toMatch(/clear|session|removed|no session/);
|
||||
});
|
||||
});
|
||||
73
e2e/specs/cli-help.e2e.ts
Normal file
73
e2e/specs/cli-help.e2e.ts
Normal file
@ -0,0 +1,73 @@
|
||||
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 */ }
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// E2E更新時は docs/testing/e2e.md も更新すること
|
||||
describe('E2E: Help command (takt --help)', () => {
|
||||
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 display subcommand list with --help', () => {
|
||||
// Given: a local repo with isolated env
|
||||
|
||||
// When: running takt --help
|
||||
const result = runTakt({
|
||||
args: ['--help'],
|
||||
cwd: repo.path,
|
||||
env: isolatedEnv.env,
|
||||
});
|
||||
|
||||
// Then: output lists subcommands
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toMatch(/run/);
|
||||
expect(result.stdout).toMatch(/add/);
|
||||
expect(result.stdout).toMatch(/list/);
|
||||
expect(result.stdout).toMatch(/eject/);
|
||||
});
|
||||
|
||||
it('should display run subcommand help with takt run --help', () => {
|
||||
// Given: a local repo with isolated env
|
||||
|
||||
// When: running takt run --help
|
||||
const result = runTakt({
|
||||
args: ['run', '--help'],
|
||||
cwd: repo.path,
|
||||
env: isolatedEnv.env,
|
||||
});
|
||||
|
||||
// Then: output contains run command description
|
||||
expect(result.exitCode).toBe(0);
|
||||
const output = result.stdout.toLowerCase();
|
||||
expect(output).toMatch(/run|task|pending/);
|
||||
});
|
||||
});
|
||||
76
e2e/specs/cli-prompt.e2e.ts
Normal file
76
e2e/specs/cli-prompt.e2e.ts
Normal file
@ -0,0 +1,76 @@
|
||||
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';
|
||||
|
||||
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 };
|
||||
|
||||
beforeEach(() => {
|
||||
isolatedEnv = createIsolatedEnv();
|
||||
repo = createLocalRepo();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
try { repo.cleanup(); } catch { /* best-effort */ }
|
||||
try { isolatedEnv.cleanup(); } catch { /* best-effort */ }
|
||||
});
|
||||
|
||||
it('should output prompt preview header and movement info for a piece', () => {
|
||||
// Given: a piece file path
|
||||
const piecePath = resolve(__dirname, '../fixtures/pieces/mock-single-step.yaml');
|
||||
|
||||
// When: running takt prompt with piece path
|
||||
const result = runTakt({
|
||||
args: ['prompt', piecePath],
|
||||
cwd: repo.path,
|
||||
env: isolatedEnv.env,
|
||||
});
|
||||
|
||||
// Then: output contains "Prompt Preview" header and movement info
|
||||
// (may fail on Phase 3 for pieces with tag-based rules, but header is still output)
|
||||
const combined = result.stdout + result.stderr;
|
||||
expect(combined).toMatch(/Prompt Preview|Movement 1/i);
|
||||
});
|
||||
|
||||
it('should report not found for a nonexistent piece name', () => {
|
||||
// Given: a nonexistent piece name
|
||||
|
||||
// When: running takt prompt with invalid piece
|
||||
const result = runTakt({
|
||||
args: ['prompt', 'nonexistent-piece-xyz'],
|
||||
cwd: repo.path,
|
||||
env: isolatedEnv.env,
|
||||
});
|
||||
|
||||
// Then: reports piece not found
|
||||
const combined = result.stdout + result.stderr;
|
||||
expect(combined).toMatch(/not found/i);
|
||||
});
|
||||
});
|
||||
70
e2e/specs/cli-switch.e2e.ts
Normal file
70
e2e/specs/cli-switch.e2e.ts
Normal file
@ -0,0 +1,70 @@
|
||||
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 */ }
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// E2E更新時は docs/testing/e2e.md も更新すること
|
||||
describe('E2E: Switch piece command (takt switch)', () => {
|
||||
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 piece when a valid piece name is given', () => {
|
||||
// Given: a local repo with isolated env
|
||||
|
||||
// When: running takt switch default
|
||||
const result = runTakt({
|
||||
args: ['switch', 'default'],
|
||||
cwd: repo.path,
|
||||
env: isolatedEnv.env,
|
||||
});
|
||||
|
||||
// Then: exits successfully
|
||||
expect(result.exitCode).toBe(0);
|
||||
const output = result.stdout.toLowerCase();
|
||||
expect(output).toMatch(/default|switched|piece/);
|
||||
});
|
||||
|
||||
it('should error when a nonexistent piece name is given', () => {
|
||||
// Given: a local repo with isolated env
|
||||
|
||||
// When: running takt switch with a nonexistent piece name
|
||||
const result = runTakt({
|
||||
args: ['switch', 'nonexistent-piece-xyz'],
|
||||
cwd: repo.path,
|
||||
env: isolatedEnv.env,
|
||||
});
|
||||
|
||||
// Then: error output
|
||||
const combined = result.stdout + result.stderr;
|
||||
expect(combined).toMatch(/not found|error|does not exist/i);
|
||||
});
|
||||
});
|
||||
157
e2e/specs/error-handling.e2e.ts
Normal file
157
e2e/specs/error-handling.e2e.ts
Normal file
@ -0,0 +1,157 @@
|
||||
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';
|
||||
|
||||
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 };
|
||||
|
||||
beforeEach(() => {
|
||||
isolatedEnv = createIsolatedEnv();
|
||||
repo = createLocalRepo();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
try { repo.cleanup(); } catch { /* best-effort */ }
|
||||
try { isolatedEnv.cleanup(); } catch { /* best-effort */ }
|
||||
});
|
||||
|
||||
it('should error when --piece points to a nonexistent file path', () => {
|
||||
// Given: a nonexistent piece file path
|
||||
|
||||
// When: running with a bad piece path
|
||||
const result = runTakt({
|
||||
args: [
|
||||
'--task', 'test',
|
||||
'--piece', '/nonexistent/path/to/piece.yaml',
|
||||
'--create-worktree', 'no',
|
||||
'--provider', 'mock',
|
||||
],
|
||||
cwd: repo.path,
|
||||
env: isolatedEnv.env,
|
||||
timeout: 240_000,
|
||||
});
|
||||
|
||||
// Then: exits with error
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
const combined = result.stdout + result.stderr;
|
||||
expect(combined).toMatch(/not found|does not exist|ENOENT/i);
|
||||
}, 240_000);
|
||||
|
||||
it('should report error when --piece specifies a nonexistent piece name', () => {
|
||||
// Given: a nonexistent piece name
|
||||
|
||||
// When: running with a bad piece name
|
||||
const result = runTakt({
|
||||
args: [
|
||||
'--task', 'test',
|
||||
'--piece', 'nonexistent-piece-name-xyz',
|
||||
'--create-worktree', 'no',
|
||||
'--provider', 'mock',
|
||||
],
|
||||
cwd: repo.path,
|
||||
env: isolatedEnv.env,
|
||||
timeout: 240_000,
|
||||
});
|
||||
|
||||
// Then: output contains error about piece not found
|
||||
// Note: takt reports the error but currently exits with code 0
|
||||
const combined = result.stdout + result.stderr;
|
||||
expect(combined).toMatch(/not found/i);
|
||||
}, 240_000);
|
||||
|
||||
it('should error when --pipeline is used without --task or --issue', () => {
|
||||
// Given: pipeline mode with no task or issue
|
||||
const piecePath = resolve(__dirname, '../fixtures/pieces/mock-single-step.yaml');
|
||||
|
||||
// When: running in pipeline mode without a task
|
||||
const result = runTakt({
|
||||
args: [
|
||||
'--pipeline',
|
||||
'--piece', piecePath,
|
||||
'--skip-git',
|
||||
'--provider', 'mock',
|
||||
],
|
||||
cwd: repo.path,
|
||||
env: isolatedEnv.env,
|
||||
timeout: 240_000,
|
||||
});
|
||||
|
||||
// Then: exits with error (should not hang in interactive mode due to TAKT_NO_TTY=1)
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
const combined = result.stdout + result.stderr;
|
||||
expect(combined).toMatch(/task|issue|required/i);
|
||||
}, 240_000);
|
||||
|
||||
it('should error when --create-worktree receives an invalid value', () => {
|
||||
// Given: invalid worktree value
|
||||
const piecePath = resolve(__dirname, '../fixtures/pieces/mock-single-step.yaml');
|
||||
|
||||
// When: running with invalid worktree option
|
||||
const result = runTakt({
|
||||
args: [
|
||||
'--task', 'test',
|
||||
'--piece', piecePath,
|
||||
'--create-worktree', 'invalid-value',
|
||||
'--provider', 'mock',
|
||||
],
|
||||
cwd: repo.path,
|
||||
env: isolatedEnv.env,
|
||||
timeout: 240_000,
|
||||
});
|
||||
|
||||
// Then: exits with error or warning about invalid value
|
||||
const combined = result.stdout + result.stderr;
|
||||
const hasError = result.exitCode !== 0 || combined.match(/invalid|error|must be/i);
|
||||
expect(hasError).toBeTruthy();
|
||||
}, 240_000);
|
||||
|
||||
it('should error when piece file contains invalid YAML', () => {
|
||||
// Given: a broken YAML piece file
|
||||
const brokenPiecePath = resolve(__dirname, '../fixtures/pieces/broken.yaml');
|
||||
|
||||
// When: running with the broken piece
|
||||
const result = runTakt({
|
||||
args: [
|
||||
'--task', 'test',
|
||||
'--piece', brokenPiecePath,
|
||||
'--create-worktree', 'no',
|
||||
'--provider', 'mock',
|
||||
],
|
||||
cwd: repo.path,
|
||||
env: isolatedEnv.env,
|
||||
timeout: 240_000,
|
||||
});
|
||||
|
||||
// Then: exits with error about parsing
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
const combined = result.stdout + result.stderr;
|
||||
expect(combined).toMatch(/parse|invalid|error|validation/i);
|
||||
}, 240_000);
|
||||
});
|
||||
124
e2e/specs/piece-error-handling.e2e.ts
Normal file
124
e2e/specs/piece-error-handling.e2e.ts
Normal file
@ -0,0 +1,124 @@
|
||||
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';
|
||||
|
||||
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 };
|
||||
|
||||
beforeEach(() => {
|
||||
isolatedEnv = createIsolatedEnv();
|
||||
repo = createLocalRepo();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
try { repo.cleanup(); } catch { /* best-effort */ }
|
||||
try { isolatedEnv.cleanup(); } catch { /* best-effort */ }
|
||||
});
|
||||
|
||||
it('should abort when agent returns error status', () => {
|
||||
// Given: a piece and a scenario that returns error status
|
||||
const piecePath = resolve(__dirname, '../fixtures/pieces/mock-no-match.yaml');
|
||||
const scenarioPath = resolve(__dirname, '../fixtures/scenarios/no-match.json');
|
||||
|
||||
// When: executing the piece
|
||||
const result = runTakt({
|
||||
args: [
|
||||
'--task', 'Test error status abort',
|
||||
'--piece', piecePath,
|
||||
'--create-worktree', 'no',
|
||||
'--provider', 'mock',
|
||||
],
|
||||
cwd: repo.path,
|
||||
env: {
|
||||
...isolatedEnv.env,
|
||||
TAKT_MOCK_SCENARIO: scenarioPath,
|
||||
},
|
||||
timeout: 240_000,
|
||||
});
|
||||
|
||||
// Then: piece aborts with a non-zero exit code
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
const combined = result.stdout + result.stderr;
|
||||
expect(combined).toMatch(/failed|aborted|error/i);
|
||||
}, 240_000);
|
||||
|
||||
it('should abort when max_iterations is reached', () => {
|
||||
// Given: a piece with max_iterations=2 that loops between step-a and step-b
|
||||
const piecePath = resolve(__dirname, '../fixtures/pieces/mock-max-iter.yaml');
|
||||
const scenarioPath = resolve(__dirname, '../fixtures/scenarios/max-iter-loop.json');
|
||||
|
||||
// When: executing the piece
|
||||
const result = runTakt({
|
||||
args: [
|
||||
'--task', 'Test max iterations',
|
||||
'--piece', piecePath,
|
||||
'--create-worktree', 'no',
|
||||
'--provider', 'mock',
|
||||
],
|
||||
cwd: repo.path,
|
||||
env: {
|
||||
...isolatedEnv.env,
|
||||
TAKT_MOCK_SCENARIO: scenarioPath,
|
||||
},
|
||||
timeout: 240_000,
|
||||
});
|
||||
|
||||
// Then: piece aborts due to iteration limit
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
const combined = result.stdout + result.stderr;
|
||||
expect(combined).toMatch(/Max iterations|iteration|aborted/i);
|
||||
}, 240_000);
|
||||
|
||||
it('should pass previous response between sequential steps', () => {
|
||||
// Given: a two-step piece and a scenario with distinct step outputs
|
||||
const piecePath = resolve(__dirname, '../fixtures/pieces/mock-two-step.yaml');
|
||||
const scenarioPath = resolve(__dirname, '../fixtures/scenarios/two-step-done.json');
|
||||
|
||||
// When: executing the piece
|
||||
const result = runTakt({
|
||||
args: [
|
||||
'--task', 'Test previous response passing',
|
||||
'--piece', piecePath,
|
||||
'--create-worktree', 'no',
|
||||
'--provider', 'mock',
|
||||
],
|
||||
cwd: repo.path,
|
||||
env: {
|
||||
...isolatedEnv.env,
|
||||
TAKT_MOCK_SCENARIO: scenarioPath,
|
||||
},
|
||||
timeout: 240_000,
|
||||
});
|
||||
|
||||
// Then: piece completes successfully (both steps execute)
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('Piece completed');
|
||||
}, 240_000);
|
||||
});
|
||||
133
e2e/specs/provider-error.e2e.ts
Normal file
133
e2e/specs/provider-error.e2e.ts
Normal file
@ -0,0 +1,133 @@
|
||||
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';
|
||||
|
||||
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 };
|
||||
|
||||
beforeEach(() => {
|
||||
isolatedEnv = createIsolatedEnv();
|
||||
repo = createLocalRepo();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
try { repo.cleanup(); } catch { /* best-effort */ }
|
||||
try { isolatedEnv.cleanup(); } catch { /* best-effort */ }
|
||||
});
|
||||
|
||||
it('should override config provider with --provider flag', () => {
|
||||
// Given: config.yaml has provider: claude, but CLI flag specifies mock
|
||||
writeFileSync(
|
||||
join(isolatedEnv.taktDir, 'config.yaml'),
|
||||
[
|
||||
'provider: claude',
|
||||
'language: en',
|
||||
'log_level: info',
|
||||
'default_piece: default',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
const piecePath = resolve(__dirname, '../fixtures/pieces/mock-single-step.yaml');
|
||||
const scenarioPath = resolve(__dirname, '../fixtures/scenarios/execute-done.json');
|
||||
|
||||
// When: running with --provider mock
|
||||
const result = runTakt({
|
||||
args: [
|
||||
'--task', 'Test provider override',
|
||||
'--piece', piecePath,
|
||||
'--create-worktree', 'no',
|
||||
'--provider', 'mock',
|
||||
],
|
||||
cwd: repo.path,
|
||||
env: {
|
||||
...isolatedEnv.env,
|
||||
TAKT_MOCK_SCENARIO: scenarioPath,
|
||||
},
|
||||
timeout: 240_000,
|
||||
});
|
||||
|
||||
// Then: executes successfully with mock provider
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('Piece completed');
|
||||
}, 240_000);
|
||||
|
||||
it('should use default mock response when scenario entries are exhausted', () => {
|
||||
// Given: a two-step piece with only 1 scenario entry
|
||||
const piecePath = resolve(__dirname, '../fixtures/pieces/mock-two-step.yaml');
|
||||
const scenarioPath = resolve(__dirname, '../fixtures/scenarios/one-entry-only.json');
|
||||
|
||||
// When: executing the piece (step-2 will have no scenario entry)
|
||||
const result = runTakt({
|
||||
args: [
|
||||
'--task', 'Test scenario exhaustion',
|
||||
'--piece', piecePath,
|
||||
'--create-worktree', 'no',
|
||||
'--provider', 'mock',
|
||||
],
|
||||
cwd: repo.path,
|
||||
env: {
|
||||
...isolatedEnv.env,
|
||||
TAKT_MOCK_SCENARIO: scenarioPath,
|
||||
},
|
||||
timeout: 240_000,
|
||||
});
|
||||
|
||||
// Then: does not crash; either completes or aborts gracefully
|
||||
const combined = result.stdout + result.stderr;
|
||||
expect(combined).not.toContain('UnhandledPromiseRejection');
|
||||
expect(combined).not.toContain('SIGTERM');
|
||||
}, 240_000);
|
||||
|
||||
it('should error when scenario file does not exist', () => {
|
||||
// Given: TAKT_MOCK_SCENARIO pointing to a non-existent file
|
||||
const piecePath = resolve(__dirname, '../fixtures/pieces/mock-single-step.yaml');
|
||||
|
||||
// When: executing with a bad scenario path
|
||||
const result = runTakt({
|
||||
args: [
|
||||
'--task', 'Test bad scenario',
|
||||
'--piece', piecePath,
|
||||
'--create-worktree', 'no',
|
||||
'--provider', 'mock',
|
||||
],
|
||||
cwd: repo.path,
|
||||
env: {
|
||||
...isolatedEnv.env,
|
||||
TAKT_MOCK_SCENARIO: '/nonexistent/path/scenario.json',
|
||||
},
|
||||
timeout: 240_000,
|
||||
});
|
||||
|
||||
// Then: exits with error and clear message
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
const combined = result.stdout + result.stderr;
|
||||
expect(combined).toMatch(/[Ss]cenario file not found|ENOENT/);
|
||||
}, 240_000);
|
||||
});
|
||||
72
e2e/specs/quiet-mode.e2e.ts
Normal file
72
e2e/specs/quiet-mode.e2e.ts
Normal file
@ -0,0 +1,72 @@
|
||||
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';
|
||||
|
||||
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 };
|
||||
|
||||
beforeEach(() => {
|
||||
isolatedEnv = createIsolatedEnv();
|
||||
repo = createLocalRepo();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
try { repo.cleanup(); } catch { /* best-effort */ }
|
||||
try { isolatedEnv.cleanup(); } catch { /* best-effort */ }
|
||||
});
|
||||
|
||||
it('should suppress AI stream output in quiet mode', () => {
|
||||
// Given: a simple piece and scenario
|
||||
const piecePath = resolve(__dirname, '../fixtures/pieces/mock-single-step.yaml');
|
||||
const scenarioPath = resolve(__dirname, '../fixtures/scenarios/execute-done.json');
|
||||
|
||||
// When: running with --quiet flag
|
||||
const result = runTakt({
|
||||
args: [
|
||||
'--task', 'Test quiet mode',
|
||||
'--piece', piecePath,
|
||||
'--create-worktree', 'no',
|
||||
'--provider', 'mock',
|
||||
'--quiet',
|
||||
],
|
||||
cwd: repo.path,
|
||||
env: {
|
||||
...isolatedEnv.env,
|
||||
TAKT_MOCK_SCENARIO: scenarioPath,
|
||||
},
|
||||
timeout: 240_000,
|
||||
});
|
||||
|
||||
// Then: completes successfully; mock content should not appear in output
|
||||
expect(result.exitCode).toBe(0);
|
||||
// In quiet mode, the raw mock response text should be suppressed
|
||||
expect(result.stdout).not.toContain('Mock response for persona');
|
||||
}, 240_000);
|
||||
});
|
||||
185
e2e/specs/run-multiple-tasks.e2e.ts
Normal file
185
e2e/specs/run-multiple-tasks.e2e.ts
Normal file
@ -0,0 +1,185 @@
|
||||
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 { 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';
|
||||
|
||||
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 };
|
||||
|
||||
const piecePath = resolve(__dirname, '../fixtures/pieces/mock-single-step.yaml');
|
||||
|
||||
beforeEach(() => {
|
||||
isolatedEnv = createIsolatedEnv();
|
||||
repo = createLocalRepo();
|
||||
|
||||
// Override config to use mock provider
|
||||
writeFileSync(
|
||||
join(isolatedEnv.taktDir, 'config.yaml'),
|
||||
[
|
||||
'provider: mock',
|
||||
'language: en',
|
||||
'log_level: info',
|
||||
'default_piece: default',
|
||||
].join('\n'),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
try { repo.cleanup(); } catch { /* best-effort */ }
|
||||
try { isolatedEnv.cleanup(); } catch { /* best-effort */ }
|
||||
});
|
||||
|
||||
it('should execute all pending tasks sequentially', () => {
|
||||
// Given: 3 pending tasks in tasks.yaml
|
||||
const scenarioPath = resolve(__dirname, '../fixtures/scenarios/run-three-tasks.json');
|
||||
const now = new Date().toISOString();
|
||||
|
||||
mkdirSync(join(repo.path, '.takt'), { recursive: true });
|
||||
writeFileSync(
|
||||
join(repo.path, '.takt', 'tasks.yaml'),
|
||||
[
|
||||
'tasks:',
|
||||
' - name: task-1',
|
||||
' status: pending',
|
||||
' content: "E2E task 1"',
|
||||
` piece: "${piecePath}"`,
|
||||
` created_at: "${now}"`,
|
||||
' started_at: null',
|
||||
' completed_at: null',
|
||||
' - name: task-2',
|
||||
' status: pending',
|
||||
' content: "E2E task 2"',
|
||||
` piece: "${piecePath}"`,
|
||||
` created_at: "${now}"`,
|
||||
' started_at: null',
|
||||
' completed_at: null',
|
||||
' - name: task-3',
|
||||
' status: pending',
|
||||
' content: "E2E task 3"',
|
||||
` piece: "${piecePath}"`,
|
||||
` created_at: "${now}"`,
|
||||
' started_at: null',
|
||||
' completed_at: null',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
// When: running takt run
|
||||
const result = runTakt({
|
||||
args: ['run', '--provider', 'mock'],
|
||||
cwd: repo.path,
|
||||
env: {
|
||||
...isolatedEnv.env,
|
||||
TAKT_MOCK_SCENARIO: scenarioPath,
|
||||
},
|
||||
timeout: 240_000,
|
||||
});
|
||||
|
||||
// Then: all 3 tasks complete
|
||||
expect(result.exitCode).toBe(0);
|
||||
const combined = result.stdout + result.stderr;
|
||||
expect(combined).toContain('task-1');
|
||||
expect(combined).toContain('task-2');
|
||||
expect(combined).toContain('task-3');
|
||||
}, 240_000);
|
||||
|
||||
it('should continue remaining tasks when one task fails', () => {
|
||||
// Given: 3 tasks where the 2nd will fail (error status)
|
||||
const scenarioPath = resolve(__dirname, '../fixtures/scenarios/run-with-failure.json');
|
||||
const now = new Date().toISOString();
|
||||
|
||||
mkdirSync(join(repo.path, '.takt'), { recursive: true });
|
||||
writeFileSync(
|
||||
join(repo.path, '.takt', 'tasks.yaml'),
|
||||
[
|
||||
'tasks:',
|
||||
' - name: task-ok-1',
|
||||
' status: pending',
|
||||
' content: "Should succeed"',
|
||||
` piece: "${piecePath}"`,
|
||||
` created_at: "${now}"`,
|
||||
' started_at: null',
|
||||
' completed_at: null',
|
||||
' - name: task-fail',
|
||||
' status: pending',
|
||||
' content: "Should fail"',
|
||||
` piece: "${piecePath}"`,
|
||||
` created_at: "${now}"`,
|
||||
' started_at: null',
|
||||
' completed_at: null',
|
||||
' - name: task-ok-2',
|
||||
' status: pending',
|
||||
' content: "Should succeed after failure"',
|
||||
` piece: "${piecePath}"`,
|
||||
` created_at: "${now}"`,
|
||||
' started_at: null',
|
||||
' completed_at: null',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
// When: running takt run
|
||||
const result = runTakt({
|
||||
args: ['run', '--provider', 'mock'],
|
||||
cwd: repo.path,
|
||||
env: {
|
||||
...isolatedEnv.env,
|
||||
TAKT_MOCK_SCENARIO: scenarioPath,
|
||||
},
|
||||
timeout: 240_000,
|
||||
});
|
||||
|
||||
// Then: exit code is non-zero (failure occurred), but task-ok-2 was still attempted
|
||||
const combined = result.stdout + result.stderr;
|
||||
expect(combined).toContain('task-ok-1');
|
||||
expect(combined).toContain('task-fail');
|
||||
expect(combined).toContain('task-ok-2');
|
||||
}, 240_000);
|
||||
|
||||
it('should exit cleanly when no pending tasks exist', () => {
|
||||
// Given: an empty tasks.yaml
|
||||
mkdirSync(join(repo.path, '.takt'), { recursive: true });
|
||||
writeFileSync(
|
||||
join(repo.path, '.takt', 'tasks.yaml'),
|
||||
'tasks: []\n',
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
// When: running takt run
|
||||
const result = runTakt({
|
||||
args: ['run', '--provider', 'mock'],
|
||||
cwd: repo.path,
|
||||
env: isolatedEnv.env,
|
||||
timeout: 240_000,
|
||||
});
|
||||
|
||||
// Then: exits cleanly with code 0
|
||||
expect(result.exitCode).toBe(0);
|
||||
}, 240_000);
|
||||
});
|
||||
136
e2e/specs/task-content-file.e2e.ts
Normal file
136
e2e/specs/task-content-file.e2e.ts
Normal file
@ -0,0 +1,136 @@
|
||||
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 { 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';
|
||||
|
||||
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 };
|
||||
|
||||
const piecePath = resolve(__dirname, '../fixtures/pieces/mock-single-step.yaml');
|
||||
|
||||
beforeEach(() => {
|
||||
isolatedEnv = createIsolatedEnv();
|
||||
repo = createLocalRepo();
|
||||
|
||||
writeFileSync(
|
||||
join(isolatedEnv.taktDir, 'config.yaml'),
|
||||
[
|
||||
'provider: mock',
|
||||
'language: en',
|
||||
'log_level: info',
|
||||
'default_piece: default',
|
||||
].join('\n'),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
try { repo.cleanup(); } catch { /* best-effort */ }
|
||||
try { isolatedEnv.cleanup(); } catch { /* best-effort */ }
|
||||
});
|
||||
|
||||
it('should execute task using content_file reference', () => {
|
||||
// Given: a task with content_file pointing to an existing file
|
||||
const scenarioPath = resolve(__dirname, '../fixtures/scenarios/execute-done.json');
|
||||
const now = new Date().toISOString();
|
||||
|
||||
mkdirSync(join(repo.path, '.takt'), { recursive: true });
|
||||
|
||||
// Create the content file
|
||||
writeFileSync(
|
||||
join(repo.path, 'task-content.txt'),
|
||||
'Create a noop file for E2E testing.',
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
writeFileSync(
|
||||
join(repo.path, '.takt', 'tasks.yaml'),
|
||||
[
|
||||
'tasks:',
|
||||
' - name: content-file-task',
|
||||
' status: pending',
|
||||
' content_file: "./task-content.txt"',
|
||||
` piece: "${piecePath}"`,
|
||||
` created_at: "${now}"`,
|
||||
' started_at: null',
|
||||
' completed_at: null',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
// When: running takt run
|
||||
const result = runTakt({
|
||||
args: ['run', '--provider', 'mock'],
|
||||
cwd: repo.path,
|
||||
env: {
|
||||
...isolatedEnv.env,
|
||||
TAKT_MOCK_SCENARIO: scenarioPath,
|
||||
},
|
||||
timeout: 240_000,
|
||||
});
|
||||
|
||||
// Then: task executes successfully
|
||||
expect(result.exitCode).toBe(0);
|
||||
const combined = result.stdout + result.stderr;
|
||||
expect(combined).toContain('content-file-task');
|
||||
}, 240_000);
|
||||
|
||||
it('should fail when content_file references a nonexistent file', () => {
|
||||
// Given: a task with content_file pointing to a nonexistent file
|
||||
const now = new Date().toISOString();
|
||||
|
||||
mkdirSync(join(repo.path, '.takt'), { recursive: true });
|
||||
|
||||
writeFileSync(
|
||||
join(repo.path, '.takt', 'tasks.yaml'),
|
||||
[
|
||||
'tasks:',
|
||||
' - name: bad-content-file-task',
|
||||
' status: pending',
|
||||
' content_file: "./nonexistent-content.txt"',
|
||||
` piece: "${piecePath}"`,
|
||||
` created_at: "${now}"`,
|
||||
' started_at: null',
|
||||
' completed_at: null',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
// When: running takt run
|
||||
const result = runTakt({
|
||||
args: ['run', '--provider', 'mock'],
|
||||
cwd: repo.path,
|
||||
env: isolatedEnv.env,
|
||||
timeout: 240_000,
|
||||
});
|
||||
|
||||
// Then: task fails with a meaningful error
|
||||
const combined = result.stdout + result.stderr;
|
||||
expect(combined).toMatch(/not found|ENOENT|missing|error/i);
|
||||
}, 240_000);
|
||||
});
|
||||
@ -11,6 +11,17 @@ export default defineConfig({
|
||||
'e2e/specs/list-non-interactive.e2e.ts',
|
||||
'e2e/specs/multi-step-parallel.e2e.ts',
|
||||
'e2e/specs/run-sigint-graceful.e2e.ts',
|
||||
'e2e/specs/piece-error-handling.e2e.ts',
|
||||
'e2e/specs/run-multiple-tasks.e2e.ts',
|
||||
'e2e/specs/provider-error.e2e.ts',
|
||||
'e2e/specs/error-handling.e2e.ts',
|
||||
'e2e/specs/cli-catalog.e2e.ts',
|
||||
'e2e/specs/cli-prompt.e2e.ts',
|
||||
'e2e/specs/cli-switch.e2e.ts',
|
||||
'e2e/specs/cli-help.e2e.ts',
|
||||
'e2e/specs/cli-clear.e2e.ts',
|
||||
'e2e/specs/quiet-mode.e2e.ts',
|
||||
'e2e/specs/task-content-file.e2e.ts',
|
||||
],
|
||||
environment: 'node',
|
||||
globals: false,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user