664 lines
21 KiB
TypeScript
664 lines
21 KiB
TypeScript
/**
|
|
* Tests for session log incremental writes and pointer management
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { existsSync, readFileSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import {
|
|
initNdjsonLog,
|
|
appendNdjsonLine,
|
|
loadNdjsonLog,
|
|
loadSessionLog,
|
|
extractFailureInfo,
|
|
type SessionLog,
|
|
type NdjsonRecord,
|
|
type NdjsonStepComplete,
|
|
type NdjsonPieceComplete,
|
|
type NdjsonPieceAbort,
|
|
type NdjsonPhaseStart,
|
|
type NdjsonPhaseComplete,
|
|
type NdjsonInteractiveStart,
|
|
type NdjsonInteractiveEnd,
|
|
} from '../infra/fs/session.js';
|
|
|
|
/** Create a temp project directory for each test */
|
|
function createTempProject(): string {
|
|
const dir = join(tmpdir(), `takt-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
mkdirSync(dir, { recursive: true });
|
|
return dir;
|
|
}
|
|
|
|
function initTestNdjsonLog(sessionId: string, task: string, pieceName: string, projectDir: string): string {
|
|
const logsDir = join(projectDir, '.takt', 'runs', 'test-run', 'logs');
|
|
mkdirSync(logsDir, { recursive: true });
|
|
return initNdjsonLog(sessionId, task, pieceName, { logsDir });
|
|
}
|
|
|
|
describe('NDJSON log', () => {
|
|
let projectDir: string;
|
|
|
|
beforeEach(() => {
|
|
projectDir = createTempProject();
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(projectDir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('initNdjsonLog', () => {
|
|
it('should create a .jsonl file with piece_start record', () => {
|
|
const filepath = initTestNdjsonLog('sess-001', 'my task', 'default', projectDir);
|
|
|
|
expect(filepath).toContain('sess-001.jsonl');
|
|
expect(existsSync(filepath)).toBe(true);
|
|
|
|
const content = readFileSync(filepath, 'utf-8');
|
|
const lines = content.trim().split('\n');
|
|
expect(lines).toHaveLength(1);
|
|
|
|
const record = JSON.parse(lines[0]!) as NdjsonRecord;
|
|
expect(record.type).toBe('piece_start');
|
|
if (record.type === 'piece_start') {
|
|
expect(record.task).toBe('my task');
|
|
expect(record.pieceName).toBe('default');
|
|
expect(record.startTime).toBeDefined();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('appendNdjsonLine', () => {
|
|
it('should append records as individual lines', () => {
|
|
const filepath = initTestNdjsonLog('sess-002', 'task', 'wf', projectDir);
|
|
|
|
const stepStart: NdjsonRecord = {
|
|
type: 'step_start',
|
|
step: 'plan',
|
|
persona: 'planner',
|
|
iteration: 1,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
appendNdjsonLine(filepath, stepStart);
|
|
|
|
const stepComplete: NdjsonStepComplete = {
|
|
type: 'step_complete',
|
|
step: 'plan',
|
|
persona: 'planner',
|
|
status: 'done',
|
|
content: 'Plan completed',
|
|
instruction: 'Create a plan',
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
appendNdjsonLine(filepath, stepComplete);
|
|
|
|
const content = readFileSync(filepath, 'utf-8');
|
|
const lines = content.trim().split('\n');
|
|
expect(lines).toHaveLength(3); // piece_start + step_start + step_complete
|
|
|
|
const parsed0 = JSON.parse(lines[0]!) as NdjsonRecord;
|
|
expect(parsed0.type).toBe('piece_start');
|
|
|
|
const parsed1 = JSON.parse(lines[1]!) as NdjsonRecord;
|
|
expect(parsed1.type).toBe('step_start');
|
|
if (parsed1.type === 'step_start') {
|
|
expect(parsed1.step).toBe('plan');
|
|
expect(parsed1.persona).toBe('planner');
|
|
expect(parsed1.iteration).toBe(1);
|
|
}
|
|
|
|
const parsed2 = JSON.parse(lines[2]!) as NdjsonRecord;
|
|
expect(parsed2.type).toBe('step_complete');
|
|
if (parsed2.type === 'step_complete') {
|
|
expect(parsed2.step).toBe('plan');
|
|
expect(parsed2.content).toBe('Plan completed');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('loadNdjsonLog', () => {
|
|
it('should reconstruct SessionLog from NDJSON file', () => {
|
|
const filepath = initTestNdjsonLog('sess-003', 'build app', 'default', projectDir);
|
|
|
|
// Add step_start + step_complete
|
|
appendNdjsonLine(filepath, {
|
|
type: 'step_start',
|
|
step: 'plan',
|
|
persona: 'planner',
|
|
iteration: 1,
|
|
timestamp: '2025-01-01T00:00:01.000Z',
|
|
});
|
|
|
|
const stepComplete: NdjsonStepComplete = {
|
|
type: 'step_complete',
|
|
step: 'plan',
|
|
persona: 'planner',
|
|
status: 'done',
|
|
content: 'Plan completed',
|
|
instruction: 'Create a plan',
|
|
matchedRuleIndex: 0,
|
|
matchedRuleMethod: 'phase3_tag',
|
|
timestamp: '2025-01-01T00:00:02.000Z',
|
|
};
|
|
appendNdjsonLine(filepath, stepComplete);
|
|
|
|
const complete: NdjsonPieceComplete = {
|
|
type: 'piece_complete',
|
|
iterations: 1,
|
|
endTime: '2025-01-01T00:00:03.000Z',
|
|
};
|
|
appendNdjsonLine(filepath, complete);
|
|
|
|
const log = loadNdjsonLog(filepath);
|
|
expect(log).not.toBeNull();
|
|
expect(log!.task).toBe('build app');
|
|
expect(log!.pieceName).toBe('default');
|
|
expect(log!.status).toBe('completed');
|
|
expect(log!.iterations).toBe(1);
|
|
expect(log!.endTime).toBe('2025-01-01T00:00:03.000Z');
|
|
expect(log!.history).toHaveLength(1);
|
|
expect(log!.history[0]!.step).toBe('plan');
|
|
expect(log!.history[0]!.content).toBe('Plan completed');
|
|
expect(log!.history[0]!.matchedRuleIndex).toBe(0);
|
|
expect(log!.history[0]!.matchedRuleMethod).toBe('phase3_tag');
|
|
});
|
|
|
|
it('should handle aborted piece', () => {
|
|
const filepath = initTestNdjsonLog('sess-004', 'failing task', 'wf', projectDir);
|
|
|
|
appendNdjsonLine(filepath, {
|
|
type: 'step_start',
|
|
step: 'impl',
|
|
persona: 'coder',
|
|
iteration: 1,
|
|
timestamp: '2025-01-01T00:00:01.000Z',
|
|
});
|
|
|
|
appendNdjsonLine(filepath, {
|
|
type: 'step_complete',
|
|
step: 'impl',
|
|
persona: 'coder',
|
|
status: 'error',
|
|
content: 'Failed',
|
|
instruction: 'Do the thing',
|
|
error: 'compile error',
|
|
timestamp: '2025-01-01T00:00:02.000Z',
|
|
} satisfies NdjsonStepComplete);
|
|
|
|
const abort: NdjsonPieceAbort = {
|
|
type: 'piece_abort',
|
|
iterations: 1,
|
|
reason: 'Max movements reached',
|
|
endTime: '2025-01-01T00:00:03.000Z',
|
|
};
|
|
appendNdjsonLine(filepath, abort);
|
|
|
|
const log = loadNdjsonLog(filepath);
|
|
expect(log).not.toBeNull();
|
|
expect(log!.status).toBe('aborted');
|
|
expect(log!.history[0]!.error).toBe('compile error');
|
|
});
|
|
|
|
it('should return null for non-existent file', () => {
|
|
const result = loadNdjsonLog('/nonexistent/path.jsonl');
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should return null for empty file', () => {
|
|
const logsDir = join(projectDir, '.takt', 'logs');
|
|
mkdirSync(logsDir, { recursive: true });
|
|
const filepath = join(logsDir, 'empty.jsonl');
|
|
writeFileSync(filepath, '', 'utf-8');
|
|
|
|
const result = loadNdjsonLog(filepath);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should skip step_start records when reconstructing SessionLog', () => {
|
|
const filepath = initTestNdjsonLog('sess-005', 'task', 'wf', projectDir);
|
|
|
|
// Add various records
|
|
appendNdjsonLine(filepath, {
|
|
type: 'step_start',
|
|
step: 'plan',
|
|
persona: 'planner',
|
|
iteration: 1,
|
|
timestamp: '2025-01-01T00:00:01.000Z',
|
|
});
|
|
|
|
appendNdjsonLine(filepath, {
|
|
type: 'step_complete',
|
|
step: 'plan',
|
|
persona: 'planner',
|
|
status: 'done',
|
|
content: 'Done',
|
|
instruction: 'Plan it',
|
|
timestamp: '2025-01-01T00:00:02.000Z',
|
|
} satisfies NdjsonStepComplete);
|
|
|
|
appendNdjsonLine(filepath, {
|
|
type: 'piece_complete',
|
|
iterations: 1,
|
|
endTime: '2025-01-01T00:00:03.000Z',
|
|
});
|
|
|
|
const log = loadNdjsonLog(filepath);
|
|
expect(log).not.toBeNull();
|
|
// Only step_complete adds to history
|
|
expect(log!.history).toHaveLength(1);
|
|
expect(log!.iterations).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('loadSessionLog with .jsonl extension', () => {
|
|
it('should delegate to loadNdjsonLog for .jsonl files', () => {
|
|
const filepath = initTestNdjsonLog('sess-006', 'jsonl task', 'wf', projectDir);
|
|
|
|
appendNdjsonLine(filepath, {
|
|
type: 'step_complete',
|
|
step: 'plan',
|
|
persona: 'planner',
|
|
status: 'done',
|
|
content: 'Plan done',
|
|
instruction: 'Plan',
|
|
timestamp: '2025-01-01T00:00:02.000Z',
|
|
} satisfies NdjsonStepComplete);
|
|
|
|
appendNdjsonLine(filepath, {
|
|
type: 'piece_complete',
|
|
iterations: 1,
|
|
endTime: '2025-01-01T00:00:03.000Z',
|
|
});
|
|
|
|
// loadSessionLog should handle .jsonl
|
|
const log = loadSessionLog(filepath);
|
|
expect(log).not.toBeNull();
|
|
expect(log!.task).toBe('jsonl task');
|
|
expect(log!.status).toBe('completed');
|
|
});
|
|
|
|
it('should still load legacy .json files', () => {
|
|
const logsDir = join(projectDir, '.takt', 'logs');
|
|
mkdirSync(logsDir, { recursive: true });
|
|
const legacyPath = join(logsDir, 'legacy-001.json');
|
|
const legacyLog: SessionLog = {
|
|
task: 'legacy task',
|
|
projectDir,
|
|
pieceName: 'wf',
|
|
iterations: 0,
|
|
startTime: new Date().toISOString(),
|
|
status: 'running',
|
|
history: [],
|
|
};
|
|
writeFileSync(legacyPath, JSON.stringify(legacyLog, null, 2), 'utf-8');
|
|
|
|
const log = loadSessionLog(legacyPath);
|
|
expect(log).not.toBeNull();
|
|
expect(log!.task).toBe('legacy task');
|
|
});
|
|
});
|
|
|
|
describe('appendNdjsonLine real-time characteristics', () => {
|
|
it('should append without overwriting previous content', () => {
|
|
const filepath = initTestNdjsonLog('sess-007', 'task', 'wf', projectDir);
|
|
|
|
// Read after init
|
|
const after1 = readFileSync(filepath, 'utf-8').trim().split('\n');
|
|
expect(after1).toHaveLength(1);
|
|
|
|
// Append more records
|
|
appendNdjsonLine(filepath, {
|
|
type: 'step_start',
|
|
step: 'plan',
|
|
persona: 'planner',
|
|
iteration: 1,
|
|
timestamp: '2025-01-01T00:00:01.000Z',
|
|
});
|
|
|
|
const after2 = readFileSync(filepath, 'utf-8').trim().split('\n');
|
|
expect(after2).toHaveLength(2);
|
|
// First line should still be piece_start
|
|
expect(JSON.parse(after2[0]!).type).toBe('piece_start');
|
|
});
|
|
|
|
it('should produce valid JSON on each line', () => {
|
|
const filepath = initTestNdjsonLog('sess-008', 'task', 'wf', projectDir);
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
appendNdjsonLine(filepath, {
|
|
type: 'step_start',
|
|
step: `step-${i}`,
|
|
persona: 'planner',
|
|
iteration: i + 1,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
}
|
|
|
|
const content = readFileSync(filepath, 'utf-8');
|
|
const lines = content.trim().split('\n');
|
|
expect(lines).toHaveLength(6); // 1 init + 5 step_start
|
|
|
|
// Every line should be valid JSON
|
|
for (const line of lines) {
|
|
expect(() => JSON.parse(line)).not.toThrow();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('phase NDJSON records', () => {
|
|
it('should serialize and append phase_start records', () => {
|
|
const filepath = initTestNdjsonLog('sess-phase-001', 'task', 'wf', projectDir);
|
|
|
|
const record: NdjsonPhaseStart = {
|
|
type: 'phase_start',
|
|
step: 'plan',
|
|
phase: 1,
|
|
phaseName: 'execute',
|
|
timestamp: '2025-01-01T00:00:01.000Z',
|
|
instruction: 'Do the planning',
|
|
};
|
|
appendNdjsonLine(filepath, record);
|
|
|
|
const content = readFileSync(filepath, 'utf-8');
|
|
const lines = content.trim().split('\n');
|
|
expect(lines).toHaveLength(2); // piece_start + phase_start
|
|
|
|
const parsed = JSON.parse(lines[1]!) as NdjsonRecord;
|
|
expect(parsed.type).toBe('phase_start');
|
|
if (parsed.type === 'phase_start') {
|
|
expect(parsed.step).toBe('plan');
|
|
expect(parsed.phase).toBe(1);
|
|
expect(parsed.phaseName).toBe('execute');
|
|
expect(parsed.instruction).toBe('Do the planning');
|
|
}
|
|
});
|
|
|
|
it('should serialize and append phase_complete records', () => {
|
|
const filepath = initTestNdjsonLog('sess-phase-002', 'task', 'wf', projectDir);
|
|
|
|
const record: NdjsonPhaseComplete = {
|
|
type: 'phase_complete',
|
|
step: 'plan',
|
|
phase: 2,
|
|
phaseName: 'report',
|
|
status: 'done',
|
|
content: 'Report output',
|
|
timestamp: '2025-01-01T00:00:02.000Z',
|
|
};
|
|
appendNdjsonLine(filepath, record);
|
|
|
|
const content = readFileSync(filepath, 'utf-8');
|
|
const lines = content.trim().split('\n');
|
|
expect(lines).toHaveLength(2);
|
|
|
|
const parsed = JSON.parse(lines[1]!) as NdjsonRecord;
|
|
expect(parsed.type).toBe('phase_complete');
|
|
if (parsed.type === 'phase_complete') {
|
|
expect(parsed.step).toBe('plan');
|
|
expect(parsed.phase).toBe(2);
|
|
expect(parsed.phaseName).toBe('report');
|
|
expect(parsed.status).toBe('done');
|
|
expect(parsed.content).toBe('Report output');
|
|
}
|
|
});
|
|
|
|
it('should serialize phase_complete with error', () => {
|
|
const filepath = initTestNdjsonLog('sess-phase-003', 'task', 'wf', projectDir);
|
|
|
|
const record: NdjsonPhaseComplete = {
|
|
type: 'phase_complete',
|
|
step: 'impl',
|
|
phase: 3,
|
|
phaseName: 'judge',
|
|
status: 'error',
|
|
timestamp: '2025-01-01T00:00:03.000Z',
|
|
error: 'Status judgment phase failed',
|
|
};
|
|
appendNdjsonLine(filepath, record);
|
|
|
|
const content = readFileSync(filepath, 'utf-8');
|
|
const lines = content.trim().split('\n');
|
|
const parsed = JSON.parse(lines[1]!) as NdjsonRecord;
|
|
expect(parsed.type).toBe('phase_complete');
|
|
if (parsed.type === 'phase_complete') {
|
|
expect(parsed.error).toBe('Status judgment phase failed');
|
|
expect(parsed.phase).toBe(3);
|
|
expect(parsed.phaseName).toBe('judge');
|
|
}
|
|
});
|
|
|
|
it('should be skipped by loadNdjsonLog (default case)', () => {
|
|
const filepath = initTestNdjsonLog('sess-phase-004', 'task', 'wf', projectDir);
|
|
|
|
// Add phase records
|
|
appendNdjsonLine(filepath, {
|
|
type: 'phase_start',
|
|
step: 'plan',
|
|
phase: 1,
|
|
phaseName: 'execute',
|
|
timestamp: '2025-01-01T00:00:01.000Z',
|
|
instruction: 'Plan it',
|
|
} satisfies NdjsonPhaseStart);
|
|
|
|
appendNdjsonLine(filepath, {
|
|
type: 'phase_complete',
|
|
step: 'plan',
|
|
phase: 1,
|
|
phaseName: 'execute',
|
|
status: 'done',
|
|
content: 'Planned',
|
|
timestamp: '2025-01-01T00:00:02.000Z',
|
|
} satisfies NdjsonPhaseComplete);
|
|
|
|
// Add a step_complete so we can verify history
|
|
appendNdjsonLine(filepath, {
|
|
type: 'step_complete',
|
|
step: 'plan',
|
|
persona: 'planner',
|
|
status: 'done',
|
|
content: 'Plan completed',
|
|
instruction: 'Plan it',
|
|
timestamp: '2025-01-01T00:00:03.000Z',
|
|
} satisfies NdjsonStepComplete);
|
|
|
|
const log = loadNdjsonLog(filepath);
|
|
expect(log).not.toBeNull();
|
|
// Only step_complete should contribute to history
|
|
expect(log!.history).toHaveLength(1);
|
|
expect(log!.iterations).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('interactive NDJSON records', () => {
|
|
it('should serialize and append interactive_start records', () => {
|
|
const filepath = initTestNdjsonLog('sess-interactive-001', 'task', 'wf', projectDir);
|
|
|
|
const record: NdjsonInteractiveStart = {
|
|
type: 'interactive_start',
|
|
timestamp: '2025-01-01T00:00:01.000Z',
|
|
};
|
|
appendNdjsonLine(filepath, record);
|
|
|
|
const content = readFileSync(filepath, 'utf-8');
|
|
const lines = content.trim().split('\n');
|
|
expect(lines).toHaveLength(2);
|
|
|
|
const parsed = JSON.parse(lines[1]!) as NdjsonRecord;
|
|
expect(parsed.type).toBe('interactive_start');
|
|
if (parsed.type === 'interactive_start') {
|
|
expect(parsed.timestamp).toBe('2025-01-01T00:00:01.000Z');
|
|
}
|
|
});
|
|
|
|
it('should serialize and append interactive_end records', () => {
|
|
const filepath = initTestNdjsonLog('sess-interactive-002', 'task', 'wf', projectDir);
|
|
|
|
const record: NdjsonInteractiveEnd = {
|
|
type: 'interactive_end',
|
|
confirmed: true,
|
|
task: 'Build a feature',
|
|
timestamp: '2025-01-01T00:00:02.000Z',
|
|
};
|
|
appendNdjsonLine(filepath, record);
|
|
|
|
const content = readFileSync(filepath, 'utf-8');
|
|
const lines = content.trim().split('\n');
|
|
expect(lines).toHaveLength(2);
|
|
|
|
const parsed = JSON.parse(lines[1]!) as NdjsonRecord;
|
|
expect(parsed.type).toBe('interactive_end');
|
|
if (parsed.type === 'interactive_end') {
|
|
expect(parsed.confirmed).toBe(true);
|
|
expect(parsed.task).toBe('Build a feature');
|
|
}
|
|
});
|
|
|
|
it('should be skipped by loadNdjsonLog (default case)', () => {
|
|
const filepath = initTestNdjsonLog('sess-interactive-003', 'task', 'wf', projectDir);
|
|
|
|
appendNdjsonLine(filepath, {
|
|
type: 'interactive_start',
|
|
timestamp: '2025-01-01T00:00:01.000Z',
|
|
} satisfies NdjsonInteractiveStart);
|
|
|
|
appendNdjsonLine(filepath, {
|
|
type: 'interactive_end',
|
|
confirmed: true,
|
|
task: 'Some task',
|
|
timestamp: '2025-01-01T00:00:02.000Z',
|
|
} satisfies NdjsonInteractiveEnd);
|
|
|
|
const log = loadNdjsonLog(filepath);
|
|
expect(log).not.toBeNull();
|
|
expect(log!.history).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('extractFailureInfo', () => {
|
|
it('should return null for non-existent file', () => {
|
|
const result = extractFailureInfo('/nonexistent/path.jsonl');
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should extract failure info from aborted piece log', () => {
|
|
const filepath = initTestNdjsonLog('20260205-120000-abc123', 'failing task', 'wf', projectDir);
|
|
|
|
// Add step_start for plan
|
|
appendNdjsonLine(filepath, {
|
|
type: 'step_start',
|
|
step: 'plan',
|
|
persona: 'planner',
|
|
iteration: 1,
|
|
timestamp: '2025-01-01T00:00:01.000Z',
|
|
});
|
|
|
|
// Add step_complete for plan
|
|
appendNdjsonLine(filepath, {
|
|
type: 'step_complete',
|
|
step: 'plan',
|
|
persona: 'planner',
|
|
status: 'done',
|
|
content: 'Plan done',
|
|
instruction: 'Plan it',
|
|
timestamp: '2025-01-01T00:00:02.000Z',
|
|
} satisfies NdjsonStepComplete);
|
|
|
|
// Add step_start for implement (fails before completing)
|
|
appendNdjsonLine(filepath, {
|
|
type: 'step_start',
|
|
step: 'implement',
|
|
persona: 'coder',
|
|
iteration: 2,
|
|
timestamp: '2025-01-01T00:00:03.000Z',
|
|
});
|
|
|
|
// Add piece_abort
|
|
appendNdjsonLine(filepath, {
|
|
type: 'piece_abort',
|
|
iterations: 1,
|
|
reason: 'spawn node ENOENT',
|
|
endTime: '2025-01-01T00:00:04.000Z',
|
|
} satisfies NdjsonPieceAbort);
|
|
|
|
const result = extractFailureInfo(filepath);
|
|
expect(result).not.toBeNull();
|
|
expect(result!.lastCompletedMovement).toBe('plan');
|
|
expect(result!.failedMovement).toBe('implement');
|
|
expect(result!.iterations).toBe(1);
|
|
expect(result!.errorMessage).toBe('spawn node ENOENT');
|
|
expect(result!.sessionId).toBe('20260205-120000-abc123');
|
|
});
|
|
|
|
it('should handle log with only completed movements (no abort)', () => {
|
|
const filepath = initTestNdjsonLog('sess-success-001', 'task', 'wf', projectDir);
|
|
|
|
appendNdjsonLine(filepath, {
|
|
type: 'step_start',
|
|
step: 'plan',
|
|
persona: 'planner',
|
|
iteration: 1,
|
|
timestamp: '2025-01-01T00:00:01.000Z',
|
|
});
|
|
|
|
appendNdjsonLine(filepath, {
|
|
type: 'step_complete',
|
|
step: 'plan',
|
|
persona: 'planner',
|
|
status: 'done',
|
|
content: 'Plan done',
|
|
instruction: 'Plan it',
|
|
timestamp: '2025-01-01T00:00:02.000Z',
|
|
} satisfies NdjsonStepComplete);
|
|
|
|
appendNdjsonLine(filepath, {
|
|
type: 'piece_complete',
|
|
iterations: 1,
|
|
endTime: '2025-01-01T00:00:03.000Z',
|
|
});
|
|
|
|
const result = extractFailureInfo(filepath);
|
|
expect(result).not.toBeNull();
|
|
expect(result!.lastCompletedMovement).toBe('plan');
|
|
expect(result!.failedMovement).toBeNull();
|
|
expect(result!.iterations).toBe(1);
|
|
expect(result!.errorMessage).toBeNull();
|
|
});
|
|
|
|
it('should handle log with no step_complete records', () => {
|
|
const filepath = initTestNdjsonLog('sess-fail-early-001', 'task', 'wf', projectDir);
|
|
|
|
appendNdjsonLine(filepath, {
|
|
type: 'step_start',
|
|
step: 'plan',
|
|
persona: 'planner',
|
|
iteration: 1,
|
|
timestamp: '2025-01-01T00:00:01.000Z',
|
|
});
|
|
|
|
appendNdjsonLine(filepath, {
|
|
type: 'piece_abort',
|
|
iterations: 0,
|
|
reason: 'API error',
|
|
endTime: '2025-01-01T00:00:02.000Z',
|
|
} satisfies NdjsonPieceAbort);
|
|
|
|
const result = extractFailureInfo(filepath);
|
|
expect(result).not.toBeNull();
|
|
expect(result!.lastCompletedMovement).toBeNull();
|
|
expect(result!.failedMovement).toBe('plan');
|
|
expect(result!.iterations).toBe(0);
|
|
expect(result!.errorMessage).toBe('API error');
|
|
});
|
|
|
|
it('should return null for empty file', () => {
|
|
const logsDir = join(projectDir, '.takt', 'logs');
|
|
mkdirSync(logsDir, { recursive: true });
|
|
const filepath = join(logsDir, 'empty.jsonl');
|
|
writeFileSync(filepath, '', 'utf-8');
|
|
|
|
const result = extractFailureInfo(filepath);
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
});
|