Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
102 changes: 102 additions & 0 deletions test/helpers/openclaw-gemini-runtime-image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { stripVTControlCharacters } from "node:util";

import type {
DockerSpawnSyncOptions,
DockerSpawnSyncResult,
} from "../../src/lib/adapters/docker/exec";
import { dockerSpawnSync } from "../../src/lib/adapters/docker/exec";
import { redact, redactFull } from "../../src/lib/security/redact";

export const OPENCLAW_GEMINI_IMAGE_INSPECT_TIMEOUT_MS = 15_000;
export const OPENCLAW_GEMINI_IMAGE_PULL_TIMEOUT_MS = 5 * 60_000;

const PULL_CAPTURE_MAX_BYTES = 256 * 1024;
const PULL_DIAGNOSTIC_MAX_BYTES = 4 * 1024;
const TRUNCATION_SUFFIX = "\n[diagnostic truncated]";
const UNSAFE_TERMINAL_CONTROL_CHARACTERS =
/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/gu;

export type DockerImageSetupRunner = (
args: readonly string[],
options: DockerSpawnSyncOptions,
) => Pick<DockerSpawnSyncResult, "error" | "signal" | "status" | "stderr" | "stdout">;

function outputText(value: unknown): string {
if (Buffer.isBuffer(value)) return value.toString("utf8");
return typeof value === "string" ? value : "";
}

function boundedTail(value: string, maximumBytes: number): string {
if (Buffer.byteLength(value, "utf8") <= maximumBytes) return value;

const contentLimit = maximumBytes - Buffer.byteLength(TRUNCATION_SUFFIX, "utf8");
let content = "";
let contentBytes = 0;
for (const character of Array.from(value).reverse()) {
const characterBytes = Buffer.byteLength(character, "utf8");
if (contentBytes + characterBytes > contentLimit) break;
content = character + content;
contentBytes += characterBytes;
}
return `${content}${TRUNCATION_SUFFIX}`;
}

function commandFailureReason(result: ReturnType<DockerImageSetupRunner>): string {
if (result.error) {
const code = (result.error as NodeJS.ErrnoException).code;
return code === "ETIMEDOUT" ? "timed out" : "failed to execute";
}
if (result.signal) return `terminated by ${result.signal}`;
return `exited with status ${String(result.status)}`;
}

function pullFailureDiagnostic(result: ReturnType<DockerImageSetupRunner>): string {
const output = [outputText(result.stderr), outputText(result.stdout)]
.filter((entry) => entry.length > 0)
.join("\n")
.trim();
return output.length > 0
? boundedTail(
redact(
redactFull(
stripVTControlCharacters(output)
.replace(/\r\n?/gu, "\n")
.replace(UNSAFE_TERMINAL_CONTROL_CHARACTERS, ""),
),
),
PULL_DIAGNOSTIC_MAX_BYTES,
)
: "docker pull produced no diagnostic output";
}

export function ensureOpenClawGeminiRuntimeImage(
image: string,
runDocker: DockerImageSetupRunner = dockerSpawnSync,
): "cached" | "pulled" {
const inspect = runDocker(["image", "inspect", image], {
stdio: "ignore",
timeout: OPENCLAW_GEMINI_IMAGE_INSPECT_TIMEOUT_MS,
killSignal: "SIGKILL",
});
if (inspect.status === 0) return "cached";
if (inspect.error || inspect.signal || inspect.status === null) {
throw new Error(
`Pinned OpenClaw runtime image inspection ${commandFailureReason(inspect)}: ${image}`,
);
}

const pull = runDocker(["pull", image], {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
encoding: "utf8",
timeout: OPENCLAW_GEMINI_IMAGE_PULL_TIMEOUT_MS,
killSignal: "SIGKILL",
maxBuffer: PULL_CAPTURE_MAX_BYTES,
});
if (pull.status === 0) return "pulled";

throw new Error(
`Pinned OpenClaw runtime image pull ${commandFailureReason(pull)}: ${image}\n${pullFailureDiagnostic(pull)}`,
);
}
8 changes: 7 additions & 1 deletion test/openclaw-gemini-inference-compat-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest";

import { main } from "../scripts/generate-openclaw-config.mts";
import { dockerSpawnSync } from "../src/lib/adapters/docker/exec";
import {
ensureOpenClawGeminiRuntimeImage,
OPENCLAW_GEMINI_IMAGE_INSPECT_TIMEOUT_MS,
OPENCLAW_GEMINI_IMAGE_PULL_TIMEOUT_MS,
} from "./helpers/openclaw-gemini-runtime-image";

const OPENCLAW_RUNTIME_IMAGE =
"ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:f3f0184b96c208c7d50e5a46171a59a6e371f726b06d41972412c36b427a78d4";
Expand Down Expand Up @@ -263,6 +268,7 @@ function parseJsonOutput(output: string): unknown {
suite("OpenClaw Gemini managed-route runtime compatibility", () => {
beforeAll(() => {
contextDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-gemini-runtime-"));
ensureOpenClawGeminiRuntimeImage(OPENCLAW_RUNTIME_IMAGE);
stagedPluginPath = path.join(contextDir, "plugin");
fs.cpSync(PLUGIN_SOURCE_PATH, stagedPluginPath, { recursive: true });
fs.chmodSync(stagedPluginPath, 0o755);
Expand All @@ -272,7 +278,7 @@ suite("OpenClaw Gemini managed-route runtime compatibility", () => {
containerUser = `${pluginStat.uid}:${pluginStat.gid}`;
generatedConfigPath = generateConfig();
fs.chmodSync(generatedConfigPath, 0o444);
});
}, OPENCLAW_GEMINI_IMAGE_INSPECT_TIMEOUT_MS + OPENCLAW_GEMINI_IMAGE_PULL_TIMEOUT_MS + 10_000);

afterAll(() => {
fs.rmSync(contextDir, { recursive: true, force: true });
Expand Down
135 changes: 135 additions & 0 deletions test/openclaw-gemini-runtime-image.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it, vi } from "vitest";

import {
ensureOpenClawGeminiRuntimeImage,
OPENCLAW_GEMINI_IMAGE_INSPECT_TIMEOUT_MS,
OPENCLAW_GEMINI_IMAGE_PULL_TIMEOUT_MS,
type DockerImageSetupRunner,
} from "./helpers/openclaw-gemini-runtime-image";

const PINNED_IMAGE = `ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:${"a".repeat(64)}`;

function result(
status: number | null,
options: {
error?: NodeJS.ErrnoException;
signal?: NodeJS.Signals | null;
stderr?: string;
stdout?: string;
} = {},
): ReturnType<DockerImageSetupRunner> {
return {
error: options.error,
signal: options.signal ?? null,
status,
stderr: options.stderr ?? "",
stdout: options.stdout ?? "",
};
}

describe("OpenClaw Gemini runtime image setup", () => {
it("pulls one cold pinned image before the runtime probe (#9944)", () => {
const runDocker = vi
.fn<DockerImageSetupRunner>()
.mockReturnValueOnce(result(1))
.mockReturnValueOnce(result(0));

expect(ensureOpenClawGeminiRuntimeImage(PINNED_IMAGE, runDocker)).toBe("pulled");
expect(runDocker).toHaveBeenNthCalledWith(1, ["image", "inspect", PINNED_IMAGE], {
stdio: "ignore",
timeout: OPENCLAW_GEMINI_IMAGE_INSPECT_TIMEOUT_MS,
killSignal: "SIGKILL",
});
expect(runDocker).toHaveBeenNthCalledWith(
2,
["pull", PINNED_IMAGE],
expect.objectContaining({
timeout: OPENCLAW_GEMINI_IMAGE_PULL_TIMEOUT_MS,
killSignal: "SIGKILL",
}),
);
expect(runDocker).toHaveBeenCalledTimes(2);
});

it("skips the pull when the pinned image is cached (#9944)", () => {
const runDocker = vi.fn<DockerImageSetupRunner>().mockReturnValue(result(0));

expect(ensureOpenClawGeminiRuntimeImage(PINNED_IMAGE, runDocker)).toBe("cached");
expect(runDocker).toHaveBeenCalledOnce();
});

it("stops setup with a bounded pull diagnostic before the runtime probe (#9944)", () => {
const authorizationSecret = "registry-authorization-secret";
const cookieSecret = "registry-cookie-secret";
const urlPassword = "registry-url-password";
const querySecret = "registry-query-secret";
const runDocker = vi
.fn<DockerImageSetupRunner>()
.mockReturnValueOnce(result(1))
.mockReturnValueOnce(
result(1, {
stderr: [
"x".repeat(8 * 1024),
"pull access denied",
`\u001b[31mAuthorization: Bearer ${authorizationSecret}\u001b[0m`,
`Cookie: session=${cookieSecret}`,
`https://registry-user:${urlPassword}@registry.example/v2/image?token=${querySecret}`,
"\u001b]0;forged-title\u0007::warning::forged-command\u000b",
].join("\r\n"),
}),
);
const runtimeProbe = vi.fn();

let failure: Error | undefined;
try {
ensureOpenClawGeminiRuntimeImage(PINNED_IMAGE, runDocker);
runtimeProbe();
} catch (error) {
failure = error as Error;
}

expect(failure?.message).toContain("Pinned OpenClaw runtime image pull exited with status 1");
expect(failure?.message).toContain("pull access denied");
expect(failure?.message).toContain("<REDACTED>");
expect(failure?.message).toContain("[diagnostic truncated]");
expect(failure?.message).not.toContain(authorizationSecret);
expect(failure?.message).not.toContain(cookieSecret);
expect(failure?.message).not.toContain(urlPassword);
expect(failure?.message).not.toContain(querySecret);
expect(failure?.message).not.toMatch(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/u);
const diagnostic = failure?.message.split("\n").slice(1).join("\n") ?? "";
expect(Buffer.byteLength(diagnostic, "utf8")).toBeLessThanOrEqual(4 * 1024);
expect(runtimeProbe).not.toHaveBeenCalled();
expect(runDocker).toHaveBeenCalledTimes(2);
});

it.each([
{
expected: "timed out",
inspect: result(null, {
error: Object.assign(new Error("spawnSync docker ETIMEDOUT"), { code: "ETIMEDOUT" }),
}),
scenario: "times out",
},
{
expected: "terminated by SIGKILL",
inspect: result(null, { signal: "SIGKILL" }),
scenario: "is terminated",
},
{
expected: "exited with status null",
inspect: result(null),
scenario: "has no normal exit status",
},
])("fails without pulling when image inspection $scenario (#9944)", ({ expected, inspect }) => {
const runDocker = vi.fn<DockerImageSetupRunner>().mockReturnValue(inspect);

expect(() => ensureOpenClawGeminiRuntimeImage(PINNED_IMAGE, runDocker)).toThrow(
`Pinned OpenClaw runtime image inspection ${expected}`,
);
expect(runDocker).toHaveBeenCalledOnce();
});
});
Loading