Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 4 additions & 82 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ jobs:
fi
larger_runner="${LARGER_RUNNER_LABEL}"
fi
runner_routing="$(jq -cn --arg standard "ubuntu-latest" --arg larger "${larger_runner}" '{"channels-stop-start-hermes":$larger,"channels-stop-start-openclaw":$standard,"common-egress-agent":$larger,"hermes-dashboard":$larger,"hermes-discord":$larger,"hermes-e2e":$larger,"hermes-inference-switch":$larger,"hermes-shields-config":$larger,"mcp-bridge-deepagents":$larger,"mcp-bridge-hermes":$larger,"mcp-bridge-openclaw":$standard,"rebuild-hermes":$larger,"rebuild-hermes-stale-base":$larger,"security-posture-hermes":$larger,"security-posture-openclaw":$standard}')"
runner_routing="$(jq -cn --arg standard "ubuntu-latest" --arg larger "${larger_runner}" '{"channels-stop-start-hermes":$larger,"channels-stop-start-openclaw":$standard,"common-egress-agent":$larger,"hermes-discord":$larger,"hermes-e2e":$larger,"hermes-inference-switch":$larger,"hermes-shields-config":$larger,"mcp-bridge-deepagents":$larger,"mcp-bridge-hermes":$larger,"mcp-bridge-openclaw":$standard,"rebuild-hermes":$larger,"rebuild-hermes-stale-base":$larger,"security-posture-hermes":$larger,"security-posture-openclaw":$standard}')"
printf 'runner_routing=%s\n' "${runner_routing}" >> "${GITHUB_OUTPUT}"

- name: Authenticate controller dispatch
Expand Down Expand Up @@ -2170,6 +2170,7 @@ jobs:
NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js
NEMOCLAW_RUN_LIVE_E2E: "1"
NEMOCLAW_E2E_INFERENCE_MODE: ${{ inputs.inference_mode || 'mock' }}
NEMOCLAW_E2E_HERMES_DASHBOARD: "1"
NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1"
NEMOCLAW_AGENT: hermes
NEMOCLAW_NON_INTERACTIVE: "1"
Expand All @@ -2179,7 +2180,7 @@ jobs:
steps:
- id: trusted_hermes_swap
name: Provision trusted Hermes E2E swap
if: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && (github.event_name == 'schedule' || inputs.checkout_sha == '' || (github.event_name == 'workflow_dispatch' && inputs.checkout_sha != '' && (contains(format(',{0},', inputs.jobs), ',hermes-e2e,') || contains(format(',{0},', inputs.targets), ',hermes-e2e,')))) }}
if: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && (github.event_name == 'schedule' || inputs.checkout_sha == '' || (github.event_name == 'workflow_dispatch' && inputs.checkout_sha != '' && (contains(format(',{0},', inputs.jobs), ',hermes-e2e,') || contains(format(',{0},', inputs.targets), ',hermes-e2e,') || contains(format(',{0},', inputs.jobs), ',hermes-dashboard,') || contains(format(',{0},', inputs.targets), ',hermes-dashboard,')))) }}
shell: /bin/bash --noprofile --norc -e -o pipefail {0}
env:
BASH_ENV: /dev/null
Expand Down Expand Up @@ -2499,85 +2500,6 @@ jobs:
shell: bash
run: bash .github/scripts/docker-auth-cleanup.sh

hermes-dashboard:
needs: generate-matrix
if: ${{ (github.event_name != 'workflow_dispatch' || (inputs.jobs == '' && inputs.targets == '')) || contains(format(',{0},', inputs.jobs), ',hermes-dashboard,') || contains(format(',{0},', inputs.targets), ',hermes-dashboard,') }}
runs-on: ${{ fromJSON(needs.generate-matrix.outputs.runner_routing)['hermes-dashboard'] }}
timeout-minutes: 85
env:
E2E_JOB: "1"
E2E_TARGET_ID: "hermes-dashboard"
E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live/hermes-dashboard
NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js
NEMOCLAW_RUN_LIVE_E2E: "1"
NEMOCLAW_E2E_USE_HOSTED_INFERENCE: "1"
NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1"
NEMOCLAW_AGENT: hermes
NEMOCLAW_NON_INTERACTIVE: "1"
NEMOCLAW_RECREATE_SANDBOX: "1"
NEMOCLAW_SANDBOX_NAME: e2e-hermes-dashboard
NEMOCLAW_E2E_HERMES_DASHBOARD: "1"
NEMOCLAW_ONBOARD_VALIDATION_TIMEOUT_SECONDS: "60"
steps:
- id: trusted_hermes_swap
name: Provision trusted Hermes E2E swap
if: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }}
shell: /bin/bash --noprofile --norc -e -o pipefail {0}
env:
BASH_ENV: /dev/null
CHECKOUT_SHA: ${{ inputs.checkout_sha }}
DISPATCH_SHA: ${{ github.sha }}
ENV: /dev/null
EVENT_NAME: ${{ github.event_name }}
EXPECTED_WORKFLOW_SHA: ${{ inputs.workflow_sha }}
LC_ALL: C
REF: ${{ github.ref }}
REPOSITORY: ${{ github.repository }}
RUNNER_ARCH_KIND: ${{ runner.arch }}
RUNNER_ENVIRONMENT_KIND: ${{ runner.environment }}
RUNNER_OS_KIND: ${{ runner.os }}
WORKFLOW_SHA: ${{ github.workflow_sha }}
run: *trusted-hermes-e2e-swap

- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
repository: ${{ inputs.checkout_repository || github.repository }}
ref: ${{ inputs.checkout_sha || github.sha }}
persist-credentials: false

- *dockerhub-auth

- name: Prepare E2E workspace
uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75

- name: Initialize runner comparison telemetry
if: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && inputs.checkout_sha == '' }}
continue-on-error: true
shell: bash
run: npx tsx tools/e2e/runner-comparison.mts initialize

- name: Run Hermes dashboard live Vitest test
env:
NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }}
run: |
set -euo pipefail
npx tsx tools/e2e/live-vitest-invocation.mts run --test-path test/e2e/live/hermes-e2e.test.ts

- name: Finalize runner comparison telemetry
if: ${{ always() && github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && inputs.checkout_sha == '' }}
continue-on-error: true
shell: bash
run: npx tsx tools/e2e/runner-comparison.mts finalize

- name: Upload Hermes dashboard live Vitest artifacts
if: always()
uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57

- name: Clean up Docker auth
if: always()
shell: bash
run: bash .github/scripts/docker-auth-cleanup.sh

hermes-discord:
needs: generate-matrix
if: ${{ (github.event_name != 'workflow_dispatch' || (inputs.jobs == '' && inputs.targets == '')) || contains(format(',{0},', inputs.jobs), ',hermes-discord,') || contains(format(',{0},', inputs.targets), ',hermes-discord,') }}
Expand Down Expand Up @@ -5978,7 +5900,6 @@ jobs:
sessions-agents-cli,
hermes-e2e,
hermes-gpu-startup,
hermes-dashboard,
hermes-slack,
hermes-discord,
network-policy,
Expand Down Expand Up @@ -6046,6 +5967,7 @@ jobs:
persist-credentials: false
sparse-checkout: |
tools/e2e/report-e2e-results.mts
tools/e2e/selector-aliases.mts
sparse-checkout-cone-mode: false
- name: Post E2E target results to PR
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
Expand Down
15 changes: 10 additions & 5 deletions test/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ discovery command locally to inspect the generated test matrix:
npx tsx tools/e2e/credential-free-tests.mts
```

The retired `hermes-dashboard` selector remains a compatibility alias for
`hermes-e2e` in both selector inputs. Reports use the canonical
`hermes-e2e` name. That lane always enables dashboard coverage while preserving
the manually selected `mock`, `internal-nvidia`, or `public-nvidia` inference
mode.

## Larger-runner routing

The larger-runner experiment is inactive while the configuration variable
Expand Down Expand Up @@ -117,7 +123,7 @@ Successful state is discarded with the ephemeral runner.

The fallback covers agent-turn latency, Hermes inference switch and shields,
the Hermes Bedrock and stable MCP shards, the Hermes common-egress and channel
stop/start shards, and the `hermes-e2e`, `hermes-dashboard`, `hermes-discord`,
stop/start shards, the dashboard-bearing `hermes-e2e` lane, `hermes-discord`,
and Hermes security-posture tests. Rebuild lanes with workflow-managed swap,
dedicated-runner lanes, `mcp-bridge-dev`, and non-Hermes shards do not use it.
Candidate-authored workflow definitions and fork-owned runs cannot reach it.
Expand All @@ -135,7 +141,7 @@ The eligible set is limited to the measured or repeatedly interrupted heavy
lanes:

- `common-egress-agent`;
- `hermes-e2e`, `hermes-dashboard`, and `hermes-discord`;
- `hermes-e2e`, including dashboard coverage, and `hermes-discord`;
- both `hermes-inference-switch` modes;
- `hermes-shields-config`;
- the Hermes shards of `security-posture` and `channels-stop-start`;
Expand Down Expand Up @@ -260,7 +266,7 @@ window.
### Runner comparison telemetry

Trusted `main` runs without an alternate checkout SHA record runner-comparison
telemetry for 14 routed workflow lane identities / 17
telemetry for 13 routed workflow lane identities / 16
concrete job executions.

- `agent-turn-latency`, spanning its sequential OpenClaw and Hermes setup
Expand All @@ -272,9 +278,8 @@ concrete job executions.
- `mcp-bridge` with the `hermes` shard
- `mcp-bridge` with the `deepagents` shard
- `channels-stop-start` with the `hermes` shard
- `hermes-dashboard`
- `hermes-discord`
- `hermes-e2e`
- `hermes-e2e`, including dashboard coverage
- `hermes-inference-switch` with the `hosted` and `anthropic` modes
- `hermes-shields-config`
- `security-posture` with the `hermes` shard
Expand Down
4 changes: 2 additions & 2 deletions test/e2e/support/e2e-progress-fixture.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ it.each([
});

it.each([
"hermes-dashboard",
"hermes-e2e",
"hermes-discord",
"hermes-shields-config",
null,
Expand All @@ -40,7 +40,7 @@ it.each([
it.each([
["rebuild-hermes", 15_000],
["rebuild-hermes-stale-base", 15_000],
["hermes-dashboard", 60_000],
["hermes-e2e", 60_000],
] as const)("wires the live %s comparison cadence into progress options (#7144)", (targetId, intervalMs) => {
const samples: Array<{ kind: string; phase: string }> = [];
const options = runnerComparisonProgressOptions(
Expand Down
38 changes: 38 additions & 0 deletions test/e2e/support/e2e-report-to-pr-workflow-boundary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,44 @@ it("renders comment content from job evidence without a live GitHub mutation", (
expect(report.body).toContain("All requested tests passed");
});

it.each([
{
env: { JOB_TARGETS: "", JOBS: "hermes-dashboard" },
label: "test ID",
requestedLine: "**Requested test IDs:** `hermes-e2e`",
},
{
env: { JOB_TARGETS: "hermes-dashboard", JOBS: "" },
label: "target",
requestedLine: "**Requested targets:** `hermes-e2e`",
},
])("reports the canonical Hermes result for a retired dashboard $label selector", ({
env,
requestedLine,
}) => {
const report = renderE2eReport({
needs: {
"generate-matrix": { result: "success" },
"hermes-e2e": { result: "success" },
},
env: {
EXPLICIT_ONLY_JOBS: "",
TEST_MATRIX: "[]",
JOB_PR_NUMBER: "42",
...env,
},
apiJobs: [{ conclusion: "success", name: "hermes-e2e", status: "completed" }],
apiJobsLoaded: true,
context: REPORT_CONTEXT,
});

expect(report.fatal).toBeUndefined();
expect(report.body).toContain(requestedLine);
expect(report.body).toContain("| hermes-e2e | ✅ success | — |");
expect(report.body).not.toContain("| hermes-dashboard |");
expect(report.body).not.toContain("not reported");
});

it("fails closed on an invalid test matrix without rendering a comment", () => {
const report = renderE2eReport({
needs: {
Expand Down
8 changes: 8 additions & 0 deletions test/e2e/support/e2e-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,14 @@ describe("e2e workflow boundary", () => {
selectedFreeStandingJobs: ["network-policy"],
registryTargets: ["ubuntu-repo-cloud-openclaw"],
});
for (const selectors of [{ jobs: "hermes-dashboard" }, { targets: "hermes-dashboard" }]) {
expect(evaluateE2eWorkflowDispatchSelectors(selectors)).toMatchObject({
valid: true,
liveTargetsRun: false,
selectedFreeStandingJobs: ["hermes-e2e"],
registryTargets: [],
});
}
},
);

Expand Down
92 changes: 34 additions & 58 deletions test/e2e/support/hermes-dashboard-workflow-boundary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,73 +7,49 @@ import {
validateHermesDashboardWorkflow,
validateHermesDashboardWorkflowBoundary,
} from "../../../tools/e2e/hermes-dashboard-workflow-boundary.mts";
import {
HERMES_DASHBOARD_JOB_TIMEOUT_MAX_MINUTES,
HERMES_DASHBOARD_JOB_TIMEOUT_MINUTES,
} from "../../../tools/e2e/hermes-timeout-contract.mts";

describe("Hermes dashboard workflow boundary", () => {
it("accepts the checked-in workflow and rejects dashboard mode, execution, and reporting drift", () => {
it("keeps dashboard coverage in the canonical Hermes lane", () => {
expect(validateHermesDashboardWorkflowBoundary()).toEqual([]);
const dashboardMode = readHermesDashboardWorkflow();
const dashboardJob = dashboardMode.jobs["hermes-dashboard"];
dashboardJob["timeout-minutes"] = 30;
dashboardJob.env!.E2E_ARTIFACT_DIR = "/tmp/hermes-dashboard";
dashboardJob.env!.NEMOCLAW_E2E_HERMES_DASHBOARD = "0";
dashboardJob.env!.NVIDIA_INFERENCE_API_KEY = "${{ secrets.NVIDIA_INFERENCE_API_KEY }}";
const checkout = dashboardJob.steps!.find((step) =>

const workflow = readHermesDashboardWorkflow();
const canonicalJob = workflow.jobs["hermes-e2e"];
canonicalJob.env!.NEMOCLAW_E2E_HERMES_DASHBOARD = "0";
canonicalJob.env!.NEMOCLAW_E2E_INFERENCE_MODE = "mock";
canonicalJob.env!.E2E_TARGET_ID = "hermes-dashboard";
canonicalJob.steps!.find((step) => step.name === "Run Hermes live Vitest test")!.run =
"echo skipped";
workflow.jobs["hermes-dashboard"] = structuredClone(canonicalJob);
workflow.jobs["hermes-discord"].env!.NEMOCLAW_E2E_HERMES_DASHBOARD = "1";
workflow.jobs["report-to-pr"].needs = ["hermes-dashboard"];

expect(validateHermesDashboardWorkflow(workflow)).toEqual(
expect.arrayContaining([
"hermes-dashboard must remain consolidated into hermes-e2e",
"only hermes-e2e may enable Hermes dashboard E2E coverage (found on hermes-discord)",
"hermes-e2e must enable Hermes dashboard coverage",
"hermes-e2e must preserve manual inference-mode selection",
"hermes-e2e must publish its canonical selector",
"hermes-e2e must run the live Vitest project",
"report-to-pr must wait for hermes-e2e",
"report-to-pr must not wait for retired hermes-dashboard",
]),
);
});

it("keeps the canonical checkout trust boundary", () => {
const workflow = readHermesDashboardWorkflow();
const checkout = workflow.jobs["hermes-e2e"].steps!.find((step) =>
step.uses?.startsWith("actions/checkout@"),
)!;
checkout.uses = "actions/checkout@v6";
checkout.with!["persist-credentials"] = true;
expect(validateHermesDashboardWorkflow(dashboardMode)).toEqual(

expect(validateHermesDashboardWorkflow(workflow)).toEqual(
expect.arrayContaining([
`hermes-dashboard timeout must be between ${HERMES_DASHBOARD_JOB_TIMEOUT_MINUTES} and ${HERMES_DASHBOARD_JOB_TIMEOUT_MAX_MINUTES} minutes`,
"hermes-dashboard must use its isolated artifact directory",
"hermes-dashboard must enable Hermes dashboard coverage",
"hermes-dashboard must not expose the inference key at job scope",
"hermes-dashboard checkout must pin a full action SHA",
"hermes-dashboard checkout must disable persisted credentials",
"hermes-e2e checkout must pin a full action SHA",
"hermes-e2e checkout must disable persisted credentials",
]),
);

const misplacedDashboardMode = readHermesDashboardWorkflow();
misplacedDashboardMode.jobs["hermes-e2e"].env!.NEMOCLAW_E2E_HERMES_DASHBOARD = "1";
expect(validateHermesDashboardWorkflow(misplacedDashboardMode)).toContain(
"only hermes-dashboard may enable Hermes dashboard E2E coverage (found on hermes-e2e)",
);

const execution = readHermesDashboardWorkflow();
execution.jobs["hermes-dashboard"].steps!.find(
(step) => step.name === "Run Hermes dashboard live Vitest test",
)!.run = "echo skipped";
expect(validateHermesDashboardWorkflow(execution)).toContain(
"hermes-dashboard must run the live Vitest project",
);

const reporting = readHermesDashboardWorkflow();
reporting.jobs["report-to-pr"].needs = [];
expect(validateHermesDashboardWorkflow(reporting)).toContain(
"report-to-pr must wait for hermes-dashboard",
);
});

it("shares the bounded Hermes timeout headroom contract", () => {
const upperBound = readHermesDashboardWorkflow();
upperBound.jobs["hermes-dashboard"]["timeout-minutes"] =
HERMES_DASHBOARD_JOB_TIMEOUT_MAX_MINUTES;
expect(validateHermesDashboardWorkflow(upperBound)).toEqual([]);

for (const timeoutMinutes of [
HERMES_DASHBOARD_JOB_TIMEOUT_MINUTES - 1,
HERMES_DASHBOARD_JOB_TIMEOUT_MINUTES + 0.5,
HERMES_DASHBOARD_JOB_TIMEOUT_MAX_MINUTES + 1,
]) {
const invalid = readHermesDashboardWorkflow();
invalid.jobs["hermes-dashboard"]["timeout-minutes"] = timeoutMinutes;
expect(validateHermesDashboardWorkflow(invalid)).toContain(
`hermes-dashboard timeout must be between ${HERMES_DASHBOARD_JOB_TIMEOUT_MINUTES} and ${HERMES_DASHBOARD_JOB_TIMEOUT_MAX_MINUTES} minutes`,
);
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ const standardRouting = {
"channels-stop-start-hermes": "ubuntu-latest",
"channels-stop-start-openclaw": "ubuntu-latest",
"common-egress-agent": "ubuntu-latest",
"hermes-dashboard": "ubuntu-latest",
"hermes-discord": "ubuntu-latest",
"hermes-e2e": "ubuntu-latest",
"hermes-inference-switch": "ubuntu-latest",
Expand Down Expand Up @@ -139,7 +138,6 @@ describe("larger-runner workflow routing boundary", () => {
...standardRouting,
"channels-stop-start-hermes": largerRunner,
"common-egress-agent": largerRunner,
"hermes-dashboard": largerRunner,
"hermes-discord": largerRunner,
"hermes-e2e": largerRunner,
"hermes-inference-switch": largerRunner,
Expand Down
Loading
Loading