Task-level approval (requiresApproval, mergeRequiresApproval, pending_approval status) was redundant with executionMode (yolo vs review_per_phase) and blocked the orchestrator's phase completion flow. Tasks now complete directly; phase-level review via executionMode is the right granularity. Removed: schema columns (left in DB, removed from Drizzle), TaskPendingApprovalEvent, approveTask/listPendingApprovals procedures, findPendingApproval repository method, and all frontend approval UI.
157 lines
4.8 KiB
TypeScript
157 lines
4.8 KiB
TypeScript
/**
|
|
* Full-Flow Test Report Utility
|
|
*
|
|
* Plain console.log formatters for human-readable output at each stage of the
|
|
* full-flow integration test. No external dependencies.
|
|
*/
|
|
|
|
import { execSync } from 'node:child_process';
|
|
import { join } from 'node:path';
|
|
import type { Phase, Task } from '../../../db/schema.js';
|
|
import type { AgentResult } from '../../../agent/types.js';
|
|
|
|
// =============================================================================
|
|
// Types
|
|
// =============================================================================
|
|
|
|
export interface ExecutedTask {
|
|
task: Task;
|
|
result: AgentResult | null;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Helpers
|
|
// =============================================================================
|
|
|
|
const DIVIDER = '═'.repeat(60);
|
|
const THIN = '─'.repeat(60);
|
|
|
|
function section(title: string): void {
|
|
console.log(`\n${DIVIDER}`);
|
|
console.log(` ${title}`);
|
|
console.log(DIVIDER);
|
|
}
|
|
|
|
function line(msg: string): void {
|
|
console.log(` ${msg}`);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Stage reporters
|
|
// =============================================================================
|
|
|
|
export function printHeader(initiativeName: string): void {
|
|
section(`FULL-FLOW TEST: ${initiativeName}`);
|
|
console.log(` Started at: ${new Date().toISOString()}`);
|
|
}
|
|
|
|
export function printDiscussResult(agentId: string, result: AgentResult | null): void {
|
|
console.log(`\n[DISCUSS]`);
|
|
console.log(THIN);
|
|
line(`Agent: ${agentId}`);
|
|
if (result) {
|
|
line(`Success: ${result.success}`);
|
|
if (result.message) line(`Message: ${result.message.slice(0, 200)}`);
|
|
} else {
|
|
line('Result: null (agent may have crashed)');
|
|
}
|
|
}
|
|
|
|
export function printPlanResult(phases: Phase[]): void {
|
|
console.log(`\n[PLAN] ${phases.length} phase(s) created`);
|
|
console.log(THIN);
|
|
phases.forEach((ph, i) => {
|
|
line(`${i + 1}. ${ph.name}`);
|
|
});
|
|
}
|
|
|
|
export function printDetailResult(phase: Phase, tasks: Task[]): void {
|
|
console.log(`\n[DETAIL] Phase "${phase.name}" → ${tasks.length} task(s)`);
|
|
console.log(THIN);
|
|
tasks.forEach((t, i) => {
|
|
const flags = [t.category, t.type].join(', ');
|
|
line(`${i + 1}. ${t.name} [${flags}]`);
|
|
if (t.description) {
|
|
line(` ${t.description.slice(0, 120)}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
export function printExecuteResult(executed: ExecutedTask[]): void {
|
|
const succeeded = executed.filter((e) => e.result?.success).length;
|
|
console.log(`\n[EXECUTE] ${succeeded}/${executed.length} task(s) succeeded`);
|
|
console.log(THIN);
|
|
for (const { task, result } of executed) {
|
|
const icon = result?.success ? '✓' : '✗';
|
|
line(`${icon} ${task.name}`);
|
|
if (result && !result.success) {
|
|
line(` Error: ${result.message?.slice(0, 120)}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function printGitDiff(workspaceRoot: string, projectName: string): void {
|
|
console.log('\n[GIT DIFF — agent worktrees]');
|
|
console.log(THIN);
|
|
|
|
// Find all agent worktrees for this project
|
|
const worktreesBase = join(workspaceRoot, 'agent-workdirs');
|
|
try {
|
|
const dirs = execSync(`ls "${worktreesBase}" 2>/dev/null || echo ""`, { encoding: 'utf8' })
|
|
.trim()
|
|
.split('\n')
|
|
.filter(Boolean);
|
|
|
|
for (const dir of dirs) {
|
|
const projectDir = join(worktreesBase, dir, projectName);
|
|
try {
|
|
const stat = execSync(`git -C "${projectDir}" diff HEAD~1 --stat 2>/dev/null || echo ""`, {
|
|
encoding: 'utf8',
|
|
}).trim();
|
|
if (stat) {
|
|
line(`Worktree: ${dir}/${projectName}`);
|
|
stat.split('\n').forEach((l) => line(` ${l}`));
|
|
}
|
|
} catch {
|
|
// Worktree might not have commits — skip silently
|
|
}
|
|
}
|
|
} catch {
|
|
line('(no agent worktrees found)');
|
|
}
|
|
}
|
|
|
|
export function printNpmTestResult(projectDir: string): void {
|
|
console.log('\n[NPM TEST]');
|
|
console.log(THIN);
|
|
try {
|
|
const output = execSync('node --test src/todo.test.js', {
|
|
cwd: projectDir,
|
|
encoding: 'utf8',
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
line('Tests passed:');
|
|
output.split('\n').forEach((l) => line(` ${l}`));
|
|
} catch (err: unknown) {
|
|
const e = err as { stdout?: string; stderr?: string; status?: number };
|
|
line(`Tests FAILED (exit ${e.status ?? '?'})`);
|
|
if (e.stdout) e.stdout.split('\n').forEach((l) => line(` ${l}`));
|
|
if (e.stderr) e.stderr.split('\n').forEach((l) => line(` ${l}`));
|
|
}
|
|
}
|
|
|
|
export function printFinalSummary(
|
|
initiativeName: string,
|
|
phases: Phase[],
|
|
tasks: Task[],
|
|
executed: ExecutedTask[],
|
|
durationMs: number,
|
|
): void {
|
|
section(`SUMMARY: ${initiativeName}`);
|
|
line(`Duration : ${Math.round(durationMs / 1000)}s`);
|
|
line(`Phases : ${phases.length}`);
|
|
line(`Tasks : ${tasks.length}`);
|
|
line(`Executed : ${executed.filter((e) => e.result?.success).length}/${executed.length} succeeded`);
|
|
console.log(DIVIDER);
|
|
}
|