From 356b4ace81edcfdf7a01e089583c3725b1ddbcbd Mon Sep 17 00:00:00 2001 From: Rebecca Sliter <571084+rsliter@users.noreply.github.com> Date: Fri, 21 Aug 2026 17:55:21 -0700 Subject: [PATCH 1/2] test(cli): separate Gemini image acquisition Signed-off-by: Rebecca Sliter <571084+rsliter@users.noreply.github.com> --- test/helpers/openclaw-gemini-runtime-image.ts | 83 +++++++++++++++++++ ...aw-gemini-inference-compat-runtime.test.ts | 8 +- test/openclaw-gemini-runtime-image.test.ts | 81 ++++++++++++++++++ 3 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 test/helpers/openclaw-gemini-runtime-image.ts create mode 100644 test/openclaw-gemini-runtime-image.test.ts diff --git a/test/helpers/openclaw-gemini-runtime-image.ts b/test/helpers/openclaw-gemini-runtime-image.ts new file mode 100644 index 00000000000..be6ba4fc1cc --- /dev/null +++ b/test/helpers/openclaw-gemini-runtime-image.ts @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import type { + DockerSpawnSyncOptions, + DockerSpawnSyncResult, +} from "../../src/lib/adapters/docker/exec"; +import { dockerSpawnSync } from "../../src/lib/adapters/docker/exec"; + +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]"; + +export type DockerImageSetupRunner = ( + args: readonly string[], + options: DockerSpawnSyncOptions, +) => Pick; + +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 pullFailureReason(result: ReturnType): 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): string { + const output = [outputText(result.stderr), outputText(result.stdout)] + .filter((entry) => entry.length > 0) + .join("\n") + .trim(); + return output.length > 0 + ? boundedTail(output, 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"; + + const pull = runDocker(["pull", image], { + 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 ${pullFailureReason(pull)}: ${image}\n${pullFailureDiagnostic(pull)}`, + ); +} diff --git a/test/openclaw-gemini-inference-compat-runtime.test.ts b/test/openclaw-gemini-inference-compat-runtime.test.ts index f9bdc123f50..61098969722 100644 --- a/test/openclaw-gemini-inference-compat-runtime.test.ts +++ b/test/openclaw-gemini-inference-compat-runtime.test.ts @@ -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"; @@ -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); @@ -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 }); diff --git a/test/openclaw-gemini-runtime-image.test.ts b/test/openclaw-gemini-runtime-image.test.ts new file mode 100644 index 00000000000..481fd9b84c1 --- /dev/null +++ b/test/openclaw-gemini-runtime-image.test.ts @@ -0,0 +1,81 @@ +// 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: { stderr?: string; stdout?: string } = {}, +): ReturnType { + return { + error: undefined, + 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() + .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().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 runDocker = vi + .fn() + .mockReturnValueOnce(result(1)) + .mockReturnValueOnce(result(1, { stderr: `first failure\n${"x".repeat(8 * 1024)}` })); + 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("[diagnostic truncated]"); + 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); + }); +}); From c3b3867b2e6f2eeb5de2c86a8fe0c264e66cfcd7 Mon Sep 17 00:00:00 2001 From: Rebecca Sliter <571084+rsliter@users.noreply.github.com> Date: Fri, 21 Aug 2026 18:36:08 -0700 Subject: [PATCH 2/2] fix(test): harden Gemini image setup failures Signed-off-by: Rebecca Sliter <571084+rsliter@users.noreply.github.com> --- test/helpers/openclaw-gemini-runtime-image.ts | 25 +++++++- test/openclaw-gemini-runtime-image.test.ts | 62 +++++++++++++++++-- 2 files changed, 80 insertions(+), 7 deletions(-) diff --git a/test/helpers/openclaw-gemini-runtime-image.ts b/test/helpers/openclaw-gemini-runtime-image.ts index be6ba4fc1cc..1110a10f794 100644 --- a/test/helpers/openclaw-gemini-runtime-image.ts +++ b/test/helpers/openclaw-gemini-runtime-image.ts @@ -1,11 +1,14 @@ // 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; @@ -13,6 +16,8 @@ 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[], @@ -39,7 +44,7 @@ function boundedTail(value: string, maximumBytes: number): string { return `${content}${TRUNCATION_SUFFIX}`; } -function pullFailureReason(result: ReturnType): string { +function commandFailureReason(result: ReturnType): string { if (result.error) { const code = (result.error as NodeJS.ErrnoException).code; return code === "ETIMEDOUT" ? "timed out" : "failed to execute"; @@ -54,7 +59,16 @@ function pullFailureDiagnostic(result: ReturnType): stri .join("\n") .trim(); return output.length > 0 - ? boundedTail(output, PULL_DIAGNOSTIC_MAX_BYTES) + ? 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"; } @@ -68,6 +82,11 @@ export function ensureOpenClawGeminiRuntimeImage( 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], { encoding: "utf8", @@ -78,6 +97,6 @@ export function ensureOpenClawGeminiRuntimeImage( if (pull.status === 0) return "pulled"; throw new Error( - `Pinned OpenClaw runtime image pull ${pullFailureReason(pull)}: ${image}\n${pullFailureDiagnostic(pull)}`, + `Pinned OpenClaw runtime image pull ${commandFailureReason(pull)}: ${image}\n${pullFailureDiagnostic(pull)}`, ); } diff --git a/test/openclaw-gemini-runtime-image.test.ts b/test/openclaw-gemini-runtime-image.test.ts index 481fd9b84c1..1a0898cc32c 100644 --- a/test/openclaw-gemini-runtime-image.test.ts +++ b/test/openclaw-gemini-runtime-image.test.ts @@ -14,11 +14,16 @@ const PINNED_IMAGE = `ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:${"a".repeat(6 function result( status: number | null, - options: { stderr?: string; stdout?: string } = {}, + options: { + error?: NodeJS.ErrnoException; + signal?: NodeJS.Signals | null; + stderr?: string; + stdout?: string; + } = {}, ): ReturnType { return { - error: undefined, - signal: null, + error: options.error, + signal: options.signal ?? null, status, stderr: options.stderr ?? "", stdout: options.stdout ?? "", @@ -57,10 +62,25 @@ describe("OpenClaw Gemini runtime image setup", () => { }); 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() .mockReturnValueOnce(result(1)) - .mockReturnValueOnce(result(1, { stderr: `first failure\n${"x".repeat(8 * 1024)}` })); + .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; @@ -72,10 +92,44 @@ describe("OpenClaw Gemini runtime image setup", () => { } expect(failure?.message).toContain("Pinned OpenClaw runtime image pull exited with status 1"); + expect(failure?.message).toContain("pull access denied"); + expect(failure?.message).toContain(""); 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().mockReturnValue(inspect); + + expect(() => ensureOpenClawGeminiRuntimeImage(PINNED_IMAGE, runDocker)).toThrow( + `Pinned OpenClaw runtime image inspection ${expected}`, + ); + expect(runDocker).toHaveBeenCalledOnce(); + }); });