* dist-tag 検証をリトライ付きに変更(npm レジストリの結果整合性対策) * takt run 実行時に蓋閉じスリープを抑制 * takt: github-issue-245-report
212 lines
6.6 KiB
TypeScript
212 lines
6.6 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { existsSync, mkdtempSync, readFileSync, 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';
|
|
|
|
vi.mock('../agents/runner.js', () => ({
|
|
runAgent: vi.fn(),
|
|
}));
|
|
|
|
import { runAgent } from '../agents/runner.js';
|
|
|
|
function createStep(fileName: string): PieceMovement {
|
|
return {
|
|
name: 'implement',
|
|
persona: 'coder',
|
|
personaDisplayName: 'Coder',
|
|
instructionTemplate: 'Implement task',
|
|
passPreviousResponse: false,
|
|
outputContracts: [{ name: fileName }],
|
|
};
|
|
}
|
|
|
|
function createContext(reportDir: string, lastResponse = 'Phase 1 result'): PhaseRunnerContext {
|
|
let currentSessionId = 'session-resume-1';
|
|
|
|
return {
|
|
cwd: reportDir,
|
|
reportDir,
|
|
language: 'en',
|
|
lastResponse,
|
|
getSessionId: (_persona: string) => currentSessionId,
|
|
buildResumeOptions: (_step, sessionId, overrides) => ({
|
|
cwd: reportDir,
|
|
sessionId,
|
|
allowedTools: overrides.allowedTools,
|
|
maxTurns: overrides.maxTurns,
|
|
}),
|
|
buildNewSessionReportOptions: (_step, overrides) => ({
|
|
cwd: reportDir,
|
|
allowedTools: overrides.allowedTools,
|
|
maxTurns: overrides.maxTurns,
|
|
}),
|
|
updatePersonaSession: (_persona, sessionId) => {
|
|
if (sessionId) {
|
|
currentSessionId = sessionId;
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
describe('runReportPhase retry with new session', () => {
|
|
let tmpRoot: string;
|
|
|
|
beforeEach(() => {
|
|
tmpRoot = mkdtempSync(join(tmpdir(), 'takt-report-retry-'));
|
|
vi.resetAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (existsSync(tmpRoot)) {
|
|
rmSync(tmpRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('should retry with new session when first attempt returns empty content', async () => {
|
|
// Given
|
|
const reportDir = join(tmpRoot, '.takt', 'runs', 'sample-run', 'reports');
|
|
const step = createStep('02-coder.md');
|
|
const ctx = createContext(reportDir, 'Implemented feature X');
|
|
const runAgentMock = vi.mocked(runAgent);
|
|
runAgentMock
|
|
.mockResolvedValueOnce({
|
|
persona: 'coder',
|
|
status: 'done',
|
|
content: ' ',
|
|
timestamp: new Date('2026-02-11T00:00:00Z'),
|
|
sessionId: 'session-resume-2',
|
|
})
|
|
.mockResolvedValueOnce({
|
|
persona: 'coder',
|
|
status: 'done',
|
|
content: '# Report\nRecovered output',
|
|
timestamp: new Date('2026-02-11T00:00:01Z'),
|
|
sessionId: 'session-fresh-1',
|
|
});
|
|
|
|
// When
|
|
await runReportPhase(step, 1, ctx);
|
|
|
|
// Then
|
|
const reportPath = join(reportDir, '02-coder.md');
|
|
expect(readFileSync(reportPath, 'utf-8')).toBe('# Report\nRecovered output');
|
|
expect(runAgentMock).toHaveBeenCalledTimes(2);
|
|
|
|
const secondCallOptions = runAgentMock.mock.calls[1]?.[2] as { sessionId?: string };
|
|
expect(secondCallOptions.sessionId).toBeUndefined();
|
|
|
|
const secondInstruction = runAgentMock.mock.calls[1]?.[1] as string;
|
|
expect(secondInstruction).toContain('## Previous Work Context');
|
|
expect(secondInstruction).toContain('Implemented feature X');
|
|
});
|
|
|
|
it('should retry with new session when first attempt status is error', async () => {
|
|
// Given
|
|
const reportDir = join(tmpRoot, '.takt', 'runs', 'sample-run', 'reports');
|
|
const step = createStep('03-review.md');
|
|
const ctx = createContext(reportDir);
|
|
const runAgentMock = vi.mocked(runAgent);
|
|
runAgentMock
|
|
.mockResolvedValueOnce({
|
|
persona: 'coder',
|
|
status: 'error',
|
|
content: 'Tool use is not allowed in this phase',
|
|
timestamp: new Date('2026-02-11T00:01:00Z'),
|
|
error: 'Tool use is not allowed in this phase',
|
|
})
|
|
.mockResolvedValueOnce({
|
|
persona: 'coder',
|
|
status: 'done',
|
|
content: 'Recovered report',
|
|
timestamp: new Date('2026-02-11T00:01:01Z'),
|
|
});
|
|
|
|
// When
|
|
await runReportPhase(step, 1, ctx);
|
|
|
|
// Then
|
|
const reportPath = join(reportDir, '03-review.md');
|
|
expect(readFileSync(reportPath, 'utf-8')).toBe('Recovered report');
|
|
expect(runAgentMock).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('should throw when both attempts return empty output', async () => {
|
|
// Given
|
|
const reportDir = join(tmpRoot, '.takt', 'runs', 'sample-run', 'reports');
|
|
const step = createStep('04-qa.md');
|
|
const ctx = createContext(reportDir);
|
|
const runAgentMock = vi.mocked(runAgent);
|
|
runAgentMock
|
|
.mockResolvedValueOnce({
|
|
persona: 'coder',
|
|
status: 'done',
|
|
content: ' ',
|
|
timestamp: new Date('2026-02-11T00:02:00Z'),
|
|
})
|
|
.mockResolvedValueOnce({
|
|
persona: 'coder',
|
|
status: 'done',
|
|
content: '\n\n',
|
|
timestamp: new Date('2026-02-11T00:02:01Z'),
|
|
});
|
|
|
|
// When / Then
|
|
await expect(runReportPhase(step, 1, ctx)).rejects.toThrow('Report phase failed for 04-qa.md: Report output is empty');
|
|
expect(runAgentMock).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('should not retry when first attempt succeeds', async () => {
|
|
// Given
|
|
const reportDir = join(tmpRoot, '.takt', 'runs', 'sample-run', 'reports');
|
|
const step = createStep('05-ok.md');
|
|
const ctx = createContext(reportDir);
|
|
const runAgentMock = vi.mocked(runAgent);
|
|
runAgentMock.mockResolvedValueOnce({
|
|
persona: 'coder',
|
|
status: 'done',
|
|
content: 'Single-pass success',
|
|
timestamp: new Date('2026-02-11T00:03:00Z'),
|
|
sessionId: 'session-resume-2',
|
|
});
|
|
|
|
// When
|
|
await runReportPhase(step, 1, ctx);
|
|
|
|
// Then
|
|
expect(runAgentMock).toHaveBeenCalledTimes(1);
|
|
const reportPath = join(reportDir, '05-ok.md');
|
|
expect(readFileSync(reportPath, 'utf-8')).toBe('Single-pass success');
|
|
});
|
|
|
|
it('should return blocked result without retry', async () => {
|
|
// Given
|
|
const reportDir = join(tmpRoot, '.takt', 'runs', 'sample-run', 'reports');
|
|
const step = createStep('06-blocked.md');
|
|
const ctx = createContext(reportDir);
|
|
const runAgentMock = vi.mocked(runAgent);
|
|
runAgentMock.mockResolvedValueOnce({
|
|
persona: 'coder',
|
|
status: 'blocked',
|
|
content: 'Need permission',
|
|
timestamp: new Date('2026-02-11T00:04:00Z'),
|
|
});
|
|
|
|
// When
|
|
const result = await runReportPhase(step, 1, ctx);
|
|
|
|
// Then
|
|
expect(result).toEqual({
|
|
blocked: true,
|
|
response: {
|
|
persona: 'coder',
|
|
status: 'blocked',
|
|
content: 'Need permission',
|
|
timestamp: new Date('2026-02-11T00:04:00Z'),
|
|
},
|
|
});
|
|
expect(runAgentMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|