diff --git a/ci/source-architecture-budget.json b/ci/source-architecture-budget.json index 80eaf4a895e..b5c321f1036 100644 --- a/ci/source-architecture-budget.json +++ b/ci/source-architecture-budget.json @@ -47,7 +47,7 @@ "src/lib/actions/uninstall/run-plan.ts": 26, "src/lib/inference/onboard-probes.ts": 20, "src/lib/inference/vllm.ts": 21, - "src/lib/onboard.ts": 219, + "src/lib/onboard.ts": 212, "src/lib/onboard/machine/handlers/sandbox.ts": 21, "src/lib/sandbox/config.ts": 22, "src/lib/shields/index.ts": 23 diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 83370b936d9..c6108403c90 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -127,9 +127,6 @@ const { isLinuxDockerDriverGatewayEnabled } = dockerDriverPlatform; const { reconcileGatewayGpuReuseForGpuIntent, }: typeof import("./onboard/gateway-gpu-passthrough") = require("./onboard/gateway-gpu-passthrough"); -const { - syncPresetSelection, -}: typeof import("./onboard/policy-preset-sync") = require("./onboard/policy-preset-sync"); const { maybeForceE2eStepFailure, }: typeof import("./onboard/e2e-failure-injection") = require("./onboard/e2e-failure-injection"); @@ -495,10 +492,7 @@ const { }: typeof import("./onboard/machine/initial-flow-composition") = require("./onboard/machine/initial-flow-composition"); const { skippedStepMessage }: typeof import("./onboard/skipped-step-message") = require("./onboard/skipped-step-message"); -const policies: typeof import("./policy") = require("./policy"); const policyPresetCarry: typeof import("./onboard/policy-preset-persistence") = require("./onboard/policy-preset-persistence"); -const tiers: typeof import("./policy/tiers") = require("./policy/tiers"); -const policyTierEnv: typeof import("./onboard/policy-tier-env") = require("./onboard/policy-tier-env"); const { ensureUsageNoticeConsent } = require("./onboard/usage-notice"); const { findAvailableDashboardPort, @@ -589,7 +583,6 @@ import { setupHermesToolGateways, stringSetsEqual, } from "./onboard/hermes-managed-tools"; -import { mergePolicyMessagingChannels } from "./onboard/messaging-policy-presets"; import { filterEnabledChannelsByAgent } from "./onboard/messaging-state"; import { getValidatedMessagingTokenByEnvKey } from "./onboard/messaging-token"; import * as ollamaFlow from "./onboard/ollama-probe-failure"; @@ -600,15 +593,7 @@ import type { OpenShellInstallDeps, OpenShellInstallResult, } from "./onboard/openshell-install"; -import { getSuggestedPolicyPresets } from "./onboard/policy-presets"; -import { - computeSetupPresetSuggestions as computeSetupPresetSuggestionsImpl, - preparePolicyPresetResumeSelection, - type SetupPolicySelectionOptions, - type SetupPresetSuggestionOptions, - setupPoliciesWithSelection as setupPoliciesWithSelectionImpl, -} from "./onboard/policy-selection"; -import { createPolicySelectionPromptHelpers } from "./onboard/policy-selection-prompts"; +import { createOnboardPolicyApplication } from "./onboard/policy-selection"; import { printLowMemoryWarning, printMessagingProviderMissing, @@ -3682,89 +3667,6 @@ const setupOpenclaw = createOpenclawSetup({ cleanupTempDir, }); -// ── Step 7: Policy presets ─────────────────────────────────────── - -function arePolicyPresetsApplied(sandboxName: string, selectedPresets: string[] = []): boolean { - if (!Array.isArray(selectedPresets) || selectedPresets.length === 0) return false; - const applied = new Set(policies.getAppliedPresets(sandboxName)); - return selectedPresets.every((preset) => applied.has(preset)); -} - -function getPolicySelectionPromptHelpers(): ReturnType { - return createPolicySelectionPromptHelpers({ - tiers, - policyTierEnv, - isNonInteractive, - note, - prompt, - selectFromNumberedMenuOrExit, - makeOnboardCancelExit, - sandboxCancelRollback, - useColor: USE_COLOR, - }); -} - -async function selectPolicyTier(): Promise { - return getPolicySelectionPromptHelpers().selectPolicyTier(); -} - -async function selectTierPresetsAndAccess( - tierName: string, - allPresets: Array<{ name: string; description?: string }>, - initialSelected?: string[], -): Promise> { - return getPolicySelectionPromptHelpers().selectTierPresetsAndAccess( - tierName, - allPresets, - initialSelected, - ); -} - -async function presetsCheckboxSelector( - allPresets: Array<{ name: string; description: string }>, - initialSelected: string[], -): Promise { - return getPolicySelectionPromptHelpers().presetsCheckboxSelector(allPresets, initialSelected); -} - -const computeSetupPresetSuggestions = ( - tierName: string, - options: SetupPresetSuggestionOptions = {}, -): string[] => - computeSetupPresetSuggestionsImpl( - { policies, tiers, localInferenceProviders: [...LOCAL_INFERENCE_PROVIDERS, "llama-cpp-local"] }, - tierName, - options, - ); -async function setupPoliciesWithSelection( - sandboxName: string, - options: SetupPolicySelectionOptions = {}, -) { - return sandboxMutationLock.withSandboxMutationLock(sandboxName, () => - setupPoliciesWithSelectionImpl( - { - policies, - tiers, - localInferenceProviders: [...LOCAL_INFERENCE_PROVIDERS, "llama-cpp-local"], - step, - note, - isNonInteractive, - waitForSandboxReady, - waitForSandboxControlPlaneReady: finalizationHandlerDeps.waitForSandboxControlPlaneReady, - syncPresetSelection, - selectPolicyTier, - setPolicyTier: (s, t) => registry.updateSandbox(s, { policyTier: t }), - getRecordedPolicyTier: (s) => registry.getSandbox(s)?.policyTier ?? null, - selectTierPresetsAndAccess, - parsePolicyPresetEnv, - env: process.env, - }, - sandboxName, - options, - ), - ); -} - const { buildChain, buildControlUiUrls, @@ -3804,6 +3706,39 @@ const sandboxCancelRollback = installSandboxCancelRollback({ clearOnboardSession: onboardSession.clearSession, }); // #4614 +const { + arePolicyPresetsApplied, + computeSetupPresetSuggestions, + filterSetupPolicyPresets, + getSuggestedPolicyPresets, + mergePolicyMessagingChannels, + preparePolicyPresetResumeSelection, + presetsCheckboxSelector, + resolveSandboxBaselinePolicy, + selectPolicyTier, + selectTierPresetsAndAccess, + setupPoliciesWithSelection, + validatePolicyTierEnvEarly, +} = createOnboardPolicyApplication({ + localInferenceProviders: [...LOCAL_INFERENCE_PROVIDERS, "llama-cpp-local"], + step, + note, + isNonInteractive, + prompt, + selectFromNumberedMenuOrExit, + makeOnboardCancelExit, + sandboxCancelRollback, + useColor: USE_COLOR, + withSandboxMutationLock: sandboxMutationLock.withSandboxMutationLock, + waitForSandboxReady, + waitForSandboxControlPlaneReady: finalizationHandlerDeps.waitForSandboxControlPlaneReady, + setPolicyTier: (sandboxName, tierName) => + registry.updateSandbox(sandboxName, { policyTier: tierName }), + getRecordedPolicyTier: (sandboxName) => registry.getSandbox(sandboxName)?.policyTier ?? null, + parsePolicyPresetEnv, + env: process.env, +}); + const startRecordedStep = onboardRuntimeBoundary.startRecordedStep.bind(onboardRuntimeBoundary); const recordStepComplete = onboardRuntimeBoundary.recordStepComplete.bind(onboardRuntimeBoundary); const recordStepSkipped = onboardRuntimeBoundary.recordStepSkipped.bind(onboardRuntimeBoundary); @@ -3834,7 +3769,7 @@ async function preflightAuthoritativeRebuildTarget( await authoritativeRebuildTarget.preflightAuthoritativeRebuildTarget( { ...opts, controlUiPort: opts.controlUiPort ?? null }, { - resolveBaselinePolicy: (sandboxName) => policies.resolveSandboxBaselinePolicy(sandboxName), + resolveBaselinePolicy: resolveSandboxBaselinePolicy, runFatalRuntimePreflight: () => fatalRuntimePreflight.runFatalOnboardRuntimePreflight( { @@ -3914,7 +3849,7 @@ async function runOnboard(opts: OnboardOptions = {}): Promise { initialPreResolvedMetadata: opts.preResolvedBaseImageMetadata, }); const onboardingComputePlan = dockerDriverPlatform.resolveCurrentOpenShellComputePlan(); - if (isNonInteractive()) policyTierEnv.validatePolicyTierEnvEarly(); + if (isNonInteractive()) validatePolicyTierEnvEarly(); const noticeAccepted = await ensureUsageNoticeConsent({ nonInteractive: isNonInteractive(), acceptedByFlag: opts.acceptThirdPartySoftware === true, @@ -4445,8 +4380,7 @@ async function runOnboard(opts: OnboardOptions = {}): Promise { mergePolicyMessagingChannels, // biome-ignore format: keep src/lib/onboard.ts net-neutral for growth guardrail. verifyCompatibleEndpointSandboxSmoke: (options) => verifyCompatibleEndpointSandboxSmoke({ ...options, runOpenshell: runCoreGatewayOpenshell, redact }), - preparePolicyPresetResumeSelection: (name, options) => - preparePolicyPresetResumeSelection({ policies }, name, options), + preparePolicyPresetResumeSelection, arePolicyPresetsApplied, skippedStepMessage, recordStateSkipped, @@ -4655,7 +4589,7 @@ module.exports = { getSuggestedPolicyPresets, computeSetupPresetSuggestions, mergeRequiredHermesToolGatewayPolicyPresets, - filterSetupPolicyPresets: policies.filterSetupPolicyPresets, + filterSetupPolicyPresets, LOCAL_INFERENCE_PROVIDERS, presetsCheckboxSelector, selectPolicyTier, diff --git a/src/lib/onboard/policy-selection-application.test.ts b/src/lib/onboard/policy-selection-application.test.ts new file mode 100644 index 00000000000..6e63e94722f --- /dev/null +++ b/src/lib/onboard/policy-selection-application.test.ts @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it, vi } from "vitest"; +import { + createOnboardPolicyApplication, + type OnboardPolicyApplicationDeps, +} from "./policy-selection"; +import { selectFromNumberedMenuOrExit } from "./prompt-helpers"; + +const { seedInitialPolicyContext, syncPresetSelection } = vi.hoisted(() => ({ + seedInitialPolicyContext: vi.fn(), + syncPresetSelection: vi.fn(), +})); + +vi.mock("../policy", () => ({ + clampSetupPolicyPresetNames: vi.fn((names: string[]) => names), + customPresetOwnsNetworkPolicyKey: vi.fn(() => false), + filterSetupPolicyPresets: vi.fn(), + getAppliedPresets: vi.fn(() => []), + listCustomPresets: vi.fn(() => []), + listSetupPolicyPresets: vi.fn(() => [{ name: "npm" }]), + resolveSandboxBaselinePolicy: vi.fn(), + setupPolicyPresetSupported: vi.fn(() => true), +})); +vi.mock("./policy-context-seed", () => ({ seedInitialPolicyContext })); +vi.mock("./policy-preset-sync", () => ({ syncPresetSelection })); + +describe("onboarding policy application", () => { + it("runs policy application while holding the sandbox mutation lock", async () => { + const events: string[] = []; + const withSandboxMutationLock: OnboardPolicyApplicationDeps["withSandboxMutationLock"] = vi.fn( + async (_sandboxName, action) => { + events.push("lock entered"); + try { + return await action(); + } finally { + events.push("lock released"); + } + }, + ); + syncPresetSelection.mockImplementation(() => events.push("policies synchronized")); + seedInitialPolicyContext.mockImplementation(() => events.push("policy context seeded")); + const application = createOnboardPolicyApplication({ + localInferenceProviders: [], + step: vi.fn(), + note: vi.fn(), + isNonInteractive: vi.fn(() => true), + prompt: vi.fn(async () => ""), + selectFromNumberedMenuOrExit, + makeOnboardCancelExit: (rollback, cleanup) => () => { + cleanup(); + rollback.markCancelled(); + }, + sandboxCancelRollback: { markCancelled: vi.fn() }, + useColor: false, + withSandboxMutationLock, + waitForSandboxReady: vi.fn(() => true), + waitForSandboxControlPlaneReady: vi.fn(() => true), + setPolicyTier: vi.fn(), + getRecordedPolicyTier: vi.fn(() => null), + parsePolicyPresetEnv: vi.fn(() => []), + env: {}, + }); + + await expect( + application.setupPoliciesWithSelection("alpha", { selectedPresets: ["npm"] }), + ).resolves.toEqual(["npm"]); + expect(withSandboxMutationLock).toHaveBeenCalledOnce(); + expect(withSandboxMutationLock).toHaveBeenCalledWith("alpha", expect.any(Function)); + expect(syncPresetSelection).toHaveBeenCalledWith("alpha", [], ["npm"]); + expect(events).toEqual([ + "lock entered", + "policies synchronized", + "policy context seeded", + "lock released", + ]); + }); +}); diff --git a/src/lib/onboard/policy-selection.ts b/src/lib/onboard/policy-selection.ts index 505b7030425..57103f7fdb2 100644 --- a/src/lib/onboard/policy-selection.ts +++ b/src/lib/onboard/policy-selection.ts @@ -2,7 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 import { type WebSearchConfig, webSearchProviderForConfig } from "../inference/web-search"; +import * as policies from "../policy"; import { PERSONAL_POLICY_TIER_NAME } from "../policy/tiers"; +import * as tiers from "../policy/tiers"; import { filterSetupPolicyPresetNamesForAgent, filterSetupPolicyPresetsForAgent, @@ -12,7 +14,10 @@ import { allHermesToolGatewayPolicyPresets, HERMES_TOOL_GATEWAY_PRESET_NAMES, } from "./hermes-managed-tools"; -import { allMessagingChannelPolicyPresets } from "./messaging-policy-presets"; +import { + allMessagingChannelPolicyPresets, + mergePolicyMessagingChannels, +} from "./messaging-policy-presets"; import { isInactiveObservabilityPolicyPreset, OBSERVABILITY_OTLP_LOCAL_POLICY_PRESET, @@ -24,6 +29,17 @@ import { isStaleBuiltinWebSearchPolicyPreset, mergeRequiredSetupPolicyPresets, } from "./policy-preset-reconciliation"; +import { syncPresetSelection } from "./policy-preset-sync"; +import { getSuggestedPolicyPresets } from "./policy-presets"; +import { + type PreparedPolicyResumeSelection, + preparePolicyPresetResumeSelection, +} from "./policy-resume-selection"; +import { + createPolicySelectionPromptHelpers, + type PolicySelectionPromptDeps, +} from "./policy-selection-prompts"; +import * as policyTierEnv from "./policy-tier-env"; import { agentRequiredPresetAdditions, emitSuppressedAgentRequiredPresetsNote, @@ -38,6 +54,21 @@ export { } from "./policy-preset-reconciliation"; export { suppressedAgentRequiredPresets } from "./policy-tier-suppression"; +export type OnboardPolicyApplicationDeps = Omit< + PolicySelectionPromptDeps, + "tiers" | "policyTierEnv" +> & { + step: (number: number, total: number, title: string) => void; + localInferenceProviders: readonly string[]; + withSandboxMutationLock: typeof import("../state/mcp-lifecycle-lock").withSandboxMutationLock; + waitForSandboxReady(sandboxName: string): boolean; + waitForSandboxControlPlaneReady(sandboxName: string): boolean; + setPolicyTier(sandboxName: string, tierName: string): void; + getRecordedPolicyTier(sandboxName: string): string | null | undefined; + parsePolicyPresetEnv(raw: string): string[]; + env: NodeJS.ProcessEnv; +}; + type Preset = { name: string; access?: string }; type SupportOptions = { webSearchSupported?: boolean | null; agent?: string | null }; type PoliciesApi = { @@ -116,6 +147,86 @@ export type SetupPolicySelectionDeps = { env?: NodeJS.ProcessEnv; }; +export function createOnboardPolicyApplication(deps: OnboardPolicyApplicationDeps) { + const promptHelpers = () => + createPolicySelectionPromptHelpers({ + ...deps, + tiers, + policyTierEnv, + }); + const selectPolicyTier = () => promptHelpers().selectPolicyTier(); + const selectTierPresetsAndAccess = ( + tierName: string, + allPresets: Array<{ name: string; description?: string }>, + initialSelected?: string[], + ) => promptHelpers().selectTierPresetsAndAccess(tierName, allPresets, initialSelected); + const presetsCheckboxSelector = ( + allPresets: Array<{ name: string; description: string }>, + initialSelected: string[], + ) => promptHelpers().presetsCheckboxSelector(allPresets, initialSelected); + const setupDeps: SetupPolicySelectionDeps = { + policies, + tiers, + localInferenceProviders: deps.localInferenceProviders, + step: deps.step, + note: deps.note, + isNonInteractive: deps.isNonInteractive, + waitForSandboxReady: deps.waitForSandboxReady, + waitForSandboxControlPlaneReady: deps.waitForSandboxControlPlaneReady, + syncPresetSelection, + selectPolicyTier, + setPolicyTier: deps.setPolicyTier, + getRecordedPolicyTier: deps.getRecordedPolicyTier, + selectTierPresetsAndAccess, + parsePolicyPresetEnv: deps.parsePolicyPresetEnv, + env: deps.env, + }; + + return { + arePolicyPresetsApplied(sandboxName: string, selectedPresets: string[] = []): boolean { + if (!Array.isArray(selectedPresets) || selectedPresets.length === 0) return false; + const applied = new Set(policies.getAppliedPresets(sandboxName)); + return selectedPresets.every((preset) => applied.has(preset)); + }, + computeSetupPresetSuggestions( + tierName: string, + options: SetupPresetSuggestionOptions = {}, + ): string[] { + return computeSetupPresetSuggestions( + { + policies, + tiers, + localInferenceProviders: deps.localInferenceProviders, + }, + tierName, + options, + ); + }, + filterSetupPolicyPresets: policies.filterSetupPolicyPresets, + getSuggestedPolicyPresets, + mergePolicyMessagingChannels, + preparePolicyPresetResumeSelection( + sandboxName: string, + options: Parameters[2], + ): PreparedPolicyResumeSelection { + return preparePolicyPresetResumeSelection({ policies }, sandboxName, options); + }, + presetsCheckboxSelector, + resolveSandboxBaselinePolicy: policies.resolveSandboxBaselinePolicy, + selectPolicyTier, + selectTierPresetsAndAccess, + setupPoliciesWithSelection( + sandboxName: string, + options: SetupPolicySelectionOptions = {}, + ): Promise { + return deps.withSandboxMutationLock(sandboxName, () => + setupPoliciesWithSelection(setupDeps, sandboxName, options), + ); + }, + validatePolicyTierEnvEarly: policyTierEnv.validatePolicyTierEnvEarly, + }; +} + export function computeSetupPresetSuggestions( deps: { policies: PoliciesApi; @@ -213,10 +324,7 @@ export function computeSetupPresetSuggestions( return suggestions; } -export { - type PreparedPolicyResumeSelection, - preparePolicyPresetResumeSelection, -} from "./policy-resume-selection"; +export { type PreparedPolicyResumeSelection, preparePolicyPresetResumeSelection }; export async function setupPoliciesWithSelection( deps: SetupPolicySelectionDeps, diff --git a/test/onboard-policy-application-wiring.test.ts b/test/onboard-policy-application-wiring.test.ts new file mode 100644 index 00000000000..f9a7c66fdba --- /dev/null +++ b/test/onboard-policy-application-wiring.test.ts @@ -0,0 +1,157 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import assert from "node:assert/strict"; + +import { createRequire } from "node:module"; +import { describe, expect, it, vi } from "vitest"; +import type { OnboardPolicyApplicationDeps } from "../src/lib/onboard/policy-selection.js"; + +const require = createRequire(import.meta.url); + +type PolicySelectionModule = typeof import("../src/lib/onboard/policy-selection.js"); +type PolicyApplication = ReturnType; + +function replaceCachedExports(modulePath: string, exports: unknown): void { + const cached = require.cache[modulePath]; + assert.ok(cached, `Expected ${modulePath} to be loaded`); + cached.exports = exports; +} + +function restoreRequireCache(prior: Map): void { + const addedModulePaths = Object.keys(require.cache).filter( + (modulePath) => !prior.has(modulePath), + ); + for (const modulePath of addedModulePaths) delete require.cache[modulePath]; + for (const [modulePath, cached] of prior) require.cache[modulePath] = cached; +} + +describe("onboarding policy application production wiring", () => { + it("wires resume policy application to the sandbox registry, readiness checks, and sandbox mutation lock (#7695)", async () => { + const priorCache = new Map( + Object.entries(require.cache).filter( + (entry): entry is [string, NodeModule] => entry[1] !== undefined, + ), + ); + const events: string[] = []; + const getSandbox = vi.fn(() => { + events.push("registry tier read"); + return { policyTier: "restricted" }; + }); + const updateSandbox = vi.fn(); + const waitForSandboxReady = vi.fn(() => { + events.push("sandbox ready"); + return true; + }); + const waitForSandboxControlPlaneReady = vi.fn(() => { + events.push("control plane ready"); + return true; + }); + const withSandboxMutationLock: OnboardPolicyApplicationDeps["withSandboxMutationLock"] = vi.fn( + async (_sandboxName, action) => { + events.push("lock entered"); + try { + return await action(); + } finally { + events.push("lock released"); + } + }, + ); + const syncPresetSelection = vi.fn(() => events.push("policies synchronized")); + const seedInitialPolicyContext = vi.fn(() => events.push("policy context seeded")); + let capturedDeps: OnboardPolicyApplicationDeps | undefined; + let application: PolicyApplication | undefined; + + const onboardPath = require.resolve("../src/lib/onboard.js"); + const policyPath = require.resolve("../src/lib/policy/index.js"); + const syncPath = require.resolve("../src/lib/onboard/policy-preset-sync.js"); + const seedPath = require.resolve("../src/lib/onboard/policy-context-seed.js"); + const policySelectionPath = require.resolve("../src/lib/onboard/policy-selection.js"); + const registryPath = require.resolve("../src/lib/state/registry.js"); + const lockPath = require.resolve("../src/lib/state/mcp-lifecycle-lock.js"); + const readinessPath = require.resolve("../src/lib/onboard/sandbox-readiness-tracing.js"); + const finalFlowPath = require.resolve("../src/lib/onboard/machine/final-flow-composition.js"); + + try { + const policy = require(policyPath) as Record; + replaceCachedExports(policyPath, { + ...policy, + clampSetupPolicyPresetNames: vi.fn((names: string[]) => [...names]), + customPresetOwnsNetworkPolicyKey: vi.fn(() => false), + getAppliedPresets: vi.fn(() => []), + listCustomPresets: vi.fn(() => []), + listSetupPolicyPresets: vi.fn(() => [{ name: "npm" }]), + setupPolicyPresetSupported: vi.fn(() => true), + }); + + require(syncPath); + replaceCachedExports(syncPath, { syncPresetSelection }); + require(seedPath); + replaceCachedExports(seedPath, { seedInitialPolicyContext }); + + delete require.cache[policySelectionPath]; + const policySelection = require(policySelectionPath) as PolicySelectionModule; + replaceCachedExports(policySelectionPath, { + ...policySelection, + createOnboardPolicyApplication: (deps: OnboardPolicyApplicationDeps) => { + capturedDeps = deps; + application = policySelection.createOnboardPolicyApplication(deps); + return application; + }, + }); + + const registry = require(registryPath) as Record; + replaceCachedExports(registryPath, { ...registry, getSandbox, updateSandbox }); + const lock = require(lockPath) as Record; + replaceCachedExports(lockPath, { ...lock, withSandboxMutationLock }); + const readiness = require(readinessPath) as Record; + replaceCachedExports(readinessPath, { + ...readiness, + createSandboxReadyWaiter: vi.fn(() => waitForSandboxReady), + }); + const finalFlow = require(finalFlowPath) as { + finalizationHandlerDeps: Record; + [key: string]: unknown; + }; + replaceCachedExports(finalFlowPath, { + ...finalFlow, + finalizationHandlerDeps: { + ...finalFlow.finalizationHandlerDeps, + waitForSandboxControlPlaneReady, + }, + }); + + delete require.cache[onboardPath]; + require(onboardPath); + + assert.ok(capturedDeps, "Expected onboard.ts to capture policy application dependencies"); + assert.ok(application, "Expected onboard.ts to create the policy application"); + expect(capturedDeps.withSandboxMutationLock).toBe(withSandboxMutationLock); + expect(capturedDeps.waitForSandboxReady).toBe(waitForSandboxReady); + expect(capturedDeps.waitForSandboxControlPlaneReady).toBe(waitForSandboxControlPlaneReady); + + capturedDeps.setPolicyTier("beta", "balanced"); + expect(updateSandbox).toHaveBeenCalledWith("beta", { policyTier: "balanced" }); + + await expect( + application.setupPoliciesWithSelection("alpha", { selectedPresets: ["npm"] }), + ).resolves.toEqual(["npm"]); + expect(getSandbox).toHaveBeenCalledWith("alpha"); + expect(waitForSandboxReady).toHaveBeenCalledTimes(2); + expect(waitForSandboxControlPlaneReady).toHaveBeenCalledOnce(); + expect(syncPresetSelection).toHaveBeenCalledWith("alpha", [], ["npm"]); + expect(events).toEqual([ + "lock entered", + "registry tier read", + "sandbox ready", + "policies synchronized", + "sandbox ready", + "control plane ready", + "policy context seeded", + "lock released", + ]); + } finally { + restoreRequireCache(priorCache); + } + }); +});