Skip to content
Merged
11 changes: 8 additions & 3 deletions test/dcode-wrapper-empty-prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,17 @@ type WrapperRun = {
function runWrapper(args: string[]): WrapperRun {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-wrapper-"));
try {
fs.copyFileSync(WRAPPER, path.join(dir, "dcode"));
fs.chmodSync(path.join(dir, "dcode"), 0o755);

const marker = path.join(dir, "launched.txt");
const bin = path.join(dir, "bin");
fs.mkdirSync(bin);
const wrapperFixture = fs
.readFileSync(WRAPPER, "utf8")
.replace(
'export PATH="/usr/local/bin:/opt/venv/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin"',
`export PATH="${bin}:/usr/local/bin:/opt/venv/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin"`,
);
fs.writeFileSync(path.join(dir, "dcode"), wrapperFixture, { mode: 0o755 });

fs.writeFileSync(
path.join(bin, "python3"),
`#!/usr/bin/env bash\nprintf '%s' "$*" > ${JSON.stringify(marker)}\nexit 0\n`,
Expand Down
52 changes: 51 additions & 1 deletion test/e2e-scenario/fixtures/phases/onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export interface OnboardingExpectedFailure {
export interface NemoClawInstance {
onboarding: string;
sandboxName: string;
agent: "openclaw" | "hermes";
agent: "openclaw" | "hermes" | "langchain-deepagents-code";
provider: "nvidia" | "ollama";
providerEnv: "cloud" | "local";
platformOs?: "ubuntu" | "macos" | "windows";
Expand Down Expand Up @@ -180,6 +180,9 @@ export class OnboardingPhaseFixture {
case "cloud-openclaw-no-docker":
result = await this.cloudOpenClawNoDocker(environment, options);
break;
case "cloud-langchain-deepagents-code":
result = await this.cloudLangchainDeepAgentsCode(environment, options);
break;
default:
throw new Error(`Unsupported onboarding profile '${environment.onboarding}'.`);
}
Expand Down Expand Up @@ -219,6 +222,53 @@ export class OnboardingPhaseFixture {
};
}

async cloudLangchainDeepAgentsCode(
environment: EnvironmentReady,
options: OnboardingOptions = {},
): Promise<NemoClawInstance> {
if (!environment.docker.available) {
throw new Error(
"cloud-langchain-deepagents-code onboarding requires an available Docker runtime.",
);
}
const sandboxName = sandboxNameFromOptions(environment.onboarding, options);
const apiKey = this.secrets.required("NVIDIA_INFERENCE_API_KEY");
this.registerSandboxCleanup(sandboxName);
const result = await this.host.nemoclaw(ONBOARD_ARGS, {
artifactName: "onboard-cloud-langchain-deepagents-code",
env: commandEnv(sandboxName, {
NEMOCLAW_AGENT: "langchain-deepagents-code",
NEMOCLAW_E2E_USE_HOSTED_INFERENCE: "1",
NEMOCLAW_PROVIDER: HOSTED_INFERENCE_PROVIDER,
NEMOCLAW_ENDPOINT_URL:
process.env.NEMOCLAW_ENDPOINT_URL || DEFAULT_HOSTED_INFERENCE_BASE_URL,
NEMOCLAW_MODEL:
process.env.NEMOCLAW_MODEL ||
process.env.NEMOCLAW_COMPAT_MODEL ||
DEFAULT_HOSTED_INFERENCE_MODEL,
NEMOCLAW_COMPAT_MODEL:
process.env.NEMOCLAW_MODEL ||
process.env.NEMOCLAW_COMPAT_MODEL ||
DEFAULT_HOSTED_INFERENCE_MODEL,
NEMOCLAW_PREFERRED_API: process.env.NEMOCLAW_PREFERRED_API || "openai-completions",
NVIDIA_INFERENCE_API_KEY: apiKey,
[HOSTED_INFERENCE_CREDENTIAL_ENV]: apiKey,
}),
redactionValues: [apiKey],
timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
});
assertExitZero(result, "cloud-langchain-deepagents-code onboarding");
return {
onboarding: environment.onboarding,
sandboxName,
agent: "langchain-deepagents-code",
provider: "nvidia",
providerEnv: "cloud",
gatewayUrl: OPENCLAW_GATEWAY_URL,
result,
};
}

async cloudOpenClawNoDocker(
environment: EnvironmentReady,
options: OnboardingOptions = {},
Expand Down
15 changes: 15 additions & 0 deletions test/e2e-scenario/live/cloud-experimental-check-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

export const DEEPAGENTS_CLOUD_EXPERIMENTAL_CHECKS = [
"test/e2e/e2e-cloud-experimental/checks/05-deepagents-code-landlock-readonly.sh",
"test/e2e/e2e-cloud-experimental/checks/06-deepagents-code-python-egress.sh",
] as const;

export function cloudExperimentalChecksForOnboarding(
onboarding: string | undefined,
): readonly string[] {
return onboarding === "cloud-langchain-deepagents-code"
? DEEPAGENTS_CLOUD_EXPERIMENTAL_CHECKS
: [];
}
95 changes: 95 additions & 0 deletions test/e2e-scenario/live/cloud-experimental-checks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import path from "node:path";

import { expect } from "vitest";
import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts";
import { resultText } from "../fixtures/clients/command.ts";
import type { E2EScenarioFixtures } from "../fixtures/e2e-test.ts";
import type { ShellProbeResult } from "../fixtures/shell-probe.ts";

const REPO_ROOT = path.resolve(import.meta.dirname, "../../..");
const REQUIRED_CHECK_SKIP_PATTERN = /(^|\n).*\bSKIP\b/i;

export function buildCloudExperimentalCommandEnv(
sandboxName: string,
apiKey: string,
base: NodeJS.ProcessEnv = process.env,
): NodeJS.ProcessEnv {
return {
...buildAvailabilityProbeEnv(base),
CLOUD_EXPERIMENTAL_MODEL: base.NEMOCLAW_MODEL,
COMPATIBLE_API_KEY: apiKey,
NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1",
NEMOCLAW_E2E_CLOUD_API_KEY_ENV: "COMPATIBLE_API_KEY",
NEMOCLAW_NON_INTERACTIVE: "1",
NEMOCLAW_SANDBOX_NAME: sandboxName,
OPENSHELL_GATEWAY: "nemoclaw",
REPO: REPO_ROOT,
SANDBOX_NAME: sandboxName,
};
}

export function assertRequiredCloudExperimentalResult(
scriptPath: string,
result: ShellProbeResult,
): void {
const output = resultText(result);
expect(result.exitCode, `${scriptPath}: ${output}`).toBe(0);
expect(output, `${scriptPath}: required cloud-experimental check must not skip`).not.toMatch(
REQUIRED_CHECK_SKIP_PATTERN,
);
}

async function assertDeepAgentsRuntimeObserved(
sandboxName: string,
context: Pick<E2EScenarioFixtures, "host">,
): Promise<void> {
const result = await context.host.command(
"openshell",
[
"sandbox",
"exec",
"--name",
sandboxName,
"--",
"bash",
"-c",
"test -d /sandbox/.deepagents && command -v dcode >/dev/null",
],
{
artifactName: "cloud-experimental-deepagents-runtime",
env: buildCloudExperimentalCommandEnv(sandboxName, ""),
timeoutMs: 30_000,
},
);
expect(result.exitCode, `Deep Agents Code runtime marker missing: ${resultText(result)}`).toBe(0);
}

export async function runE2eCloudExperimentalChecks(
scenarioId: string,
sandboxName: string,
checkScripts: readonly string[],
context: Pick<E2EScenarioFixtures, "artifacts" | "host" | "secrets">,
): Promise<void> {
const apiKey = context.secrets.optional("NVIDIA_INFERENCE_API_KEY") ?? "";
await context.artifacts.writeJson("e2e-cloud-experimental-checks.json", {
scenarioId,
sandboxName,
checkScripts,
});
await Promise.resolve(
checkScripts.length > 0 ? assertDeepAgentsRuntimeObserved(sandboxName, context) : undefined,
);
for (const scriptPath of checkScripts) {
const result = await context.host.command("bash", [path.join(REPO_ROOT, scriptPath)], {
artifactName: `cloud-experimental-${path.basename(scriptPath, ".sh")}`,
cwd: REPO_ROOT,
env: buildCloudExperimentalCommandEnv(sandboxName, apiKey),
redactionValues: [apiKey],
timeoutMs: 180_000,
});
assertRequiredCloudExperimentalResult(scriptPath, result);
}
}
73 changes: 72 additions & 1 deletion test/e2e-scenario/live/gpu-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,68 @@ import {

const TIMEOUT_MS = 75 * 60_000;

function asRecord(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}

function modelIdentifier(value: Record<string, unknown>, key: string): string | undefined {
return typeof value[key] === "string" ? (value[key] as string) : undefined;
}

function assertSmallContextCompactionPolicy(configText: string): void {
const config = asRecord(JSON.parse(configText));
const agents = asRecord(config?.agents);
const defaults = asRecord(agents?.defaults);
const modelDefaults = asRecord(defaults?.model);
const primary = modelIdentifier(modelDefaults ?? {}, "primary");
const compaction = asRecord(defaults?.compaction);
const modelsRoot = asRecord(config?.models);
const providers = asRecord(modelsRoot?.providers);
const primaryWithoutProvider = primary?.startsWith("inference/")
? primary.slice("inference/".length)
: primary;
const model = Object.values(providers ?? {})
.flatMap((provider) => {
const models = asRecord(provider)?.models;
return Array.isArray(models) ? models : [];
})
.map(asRecord)
.find((candidate) => {
const identifiers =
candidate && primary && primaryWithoutProvider
? ["id", "name", "label"].flatMap((key) => {
const value = modelIdentifier(candidate, key);
return value ? [value] : [];
})
: [];
return identifiers.some(
(identifier) =>
identifier === primary ||
identifier === primaryWithoutProvider ||
identifier === `inference/${primaryWithoutProvider}`,
);
});

expect(primary, "OpenClaw config must declare the active model").toBeTruthy();
expect(model, `OpenClaw config must include active Ollama model ${primary}`).toBeDefined();
expect(typeof model?.contextWindow).toBe("number");
expect(typeof model?.maxTokens).toBe("number");
const contextWindow = model?.contextWindow as number;
const maxTokens = model?.maxTokens as number;
expect(
contextWindow,
`active Ollama model ${primary} must stay on the small-context lane`,
).toBeLessThanOrEqual(28_000);
const expectedReserve = Math.min(maxTokens, Math.max(0, contextWindow - 8_000));

expect(compaction).toEqual({
reserveTokens: expectedReserve,
reserveTokensFloor: expectedReserve,
});
}

test.skipIf(!shouldRunLiveE2EScenarios())(
"GPU Ollama onboard enables CUDA, auth proxy, and sandbox inference",
{ timeout: TIMEOUT_MS },
Expand All @@ -43,7 +105,7 @@ test.skipIf(!shouldRunLiveE2EScenarios())(
sandboxName: SANDBOX_NAME,
delegatedLegacyContracts: [
"Phase 11 shell retirement decides whether uninstall --delete-models remains a separate cleanup lane",
"The #5468 OpenClaw TUI compaction guard remains in the retained legacy shell until a TUI fixture exists",
"The #5468 interactive TUI first-turn smoke remains waived until a TUI fixture exists; this Vitest asserts the baked compaction budget directly",
],
});

Expand Down Expand Up @@ -75,6 +137,15 @@ test.skipIf(!shouldRunLiveE2EScenarios())(
expect(install.exitCode, resultText(install)).toBe(0);
await artifacts.writeText("install-gpu-ollama.log", resultText(install));

const config = await sandbox.execShell(
SANDBOX_NAME,
trustedSandboxShellScript("cat /sandbox/.openclaw/openclaw.json"),
{ artifactName: "sandbox-openclaw-config", env: env(), timeoutMs: 30_000 },
);
expect(config.exitCode, resultText(config)).toBe(0);
await artifacts.writeText("openclaw-config.json", config.stdout);
assertSmallContextCompactionPolicy(config.stdout);

const status = await host.command("node", [CLI, SANDBOX_NAME, "status"], {
artifactName: "status-gpu-ollama",
env: env(),
Expand Down
25 changes: 23 additions & 2 deletions test/e2e-scenario/live/registry-scenarios.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { expect, test } from "../fixtures/e2e-test.ts";
import type { LifecycleProfile } from "../fixtures/phases/index.ts";
import { listScenarios } from "../scenarios/registry.ts";
import { liveScenarioSupport, liveScenarioTestName } from "../scenarios/runtime-support.ts";
import { cloudExperimentalChecksForOnboarding } from "./cloud-experimental-check-list.ts";
import { runE2eCloudExperimentalChecks } from "./cloud-experimental-checks.ts";
import { buildLiveScenarioRunPlan } from "./run-plan.ts";

const LIFECYCLE_PROFILES: ReadonlySet<LifecycleProfile> = new Set(["post-reboot-recovery"]);
Expand All @@ -18,6 +20,10 @@ function isLifecycleProfile(value: string | undefined): value is LifecycleProfil

const REPO_ROOT = path.resolve(import.meta.dirname, "../../..");
const CLI_DIST_ENTRYPOINT = path.join(REPO_ROOT, "dist", "nemoclaw.js");
const E2E_CLOUD_EXPERIMENTAL_CHECKS_DIR = path.join(
REPO_ROOT,
"test/e2e/e2e-cloud-experimental/checks",
);
process.env.NEMOCLAW_CLI_BIN ??= path.join(REPO_ROOT, "bin", "nemoclaw.js");

// The workflow filters by exact scenario id via `-t "^${SCENARIO_ID}$"`.
Expand All @@ -38,7 +44,7 @@ for (const scenario of listScenarios()) {

test(
liveScenarioTestName(scenario),
async ({ artifacts, environment, lifecycle, onboard, secrets, stateValidation }) => {
async ({ artifacts, environment, host, lifecycle, onboard, secrets, stateValidation }) => {
for (const secret of scenario.requiredSecrets ?? []) {
secrets.required(secret);
}
Expand All @@ -61,7 +67,8 @@ for (const scenario of listScenarios()) {
pendingRuntimeSuites: support.pendingRuntimeSuites,
});

await artifacts.writeJson("run-plan.json", buildLiveScenarioRunPlan(scenario));
const runPlan = buildLiveScenarioRunPlan(scenario);
await artifacts.writeJson("run-plan.json", runPlan);

const ready = await environment.assertReady(scenario.environment);
const instance = await onboard.from(ready, { sandboxName: `e2e-${scenario.id}` });
Expand Down Expand Up @@ -92,6 +99,20 @@ for (const scenario of listScenarios()) {

const validation = await stateValidation.from(scenario.expectedStateId, instance);

const checkScripts = runPlan.e2eCloudExperimentalChecks ?? [];
expect(checkScripts).toEqual(
cloudExperimentalChecksForOnboarding(scenario.environment.onboarding),
);
for (const scriptPath of checkScripts) {
expect(fs.existsSync(path.join(REPO_ROOT, scriptPath))).toBe(true);
}
expect(fs.existsSync(E2E_CLOUD_EXPERIMENTAL_CHECKS_DIR)).toBe(true);
await runE2eCloudExperimentalChecks(scenario.id, instance.sandboxName, checkScripts, {
artifacts,
host,
secrets,
});

await artifacts.writeJson("scenario-result.json", {
id: scenario.id,
expectedStateId: validation.state.id,
Expand Down
11 changes: 10 additions & 1 deletion test/e2e-scenario/live/run-plan.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { cloudExperimentalChecksForOnboarding } from "./cloud-experimental-check-list.ts";
import type { ScenarioDefinition } from "../scenarios/types.ts";

export interface LiveScenarioRunPlan {
Expand All @@ -9,10 +10,11 @@ export interface LiveScenarioRunPlan {
expectedStateId: string | undefined;
suiteIds: string[];
phases: string[];
e2eCloudExperimentalChecks?: string[];
}

export function buildLiveScenarioRunPlan(scenario: ScenarioDefinition): LiveScenarioRunPlan {
return {
const plan: LiveScenarioRunPlan = {
scenarioId: scenario.id,
manifestPath: scenario.manifestPath ?? null,
expectedStateId: scenario.expectedStateId,
Expand All @@ -24,4 +26,11 @@ export function buildLiveScenarioRunPlan(scenario: ScenarioDefinition): LiveScen
"state-validation",
],
};
const cloudExperimentalChecks = cloudExperimentalChecksForOnboarding(
scenario.environment?.onboarding,
);
if (cloudExperimentalChecks.length > 0) {
plan.e2eCloudExperimentalChecks = [...cloudExperimentalChecks];
}
return plan;
}
Loading