diff --git a/test/e2e/fixtures/environment-profiles.ts b/test/e2e/fixtures/environment-profiles.ts new file mode 100644 index 00000000000..a6e66c35377 --- /dev/null +++ b/test/e2e/fixtures/environment-profiles.ts @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import os from "node:os"; +import path from "node:path"; + +import { buildAvailabilityProbeEnv } from "./availability-env.ts"; + +function withDefaults( + base: NodeJS.ProcessEnv, + extra: NodeJS.ProcessEnv, + gateway: string | undefined, +): NodeJS.ProcessEnv { + return { + ...base, + NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1", + NEMOCLAW_NON_INTERACTIVE: "1", + OPENSHELL_GATEWAY: gateway ?? "nemoclaw", + ...extra, + }; +} + +function withInstalledCliPath(base: NodeJS.ProcessEnv, home: string): NodeJS.ProcessEnv { + const entries = [ + path.join(home, ".local", "bin"), + path.join(home, ".npm-global", "bin"), + ...(base.PATH?.split(path.delimiter) ?? []), + ]; + return { + ...base, + HOME: home, + PATH: [...new Set(entries.filter(Boolean))].join(path.delimiter), + }; +} + +export function commandEnvironment( + extra: NodeJS.ProcessEnv = {}, + source: NodeJS.ProcessEnv = process.env, +): NodeJS.ProcessEnv { + return withDefaults(buildAvailabilityProbeEnv(source), extra, source.OPENSHELL_GATEWAY); +} + +export function installedCommandEnvironment( + extra: NodeJS.ProcessEnv = {}, + home = os.homedir(), + source: NodeJS.ProcessEnv = process.env, +): NodeJS.ProcessEnv { + const base = buildAvailabilityProbeEnv({ ...source, HOME: home }); + return withDefaults(withInstalledCliPath(base, home), extra, source.OPENSHELL_GATEWAY); +} + +export function testHomeEnvironment( + home: string, + extra: NodeJS.ProcessEnv = {}, + source: NodeJS.ProcessEnv = process.env, +): NodeJS.ProcessEnv { + return installedCommandEnvironment(extra, home, source); +} + +export function sandboxCommandEnvironment( + sandboxName: string, + extra: NodeJS.ProcessEnv = {}, + source: NodeJS.ProcessEnv = process.env, +): NodeJS.ProcessEnv { + return commandEnvironment( + { + NEMOCLAW_RECREATE_SANDBOX: "1", + NEMOCLAW_SANDBOX_NAME: sandboxName, + ...extra, + }, + source, + ); +} diff --git a/test/e2e/live/bedrock-runtime-compatible-anthropic.test.ts b/test/e2e/live/bedrock-runtime-compatible-anthropic.test.ts index 1f53550ef4c..af2486c4155 100644 --- a/test/e2e/live/bedrock-runtime-compatible-anthropic.test.ts +++ b/test/e2e/live/bedrock-runtime-compatible-anthropic.test.ts @@ -19,6 +19,7 @@ import { validateSandboxName, } from "../fixtures/clients/sandbox.ts"; import { expect, test } from "../fixtures/e2e-test.ts"; +import { testHomeEnvironment } from "../fixtures/environment-profiles.ts"; import { shouldRunLiveE2E } from "../fixtures/live-project-gate.ts"; import { redactString } from "../fixtures/redaction.ts"; import { @@ -134,16 +135,7 @@ function assertAgent(value: string): asserts value is AgentName { } function testEnv(home: string, extra: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv { - const base = buildAvailabilityProbeEnv({ ...process.env, HOME: home }); - return { - ...base, - HOME: home, - PATH: [path.join(home, ".local", "bin"), base.PATH].filter(Boolean).join(":"), - NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1", - NEMOCLAW_NON_INTERACTIVE: "1", - OPENSHELL_GATEWAY: process.env.OPENSHELL_GATEWAY ?? "nemoclaw", - ...extra, - }; + return testHomeEnvironment(home, extra); } function onboardEnv(home: string, agent: AgentName): NodeJS.ProcessEnv { diff --git a/test/e2e/live/cloud-inference.test.ts b/test/e2e/live/cloud-inference.test.ts index 5f2574f0653..04abd8c69b5 100644 --- a/test/e2e/live/cloud-inference.test.ts +++ b/test/e2e/live/cloud-inference.test.ts @@ -17,6 +17,7 @@ import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts"; import type { HostCliClient } from "../fixtures/clients/host.ts"; import { type SandboxClient, validateSandboxName } from "../fixtures/clients/sandbox.ts"; import { expect, test } from "../fixtures/e2e-test.ts"; +import { testHomeEnvironment } from "../fixtures/environment-profiles.ts"; import { requireHostedInferenceConfig } from "../fixtures/hosted-inference.ts"; import { shouldRunLiveE2E } from "../fixtures/live-project-gate.ts"; import type { ShellProbeResult } from "../fixtures/shell-probe.ts"; @@ -86,18 +87,7 @@ async function writePreContractExternalProviderSkip( } function testEnv(home: string, extra: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv { - const base = buildAvailabilityProbeEnv(); - return { - ...base, - HOME: home, - PATH: [path.join(home, ".local", "bin"), path.join(home, ".npm-global", "bin"), base.PATH] - .filter(Boolean) - .join(":"), - NEMOCLAW_NON_INTERACTIVE: "1", - NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1", - OPENSHELL_GATEWAY: "nemoclaw", - ...extra, - }; + return testHomeEnvironment(home, extra, { ...process.env, OPENSHELL_GATEWAY: "nemoclaw" }); } async function bestEffort(run: () => Promise): Promise { diff --git a/test/e2e/live/credential-migration.test.ts b/test/e2e/live/credential-migration.test.ts index aa564bafeff..85d419bf108 100644 --- a/test/e2e/live/credential-migration.test.ts +++ b/test/e2e/live/credential-migration.test.ts @@ -9,6 +9,7 @@ import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts"; import type { HostCliClient } from "../fixtures/clients/host.ts"; import { validateSandboxName } from "../fixtures/clients/sandbox.ts"; import { expect, test } from "../fixtures/e2e-test.ts"; +import { testHomeEnvironment } from "../fixtures/environment-profiles.ts"; import { requireHostedInferenceConfig } from "../fixtures/hosted-inference.ts"; import { shouldRunLiveE2E } from "../fixtures/live-project-gate.ts"; @@ -40,15 +41,7 @@ function resultText(result: { stdout: string; stderr: string }): string { } function testEnv(home: string, extra: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv { - const base = buildAvailabilityProbeEnv(); - return { - ...base, - HOME: home, - PATH: [path.join(home, ".local", "bin"), base.PATH].filter(Boolean).join(":"), - NEMOCLAW_NON_INTERACTIVE: "1", - NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1", - ...extra, - }; + return testHomeEnvironment(home, extra); } async function bestEffort(run: () => Promise): Promise { diff --git a/test/e2e/live/credential-sanitization.test.ts b/test/e2e/live/credential-sanitization.test.ts index ceca327dee9..b3eebd0257c 100644 --- a/test/e2e/live/credential-sanitization.test.ts +++ b/test/e2e/live/credential-sanitization.test.ts @@ -25,6 +25,7 @@ import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts"; import type { HostCliClient } from "../fixtures/clients/host.ts"; import { type SandboxClient, validateSandboxName } from "../fixtures/clients/sandbox.ts"; import { expect, test } from "../fixtures/e2e-test.ts"; +import { testHomeEnvironment } from "../fixtures/environment-profiles.ts"; const REPO_ROOT = path.resolve(import.meta.dirname, "../../.."); const CLI_ENTRYPOINT = path.join(REPO_ROOT, "bin", "nemoclaw.js"); @@ -49,16 +50,7 @@ function resultText(result: CommandText): string { } function testEnv(home: string, extra: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv { - const base = buildAvailabilityProbeEnv(); - return { - ...base, - HOME: home, - PATH: [path.join(home, ".local", "bin"), base.PATH].filter(Boolean).join(":"), - NEMOCLAW_NON_INTERACTIVE: "1", - NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1", - OPENSHELL_GATEWAY: process.env.OPENSHELL_GATEWAY ?? "nemoclaw", - ...extra, - }; + return testHomeEnvironment(home, extra); } async function bestEffort(run: () => Promise): Promise { diff --git a/test/e2e/live/diagnostics.test.ts b/test/e2e/live/diagnostics.test.ts index 041b87f7ed1..5b34407497a 100644 --- a/test/e2e/live/diagnostics.test.ts +++ b/test/e2e/live/diagnostics.test.ts @@ -16,6 +16,7 @@ import path from "node:path"; import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts"; import { validateSandboxName } from "../fixtures/clients/sandbox.ts"; import { expect, test } from "../fixtures/e2e-test.ts"; +import { testHomeEnvironment } from "../fixtures/environment-profiles.ts"; import { requireHostedInferenceConfig } from "../fixtures/hosted-inference.ts"; import { shouldRunLiveE2E } from "../fixtures/live-project-gate.ts"; import type { ShellProbeResult } from "../fixtures/shell-probe.ts"; @@ -73,19 +74,12 @@ function runRawNodeCliForLeakAssertion( } function testEnv(home: string, extra: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv { - const base = buildAvailabilityProbeEnv(); - return { - ...base, - HOME: home, - PATH: [path.join(home, ".local", "bin"), base.PATH].filter(Boolean).join(":"), - NEMOCLAW_NON_INTERACTIVE: "1", - NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1", + return testHomeEnvironment(home, { NEMOCLAW_RECREATE_SANDBOX: "1", NEMOCLAW_SANDBOX_NAME: SANDBOX_NAME, NEMOCLAW_DISABLE_GATEWAY_DRIFT_PREFLIGHT: "1", - OPENSHELL_GATEWAY: process.env.OPENSHELL_GATEWAY ?? "nemoclaw", ...extra, - }; + }); } async function bestEffort(run: () => Promise): Promise { diff --git a/test/e2e/live/openclaw-skill-cli.test.ts b/test/e2e/live/openclaw-skill-cli.test.ts index b9e4eb46547..452066cd74a 100644 --- a/test/e2e/live/openclaw-skill-cli.test.ts +++ b/test/e2e/live/openclaw-skill-cli.test.ts @@ -15,6 +15,7 @@ import { validateSandboxName, } from "../fixtures/clients/sandbox.ts"; import { expect, test } from "../fixtures/e2e-test.ts"; +import { testHomeEnvironment } from "../fixtures/environment-profiles.ts"; import { requireHostedInferenceConfig } from "../fixtures/hosted-inference.ts"; import { shouldRunLiveE2E } from "../fixtures/live-project-gate.ts"; import type { ShellProbeResult } from "../fixtures/shell-probe.ts"; @@ -52,18 +53,7 @@ function singleLineSandboxScript(script: string) { } function testEnv(home: string, extra: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv { - const base = buildAvailabilityProbeEnv(); - return { - ...base, - HOME: home, - PATH: [path.join(home, ".local", "bin"), path.join(home, ".npm-global", "bin"), base.PATH] - .filter(Boolean) - .join(":"), - NEMOCLAW_NON_INTERACTIVE: "1", - NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1", - OPENSHELL_GATEWAY: process.env.OPENSHELL_GATEWAY ?? "nemoclaw", - ...extra, - }; + return testHomeEnvironment(home, extra); } async function bestEffort(run: () => Promise): Promise { diff --git a/test/e2e/support/e2e-environment-profiles.test.ts b/test/e2e/support/e2e-environment-profiles.test.ts new file mode 100644 index 00000000000..71dd2cec0fa --- /dev/null +++ b/test/e2e/support/e2e-environment-profiles.test.ts @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import path from "node:path"; + +import { describe, expect, it } from "vitest"; + +import { + commandEnvironment, + sandboxCommandEnvironment, + testHomeEnvironment, +} from "../fixtures/environment-profiles.ts"; + +describe("E2E environment profiles", () => { + it("filters the source and lets caller overrides win without mutation", () => { + const source = { + PATH: "/usr/bin", + OPENSHELL_GATEWAY: "source-gateway", + UNRELATED_SECRET: "must-not-pass", + }; + const extra = { + NEMOCLAW_NON_INTERACTIVE: "override", + NVIDIA_INFERENCE_API_KEY: "test-secret-overlay", + }; + + const result = commandEnvironment(extra, source); + + expect(result).toMatchObject({ + PATH: "/usr/bin", + NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1", + NEMOCLAW_NON_INTERACTIVE: "override", + NVIDIA_INFERENCE_API_KEY: "test-secret-overlay", + OPENSHELL_GATEWAY: "source-gateway", + }); + expect(result.UNRELATED_SECRET).toBeUndefined(); + expect(source).toEqual({ + PATH: "/usr/bin", + OPENSHELL_GATEWAY: "source-gateway", + UNRELATED_SECRET: "must-not-pass", + }); + }); + + it("centralizes test HOME CLI paths with caller precedence", () => { + const home = path.join(path.sep, "tmp", "nemoclaw-test-home"); + const result = testHomeEnvironment(home, { HOME: "/override-home" }, { PATH: "/usr/bin" }); + + expect(result.HOME).toBe("/override-home"); + expect(result.PATH?.split(path.delimiter)).toEqual([ + path.join(home, ".local", "bin"), + path.join(home, ".npm-global", "bin"), + "/usr/bin", + ]); + }); + + it("composes sandbox identity and secret-bearing overlays", () => { + const result = sandboxCommandEnvironment( + "e2e-profile", + { + COMPATIBLE_API_KEY: "compatible-secret", + NEMOCLAW_RECREATE_SANDBOX: "0", + }, + { PATH: "/usr/bin" }, + ); + + expect(result).toMatchObject({ + COMPATIBLE_API_KEY: "compatible-secret", + NEMOCLAW_RECREATE_SANDBOX: "0", + NEMOCLAW_SANDBOX_NAME: "e2e-profile", + OPENSHELL_GATEWAY: "nemoclaw", + }); + }); +});