Files
Codewalkers/src/agent/completion-detection.test.ts
Lukas May fab7706f5c feat: Phase schema refactor, agent lifecycle module, and log chunks
Phase model changes:
- Drop `number` column (ordering now by createdAt + dependency DAG)
- Replace `description` (plain text) with `content` (Tiptap JSON)
- Add `approved` status as dispatch gate
- Add phase dependency management (list, remove, dependents)
- Approval gate in PhaseDispatchManager.queuePhase()

Agent log chunks:
- New `agent_log_chunks` table for DB-first output persistence
- LogChunkRepository port + DrizzleLogChunkRepository adapter
- FileTailer onRawContent callback streams chunks to DB
- getAgentOutput reads from DB first, falls back to file

Agent lifecycle module (src/agent/lifecycle/):
- SignalManager: atomic signal.json read/write/wait operations
- RetryPolicy: exponential backoff with error-specific strategies
- ErrorAnalyzer: pattern-based error classification
- CleanupStrategy: debug archival vs production cleanup
- AgentLifecycleController: orchestrates retry/recovery flow
- Missing signal recovery with instruction injection

Completion detection fixes:
- Read signal.json file instead of parsing stdout as JSON
- Cancellable pollForCompletion with { cancel } handle
- Centralized state cleanup via cleanupAgentState()
- Credential handler consolidation (prepareProcessEnv)

Prompts refactor:
- Split monolithic prompts.ts into per-mode modules
- Add workspace layout section to agent prompts
- Fix markdown-to-tiptap double-serialization

Server/tRPC:
- Subscription heartbeat (30s) and bounded queue (1000 max)
- Phase CRUD: approvePhase, deletePhase, dependency queries
- Page: findByIds, getPageUpdatedAtMap
- Wire new repositories through container and context
2026-02-09 22:33:28 +01:00

133 lines
4.5 KiB
TypeScript

/**
* Test for completion detection via readSignalCompletion
*/
import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
import { mkdtemp, writeFile, mkdir } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { rmSync } from 'node:fs';
import { OutputHandler } from './output-handler.js';
import type { AgentRepository } from '../db/repositories/agent-repository.js';
import type { ProposalRepository } from '../db/repositories/proposal-repository.js';
describe('Completion Detection Fix', () => {
let tempDir: string;
let outputHandler: OutputHandler;
let mockAgentRepo: AgentRepository;
let mockProposalRepo: ProposalRepository;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'completion-test-'));
// Mock repositories
mockAgentRepo = {
update: vi.fn(),
findById: vi.fn().mockResolvedValue({ id: 'test-agent', mode: 'refine' }),
} as any;
mockProposalRepo = {
create: vi.fn(),
} as any;
outputHandler = new OutputHandler(mockAgentRepo, undefined, mockProposalRepo);
});
afterEach(() => {
rmSync(tempDir, { recursive: true, force: true });
});
test('detects completion from signal.json with "questions" status', async () => {
const agentWorkdir = join(tempDir, 'test-agent');
const cwDir = join(agentWorkdir, '.cw/output');
await mkdir(cwDir, { recursive: true });
const signalContent = JSON.stringify({
status: 'questions',
questions: [{ id: 'q1', text: 'Do you want to proceed?' }]
});
await writeFile(join(cwDir, 'signal.json'), signalContent);
const readSignalCompletion = (outputHandler as any).readSignalCompletion.bind(outputHandler);
const result = await readSignalCompletion(agentWorkdir);
expect(result).not.toBeNull();
expect(JSON.parse(result).status).toBe('questions');
});
test('detects completion from signal.json with "done" status', async () => {
const agentWorkdir = join(tempDir, 'test-agent');
const cwDir = join(agentWorkdir, '.cw/output');
await mkdir(cwDir, { recursive: true });
const signalContent = JSON.stringify({
status: 'done',
result: 'Task completed successfully'
});
await writeFile(join(cwDir, 'signal.json'), signalContent);
const readSignalCompletion = (outputHandler as any).readSignalCompletion.bind(outputHandler);
const result = await readSignalCompletion(agentWorkdir);
expect(result).not.toBeNull();
expect(JSON.parse(result).status).toBe('done');
});
test('detects completion from signal.json with "error" status', async () => {
const agentWorkdir = join(tempDir, 'test-agent');
const cwDir = join(agentWorkdir, '.cw/output');
await mkdir(cwDir, { recursive: true });
const signalContent = JSON.stringify({
status: 'error',
error: 'Something went wrong'
});
await writeFile(join(cwDir, 'signal.json'), signalContent);
const readSignalCompletion = (outputHandler as any).readSignalCompletion.bind(outputHandler);
const result = await readSignalCompletion(agentWorkdir);
expect(result).not.toBeNull();
expect(JSON.parse(result).status).toBe('error');
});
test('returns null when signal.json does not exist', async () => {
const agentWorkdir = join(tempDir, 'test-agent');
const readSignalCompletion = (outputHandler as any).readSignalCompletion.bind(outputHandler);
const result = await readSignalCompletion(agentWorkdir);
expect(result).toBeNull();
});
test('returns null for incomplete status', async () => {
const agentWorkdir = join(tempDir, 'test-agent');
const cwDir = join(agentWorkdir, '.cw/output');
await mkdir(cwDir, { recursive: true });
const signalContent = JSON.stringify({
status: 'running',
progress: 'Still working...'
});
await writeFile(join(cwDir, 'signal.json'), signalContent);
const readSignalCompletion = (outputHandler as any).readSignalCompletion.bind(outputHandler);
const result = await readSignalCompletion(agentWorkdir);
expect(result).toBeNull();
});
test('handles malformed signal.json gracefully', async () => {
const agentWorkdir = join(tempDir, 'test-agent');
const cwDir = join(agentWorkdir, '.cw/output');
await mkdir(cwDir, { recursive: true });
await writeFile(join(cwDir, 'signal.json'), '{ invalid json }');
const readSignalCompletion = (outputHandler as any).readSignalCompletion.bind(outputHandler);
const result = await readSignalCompletion(agentWorkdir);
expect(result).toBeNull();
});
});