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
17 changes: 14 additions & 3 deletions .github/workflows/e2e-parity-compare.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
# the same runner, collects PASS/FAIL per assertion from both, and fails
# the job if any mapped assertion in test/e2e/docs/parity-map.yaml diverges.
#
# Manual-only (workflow_dispatch). Each migration phase dispatches this
# workflow for every scenario it introduces and records zero-divergence
# before marking the phase complete.
# Manual-only (workflow_dispatch). This is also the source of truth for generated
# parity reports: it regenerates the legacy assertion inventory and uploads it
# as a workflow artifact instead of requiring every PR to commit regenerated
# parity inventory churn.

name: E2E / Parity Compare

Expand Down Expand Up @@ -94,6 +95,16 @@ jobs:
- name: Install root dependencies
run: npm ci --ignore-scripts

- name: Generate legacy assertion inventory
run: |
mkdir -p .e2e/parity
npx tsx scripts/e2e/extract-legacy-assertions.ts
cp test/e2e/docs/parity-inventory.generated.json .e2e/parity/parity-inventory.generated.json

- name: Validate parity map
run: |
npx tsx scripts/e2e/check-parity-map.ts --strict

- name: Run legacy script
id: legacy
if: ${{ github.event.inputs.legacy_script != '' }}
Expand Down
98 changes: 10 additions & 88 deletions scripts/e2e/lint-conventions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
* is rejected in suite step scripts; use
* `SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"` and
* walk up.
* - Every `test/e2e/test-*.sh` script MUST have an entry in
* `test/e2e/docs/parity-map.yaml` (Risk #1: guards against new
* legacy scripts landing unmapped).
* - The generated parity inventory MUST match current legacy assertions.
*
* Normal PR lint intentionally excludes legacy parity bookkeeping. Generate and
* validate legacy assertion parity from `.github/workflows/e2e-parity-compare.yaml`
* when producing a parity report.
*
* Invocation:
* tsx scripts/e2e/lint-conventions.ts [--root <repo-root>]
Expand All @@ -36,9 +36,6 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import yaml from "js-yaml";

import { buildLegacyAssertionInventory } from "./extract-legacy-assertions";
import { validateParityMap } from "./check-parity-map";

interface Rule {
id: string;
describe: string;
Expand Down Expand Up @@ -173,53 +170,17 @@ function lintSuiteSteps(root: string): LintFinding[] {
for (const rule of STEP_RULES) {
const msg = rule.test(body);
if (msg) {
findings.push({ file: path.relative(root, file), rule: rule.id, message: msg });
findings.push({
file: path.relative(root, file),
rule: rule.id,
message: msg,
});
}
}
}
return findings;
}

/**
* Read `test/e2e/docs/parity-map.yaml` and return the set of legacy-script
* names that have an entry. Uses a narrow parser to avoid a runtime
* dependency when js-yaml is not available.
*/
function readParityMapScripts(mapFile: string): Set<string> {
const set = new Set<string>();
if (!fs.existsSync(mapFile)) return set;
const text = fs.readFileSync(mapFile, "utf8");
for (const raw of text.split("\n")) {
const m = raw.match(/^\s{2}([\w.\-]+):\s*$/);
if (m) set.add(m[1]);
}
return set;
}

function lintLegacyFrontier(root: string): LintFinding[] {
const findings: LintFinding[] = [];
const e2eDir = path.join(root, "test/e2e");
const mapFile = path.join(e2eDir, "docs", "parity-map.yaml");
const mapped = readParityMapScripts(mapFile);
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(e2eDir, { withFileTypes: true });
} catch {
return findings;
}
for (const ent of entries) {
if (!ent.isFile()) continue;
if (!/^test-.*\.sh$/.test(ent.name)) continue;
if (mapped.has(ent.name)) continue;
findings.push({
file: `test/e2e/${ent.name}`,
rule: "legacy-script-needs-parity-map-entry",
message: `new legacy test/e2e/${ent.name} has no entry in test/e2e/docs/parity-map.yaml (Risk #1)`,
});
}
return findings;
}

function lintRetiredLegacyWrappers(root: string): LintFinding[] {
const findings: LintFinding[] = [];
const mapFile = path.join(root, "test/e2e/docs/parity-map.yaml");
Expand Down Expand Up @@ -255,48 +216,9 @@ function lintRetiredLegacyWrappers(root: string): LintFinding[] {
return findings;
}

function lintParityInventory(root: string): LintFinding[] {
const findings: LintFinding[] = [];
const inventoryPath = path.join(root, "test/e2e/docs/parity-inventory.generated.json");
if (!fs.existsSync(inventoryPath)) {
findings.push({
file: "test/e2e/docs/parity-inventory.generated.json",
rule: "legacy-assertion-inventory-current",
message:
"generated parity inventory is missing; run scripts/e2e/extract-legacy-assertions.ts",
});
return findings;
}

const expected = `${JSON.stringify(buildLegacyAssertionInventory(root), null, 2)}\n`;
const actual = fs.readFileSync(inventoryPath, "utf8");
if (actual !== expected) {
findings.push({
file: "test/e2e/docs/parity-inventory.generated.json",
rule: "legacy-assertion-inventory-current",
message: "generated parity inventory is stale; run scripts/e2e/extract-legacy-assertions.ts",
});
}
return findings;
}

function main(): number {
const { root } = parseArgs(process.argv);
const inventoryPath = path.join(root, "test/e2e/docs/parity-inventory.generated.json");
const parityErrors = fs.existsSync(inventoryPath)
? validateParityMap({ root, strict: false }).map((message) => ({
file: "test/e2e/docs/parity-map.yaml",
rule: "parity-map-schema",
message,
}))
: [];
const findings = [
...lintSuiteSteps(root),
...lintLegacyFrontier(root),
...lintParityInventory(root),
...lintRetiredLegacyWrappers(root),
...parityErrors,
];
const findings = [...lintSuiteSteps(root), ...lintRetiredLegacyWrappers(root)];
if (findings.length === 0) {
return 0;
}
Expand Down
24 changes: 5 additions & 19 deletions test/e2e/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,24 +71,14 @@ unchanged during the migration.

## Legacy assertion inventory

The generated inventory at `test/e2e/docs/parity-inventory.generated.json`
is the auditable source of truth for legacy E2E `PASS:` / `FAIL:`
assertions. Regenerate it after changing any `test/e2e/test-*.sh`
entrypoint or `test/e2e/brev-e2e.test.ts`:
The legacy assertion inventory is generated when `.github/workflows/e2e-parity-compare.yaml` produces a parity report. The workflow uploads `parity-inventory.generated.json` as an artifact under `.e2e/parity/`; normal feature PRs do not commit this generated inventory.

```bash
npx tsx scripts/e2e/extract-legacy-assertions.ts
```

Use `--check` to verify the committed inventory has no drift:
Generate a local inventory when debugging migration coverage:

```bash
npx tsx scripts/e2e/extract-legacy-assertions.ts --check
npx tsx scripts/e2e/extract-legacy-assertions.ts --output /tmp/parity-inventory.generated.json
```

Scripts with no extracted assertions remain listed with a review TODO so
parity gaps are visible in diffs.

`test/e2e/docs/parity-map.yaml` is the assertion-level migration map.
Every inventory assertion must be classified as `mapped`, `deferred`, or
`retired`; strict validation requires zero `unmapped` assertions:
Expand All @@ -114,10 +104,6 @@ describe the required shape; `run-scenario.sh <id> --plan-only`
validates your change without running anything destructive.

When adding a suite assertion, emit or preserve a stable `PASS: <id>` /
`FAIL: <id>` log line, add the legacy assertion mapping if one exists,
regenerate the inventory, and re-run strict parity validation. Platform-
specific scenarios such as GPU, macOS, WSL, Brev, or DGX Spark must also
list `runner_requirements` in `scenarios.yaml`.
`FAIL: <id>` log line, add the legacy assertion mapping if one exists, and use the dedicated parity workflow to regenerate inventory/report artifacts. Platform-specific scenarios such as GPU, macOS, WSL, Brev, or DGX Spark must also list `runner_requirements` in `scenarios.yaml`.

New legacy-style `test-*.sh` scripts are blocked by
`scripts/e2e/lint-conventions.ts` — migrate into the matrix instead.
Prefer new scenario-matrix coverage over new legacy-style `test-*.sh` scripts. Normal PR lint no longer blocks feature work on global parity-map bookkeeping; use the parity workflow when intentionally advancing migration coverage.
Loading
Loading