| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import { test } from "node:test"; |
| import assert from "node:assert/strict"; |
|
|
| process.env.DATABASE_URL ??= "postgres://test:test@127.0.0.1:5432/test"; |
|
|
| import { |
| runInnerLoop, |
| _setInnerLoopPersistenceHooks, |
| INNER_LOOP_STEPS, |
| type StepDecl, |
| type StepEvent, |
| } from "../runner.ts"; |
| import { LlmRoleDisabled, LlmRoleResponseInvalid } from "../../../llm-roles/errors.ts"; |
|
|
| function captureHooks() { |
| const stepEvents: StepEvent[] = []; |
| let runOutcome: string | null = null; |
| let runUsed: Record<string, boolean> | null = null; |
| _setInnerLoopPersistenceHooks({ |
| step: (e) => stepEvents.push(e), |
| runStart: () => {}, |
| runEnd: (_id, outcome, _steps, used) => { |
| runOutcome = outcome; |
| runUsed = used; |
| }, |
| }); |
| return { |
| stepEvents, |
| get runOutcome() { |
| return runOutcome; |
| }, |
| get runUsed() { |
| return runUsed; |
| }, |
| }; |
| } |
|
|
| function reset() { |
| _setInnerLoopPersistenceHooks({ step: null, runStart: null, runEnd: null }); |
| } |
|
|
| function passthrough(name: string, role: string | null = null, usesLlm = false): StepDecl { |
| return { |
| name: name as StepDecl["name"], |
| usesLlm, |
| role, |
| fn: async () => ({ ok: true, step: name }), |
| }; |
| } |
|
|
| test("runInnerLoop — happy path: 8 steps all pass, outcome=completed", async () => { |
| const cap = captureHooks(); |
| try { |
| const steps: StepDecl[] = INNER_LOOP_STEPS.map((s) => passthrough(s)); |
| const r = await runInnerLoop({ capabilityId: "cap_a", steps }); |
| assert.equal(r.outcome, "completed"); |
| assert.equal(r.steps.length, 8); |
| assert.ok(r.steps.every((e) => e.status === "ok")); |
| assert.equal(cap.runOutcome, "completed"); |
| } finally { |
| reset(); |
| } |
| }); |
|
|
| test("runInnerLoop — skipSteps: code is marked skipped, outcome still completed", async () => { |
| const cap = captureHooks(); |
| try { |
| const steps: StepDecl[] = INNER_LOOP_STEPS.map((s) => { |
| const d = passthrough(s); |
| if (s === "code") d.skip = true; |
| return d; |
| }); |
| const r = await runInnerLoop({ capabilityId: "cap_a", steps }); |
| assert.equal(r.outcome, "completed"); |
| const code = r.steps.find((e) => e.stepName === "code"); |
| assert.ok(code); |
| assert.equal(code!.status, "skipped"); |
| assert.equal(cap.runOutcome, "completed"); |
| } finally { |
| reset(); |
| } |
| }); |
|
|
| test("runInnerLoop — step not declared in manifest is recorded as skipped", async () => { |
| captureHooks(); |
| try { |
| const steps: StepDecl[] = INNER_LOOP_STEPS.filter((s) => s !== "search").map( |
| (s) => passthrough(s), |
| ); |
| const r = await runInnerLoop({ capabilityId: "cap_a", steps }); |
| const search = r.steps.find((e) => e.stepName === "search"); |
| assert.ok(search); |
| assert.equal(search!.status, "skipped"); |
| assert.equal(search!.errorMessage, "step not declared in manifest"); |
| assert.equal(r.outcome, "completed"); |
| } finally { |
| reset(); |
| } |
| }); |
|
|
| test("runInnerLoop — LLM error + fallback: status=fallback, used_llm=false", async () => { |
| captureHooks(); |
| try { |
| const steps: StepDecl[] = INNER_LOOP_STEPS.map((s) => { |
| if (s === "diagnose") { |
| return { |
| name: "diagnose", |
| usesLlm: true, |
| role: "diagnoser", |
| fn: async () => { |
| throw new LlmRoleDisabled(); |
| }, |
| fallback: async () => ({ heuristic: "ok" }), |
| }; |
| } |
| return passthrough(s); |
| }); |
| const r = await runInnerLoop({ capabilityId: "cap_a", steps }); |
| const diag = r.steps.find((e) => e.stepName === "diagnose"); |
| assert.ok(diag); |
| assert.equal(diag!.status, "fallback"); |
| assert.equal(r.usedLlmPerStep.diagnose, false); |
| assert.equal(r.outcome, "completed"); |
| } finally { |
| reset(); |
| } |
| }); |
|
|
| test("runInnerLoop — LLM error + no fallback: outcome=aborted", async () => { |
| captureHooks(); |
| try { |
| const steps: StepDecl[] = INNER_LOOP_STEPS.map((s) => { |
| if (s === "judge") { |
| return { |
| name: "judge", |
| usesLlm: true, |
| role: "judge", |
| fn: async () => { |
| throw new LlmRoleResponseInvalid("judge", null, ""); |
| }, |
| |
| }; |
| } |
| return passthrough(s); |
| }); |
| const r = await runInnerLoop({ capabilityId: "cap_a", steps }); |
| assert.equal(r.outcome, "aborted"); |
| const judge = r.steps.find((e) => e.stepName === "judge"); |
| assert.equal(judge!.status, "error"); |
| } finally { |
| reset(); |
| } |
| }); |
|
|
| test("runInnerLoop — non-LLM error: outcome=failed (no fallback attempted)", async () => { |
| captureHooks(); |
| try { |
| let fallbackCalled = false; |
| const steps: StepDecl[] = INNER_LOOP_STEPS.map((s) => { |
| if (s === "act") { |
| return { |
| name: "act", |
| usesLlm: false, |
| role: null, |
| fn: async () => { |
| throw new Error("system blew up"); |
| }, |
| fallback: async () => { |
| fallbackCalled = true; |
| return {}; |
| }, |
| }; |
| } |
| return passthrough(s); |
| }); |
| const r = await runInnerLoop({ capabilityId: "cap_a", steps }); |
| assert.equal(r.outcome, "failed"); |
| assert.equal(fallbackCalled, false, "fallback should not run for non-LLM errors"); |
| } finally { |
| reset(); |
| } |
| }); |
|
|
| test("runInnerLoop — usedLlmPerStep mirrors mixed LLM+system declarations", async () => { |
| captureHooks(); |
| try { |
| const llmSteps = new Set(["diagnose", "plan", "judge"]); |
| const steps: StepDecl[] = INNER_LOOP_STEPS.map((s) => |
| passthrough(s, llmSteps.has(s) ? s : null, llmSteps.has(s)), |
| ); |
| const r = await runInnerLoop({ capabilityId: "cap_a", steps }); |
| assert.equal(r.usedLlmPerStep.diagnose, true); |
| assert.equal(r.usedLlmPerStep.plan, true); |
| assert.equal(r.usedLlmPerStep.judge, true); |
| assert.equal(r.usedLlmPerStep.observe, false); |
| assert.equal(r.usedLlmPerStep.code, false); |
| } finally { |
| reset(); |
| } |
| }); |
|
|
| test("runInnerLoop — step events have stable order and payload_hash", async () => { |
| const cap = captureHooks(); |
| try { |
| const steps: StepDecl[] = INNER_LOOP_STEPS.map((s) => passthrough(s)); |
| const r = await runInnerLoop({ capabilityId: "cap_a", steps }); |
| |
| for (let i = 0; i < r.steps.length; i++) { |
| assert.equal(r.steps[i]!.stepIndex, i); |
| assert.equal(r.steps[i]!.stepName, INNER_LOOP_STEPS[i]); |
| assert.ok(r.steps[i]!.payloadHash, "ok step should have payload_hash"); |
| } |
| |
| for (let i = 0; i < cap.stepEvents.length; i++) { |
| assert.equal(cap.stepEvents[i]!.stepIndex, i); |
| } |
| } finally { |
| reset(); |
| } |
| }); |
|
|