takt/src/__tests__/phase-runner-report-history.test.ts
2026-03-06 09:16:14 +09:00

190 lines
5.6 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { runReportPhase, type PhaseRunnerContext } from '../core/piece/phase-runner.js';
import type { PieceMovement } from '../core/models/types.js';
import type { RunAgentOptions } from '../agents/runner.js';
vi.mock('../agents/runner.js', () => ({
runAgent: vi.fn(),
}));
import { runAgent } from '../agents/runner.js';
import type { AgentResponse } from '../core/models/types.js';
function createStep(fileName: string): PieceMovement {
return {
name: 'reviewers',
personaDisplayName: 'Reviewers',
instruction: 'review',
passPreviousResponse: false,
outputContracts: [{ name: fileName }],
};
}
function createContext(
reportDir: string,
onBuildResumeOptions?: (overrides: Pick<RunAgentOptions, 'maxTurns'>) => void,
): PhaseRunnerContext {
let currentSessionId = 'session-1';
return {
cwd: reportDir,
reportDir,
getSessionId: (_persona: string) => currentSessionId,
buildResumeOptions: (
_step,
_sessionId,
overrides,
) => {
onBuildResumeOptions?.(overrides);
return { cwd: reportDir };
},
buildNewSessionReportOptions: (
_step,
_overrides,
) => ({ cwd: reportDir }),
updatePersonaSession: (_persona, sessionId) => {
if (sessionId) {
currentSessionId = sessionId;
}
},
};
}
function queueRunAgentResponses(responses: AgentResponse[]): void {
const runAgentMock = vi.mocked(runAgent);
for (const response of responses) {
runAgentMock.mockImplementationOnce(async (persona, task, options) => {
options?.onPromptResolved?.({
systemPrompt: typeof persona === 'string' ? persona : '',
userInstruction: task,
});
return response;
});
}
}
describe('runReportPhase report history behavior', () => {
let tmpRoot: string;
beforeEach(() => {
tmpRoot = mkdtempSync(join(tmpdir(), 'takt-report-history-'));
vi.resetAllMocks();
});
afterEach(() => {
vi.useRealTimers();
if (existsSync(tmpRoot)) {
rmSync(tmpRoot, { recursive: true, force: true });
}
});
it('should overwrite report file and save versioned copy in the same report directory', async () => {
// Given
const reportDir = join(tmpRoot, '.takt', 'runs', 'sample-run', 'reports');
const step = createStep('05-architect-review.md');
const ctx = createContext(reportDir);
queueRunAgentResponses([
{
persona: 'reviewers',
status: 'done',
content: 'First review result',
timestamp: new Date('2026-02-10T06:11:43Z'),
sessionId: 'session-2',
},
{
persona: 'reviewers',
status: 'done',
content: 'Second review result',
timestamp: new Date('2026-02-10T06:14:37Z'),
sessionId: 'session-3',
},
]);
// When
await runReportPhase(step, 1, ctx);
await runReportPhase(step, 2, ctx);
// Then
const latestPath = join(reportDir, '05-architect-review.md');
const latestContent = readFileSync(latestPath, 'utf-8');
expect(latestContent).toBe('Second review result');
const versionedFiles = readdirSync(reportDir).filter(f => f !== '05-architect-review.md');
expect(versionedFiles).toHaveLength(1);
expect(versionedFiles[0]).toMatch(/^05-architect-review\.md\.\d{8}T\d{6}Z$/);
const archivedContent = readFileSync(join(reportDir, versionedFiles[0]!), 'utf-8');
expect(archivedContent).toBe('First review result');
});
it('should add sequence suffix when history file name collides in the same second', async () => {
// Given
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-02-10T06:11:43Z'));
const reportDir = join(tmpRoot, '.takt', 'runs', 'sample-run', 'reports');
const step = createStep('06-qa-review.md');
const ctx = createContext(reportDir);
queueRunAgentResponses([
{
persona: 'reviewers',
status: 'done',
content: 'v1',
timestamp: new Date('2026-02-10T06:11:43Z'),
sessionId: 'session-2',
},
{
persona: 'reviewers',
status: 'done',
content: 'v2',
timestamp: new Date('2026-02-10T06:11:43Z'),
sessionId: 'session-3',
},
{
persona: 'reviewers',
status: 'done',
content: 'v3',
timestamp: new Date('2026-02-10T06:11:43Z'),
sessionId: 'session-4',
},
]);
// When
await runReportPhase(step, 1, ctx);
await runReportPhase(step, 2, ctx);
await runReportPhase(step, 3, ctx);
// Then
const versionedFiles = readdirSync(reportDir).filter(f => f !== '06-qa-review.md').sort();
expect(versionedFiles).toEqual([
'06-qa-review.md.20260210T061143Z',
'06-qa-review.md.20260210T061143Z.1',
]);
});
it('should build report resume options with maxTurns override only', async () => {
// Given
const reportDir = join(tmpRoot, '.takt', 'runs', 'sample-run', 'reports');
const step = createStep('07-permissions-check.md');
const capturedOverrides: Array<Pick<RunAgentOptions, 'maxTurns'>> = [];
const ctx = createContext(reportDir, (overrides) => {
capturedOverrides.push(overrides);
});
queueRunAgentResponses([{
persona: 'reviewers',
status: 'done',
content: 'Permission-based report execution',
timestamp: new Date('2026-02-10T06:21:17Z'),
sessionId: 'session-2',
}]);
// When
await runReportPhase(step, 1, ctx);
// Then
expect(capturedOverrides).toEqual([{ maxTurns: 3 }]);
});
});