From 4d21be2a66a7653c8ace52718b5ec899171589e4 Mon Sep 17 00:00:00 2001 From: Rebecca Sliter <571084+rsliter@users.noreply.github.com> Date: Tue, 18 Aug 2026 11:49:22 -0700 Subject: [PATCH 1/4] test(e2e): align typed target titles and coverage inventory Signed-off-by: Rebecca Sliter <571084+rsliter@users.noreply.github.com> --- .github/workflows/e2e.yaml | 3 +- test/e2e/README.md | 9 ++- test/e2e/docs/README.md | 9 +-- test/e2e/live/registry-targets.test.ts | 8 +-- test/e2e/registry/run.ts | 60 ++++++------------- test/e2e/registry/runtime-support.ts | 47 +++++++++++---- .../e2e-live-skip-name-contract.test.ts | 46 +++++++++----- .../support/e2e-live-target-gating.test.ts | 5 +- test/e2e/support/e2e-matrix.test.ts | 20 ++++++- test/e2e/support/e2e-workflow-trace.test.ts | 12 +++- .../support/live-vitest-invocation.test.ts | 4 +- test/e2e/support/workflow-plan.test.ts | 15 ++++- tools/e2e/execution-coverage.mts | 4 ++ tools/e2e/workflow-boundary.mts | 5 +- tools/e2e/workflow-plan.mts | 8 ++- 15 files changed, 162 insertions(+), 93 deletions(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index dbca2faa476..bcfce3ed0d4 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -2548,6 +2548,7 @@ jobs: ${{ steps.workspace.outputs.work_dir }}/cleanup.json live: + name: ${{ matrix.label }} needs: [base-image-publication, generate-matrix] if: ${{ needs.generate-matrix.outputs.matrix != '[]' }} runs-on: ${{ matrix.runner }} @@ -2661,7 +2662,7 @@ jobs: TARGET_ID: ${{ matrix.id }} run: | set -euo pipefail - npx tsx tools/e2e/live-vitest-invocation.mts run --test-path test/e2e/live/registry-targets.test.ts --selector "^${TARGET_ID}$" + npx tsx tools/e2e/live-vitest-invocation.mts run --test-path test/e2e/live/registry-targets.test.ts --selector "^${TARGET_ID}:" # The sanitizer reads raw traces only after checking the workflow-owned # runner-temp path, then writes the timing-only file into upload roots. diff --git a/test/e2e/README.md b/test/e2e/README.md index 080b7ffe833..a019495b04a 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -333,6 +333,12 @@ Each execution row declares three coverage fields: - `observableOutcome` names the behavior that produces the evidence. Catalogue targets use their outcome-oriented `displayName` as this value. - `environmentOrInferenceEndpoint` names the host boundary or inference endpoint that distinguishes the evidence. +Typed registry tests derive each human-readable execution title as +` [; ]`. +Typed registry tests prefix that title with the stable target ID. Workflows use +the ID prefix for selection, while the semantic tuple makes the test purpose and +evidence boundary visible in Vitest and GitHub Actions. + Keep coverage metadata with the execution owner: - Catalogue targets declare it in `tools/e2e/target-catalogue.mts`. @@ -348,7 +354,8 @@ multiple rows. `tools/e2e/workflow-plan.mts` composes and validates these source Do not add a separate hand-maintained execution list. The default coverage matrix excludes explicit-only jobs and inert typed-registry declarations. -The rendered report lists those categories separately. +The rendered report lists those categories separately and inventories every typed declaration, +including declarations that have no executable matrix cell. Explicit-only rows keep their coverage dimensions but do not join the default release matrix. Inert declarations report unresolved coverage fields and the missing executable ownership. diff --git a/test/e2e/docs/README.md b/test/e2e/docs/README.md index 82f786b0307..144cfd1ba95 100644 --- a/test/e2e/docs/README.md +++ b/test/e2e/docs/README.md @@ -71,10 +71,11 @@ dispatch shell validation suites. ## Selecting One Target -`.github/workflows/e2e.yaml` runs one matrix target by passing its id through -`TARGET_ID` and selecting the matching test with `-t "^${TARGET_ID}$"`. The -selector performs the restriction; `TARGET_ID` alone does not limit which -targets run. +`.github/workflows/e2e.yaml` runs one matrix target by passing its ID through +`TARGET_ID`. The workflow selects the test title with the stable +`-t "^${TARGET_ID}:"` prefix. The title suffix contains the observable outcome, +agent runtime, and environment or inference endpoint. The selector performs the +restriction; `TARGET_ID` alone does not limit which targets run. The `generate-matrix` job resolves dispatch input through `requireTargets`, so an unknown id fails there before any target job starts. diff --git a/test/e2e/live/registry-targets.test.ts b/test/e2e/live/registry-targets.test.ts index 8d3a5c6d16d..2379a61ddef 100644 --- a/test/e2e/live/registry-targets.test.ts +++ b/test/e2e/live/registry-targets.test.ts @@ -13,7 +13,7 @@ import { readRegistrySandboxEntry, } from "../fixtures/phases/index.ts"; import { listTargets, requireTargets } from "../registry/registry.ts"; -import { liveTargetSupport, liveTargetTestName } from "../registry/runtime-support.ts"; +import { liveTargetSupport, liveTargetTestTitle } from "../registry/runtime-support.ts"; import { cloudExperimentalChecksForOnboarding } from "./cloud-experimental-check-list.ts"; import { runE2eCloudExperimentalChecks } from "./cloud-experimental-checks.ts"; import { @@ -42,7 +42,7 @@ const E2E_CLOUD_EXPERIMENTAL_CHECKS_DIR = path.join( ); process.env.NEMOCLAW_CLI_BIN ??= CLI_ENTRYPOINT; -// The workflow filters by target ID via `-t "^${TARGET_ID}$"`. +// The workflow filters by the stable target ID prefix via `-t "^${TARGET_ID}:"`. // When that env is set, surface the structured `[not wired]` reason for the // targeted unsupported target at module load so the job log/summary // captures it before Vitest reports the skipped test by ID. @@ -77,7 +77,7 @@ for (const [targetIndex, target] of listTargets().entries()) { console.warn(`[not wired] ${target.id}: ${support.reasons.join("; ")}`); } test.skip( - liveTargetTestName(target), + liveTargetTestTitle(target, support), { meta: { e2ePhases: REGISTRY_TARGET_PHASES } }, () => {}, ); @@ -85,7 +85,7 @@ for (const [targetIndex, target] of listTargets().entries()) { } test( - liveTargetTestName(target), + liveTargetTestTitle(target, support), { meta: { e2ePhases: REGISTRY_TARGET_PHASES } }, async ({ artifacts, diff --git a/test/e2e/registry/run.ts b/test/e2e/registry/run.ts index d73f6fa372c..e8612647679 100644 --- a/test/e2e/registry/run.ts +++ b/test/e2e/registry/run.ts @@ -4,14 +4,16 @@ import { realpathSync } from "node:fs"; import { fileURLToPath } from "node:url"; -import { - type E2eAgentRuntime, - validateE2eExecutionMetadata, -} from "../../../tools/e2e/execution-coverage.mts"; +import type { E2eAgentRuntime } from "../../../tools/e2e/execution-coverage.mts"; import { listTargets, requireTargets } from "./registry.ts"; import { resolveRunnerForTarget } from "./runner-routing.ts"; -import { type LiveTargetSupport, liveTargetSupport } from "./runtime-support.ts"; +import { + liveTargetExecutionCoverage, + type LiveTargetSupport, + liveTargetSupport, + liveTargetTestTitle, +} from "./runtime-support.ts"; import type { TargetDefinition } from "./types.ts"; interface Args { @@ -80,46 +82,17 @@ function printList() { } } -function buildLabel(target: TargetDefinition): string { - const platform = target.environment?.platform ?? "unknown-platform"; - const suites = target.suiteIds ?? []; - if (target.expectedFailure) { - const cls = target.expectedFailure.errorClass ?? "expected-failure"; - return `${platform} · ${target.id} · expect-fail:${cls}`; - } - if (suites.length === 0) { - return `${platform} · ${target.id}`; - } - if (suites.length <= 3) { - return `${platform} · ${target.id} · ${suites.join("+")}`; - } - return `${platform} · ${target.id} · ${suites.length} suites`; -} - function liveMatrixEntry( target: TargetDefinition, support: LiveTargetSupport, ): LiveTargetMatrixEntry { const { runner } = resolveRunnerForTarget(target); - if (support.supported && !target.executionCoverage) { - throw new Error( - `Executable typed E2E target ${target.id} requires execution coverage metadata`, - ); - } - const executionCoverage = validateE2eExecutionMetadata( - target.executionCoverage ?? { - agentRuntime: "unresolved", - observableOutcome: "unresolved", - environmentOrInferenceEndpoint: "unresolved", - unresolvedReason: "This typed registry declaration has no executable owner", - }, - `Typed E2E target ${target.id}`, - ); + const executionCoverage = liveTargetExecutionCoverage(target, support); return { id: target.id, ...executionCoverage, runner, - label: buildLabel(target), + label: liveTargetTestTitle(target, support), platform: target.environment?.platform ?? "unknown", install: target.environment?.install ?? "unknown", runtime: target.environment?.runtime ?? "unknown", @@ -133,14 +106,15 @@ function liveMatrixEntry( }; } +export function buildLiveTargetInventory(): LiveTargetMatrixEntry[] { + return listTargets().map((target) => liveMatrixEntry(target, liveTargetSupport(target))); +} + export function buildLiveTargetMatrix(ids: string[] = []): LiveTargetMatrixEntry[] { - const targetSupport = (ids.length > 0 ? requireTargets(ids) : listTargets()).map((target) => ({ - target, - support: liveTargetSupport(target), - })); - const liveEntries = - ids.length > 0 ? targetSupport : targetSupport.filter(({ support }) => support.supported); - return liveEntries.map(({ target, support }) => liveMatrixEntry(target, support)); + if (ids.length === 0) { + return buildLiveTargetInventory().filter((entry) => entry.supported); + } + return requireTargets(ids).map((target) => liveMatrixEntry(target, liveTargetSupport(target))); } function emitLiveMatrix(ids: string[]) { diff --git a/test/e2e/registry/runtime-support.ts b/test/e2e/registry/runtime-support.ts index ce14cd75e01..23f0dc48ac7 100644 --- a/test/e2e/registry/runtime-support.ts +++ b/test/e2e/registry/runtime-support.ts @@ -1,6 +1,11 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { + e2eExecutionTitle, + type E2eExecutionMetadata, + validateE2eExecutionMetadata, +} from "../../../tools/e2e/execution-coverage.mts"; import type { TargetDefinition } from "./types.ts"; const SUPPORTED_PLATFORMS = new Set(["ubuntu-local"]); @@ -25,17 +30,6 @@ export interface LiveTargetSupport { pendingRuntimeSuites: string[]; } -/** - * Canonical name under which a target is registered with Vitest in the - * live registry-targets test file. The workflow filters by exact ID via - * `-t "^${TARGET_ID}$"`, so both supported and unsupported targets MUST - * be registered under this exact name. Skip reasons are surfaced via the - * job log instead of the test name suffix. - */ -export function liveTargetTestName(target: TargetDefinition): string { - return target.id; -} - export function liveTargetSupport(target: TargetDefinition): LiveTargetSupport { const reasons: string[] = []; const environment = target.environment; @@ -71,3 +65,34 @@ export function liveTargetSupport(target: TargetDefinition): LiveTargetSupport { pendingRuntimeSuites: target.suiteIds ?? [], }; } + +export function liveTargetExecutionCoverage( + target: TargetDefinition, + support = liveTargetSupport(target), +): E2eExecutionMetadata { + if (support.supported && !target.executionCoverage) { + throw new Error( + `Executable typed E2E target ${target.id} requires execution coverage metadata`, + ); + } + return validateE2eExecutionMetadata( + target.executionCoverage ?? { + agentRuntime: "unresolved", + observableOutcome: "unresolved", + environmentOrInferenceEndpoint: "unresolved", + unresolvedReason: "This typed registry declaration has no executable owner", + }, + `Typed E2E target ${target.id}`, + ); +} + +/** + * The stable target ID remains the workflow selector. The semantic tuple + * supplies the human-visible Vitest title without changing target identity. + */ +export function liveTargetTestTitle( + target: TargetDefinition, + support = liveTargetSupport(target), +): string { + return `${target.id}: ${e2eExecutionTitle(liveTargetExecutionCoverage(target, support))}`; +} diff --git a/test/e2e/support/e2e-live-skip-name-contract.test.ts b/test/e2e/support/e2e-live-skip-name-contract.test.ts index ab8f758bfa3..d03fed2a95c 100644 --- a/test/e2e/support/e2e-live-skip-name-contract.test.ts +++ b/test/e2e/support/e2e-live-skip-name-contract.test.ts @@ -5,11 +5,11 @@ import { describe, expect, it } from "vitest"; import { target } from "../registry/builder.ts"; import { listTargets } from "../registry/registry.ts"; -import { liveTargetSupport, liveTargetTestName } from "../registry/runtime-support.ts"; +import { liveTargetSupport, liveTargetTestTitle } from "../registry/runtime-support.ts"; import type { TargetDefinition } from "../registry/types.ts"; function syntheticTarget(platform: string): TargetDefinition { - return target(`synthetic-${platform}`) + const definition = target(`synthetic-${platform}`) .environment({ platform, install: "repo-current", @@ -18,34 +18,48 @@ function syntheticTarget(platform: string): TargetDefinition { }) .expectedState("synthetic-ready") .build(); + return platform === "ubuntu-local" + ? { + ...definition, + executionCoverage: { + agentRuntime: "openclaw", + observableOutcome: "Synthetic target reaches the expected state", + environmentOrInferenceEndpoint: "Ubuntu test fixture; no inference endpoint", + unresolvedReason: "", + }, + } + : definition; } /** * Locks the contract that the live registry-targets test file registers - * each target under a name equal to `target.id` (no `[not wired: ...]` - * suffix), so the workflow's exact `-t "^${TARGET_ID}$"` filter matches - * supported AND unsupported entries identically. Without this contract, - * explicit unsupported selections on `workflow_dispatch` would match zero - * tests and Vitest would exit non-zero with no structured skip reason. + * each target under a title prefixed by `target.id`, so the workflow's stable + * ID filter matches supported and unsupported entries identically. Without + * this contract, explicit unsupported selections on `workflow_dispatch` would + * match zero tests and Vitest would exit non-zero with no structured skip reason. */ describe("live registry-targets skip-name contract", () => { - it("keeps a synthetically unsupported target selectable under its exact id", () => { + it("keeps an unsupported target selectable by stable ID with an unresolved title", () => { const unsupported = syntheticTarget("synthetic-unwired-platform"); const support = liveTargetSupport(unsupported); expect(support.supported).toBe(false); - const name = liveTargetTestName(unsupported); - const filter = new RegExp(`^${unsupported.id}$`); - expect(filter.test(name)).toBe(true); - // Negative: any historical `[not wired: ...]` suffix would break the workflow filter. - expect(name).not.toMatch(/\[not wired:/); + const title = liveTargetTestTitle(unsupported); + const filter = new RegExp(`^${unsupported.id}:`); + expect(title).toBe(`${unsupported.id}: unresolved [unresolved; unresolved]`); + expect(filter.test(title)).toBe(true); + expect(new RegExp(`^other-target:`).test(title)).toBe(false); + expect(title).not.toMatch(/\[not wired:/); }); - it("keeps a synthetically supported target selectable under its exact id", () => { + it("keeps a supported target selectable by stable ID with its semantic title", () => { const supported = syntheticTarget("ubuntu-local"); + const title = liveTargetTestTitle(supported); expect(liveTargetSupport(supported).supported).toBe(true); - expect(liveTargetTestName(supported)).toBe(supported.id); + expect(title).toBe( + `${supported.id}: Synthetic target reaches the expected state [openclaw; Ubuntu test fixture; no inference endpoint]`, + ); + expect(new RegExp(`^${supported.id}:`).test(title)).toBe(true); }); - }); diff --git a/test/e2e/support/e2e-live-target-gating.test.ts b/test/e2e/support/e2e-live-target-gating.test.ts index e100d242186..5fda5bd9037 100644 --- a/test/e2e/support/e2e-live-target-gating.test.ts +++ b/test/e2e/support/e2e-live-target-gating.test.ts @@ -184,9 +184,8 @@ describe("live E2E target gating", () => { it("rejects a TARGET_ID no registry target declares (#8286)", testTimeoutOptions(30_000), () => { const file = "registry-targets.test.ts"; - // The workflow selects one target with `-t "^${TARGET_ID}$"`. An id no - // target declares matches nothing, and an empty id builds `-t "^$"`, - // which also matches nothing, so the run would collect no test and + // The workflow selects one target by its stable title prefix. An ID no + // target declares matches nothing, so the run would collect no test and // exit 0 having executed no target. const unknown = listLiveTests({ enabled: true, diff --git a/test/e2e/support/e2e-matrix.test.ts b/test/e2e/support/e2e-matrix.test.ts index ed5184c6b06..da2bbe73973 100644 --- a/test/e2e/support/e2e-matrix.test.ts +++ b/test/e2e/support/e2e-matrix.test.ts @@ -7,7 +7,7 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; import { target } from "../registry/builder.ts"; import { listTargets } from "../registry/registry.ts"; -import { buildLiveTargetMatrix } from "../registry/run.ts"; +import { buildLiveTargetInventory, buildLiveTargetMatrix } from "../registry/run.ts"; import { resolveRunnerForTarget } from "../registry/runner-routing.ts"; import { liveTargetSupport } from "../registry/runtime-support.ts"; @@ -114,6 +114,24 @@ describe("live E2E target matrix", () => { expectExecutableTypedTargetCoverage(); }); + it("keeps every inert declaration in the audit inventory without scheduling it (#9167)", () => { + const inventory = buildLiveTargetInventory(); + const executable = inventory.filter((entry) => entry.supported); + const inert = inventory.filter((entry) => !entry.supported); + + expect(executable).toEqual(buildLiveTargetMatrix()); + expect(inert.length).toBeGreaterThan(0); + expect( + inert.every( + (entry) => + entry.agentRuntime === "unresolved" && + entry.observableOutcome === "unresolved" && + entry.environmentOrInferenceEndpoint === "unresolved" && + entry.unresolvedReason === "This typed registry declaration has no executable owner", + ), + ).toBe(true); + }); + it("prints a single-line JSON array of supported live E2E targets for --emit-live-matrix", () => { const result = runEmitLiveMatrix(); expect(result.status, result.stderr).toBe(0); diff --git a/test/e2e/support/e2e-workflow-trace.test.ts b/test/e2e/support/e2e-workflow-trace.test.ts index 30a71df98f8..a65d3381301 100644 --- a/test/e2e/support/e2e-workflow-trace.test.ts +++ b/test/e2e/support/e2e-workflow-trace.test.ts @@ -12,7 +12,7 @@ import { validateE2eWorkflowBoundary } from "../../../tools/e2e/workflow-boundar import { readWorkflow } from "../../helpers/e2e-workflow-contract"; type E2eWorkflow = { - jobs: Record> }>; + jobs: Record> }>; }; function validateMutatedWorkflow(mutator: (workflow: E2eWorkflow) => void): string[] { @@ -34,7 +34,15 @@ function liveStep(workflow: E2eWorkflow, name: string): Record return step!; } -describe("e2e workflow live trace boundary", () => { +describe("e2e workflow live job boundary", () => { + it("rejects a live job that hides the semantic matrix label (#9167)", () => { + const errors = validateMutatedWorkflow((workflow) => { + workflow.jobs.live.name = "Live E2E"; + }); + + expect(errors).toContain("live job name must expose the semantic matrix label"); + }); + it.each([ "Configure live E2E trace directory", "Build trusted live E2E timing summary", diff --git a/test/e2e/support/live-vitest-invocation.test.ts b/test/e2e/support/live-vitest-invocation.test.ts index 4e24f759eba..5fe3560e636 100644 --- a/test/e2e/support/live-vitest-invocation.test.ts +++ b/test/e2e/support/live-vitest-invocation.test.ts @@ -152,7 +152,7 @@ describe("buildLiveVitestArgs (#6961)", () => { expect( buildLiveVitestArgs({ testPath: "test/e2e/live/registry-targets.test.ts", - selector: "^ubuntu-repo-cloud-openclaw$", + selector: "^ubuntu-repo-cloud-openclaw:", }), ).toEqual([ "vitest", @@ -161,7 +161,7 @@ describe("buildLiveVitestArgs (#6961)", () => { "e2e-live", "test/e2e/live/registry-targets.test.ts", "-t", - "^ubuntu-repo-cloud-openclaw$", + "^ubuntu-repo-cloud-openclaw:", "--silent=false", "--reporter=default", `--reporter=${RISK_SIGNAL_REPORTER}`, diff --git a/test/e2e/support/workflow-plan.test.ts b/test/e2e/support/workflow-plan.test.ts index 0ce35ea6b84..b12b1a3a409 100644 --- a/test/e2e/support/workflow-plan.test.ts +++ b/test/e2e/support/workflow-plan.test.ts @@ -33,7 +33,7 @@ import { writeE2eWorkflowPlanCiOutput, } from "../../../tools/e2e/workflow-plan.mts"; import { REPO_ROOT } from "../fixtures/paths.ts"; -import { buildLiveTargetMatrix } from "../registry/run.ts"; +import { buildLiveTargetInventory, buildLiveTargetMatrix } from "../registry/run.ts"; const PLANNER_CLI = path.join(REPO_ROOT, "tools", "e2e", "workflow-plan.mts"); const TSX = path.join(REPO_ROOT, "node_modules", ".bin", "tsx"); @@ -1204,7 +1204,18 @@ describe("E2E workflow plan", () => { "| `llama-cpp-dgx-spark-qualification` | unresolved | Exact NemoClaw-built llama.cpp image produces protected DGX Spark evidence | NVIDIA DGX Spark GB10; local llama.cpp inference | Explicit dispatch only; excluded from the default release matrix | The protected plan can enable or skip its OpenClaw subqualification |", ); expect(complete.stdout).toContain("### Unsupported or unresolved typed declarations"); - expect(complete.stdout).toMatch(/The \d+ inert typed declarations above/); + const inertDeclarations = buildLiveTargetInventory().filter((row) => !row.supported); + expect(complete.stdout).toContain( + `The ${inertDeclarations.length} inert typed declarations above`, + ); + expect(complete.stdout).toContain( + inertDeclarations + .map( + (row) => + `| \`${row.id}\` | ${row.agentRuntime} | ${row.observableOutcome} | ${row.environmentOrInferenceEndpoint} | ${row.supportReasons.join("; ")} |`, + ) + .join("\n"), + ); expect(complete.stdout).toContain("#8285"); expect(complete.stdout).toContain("#8286"); }); diff --git a/tools/e2e/execution-coverage.mts b/tools/e2e/execution-coverage.mts index bfa86795b81..65fb9d73721 100644 --- a/tools/e2e/execution-coverage.mts +++ b/tools/e2e/execution-coverage.mts @@ -116,3 +116,7 @@ export function validateE2eExecutionRows( export function e2eExecutionLabel(row: E2eExecutionRow): string { return row.variant === "" ? row.id : `${row.id} / ${row.variant}`; } + +export function e2eExecutionTitle(metadata: E2eExecutionMetadata): string { + return `${metadata.observableOutcome} [${metadata.agentRuntime}; ${metadata.environmentOrInferenceEndpoint}]`; +} diff --git a/tools/e2e/workflow-boundary.mts b/tools/e2e/workflow-boundary.mts index c329d7a4406..55c37ea89c9 100644 --- a/tools/e2e/workflow-boundary.mts +++ b/tools/e2e/workflow-boundary.mts @@ -2531,6 +2531,9 @@ export function validateE2eWorkflow(workflowValue: unknown): string[] { const liveTargets = asRecord(jobs["live"]); if (Object.keys(liveTargets).length === 0) errors.push("workflow missing live job"); + if (liveTargets.name !== "${{ matrix.label }}") { + errors.push("live job name must expose the semantic matrix label"); + } if (liveTargets["runs-on"] !== "${{ matrix.runner }}") { errors.push("live job must run on the matrix runner"); } @@ -2700,7 +2703,7 @@ export function validateE2eWorkflow(workflowValue: unknown): string[] { } requireRunContains(errors, runVitest, "tools/e2e/live-vitest-invocation.mts run --test-path"); requireRunContains(errors, runVitest, "test/e2e/live/registry-targets.test.ts"); - requireRunContains(errors, runVitest, '"^${TARGET_ID}$"'); + requireRunContains(errors, runVitest, '"^${TARGET_ID}:"'); const sanitizeTrace = requireStep(errors, steps, "Build trusted live E2E timing summary"); const sanitizeTraceEnv = asRecord(sanitizeTrace?.env); diff --git a/tools/e2e/workflow-plan.mts b/tools/e2e/workflow-plan.mts index 43670a7aa4c..0c4245085dd 100644 --- a/tools/e2e/workflow-plan.mts +++ b/tools/e2e/workflow-plan.mts @@ -6,7 +6,11 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; import { isDeepStrictEqual } from "node:util"; -import { buildLiveTargetMatrix, type LiveTargetMatrixEntry } from "../../test/e2e/registry/run.ts"; +import { + buildLiveTargetInventory, + buildLiveTargetMatrix, + type LiveTargetMatrixEntry, +} from "../../test/e2e/registry/run.ts"; import { buildRiskPlan } from "../advisors/risk-plan.mts"; import { type CredentialFreeTestMatrixRow, @@ -842,7 +846,7 @@ export function renderE2eWorkflowPlanSummary( const explicitOnlyRows = inventory.coverageRows.filter((row) => plan.explicitOnlyJobs.includes(row.id), ); - const unsupportedDeclarations = plan.matrix.filter((row) => !row.supported); + const unsupportedDeclarations = buildLiveTargetInventory().filter((row) => !row.supported); const outcomeRows = new Map(); for (const row of plan.coverageMatrix) { const rows = outcomeRows.get(row.observableOutcome) ?? []; From 37ce465d76e35f1001e2a08c1431da30fc669d33 Mon Sep 17 00:00:00 2001 From: Rebecca Sliter <571084+rsliter@users.noreply.github.com> Date: Tue, 18 Aug 2026 12:37:19 -0700 Subject: [PATCH 2/4] refactor(e2e): separate audit inventory from execution matrix Signed-off-by: Rebecca Sliter <571084+rsliter@users.noreply.github.com> --- test/e2e/registry/run.ts | 40 ++++++++++++------- .../e2e-live-registry-discovery.test.ts | 15 +++++++ test/e2e/support/e2e-matrix.test.ts | 20 +--------- test/e2e/support/workflow-plan.test.ts | 12 ++++-- 4 files changed, 49 insertions(+), 38 deletions(-) diff --git a/test/e2e/registry/run.ts b/test/e2e/registry/run.ts index e8612647679..906351f3b7d 100644 --- a/test/e2e/registry/run.ts +++ b/test/e2e/registry/run.ts @@ -4,7 +4,7 @@ import { realpathSync } from "node:fs"; import { fileURLToPath } from "node:url"; -import type { E2eAgentRuntime } from "../../../tools/e2e/execution-coverage.mts"; +import type { E2eExecutionMetadata } from "../../../tools/e2e/execution-coverage.mts"; import { listTargets, requireTargets } from "./registry.ts"; import { resolveRunnerForTarget } from "./runner-routing.ts"; @@ -22,12 +22,13 @@ interface Args { targets: string[]; } -export interface LiveTargetMatrixEntry { +export interface LiveTargetInventoryEntry extends E2eExecutionMetadata { id: string; - agentRuntime: E2eAgentRuntime; - observableOutcome: string; - environmentOrInferenceEndpoint: string; - unresolvedReason: string; + supported: boolean; + supportReasons: string[]; +} + +export interface LiveTargetMatrixEntry extends LiveTargetInventoryEntry { runner: string; label: string; platform: string; @@ -37,8 +38,6 @@ export interface LiveTargetMatrixEntry { expectedStateId: string; suites: string[]; requiredSecrets: string[]; - supported: boolean; - supportReasons: string[]; pendingRuntimeSuites: string[]; } @@ -87,10 +86,8 @@ function liveMatrixEntry( support: LiveTargetSupport, ): LiveTargetMatrixEntry { const { runner } = resolveRunnerForTarget(target); - const executionCoverage = liveTargetExecutionCoverage(target, support); return { - id: target.id, - ...executionCoverage, + ...liveTargetInventoryEntry(target, support), runner, label: liveTargetTestTitle(target, support), platform: target.environment?.platform ?? "unknown", @@ -100,19 +97,32 @@ function liveMatrixEntry( expectedStateId: target.expectedStateId ?? "", suites: target.suiteIds ?? [], requiredSecrets: target.requiredSecrets ?? [], + pendingRuntimeSuites: support.pendingRuntimeSuites, + }; +} + +export function liveTargetInventoryEntry( + target: TargetDefinition, + support = liveTargetSupport(target), +): LiveTargetInventoryEntry { + return { + id: target.id, + ...liveTargetExecutionCoverage(target, support), supported: support.supported, supportReasons: support.reasons, - pendingRuntimeSuites: support.pendingRuntimeSuites, }; } -export function buildLiveTargetInventory(): LiveTargetMatrixEntry[] { - return listTargets().map((target) => liveMatrixEntry(target, liveTargetSupport(target))); +export function buildLiveTargetInventory(): LiveTargetInventoryEntry[] { + return listTargets().map((target) => liveTargetInventoryEntry(target)); } export function buildLiveTargetMatrix(ids: string[] = []): LiveTargetMatrixEntry[] { if (ids.length === 0) { - return buildLiveTargetInventory().filter((entry) => entry.supported); + return listTargets().flatMap((target) => { + const support = liveTargetSupport(target); + return support.supported ? [liveMatrixEntry(target, support)] : []; + }); } return requireTargets(ids).map((target) => liveMatrixEntry(target, liveTargetSupport(target))); } diff --git a/test/e2e/support/e2e-live-registry-discovery.test.ts b/test/e2e/support/e2e-live-registry-discovery.test.ts index 41dd8dd774e..2e3aa6ab775 100644 --- a/test/e2e/support/e2e-live-registry-discovery.test.ts +++ b/test/e2e/support/e2e-live-registry-discovery.test.ts @@ -5,6 +5,7 @@ import { describe, expect, it } from "vitest"; import { buildLiveTargetRunPlan } from "../live/run-plan.ts"; import { target } from "../registry/builder.ts"; +import { liveTargetInventoryEntry } from "../registry/run.ts"; import { liveTargetSupport } from "../registry/runtime-support.ts"; import type { TargetDefinition, TargetEnvironment } from "../registry/types.ts"; @@ -79,6 +80,20 @@ describe("live target registry discovery support", () => { }); }); + it("reports a missing-environment declaration without resolving a runner (#9167)", () => { + const declaration = target("synthetic-no-environment").expectedState("synthetic-ready").build(); + + expect(liveTargetInventoryEntry(declaration)).toEqual({ + id: declaration.id, + agentRuntime: "unresolved", + observableOutcome: "unresolved", + environmentOrInferenceEndpoint: "unresolved", + unresolvedReason: "This typed registry declaration has no executable owner", + supported: false, + supportReasons: ["missing environment"], + }); + }); + it("compiles a run plan from synthetic target behavior", () => { const registered = syntheticTarget(); diff --git a/test/e2e/support/e2e-matrix.test.ts b/test/e2e/support/e2e-matrix.test.ts index da2bbe73973..ed5184c6b06 100644 --- a/test/e2e/support/e2e-matrix.test.ts +++ b/test/e2e/support/e2e-matrix.test.ts @@ -7,7 +7,7 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; import { target } from "../registry/builder.ts"; import { listTargets } from "../registry/registry.ts"; -import { buildLiveTargetInventory, buildLiveTargetMatrix } from "../registry/run.ts"; +import { buildLiveTargetMatrix } from "../registry/run.ts"; import { resolveRunnerForTarget } from "../registry/runner-routing.ts"; import { liveTargetSupport } from "../registry/runtime-support.ts"; @@ -114,24 +114,6 @@ describe("live E2E target matrix", () => { expectExecutableTypedTargetCoverage(); }); - it("keeps every inert declaration in the audit inventory without scheduling it (#9167)", () => { - const inventory = buildLiveTargetInventory(); - const executable = inventory.filter((entry) => entry.supported); - const inert = inventory.filter((entry) => !entry.supported); - - expect(executable).toEqual(buildLiveTargetMatrix()); - expect(inert.length).toBeGreaterThan(0); - expect( - inert.every( - (entry) => - entry.agentRuntime === "unresolved" && - entry.observableOutcome === "unresolved" && - entry.environmentOrInferenceEndpoint === "unresolved" && - entry.unresolvedReason === "This typed registry declaration has no executable owner", - ), - ).toBe(true); - }); - it("prints a single-line JSON array of supported live E2E targets for --emit-live-matrix", () => { const result = runEmitLiveMatrix(); expect(result.status, result.stderr).toBe(0); diff --git a/test/e2e/support/workflow-plan.test.ts b/test/e2e/support/workflow-plan.test.ts index b12b1a3a409..4bc52743f6c 100644 --- a/test/e2e/support/workflow-plan.test.ts +++ b/test/e2e/support/workflow-plan.test.ts @@ -33,7 +33,9 @@ import { writeE2eWorkflowPlanCiOutput, } from "../../../tools/e2e/workflow-plan.mts"; import { REPO_ROOT } from "../fixtures/paths.ts"; -import { buildLiveTargetInventory, buildLiveTargetMatrix } from "../registry/run.ts"; +import { listTargets } from "../registry/registry.ts"; +import { buildLiveTargetMatrix } from "../registry/run.ts"; +import { liveTargetSupport } from "../registry/runtime-support.ts"; const PLANNER_CLI = path.join(REPO_ROOT, "tools", "e2e", "workflow-plan.mts"); const TSX = path.join(REPO_ROOT, "node_modules", ".bin", "tsx"); @@ -1204,15 +1206,17 @@ describe("E2E workflow plan", () => { "| `llama-cpp-dgx-spark-qualification` | unresolved | Exact NemoClaw-built llama.cpp image produces protected DGX Spark evidence | NVIDIA DGX Spark GB10; local llama.cpp inference | Explicit dispatch only; excluded from the default release matrix | The protected plan can enable or skip its OpenClaw subqualification |", ); expect(complete.stdout).toContain("### Unsupported or unresolved typed declarations"); - const inertDeclarations = buildLiveTargetInventory().filter((row) => !row.supported); + const inertDeclarations = listTargets() + .map((target) => ({ target, support: liveTargetSupport(target) })) + .filter(({ support }) => !support.supported); expect(complete.stdout).toContain( `The ${inertDeclarations.length} inert typed declarations above`, ); expect(complete.stdout).toContain( inertDeclarations .map( - (row) => - `| \`${row.id}\` | ${row.agentRuntime} | ${row.observableOutcome} | ${row.environmentOrInferenceEndpoint} | ${row.supportReasons.join("; ")} |`, + ({ target, support }) => + `| \`${target.id}\` | unresolved | unresolved | unresolved | ${support.reasons.join("; ")} |`, ) .join("\n"), ); From 1ec2d3131a978565e49249b8ab196f1e92161054 Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Tue, 18 Aug 2026 13:34:35 -0700 Subject: [PATCH 3/4] test: align Dockerfile plan fixture with current schema Signed-off-by: Prekshi Vyas --- src/lib/onboard/dockerfile-patch.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/onboard/dockerfile-patch.test.ts b/src/lib/onboard/dockerfile-patch.test.ts index 70d4adc54e5..be800de6caf 100644 --- a/src/lib/onboard/dockerfile-patch.test.ts +++ b/src/lib/onboard/dockerfile-patch.test.ts @@ -707,7 +707,7 @@ describe("dockerfile patch helpers", () => { { channelId: "telegram", active: true }, ], agentRender: [ - { channelId: "discord", target: "openclaw.json", path: ["channels", "discord"] }, + { agent: "openclaw", channelId: "discord", target: "config", path: ["discord"] }, ], }); const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-dockerfile-plan-")); From 1aa872151c4d4aaf0aa6617a5b3ce3d26bb27157 Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Tue, 18 Aug 2026 14:34:34 -0700 Subject: [PATCH 4/4] test: use supported OpenClaw render target Signed-off-by: Prekshi Vyas --- src/lib/onboard/dockerfile-patch.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/onboard/dockerfile-patch.test.ts b/src/lib/onboard/dockerfile-patch.test.ts index be800de6caf..339857ab616 100644 --- a/src/lib/onboard/dockerfile-patch.test.ts +++ b/src/lib/onboard/dockerfile-patch.test.ts @@ -707,7 +707,7 @@ describe("dockerfile patch helpers", () => { { channelId: "telegram", active: true }, ], agentRender: [ - { agent: "openclaw", channelId: "discord", target: "config", path: ["discord"] }, + { agent: "openclaw", channelId: "discord", target: "openclaw.json", path: ["discord"] }, ], }); const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-dockerfile-plan-"));