From 574d229f7d62fdf9736450ad41f10376025113a7 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sat, 1 Aug 2026 20:32:13 -0700 Subject: [PATCH 1/9] test(e2e): consolidate sandbox resource-limit coverage Signed-off-by: Carlos Villela --- .github/workflows/e2e.yaml | 69 +-------- .../fixtures/resource-limit-diagnostics.ts | 9 ++ test/e2e/live/sandbox-operations.test.ts | 67 +++++++++ test/e2e/live/sandbox-rlimits-connect.test.ts | 141 ------------------ test/e2e/mock-parity.json | 12 +- .../support/e2e-live-target-gating.test.ts | 7 +- test/e2e/support/e2e-scorecard.test.ts | 10 +- test/e2e/support/e2e-workflow.test.ts | 62 ++------ .../resource-limit-diagnostics.test.ts | 24 +++ ...ndbox-operations-workflow-boundary.test.ts | 9 ++ test/e2e/support/workflow-plan.test.ts | 12 ++ test/release-e2e-evidence.test.ts | 9 +- tools/e2e/report-e2e-results.mts | 7 +- .../sandbox-operations-workflow-boundary.mts | 4 + tools/e2e/selector-aliases.mts | 1 + tools/e2e/workflow-boundary.mts | 69 ++------- 16 files changed, 164 insertions(+), 348 deletions(-) create mode 100644 test/e2e/fixtures/resource-limit-diagnostics.ts delete mode 100644 test/e2e/live/sandbox-rlimits-connect.test.ts create mode 100644 test/e2e/support/resource-limit-diagnostics.test.ts diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 52c5d9a6a0a..7b007f20f8b 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -15,7 +15,7 @@ on: default: "" type: string jobs: - description: "Optional comma-separated E2E test IDs. Empty runs default-enabled tests only when targets is also empty; explicit-only tests openshell-gateway-auth-contract, mcp-bridge-dev, hermes-gpu-startup, sandbox-rlimits-connect, jetson-nvmap-gpu, and staging-brev-launchable are skipped unless selected." + description: "Optional comma-separated E2E test IDs. Empty runs default-enabled tests only when targets is also empty; explicit-only tests openshell-gateway-auth-contract, mcp-bridge-dev, hermes-gpu-startup, jetson-nvmap-gpu, and staging-brev-launchable are skipped unless selected." required: false default: "" type: string @@ -3350,70 +3350,6 @@ jobs: shell: bash run: bash .github/scripts/docker-auth-cleanup.sh - sandbox-rlimits-connect: - needs: generate-matrix - if: ${{ contains(format(',{0},', inputs.jobs), ',sandbox-rlimits-connect,') || contains(format(',{0},', inputs.targets), ',sandbox-rlimits-connect,') }} - runs-on: ubuntu-latest - timeout-minutes: 60 - env: - E2E_JOB: "1" - E2E_DEFAULT_ENABLED: "0" - E2E_TARGET_ID: "sandbox-rlimits-connect" - E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live/sandbox-rlimits-connect - NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js - NEMOCLAW_RUN_LIVE_E2E: "1" - NEMOCLAW_E2E_CONNECT_RLIMITS: "1" - NEMOCLAW_NON_INTERACTIVE: "1" - NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" - NEMOCLAW_SANDBOX_NAME: e2e-rlimits-connect - OPENSHELL_GATEWAY: nemoclaw - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - repository: ${{ inputs.checkout_repository || github.repository }} - ref: ${{ inputs.checkout_sha || github.sha }} - persist-credentials: false - - - *dockerhub-auth - - - name: Prepare E2E workspace - uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75 - - - name: Install OpenShell - env: - NEMOCLAW_NON_INTERACTIVE: "1" - run: | - set -euo pipefail - env -u DOCKER_CONFIG -u DOCKERHUB_USERNAME -u DOCKERHUB_TOKEN -u NVIDIA_API_KEY -u NVIDIA_INFERENCE_API_KEY -u GITHUB_TOKEN bash scripts/install-openshell.sh - - - name: Run sandbox rlimit connect live test - env: - NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} - run: | - set -euo pipefail - export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" - if command -v openshell >/dev/null 2>&1; then - OPENSHELL_BIN="$(command -v openshell)" - elif [ -x "$HOME/.local/bin/openshell" ]; then - OPENSHELL_BIN="$HOME/.local/bin/openshell" - else - echo "::error::OpenShell CLI not found after install" - ls -la /usr/local/bin/openshell "$HOME/.local/bin/openshell" 2>&1 || true - exit 1 - fi - export OPENSHELL_BIN - "$OPENSHELL_BIN" --version - npx tsx tools/e2e/live-vitest-invocation.mts run --test-path test/e2e/live/sandbox-rlimits-connect.test.ts - - - name: Upload sandbox rlimit connect artifacts - if: always() - uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 - - - name: Clean up Docker auth - if: always() - shell: bash - run: bash .github/scripts/docker-auth-cleanup.sh - overlayfs-autofix: needs: generate-matrix if: ${{ (github.event_name != 'workflow_dispatch' || (inputs.jobs == '' && inputs.targets == '')) || contains(format(',{0},', inputs.jobs), ',overlayfs-autofix,') || contains(format(',{0},', inputs.targets), ',overlayfs-autofix,') }} @@ -4613,7 +4549,7 @@ jobs: sandbox-operations: needs: generate-matrix - if: ${{ (github.event_name != 'workflow_dispatch' || (inputs.jobs == '' && inputs.targets == '')) || contains(format(',{0},', inputs.jobs), ',sandbox-operations,') || contains(format(',{0},', inputs.targets), ',sandbox-operations,') }} + if: ${{ (github.event_name != 'workflow_dispatch' || (inputs.jobs == '' && inputs.targets == '')) || contains(format(',{0},', inputs.jobs), ',sandbox-operations,') || contains(format(',{0},', inputs.jobs), ',sandbox-rlimits-connect,') || contains(format(',{0},', inputs.targets), ',sandbox-operations,') || contains(format(',{0},', inputs.targets), ',sandbox-rlimits-connect,') }} runs-on: ubuntu-latest # The live test receives 45 minutes for two onboards plus process/gateway # recovery. The remaining 15 minutes cover checkout, build, OpenShell setup, @@ -5802,7 +5738,6 @@ jobs: rebuild-openclaw, rebuild-hermes, rebuild-hermes-stale-base, - sandbox-rlimits-connect, overlayfs-autofix, state-backup-restore, openshell-gateway-upgrade, diff --git a/test/e2e/fixtures/resource-limit-diagnostics.ts b/test/e2e/fixtures/resource-limit-diagnostics.ts new file mode 100644 index 00000000000..f5b3b4648f6 --- /dev/null +++ b/test/e2e/fixtures/resource-limit-diagnostics.ts @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const SECURITY_RESOURCE_LIMIT_DIAGNOSTIC = + /\[SECURITY\][^\r\n]*(?:resource limits?|nproc|nofile)/iu; + +export function containsSecurityResourceLimitDiagnostic(output: string): boolean { + return SECURITY_RESOURCE_LIMIT_DIAGNOSTIC.test(output); +} diff --git a/test/e2e/live/sandbox-operations.test.ts b/test/e2e/live/sandbox-operations.test.ts index f713cda0d58..7dc6ba077ff 100644 --- a/test/e2e/live/sandbox-operations.test.ts +++ b/test/e2e/live/sandbox-operations.test.ts @@ -26,6 +26,7 @@ import { type HostedInferenceConfig, requireHostedInferenceConfig, } from "../fixtures/hosted-inference.ts"; +import { containsSecurityResourceLimitDiagnostic } from "../fixtures/resource-limit-diagnostics.ts"; import type { ShellProbeResult } from "../fixtures/shell-probe.ts"; import { ubuntuRepoDocker } from "../registry/matrix.ts"; @@ -36,6 +37,65 @@ const REGISTRY_FILE = path.join(process.env.HOME ?? os.homedir(), ".nemoclaw", " const GATEWAY_CONTAINER = "openshell-cluster-nemoclaw"; const GATEWAY_PORT = process.env.NEMOCLAW_GATEWAY_PORT ?? "8080"; +function numericProbe(text: string, key: string): number { + const match = text.match(new RegExp(`${key}=(\\d+)`)); + expect(match, `Missing ${key} in connect output:\n${text}`).not.toBeNull(); + return Number(match?.[1] ?? "NaN"); +} + +function connectRlimitProbeScript(cliPath: string): string { + const cli = JSON.stringify(cliPath); + const shellProbe = [ + "set +e", + 'nproc_soft="$(builtin ulimit -Su)"', + 'nproc_hard="$(builtin ulimit -Hu)"', + 'nofile_soft="$(builtin ulimit -Sn)"', + 'nofile_hard="$(builtin ulimit -Hn)"', + "(builtin ulimit -Su 5000) >/dev/null 2>&1", + 'raise_nproc="$?"', + "(builtin ulimit -Sn 1048576) >/dev/null 2>&1", + 'raise_nofile="$?"', + "set -e", + 'printf "nproc_soft=%s\\nnproc_hard=%s\\nnofile_soft=%s\\nnofile_hard=%s\\nraise_nproc=%s\\nraise_nofile=%s\\n" "$nproc_soft" "$nproc_hard" "$nofile_soft" "$nofile_hard" "$raise_nproc" "$raise_nofile"', + ].join("; "); + return [ + "set -euo pipefail", + `cat <<'NEMOCLAW_CONNECT_RLIMITS' | ${cli} connect`, + "set -euo pipefail", + 'printf "__NEMOCLAW_RLIMIT_CONNECT_BEGIN__\\n"', + `bash -lc '${shellProbe}' | sed 's/^/login_/'`, + `bash -ic '${shellProbe}' 2>&1 | sed 's/^/interactive_/'`, + 'printf "__NEMOCLAW_RLIMIT_CONNECT_END__\\n"', + "exit", + "NEMOCLAW_CONNECT_RLIMITS", + ].join("\n"); +} + +async function assertConnectResourceLimits(host: HostCliClient): Promise { + const connect = await host.command("bash", ["-lc", connectRlimitProbeScript(host.commandPath)], { + artifactName: "tc-sbx-13-connect-rlimits", + env: buildAvailabilityProbeEnv(), + timeoutMs: 3 * 60_000, + }); + const output = resultText(connect); + expectExitZero(connect, "nemoclaw connect resource-limit probe"); + expect(output).toContain("__NEMOCLAW_RLIMIT_CONNECT_BEGIN__"); + expect(output).toContain("__NEMOCLAW_RLIMIT_CONNECT_END__"); + expect( + containsSecurityResourceLimitDiagnostic(output), + "connect shell startup must not print resource-limit security diagnostics", + ).toBe(false); + for (const shell of ["login", "interactive"]) { + expect(numericProbe(output, `${shell}_nproc_soft`)).toBeLessThanOrEqual(4096); + expect(numericProbe(output, `${shell}_nproc_hard`)).toBeLessThanOrEqual(4096); + expect(numericProbe(output, `${shell}_nofile_soft`)).toBeLessThanOrEqual(65536); + expect(numericProbe(output, `${shell}_nofile_hard`)).toBeLessThanOrEqual(65536); + expect(numericProbe(output, `${shell}_raise_nproc`)).not.toBe(0); + expect(numericProbe(output, `${shell}_raise_nofile`)).not.toBe(0); + } + return output; +} + async function onboardSandbox( host: HostCliClient, cleanup: CleanupRegistry, @@ -627,6 +687,7 @@ test( e2ePhases: [ "confirm Docker and clear the sandbox operation fixtures", "onboard the primary sandbox", + "validate connected shell resource limits", "exercise primary CLI inference and logs", "exercise terminal registry and process recovery", "onboard the secondary sandbox", @@ -657,6 +718,7 @@ test( "TC-SBX-10 two sandboxes list with model/provider metadata", "TC-SBX-11 sandboxes cannot reach each other by hostname", "TC-SBX-12 destroying the non-final sandbox preserves the survivor and final destroy releases the gateway port through the macOS default or explicit non-macOS cleanup", + "TC-SBX-13 bare connect routes to the default sandbox and enforces login and interactive shell resource limits without startup diagnostics (#2173)", ], }); @@ -673,6 +735,10 @@ test( progress.phase("onboard the primary sandbox"); await onboardSandbox(host, cleanup, SANDBOX_A, "onboard-sandbox-a", hosted); + progress.phase("validate connected shell resource limits"); + const connectRlimitOutput = await assertConnectResourceLimits(host); + await artifacts.writeText("connect-rlimits-output.txt", connectRlimitOutput); + progress.phase("exercise primary CLI inference and logs"); await expectListed(host, SANDBOX_A, "tc-sbx-01-list-sandbox-a"); await assertAgentCanAnswer(host, SANDBOX_A); @@ -717,6 +783,7 @@ test( finalDestroyCleanupMode, finalGatewayPortReleased: true, gatewayRecovery, + connectRlimitsValidated: true, legacySource: "test/e2e/test-sandbox-operations.sh", }); }, diff --git a/test/e2e/live/sandbox-rlimits-connect.test.ts b/test/e2e/live/sandbox-rlimits-connect.test.ts deleted file mode 100644 index e97d886cb89..00000000000 --- a/test/e2e/live/sandbox-rlimits-connect.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts"; -import { resultText } from "../fixtures/clients/command.ts"; -import { validateSandboxName } from "../fixtures/clients/sandbox.ts"; -import { expect, test } from "../fixtures/e2e-test.ts"; -import type { ShellProbeResult } from "../fixtures/shell-probe.ts"; - -const SANDBOX_NAME = process.env.NEMOCLAW_SANDBOX_NAME ?? "test-490817"; -const LIVE_TIMEOUT_MS = 45 * 60_000; -const runConnectRlimitTest = process.env.NEMOCLAW_E2E_CONNECT_RLIMITS === "1" ? test : test.skip; - -validateSandboxName(SANDBOX_NAME); - -function numericProbe(text: string, key: string): number { - const match = text.match(new RegExp(`${key}=(\\d+)`)); - expect(match, `Missing ${key} in connect output:\n${text}`).not.toBeNull(); - return Number(match?.[1] ?? "NaN"); -} - -function expectNoRlimitStartupDiagnostics(text: string): void { - expect(text, "connect shell startup must not print rlimit security diagnostics").not.toMatch( - /\[SECURITY\].*(?:Effective|Could not set).*(?:nproc|nofile).*limit/iu, - ); -} - -function connectAcceptanceScript(cliPath: string): string { - const cli = JSON.stringify(cliPath); - return [ - "set -euo pipefail", - `cat <<'NEMOCLAW_CONNECT_RLIMITS' | ${cli} connect`, - "set -euo pipefail", - 'printf "__NEMOCLAW_RLIMIT_CONNECT_BEGIN__\\n"', - 'bash -lc \'printf "login_nproc=%s\\nlogin_nofile=%s\\n" "$(ulimit -u)" "$(ulimit -n)"; ulimit -a\'', - 'bash -ic \'printf "interactive_nproc=%s\\ninteractive_nofile=%s\\n" "$(ulimit -u)" "$(ulimit -n)"\' 2>&1', - 'fork_log="$(mktemp)"', - "pids=()", - 'for i in $(seq 1 5000); do sleep 60 & pids+=("$!"); done 2>"$fork_log" || true', - 'tail -5 "$fork_log"', - 'for pid in "${pids[@]}"; do kill "$pid" 2>/dev/null || true; done', - 'rm -f "$fork_log"', - 'printf "__NEMOCLAW_RLIMIT_CONNECT_END__\\n"', - "exit", - "NEMOCLAW_CONNECT_RLIMITS", - ].join("\n"); -} - -runConnectRlimitTest( - "connect shell enforces sandbox rlimits through rebuilt OpenClaw runtime (#2173)", - { - timeout: LIVE_TIMEOUT_MS, - meta: { - e2ePhases: [ - "confirm Docker and sandbox ownership", - "onboard the rebuilt OpenClaw runtime", - "open connected shells and saturate the process limit", - "validate login and interactive rlimits", - ], - }, - }, - async ({ artifacts, cleanup, host, progress, secrets }) => { - const apiKey = secrets.required("NVIDIA_API_KEY"); - const redactionValues = secrets.redactionValues([apiKey]); - await artifacts.target.declare({ - id: "sandbox-rlimits-connect", - issue: 2173, - optIn: "NEMOCLAW_RUN_LIVE_E2E=1 NEMOCLAW_E2E_CONNECT_RLIMITS=1", - sandboxName: SANDBOX_NAME, - acceptancePath: [ - "nemoclaw onboard --non-interactive --yes-i-accept-third-party-software", - "nemoclaw connect (routes through the onboarded default sandbox)", - "bash -lc 'ulimit -u; ulimit -n'", - "bash -ic 'ulimit -u; ulimit -n'", - "ulimit -a", - "shell startup does not emit [SECURITY] rlimit diagnostics before user commands", - "for i in $(seq 1 5000); do sleep 60 & done 2>&1 | tail -5", - ], - hermesCoverage: - "Hermes copies scripts/lib/sandbox-rlimits.sh and installs the same /etc/profile.d plus /etc/bash.bashrc shims; test/sandbox-rlimit-hooks.test.ts covers Hermes stale-base replay.", - }); - - const docker = await host.command("docker", ["info"], { - artifactName: "phase-0-docker-info", - env: buildAvailabilityProbeEnv(), - timeoutMs: 30_000, - }); - expect(docker.exitCode, resultText(docker)).toBe(0); - - cleanup.trackSandbox(host, SANDBOX_NAME, { - env: buildAvailabilityProbeEnv(), - timeoutMs: 15 * 60_000, - }); - await host.bestEffortCleanupSandbox(SANDBOX_NAME, { - env: buildAvailabilityProbeEnv(), - timeoutMs: 15 * 60_000, - }); - - progress.phase("onboard the rebuilt OpenClaw runtime"); - const onboard = await host.nemoclaw( - ["onboard", "--non-interactive", "--yes", "--yes-i-accept-third-party-software"], - { - artifactName: "phase-1-onboard", - env: { - ...buildAvailabilityProbeEnv(), - NVIDIA_API_KEY: apiKey, - NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1", - NEMOCLAW_AGENT: "openclaw", - NEMOCLAW_POLICY_MODE: "suggested", - NEMOCLAW_PROVIDER: "build", - NEMOCLAW_RECREATE_SANDBOX: "1", - NEMOCLAW_SANDBOX_NAME: SANDBOX_NAME, - }, - redactionValues, - timeoutMs: 25 * 60_000, - }, - ); - expect(onboard.exitCode, resultText(onboard)).toBe(0); - - progress.phase("open connected shells and saturate the process limit"); - const connect = await host.command("bash", ["-lc", connectAcceptanceScript(host.commandPath)], { - artifactName: "phase-2-connect-rlimits", - env: buildAvailabilityProbeEnv(), - redactionValues, - timeoutMs: 10 * 60_000, - }); - const output = resultText(connect); - await artifacts.writeText("connect-rlimits-output.txt", output); - expect(connect.exitCode, output).toBe(0); - expect(output).toContain("__NEMOCLAW_RLIMIT_CONNECT_BEGIN__"); - expect(output).toContain("__NEMOCLAW_RLIMIT_CONNECT_END__"); - expectNoRlimitStartupDiagnostics(output); - - progress.phase("validate login and interactive rlimits"); - expect(numericProbe(output, "login_nproc")).toBeLessThanOrEqual(4096); - expect(numericProbe(output, "login_nofile")).toBeLessThanOrEqual(65536); - expect(numericProbe(output, "interactive_nproc")).toBeLessThanOrEqual(4096); - expect(numericProbe(output, "interactive_nofile")).toBeLessThanOrEqual(65536); - expect(output).toMatch(/Resource temporarily unavailable|fork: retry|fork/i); - }, -); diff --git a/test/e2e/mock-parity.json b/test/e2e/mock-parity.json index ac7b39ee1c2..0b3d3c8cf3e 100644 --- a/test/e2e/mock-parity.json +++ b/test/e2e/mock-parity.json @@ -111,7 +111,7 @@ }, { "live": "test/e2e/live/sandbox-operations.test.ts", - "liveOnlyReason": "The final-destroy gateway release contract needs two real Docker/OpenShell sandboxes plus host 127.0.0.1 port binding; a fast mock cannot prove the shared-gateway ownership boundary." + "liveOnlyReason": "The resource-limit and final-destroy contracts require real Docker and OpenShell sandboxes, the repository CLI, and host port binding; fast mocks cannot verify these runtime boundaries." }, { "live": "test/e2e/live/onboard-repair.test.ts", @@ -140,16 +140,6 @@ "test/e2e/support/e2e-clients.test.ts" ] }, - { - "live": "test/e2e/live/sandbox-rlimits-connect.test.ts", - "fast": [ - "test/cli/dispatch-basics.test.ts", - "test/e2e/support/e2e-workflow.test.ts", - "test/sandbox-rlimit-hooks.test.ts", - "test/e2e/support/e2e-cleanup-resources.test.ts", - "test/e2e/support/e2e-clients.test.ts" - ] - }, { "live": "test/e2e/live/snapshot-commands.test.ts", "fast": [ diff --git a/test/e2e/support/e2e-live-target-gating.test.ts b/test/e2e/support/e2e-live-target-gating.test.ts index 32b4428cfa7..34d77390a26 100644 --- a/test/e2e/support/e2e-live-target-gating.test.ts +++ b/test/e2e/support/e2e-live-target-gating.test.ts @@ -11,11 +11,7 @@ import { testTimeoutOptions } from "../../helpers/timeouts.ts"; import { LIVE_E2E_ROOT, REPO_ROOT } from "../fixtures/paths.ts"; const VITEST = path.join(REPO_ROOT, "node_modules", "vitest", "vitest.mjs"); -const SPECIAL_GATE_ENV = [ - "NEMOCLAW_E2E_CONNECT_RLIMITS", - "NEMOCLAW_ISSUE_4434_LIVE", - "NEMOCLAW_MCP_BRIDGE_AGENT", -] as const; +const SPECIAL_GATE_ENV = ["NEMOCLAW_ISSUE_4434_LIVE", "NEMOCLAW_MCP_BRIDGE_AGENT"] as const; function liveTestFiles(root = LIVE_E2E_ROOT): string[] { return fs.readdirSync(root, { withFileTypes: true }).flatMap((entry) => { @@ -123,7 +119,6 @@ describe("live E2E target gating", () => { testTimeoutOptions(15_000), () => { const gatedFiles = [ - ["sandbox-rlimits-connect.test.ts", "NEMOCLAW_E2E_CONNECT_RLIMITS"], ["issue-4434-tui-unreachable-inference.test.ts", "NEMOCLAW_ISSUE_4434_LIVE"], ] as const; const files = gatedFiles.map(([file]) => file); diff --git a/test/e2e/support/e2e-scorecard.test.ts b/test/e2e/support/e2e-scorecard.test.ts index 7c6e52eeebd..7fd9e0464b8 100644 --- a/test/e2e/support/e2e-scorecard.test.ts +++ b/test/e2e/support/e2e-scorecard.test.ts @@ -464,13 +464,13 @@ describe("E2E scorecard", () => { { conclusion: "skipped", name: "jetson-nvmap-gpu", status: "completed" }, { conclusion: "success", - name: "sandbox-rlimits-connect", + name: "mcp-bridge-dev", status: "completed", }, { conclusion: "success", name: "report-to-pr", status: "completed" }, ], - explicitOnlyJobNames: ["jetson-nvmap-gpu", "sandbox-rlimits-connect"], - explicitlySelected: ["sandbox-rlimits-connect"], + explicitOnlyJobNames: ["jetson-nvmap-gpu", "mcp-bridge-dev"], + explicitlySelected: ["mcp-bridge-dev"], metaJobNames: ["generate-matrix", "report-to-pr", "scorecard"], needs: {}, }), @@ -522,7 +522,7 @@ describe("E2E scorecard", () => { expect( scorecardJobs.summarizeJobs({ apiJobs: null, - explicitOnlyJobNames: ["jetson-nvmap-gpu", "sandbox-rlimits-connect"], + explicitOnlyJobNames: ["jetson-nvmap-gpu", "mcp-bridge-dev"], explicitlySelected: ["jetson-nvmap-gpu"], metaJobNames: ["generate-matrix", "report-to-pr", "scorecard"], needs: { @@ -530,7 +530,7 @@ describe("E2E scorecard", () => { cloud: { result: "success" }, malformed: { result: "timed_out" }, "jetson-nvmap-gpu": { result: "skipped" }, - "sandbox-rlimits-connect": { result: "skipped" }, + "mcp-bridge-dev": { result: "skipped" }, "report-to-pr": { result: "success" }, }, }), diff --git a/test/e2e/support/e2e-workflow.test.ts b/test/e2e/support/e2e-workflow.test.ts index 894ed0edbd9..185e190b026 100644 --- a/test/e2e/support/e2e-workflow.test.ts +++ b/test/e2e/support/e2e-workflow.test.ts @@ -690,13 +690,18 @@ describe("e2e workflow boundary", () => { selectedFreeStandingJobs: ["network-policy"], registryTargets: ["ubuntu-repo-cloud-openclaw"], }); - for (const selectors of [{ jobs: "hermes-dashboard" }, { targets: "hermes-dashboard" }]) { - expect(evaluateE2eWorkflowDispatchSelectors(selectors)).toMatchObject({ - valid: true, - liveTargetsRun: false, - selectedFreeStandingJobs: ["hermes-e2e"], - registryTargets: [], - }); + for (const [legacy, canonical] of [ + ["hermes-dashboard", "hermes-e2e"], + ["sandbox-rlimits-connect", "sandbox-operations"], + ] as const) { + for (const selectors of [{ jobs: legacy }, { targets: legacy }]) { + expect(evaluateE2eWorkflowDispatchSelectors(selectors)).toMatchObject({ + valid: true, + liveTargetsRun: false, + selectedFreeStandingJobs: [canonical], + registryTargets: [], + }); + } } }, ); @@ -1277,49 +1282,6 @@ jobs: } }); - it("rejects explicit rlimit workflow trust-boundary drift", () => { - const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "e2e-rlimit-workflow-")); - const workflowPath = path.join(tmp, "workflow.yaml"); - const workflow = readWorkflow() as { - jobs: Record< - string, - Record & { - env: Record; - steps: Array>; - } - >; - }; - const job = workflow.jobs["sandbox-rlimits-connect"]; - job["runs-on"] = "self-hosted"; - job["timeout-minutes"] = 30; - job.env.E2E_DEFAULT_ENABLED = "1"; - job.env.E2E_ARTIFACT_DIR = "/tmp/rlimits"; - job.env.NEMOCLAW_CLI_BIN = "/usr/bin/nemoclaw"; - job.env.NEMOCLAW_E2E_CONNECT_RLIMITS = "0"; - const run = job.steps.find((step) => step.name === "Run sandbox rlimit connect live test")!; - run.env = {}; - run.run = "npx vitest run --project e2e-live test/e2e/live/other.test.ts"; - fs.writeFileSync(workflowPath, YAML.stringify(workflow)); - - try { - expect(validateE2eWorkflowBoundary(workflowPath)).toEqual( - expect.arrayContaining([ - 'sandbox-rlimits-connect job E2E_DEFAULT_ENABLED must be "0" when set', - "sandbox-rlimits-connect job must run on ubuntu-latest", - "sandbox-rlimits-connect job must retain its 60 minute connect budget", - "sandbox-rlimits-connect job must remain explicit-only", - "sandbox-rlimits-connect job must opt in with NEMOCLAW_E2E_CONNECT_RLIMITS=1", - "sandbox-rlimits-connect job must use the repo CLI launcher", - "sandbox-rlimits-connect job must write artifacts under e2e-artifacts/live/sandbox-rlimits-connect", - "sandbox-rlimits-connect job must run sandbox-rlimits-connect.test.ts", - "sandbox-rlimits-connect step must receive NVIDIA_API_KEY from secrets", - ]), - ); - } finally { - fs.rmSync(tmp, { recursive: true, force: true }); - } - }); - // source-shape-contract: security -- Mutates the shipped workflow to prove channel lifecycle secrets and artifacts fail closed it("rejects channels stop/start workflow-boundary drift for secret and artifact handling", () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "e2e-workflow-")); diff --git a/test/e2e/support/resource-limit-diagnostics.test.ts b/test/e2e/support/resource-limit-diagnostics.test.ts new file mode 100644 index 00000000000..538562c1e2a --- /dev/null +++ b/test/e2e/support/resource-limit-diagnostics.test.ts @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; +import { containsSecurityResourceLimitDiagnostic } from "../fixtures/resource-limit-diagnostics.ts"; + +describe("resource-limit security diagnostics", () => { + it.each([ + "[SECURITY] Sandbox resource limits were NOT hardened for this shell.", + "[SECURITY] Could not set soft nproc limit", + "[SECURITY] Could not set hard nofile limit", + "[SECURITY] Effective sandbox resource limits do not match policy", + ])("recognizes a failed hardening warning: %s", (warning) => { + expect(containsSecurityResourceLimitDiagnostic(warning)).toBe(true); + }); + + it("ignores unrelated security and shell output", () => { + expect( + containsSecurityResourceLimitDiagnostic( + "[SECURITY] Provider credentials are unavailable.\nnproc_soft=512\nnofile_soft=65536", + ), + ).toBe(false); + }); +}); diff --git a/test/e2e/support/sandbox-operations-workflow-boundary.test.ts b/test/e2e/support/sandbox-operations-workflow-boundary.test.ts index 6c5acac0e3f..7b549671555 100644 --- a/test/e2e/support/sandbox-operations-workflow-boundary.test.ts +++ b/test/e2e/support/sandbox-operations-workflow-boundary.test.ts @@ -45,6 +45,15 @@ function mutateSandboxOperationsJob(source: string, mutate: (jobSource: string) } describe("sandbox operations workflow boundary", () => { + it("rejects a workflow that restores the retired sandbox-rlimits-connect job", () => { + const workflow = readSandboxOperationsWorkflow(); + workflow.jobs["sandbox-rlimits-connect"] = structuredClone(workflow.jobs["sandbox-operations"]); + + expect(validateSandboxOperationsWorkflow(workflow)).toContain( + "sandbox-rlimits-connect must remain consolidated into sandbox-operations", + ); + }); + it("accepts shared guarded Docker authentication without a job-specific configure step", () => { const workflow = readSandboxOperationsWorkflow(); const steps = workflow.jobs["sandbox-operations"].steps!; diff --git a/test/e2e/support/workflow-plan.test.ts b/test/e2e/support/workflow-plan.test.ts index fba5f7f1ba8..57353156021 100644 --- a/test/e2e/support/workflow-plan.test.ts +++ b/test/e2e/support/workflow-plan.test.ts @@ -87,6 +87,18 @@ describe("E2E workflow plan", () => { } }); + it.each([ + "jobs", + "targets", + ] as const)("maps the retired sandbox rlimit %s selector to sandbox operations", (kind) => { + const legacyPlan = buildE2eWorkflowPlan({ [kind]: "sandbox-rlimits-connect" }); + const canonicalPlan = buildE2eWorkflowPlan({ [kind]: "sandbox-operations" }); + + expect(legacyPlan).toEqual(canonicalPlan); + expect(legacyPlan.hermesSelected).toBe(false); + expect(readFreeStandingJobsInventory().allowedJobs).not.toContain("sandbox-rlimits-connect"); + }); + it("routes a registry target into the live matrix", () => { const registryId = firstId(buildLiveTargetMatrix(), "supported registry target"); const plan = buildE2eWorkflowPlan({ targets: registryId }); diff --git a/test/release-e2e-evidence.test.ts b/test/release-e2e-evidence.test.ts index 6900363b5bd..2f3a3e647d9 100644 --- a/test/release-e2e-evidence.test.ts +++ b/test/release-e2e-evidence.test.ts @@ -94,14 +94,9 @@ describe("release E2E evidence", () => { targets: "", }); const parallelExplicitJobs = plan.dispatches.parallelExplicit.jobs.split(","); - expect(parallelExplicitJobs).toHaveLength(4); + expect(parallelExplicitJobs).toHaveLength(3); expect(new Set(parallelExplicitJobs)).toEqual( - new Set([ - "openshell-gateway-auth-contract", - "mcp-bridge-dev", - "hermes-gpu-startup", - "sandbox-rlimits-connect", - ]), + new Set(["openshell-gateway-auth-contract", "mcp-bridge-dev", "hermes-gpu-startup"]), ); expect(plan.dispatches.conditional).toEqual([ expect.objectContaining({ allowJetsonRunnerQueue: false, jobs: "jetson-nvmap-gpu" }), diff --git a/tools/e2e/report-e2e-results.mts b/tools/e2e/report-e2e-results.mts index ba1ab62f001..5700ed76e0a 100644 --- a/tools/e2e/report-e2e-results.mts +++ b/tools/e2e/report-e2e-results.mts @@ -199,11 +199,6 @@ export function renderE2eReport(input: { reason: "default dispatch excludes Jetson; explicit dispatch requires allow_jetson_runner_queue=true after confirming an online Jetson runner because queued jobs do not honor timeout-minutes before assignment", }, - "sandbox-rlimits-connect": { - job: "sandbox-rlimits-connect", - target: "sandbox-rlimits-connect", - reason: "default dispatch excludes the destructive rlimit fork/connect probe unless selected", - }, }; const explicitOnlySkippedJobs = (env.EXPLICIT_ONLY_JOBS || "") .split(",") @@ -428,7 +423,7 @@ export function renderE2eReport(input: { ? "**Requested test IDs:** _(selector rejected by workflow validation)_" : requestedTestIdsCsv ? `**Requested test IDs:** \`${requestedTestIdsCsv}\`` - : "**Requested test IDs:** _(default — all default-enabled tests; explicit-only tests `openshell-gateway-auth-contract`, `mcp-bridge-dev`, `hermes-gpu-startup`, `sandbox-rlimits-connect`, and `jetson-nvmap-gpu` are skipped unless selected)_", + : "**Requested test IDs:** _(default — all default-enabled tests; explicit-only tests `openshell-gateway-auth-contract`, `mcp-bridge-dev`, `hermes-gpu-startup`, and `jetson-nvmap-gpu` are skipped unless selected)_", `**Summary:** ${passed.length} passed, ${failed.length} failed, ${cancelled.length} cancelled, ${skipped.length} skipped, ${unknown.length} unknown`, "", "| Test | Result | Total wall clock time |", diff --git a/tools/e2e/sandbox-operations-workflow-boundary.mts b/tools/e2e/sandbox-operations-workflow-boundary.mts index 76dabc3b0b4..1f6f1db5f48 100644 --- a/tools/e2e/sandbox-operations-workflow-boundary.mts +++ b/tools/e2e/sandbox-operations-workflow-boundary.mts @@ -14,6 +14,7 @@ import { PREPARE_E2E_STEP } from "./prepare-e2e-workflow-boundary.mts"; const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); const DEFAULT_WORKFLOW_PATH = join(REPO_ROOT, ".github", "workflows", "e2e.yaml"); const JOB_NAME = "sandbox-operations"; +const RETIRED_JOB_NAME = "sandbox-rlimits-connect"; const FULL_SHA_ACTION = /^[^\s@]+@[0-9a-f]{40}$/u; const GITHUB_ENV_REFERENCE = /\$\{?GITHUB_ENV\}?/u; const DOCKER_CREDENTIALS = ["DOCKERHUB_USERNAME", "DOCKERHUB_TOKEN"] as const; @@ -75,6 +76,9 @@ export function validateSandboxOperationsWorkflow(workflow: { const jobEnv = job.env ?? {}; const steps = job.steps ?? []; + if (workflow.jobs[RETIRED_JOB_NAME] !== undefined) { + errors.push(`${RETIRED_JOB_NAME} must remain consolidated into ${JOB_NAME}`); + } if (job["runs-on"] !== "ubuntu-latest") { errors.push(`${JOB_NAME} must run on ubuntu-latest`); } diff --git a/tools/e2e/selector-aliases.mts b/tools/e2e/selector-aliases.mts index 7344c362974..5f0bb7da114 100644 --- a/tools/e2e/selector-aliases.mts +++ b/tools/e2e/selector-aliases.mts @@ -3,6 +3,7 @@ export const E2E_SELECTOR_ALIASES = Object.freeze({ "hermes-dashboard": "hermes-e2e", + "sandbox-rlimits-connect": "sandbox-operations", } as const); export function normalizeE2eSelectorId(selector: string): string { diff --git a/tools/e2e/workflow-boundary.mts b/tools/e2e/workflow-boundary.mts index 13987587f36..a9e60c94b0b 100644 --- a/tools/e2e/workflow-boundary.mts +++ b/tools/e2e/workflow-boundary.mts @@ -44,7 +44,10 @@ import { validateRunnerComparisonWorkflowBoundary } from "./runner-comparison-wo import { validateRunnerPressureWorkflow } from "./runner-pressure-workflow-boundary.mts"; import { validateSandboxOperationsWorkflow } from "./sandbox-operations-workflow-boundary.mts"; import { validateSecurityPostureWorkflow } from "./security-posture-workflow-boundary.mts"; -import { normalizeE2eSelectorIds } from "./selector-aliases.mts"; +import { + normalizeE2eSelectorIds, + selectorsForCanonicalE2eId, +} from "./selector-aliases.mts"; import { validateTrustedHermesSwapHelperSource, validateTrustedHermesSwapWorkflow, @@ -1036,10 +1039,16 @@ function requireNoDispatchInputInterpolation( } function freeStandingJobIf(jobName: string, targetName?: string): string { - const targetSelector = targetName - ? ` || contains(format(',{0},', inputs.targets), ',${targetName},')` - : ""; - return `\${{ (github.event_name != 'workflow_dispatch' || (inputs.jobs == '' && inputs.targets == '')) || contains(format(',{0},', inputs.jobs), ',${jobName},')${targetSelector} }}`; + const jobSelectors = selectorsForCanonicalE2eId(jobName).map( + (selector) => `contains(format(',{0},', inputs.jobs), ',${selector},')`, + ); + const targetSelectors = targetName + ? selectorsForCanonicalE2eId(targetName).map( + (selector) => `contains(format(',{0},', inputs.targets), ',${selector},')`, + ) + : []; + const selectors = [...jobSelectors, ...targetSelectors].join(" || "); + return `\${{ (github.event_name != 'workflow_dispatch' || (inputs.jobs == '' && inputs.targets == '')) || ${selectors} }}`; } function explicitOnlyFreeStandingJobIf(jobName: string, targetName?: string): string { @@ -3918,54 +3927,6 @@ export function validateJetsonRunnerDispatchBoundary(workflow: unknown): string[ return errors; } -function validateSandboxRlimitConnectJob(errors: string[], jobs: WorkflowRecord): void { - const jobName = "sandbox-rlimits-connect"; - const job = asRecord(jobs[jobName]); - if (job.needs !== "generate-matrix") { - errors.push(`${jobName} job must depend on generate-matrix`); - } - if (job.if !== explicitOnlyFreeStandingJobIf(jobName, jobName)) { - errors.push(`${jobName} job must run only when explicitly selected`); - } - if (job["runs-on"] !== "ubuntu-latest") { - errors.push(`${jobName} job must run on ubuntu-latest`); - } - if (job["timeout-minutes"] !== 60) { - errors.push(`${jobName} job must retain its 60 minute connect budget`); - } - - const env = asRecord(job.env); - if (env.E2E_DEFAULT_ENABLED !== "0") { - errors.push(`${jobName} job must remain explicit-only`); - } - if (env.NEMOCLAW_RUN_LIVE_E2E !== "1") { - errors.push(`${jobName} job must set NEMOCLAW_RUN_LIVE_E2E=1`); - } - if (env.NEMOCLAW_E2E_CONNECT_RLIMITS !== "1") { - errors.push(`${jobName} job must opt in with NEMOCLAW_E2E_CONNECT_RLIMITS=1`); - } - if (env.NEMOCLAW_CLI_BIN !== "${{ github.workspace }}/bin/nemoclaw.js") { - errors.push(`${jobName} job must use the repo CLI launcher`); - } - if ( - env.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/live/sandbox-rlimits-connect" - ) { - errors.push(`${jobName} job must write artifacts under e2e-artifacts/live/${jobName}`); - } - - const run = namedStep(asSteps(job.steps), "Run sandbox rlimit connect live test"); - if (!run) { - errors.push(`${jobName} job missing step: Run sandbox rlimit connect live test`); - return; - } - if (!stringValue(run.run).includes("test/e2e/live/sandbox-rlimits-connect.test.ts")) { - errors.push(`${jobName} job must run sandbox-rlimits-connect.test.ts`); - } - if (asRecord(run.env).NVIDIA_API_KEY !== "${{ secrets.NVIDIA_API_KEY }}") { - errors.push(`${jobName} step must receive NVIDIA_API_KEY from secrets`); - } -} - function validateInferenceModeInput( errors: string[], workflow: WorkflowRecord, @@ -4860,8 +4821,6 @@ export function validateE2eWorkflow(workflowValue: unknown): string[] { validateTunnelLifecycleJob(errors, jobs); - validateSandboxRlimitConnectJob(errors, jobs); - validateFreeStandingJobSelector( errors, jobs, From d88040087d60d4fc409a569a46000ea2f300b52e Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sat, 1 Aug 2026 20:50:19 -0700 Subject: [PATCH 2/9] fix(e2e): redact connected-shell probe output --- .../fixtures/resource-limit-diagnostics.ts | 22 ++++++++ test/e2e/live/sandbox-operations.test.ts | 51 +++++++++++-------- .../resource-limit-diagnostics.test.ts | 50 +++++++++++++++++- 3 files changed, 102 insertions(+), 21 deletions(-) diff --git a/test/e2e/fixtures/resource-limit-diagnostics.ts b/test/e2e/fixtures/resource-limit-diagnostics.ts index f5b3b4648f6..566bd663422 100644 --- a/test/e2e/fixtures/resource-limit-diagnostics.ts +++ b/test/e2e/fixtures/resource-limit-diagnostics.ts @@ -3,7 +3,29 @@ const SECURITY_RESOURCE_LIMIT_DIAGNOSTIC = /\[SECURITY\][^\r\n]*(?:resource limits?|nproc|nofile)/iu; +const RESOURCE_LIMIT_PROBE_TOKEN = + /__NEMOCLAW_RLIMIT_CONNECT_(?:BEGIN|END)__|(?:login|interactive)_(?:nproc|nofile)_(?:soft|hard)=\d+|(?:login|interactive)_raise_(?:nproc|nofile)=\d+/gu; export function containsSecurityResourceLimitDiagnostic(output: string): boolean { return SECURITY_RESOURCE_LIMIT_DIAGNOSTIC.test(output); } + +export function resourceLimitOutputFilterScript(): string { + return [ + '"use strict";', + 'const readline = require("node:readline");', + `const diagnostic = new RegExp(${JSON.stringify(SECURITY_RESOURCE_LIMIT_DIAGNOSTIC.source)}, ${JSON.stringify(SECURITY_RESOURCE_LIMIT_DIAGNOSTIC.flags)});`, + `const probeToken = new RegExp(${JSON.stringify(RESOURCE_LIMIT_PROBE_TOKEN.source)}, ${JSON.stringify(RESOURCE_LIMIT_PROBE_TOKEN.flags)});`, + "let diagnosticFound = false;", + "const lines = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });", + 'lines.on("line", (line) => {', + " if (diagnostic.test(line)) diagnosticFound = true;", + " for (const match of line.matchAll(probeToken)) {", + ' process.stdout.write(match[0] + "\\n");', + " }", + "});", + 'lines.on("close", () => {', + ' process.stdout.write("resource_limit_diagnostic=" + (diagnosticFound ? "1" : "0") + "\\n");', + "});", + ].join("\n"); +} diff --git a/test/e2e/live/sandbox-operations.test.ts b/test/e2e/live/sandbox-operations.test.ts index 7dc6ba077ff..b0f6132848e 100644 --- a/test/e2e/live/sandbox-operations.test.ts +++ b/test/e2e/live/sandbox-operations.test.ts @@ -18,6 +18,7 @@ import { assertExitZero as expectExitZero, outputContainsSandbox, resultText, + shellQuote, } from "../fixtures/clients/command.ts"; import type { HostCliClient } from "../fixtures/clients/host.ts"; import { type SandboxClient, trustedSandboxShellScript } from "../fixtures/clients/sandbox.ts"; @@ -26,7 +27,7 @@ import { type HostedInferenceConfig, requireHostedInferenceConfig, } from "../fixtures/hosted-inference.ts"; -import { containsSecurityResourceLimitDiagnostic } from "../fixtures/resource-limit-diagnostics.ts"; +import { resourceLimitOutputFilterScript } from "../fixtures/resource-limit-diagnostics.ts"; import type { ShellProbeResult } from "../fixtures/shell-probe.ts"; import { ubuntuRepoDocker } from "../registry/matrix.ts"; @@ -38,13 +39,16 @@ const GATEWAY_CONTAINER = "openshell-cluster-nemoclaw"; const GATEWAY_PORT = process.env.NEMOCLAW_GATEWAY_PORT ?? "8080"; function numericProbe(text: string, key: string): number { - const match = text.match(new RegExp(`${key}=(\\d+)`)); - expect(match, `Missing ${key} in connect output:\n${text}`).not.toBeNull(); - return Number(match?.[1] ?? "NaN"); + const prefix = `${key}=`; + const line = text.split(/\r?\n/u).find((candidate) => candidate.startsWith(prefix)); + const value = line?.slice(prefix.length); + expect(value, `Missing ${key} in sanitized connect summary`).toMatch(/^\d+$/u); + return Number(value ?? "NaN"); } function connectRlimitProbeScript(cliPath: string): string { const cli = JSON.stringify(cliPath); + const outputFilter = `${shellQuote(process.execPath)} -e ${shellQuote(resourceLimitOutputFilterScript())}`; const shellProbe = [ "set +e", 'nproc_soft="$(builtin ulimit -Su)"', @@ -60,7 +64,7 @@ function connectRlimitProbeScript(cliPath: string): string { ].join("; "); return [ "set -euo pipefail", - `cat <<'NEMOCLAW_CONNECT_RLIMITS' | ${cli} connect`, + `cat <<'NEMOCLAW_CONNECT_RLIMITS' | ${cli} connect 2>&1 | ${outputFilter}`, "set -euo pipefail", 'printf "__NEMOCLAW_RLIMIT_CONNECT_BEGIN__\\n"', `bash -lc '${shellProbe}' | sed 's/^/login_/'`, @@ -77,23 +81,30 @@ async function assertConnectResourceLimits(host: HostCliClient): Promise env: buildAvailabilityProbeEnv(), timeoutMs: 3 * 60_000, }); - const output = resultText(connect); - expectExitZero(connect, "nemoclaw connect resource-limit probe"); - expect(output).toContain("__NEMOCLAW_RLIMIT_CONNECT_BEGIN__"); - expect(output).toContain("__NEMOCLAW_RLIMIT_CONNECT_END__"); + const summary = resultText(connect); + if (connect.exitCode !== 0) { + const exit = connect.signal + ? `signal=${connect.signal}` + : `exit=${connect.exitCode ?? "unknown"}`; + throw new Error( + `nemoclaw connect resource-limit probe failed: ${exit}, timedOut=${String(connect.timedOut)}`, + ); + } + expect(summary).toContain("__NEMOCLAW_RLIMIT_CONNECT_BEGIN__"); + expect(summary).toContain("__NEMOCLAW_RLIMIT_CONNECT_END__"); expect( - containsSecurityResourceLimitDiagnostic(output), + numericProbe(summary, "resource_limit_diagnostic"), "connect shell startup must not print resource-limit security diagnostics", - ).toBe(false); + ).toBe(0); for (const shell of ["login", "interactive"]) { - expect(numericProbe(output, `${shell}_nproc_soft`)).toBeLessThanOrEqual(4096); - expect(numericProbe(output, `${shell}_nproc_hard`)).toBeLessThanOrEqual(4096); - expect(numericProbe(output, `${shell}_nofile_soft`)).toBeLessThanOrEqual(65536); - expect(numericProbe(output, `${shell}_nofile_hard`)).toBeLessThanOrEqual(65536); - expect(numericProbe(output, `${shell}_raise_nproc`)).not.toBe(0); - expect(numericProbe(output, `${shell}_raise_nofile`)).not.toBe(0); + expect(numericProbe(summary, `${shell}_nproc_soft`)).toBeLessThanOrEqual(4096); + expect(numericProbe(summary, `${shell}_nproc_hard`)).toBeLessThanOrEqual(4096); + expect(numericProbe(summary, `${shell}_nofile_soft`)).toBeLessThanOrEqual(65536); + expect(numericProbe(summary, `${shell}_nofile_hard`)).toBeLessThanOrEqual(65536); + expect(numericProbe(summary, `${shell}_raise_nproc`)).not.toBe(0); + expect(numericProbe(summary, `${shell}_raise_nofile`)).not.toBe(0); } - return output; + return summary; } async function onboardSandbox( @@ -736,8 +747,8 @@ test( await onboardSandbox(host, cleanup, SANDBOX_A, "onboard-sandbox-a", hosted); progress.phase("validate connected shell resource limits"); - const connectRlimitOutput = await assertConnectResourceLimits(host); - await artifacts.writeText("connect-rlimits-output.txt", connectRlimitOutput); + const connectRlimitSummary = await assertConnectResourceLimits(host); + await artifacts.writeText("connect-rlimits-summary.txt", connectRlimitSummary); progress.phase("exercise primary CLI inference and logs"); await expectListed(host, SANDBOX_A, "tc-sbx-01-list-sandbox-a"); diff --git a/test/e2e/support/resource-limit-diagnostics.test.ts b/test/e2e/support/resource-limit-diagnostics.test.ts index 538562c1e2a..71d4630054a 100644 --- a/test/e2e/support/resource-limit-diagnostics.test.ts +++ b/test/e2e/support/resource-limit-diagnostics.test.ts @@ -1,8 +1,21 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { spawnSync } from "node:child_process"; import { describe, expect, it } from "vitest"; -import { containsSecurityResourceLimitDiagnostic } from "../fixtures/resource-limit-diagnostics.ts"; +import { + containsSecurityResourceLimitDiagnostic, + resourceLimitOutputFilterScript, +} from "../fixtures/resource-limit-diagnostics.ts"; + +function filterResourceLimitOutput(input: string): string { + const result = spawnSync(process.execPath, ["-e", resourceLimitOutputFilterScript()], { + encoding: "utf8", + input, + }); + expect(result.status, result.stderr).toBe(0); + return result.stdout; +} describe("resource-limit security diagnostics", () => { it.each([ @@ -21,4 +34,39 @@ describe("resource-limit security diagnostics", () => { ), ).toBe(false); }); + + it("retains only content-free probe fields before artifact capture", () => { + const summary = filterResourceLimitOutput( + [ + "connected shell token=do-not-retain", + "prompt> __NEMOCLAW_RLIMIT_CONNECT_BEGIN__", + "login_nproc_soft=512", + "interactive_raise_nofile=1", + "__NEMOCLAW_RLIMIT_CONNECT_END__", + "request body must not be retained", + ].join("\n"), + ); + + expect(summary).toBe( + [ + "__NEMOCLAW_RLIMIT_CONNECT_BEGIN__", + "login_nproc_soft=512", + "interactive_raise_nofile=1", + "__NEMOCLAW_RLIMIT_CONNECT_END__", + "resource_limit_diagnostic=0", + "", + ].join("\n"), + ); + expect(summary).not.toContain("do-not-retain"); + expect(summary).not.toContain("request body"); + }); + + it("reports a resource-limit warning without retaining its text", () => { + const warning = "[SECURITY] Could not set hard nofile limit token=do-not-retain"; + const summary = filterResourceLimitOutput(warning); + + expect(summary).toBe("resource_limit_diagnostic=1\n"); + expect(summary).not.toContain(warning); + expect(summary).not.toContain("do-not-retain"); + }); }); From d28f7661778003436c1dc9b9921717d5a5fdd3ec Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sat, 1 Aug 2026 20:56:36 -0700 Subject: [PATCH 3/9] fix(e2e): keep connect probe assertion linear --- test/e2e/live/sandbox-operations.test.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/test/e2e/live/sandbox-operations.test.ts b/test/e2e/live/sandbox-operations.test.ts index b0f6132848e..9403052a5fe 100644 --- a/test/e2e/live/sandbox-operations.test.ts +++ b/test/e2e/live/sandbox-operations.test.ts @@ -82,14 +82,13 @@ async function assertConnectResourceLimits(host: HostCliClient): Promise timeoutMs: 3 * 60_000, }); const summary = resultText(connect); - if (connect.exitCode !== 0) { - const exit = connect.signal - ? `signal=${connect.signal}` - : `exit=${connect.exitCode ?? "unknown"}`; - throw new Error( - `nemoclaw connect resource-limit probe failed: ${exit}, timedOut=${String(connect.timedOut)}`, - ); - } + const exit = connect.signal + ? `signal=${connect.signal}` + : `exit=${connect.exitCode ?? "unknown"}`; + expect( + connect.exitCode, + `nemoclaw connect resource-limit probe failed: ${exit}, timedOut=${String(connect.timedOut)}`, + ).toBe(0); expect(summary).toContain("__NEMOCLAW_RLIMIT_CONNECT_BEGIN__"); expect(summary).toContain("__NEMOCLAW_RLIMIT_CONNECT_END__"); expect( From d43f12753911f2c01291dd3800fb40eeb2020132 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sat, 1 Aug 2026 21:03:49 -0700 Subject: [PATCH 4/9] fix(e2e): frame resource-limit probe output --- .../fixtures/resource-limit-diagnostics.ts | 48 ++++++++++++++-- test/e2e/live/sandbox-operations.test.ts | 29 +++++++--- .../resource-limit-diagnostics.test.ts | 56 +++++++++++++++++-- 3 files changed, 113 insertions(+), 20 deletions(-) diff --git a/test/e2e/fixtures/resource-limit-diagnostics.ts b/test/e2e/fixtures/resource-limit-diagnostics.ts index 566bd663422..87b78720506 100644 --- a/test/e2e/fixtures/resource-limit-diagnostics.ts +++ b/test/e2e/fixtures/resource-limit-diagnostics.ts @@ -3,8 +3,11 @@ const SECURITY_RESOURCE_LIMIT_DIAGNOSTIC = /\[SECURITY\][^\r\n]*(?:resource limits?|nproc|nofile)/iu; -const RESOURCE_LIMIT_PROBE_TOKEN = - /__NEMOCLAW_RLIMIT_CONNECT_(?:BEGIN|END)__|(?:login|interactive)_(?:nproc|nofile)_(?:soft|hard)=\d+|(?:login|interactive)_raise_(?:nproc|nofile)=\d+/gu; +const RESOURCE_LIMIT_PROBE_FIELD = + /^(?:login|interactive)_(?:(?:nproc|nofile)_(?:soft|hard)|raise_(?:nproc|nofile))=\d+$/u; + +export const RESOURCE_LIMIT_CONNECT_BEGIN_MARKER = "__NEMOCLAW_RLIMIT_CONNECT_BEGIN__"; +export const RESOURCE_LIMIT_CONNECT_END_MARKER = "__NEMOCLAW_RLIMIT_CONNECT_END__"; export function containsSecurityResourceLimitDiagnostic(output: string): boolean { return SECURITY_RESOURCE_LIMIT_DIAGNOSTIC.test(output); @@ -15,17 +18,52 @@ export function resourceLimitOutputFilterScript(): string { '"use strict";', 'const readline = require("node:readline");', `const diagnostic = new RegExp(${JSON.stringify(SECURITY_RESOURCE_LIMIT_DIAGNOSTIC.source)}, ${JSON.stringify(SECURITY_RESOURCE_LIMIT_DIAGNOSTIC.flags)});`, - `const probeToken = new RegExp(${JSON.stringify(RESOURCE_LIMIT_PROBE_TOKEN.source)}, ${JSON.stringify(RESOURCE_LIMIT_PROBE_TOKEN.flags)});`, + `const probeField = new RegExp(${JSON.stringify(RESOURCE_LIMIT_PROBE_FIELD.source)}, ${JSON.stringify(RESOURCE_LIMIT_PROBE_FIELD.flags)});`, + `const beginMarker = ${JSON.stringify(RESOURCE_LIMIT_CONNECT_BEGIN_MARKER)};`, + `const endMarker = ${JSON.stringify(RESOURCE_LIMIT_CONNECT_END_MARKER)};`, "let diagnosticFound = false;", + "let frameComplete = false;", + "let frameOpen = false;", + "let protocolError = false;", + "const seenFields = new Set();", "const lines = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });", 'lines.on("line", (line) => {', " if (diagnostic.test(line)) diagnosticFound = true;", - " for (const match of line.matchAll(probeToken)) {", - ' process.stdout.write(match[0] + "\\n");', + " if (line === beginMarker) {", + " if (frameOpen || frameComplete) protocolError = true;", + " else {", + " frameOpen = true;", + ' process.stdout.write(beginMarker + "\\n");', + " }", + " return;", + " }", + " if (line === endMarker) {", + " if (!frameOpen || frameComplete) protocolError = true;", + " else {", + " frameOpen = false;", + " frameComplete = true;", + ' process.stdout.write(endMarker + "\\n");', + " }", + " return;", + " }", + " if (line.includes(beginMarker) || line.includes(endMarker)) {", + " protocolError = true;", + " return;", + " }", + " if (!probeField.test(line)) return;", + " if (!frameOpen || frameComplete) {", + " protocolError = true;", + " return;", " }", + ' const key = line.slice(0, line.indexOf("="));', + " if (seenFields.has(key)) protocolError = true;", + " seenFields.add(key);", + ' process.stdout.write(line + "\\n");', "});", 'lines.on("close", () => {', + " if (frameOpen || !frameComplete) protocolError = true;", ' process.stdout.write("resource_limit_diagnostic=" + (diagnosticFound ? "1" : "0") + "\\n");', + ' process.stdout.write("resource_limit_protocol_error=" + (protocolError ? "1" : "0") + "\\n");', "});", ].join("\n"); } diff --git a/test/e2e/live/sandbox-operations.test.ts b/test/e2e/live/sandbox-operations.test.ts index 9403052a5fe..cd5a9562ff6 100644 --- a/test/e2e/live/sandbox-operations.test.ts +++ b/test/e2e/live/sandbox-operations.test.ts @@ -27,7 +27,11 @@ import { type HostedInferenceConfig, requireHostedInferenceConfig, } from "../fixtures/hosted-inference.ts"; -import { resourceLimitOutputFilterScript } from "../fixtures/resource-limit-diagnostics.ts"; +import { + RESOURCE_LIMIT_CONNECT_BEGIN_MARKER, + RESOURCE_LIMIT_CONNECT_END_MARKER, + resourceLimitOutputFilterScript, +} from "../fixtures/resource-limit-diagnostics.ts"; import type { ShellProbeResult } from "../fixtures/shell-probe.ts"; import { ubuntuRepoDocker } from "../registry/matrix.ts"; @@ -40,10 +44,13 @@ const GATEWAY_PORT = process.env.NEMOCLAW_GATEWAY_PORT ?? "8080"; function numericProbe(text: string, key: string): number { const prefix = `${key}=`; - const line = text.split(/\r?\n/u).find((candidate) => candidate.startsWith(prefix)); - const value = line?.slice(prefix.length); - expect(value, `Missing ${key} in sanitized connect summary`).toMatch(/^\d+$/u); - return Number(value ?? "NaN"); + const values = text + .split(/\r?\n/u) + .filter((candidate) => candidate.startsWith(prefix)) + .map((candidate) => candidate.slice(prefix.length)); + expect(values, `Expected exactly one ${key} in sanitized connect summary`).toHaveLength(1); + expect(values[0], `Expected a numeric ${key} in sanitized connect summary`).toMatch(/^\d+$/u); + return Number(values[0] ?? "NaN"); } function connectRlimitProbeScript(cliPath: string): string { @@ -66,10 +73,10 @@ function connectRlimitProbeScript(cliPath: string): string { "set -euo pipefail", `cat <<'NEMOCLAW_CONNECT_RLIMITS' | ${cli} connect 2>&1 | ${outputFilter}`, "set -euo pipefail", - 'printf "__NEMOCLAW_RLIMIT_CONNECT_BEGIN__\\n"', + 'printf "__NEMOCLAW_RLIMIT_CONNECT_%s__\\n" BEGIN', `bash -lc '${shellProbe}' | sed 's/^/login_/'`, `bash -ic '${shellProbe}' 2>&1 | sed 's/^/interactive_/'`, - 'printf "__NEMOCLAW_RLIMIT_CONNECT_END__\\n"', + 'printf "__NEMOCLAW_RLIMIT_CONNECT_%s__\\n" END', "exit", "NEMOCLAW_CONNECT_RLIMITS", ].join("\n"); @@ -89,12 +96,16 @@ async function assertConnectResourceLimits(host: HostCliClient): Promise connect.exitCode, `nemoclaw connect resource-limit probe failed: ${exit}, timedOut=${String(connect.timedOut)}`, ).toBe(0); - expect(summary).toContain("__NEMOCLAW_RLIMIT_CONNECT_BEGIN__"); - expect(summary).toContain("__NEMOCLAW_RLIMIT_CONNECT_END__"); + expect(summary).toContain(RESOURCE_LIMIT_CONNECT_BEGIN_MARKER); + expect(summary).toContain(RESOURCE_LIMIT_CONNECT_END_MARKER); expect( numericProbe(summary, "resource_limit_diagnostic"), "connect shell startup must not print resource-limit security diagnostics", ).toBe(0); + expect( + numericProbe(summary, "resource_limit_protocol_error"), + "connect resource-limit summary must contain exactly one complete probe frame", + ).toBe(0); for (const shell of ["login", "interactive"]) { expect(numericProbe(summary, `${shell}_nproc_soft`)).toBeLessThanOrEqual(4096); expect(numericProbe(summary, `${shell}_nproc_hard`)).toBeLessThanOrEqual(4096); diff --git a/test/e2e/support/resource-limit-diagnostics.test.ts b/test/e2e/support/resource-limit-diagnostics.test.ts index 71d4630054a..327ad95b5bb 100644 --- a/test/e2e/support/resource-limit-diagnostics.test.ts +++ b/test/e2e/support/resource-limit-diagnostics.test.ts @@ -5,6 +5,8 @@ import { spawnSync } from "node:child_process"; import { describe, expect, it } from "vitest"; import { containsSecurityResourceLimitDiagnostic, + RESOURCE_LIMIT_CONNECT_BEGIN_MARKER, + RESOURCE_LIMIT_CONNECT_END_MARKER, resourceLimitOutputFilterScript, } from "../fixtures/resource-limit-diagnostics.ts"; @@ -39,21 +41,22 @@ describe("resource-limit security diagnostics", () => { const summary = filterResourceLimitOutput( [ "connected shell token=do-not-retain", - "prompt> __NEMOCLAW_RLIMIT_CONNECT_BEGIN__", + RESOURCE_LIMIT_CONNECT_BEGIN_MARKER, "login_nproc_soft=512", "interactive_raise_nofile=1", - "__NEMOCLAW_RLIMIT_CONNECT_END__", + RESOURCE_LIMIT_CONNECT_END_MARKER, "request body must not be retained", ].join("\n"), ); expect(summary).toBe( [ - "__NEMOCLAW_RLIMIT_CONNECT_BEGIN__", + RESOURCE_LIMIT_CONNECT_BEGIN_MARKER, "login_nproc_soft=512", "interactive_raise_nofile=1", - "__NEMOCLAW_RLIMIT_CONNECT_END__", + RESOURCE_LIMIT_CONNECT_END_MARKER, "resource_limit_diagnostic=0", + "resource_limit_protocol_error=0", "", ].join("\n"), ); @@ -63,10 +66,51 @@ describe("resource-limit security diagnostics", () => { it("reports a resource-limit warning without retaining its text", () => { const warning = "[SECURITY] Could not set hard nofile limit token=do-not-retain"; - const summary = filterResourceLimitOutput(warning); + const summary = filterResourceLimitOutput( + [warning, RESOURCE_LIMIT_CONNECT_BEGIN_MARKER, RESOURCE_LIMIT_CONNECT_END_MARKER].join("\n"), + ); - expect(summary).toBe("resource_limit_diagnostic=1\n"); + expect(summary).toContain("resource_limit_diagnostic=1\n"); + expect(summary).toContain("resource_limit_protocol_error=0\n"); expect(summary).not.toContain(warning); expect(summary).not.toContain("do-not-retain"); }); + + it("rejects a marker embedded in connected-shell output", () => { + const summary = filterResourceLimitOutput( + [`prompt> ${RESOURCE_LIMIT_CONNECT_BEGIN_MARKER}`, RESOURCE_LIMIT_CONNECT_END_MARKER].join( + "\n", + ), + ); + + expect(summary).not.toContain(`${RESOURCE_LIMIT_CONNECT_BEGIN_MARKER}\n`); + expect(summary).toContain("resource_limit_protocol_error=1\n"); + }); + + it("rejects a probe field outside the marker frame", () => { + const summary = filterResourceLimitOutput( + [ + "login_nproc_soft=1", + RESOURCE_LIMIT_CONNECT_BEGIN_MARKER, + RESOURCE_LIMIT_CONNECT_END_MARKER, + ].join("\n"), + ); + + expect(summary).not.toContain("login_nproc_soft=1"); + expect(summary).toContain("resource_limit_protocol_error=1\n"); + }); + + it("rejects a duplicate probe field inside the marker frame", () => { + const summary = filterResourceLimitOutput( + [ + RESOURCE_LIMIT_CONNECT_BEGIN_MARKER, + "login_nproc_soft=1", + "login_nproc_soft=4096", + RESOURCE_LIMIT_CONNECT_END_MARKER, + ].join("\n"), + ); + + expect(summary.match(/^login_nproc_soft=/gmu)).toHaveLength(2); + expect(summary).toContain("resource_limit_protocol_error=1\n"); + }); }); From 5218a1558d8ff29658b2a9373b97f5f56604276a Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sat, 1 Aug 2026 21:11:49 -0700 Subject: [PATCH 5/9] test(e2e): cover incomplete resource-limit frame --- test/e2e/support/resource-limit-diagnostics.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/e2e/support/resource-limit-diagnostics.test.ts b/test/e2e/support/resource-limit-diagnostics.test.ts index 327ad95b5bb..922b5ed0ba1 100644 --- a/test/e2e/support/resource-limit-diagnostics.test.ts +++ b/test/e2e/support/resource-limit-diagnostics.test.ts @@ -113,4 +113,10 @@ describe("resource-limit security diagnostics", () => { expect(summary.match(/^login_nproc_soft=/gmu)).toHaveLength(2); expect(summary).toContain("resource_limit_protocol_error=1\n"); }); + + it("rejects an incomplete marker frame", () => { + const summary = filterResourceLimitOutput(RESOURCE_LIMIT_CONNECT_BEGIN_MARKER); + + expect(summary).toContain("resource_limit_protocol_error=1\n"); + }); }); From d7e4e39e7d93c34c230ebd5c73def0fe722d1a1b Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sat, 1 Aug 2026 21:27:28 -0700 Subject: [PATCH 6/9] test(e2e): update workflow-plan compatibility digest --- test/e2e/support/e2e-cross-runtime-compatibility.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/support/e2e-cross-runtime-compatibility.test.ts b/test/e2e/support/e2e-cross-runtime-compatibility.test.ts index 2e53610d0dd..f95185b29c8 100644 --- a/test/e2e/support/e2e-cross-runtime-compatibility.test.ts +++ b/test/e2e/support/e2e-cross-runtime-compatibility.test.ts @@ -29,7 +29,7 @@ describe("cross-runtime foundation compatibility", () => { ), ).toBe("6272aab16cf4b9555bdc4b3f4c0cdd24b5faa55118cbd61cbb4b30a3d418a63a"); expect(digestOutput(buildE2eWorkflowPlan())).toBe( - "9c391dfd06884da3898cc5590e7b8d4f43f1a2ad5ebb0c141da5f6383021aeb5", + "45a00867e0c501ba5004a8cd9557d846371524eeb300a5ec979ce840500f74e9", ); }); From 70d27a7acb493d0696a146358a096a273f1500df Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sat, 1 Aug 2026 21:57:20 -0700 Subject: [PATCH 7/9] test(e2e): bridge retired rlimit selector --- test/e2e/live/sandbox-rlimits-connect.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 test/e2e/live/sandbox-rlimits-connect.test.ts diff --git a/test/e2e/live/sandbox-rlimits-connect.test.ts b/test/e2e/live/sandbox-rlimits-connect.test.ts new file mode 100644 index 00000000000..9a5d814206c --- /dev/null +++ b/test/e2e/live/sandbox-rlimits-connect.test.ts @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { normalizeE2eSelectorId } from "../../../tools/e2e/selector-aliases.mts"; +import { expect, test } from "../fixtures/e2e-test.ts"; + +test("the retired sandbox resource-limit selector resolves to sandbox operations", { + meta: { + e2ePhases: [ + "resolve the retired sandbox resource-limit selector", + "record the sandbox operations replacement", + ], + }, +}, ({ progress }) => { + progress.phase("resolve the retired sandbox resource-limit selector"); + expect(normalizeE2eSelectorId("sandbox-rlimits-connect")).toBe("sandbox-operations"); + progress.phase("record the sandbox operations replacement"); +}); From f68f7b25009f393849e53a9b405a4a77983e9ab9 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sat, 1 Aug 2026 22:00:36 -0700 Subject: [PATCH 8/9] test(e2e): classify rlimit selector bridge --- test/e2e/mock-parity.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/e2e/mock-parity.json b/test/e2e/mock-parity.json index 0b3d3c8cf3e..8ba1529fcf7 100644 --- a/test/e2e/mock-parity.json +++ b/test/e2e/mock-parity.json @@ -109,6 +109,12 @@ "live": "test/e2e/live/dashboard-remote-bind.test.ts", "liveOnlyReason": "Requires a real OpenShell dashboard forward on a remote Brev host to prove all-interface bind behavior after background connect." }, + { + "live": "test/e2e/live/sandbox-rlimits-connect.test.ts", + "fast": [ + "test/e2e/support/workflow-plan.test.ts" + ] + }, { "live": "test/e2e/live/sandbox-operations.test.ts", "liveOnlyReason": "The resource-limit and final-destroy contracts require real Docker and OpenShell sandboxes, the repository CLI, and host port binding; fast mocks cannot verify these runtime boundaries." From 84ed8512223b90f7ec1787cb39f1919b7f7105e7 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sat, 1 Aug 2026 22:02:47 -0700 Subject: [PATCH 9/9] test(e2e): document selector bridge lifetime --- test/e2e/live/sandbox-rlimits-connect.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/e2e/live/sandbox-rlimits-connect.test.ts b/test/e2e/live/sandbox-rlimits-connect.test.ts index 9a5d814206c..9348549d3c1 100644 --- a/test/e2e/live/sandbox-rlimits-connect.test.ts +++ b/test/e2e/live/sandbox-rlimits-connect.test.ts @@ -4,6 +4,9 @@ import { normalizeE2eSelectorId } from "../../../tools/e2e/selector-aliases.mts"; import { expect, test } from "../fixtures/e2e-test.ts"; +// PR E2E runs the trusted base workflow, which can still invoke this candidate path during job +// removal. This bridge verifies selector compatibility, not live resource limits. Delete it after +// the trusted workflow no longer references the old job. test("the retired sandbox resource-limit selector resolves to sandbox operations", { meta: { e2ePhases: [