takt/src/__tests__/engine-parallel.test.ts
2026-02-07 08:52:37 +09:00

164 lines
6.0 KiB
TypeScript

/**
* PieceEngine integration tests: parallel movement aggregation.
*
* Covers:
* - Aggregated output format (## headers and --- separators)
* - Individual sub-movement output storage
* - Concurrent execution of sub-movements
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { existsSync, rmSync } from 'node:fs';
// --- Mock setup (must be before imports that use these modules) ---
vi.mock('../agents/runner.js', () => ({
runAgent: vi.fn(),
}));
vi.mock('../core/piece/evaluation/index.js', () => ({
detectMatchedRule: vi.fn(),
}));
vi.mock('../core/piece/phase-runner.js', () => ({
needsStatusJudgmentPhase: vi.fn().mockReturnValue(false),
runReportPhase: vi.fn().mockResolvedValue(undefined),
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
}));
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
}));
// --- Imports (after mocks) ---
import { PieceEngine } from '../core/piece/index.js';
import { runAgent } from '../agents/runner.js';
import {
makeResponse,
buildDefaultPieceConfig,
mockRunAgentSequence,
mockDetectMatchedRuleSequence,
createTestTmpDir,
applyDefaultMocks,
} from './engine-test-helpers.js';
describe('PieceEngine Integration: Parallel Movement Aggregation', () => {
let tmpDir: string;
beforeEach(() => {
vi.resetAllMocks();
applyDefaultMocks();
tmpDir = createTestTmpDir();
});
afterEach(() => {
if (existsSync(tmpDir)) {
rmSync(tmpDir, { recursive: true, force: true });
}
});
it('should aggregate sub-movement outputs with ## headers and --- separators', async () => {
const config = buildDefaultPieceConfig();
const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
mockRunAgentSequence([
makeResponse({ persona: 'plan', content: 'Plan done' }),
makeResponse({ persona: 'implement', content: 'Impl done' }),
makeResponse({ persona: 'ai_review', content: 'OK' }),
makeResponse({ persona: 'arch-review', content: 'Architecture review content' }),
makeResponse({ persona: 'security-review', content: 'Security review content' }),
makeResponse({ persona: 'supervise', content: 'All passed' }),
]);
mockDetectMatchedRuleSequence([
{ index: 0, method: 'phase1_tag' },
{ index: 0, method: 'phase1_tag' },
{ index: 0, method: 'phase1_tag' },
{ index: 0, method: 'phase1_tag' }, // arch-review
{ index: 0, method: 'phase1_tag' }, // security-review
{ index: 0, method: 'aggregate' }, // reviewers
{ index: 0, method: 'phase1_tag' },
]);
const state = await engine.run();
expect(state.status).toBe('completed');
const reviewersOutput = state.movementOutputs.get('reviewers');
expect(reviewersOutput).toBeDefined();
expect(reviewersOutput!.content).toContain('## arch-review');
expect(reviewersOutput!.content).toContain('Architecture review content');
expect(reviewersOutput!.content).toContain('---');
expect(reviewersOutput!.content).toContain('## security-review');
expect(reviewersOutput!.content).toContain('Security review content');
expect(reviewersOutput!.matchedRuleMethod).toBe('aggregate');
});
it('should store individual sub-movement outputs in movementOutputs', async () => {
const config = buildDefaultPieceConfig();
const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
mockRunAgentSequence([
makeResponse({ persona: 'plan', content: 'Plan' }),
makeResponse({ persona: 'implement', content: 'Impl' }),
makeResponse({ persona: 'ai_review', content: 'OK' }),
makeResponse({ persona: 'arch-review', content: 'Arch content' }),
makeResponse({ persona: 'security-review', content: 'Sec content' }),
makeResponse({ persona: 'supervise', content: 'Pass' }),
]);
mockDetectMatchedRuleSequence([
{ index: 0, method: 'phase1_tag' },
{ index: 0, method: 'phase1_tag' },
{ index: 0, method: 'phase1_tag' },
{ index: 0, method: 'phase1_tag' },
{ index: 0, method: 'phase1_tag' },
{ index: 0, method: 'aggregate' },
{ index: 0, method: 'phase1_tag' },
]);
const state = await engine.run();
expect(state.movementOutputs.has('arch-review')).toBe(true);
expect(state.movementOutputs.has('security-review')).toBe(true);
expect(state.movementOutputs.has('reviewers')).toBe(true);
expect(state.movementOutputs.get('arch-review')!.content).toBe('Arch content');
expect(state.movementOutputs.get('security-review')!.content).toBe('Sec content');
});
it('should execute sub-movements concurrently (both runAgent calls happen)', async () => {
const config = buildDefaultPieceConfig();
const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
mockRunAgentSequence([
makeResponse({ persona: 'plan', content: 'Plan' }),
makeResponse({ persona: 'implement', content: 'Impl' }),
makeResponse({ persona: 'ai_review', content: 'OK' }),
makeResponse({ persona: 'arch-review', content: 'OK' }),
makeResponse({ persona: 'security-review', content: 'OK' }),
makeResponse({ persona: 'supervise', content: 'Pass' }),
]);
mockDetectMatchedRuleSequence([
{ index: 0, method: 'phase1_tag' },
{ index: 0, method: 'phase1_tag' },
{ index: 0, method: 'phase1_tag' },
{ index: 0, method: 'phase1_tag' },
{ index: 0, method: 'phase1_tag' },
{ index: 0, method: 'aggregate' },
{ index: 0, method: 'phase1_tag' },
]);
await engine.run();
// 6 total: 4 normal + 2 parallel sub-movements
expect(vi.mocked(runAgent)).toHaveBeenCalledTimes(6);
const calledAgents = vi.mocked(runAgent).mock.calls.map(call => call[0]);
expect(calledAgents).toContain('../personas/arch-review.md');
expect(calledAgents).toContain('../personas/security-review.md');
});
});