From 80e6b12d00143f36a75892cd71cde1db1c1372be Mon Sep 17 00:00:00 2001 From: Tinson Lai Date: Fri, 7 Aug 2026 18:19:28 +0000 Subject: [PATCH 1/2] refactor(sandbox): give rebuild test harnesses one shared core Signed-off-by: Tinson Lai --- scripts/checks/test-create-require-budget.mts | 1 - ...rebuild-base-image-resolution-flow.test.ts | 2 +- .../rebuild-dcode-artifact-drift.test.ts | 10 +- .../rebuild-dcode-base-image-lease.test.ts | 10 +- .../rebuild-dcode-mutation-edge.test.ts | 10 +- .../rebuild-dcode-pre-delete-drift.test.ts | 10 +- .../sandbox/rebuild-dcode-preflight.test.ts | 10 +- .../rebuild-dcode-recovered-provider.test.ts | 10 +- .../sandbox/rebuild-dcode-recovery.test.ts | 10 +- .../rebuild-flow-credential-preflight.test.ts | 2 +- .../sandbox/rebuild-flow-lifecycle.test.ts | 2 +- .../sandbox/rebuild-flow-recovery.test.ts | 2 +- .../rebuild-flow-target-credentials.test.ts | 2 +- .../sandbox/rebuild-flow-target-image.test.ts | 2 +- .../rebuild-flow-target-session.test.ts | 2 +- .../rebuild-hermes-post-restore.test.ts | 10 +- .../rebuild-local-provider-recreate.test.ts | 10 +- .../sandbox/rebuild-prepared-recovery.test.ts | 22 +- test/helpers/rebuild-dcode-flow-helpers.ts | 2 +- test/helpers/rebuild-flow-dcode-harness.ts | 721 ++++++++++++++ ...ess.ts => rebuild-flow-generic-harness.ts} | 131 +-- test/helpers/rebuild-flow-harness.ts | 881 ++---------------- test/rebuild-stale-recovery.test.ts | 2 +- 23 files changed, 892 insertions(+), 972 deletions(-) create mode 100644 test/helpers/rebuild-flow-dcode-harness.ts rename test/helpers/{rebuild-flow-test-harness.ts => rebuild-flow-generic-harness.ts} (85%) diff --git a/scripts/checks/test-create-require-budget.mts b/scripts/checks/test-create-require-budget.mts index 5705d675503..cc8bf96205c 100644 --- a/scripts/checks/test-create-require-budget.mts +++ b/scripts/checks/test-create-require-budget.mts @@ -53,7 +53,6 @@ export const TEST_SUPPORT_CREATE_REQUIRE_FILES = [ "test/helpers/base-image-test-harness.ts", "test/helpers/destroy-flow-test-harness.ts", "test/helpers/rebuild-flow-harness.ts", - "test/helpers/rebuild-flow-test-harness.ts", "test/support/connect-flow-test-harness.ts", "test/support/status-flow-test-harness.ts", ] as const; diff --git a/src/lib/actions/sandbox/rebuild-base-image-resolution-flow.test.ts b/src/lib/actions/sandbox/rebuild-base-image-resolution-flow.test.ts index 5856d6ddadb..6b1eeefd77f 100644 --- a/src/lib/actions/sandbox/rebuild-base-image-resolution-flow.test.ts +++ b/src/lib/actions/sandbox/rebuild-base-image-resolution-flow.test.ts @@ -6,7 +6,7 @@ import { createRebuildFlowHarness, installRebuildFlowTestHooks, snapshotEnv, -} from "../../../../test/helpers/rebuild-flow-test-harness"; +} from "../../../../test/helpers/rebuild-flow-generic-harness"; import { SANDBOX_BASE_RESOLUTION_LABEL, type SandboxBaseImageResolutionMetadata, diff --git a/src/lib/actions/sandbox/rebuild-dcode-artifact-drift.test.ts b/src/lib/actions/sandbox/rebuild-dcode-artifact-drift.test.ts index 2f424256950..f6f9906a97c 100644 --- a/src/lib/actions/sandbox/rebuild-dcode-artifact-drift.test.ts +++ b/src/lib/actions/sandbox/rebuild-dcode-artifact-drift.test.ts @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { configureDcodeSession, makeDcodeSandboxEntry, @@ -9,14 +9,12 @@ import { import { expectNoSandboxDelete } from "../../../../test/helpers/rebuild-delete-assertions"; import { createRebuildFlowHarness, - resetRebuildFlowTestEnvironment, - restoreRebuildFlowTestEnvironment, + installRebuildFlowTestHooks, snapshotEnv, -} from "../../../../test/helpers/rebuild-flow-harness"; +} from "../../../../test/helpers/rebuild-flow-dcode-harness"; describe("rebuildSandbox DCode flow: prepared artifact drift", () => { - beforeEach(resetRebuildFlowTestEnvironment); - afterEach(restoreRebuildFlowTestEnvironment); + installRebuildFlowTestHooks({ acceptThirdPartySoftware: true }); it("preserves live DCode when retained replacement inputs drift after backup (#6195)", async () => { const harness = createRebuildFlowHarness({ diff --git a/src/lib/actions/sandbox/rebuild-dcode-base-image-lease.test.ts b/src/lib/actions/sandbox/rebuild-dcode-base-image-lease.test.ts index 47b70b437d5..955aec3f8fd 100644 --- a/src/lib/actions/sandbox/rebuild-dcode-base-image-lease.test.ts +++ b/src/lib/actions/sandbox/rebuild-dcode-base-image-lease.test.ts @@ -1,17 +1,16 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { configureDcodeSession, makeDcodeSandboxEntry, } from "../../../../test/helpers/rebuild-dcode-flow-helpers"; import { createRebuildFlowHarness, - resetRebuildFlowTestEnvironment, - restoreRebuildFlowTestEnvironment, + installRebuildFlowTestHooks, snapshotEnv, -} from "../../../../test/helpers/rebuild-flow-harness"; +} from "../../../../test/helpers/rebuild-flow-dcode-harness"; const overrideEnvName = "NEMOCLAW_LANGCHAIN_DEEPAGENTS_CODE_SANDBOX_BASE_IMAGE_REF"; const trustedLocalOverride = { @@ -21,8 +20,7 @@ const trustedLocalOverride = { const trustedRemoteRef = `ghcr.io/nvidia/nemoclaw/langchain-deepagents-code-sandbox-base@sha256:${"d".repeat(64)}`; describe("rebuildSandbox DCode flow: base-image trust lease", () => { - beforeEach(resetRebuildFlowTestEnvironment); - afterEach(restoreRebuildFlowTestEnvironment); + installRebuildFlowTestHooks({ acceptThirdPartySoftware: true }); it("keeps the current base-image trust lease active through replacement preparation (#6195)", async () => { const harness = createRebuildFlowHarness({ diff --git a/src/lib/actions/sandbox/rebuild-dcode-mutation-edge.test.ts b/src/lib/actions/sandbox/rebuild-dcode-mutation-edge.test.ts index 7d2e7f2a45b..a4d3c5f98fa 100644 --- a/src/lib/actions/sandbox/rebuild-dcode-mutation-edge.test.ts +++ b/src/lib/actions/sandbox/rebuild-dcode-mutation-edge.test.ts @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { configureDcodeSession, makeDcodeSandboxEntry, @@ -9,13 +9,11 @@ import { import { expectNoSandboxDelete } from "../../../../test/helpers/rebuild-delete-assertions"; import { createRebuildFlowHarness, - resetRebuildFlowTestEnvironment, - restoreRebuildFlowTestEnvironment, -} from "../../../../test/helpers/rebuild-flow-harness"; + installRebuildFlowTestHooks, +} from "../../../../test/helpers/rebuild-flow-dcode-harness"; describe("rebuildSandbox DCode flow: mutation edge", () => { - beforeEach(resetRebuildFlowTestEnvironment); - afterEach(restoreRebuildFlowTestEnvironment); + installRebuildFlowTestHooks({ acceptThirdPartySoftware: true }); it("finishes DCode preparation and recheck before backup, delete, and recreate (#6195)", async () => { const mcpEntry = { server: "search", providerName: "mcp-search" }; diff --git a/src/lib/actions/sandbox/rebuild-dcode-pre-delete-drift.test.ts b/src/lib/actions/sandbox/rebuild-dcode-pre-delete-drift.test.ts index f456f1fd2dc..5dffdc1549a 100644 --- a/src/lib/actions/sandbox/rebuild-dcode-pre-delete-drift.test.ts +++ b/src/lib/actions/sandbox/rebuild-dcode-pre-delete-drift.test.ts @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { configureDcodeSession, expectNoDcodeMutation, @@ -10,14 +10,12 @@ import { import { expectNoSandboxDelete } from "../../../../test/helpers/rebuild-delete-assertions"; import { createRebuildFlowHarness, - resetRebuildFlowTestEnvironment, - restoreRebuildFlowTestEnvironment, -} from "../../../../test/helpers/rebuild-flow-harness"; + installRebuildFlowTestHooks, +} from "../../../../test/helpers/rebuild-flow-dcode-harness"; import { revalidateDcodeReplacementAtMutationEdge } from "./rebuild-dcode-preflight"; describe("rebuildSandbox DCode flow: pre-delete drift", () => { - beforeEach(resetRebuildFlowTestEnvironment); - afterEach(restoreRebuildFlowTestEnvironment); + installRebuildFlowTestHooks({ acceptThirdPartySoftware: true }); it("rejects prepared-image tool-disclosure drift before gateway or mutation work", async () => { const checkGatewaySchema = vi.fn(() => true); diff --git a/src/lib/actions/sandbox/rebuild-dcode-preflight.test.ts b/src/lib/actions/sandbox/rebuild-dcode-preflight.test.ts index e49b04b0775..f7bc8f017d0 100644 --- a/src/lib/actions/sandbox/rebuild-dcode-preflight.test.ts +++ b/src/lib/actions/sandbox/rebuild-dcode-preflight.test.ts @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { configureDcodeSession, expectNoDcodeMutation, @@ -9,15 +9,13 @@ import { } from "../../../../test/helpers/rebuild-dcode-flow-helpers"; import { createRebuildFlowHarness, - resetRebuildFlowTestEnvironment, - restoreRebuildFlowTestEnvironment, + installRebuildFlowTestHooks, snapshotEnv, -} from "../../../../test/helpers/rebuild-flow-harness"; +} from "../../../../test/helpers/rebuild-flow-dcode-harness"; import { resolveRebuildDurableConfig } from "./rebuild-durable-config"; describe("rebuildSandbox DCode flow: preflight", () => { - beforeEach(resetRebuildFlowTestEnvironment); - afterEach(restoreRebuildFlowTestEnvironment); + installRebuildFlowTestHooks({ acceptThirdPartySoftware: true }); it.each([ ["defaults legacy state to disabled", undefined, undefined, "disabled", null], diff --git a/src/lib/actions/sandbox/rebuild-dcode-recovered-provider.test.ts b/src/lib/actions/sandbox/rebuild-dcode-recovered-provider.test.ts index 3747d9a56e2..72cba356e9b 100644 --- a/src/lib/actions/sandbox/rebuild-dcode-recovered-provider.test.ts +++ b/src/lib/actions/sandbox/rebuild-dcode-recovered-provider.test.ts @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { configureDcodeSession, @@ -11,15 +11,13 @@ import { } from "../../../../test/helpers/rebuild-dcode-flow-helpers"; import { createRebuildFlowHarness, + installRebuildFlowTestHooks, makePreparedRecoveryManifest, - resetRebuildFlowTestEnvironment, - restoreRebuildFlowTestEnvironment, snapshotEnv, -} from "../../../../test/helpers/rebuild-flow-harness"; +} from "../../../../test/helpers/rebuild-flow-dcode-harness"; describe("rebuildSandbox DCode recovered provider", () => { - beforeEach(resetRebuildFlowTestEnvironment); - afterEach(restoreRebuildFlowTestEnvironment); + installRebuildFlowTestHooks({ acceptThirdPartySoftware: true }); it("rejects incompatible keyless provider reuse after the live DCode route proof", async () => { const restoreEnv = snapshotEnv(["COMPATIBLE_API_KEY"]); diff --git a/src/lib/actions/sandbox/rebuild-dcode-recovery.test.ts b/src/lib/actions/sandbox/rebuild-dcode-recovery.test.ts index c0ef00b510f..a6a8312fd40 100644 --- a/src/lib/actions/sandbox/rebuild-dcode-recovery.test.ts +++ b/src/lib/actions/sandbox/rebuild-dcode-recovery.test.ts @@ -1,21 +1,19 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { configureDcodeSession, makeDcodeSandboxEntry, } from "../../../../test/helpers/rebuild-dcode-flow-helpers"; import { createRebuildFlowHarness, + installRebuildFlowTestHooks, makePreparedRecoveryManifest, - resetRebuildFlowTestEnvironment, - restoreRebuildFlowTestEnvironment, -} from "../../../../test/helpers/rebuild-flow-harness"; +} from "../../../../test/helpers/rebuild-flow-dcode-harness"; describe("rebuildSandbox DCode flow: recovery", () => { - beforeEach(resetRebuildFlowTestEnvironment); - afterEach(restoreRebuildFlowTestEnvironment); + installRebuildFlowTestHooks({ acceptThirdPartySoftware: true }); it("recreates non-Ready DCode from a validated backup without requiring a live route (#6195)", async () => { const recoveryManifest = { diff --git a/src/lib/actions/sandbox/rebuild-flow-credential-preflight.test.ts b/src/lib/actions/sandbox/rebuild-flow-credential-preflight.test.ts index f0d69555c12..5ea373bfa92 100644 --- a/src/lib/actions/sandbox/rebuild-flow-credential-preflight.test.ts +++ b/src/lib/actions/sandbox/rebuild-flow-credential-preflight.test.ts @@ -7,7 +7,7 @@ import { expectNoSandboxDelete } from "../../../../test/helpers/rebuild-delete-a import { createRebuildFlowHarness, installRebuildFlowTestHooks, -} from "../../../../test/helpers/rebuild-flow-test-harness"; +} from "../../../../test/helpers/rebuild-flow-generic-harness"; import { makePreparedRecoveryManifest } from "./rebuild-flow-test-fixtures"; type Harness = ReturnType; diff --git a/src/lib/actions/sandbox/rebuild-flow-lifecycle.test.ts b/src/lib/actions/sandbox/rebuild-flow-lifecycle.test.ts index 6f876c6250a..5b2158b1daf 100644 --- a/src/lib/actions/sandbox/rebuild-flow-lifecycle.test.ts +++ b/src/lib/actions/sandbox/rebuild-flow-lifecycle.test.ts @@ -8,7 +8,7 @@ import { installRebuildFlowTestHooks, originalSandboxName, snapshotEnv, -} from "../../../../test/helpers/rebuild-flow-test-harness"; +} from "../../../../test/helpers/rebuild-flow-generic-harness"; import { makePreparedRecoveryManifest } from "./rebuild-flow-test-fixtures"; describe("rebuildSandbox flow: lifecycle", () => { diff --git a/src/lib/actions/sandbox/rebuild-flow-recovery.test.ts b/src/lib/actions/sandbox/rebuild-flow-recovery.test.ts index d07e8f10d16..eec92920309 100644 --- a/src/lib/actions/sandbox/rebuild-flow-recovery.test.ts +++ b/src/lib/actions/sandbox/rebuild-flow-recovery.test.ts @@ -8,7 +8,7 @@ import { expectNoSandboxDelete } from "../../../../test/helpers/rebuild-delete-a import { createRebuildFlowHarness, installRebuildFlowTestHooks, -} from "../../../../test/helpers/rebuild-flow-test-harness"; +} from "../../../../test/helpers/rebuild-flow-generic-harness"; import { fingerprintSandboxLiveIdentity } from "../../onboard/sandbox-recreate-transaction"; import { makeActiveTeamsMessagingPlan, diff --git a/src/lib/actions/sandbox/rebuild-flow-target-credentials.test.ts b/src/lib/actions/sandbox/rebuild-flow-target-credentials.test.ts index d8fc0b1ebe9..c815270c985 100644 --- a/src/lib/actions/sandbox/rebuild-flow-target-credentials.test.ts +++ b/src/lib/actions/sandbox/rebuild-flow-target-credentials.test.ts @@ -11,7 +11,7 @@ import { createRebuildFlowHarness, installRebuildFlowTestHooks, snapshotEnv, -} from "../../../../test/helpers/rebuild-flow-test-harness"; +} from "../../../../test/helpers/rebuild-flow-generic-harness"; describe("rebuildSandbox flow: target credentials", () => { installRebuildFlowTestHooks(); diff --git a/src/lib/actions/sandbox/rebuild-flow-target-image.test.ts b/src/lib/actions/sandbox/rebuild-flow-target-image.test.ts index 347b1f1c1a5..2b6166bd336 100644 --- a/src/lib/actions/sandbox/rebuild-flow-target-image.test.ts +++ b/src/lib/actions/sandbox/rebuild-flow-target-image.test.ts @@ -12,7 +12,7 @@ import { installRebuildFlowTestHooks, originalSandboxName, snapshotEnv, -} from "../../../../test/helpers/rebuild-flow-test-harness"; +} from "../../../../test/helpers/rebuild-flow-generic-harness"; import { fingerprintBuildContext } from "../../adapters/fs/build-context-fingerprint"; import { createBuildContextVerifier } from "./rebuild-prepared-image-context"; diff --git a/src/lib/actions/sandbox/rebuild-flow-target-session.test.ts b/src/lib/actions/sandbox/rebuild-flow-target-session.test.ts index 3b27ea5b527..7eccac91ecb 100644 --- a/src/lib/actions/sandbox/rebuild-flow-target-session.test.ts +++ b/src/lib/actions/sandbox/rebuild-flow-target-session.test.ts @@ -7,7 +7,7 @@ import { createRebuildFlowHarness, installRebuildFlowTestHooks, snapshotEnv, -} from "../../../../test/helpers/rebuild-flow-test-harness"; +} from "../../../../test/helpers/rebuild-flow-generic-harness"; describe("rebuildSandbox flow: target session", () => { installRebuildFlowTestHooks(); diff --git a/src/lib/actions/sandbox/rebuild-hermes-post-restore.test.ts b/src/lib/actions/sandbox/rebuild-hermes-post-restore.test.ts index b0f37168115..0b248eb20d3 100644 --- a/src/lib/actions/sandbox/rebuild-hermes-post-restore.test.ts +++ b/src/lib/actions/sandbox/rebuild-hermes-post-restore.test.ts @@ -1,12 +1,11 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { createRebuildFlowHarness, - resetRebuildFlowTestEnvironment, - restoreRebuildFlowTestEnvironment, -} from "../../../../test/helpers/rebuild-flow-harness"; + installRebuildFlowTestHooks, +} from "../../../../test/helpers/rebuild-flow-dcode-harness"; import { ensureHermesGatewayAfterStateRestore } from "./rebuild-hermes-post-restore"; describe("Hermes gateway post-restore recheck", () => { @@ -72,8 +71,7 @@ describe("Hermes gateway post-restore recheck", () => { }); describe("Hermes rebuild post-restore verification", () => { - beforeEach(resetRebuildFlowTestEnvironment); - afterEach(restoreRebuildFlowTestEnvironment); + installRebuildFlowTestHooks({ acceptThirdPartySoftware: true }); it("fails instead of reporting readiness when restored state leaves the gateway down (#7084)", async () => { const mcpEntry = { diff --git a/src/lib/actions/sandbox/rebuild-local-provider-recreate.test.ts b/src/lib/actions/sandbox/rebuild-local-provider-recreate.test.ts index 19eef519066..1926098112b 100644 --- a/src/lib/actions/sandbox/rebuild-local-provider-recreate.test.ts +++ b/src/lib/actions/sandbox/rebuild-local-provider-recreate.test.ts @@ -3,13 +3,12 @@ import { createRequire } from "node:module"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { createRebuildFlowHarness, + installRebuildFlowTestHooks, type RebuildFlowHarness, - resetRebuildFlowTestEnvironment, - restoreRebuildFlowTestEnvironment, -} from "../../../../test/helpers/rebuild-flow-harness"; +} from "../../../../test/helpers/rebuild-flow-dcode-harness"; import { setupOllamaLocalInference, setupVllmLocalInference, @@ -136,8 +135,7 @@ function makeRouteApplier() { }); } -beforeEach(resetRebuildFlowTestEnvironment); -afterEach(restoreRebuildFlowTestEnvironment); +installRebuildFlowTestHooks({ acceptThirdPartySoftware: true }); describe("rebuild local-provider recreation", () => { it.each( diff --git a/src/lib/actions/sandbox/rebuild-prepared-recovery.test.ts b/src/lib/actions/sandbox/rebuild-prepared-recovery.test.ts index 705c43e791a..68875abda42 100644 --- a/src/lib/actions/sandbox/rebuild-prepared-recovery.test.ts +++ b/src/lib/actions/sandbox/rebuild-prepared-recovery.test.ts @@ -1,30 +1,16 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { expectNoSandboxDelete } from "../../../../test/helpers/rebuild-delete-assertions"; import { createRebuildFlowHarness, + installRebuildFlowTestHooks, makePreparedRecoveryManifest, - resetRebuildFlowTestEnvironment, - restoreRebuildFlowTestEnvironment, - snapshotEnv, -} from "../../../../test/helpers/rebuild-flow-harness"; - -const restoreSandboxEnv = snapshotEnv([ - "NEMOCLAW_SANDBOX_NAME", - "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE", -]); +} from "../../../../test/helpers/rebuild-flow-dcode-harness"; describe("prepared rebuild recovery", () => { - beforeEach(() => { - resetRebuildFlowTestEnvironment(); - }); - - afterEach(() => { - restoreRebuildFlowTestEnvironment(); - restoreSandboxEnv(); - }); + installRebuildFlowTestHooks({ acceptThirdPartySoftware: true }); it("restores the validated pre-upgrade manifest without taking a second backup (#6114)", async () => { const harness = createRebuildFlowHarness({ diff --git a/test/helpers/rebuild-dcode-flow-helpers.ts b/test/helpers/rebuild-dcode-flow-helpers.ts index 1b8a783726d..77891a20370 100644 --- a/test/helpers/rebuild-dcode-flow-helpers.ts +++ b/test/helpers/rebuild-dcode-flow-helpers.ts @@ -4,7 +4,7 @@ import { expect } from "vitest"; import { expectNoSandboxDelete } from "./rebuild-delete-assertions"; -import type { RebuildFlowHarness } from "./rebuild-flow-harness"; +import type { RebuildFlowHarness } from "./rebuild-flow-dcode-harness"; export function makeDcodeSandboxEntry(): Record { return { diff --git a/test/helpers/rebuild-flow-dcode-harness.ts b/test/helpers/rebuild-flow-dcode-harness.ts new file mode 100644 index 00000000000..a40dbba8e2f --- /dev/null +++ b/test/helpers/rebuild-flow-dcode-harness.ts @@ -0,0 +1,721 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { type MockInstance, vi } from "vitest"; +import { makePreparedRecoveryManifest } from "../../src/lib/actions/sandbox/rebuild-flow-test-fixtures"; +import { + agentDefs, + agentOnboard, + agentRuntime, + createRebuildFlowSession, + destroy, + dockerImage, + dockerInspect, + gatewayDrift, + gatewayRuntime, + gatewayState, + gatewayTeardownAuthority, + installTerminalStepFailureMock, + loadRebuildSandbox, + mcpBridge, + messaging, + messagingHostForwardLifecycle, + nim, + onboardCredentialEnv, + onboardSession, + openshellRuntime, + policies, + processRecovery, + purgeRebuildModule, + type RebuildFlowSession, + type RebuildSandbox, + rebuildCustomImagePreflight, + rebuildFlowHelpers, + rebuildInference, + rebuildManagedImage, + rebuildMessagingConflict, + rebuildOnboardDependencies, + rebuildRoutePreflight, + rebuildShields, + registry, + registryPersistence, + resolve, + sandboxList, + sandboxSession, + sandboxState, + sandboxVersion, + shields, + snapshotEnv, + sourceSandboxGateway, +} from "./rebuild-flow-harness"; + +export { installRebuildFlowTestHooks } from "./rebuild-flow-harness"; +export { makePreparedRecoveryManifest, snapshotEnv }; + +export type RebuildFlowOverrides = { + agentName?: string; + sessionAgentName?: string | null; + applyPreset?: (presetName: string) => boolean; + captureOpenshell?: ( + args: string[], + options?: Record, + ) => { + status: number | null; + output?: string; + stdout?: string; + stderr?: string; + error?: Error; + }; + executeSandboxCommand?: () => { status: number; stdout: string; stderr: string } | null; + checkAndRecoverSandboxProcesses?: () => { + checked: boolean; + wasRunning: boolean | null; + recovered: boolean; + forwardRecovered: boolean; + forwardRecoveryFailed?: boolean; + secretBoundaryRefused?: boolean; + mcpReconciliationRefused?: boolean; + }; + onboard?: (session: RebuildFlowSession) => Promise | void; + repairMutableConfigPerms?: () => + | { applied: false; skipReason: "agent" | "locked" | "unreadable"; reason: string } + | { applied: true; verified: boolean; errors: string[] }; + restoreSandboxState?: () => { + success: boolean; + restoredDirs: string[]; + restoredFiles: string[]; + failedDirs: string[]; + failedFiles: string[]; + }; + buildMessagingRebuildPlan?: () => Promise | unknown; + sandboxEntry?: Record; + sandboxEntryReads?: Array | null>; + sessionSandboxName?: string; + sandboxListOutput?: string; + backupPolicyPresets?: string[]; + gatewayPresets?: string[]; + verificationUnavailableAfterPresetRemoval?: boolean; + preDeleteSandboxEntry?: Record; + preDeleteDefaultSandbox?: string | null; + preDeleteLatestManifest?: Record | null; + recoveryManifestValidation?: ( + manifest: Record, + ) => { ok: true; manifest: Record } | { ok: false; reason: string }; + managedImageEvidence?: boolean; + updateSession?: () => void; + dcodeRouteResults?: Array<{ ok: true } | { ok: false; detail: string }>; + gatewayRecoveryResult?: Record; + reconciledSandboxGatewayState?: Record; + dcodeImageVerificationResults?: boolean[]; + dcodeBaseImageIds?: string[]; + sandboxBaseImageLabelsOutput?: string; + dcodeImageResult?: + | { ok: true; prepared: Record & { cleanupBuildCtx: () => boolean } } + | { ok: false; detail: string }; + openShieldsWindow?: () => { relocked: boolean; wasLocked: boolean } | null; + preflightMessagingConflicts?: () => Promise | void; + preflightAuthoritativeRebuildTarget?: (options: Record) => Promise | void; + revalidateRebuildRouteBeforeDelete?: ( + receipt: Record, + ) => { ok: true; receipt: Record } | { ok: false; message: string }; + mcpPreparation?: { + entries: Array>; + detachedProviderEntries: Array>; + scrubbedAdapterEntries: Array>; + }; +}; + +export type RebuildFlowHarness = { + rebuildSandbox: RebuildSandbox; + applyPresetSpy: MockInstance; + applyPresetContentSpy: MockInstance; + backupSandboxStateSpy: MockInstance; + disposePreparedDcodeRebuildImageSpy: MockInstance; + dockerRmiSpy: MockInstance; + errorSpy: MockInstance; + ensureAgentBaseImageSpy: MockInstance; + pinTrustedAgentBaseImageOverrideForOperationSpy: MockInstance; + pinTrustedAgentRemoteBaseImageOverrideForOperationSpy: MockInstance; + restoreTrustedAgentBaseImageOverrideSpy: MockInstance; + restoreTrustedAgentRemoteBaseImageOverrideSpy: MockInstance; + executeSandboxCommandSpy: MockInstance; + checkAndRecoverSandboxProcessesSpy: MockInstance; + ensureMessagingHostForwardAfterRebuildSpy: MockInstance; + logSpy: MockInstance; + finalizeIncompleteOnboardStepSpy: MockInstance; + openShieldsSpy: MockInstance; + onboardSpy: MockInstance; + preflightAuthoritativeRebuildTargetSpy: MockInstance; + preflightMessagingConflictsSpy: MockInstance; + preflightDcodeRouteSpy: MockInstance; + prepareManagedDcodeRebuildImageSpy: MockInstance; + removePresetSpy: MockInstance; + removeSandboxRegistryEntrySpy: MockInstance; + registryUpdateSpy: MockInstance; + releaseOnboardLockSpy: MockInstance; + relockSpy: MockInstance; + restoreSandboxEntrySpy: MockInstance; + restoreRegistryEntryIfMissingSpy: MockInstance; + restoreSandboxStateSpy: MockInstance; + captureOpenshellSpy: MockInstance; + runOpenshellSpy: MockInstance; + messagingRebuildPlanSpy: MockInstance; + prepareMcpBridgesForRebuildSpy: MockInstance; + reattachMcpProvidersAfterRebuildAbortSpy: MockInstance; + restoreMcpBridgesAfterRebuildSpy: MockInstance; + warnUnpreservedUserManagedFilesSpy: MockInstance; + preparedDcodeBuildContext: Record & { cleanupBuildCtx: MockInstance }; + session: RebuildFlowSession; +}; + +export function createRebuildFlowHarness(overrides: RebuildFlowOverrides = {}): RebuildFlowHarness { + purgeRebuildModule(); + + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); + const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined); + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined); + + const session = createRebuildFlowSession(onboardSession.MACHINE_SNAPSHOT_VERSION); + const rebuildShieldsWindow = { relocked: false, wasLocked: false }; + const agentName = overrides.agentName ?? "openclaw"; + const agentDisplayName = + agentName === "langchain-deepagents-code" + ? "Deep Agents Code" + : agentName === "hermes" + ? "Hermes Agent" + : "OpenClaw"; + const agentBaseImageId = `sha256:${"a".repeat(64)}`; + const agentBaseImageRef = `nemoclaw-${agentName}-sandbox-base-local:image-${agentBaseImageId.slice("sha256:".length)}`; + const agentDef = { + name: agentName, + displayName: agentDisplayName, + expectedVersion: "0.2.0", + dockerfileBasePath: "/tmp/Dockerfile.base", + runtime: { kind: "terminal" }, + }; + + vi.spyOn(gatewayDrift, "detectOpenShellStateRpcPreflightIssue").mockReturnValue(null); + vi.spyOn(gatewayDrift, "detectOpenShellStateRpcResultIssue").mockReturnValue(null); + vi.spyOn(gatewayTeardownAuthority, "resolveGatewayTeardownAuthority").mockImplementation( + ({ gatewayName, gatewayPort }: { gatewayName: string; gatewayPort: number }) => ({ + gatewayName, + gatewayPort, + mode: "nemoclaw-managed", + source: "standalone", + endpoint: null, + stateDir: null, + supervisor: null, + requiredCapabilities: [], + }), + ); + vi.spyOn(sandboxList, "captureSandboxListWithGatewayRecovery").mockResolvedValue({ + result: { status: 0, output: overrides.sandboxListOutput ?? "alpha Ready" }, + }); + vi.spyOn(resolve, "resolveOpenshell").mockReturnValue(null); + vi.spyOn(dockerImage, "dockerBuild").mockReturnValue({ status: 0 }); + vi.spyOn(rebuildCustomImagePreflight, "preflightRebuildImage").mockResolvedValue({ + ok: true, + imageTag: null, + }); + const imageIdsByRef = new Map([ + [agentBaseImageRef, agentBaseImageId], + [agentBaseImageId, agentBaseImageId], + ]); + const dcodeBaseImageIds = [...(overrides.dcodeBaseImageIds ?? [])]; + vi.spyOn(dockerInspect, "dockerImageInspectFormat").mockImplementation((...args: unknown[]) => { + if ( + args[0] === "{{json .Config.Labels}}" && + overrides.sandboxBaseImageLabelsOutput !== undefined + ) { + return overrides.sandboxBaseImageLabelsOutput; + } + if (args[0] === "{{.Id}}") { + const imageRef = String(args[1]); + if (imageRef === agentBaseImageRef && dcodeBaseImageIds.length > 0) { + return dcodeBaseImageIds.shift()!; + } + const imageId = imageIdsByRef.get(imageRef); + if (imageId) return imageId; + } + return dcodeBaseImageIds.shift() ?? "sha256:dcode-base"; + }); + const dockerRmiSpy = vi.spyOn(dockerImage, "dockerRmi").mockReturnValue({ status: 0 }); + vi.spyOn(dockerImage, "dockerTag").mockImplementation((source: unknown, target: unknown) => { + const sourceRef = String(source); + const sourceId = + imageIdsByRef.get(sourceRef) ?? (sourceRef.startsWith("sha256:") ? sourceRef : null); + if (sourceId) imageIdsByRef.set(String(target), sourceId); + return { status: 0 }; + }); + vi.spyOn(agentDefs, "loadAgent").mockReturnValue(agentDef); + const trustedLocalOverride = { + ref: agentBaseImageRef, + provenance: `${"b".repeat(64)}.${"c".repeat(64)}`, + }; + const ensureAgentBaseImageSpy = vi.spyOn(agentOnboard, "ensureAgentBaseImage").mockReturnValue({ + imageTag: agentBaseImageRef, + built: true, + trustedLocalOverride, + }); + const restoreTrustedAgentBaseImageOverrideSpy = vi.fn(); + const pinTrustedAgentBaseImageOverrideForOperationSpy = vi + .spyOn(agentOnboard, "pinTrustedAgentBaseImageOverrideForOperation") + .mockReturnValue(restoreTrustedAgentBaseImageOverrideSpy); + const restoreTrustedAgentRemoteBaseImageOverrideSpy = vi.fn(); + const pinTrustedAgentRemoteBaseImageOverrideForOperationSpy = vi + .spyOn(agentOnboard, "pinTrustedAgentRemoteBaseImageOverrideForOperation") + .mockReturnValue(restoreTrustedAgentRemoteBaseImageOverrideSpy); + const sessionAgentName = + overrides.sessionAgentName === undefined ? agentName : overrides.sessionAgentName; + vi.spyOn(agentRuntime, "getSessionAgent").mockReturnValue( + sessionAgentName === null || sessionAgentName === "openclaw" + ? null + : ({ name: sessionAgentName } as never), + ); + vi.spyOn(agentRuntime, "getAgentDisplayName").mockReturnValue(agentDisplayName); + vi.spyOn(gatewayRuntime, "recoverNamedGatewayRuntime").mockImplementation( + async (...args: unknown[]) => { + const gatewayName = + (args[0] as { gatewayName?: string } | undefined)?.gatewayName ?? "nemoclaw"; + const state = { state: "healthy_named", activeGateway: gatewayName }; + return ( + overrides.gatewayRecoveryResult ?? { + recovered: true, + attempted: false, + before: state, + after: state, + } + ); + }, + ); + vi.spyOn(gatewayState, "getReconciledSandboxGatewayState").mockResolvedValue( + overrides.reconciledSandboxGatewayState ?? { state: "present", output: "alpha Ready" }, + ); + vi.spyOn(onboardSession, "loadSession").mockReturnValue(session); + vi.spyOn(onboardSession, "acquireOnboardLock").mockReturnValue({ acquired: true }); + vi.spyOn(onboardSession, "updateSession").mockImplementation((mutator: unknown) => { + overrides.updateSession?.(); + if (typeof mutator !== "function") { + throw new TypeError("updateSession expected a mutator function"); + } + (mutator as (value: typeof session) => typeof session | void)(session); + return session; + }); + const releaseOnboardLockSpy = vi + .spyOn(onboardSession, "releaseOnboardLock") + .mockImplementation(() => undefined); + const finalizeIncompleteOnboardStepSpy = installTerminalStepFailureMock(onboardSession, session); + session.sandboxName = overrides.sessionSandboxName ?? session.sandboxName; + const sandboxEntry = { + name: "alpha", + provider: "ollama-local", + model: "nvidia/nemotron", + policies: ["npm"], + agent: null, + agentVersion: "0.1.0", + // A current managed-image registry row carries positive NemoClaw provenance. + // Tests that exercise the legacy ambiguous-image path override this explicitly. + nemoclawVersion: "0.0.71", + nimContainer: null, + ...(overrides.sandboxEntry ?? {}), + }; + const preDeleteDefaultSandbox = + overrides.preDeleteDefaultSandbox === undefined ? "alpha" : overrides.preDeleteDefaultSandbox; + let sandboxEntryReadCount = 0; + vi.spyOn(registry, "getSandbox").mockImplementation(() => { + const configuredReads = overrides.sandboxEntryReads ?? []; + return ( + sandboxEntryReadCount < configuredReads.length + ? configuredReads[sandboxEntryReadCount++] + : sandboxEntry + ) as never; + }); + let registryLoadCount = 0; + vi.spyOn(registryPersistence, "load").mockImplementation(() => { + const isPreDeleteRead = registryLoadCount > 0; + registryLoadCount++; + return { + defaultSandbox: isPreDeleteRead ? preDeleteDefaultSandbox : "alpha", + sandboxes: { + alpha: + isPreDeleteRead && overrides.preDeleteSandboxEntry + ? overrides.preDeleteSandboxEntry + : sandboxEntry, + }, + }; + }); + vi.spyOn(registry, "listSandboxes").mockReturnValue({ sandboxes: [] }); + const registryUpdateSpy = vi.spyOn(registry, "updateSandbox").mockReturnValue(true); + vi.spyOn(rebuildRoutePreflight, "commitRebuildRoutePreflight").mockImplementation( + (...args: unknown[]) => { + const input = args[0] as { + sandboxName: string; + gatewayName: string; + targetUpdate: Record; + }; + if (!registry.updateSandbox(input.sandboxName, input.targetUpdate)) { + return { + ok: false, + message: "Sandbox registry entry disappeared during rebuild route preflight.", + }; + } + return { + ok: true, + receipt: { + sandboxName: input.sandboxName, + gatewayName: input.gatewayName, + route: { + provider: input.targetUpdate.provider ?? null, + model: input.targetUpdate.model ?? null, + endpointUrl: input.targetUpdate.endpointUrl ?? null, + preferredInferenceApi: input.targetUpdate.preferredInferenceApi ?? null, + credentialEnv: input.targetUpdate.credentialEnv ?? null, + }, + migratedSandboxNames: [], + }, + }; + }, + ); + vi.spyOn(rebuildRoutePreflight, "revalidateRebuildRouteBeforeDelete").mockImplementation( + (...args: unknown[]) => { + const receipt = args[0] as Record; + return overrides.revalidateRebuildRouteBeforeDelete?.(receipt) ?? { ok: true, receipt }; + }, + ); + const restoreSandboxEntrySpy = vi + .spyOn(registry, "restoreSandboxEntry") + .mockImplementation(() => undefined); + const restoreRegistryEntryIfMissingSpy = vi + .spyOn(registry, "restoreSandboxEntryIfMissing") + .mockReturnValue(true); + vi.spyOn(sandboxSession, "getActiveSandboxSessions").mockReturnValue({ + detected: false, + sessions: [], + }); + vi.spyOn(sandboxVersion, "checkAgentVersion").mockReturnValue({ + expectedVersion: "0.2.0", + sandboxVersion: "0.1.0", + }); + vi.spyOn(nim, "detectGpu").mockReturnValue(null); + const routeResults = [...(overrides.dcodeRouteResults ?? [{ ok: true }])]; + const preflightDcodeRouteSpy = vi + .spyOn(rebuildInference, "preflightRebuildInferenceRoute") + .mockImplementation(() => routeResults.shift() ?? { ok: true }); + const preparedDcodeBuildContext = { + buildCtx: "/tmp/dcode-rebuild-context", + stagedDockerfile: "/tmp/dcode-rebuild-context/Dockerfile", + buildId: "dcode-build", + contextFingerprint: "dcode-context", + dockerGpuPatchNetwork: null, + cleanupBuildCtx: vi.fn(() => true), + }; + const prepareManagedDcodeRebuildImageSpy = vi + .spyOn(rebuildManagedImage, "prepareManagedDcodeRebuildImage") + .mockImplementation( + async () => + (overrides.dcodeImageResult ?? { + ok: true, + prepared: preparedDcodeBuildContext, + }) as never, + ); + const disposePreparedDcodeRebuildImageSpy = vi + .spyOn(rebuildManagedImage, "disposePreparedDcodeRebuildImage") + .mockImplementation((prepared: unknown) => + (prepared as { cleanupBuildCtx: () => boolean }).cleanupBuildCtx(), + ); + const imageVerificationResults = [...(overrides.dcodeImageVerificationResults ?? [true])]; + vi.spyOn(rebuildManagedImage, "verifyPreparedDcodeRebuildImage").mockImplementation( + () => imageVerificationResults.shift() ?? true, + ); + const openShieldsSpy = vi + .spyOn(rebuildShields, "openRebuildShieldsWindow") + .mockImplementation(overrides.openShieldsWindow ?? (() => rebuildShieldsWindow)); + const relockSpy = vi + .spyOn(rebuildShields, "relockRebuildShieldsWindow") + .mockImplementation((...args: unknown[]) => { + const window = args[1] as typeof rebuildShieldsWindow; + window.relocked = true; + return true; + }); + const backupSandboxStateSpy = vi.spyOn(sandboxState, "backupSandboxState").mockReturnValue({ + success: true, + backedUpDirs: ["workspace"], + backedUpFiles: ["user.md"], + failedDirs: [], + failedFiles: [], + manifest: { + agentType: overrides.agentName ?? "openclaw", + backupPath: "/tmp/nemoclaw-rebuild-backup", + timestamp: "2026-06-01T00:00:00.000Z", + policyPresets: overrides.backupPolicyPresets ?? ["npm", "bad", "throw"], + }, + }); + vi.spyOn(sandboxState, "validateRebuildRecoveryManifest").mockImplementation( + (...args: unknown[]) => { + const manifest = args[2] as Record; + return overrides.recoveryManifestValidation?.(manifest) ?? { ok: true as const, manifest }; + }, + ); + vi.spyOn(sandboxState, "getLatestBackup").mockImplementation( + () => + (overrides.preDeleteLatestManifest === undefined + ? makePreparedRecoveryManifest() + : overrides.preDeleteLatestManifest) as ReturnType, + ); + vi.spyOn(sandboxState, "hasPositiveManagedImageEvidence").mockReturnValue( + overrides.managedImageEvidence ?? true, + ); + const restoreSandboxStateSpy = vi + .spyOn(sandboxState, "restoreRecreatedSandboxState") + .mockImplementation( + overrides.restoreSandboxState ?? + (() => ({ + success: true, + restoredDirs: ["workspace"], + restoredFiles: ["user.md"], + failedDirs: [], + failedFiles: [], + })), + ); + const captureOpenshellSpy = vi + .spyOn(openshellRuntime, "captureOpenshell") + .mockImplementation((args: unknown, options?: unknown) => { + const argv = Array.isArray(args) ? args.map(String) : []; + if (overrides.captureOpenshell) { + return overrides.captureOpenshell(argv, options as Record | undefined); + } + const probedGateway = sourceSandboxGateway(argv, "get"); + const liveSource = "Name: alpha\nId: sbx-alpha-source\nPhase: Ready\n"; + return probedGateway && !deletedSourceGateways.has(probedGateway) + ? { status: 0, output: liveSource, stdout: liveSource, stderr: "" } + : { + status: 1, + output: "", + stdout: "", + stderr: "Error: sandbox alpha not found", + }; + }); + const deletedSourceGateways = new Set(); + const runOpenshellSpy = vi.spyOn(openshellRuntime, "runOpenshell").mockImplementation((args) => { + const argv = args as string[]; + const deleteGateway = sourceSandboxGateway(argv, "delete"); + if (deleteGateway) { + deletedSourceGateways.add(deleteGateway); + return { status: 0, output: "" }; + } + if ( + argv.join(" ") === "sandbox get alpha" || + argv.join(" ") === "sandbox get -g nemoclaw alpha" + ) { + return { + status: 1, + output: "sandbox alpha not found", + stdout: "", + stderr: "sandbox alpha not found", + }; + } + return argv[0] === "provider" && argv[1] === "get" + ? { + status: 0, + stdout: + "Name: compatible-endpoint\nType: openai\nCredential keys: COMPATIBLE_API_KEY\nConfig keys: OPENAI_BASE_URL\n", + stderr: "", + } + : { status: 0, output: "" }; + }); + const removeSandboxRegistryEntrySpy = vi + .spyOn(destroy, "removeSandboxRegistryEntryWithReceipt") + .mockReturnValue({ + entry: { name: "alpha", imageTag: "old-image" }, + wasDefault: preDeleteDefaultSandbox === "alpha", + fallbackDefault: null, + postRemovalDefaultSelectionRevision: 1, + }); + vi.spyOn(nim, "stopNimContainer").mockImplementation(() => undefined); + vi.spyOn(nim, "stopNimContainerByName").mockImplementation(() => undefined); + const onboardSpy = vi + .spyOn(rebuildOnboardDependencies, "onboard") + .mockImplementation(async () => { + await overrides.onboard?.(session); + }); + vi.spyOn(rebuildOnboardDependencies, "hydrateCredentialEnv").mockImplementation( + (...args: unknown[]) => onboardCredentialEnv.hydrateCredentialEnv(String(args[0] ?? "")), + ); + const preflightAuthoritativeRebuildTargetSpy = vi + .spyOn(rebuildOnboardDependencies, "preflightAuthoritativeRebuildTarget") + .mockImplementation(async (options: unknown) => { + await overrides.preflightAuthoritativeRebuildTarget?.( + (options ?? {}) as Record, + ); + }); + const livePolicyPresets = new Set(overrides.gatewayPresets ?? []); + const managedObservabilityPreset = "observability-otlp-local"; + const managedObservabilityContent = + "network_policies:\n observability-otlp-local:\n name: observability-otlp-local\n"; + let liveManagedObservabilityContent = livePolicyPresets.has(managedObservabilityPreset) + ? managedObservabilityContent + : null; + let policyRemovalObserved = false; + const applyPresetSpy = vi + .spyOn(policies, "applyPreset") + .mockImplementation((_sandboxName: unknown, presetName: unknown) => { + const normalizedPresetName = String(presetName); + let applied: boolean; + if (overrides.applyPreset) { + applied = overrides.applyPreset(normalizedPresetName); + } else if (normalizedPresetName === "throw") { + throw new Error("preset boom"); + } else { + applied = normalizedPresetName === "npm"; + } + if (applied) { + livePolicyPresets.add(normalizedPresetName); + if (normalizedPresetName === managedObservabilityPreset) { + liveManagedObservabilityContent = managedObservabilityContent; + } + } + return applied; + }); + const applyPresetContentSpy = vi + .spyOn(policies, "applyPresetContent") + .mockImplementation((_sandboxName: unknown, presetName: unknown, presetContent: unknown) => { + livePolicyPresets.add(String(presetName)); + const content = String(presetContent); + if (policies.parsePresetPolicyKeys(content).includes(managedObservabilityPreset)) { + liveManagedObservabilityContent = content; + } + return true; + }); + vi.spyOn(policies, "loadPresetForSandbox").mockImplementation( + (_sandboxName: unknown, presetName: unknown) => + String(presetName) === managedObservabilityPreset ? managedObservabilityContent : null, + ); + vi.spyOn(policies, "getPresetContentGatewayState").mockImplementation( + (_sandboxName: unknown, presetContent: unknown) => { + if (overrides.verificationUnavailableAfterPresetRemoval && policyRemovalObserved) return null; + const content = String(presetContent); + if (!policies.parsePresetPolicyKeys(content).includes(managedObservabilityPreset)) { + return "absent"; + } + if (liveManagedObservabilityContent === null) return "absent"; + return liveManagedObservabilityContent === content ? "match" : "drift"; + }, + ); + vi.spyOn(policies, "getGatewayPresets").mockImplementation(() => + overrides.verificationUnavailableAfterPresetRemoval && policyRemovalObserved + ? null + : [...livePolicyPresets], + ); + const removePresetSpy = vi + .spyOn(policies, "removePreset") + .mockImplementation((_sandboxName: unknown, presetName: unknown) => { + const removed = livePolicyPresets.delete(String(presetName)); + if ( + String(presetName) === managedObservabilityPreset && + liveManagedObservabilityContent === managedObservabilityContent + ) { + liveManagedObservabilityContent = null; + } + if (removed) policyRemovalObserved = true; + return removed; + }); + const executeSandboxCommandSpy = vi + .spyOn(processRecovery, "executeSandboxCommand") + .mockImplementation( + overrides.executeSandboxCommand ?? (() => ({ status: 0, stdout: "doctor ok", stderr: "" })), + ); + const checkAndRecoverSandboxProcessesSpy = vi + .spyOn(processRecovery, "checkAndRecoverSandboxProcesses") + .mockImplementation( + overrides.checkAndRecoverSandboxProcesses ?? + (() => ({ + checked: true, + wasRunning: true, + recovered: false, + forwardRecovered: false, + })), + ); + vi.spyOn(shields, "repairMutableConfigPerms").mockImplementation( + overrides.repairMutableConfigPerms ?? (() => ({ applied: true, verified: true, errors: [] })), + ); + vi.spyOn(shields, "isShieldsDown").mockReturnValue(true); + vi.spyOn(shields, "clearShieldsState").mockImplementation(() => undefined); + const messagingRebuildPlanSpy = vi + .spyOn(messaging.MessagingWorkflowPlanner.prototype, "buildRebuildPlanFromSandboxEntry") + .mockImplementation(overrides.buildMessagingRebuildPlan ?? (() => null)); + const preflightMessagingConflictsSpy = vi + .spyOn(rebuildMessagingConflict, "preflightRebuildMessagingConflicts") + .mockImplementation(async () => { + await overrides.preflightMessagingConflicts?.(); + }); + const ensureMessagingHostForwardAfterRebuildSpy = vi + .spyOn(messagingHostForwardLifecycle, "ensureMessagingHostForwardAfterRebuild") + .mockReturnValue(true); + const emptyMcpPreparation = { + entries: [], + detachedProviderEntries: [], + scrubbedAdapterEntries: [], + }; + const prepareMcpBridgesForRebuildSpy = vi + .spyOn(mcpBridge, "prepareMcpBridgesForRebuild") + .mockResolvedValue(overrides.mcpPreparation ?? emptyMcpPreparation); + vi.spyOn(mcpBridge, "prepareMcpBridgesForAbsentSandboxRebuild").mockResolvedValue( + overrides.mcpPreparation ?? emptyMcpPreparation, + ); + const reattachMcpProvidersAfterRebuildAbortSpy = vi + .spyOn(mcpBridge, "reattachMcpProvidersAfterRebuildAbort") + .mockResolvedValue(undefined); + const restoreMcpBridgesAfterRebuildSpy = vi + .spyOn(mcpBridge, "restoreMcpBridgesAfterRebuild") + .mockResolvedValue(undefined); + const warnUnpreservedUserManagedFilesSpy = vi + .spyOn(rebuildFlowHelpers, "warnUnpreservedUserManagedFiles") + .mockImplementation(() => undefined); + + errorSpy.mockClear(); + logSpy.mockClear(); + warnSpy.mockClear(); + + return { + rebuildSandbox: loadRebuildSandbox(), + applyPresetSpy, + applyPresetContentSpy, + backupSandboxStateSpy, + disposePreparedDcodeRebuildImageSpy, + dockerRmiSpy, + errorSpy, + ensureAgentBaseImageSpy, + pinTrustedAgentBaseImageOverrideForOperationSpy, + pinTrustedAgentRemoteBaseImageOverrideForOperationSpy, + restoreTrustedAgentBaseImageOverrideSpy, + restoreTrustedAgentRemoteBaseImageOverrideSpy, + executeSandboxCommandSpy, + checkAndRecoverSandboxProcessesSpy, + ensureMessagingHostForwardAfterRebuildSpy, + logSpy, + finalizeIncompleteOnboardStepSpy, + openShieldsSpy, + onboardSpy, + preflightAuthoritativeRebuildTargetSpy, + preflightMessagingConflictsSpy, + preflightDcodeRouteSpy, + prepareManagedDcodeRebuildImageSpy, + removePresetSpy, + removeSandboxRegistryEntrySpy, + registryUpdateSpy, + releaseOnboardLockSpy, + relockSpy, + restoreSandboxEntrySpy, + restoreRegistryEntryIfMissingSpy, + restoreSandboxStateSpy, + captureOpenshellSpy, + runOpenshellSpy, + messagingRebuildPlanSpy, + prepareMcpBridgesForRebuildSpy, + reattachMcpProvidersAfterRebuildAbortSpy, + restoreMcpBridgesAfterRebuildSpy, + warnUnpreservedUserManagedFilesSpy, + preparedDcodeBuildContext, + session, + }; +} diff --git a/test/helpers/rebuild-flow-test-harness.ts b/test/helpers/rebuild-flow-generic-harness.ts similarity index 85% rename from test/helpers/rebuild-flow-test-harness.ts rename to test/helpers/rebuild-flow-generic-harness.ts index c56310de928..4ace63fe0bf 100644 --- a/test/helpers/rebuild-flow-test-harness.ts +++ b/test/helpers/rebuild-flow-generic-harness.ts @@ -2,77 +2,62 @@ // SPDX-License-Identifier: Apache-2.0 import fs from "node:fs"; -import { createRequire } from "node:module"; -import os from "node:os"; import path from "node:path"; -import { afterEach, beforeEach, vi } from "vitest"; +import { vi } from "vitest"; import { makePreparedRecoveryManifest } from "../../src/lib/actions/sandbox/rebuild-flow-test-fixtures"; import type { RebuildRecreateOnboardOpts } from "../../src/lib/actions/sandbox/rebuild-gpu-opt-out"; import { + agentDefs, + agentRuntime, + buildContextFingerprint, + createHarnessTempDir, createRebuildFlowSession, + destroy, + dockerInspect, + gatewayDrift, + gatewayState, + gatewayTeardownAuthority, + hermesProviderAuth, installTerminalStepFailureMock, - originalSandboxName, + loadRebuildSandbox, + mcpBridge, + messaging, + messagingHostForwardLifecycle, + nim, + onboardCredentialEnv, + onboardSession, + openshellRuntime, + policies, + processRecovery, + purgeRebuildModule, type RebuildFlowHarness, type RebuildFlowOverrides, -} from "./rebuild-flow-test-support"; - -export { originalSandboxName, snapshotEnv } from "./rebuild-flow-test-support"; - -const requireDist = createRequire( - new URL("../../src/lib/actions/sandbox/rebuild.ts", import.meta.url), -); -const rebuildModulePath = "./rebuild.js"; -requireDist(rebuildModulePath); -delete require.cache[requireDist.resolve(rebuildModulePath)]; -const harnessTempDirs: string[] = []; + rebuildCustomImagePreflight, + rebuildFlowHelpers, + rebuildOnboardDependencies, + rebuildPreparedImageContext, + rebuildRoutePreflight, + rebuildShields, + rebuildUsageNotice, + registry, + registryPersistence, + resolve, + sandboxList, + sandboxSession, + sandboxState, + sandboxVersion, + shields, + sourceSandboxGateway, +} from "./rebuild-flow-harness"; -// Cache stable dependency modules outside each test's timeout. The rebuild -// entry itself is still reloaded after these modules receive fresh spies. -const gatewayDrift = requireDist("../../adapters/openshell/gateway-drift.js"); -const openshellRuntime = requireDist("../../adapters/openshell/runtime.js"); -const dockerInspect = requireDist("../../adapters/docker/inspect.js"); -const sandboxList = requireDist("../../openshell-sandbox-list.js"); -const resolve = requireDist("../../adapters/openshell/resolve.js"); -const gatewayTeardownAuthority = requireDist( - "../../onboard/gateway-teardown-authority.js", -) as typeof import("../../src/lib/onboard/gateway-teardown-authority"); -const agentDefs = requireDist("../../agent/defs.js"); -const agentRuntime = requireDist("../../agent/runtime.js"); -const { rebuildOnboardDependencies } = requireDist("./rebuild-onboard-dependencies.js"); -const onboardCredentialEnv = requireDist("../../onboard/credential-env.js"); -const hermesProviderAuth = requireDist("../../hermes-provider-auth.js"); -const onboardSession = requireDist("../../state/onboard-session.js"); -const registry = requireDist("../../state/registry.js"); -const registryPersistence = requireDist("../../state/registry/persistence.js"); -const sandboxState = requireDist("../../state/sandbox.js"); -const sandboxSession = requireDist("../../state/sandbox-session.js"); -const sandboxVersion = requireDist("../../sandbox/version.js"); -const destroy = requireDist("./destroy.js"); -const gatewayState = requireDist("./gateway-state.js"); -const rebuildFlowHelpers = requireDist("./rebuild-flow-helpers.js"); -const rebuildCustomImagePreflight = requireDist("./rebuild-custom-image-preflight.js"); -const rebuildPreparedImageContext = requireDist("./rebuild-prepared-image-context.js"); -const rebuildRoutePreflight = requireDist("./rebuild-preflight-guards.js"); -const buildContextFingerprint = requireDist("../../adapters/fs/build-context-fingerprint.js"); -const rebuildUsageNotice = requireDist("./rebuild-usage-notice.js"); -const rebuildShields = requireDist("./rebuild-shields.js"); -const nim = requireDist("../../inference/nim.js"); -const policies = requireDist("../../policy/index.js"); -const processRecovery = requireDist("./process-recovery.js"); -const messagingHostForwardLifecycle = requireDist("./messaging-host-forward-lifecycle.js"); -const mcpBridge = requireDist("./mcp-bridge.js"); -const messaging = requireDist("../../messaging/index.js"); -const shields = requireDist("../../shields/index.js"); - -function sourceSandboxGateway(argv: string[], verb: string): string | null { - const gatewayFlag = argv.indexOf("-g"); - return argv[0] === "sandbox" && argv[1] === verb && argv.at(-1) === "alpha" && gatewayFlag > 0 - ? (argv[gatewayFlag + 1] ?? null) - : null; -} +export { + installRebuildFlowTestHooks, + originalSandboxName, + snapshotEnv, +} from "./rebuild-flow-harness"; export function createRebuildFlowHarness(overrides: RebuildFlowOverrides = {}): RebuildFlowHarness { - delete require.cache[requireDist.resolve(rebuildModulePath)]; + purgeRebuildModule(); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined); @@ -82,8 +67,7 @@ export function createRebuildFlowHarness(overrides: RebuildFlowOverrides = {}): const rebuildShieldsWindow = { relocked: false, wasLocked: false }; let policyAdditionsPath: string | null = null; if (typeof overrides.agentPolicyAdditionsContent === "string") { - const policyDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-rebuild-agent-policy-")); - harnessTempDirs.push(policyDir); + const policyDir = createHarnessTempDir("nemoclaw-rebuild-agent-policy-"); policyAdditionsPath = path.join(policyDir, "policy-additions.yaml"); fs.writeFileSync(policyAdditionsPath, overrides.agentPolicyAdditionsContent); } @@ -131,8 +115,7 @@ export function createRebuildFlowHarness(overrides: RebuildFlowOverrides = {}): const ensureTargetGatewaySpy = vi .spyOn(rebuildFlowHelpers, "ensureRebuildTargetGatewaySelected") .mockResolvedValue(true); - const preparedBuildCtx = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-rebuild-flow-image-")); - harnessTempDirs.push(preparedBuildCtx); + const preparedBuildCtx = createHarnessTempDir("nemoclaw-rebuild-flow-image-"); const preparedDockerfile = path.join(preparedBuildCtx, "Dockerfile"); fs.writeFileSync(preparedDockerfile, "FROM scratch\n"); const rebuildAgent = @@ -629,7 +612,7 @@ export function createRebuildFlowHarness(overrides: RebuildFlowOverrides = {}): warnSpy.mockClear(); return { - rebuildSandbox: requireDist(rebuildModulePath).rebuildSandbox, + rebuildSandbox: loadRebuildSandbox(), applyPresetSpy, backupSandboxStateSpy, checkAndRecoverSandboxProcessesSpy, @@ -675,21 +658,3 @@ export function createRebuildFlowHarness(overrides: RebuildFlowOverrides = {}): session, }; } - -export function installRebuildFlowTestHooks(): void { - beforeEach(() => { - delete process.env.NEMOCLAW_SANDBOX_NAME; - }); - afterEach(() => { - vi.restoreAllMocks(); - delete require.cache[requireDist.resolve(rebuildModulePath)]; - for (const dir of harnessTempDirs.splice(0)) { - fs.rmSync(dir, { recursive: true, force: true }); - } - if (originalSandboxName === undefined) { - delete process.env.NEMOCLAW_SANDBOX_NAME; - } else { - process.env.NEMOCLAW_SANDBOX_NAME = originalSandboxName; - } - }); -} diff --git a/test/helpers/rebuild-flow-harness.ts b/test/helpers/rebuild-flow-harness.ts index 124f6cff6e1..092b7046227 100644 --- a/test/helpers/rebuild-flow-harness.ts +++ b/test/helpers/rebuild-flow-harness.ts @@ -1,19 +1,17 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import fs from "node:fs"; import { createRequire } from "node:module"; +import os from "node:os"; import path from "node:path"; +import { afterEach, beforeEach, vi } from "vitest"; +import { type RebuildSandbox, snapshotEnv } from "./rebuild-flow-test-support"; -import { type MockInstance, vi } from "vitest"; -import { makePreparedRecoveryManifest } from "../../src/lib/actions/sandbox/rebuild-flow-test-fixtures"; -import { snapshotEnv } from "./rebuild-flow-test-support"; - -type RebuildSandbox = typeof import("../../src/lib/actions/sandbox/rebuild")["rebuildSandbox"]; - -export { makePreparedRecoveryManifest, snapshotEnv }; +export * from "./rebuild-flow-test-support"; const requireDist = createRequire( - path.join(process.cwd(), "src/lib/actions/sandbox/rebuild-flow-harness.ts"), + new URL("../../src/lib/actions/sandbox/rebuild.ts", import.meta.url), ); const rebuildModulePath = "./rebuild.js"; @@ -24,825 +22,94 @@ delete require.cache[requireDist.resolve(rebuildModulePath)]; // Cache stable dependency modules outside each test's timeout. The rebuild // entry itself is still reloaded after these modules receive fresh spies. -const gatewayDrift = requireDist("../../adapters/openshell/gateway-drift.js"); -const openshellRuntime = requireDist("../../adapters/openshell/runtime.js"); -const dockerImage = requireDist("../../adapters/docker/image.js"); -const dockerInspect = requireDist("../../adapters/docker/inspect.js"); -const sandboxList = requireDist("../../openshell-sandbox-list.js"); -const resolve = requireDist("../../adapters/openshell/resolve.js"); -const agentDefs = requireDist("../../agent/defs.js"); -const agentOnboard = requireDist("../../agent/onboard.js"); -const agentRuntime = requireDist("../../agent/runtime.js"); -const gatewayRuntime = requireDist("../../gateway-runtime-action.js"); -const gatewayState = requireDist("./gateway-state.js"); -const { rebuildOnboardDependencies } = requireDist("./rebuild-onboard-dependencies.js"); -const onboardCredentialEnv = requireDist("../../onboard/credential-env.js"); -const onboardSession = requireDist("../../state/onboard-session.js"); -const registry = requireDist("../../state/registry.js"); -const registryPersistence = requireDist("../../state/registry/persistence.js"); -const sandboxState = requireDist("../../state/sandbox.js"); -const sandboxSession = requireDist("../../state/sandbox-session.js"); -const sandboxVersion = requireDist("../../sandbox/version.js"); -const destroy = requireDist("./destroy.js"); -const rebuildShields = requireDist("./rebuild-shields.js"); -const nim = requireDist("../../inference/nim.js"); -const policies = requireDist("../../policy/index.js"); -const processRecovery = requireDist("./process-recovery.js"); -const messagingHostForwardLifecycle = requireDist("./messaging-host-forward-lifecycle.js"); -const messaging = requireDist("../../messaging/index.js"); -const mcpBridge = requireDist("./mcp-bridge.js"); -const rebuildCustomImagePreflight = requireDist("./rebuild-custom-image-preflight.js"); -const rebuildInference = requireDist("./rebuild-inference-preflight.js"); -const rebuildFlowHelpers = requireDist("./rebuild-flow-helpers.js"); -const rebuildManagedImage = requireDist("./rebuild-managed-image-preflight.js"); -const rebuildMessagingConflict = requireDist("./rebuild-messaging-conflict-preflight.js"); -const rebuildRoutePreflight = requireDist("./rebuild-preflight-guards.js"); -const gatewayTeardownAuthority = requireDist( +export const agentDefs = requireDist("../../agent/defs.js"); +export const agentOnboard = requireDist("../../agent/onboard.js"); +export const agentRuntime = requireDist("../../agent/runtime.js"); +export const buildContextFingerprint = requireDist( + "../../adapters/fs/build-context-fingerprint.js", +); +export const destroy = requireDist("./destroy.js"); +export const dockerImage = requireDist("../../adapters/docker/image.js"); +export const dockerInspect = requireDist("../../adapters/docker/inspect.js"); +export const gatewayDrift = requireDist("../../adapters/openshell/gateway-drift.js"); +export const gatewayRuntime = requireDist("../../gateway-runtime-action.js"); +export const gatewayState = requireDist("./gateway-state.js"); +export const gatewayTeardownAuthority = requireDist( "../../onboard/gateway-teardown-authority.js", ) as typeof import("../../src/lib/onboard/gateway-teardown-authority"); -const shields = requireDist("../../shields/index.js"); - -type RebuildFlowStep = { - status: string; - startedAt: string | null; - completedAt: string | null; - error: string | null; -}; - -export type RebuildFlowSession = Record & { - lastStepStarted: string | null; - status: string; - failure: { step: string; message: string | null; recordedAt: string } | null; - machine: { - version: number; - state: string; - stateEnteredAt: string; - revision: number; - }; - steps: Record; -}; - -export type RebuildFlowOverrides = { - agentName?: string; - sessionAgentName?: string | null; - applyPreset?: (presetName: string) => boolean; - captureOpenshell?: ( - args: string[], - options?: Record, - ) => { - status: number | null; - output?: string; - stdout?: string; - stderr?: string; - error?: Error; - }; - executeSandboxCommand?: () => { status: number; stdout: string; stderr: string } | null; - checkAndRecoverSandboxProcesses?: () => { - checked: boolean; - wasRunning: boolean | null; - recovered: boolean; - forwardRecovered: boolean; - forwardRecoveryFailed?: boolean; - secretBoundaryRefused?: boolean; - mcpReconciliationRefused?: boolean; - }; - onboard?: (session: RebuildFlowSession) => Promise | void; - repairMutableConfigPerms?: () => - | { applied: false; skipReason: "agent" | "locked" | "unreadable"; reason: string } - | { applied: true; verified: boolean; errors: string[] }; - restoreSandboxState?: () => { - success: boolean; - restoredDirs: string[]; - restoredFiles: string[]; - failedDirs: string[]; - failedFiles: string[]; - }; - buildMessagingRebuildPlan?: () => Promise | unknown; - sandboxEntry?: Record; - sandboxEntryReads?: Array | null>; - sessionSandboxName?: string; - sandboxListOutput?: string; - backupPolicyPresets?: string[]; - gatewayPresets?: string[]; - verificationUnavailableAfterPresetRemoval?: boolean; - preDeleteSandboxEntry?: Record; - preDeleteDefaultSandbox?: string | null; - preDeleteLatestManifest?: Record | null; - recoveryManifestValidation?: ( - manifest: Record, - ) => { ok: true; manifest: Record } | { ok: false; reason: string }; - managedImageEvidence?: boolean; - updateSession?: () => void; - dcodeRouteResults?: Array<{ ok: true } | { ok: false; detail: string }>; - gatewayRecoveryResult?: Record; - reconciledSandboxGatewayState?: Record; - dcodeImageVerificationResults?: boolean[]; - dcodeBaseImageIds?: string[]; - sandboxBaseImageLabelsOutput?: string; - dcodeImageResult?: - | { ok: true; prepared: Record & { cleanupBuildCtx: () => boolean } } - | { ok: false; detail: string }; - openShieldsWindow?: () => { relocked: boolean; wasLocked: boolean } | null; - preflightMessagingConflicts?: () => Promise | void; - preflightAuthoritativeRebuildTarget?: (options: Record) => Promise | void; - revalidateRebuildRouteBeforeDelete?: ( - receipt: Record, - ) => { ok: true; receipt: Record } | { ok: false; message: string }; - mcpPreparation?: { - entries: Array>; - detachedProviderEntries: Array>; - scrubbedAdapterEntries: Array>; - }; -}; - -export type RebuildFlowHarness = { - rebuildSandbox: RebuildSandbox; - applyPresetSpy: MockInstance; - applyPresetContentSpy: MockInstance; - backupSandboxStateSpy: MockInstance; - disposePreparedDcodeRebuildImageSpy: MockInstance; - dockerRmiSpy: MockInstance; - errorSpy: MockInstance; - ensureAgentBaseImageSpy: MockInstance; - pinTrustedAgentBaseImageOverrideForOperationSpy: MockInstance; - pinTrustedAgentRemoteBaseImageOverrideForOperationSpy: MockInstance; - restoreTrustedAgentBaseImageOverrideSpy: MockInstance; - restoreTrustedAgentRemoteBaseImageOverrideSpy: MockInstance; - executeSandboxCommandSpy: MockInstance; - checkAndRecoverSandboxProcessesSpy: MockInstance; - ensureMessagingHostForwardAfterRebuildSpy: MockInstance; - logSpy: MockInstance; - finalizeIncompleteOnboardStepSpy: MockInstance; - openShieldsSpy: MockInstance; - onboardSpy: MockInstance; - preflightAuthoritativeRebuildTargetSpy: MockInstance; - preflightMessagingConflictsSpy: MockInstance; - preflightDcodeRouteSpy: MockInstance; - prepareManagedDcodeRebuildImageSpy: MockInstance; - removePresetSpy: MockInstance; - removeSandboxRegistryEntrySpy: MockInstance; - registryUpdateSpy: MockInstance; - releaseOnboardLockSpy: MockInstance; - relockSpy: MockInstance; - restoreSandboxEntrySpy: MockInstance; - restoreRegistryEntryIfMissingSpy: MockInstance; - restoreSandboxStateSpy: MockInstance; - captureOpenshellSpy: MockInstance; - runOpenshellSpy: MockInstance; - messagingRebuildPlanSpy: MockInstance; - prepareMcpBridgesForRebuildSpy: MockInstance; - reattachMcpProvidersAfterRebuildAbortSpy: MockInstance; - restoreMcpBridgesAfterRebuildSpy: MockInstance; - warnUnpreservedUserManagedFilesSpy: MockInstance; - preparedDcodeBuildContext: Record & { cleanupBuildCtx: MockInstance }; - session: RebuildFlowSession; -}; - -const restoreRebuildFlowEnv = snapshotEnv([ - "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE", - "NEMOCLAW_SANDBOX_NAME", -]); - -export function resetRebuildFlowTestEnvironment(): void { - delete process.env.NEMOCLAW_SANDBOX_NAME; - process.env.NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE = "1"; -} - -export function restoreRebuildFlowTestEnvironment(): void { - vi.restoreAllMocks(); +export const hermesProviderAuth = requireDist("../../hermes-provider-auth.js"); +export const mcpBridge = requireDist("./mcp-bridge.js"); +export const messaging = requireDist("../../messaging/index.js"); +export const messagingHostForwardLifecycle = requireDist("./messaging-host-forward-lifecycle.js"); +export const nim = requireDist("../../inference/nim.js"); +export const onboardCredentialEnv = requireDist("../../onboard/credential-env.js"); +export const onboardSession = requireDist("../../state/onboard-session.js"); +export const openshellRuntime = requireDist("../../adapters/openshell/runtime.js"); +export const policies = requireDist("../../policy/index.js"); +export const processRecovery = requireDist("./process-recovery.js"); +export const { rebuildOnboardDependencies } = requireDist("./rebuild-onboard-dependencies.js"); +export const rebuildCustomImagePreflight = requireDist("./rebuild-custom-image-preflight.js"); +export const rebuildFlowHelpers = requireDist("./rebuild-flow-helpers.js"); +export const rebuildInference = requireDist("./rebuild-inference-preflight.js"); +export const rebuildManagedImage = requireDist("./rebuild-managed-image-preflight.js"); +export const rebuildMessagingConflict = requireDist("./rebuild-messaging-conflict-preflight.js"); +export const rebuildPreparedImageContext = requireDist("./rebuild-prepared-image-context.js"); +export const rebuildRoutePreflight = requireDist("./rebuild-preflight-guards.js"); +export const rebuildShields = requireDist("./rebuild-shields.js"); +export const rebuildUsageNotice = requireDist("./rebuild-usage-notice.js"); +export const registry = requireDist("../../state/registry.js"); +export const registryPersistence = requireDist("../../state/registry/persistence.js"); +export const resolve = requireDist("../../adapters/openshell/resolve.js"); +export const sandboxList = requireDist("../../openshell-sandbox-list.js"); +export const sandboxSession = requireDist("../../state/sandbox-session.js"); +export const sandboxState = requireDist("../../state/sandbox.js"); +export const sandboxVersion = requireDist("../../sandbox/version.js"); +export const shields = requireDist("../../shields/index.js"); + +export function purgeRebuildModule(): void { delete require.cache[requireDist.resolve(rebuildModulePath)]; - restoreRebuildFlowEnv(); } -function createStep(status: string): RebuildFlowStep { - return { status, startedAt: null, completedAt: null, error: null }; +export function loadRebuildSandbox(): RebuildSandbox { + return requireDist(rebuildModulePath).rebuildSandbox; } -function sourceSandboxGateway(argv: string[], verb: string): string | null { +export function sourceSandboxGateway(argv: string[], verb: string): string | null { const gatewayFlag = argv.indexOf("-g"); return argv[0] === "sandbox" && argv[1] === verb && argv.at(-1) === "alpha" && gatewayFlag > 0 ? (argv[gatewayFlag + 1] ?? null) : null; } -function createRebuildFlowSession(machineSnapshotVersion: number): RebuildFlowSession { - return { - sessionId: "rebuild-flow-session", - updatedAt: "2026-06-01T00:00:00.000Z", - sandboxName: "alpha", - agent: null, - provider: "ollama-local", - model: "nvidia/nemotron", - credentialEnv: null, - checkpoint: null, - webSearchConfig: null, - resourceProfile: null, - messagingPlan: null, - sandboxPromptProgress: { - sandboxName: true, - webSearch: false, - messaging: false, - resourceProfile: false, - }, - metadata: {}, - hermesToolGateways: [], - lastStepStarted: null, - status: "in_progress", - failure: null, - machine: { - version: machineSnapshotVersion, - state: "gateway", - stateEnteredAt: "2026-06-01T00:00:00.000Z", - revision: 2, - }, - steps: { - preflight: createStep("complete"), - gateway: createStep("complete"), - provider_selection: createStep("pending"), - inference: createStep("pending"), - sandbox: createStep("pending"), - openclaw: createStep("pending"), - agent_setup: createStep("pending"), - policies: createStep("pending"), - }, - }; -} +const harnessTempDirs: string[] = []; -function installTerminalStepFailureMock( - onboardSession: { finalizeIncompleteOnboardStep: (...args: unknown[]) => unknown }, - session: RebuildFlowSession, -): MockInstance { - return vi - .spyOn(onboardSession, "finalizeIncompleteOnboardStep") - .mockImplementation((stepName: unknown, message: unknown) => { - if (session.machine.state === "failed" || session.machine.state === "complete") { - return session; - } - const stepKey = String(stepName); - const step = session.steps[stepKey]; - if (!step) return session; - step.status = "failed"; - step.error = typeof message === "string" ? message : null; - session.status = "failed"; - session.failure = { - step: stepKey, - message: typeof message === "string" ? message : null, - recordedAt: "2026-06-01T00:02:00.000Z", - }; - session.machine.state = "failed"; - session.machine.revision += 1; - return session; - }); +export function createHarnessTempDir(prefix: string): string { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); + harnessTempDirs.push(dir); + return dir; } -export function createRebuildFlowHarness(overrides: RebuildFlowOverrides = {}): RebuildFlowHarness { - delete require.cache[requireDist.resolve(rebuildModulePath)]; - - const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); - const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined); - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined); - - const session = createRebuildFlowSession(onboardSession.MACHINE_SNAPSHOT_VERSION); - const rebuildShieldsWindow = { relocked: false, wasLocked: false }; - const agentName = overrides.agentName ?? "openclaw"; - const agentDisplayName = - agentName === "langchain-deepagents-code" - ? "Deep Agents Code" - : agentName === "hermes" - ? "Hermes Agent" - : "OpenClaw"; - const agentBaseImageId = `sha256:${"a".repeat(64)}`; - const agentBaseImageRef = `nemoclaw-${agentName}-sandbox-base-local:image-${agentBaseImageId.slice("sha256:".length)}`; - const agentDef = { - name: agentName, - displayName: agentDisplayName, - expectedVersion: "0.2.0", - dockerfileBasePath: "/tmp/Dockerfile.base", - runtime: { kind: "terminal" }, - }; +export type RebuildFlowTestHookOptions = { + acceptThirdPartySoftware?: boolean; +}; - vi.spyOn(gatewayDrift, "detectOpenShellStateRpcPreflightIssue").mockReturnValue(null); - vi.spyOn(gatewayDrift, "detectOpenShellStateRpcResultIssue").mockReturnValue(null); - vi.spyOn(gatewayTeardownAuthority, "resolveGatewayTeardownAuthority").mockImplementation( - ({ gatewayName, gatewayPort }: { gatewayName: string; gatewayPort: number }) => ({ - gatewayName, - gatewayPort, - mode: "nemoclaw-managed", - source: "standalone", - endpoint: null, - stateDir: null, - supervisor: null, - requiredCapabilities: [], - }), - ); - vi.spyOn(sandboxList, "captureSandboxListWithGatewayRecovery").mockResolvedValue({ - result: { status: 0, output: overrides.sandboxListOutput ?? "alpha Ready" }, - }); - vi.spyOn(resolve, "resolveOpenshell").mockReturnValue(null); - vi.spyOn(dockerImage, "dockerBuild").mockReturnValue({ status: 0 }); - vi.spyOn(rebuildCustomImagePreflight, "preflightRebuildImage").mockResolvedValue({ - ok: true, - imageTag: null, - }); - const imageIdsByRef = new Map([ - [agentBaseImageRef, agentBaseImageId], - [agentBaseImageId, agentBaseImageId], +export function installRebuildFlowTestHooks(options: RebuildFlowTestHookOptions = {}): void { + const restoreRebuildFlowEnv = snapshotEnv([ + "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE", + "NEMOCLAW_SANDBOX_NAME", ]); - const dcodeBaseImageIds = [...(overrides.dcodeBaseImageIds ?? [])]; - vi.spyOn(dockerInspect, "dockerImageInspectFormat").mockImplementation((...args: unknown[]) => { - if ( - args[0] === "{{json .Config.Labels}}" && - overrides.sandboxBaseImageLabelsOutput !== undefined - ) { - return overrides.sandboxBaseImageLabelsOutput; - } - if (args[0] === "{{.Id}}") { - const imageRef = String(args[1]); - if (imageRef === agentBaseImageRef && dcodeBaseImageIds.length > 0) { - return dcodeBaseImageIds.shift()!; - } - const imageId = imageIdsByRef.get(imageRef); - if (imageId) return imageId; - } - return dcodeBaseImageIds.shift() ?? "sha256:dcode-base"; - }); - const dockerRmiSpy = vi.spyOn(dockerImage, "dockerRmi").mockReturnValue({ status: 0 }); - vi.spyOn(dockerImage, "dockerTag").mockImplementation((source: unknown, target: unknown) => { - const sourceRef = String(source); - const sourceId = - imageIdsByRef.get(sourceRef) ?? (sourceRef.startsWith("sha256:") ? sourceRef : null); - if (sourceId) imageIdsByRef.set(String(target), sourceId); - return { status: 0 }; - }); - vi.spyOn(agentDefs, "loadAgent").mockReturnValue(agentDef); - const trustedLocalOverride = { - ref: agentBaseImageRef, - provenance: `${"b".repeat(64)}.${"c".repeat(64)}`, - }; - const ensureAgentBaseImageSpy = vi.spyOn(agentOnboard, "ensureAgentBaseImage").mockReturnValue({ - imageTag: agentBaseImageRef, - built: true, - trustedLocalOverride, - }); - const restoreTrustedAgentBaseImageOverrideSpy = vi.fn(); - const pinTrustedAgentBaseImageOverrideForOperationSpy = vi - .spyOn(agentOnboard, "pinTrustedAgentBaseImageOverrideForOperation") - .mockReturnValue(restoreTrustedAgentBaseImageOverrideSpy); - const restoreTrustedAgentRemoteBaseImageOverrideSpy = vi.fn(); - const pinTrustedAgentRemoteBaseImageOverrideForOperationSpy = vi - .spyOn(agentOnboard, "pinTrustedAgentRemoteBaseImageOverrideForOperation") - .mockReturnValue(restoreTrustedAgentRemoteBaseImageOverrideSpy); - const sessionAgentName = - overrides.sessionAgentName === undefined ? agentName : overrides.sessionAgentName; - vi.spyOn(agentRuntime, "getSessionAgent").mockReturnValue( - sessionAgentName === null || sessionAgentName === "openclaw" - ? null - : ({ name: sessionAgentName } as never), - ); - vi.spyOn(agentRuntime, "getAgentDisplayName").mockReturnValue(agentDisplayName); - vi.spyOn(gatewayRuntime, "recoverNamedGatewayRuntime").mockImplementation( - async (...args: unknown[]) => { - const gatewayName = - (args[0] as { gatewayName?: string } | undefined)?.gatewayName ?? "nemoclaw"; - const state = { state: "healthy_named", activeGateway: gatewayName }; - return ( - overrides.gatewayRecoveryResult ?? { - recovered: true, - attempted: false, - before: state, - after: state, - } - ); - }, - ); - vi.spyOn(gatewayState, "getReconciledSandboxGatewayState").mockResolvedValue( - overrides.reconciledSandboxGatewayState ?? { state: "present", output: "alpha Ready" }, - ); - vi.spyOn(onboardSession, "loadSession").mockReturnValue(session); - vi.spyOn(onboardSession, "acquireOnboardLock").mockReturnValue({ acquired: true }); - vi.spyOn(onboardSession, "updateSession").mockImplementation((mutator: unknown) => { - overrides.updateSession?.(); - if (typeof mutator !== "function") { - throw new TypeError("updateSession expected a mutator function"); + beforeEach(() => { + delete process.env.NEMOCLAW_SANDBOX_NAME; + if (options.acceptThirdPartySoftware) { + process.env.NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE = "1"; } - (mutator as (value: typeof session) => typeof session | void)(session); - return session; }); - const releaseOnboardLockSpy = vi - .spyOn(onboardSession, "releaseOnboardLock") - .mockImplementation(() => undefined); - const finalizeIncompleteOnboardStepSpy = installTerminalStepFailureMock(onboardSession, session); - session.sandboxName = overrides.sessionSandboxName ?? session.sandboxName; - const sandboxEntry = { - name: "alpha", - provider: "ollama-local", - model: "nvidia/nemotron", - policies: ["npm"], - agent: null, - agentVersion: "0.1.0", - // A current managed-image registry row carries positive NemoClaw provenance. - // Tests that exercise the legacy ambiguous-image path override this explicitly. - nemoclawVersion: "0.0.71", - nimContainer: null, - ...(overrides.sandboxEntry ?? {}), - }; - const preDeleteDefaultSandbox = - overrides.preDeleteDefaultSandbox === undefined ? "alpha" : overrides.preDeleteDefaultSandbox; - let sandboxEntryReadCount = 0; - vi.spyOn(registry, "getSandbox").mockImplementation(() => { - const configuredReads = overrides.sandboxEntryReads ?? []; - return ( - sandboxEntryReadCount < configuredReads.length - ? configuredReads[sandboxEntryReadCount++] - : sandboxEntry - ) as never; - }); - let registryLoadCount = 0; - vi.spyOn(registryPersistence, "load").mockImplementation(() => { - const isPreDeleteRead = registryLoadCount > 0; - registryLoadCount++; - return { - defaultSandbox: isPreDeleteRead ? preDeleteDefaultSandbox : "alpha", - sandboxes: { - alpha: - isPreDeleteRead && overrides.preDeleteSandboxEntry - ? overrides.preDeleteSandboxEntry - : sandboxEntry, - }, - }; - }); - vi.spyOn(registry, "listSandboxes").mockReturnValue({ sandboxes: [] }); - const registryUpdateSpy = vi.spyOn(registry, "updateSandbox").mockReturnValue(true); - vi.spyOn(rebuildRoutePreflight, "commitRebuildRoutePreflight").mockImplementation( - (...args: unknown[]) => { - const input = args[0] as { - sandboxName: string; - gatewayName: string; - targetUpdate: Record; - }; - if (!registry.updateSandbox(input.sandboxName, input.targetUpdate)) { - return { - ok: false, - message: "Sandbox registry entry disappeared during rebuild route preflight.", - }; - } - return { - ok: true, - receipt: { - sandboxName: input.sandboxName, - gatewayName: input.gatewayName, - route: { - provider: input.targetUpdate.provider ?? null, - model: input.targetUpdate.model ?? null, - endpointUrl: input.targetUpdate.endpointUrl ?? null, - preferredInferenceApi: input.targetUpdate.preferredInferenceApi ?? null, - credentialEnv: input.targetUpdate.credentialEnv ?? null, - }, - migratedSandboxNames: [], - }, - }; - }, - ); - vi.spyOn(rebuildRoutePreflight, "revalidateRebuildRouteBeforeDelete").mockImplementation( - (...args: unknown[]) => { - const receipt = args[0] as Record; - return overrides.revalidateRebuildRouteBeforeDelete?.(receipt) ?? { ok: true, receipt }; - }, - ); - const restoreSandboxEntrySpy = vi - .spyOn(registry, "restoreSandboxEntry") - .mockImplementation(() => undefined); - const restoreRegistryEntryIfMissingSpy = vi - .spyOn(registry, "restoreSandboxEntryIfMissing") - .mockReturnValue(true); - vi.spyOn(sandboxSession, "getActiveSandboxSessions").mockReturnValue({ - detected: false, - sessions: [], - }); - vi.spyOn(sandboxVersion, "checkAgentVersion").mockReturnValue({ - expectedVersion: "0.2.0", - sandboxVersion: "0.1.0", - }); - vi.spyOn(nim, "detectGpu").mockReturnValue(null); - const routeResults = [...(overrides.dcodeRouteResults ?? [{ ok: true }])]; - const preflightDcodeRouteSpy = vi - .spyOn(rebuildInference, "preflightRebuildInferenceRoute") - .mockImplementation(() => routeResults.shift() ?? { ok: true }); - const preparedDcodeBuildContext = { - buildCtx: "/tmp/dcode-rebuild-context", - stagedDockerfile: "/tmp/dcode-rebuild-context/Dockerfile", - buildId: "dcode-build", - contextFingerprint: "dcode-context", - dockerGpuPatchNetwork: null, - cleanupBuildCtx: vi.fn(() => true), - }; - const prepareManagedDcodeRebuildImageSpy = vi - .spyOn(rebuildManagedImage, "prepareManagedDcodeRebuildImage") - .mockImplementation( - async () => - (overrides.dcodeImageResult ?? { - ok: true, - prepared: preparedDcodeBuildContext, - }) as never, - ); - const disposePreparedDcodeRebuildImageSpy = vi - .spyOn(rebuildManagedImage, "disposePreparedDcodeRebuildImage") - .mockImplementation((prepared: unknown) => - (prepared as { cleanupBuildCtx: () => boolean }).cleanupBuildCtx(), - ); - const imageVerificationResults = [...(overrides.dcodeImageVerificationResults ?? [true])]; - vi.spyOn(rebuildManagedImage, "verifyPreparedDcodeRebuildImage").mockImplementation( - () => imageVerificationResults.shift() ?? true, - ); - const openShieldsSpy = vi - .spyOn(rebuildShields, "openRebuildShieldsWindow") - .mockImplementation(overrides.openShieldsWindow ?? (() => rebuildShieldsWindow)); - const relockSpy = vi - .spyOn(rebuildShields, "relockRebuildShieldsWindow") - .mockImplementation((...args: unknown[]) => { - const window = args[1] as typeof rebuildShieldsWindow; - window.relocked = true; - return true; - }); - const backupSandboxStateSpy = vi.spyOn(sandboxState, "backupSandboxState").mockReturnValue({ - success: true, - backedUpDirs: ["workspace"], - backedUpFiles: ["user.md"], - failedDirs: [], - failedFiles: [], - manifest: { - agentType: overrides.agentName ?? "openclaw", - backupPath: "/tmp/nemoclaw-rebuild-backup", - timestamp: "2026-06-01T00:00:00.000Z", - policyPresets: overrides.backupPolicyPresets ?? ["npm", "bad", "throw"], - }, - }); - vi.spyOn(sandboxState, "validateRebuildRecoveryManifest").mockImplementation( - (...args: unknown[]) => { - const manifest = args[2] as Record; - return overrides.recoveryManifestValidation?.(manifest) ?? { ok: true as const, manifest }; - }, - ); - vi.spyOn(sandboxState, "getLatestBackup").mockImplementation( - () => - (overrides.preDeleteLatestManifest === undefined - ? makePreparedRecoveryManifest() - : overrides.preDeleteLatestManifest) as ReturnType, - ); - vi.spyOn(sandboxState, "hasPositiveManagedImageEvidence").mockReturnValue( - overrides.managedImageEvidence ?? true, - ); - const restoreSandboxStateSpy = vi - .spyOn(sandboxState, "restoreRecreatedSandboxState") - .mockImplementation( - overrides.restoreSandboxState ?? - (() => ({ - success: true, - restoredDirs: ["workspace"], - restoredFiles: ["user.md"], - failedDirs: [], - failedFiles: [], - })), - ); - const captureOpenshellSpy = vi - .spyOn(openshellRuntime, "captureOpenshell") - .mockImplementation((args: unknown, options?: unknown) => { - const argv = Array.isArray(args) ? args.map(String) : []; - if (overrides.captureOpenshell) { - return overrides.captureOpenshell(argv, options as Record | undefined); - } - const probedGateway = sourceSandboxGateway(argv, "get"); - const liveSource = "Name: alpha\nId: sbx-alpha-source\nPhase: Ready\n"; - return probedGateway && !deletedSourceGateways.has(probedGateway) - ? { status: 0, output: liveSource, stdout: liveSource, stderr: "" } - : { - status: 1, - output: "", - stdout: "", - stderr: "Error: sandbox alpha not found", - }; - }); - const deletedSourceGateways = new Set(); - const runOpenshellSpy = vi.spyOn(openshellRuntime, "runOpenshell").mockImplementation((args) => { - const argv = args as string[]; - const deleteGateway = sourceSandboxGateway(argv, "delete"); - if (deleteGateway) { - deletedSourceGateways.add(deleteGateway); - return { status: 0, output: "" }; + afterEach(() => { + vi.restoreAllMocks(); + purgeRebuildModule(); + for (const dir of harnessTempDirs.splice(0)) { + fs.rmSync(dir, { recursive: true, force: true }); } - if ( - argv.join(" ") === "sandbox get alpha" || - argv.join(" ") === "sandbox get -g nemoclaw alpha" - ) { - return { - status: 1, - output: "sandbox alpha not found", - stdout: "", - stderr: "sandbox alpha not found", - }; - } - return argv[0] === "provider" && argv[1] === "get" - ? { - status: 0, - stdout: - "Name: compatible-endpoint\nType: openai\nCredential keys: COMPATIBLE_API_KEY\nConfig keys: OPENAI_BASE_URL\n", - stderr: "", - } - : { status: 0, output: "" }; + restoreRebuildFlowEnv(); }); - const removeSandboxRegistryEntrySpy = vi - .spyOn(destroy, "removeSandboxRegistryEntryWithReceipt") - .mockReturnValue({ - entry: { name: "alpha", imageTag: "old-image" }, - wasDefault: preDeleteDefaultSandbox === "alpha", - fallbackDefault: null, - postRemovalDefaultSelectionRevision: 1, - }); - vi.spyOn(nim, "stopNimContainer").mockImplementation(() => undefined); - vi.spyOn(nim, "stopNimContainerByName").mockImplementation(() => undefined); - const onboardSpy = vi - .spyOn(rebuildOnboardDependencies, "onboard") - .mockImplementation(async () => { - await overrides.onboard?.(session); - }); - vi.spyOn(rebuildOnboardDependencies, "hydrateCredentialEnv").mockImplementation( - (...args: unknown[]) => onboardCredentialEnv.hydrateCredentialEnv(String(args[0] ?? "")), - ); - const preflightAuthoritativeRebuildTargetSpy = vi - .spyOn(rebuildOnboardDependencies, "preflightAuthoritativeRebuildTarget") - .mockImplementation(async (options: unknown) => { - await overrides.preflightAuthoritativeRebuildTarget?.( - (options ?? {}) as Record, - ); - }); - const livePolicyPresets = new Set(overrides.gatewayPresets ?? []); - const managedObservabilityPreset = "observability-otlp-local"; - const managedObservabilityContent = - "network_policies:\n observability-otlp-local:\n name: observability-otlp-local\n"; - let liveManagedObservabilityContent = livePolicyPresets.has(managedObservabilityPreset) - ? managedObservabilityContent - : null; - let policyRemovalObserved = false; - const applyPresetSpy = vi - .spyOn(policies, "applyPreset") - .mockImplementation((_sandboxName: unknown, presetName: unknown) => { - const normalizedPresetName = String(presetName); - let applied: boolean; - if (overrides.applyPreset) { - applied = overrides.applyPreset(normalizedPresetName); - } else if (normalizedPresetName === "throw") { - throw new Error("preset boom"); - } else { - applied = normalizedPresetName === "npm"; - } - if (applied) { - livePolicyPresets.add(normalizedPresetName); - if (normalizedPresetName === managedObservabilityPreset) { - liveManagedObservabilityContent = managedObservabilityContent; - } - } - return applied; - }); - const applyPresetContentSpy = vi - .spyOn(policies, "applyPresetContent") - .mockImplementation((_sandboxName: unknown, presetName: unknown, presetContent: unknown) => { - livePolicyPresets.add(String(presetName)); - const content = String(presetContent); - if (policies.parsePresetPolicyKeys(content).includes(managedObservabilityPreset)) { - liveManagedObservabilityContent = content; - } - return true; - }); - vi.spyOn(policies, "loadPresetForSandbox").mockImplementation( - (_sandboxName: unknown, presetName: unknown) => - String(presetName) === managedObservabilityPreset ? managedObservabilityContent : null, - ); - vi.spyOn(policies, "getPresetContentGatewayState").mockImplementation( - (_sandboxName: unknown, presetContent: unknown) => { - if (overrides.verificationUnavailableAfterPresetRemoval && policyRemovalObserved) return null; - const content = String(presetContent); - if (!policies.parsePresetPolicyKeys(content).includes(managedObservabilityPreset)) { - return "absent"; - } - if (liveManagedObservabilityContent === null) return "absent"; - return liveManagedObservabilityContent === content ? "match" : "drift"; - }, - ); - vi.spyOn(policies, "getGatewayPresets").mockImplementation(() => - overrides.verificationUnavailableAfterPresetRemoval && policyRemovalObserved - ? null - : [...livePolicyPresets], - ); - const removePresetSpy = vi - .spyOn(policies, "removePreset") - .mockImplementation((_sandboxName: unknown, presetName: unknown) => { - const removed = livePolicyPresets.delete(String(presetName)); - if ( - String(presetName) === managedObservabilityPreset && - liveManagedObservabilityContent === managedObservabilityContent - ) { - liveManagedObservabilityContent = null; - } - if (removed) policyRemovalObserved = true; - return removed; - }); - const executeSandboxCommandSpy = vi - .spyOn(processRecovery, "executeSandboxCommand") - .mockImplementation( - overrides.executeSandboxCommand ?? (() => ({ status: 0, stdout: "doctor ok", stderr: "" })), - ); - const checkAndRecoverSandboxProcessesSpy = vi - .spyOn(processRecovery, "checkAndRecoverSandboxProcesses") - .mockImplementation( - overrides.checkAndRecoverSandboxProcesses ?? - (() => ({ - checked: true, - wasRunning: true, - recovered: false, - forwardRecovered: false, - })), - ); - vi.spyOn(shields, "repairMutableConfigPerms").mockImplementation( - overrides.repairMutableConfigPerms ?? (() => ({ applied: true, verified: true, errors: [] })), - ); - vi.spyOn(shields, "isShieldsDown").mockReturnValue(true); - vi.spyOn(shields, "clearShieldsState").mockImplementation(() => undefined); - const messagingRebuildPlanSpy = vi - .spyOn(messaging.MessagingWorkflowPlanner.prototype, "buildRebuildPlanFromSandboxEntry") - .mockImplementation(overrides.buildMessagingRebuildPlan ?? (() => null)); - const preflightMessagingConflictsSpy = vi - .spyOn(rebuildMessagingConflict, "preflightRebuildMessagingConflicts") - .mockImplementation(async () => { - await overrides.preflightMessagingConflicts?.(); - }); - const ensureMessagingHostForwardAfterRebuildSpy = vi - .spyOn(messagingHostForwardLifecycle, "ensureMessagingHostForwardAfterRebuild") - .mockReturnValue(true); - const emptyMcpPreparation = { - entries: [], - detachedProviderEntries: [], - scrubbedAdapterEntries: [], - }; - const prepareMcpBridgesForRebuildSpy = vi - .spyOn(mcpBridge, "prepareMcpBridgesForRebuild") - .mockResolvedValue(overrides.mcpPreparation ?? emptyMcpPreparation); - vi.spyOn(mcpBridge, "prepareMcpBridgesForAbsentSandboxRebuild").mockResolvedValue( - overrides.mcpPreparation ?? emptyMcpPreparation, - ); - const reattachMcpProvidersAfterRebuildAbortSpy = vi - .spyOn(mcpBridge, "reattachMcpProvidersAfterRebuildAbort") - .mockResolvedValue(undefined); - const restoreMcpBridgesAfterRebuildSpy = vi - .spyOn(mcpBridge, "restoreMcpBridgesAfterRebuild") - .mockResolvedValue(undefined); - const warnUnpreservedUserManagedFilesSpy = vi - .spyOn(rebuildFlowHelpers, "warnUnpreservedUserManagedFiles") - .mockImplementation(() => undefined); - - errorSpy.mockClear(); - logSpy.mockClear(); - warnSpy.mockClear(); - - return { - rebuildSandbox: requireDist(rebuildModulePath).rebuildSandbox, - applyPresetSpy, - applyPresetContentSpy, - backupSandboxStateSpy, - disposePreparedDcodeRebuildImageSpy, - dockerRmiSpy, - errorSpy, - ensureAgentBaseImageSpy, - pinTrustedAgentBaseImageOverrideForOperationSpy, - pinTrustedAgentRemoteBaseImageOverrideForOperationSpy, - restoreTrustedAgentBaseImageOverrideSpy, - restoreTrustedAgentRemoteBaseImageOverrideSpy, - executeSandboxCommandSpy, - checkAndRecoverSandboxProcessesSpy, - ensureMessagingHostForwardAfterRebuildSpy, - logSpy, - finalizeIncompleteOnboardStepSpy, - openShieldsSpy, - onboardSpy, - preflightAuthoritativeRebuildTargetSpy, - preflightMessagingConflictsSpy, - preflightDcodeRouteSpy, - prepareManagedDcodeRebuildImageSpy, - removePresetSpy, - removeSandboxRegistryEntrySpy, - registryUpdateSpy, - releaseOnboardLockSpy, - relockSpy, - restoreSandboxEntrySpy, - restoreRegistryEntryIfMissingSpy, - restoreSandboxStateSpy, - captureOpenshellSpy, - runOpenshellSpy, - messagingRebuildPlanSpy, - prepareMcpBridgesForRebuildSpy, - reattachMcpProvidersAfterRebuildAbortSpy, - restoreMcpBridgesAfterRebuildSpy, - warnUnpreservedUserManagedFilesSpy, - preparedDcodeBuildContext, - session, - }; } diff --git a/test/rebuild-stale-recovery.test.ts b/test/rebuild-stale-recovery.test.ts index ccf8598f343..0af4f42b9cd 100644 --- a/test/rebuild-stale-recovery.test.ts +++ b/test/rebuild-stale-recovery.test.ts @@ -32,7 +32,7 @@ import { expectNoSandboxDelete } from "./helpers/rebuild-delete-assertions"; import { createRebuildFlowHarness, installRebuildFlowTestHooks, -} from "./helpers/rebuild-flow-test-harness"; +} from "./helpers/rebuild-flow-generic-harness"; const REPO_ROOT = path.join(import.meta.dirname, ".."); const NODE_BIN = path.dirname(process.execPath); From 3136054a569c8de31a57a9cb769bc077384ab266 Mon Sep 17 00:00:00 2001 From: Tinson Lai Date: Sun, 9 Aug 2026 03:46:21 +0000 Subject: [PATCH 2/2] test(sandbox): withhold third-party acceptance in the generic rebuild profile Signed-off-by: Tinson Lai --- test/helpers/rebuild-flow-harness.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/helpers/rebuild-flow-harness.ts b/test/helpers/rebuild-flow-harness.ts index 092b7046227..177383f6842 100644 --- a/test/helpers/rebuild-flow-harness.ts +++ b/test/helpers/rebuild-flow-harness.ts @@ -102,6 +102,8 @@ export function installRebuildFlowTestHooks(options: RebuildFlowTestHookOptions delete process.env.NEMOCLAW_SANDBOX_NAME; if (options.acceptThirdPartySoftware) { process.env.NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE = "1"; + } else { + delete process.env.NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE; } }); afterEach(() => {