Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d0677ed
test(e2e): allow whitelisted lifecycle profiles in live runner
jyaunches Jun 9, 2026
a220f87
test(e2e): add host-side registry and Docker container probes
jyaunches Jun 9, 2026
0516112
test(e2e): add LifecyclePhaseFixture for post-reboot recovery
jyaunches Jun 9, 2026
e276ef3
Merge branch 'main' into e2e-scenario-lifecycle-fixture-prereq
cv Jun 9, 2026
5d132a7
test(e2e): add post-reboot-recovery scenario as #4423 regression guard
jyaunches Jun 9, 2026
34466dd
chore(e2e): scaffold inventory internals migration draft
cv Jun 9, 2026
54ac3e3
chore(e2e): scaffold fan-out draft 05
cv Jun 9, 2026
57b84ae
chore(e2e): scaffold fan-out draft 06
cv Jun 9, 2026
06b84f3
chore(e2e): apply biome formatting
jyaunches Jun 9, 2026
839b7bf
chore(e2e): drop accidental node_modules symlink
jyaunches Jun 9, 2026
5ed1735
test(e2e): extend migration inventory to runner internals
cv Jun 9, 2026
f3e6f16
merge existing draft PR history
cv Jun 9, 2026
a08d680
merge updated lifecycle fixture prerequisite
cv Jun 9, 2026
24b7e89
chore(e2e): apply static formatting
cv Jun 9, 2026
e268958
Merge remote-tracking branch 'origin/main' into codex/e2e-fanout-01-i…
cv Jun 9, 2026
d47a723
chore(e2e): scaffold fan-out draft 05
cv Jun 9, 2026
9c82884
test(e2e): add inference runtime helpers
cv Jun 9, 2026
ff537c4
chore(e2e): preserve inference helper branch history
cv Jun 9, 2026
1ffe608
test(e2e): validate inference model helper responses
cv Jun 9, 2026
b23308d
test(e2e): redact inference helper headers
cv Jun 9, 2026
05b6d1b
test(e2e): harden provider runtime probes
cv Jun 9, 2026
0466e26
chore(e2e): sync cloud inference migration branch
cv Jun 9, 2026
1d3c444
test(e2e): run cloud inference suites in Vitest
cv Jun 9, 2026
b467cb8
test(e2e): narrow inference migration inventory status
cv Jun 9, 2026
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
14 changes: 9 additions & 5 deletions test/e2e-scenario/docs/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,18 +84,22 @@ 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
deletion-readiness evidence 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:
plus any explicitly retained bridge entrypoints such as Brev. It also tracks
coarse internal legacy runner surfaces such as the YAML/bash scenario workers,
validation suites, TypeScript shell-runner orchestrators, and runtime helper
libraries so those surfaces cannot be removed without #4357 evidence. 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.
Do not set `deletionReady: true` on a script entry or internal surface unless
the record 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
Expand Down
9 changes: 6 additions & 3 deletions test/e2e-scenario/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,12 @@ 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.
entrypoints. It also tracks coarse internal legacy runner surfaces such as
scenario shell workers, validation suites, shell-runner orchestrators, and
runtime helper libraries so they cannot be removed without #4357 evidence. 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
Expand Down
52 changes: 52 additions & 0 deletions test/e2e-scenario/framework-tests/e2e-clients.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,58 @@ describe("E2E fixture clients", () => {
});
});

it("provider client posts JSON bodies with --data-raw", async () => {
const runner = new FakeRunner();
runner.stdout = JSON.stringify({ ok: true });
const provider = new ProviderClient(runner);
const endpoint = trustedProviderEndpoint("https://api.example.test/v1/chat/completions", {
allowedHosts: ["api.example.test"],
});

await expect(
provider.requestJson(endpoint, {
body: '{"messages":[]}',
curlMaxTimeSeconds: 5,
headers: ["Content-Type: application/json"],
}),
).resolves.toMatchObject({ json: { ok: true } });

expect(runner.calls[0]?.args).toEqual([
"-fsS",
"--max-time",
"5",
"-H",
"Content-Type: application/json",
"--data-raw",
'{"messages":[]}',
"https://api.example.test/v1/chat/completions",
]);
});

it("provider client rejects curl-sensitive request options before command construction", async () => {
const endpoint = trustedProviderEndpoint("https://api.example.test/v1/models", {
allowedHosts: ["api.example.test"],
});

for (const options of [
{ body: "@/etc/passwd" },
{ headers: ["@/tmp/headers"] },
{ headers: ["Authorization: Bearer token\nX-Leak: value"] },
{ curlMaxTimeSeconds: 0 },
{ curlMaxTimeSeconds: -1 },
{ curlMaxTimeSeconds: Number.NaN },
{ curlMaxTimeSeconds: Number.POSITIVE_INFINITY },
]) {
const runner = new FakeRunner();
const provider = new ProviderClient(runner);

await expect(provider.requestJson(endpoint, options)).rejects.toThrow(
/@file|CR or LF|finite positive/,
);
expect(runner.calls).toEqual([]);
}
});

it("provider client does not follow redirects after endpoint validation", async () => {
const runner = new FakeRunner();
runner.stdout = JSON.stringify({ ok: true });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ describe("live Vitest registry discovery support", () => {

expect(scenario).toBeTruthy();
expect(liveScenarioSupport(scenario!).supported).toBe(true);
expect(liveScenarioSupport(scenario!).pendingRuntimeSuites).toEqual([
"smoke",
expect(liveScenarioSupport(scenario!).runtimeSuites).toEqual([
"inference",
"credentials",
"inference-routing",
]);
expect(liveScenarioSupport(scenario!).pendingRuntimeSuites).toEqual(["smoke", "credentials"]);
});

it("keeps unsupported onboarding profiles skipped with a concrete reason", () => {
Expand Down
148 changes: 124 additions & 24 deletions test/e2e-scenario/framework-tests/e2e-migration-inventory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ const INVENTORY_PATH = path.resolve(import.meta.dirname, "../migration/legacy-in
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;
const INTERNAL_SURFACE_ROOTS = [
"test/e2e-scenario/nemoclaw_scenarios",
"test/e2e-scenario/onboarding_assertions",
"test/e2e-scenario/runtime/lib",
"test/e2e-scenario/runtime/reports",
"test/e2e-scenario/scenarios/orchestrators",
"test/e2e-scenario/validation_suites",
] as const;

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

Expand All @@ -26,13 +34,29 @@ interface LegacyInventoryEntry {
notes: string;
}

interface LegacyInternalSurface {
id: string;
paths: string[];
domain: string;
ownerIssue: string;
status: MigrationStatus;
replacementSurface: string;
targetVitestScenarios: string[];
bridgeProbes: string[];
retiredReason: string;
deletionReady: boolean;
deletionApprovalIssue?: string;
notes: string;
}

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

function loadInventory(): LegacyInventory {
Expand All @@ -54,16 +78,86 @@ function listLegacyShellEntrypoints(): string[] {
.sort();
}

function listRepoFilesUnder(repoRelativeDir: string): string[] {
const absoluteDir = path.join(REPO_ROOT, repoRelativeDir);
const files: string[] = [];
const visit = (dir: string) => {
for (const dirent of fs.readdirSync(dir, { withFileTypes: true })) {
const absolutePath = path.join(dir, dirent.name);
if (dirent.isDirectory()) {
visit(absolutePath);
} else if (dirent.isFile()) {
files.push(path.relative(REPO_ROOT, absolutePath).split(path.sep).join("/"));
}
}
};
visit(absoluteDir);
return files.sort();
}

function isCoveredByInventoryPath(filePath: string, inventoryPath: string): boolean {
return filePath === inventoryPath || filePath.startsWith(`${inventoryPath}/`);
}

function expectPathListIsRepoRelative(paths: readonly string[]) {
expect(paths.length).toBeGreaterThan(0);
for (const repoRelativePath of paths) {
expect(repoRelativePath).not.toBe("");
expect(repoPathExists(repoRelativePath)).toBe(true);
}
}

function expectMigrationRecordDeletionGate(
record: Pick<
LegacyInventoryEntry | LegacyInternalSurface,
| "status"
| "targetVitestScenarios"
| "bridgeProbes"
| "retiredReason"
| "deletionReady"
| "deletionApprovalIssue"
>,
) {
if (record.status === "covered") {
expect(record.targetVitestScenarios.length).toBeGreaterThan(0);
for (const scenario of record.targetVitestScenarios) {
expect(scenario).toMatch(/^test\/e2e-scenario\/live\/.+\.test\.ts$/);
expect(repoPathExists(scenario)).toBe(true);
}
}

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

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

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

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>();
const internalSurfaceIds = 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);
expect(inventory.internalSurfaces.length).toBeGreaterThan(0);

for (const entry of inventory.entries) {
expect(statuses.has(entry.status)).toBe(true);
Expand All @@ -75,6 +169,18 @@ describe("E2E migration inventory deletion gates", () => {
expect(entry.ownerIssue).toMatch(/^#(?:3588|434[7-9]|435[0-7]|4941)$/);
expect(entry.notes).not.toBe("");
}

for (const surface of inventory.internalSurfaces) {
expect(statuses.has(surface.status)).toBe(true);
expect(surface.id).toMatch(/^[a-z0-9-]+$/);
expect(internalSurfaceIds.has(surface.id)).toBe(false);
internalSurfaceIds.add(surface.id);
expectPathListIsRepoRelative(surface.paths);
expect(surface.domain).not.toBe("");
expect(surface.ownerIssue).toMatch(/^#(?:3588|434[7-9]|435[0-7]|4941)$/);
expect(surface.replacementSurface).not.toBe("");
expect(surface.notes).not.toBe("");
}
});

it("covers every current direct legacy shell entrypoint", () => {
Expand All @@ -87,36 +193,30 @@ describe("E2E migration inventory deletion gates", () => {
expect(inventoriedShellScripts).toEqual(listLegacyShellEntrypoints());
});

it("requires coverage, retirement evidence, and #4357 approval before deletion", () => {
it("covers legacy scenario runner internal surfaces by path", () => {
const inventory = loadInventory();
const surfacePaths = inventory.internalSurfaces.flatMap((surface) => surface.paths);

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);
}
for (const root of INTERNAL_SURFACE_ROOTS) {
const files = listRepoFilesUnder(root);
expect(files.length).toBeGreaterThan(0);
for (const file of files) {
expect(
surfacePaths.some((surfacePath) => isCoveredByInventoryPath(file, surfacePath)),
).toBe(true);
}
}
});

if (entry.status === "bridge-probe") {
expect(entry.bridgeProbes.length).toBeGreaterThan(0);
for (const probe of entry.bridgeProbes) {
expect(repoPathExists(probe)).toBe(true);
}
}
it("requires coverage, retirement evidence, and #4357 approval before deletion", () => {
const inventory = loadInventory();

if (entry.status === "retired") {
expect(entry.retiredReason).not.toBe("");
}
for (const entry of inventory.entries) {
expectMigrationRecordDeletionGate(entry);
}

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