diff --git a/test/e2e/README.md b/test/e2e/README.md index 4d0e2577fda..858cadcb0e8 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -638,10 +638,20 @@ Changes to `src/lib/actions/sandbox/status-snapshot.ts` select the exact delivery-recovery changes bound to the reboot simulation that independently probes the restored gateway and host forwarding. Every internal revision with selected jobs or targets automatically dispatches -its deterministic plan after eligible PR CI passes. This behavior includes all -internal E2E control-plane changes. Internal dispatch does not use -`approve-e2e` or a maintainer role check. If no job or target is selected, the -required check passes without an E2E run. +its deterministic plan after eligible PR CI passes. Automatic PR E2E planning +and PR Review Advisor E2E recommendations temporarily omit only +`jetson-nvmap-gpu`. This behavior includes all internal E2E control-plane +changes. Internal dispatch does not use +`approve-e2e` or a maintainer role check. If no job or target remains after the +Jetson omission, the required check passes without an E2E run. + +The `jetson-nvmap-gpu` job remains available through an explicit manual `E2E +main` dispatch with `jobs=jetson-nvmap-gpu`. Before setting +`allow_jetson_runner_queue=true`, a repository administrator must confirm that +the Jetson runner is online in the authoritative repository runner inventory. +Restore Jetson automatic planning and PR Review Advisor recommendations only +after the Colossus-backed Jetson runner path is available and its online state +can be confirmed before queueing. When selected E2E fails, a later internal PR commit starts a new dispatch after eligible PR CI passes for that commit. The dispatch runs the complete deterministic plan for the later commit, not only the selection that failed. diff --git a/test/pr-e2e-gate-jetson-exclusion.test.ts b/test/pr-e2e-gate-jetson-exclusion.test.ts new file mode 100644 index 00000000000..355b06199ee --- /dev/null +++ b/test/pr-e2e-gate-jetson-exclusion.test.ts @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { expect, it } from "vitest"; +import { + normalizeE2eCoverageResult, + normalizeE2eTargetAdvisorResult, + trustedE2eRecommendationInventory, +} from "../tools/advisors/e2e-recommendations.mts"; +import { buildRiskPlan, riskPlanRequiredJobIds } from "../tools/advisors/risk-plan.mts"; +import { + focusedPrGateE2eJobsForChangedFiles, + validateRiskPlan, +} from "../tools/e2e/pr-e2e-gate.mts"; +import { focusedE2eJobsForChangedFiles } from "../tools/e2e/workflow-boundary.mts"; + +it("omits the guarded Jetson job from automatic PR E2E plans (#7610)", () => { + const changedFiles = ["test/e2e/live/jetson-nvmap-gpu.test.ts"]; + + expect(focusedE2eJobsForChangedFiles(changedFiles)).toEqual([ + { id: "jetson-nvmap-gpu", matchedFiles: changedFiles }, + ]); + const plan = buildRiskPlan({ + headSha: "a".repeat(40), + changedFiles, + focusedE2eJobs: focusedPrGateE2eJobsForChangedFiles(changedFiles), + }); + + expect(focusedPrGateE2eJobsForChangedFiles(changedFiles)).toEqual([]); + expect(validateRiskPlan(plan, new Set(riskPlanRequiredJobIds(plan)))).toEqual(plan); + expect(riskPlanRequiredJobIds(plan)).not.toContain("jetson-nvmap-gpu"); + expect(riskPlanRequiredJobIds(plan)).toEqual( + expect.arrayContaining(["cloud-inference", "cloud-onboard", "security-posture"]), + ); +}); + +it("omits the guarded Jetson job from advisor E2E recommendations (#7610)", () => { + const changedFiles = ["test/e2e/live/jetson-nvmap-gpu.test.ts"]; + const metadata = { baseRef: "origin/main", headRef: "HEAD", changedFiles }; + const riskPlan = buildRiskPlan({ + headSha: "a".repeat(40), + changedFiles, + focusedE2eJobs: focusedPrGateE2eJobsForChangedFiles(changedFiles), + }); + const modelGuidance = { + requiredTests: [{ id: "jetson-nvmap-gpu", reason: "Model-selected coverage." }], + optionalTests: [{ id: "jetson-nvmap-gpu", reason: "Model-selected coverage." }], + required: [ + { + id: "jetson-nvmap-gpu", + workflow: "e2e.yaml", + selectorType: "job", + reason: "Model-selected job.", + }, + ], + optional: [], + confidence: "high", + }; + + const coverage = normalizeE2eCoverageResult(modelGuidance, metadata, riskPlan); + const targets = normalizeE2eTargetAdvisorResult(modelGuidance, metadata, { riskPlan }); + const dynamicallyDiscovered = normalizeE2eTargetAdvisorResult(modelGuidance, metadata, { + riskPlan, + changedFileSources: { + [changedFiles[0]]: "// @module-tag e2e/credential-free\n", + }, + }); + + expect(trustedE2eRecommendationInventory().allowedJobIds).not.toContain("jetson-nvmap-gpu"); + expect(coverage.requiredTests.map((item) => item.id)).not.toContain("jetson-nvmap-gpu"); + expect(coverage.optionalTests.map((item) => item.id)).not.toContain("jetson-nvmap-gpu"); + expect(targets.required.map((item) => item.id)).not.toContain("jetson-nvmap-gpu"); + expect(targets.optional.map((item) => item.id)).not.toContain("jetson-nvmap-gpu"); + expect(dynamicallyDiscovered.changedCredentialFreeTests).toEqual([]); + expect(dynamicallyDiscovered.required.map((item) => item.id)).not.toContain("jetson-nvmap-gpu"); +}); diff --git a/tools/advisors/e2e-recommendations.mts b/tools/advisors/e2e-recommendations.mts index 62bde0712f0..566d48162e4 100644 --- a/tools/advisors/e2e-recommendations.mts +++ b/tools/advisors/e2e-recommendations.mts @@ -11,7 +11,7 @@ import { liveTargetSupport } from "../../test/e2e/registry/runtime-support.ts"; import { moduleTagDeclarations } from "../e2e/module-tags.mts"; import { containsCommandShapedE2eText } from "./e2e-text.mts"; import { enumValue, recordItems, stringOrUndefined } from "./json.mts"; -import { buildRiskPlan, type RiskPlan } from "./risk-plan.mts"; +import { buildRiskPlan, isPrE2ePlanningJob, type RiskPlan } from "./risk-plan.mts"; const E2E_WORKFLOW = "e2e.yaml"; const E2E_WORKFLOW_PATH = `.github/workflows/${E2E_WORKFLOW}`; @@ -191,12 +191,13 @@ export function normalizeE2eCoverageResult( } function deterministicCoverageTests(changedFiles: string[], riskPlan: RiskPlan): E2eCoverageTest[] { - const tests: E2eCoverageTest[] = [...riskPlan.requiredJobs, ...riskPlan.requiredTargets].map( - (selection) => ({ - id: selection.id, - reason: selection.reasons.join(" "), - }), - ); + const tests: E2eCoverageTest[] = [ + ...riskPlan.requiredJobs.filter((job) => isPrE2ePlanningJob(job.id)), + ...riskPlan.requiredTargets, + ].map((selection) => ({ + id: selection.id, + reason: selection.reasons.join(" "), + })); if (requiresCloudOnboardE2e(changedFiles) && !tests.some((test) => test.id === "cloud-onboard")) { tests.push({ id: "cloud-onboard", @@ -352,7 +353,9 @@ function buildE2eTargetNormalizationContext( const trustedWorkflowText = readTrustedE2eWorkflowText(); const trustedCredentialFreeTests = discoverTrustedCredentialFreeTests(); const allowedJobIds = new Set( - extractAllowedE2eJobIds(trustedWorkflowText, trustedCredentialFreeTests), + extractAllowedE2eJobIds(trustedWorkflowText, trustedCredentialFreeTests).filter( + isPrE2ePlanningJob, + ), ); // The analyzed workflow is untrusted input. It may explain why a changed test is // unwired, but it must never introduce a selector that CI could later dispatch. @@ -380,7 +383,7 @@ function buildE2eTargetNormalizationContext( for (const [file, project] of changedCredentialFreeProjects) { const source = changedSource(file, changedFileSources); const row = source ? credentialFreeTestRow(file, source) : undefined; - if (!row || !project) continue; + if (!row || !project || !isPrE2ePlanningJob(row.id)) continue; addMapValue(liveTestToJobs, row.file, row.id); allowedJobIds.add(row.id); changedCredentialFreeTests.push(row); @@ -454,7 +457,7 @@ function trustedAllowedJobIds(): string[] { return extractAllowedE2eJobIds( readTrustedE2eWorkflowText(), discoverTrustedCredentialFreeTests(), - ); + ).filter(isPrE2ePlanningJob); } function extractAllowedE2eJobIds( diff --git a/tools/advisors/risk-plan.mts b/tools/advisors/risk-plan.mts index 1727be449c1..c69d2f7d81c 100644 --- a/tools/advisors/risk-plan.mts +++ b/tools/advisors/risk-plan.mts @@ -11,6 +11,7 @@ export const PR_E2E_TYPED_TARGET_IDS = [ ] as const; const PR_E2E_TYPED_TARGET_ID_SET = new Set(PR_E2E_TYPED_TARGET_IDS); +const PR_E2E_PLANNING_OMITTED_JOB_IDS = new Set(["jetson-nvmap-gpu"]); const DEEPAGENTS_HEADLESS_INFERENCE_CHECK = "test/e2e/e2e-cloud-experimental/checks/07-deepagents-code-headless-inference.sh"; const DEEPAGENTS_CODE_RUNTIME_ROOT = "agents/langchain-deepagents-code/"; @@ -153,6 +154,12 @@ export function isPrE2eTypedTargetId(value: string): boolean { return PR_E2E_TYPED_TARGET_ID_SET.has(value); } +export function isPrE2ePlanningJob(value: string): boolean { + // Automatic PR planning cannot confirm an online self-hosted Jetson runner. + // Remove this exclusion after the Colossus-backed runner path can make that confirmation. + return !PR_E2E_PLANNING_OMITTED_JOB_IDS.has(value); +} + export function focusedPrE2eTargetsForChangedFiles( changedFiles: readonly string[], ): TrustedFocusedE2eTarget[] { diff --git a/tools/e2e/pr-e2e-gate.mts b/tools/e2e/pr-e2e-gate.mts index ef87911bf11..a317b6066c1 100755 --- a/tools/e2e/pr-e2e-gate.mts +++ b/tools/e2e/pr-e2e-gate.mts @@ -15,6 +15,7 @@ import { githubApi, githubApiWithResponse, githubRestPaginated } from "../adviso import { parseArgs } from "../advisors/io.mts"; import { buildRiskPlan, + isPrE2ePlanningJob, isPrE2eTypedTargetId, RISK_PLAN_VERSION, type RiskPlan, @@ -690,7 +691,7 @@ export function validateRiskPlan(value: unknown, allowedJobs: ReadonlySet { + return focusedE2eJobsForChangedFiles(changedFiles, inventory).filter((selection) => + isPrE2ePlanningJob(selection.id), + ); +} + function riskPlanSelectionIds(plan: RiskPlan): string[] { return [...riskPlanRequiredJobIds(plan), ...riskPlanRequiredTargetIds(plan)]; } @@ -3135,7 +3145,7 @@ export async function startPrGate( buildRiskPlan({ headSha: command.headSha, changedFiles, - focusedE2eJobs: focusedE2eJobsForChangedFiles(changedFiles, inventory), + focusedE2eJobs: focusedPrGateE2eJobsForChangedFiles(changedFiles, inventory), }), allowedJobs, ); @@ -3257,7 +3267,7 @@ async function startAuthorizedPrGate(command: AuthorizedE2ECommand): Promise + isPrE2ePlanningJob(selection.id), + ), }); const riskyAreas = [ ...detectRiskyAreas(options.changedFiles),