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
19 changes: 19 additions & 0 deletions test/e2e-scenario/docs/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,25 @@ Do not add migration status tables, per-legacy-script checklists, temporary
coverage counts, or owner queues to this file. Put those in the issue or PR
that owns the work instead.

The one repo-local exception is the machine-readable deletion gate inventory at
`test/e2e-scenario/migration/legacy-inventory.json`. Keep that file focused on
script-level migration state that prevents accidental legacy E2E deletion. It
must cover every direct legacy shell entrypoint under `test/e2e/test-*.sh`,
plus any explicitly retained bridge entrypoints such as Brev. It is not a
progress dashboard or owner queue:

- `not-migrated`: legacy coverage still has no equivalent Vitest scenario.
- `bridge-probe`: coverage is temporarily represented by a bridge path.
- `covered`: equivalent Vitest live scenario coverage exists.
- `retired`: maintainers agreed the legacy coverage is no longer required.

Do not set `deletionReady: true` unless the entry is `covered` or `retired` and
the deletion approval is recorded through #4357.

After #4357 completes final legacy E2E reconciliation, remove the inventory if
there are no remaining legacy entrypoints to guard. If maintainers keep it, keep
it as an audit artifact rather than as a living migration checklist.

## What to migrate next

When moving behavior from a legacy E2E script into the scenario framework:
Expand Down
7 changes: 7 additions & 0 deletions test/e2e-scenario/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,13 @@ audit-coverage work is tracked by the #4347–#4357 issue set, with focused
follow-ups such as #4378 for specific drift fixes. The execution-model decision
is tracked in #4941.

The narrow repo-local exception is
`test/e2e-scenario/migration/legacy-inventory.json`, a machine-readable deletion
gate for direct legacy `test/e2e/test-*.sh` entrypoints and explicit bridge
entrypoints. It should prevent accidental deletions, not become a parallel
status table. Remove it after #4357 completes final legacy E2E reconciliation,
or keep it only as an audit artifact if maintainers still need that record.

The old workflow-level parity report has been removed. Use scenario framework
tests, the coverage report, PR review, and the audit issues to decide what to
migrate next.
Expand Down
123 changes: 123 additions & 0 deletions test/e2e-scenario/framework-tests/e2e-migration-inventory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import fs from "node:fs";
import path from "node:path";

import { describe, expect, it } from "vitest";

const INVENTORY_PATH = path.resolve(
import.meta.dirname,
"../migration/legacy-inventory.json",
);
const REPO_ROOT = path.resolve(import.meta.dirname, "../../..");
const LEGACY_E2E_DIR = path.join(REPO_ROOT, "test/e2e");
const EXPECTED_STATUS_VALUES = ["not-migrated", "bridge-probe", "covered", "retired"] as const;

type MigrationStatus = "not-migrated" | "bridge-probe" | "covered" | "retired";

interface LegacyInventoryEntry {
legacyScript: string;
domain: string;
ownerIssue: string;
status: MigrationStatus;
targetVitestScenarios: string[];
bridgeProbes: string[];
retiredReason: string;
deletionReady: boolean;
deletionApprovalIssue?: string;
notes: string;
}

interface LegacyInventory {
version: number;
statusValues: MigrationStatus[];
deletionReadiness: {
requires: string[];
};
entries: LegacyInventoryEntry[];
}

function loadInventory(): LegacyInventory {
return JSON.parse(fs.readFileSync(INVENTORY_PATH, "utf8")) as LegacyInventory;
}

function repoPathExists(repoRelativePath: string): boolean {
expect(path.isAbsolute(repoRelativePath)).toBe(false);
expect(repoRelativePath).not.toContain("..");

return fs.existsSync(path.join(REPO_ROOT, repoRelativePath));
}

function listLegacyShellEntrypoints(): string[] {
return fs
.readdirSync(LEGACY_E2E_DIR)
.filter((name) => /^test-.*\.sh$/.test(name))
.map((name) => `test/e2e/${name}`)
.sort();
}

describe("E2E migration inventory deletion gates", () => {
it("uses a constrained migration vocabulary with owning issues", () => {
const inventory = loadInventory();
const statuses = new Set(inventory.statusValues);
const legacyScripts = new Set<string>();

expect(inventory.version).toBe(1);
expect(inventory.statusValues).toEqual([...EXPECTED_STATUS_VALUES]);
expect(inventory.deletionReadiness.requires.length).toBeGreaterThan(0);
expect(inventory.entries.length).toBeGreaterThan(0);

for (const entry of inventory.entries) {
expect(statuses.has(entry.status)).toBe(true);
expect(entry.legacyScript).not.toBe("");
expect(repoPathExists(entry.legacyScript)).toBe(true);
expect(legacyScripts.has(entry.legacyScript)).toBe(false);
legacyScripts.add(entry.legacyScript);
expect(entry.domain).not.toBe("");
expect(entry.ownerIssue).toMatch(/^#(?:3588|434[7-9]|435[0-7]|4941)$/);
expect(entry.notes).not.toBe("");
}
});

it("covers every current direct legacy shell entrypoint", () => {
const inventory = loadInventory();
const inventoriedShellScripts = inventory.entries
.map((entry) => entry.legacyScript)
.filter((legacyScript) => /^test\/e2e\/test-.+\.sh$/.test(legacyScript))
.sort();

expect(inventoriedShellScripts).toEqual(listLegacyShellEntrypoints());
});

it("requires coverage, retirement evidence, and #4357 approval before deletion", () => {
const inventory = loadInventory();

for (const entry of inventory.entries) {
if (entry.status === "covered") {
expect(entry.targetVitestScenarios.length).toBeGreaterThan(0);
for (const scenario of entry.targetVitestScenarios) {
expect(scenario).toMatch(/^test\/e2e-scenario\/live\/.+\.test\.ts$/);
expect(repoPathExists(scenario)).toBe(true);
}
}

if (entry.status === "bridge-probe") {
expect(entry.bridgeProbes.length).toBeGreaterThan(0);
for (const probe of entry.bridgeProbes) {
expect(repoPathExists(probe)).toBe(true);
}
}

if (entry.status === "retired") {
expect(entry.retiredReason).not.toBe("");
}

if (entry.deletionReady) {
expect(["covered", "retired"]).toContain(entry.status);
expect(entry.deletionApprovalIssue).toBe("#4357");
expect(entry.status === "retired" ? entry.retiredReason : entry.targetVitestScenarios.length).toBeTruthy();
}
}
});
});
Loading
Loading