Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6382,6 +6382,7 @@ const onboardRuntimeBoundary = new OnboardRuntimeBoundary({
maybeForceE2eStepFailure,
});

const recordOnboardStarted = onboardRuntimeBoundary.recordOnboardStarted.bind(onboardRuntimeBoundary);
const startRecordedStep = onboardRuntimeBoundary.startRecordedStep.bind(onboardRuntimeBoundary);
const recordStepComplete = onboardRuntimeBoundary.recordStepComplete.bind(onboardRuntimeBoundary);
const recordStepSkipped = onboardRuntimeBoundary.recordStepSkipped.bind(onboardRuntimeBoundary);
Expand Down Expand Up @@ -6675,6 +6676,8 @@ async function onboard(opts: OnboardOptions = {}): Promise<void> {
);
}

await recordOnboardStarted(resume);

// Backstop for the resume path: a session may exist (so the early guard
// skipped because resume === true) but never have recorded a sandboxName
// — sandbox creation could have failed before that step ran. Without a
Expand Down
94 changes: 94 additions & 0 deletions src/lib/onboard/runtime-boundary.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it } from "vitest";

import {
createSession,
filterSafeUpdates,
normalizeSession,
type Session,
type SessionUpdates,
} from "../state/onboard-session";
import type { OnboardMachineEvent } from "./machine/events";
import { OnboardRuntime, type OnboardRuntimeDeps } from "./machine/runtime";
import { OnboardRuntimeBoundary } from "./runtime-boundary";

function cloneSession(session: Session): Session {
return normalizeSession(JSON.parse(JSON.stringify(session))) ?? session;
}

function createRuntimeHarness() {
let session: Session | null = createSession();
const events: OnboardMachineEvent[] = [];
const updateSession = (mutator: (value: Session) => Session | void): Session => {
const current = session ? cloneSession(session) : createSession();
session = cloneSession(mutator(current) ?? current);
return cloneSession(session);
};
const deps: OnboardRuntimeDeps = {
loadSession: () => (session ? cloneSession(session) : null),
createSession,
saveSession: (next) => {
session = cloneSession(next);
return cloneSession(session);
},
updateSession,
markStepStarted: (stepName) =>
updateSession((current) => {
current.steps[stepName].status = "in_progress";
return current;
}),
markStepComplete: (stepName, updates: SessionUpdates = {}) =>
updateSession((current) => {
current.steps[stepName].status = "complete";
Object.assign(current, filterSafeUpdates(updates));
return current;
}),
markStepSkipped: (stepName) =>
updateSession((current) => {
current.steps[stepName].status = "skipped";
return current;
}),
markStepFailed: (stepName, message) =>
updateSession((current) => {
current.steps[stepName].status = "failed";
current.failure = { step: stepName, message: message ?? null, recordedAt: "now" };
return current;
}),
completeSession: (updates: SessionUpdates = {}) =>
updateSession((current) => {
Object.assign(current, filterSafeUpdates(updates));
current.status = "complete";
return current;
}),
filterSafeUpdates,
emitEvent: (event) => events.push(event),
now: () => "2026-05-27T00:00:00.000Z",
};
return {
createRuntime: () => new OnboardRuntime(deps),
events,
};
}

describe("OnboardRuntimeBoundary", () => {
it("records started and resumed lifecycle events through the runtime", async () => {
const harness = createRuntimeHarness();
const boundary = new OnboardRuntimeBoundary({
toSessionUpdates: (updates) => filterSafeUpdates(updates as SessionUpdates) as SessionUpdates,
maybeForceE2eStepFailure: () => undefined,
createRuntime: harness.createRuntime,
});

await boundary.recordOnboardStarted(false);
await boundary.recordOnboardStarted(true);

expect(harness.events.map((event) => event.type)).toEqual([
"onboard.started",
"onboard.resumed",
]);
expect(harness.events[0]).toMatchObject({ state: "init" });
expect(harness.events[1]).toMatchObject({ state: "init" });
});
});
10 changes: 8 additions & 2 deletions src/lib/onboard/runtime-boundary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { OnboardMachineEventType, OnboardMachineState } from "./machine/typ
export interface OnboardRuntimeBoundaryOptions {
toSessionUpdates(updates: Record<string, unknown>): SessionUpdates;
maybeForceE2eStepFailure(stepName: string): void;
createRuntime?(): OnboardRuntime;
}

export class OnboardRuntimeBoundary {
Expand All @@ -16,20 +17,21 @@ export class OnboardRuntimeBoundary {
constructor(private readonly options: OnboardRuntimeBoundaryOptions) {}

reset(): void {
this.runtime = new OnboardRuntime();
this.runtime = this.options.createRuntime?.() ?? new OnboardRuntime();
}

clear(): void {
this.runtime = null;
}

getRuntime(): OnboardRuntime {
if (!this.runtime) this.runtime = new OnboardRuntime();
if (!this.runtime) this.runtime = this.options.createRuntime?.() ?? new OnboardRuntime();
return this.runtime;
}

recorders() {
return {
recordOnboardStarted: this.recordOnboardStarted.bind(this),
startRecordedStep: this.startRecordedStep.bind(this),
recordStepComplete: this.recordStepComplete.bind(this),
recordStepSkipped: this.recordStepSkipped.bind(this),
Expand All @@ -41,6 +43,10 @@ export class OnboardRuntimeBoundary {
};
}

async recordOnboardStarted(resumed: boolean): Promise<Session> {
return this.getRuntime().start({ resumed });
}

async startRecordedStep(
stepName: string,
updates: {
Expand Down
Loading