import type { RoleResult, StartStep, WorkflowMessage } from "@uncaged/nerve-core"; import { spawnSafe } from "@uncaged/nerve-workflow-utils"; import type { SpawnError } from "@uncaged/nerve-workflow-utils"; import { existsSync } from "node:fs"; import { join } from "node:path"; import { NERVE_ROOT, SENSES_DIR } from "../shared.js"; import type { SenseMeta } from "../types.js"; function formatSpawnFailure(error: SpawnError): string { if (error.kind === "spawn_failed") return error.message; if (error.kind === "timeout") return `timeout (stdout=${error.stdout.slice(0, 200)})`; return `exit ${error.exitCode} stderr=${error.stderr.slice(0, 400)}`; } async function runSenseSmokeTest( senseName: string, ): Promise<{ ok: boolean; log: string; reason: string }> { const logParts: string[] = []; const runNerve = async ( args: string[], ): Promise<{ ok: true; out: string } | { ok: false; err: string }> => { const result = await spawnSafe("nerve", args, { cwd: NERVE_ROOT, env: null, timeoutMs: 300_000, }); if (!result.ok) return { ok: false, err: formatSpawnFailure(result.error) }; return { ok: true, out: result.value.stdout }; }; const statusRun = await runNerve(["status"]); if (!statusRun.ok) { return { ok: false, log: `=== nerve status ===\nERROR: ${statusRun.err}`, reason: `Smoke test command failed: ${statusRun.err}`, }; } logParts.push("=== nerve status ===\n" + statusRun.out); if (!statusRun.out.includes(senseName)) { return { ok: false, log: logParts.join("\n\n"), reason: `Sense "${senseName}" not listed in \`nerve status\` output`, }; } const triggerRun = await runNerve(["sense", "trigger", senseName]); if (!triggerRun.ok) { logParts.push(`=== nerve sense trigger ===\nERROR: ${triggerRun.err}`); return { ok: false, log: logParts.join("\n\n"), reason: `Trigger failed: ${triggerRun.err}` }; } logParts.push("=== nerve sense trigger ===\n" + triggerRun.out); let lastQuery = ""; for (let i = 0; i < 25; i++) { await new Promise((r) => setTimeout(r, 1000)); const queryRun = await runNerve(["sense", "query", senseName]); if (!queryRun.ok) { logParts.push(`=== query attempt ${i + 1} ===\nERROR: ${queryRun.err}`); } else { lastQuery = queryRun.out; logParts.push(`=== query attempt ${i + 1} ===\n${lastQuery}`); if (!lastQuery.includes("(0 rows)")) { return { ok: true, log: logParts.join("\n\n"), reason: "Trigger succeeded and query returned at least one row", }; } } } return { ok: false, log: logParts.join("\n\n"), reason: lastQuery.includes("(0 rows)") ? "Query still returned 0 rows after trigger" : "Timed out waiting for successful sense query", }; } export async function tester( _start: StartStep, messages: WorkflowMessage[], ): Promise> { const attempt = messages.filter((m) => m.role === "tester").length + 1; const plannerStep = messages.find((m) => m.role === "planner"); const senseName = plannerStep ? (plannerStep.meta as SenseMeta["planner"]).senseName : ""; if (senseName.length === 0) { return { content: "FAIL — no senseName from planner", meta: { passed: false, attempt }, }; } const senseDir = join(SENSES_DIR, senseName); const missing = [ existsSync(join(senseDir, "index.js")) ? null : "index.js", existsSync(join(senseDir, "schema.ts")) ? null : "schema.ts", existsSync(join(senseDir, "migrations", "0001_init.sql")) ? null : "migrations/0001_init.sql", ].filter((x) => x !== null); if (missing.length > 0) { return { content: `FAIL — missing files: ${missing.join(", ")}`, meta: { passed: false, attempt }, }; } const smoke = await runSenseSmokeTest(senseName); return { content: `${smoke.ok ? "PASS" : "FAIL"} — ${smoke.reason}`, meta: { passed: smoke.ok, attempt }, }; }