From f51079bc5daa3969b3f9dad3a054d9caa4e07cd6 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Fri, 12 Jun 2026 14:16:56 -0400 Subject: [PATCH 1/3] test(e2e): migrate test-gateway-drift-preflight.sh to vitest Reserve Phase 4 E2E migration work for test-gateway-drift-preflight.sh. Refs #5098 From 3a5f65dc9820d3865c22434a14a504f2e7105e95 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Fri, 12 Jun 2026 15:33:10 -0400 Subject: [PATCH 2/3] test(e2e): migrate test-gateway-drift-preflight.sh to vitest Add a focused Vitest replacement for the gateway drift preflight legacy bash guard and wire it into e2e-vitest-scenarios for selective same-runner dispatch. Refs #5098 Refs #3399 Refs #3423 --- .github/workflows/e2e-vitest-scenarios.yaml | 49 ++ .../e2e-scenarios-workflow.test.ts | 16 + test/gateway-drift-preflight.test.ts | 439 ++++++++++++++++++ tools/e2e-scenarios/free-standing-jobs.env | 6 +- tools/e2e-scenarios/workflow-boundary.mts | 1 + 5 files changed, 508 insertions(+), 3 deletions(-) create mode 100644 test/gateway-drift-preflight.test.ts diff --git a/.github/workflows/e2e-vitest-scenarios.yaml b/.github/workflows/e2e-vitest-scenarios.yaml index f199050c83..6ead693b5a 100644 --- a/.github/workflows/e2e-vitest-scenarios.yaml +++ b/.github/workflows/e2e-vitest-scenarios.yaml @@ -1692,6 +1692,54 @@ jobs: docker logout docker.io || true rm -rf "${DOCKER_CONFIG}" + gateway-drift-preflight-vitest: + needs: generate-matrix + if: ${{ (inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',gateway-drift-preflight-vitest,') || contains(format(',{0},', inputs.scenarios), ',gateway-drift-preflight,') }} + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/vitest/gateway-drift-preflight + NEMOCLAW_RUN_E2E_SCENARIOS: "1" + NEMOCLAW_NON_INTERACTIVE: "1" + NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" + 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: Run gateway drift preflight Vitest test + # Migrated from test/e2e/test-gateway-drift-preflight.sh. This keeps + # the real repo CLI, PATH-resolved openshell/docker shims, host-process + # marker/PID probes, and process exit behavior while avoiding live + # Docker/OpenShell mutation. + run: | + set -euo pipefail + npx vitest run --project cli \ + test/gateway-drift-preflight.test.ts \ + --silent=false --reporter=default + + - name: Upload gateway drift preflight artifacts + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: e2e-vitest-scenarios-gateway-drift-preflight + path: e2e-artifacts/vitest/gateway-drift-preflight/ + include-hidden-files: false + if-no-files-found: ignore + retention-days: 14 + # Focused coverage slice for the #2603/#3145 OpenClaw websocket # protocol/history contract. The retained legacy bash lane remains the # source for full closeout until a later PR proves replacement and deletes it. @@ -1890,6 +1938,7 @@ jobs: double-onboard-vitest, model-router-provider-routed-inference-vitest, sandbox-survival-vitest, + gateway-drift-preflight-vitest, openclaw-tui-chat-correlation-vitest, gateway-guard-recovery, issue-4434-tui-unreachable-inference-vitest, diff --git a/test/e2e-scenario/support-tests/e2e-scenarios-workflow.test.ts b/test/e2e-scenario/support-tests/e2e-scenarios-workflow.test.ts index 354712ecde..b9205c00a2 100644 --- a/test/e2e-scenario/support-tests/e2e-scenarios-workflow.test.ts +++ b/test/e2e-scenario/support-tests/e2e-scenarios-workflow.test.ts @@ -255,6 +255,22 @@ describe("e2e-vitest-scenarios workflow boundary", () => { selectedFreeStandingJobs: ["model-router-provider-routed-inference-vitest"], registryScenarios: [], }); + expect( + evaluateE2eVitestWorkflowDispatchSelectors({ scenarios: "gateway-drift-preflight" }), + ).toMatchObject({ + valid: true, + liveScenariosRuns: false, + selectedFreeStandingJobs: ["gateway-drift-preflight-vitest"], + registryScenarios: [], + }); + expect( + evaluateE2eVitestWorkflowDispatchSelectors({ jobs: "gateway-drift-preflight-vitest" }), + ).toMatchObject({ + valid: true, + liveScenariosRuns: false, + selectedFreeStandingJobs: ["gateway-drift-preflight-vitest"], + registryScenarios: [], + }); }); it("keeps the free-standing inventory internally consistent and data-only", () => { diff --git a/test/gateway-drift-preflight.test.ts b/test/gateway-drift-preflight.test.ts new file mode 100644 index 0000000000..8f08a21520 --- /dev/null +++ b/test/gateway-drift-preflight.test.ts @@ -0,0 +1,439 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { spawn, spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import { afterAll, describe, expect, it } from "vitest"; + +import { testTimeoutOptions } from "./helpers/timeouts"; + +const REPO_ROOT = path.join(import.meta.dirname, ".."); +const CLI_ENTRYPOINT = path.join(REPO_ROOT, "bin", "nemoclaw.js"); +const ARTIFACT_ROOT = process.env.E2E_ARTIFACT_DIR; +const WORK_ROOT = (() => { + const parent = ARTIFACT_ROOT ?? os.tmpdir(); + fs.mkdirSync(parent, { recursive: true }); + return fs.mkdtempSync(path.join(parent, "nemoclaw-gateway-drift-preflight-")); +})(); +const commandTimeoutMs = 45_000; + +const liveGatewayPids: number[] = []; + +afterAll(() => { + for (const pid of liveGatewayPids.splice(0)) { + try { + process.kill(pid, "SIGTERM"); + } catch { + // Already exited. + } + } + if (!ARTIFACT_ROOT) fs.rmSync(WORK_ROOT, { recursive: true, force: true }); +}); + +type CommandResult = { + caseDir: string; + output: string; + status: number | null; + signal: NodeJS.Signals | null; +}; + +function writeFileExecutable(filePath: string, content: string): void { + fs.writeFileSync(filePath, content, { encoding: "utf-8", mode: 0o755 }); +} + +function writeRegistry(home: string): void { + const stateDir = path.join(home, ".nemoclaw"); + fs.mkdirSync(stateDir, { recursive: true }); + fs.writeFileSync( + path.join(stateDir, "sandboxes.json"), + `${JSON.stringify( + { + sandboxes: { + alpha: { + name: "alpha", + model: "test-model", + provider: "nvidia-prod", + gpuEnabled: false, + policies: [], + agent: "openclaw", + agentVersion: "test-version", + }, + }, + defaultSandbox: "alpha", + }, + null, + 2, + )}\n`, + { mode: 0o600 }, + ); +} + +function writeFakeOpenshell(binDir: string): void { + writeFileExecutable( + path.join(binDir, "openshell"), + `#!/usr/bin/env bash +set -uo pipefail +: "\${NEMOCLAW_FAKE_CASE_DIR:?}" +printf '%s\n' "$*" >> "$NEMOCLAW_FAKE_CASE_DIR/openshell-calls.log" +case "\${1:-}" in + --version|-V) + printf 'openshell 0.0.37\n' + exit 0 + ;; + status) + printf 'Server Status\n\n Gateway: nemoclaw\n Gateway endpoint: http://127.0.0.1:8080\n Status: Connected\n' + exit 0 + ;; + gateway) + if [ "\${2:-}" = "info" ]; then + printf 'Gateway Info\n\n Gateway: nemoclaw\n Gateway endpoint: http://127.0.0.1:8080\n' + exit 0 + fi + ;; + sandbox) + if [ "\${2:-}" = "list" ]; then + printf '%s\n' 'Error: status: Internal, message: "failed to decode Protobuf message: Sandbox.metadata: SandboxResponse.sandbox: invalid wire type value: 6"' >&2 + exit "\${NEMOCLAW_FAKE_SANDBOX_LIST_EXIT:-1}" + fi + ;; +esac +printf 'unexpected openshell args: %s\n' "$*" >&2 +exit 9 +`, + ); +} + +function writeFakeDocker( + binDir: string, + options: { + gatewayImage?: string; + gatewayPorts?: string; + gatewayRunning?: string; + } = {}, +): void { + const gatewayRunning = options.gatewayRunning ?? "true"; + const gatewayPorts = + options.gatewayPorts ?? '{"30051/tcp":[{"HostIp":"0.0.0.0","HostPort":"8080"}]}'; + const gatewayImage = options.gatewayImage ?? "ghcr.io/nvidia/openshell/cluster:0.0.37"; + writeFileExecutable( + path.join(binDir, "docker"), + `#!/usr/bin/env bash +set -uo pipefail +case_dir="\${NEMOCLAW_FAKE_CASE_DIR:-\${TMPDIR:-/tmp}/nemoclaw-gateway-drift-preflight-current}" +printf '%s\n' "$*" >> "$case_dir/docker-calls.log" +format="" +if [ "\${1:-}" = "inspect" ] || { [ "\${1:-}" = "container" ] && [ "\${2:-}" = "inspect" ]; }; then + while [ "$#" -gt 0 ]; do + if [ "\${1:-}" = "--format" ]; then + shift + format="\${1:-}" + break + fi + shift + done + case "$format" in + '{{.State.Running}}'|"'{{.State.Running}}'") + printf '%s\n' ${JSON.stringify(gatewayRunning)} + exit 0 + ;; + '{{json .NetworkSettings.Ports}}'|"'{{json .NetworkSettings.Ports}}'") + printf '%s\n' ${JSON.stringify(gatewayPorts)} + exit 0 + ;; + '{{.Config.Image}}'|"'{{.Config.Image}}'") + printf '%s\n' ${JSON.stringify(gatewayImage)} + exit 0 + ;; + esac +fi +printf 'unexpected docker args: %s\n' "$*" >&2 +exit 9 +`, + ); +} + +function writeFakeDockerNoCluster(binDir: string): void { + writeFileExecutable( + path.join(binDir, "docker"), + `#!/usr/bin/env bash +set -uo pipefail +printf '%s\n' "$*" >> "$NEMOCLAW_FAKE_CASE_DIR/docker-calls.log" +if [ "\${1:-}" = "inspect" ] || { [ "\${1:-}" = "container" ] && [ "\${2:-}" = "inspect" ]; }; then + printf 'Error: No such object\n' >&2 + exit 1 +fi +exit 0 +`, + ); +} + +function writeFakeGatewayBinary(binDir: string, version = "0.0.43"): string { + const gatewayBin = path.join(binDir, "openshell-gateway"); + writeFileExecutable( + gatewayBin, + `#!/usr/bin/env bash +case "\${1:-}" in --version|-V) printf 'openshell-gateway %s\n' ${JSON.stringify(version)}; exit 0 ;; esac +exec -a "$0" sleep 600 +`, + ); + return gatewayBin; +} + +function writeHostProcessMarker(home: string, gatewayBin: string, pid = 999999): void { + const stateDir = path.join(home, ".local", "state", "nemoclaw", "openshell-docker-gateway"); + fs.mkdirSync(stateDir, { recursive: true }); + fs.writeFileSync( + path.join(stateDir, "runtime.json"), + `${JSON.stringify( + { + version: 1, + pid, + driver: "docker", + platform: process.platform, + arch: process.arch, + endpoint: "http://127.0.0.1:8080", + desiredEnvHash: "deadbeef", + gatewayBin, + openshellVersion: "0.0.44", + dockerHost: "unix:///run/docker.sock", + createdAt: "2026-05-25T10:27:03.702Z", + }, + null, + 2, + )}\n`, + { mode: 0o600 }, + ); +} + +function prepareCase(name: string): { binDir: string; caseDir: string; home: string } { + const caseDir = path.join(WORK_ROOT, name); + const home = path.join(caseDir, "home"); + const binDir = path.join(caseDir, "bin"); + fs.mkdirSync(home, { recursive: true }); + fs.mkdirSync(binDir, { recursive: true }); + fs.writeFileSync(path.join(caseDir, "openshell-calls.log"), ""); + fs.writeFileSync(path.join(caseDir, "docker-calls.log"), ""); + writeRegistry(home); + writeFakeOpenshell(binDir); + return { binDir, caseDir, home }; +} + +function runCli(caseDir: string, home: string, binDir: string, args: string[]): CommandResult { + const result = spawnSync(process.execPath, [CLI_ENTRYPOINT, ...args], { + cwd: REPO_ROOT, + encoding: "utf-8", + env: { + ...process.env, + HOME: home, + PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ""}`, + TMPDIR: caseDir, + NO_COLOR: "1", + NEMOCLAW_DISABLE_GATEWAY_DRIFT_PREFLIGHT: "0", + NEMOCLAW_FAKE_CASE_DIR: caseDir, + NEMOCLAW_NON_INTERACTIVE: "1", + NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1", + }, + timeout: commandTimeoutMs, + }); + return { + caseDir, + output: [result.stdout, result.stderr].filter(Boolean).join("\n"), + status: result.status, + signal: result.signal, + }; +} + +function runBackupCase( + name: string, + options: { gatewayImage?: string; gatewayRunning?: string } = {}, +): CommandResult { + const { binDir, caseDir, home } = prepareCase(name); + writeFakeDocker(binDir, options); + return runCli(caseDir, home, binDir, ["backup-all"]); +} + +function runHostProcessCase( + name: string, + options: { liveMarker?: boolean; noMarker?: boolean; version?: string; command?: string[] } = {}, +): CommandResult { + const { binDir, caseDir, home } = prepareCase(name); + writeFakeDockerNoCluster(binDir); + const gatewayBin = writeFakeGatewayBinary(binDir, options.version ?? "0.0.43"); + if (options.noMarker !== true) { + if (options.liveMarker) { + const child = spawn(gatewayBin, ["serve"], { detached: false, stdio: "ignore" }); + expect(child.pid, "fake gateway process must have a pid").toBeTypeOf("number"); + const pid = child.pid as number; + liveGatewayPids.push(pid); + writeHostProcessMarker(home, gatewayBin, pid); + } else { + writeHostProcessMarker(home, gatewayBin, 999999); + } + } + return runCli(caseDir, home, binDir, options.command ?? ["backup-all"]); +} + +function logsFor(caseDir: string): string { + const readIfExists = (name: string) => { + const file = path.join(caseDir, name); + return fs.existsSync(file) ? fs.readFileSync(file, "utf-8") : ""; + }; + return [ + "--- fake openshell calls ---", + readIfExists("openshell-calls.log"), + "--- fake docker calls ---", + readIfExists("docker-calls.log"), + ].join("\n"); +} + +function expectContains(result: CommandResult, pattern: RegExp, description: string): void { + expect( + result.output, + `${description}\n${logsFor(result.caseDir)}\n--- command output ---\n${result.output}`, + ).toMatch(pattern); +} + +function expectNotContains(result: CommandResult, pattern: RegExp, description: string): void { + expect( + result.output, + `${description}\n${logsFor(result.caseDir)}\n--- command output ---\n${result.output}`, + ).not.toMatch(pattern); +} + +function expectSandboxListCalled(result: CommandResult, expected: boolean): void { + const calls = fs.readFileSync(path.join(result.caseDir, "openshell-calls.log"), "utf-8"); + const called = calls.split(/\r?\n/).includes("sandbox list"); + expect( + called, + `sandbox list calls expectation failed\n${logsFor(result.caseDir)}\n${result.output}`, + ).toBe(expected); +} + +describe("gateway drift preflight E2E migration", () => { + it( + "fails closed before unsafe sandbox state mutation when gateway schema or binary drift is detected", + testTimeoutOptions(180_000), + () => { + expect(fs.existsSync(CLI_ENTRYPOINT), "repo CLI entrypoint must exist").toBe(true); + + const protobuf = runBackupCase("protobuf-mismatch", { + gatewayImage: "ghcr.io/nvidia/openshell/cluster:0.0.37", + gatewayRunning: "false", + }); + expect(protobuf.signal, protobuf.output).toBeNull(); + expectContains( + protobuf, + /protobuf|schema mismatch|invalid wire type/i, + "protobuf mismatch is surfaced", + ); + expectContains( + protobuf, + /No sandbox data was changed|Refusing to trust OpenShell sandbox state/i, + "fail-closed no-mutation guidance is printed", + ); + expectNotContains( + protobuf, + /Skipping '?alpha'? \(not running\)/, + "running sandbox is not misclassified as stopped", + ); + expectNotContains( + protobuf, + /Backup complete/i, + "backup does not proceed after unsafe state RPC", + ); + expectSandboxListCalled(protobuf, true); + + const imageDrift = runBackupCase("patched-image-drift", { + gatewayImage: "nemoclaw-cluster:0.0.36-fuse-overlayfs-aa8b8487", + }); + expect(imageDrift.status, imageDrift.output).not.toBe(0); + expectContains( + imageDrift, + /schema preflight failed|gateway schema preflight failed|image.*does not match|Running gateway image/i, + "gateway image drift preflight is surfaced", + ); + expectContains(imageDrift, /0\.0\.37/, "installed OpenShell version is reported"); + expectContains( + imageDrift, + /nemoclaw-cluster:0\.0\.36-fuse-overlayfs-aa8b8487|0\.0\.36/, + "patched stale gateway image/version is reported", + ); + expectSandboxListCalled(imageDrift, false); + + const hostBackup = runHostProcessCase("host-process-backup", { liveMarker: true }); + expect(hostBackup.status, hostBackup.output).not.toBe(0); + expectContains( + hostBackup, + /schema preflight failed|gateway schema preflight failed|Running gateway binary/i, + "host-process gateway drift preflight is surfaced", + ); + expectContains(hostBackup, /0\.0\.37/, "installed OpenShell version is reported"); + expectContains( + hostBackup, + /Running gateway binary.*0\.0\.43/, + "running host-process gateway binary/version is reported", + ); + expectContains( + hostBackup, + /No sandbox data was changed|Refusing to trust OpenShell sandbox state/i, + "fail-closed no-mutation guidance is printed", + ); + expectNotContains( + hostBackup, + /Running gateway image/i, + "host-process drift does not claim a cluster image", + ); + expectSandboxListCalled(hostBackup, false); + + const hostUpgrade = runHostProcessCase("host-process-upgrade", { + command: ["upgrade-sandboxes", "--check"], + }); + expect(hostUpgrade.status, hostUpgrade.output).not.toBe(0); + expectContains( + hostUpgrade, + /schema preflight failed|gateway schema preflight failed|Running gateway binary/i, + "host-process gateway drift preflight is surfaced for upgrade-sandboxes", + ); + expectContains( + hostUpgrade, + /Running gateway binary.*0\.0\.43/, + "running host-process gateway binary/version is reported for upgrade-sandboxes", + ); + expectSandboxListCalled(hostUpgrade, false); + + const noMarker = runHostProcessCase("host-process-no-marker", { noMarker: true }); + expect(noMarker.status, noMarker.output).not.toBe(0); + expectContains( + noMarker, + /schema preflight failed|gateway schema preflight failed|Running gateway binary/i, + "host-process gateway drift is detected via fallback resolver without runtime marker", + ); + expectContains( + noMarker, + /Running gateway binary.*0\.0\.43/, + "fallback-resolved gateway binary/version is reported", + ); + expectSandboxListCalled(noMarker, false); + + const stale = (() => { + const { binDir, caseDir, home } = prepareCase("host-process-stale-marker"); + const oldInstall = path.join(caseDir, "old-install"); + fs.mkdirSync(oldInstall, { recursive: true }); + writeFakeDockerNoCluster(binDir); + writeFakeGatewayBinary(binDir, "0.0.37"); + const staleGateway = writeFakeGatewayBinary(oldInstall, "0.0.43"); + writeHostProcessMarker(home, staleGateway, 999999); + return runCli(caseDir, home, binDir, ["backup-all"]); + })(); + expectNotContains( + stale, + /Running gateway binary.*0\.0\.43/, + "stale marker binary is not used to fabricate drift", + ); + expectSandboxListCalled(stale, true); + }, + ); +}); diff --git a/tools/e2e-scenarios/free-standing-jobs.env b/tools/e2e-scenarios/free-standing-jobs.env index 68ce23e36d..2f7183ad58 100644 --- a/tools/e2e-scenarios/free-standing-jobs.env +++ b/tools/e2e-scenarios/free-standing-jobs.env @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -allowed_jobs=openshell-version-pin-vitest,onboard-negative-paths-vitest,skill-agent-vitest,inference-routing-vitest,credential-migration-vitest,runtime-overrides-vitest,hermes-e2e-vitest,hermes-root-entrypoint-smoke-vitest,network-policy-vitest,shields-config-vitest,rebuild-openclaw-vitest,sandbox-rebuild-vitest,token-rotation-vitest,launchable-smoke-vitest,openclaw-tui-chat-correlation-vitest,gateway-guard-recovery,double-onboard-vitest,issue-4434-tui-unreachable-inference-vitest,model-router-provider-routed-inference-vitest,credential-sanitization-vitest,sandbox-survival-vitest -free_standing_scenarios_csv=openshell-version-pin,onboard-negative-paths,skill-agent,inference-routing,runtime-overrides,hermes-e2e,hermes-root-entrypoint-smoke,network-policy,shields-config,rebuild-openclaw,sandbox-rebuild,token-rotation,openclaw-tui-chat-correlation,double-onboard,issue-4434-tui-unreachable-inference,model-router-provider-routed-inference,credential-sanitization,sandbox-survival -free_standing_scenario_jobs_csv=openshell-version-pin:openshell-version-pin-vitest,onboard-negative-paths:onboard-negative-paths-vitest,skill-agent:skill-agent-vitest,inference-routing:inference-routing-vitest,runtime-overrides:runtime-overrides-vitest,hermes-e2e:hermes-e2e-vitest,hermes-root-entrypoint-smoke:hermes-root-entrypoint-smoke-vitest,network-policy:network-policy-vitest,shields-config:shields-config-vitest,rebuild-openclaw:rebuild-openclaw-vitest,sandbox-rebuild:sandbox-rebuild-vitest,token-rotation:token-rotation-vitest,openclaw-tui-chat-correlation:openclaw-tui-chat-correlation-vitest,double-onboard:double-onboard-vitest,issue-4434-tui-unreachable-inference:issue-4434-tui-unreachable-inference-vitest,model-router-provider-routed-inference:model-router-provider-routed-inference-vitest,credential-sanitization:credential-sanitization-vitest,sandbox-survival:sandbox-survival-vitest +allowed_jobs=openshell-version-pin-vitest,onboard-negative-paths-vitest,skill-agent-vitest,inference-routing-vitest,credential-migration-vitest,runtime-overrides-vitest,hermes-e2e-vitest,hermes-root-entrypoint-smoke-vitest,network-policy-vitest,shields-config-vitest,rebuild-openclaw-vitest,sandbox-rebuild-vitest,token-rotation-vitest,launchable-smoke-vitest,openclaw-tui-chat-correlation-vitest,gateway-guard-recovery,double-onboard-vitest,issue-4434-tui-unreachable-inference-vitest,model-router-provider-routed-inference-vitest,credential-sanitization-vitest,sandbox-survival-vitest,gateway-drift-preflight-vitest +free_standing_scenarios_csv=openshell-version-pin,onboard-negative-paths,skill-agent,inference-routing,runtime-overrides,hermes-e2e,hermes-root-entrypoint-smoke,network-policy,shields-config,rebuild-openclaw,sandbox-rebuild,token-rotation,openclaw-tui-chat-correlation,double-onboard,issue-4434-tui-unreachable-inference,model-router-provider-routed-inference,credential-sanitization,sandbox-survival,gateway-drift-preflight +free_standing_scenario_jobs_csv=openshell-version-pin:openshell-version-pin-vitest,onboard-negative-paths:onboard-negative-paths-vitest,skill-agent:skill-agent-vitest,inference-routing:inference-routing-vitest,runtime-overrides:runtime-overrides-vitest,hermes-e2e:hermes-e2e-vitest,hermes-root-entrypoint-smoke:hermes-root-entrypoint-smoke-vitest,network-policy:network-policy-vitest,shields-config:shields-config-vitest,rebuild-openclaw:rebuild-openclaw-vitest,sandbox-rebuild:sandbox-rebuild-vitest,token-rotation:token-rotation-vitest,openclaw-tui-chat-correlation:openclaw-tui-chat-correlation-vitest,double-onboard:double-onboard-vitest,issue-4434-tui-unreachable-inference:issue-4434-tui-unreachable-inference-vitest,model-router-provider-routed-inference:model-router-provider-routed-inference-vitest,credential-sanitization:credential-sanitization-vitest,sandbox-survival:sandbox-survival-vitest,gateway-drift-preflight:gateway-drift-preflight-vitest diff --git a/tools/e2e-scenarios/workflow-boundary.mts b/tools/e2e-scenarios/workflow-boundary.mts index 9e55fc3ff3..6d3c18454b 100644 --- a/tools/e2e-scenarios/workflow-boundary.mts +++ b/tools/e2e-scenarios/workflow-boundary.mts @@ -2133,6 +2133,7 @@ export function validateE2eVitestScenariosWorkflowBoundary( "issue-4434-tui-unreachable-inference", ); validateModelRouterProviderRoutedInferenceVitestJob(errors, jobs); + validateFreeStandingJobSelector(errors, jobs, "gateway-drift-preflight-vitest", "gateway-drift-preflight"); const reportToPr = asRecord(jobs["report-to-pr"]); if (Object.keys(reportToPr).length === 0) { From 8a8b026c54528196047dc769b19a369c60c912c5 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Fri, 12 Jun 2026 17:44:15 -0400 Subject: [PATCH 3/3] test(e2e): tolerate sandbox list flags --- test/gateway-drift-preflight.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/gateway-drift-preflight.test.ts b/test/gateway-drift-preflight.test.ts index 8f08a21520..7e86522a11 100644 --- a/test/gateway-drift-preflight.test.ts +++ b/test/gateway-drift-preflight.test.ts @@ -305,7 +305,7 @@ function expectNotContains(result: CommandResult, pattern: RegExp, description: function expectSandboxListCalled(result: CommandResult, expected: boolean): void { const calls = fs.readFileSync(path.join(result.caseDir, "openshell-calls.log"), "utf-8"); - const called = calls.split(/\r?\n/).includes("sandbox list"); + const called = calls.split(/\r?\n/).some((line) => /^sandbox\s+list(?:\s|$)/.test(line.trim())); expect( called, `sandbox list calls expectation failed\n${logsFor(result.caseDir)}\n${result.output}`,