Skip to content

Commit 918415c

Browse files
committed
feat(h09): Save → Load round-trip extras parity e2e
Closes V5-G11 honesty gap: Save/Load buttons existed since F-C but no test ever proved that a Loaded spec actually reproduces the same training extras as building it from scratch. The new scenario does the full round-trip through the GUI: 1. Drop llama3_8b → click Save → intercept the browser download. 2. Train baseline, snapshot extras. 3. gotoApp afresh → Load the saved file via spec-load-input → wait for canvas to populate from the loaded spec. 4. Train again, snapshot extras_after_load. 5. Assert model_summary equal AND every loss matches within 1e-4 (stage_train synthetic data is seed-deterministic). Tests: - vbgui e2e 63_save_load_roundtrip.spec.ts: 1/1 passing. - vbgui e2e V6 suite (55–63): 11/11 passing — no regression.
1 parent 7e6a69e commit 918415c

1 file changed

Lines changed: 81 additions & 0 deletions

File tree

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// H09: Save → Load round-trip extras parity through the GUI.
2+
//
3+
// V5-G11 added Save/Load buttons to TopBar. v6 closes the honesty gap:
4+
// previously nobody had asserted that a Loaded spec actually produces
5+
// the same training extras as building the same spec from scratch.
6+
// This test does:
7+
// 1. Build spec via preset (llama3_8b) → click Save → capture the
8+
// download as a Blob → Train and snapshot extras_baseline.
9+
// 2. Fresh page (gotoApp again) → Load the captured file via the
10+
// file input → Train and snapshot extras_after_load.
11+
// 3. Assert model_summary fields match exactly + losses match
12+
// within 1e-4 (synthetic data is seed-deterministic in stage_train).
13+
14+
import { test, expect } from "@playwright/test";
15+
import { gotoApp, selectPreset, closeModal } from "../fixtures";
16+
import { readTrainExtras } from "../utils/train_extras";
17+
import { tmpdir } from "node:os";
18+
import { join } from "node:path";
19+
import { writeFileSync, readFileSync, existsSync, unlinkSync } from "node:fs";
20+
21+
const SAVE_PATH = join(tmpdir(), "vbgui_h09_spec.json");
22+
23+
test.beforeAll(() => {
24+
if (existsSync(SAVE_PATH)) unlinkSync(SAVE_PATH);
25+
});
26+
27+
test("H09: Save → fresh page → Load → Train extras match baseline",
28+
async ({ page }) => {
29+
test.setTimeout(180_000);
30+
31+
// ----- 1) Build, Save, Train baseline ---------------------------------
32+
await gotoApp(page);
33+
await selectPreset(page, "llama3_8b");
34+
35+
// Trigger Save and intercept the download.
36+
const [download] = await Promise.all([
37+
page.waitForEvent("download"),
38+
page.getByTestId("spec-save").click(),
39+
]);
40+
const tmp = await download.path();
41+
expect(tmp).toBeTruthy();
42+
const bytes = readFileSync(tmp!);
43+
writeFileSync(SAVE_PATH, bytes);
44+
expect(existsSync(SAVE_PATH)).toBe(true);
45+
46+
// Baseline Train.
47+
await page.getByTestId("run-pipeline-toggle").click();
48+
await page.getByTestId("train-num-steps").fill("2");
49+
await page.getByTestId("run-pipeline-train").click();
50+
await page.getByTestId("run-result-modal").waitFor({ timeout: 60_000 });
51+
const baseline = await readTrainExtras(page);
52+
await closeModal(page);
53+
54+
// ----- 2) Fresh page + Load + Train -----------------------------------
55+
await gotoApp(page);
56+
// No preset — feed the saved spec via the file input so the test
57+
// proves Load alone reproduces the canvas state.
58+
const loadInput = page.getByTestId("spec-load-input");
59+
await loadInput.setInputFiles(SAVE_PATH);
60+
// Wait for nodes to materialise from the loaded spec.
61+
await expect.poll(async () =>
62+
await page.locator("[data-testid^='brick-node-']").count(),
63+
{ timeout: 8_000 },
64+
).toBeGreaterThan(0);
65+
66+
await page.getByTestId("run-pipeline-toggle").click();
67+
await page.getByTestId("train-num-steps").fill("2");
68+
await page.getByTestId("run-pipeline-train").click();
69+
await page.getByTestId("run-result-modal").waitFor({ timeout: 60_000 });
70+
const afterLoad = await readTrainExtras(page);
71+
await closeModal(page);
72+
73+
// ----- 3) Assert parity ------------------------------------------------
74+
expect(afterLoad.model_summary).toEqual(baseline.model_summary);
75+
expect(afterLoad.num_steps).toBe(baseline.num_steps);
76+
expect(afterLoad.losses.length).toBe(baseline.losses.length);
77+
for (let i = 0; i < baseline.losses.length; i++) {
78+
expect(Math.abs(afterLoad.losses[i] - baseline.losses[i]))
79+
.toBeLessThan(1e-4);
80+
}
81+
});

0 commit comments

Comments
 (0)