Following nerve-dev best practice: each role gets its own directory. Structure: index.ts — 31 lines (WorkflowDefinition + moderator) roles/planner/index.ts — 48 lines (createCursorRole) roles/coder/index.ts — 33 lines (createCursorRole) roles/tester/index.ts — 122 lines (hand-written smoke test) roles/shared.ts — 63 lines (providers, helpers) roles/types.ts — 5 lines (SenseMeta) Was: single 416-line index.ts Refs uncaged/nerve#210 小橘 🍊(NEKO Team)
123 lines
3.9 KiB
TypeScript
123 lines
3.9 KiB
TypeScript
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<RoleResult<SenseMeta["tester"]>> {
|
|
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 },
|
|
};
|
|
}
|