This commit is contained in:
nrslib 2026-02-13 07:24:12 +09:00
parent 4919bc759f
commit 0fe835ecd9
12 changed files with 109 additions and 23 deletions

View File

@ -17,7 +17,8 @@ E2Eテストを追加・変更した場合は、このドキュメントも更
## E2E用config.yaml
- E2Eのグローバル設定は `e2e/fixtures/config.e2e.yaml` を基準に生成する。
- `createIsolatedEnv()` は毎回一時ディレクトリ配下(`$TAKT_CONFIG_DIR/config.yaml`)にこの基準設定を書き出す。
- 通知音は `notification_sound_events` でタイミング別に制御し、E2E既定では道中`iteration_limit` / `piece_complete` / `piece_abort`をOFF、全体終了時`run_complete` / `run_abort`のみONにする。
- E2E実行中の `takt` 内通知音は `notification_sound: false` で無効化する。
- `npm run test:e2e` は成否にかかわらず最後に1回ベルを鳴らし、終了コードはテスト結果を維持する。
- 各スペックで `provider``concurrency` を変更する場合は、`updateIsolatedConfig()` を使って差分のみ上書きする。
- `~/.takt/config.yaml` はE2Eでは参照されないため、通常実行の設定には影響しない。

View File

@ -2,10 +2,10 @@ provider: claude
language: en
log_level: info
default_piece: default
notification_sound: true
notification_sound: false
notification_sound_events:
iteration_limit: false
piece_complete: false
piece_abort: false
run_complete: true
run_abort: true
run_abort: false

View File

@ -1,10 +1,13 @@
[
{"persona": "agents/test-reviewer-a", "status": "done", "content": "[REVIEW:2]\n\nNeeds fix."},
{"persona": "conductor", "status": "done", "content": "[REVIEW:2]"},
{"persona": "conductor", "status": "done", "content": "[REVIEW:2]"},
{"persona": "agents/test-coder", "status": "done", "content": "[FIX:1]\n\nFixed."},
{"persona": "agents/test-reviewer-a", "status": "done", "content": "[REVIEW:2]\n\nStill needs fix."},
{"persona": "conductor", "status": "done", "content": "[REVIEW:2]"},
{"persona": "conductor", "status": "done", "content": "[REVIEW:2]"},
{"persona": "agents/test-coder", "status": "done", "content": "[FIX:1]\n\nFixed again."},
{"persona": "agents/test-reviewer-b", "status": "done", "content": "[_LOOP_JUDGE_REVIEW_FIX:2]\n\nAbort this loop."},
{"persona": "conductor", "status": "done", "content": "[_LOOP_JUDGE_REVIEW_FIX:2]"},
{"persona": "conductor", "status": "done", "content": "[_LOOP_JUDGE_REVIEW_FIX:2]"}
]

View File

@ -1,7 +1,9 @@
[
{"persona": "agents/test-reviewer-a", "status": "done", "content": "[REVIEW:2]\n\nNeeds fix."},
{"persona": "conductor", "status": "done", "content": "[REVIEW:2]"},
{"persona": "conductor", "status": "done", "content": "[REVIEW:2]"},
{"persona": "agents/test-coder", "status": "done", "content": "[FIX:1]\n\nFixed."},
{"persona": "agents/test-reviewer-a", "status": "done", "content": "[REVIEW:1]\n\nApproved."},
{"persona": "conductor", "status": "done", "content": "[REVIEW:1]"},
{"persona": "conductor", "status": "done", "content": "[REVIEW:1]"}
]

View File

@ -1,7 +1,9 @@
[
{ "persona": "test-coder", "status": "done", "content": "Plan created." },
{ "persona": "test-reviewer-a", "status": "done", "content": "Architecture approved." },
{ "persona": "test-reviewer-b", "status": "done", "content": "Security approved." },
{ "persona": "agents/test-coder", "status": "done", "content": "Plan created." },
{ "persona": "agents/test-reviewer-a", "status": "done", "content": "Architecture approved." },
{ "persona": "agents/test-reviewer-b", "status": "done", "content": "Security approved." },
{ "persona": "conductor", "status": "done", "content": "[ARCH-REVIEW:1] [SECURITY-REVIEW:1]" },
{ "persona": "conductor", "status": "done", "content": "[ARCH-REVIEW:1] [SECURITY-REVIEW:1]" },
{ "persona": "conductor", "status": "done", "content": "[ARCH-REVIEW:1] [SECURITY-REVIEW:1]" },
{ "persona": "conductor", "status": "done", "content": "[ARCH-REVIEW:1] [SECURITY-REVIEW:1]" }
]

View File

@ -1,15 +1,19 @@
[
{ "persona": "test-coder", "status": "done", "content": "Plan created." },
{ "persona": "agents/test-coder", "status": "done", "content": "Plan created." },
{ "persona": "test-reviewer-a", "status": "done", "content": "Architecture looks good." },
{ "persona": "test-reviewer-b", "status": "done", "content": "Security issues found." },
{ "persona": "agents/test-reviewer-a", "status": "done", "content": "Architecture looks good." },
{ "persona": "agents/test-reviewer-b", "status": "done", "content": "Security issues found." },
{ "persona": "conductor", "status": "done", "content": "[ARCH-REVIEW:1] [SECURITY-REVIEW:2]" },
{ "persona": "conductor", "status": "done", "content": "[ARCH-REVIEW:1] [SECURITY-REVIEW:2]" },
{ "persona": "conductor", "status": "done", "content": "[ARCH-REVIEW:1] [SECURITY-REVIEW:2]" },
{ "persona": "conductor", "status": "done", "content": "[ARCH-REVIEW:1] [SECURITY-REVIEW:2]" },
{ "persona": "test-coder", "status": "done", "content": "Fix applied." },
{ "persona": "agents/test-coder", "status": "done", "content": "Fix applied." },
{ "persona": "test-reviewer-a", "status": "done", "content": "Architecture still approved." },
{ "persona": "test-reviewer-b", "status": "done", "content": "Security now approved." },
{ "persona": "agents/test-reviewer-a", "status": "done", "content": "Architecture still approved." },
{ "persona": "agents/test-reviewer-b", "status": "done", "content": "Security now approved." },
{ "persona": "conductor", "status": "done", "content": "[ARCH-REVIEW:1] [SECURITY-REVIEW:1]" },
{ "persona": "conductor", "status": "done", "content": "[ARCH-REVIEW:1] [SECURITY-REVIEW:1]" },
{ "persona": "conductor", "status": "done", "content": "[ARCH-REVIEW:1] [SECURITY-REVIEW:1]" },
{ "persona": "conductor", "status": "done", "content": "[ARCH-REVIEW:1] [SECURITY-REVIEW:1]" }
]

View File

@ -9,6 +9,11 @@
"status": "done",
"content": "Report summary: OK"
},
{
"persona": "conductor",
"status": "done",
"content": "[EXECUTE:1]"
},
{
"persona": "conductor",
"status": "done",

View File

@ -54,6 +54,66 @@ function getGitHubUser(): string {
return user;
}
function canUseGitHubRepo(): boolean {
try {
const user = getGitHubUser();
const repoName = `${user}/takt-testing`;
execFileSync('gh', ['repo', 'view', repoName], {
encoding: 'utf-8',
stdio: 'pipe',
});
return true;
} catch {
return false;
}
}
export function isGitHubE2EAvailable(): boolean {
return canUseGitHubRepo();
}
function createOfflineTestRepo(options?: CreateTestRepoOptions): TestRepo {
const sandboxPath = mkdtempSync(join(tmpdir(), 'takt-e2e-repo-'));
const originPath = join(sandboxPath, 'origin.git');
const repoPath = join(sandboxPath, 'work');
execFileSync('git', ['init', '--bare', originPath], { stdio: 'pipe' });
execFileSync('git', ['clone', originPath, 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' });
execFileSync('git', ['push', '-u', 'origin', 'HEAD'], { cwd: repoPath, stdio: 'pipe' });
const testBranch = options?.skipBranch ? undefined : `e2e-test-${Date.now()}`;
if (testBranch) {
execFileSync('git', ['checkout', '-b', testBranch], {
cwd: repoPath,
stdio: 'pipe',
});
}
const currentBranch = testBranch
?? execFileSync('git', ['branch', '--show-current'], {
cwd: repoPath,
encoding: 'utf-8',
}).trim();
return {
path: repoPath,
repoName: 'local/takt-testing',
branch: currentBranch,
cleanup: () => {
try {
rmSync(sandboxPath, { recursive: true, force: true });
} catch {
// Best-effort cleanup
}
},
};
}
/**
* Clone the takt-testing repository and create a test branch.
*
@ -63,6 +123,10 @@ function getGitHubUser(): string {
* 3. Delete local directory
*/
export function createTestRepo(options?: CreateTestRepoOptions): TestRepo {
if (!canUseGitHubRepo()) {
return createOfflineTestRepo(options);
}
const user = getGitHubUser();
const repoName = `${user}/takt-testing`;

View File

@ -9,11 +9,12 @@ import {
updateIsolatedConfig,
type IsolatedEnv,
} from '../helpers/isolated-env';
import { createTestRepo, type TestRepo } from '../helpers/test-repo';
import { createTestRepo, isGitHubE2EAvailable, type TestRepo } from '../helpers/test-repo';
import { runTakt } from '../helpers/takt-runner';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const requiresGitHub = isGitHubE2EAvailable();
// E2E更新時は docs/testing/e2e.md も更新すること
describe('E2E: Add task from GitHub issue (takt add)', () => {
@ -67,7 +68,7 @@ describe('E2E: Add task from GitHub issue (takt add)', () => {
}
});
it('should create a task file from issue reference', () => {
it.skipIf(!requiresGitHub)('should create a task file from issue reference', () => {
const scenarioPath = resolve(__dirname, '../fixtures/scenarios/add-task.json');
const result = runTakt({

View File

@ -14,8 +14,8 @@
"watch": "tsc --watch",
"test": "vitest run",
"test:watch": "vitest",
"test:e2e": "npm run test:e2e:all",
"test:e2e:mock": "vitest run --config vitest.config.e2e.mock.ts --reporter=verbose",
"test:e2e": "npm run test:e2e:mock; code=$?; if [ \"$code\" -eq 0 ]; then msg='test:e2e passed'; else msg=\"test:e2e failed (exit=$code)\"; fi; if command -v osascript >/dev/null 2>&1; then osascript -e \"display notification \\\"$msg\\\" with title \\\"takt\\\" subtitle \\\"E2E\\\"\" >/dev/null 2>&1 || true; fi; echo \"[takt] $msg\"; exit $code",
"test:e2e:mock": "TAKT_E2E_PROVIDER=mock vitest run --config vitest.config.e2e.mock.ts --reporter=verbose",
"test:e2e:all": "npm run test:e2e:mock && npm run test:e2e:provider",
"test:e2e:provider": "npm run test:e2e:provider:claude && npm run test:e2e:provider:codex",
"test:e2e:provider:claude": "TAKT_E2E_PROVIDER=claude vitest run --config vitest.config.e2e.provider.ts --reporter=verbose",

View File

@ -76,7 +76,7 @@ describe('createIsolatedEnv', () => {
expect(isolated.env.GIT_CONFIG_GLOBAL).toContain('takt-e2e-');
});
it('should create config.yaml from E2E fixture with notification_sound timing controls', () => {
it('should create config.yaml from E2E fixture with notification_sound disabled', () => {
const isolated = createIsolatedEnv();
cleanups.push(isolated.cleanup);
@ -86,13 +86,13 @@ describe('createIsolatedEnv', () => {
expect(config.language).toBe('en');
expect(config.log_level).toBe('info');
expect(config.default_piece).toBe('default');
expect(config.notification_sound).toBe(true);
expect(config.notification_sound).toBe(false);
expect(config.notification_sound_events).toEqual({
iteration_limit: false,
piece_complete: false,
piece_abort: false,
run_complete: true,
run_abort: true,
run_abort: false,
});
});
@ -120,13 +120,13 @@ describe('createIsolatedEnv', () => {
expect(config.provider).toBe('mock');
expect(config.concurrency).toBe(2);
expect(config.notification_sound).toBe(true);
expect(config.notification_sound).toBe(false);
expect(config.notification_sound_events).toEqual({
iteration_limit: false,
piece_complete: false,
piece_abort: false,
run_complete: true,
run_abort: true,
run_abort: false,
});
expect(config.language).toBe('en');
});
@ -149,7 +149,7 @@ describe('createIsolatedEnv', () => {
piece_complete: false,
piece_abort: false,
run_complete: false,
run_abort: true,
run_abort: false,
});
});

View File

@ -123,7 +123,11 @@ export async function runWithWorkerPool(
selfSigintInjected = true;
process.emit('SIGINT');
if (selfSigintTwice) {
process.emit('SIGINT');
// E2E deterministic path: force-exit shortly after graceful SIGINT.
// Avoids intermittent hangs caused by listener ordering/races.
setTimeout(() => {
process.exit(EXIT_SIGINT);
}, 25);
}
}
}