Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8614270
test(e2e): migrate GPU Ollama flow to vitest
cv Jun 19, 2026
d44b775
test(e2e): tighten GPU Ollama migration
cv Jun 19, 2026
3e22d0c
test(e2e): tighten GPU Ollama assertions
cv Jun 19, 2026
3d5f6fb
test(e2e): relax workflow inventory timeout
cv Jun 19, 2026
c2c18a1
Merge remote-tracking branch 'origin/main' into e2e-migrate/test-gpu-e2e
cv Jun 19, 2026
87b3da6
Merge remote-tracking branch 'origin/main' into e2e-migrate/test-gpu-e2e
cv Jun 19, 2026
8c1a6fb
Merge branch 'main' into e2e-migrate/test-gpu-e2e
cv Jun 20, 2026
5f43550
test(e2e): move scenario logic out of test wrapper
cv Jun 20, 2026
1228510
Merge remote-tracking branch 'origin/e2e-migrate/test-gpu-e2e' into e…
cv Jun 20, 2026
1b94314
test(e2e): restore scenario logic to test file
cv Jun 20, 2026
1abe9ee
test(e2e): move GPU setup branches to helpers
cv Jun 20, 2026
528a2c5
test(e2e): remove unused GPU import
cv Jun 20, 2026
ca1ac0b
Merge remote-tracking branch 'origin/main' into e2e-migrate/test-gpu-e2e
cv Jun 20, 2026
9a254b1
Merge branch 'main' into e2e-migrate/test-gpu-e2e
cv Jun 20, 2026
766425e
Merge remote-tracking branch 'origin/main' into e2e-migrate/test-gpu-e2e
cv Jun 20, 2026
9ecdc0a
fix(e2e): harden gpu vitest migration
cv Jun 20, 2026
2f4a804
Merge remote-tracking branch 'origin/e2e-migrate/test-gpu-e2e' into e…
cv Jun 20, 2026
7e29694
fix(e2e): keep gpu test bodies linear
cv Jun 20, 2026
3ea6943
Merge remote-tracking branch 'origin/main' into e2e-migrate/test-gpu-e2e
cv Jun 20, 2026
f319658
Merge remote-tracking branch 'origin/main' into e2e-migrate/test-gpu-e2e
cv Jun 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/e2e-vitest-scenarios.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,53 @@ jobs:
if-no-files-found: ignore
retention-days: 14

gpu-e2e-vitest:
needs: generate-matrix
if: ${{ (inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',gpu-e2e-vitest,') || contains(format(',{0},', inputs.scenarios), ',gpu-e2e,') }}
runs-on: linux-amd64-gpu-rtxpro6000-latest-1
timeout-minutes: 90
env:
FREE_STANDING_VITEST_JOB: "1"
FREE_STANDING_SCENARIO_ID: "gpu-e2e"
E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/vitest/gpu-e2e
NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js
NEMOCLAW_RUN_E2E_SCENARIOS: "1"
NEMOCLAW_NON_INTERACTIVE: "1"
NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1"
NEMOCLAW_PROVIDER: "ollama"
NEMOCLAW_SANDBOX_NAME: "e2e-gpu-ollama"
OPENSHELL_GATEWAY: "nemoclaw"
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.0.0
with:
node-version: 22
cache: npm
- name: Install root dependencies
run: npm ci --ignore-scripts
- name: Build CLI
run: npm run build:cli
- name: Install OpenShell CLI
run: bash scripts/install-openshell.sh
- name: Run GPU Ollama live Vitest test
run: |
set -euo pipefail
export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH"
export OPENSHELL_BIN="$(command -v openshell || true)"
npx vitest run --project e2e-scenarios-live test/e2e-scenario/live/gpu-e2e.test.ts --silent=false --reporter=default
- name: Upload GPU E2E artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: e2e-vitest-scenarios-gpu-e2e
path: e2e-artifacts/vitest/gpu-e2e/
include-hidden-files: false
if-no-files-found: ignore
retention-days: 14

issue-4434-tui-unreachable-inference-vitest:
needs: generate-matrix
if: ${{ (inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',issue-4434-tui-unreachable-inference-vitest,') || contains(format(',{0},', inputs.scenarios), ',issue-4434-tui-unreachable-inference,') }}
Expand Down Expand Up @@ -3586,6 +3633,7 @@ jobs:
openclaw-skill-cli-vitest,
inference-routing-vitest,
cloud-inference-vitest,
gpu-e2e-vitest,
credential-sanitization-vitest,
credential-migration-vitest,
sessions-agents-cli-vitest,
Expand Down
228 changes: 228 additions & 0 deletions test/e2e-scenario/live/gpu-e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

/** Live Vitest replacement for test/e2e/test-gpu-e2e.sh. */

import fs from "node:fs";
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
import path from "node:path";

import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts";
import { resultText } from "../fixtures/clients/index.ts";
import { trustedSandboxShellScript, validateSandboxName } from "../fixtures/clients/sandbox.ts";
import { expect, test } from "../fixtures/e2e-test.ts";
import { shouldRunLiveE2EScenarios } from "../fixtures/live-project-gate.ts";

const REPO_ROOT = path.resolve(import.meta.dirname, "../../..");
const CLI = path.join(REPO_ROOT, "bin", "nemoclaw.js");
const SANDBOX_NAME = process.env.NEMOCLAW_SANDBOX_NAME ?? "e2e-gpu-ollama";
validateSandboxName(SANDBOX_NAME);
const PROXY_PORT = process.env.NEMOCLAW_OLLAMA_PROXY_PORT ?? "11435";
const TIMEOUT_MS = 75 * 60_000;

function env(extra: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
return {
...buildAvailabilityProbeEnv(),
NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1",
NEMOCLAW_NON_INTERACTIVE: "1",
NEMOCLAW_PROVIDER: "ollama",
NEMOCLAW_RECREATE_SANDBOX: "1",
NEMOCLAW_SANDBOX_NAME: SANDBOX_NAME,
OPENSHELL_GATEWAY: process.env.OPENSHELL_GATEWAY ?? "nemoclaw",
...extra,
};
}

async function bestEffort(run: () => Promise<unknown>): Promise<void> {
try {
await run();
} catch {}
}

function chatContent(raw: string): string {
const parsed = JSON.parse(raw) as {
choices?: Array<{ message?: Record<string, unknown>; text?: unknown }>;
};
const choice = parsed.choices?.[0];
const message = choice?.message ?? {};
for (const value of [
message.content,
message.reasoning_content,
message.reasoning,
choice?.text,
]) {
if (typeof value === "string" && value.trim()) return value.trim();
}
return "";
}

test.skipIf(!shouldRunLiveE2EScenarios())(
"GPU Ollama onboard enables CUDA, auth proxy, and sandbox inference",
{ timeout: TIMEOUT_MS },
async ({ artifacts, cleanup, host, sandbox, skip }) => {
await artifacts.writeJson("scenario.json", {
id: "gpu-e2e",
legacySource: "test/e2e/test-gpu-e2e.sh",
boundary:
"GPU host + install.sh Ollama provider + OpenShell sandbox + auth proxy + inference.local",
sandboxName: SANDBOX_NAME,
});

cleanup.add("destroy GPU Ollama sandbox", async () => {
await bestEffort(() =>
host.command("node", [CLI, SANDBOX_NAME, "destroy", "--yes"], {
artifactName: "cleanup-destroy-gpu",
env: env(),
timeoutMs: 120_000,
}),
);
await bestEffort(() =>
sandbox.openshell(["sandbox", "delete", SANDBOX_NAME], {
artifactName: "cleanup-delete-gpu",
env: env(),
timeoutMs: 60_000,
}),
);
await bestEffort(() =>
host.command(
"bash",
[
"-lc",
"pkill -f 'ollama serve' 2>/dev/null || true; pkill -f 'ollama-auth-proxy' 2>/dev/null || true",
],
{ artifactName: "cleanup-ollama-processes", env: env(), timeoutMs: 30_000 },
),
);
});

const docker = await host.command("docker", ["info"], {
artifactName: "docker-info",
env: buildAvailabilityProbeEnv(),
timeoutMs: 30_000,
});
expect(docker.exitCode, resultText(docker)).toBe(0);
const nvidia = await host.command("nvidia-smi", [], {
artifactName: "nvidia-smi",
env: buildAvailabilityProbeEnv(),
timeoutMs: 30_000,
});
if (nvidia.exitCode !== 0) skip(`GPU runner required: ${resultText(nvidia)}`);

const ollamaExists = await host.command("bash", ["-lc", "command -v ollama"], {
artifactName: "command-v-ollama",
env: env(),
timeoutMs: 30_000,
});
if (ollamaExists.exitCode !== 0) {
const installOllama = await host.command(
"bash",
["-lc", "curl -fsSL https://ollama.com/install.sh | sh"],
{ artifactName: "install-ollama", env: env(), timeoutMs: 10 * 60_000 },
);
expect(installOllama.exitCode, resultText(installOllama)).toBe(0);
}

await host.command(
"bash",
[
"-lc",
"systemctl --user stop ollama 2>/dev/null || true; systemctl stop ollama 2>/dev/null || true; pkill -f 'ollama serve' 2>/dev/null || true; pkill -f 'ollama-auth-proxy' 2>/dev/null || true",
],
{ artifactName: "pre-cleanup-ollama", env: env(), timeoutMs: 30_000 },
);

const install = await host.command("bash", ["install.sh", "--non-interactive"], {
artifactName: "install-gpu-ollama",
cwd: REPO_ROOT,
env: env(),
timeoutMs: 45 * 60_000,
});
expect(install.exitCode, resultText(install)).toBe(0);
await artifacts.writeText("install-gpu-ollama.log", resultText(install));

const status = await host.command("node", [CLI, SANDBOX_NAME, "status"], {
artifactName: "status-gpu-ollama",
env: env(),
timeoutMs: 120_000,
});
expect(status.exitCode, resultText(status)).toBe(0);
expect(resultText(status)).toContain("Sandbox GPU: enabled");
expect(resultText(status)).toMatch(/CUDA verified|CUDA unverified|last CUDA proof failed/i);
expect(resultText(status)).not.toMatch(/last CUDA proof failed|CUDA unverified/i);

const log = resultText(install);
expect(log).toContain("GPU proof passed: nvidia-smi when available");
expect(log).toContain("GPU proof passed: cuInit(0) via libcuda.so.1");

const tokenFile = path.join(process.env.HOME ?? "", ".nemoclaw", "ollama-proxy-token");
expect(fs.existsSync(tokenFile)).toBe(true);
expect((fs.statSync(tokenFile).mode & 0o777).toString(8)).toBe("600");
const token = fs.readFileSync(tokenFile, "utf8").trim();
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
expect(token).not.toBe("");

const proxyUnauth = await host.command(
"curl",
[
"-s",
"-o",
"/dev/null",
"-w",
"%{http_code}",
"-X",
"POST",
`http://127.0.0.1:${PROXY_PORT}/api/generate`,
"-d",
"{}",
],
{ artifactName: "proxy-unauth-generate-status", env: env(), timeoutMs: 30_000 },
);
expect(proxyUnauth.stdout.trim()).toBe("401");

const model =
process.env.NEMOCLAW_MODEL ||
(
await host.command(
"bash",
[
"-lc",
'curl -sf http://127.0.0.1:11434/api/tags | python3 -c \'import json,sys; m=json.load(sys.stdin).get("models",[]); print(m[0]["name"] if m else "")\'',
],
{ artifactName: "detect-ollama-model", env: env(), timeoutMs: 30_000 },
)
).stdout.trim();
expect(model).not.toBe("");

const payload = JSON.stringify({
model,
messages: [{ role: "user", content: "Reply with exactly one word: PONG" }],
max_tokens: 200,
});
const direct = await host.command(
"curl",
[
"-s",
"--max-time",
"120",
"-X",
"POST",
"http://127.0.0.1:11434/v1/chat/completions",
"-H",
"Content-Type: application/json",
"-d",
payload,
],
{ artifactName: "direct-ollama-chat", env: env(), timeoutMs: 150_000 },
);
expect(direct.exitCode, resultText(direct)).toBe(0);
expect(chatContent(direct.stdout)).toMatch(/PONG/i);

const sandboxChat = await sandbox.execShell(
SANDBOX_NAME,
trustedSandboxShellScript(
`curl -skS --max-time 90 https://inference.local/v1/chat/completions -H 'Content-Type: application/json' -d '${payload.replace(/'/gu, `'\\''`)}'`,
),
{ artifactName: "sandbox-inference-local-chat", env: env(), timeoutMs: 150_000 },
);
expect(sandboxChat.exitCode, resultText(sandboxChat)).toBe(0);
expect(chatContent(sandboxChat.stdout)).toMatch(/PONG/i);
},
);