diff --git a/src/lib/onboard/machine/runtime.test.ts b/src/lib/onboard/machine/runtime.test.ts index d48da85e0a..512ee7f56b 100644 --- a/src/lib/onboard/machine/runtime.test.ts +++ b/src/lib/onboard/machine/runtime.test.ts @@ -12,6 +12,13 @@ import { type SessionUpdates, } from "../../state/onboard-session"; import type { OnboardMachineEvent } from "./events"; +import { + advanceTo, + branchTo, + completeOnboardMachine, + failOnboardMachine, + retryTo, +} from "./result"; import { OnboardRuntime, type OnboardRuntimeDeps } from "./runtime"; import { InvalidOnboardMachineTransitionError } from "./transitions"; @@ -159,6 +166,65 @@ describe("OnboardRuntime", () => { expect(JSON.stringify(events)).not.toContain("super-secret"); }); + it("applies explicit advance results through validated runtime transitions", async () => { + const { runtime, events, getSession } = createHarness(); + + await runtime.applyResult( + advanceTo("preflight", { + updates: { sandboxName: "my-assistant" }, + metadata: { source: "handler" }, + }), + ); + + expect(getSession()).toMatchObject({ + sandboxName: "my-assistant", + machine: { state: "preflight", revision: 1 }, + }); + expect(events.map((event) => event.type)).toEqual([ + "context.updated", + "state.exited", + "state.entered", + ]); + expect(events[0].metadata.fields).toEqual(["sandboxName"]); + expect(events[1]).toMatchObject({ state: "init", metadata: { source: "handler" } }); + expect(events[2]).toMatchObject({ state: "preflight", metadata: { source: "handler" } }); + }); + + it("applies explicit retry, branch, completion, and failure results", async () => { + const retryHarness = createHarness(sessionInState("inference")); + await retryHarness.runtime.applyResult(retryTo("provider_selection")); + expect(retryHarness.getSession().machine).toMatchObject({ state: "provider_selection" }); + + const branchHarness = createHarness(sessionInState("sandbox")); + await branchHarness.runtime.applyResult(branchTo("agent_setup")); + expect(branchHarness.getSession().machine).toMatchObject({ state: "agent_setup" }); + + const completeHarness = createHarness(sessionInState("post_verify")); + await completeHarness.runtime.applyResult(completeOnboardMachine({ sandboxName: "done" })); + expect(completeHarness.getSession()).toMatchObject({ + status: "complete", + sandboxName: "done", + machine: { state: "complete" }, + }); + + const failedHarness = createHarness(sessionInState("gateway")); + await failedHarness.runtime.applyResult(failOnboardMachine("boom", { step: "gateway" })); + expect(failedHarness.getSession()).toMatchObject({ + status: "failed", + failure: { step: "gateway", message: "boom" }, + machine: { state: "failed" }, + }); + }); + + it("rejects invalid explicit transition kinds before mutating context", async () => { + const { runtime, getSession } = createHarness(sessionInState("inference")); + + await expect( + runtime.applyResult(advanceTo("provider_selection", { updates: { sandboxName: "mutated" } })), + ).rejects.toThrow("expected advance, got retry"); + expect(getSession()).toMatchObject({ sandboxName: null, machine: { state: "inference" } }); + }); + it("fails non-terminal sessions with redacted failure events", async () => { const { runtime, events, getSession } = createHarness(sessionInState("gateway")); diff --git a/src/lib/onboard/machine/runtime.ts b/src/lib/onboard/machine/runtime.ts index 65516c3212..47cee9f0d2 100644 --- a/src/lib/onboard/machine/runtime.ts +++ b/src/lib/onboard/machine/runtime.ts @@ -9,6 +9,7 @@ import { emitOnboardMachineEvent, type OnboardMachineEvent, } from "./events"; +import type { OnboardStateResult } from "./result"; import { assertValidOnboardMachineTransition, canTransitionOnboardMachineState, @@ -197,6 +198,33 @@ export class OnboardRuntime { return updated; } + async applyResult(result: OnboardStateResult): Promise { + if (result.type === "complete") { + return this.complete(result.updates ?? {}); + } + if (result.type === "failed") { + return this.fail(result.error, { + step: result.step, + metadata: result.metadata, + }); + } + + const current = this.ensureSession(); + const transition = assertValidOnboardMachineTransition(current.machine.state, result.next); + if (result.transitionKind && transition.kind !== result.transitionKind) { + throw new Error( + `Invalid onboarding machine transition kind: ${current.machine.state} -> ${result.next} expected ${result.transitionKind}, got ${transition.kind}`, + ); + } + if (result.updates && Object.keys(this.deps.filterSafeUpdates(result.updates)).length > 0) { + await this.updateContext(result.updates, { + state: current.machine.state, + metadata: result.metadata, + }); + } + return this.transition(result.next, { metadata: result.metadata }); + } + async fail(message: string | null, options: OnboardRuntimeFailureOptions = {}): Promise { const current = this.ensureSession(); const from = current.machine.state;