From 2ed6df6295de2f6d204e7c15ea546f5c93a86da0 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sun, 12 Jul 2026 10:00:25 -0700 Subject: [PATCH] fix(e2e): repair release gate regressions Signed-off-by: Carlos Villela --- src/lib/onboard/docker-gpu-patch.ts | 19 +++++++ .../docker-startup-command-patch.test.ts | 42 ++++++++++++++++ ...messaging-providers-slack-runtime-proof.ts | 36 +++++++++++++- ...shell-gateway-auth-source-contract.test.ts | 13 +++-- ...messaging-providers-runtime-proofs.test.ts | 49 +++++++++++++++++++ 5 files changed, 152 insertions(+), 7 deletions(-) diff --git a/src/lib/onboard/docker-gpu-patch.ts b/src/lib/onboard/docker-gpu-patch.ts index 6dc3c7ac8c2..30cf0ae4a3a 100644 --- a/src/lib/onboard/docker-gpu-patch.ts +++ b/src/lib/onboard/docker-gpu-patch.ts @@ -257,6 +257,10 @@ export type DockerContainerInspect = { GroupAdd?: string[] | null; Dns?: string[] | null; DnsSearch?: string[] | null; + DeviceRequests?: Array<{ + Driver?: string; + DeviceIDs?: string[] | null; + }> | null; ShmSize?: number; ReadonlyPaths?: string[] | null; MaskedPaths?: string[] | null; @@ -676,6 +680,21 @@ export function buildDockerGpuCloneRunArgs( const args: string[] = ["--name", dockerContainerName(inspect), ...mode.args]; const gpuAugment = mode.kind !== "startup-command"; + // Hermes restart persistence recreates the OpenShell-managed container + // without selecting NemoClaw's compatibility GPU mode. Preserve Docker's + // native CDI requests from the inspected container so that recreation does + // not silently drop OpenShell's GPU attachment (and the injected libcuda). + if (!gpuAugment) { + const cdiDeviceIds = new Set( + (host.DeviceRequests ?? []) + .filter((request) => request.Driver === "cdi") + .flatMap((request) => stringArray(request.DeviceIDs)) + .map((deviceId) => deviceId.trim()) + .filter(Boolean), + ); + for (const deviceId of cdiDeviceIds) args.push("--device", deviceId); + } + pushStringFlag(args, "--hostname", config.Hostname); pushStringFlag(args, "--user", config.User); pushStringFlag(args, "--workdir", config.WorkingDir); diff --git a/src/lib/onboard/docker-startup-command-patch.test.ts b/src/lib/onboard/docker-startup-command-patch.test.ts index 4ec319cd4eb..14a10de0fc5 100644 --- a/src/lib/onboard/docker-startup-command-patch.test.ts +++ b/src/lib/onboard/docker-startup-command-patch.test.ts @@ -72,6 +72,7 @@ describe("Docker startup-command patch", () => { ]), ); expect(cloneArgs).not.toContain("--gpus"); + expect(cloneArgs).not.toContain("--device"); expect(cloneArgs).toEqual(expect.arrayContaining(["--env", "NVIDIA_VISIBLE_DEVICES=void"])); expect(cloneArgs).not.toEqual(expect.arrayContaining(["--cap-add", "SYS_PTRACE"])); expect(cloneArgs).not.toEqual( @@ -85,6 +86,47 @@ describe("Docker startup-command patch", () => { ); }); + it("preserves OpenShell's native CDI GPU request during restart-persistence recreation", () => { + const inspect = inspectFixture(); + inspect.HostConfig!.DeviceRequests = [ + { + Driver: "cdi", + DeviceIDs: ["nvidia.com/gpu=all"], + }, + ]; + const dockerRunDetached = vi.fn((_args: readonly string[]) => ({ + status: 0, + stdout: "new-container-id\n", + })); + + recreateOpenShellDockerSandboxWithStartupCommand( + { + sandboxName: "alpha", + timeoutSecs: 1, + waitForSupervisor: false, + openshellSandboxCommand: ["env", "nemoclaw-start"], + }, + { + dockerCapture: vi.fn((args: readonly string[]) => + args[0] === "ps" ? "old-container-id\n" : JSON.stringify([inspect]), + ), + dockerRunDetached, + dockerRename: vi.fn(() => ({ status: 0 })), + dockerStop: vi.fn(() => ({ status: 0 })), + sleep: vi.fn(), + now: () => new Date("2026-07-10T00:00:00Z"), + }, + ); + + const cloneArgs = dockerRunDetached.mock.calls[0]?.[0] ?? []; + expect(cloneArgs).toEqual(expect.arrayContaining(["--device", "nvidia.com/gpu=all"])); + expect(cloneArgs).not.toContain("--gpus"); + expect(cloneArgs).not.toEqual(expect.arrayContaining(["--cap-add", "SYS_PTRACE"])); + expect(cloneArgs).not.toEqual( + expect.arrayContaining(["--security-opt", "apparmor=unconfined"]), + ); + }); + it("rejects an empty restart-persistence command before Docker mutation", () => { expect(() => recreateOpenShellDockerSandboxWithStartupCommand({ diff --git a/test/e2e/live/messaging-providers-slack-runtime-proof.ts b/test/e2e/live/messaging-providers-slack-runtime-proof.ts index a3874d0fde6..bda0a3ffc00 100644 --- a/test/e2e/live/messaging-providers-slack-runtime-proof.ts +++ b/test/e2e/live/messaging-providers-slack-runtime-proof.ts @@ -20,6 +20,33 @@ export type InstalledSlackRuntimeProof = { channelId: string; }; +export const SLACK_MANAGED_NPM_PROJECT_DISCOVERY_SOURCE = String.raw` +function addManagedNpmProjectSlackCandidates(projectsDir, addExternalCandidate) { + let entries; + try { + entries = fs.readdirSync(projectsDir, { withFileTypes: true }); + } catch { + return; + } + for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) { + if (!entry.isDirectory()) continue; + const projectRoot = path.join(projectsDir, entry.name); + let dependencies; + try { + dependencies = JSON.parse( + fs.readFileSync(path.join(projectRoot, "package.json"), "utf8"), + ).dependencies; + } catch { + continue; + } + if (!dependencies || !Object.hasOwn(dependencies, "@openclaw/slack")) continue; + addExternalCandidate( + path.join(projectRoot, "node_modules", "@openclaw", "slack"), + ); + } +} +`; + export const SLACK_INSTALLED_RUNTIME_PROOF_SOURCE = String.raw` import { execFileSync } from "node:child_process"; import fs from "node:fs"; @@ -30,6 +57,8 @@ import { pathToFileURL } from "node:url"; const allowLegacyTestApi = process.env.NEMOCLAW_E2E_ALLOW_LEGACY_SLACK_TEST_API === "1"; +${SLACK_MANAGED_NPM_PROJECT_DISCOVERY_SOURCE} + function invariant(condition, message) { if (!condition) throw new Error(message); } @@ -78,8 +107,11 @@ function resolveOpenClawSlackApiLocation() { } }; - addExternalCandidate( - path.join(process.env.OPENCLAW_STATE_DIR || "/sandbox/.openclaw", "extensions", "slack"), + const openclawStateDir = process.env.OPENCLAW_STATE_DIR || "/sandbox/.openclaw"; + addExternalCandidate(path.join(openclawStateDir, "extensions", "slack")); + addManagedNpmProjectSlackCandidates( + path.join(openclawStateDir, "npm", "projects"), + addExternalCandidate, ); addExternalCandidate(process.env.OPENCLAW_SLACK_PACKAGE_ROOT); addCoreCandidate(process.env.OPENCLAW_PACKAGE_ROOT); diff --git a/test/e2e/live/openshell-gateway-auth-source-contract.test.ts b/test/e2e/live/openshell-gateway-auth-source-contract.test.ts index 83bf11d6a35..8e72703339c 100644 --- a/test/e2e/live/openshell-gateway-auth-source-contract.test.ts +++ b/test/e2e/live/openshell-gateway-auth-source-contract.test.ts @@ -12,9 +12,12 @@ const OPENSHELL_GATEWAY_AUTH_CONTRACT_VERSION = "0.0.72"; test( `OpenShell ${OPENSHELL_GATEWAY_AUTH_CONTRACT_VERSION} Docker-driver gateway auth uses NemoClaw mTLS plus sandbox JWT`, { timeout: LIVE_TIMEOUT_MS }, - (fixtures) => - runOpenShellGatewayAuthSourceContractScenario(fixtures, { - buildDockerDriverGatewayLaunch, - ensureDockerDriverGatewayLocalTlsBundle, - }), + ({ artifacts, cleanup, host, skip }) => + runOpenShellGatewayAuthSourceContractScenario( + { artifacts, cleanup, host, skip }, + { + buildDockerDriverGatewayLaunch, + ensureDockerDriverGatewayLocalTlsBundle, + }, + ), ); diff --git a/test/e2e/support/messaging-providers-runtime-proofs.test.ts b/test/e2e/support/messaging-providers-runtime-proofs.test.ts index 758666b6013..1fc3f876d85 100644 --- a/test/e2e/support/messaging-providers-runtime-proofs.test.ts +++ b/test/e2e/support/messaging-providers-runtime-proofs.test.ts @@ -18,6 +18,7 @@ import { import { parseInstalledSlackProof, SLACK_INSTALLED_RUNTIME_PROOF_SOURCE, + SLACK_MANAGED_NPM_PROJECT_DISCOVERY_SOURCE, } from "../live/messaging-providers-slack-runtime-proof.ts"; import { TELEGRAM_INSTALLED_RUNTIME_PROOF_SOURCE } from "../live/messaging-providers-telegram-runtime-proof.ts"; @@ -155,6 +156,54 @@ describe("messaging provider installed-runtime proofs", () => { expect(SLACK_INSTALLED_RUNTIME_PROOF_SOURCE).toContain("/api/chat.postMessage"); }); + it("finds Slack only in its canonical managed npm project", () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-slack-managed-project-")); + const projectsDir = path.join(dir, "npm", "projects"); + const slackProject = path.join(projectsDir, "openclaw-slack-reviewed"); + const unrelatedProject = path.join(projectsDir, "unrelated-plugin"); + const malformedProject = path.join(projectsDir, "malformed-plugin"); + const slackPackageRoot = path.join(slackProject, "node_modules", "@openclaw", "slack"); + + try { + fs.mkdirSync(slackPackageRoot, { recursive: true }); + fs.writeFileSync( + path.join(slackProject, "package.json"), + JSON.stringify({ dependencies: { "@openclaw/slack": "2026.6.10" } }), + ); + fs.mkdirSync(path.join(unrelatedProject, "node_modules", "@openclaw", "slack"), { + recursive: true, + }); + fs.writeFileSync( + path.join(unrelatedProject, "package.json"), + JSON.stringify({ dependencies: { "@openclaw/discord": "2026.6.10" } }), + ); + fs.mkdirSync(malformedProject, { recursive: true }); + fs.writeFileSync(path.join(malformedProject, "package.json"), "not json"); + + const source = [ + 'import fs from "node:fs";', + 'import path from "node:path";', + SLACK_MANAGED_NPM_PROJECT_DISCOVERY_SOURCE, + "const candidates = [];", + "addManagedNpmProjectSlackCandidates(", + " process.env.NEMOCLAW_TEST_PROJECTS_DIR,", + " (candidate) => candidates.push(path.resolve(candidate)),", + ");", + "process.stdout.write(JSON.stringify(candidates));", + ].join("\n"); + const result = spawnSync(process.execPath, ["--input-type=module", "-"], { + encoding: "utf8", + env: { ...process.env, NEMOCLAW_TEST_PROJECTS_DIR: projectsDir }, + input: source, + }); + + expect(result.status, result.stderr).toBe(0); + expect(JSON.parse(result.stdout)).toEqual([path.resolve(slackPackageRoot)]); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + it("reports loader stderr without accepting stderr as a Slack proof (#6467)", () => { const proof = JSON.stringify({ ok: true,