From 7c8e8e7db7bdb8d470305034d6a7de2ec1f28653 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Mon, 22 Jun 2026 16:41:16 -0400 Subject: [PATCH 1/6] test(e2e): migrate test-hermes-sandbox-secret-boundary.sh to vitest --- .github/workflows/e2e-vitest-scenarios.yaml | 45 ++ .../hermes-sandbox-secret-boundary.test.ts | 595 ++++++++++++++++++ .../hermes-secret-boundary-workflow.test.ts | 38 ++ tools/e2e-scenarios/workflow-boundary.mts | 129 ++++ 4 files changed, 807 insertions(+) create mode 100644 test/e2e-scenario/live/hermes-sandbox-secret-boundary.test.ts create mode 100644 test/e2e-scenario/support-tests/hermes-secret-boundary-workflow.test.ts diff --git a/.github/workflows/e2e-vitest-scenarios.yaml b/.github/workflows/e2e-vitest-scenarios.yaml index 19024471884..1df57ce1d6d 100644 --- a/.github/workflows/e2e-vitest-scenarios.yaml +++ b/.github/workflows/e2e-vitest-scenarios.yaml @@ -639,6 +639,50 @@ jobs: if-no-files-found: ignore retention-days: 14 + hermes-sandbox-secret-boundary-vitest: + needs: generate-matrix + if: ${{ (inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',hermes-sandbox-secret-boundary-vitest,') || contains(format(',{0},', inputs.scenarios), ',hermes-sandbox-secret-boundary,') }} + runs-on: ubuntu-latest + timeout-minutes: 60 + env: + FREE_STANDING_VITEST_JOB: "1" + FREE_STANDING_SCENARIO_ID: "hermes-sandbox-secret-boundary" + E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/vitest/hermes-sandbox-secret-boundary + NEMOCLAW_RUN_E2E_SCENARIOS: "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: Run Hermes sandbox secret-boundary live test + # Migrated from test/e2e/test-hermes-sandbox-secret-boundary.sh. This + # builds the real Hermes images unless prebuilt NEMOCLAW_HERMES_* image + # env vars are supplied, then probes image and startup secret boundaries. + run: | + set -euo pipefail + npx vitest run --project e2e-scenarios-live \ + test/e2e-scenario/live/hermes-sandbox-secret-boundary.test.ts \ + --silent=false --reporter=default + + - name: Upload Hermes sandbox secret-boundary artifacts + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: e2e-vitest-scenarios-hermes-sandbox-secret-boundary + path: e2e-artifacts/vitest/hermes-sandbox-secret-boundary/ + include-hidden-files: false + if-no-files-found: ignore + retention-days: 14 + inference-routing-vitest: needs: generate-matrix if: ${{ (inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',inference-routing-vitest,') || contains(format(',{0},', inputs.scenarios), ',inference-routing,') }} @@ -4802,6 +4846,7 @@ jobs: runtime-overrides-vitest, hermes-e2e-vitest, hermes-root-entrypoint-smoke-vitest, + hermes-sandbox-secret-boundary-vitest, network-policy-vitest, common-egress-agent-vitest, shields-config-vitest, diff --git a/test/e2e-scenario/live/hermes-sandbox-secret-boundary.test.ts b/test/e2e-scenario/live/hermes-sandbox-secret-boundary.test.ts new file mode 100644 index 00000000000..82c90e8c826 --- /dev/null +++ b/test/e2e-scenario/live/hermes-sandbox-secret-boundary.test.ts @@ -0,0 +1,595 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Buffer } from "node:buffer"; + +import { type DockerCommandResult, DockerProbe, resultText } from "../fixtures/docker-probe.ts"; +import { expect, test } from "../fixtures/e2e-test.ts"; + +// Migrated from test/e2e/test-hermes-sandbox-secret-boundary.sh. This remains +// a real Docker/image-boundary smoke: it builds the Hermes sandbox images when +// prebuilt image env vars are absent, inspects /sandbox/.hermes inside the +// images, and executes /usr/local/bin/nemoclaw-start to prove raw secret-shaped +// config/process env is rejected without echoing values. + +const BUILD_TIMEOUT_MS = 10 * 60_000; +const RUN_TIMEOUT_MS = 60_000; +const RAW_SECRET_SENTINEL = "SENTINEL_RAW_SECRET_VALUE"; +const RAW_REFRESH_TOKEN = "raw-refresh-token"; + +const liveTest = process.env.NEMOCLAW_RUN_E2E_SCENARIOS === "1" ? test : test.skip; + +const IMAGE_INSPECTION_SCRIPT = String.raw` +import re +import sys +from pathlib import Path + +secret_key_re = re.compile(r"(^|_)(TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL|API)(_|$)") +slack_alias_re = re.compile(r"^(xoxb|xapp)-OPENSHELL-RESOLVE-ENV-[A-Z0-9_]+$") +allowed_nonsecret_keys = {"API_SERVER_HOST", "API_SERVER_PORT"} +allowed_literals = {"", "[STRIPPED_BY_MIGRATION]"} +required_remote_toolsets = { + "web", + "browser", + "terminal", + "file", + "code_execution", + "vision", + "image_gen", + "skills", + "todo", + "memory", + "session_search", + "delegation", + "cronjob", + "nemoclaw", + "audio", +} + + +def unquote(value: str) -> str: + value = value.strip() + if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'): + return value[1:-1] + return value + + +def env_violations(path: Path) -> list[str]: + violations: list[str] = [] + for lineno, raw_line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + stripped = raw_line.strip() + if not stripped or stripped.startswith("#") or "=" not in stripped: + continue + if stripped.startswith("export "): + stripped = stripped[len("export ") :].lstrip() + key, value = stripped.split("=", 1) + key = key.strip() + if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", key): + continue + if key in allowed_nonsecret_keys: + continue + if not secret_key_re.search(key): + continue + value = unquote(value) + if ( + value in allowed_literals + or value.startswith("openshell:resolve:env:") + or slack_alias_re.fullmatch(value) + ): + continue + violations.append(f"{key} line {lineno}") + return violations + + +def parse_platform_toolsets(text: str) -> dict[str, list[str]]: + toolsets: dict[str, list[str]] = {} + in_block = False + block_indent = 0 + current: str | None = None + for raw_line in text.splitlines(): + stripped = raw_line.strip() + if not stripped or stripped.startswith("#"): + continue + indent = len(raw_line) - len(raw_line.lstrip(" ")) + if stripped == "platform_toolsets:": + in_block = True + block_indent = indent + continue + if not in_block: + continue + if indent <= block_indent and not stripped.startswith("- "): + break + key_match = re.fullmatch(r"([A-Za-z0-9_-]+):(?:\s*\[\])?", stripped) + if key_match: + current = key_match.group(1) + toolsets[current] = [] + continue + if stripped.startswith("- ") and current: + toolsets[current].append(unquote(stripped[2:])) + return toolsets + + +env_path = Path("/sandbox/.hermes/.env") +config_path = Path("/sandbox/.hermes/config.yaml") +if env_path.is_symlink(): + print(f"{env_path} is a symlink", file=sys.stderr) + sys.exit(1) +if not env_path.is_file(): + print(f"{env_path} missing", file=sys.stderr) + sys.exit(1) +if not config_path.is_file(): + print(f"{config_path} missing", file=sys.stderr) + sys.exit(1) + +violations = env_violations(env_path) +if violations: + print("raw secret-shaped Hermes .env values:", ", ".join(violations), file=sys.stderr) + sys.exit(1) + +toolsets = parse_platform_toolsets(config_path.read_text(encoding="utf-8")) +api_server_toolsets = set(toolsets.get("api_server", [])) +if not api_server_toolsets: + print("platform_toolsets.api_server missing", file=sys.stderr) + sys.exit(1) +missing = sorted(required_remote_toolsets - api_server_toolsets) +if missing: + print(f"platform_toolsets.api_server missing expected Hermes toolsets: {missing}", file=sys.stderr) + sys.exit(1) +if "no_mcp" in api_server_toolsets: + print("platform_toolsets.api_server unexpectedly disables default MCP servers with no_mcp", file=sys.stderr) + sys.exit(1) +`; + +const MANAGED_TOOL_INSPECTION_SCRIPT = String.raw` +import re +import sys +from pathlib import Path + +secret_key_re = re.compile(r"(^|_)(TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL|API)(_|$)") +slack_alias_re = re.compile(r"^(xoxb|xapp)-OPENSHELL-RESOLVE-ENV-[A-Z0-9_]+$") +allowed_nonsecret_keys = {"API_SERVER_HOST", "API_SERVER_PORT"} +allowed_literals = {"", "[STRIPPED_BY_MIGRATION]"} +required_env_lines = { + "NEMOCLAW_HERMES_TOOL_GATEWAY_BROKER=1", + "FIRECRAWL_GATEWAY_URL=http://host.openshell.internal:11436/firecrawl", + "OPENAI_AUDIO_GATEWAY_URL=http://host.openshell.internal:11436/openai-audio", + "BROWSER_USE_GATEWAY_URL=http://host.openshell.internal:11436/browser-use", + "FAL_QUEUE_GATEWAY_URL=http://host.openshell.internal:11436/fal-queue", + "MODAL_GATEWAY_URL=http://host.openshell.internal:11436/modal", +} +required_config_fragments = [ + "backend: firecrawl", + "provider: openai", + "cloud_provider: browser-use", + "image_gen:", + "backend: modal", + "modal_mode: managed", + "tts:", +] + + +def unquote(value: str) -> str: + value = value.strip() + if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'): + return value[1:-1] + return value + + +def env_violations(path: Path) -> list[str]: + violations: list[str] = [] + for lineno, raw_line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + stripped = raw_line.strip() + if not stripped or stripped.startswith("#") or "=" not in stripped: + continue + if stripped.startswith("export "): + stripped = stripped[len("export ") :].lstrip() + key, value = stripped.split("=", 1) + key = key.strip() + if key in allowed_nonsecret_keys: + continue + if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", key): + continue + if not secret_key_re.search(key): + continue + value = unquote(value) + if ( + value in allowed_literals + or value.startswith("openshell:resolve:env:") + or slack_alias_re.fullmatch(value) + ): + continue + violations.append(f"{key} line {lineno}") + return violations + + +env_path = Path("/sandbox/.hermes/.env") +config_path = Path("/sandbox/.hermes/config.yaml") +if not env_path.is_file() or env_path.is_symlink(): + print(f"{env_path} missing, not a file, or unsafe symlink", file=sys.stderr) + sys.exit(1) +if not config_path.is_file(): + print(f"{config_path} missing", file=sys.stderr) + sys.exit(1) + +env_text = env_path.read_text(encoding="utf-8") +config_text = config_path.read_text(encoding="utf-8") +env_lines = set(env_text.splitlines()) +violations = env_violations(env_path) +if violations: + print("raw secret-shaped managed-tool .env values:", ", ".join(violations), file=sys.stderr) + sys.exit(1) + +missing_env = sorted(required_env_lines - env_lines) +missing_config = [fragment for fragment in required_config_fragments if fragment not in config_text] +for forbidden in ( + "TOOL_GATEWAY_USER_TOKEN", + "NEMOCLAW_HERMES_TOOL_GATEWAY_REFRESH_TOKEN=", + "raw-refresh-token", +): + if forbidden in env_text or forbidden in config_text: + print(f"managed-tool sandbox config contains forbidden token surface: {forbidden}", file=sys.stderr) + sys.exit(1) + +if missing_env: + print("managed-tool .env missing expected gateway lines: " + ", ".join(missing_env), file=sys.stderr) + sys.exit(1) +if missing_config: + print("managed-tool config.yaml missing expected fragments: " + ", ".join(missing_config), file=sys.stderr) + sys.exit(1) +`; + +function safeTag(value: string): string { + return value.replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "") || "local"; +} + +function shellQuote(value: string): string { + return `'${value.replaceAll("'", "'\\''")}'`; +} + +function safeArtifactPart(value: string): string { + return value.toLowerCase().replaceAll("_", "-"); +} + +function managedPresetsBase64(): string { + return Buffer.from( + JSON.stringify(["nous-web", "nous-audio", "nous-browser", "nous-image", "nous-code"]), + ).toString("base64"); +} + +async function requireDocker(probe: DockerProbe, skip: (message: string) => void): Promise { + const result = await probe.run(["info"], { artifactName: "docker-info", timeoutMs: 30_000 }); + if (result.exitCode === 0) return; + + if (process.env.GITHUB_ACTIONS === "true") { + throw new Error( + `Docker is required for Hermes sandbox secret-boundary:\n${resultText(result)}`, + ); + } + skip("Docker daemon is required for Hermes sandbox secret-boundary"); +} + +async function buildHermesImageIfNeeded( + probe: DockerProbe, + image: string, + baseImage: string, + baseImageFromEnv: boolean, +): Promise { + if (process.env.NEMOCLAW_HERMES_TEST_IMAGE) { + await probe.expect(["image", "inspect", image], { + artifactName: "inspect-prebuilt-hermes-image", + timeoutMs: 30_000, + }); + return false; + } + + if (!baseImageFromEnv) { + await probe.expect(["build", "-f", "agents/hermes/Dockerfile.base", "-t", baseImage, "."], { + artifactName: "build-hermes-base-image", + timeoutMs: BUILD_TIMEOUT_MS, + }); + } + + await probe.expect( + [ + "build", + "-f", + "agents/hermes/Dockerfile", + "--build-arg", + `BASE_IMAGE=${baseImage}`, + "-t", + image, + ".", + ], + { artifactName: "build-hermes-production-image", timeoutMs: BUILD_TIMEOUT_MS }, + ); + return true; +} + +async function buildManagedImageIfNeeded( + probe: DockerProbe, + managedImage: string, + baseImage: string, + baseImageFromEnv: boolean, +): Promise { + if (process.env.NEMOCLAW_HERMES_MANAGED_TEST_IMAGE) { + await probe.expect(["image", "inspect", managedImage], { + artifactName: "inspect-prebuilt-managed-hermes-image", + timeoutMs: 30_000, + }); + return false; + } + + const baseExists = await probe.run(["image", "inspect", baseImage], { + artifactName: "inspect-managed-base-image", + timeoutMs: 30_000, + }); + if (!baseImageFromEnv && baseExists.exitCode !== 0) { + await probe.expect(["build", "-f", "agents/hermes/Dockerfile.base", "-t", baseImage, "."], { + artifactName: "build-managed-hermes-base-image", + timeoutMs: BUILD_TIMEOUT_MS, + }); + } + + await probe.expect( + [ + "build", + "-f", + "agents/hermes/Dockerfile", + "--build-arg", + `BASE_IMAGE=${baseImage}`, + "--build-arg", + "NEMOCLAW_HERMES_TOOL_GATEWAY_BROKER=1", + "--build-arg", + `NEMOCLAW_HERMES_TOOL_GATEWAY_PRESETS_B64=${managedPresetsBase64()}`, + "-t", + managedImage, + ".", + ], + { artifactName: "build-managed-hermes-production-image", timeoutMs: BUILD_TIMEOUT_MS }, + ); + return true; +} + +async function inspectImageBoundary(probe: DockerProbe, image: string): Promise { + const result = await probe.run( + ["run", "--rm", "--entrypoint", "python3", image, "-c", IMAGE_INSPECTION_SCRIPT], + { + artifactName: "inspect-hermes-sandbox-boundary", + timeoutMs: RUN_TIMEOUT_MS, + }, + ); + + expect( + result.exitCode, + `Hermes image should have no raw secret-shaped .env values and preserve remote toolsets\n${resultText(result)}`, + ).toBe(0); +} + +async function inspectManagedToolBoundary(probe: DockerProbe, image: string): Promise { + const result = await probe.run( + ["run", "--rm", "--entrypoint", "python3", image, "-c", MANAGED_TOOL_INSPECTION_SCRIPT], + { + artifactName: "inspect-managed-tool-hermes-boundary", + timeoutMs: RUN_TIMEOUT_MS, + }, + ); + + expect( + result.exitCode, + `Managed-tool Hermes image should keep gateway auth out of sandbox config\n${resultText(result)}`, + ).toBe(0); +} + +async function runStartupWithEnvFileEntry( + probe: DockerProbe, + image: string, + assignment: string, + key: string, +): Promise { + const script = `set -euo pipefail; printf '%s\n' ${shellQuote( + assignment, + )} >> /sandbox/.hermes/.env; exec /usr/local/bin/nemoclaw-start true`; + return probe.run( + ["run", "--rm", "--user", "sandbox", "--entrypoint", "/bin/bash", image, "-lc", script], + { + artifactName: `startup-rejects-env-file-${safeArtifactPart(key)}`, + timeoutMs: RUN_TIMEOUT_MS, + }, + ); +} + +async function runStartupWithRuntimeEnvEntry( + probe: DockerProbe, + image: string, + assignment: string, + key: string, +): Promise { + return probe.run( + [ + "run", + "--rm", + "--user", + "sandbox", + "--env", + assignment, + "--entrypoint", + "/usr/local/bin/nemoclaw-start", + image, + "true", + ], + { + artifactName: `startup-rejects-runtime-env-${safeArtifactPart(key)}`, + timeoutMs: RUN_TIMEOUT_MS, + }, + ); +} + +async function expectStartupRejectsEnvFileEntry( + probe: DockerProbe, + image: string, + assignment: string, + key: string, + value: string, +): Promise { + const result = await runStartupWithEnvFileEntry(probe, image, assignment, key); + const output = `${result.stdout}\n${result.stderr}`; + + expect(result.exitCode, `Hermes startup should reject ${key}\n${resultText(result)}`).not.toBe(0); + expect( + output, + `Hermes startup rejection should mention raw secret-shaped values for ${key}`, + ).toContain("raw secret-shaped values"); + expect(output, `Hermes startup rejection should name ${key}`).toContain(key); + expect(output, `Hermes startup rejection must not print ${key}'s raw value`).not.toContain(value); +} + +async function expectStartupRejectsRuntimeEnvEntry( + probe: DockerProbe, + image: string, + assignment: string, + key: string, + value: string, +): Promise { + const result = await runStartupWithRuntimeEnvEntry(probe, image, assignment, key); + const output = `${result.stdout}\n${result.stderr}`; + + expect( + result.exitCode, + `Hermes startup should reject runtime env ${key}\n${resultText(result)}`, + ).not.toBe(0); + expect( + output, + `Hermes startup rejection should mention process environment for ${key}`, + ).toContain("process environment"); + expect(output, `Hermes startup rejection should name ${key}`).toContain(key); + expect(output, `Hermes startup rejection must not print ${key}'s raw value`).not.toContain(value); +} + +liveTest( + "hermes sandbox secret boundary keeps raw secrets out of images and startup", + async ({ artifacts, cleanup, secrets, skip }) => { + const probe = new DockerProbe(artifacts, (text, extraValues) => + secrets.redact(text, extraValues), + ); + const runId = safeTag(`${process.env.GITHUB_RUN_ID ?? "local"}-${process.pid}-${Date.now()}`); + const baseImageFromEnv = Boolean( + process.env.NEMOCLAW_HERMES_BASE_IMAGE ?? process.env.HERMES_BASE_IMAGE, + ); + const image = + process.env.NEMOCLAW_HERMES_TEST_IMAGE ?? `nemoclaw-hermes-secret-boundary:${runId}`; + const baseImage = + process.env.NEMOCLAW_HERMES_BASE_IMAGE ?? + process.env.HERMES_BASE_IMAGE ?? + `nemoclaw-hermes-secret-boundary-base:${runId}`; + const managedImage = + process.env.NEMOCLAW_HERMES_MANAGED_TEST_IMAGE ?? + `nemoclaw-hermes-secret-boundary-managed:${runId}`; + let removeImage = false; + let removeManagedImage = false; + let removeBaseImage = false; + + await artifacts.writeJson("scenario.json", { + id: "hermes-sandbox-secret-boundary", + runner: "vitest", + boundary: "docker-hermes-image-and-startup", + legacySource: "test/e2e/test-hermes-sandbox-secret-boundary.sh", + image, + baseImage, + managedImage, + prebuiltImage: Boolean(process.env.NEMOCLAW_HERMES_TEST_IMAGE), + prebuiltManagedImage: Boolean(process.env.NEMOCLAW_HERMES_MANAGED_TEST_IMAGE), + contract: [ + "Docker is required and prebuilt image env vars must reference inspectable images", + "Hermes .env in the sandbox image is a real file and has no raw secret-shaped values", + "Hermes config preserves api_server remote platform toolsets and does not use no_mcp", + "managed-tool image keeps gateway auth tokens out of sandbox env/config while preserving gateway URLs/config", + "nemoclaw-start rejects raw secret-shaped .env entries without echoing their values", + "nemoclaw-start rejects raw secret-shaped process env entries without echoing their values", + ], + }); + + cleanup.add("remove Hermes sandbox secret-boundary images", async () => { + const images = [ + removeImage ? image : undefined, + removeManagedImage ? managedImage : undefined, + removeBaseImage ? baseImage : undefined, + ].filter((value): value is string => Boolean(value)); + if (images.length === 0) return; + await probe.run(["rmi", "-f", ...images], { + artifactName: "cleanup-hermes-secret-boundary-images", + timeoutMs: 60_000, + }); + }); + + await requireDocker(probe, skip); + + removeImage = await buildHermesImageIfNeeded(probe, image, baseImage, baseImageFromEnv); + await probe.expect(["image", "inspect", image], { + artifactName: "inspect-hermes-image-after-build", + timeoutMs: 30_000, + }); + removeManagedImage = await buildManagedImageIfNeeded( + probe, + managedImage, + baseImage, + baseImageFromEnv, + ); + removeBaseImage = !baseImageFromEnv && (removeImage || removeManagedImage); + await probe.expect(["image", "inspect", managedImage], { + artifactName: "inspect-managed-hermes-image-after-build", + timeoutMs: 30_000, + }); + + await inspectImageBoundary(probe, image); + await inspectManagedToolBoundary(probe, managedImage); + await expectStartupRejectsEnvFileEntry( + probe, + image, + `DEVTEST_API_TOKEN=${RAW_SECRET_SENTINEL}`, + "DEVTEST_API_TOKEN", + RAW_SECRET_SENTINEL, + ); + await expectStartupRejectsEnvFileEntry( + probe, + image, + `INTERNAL_API=${RAW_SECRET_SENTINEL}`, + "INTERNAL_API", + RAW_SECRET_SENTINEL, + ); + await expectStartupRejectsEnvFileEntry( + probe, + image, + "OPENAI_API_KEY=sk-OPENSHELL-PROXY-REWRITE", + "OPENAI_API_KEY", + "sk-OPENSHELL-PROXY-REWRITE", + ); + await expectStartupRejectsRuntimeEnvEntry( + probe, + image, + `DEVTEST_API_TOKEN=${RAW_SECRET_SENTINEL}`, + "DEVTEST_API_TOKEN", + RAW_SECRET_SENTINEL, + ); + await expectStartupRejectsRuntimeEnvEntry( + probe, + image, + `NEMOCLAW_HERMES_TOOL_GATEWAY_REFRESH_TOKEN=${RAW_REFRESH_TOKEN}`, + "NEMOCLAW_HERMES_TOOL_GATEWAY_REFRESH_TOKEN", + RAW_REFRESH_TOKEN, + ); + + await artifacts.writeJson("scenario-result.json", { + id: "hermes-sandbox-secret-boundary", + image, + managedImage, + assertions: { + imageEnvSecretBoundaryVerified: true, + imageRemoteToolsetsVerified: true, + managedToolGatewayAuthBoundaryVerified: true, + envFileSecretRejectionsVerified: true, + runtimeEnvSecretRejectionsVerified: true, + rejectionOutputRedactionVerified: true, + }, + }); + }, +); diff --git a/test/e2e-scenario/support-tests/hermes-secret-boundary-workflow.test.ts b/test/e2e-scenario/support-tests/hermes-secret-boundary-workflow.test.ts new file mode 100644 index 00000000000..c8f614bd5bb --- /dev/null +++ b/test/e2e-scenario/support-tests/hermes-secret-boundary-workflow.test.ts @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { expect, it } from "vitest"; + +import { + evaluateE2eVitestWorkflowDispatchSelectors, + readFreeStandingJobsInventory, +} from "../../../tools/e2e-scenarios/workflow-boundary.mts"; + +it("routes Hermes sandbox secret-boundary selective dispatch to its free-standing Vitest job", () => { + const inventory = readFreeStandingJobsInventory(); + + expect(inventory.allowedJobs).toContain("hermes-sandbox-secret-boundary-vitest"); + expect(inventory.scenarioToJob.get("hermes-sandbox-secret-boundary")).toBe( + "hermes-sandbox-secret-boundary-vitest", + ); + expect( + evaluateE2eVitestWorkflowDispatchSelectors({ + scenarios: "hermes-sandbox-secret-boundary", + }), + ).toMatchObject({ + valid: true, + liveScenariosRuns: false, + selectedFreeStandingJobs: ["hermes-sandbox-secret-boundary-vitest"], + registryScenarios: [], + }); + expect( + evaluateE2eVitestWorkflowDispatchSelectors({ + jobs: "hermes-sandbox-secret-boundary-vitest", + }), + ).toMatchObject({ + valid: true, + liveScenariosRuns: false, + selectedFreeStandingJobs: ["hermes-sandbox-secret-boundary-vitest"], + registryScenarios: [], + }); +}); diff --git a/tools/e2e-scenarios/workflow-boundary.mts b/tools/e2e-scenarios/workflow-boundary.mts index 09d2b254813..854b12423e1 100644 --- a/tools/e2e-scenarios/workflow-boundary.mts +++ b/tools/e2e-scenarios/workflow-boundary.mts @@ -2813,6 +2813,134 @@ function validateHermesRootEntrypointSmokeVitestJob(errors: string[], jobs: Work } } +function validateHermesSandboxSecretBoundaryVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { + const jobName = "hermes-sandbox-secret-boundary-vitest"; + const scenarioName = "hermes-sandbox-secret-boundary"; + const job = asRecord(jobs[jobName]); + if (Object.keys(job).length === 0) { + errors.push("workflow missing hermes-sandbox-secret-boundary-vitest job"); + return; + } + + if (job["runs-on"] !== "ubuntu-latest") { + errors.push("hermes-sandbox-secret-boundary-vitest job must run on ubuntu-latest"); + } + validateFreeStandingJobSelector(errors, jobs, jobName, scenarioName); + if (job["timeout-minutes"] !== 60) { + errors.push("hermes-sandbox-secret-boundary-vitest job must keep the 60 minute timeout"); + } + + const jobEnv = asRecord(job.env); + if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { + errors.push("hermes-sandbox-secret-boundary-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + } + if ( + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/hermes-sandbox-secret-boundary" + ) { + errors.push( + "hermes-sandbox-secret-boundary-vitest job must write artifacts under e2e-artifacts/vitest/hermes-sandbox-secret-boundary", + ); + } + for (const secret of ["NVIDIA_INFERENCE_API_KEY", "DOCKERHUB_USERNAME", "DOCKERHUB_TOKEN"]) { + requireEnvDoesNotExposeSecret( + errors, + "hermes-sandbox-secret-boundary-vitest job", + jobEnv, + secret, + ); + } + + const steps = asSteps(job.steps); + requireNoDispatchInputInterpolation(errors, steps); + for (const step of steps) { + const stepName = `hermes-sandbox-secret-boundary-vitest step '${ + step.name ?? step.uses ?? "" + }'`; + const stepEnv = asRecord(step.env); + requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_INFERENCE_API_KEY"); + requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); + requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); + } + + if (namedStep(steps, "Authenticate to Docker Hub")) { + errors.push( + "hermes-sandbox-secret-boundary-vitest must not authenticate to Docker Hub before branch-controlled test code runs", + ); + } + + const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + if (!checkout) errors.push("hermes-sandbox-secret-boundary-vitest job missing checkout step"); + requireFullShaAction(errors, checkout, "hermes-sandbox-secret-boundary-vitest checkout"); + if (asRecord(checkout?.with)["persist-credentials"] !== false) { + errors.push( + "hermes-sandbox-secret-boundary-vitest checkout step must set persist-credentials=false", + ); + } + + const setupNode = namedStep(steps, "Set up Node"); + if (!setupNode) + errors.push("hermes-sandbox-secret-boundary-vitest job missing step: Set up Node"); + requireFullShaAction(errors, setupNode, "hermes-sandbox-secret-boundary-vitest setup-node"); + + const installRootDependencies = requireJobStep( + errors, + jobName, + steps, + "Install root dependencies", + ); + requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run Hermes sandbox secret-boundary live test", + ); + requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/hermes-sandbox-secret-boundary.test.ts", + ); + requireRunDoesNotContain(errors, runVitest, "${{ inputs."); + + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload Hermes sandbox secret-boundary artifacts", + ); + requireFullShaAction(errors, upload, "hermes-sandbox-secret-boundary-vitest upload-artifact"); + const uploadWith = asRecord(upload?.with); + if (uploadWith.name !== "e2e-vitest-scenarios-hermes-sandbox-secret-boundary") { + errors.push("hermes-sandbox-secret-boundary-vitest artifact upload name must be stable"); + } + const uploadPath = stringValue(uploadWith.path); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/hermes-sandbox-secret-boundary/", + ); + if (uploadWith["include-hidden-files"] !== false) { + errors.push( + "hermes-sandbox-secret-boundary-vitest artifact upload must set include-hidden-files: false", + ); + } + if (uploadWith["if-no-files-found"] !== "ignore") { + errors.push( + "hermes-sandbox-secret-boundary-vitest artifact upload must ignore missing fixture artifacts", + ); + } + if (uploadWith["retention-days"] !== 14) { + errors.push("hermes-sandbox-secret-boundary-vitest artifact upload retention-days must be 14"); + } +} + function validateDiagnosticsVitestJob(errors: string[], jobs: WorkflowRecord): void { const jobName = "diagnostics-vitest"; const scenarioName = "diagnostics"; @@ -4786,6 +4914,7 @@ export function validateE2eVitestScenariosWorkflowBoundary( validateDoubleOnboardVitestJob(errors, jobs); validateHermesE2EVitestJob(errors, jobs); validateHermesRootEntrypointSmokeVitestJob(errors, jobs); + validateHermesSandboxSecretBoundaryVitestJob(errors, jobs); validateNetworkPolicyVitestJob(errors, jobs); validateCommonEgressAgentVitestJob(errors, jobs); validateShieldsConfigVitestJob(errors, jobs); From 020348b34c04a800255051b1fdaf9f316933aa57 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Mon, 22 Jun 2026 16:52:48 -0400 Subject: [PATCH 2/6] test(e2e): satisfy test guardrails --- .../hermes-sandbox-secret-boundary.test.ts | 128 ++++++++++++------ 1 file changed, 86 insertions(+), 42 deletions(-) diff --git a/test/e2e-scenario/live/hermes-sandbox-secret-boundary.test.ts b/test/e2e-scenario/live/hermes-sandbox-secret-boundary.test.ts index 82c90e8c826..968e5f6d632 100644 --- a/test/e2e-scenario/live/hermes-sandbox-secret-boundary.test.ts +++ b/test/e2e-scenario/live/hermes-sandbox-secret-boundary.test.ts @@ -256,39 +256,47 @@ function managedPresetsBase64(): string { ).toString("base64"); } +function throwDockerRequired(result: DockerCommandResult): never { + throw new Error(`Docker is required for Hermes sandbox secret-boundary:\n${resultText(result)}`); +} + async function requireDocker(probe: DockerProbe, skip: (message: string) => void): Promise { const result = await probe.run(["info"], { artifactName: "docker-info", timeoutMs: 30_000 }); - if (result.exitCode === 0) return; + result.exitCode === 0 + ? undefined + : process.env.GITHUB_ACTIONS === "true" + ? throwDockerRequired(result) + : skip("Docker daemon is required for Hermes sandbox secret-boundary"); +} - if (process.env.GITHUB_ACTIONS === "true") { - throw new Error( - `Docker is required for Hermes sandbox secret-boundary:\n${resultText(result)}`, - ); - } - skip("Docker daemon is required for Hermes sandbox secret-boundary"); +function inspectPrebuiltImage(probe: DockerProbe, image: string): Promise { + return probe + .expect(["image", "inspect", image], { + artifactName: "inspect-prebuilt-hermes-image", + timeoutMs: 30_000, + }) + .then(() => false); } -async function buildHermesImageIfNeeded( +async function buildBaseImageWhenNeeded( probe: DockerProbe, - image: string, baseImage: string, baseImageFromEnv: boolean, -): Promise { - if (process.env.NEMOCLAW_HERMES_TEST_IMAGE) { - await probe.expect(["image", "inspect", image], { - artifactName: "inspect-prebuilt-hermes-image", - timeoutMs: 30_000, - }); - return false; - } - - if (!baseImageFromEnv) { - await probe.expect(["build", "-f", "agents/hermes/Dockerfile.base", "-t", baseImage, "."], { - artifactName: "build-hermes-base-image", - timeoutMs: BUILD_TIMEOUT_MS, - }); - } + artifactName: string, +): Promise { + await (baseImageFromEnv + ? Promise.resolve() + : probe.expect(["build", "-f", "agents/hermes/Dockerfile.base", "-t", baseImage, "."], { + artifactName, + timeoutMs: BUILD_TIMEOUT_MS, + })); +} +async function buildHermesProductionImage( + probe: DockerProbe, + image: string, + baseImage: string, +): Promise { await probe.expect( [ "build", @@ -305,31 +313,54 @@ async function buildHermesImageIfNeeded( return true; } -async function buildManagedImageIfNeeded( +async function buildHermesImageIfNeeded( probe: DockerProbe, - managedImage: string, + image: string, baseImage: string, baseImageFromEnv: boolean, ): Promise { - if (process.env.NEMOCLAW_HERMES_MANAGED_TEST_IMAGE) { - await probe.expect(["image", "inspect", managedImage], { + return process.env.NEMOCLAW_HERMES_TEST_IMAGE + ? inspectPrebuiltImage(probe, image) + : (await buildBaseImageWhenNeeded( + probe, + baseImage, + baseImageFromEnv, + "build-hermes-base-image", + ), + buildHermesProductionImage(probe, image, baseImage)); +} + +function inspectPrebuiltManagedImage(probe: DockerProbe, managedImage: string): Promise { + return probe + .expect(["image", "inspect", managedImage], { artifactName: "inspect-prebuilt-managed-hermes-image", timeoutMs: 30_000, - }); - return false; - } + }) + .then(() => false); +} +async function buildManagedBaseImageWhenNeeded( + probe: DockerProbe, + baseImage: string, + baseImageFromEnv: boolean, +): Promise { const baseExists = await probe.run(["image", "inspect", baseImage], { artifactName: "inspect-managed-base-image", timeoutMs: 30_000, }); - if (!baseImageFromEnv && baseExists.exitCode !== 0) { - await probe.expect(["build", "-f", "agents/hermes/Dockerfile.base", "-t", baseImage, "."], { - artifactName: "build-managed-hermes-base-image", - timeoutMs: BUILD_TIMEOUT_MS, - }); - } + await (baseImageFromEnv || baseExists.exitCode === 0 + ? Promise.resolve() + : probe.expect(["build", "-f", "agents/hermes/Dockerfile.base", "-t", baseImage, "."], { + artifactName: "build-managed-hermes-base-image", + timeoutMs: BUILD_TIMEOUT_MS, + })); +} +async function buildManagedProductionImage( + probe: DockerProbe, + managedImage: string, + baseImage: string, +): Promise { await probe.expect( [ "build", @@ -350,6 +381,18 @@ async function buildManagedImageIfNeeded( return true; } +async function buildManagedImageIfNeeded( + probe: DockerProbe, + managedImage: string, + baseImage: string, + baseImageFromEnv: boolean, +): Promise { + return process.env.NEMOCLAW_HERMES_MANAGED_TEST_IMAGE + ? inspectPrebuiltManagedImage(probe, managedImage) + : (await buildManagedBaseImageWhenNeeded(probe, baseImage, baseImageFromEnv), + buildManagedProductionImage(probe, managedImage, baseImage)); +} + async function inspectImageBoundary(probe: DockerProbe, image: string): Promise { const result = await probe.run( ["run", "--rm", "--entrypoint", "python3", image, "-c", IMAGE_INSPECTION_SCRIPT], @@ -514,11 +557,12 @@ liveTest( removeManagedImage ? managedImage : undefined, removeBaseImage ? baseImage : undefined, ].filter((value): value is string => Boolean(value)); - if (images.length === 0) return; - await probe.run(["rmi", "-f", ...images], { - artifactName: "cleanup-hermes-secret-boundary-images", - timeoutMs: 60_000, - }); + await (images.length === 0 + ? Promise.resolve() + : probe.run(["rmi", "-f", ...images], { + artifactName: "cleanup-hermes-secret-boundary-images", + timeoutMs: 60_000, + })); }); await requireDocker(probe, skip); From f6d2c6321764f3e8bad3a33bfa8f4bb2373e9eda Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Mon, 22 Jun 2026 21:55:06 -0400 Subject: [PATCH 3/6] test(e2e): restore main workflow boundary changes --- .github/workflows/e2e-vitest-scenarios.yaml | 193 + tools/e2e-scenarios/workflow-boundary.mts | 4170 +++++++++++++++---- 2 files changed, 3544 insertions(+), 819 deletions(-) diff --git a/.github/workflows/e2e-vitest-scenarios.yaml b/.github/workflows/e2e-vitest-scenarios.yaml index a9e5972a732..62e7d27629d 100644 --- a/.github/workflows/e2e-vitest-scenarios.yaml +++ b/.github/workflows/e2e-vitest-scenarios.yaml @@ -1563,6 +1563,103 @@ jobs: if-no-files-found: ignore retention-days: 14 + hermes-slack-vitest: + needs: generate-matrix + if: ${{ (inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',hermes-slack-vitest,') || contains(format(',{0},', inputs.scenarios), ',hermes-slack,') }} + runs-on: linux-amd64-cpu4 + timeout-minutes: 75 + env: + FREE_STANDING_VITEST_JOB: "1" + FREE_STANDING_SCENARIO_ID: "hermes-slack" + E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/vitest/hermes-slack-e2e + NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js + NEMOCLAW_RUN_E2E_SCENARIOS: "1" + NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" + NEMOCLAW_AGENT: hermes + NEMOCLAW_NON_INTERACTIVE: "1" + NEMOCLAW_POLICY_TIER: open + NEMOCLAW_RECREATE_SANDBOX: "1" + NEMOCLAW_SANDBOX_NAME: e2e-hermes-slack + OPENSHELL_GATEWAY: nemoclaw + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Configure isolated Docker auth directory + run: echo "DOCKER_CONFIG=${RUNNER_TEMP}/docker-config-hermes-slack" >> "$GITHUB_ENV" + + - name: Authenticate to Docker Hub + env: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + shell: bash + run: | + set -euo pipefail + if [[ -z "${DOCKERHUB_USERNAME}" || -z "${DOCKERHUB_TOKEN}" ]]; then + echo "::notice::Docker Hub credentials not configured; continuing with anonymous pulls." + exit 0 + fi + mkdir -p "${DOCKER_CONFIG}" + chmod 700 "${DOCKER_CONFIG}" + login_succeeded=0 + for attempt in 1 2 3; do + if echo "${DOCKERHUB_TOKEN}" | timeout 30s docker login docker.io --username "${DOCKERHUB_USERNAME}" --password-stdin; then + login_succeeded=1 + break + fi + if [[ "$attempt" -lt 3 ]]; then + echo "::warning::Docker Hub login attempt ${attempt} failed; retrying." + sleep 5 + fi + done + if [[ "$login_succeeded" -ne 1 ]]; then + echo "::warning::Docker Hub login failed after 3 attempts; continuing with anonymous pulls." + fi + + - 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 Hermes Slack live test + # Migrated from test/e2e/test-hermes-slack-e2e.sh. Preserves the + # linux-amd64-cpu4 Docker/OpenShell/Hermes Slack policy, placeholder, + # provider, secret-boundary, and Python Slack egress contracts. + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} + SLACK_BOT_TOKEN: xoxb-test-hermes-slack-token + SLACK_APP_TOKEN: xapp-test-hermes-slack-app-token + run: | + set -euo pipefail + npx vitest run --project e2e-scenarios-live \ + test/e2e-scenario/live/hermes-slack-e2e.test.ts \ + --silent=false --reporter=default + + - name: Upload Hermes Slack artifacts + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: e2e-vitest-scenarios-hermes-slack + path: e2e-artifacts/vitest/hermes-slack-e2e/ + include-hidden-files: false + if-no-files-found: ignore + retention-days: 14 + + - name: Clean up Docker auth + if: always() + run: | + set -euo pipefail + docker logout docker.io || true + rm -rf "${DOCKER_CONFIG}" + hermes-e2e-vitest: needs: generate-matrix if: ${{ needs.generate-matrix.outputs.hermes_selected == 'true' }} @@ -1617,6 +1714,100 @@ jobs: if-no-files-found: ignore retention-days: 14 + hermes-discord-vitest: + needs: generate-matrix + if: ${{ (inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',hermes-discord-vitest,') || contains(format(',{0},', inputs.scenarios), ',hermes-discord,') }} + runs-on: ubuntu-latest + timeout-minutes: 75 + env: + FREE_STANDING_VITEST_JOB: "1" + FREE_STANDING_SCENARIO_ID: "hermes-discord" + E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/vitest/hermes-discord + NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js + NEMOCLAW_RUN_E2E_SCENARIOS: "1" + NEMOCLAW_SANDBOX_NAME: e2e-hermes-discord + NEMOCLAW_AGENT: hermes + NEMOCLAW_POLICY_TIER: open + OPENSHELL_GATEWAY: nemoclaw + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Isolate Docker auth + run: echo "DOCKER_CONFIG=${RUNNER_TEMP}/docker-config-hermes-discord" >> "$GITHUB_ENV" + + - name: Authenticate to Docker Hub + env: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + shell: bash + run: | + set -euo pipefail + if [[ -z "${DOCKERHUB_USERNAME}" || -z "${DOCKERHUB_TOKEN}" ]]; then + echo "::notice::Docker Hub credentials not configured; continuing with anonymous pulls." + exit 0 + fi + login_succeeded=0 + for attempt in 1 2 3; do + if echo "${DOCKERHUB_TOKEN}" | timeout 30s docker login docker.io --username "${DOCKERHUB_USERNAME}" --password-stdin; then + login_succeeded=1 + break + fi + if [[ "$attempt" -lt 3 ]]; then + echo "::warning::Docker Hub login attempt ${attempt} failed; retrying." + sleep 5 + fi + done + if [[ "$login_succeeded" -ne 1 ]]; then + echo "::warning::Docker Hub login failed after 3 attempts; continuing with anonymous pulls." + fi + + - 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 Hermes Discord live test + # Migrated from test/e2e/test-hermes-discord-e2e.sh. Preserves the + # ubuntu-latest Docker/OpenShell/Hermes Discord schema, provider, + # placeholder isolation, native gateway rewrite, and rebuild contracts. + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} + DISCORD_BOT_TOKEN: test-fake-discord-token-hermes-e2e + DISCORD_SERVER_IDS: "1491590992753590594" + DISCORD_ALLOWED_IDS: "1005536447329222676" + DISCORD_REQUIRE_MENTION: "0" + run: | + set -euo pipefail + npx vitest run --project e2e-scenarios-live \ + test/e2e-scenario/live/hermes-discord.test.ts \ + --silent=false --reporter=default + + - name: Upload Hermes Discord artifacts + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: e2e-vitest-scenarios-hermes-discord + path: e2e-artifacts/vitest/hermes-discord/ + include-hidden-files: false + if-no-files-found: ignore + retention-days: 14 + + - name: Clean up Docker auth + if: always() + run: | + set -euo pipefail + docker logout docker.io || true + rm -rf "${DOCKER_CONFIG:-}" + network-policy-vitest: needs: generate-matrix if: ${{ (inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',network-policy-vitest,') || contains(format(',{0},', inputs.scenarios), ',network-policy,') }} @@ -5176,6 +5367,8 @@ jobs: sessions-agents-cli-vitest, runtime-overrides-vitest, hermes-e2e-vitest, + hermes-slack-vitest, + hermes-discord-vitest, hermes-root-entrypoint-smoke-vitest, hermes-sandbox-secret-boundary-vitest, network-policy-vitest, diff --git a/tools/e2e-scenarios/workflow-boundary.mts b/tools/e2e-scenarios/workflow-boundary.mts index dda3500835d..4ab480940a6 100644 --- a/tools/e2e-scenarios/workflow-boundary.mts +++ b/tools/e2e-scenarios/workflow-boundary.mts @@ -84,7 +84,9 @@ function deriveFreeStandingJobsInventoryFromJobs(jobs: WorkflowRecord): { if (!hasJobMarker && !hasScenarioMarker) continue; if (!SELECTOR_ID_PATTERN.test(jobId)) { - errors.push(`free-standing workflow metadata contains invalid job id: ${jobId}`); + errors.push( + `free-standing workflow metadata contains invalid job id: ${jobId}`, + ); } if (!hasJobMarker) { errors.push( @@ -102,7 +104,9 @@ function deriveFreeStandingJobsInventoryFromJobs(jobs: WorkflowRecord): { const scenario = env[FREE_STANDING_SCENARIO_MARKER]; if (typeof scenario !== "string" || !SELECTOR_ID_PATTERN.test(scenario)) { - errors.push(`${jobId} job ${FREE_STANDING_SCENARIO_MARKER} must be a selector id`); + errors.push( + `${jobId} job ${FREE_STANDING_SCENARIO_MARKER} must be a selector id`, + ); continue; } freeStandingScenarios.push(scenario); @@ -110,13 +114,17 @@ function deriveFreeStandingJobsInventoryFromJobs(jobs: WorkflowRecord): { } if (allowedJobs.length === 0) { - errors.push("free-standing workflow metadata must declare at least one job"); + errors.push( + "free-standing workflow metadata must declare at least one job", + ); } for (const duplicate of findDuplicates(allowedJobs)) { errors.push(`free-standing workflow metadata repeats job id: ${duplicate}`); } for (const duplicate of findDuplicates(freeStandingScenarios)) { - errors.push(`free-standing workflow metadata repeats scenario id: ${duplicate}`); + errors.push( + `free-standing workflow metadata repeats scenario id: ${duplicate}`, + ); } return { @@ -129,7 +137,10 @@ function deriveFreeStandingJobsInventoryFromJobs(jobs: WorkflowRecord): { }; } -const freeStandingJobsInventoryCache = new Map(); +const freeStandingJobsInventoryCache = new Map< + string, + CachedFreeStandingJobsInventory +>(); function readWorkflowRecord(workflowPath: string): WorkflowRecord { return asRecord(YAML.parse(readFileSync(workflowPath, "utf-8"))); @@ -149,7 +160,8 @@ export function validateFreeStandingWorkflowInventory( workflowPath = DEFAULT_VITEST_WORKFLOW_PATH, ): string[] { const workflow = readWorkflowRecord(workflowPath); - return deriveFreeStandingJobsInventoryFromJobs(asRecord(workflow.jobs)).errors; + return deriveFreeStandingJobsInventoryFromJobs(asRecord(workflow.jobs)) + .errors; } export function readFreeStandingJobsInventory( @@ -157,14 +169,22 @@ export function readFreeStandingJobsInventory( ): FreeStandingJobsInventory { const stats = statSync(workflowPath); const cached = freeStandingJobsInventoryCache.get(workflowPath); - if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) { + if ( + cached && + cached.mtimeMs === stats.mtimeMs && + cached.size === stats.size + ) { return cloneFreeStandingJobsInventory(cached.inventory); } const workflow = readWorkflowRecord(workflowPath); - const { errors, inventory } = deriveFreeStandingJobsInventoryFromJobs(asRecord(workflow.jobs)); + const { errors, inventory } = deriveFreeStandingJobsInventoryFromJobs( + asRecord(workflow.jobs), + ); if (errors.length > 0) { - throw new Error(`Invalid free-standing workflow inventory:\n${errors.join("\n")}`); + throw new Error( + `Invalid free-standing workflow inventory:\n${errors.join("\n")}`, + ); } freeStandingJobsInventoryCache.set(workflowPath, { mtimeMs: stats.mtimeMs, @@ -288,12 +308,20 @@ export function evaluateE2eVitestWorkflowDispatchSelectors(input: { }; } -function namedStep(steps: readonly WorkflowStep[], name: string): WorkflowStep | undefined { +function namedStep( + steps: readonly WorkflowStep[], + name: string, +): WorkflowStep | undefined { return steps.find((step) => step.name === name); } -function requireInput(errors: string[], inputs: WorkflowRecord, name: string): void { - if (!Object.hasOwn(inputs, name)) errors.push(`workflow_dispatch missing input: ${name}`); +function requireInput( + errors: string[], + inputs: WorkflowRecord, + name: string, +): void { + if (!Object.hasOwn(inputs, name)) + errors.push(`workflow_dispatch missing input: ${name}`); } function requireStep( @@ -324,7 +352,9 @@ function requireRunContains( ): void { if (!step) return; if (!stringValue(step.run).includes(expected)) { - errors.push(`step '${step.name ?? ""}' run script must include ${expected}`); + errors.push( + `step '${step.name ?? ""}' run script must include ${expected}`, + ); } } @@ -335,11 +365,17 @@ function requireRunDoesNotContain( ): void { if (!step) return; if (stringValue(step.run).includes(forbidden)) { - errors.push(`step '${step.name ?? ""}' run script must not include ${forbidden}`); + errors.push( + `step '${step.name ?? ""}' run script must not include ${forbidden}`, + ); } } -function requireUploadPathContains(errors: string[], uploadPath: string, expected: string): void { +function requireUploadPathContains( + errors: string[], + uploadPath: string, + expected: string, +): void { if (!uploadPath.includes(expected)) { errors.push(`artifact upload path must include ${expected}`); } @@ -356,16 +392,28 @@ function requireEnvDoesNotExposeSecret( } } -function requireWorkflowDispatch(errors: string[], triggers: WorkflowRecord): WorkflowRecord { +function requireWorkflowDispatch( + errors: string[], + triggers: WorkflowRecord, +): WorkflowRecord { const workflowDispatch = asRecord(triggers.workflow_dispatch); if (Object.keys(workflowDispatch).length === 0) errors.push("workflow must support workflow_dispatch"); return workflowDispatch; } -function rejectAutomaticTriggers(errors: string[], triggers: WorkflowRecord): void { - for (const unsafe of ["push", "pull_request", "pull_request_target", "schedule"]) { - if (Object.hasOwn(triggers, unsafe)) errors.push(`workflow must not run on ${unsafe}`); +function rejectAutomaticTriggers( + errors: string[], + triggers: WorkflowRecord, +): void { + for (const unsafe of [ + "push", + "pull_request", + "pull_request_target", + "schedule", + ]) { + if (Object.hasOwn(triggers, unsafe)) + errors.push(`workflow must not run on ${unsafe}`); } } @@ -384,7 +432,8 @@ function requireNoDispatchInputInterpolation( errors: string[], steps: readonly WorkflowStep[], ): void { - const expressionPattern = /\$\{\{\s*(?:inputs|github\.event\.inputs)\s*(?:\.|\[)/; + const expressionPattern = + /\$\{\{\s*(?:inputs|github\.event\.inputs)\s*(?:\.|\[)/; for (const step of steps) { if (expressionPattern.test(stringValue(step.run))) { errors.push( @@ -430,7 +479,12 @@ function validateFreeStandingInventoryBoundary( if (Object.keys(job).length === 0) continue; if (!FREE_STANDING_SELECTOR_SPECIAL_CASES.has(jobName)) { - validateFreeStandingJobSelector(errors, jobs, jobName, scenarioByJob.get(jobName)); + validateFreeStandingJobSelector( + errors, + jobs, + jobName, + scenarioByJob.get(jobName), + ); } const jobEnv = asRecord(job.env); @@ -442,7 +496,11 @@ function validateFreeStandingInventoryBoundary( requireNoDispatchInputInterpolation(errors, steps); for (const step of steps) { if (step.uses) { - requireFullShaAction(errors, step, `${jobName} step '${step.name ?? step.uses}'`); + requireFullShaAction( + errors, + step, + `${jobName} step '${step.name ?? step.uses}'`, + ); } if (/\$\{\{\s*secrets\./.test(stringValue(step.run))) { errors.push( @@ -469,7 +527,9 @@ function validateFreeStandingInventoryCoverage( } for (const [scenario, jobId] of inventory.scenarioToJob) { if (!inventory.allowedJobs.includes(jobId)) { - errors.push(`free-standing inventory maps ${scenario} to unknown job ${jobId}`); + errors.push( + `free-standing inventory maps ${scenario} to unknown job ${jobId}`, + ); continue; } const job = asRecord(jobs[jobId]); @@ -487,7 +547,10 @@ function validateFreeStandingInventoryCoverage( } } -function validateOpenShellVersionPinVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateOpenShellVersionPinVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "openshell-version-pin-vitest"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { @@ -498,14 +561,22 @@ function validateOpenShellVersionPinVitestJob(errors: string[], jobs: WorkflowRe if (job["runs-on"] !== "ubuntu-latest") { errors.push("openshell-version-pin-vitest job must run on ubuntu-latest"); } - validateFreeStandingJobSelector(errors, jobs, jobName, "openshell-version-pin"); + validateFreeStandingJobSelector( + errors, + jobs, + jobName, + "openshell-version-pin", + ); const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("openshell-version-pin-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "openshell-version-pin-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if ( - jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/openshell-version-pin" + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/openshell-version-pin" ) { errors.push( "openshell-version-pin-vitest job must write artifacts under e2e-artifacts/vitest/openshell-version-pin", @@ -529,16 +600,30 @@ function validateOpenShellVersionPinVitestJob(errors: string[], jobs: WorkflowRe ); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("openshell-version-pin-vitest job missing checkout step"); - requireFullShaAction(errors, checkout, "openshell-version-pin-vitest checkout"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push("openshell-version-pin-vitest job missing checkout step"); + requireFullShaAction( + errors, + checkout, + "openshell-version-pin-vitest checkout", + ); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("openshell-version-pin-vitest checkout step must set persist-credentials=false"); + errors.push( + "openshell-version-pin-vitest checkout step must set persist-credentials=false", + ); } const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("openshell-version-pin-vitest job missing step: Set up Node"); - requireFullShaAction(errors, setupNode, "openshell-version-pin-vitest setup-node"); + if (!setupNode) + errors.push("openshell-version-pin-vitest job missing step: Set up Node"); + requireFullShaAction( + errors, + setupNode, + "openshell-version-pin-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -546,20 +631,52 @@ function validateOpenShellVersionPinVitestJob(errors: string[], jobs: WorkflowRe steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); - const runVitest = requireJobStep(errors, jobName, steps, "Run OpenShell version-pin live test"); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/openshell-version-pin.test.ts"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run OpenShell version-pin live test", + ); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/openshell-version-pin.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload OpenShell version-pin artifacts"); - requireFullShaAction(errors, upload, "openshell-version-pin-vitest upload-artifact"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload OpenShell version-pin artifacts", + ); + requireFullShaAction( + errors, + upload, + "openshell-version-pin-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-openshell-version-pin") { - errors.push("openshell-version-pin-vitest artifact upload name must be stable"); + errors.push( + "openshell-version-pin-vitest artifact upload name must be stable", + ); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/openshell-version-pin/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/openshell-version-pin/", + ); if (uploadWith["include-hidden-files"] !== false) { errors.push( "openshell-version-pin-vitest artifact upload must set include-hidden-files: false", @@ -571,11 +688,16 @@ function validateOpenShellVersionPinVitestJob(errors: string[], jobs: WorkflowRe ); } if (uploadWith["retention-days"] !== 14) { - errors.push("openshell-version-pin-vitest artifact upload retention-days must be 14"); + errors.push( + "openshell-version-pin-vitest artifact upload retention-days must be 14", + ); } } -function validateSkillAgentVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateSkillAgentVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "skill-agent-vitest"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { @@ -592,13 +714,18 @@ function validateSkillAgentVitestJob(errors: string[], jobs: WorkflowRecord): vo if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { errors.push("skill-agent-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); } - if (jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/skill-agent") { + if ( + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/skill-agent" + ) { errors.push( "skill-agent-vitest job must write artifacts under e2e-artifacts/vitest/skill-agent", ); } if (!stringValue(jobEnv.NEMOCLAW_CLI_BIN).includes("bin/nemoclaw.js")) { - errors.push("skill-agent-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "skill-agent-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } requireEnvDoesNotExposeSecret( errors, @@ -620,15 +747,20 @@ function validateSkillAgentVitestJob(errors: string[], jobs: WorkflowRecord): vo } } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) errors.push("skill-agent-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "skill-agent-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("skill-agent-vitest checkout step must set persist-credentials=false"); + errors.push( + "skill-agent-vitest checkout step must set persist-credentials=false", + ); } const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("skill-agent-vitest job missing step: Set up Node"); + if (!setupNode) + errors.push("skill-agent-vitest job missing step: Set up Node"); requireFullShaAction(errors, setupNode, "skill-agent-vitest setup-node"); const installRootDependencies = requireJobStep( @@ -637,30 +769,70 @@ function validateSkillAgentVitestJob(errors: string[], jobs: WorkflowRecord): vo steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const installOpenShell = requireJobStep(errors, jobName, steps, "Install OpenShell CLI"); - requireRunContains(errors, installOpenShell, "bash scripts/install-openshell.sh"); + const installOpenShell = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell CLI", + ); + requireRunContains( + errors, + installOpenShell, + "bash scripts/install-openshell.sh", + ); - const runVitest = requireJobStep(errors, jobName, steps, "Run skill-agent live test"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run skill-agent live test", + ); const runEnv = asRecord(runVitest?.env); - if (runEnv.NVIDIA_INFERENCE_API_KEY !== "${{ secrets.NVIDIA_INFERENCE_API_KEY }}") { - errors.push("skill-agent-vitest run step must receive NVIDIA_INFERENCE_API_KEY from secrets"); + if ( + runEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { + errors.push( + "skill-agent-vitest run step must receive NVIDIA_INFERENCE_API_KEY from secrets", + ); } requireRunContains( errors, runVitest, 'export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH"', ); - requireRunContains(errors, runVitest, 'OPENSHELL_BIN="$(command -v openshell)"'); + requireRunContains( + errors, + runVitest, + 'OPENSHELL_BIN="$(command -v openshell)"', + ); requireRunContains(errors, runVitest, "export OPENSHELL_BIN"); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/skill-agent.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/skill-agent.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload skill-agent artifacts"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload skill-agent artifacts", + ); requireFullShaAction(errors, upload, "skill-agent-vitest upload-artifact"); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-skill-agent") { @@ -687,17 +859,24 @@ function validateSkillAgentVitestJob(errors: string[], jobs: WorkflowRecord): vo } } if (uploadWith["include-hidden-files"] !== false) { - errors.push("skill-agent-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "skill-agent-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("skill-agent-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "skill-agent-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { errors.push("skill-agent-vitest artifact upload retention-days must be 14"); } } -function validateNetworkPolicyVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateNetworkPolicyVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "network-policy-vitest"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { @@ -718,18 +897,27 @@ function validateNetworkPolicyVitestJob(errors: string[], jobs: WorkflowRecord): const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("network-policy-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "network-policy-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } - if (jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/network-policy") { + if ( + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/network-policy" + ) { errors.push( "network-policy-vitest job must write artifacts under e2e-artifacts/vitest/network-policy", ); } if (!stringValue(jobEnv.NEMOCLAW_CLI_BIN).includes("bin/nemoclaw.js")) { - errors.push("network-policy-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "network-policy-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } if (jobEnv.OPENSHELL_GATEWAY !== "nemoclaw") { - errors.push("network-policy-vitest job must force OPENSHELL_GATEWAY=nemoclaw"); + errors.push( + "network-policy-vitest job must force OPENSHELL_GATEWAY=nemoclaw", + ); } for (const secret of [ "NVIDIA_INFERENCE_API_KEY", @@ -737,7 +925,12 @@ function validateNetworkPolicyVitestJob(errors: string[], jobs: WorkflowRecord): "DOCKERHUB_TOKEN", "GITHUB_TOKEN", ]) { - requireEnvDoesNotExposeSecret(errors, "network-policy-vitest job", jobEnv, secret); + requireEnvDoesNotExposeSecret( + errors, + "network-policy-vitest job", + jobEnv, + secret, + ); } const steps = asSteps(job.steps); @@ -775,15 +968,20 @@ function validateNetworkPolicyVitestJob(errors: string[], jobs: WorkflowRecord): ); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) errors.push("network-policy-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "network-policy-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("network-policy-vitest checkout step must set persist-credentials=false"); + errors.push( + "network-policy-vitest checkout step must set persist-credentials=false", + ); } const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("network-policy-vitest job missing step: Set up Node"); + if (!setupNode) + errors.push("network-policy-vitest job missing step: Set up Node"); requireFullShaAction(errors, setupNode, "network-policy-vitest setup-node"); const installRootDependencies = requireJobStep( @@ -792,53 +990,102 @@ function validateNetworkPolicyVitestJob(errors: string[], jobs: WorkflowRecord): steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); if (namedStep(steps, "Authenticate to Docker Hub")) { - errors.push("network-policy-vitest must not include unused Docker Hub authentication"); + errors.push( + "network-policy-vitest must not include unused Docker Hub authentication", + ); } - const installOpenShell = requireJobStep(errors, jobName, steps, "Install OpenShell"); - requireRunContains(errors, installOpenShell, "bash scripts/install-openshell.sh"); + const installOpenShell = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell", + ); + requireRunContains( + errors, + installOpenShell, + "bash scripts/install-openshell.sh", + ); requireRunContains(errors, installOpenShell, "env -u DOCKER_CONFIG"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_USERNAME"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_TOKEN"); requireRunContains(errors, installOpenShell, "-u NVIDIA_INFERENCE_API_KEY"); requireRunContains(errors, installOpenShell, "-u GITHUB_TOKEN"); - const runVitest = requireJobStep(errors, jobName, steps, "Run network-policy live test"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run network-policy live test", + ); const runVitestEnv = asRecord(runVitest?.env); - if (runVitestEnv.NVIDIA_INFERENCE_API_KEY !== "${{ secrets.NVIDIA_INFERENCE_API_KEY }}") { + if ( + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { errors.push( "network-policy-vitest Vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", ); } - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/network-policy.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/network-policy.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload network-policy artifacts"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload network-policy artifacts", + ); requireFullShaAction(errors, upload, "network-policy-vitest upload-artifact"); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-network-policy") { errors.push("network-policy-vitest artifact upload name must be stable"); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/network-policy/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/network-policy/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("network-policy-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "network-policy-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("network-policy-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "network-policy-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("network-policy-vitest artifact upload retention-days must be 14"); + errors.push( + "network-policy-vitest artifact upload retention-days must be 14", + ); } } -function validateCommonEgressAgentVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateCommonEgressAgentVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "common-egress-agent-vitest"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { @@ -851,34 +1098,49 @@ function validateCommonEgressAgentVitestJob(errors: string[], jobs: WorkflowReco } validateFreeStandingJobSelector(errors, jobs, jobName, "common-egress-agent"); if (job["timeout-minutes"] !== 120) { - errors.push("common-egress-agent-vitest job must keep the legacy 120 minute timeout"); + errors.push( + "common-egress-agent-vitest job must keep the legacy 120 minute timeout", + ); } const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("common-egress-agent-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "common-egress-agent-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if ( - jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/common-egress-agent" + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/common-egress-agent" ) { errors.push( "common-egress-agent-vitest job must write artifacts under e2e-artifacts/vitest/common-egress-agent", ); } if (!stringValue(jobEnv.NEMOCLAW_CLI_BIN).includes("bin/nemoclaw.js")) { - errors.push("common-egress-agent-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "common-egress-agent-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } if (jobEnv.NEMOCLAW_NON_INTERACTIVE !== "1") { - errors.push("common-egress-agent-vitest job must set NEMOCLAW_NON_INTERACTIVE=1"); + errors.push( + "common-egress-agent-vitest job must set NEMOCLAW_NON_INTERACTIVE=1", + ); } if (jobEnv.NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE !== "1") { - errors.push("common-egress-agent-vitest job must set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1"); + errors.push( + "common-egress-agent-vitest job must set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1", + ); } if (jobEnv.NEMOCLAW_RECREATE_SANDBOX !== "1") { - errors.push("common-egress-agent-vitest job must set NEMOCLAW_RECREATE_SANDBOX=1"); + errors.push( + "common-egress-agent-vitest job must set NEMOCLAW_RECREATE_SANDBOX=1", + ); } if (jobEnv.OPENSHELL_GATEWAY !== "nemoclaw") { - errors.push("common-egress-agent-vitest job must force OPENSHELL_GATEWAY=nemoclaw"); + errors.push( + "common-egress-agent-vitest job must force OPENSHELL_GATEWAY=nemoclaw", + ); } for (const secret of [ "NVIDIA_API_KEY", @@ -886,7 +1148,12 @@ function validateCommonEgressAgentVitestJob(errors: string[], jobs: WorkflowReco "DOCKERHUB_TOKEN", "GITHUB_TOKEN", ]) { - requireEnvDoesNotExposeSecret(errors, "common-egress-agent-vitest job", jobEnv, secret); + requireEnvDoesNotExposeSecret( + errors, + "common-egress-agent-vitest job", + jobEnv, + secret, + ); } const steps = asSteps(job.steps); @@ -902,7 +1169,11 @@ function validateCommonEgressAgentVitestJob(errors: string[], jobs: WorkflowReco "NVIDIA_API_KEY", ); } - for (const secret of ["DOCKERHUB_USERNAME", "DOCKERHUB_TOKEN", "GITHUB_TOKEN"]) { + for (const secret of [ + "DOCKERHUB_USERNAME", + "DOCKERHUB_TOKEN", + "GITHUB_TOKEN", + ]) { requireEnvDoesNotExposeSecret( errors, `common-egress-agent-vitest step '${stepName}'`, @@ -912,16 +1183,26 @@ function validateCommonEgressAgentVitestJob(errors: string[], jobs: WorkflowReco } } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("common-egress-agent-vitest job missing checkout step"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push("common-egress-agent-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "common-egress-agent-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("common-egress-agent-vitest checkout step must set persist-credentials=false"); + errors.push( + "common-egress-agent-vitest checkout step must set persist-credentials=false", + ); } const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("common-egress-agent-vitest job missing step: Set up Node"); - requireFullShaAction(errors, setupNode, "common-egress-agent-vitest setup-node"); + if (!setupNode) + errors.push("common-egress-agent-vitest job missing step: Set up Node"); + requireFullShaAction( + errors, + setupNode, + "common-egress-agent-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -929,48 +1210,100 @@ function validateCommonEgressAgentVitestJob(errors: string[], jobs: WorkflowReco steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const installOpenShell = requireJobStep(errors, jobName, steps, "Install OpenShell"); - requireRunContains(errors, installOpenShell, "bash scripts/install-openshell.sh"); + const installOpenShell = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell", + ); + requireRunContains( + errors, + installOpenShell, + "bash scripts/install-openshell.sh", + ); requireRunContains(errors, installOpenShell, "env -u DOCKER_CONFIG"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_USERNAME"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_TOKEN"); requireRunContains(errors, installOpenShell, "-u NVIDIA_API_KEY"); requireRunContains(errors, installOpenShell, "-u GITHUB_TOKEN"); - const runVitest = requireJobStep(errors, jobName, steps, "Run common-egress agent live test"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run common-egress agent live test", + ); const runVitestEnv = asRecord(runVitest?.env); if (runVitestEnv.NVIDIA_API_KEY !== "${{ secrets.NVIDIA_API_KEY }}") { - errors.push("common-egress-agent-vitest step must receive NVIDIA_API_KEY from secrets"); + errors.push( + "common-egress-agent-vitest step must receive NVIDIA_API_KEY from secrets", + ); } requireRunContains(errors, runVitest, "OPENSHELL_BIN"); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/common-egress-agent.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/common-egress-agent.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload common-egress agent artifacts"); - requireFullShaAction(errors, upload, "common-egress-agent-vitest upload-artifact"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload common-egress agent artifacts", + ); + requireFullShaAction( + errors, + upload, + "common-egress-agent-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-common-egress-agent") { - errors.push("common-egress-agent-vitest artifact upload name must be stable"); + errors.push( + "common-egress-agent-vitest artifact upload name must be stable", + ); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/common-egress-agent/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/common-egress-agent/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("common-egress-agent-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "common-egress-agent-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("common-egress-agent-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "common-egress-agent-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("common-egress-agent-vitest artifact upload retention-days must be 14"); + errors.push( + "common-egress-agent-vitest artifact upload retention-days must be 14", + ); } } -function validateShieldsConfigVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateShieldsConfigVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "shields-config-vitest"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { @@ -983,28 +1316,43 @@ function validateShieldsConfigVitestJob(errors: string[], jobs: WorkflowRecord): } validateFreeStandingJobSelector(errors, jobs, jobName, "shields-config"); if (job["timeout-minutes"] !== 45) { - errors.push("shields-config-vitest job must keep the legacy 45 minute timeout"); + errors.push( + "shields-config-vitest job must keep the legacy 45 minute timeout", + ); } const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("shields-config-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "shields-config-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } - if (jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/shields-config") { + if ( + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/shields-config" + ) { errors.push( "shields-config-vitest job must write artifacts under e2e-artifacts/vitest/shields-config", ); } if (jobEnv.OPENSHELL_GATEWAY !== "nemoclaw") { - errors.push("shields-config-vitest job must force OPENSHELL_GATEWAY=nemoclaw"); + errors.push( + "shields-config-vitest job must force OPENSHELL_GATEWAY=nemoclaw", + ); } if (jobEnv.NEMOCLAW_NON_INTERACTIVE !== "1") { - errors.push("shields-config-vitest job must set NEMOCLAW_NON_INTERACTIVE=1"); + errors.push( + "shields-config-vitest job must set NEMOCLAW_NON_INTERACTIVE=1", + ); } if (jobEnv.NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE !== "1") { - errors.push("shields-config-vitest job must set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1"); + errors.push( + "shields-config-vitest job must set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1", + ); } if (jobEnv.NEMOCLAW_SANDBOX_NAME !== "e2e-shields") { - errors.push("shields-config-vitest job must set NEMOCLAW_SANDBOX_NAME=e2e-shields"); + errors.push( + "shields-config-vitest job must set NEMOCLAW_SANDBOX_NAME=e2e-shields", + ); } requireEnvDoesNotExposeSecret( errors, @@ -1012,8 +1360,18 @@ function validateShieldsConfigVitestJob(errors: string[], jobs: WorkflowRecord): jobEnv, "NVIDIA_INFERENCE_API_KEY", ); - requireEnvDoesNotExposeSecret(errors, "shields-config-vitest job", jobEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, "shields-config-vitest job", jobEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + "shields-config-vitest job", + jobEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + "shields-config-vitest job", + jobEnv, + "DOCKERHUB_TOKEN", + ); const steps = asSteps(job.steps); requireNoDispatchInputInterpolation(errors, steps); @@ -1044,14 +1402,23 @@ function validateShieldsConfigVitestJob(errors: string[], jobs: WorkflowRecord): } } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) errors.push("shields-config-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "shields-config-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("shields-config-vitest checkout step must set persist-credentials=false"); + errors.push( + "shields-config-vitest checkout step must set persist-credentials=false", + ); } - const dockerHubAuth = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerHubAuth = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerHubEnv = asRecord(dockerHubAuth?.env); if (dockerHubEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { errors.push( @@ -1059,12 +1426,15 @@ function validateShieldsConfigVitestJob(errors: string[], jobs: WorkflowRecord): ); } if (dockerHubEnv.DOCKERHUB_TOKEN !== "${{ secrets.DOCKERHUB_TOKEN }}") { - errors.push("shields-config-vitest Docker Hub auth must receive DOCKERHUB_TOKEN from secrets"); + errors.push( + "shields-config-vitest Docker Hub auth must receive DOCKERHUB_TOKEN from secrets", + ); } requireRunContains(errors, dockerHubAuth, "docker login docker.io"); const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("shields-config-vitest job missing step: Set up Node"); + if (!setupNode) + errors.push("shields-config-vitest job missing step: Set up Node"); requireFullShaAction(errors, setupNode, "shields-config-vitest setup-node"); const installRootDependencies = requireJobStep( @@ -1073,37 +1443,81 @@ function validateShieldsConfigVitestJob(errors: string[], jobs: WorkflowRecord): steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); - const runVitest = requireJobStep(errors, jobName, steps, "Run shields-config live test"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run shields-config live test", + ); const runVitestEnv = asRecord(runVitest?.env); - if (runVitestEnv.NVIDIA_INFERENCE_API_KEY !== "${{ secrets.NVIDIA_INFERENCE_API_KEY }}") { - errors.push("shields-config-vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets"); + if ( + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { + errors.push( + "shields-config-vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", + ); } - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/shields-config.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/shields-config.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload shields-config artifacts"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload shields-config artifacts", + ); requireFullShaAction(errors, upload, "shields-config-vitest upload-artifact"); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-shields-config") { errors.push("shields-config-vitest artifact upload name must be stable"); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/shields-config/"); - requireUploadPathContains(errors, uploadPath, "/tmp/nemoclaw-e2e-shields-install.log"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/shields-config/", + ); + requireUploadPathContains( + errors, + uploadPath, + "/tmp/nemoclaw-e2e-shields-install.log", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("shields-config-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "shields-config-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("shields-config-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "shields-config-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("shields-config-vitest artifact upload retention-days must be 14"); + errors.push( + "shields-config-vitest artifact upload retention-days must be 14", + ); } } -function validateRebuildOpenClawVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateRebuildOpenClawVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "rebuild-openclaw-vitest"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { @@ -1116,19 +1530,28 @@ function validateRebuildOpenClawVitestJob(errors: string[], jobs: WorkflowRecord } validateFreeStandingJobSelector(errors, jobs, jobName, "rebuild-openclaw"); if (job["timeout-minutes"] !== 130) { - errors.push("rebuild-openclaw-vitest job must keep the legacy 130 minute timeout"); + errors.push( + "rebuild-openclaw-vitest job must keep the legacy 130 minute timeout", + ); } const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("rebuild-openclaw-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "rebuild-openclaw-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } - if (jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/rebuild-openclaw") { + if ( + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/rebuild-openclaw" + ) { errors.push( "rebuild-openclaw-vitest job must write artifacts under e2e-artifacts/vitest/rebuild-openclaw", ); } if (!stringValue(jobEnv.NEMOCLAW_CLI_BIN).includes("bin/nemoclaw.js")) { - errors.push("rebuild-openclaw-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "rebuild-openclaw-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } requireEnvDoesNotExposeSecret( errors, @@ -1150,14 +1573,24 @@ function validateRebuildOpenClawVitestJob(errors: string[], jobs: WorkflowRecord } } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("rebuild-openclaw-vitest job missing checkout step"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push("rebuild-openclaw-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "rebuild-openclaw-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("rebuild-openclaw-vitest checkout step must set persist-credentials=false"); + errors.push( + "rebuild-openclaw-vitest checkout step must set persist-credentials=false", + ); } - const dockerHubAuth = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerHubAuth = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerHubEnv = asRecord(dockerHubAuth?.env); if (dockerHubEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { errors.push( @@ -1172,7 +1605,8 @@ function validateRebuildOpenClawVitestJob(errors: string[], jobs: WorkflowRecord requireRunContains(errors, dockerHubAuth, "docker login docker.io"); const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("rebuild-openclaw-vitest job missing step: Set up Node"); + if (!setupNode) + errors.push("rebuild-openclaw-vitest job missing step: Set up Node"); requireFullShaAction(errors, setupNode, "rebuild-openclaw-vitest setup-node"); const installRootDependencies = requireJobStep( @@ -1181,50 +1615,100 @@ function validateRebuildOpenClawVitestJob(errors: string[], jobs: WorkflowRecord steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const installOpenShell = requireJobStep(errors, jobName, steps, "Install OpenShell"); + const installOpenShell = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell", + ); requireEnvDoesNotExposeSecret( errors, "rebuild-openclaw-vitest step 'Install OpenShell'", asRecord(installOpenShell?.env), "GITHUB_TOKEN", ); - requireRunContains(errors, installOpenShell, "bash scripts/install-openshell.sh"); + requireRunContains( + errors, + installOpenShell, + "bash scripts/install-openshell.sh", + ); requireRunContains(errors, installOpenShell, "env -u DOCKER_CONFIG"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_USERNAME"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_TOKEN"); requireRunContains(errors, installOpenShell, "-u NVIDIA_INFERENCE_API_KEY"); requireRunContains(errors, installOpenShell, "-u GITHUB_TOKEN"); - const runVitest = requireJobStep(errors, jobName, steps, "Run OpenClaw rebuild live test"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run OpenClaw rebuild live test", + ); const runVitestEnv = asRecord(runVitest?.env); - if (runVitestEnv.NVIDIA_INFERENCE_API_KEY !== "${{ secrets.NVIDIA_INFERENCE_API_KEY }}") { - errors.push("rebuild-openclaw-vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets"); + if ( + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { + errors.push( + "rebuild-openclaw-vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", + ); } requireRunContains(errors, runVitest, "OPENSHELL_BIN"); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/rebuild-openclaw.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/rebuild-openclaw.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload OpenClaw rebuild artifacts"); - requireFullShaAction(errors, upload, "rebuild-openclaw-vitest upload-artifact"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload OpenClaw rebuild artifacts", + ); + requireFullShaAction( + errors, + upload, + "rebuild-openclaw-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-rebuild-openclaw") { errors.push("rebuild-openclaw-vitest artifact upload name must be stable"); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/rebuild-openclaw/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/rebuild-openclaw/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("rebuild-openclaw-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "rebuild-openclaw-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("rebuild-openclaw-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "rebuild-openclaw-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("rebuild-openclaw-vitest artifact upload retention-days must be 14"); + errors.push( + "rebuild-openclaw-vitest artifact upload retention-days must be 14", + ); } } @@ -1233,8 +1717,12 @@ function validateRebuildHermesVitestJob( jobs: WorkflowRecord, options: { staleBase: boolean }, ): void { - const jobName = options.staleBase ? "rebuild-hermes-stale-base-vitest" : "rebuild-hermes-vitest"; - const scenarioName = options.staleBase ? "rebuild-hermes-stale-base" : "rebuild-hermes"; + const jobName = options.staleBase + ? "rebuild-hermes-stale-base-vitest" + : "rebuild-hermes-vitest"; + const scenarioName = options.staleBase + ? "rebuild-hermes-stale-base" + : "rebuild-hermes"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { errors.push(`workflow missing ${jobName} job`); @@ -1262,7 +1750,9 @@ function validateRebuildHermesVitestJob( errors.push(`${jobName} job must set NEMOCLAW_AGENT=hermes`); } if (jobEnv.NEMOCLAW_PROVIDER !== "custom") { - errors.push(`${jobName} job must use the hosted compatible endpoint provider`); + errors.push( + `${jobName} job must use the hosted compatible endpoint provider`, + ); } if (jobEnv.NEMOCLAW_ENDPOINT_URL !== "https://inference-api.nvidia.com/v1") { errors.push(`${jobName} job must target hosted CI inference endpoint`); @@ -1278,13 +1768,19 @@ function validateRebuildHermesVitestJob( } if (options.staleBase) { if (jobEnv.NEMOCLAW_HERMES_STALE_BASE_REBUILD_E2E !== "1") { - errors.push(`${jobName} job must enable NEMOCLAW_HERMES_STALE_BASE_REBUILD_E2E=1`); + errors.push( + `${jobName} job must enable NEMOCLAW_HERMES_STALE_BASE_REBUILD_E2E=1`, + ); } if (jobEnv.NEMOCLAW_SANDBOX_NAME !== "e2e-rebuild-hermes-base") { - errors.push(`${jobName} job must set NEMOCLAW_SANDBOX_NAME=e2e-rebuild-hermes-base`); + errors.push( + `${jobName} job must set NEMOCLAW_SANDBOX_NAME=e2e-rebuild-hermes-base`, + ); } } else if (jobEnv.NEMOCLAW_SANDBOX_NAME !== "e2e-rebuild-hermes") { - errors.push(`${jobName} job must set NEMOCLAW_SANDBOX_NAME=e2e-rebuild-hermes`); + errors.push( + `${jobName} job must set NEMOCLAW_SANDBOX_NAME=e2e-rebuild-hermes`, + ); } for (const secret of [ "NVIDIA_INFERENCE_API_KEY", @@ -1301,30 +1797,56 @@ function validateRebuildHermesVitestJob( const stepName = `${jobName} step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); if (!step.name?.startsWith("Run Hermes")) { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_INFERENCE_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_INFERENCE_API_KEY", + ); } if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) errors.push(`${jobName} job missing checkout step`); requireFullShaAction(errors, checkout, `${jobName} checkout`); if (asRecord(checkout?.with)["persist-credentials"] !== false) { errors.push(`${jobName} checkout step must set persist-credentials=false`); } - const dockerHubAuth = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerHubAuth = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerHubEnv = asRecord(dockerHubAuth?.env); if (dockerHubEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { - errors.push(`${jobName} Docker Hub auth must receive DOCKERHUB_USERNAME from secrets`); + errors.push( + `${jobName} Docker Hub auth must receive DOCKERHUB_USERNAME from secrets`, + ); } if (dockerHubEnv.DOCKERHUB_TOKEN !== "${{ secrets.DOCKERHUB_TOKEN }}") { - errors.push(`${jobName} Docker Hub auth must receive DOCKERHUB_TOKEN from secrets`); + errors.push( + `${jobName} Docker Hub auth must receive DOCKERHUB_TOKEN from secrets`, + ); } requireRunContains(errors, dockerHubAuth, "docker login docker.io"); requireRunContains(errors, dockerHubAuth, "continuing with anonymous pulls"); @@ -1339,20 +1861,39 @@ function validateRebuildHermesVitestJob( steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const runVitest = requireJobStep( errors, jobName, steps, - options.staleBase ? "Run Hermes stale-base rebuild live test" : "Run Hermes rebuild live test", + options.staleBase + ? "Run Hermes stale-base rebuild live test" + : "Run Hermes rebuild live test", ); const runVitestEnv = asRecord(runVitest?.env); - if (runVitestEnv.NVIDIA_INFERENCE_API_KEY !== "${{ secrets.NVIDIA_INFERENCE_API_KEY }}") { - errors.push(`${jobName} step must receive NVIDIA_INFERENCE_API_KEY from secrets`); + if ( + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { + errors.push( + `${jobName} step must receive NVIDIA_INFERENCE_API_KEY from secrets`, + ); } - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/rebuild-hermes.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/rebuild-hermes.test.ts", + ); const upload = requireJobStep( errors, @@ -1379,17 +1920,24 @@ function validateRebuildHermesVitestJob( : "e2e-artifacts/vitest/rebuild-hermes/", ); if (uploadWith["include-hidden-files"] !== false) { - errors.push(`${jobName} artifact upload must set include-hidden-files: false`); + errors.push( + `${jobName} artifact upload must set include-hidden-files: false`, + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push(`${jobName} artifact upload must ignore missing fixture artifacts`); + errors.push( + `${jobName} artifact upload must ignore missing fixture artifacts`, + ); } if (uploadWith["retention-days"] !== 14) { errors.push(`${jobName} artifact upload retention-days must be 14`); } } -function validateSandboxRebuildVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateSandboxRebuildVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "sandbox-rebuild-vitest"; const scenarioName = "sandbox-rebuild"; const job = asRecord(jobs[jobName]); @@ -1403,22 +1951,33 @@ function validateSandboxRebuildVitestJob(errors: string[], jobs: WorkflowRecord) } validateFreeStandingJobSelector(errors, jobs, jobName, scenarioName); if (job["timeout-minutes"] !== 90) { - errors.push("sandbox-rebuild-vitest job must keep the legacy 90 minute timeout"); + errors.push( + "sandbox-rebuild-vitest job must keep the legacy 90 minute timeout", + ); } const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("sandbox-rebuild-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "sandbox-rebuild-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } - if (jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/sandbox-rebuild") { + if ( + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/sandbox-rebuild" + ) { errors.push( "sandbox-rebuild-vitest job must write artifacts under e2e-artifacts/vitest/sandbox-rebuild", ); } if (jobEnv.NEMOCLAW_CLI_BIN !== "${{ github.workspace }}/bin/nemoclaw.js") { - errors.push("sandbox-rebuild-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "sandbox-rebuild-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } if (jobEnv.OPENSHELL_GATEWAY !== "nemoclaw") { - errors.push("sandbox-rebuild-vitest job must force OPENSHELL_GATEWAY=nemoclaw"); + errors.push( + "sandbox-rebuild-vitest job must force OPENSHELL_GATEWAY=nemoclaw", + ); } for (const secret of [ "NVIDIA_INFERENCE_API_KEY", @@ -1426,7 +1985,12 @@ function validateSandboxRebuildVitestJob(errors: string[], jobs: WorkflowRecord) "DOCKERHUB_TOKEN", "GITHUB_TOKEN", ]) { - requireEnvDoesNotExposeSecret(errors, "sandbox-rebuild-vitest job", jobEnv, secret); + requireEnvDoesNotExposeSecret( + errors, + "sandbox-rebuild-vitest job", + jobEnv, + secret, + ); } const steps = asSteps(job.steps); @@ -1435,24 +1999,49 @@ function validateSandboxRebuildVitestJob(errors: string[], jobs: WorkflowRecord) const stepName = `sandbox-rebuild-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); if (step.name !== "Run sandbox rebuild live test") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_INFERENCE_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_INFERENCE_API_KEY", + ); } if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("sandbox-rebuild-vitest job missing checkout step"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push("sandbox-rebuild-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "sandbox-rebuild-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("sandbox-rebuild-vitest checkout step must set persist-credentials=false"); + errors.push( + "sandbox-rebuild-vitest checkout step must set persist-credentials=false", + ); } - const dockerHubAuth = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerHubAuth = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerHubEnv = asRecord(dockerHubAuth?.env); if (dockerHubEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { errors.push( @@ -1460,13 +2049,16 @@ function validateSandboxRebuildVitestJob(errors: string[], jobs: WorkflowRecord) ); } if (dockerHubEnv.DOCKERHUB_TOKEN !== "${{ secrets.DOCKERHUB_TOKEN }}") { - errors.push("sandbox-rebuild-vitest Docker Hub auth must receive DOCKERHUB_TOKEN from secrets"); + errors.push( + "sandbox-rebuild-vitest Docker Hub auth must receive DOCKERHUB_TOKEN from secrets", + ); } requireRunContains(errors, dockerHubAuth, "docker login docker.io"); requireRunContains(errors, dockerHubAuth, "continuing with anonymous pulls"); const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("sandbox-rebuild-vitest job missing step: Set up Node"); + if (!setupNode) + errors.push("sandbox-rebuild-vitest job missing step: Set up Node"); requireFullShaAction(errors, setupNode, "sandbox-rebuild-vitest setup-node"); const installRootDependencies = requireJobStep( @@ -1475,48 +2067,101 @@ function validateSandboxRebuildVitestJob(errors: string[], jobs: WorkflowRecord) steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const installOpenShell = requireJobStep(errors, jobName, steps, "Install OpenShell"); - requireRunContains(errors, installOpenShell, "bash scripts/install-openshell.sh"); + const installOpenShell = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell", + ); + requireRunContains( + errors, + installOpenShell, + "bash scripts/install-openshell.sh", + ); requireRunContains(errors, installOpenShell, "env -u DOCKER_CONFIG"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_USERNAME"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_TOKEN"); requireRunContains(errors, installOpenShell, "-u NVIDIA_INFERENCE_API_KEY"); requireRunContains(errors, installOpenShell, "-u GITHUB_TOKEN"); - const runVitest = requireJobStep(errors, jobName, steps, "Run sandbox rebuild live test"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run sandbox rebuild live test", + ); const runVitestEnv = asRecord(runVitest?.env); - if (runVitestEnv.NVIDIA_INFERENCE_API_KEY !== "${{ secrets.NVIDIA_INFERENCE_API_KEY }}") { - errors.push("sandbox-rebuild-vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets"); + if ( + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { + errors.push( + "sandbox-rebuild-vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", + ); } requireRunContains(errors, runVitest, "OPENSHELL_BIN"); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/sandbox-rebuild.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/sandbox-rebuild.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload sandbox rebuild artifacts"); - requireFullShaAction(errors, upload, "sandbox-rebuild-vitest upload-artifact"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload sandbox rebuild artifacts", + ); + requireFullShaAction( + errors, + upload, + "sandbox-rebuild-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-sandbox-rebuild") { errors.push("sandbox-rebuild-vitest artifact upload name must be stable"); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/sandbox-rebuild/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/sandbox-rebuild/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("sandbox-rebuild-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "sandbox-rebuild-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("sandbox-rebuild-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "sandbox-rebuild-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("sandbox-rebuild-vitest artifact upload retention-days must be 14"); + errors.push( + "sandbox-rebuild-vitest artifact upload retention-days must be 14", + ); } } -function validateStateBackupRestoreVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateStateBackupRestoreVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "state-backup-restore-vitest"; const scenarioName = "state-backup-restore"; const job = asRecord(jobs[jobName]); @@ -1530,33 +2175,48 @@ function validateStateBackupRestoreVitestJob(errors: string[], jobs: WorkflowRec } validateFreeStandingJobSelector(errors, jobs, jobName, scenarioName); if (job["timeout-minutes"] !== 60) { - errors.push("state-backup-restore-vitest job must keep the legacy 60 minute timeout"); + errors.push( + "state-backup-restore-vitest job must keep the legacy 60 minute timeout", + ); } const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("state-backup-restore-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "state-backup-restore-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if ( - jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/state-backup-restore" + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/state-backup-restore" ) { errors.push( "state-backup-restore-vitest job must write artifacts under e2e-artifacts/vitest/state-backup-restore", ); } if (jobEnv.NEMOCLAW_CLI_BIN !== "${{ github.workspace }}/bin/nemoclaw.js") { - errors.push("state-backup-restore-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "state-backup-restore-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } if (jobEnv.OPENSHELL_GATEWAY !== "nemoclaw") { - errors.push("state-backup-restore-vitest job must force OPENSHELL_GATEWAY=nemoclaw"); + errors.push( + "state-backup-restore-vitest job must force OPENSHELL_GATEWAY=nemoclaw", + ); } if (jobEnv.NEMOCLAW_NON_INTERACTIVE !== "1") { - errors.push("state-backup-restore-vitest job must set NEMOCLAW_NON_INTERACTIVE=1"); + errors.push( + "state-backup-restore-vitest job must set NEMOCLAW_NON_INTERACTIVE=1", + ); } if (jobEnv.NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE !== "1") { - errors.push("state-backup-restore-vitest job must set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1"); + errors.push( + "state-backup-restore-vitest job must set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1", + ); } if (jobEnv.NEMOCLAW_SANDBOX_NAME !== "e2e-state-backup") { - errors.push("state-backup-restore-vitest job must set NEMOCLAW_SANDBOX_NAME=e2e-state-backup"); + errors.push( + "state-backup-restore-vitest job must set NEMOCLAW_SANDBOX_NAME=e2e-state-backup", + ); } for (const secret of [ "NVIDIA_API_KEY", @@ -1564,7 +2224,12 @@ function validateStateBackupRestoreVitestJob(errors: string[], jobs: WorkflowRec "DOCKERHUB_TOKEN", "GITHUB_TOKEN", ]) { - requireEnvDoesNotExposeSecret(errors, "state-backup-restore-vitest job", jobEnv, secret); + requireEnvDoesNotExposeSecret( + errors, + "state-backup-restore-vitest job", + jobEnv, + secret, + ); } const steps = asSteps(job.steps); @@ -1573,24 +2238,53 @@ function validateStateBackupRestoreVitestJob(errors: string[], jobs: WorkflowRec const stepName = `state-backup-restore-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); if (step.name !== "Run state backup restore live test") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_API_KEY", + ); } if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("state-backup-restore-vitest job missing checkout step"); - requireFullShaAction(errors, checkout, "state-backup-restore-vitest checkout"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push("state-backup-restore-vitest job missing checkout step"); + requireFullShaAction( + errors, + checkout, + "state-backup-restore-vitest checkout", + ); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("state-backup-restore-vitest checkout step must set persist-credentials=false"); + errors.push( + "state-backup-restore-vitest checkout step must set persist-credentials=false", + ); } - const dockerHubAuth = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerHubAuth = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerHubEnv = asRecord(dockerHubAuth?.env); if (dockerHubEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { errors.push( @@ -1606,8 +2300,13 @@ function validateStateBackupRestoreVitestJob(errors: string[], jobs: WorkflowRec requireRunContains(errors, dockerHubAuth, "continuing with anonymous pulls"); const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("state-backup-restore-vitest job missing step: Set up Node"); - requireFullShaAction(errors, setupNode, "state-backup-restore-vitest setup-node"); + if (!setupNode) + errors.push("state-backup-restore-vitest job missing step: Set up Node"); + requireFullShaAction( + errors, + setupNode, + "state-backup-restore-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -1615,38 +2314,83 @@ function validateStateBackupRestoreVitestJob(errors: string[], jobs: WorkflowRec steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const installOpenShell = requireJobStep(errors, jobName, steps, "Install OpenShell"); - requireRunContains(errors, installOpenShell, "bash scripts/install-openshell.sh"); + const installOpenShell = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell", + ); + requireRunContains( + errors, + installOpenShell, + "bash scripts/install-openshell.sh", + ); requireRunContains(errors, installOpenShell, "env -u DOCKER_CONFIG"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_USERNAME"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_TOKEN"); requireRunContains(errors, installOpenShell, "-u NVIDIA_API_KEY"); requireRunContains(errors, installOpenShell, "-u GITHUB_TOKEN"); - const runVitest = requireJobStep(errors, jobName, steps, "Run state backup restore live test"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run state backup restore live test", + ); const runVitestEnv = asRecord(runVitest?.env); if (runVitestEnv.NVIDIA_API_KEY !== "${{ secrets.NVIDIA_API_KEY }}") { - errors.push("state-backup-restore-vitest step must receive NVIDIA_API_KEY from secrets"); + errors.push( + "state-backup-restore-vitest step must receive NVIDIA_API_KEY from secrets", + ); } requireRunContains(errors, runVitest, "OPENSHELL_BIN"); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/state-backup-restore.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/state-backup-restore.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload state backup restore artifacts"); - requireFullShaAction(errors, upload, "state-backup-restore-vitest upload-artifact"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload state backup restore artifacts", + ); + requireFullShaAction( + errors, + upload, + "state-backup-restore-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-state-backup-restore") { - errors.push("state-backup-restore-vitest artifact upload name must be stable"); + errors.push( + "state-backup-restore-vitest artifact upload name must be stable", + ); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/state-backup-restore/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/state-backup-restore/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("state-backup-restore-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "state-backup-restore-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { errors.push( @@ -1654,11 +2398,16 @@ function validateStateBackupRestoreVitestJob(errors: string[], jobs: WorkflowRec ); } if (uploadWith["retention-days"] !== 14) { - errors.push("state-backup-restore-vitest artifact upload retention-days must be 14"); + errors.push( + "state-backup-restore-vitest artifact upload retention-days must be 14", + ); } } -function validateUpgradeStaleSandboxVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateUpgradeStaleSandboxVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "upgrade-stale-sandbox-vitest"; const scenarioName = "upgrade-stale-sandbox"; const job = asRecord(jobs[jobName]); @@ -1672,25 +2421,34 @@ function validateUpgradeStaleSandboxVitestJob(errors: string[], jobs: WorkflowRe } validateFreeStandingJobSelector(errors, jobs, jobName, scenarioName); if (job["timeout-minutes"] !== 55) { - errors.push("upgrade-stale-sandbox-vitest job must keep the legacy 55 minute timeout"); + errors.push( + "upgrade-stale-sandbox-vitest job must keep the legacy 55 minute timeout", + ); } const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("upgrade-stale-sandbox-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "upgrade-stale-sandbox-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if ( - jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/upgrade-stale-sandbox" + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/upgrade-stale-sandbox" ) { errors.push( "upgrade-stale-sandbox-vitest job must write artifacts under e2e-artifacts/vitest/upgrade-stale-sandbox", ); } if (jobEnv.NEMOCLAW_CLI_BIN !== "${{ github.workspace }}/bin/nemoclaw.js") { - errors.push("upgrade-stale-sandbox-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "upgrade-stale-sandbox-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } if (jobEnv.OPENSHELL_GATEWAY !== "nemoclaw") { - errors.push("upgrade-stale-sandbox-vitest job must force OPENSHELL_GATEWAY=nemoclaw"); + errors.push( + "upgrade-stale-sandbox-vitest job must force OPENSHELL_GATEWAY=nemoclaw", + ); } if (jobEnv.NEMOCLAW_SANDBOX_NAME !== "e2e-upgrade-stale") { errors.push( @@ -1698,10 +2456,20 @@ function validateUpgradeStaleSandboxVitestJob(errors: string[], jobs: WorkflowRe ); } if ("DOCKER_CONFIG" in jobEnv) { - errors.push("upgrade-stale-sandbox-vitest job must not set DOCKER_CONFIG at job level"); + errors.push( + "upgrade-stale-sandbox-vitest job must not set DOCKER_CONFIG at job level", + ); } - for (const secret of ["NVIDIA_INFERENCE_API_KEY", ...COMMON_SECRET_ENV_NAMES]) { - requireEnvDoesNotExposeSecret(errors, "upgrade-stale-sandbox-vitest job", jobEnv, secret); + for (const secret of [ + "NVIDIA_INFERENCE_API_KEY", + ...COMMON_SECRET_ENV_NAMES, + ]) { + requireEnvDoesNotExposeSecret( + errors, + "upgrade-stale-sandbox-vitest job", + jobEnv, + secret, + ); } const steps = asSteps(job.steps); @@ -1710,22 +2478,51 @@ function validateUpgradeStaleSandboxVitestJob(errors: string[], jobs: WorkflowRe const stepName = `upgrade-stale-sandbox-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); if (step.name !== "Run upgrade stale sandbox live Vitest test") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_INFERENCE_API_KEY"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_INFERENCE_API_KEY", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_API_KEY", + ); } if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("upgrade-stale-sandbox-vitest job missing checkout step"); - requireFullShaAction(errors, checkout, "upgrade-stale-sandbox-vitest checkout"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push("upgrade-stale-sandbox-vitest job missing checkout step"); + requireFullShaAction( + errors, + checkout, + "upgrade-stale-sandbox-vitest checkout", + ); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("upgrade-stale-sandbox-vitest checkout step must set persist-credentials=false"); + errors.push( + "upgrade-stale-sandbox-vitest checkout step must set persist-credentials=false", + ); } const configureDockerAuth = requireJobStep( @@ -1740,7 +2537,12 @@ function validateUpgradeStaleSandboxVitestJob(errors: string[], jobs: WorkflowRe 'echo "DOCKER_CONFIG=${RUNNER_TEMP}/docker-config-upgrade-stale-sandbox" >> "$GITHUB_ENV"', ); - const dockerHubAuth = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerHubAuth = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerHubEnv = asRecord(dockerHubAuth?.env); if (dockerHubEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { errors.push( @@ -1758,8 +2560,13 @@ function validateUpgradeStaleSandboxVitestJob(errors: string[], jobs: WorkflowRe requireRunContains(errors, dockerHubAuth, "continuing with anonymous pulls"); const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("upgrade-stale-sandbox-vitest job missing step: Set up Node"); - requireFullShaAction(errors, setupNode, "upgrade-stale-sandbox-vitest setup-node"); + if (!setupNode) + errors.push("upgrade-stale-sandbox-vitest job missing step: Set up Node"); + requireFullShaAction( + errors, + setupNode, + "upgrade-stale-sandbox-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -1767,13 +2574,26 @@ function validateUpgradeStaleSandboxVitestJob(errors: string[], jobs: WorkflowRe steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const installOpenShell = requireJobStep(errors, jobName, steps, "Install OpenShell CLI"); - requireRunContains(errors, installOpenShell, "bash scripts/install-openshell.sh"); + const installOpenShell = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell CLI", + ); + requireRunContains( + errors, + installOpenShell, + "bash scripts/install-openshell.sh", + ); requireRunContains(errors, installOpenShell, "env -u DOCKER_CONFIG"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_USERNAME"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_TOKEN"); @@ -1788,23 +2608,49 @@ function validateUpgradeStaleSandboxVitestJob(errors: string[], jobs: WorkflowRe "Run upgrade stale sandbox live Vitest test", ); const runVitestEnv = asRecord(runVitest?.env); - if (runVitestEnv.NVIDIA_INFERENCE_API_KEY !== "${{ secrets.NVIDIA_INFERENCE_API_KEY }}") { + if ( + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { errors.push( "upgrade-stale-sandbox-vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", ); } requireRunContains(errors, runVitest, "OPENSHELL_BIN"); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/upgrade-stale-sandbox.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/upgrade-stale-sandbox.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload upgrade stale sandbox artifacts"); - requireFullShaAction(errors, upload, "upgrade-stale-sandbox-vitest upload-artifact"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload upgrade stale sandbox artifacts", + ); + requireFullShaAction( + errors, + upload, + "upgrade-stale-sandbox-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-upgrade-stale-sandbox") { - errors.push("upgrade-stale-sandbox-vitest artifact upload name must be stable"); + errors.push( + "upgrade-stale-sandbox-vitest artifact upload name must be stable", + ); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/upgrade-stale-sandbox/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/upgrade-stale-sandbox/", + ); if (uploadWith["include-hidden-files"] !== false) { errors.push( "upgrade-stale-sandbox-vitest artifact upload must set include-hidden-files: false", @@ -1816,15 +2662,25 @@ function validateUpgradeStaleSandboxVitestJob(errors: string[], jobs: WorkflowRe ); } if (uploadWith["retention-days"] !== 14) { - errors.push("upgrade-stale-sandbox-vitest artifact upload retention-days must be 14"); + errors.push( + "upgrade-stale-sandbox-vitest artifact upload retention-days must be 14", + ); } - const cleanup = requireJobStep(errors, jobName, steps, "Clean up Docker auth"); + const cleanup = requireJobStep( + errors, + jobName, + steps, + "Clean up Docker auth", + ); requireRunContains(errors, cleanup, "docker logout docker.io"); requireRunContains(errors, cleanup, 'rm -rf "${DOCKER_CONFIG}"'); } -function validateTokenRotationVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateTokenRotationVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "token-rotation-vitest"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { @@ -1837,19 +2693,28 @@ function validateTokenRotationVitestJob(errors: string[], jobs: WorkflowRecord): } validateFreeStandingJobSelector(errors, jobs, jobName, "token-rotation"); if (job["timeout-minutes"] !== 45) { - errors.push("token-rotation-vitest job must keep the legacy 45 minute timeout"); + errors.push( + "token-rotation-vitest job must keep the legacy 45 minute timeout", + ); } const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("token-rotation-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "token-rotation-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } - if (jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/token-rotation") { + if ( + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/token-rotation" + ) { errors.push( "token-rotation-vitest job must write artifacts under e2e-artifacts/vitest/token-rotation", ); } if (!stringValue(jobEnv.NEMOCLAW_CLI_BIN).includes("bin/nemoclaw.js")) { - errors.push("token-rotation-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "token-rotation-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } requireEnvDoesNotExposeSecret( errors, @@ -1871,14 +2736,23 @@ function validateTokenRotationVitestJob(errors: string[], jobs: WorkflowRecord): } } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) errors.push("token-rotation-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "token-rotation-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("token-rotation-vitest checkout step must set persist-credentials=false"); + errors.push( + "token-rotation-vitest checkout step must set persist-credentials=false", + ); } - const dockerHubAuth = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerHubAuth = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerHubEnv = asRecord(dockerHubAuth?.env); if (dockerHubEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { errors.push( @@ -1886,12 +2760,15 @@ function validateTokenRotationVitestJob(errors: string[], jobs: WorkflowRecord): ); } if (dockerHubEnv.DOCKERHUB_TOKEN !== "${{ secrets.DOCKERHUB_TOKEN }}") { - errors.push("token-rotation-vitest Docker Hub auth must receive DOCKERHUB_TOKEN from secrets"); + errors.push( + "token-rotation-vitest Docker Hub auth must receive DOCKERHUB_TOKEN from secrets", + ); } requireRunContains(errors, dockerHubAuth, "docker login docker.io"); const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("token-rotation-vitest job missing step: Set up Node"); + if (!setupNode) + errors.push("token-rotation-vitest job missing step: Set up Node"); requireFullShaAction(errors, setupNode, "token-rotation-vitest setup-node"); const installRootDependencies = requireJobStep( @@ -1900,12 +2777,21 @@ function validateTokenRotationVitestJob(errors: string[], jobs: WorkflowRecord): steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const runVitest = requireJobStep(errors, jobName, steps, "Run token rotation live test"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run token rotation live test", + ); const runVitestEnv = asRecord(runVitest?.env); requireEnvDoesNotExposeSecret( errors, @@ -1914,7 +2800,9 @@ function validateTokenRotationVitestJob(errors: string[], jobs: WorkflowRecord): "NVIDIA_INFERENCE_API_KEY", ); if (runVitestEnv.GITHUB_TOKEN !== "${{ github.token }}") { - errors.push("token-rotation-vitest step must receive GITHUB_TOKEN from github.token"); + errors.push( + "token-rotation-vitest step must receive GITHUB_TOKEN from github.token", + ); } for (const tokenName of [ "TELEGRAM_BOT_TOKEN_A", @@ -1935,25 +2823,48 @@ function validateTokenRotationVitestJob(errors: string[], jobs: WorkflowRecord): errors.push(`token-rotation-vitest step must set ${tokenName}`); } } - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/token-rotation.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/token-rotation.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload token rotation artifacts"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload token rotation artifacts", + ); requireFullShaAction(errors, upload, "token-rotation-vitest upload-artifact"); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-token-rotation") { errors.push("token-rotation-vitest artifact upload name must be stable"); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/token-rotation/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/token-rotation/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("token-rotation-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "token-rotation-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("token-rotation-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "token-rotation-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("token-rotation-vitest artifact upload retention-days must be 14"); + errors.push( + "token-rotation-vitest artifact upload retention-days must be 14", + ); } } @@ -1969,11 +2880,20 @@ function validateMessagingCompatibleEndpointVitestJob( } if (job["runs-on"] !== "ubuntu-latest") { - errors.push("messaging-compatible-endpoint-vitest job must run on ubuntu-latest"); + errors.push( + "messaging-compatible-endpoint-vitest job must run on ubuntu-latest", + ); } - validateFreeStandingJobSelector(errors, jobs, jobName, "messaging-compatible-endpoint"); + validateFreeStandingJobSelector( + errors, + jobs, + jobName, + "messaging-compatible-endpoint", + ); if (job["timeout-minutes"] !== 45) { - errors.push("messaging-compatible-endpoint-vitest job must keep the legacy 45 minute timeout"); + errors.push( + "messaging-compatible-endpoint-vitest job must keep the legacy 45 minute timeout", + ); } const jobEnv = asRecord(job.env); @@ -1991,13 +2911,19 @@ function validateMessagingCompatibleEndpointVitestJob( ); } if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("messaging-compatible-endpoint-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "messaging-compatible-endpoint-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if (jobEnv.NEMOCLAW_SANDBOX_NAME !== "e2e-msg-compat") { - errors.push("messaging-compatible-endpoint-vitest job must pin the legacy sandbox name"); + errors.push( + "messaging-compatible-endpoint-vitest job must pin the legacy sandbox name", + ); } if (jobEnv.OPENSHELL_GATEWAY !== "nemoclaw") { - errors.push("messaging-compatible-endpoint-vitest job must force OPENSHELL_GATEWAY=nemoclaw"); + errors.push( + "messaging-compatible-endpoint-vitest job must force OPENSHELL_GATEWAY=nemoclaw", + ); } requireEnvDoesNotExposeSecret( errors, @@ -2054,9 +2980,18 @@ function validateMessagingCompatibleEndpointVitestJob( ); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("messaging-compatible-endpoint-vitest job missing checkout step"); - requireFullShaAction(errors, checkout, "messaging-compatible-endpoint-vitest checkout"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push( + "messaging-compatible-endpoint-vitest job missing checkout step", + ); + requireFullShaAction( + errors, + checkout, + "messaging-compatible-endpoint-vitest checkout", + ); if (asRecord(checkout?.with)["persist-credentials"] !== false) { errors.push( "messaging-compatible-endpoint-vitest checkout step must set persist-credentials=false", @@ -2064,8 +2999,15 @@ function validateMessagingCompatibleEndpointVitestJob( } const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("messaging-compatible-endpoint-vitest job missing step: Set up Node"); - requireFullShaAction(errors, setupNode, "messaging-compatible-endpoint-vitest setup-node"); + if (!setupNode) + errors.push( + "messaging-compatible-endpoint-vitest job missing step: Set up Node", + ); + requireFullShaAction( + errors, + setupNode, + "messaging-compatible-endpoint-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -2073,7 +3015,11 @@ function validateMessagingCompatibleEndpointVitestJob( steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); @@ -2097,12 +3043,20 @@ function validateMessagingCompatibleEndpointVitestJob( ); } if (runVitestEnv.TELEGRAM_BOT_TOKEN !== "test-fake-telegram-token-e2e") { - errors.push("messaging-compatible-endpoint-vitest step must set a fake Telegram token"); + errors.push( + "messaging-compatible-endpoint-vitest step must set a fake Telegram token", + ); } if (runVitestEnv.TELEGRAM_ALLOWED_IDS !== "123456789") { - errors.push("messaging-compatible-endpoint-vitest step must set fake Telegram allowed ids"); + errors.push( + "messaging-compatible-endpoint-vitest step must set fake Telegram allowed ids", + ); } - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); requireRunContains( errors, runVitest, @@ -2115,10 +3069,18 @@ function validateMessagingCompatibleEndpointVitestJob( steps, "Upload messaging compatible endpoint artifacts", ); - requireFullShaAction(errors, upload, "messaging-compatible-endpoint-vitest upload-artifact"); + requireFullShaAction( + errors, + upload, + "messaging-compatible-endpoint-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); - if (uploadWith.name !== "e2e-vitest-scenarios-messaging-compatible-endpoint") { - errors.push("messaging-compatible-endpoint-vitest artifact upload name must be stable"); + if ( + uploadWith.name !== "e2e-vitest-scenarios-messaging-compatible-endpoint" + ) { + errors.push( + "messaging-compatible-endpoint-vitest artifact upload name must be stable", + ); } const uploadPath = stringValue(uploadWith.path); requireUploadPathContains( @@ -2137,11 +3099,16 @@ function validateMessagingCompatibleEndpointVitestJob( ); } if (uploadWith["retention-days"] !== 14) { - errors.push("messaging-compatible-endpoint-vitest artifact upload retention-days must be 14"); + errors.push( + "messaging-compatible-endpoint-vitest artifact upload retention-days must be 14", + ); } } -function validateOnboardNegativePathsVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateOnboardNegativePathsVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "onboard-negative-paths-vitest"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { @@ -2152,11 +3119,18 @@ function validateOnboardNegativePathsVitestJob(errors: string[], jobs: WorkflowR if (job["runs-on"] !== "ubuntu-latest") { errors.push("onboard-negative-paths-vitest job must run on ubuntu-latest"); } - validateFreeStandingJobSelector(errors, jobs, jobName, "onboard-negative-paths"); + validateFreeStandingJobSelector( + errors, + jobs, + jobName, + "onboard-negative-paths", + ); const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("onboard-negative-paths-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "onboard-negative-paths-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if ( jobEnv.E2E_ARTIFACT_DIR !== @@ -2184,16 +3158,30 @@ function validateOnboardNegativePathsVitestJob(errors: string[], jobs: WorkflowR ); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("onboard-negative-paths-vitest job missing checkout step"); - requireFullShaAction(errors, checkout, "onboard-negative-paths-vitest checkout"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push("onboard-negative-paths-vitest job missing checkout step"); + requireFullShaAction( + errors, + checkout, + "onboard-negative-paths-vitest checkout", + ); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("onboard-negative-paths-vitest checkout step must set persist-credentials=false"); + errors.push( + "onboard-negative-paths-vitest checkout step must set persist-credentials=false", + ); } const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("onboard-negative-paths-vitest job missing step: Set up Node"); - requireFullShaAction(errors, setupNode, "onboard-negative-paths-vitest setup-node"); + if (!setupNode) + errors.push("onboard-negative-paths-vitest job missing step: Set up Node"); + requireFullShaAction( + errors, + setupNode, + "onboard-negative-paths-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -2201,23 +3189,55 @@ function validateOnboardNegativePathsVitestJob(errors: string[], jobs: WorkflowR steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const runVitest = requireJobStep(errors, jobName, steps, "Run onboard negative-paths live test"); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/onboard-negative-paths.test.ts"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run onboard negative-paths live test", + ); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/onboard-negative-paths.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload onboard negative-paths artifacts"); - requireFullShaAction(errors, upload, "onboard-negative-paths-vitest upload-artifact"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload onboard negative-paths artifacts", + ); + requireFullShaAction( + errors, + upload, + "onboard-negative-paths-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-onboard-negative-paths") { - errors.push("onboard-negative-paths-vitest artifact upload name must be stable"); + errors.push( + "onboard-negative-paths-vitest artifact upload name must be stable", + ); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/onboard-negative-paths/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/onboard-negative-paths/", + ); if (uploadWith["include-hidden-files"] !== false) { errors.push( "onboard-negative-paths-vitest artifact upload must set include-hidden-files: false", @@ -2229,11 +3249,16 @@ function validateOnboardNegativePathsVitestJob(errors: string[], jobs: WorkflowR ); } if (uploadWith["retention-days"] !== 14) { - errors.push("onboard-negative-paths-vitest artifact upload retention-days must be 14"); + errors.push( + "onboard-negative-paths-vitest artifact upload retention-days must be 14", + ); } } -function validateCloudInferenceVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateCloudInferenceVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "cloud-inference-vitest"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { @@ -2250,24 +3275,40 @@ function validateCloudInferenceVitestJob(errors: string[], jobs: WorkflowRecord) } const jobEnv = asRecord(job.env); - if (jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/cloud-inference") { + if ( + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/cloud-inference" + ) { errors.push( "cloud-inference-vitest job must write artifacts under e2e-artifacts/vitest/cloud-inference", ); } if (jobEnv.NEMOCLAW_CLI_BIN !== "${{ github.workspace }}/bin/nemoclaw.js") { - errors.push("cloud-inference-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "cloud-inference-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("cloud-inference-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "cloud-inference-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if (jobEnv.NEMOCLAW_SANDBOX_NAME !== "e2e-cloud-inference") { - errors.push("cloud-inference-vitest job must set NEMOCLAW_SANDBOX_NAME=e2e-cloud-inference"); + errors.push( + "cloud-inference-vitest job must set NEMOCLAW_SANDBOX_NAME=e2e-cloud-inference", + ); } if (jobEnv.OPENSHELL_GATEWAY !== "nemoclaw") { - errors.push("cloud-inference-vitest job must force OPENSHELL_GATEWAY=nemoclaw"); + errors.push( + "cloud-inference-vitest job must force OPENSHELL_GATEWAY=nemoclaw", + ); } - requireEnvDoesNotExposeSecret(errors, "cloud-inference-vitest job", jobEnv, "NVIDIA_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + "cloud-inference-vitest job", + jobEnv, + "NVIDIA_API_KEY", + ); const steps = asSteps(job.steps); requireNoDispatchInputInterpolation(errors, steps); @@ -2282,15 +3323,21 @@ function validateCloudInferenceVitestJob(errors: string[], jobs: WorkflowRecord) } } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("cloud-inference-vitest job missing checkout step"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push("cloud-inference-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "cloud-inference-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("cloud-inference-vitest checkout step must set persist-credentials=false"); + errors.push( + "cloud-inference-vitest checkout step must set persist-credentials=false", + ); } const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("cloud-inference-vitest job missing step: Set up Node"); + if (!setupNode) + errors.push("cloud-inference-vitest job missing step: Set up Node"); requireFullShaAction(errors, setupNode, "cloud-inference-vitest setup-node"); const installRootDependencies = requireJobStep( @@ -2299,48 +3346,96 @@ function validateCloudInferenceVitestJob(errors: string[], jobs: WorkflowRecord) steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const runVitest = requireJobStep(errors, jobName, steps, "Run cloud inference live test"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run cloud inference live test", + ); const runVitestEnv = asRecord(runVitest?.env); if (runVitestEnv.NVIDIA_API_KEY !== "${{ secrets.NVIDIA_API_KEY }}") { - errors.push("cloud-inference-vitest run step must receive NVIDIA_API_KEY from secrets"); + errors.push( + "cloud-inference-vitest run step must receive NVIDIA_API_KEY from secrets", + ); } - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/cloud-inference.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/cloud-inference.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload cloud inference artifacts"); - requireFullShaAction(errors, upload, "cloud-inference-vitest upload-artifact"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload cloud inference artifacts", + ); + requireFullShaAction( + errors, + upload, + "cloud-inference-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-cloud-inference") { errors.push("cloud-inference-vitest artifact upload name must be stable"); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/cloud-inference/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/cloud-inference/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("cloud-inference-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "cloud-inference-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("cloud-inference-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "cloud-inference-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("cloud-inference-vitest artifact upload retention-days must be 14"); + errors.push( + "cloud-inference-vitest artifact upload retention-days must be 14", + ); } } -function requireNoDockerHubAuthInRun(errors: string[], owner: string, runScript: string): void { +function requireNoDockerHubAuthInRun( + errors: string[], + owner: string, + runScript: string, +): void { if (!runScript) return; const usesDockerLogin = /\bdocker\s+login\b/i.test(runScript); - const referencesSecret = /\bsecrets\.[A-Za-z0-9_]+\b|\$\{\{\s*secrets\.[^}]+\}\}/.test(runScript); + const referencesSecret = + /\bsecrets\.[A-Za-z0-9_]+\b|\$\{\{\s*secrets\.[^}]+\}\}/.test(runScript); if (usesDockerLogin || referencesSecret) { - errors.push(`${owner} run script must not use docker login or inline secret interpolation`); + errors.push( + `${owner} run script must not use docker login or inline secret interpolation`, + ); } } -function validateDoubleOnboardVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateDoubleOnboardVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "double-onboard-vitest"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { @@ -2355,12 +3450,19 @@ function validateDoubleOnboardVitestJob(errors: string[], jobs: WorkflowRecord): const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("double-onboard-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "double-onboard-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if (jobEnv.NEMOCLAW_CLI_BIN !== "${{ github.workspace }}/bin/nemoclaw.js") { - errors.push("double-onboard-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "double-onboard-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } - if (jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/double-onboard") { + if ( + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/double-onboard" + ) { errors.push( "double-onboard-vitest job must write artifacts under e2e-artifacts/vitest/double-onboard", ); @@ -2371,7 +3473,12 @@ function validateDoubleOnboardVitestJob(errors: string[], jobs: WorkflowRecord): jobEnv, "NVIDIA_INFERENCE_API_KEY", ); - requireEnvDoesNotExposeSecret(errors, "double-onboard-vitest job", jobEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + "double-onboard-vitest job", + jobEnv, + "DOCKERHUB_TOKEN", + ); const steps = asSteps(job.steps); requireNoDispatchInputInterpolation(errors, steps); @@ -2392,28 +3499,42 @@ function validateDoubleOnboardVitestJob(errors: string[], jobs: WorkflowRecord): ); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) errors.push("double-onboard-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "double-onboard-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("double-onboard-vitest checkout step must set persist-credentials=false"); + errors.push( + "double-onboard-vitest checkout step must set persist-credentials=false", + ); } - const dockerLogin = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerLogin = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerLoginEnv = asRecord(dockerLogin?.env); - if (dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { + if ( + dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}" + ) { errors.push( "double-onboard-vitest Docker login step must read DOCKERHUB_USERNAME from secrets", ); } if (dockerLoginEnv.DOCKERHUB_TOKEN !== "${{ secrets.DOCKERHUB_TOKEN }}") { - errors.push("double-onboard-vitest Docker login step must read DOCKERHUB_TOKEN from secrets"); + errors.push( + "double-onboard-vitest Docker login step must read DOCKERHUB_TOKEN from secrets", + ); } requireRunContains(errors, dockerLogin, "docker login docker.io"); requireRunContains(errors, dockerLogin, "continuing with anonymous pulls"); const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("double-onboard-vitest job missing step: Set up Node"); + if (!setupNode) + errors.push("double-onboard-vitest job missing step: Set up Node"); requireFullShaAction(errors, setupNode, "double-onboard-vitest setup-node"); const installRootDependencies = requireJobStep( @@ -2422,38 +3543,78 @@ function validateDoubleOnboardVitestJob(errors: string[], jobs: WorkflowRecord): steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const installTools = requireJobStep(errors, jobName, steps, "Install OpenShell CLI"); + const installTools = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell CLI", + ); requireRunContains(errors, installTools, "bash scripts/install-openshell.sh"); - const runVitest = requireJobStep(errors, jobName, steps, "Run double-onboard live Vitest test"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run double-onboard live Vitest test", + ); requireRunContains(errors, runVitest, "OPENSHELL_BIN"); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/double-onboard.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/double-onboard.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload double-onboard Vitest artifacts"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload double-onboard Vitest artifacts", + ); requireFullShaAction(errors, upload, "double-onboard-vitest upload-artifact"); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-double-onboard") { errors.push("double-onboard-vitest artifact upload name must be stable"); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/double-onboard/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/double-onboard/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("double-onboard-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "double-onboard-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("double-onboard-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "double-onboard-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("double-onboard-vitest artifact upload retention-days must be 14"); + errors.push( + "double-onboard-vitest artifact upload retention-days must be 14", + ); } } -function validateRuntimeOverridesVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateRuntimeOverridesVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "runtime-overrides-vitest"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { @@ -2468,10 +3629,13 @@ function validateRuntimeOverridesVitestJob(errors: string[], jobs: WorkflowRecor const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("runtime-overrides-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "runtime-overrides-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if ( - jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/runtime-overrides" + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/runtime-overrides" ) { errors.push( "runtime-overrides-vitest job must write artifacts under e2e-artifacts/vitest/runtime-overrides", @@ -2489,62 +3653,126 @@ function validateRuntimeOverridesVitestJob(errors: string[], jobs: WorkflowRecor jobEnv, "DOCKERHUB_USERNAME", ); - requireEnvDoesNotExposeSecret(errors, "runtime-overrides-vitest job", jobEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + "runtime-overrides-vitest job", + jobEnv, + "DOCKERHUB_TOKEN", + ); const steps = asSteps(job.steps); requireNoDispatchInputInterpolation(errors, steps); for (const step of steps) { const stepName = `runtime-overrides-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_INFERENCE_API_KEY"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_INFERENCE_API_KEY", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("runtime-overrides-vitest job missing checkout step"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push("runtime-overrides-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "runtime-overrides-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("runtime-overrides-vitest checkout step must set persist-credentials=false"); + errors.push( + "runtime-overrides-vitest checkout step must set persist-credentials=false", + ); } const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("runtime-overrides-vitest job missing step: Set up Node"); - requireFullShaAction(errors, setupNode, "runtime-overrides-vitest setup-node"); + if (!setupNode) + errors.push("runtime-overrides-vitest job missing step: Set up Node"); + requireFullShaAction( + errors, + setupNode, + "runtime-overrides-vitest setup-node", + ); + + const installRootDependencies = requireJobStep( + errors, + jobName, + steps, + "Install root dependencies", + ); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); + + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run runtime overrides live test", + ); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/runtime-overrides.test.ts", + ); - const installRootDependencies = requireJobStep( + const upload = requireJobStep( errors, jobName, steps, - "Install root dependencies", + "Upload runtime overrides artifacts", + ); + requireFullShaAction( + errors, + upload, + "runtime-overrides-vitest upload-artifact", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); - - const runVitest = requireJobStep(errors, jobName, steps, "Run runtime overrides live test"); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/runtime-overrides.test.ts"); - - const upload = requireJobStep(errors, jobName, steps, "Upload runtime overrides artifacts"); - requireFullShaAction(errors, upload, "runtime-overrides-vitest upload-artifact"); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-runtime-overrides") { errors.push("runtime-overrides-vitest artifact upload name must be stable"); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/runtime-overrides/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/runtime-overrides/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("runtime-overrides-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "runtime-overrides-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("runtime-overrides-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "runtime-overrides-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("runtime-overrides-vitest artifact upload retention-days must be 14"); + errors.push( + "runtime-overrides-vitest artifact upload retention-days must be 14", + ); } } -function validateHermesE2EVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateHermesE2EVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "hermes-e2e-vitest"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { @@ -2556,13 +3784,21 @@ function validateHermesE2EVitestJob(errors: string[], jobs: WorkflowRecord): voi errors.push("hermes-e2e-vitest job must run on ubuntu-latest"); } if (job.needs !== "generate-matrix") { - errors.push("hermes-e2e-vitest job must depend on generate-matrix validation"); + errors.push( + "hermes-e2e-vitest job must depend on generate-matrix validation", + ); } - if (job.if !== "${{ needs.generate-matrix.outputs.hermes_selected == 'true' }}") { - errors.push("hermes-e2e-vitest job must use validated hermes_selected output"); + if ( + job.if !== "${{ needs.generate-matrix.outputs.hermes_selected == 'true' }}" + ) { + errors.push( + "hermes-e2e-vitest job must use validated hermes_selected output", + ); } if (stringValue(job.if).includes("inputs.scenarios")) { - errors.push("hermes-e2e-vitest job must not inspect raw workflow dispatch scenarios"); + errors.push( + "hermes-e2e-vitest job must not inspect raw workflow dispatch scenarios", + ); } const jobEnv = asRecord(job.env); @@ -2570,10 +3806,17 @@ function validateHermesE2EVitestJob(errors: string[], jobs: WorkflowRecord): voi errors.push("hermes-e2e-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); } if (jobEnv.NEMOCLAW_CLI_BIN !== "${{ github.workspace }}/bin/nemoclaw.js") { - errors.push("hermes-e2e-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "hermes-e2e-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } - if (jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/hermes-e2e") { - errors.push("hermes-e2e-vitest job must write artifacts under e2e-artifacts/vitest/hermes-e2e"); + if ( + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/hermes-e2e" + ) { + errors.push( + "hermes-e2e-vitest job must write artifacts under e2e-artifacts/vitest/hermes-e2e", + ); } if (jobEnv.NEMOCLAW_AGENT !== "hermes") { errors.push("hermes-e2e-vitest job must set NEMOCLAW_AGENT=hermes"); @@ -2582,7 +3825,9 @@ function validateHermesE2EVitestJob(errors: string[], jobs: WorkflowRecord): voi errors.push("hermes-e2e-vitest job must pin the CI-safe Hermes model"); } if (jobEnv.NEMOCLAW_ONBOARD_VALIDATION_TIMEOUT_SECONDS !== "60") { - errors.push("hermes-e2e-vitest job must give hosted endpoint validation a CI-safe timeout"); + errors.push( + "hermes-e2e-vitest job must give hosted endpoint validation a CI-safe timeout", + ); } requireEnvDoesNotExposeSecret( errors, @@ -2604,15 +3849,20 @@ function validateHermesE2EVitestJob(errors: string[], jobs: WorkflowRecord): voi } } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) errors.push("hermes-e2e-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "hermes-e2e-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("hermes-e2e-vitest checkout step must set persist-credentials=false"); + errors.push( + "hermes-e2e-vitest checkout step must set persist-credentials=false", + ); } const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("hermes-e2e-vitest job missing step: Set up Node"); + if (!setupNode) + errors.push("hermes-e2e-vitest job missing step: Set up Node"); requireFullShaAction(errors, setupNode, "hermes-e2e-vitest setup-node"); const installRootDependencies = requireJobStep( @@ -2621,40 +3871,78 @@ function validateHermesE2EVitestJob(errors: string[], jobs: WorkflowRecord): voi steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const runVitest = requireJobStep(errors, jobName, steps, "Run Hermes live Vitest test"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run Hermes live Vitest test", + ); const runVitestEnv = asRecord(runVitest?.env); - if (runVitestEnv.NVIDIA_INFERENCE_API_KEY !== "${{ secrets.NVIDIA_INFERENCE_API_KEY }}") { - errors.push("hermes-e2e-vitest Vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets"); + if ( + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { + errors.push( + "hermes-e2e-vitest Vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", + ); } - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/hermes-e2e.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/hermes-e2e.test.ts", + ); requireRunDoesNotContain(errors, runVitest, "${{ inputs."); - const upload = requireJobStep(errors, jobName, steps, "Upload Hermes live Vitest artifacts"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload Hermes live Vitest artifacts", + ); requireFullShaAction(errors, upload, "hermes-e2e-vitest upload-artifact"); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-hermes-e2e") { errors.push("hermes-e2e-vitest artifact upload name must be stable"); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/hermes-e2e/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/hermes-e2e/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("hermes-e2e-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "hermes-e2e-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("hermes-e2e-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "hermes-e2e-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { errors.push("hermes-e2e-vitest artifact upload retention-days must be 14"); } } -function validateHermesRootEntrypointSmokeVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateHermesRootEntrypointSmokeVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "hermes-root-entrypoint-smoke-vitest"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { @@ -2663,10 +3951,14 @@ function validateHermesRootEntrypointSmokeVitestJob(errors: string[], jobs: Work } if (job["runs-on"] !== "ubuntu-latest") { - errors.push("hermes-root-entrypoint-smoke-vitest job must run on ubuntu-latest"); + errors.push( + "hermes-root-entrypoint-smoke-vitest job must run on ubuntu-latest", + ); } if (job.needs !== "generate-matrix") { - errors.push("hermes-root-entrypoint-smoke-vitest job must depend on generate-matrix"); + errors.push( + "hermes-root-entrypoint-smoke-vitest job must depend on generate-matrix", + ); } const expectedIf = "${{ needs.generate-matrix.result == 'success' && ((inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',hermes-root-entrypoint-smoke-vitest,') || contains(format(',{0},', inputs.scenarios), ',hermes-root-entrypoint-smoke,')) }}"; @@ -2676,12 +3968,16 @@ function validateHermesRootEntrypointSmokeVitestJob(errors: string[], jobs: Work ); } if (job["timeout-minutes"] !== 45) { - errors.push("hermes-root-entrypoint-smoke-vitest job must keep the 45 minute timeout"); + errors.push( + "hermes-root-entrypoint-smoke-vitest job must keep the 45 minute timeout", + ); } const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("hermes-root-entrypoint-smoke-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "hermes-root-entrypoint-smoke-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if ( jobEnv.E2E_ARTIFACT_DIR !== @@ -2746,9 +4042,18 @@ function validateHermesRootEntrypointSmokeVitestJob(errors: string[], jobs: Work ); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("hermes-root-entrypoint-smoke-vitest job missing checkout step"); - requireFullShaAction(errors, checkout, "hermes-root-entrypoint-smoke-vitest checkout"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push( + "hermes-root-entrypoint-smoke-vitest job missing checkout step", + ); + requireFullShaAction( + errors, + checkout, + "hermes-root-entrypoint-smoke-vitest checkout", + ); if (asRecord(checkout?.with)["persist-credentials"] !== false) { errors.push( "hermes-root-entrypoint-smoke-vitest checkout step must set persist-credentials=false", @@ -2756,8 +4061,15 @@ function validateHermesRootEntrypointSmokeVitestJob(errors: string[], jobs: Work } const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("hermes-root-entrypoint-smoke-vitest job missing step: Set up Node"); - requireFullShaAction(errors, setupNode, "hermes-root-entrypoint-smoke-vitest setup-node"); + if (!setupNode) + errors.push( + "hermes-root-entrypoint-smoke-vitest job missing step: Set up Node", + ); + requireFullShaAction( + errors, + setupNode, + "hermes-root-entrypoint-smoke-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -2765,7 +4077,11 @@ function validateHermesRootEntrypointSmokeVitestJob(errors: string[], jobs: Work steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const runVitest = requireJobStep( errors, @@ -2773,7 +4089,11 @@ function validateHermesRootEntrypointSmokeVitestJob(errors: string[], jobs: Work steps, "Run Hermes root entrypoint smoke live test", ); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); requireRunContains( errors, runVitest, @@ -2787,10 +4107,16 @@ function validateHermesRootEntrypointSmokeVitestJob(errors: string[], jobs: Work steps, "Upload Hermes root entrypoint smoke artifacts", ); - requireFullShaAction(errors, upload, "hermes-root-entrypoint-smoke-vitest upload-artifact"); + requireFullShaAction( + errors, + upload, + "hermes-root-entrypoint-smoke-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-hermes-root-entrypoint-smoke") { - errors.push("hermes-root-entrypoint-smoke-vitest artifact upload name must be stable"); + errors.push( + "hermes-root-entrypoint-smoke-vitest artifact upload name must be stable", + ); } const uploadPath = stringValue(uploadWith.path); requireUploadPathContains( @@ -2809,7 +4135,9 @@ function validateHermesRootEntrypointSmokeVitestJob(errors: string[], jobs: Work ); } if (uploadWith["retention-days"] !== 14) { - errors.push("hermes-root-entrypoint-smoke-vitest artifact upload retention-days must be 14"); + errors.push( + "hermes-root-entrypoint-smoke-vitest artifact upload retention-days must be 14", + ); } } @@ -2941,7 +4269,11 @@ function validateHermesSandboxSecretBoundaryVitestJob( } } -function validateDiagnosticsVitestJob(errors: string[], jobs: WorkflowRecord): void { + +function validateDiagnosticsVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "diagnostics-vitest"; const scenarioName = "diagnostics"; const job = asRecord(jobs[jobName]); @@ -2960,15 +4292,22 @@ function validateDiagnosticsVitestJob(errors: string[], jobs: WorkflowRecord): v const jobEnv = asRecord(job.env); if ("DOCKER_CONFIG" in jobEnv) { - errors.push("diagnostics-vitest job must not expose Docker auth to branch-controlled steps"); + errors.push( + "diagnostics-vitest job must not expose Docker auth to branch-controlled steps", + ); } - if (jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/diagnostics") { + if ( + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/diagnostics" + ) { errors.push( "diagnostics-vitest job must write artifacts under e2e-artifacts/vitest/diagnostics", ); } if (jobEnv.NEMOCLAW_CLI_BIN !== "${{ github.workspace }}/bin/nemoclaw.js") { - errors.push("diagnostics-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "diagnostics-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { errors.push("diagnostics-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); @@ -2977,10 +4316,14 @@ function validateDiagnosticsVitestJob(errors: string[], jobs: WorkflowRecord): v errors.push("diagnostics-vitest job must set NEMOCLAW_NON_INTERACTIVE=1"); } if (jobEnv.NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE !== "1") { - errors.push("diagnostics-vitest job must set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1"); + errors.push( + "diagnostics-vitest job must set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1", + ); } if (jobEnv.NEMOCLAW_SANDBOX_NAME !== "e2e-diag") { - errors.push("diagnostics-vitest job must use the stable e2e-diag sandbox name"); + errors.push( + "diagnostics-vitest job must use the stable e2e-diag sandbox name", + ); } if (jobEnv.OPENSHELL_GATEWAY !== "nemoclaw") { errors.push("diagnostics-vitest job must force OPENSHELL_GATEWAY=nemoclaw"); @@ -2991,7 +4334,12 @@ function validateDiagnosticsVitestJob(errors: string[], jobs: WorkflowRecord): v "DOCKERHUB_TOKEN", "GITHUB_TOKEN", ]) { - requireEnvDoesNotExposeSecret(errors, "diagnostics-vitest job", jobEnv, secret); + requireEnvDoesNotExposeSecret( + errors, + "diagnostics-vitest job", + jobEnv, + secret, + ); } const steps = asSteps(job.steps); @@ -3000,9 +4348,19 @@ function validateDiagnosticsVitestJob(errors: string[], jobs: WorkflowRecord): v const stepName = `diagnostics-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); if (step.name !== "Run diagnostics live test") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_API_KEY", + ); } - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); @@ -3014,15 +4372,20 @@ function validateDiagnosticsVitestJob(errors: string[], jobs: WorkflowRecord): v ); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) errors.push("diagnostics-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "diagnostics-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("diagnostics-vitest checkout step must set persist-credentials=false"); + errors.push( + "diagnostics-vitest checkout step must set persist-credentials=false", + ); } const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("diagnostics-vitest job missing step: Set up Node"); + if (!setupNode) + errors.push("diagnostics-vitest job missing step: Set up Node"); requireFullShaAction(errors, setupNode, "diagnostics-vitest setup-node"); const installRootDependencies = requireJobStep( @@ -3031,40 +4394,75 @@ function validateDiagnosticsVitestJob(errors: string[], jobs: WorkflowRecord): v steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const runVitest = requireJobStep(errors, jobName, steps, "Run diagnostics live test"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run diagnostics live test", + ); const runVitestEnv = asRecord(runVitest?.env); if (runVitestEnv.NVIDIA_API_KEY !== "${{ secrets.NVIDIA_API_KEY }}") { - errors.push("diagnostics-vitest Vitest step must receive NVIDIA_API_KEY from secrets"); + errors.push( + "diagnostics-vitest Vitest step must receive NVIDIA_API_KEY from secrets", + ); } - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/diagnostics.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/diagnostics.test.ts", + ); requireRunDoesNotContain(errors, runVitest, "${{ inputs."); - const upload = requireJobStep(errors, jobName, steps, "Upload diagnostics artifacts"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload diagnostics artifacts", + ); requireFullShaAction(errors, upload, "diagnostics-vitest upload-artifact"); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-diagnostics") { errors.push("diagnostics-vitest artifact upload name must be stable"); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/diagnostics/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/diagnostics/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("diagnostics-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "diagnostics-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("diagnostics-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "diagnostics-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { errors.push("diagnostics-vitest artifact upload retention-days must be 14"); } } -function validateSparkInstallVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateSparkInstallVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "spark-install-vitest"; const scenarioName = "spark-install"; const job = asRecord(jobs[jobName]); @@ -3082,22 +4480,31 @@ function validateSparkInstallVitestJob(errors: string[], jobs: WorkflowRecord): validateFreeStandingJobSelector(errors, jobs, jobName, scenarioName); const jobEnv = asRecord(job.env); - if (jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/spark-install") { + if ( + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/spark-install" + ) { errors.push( "spark-install-vitest job must write artifacts under e2e-artifacts/vitest/spark-install", ); } if (jobEnv.NEMOCLAW_CLI_BIN !== "${{ github.workspace }}/bin/nemoclaw.js") { - errors.push("spark-install-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "spark-install-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("spark-install-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "spark-install-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if (jobEnv.NEMOCLAW_NON_INTERACTIVE !== "1") { errors.push("spark-install-vitest job must set NEMOCLAW_NON_INTERACTIVE=1"); } if (jobEnv.NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE !== "1") { - errors.push("spark-install-vitest job must accept third-party software non-interactively"); + errors.push( + "spark-install-vitest job must accept third-party software non-interactively", + ); } if (jobEnv.NEMOCLAW_FRESH !== "1") { errors.push("spark-install-vitest job must set NEMOCLAW_FRESH=1"); @@ -3111,10 +4518,17 @@ function validateSparkInstallVitestJob(errors: string[], jobs: WorkflowRecord): errors.push("spark-install-vitest job must use the cloud provider"); } if (jobEnv.OPENSHELL_GATEWAY !== "nemoclaw") { - errors.push("spark-install-vitest job must force OPENSHELL_GATEWAY=nemoclaw"); + errors.push( + "spark-install-vitest job must force OPENSHELL_GATEWAY=nemoclaw", + ); } for (const secret of COMMON_SECRET_ENV_NAMES) { - requireEnvDoesNotExposeSecret(errors, "spark-install-vitest job", jobEnv, secret); + requireEnvDoesNotExposeSecret( + errors, + "spark-install-vitest job", + jobEnv, + secret, + ); } const steps = asSteps(job.steps); @@ -3123,20 +4537,34 @@ function validateSparkInstallVitestJob(errors: string[], jobs: WorkflowRecord): const stepName = `spark-install-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); if (step.name !== "Run Spark install live test") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_API_KEY", + ); } - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) { errors.push("spark-install-vitest job missing checkout step"); } requireFullShaAction(errors, checkout, "spark-install-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("spark-install-vitest checkout step must set persist-credentials=false"); + errors.push( + "spark-install-vitest checkout step must set persist-credentials=false", + ); } const setupNode = namedStep(steps, "Set up Node"); @@ -3151,37 +4579,74 @@ function validateSparkInstallVitestJob(errors: string[], jobs: WorkflowRecord): steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); - const runVitest = requireJobStep(errors, jobName, steps, "Run Spark install live test"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run Spark install live test", + ); const runVitestEnv = asRecord(runVitest?.env); if (runVitestEnv.NVIDIA_API_KEY !== "${{ secrets.NVIDIA_API_KEY }}") { - errors.push("spark-install-vitest Vitest step must receive NVIDIA_API_KEY from secrets"); + errors.push( + "spark-install-vitest Vitest step must receive NVIDIA_API_KEY from secrets", + ); } requireRunContains(errors, runVitest, "set -euo pipefail"); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/spark-install.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/spark-install.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload Spark install artifacts"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload Spark install artifacts", + ); requireFullShaAction(errors, upload, "spark-install-vitest upload-artifact"); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-spark-install") { errors.push("spark-install-vitest artifact upload name must be stable"); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/spark-install/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/spark-install/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("spark-install-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "spark-install-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("spark-install-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "spark-install-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("spark-install-vitest artifact upload retention-days must be 14"); + errors.push( + "spark-install-vitest artifact upload retention-days must be 14", + ); } } -function validateSnapshotCommandsVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateSnapshotCommandsVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "snapshot-commands-vitest"; const scenarioName = "snapshot-commands"; const job = asRecord(jobs[jobName]); @@ -3200,29 +4665,42 @@ function validateSnapshotCommandsVitestJob(errors: string[], jobs: WorkflowRecor const jobEnv = asRecord(job.env); if ("DOCKER_CONFIG" in jobEnv) { - errors.push("snapshot-commands-vitest job must not set DOCKER_CONFIG at job level"); + errors.push( + "snapshot-commands-vitest job must not set DOCKER_CONFIG at job level", + ); } if ( - jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/snapshot-commands" + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/snapshot-commands" ) { errors.push( "snapshot-commands-vitest job must write artifacts under e2e-artifacts/vitest/snapshot-commands", ); } if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("snapshot-commands-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "snapshot-commands-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if (jobEnv.NEMOCLAW_NON_INTERACTIVE !== "1") { - errors.push("snapshot-commands-vitest job must set NEMOCLAW_NON_INTERACTIVE=1"); + errors.push( + "snapshot-commands-vitest job must set NEMOCLAW_NON_INTERACTIVE=1", + ); } if (jobEnv.NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE !== "1") { - errors.push("snapshot-commands-vitest job must accept third-party software non-interactively"); + errors.push( + "snapshot-commands-vitest job must accept third-party software non-interactively", + ); } if (jobEnv.NEMOCLAW_SANDBOX_NAME !== "e2e-snapshot") { - errors.push("snapshot-commands-vitest job must use the stable e2e-snapshot sandbox name"); + errors.push( + "snapshot-commands-vitest job must use the stable e2e-snapshot sandbox name", + ); } if (jobEnv.OPENSHELL_GATEWAY !== "nemoclaw") { - errors.push("snapshot-commands-vitest job must force OPENSHELL_GATEWAY=nemoclaw"); + errors.push( + "snapshot-commands-vitest job must force OPENSHELL_GATEWAY=nemoclaw", + ); } for (const secret of [ "NVIDIA_API_KEY", @@ -3230,7 +4708,12 @@ function validateSnapshotCommandsVitestJob(errors: string[], jobs: WorkflowRecor "DOCKERHUB_TOKEN", "GITHUB_TOKEN", ]) { - requireEnvDoesNotExposeSecret(errors, "snapshot-commands-vitest job", jobEnv, secret); + requireEnvDoesNotExposeSecret( + errors, + "snapshot-commands-vitest job", + jobEnv, + secret, + ); } const steps = asSteps(job.steps); @@ -3239,23 +4722,42 @@ function validateSnapshotCommandsVitestJob(errors: string[], jobs: WorkflowRecor const stepName = `snapshot-commands-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); if (step.name !== "Run snapshot commands live test") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_API_KEY", + ); } if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) { errors.push("snapshot-commands-vitest job missing checkout step"); } requireFullShaAction(errors, checkout, "snapshot-commands-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("snapshot-commands-vitest checkout step must set persist-credentials=false"); + errors.push( + "snapshot-commands-vitest checkout step must set persist-credentials=false", + ); } const configureDockerAuth = requireJobStep( @@ -3271,9 +4773,16 @@ function validateSnapshotCommandsVitestJob(errors: string[], jobs: WorkflowRecor ); requireRunDoesNotContain(errors, configureDockerAuth, "${{ runner.temp }}"); - const dockerLogin = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerLogin = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerLoginEnv = asRecord(dockerLogin?.env); - if (dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { + if ( + dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}" + ) { errors.push( "snapshot-commands-vitest Docker Hub auth must receive DOCKERHUB_USERNAME from secrets", ); @@ -3293,7 +4802,11 @@ function validateSnapshotCommandsVitestJob(errors: string[], jobs: WorkflowRecor if (!setupNode) { errors.push("snapshot-commands-vitest job missing step: Set up Node"); } - requireFullShaAction(errors, setupNode, "snapshot-commands-vitest setup-node"); + requireFullShaAction( + errors, + setupNode, + "snapshot-commands-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -3301,35 +4814,78 @@ function validateSnapshotCommandsVitestJob(errors: string[], jobs: WorkflowRecor steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); - const runVitest = requireJobStep(errors, jobName, steps, "Run snapshot commands live test"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run snapshot commands live test", + ); const runVitestEnv = asRecord(runVitest?.env); if (runVitestEnv.NVIDIA_API_KEY !== "${{ secrets.NVIDIA_API_KEY }}") { - errors.push("snapshot-commands-vitest Vitest step must receive NVIDIA_API_KEY from secrets"); + errors.push( + "snapshot-commands-vitest Vitest step must receive NVIDIA_API_KEY from secrets", + ); } - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/snapshot-commands.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/snapshot-commands.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload snapshot commands artifacts"); - requireFullShaAction(errors, upload, "snapshot-commands-vitest upload-artifact"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload snapshot commands artifacts", + ); + requireFullShaAction( + errors, + upload, + "snapshot-commands-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-snapshot-commands") { errors.push("snapshot-commands-vitest artifact upload name must be stable"); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/snapshot-commands/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/snapshot-commands/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("snapshot-commands-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "snapshot-commands-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("snapshot-commands-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "snapshot-commands-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("snapshot-commands-vitest artifact upload retention-days must be 14"); + errors.push( + "snapshot-commands-vitest artifact upload retention-days must be 14", + ); } - const cleanup = requireJobStep(errors, jobName, steps, "Clean up Docker auth"); + const cleanup = requireJobStep( + errors, + jobName, + steps, + "Clean up Docker auth", + ); if (cleanup?.if !== "always()") { errors.push("snapshot-commands-vitest Docker auth cleanup must always run"); } @@ -3345,12 +4901,16 @@ function validateModelRouterProviderRoutedInferenceVitestJob( const scenarioName = "model-router-provider-routed-inference"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { - errors.push("workflow missing model-router-provider-routed-inference-vitest job"); + errors.push( + "workflow missing model-router-provider-routed-inference-vitest job", + ); return; } if (job["runs-on"] !== "ubuntu-latest") { - errors.push("model-router-provider-routed-inference-vitest job must run on ubuntu-latest"); + errors.push( + "model-router-provider-routed-inference-vitest job must run on ubuntu-latest", + ); } validateFreeStandingJobSelector(errors, jobs, jobName, scenarioName); @@ -3403,21 +4963,44 @@ function validateModelRouterProviderRoutedInferenceVitestJob( const stepName = `model-router-provider-routed-inference-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); if (step.name !== "Run Model Router provider-routed inference live test") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_INFERENCE_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_INFERENCE_API_KEY", + ); } if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) { - errors.push("model-router-provider-routed-inference-vitest job missing checkout step"); + errors.push( + "model-router-provider-routed-inference-vitest job missing checkout step", + ); } - requireFullShaAction(errors, checkout, "model-router-provider-routed-inference-vitest checkout"); + requireFullShaAction( + errors, + checkout, + "model-router-provider-routed-inference-vitest checkout", + ); if (asRecord(checkout?.with)["persist-credentials"] !== false) { errors.push( "model-router-provider-routed-inference-vitest checkout step must set persist-credentials=false", @@ -3437,9 +5020,16 @@ function validateModelRouterProviderRoutedInferenceVitestJob( ); requireRunDoesNotContain(errors, configureDockerAuth, "${{ runner.temp }}"); - const dockerLogin = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerLogin = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerLoginEnv = asRecord(dockerLogin?.env); - if (dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { + if ( + dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}" + ) { errors.push( "model-router-provider-routed-inference-vitest Docker Hub auth must receive DOCKERHUB_USERNAME from secrets", ); @@ -3457,7 +5047,9 @@ function validateModelRouterProviderRoutedInferenceVitestJob( const setupNode = namedStep(steps, "Set up Node"); if (!setupNode) { - errors.push("model-router-provider-routed-inference-vitest job missing step: Set up Node"); + errors.push( + "model-router-provider-routed-inference-vitest job missing step: Set up Node", + ); } requireFullShaAction( errors, @@ -3471,7 +5063,11 @@ function validateModelRouterProviderRoutedInferenceVitestJob( steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); @@ -3483,12 +5079,19 @@ function validateModelRouterProviderRoutedInferenceVitestJob( "Run Model Router provider-routed inference live test", ); const runVitestEnv = asRecord(runVitest?.env); - if (runVitestEnv.NVIDIA_INFERENCE_API_KEY !== "${{ secrets.NVIDIA_INFERENCE_API_KEY }}") { + if ( + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { errors.push( "model-router-provider-routed-inference-vitest Vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", ); } - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); requireRunContains( errors, runVitest, @@ -3507,7 +5110,10 @@ function validateModelRouterProviderRoutedInferenceVitestJob( "model-router-provider-routed-inference-vitest upload-artifact", ); const uploadWith = asRecord(upload?.with); - if (uploadWith.name !== "e2e-vitest-scenarios-model-router-provider-routed-inference") { + if ( + uploadWith.name !== + "e2e-vitest-scenarios-model-router-provider-routed-inference" + ) { errors.push( "model-router-provider-routed-inference-vitest artifact upload name must be stable", ); @@ -3534,7 +5140,12 @@ function validateModelRouterProviderRoutedInferenceVitestJob( ); } - const cleanup = requireJobStep(errors, jobName, steps, "Clean up Docker auth"); + const cleanup = requireJobStep( + errors, + jobName, + steps, + "Clean up Docker auth", + ); if (cleanup?.if !== "always()") { errors.push( "model-router-provider-routed-inference-vitest Docker auth cleanup must always run", @@ -3550,7 +5161,10 @@ function runContainsCloudflaredAptInstall(run: string): boolean { ); } -function validateTunnelLifecycleVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateTunnelLifecycleVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "tunnel-lifecycle-vitest"; const scenarioName = "tunnel-lifecycle"; const job = asRecord(jobs[jobName]); @@ -3569,19 +5183,29 @@ function validateTunnelLifecycleVitestJob(errors: string[], jobs: WorkflowRecord const jobEnv = asRecord(job.env); if ("DOCKER_CONFIG" in jobEnv) { - errors.push("tunnel-lifecycle-vitest job must not set DOCKER_CONFIG at job level"); + errors.push( + "tunnel-lifecycle-vitest job must not set DOCKER_CONFIG at job level", + ); } if (jobEnv.NEMOCLAW_CLI_BIN !== "${{ github.workspace }}/bin/nemoclaw.js") { - errors.push("tunnel-lifecycle-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "tunnel-lifecycle-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } if (jobEnv.FREE_STANDING_VITEST_JOB !== "1") { - errors.push("tunnel-lifecycle-vitest job must set FREE_STANDING_VITEST_JOB=1"); + errors.push( + "tunnel-lifecycle-vitest job must set FREE_STANDING_VITEST_JOB=1", + ); } if (jobEnv.FREE_STANDING_SCENARIO_ID !== scenarioName) { - errors.push(`tunnel-lifecycle-vitest job must set FREE_STANDING_SCENARIO_ID=${scenarioName}`); + errors.push( + `tunnel-lifecycle-vitest job must set FREE_STANDING_SCENARIO_ID=${scenarioName}`, + ); } if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("tunnel-lifecycle-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "tunnel-lifecycle-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } requireEnvDoesNotExposeSecret( errors, @@ -3597,23 +5221,47 @@ function validateTunnelLifecycleVitestJob(errors: string[], jobs: WorkflowRecord const stepEnv = asRecord(step.env); requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); if (step.name !== "Run tunnel lifecycle live test") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_INFERENCE_API_KEY"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_INFERENCE_API_KEY", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_API_KEY", + ); } if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) { errors.push("tunnel-lifecycle-vitest job missing checkout step"); } requireFullShaAction(errors, checkout, "tunnel-lifecycle-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("tunnel-lifecycle-vitest checkout step must set persist-credentials=false"); + errors.push( + "tunnel-lifecycle-vitest checkout step must set persist-credentials=false", + ); } const configureDockerAuth = requireJobStep( @@ -3628,11 +5276,22 @@ function validateTunnelLifecycleVitestJob(errors: string[], jobs: WorkflowRecord 'echo "DOCKER_CONFIG=${RUNNER_TEMP}/docker-config-tunnel-lifecycle" >> "$GITHUB_ENV"', ); requireRunDoesNotContain(errors, configureDockerAuth, "${{ runner.temp }}"); - requireRunDoesNotContain(errors, configureDockerAuth, "${{ github.workspace }}"); + requireRunDoesNotContain( + errors, + configureDockerAuth, + "${{ github.workspace }}", + ); - const dockerLogin = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerLogin = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerLoginEnv = asRecord(dockerLogin?.env); - if (dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { + if ( + dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}" + ) { errors.push( "tunnel-lifecycle-vitest Docker Hub auth must receive DOCKERHUB_USERNAME from secrets", ); @@ -3660,7 +5319,11 @@ function validateTunnelLifecycleVitestJob(errors: string[], jobs: WorkflowRecord steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); @@ -3685,13 +5348,25 @@ function validateTunnelLifecycleVitestJob(errors: string[], jobs: WorkflowRecord "NVIDIA_API_KEY", ); requireRunContains(errors, cloudflaredPrereq, "cloudflared --version"); - requireRunContains(errors, cloudflaredPrereq, "test/e2e/lib/cloudflared-version-resolver.sh"); + requireRunContains( + errors, + cloudflaredPrereq, + "test/e2e/lib/cloudflared-version-resolver.sh", + ); requireRunContains(errors, cloudflaredPrereq, "sudo apt-get install -y"); requireRunContains(errors, cloudflaredPrereq, "cloudflared=${cf_version}"); - const runVitest = requireJobStep(errors, jobName, steps, "Run tunnel lifecycle live test"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run tunnel lifecycle live test", + ); const runVitestEnv = asRecord(runVitest?.env); - if (runVitestEnv.NVIDIA_INFERENCE_API_KEY !== "${{ secrets.NVIDIA_INFERENCE_API_KEY }}") { + if ( + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { errors.push( "tunnel-lifecycle-vitest Vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", ); @@ -3701,28 +5376,60 @@ function validateTunnelLifecycleVitestJob(errors: string[], jobs: WorkflowRecord "tunnel-lifecycle-vitest Vitest step must not run cloudflared APT installation with NVIDIA_INFERENCE_API_KEY in scope", ); } - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/tunnel-lifecycle.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/tunnel-lifecycle.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload tunnel lifecycle artifacts"); - requireFullShaAction(errors, upload, "tunnel-lifecycle-vitest upload-artifact"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload tunnel lifecycle artifacts", + ); + requireFullShaAction( + errors, + upload, + "tunnel-lifecycle-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-tunnel-lifecycle") { errors.push("tunnel-lifecycle-vitest artifact upload name must be stable"); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/tunnel-lifecycle/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/tunnel-lifecycle/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("tunnel-lifecycle-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "tunnel-lifecycle-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("tunnel-lifecycle-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "tunnel-lifecycle-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("tunnel-lifecycle-vitest artifact upload retention-days must be 14"); + errors.push( + "tunnel-lifecycle-vitest artifact upload retention-days must be 14", + ); } - const cleanup = requireJobStep(errors, jobName, steps, "Clean up Docker auth"); + const cleanup = requireJobStep( + errors, + jobName, + steps, + "Clean up Docker auth", + ); if (cleanup?.if !== "always()") { errors.push("tunnel-lifecycle-vitest Docker auth cleanup must always run"); } @@ -3730,7 +5437,10 @@ function validateTunnelLifecycleVitestJob(errors: string[], jobs: WorkflowRecord requireRunContains(errors, cleanup, 'rm -rf "${DOCKER_CONFIG}"'); } -function validateIssue2478CrashLoopRecoveryVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateIssue2478CrashLoopRecoveryVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "issue-2478-crash-loop-recovery-vitest"; const scenarioName = "issue-2478-crash-loop-recovery"; const job = asRecord(jobs[jobName]); @@ -3740,10 +5450,14 @@ function validateIssue2478CrashLoopRecoveryVitestJob(errors: string[], jobs: Wor } if (job["runs-on"] !== "ubuntu-latest") { - errors.push("issue-2478-crash-loop-recovery-vitest job must run on ubuntu-latest"); + errors.push( + "issue-2478-crash-loop-recovery-vitest job must run on ubuntu-latest", + ); } if (job["timeout-minutes"] !== 30) { - errors.push("issue-2478-crash-loop-recovery-vitest job must keep the 30 minute timeout"); + errors.push( + "issue-2478-crash-loop-recovery-vitest job must keep the 30 minute timeout", + ); } validateFreeStandingJobSelector(errors, jobs, jobName, scenarioName); @@ -3756,7 +5470,8 @@ function validateIssue2478CrashLoopRecoveryVitestJob(errors: string[], jobs: Wor const expectedEnv: Record = { FREE_STANDING_VITEST_JOB: "1", FREE_STANDING_SCENARIO_ID: scenarioName, - E2E_ARTIFACT_DIR: "${{ github.workspace }}/e2e-artifacts/vitest/issue-2478-crash-loop-recovery", + E2E_ARTIFACT_DIR: + "${{ github.workspace }}/e2e-artifacts/vitest/issue-2478-crash-loop-recovery", NEMOCLAW_CLI_BIN: "${{ github.workspace }}/bin/nemoclaw.js", NEMOCLAW_RUN_E2E_SCENARIOS: "1", NEMOCLAW_NON_INTERACTIVE: "1", @@ -3766,10 +5481,15 @@ function validateIssue2478CrashLoopRecoveryVitestJob(errors: string[], jobs: Wor }; for (const [key, value] of Object.entries(expectedEnv)) { if (jobEnv[key] !== value) { - errors.push(`issue-2478-crash-loop-recovery-vitest job env ${key} must be ${value}`); + errors.push( + `issue-2478-crash-loop-recovery-vitest job env ${key} must be ${value}`, + ); } } - for (const secret of ["NVIDIA_INFERENCE_API_KEY", ...COMMON_SECRET_ENV_NAMES]) { + for (const secret of [ + "NVIDIA_INFERENCE_API_KEY", + ...COMMON_SECRET_ENV_NAMES, + ]) { requireEnvDoesNotExposeSecret( errors, "issue-2478-crash-loop-recovery-vitest job", @@ -3783,21 +5503,44 @@ function validateIssue2478CrashLoopRecoveryVitestJob(errors: string[], jobs: Wor for (const step of steps) { const stepName = `issue-2478-crash-loop-recovery-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_INFERENCE_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_INFERENCE_API_KEY", + ); requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_API_KEY"); if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) { - errors.push("issue-2478-crash-loop-recovery-vitest job missing checkout step"); + errors.push( + "issue-2478-crash-loop-recovery-vitest job missing checkout step", + ); } - requireFullShaAction(errors, checkout, "issue-2478-crash-loop-recovery-vitest checkout"); + requireFullShaAction( + errors, + checkout, + "issue-2478-crash-loop-recovery-vitest checkout", + ); if (asRecord(checkout?.with)["persist-credentials"] !== false) { errors.push( "issue-2478-crash-loop-recovery-vitest checkout step must set persist-credentials=false", @@ -3816,11 +5559,22 @@ function validateIssue2478CrashLoopRecoveryVitestJob(errors: string[], jobs: Wor 'echo "DOCKER_CONFIG=${RUNNER_TEMP}/docker-config-issue-2478-crash-loop-recovery" >> "$GITHUB_ENV"', ); requireRunDoesNotContain(errors, configureDockerAuth, "${{ runner.temp }}"); - requireRunDoesNotContain(errors, configureDockerAuth, "${{ github.workspace }}"); + requireRunDoesNotContain( + errors, + configureDockerAuth, + "${{ github.workspace }}", + ); - const dockerLogin = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerLogin = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerLoginEnv = asRecord(dockerLogin?.env); - if (dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { + if ( + dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}" + ) { errors.push( "issue-2478-crash-loop-recovery-vitest Docker Hub auth must receive DOCKERHUB_USERNAME from secrets", ); @@ -3838,9 +5592,15 @@ function validateIssue2478CrashLoopRecoveryVitestJob(errors: string[], jobs: Wor const setupNode = namedStep(steps, "Set up Node"); if (!setupNode) { - errors.push("issue-2478-crash-loop-recovery-vitest job missing step: Set up Node"); + errors.push( + "issue-2478-crash-loop-recovery-vitest job missing step: Set up Node", + ); } - requireFullShaAction(errors, setupNode, "issue-2478-crash-loop-recovery-vitest setup-node"); + requireFullShaAction( + errors, + setupNode, + "issue-2478-crash-loop-recovery-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -3848,13 +5608,26 @@ function validateIssue2478CrashLoopRecoveryVitestJob(errors: string[], jobs: Wor steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const installOpenShell = requireJobStep(errors, jobName, steps, "Install OpenShell CLI"); - requireRunContains(errors, installOpenShell, "bash scripts/install-openshell.sh"); + const installOpenShell = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell CLI", + ); + requireRunContains( + errors, + installOpenShell, + "bash scripts/install-openshell.sh", + ); const runVitest = requireJobStep( errors, @@ -3869,7 +5642,11 @@ function validateIssue2478CrashLoopRecoveryVitestJob(errors: string[], jobs: Wor runVitestEnv, "NVIDIA_INFERENCE_API_KEY", ); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); requireRunContains( errors, runVitest, @@ -3882,10 +5659,18 @@ function validateIssue2478CrashLoopRecoveryVitestJob(errors: string[], jobs: Wor steps, "Upload issue #2478 crash-loop recovery artifacts", ); - requireFullShaAction(errors, upload, "issue-2478-crash-loop-recovery-vitest upload-artifact"); + requireFullShaAction( + errors, + upload, + "issue-2478-crash-loop-recovery-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); - if (uploadWith.name !== "e2e-vitest-scenarios-issue-2478-crash-loop-recovery") { - errors.push("issue-2478-crash-loop-recovery-vitest artifact upload name must be stable"); + if ( + uploadWith.name !== "e2e-vitest-scenarios-issue-2478-crash-loop-recovery" + ) { + errors.push( + "issue-2478-crash-loop-recovery-vitest artifact upload name must be stable", + ); } const uploadPath = stringValue(uploadWith.path); requireUploadPathContains( @@ -3904,18 +5689,30 @@ function validateIssue2478CrashLoopRecoveryVitestJob(errors: string[], jobs: Wor ); } if (uploadWith["retention-days"] !== 14) { - errors.push("issue-2478-crash-loop-recovery-vitest artifact upload retention-days must be 14"); + errors.push( + "issue-2478-crash-loop-recovery-vitest artifact upload retention-days must be 14", + ); } - const cleanup = requireJobStep(errors, jobName, steps, "Clean up Docker auth"); + const cleanup = requireJobStep( + errors, + jobName, + steps, + "Clean up Docker auth", + ); if (cleanup?.if !== "always()") { - errors.push("issue-2478-crash-loop-recovery-vitest Docker auth cleanup must always run"); + errors.push( + "issue-2478-crash-loop-recovery-vitest Docker auth cleanup must always run", + ); } requireRunContains(errors, cleanup, "docker logout docker.io"); requireRunContains(errors, cleanup, 'rm -rf "${DOCKER_CONFIG}"'); } -function validateChannelsAddRemoveVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateChannelsAddRemoveVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "channels-add-remove-vitest"; const scenarioName = "channels-add-remove"; const job = asRecord(jobs[jobName]); @@ -3929,21 +5726,28 @@ function validateChannelsAddRemoveVitestJob(errors: string[], jobs: WorkflowReco } validateFreeStandingJobSelector(errors, jobs, jobName, scenarioName); if (job["timeout-minutes"] !== 75) { - errors.push("channels-add-remove-vitest job must keep the legacy 75 minute timeout"); + errors.push( + "channels-add-remove-vitest job must keep the legacy 75 minute timeout", + ); } const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("channels-add-remove-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "channels-add-remove-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if ( - jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/channels-add-remove" + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/channels-add-remove" ) { errors.push( "channels-add-remove-vitest job must write artifacts under e2e-artifacts/vitest/channels-add-remove", ); } if (jobEnv.NEMOCLAW_CLI_BIN !== "${{ github.workspace }}/bin/nemoclaw.js") { - errors.push("channels-add-remove-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "channels-add-remove-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } if (jobEnv.NEMOCLAW_SANDBOX_NAME !== "e2e-channels-add-remove") { errors.push( @@ -3951,13 +5755,19 @@ function validateChannelsAddRemoveVitestJob(errors: string[], jobs: WorkflowReco ); } if (jobEnv.NEMOCLAW_NON_INTERACTIVE !== "1") { - errors.push("channels-add-remove-vitest job must set NEMOCLAW_NON_INTERACTIVE=1"); + errors.push( + "channels-add-remove-vitest job must set NEMOCLAW_NON_INTERACTIVE=1", + ); } if (jobEnv.NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE !== "1") { - errors.push("channels-add-remove-vitest job must set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1"); + errors.push( + "channels-add-remove-vitest job must set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1", + ); } if (jobEnv.OPENSHELL_GATEWAY !== "nemoclaw") { - errors.push("channels-add-remove-vitest job must force OPENSHELL_GATEWAY=nemoclaw"); + errors.push( + "channels-add-remove-vitest job must force OPENSHELL_GATEWAY=nemoclaw", + ); } for (const secret of [ "NVIDIA_API_KEY", @@ -3965,7 +5775,12 @@ function validateChannelsAddRemoveVitestJob(errors: string[], jobs: WorkflowReco "DOCKERHUB_TOKEN", "GITHUB_TOKEN", ]) { - requireEnvDoesNotExposeSecret(errors, "channels-add-remove-vitest job", jobEnv, secret); + requireEnvDoesNotExposeSecret( + errors, + "channels-add-remove-vitest job", + jobEnv, + secret, + ); } const steps = asSteps(job.steps); @@ -3974,24 +5789,49 @@ function validateChannelsAddRemoveVitestJob(errors: string[], jobs: WorkflowReco const stepName = `channels-add-remove-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); if (step.name !== "Run channels add/remove live test") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_API_KEY", + ); } if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("channels-add-remove-vitest job missing checkout step"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push("channels-add-remove-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "channels-add-remove-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("channels-add-remove-vitest checkout step must set persist-credentials=false"); + errors.push( + "channels-add-remove-vitest checkout step must set persist-credentials=false", + ); } - const dockerHubAuth = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerHubAuth = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerHubEnv = asRecord(dockerHubAuth?.env); if (dockerHubEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { errors.push( @@ -4007,8 +5847,13 @@ function validateChannelsAddRemoveVitestJob(errors: string[], jobs: WorkflowReco requireRunContains(errors, dockerHubAuth, "continuing with anonymous pulls"); const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("channels-add-remove-vitest job missing step: Set up Node"); - requireFullShaAction(errors, setupNode, "channels-add-remove-vitest setup-node"); + if (!setupNode) + errors.push("channels-add-remove-vitest job missing step: Set up Node"); + requireFullShaAction( + errors, + setupNode, + "channels-add-remove-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -4016,57 +5861,118 @@ function validateChannelsAddRemoveVitestJob(errors: string[], jobs: WorkflowReco steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const installOpenShell = requireJobStep(errors, jobName, steps, "Install OpenShell"); - requireRunContains(errors, installOpenShell, "bash scripts/install-openshell.sh"); + const installOpenShell = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell", + ); + requireRunContains( + errors, + installOpenShell, + "bash scripts/install-openshell.sh", + ); requireRunContains(errors, installOpenShell, "env -u DOCKER_CONFIG"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_USERNAME"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_TOKEN"); requireRunContains(errors, installOpenShell, "-u NVIDIA_API_KEY"); requireRunContains(errors, installOpenShell, "-u GITHUB_TOKEN"); - const runVitest = requireJobStep(errors, jobName, steps, "Run channels add/remove live test"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run channels add/remove live test", + ); const runVitestEnv = asRecord(runVitest?.env); if (runVitestEnv.NVIDIA_API_KEY !== "${{ secrets.NVIDIA_API_KEY }}") { - errors.push("channels-add-remove-vitest step must receive NVIDIA_API_KEY from secrets"); + errors.push( + "channels-add-remove-vitest step must receive NVIDIA_API_KEY from secrets", + ); } - if (runVitestEnv.TELEGRAM_BOT_TOKEN !== "test-fake-telegram-token-add-remove-e2e") { - errors.push("channels-add-remove-vitest step must set the fake Telegram token"); + if ( + runVitestEnv.TELEGRAM_BOT_TOKEN !== + "test-fake-telegram-token-add-remove-e2e" + ) { + errors.push( + "channels-add-remove-vitest step must set the fake Telegram token", + ); } if (runVitestEnv.TELEGRAM_ALLOWED_IDS !== "123456789") { - errors.push("channels-add-remove-vitest step must set TELEGRAM_ALLOWED_IDS"); + errors.push( + "channels-add-remove-vitest step must set TELEGRAM_ALLOWED_IDS", + ); } if (runVitestEnv.TELEGRAM_REQUIRE_MENTION !== "0") { - errors.push("channels-add-remove-vitest step must set TELEGRAM_REQUIRE_MENTION"); + errors.push( + "channels-add-remove-vitest step must set TELEGRAM_REQUIRE_MENTION", + ); } requireRunContains(errors, runVitest, "OPENSHELL_BIN"); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/channels-add-remove.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/channels-add-remove.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload channels add/remove artifacts"); - requireFullShaAction(errors, upload, "channels-add-remove-vitest upload-artifact"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload channels add/remove artifacts", + ); + requireFullShaAction( + errors, + upload, + "channels-add-remove-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-channels-add-remove") { - errors.push("channels-add-remove-vitest artifact upload name must be stable"); + errors.push( + "channels-add-remove-vitest artifact upload name must be stable", + ); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/channels-add-remove/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/channels-add-remove/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("channels-add-remove-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "channels-add-remove-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("channels-add-remove-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "channels-add-remove-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("channels-add-remove-vitest artifact upload retention-days must be 14"); + errors.push( + "channels-add-remove-vitest artifact upload retention-days must be 14", + ); } } -function validateOpenClawDiscordPairingVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateOpenClawDiscordPairingVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "openclaw-discord-pairing-vitest"; const scenarioName = "openclaw-discord-pairing"; const job = asRecord(jobs[jobName]); @@ -4076,19 +5982,34 @@ function validateOpenClawDiscordPairingVitestJob(errors: string[], jobs: Workflo } if (job["runs-on"] !== "ubuntu-latest") { - errors.push("openclaw-discord-pairing-vitest job must run on ubuntu-latest"); + errors.push( + "openclaw-discord-pairing-vitest job must run on ubuntu-latest", + ); } if (job["timeout-minutes"] !== 60) { - errors.push("openclaw-discord-pairing-vitest job must keep the 60 minute timeout"); + errors.push( + "openclaw-discord-pairing-vitest job must keep the 60 minute timeout", + ); } validateFreeStandingJobSelector(errors, jobs, jobName, scenarioName); const jobEnv = asRecord(job.env); if ("DOCKER_CONFIG" in jobEnv) { - errors.push("openclaw-discord-pairing-vitest job must not set DOCKER_CONFIG at job level"); + errors.push( + "openclaw-discord-pairing-vitest job must not set DOCKER_CONFIG at job level", + ); } - for (const secret of ["NVIDIA_INFERENCE_API_KEY", "NVIDIA_API_KEY", ...COMMON_SECRET_ENV_NAMES]) { - requireEnvDoesNotExposeSecret(errors, "openclaw-discord-pairing-vitest job", jobEnv, secret); + for (const secret of [ + "NVIDIA_INFERENCE_API_KEY", + "NVIDIA_API_KEY", + ...COMMON_SECRET_ENV_NAMES, + ]) { + requireEnvDoesNotExposeSecret( + errors, + "openclaw-discord-pairing-vitest job", + jobEnv, + secret, + ); } const steps = asSteps(job.steps); @@ -4097,27 +6018,58 @@ function validateOpenClawDiscordPairingVitestJob(errors: string[], jobs: Workflo const stepName = `openclaw-discord-pairing-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); if (step.name !== "Run OpenClaw Discord pairing live test") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_INFERENCE_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_INFERENCE_API_KEY", + ); } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_API_KEY"); if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("openclaw-discord-pairing-vitest job missing checkout step"); - requireFullShaAction(errors, checkout, "openclaw-discord-pairing-vitest checkout"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push("openclaw-discord-pairing-vitest job missing checkout step"); + requireFullShaAction( + errors, + checkout, + "openclaw-discord-pairing-vitest checkout", + ); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("openclaw-discord-pairing-vitest checkout step must set persist-credentials=false"); + errors.push( + "openclaw-discord-pairing-vitest checkout step must set persist-credentials=false", + ); } const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("openclaw-discord-pairing-vitest job missing step: Set up Node"); - requireFullShaAction(errors, setupNode, "openclaw-discord-pairing-vitest setup-node"); + if (!setupNode) + errors.push( + "openclaw-discord-pairing-vitest job missing step: Set up Node", + ); + requireFullShaAction( + errors, + setupNode, + "openclaw-discord-pairing-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -4125,7 +6077,11 @@ function validateOpenClawDiscordPairingVitestJob(errors: string[], jobs: Workflo steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); @@ -4142,11 +6098,22 @@ function validateOpenClawDiscordPairingVitestJob(errors: string[], jobs: Workflo 'echo "DOCKER_CONFIG=${RUNNER_TEMP}/docker-config-openclaw-discord-pairing" >> "$GITHUB_ENV"', ); requireRunDoesNotContain(errors, configureDockerAuth, "${{ runner.temp }}"); - requireRunDoesNotContain(errors, configureDockerAuth, "${{ github.workspace }}"); + requireRunDoesNotContain( + errors, + configureDockerAuth, + "${{ github.workspace }}", + ); - const dockerLogin = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerLogin = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerLoginEnv = asRecord(dockerLogin?.env); - if (dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { + if ( + dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}" + ) { errors.push( "openclaw-discord-pairing-vitest Docker Hub auth must receive DOCKERHUB_USERNAME from secrets", ); @@ -4161,8 +6128,17 @@ function validateOpenClawDiscordPairingVitestJob(errors: string[], jobs: Workflo requireRunContains(errors, dockerLogin, "docker login docker.io"); requireRunContains(errors, dockerLogin, "--password-stdin"); - const installOpenShell = requireJobStep(errors, jobName, steps, "Install OpenShell CLI"); - requireRunContains(errors, installOpenShell, "bash scripts/install-openshell.sh"); + const installOpenShell = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell CLI", + ); + requireRunContains( + errors, + installOpenShell, + "bash scripts/install-openshell.sh", + ); requireRunContains(errors, installOpenShell, "env -u DOCKER_CONFIG"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_USERNAME"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_TOKEN"); @@ -4177,16 +6153,29 @@ function validateOpenClawDiscordPairingVitestJob(errors: string[], jobs: Workflo "Run OpenClaw Discord pairing live test", ); const runVitestEnv = asRecord(runVitest?.env); - if (runVitestEnv.NVIDIA_INFERENCE_API_KEY !== "${{ secrets.NVIDIA_INFERENCE_API_KEY }}") { + if ( + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { errors.push( "openclaw-discord-pairing-vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", ); } if (runVitestEnv.DISCORD_BOT_TOKEN !== "test-fake-discord-pairing-e2e") { - errors.push("openclaw-discord-pairing-vitest step must use fake Discord token"); + errors.push( + "openclaw-discord-pairing-vitest step must use fake Discord token", + ); } - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/openclaw-discord-pairing.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/openclaw-discord-pairing.test.ts", + ); const upload = requireJobStep( errors, @@ -4194,10 +6183,18 @@ function validateOpenClawDiscordPairingVitestJob(errors: string[], jobs: Workflo steps, "Upload OpenClaw Discord pairing artifacts", ); - requireFullShaAction(errors, upload, "openclaw-discord-pairing-vitest upload-artifact"); + requireFullShaAction( + errors, + upload, + "openclaw-discord-pairing-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/openclaw-discord-pairing/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/openclaw-discord-pairing/", + ); if (uploadWith["include-hidden-files"] !== false) { errors.push( "openclaw-discord-pairing-vitest artifact upload must set include-hidden-files: false", @@ -4209,18 +6206,30 @@ function validateOpenClawDiscordPairingVitestJob(errors: string[], jobs: Workflo ); } if (uploadWith["retention-days"] !== 14) { - errors.push("openclaw-discord-pairing-vitest artifact upload retention-days must be 14"); + errors.push( + "openclaw-discord-pairing-vitest artifact upload retention-days must be 14", + ); } - const cleanup = requireJobStep(errors, jobName, steps, "Clean up Docker auth"); + const cleanup = requireJobStep( + errors, + jobName, + steps, + "Clean up Docker auth", + ); if (cleanup?.if !== "always()") { - errors.push("openclaw-discord-pairing-vitest Docker auth cleanup must always run"); + errors.push( + "openclaw-discord-pairing-vitest Docker auth cleanup must always run", + ); } requireRunContains(errors, cleanup, "docker logout docker.io"); requireRunContains(errors, cleanup, 'rm -rf "${DOCKER_CONFIG}"'); } -function validateOpenClawSlackPairingVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateOpenClawSlackPairingVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "openclaw-slack-pairing-vitest"; const scenarioName = "openclaw-slack-pairing"; const job = asRecord(jobs[jobName]); @@ -4233,16 +6242,29 @@ function validateOpenClawSlackPairingVitestJob(errors: string[], jobs: WorkflowR errors.push("openclaw-slack-pairing-vitest job must run on ubuntu-latest"); } if (job["timeout-minutes"] !== 60) { - errors.push("openclaw-slack-pairing-vitest job must keep the 60 minute timeout"); + errors.push( + "openclaw-slack-pairing-vitest job must keep the 60 minute timeout", + ); } validateFreeStandingJobSelector(errors, jobs, jobName, scenarioName); const jobEnv = asRecord(job.env); if ("DOCKER_CONFIG" in jobEnv) { - errors.push("openclaw-slack-pairing-vitest job must not set DOCKER_CONFIG at job level"); + errors.push( + "openclaw-slack-pairing-vitest job must not set DOCKER_CONFIG at job level", + ); } - for (const secret of ["NVIDIA_INFERENCE_API_KEY", "NVIDIA_API_KEY", ...COMMON_SECRET_ENV_NAMES]) { - requireEnvDoesNotExposeSecret(errors, "openclaw-slack-pairing-vitest job", jobEnv, secret); + for (const secret of [ + "NVIDIA_INFERENCE_API_KEY", + "NVIDIA_API_KEY", + ...COMMON_SECRET_ENV_NAMES, + ]) { + requireEnvDoesNotExposeSecret( + errors, + "openclaw-slack-pairing-vitest job", + jobEnv, + secret, + ); } const steps = asSteps(job.steps); @@ -4251,27 +6273,56 @@ function validateOpenClawSlackPairingVitestJob(errors: string[], jobs: WorkflowR const stepName = `openclaw-slack-pairing-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); if (step.name !== "Run OpenClaw Slack pairing live test") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_INFERENCE_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_INFERENCE_API_KEY", + ); } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_API_KEY"); if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("openclaw-slack-pairing-vitest job missing checkout step"); - requireFullShaAction(errors, checkout, "openclaw-slack-pairing-vitest checkout"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push("openclaw-slack-pairing-vitest job missing checkout step"); + requireFullShaAction( + errors, + checkout, + "openclaw-slack-pairing-vitest checkout", + ); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("openclaw-slack-pairing-vitest checkout step must set persist-credentials=false"); + errors.push( + "openclaw-slack-pairing-vitest checkout step must set persist-credentials=false", + ); } const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("openclaw-slack-pairing-vitest job missing step: Set up Node"); - requireFullShaAction(errors, setupNode, "openclaw-slack-pairing-vitest setup-node"); + if (!setupNode) + errors.push("openclaw-slack-pairing-vitest job missing step: Set up Node"); + requireFullShaAction( + errors, + setupNode, + "openclaw-slack-pairing-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -4279,7 +6330,11 @@ function validateOpenClawSlackPairingVitestJob(errors: string[], jobs: WorkflowR steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); @@ -4296,11 +6351,22 @@ function validateOpenClawSlackPairingVitestJob(errors: string[], jobs: WorkflowR 'echo "DOCKER_CONFIG=${RUNNER_TEMP}/docker-config-openclaw-slack-pairing" >> "$GITHUB_ENV"', ); requireRunDoesNotContain(errors, configureDockerAuth, "${{ runner.temp }}"); - requireRunDoesNotContain(errors, configureDockerAuth, "${{ github.workspace }}"); + requireRunDoesNotContain( + errors, + configureDockerAuth, + "${{ github.workspace }}", + ); - const dockerLogin = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerLogin = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerLoginEnv = asRecord(dockerLogin?.env); - if (dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { + if ( + dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}" + ) { errors.push( "openclaw-slack-pairing-vitest Docker Hub auth must receive DOCKERHUB_USERNAME from secrets", ); @@ -4315,8 +6381,17 @@ function validateOpenClawSlackPairingVitestJob(errors: string[], jobs: WorkflowR requireRunContains(errors, dockerLogin, "docker login docker.io"); requireRunContains(errors, dockerLogin, "--password-stdin"); - const installOpenShell = requireJobStep(errors, jobName, steps, "Install OpenShell CLI"); - requireRunContains(errors, installOpenShell, "bash scripts/install-openshell.sh"); + const installOpenShell = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell CLI", + ); + requireRunContains( + errors, + installOpenShell, + "bash scripts/install-openshell.sh", + ); requireRunContains(errors, installOpenShell, "env -u DOCKER_CONFIG"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_USERNAME"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_TOKEN"); @@ -4324,27 +6399,60 @@ function validateOpenClawSlackPairingVitestJob(errors: string[], jobs: WorkflowR requireRunContains(errors, installOpenShell, "-u NVIDIA_INFERENCE_API_KEY"); requireRunContains(errors, installOpenShell, "-u GITHUB_TOKEN"); - const runVitest = requireJobStep(errors, jobName, steps, "Run OpenClaw Slack pairing live test"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run OpenClaw Slack pairing live test", + ); const runVitestEnv = asRecord(runVitest?.env); - if (runVitestEnv.NVIDIA_INFERENCE_API_KEY !== "${{ secrets.NVIDIA_INFERENCE_API_KEY }}") { + if ( + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { errors.push( "openclaw-slack-pairing-vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", ); } if (runVitestEnv.SLACK_BOT_TOKEN !== "xoxb-fake-slack-pairing-e2e") { - errors.push("openclaw-slack-pairing-vitest step must use fake Slack bot token"); + errors.push( + "openclaw-slack-pairing-vitest step must use fake Slack bot token", + ); } if (runVitestEnv.SLACK_APP_TOKEN !== "xapp-fake-slack-pairing-e2e") { - errors.push("openclaw-slack-pairing-vitest step must use fake Slack app token"); + errors.push( + "openclaw-slack-pairing-vitest step must use fake Slack app token", + ); } - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/openclaw-slack-pairing.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/openclaw-slack-pairing.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload OpenClaw Slack pairing artifacts"); - requireFullShaAction(errors, upload, "openclaw-slack-pairing-vitest upload-artifact"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload OpenClaw Slack pairing artifacts", + ); + requireFullShaAction( + errors, + upload, + "openclaw-slack-pairing-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/openclaw-slack-pairing/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/openclaw-slack-pairing/", + ); if (uploadWith["include-hidden-files"] !== false) { errors.push( "openclaw-slack-pairing-vitest artifact upload must set include-hidden-files: false", @@ -4356,18 +6464,30 @@ function validateOpenClawSlackPairingVitestJob(errors: string[], jobs: WorkflowR ); } if (uploadWith["retention-days"] !== 14) { - errors.push("openclaw-slack-pairing-vitest artifact upload retention-days must be 14"); + errors.push( + "openclaw-slack-pairing-vitest artifact upload retention-days must be 14", + ); } - const cleanup = requireJobStep(errors, jobName, steps, "Clean up Docker auth"); + const cleanup = requireJobStep( + errors, + jobName, + steps, + "Clean up Docker auth", + ); if (cleanup?.if !== "always()") { - errors.push("openclaw-slack-pairing-vitest Docker auth cleanup must always run"); + errors.push( + "openclaw-slack-pairing-vitest Docker auth cleanup must always run", + ); } requireRunContains(errors, cleanup, "docker logout docker.io"); requireRunContains(errors, cleanup, 'rm -rf "${DOCKER_CONFIG}"'); } -function validateChannelsStopStartVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateChannelsStopStartVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "channels-stop-start-vitest"; const scenarioName = "channels-stop-start"; const job = asRecord(jobs[jobName]); @@ -4381,20 +6501,29 @@ function validateChannelsStopStartVitestJob(errors: string[], jobs: WorkflowReco } validateFreeStandingJobSelector(errors, jobs, jobName, scenarioName); if (job["timeout-minutes"] !== 90) { - errors.push("channels-stop-start-vitest job must keep the 90 minute timeout"); + errors.push( + "channels-stop-start-vitest job must keep the 90 minute timeout", + ); } const strategy = asRecord(job.strategy); if (strategy["fail-fast"] !== false) { errors.push("channels-stop-start-vitest strategy.fail-fast must be false"); } const matrix = asRecord(strategy.matrix); - if (!Array.isArray(matrix.agent) || matrix.agent.join(",") !== "openclaw,hermes") { - errors.push("channels-stop-start-vitest matrix.agent must be openclaw,hermes"); + if ( + !Array.isArray(matrix.agent) || + matrix.agent.join(",") !== "openclaw,hermes" + ) { + errors.push( + "channels-stop-start-vitest matrix.agent must be openclaw,hermes", + ); } const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("channels-stop-start-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "channels-stop-start-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if ( jobEnv.E2E_ARTIFACT_DIR !== @@ -4405,15 +6534,22 @@ function validateChannelsStopStartVitestJob(errors: string[], jobs: WorkflowReco ); } if (jobEnv.NEMOCLAW_CLI_BIN !== "${{ github.workspace }}/bin/nemoclaw.js") { - errors.push("channels-stop-start-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "channels-stop-start-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } - if (jobEnv.NEMOCLAW_SANDBOX_NAME !== "e2e-channels-stop-start-${{ matrix.agent }}") { + if ( + jobEnv.NEMOCLAW_SANDBOX_NAME !== + "e2e-channels-stop-start-${{ matrix.agent }}" + ) { errors.push( "channels-stop-start-vitest job must derive NEMOCLAW_SANDBOX_NAME from matrix.agent with the e2e-channels-stop-start- prefix", ); } if (jobEnv.NEMOCLAW_AGENT !== "${{ matrix.agent }}") { - errors.push("channels-stop-start-vitest job must pass matrix.agent through NEMOCLAW_AGENT"); + errors.push( + "channels-stop-start-vitest job must pass matrix.agent through NEMOCLAW_AGENT", + ); } if (jobEnv.NEMOCLAW_CHANNELS_STOP_START_AGENT !== "${{ matrix.agent }}") { errors.push( @@ -4421,19 +6557,27 @@ function validateChannelsStopStartVitestJob(errors: string[], jobs: WorkflowReco ); } if (jobEnv.NEMOCLAW_NON_INTERACTIVE !== "1") { - errors.push("channels-stop-start-vitest job must set NEMOCLAW_NON_INTERACTIVE=1"); + errors.push( + "channels-stop-start-vitest job must set NEMOCLAW_NON_INTERACTIVE=1", + ); } if (jobEnv.NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE !== "1") { - errors.push("channels-stop-start-vitest job must set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1"); + errors.push( + "channels-stop-start-vitest job must set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1", + ); } if (jobEnv.OPENSHELL_GATEWAY !== "nemoclaw") { - errors.push("channels-stop-start-vitest job must force OPENSHELL_GATEWAY=nemoclaw"); + errors.push( + "channels-stop-start-vitest job must force OPENSHELL_GATEWAY=nemoclaw", + ); } if ( jobEnv.DOCKER_CONFIG !== "${{ github.workspace }}/.docker-config-channels-stop-start-${{ matrix.agent }}" ) { - errors.push("channels-stop-start-vitest job must isolate Docker auth by matrix agent"); + errors.push( + "channels-stop-start-vitest job must isolate Docker auth by matrix agent", + ); } for (const secret of [ "NVIDIA_INFERENCE_API_KEY", @@ -4442,7 +6586,12 @@ function validateChannelsStopStartVitestJob(errors: string[], jobs: WorkflowReco "DOCKERHUB_TOKEN", "GITHUB_TOKEN", ]) { - requireEnvDoesNotExposeSecret(errors, "channels-stop-start-vitest job", jobEnv, secret); + requireEnvDoesNotExposeSecret( + errors, + "channels-stop-start-vitest job", + jobEnv, + secret, + ); } const steps = asSteps(job.steps); @@ -4451,25 +6600,55 @@ function validateChannelsStopStartVitestJob(errors: string[], jobs: WorkflowReco const stepName = `channels-stop-start-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); if (step.name !== "Run channels stop/start live test") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_INFERENCE_API_KEY"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_INFERENCE_API_KEY", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_API_KEY", + ); } if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("channels-stop-start-vitest job missing checkout step"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push("channels-stop-start-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "channels-stop-start-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("channels-stop-start-vitest checkout step must set persist-credentials=false"); + errors.push( + "channels-stop-start-vitest checkout step must set persist-credentials=false", + ); } - const dockerHubAuth = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerHubAuth = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerHubEnv = asRecord(dockerHubAuth?.env); if (dockerHubEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { errors.push( @@ -4488,8 +6667,13 @@ function validateChannelsStopStartVitestJob(errors: string[], jobs: WorkflowReco requireRunContains(errors, dockerHubAuth, "continuing with anonymous pulls"); const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("channels-stop-start-vitest job missing step: Set up Node"); - requireFullShaAction(errors, setupNode, "channels-stop-start-vitest setup-node"); + if (!setupNode) + errors.push("channels-stop-start-vitest job missing step: Set up Node"); + requireFullShaAction( + errors, + setupNode, + "channels-stop-start-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -4497,20 +6681,38 @@ function validateChannelsStopStartVitestJob(errors: string[], jobs: WorkflowReco steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const installOpenShell = requireJobStep(errors, jobName, steps, "Install OpenShell"); - requireRunContains(errors, installOpenShell, "bash scripts/install-openshell.sh"); + const installOpenShell = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell", + ); + requireRunContains( + errors, + installOpenShell, + "bash scripts/install-openshell.sh", + ); requireRunContains(errors, installOpenShell, "env -u DOCKER_CONFIG"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_USERNAME"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_TOKEN"); requireRunContains(errors, installOpenShell, "-u NVIDIA_API_KEY"); requireRunContains(errors, installOpenShell, "-u GITHUB_TOKEN"); - const runVitest = requireJobStep(errors, jobName, steps, "Run channels stop/start live test"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run channels stop/start live test", + ); const runVitestEnv = asRecord(runVitest?.env); requireEnvDoesNotExposeSecret( errors, @@ -4518,37 +6720,85 @@ function validateChannelsStopStartVitestJob(errors: string[], jobs: WorkflowReco runVitestEnv, "NVIDIA_API_KEY", ); - if (runVitestEnv.NVIDIA_INFERENCE_API_KEY !== "${{ secrets.NVIDIA_INFERENCE_API_KEY }}") { + if ( + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { errors.push( "channels-stop-start-vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", ); } if ( - runVitestEnv.TELEGRAM_BOT_TOKEN !== "test-fake-telegram-token-stop-start-${{ matrix.agent }}" + runVitestEnv.TELEGRAM_BOT_TOKEN !== + "test-fake-telegram-token-stop-start-${{ matrix.agent }}" ) { - errors.push("channels-stop-start-vitest step must set the fake Telegram token"); + errors.push( + "channels-stop-start-vitest step must set the fake Telegram token", + ); } - if (runVitestEnv.DISCORD_BOT_TOKEN !== "test-fake-discord-token-stop-start-${{ matrix.agent }}") { - errors.push("channels-stop-start-vitest step must set the fake Discord token"); + if ( + runVitestEnv.DISCORD_BOT_TOKEN !== + "test-fake-discord-token-stop-start-${{ matrix.agent }}" + ) { + errors.push( + "channels-stop-start-vitest step must set the fake Discord token", + ); } - if (runVitestEnv.SLACK_BOT_TOKEN !== "xoxb-fake-slack-token-stop-start-${{ matrix.agent }}") { - errors.push("channels-stop-start-vitest step must set the fake Slack bot token"); + if ( + runVitestEnv.SLACK_BOT_TOKEN !== + "xoxb-fake-slack-token-stop-start-${{ matrix.agent }}" + ) { + errors.push( + "channels-stop-start-vitest step must set the fake Slack bot token", + ); } - if (runVitestEnv.SLACK_APP_TOKEN !== "xapp-fake-slack-token-stop-start-${{ matrix.agent }}") { - errors.push("channels-stop-start-vitest step must set the fake Slack app token"); + if ( + runVitestEnv.SLACK_APP_TOKEN !== + "xapp-fake-slack-token-stop-start-${{ matrix.agent }}" + ) { + errors.push( + "channels-stop-start-vitest step must set the fake Slack app token", + ); } - if (runVitestEnv.WECHAT_BOT_TOKEN !== "test-fake-wechat-token-stop-start-${{ matrix.agent }}") { - errors.push("channels-stop-start-vitest step must set the fake WeChat token"); + if ( + runVitestEnv.WECHAT_BOT_TOKEN !== + "test-fake-wechat-token-stop-start-${{ matrix.agent }}" + ) { + errors.push( + "channels-stop-start-vitest step must set the fake WeChat token", + ); } requireRunContains(errors, runVitest, "OPENSHELL_BIN"); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/channels-stop-start.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/channels-stop-start.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload channels stop/start artifacts"); - requireFullShaAction(errors, upload, "channels-stop-start-vitest upload-artifact"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload channels stop/start artifacts", + ); + requireFullShaAction( + errors, + upload, + "channels-stop-start-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); - if (uploadWith.name !== "e2e-vitest-scenarios-channels-stop-start-${{ matrix.agent }}") { - errors.push("channels-stop-start-vitest artifact upload name must include matrix.agent"); + if ( + uploadWith.name !== + "e2e-vitest-scenarios-channels-stop-start-${{ matrix.agent }}" + ) { + errors.push( + "channels-stop-start-vitest artifact upload name must include matrix.agent", + ); } const uploadPath = stringValue(uploadWith.path); requireUploadPathContains( @@ -4557,24 +6807,40 @@ function validateChannelsStopStartVitestJob(errors: string[], jobs: WorkflowReco "e2e-artifacts/vitest/channels-stop-start/${{ matrix.agent }}/", ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("channels-stop-start-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "channels-stop-start-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("channels-stop-start-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "channels-stop-start-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("channels-stop-start-vitest artifact upload retention-days must be 14"); + errors.push( + "channels-stop-start-vitest artifact upload retention-days must be 14", + ); } - const cleanup = requireJobStep(errors, jobName, steps, "Clean up Docker auth"); + const cleanup = requireJobStep( + errors, + jobName, + steps, + "Clean up Docker auth", + ); if (cleanup?.if !== "always()") { - errors.push("channels-stop-start-vitest Docker auth cleanup must always run"); + errors.push( + "channels-stop-start-vitest Docker auth cleanup must always run", + ); } requireRunContains(errors, cleanup, "docker logout docker.io"); requireRunContains(errors, cleanup, 'rm -rf "${DOCKER_CONFIG}"'); } -function validateTelegramInjectionVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateTelegramInjectionVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "telegram-injection-vitest"; const scenarioName = "telegram-injection"; const job = asRecord(jobs[jobName]); @@ -4587,16 +6853,29 @@ function validateTelegramInjectionVitestJob(errors: string[], jobs: WorkflowReco errors.push("telegram-injection-vitest job must run on ubuntu-latest"); } if (job["timeout-minutes"] !== 45) { - errors.push("telegram-injection-vitest job must keep the 45 minute timeout"); + errors.push( + "telegram-injection-vitest job must keep the 45 minute timeout", + ); } validateFreeStandingJobSelector(errors, jobs, jobName, scenarioName); const jobEnv = asRecord(job.env); if ("DOCKER_CONFIG" in jobEnv) { - errors.push("telegram-injection-vitest job must not set DOCKER_CONFIG at job level"); + errors.push( + "telegram-injection-vitest job must not set DOCKER_CONFIG at job level", + ); } - for (const secret of ["NVIDIA_INFERENCE_API_KEY", "NVIDIA_API_KEY", ...COMMON_SECRET_ENV_NAMES]) { - requireEnvDoesNotExposeSecret(errors, "telegram-injection-vitest job", jobEnv, secret); + for (const secret of [ + "NVIDIA_INFERENCE_API_KEY", + "NVIDIA_API_KEY", + ...COMMON_SECRET_ENV_NAMES, + ]) { + requireEnvDoesNotExposeSecret( + errors, + "telegram-injection-vitest job", + jobEnv, + secret, + ); } const steps = asSteps(job.steps); @@ -4605,12 +6884,32 @@ function validateTelegramInjectionVitestJob(errors: string[], jobs: WorkflowReco const stepName = `telegram-injection-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); if (step.name !== "Run Telegram injection live test") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_INFERENCE_API_KEY"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_INFERENCE_API_KEY", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_API_KEY", + ); } if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); @@ -4628,11 +6927,22 @@ function validateTelegramInjectionVitestJob(errors: string[], jobs: WorkflowReco 'echo "DOCKER_CONFIG=${RUNNER_TEMP}/docker-config-telegram-injection" >> "$GITHUB_ENV"', ); requireRunDoesNotContain(errors, configureDockerAuth, "${{ runner.temp }}"); - requireRunDoesNotContain(errors, configureDockerAuth, "${{ github.workspace }}"); + requireRunDoesNotContain( + errors, + configureDockerAuth, + "${{ github.workspace }}", + ); - const dockerLogin = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerLogin = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerLoginEnv = asRecord(dockerLogin?.env); - if (dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { + if ( + dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}" + ) { errors.push( "telegram-injection-vitest Docker Hub auth must receive DOCKERHUB_USERNAME from secrets", ); @@ -4647,8 +6957,17 @@ function validateTelegramInjectionVitestJob(errors: string[], jobs: WorkflowReco requireRunContains(errors, dockerLogin, "docker login docker.io"); requireRunContains(errors, dockerLogin, "--password-stdin"); - const installOpenShell = requireJobStep(errors, jobName, steps, "Install OpenShell"); - requireRunContains(errors, installOpenShell, "bash scripts/install-openshell.sh"); + const installOpenShell = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell", + ); + requireRunContains( + errors, + installOpenShell, + "bash scripts/install-openshell.sh", + ); requireRunContains(errors, installOpenShell, "env -u DOCKER_CONFIG"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_USERNAME"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_TOKEN"); @@ -4656,33 +6975,71 @@ function validateTelegramInjectionVitestJob(errors: string[], jobs: WorkflowReco requireRunContains(errors, installOpenShell, "-u NVIDIA_INFERENCE_API_KEY"); requireRunContains(errors, installOpenShell, "-u GITHUB_TOKEN"); - const runVitest = requireJobStep(errors, jobName, steps, "Run Telegram injection live test"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run Telegram injection live test", + ); const runVitestEnv = asRecord(runVitest?.env); - if (runVitestEnv.NVIDIA_INFERENCE_API_KEY !== "${{ secrets.NVIDIA_INFERENCE_API_KEY }}") { + if ( + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { errors.push( "telegram-injection-vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", ); } - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/telegram-injection.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/telegram-injection.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload Telegram injection artifacts"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload Telegram injection artifacts", + ); const uploadWith = asRecord(upload?.with); const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/telegram-injection/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/telegram-injection/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("telegram-injection-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "telegram-injection-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("telegram-injection-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "telegram-injection-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("telegram-injection-vitest artifact upload retention-days must be 14"); + errors.push( + "telegram-injection-vitest artifact upload retention-days must be 14", + ); } - const cleanup = requireJobStep(errors, jobName, steps, "Clean up Docker auth"); + const cleanup = requireJobStep( + errors, + jobName, + steps, + "Clean up Docker auth", + ); if (cleanup?.if !== "always()") { - errors.push("telegram-injection-vitest Docker auth cleanup must always run"); + errors.push( + "telegram-injection-vitest Docker auth cleanup must always run", + ); } requireRunContains(errors, cleanup, "docker logout docker.io"); requireRunContains(errors, cleanup, 'rm -rf "${DOCKER_CONFIG}"'); @@ -4696,25 +7053,38 @@ function validateBedrockRuntimeCompatibleAnthropicVitestJob( const scenarioName = "bedrock-runtime-compatible-anthropic"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { - errors.push("workflow missing bedrock-runtime-compatible-anthropic-vitest job"); + errors.push( + "workflow missing bedrock-runtime-compatible-anthropic-vitest job", + ); return; } if (job["runs-on"] !== "ubuntu-latest") { - errors.push("bedrock-runtime-compatible-anthropic-vitest job must run on ubuntu-latest"); + errors.push( + "bedrock-runtime-compatible-anthropic-vitest job must run on ubuntu-latest", + ); } if (job["timeout-minutes"] !== 60) { - errors.push("bedrock-runtime-compatible-anthropic-vitest timeout-minutes must be 60"); + errors.push( + "bedrock-runtime-compatible-anthropic-vitest timeout-minutes must be 60", + ); } validateFreeStandingJobSelector(errors, jobs, jobName, scenarioName); const strategy = asRecord(job.strategy); if (strategy["fail-fast"] !== false) { - errors.push("bedrock-runtime-compatible-anthropic-vitest strategy.fail-fast must be false"); + errors.push( + "bedrock-runtime-compatible-anthropic-vitest strategy.fail-fast must be false", + ); } const matrix = asRecord(strategy.matrix); - if (!Array.isArray(matrix.agent) || matrix.agent.join(",") !== "openclaw,hermes") { - errors.push("bedrock-runtime-compatible-anthropic-vitest matrix.agent must be openclaw,hermes"); + if ( + !Array.isArray(matrix.agent) || + matrix.agent.join(",") !== "openclaw,hermes" + ) { + errors.push( + "bedrock-runtime-compatible-anthropic-vitest matrix.agent must be openclaw,hermes", + ); } const jobEnv = asRecord(job.env); @@ -4793,17 +7163,35 @@ function validateBedrockRuntimeCompatibleAnthropicVitestJob( requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_API_KEY"); requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) { - errors.push("bedrock-runtime-compatible-anthropic-vitest job missing checkout step"); + errors.push( + "bedrock-runtime-compatible-anthropic-vitest job missing checkout step", + ); } - requireFullShaAction(errors, checkout, "bedrock-runtime-compatible-anthropic-vitest checkout"); + requireFullShaAction( + errors, + checkout, + "bedrock-runtime-compatible-anthropic-vitest checkout", + ); if (asRecord(checkout?.with)["persist-credentials"] !== false) { errors.push( "bedrock-runtime-compatible-anthropic-vitest checkout step must set persist-credentials=false", @@ -4823,9 +7211,16 @@ function validateBedrockRuntimeCompatibleAnthropicVitestJob( ); requireRunDoesNotContain(errors, configureDockerAuth, "${{ runner.temp }}"); - const dockerLogin = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerLogin = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerLoginEnv = asRecord(dockerLogin?.env); - if (dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { + if ( + dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}" + ) { errors.push( "bedrock-runtime-compatible-anthropic-vitest Docker Hub auth must receive DOCKERHUB_USERNAME from secrets", ); @@ -4843,9 +7238,15 @@ function validateBedrockRuntimeCompatibleAnthropicVitestJob( const setupNode = namedStep(steps, "Set up Node"); if (!setupNode) { - errors.push("bedrock-runtime-compatible-anthropic-vitest job missing step: Set up Node"); + errors.push( + "bedrock-runtime-compatible-anthropic-vitest job missing step: Set up Node", + ); } - requireFullShaAction(errors, setupNode, "bedrock-runtime-compatible-anthropic-vitest setup-node"); + requireFullShaAction( + errors, + setupNode, + "bedrock-runtime-compatible-anthropic-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -4853,7 +7254,11 @@ function validateBedrockRuntimeCompatibleAnthropicVitestJob( steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); @@ -4864,7 +7269,11 @@ function validateBedrockRuntimeCompatibleAnthropicVitestJob( steps, "Run Bedrock Runtime compatible Anthropic live test", ); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); requireRunContains( errors, runVitest, @@ -4914,9 +7323,16 @@ function validateBedrockRuntimeCompatibleAnthropicVitestJob( ); } - const cleanup = requireJobStep(errors, jobName, steps, "Clean up Docker auth"); + const cleanup = requireJobStep( + errors, + jobName, + steps, + "Clean up Docker auth", + ); if (cleanup?.if !== "always()") { - errors.push("bedrock-runtime-compatible-anthropic-vitest Docker auth cleanup must always run"); + errors.push( + "bedrock-runtime-compatible-anthropic-vitest Docker auth cleanup must always run", + ); } requireRunContains(errors, cleanup, "docker logout docker.io"); requireRunContains(errors, cleanup, 'rm -rf "${DOCKER_CONFIG}"'); @@ -4940,7 +7356,8 @@ export function validateE2eVitestScenariosWorkflowBoundary( } const permissions = asRecord(workflow.permissions); - if (permissions.contents !== "read") errors.push("workflow permissions.contents must be read"); + if (permissions.contents !== "read") + errors.push("workflow permissions.contents must be read"); const jobs = asRecord(workflow.jobs); const { errors: inventoryErrors, inventory: freeStandingInventory } = @@ -4948,7 +7365,8 @@ export function validateE2eVitestScenariosWorkflowBoundary( errors.push(...inventoryErrors); validateFreeStandingInventoryBoundary(errors, jobs, freeStandingInventory); const generateMatrix = asRecord(jobs["generate-matrix"]); - if (Object.keys(generateMatrix).length === 0) errors.push("workflow missing generate-matrix job"); + if (Object.keys(generateMatrix).length === 0) + errors.push("workflow missing generate-matrix job"); if (generateMatrix["runs-on"] !== "ubuntu-latest") { errors.push("generate-matrix job must run on ubuntu-latest"); } @@ -4956,7 +7374,10 @@ export function validateE2eVitestScenariosWorkflowBoundary( if (generateOutputs.matrix !== "${{ steps.matrix.outputs.matrix }}") { errors.push("generate-matrix job must expose matrix output"); } - if (generateOutputs.hermes_selected !== "${{ steps.matrix.outputs.hermes_selected }}") { + if ( + generateOutputs.hermes_selected !== + "${{ steps.matrix.outputs.hermes_selected }}" + ) { errors.push("generate-matrix job must expose hermes_selected output"); } const generateSteps = asSteps(generateMatrix.steps); @@ -4964,21 +7385,31 @@ export function validateE2eVitestScenariosWorkflowBoundary( const generateCheckout = generateSteps.find((step) => stringValue(step.uses).startsWith("actions/checkout@"), ); - if (!generateCheckout) errors.push("generate-matrix job missing checkout step"); + if (!generateCheckout) + errors.push("generate-matrix job missing checkout step"); requireFullShaAction(errors, generateCheckout, "generate-matrix checkout"); if (asRecord(generateCheckout?.with)["persist-credentials"] !== false) { - errors.push("generate-matrix checkout step must set persist-credentials=false"); + errors.push( + "generate-matrix checkout step must set persist-credentials=false", + ); } const generateSetupNode = namedStep(generateSteps, "Set up Node"); - if (!generateSetupNode) errors.push("generate-matrix job missing step: Set up Node"); + if (!generateSetupNode) + errors.push("generate-matrix job missing step: Set up Node"); requireFullShaAction(errors, generateSetupNode, "generate-matrix setup-node"); - const generate = requireStep(errors, generateSteps, "Generate Vitest scenario matrix"); + const generate = requireStep( + errors, + generateSteps, + "Generate Vitest scenario matrix", + ); const generateEnv = asRecord(generate?.env); if (generateEnv.JOBS !== "${{ inputs.jobs }}") { errors.push("matrix generation step must pass jobs through JOBS env"); } if (generateEnv.SCENARIOS !== "${{ inputs.scenarios }}") { - errors.push("matrix generation step must pass scenarios through SCENARIOS env"); + errors.push( + "matrix generation step must pass scenarios through SCENARIOS env", + ); } requireRunContains(errors, generate, FREE_STANDING_WORKFLOW_INVENTORY_SCRIPT); requireRunContains( @@ -4991,18 +7422,42 @@ export function validateE2eVitestScenariosWorkflowBoundary( generate, "free_standing_scenarios_csv must match scenario mapping keys", ); - requireRunContains(errors, generate, "Free-standing scenario maps to unknown job"); - requireRunContains(errors, generate, "Use either scenarios or jobs, not both"); + requireRunContains( + errors, + generate, + "Free-standing scenario maps to unknown job", + ); + requireRunContains( + errors, + generate, + "Use either scenarios or jobs, not both", + ); requireRunContains(errors, generate, "Unknown free-standing Vitest job"); requireRunContains(errors, generate, 'matrix="[]"'); - requireRunContains(errors, generate, "npx tsx test/e2e-scenario/scenarios/run.ts"); + requireRunContains( + errors, + generate, + "npx tsx test/e2e-scenario/scenarios/run.ts", + ); requireRunContains(errors, generate, "--emit-live-matrix"); requireRunContains(errors, generate, "--scenarios"); requireRunContains(errors, generate, "^[A-Za-z0-9_-]+(,[A-Za-z0-9_-]+)*$"); - requireRunContains(errors, generate, "Invalid scenario input; use comma-separated scenario ids"); - requireRunContains(errors, generate, "Invalid jobs input; use comma-separated job ids"); + requireRunContains( + errors, + generate, + "Invalid scenario input; use comma-separated scenario ids", + ); + requireRunContains( + errors, + generate, + "Invalid jobs input; use comma-separated job ids", + ); requireRunDoesNotContain(errors, generate, "Invalid jobs input: ${JOBS}"); - requireRunDoesNotContain(errors, generate, "Invalid scenario input: ${SCENARIOS}"); + requireRunDoesNotContain( + errors, + generate, + "Invalid scenario input: ${SCENARIOS}", + ); requireRunDoesNotContain(errors, generate, "^[A-Za-z0-9._-]+"); requireRunContains(errors, generate, "hermes_selected=false"); requireRunContains(errors, generate, "hermes_selected=true"); @@ -5015,7 +7470,8 @@ export function validateE2eVitestScenariosWorkflowBoundary( requireRunContains(errors, generate, "| Scenario | Runner | Label |"); const liveScenarios = asRecord(jobs["live-scenarios"]); - if (Object.keys(liveScenarios).length === 0) errors.push("workflow missing live-scenarios job"); + if (Object.keys(liveScenarios).length === 0) + errors.push("workflow missing live-scenarios job"); if (liveScenarios["runs-on"] !== "${{ matrix.runner }}") { errors.push("live-scenarios job must run on the matrix runner"); } @@ -5023,17 +7479,24 @@ export function validateE2eVitestScenariosWorkflowBoundary( errors.push("live-scenarios job must depend on generate-matrix"); } if ( - liveScenarios.if !== "${{ inputs.jobs == '' && needs.generate-matrix.outputs.matrix != '[]' }}" + liveScenarios.if !== + "${{ inputs.jobs == '' && needs.generate-matrix.outputs.matrix != '[]' }}" ) { - errors.push("live-scenarios job must not run when a free-standing jobs selector is supplied"); + errors.push( + "live-scenarios job must not run when a free-standing jobs selector is supplied", + ); } const strategy = asRecord(liveScenarios.strategy); if (strategy["fail-fast"] !== false) { errors.push("live-scenarios strategy.fail-fast must be false"); } const matrix = asRecord(strategy.matrix); - if (matrix.include !== "${{ fromJSON(needs.generate-matrix.outputs.matrix) }}") { - errors.push("live-scenarios matrix.include must come from generate-matrix output"); + if ( + matrix.include !== "${{ fromJSON(needs.generate-matrix.outputs.matrix) }}" + ) { + errors.push( + "live-scenarios matrix.include must come from generate-matrix output", + ); } const jobEnv = asRecord(liveScenarios.env); @@ -5041,15 +7504,26 @@ export function validateE2eVitestScenariosWorkflowBoundary( errors.push("live-scenarios job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); } if (!stringValue(jobEnv.E2E_ARTIFACT_DIR).includes("e2e-artifacts/vitest")) { - errors.push("live-scenarios job must write artifacts under e2e-artifacts/vitest"); + errors.push( + "live-scenarios job must write artifacts under e2e-artifacts/vitest", + ); } if (stringValue(jobEnv.E2E_ARTIFACT_DIR).includes("${{ matrix.id }}")) { - errors.push("live-scenarios job E2E_ARTIFACT_DIR must be the Vitest artifact parent"); + errors.push( + "live-scenarios job E2E_ARTIFACT_DIR must be the Vitest artifact parent", + ); } if (!stringValue(jobEnv.NEMOCLAW_CLI_BIN).includes("bin/nemoclaw.js")) { - errors.push("live-scenarios job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "live-scenarios job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } - requireEnvDoesNotExposeSecret(errors, "live-scenarios job", jobEnv, "NVIDIA_INFERENCE_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + "live-scenarios job", + jobEnv, + "NVIDIA_INFERENCE_API_KEY", + ); const steps = asSteps(liveScenarios.steps); requireNoDispatchInputInterpolation(errors, steps); @@ -5064,7 +7538,9 @@ export function validateE2eVitestScenariosWorkflowBoundary( } } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) errors.push("live-scenarios job missing checkout step"); requireFullShaAction(errors, checkout, "checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { @@ -5083,11 +7559,24 @@ export function validateE2eVitestScenariosWorkflowBoundary( if (runVitestEnv.SCENARIO_ID !== "${{ matrix.id }}") { errors.push("Vitest step must pass matrix.id through SCENARIO_ID env"); } - if (runVitestEnv.NVIDIA_INFERENCE_API_KEY !== "${{ secrets.NVIDIA_INFERENCE_API_KEY }}") { - errors.push("Vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets"); + if ( + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { + errors.push( + "Vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", + ); } - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/registry-scenarios.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/registry-scenarios.test.ts", + ); requireRunContains(errors, runVitest, '"^${SCENARIO_ID}$"'); const summary = requireStep(errors, steps, "Summarize artifacts"); @@ -5096,7 +7585,9 @@ export function validateE2eVitestScenariosWorkflowBoundary( errors.push("summary step must pass matrix.id through SCENARIO_ID env"); } if (summaryEnv.SCENARIO_LABEL !== "${{ matrix.label }}") { - errors.push("summary step must pass matrix.label through SCENARIO_LABEL env"); + errors.push( + "summary step must pass matrix.label through SCENARIO_LABEL env", + ); } requireRunContains(errors, summary, "run-plan.json"); requireRunContains( @@ -5104,7 +7595,11 @@ export function validateE2eVitestScenariosWorkflowBoundary( summary, 'Path(os.environ["E2E_ARTIFACT_DIR"]) / os.environ["SCENARIO_ID"]', ); - requireRunContains(errors, summary, "| Scenario | Manifest | Expected state | Suites | Phases |"); + requireRunContains( + errors, + summary, + "| Scenario | Manifest | Expected state | Suites | Phases |", + ); requireRunContains(errors, summary, "SCENARIO_ID"); const upload = requireStep(errors, steps, "Upload Vitest E2E artifacts"); @@ -5144,12 +7639,26 @@ export function validateE2eVitestScenariosWorkflowBoundary( uploadPath, "e2e-artifacts/vitest/${{ matrix.id }}/state-validation.result.json", ); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/${{ matrix.id }}/actions/"); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/${{ matrix.id }}/logs/"); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/${{ matrix.id }}/shell/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/${{ matrix.id }}/actions/", + ); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/${{ matrix.id }}/logs/", + ); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/${{ matrix.id }}/shell/", + ); for (const line of uploadPath.split("\n")) { if (line.trim() === "e2e-artifacts/vitest/${{ matrix.id }}/") { - errors.push("artifact upload path must not list the whole matrix artifact directory"); + errors.push( + "artifact upload path must not list the whole matrix artifact directory", + ); } } if (uploadWith["include-hidden-files"] !== false) { @@ -5172,11 +7681,17 @@ export function validateE2eVitestScenariosWorkflowBoundary( "sessions-agents-cli-vitest", "sessions-agents-cli", ); - validateFreeStandingJobSelector(errors, jobs, "inference-routing-vitest", "inference-routing"); + validateFreeStandingJobSelector( + errors, + jobs, + "inference-routing-vitest", + "inference-routing", + ); validateCloudInferenceVitestJob(errors, jobs); validateRuntimeOverridesVitestJob(errors, jobs); validateDoubleOnboardVitestJob(errors, jobs); validateHermesE2EVitestJob(errors, jobs); + validateFreeStandingJobSelector(errors, jobs, "hermes-discord-vitest", "hermes-discord"); validateHermesRootEntrypointSmokeVitestJob(errors, jobs); validateHermesSandboxSecretBoundaryVitestJob(errors, jobs); validateNetworkPolicyVitestJob(errors, jobs); @@ -5253,9 +7768,15 @@ export function validateE2eVitestScenariosWorkflowBoundary( } else { const needs = Array.isArray(reportToPr.needs) ? reportToPr.needs : []; for (const required of ["generate-matrix", "live-scenarios"]) { - if (!needs.includes(required)) errors.push(`report-to-pr job must wait for ${required}`); + if (!needs.includes(required)) + errors.push(`report-to-pr job must wait for ${required}`); } - validateFreeStandingInventoryCoverage(errors, jobs, needs, freeStandingInventory); + validateFreeStandingInventoryCoverage( + errors, + jobs, + needs, + freeStandingInventory, + ); const reportSteps = asSteps(reportToPr.steps); const report = requireJobStep( errors, @@ -5268,12 +7789,18 @@ export function validateE2eVitestScenariosWorkflowBoundary( errors.push("report-to-pr step must pass jobs through JOBS env"); } if (reportEnv.JOB_PR_NUMBER !== "${{ inputs.pr_number }}") { - errors.push("report-to-pr step must pass pr_number through JOB_PR_NUMBER env"); + errors.push( + "report-to-pr step must pass pr_number through JOB_PR_NUMBER env", + ); } if (reportEnv.JOB_SCENARIOS !== "${{ inputs.scenarios }}") { - errors.push("report-to-pr step must pass scenarios through JOB_SCENARIOS env"); + errors.push( + "report-to-pr step must pass scenarios through JOB_SCENARIOS env", + ); } - const reportScript = stringValue(asRecord(report?.with).script ?? report?.run); + const reportScript = stringValue( + asRecord(report?.with).script ?? report?.run, + ); if (!reportScript.includes("process.env.JOBS")) { errors.push( "step 'Post Vitest scenario results to PR' run script must include process.env.JOBS", @@ -5310,7 +7837,9 @@ export function validateE2eVitestScenariosWorkflowBoundary( ); } if (!reportScript.includes("cancelled")) { - errors.push("step 'Post Vitest scenario results to PR' run script must count cancelled jobs"); + errors.push( + "step 'Post Vitest scenario results to PR' run script must count cancelled jobs", + ); } if (!reportScript.includes("**Requested jobs:**")) { errors.push( @@ -5322,7 +7851,10 @@ export function validateE2eVitestScenariosWorkflowBoundary( "step 'Post Vitest scenario results to PR' run script must include **Requested scenarios:**", ); } - for (const forbidden of ["toJSON(inputs.pr_number)", "toJSON(inputs.scenarios)"]) { + for (const forbidden of [ + "toJSON(inputs.pr_number)", + "toJSON(inputs.scenarios)", + ]) { if (reportScript.includes(forbidden)) { errors.push( `step 'Post Vitest scenario results to PR' run script must not include ${forbidden}`, From d4f7bb7571804895d6f79ffc0bc7a8b0df7a51db Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Mon, 22 Jun 2026 22:12:39 -0700 Subject: [PATCH 4/6] test(e2e): assert Hermes startup leaks before redaction Signed-off-by: Carlos Villela --- test/e2e-scenario/fixtures/docker-probe.ts | 36 +++++++++------- .../hermes-sandbox-secret-boundary.test.ts | 20 +++++++-- .../support-tests/docker-probe.test.ts | 42 +++++++++++++++++++ 3 files changed, 78 insertions(+), 20 deletions(-) diff --git a/test/e2e-scenario/fixtures/docker-probe.ts b/test/e2e-scenario/fixtures/docker-probe.ts index 42d6c64f323..afe9117d555 100644 --- a/test/e2e-scenario/fixtures/docker-probe.ts +++ b/test/e2e-scenario/fixtures/docker-probe.ts @@ -29,6 +29,13 @@ export type DockerProbeRunner = ( options: SpawnSyncOptionsWithStringEncoding, ) => SpawnSyncReturns; +type DockerProbeRunOptions = { + artifactName: string; + timeoutMs?: number; + artifactRedactionValues?: string[]; + returnRaw?: boolean; +}; + const DOCKER_ENV_ALLOWLIST = [ "DOCKER_HOST", "DOCKER_CONTEXT", @@ -98,7 +105,7 @@ export class DockerProbe { async run( args: string[], - options: { artifactName: string; timeoutMs?: number } = { artifactName: "docker" }, + options: DockerProbeRunOptions = { artifactName: "docker" }, ): Promise { fs.mkdirSync(this.dockerConfigDir, { recursive: true }); const command = ["docker", ...args]; @@ -109,16 +116,16 @@ export class DockerProbe { maxBuffer: 10 * 1024 * 1024, timeout: options.timeoutMs ?? 30_000, }); - const commandResult = redactDockerProbeResult( - { - command, - exitCode: result.status, - signal: result.signal, - stdout: result.stdout ?? "", - stderr: result.stderr ?? "", - error: result.error instanceof Error ? result.error.message : undefined, - }, - this.redact, + const rawCommandResult = { + command, + exitCode: result.status, + signal: result.signal, + stdout: result.stdout ?? "", + stderr: result.stderr ?? "", + error: result.error instanceof Error ? result.error.message : undefined, + }; + const commandResult = redactDockerProbeResult(rawCommandResult, (text) => + this.redact(text, options.artifactRedactionValues ?? []), ); const artifactBase = `docker/${String(++this.sequence).padStart(3, "0")}-${safeName( options.artifactName, @@ -126,13 +133,10 @@ export class DockerProbe { await this.artifacts.writeText(`${artifactBase}.stdout.txt`, commandResult.stdout); await this.artifacts.writeText(`${artifactBase}.stderr.txt`, commandResult.stderr); await this.artifacts.writeJson(`${artifactBase}.result.json`, commandResult); - return commandResult; + return options.returnRaw === true ? rawCommandResult : commandResult; } - async expect( - args: string[], - options: { artifactName: string; timeoutMs?: number }, - ): Promise { + async expect(args: string[], options: DockerProbeRunOptions): Promise { const result = await this.run(args, options); if (result.exitCode !== 0) { throw new Error(resultText(result)); diff --git a/test/e2e-scenario/live/hermes-sandbox-secret-boundary.test.ts b/test/e2e-scenario/live/hermes-sandbox-secret-boundary.test.ts index 968e5f6d632..171214faa25 100644 --- a/test/e2e-scenario/live/hermes-sandbox-secret-boundary.test.ts +++ b/test/e2e-scenario/live/hermes-sandbox-secret-boundary.test.ts @@ -423,11 +423,16 @@ async function inspectManagedToolBoundary(probe: DockerProbe, image: string): Pr ).toBe(0); } +function redactExpectedSecret(value: string, text: string): string { + return text.split(value).join("[REDACTED_TEST_SECRET]"); +} + async function runStartupWithEnvFileEntry( probe: DockerProbe, image: string, assignment: string, key: string, + value: string, ): Promise { const script = `set -euo pipefail; printf '%s\n' ${shellQuote( assignment, @@ -436,6 +441,8 @@ async function runStartupWithEnvFileEntry( ["run", "--rm", "--user", "sandbox", "--entrypoint", "/bin/bash", image, "-lc", script], { artifactName: `startup-rejects-env-file-${safeArtifactPart(key)}`, + artifactRedactionValues: [value], + returnRaw: true, timeoutMs: RUN_TIMEOUT_MS, }, ); @@ -446,6 +453,7 @@ async function runStartupWithRuntimeEnvEntry( image: string, assignment: string, key: string, + value: string, ): Promise { return probe.run( [ @@ -462,6 +470,8 @@ async function runStartupWithRuntimeEnvEntry( ], { artifactName: `startup-rejects-runtime-env-${safeArtifactPart(key)}`, + artifactRedactionValues: [value], + returnRaw: true, timeoutMs: RUN_TIMEOUT_MS, }, ); @@ -474,10 +484,11 @@ async function expectStartupRejectsEnvFileEntry( key: string, value: string, ): Promise { - const result = await runStartupWithEnvFileEntry(probe, image, assignment, key); + const result = await runStartupWithEnvFileEntry(probe, image, assignment, key, value); const output = `${result.stdout}\n${result.stderr}`; + const safeResultText = redactExpectedSecret(value, resultText(result)); - expect(result.exitCode, `Hermes startup should reject ${key}\n${resultText(result)}`).not.toBe(0); + expect(result.exitCode, `Hermes startup should reject ${key}\n${safeResultText}`).not.toBe(0); expect( output, `Hermes startup rejection should mention raw secret-shaped values for ${key}`, @@ -493,12 +504,13 @@ async function expectStartupRejectsRuntimeEnvEntry( key: string, value: string, ): Promise { - const result = await runStartupWithRuntimeEnvEntry(probe, image, assignment, key); + const result = await runStartupWithRuntimeEnvEntry(probe, image, assignment, key, value); const output = `${result.stdout}\n${result.stderr}`; + const safeResultText = redactExpectedSecret(value, resultText(result)); expect( result.exitCode, - `Hermes startup should reject runtime env ${key}\n${resultText(result)}`, + `Hermes startup should reject runtime env ${key}\n${safeResultText}`, ).not.toBe(0); expect( output, diff --git a/test/e2e-scenario/support-tests/docker-probe.test.ts b/test/e2e-scenario/support-tests/docker-probe.test.ts index 87139948368..deffdbcc8ed 100644 --- a/test/e2e-scenario/support-tests/docker-probe.test.ts +++ b/test/e2e-scenario/support-tests/docker-probe.test.ts @@ -106,6 +106,48 @@ describe("DockerProbe secret hygiene", () => { } }); + it("can return raw Docker output for leak assertions while writing only redacted artifacts", async () => { + const leakedSecret = "SENTINEL_RAW_SECRET_VALUE"; + const artifactsRoot = await fs.mkdtemp(path.join(os.tmpdir(), "docker-probe-raw-output-")); + const artifacts = new ArtifactSink(artifactsRoot); + const secrets = new SecretStore({}, (message) => { + throw new Error(message ?? "unexpected skip"); + }); + const probe = new DockerProbe( + artifacts, + (text, extraValues) => secrets.redact(text, extraValues), + () => ({ + pid: 123, + output: [null, "", `startup rejected DEVTEST_API_TOKEN=${leakedSecret}`], + stdout: "", + stderr: `startup rejected DEVTEST_API_TOKEN=${leakedSecret}`, + status: 1, + signal: null, + }), + ); + + const result = await probe.run(["run", "hermes"], { + artifactName: "startup-rejects-env-file-devtest-api-token", + artifactRedactionValues: [leakedSecret], + returnRaw: true, + }); + + expect(result.stderr).toContain(leakedSecret); + const stdoutArtifact = await readArtifact( + artifactsRoot, + "docker/001-startup-rejects-env-file-devtest-api-token.stdout.txt", + ); + expect(stdoutArtifact).not.toContain(leakedSecret); + for (const relativePath of [ + "docker/001-startup-rejects-env-file-devtest-api-token.stderr.txt", + "docker/001-startup-rejects-env-file-devtest-api-token.result.json", + ]) { + const artifact = await readArtifact(artifactsRoot, relativePath); + expect(artifact).not.toContain(leakedSecret); + expect(artifact).toContain("[REDACTED]"); + } + }); + it("redacts diagnostic-style Docker inspect, logs, process, start-log, and gateway-log artifacts", async () => { const secret = "docker-diagnostic-artifact-secret"; const diagnostics = new Map([ From 6894a5695e8cef8829ae8665c39e6a11b4af5b64 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Mon, 22 Jun 2026 23:00:49 -0700 Subject: [PATCH 5/6] test(e2e): keep DockerProbe expect redacted Signed-off-by: Carlos Villela --- test/e2e-scenario/fixtures/docker-probe.ts | 5 ++++ .../support-tests/docker-probe.test.ts | 29 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/test/e2e-scenario/fixtures/docker-probe.ts b/test/e2e-scenario/fixtures/docker-probe.ts index afe9117d555..f3356e49ad9 100644 --- a/test/e2e-scenario/fixtures/docker-probe.ts +++ b/test/e2e-scenario/fixtures/docker-probe.ts @@ -137,6 +137,11 @@ export class DockerProbe { } async expect(args: string[], options: DockerProbeRunOptions): Promise { + if (options.returnRaw === true) { + throw new Error( + "DockerProbe.expect cannot return raw Docker output; use run(..., { returnRaw: true }) only for explicit leak assertions that never log the raw result.", + ); + } const result = await this.run(args, options); if (result.exitCode !== 0) { throw new Error(resultText(result)); diff --git a/test/e2e-scenario/support-tests/docker-probe.test.ts b/test/e2e-scenario/support-tests/docker-probe.test.ts index deffdbcc8ed..8dec3a87084 100644 --- a/test/e2e-scenario/support-tests/docker-probe.test.ts +++ b/test/e2e-scenario/support-tests/docker-probe.test.ts @@ -148,6 +148,35 @@ describe("DockerProbe secret hygiene", () => { } }); + it("rejects raw Docker output from expect to keep thrown diagnostics redacted", async () => { + const leakedSecret = "SENTINEL_RAW_SECRET_VALUE"; + const artifactsRoot = await fs.mkdtemp(path.join(os.tmpdir(), "docker-probe-expect-raw-")); + const artifacts = new ArtifactSink(artifactsRoot); + const secrets = new SecretStore({}, (message) => { + throw new Error(message ?? "unexpected skip"); + }); + const probe = new DockerProbe( + artifacts, + (text, extraValues) => secrets.redact(text, extraValues), + () => ({ + pid: 123, + output: [null, "", `startup rejected DEVTEST_API_TOKEN=${leakedSecret}`], + stdout: "", + stderr: `startup rejected DEVTEST_API_TOKEN=${leakedSecret}`, + status: 1, + signal: null, + }), + ); + + await expect( + probe.expect(["run", "hermes"], { + artifactName: "startup-rejects-env-file-devtest-api-token", + artifactRedactionValues: [leakedSecret], + returnRaw: true, + }), + ).rejects.toThrow("DockerProbe.expect cannot return raw Docker output"); + }); + it("redacts diagnostic-style Docker inspect, logs, process, start-log, and gateway-log artifacts", async () => { const secret = "docker-diagnostic-artifact-secret"; const diagnostics = new Map([ From 7befde6431ea26784bb2b28144f392b9fc9546e3 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Mon, 22 Jun 2026 23:33:51 -0700 Subject: [PATCH 6/6] test(e2e): cover Hermes secret workflow drift Signed-off-by: Carlos Villela --- .../hermes-secret-boundary-workflow.test.ts | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/test/e2e-scenario/support-tests/hermes-secret-boundary-workflow.test.ts b/test/e2e-scenario/support-tests/hermes-secret-boundary-workflow.test.ts index c8f614bd5bb..3d89e26448a 100644 --- a/test/e2e-scenario/support-tests/hermes-secret-boundary-workflow.test.ts +++ b/test/e2e-scenario/support-tests/hermes-secret-boundary-workflow.test.ts @@ -1,13 +1,28 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + import { expect, it } from "vitest"; +import YAML from "yaml"; import { evaluateE2eVitestWorkflowDispatchSelectors, readFreeStandingJobsInventory, + validateE2eVitestScenariosWorkflowBoundary, } from "../../../tools/e2e-scenarios/workflow-boundary.mts"; +function readWorkflow(): Record { + return YAML.parse( + fs.readFileSync( + path.join(process.cwd(), ".github/workflows/e2e-vitest-scenarios.yaml"), + "utf-8", + ), + ) as Record; +} + it("routes Hermes sandbox secret-boundary selective dispatch to its free-standing Vitest job", () => { const inventory = readFreeStandingJobsInventory(); @@ -36,3 +51,53 @@ it("routes Hermes sandbox secret-boundary selective dispatch to its free-standin registryScenarios: [], }); }); + +it("rejects Hermes sandbox secret-boundary workflow secret and Docker-auth drift", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "hermes-secret-boundary-workflow-")); + const workflowPath = path.join(tmp, "workflow.yaml"); + const workflow = readWorkflow() as { + jobs: Record; steps: Array> }>; + }; + const job = workflow.jobs["hermes-sandbox-secret-boundary-vitest"]; + expect(job).toBeDefined(); + + job.env = { + ...job.env, + NVIDIA_INFERENCE_API_KEY: "${{ secrets.NVIDIA_INFERENCE_API_KEY }}", + DOCKERHUB_USERNAME: "${{ secrets.DOCKERHUB_USERNAME }}", + DOCKERHUB_TOKEN: "${{ secrets.DOCKERHUB_TOKEN }}", + }; + job.steps.splice(1, 0, { + name: "Authenticate to Docker Hub", + env: { + DOCKERHUB_USERNAME: "${{ secrets.DOCKERHUB_USERNAME }}", + DOCKERHUB_TOKEN: "${{ secrets.DOCKERHUB_TOKEN }}", + }, + run: "docker login docker.io --username $DOCKERHUB_USERNAME --password $DOCKERHUB_TOKEN", + }); + const runVitest = job.steps.find( + (step) => step.name === "Run Hermes sandbox secret-boundary live test", + ); + expect(runVitest).toBeDefined(); + runVitest!.env = { + NVIDIA_INFERENCE_API_KEY: "${{ secrets.NVIDIA_INFERENCE_API_KEY }}", + }; + fs.writeFileSync(workflowPath, YAML.stringify(workflow)); + + try { + expect(validateE2eVitestScenariosWorkflowBoundary(workflowPath)).toEqual( + expect.arrayContaining([ + "hermes-sandbox-secret-boundary-vitest job env must not include NVIDIA_INFERENCE_API_KEY", + "hermes-sandbox-secret-boundary-vitest job env must not include DOCKERHUB_USERNAME", + "hermes-sandbox-secret-boundary-vitest job env must not include DOCKERHUB_TOKEN", + "hermes-sandbox-secret-boundary-vitest must not authenticate to Docker Hub before branch-controlled test code runs", + "hermes-sandbox-secret-boundary-vitest step 'Authenticate to Docker Hub' env must not include DOCKERHUB_USERNAME", + "hermes-sandbox-secret-boundary-vitest step 'Authenticate to Docker Hub' env must not include DOCKERHUB_TOKEN", + "hermes-sandbox-secret-boundary-vitest step 'Authenticate to Docker Hub' run script must not use docker login or inline secret interpolation", + "hermes-sandbox-secret-boundary-vitest step 'Run Hermes sandbox secret-boundary live test' env must not include NVIDIA_INFERENCE_API_KEY", + ]), + ); + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } +});