From e8eebe49407de8947bf509d5cf04d9080ee4edf7 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Mon, 21 Sep 2026 07:02:13 +0800 Subject: Real historical replay: timeline pins load run's own checkpoint GLBs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - renderTimeline rebuilds #timeline-track after each snapshot and wires click-to-pin on completed checkpoints (kept placeholder nodes otherwise) - selection loads the pinned run's own steps/-*.glb (generation/ ownership guards apply), pins survive live snapshots, 跟随最新 restores live scope, replay pauses/scrubs through 已完成 versions - placeholder fallback now clearly labeled (占位…) only before any run - acceptance: historical_replay_regression (9 checks) pins mid-run, asserts no auto-follow strays, late-older-response precedence, served checkpoint sha256 vs CLI reference, and viewport geometry change - fix: timeline-track id missing in index.html crashed renderTimeline (null firstChild), leaving static placeholder nodes non-clickable - driver: 71/71 checks, zero console/page errors --- tools/acceptance/e2e_acceptance.py | 197 +++++++++++++++++++++++++++++++++++-- 1 file changed, 189 insertions(+), 8 deletions(-) (limited to 'tools/acceptance/e2e_acceptance.py') diff --git a/tools/acceptance/e2e_acceptance.py b/tools/acceptance/e2e_acceptance.py index 1b3ae6c..8271857 100755 --- a/tools/acceptance/e2e_acceptance.py +++ b/tools/acceptance/e2e_acceptance.py @@ -463,8 +463,8 @@ def stale_load_regression(browser, base_url, track, results, evidence): results.append((gh2["ready"] and gh2["kids"] == gh["kids"] and gh2["note"] == note_h, f"stale-load: late older same-run step ignored (note='{gh2['note'][:40]}')")) - # Scenario F: live GLB response still pending after entering replay must - # not repaint the selected replay frame view. + # Scenario F: entering historical replay pins a real checkpoint; a live + # GLB response still pending from before must not repaint the pinned view. run_j = f"run-stale-{stamp}-j" start_run(run_j) release(held_for(run_j, "steps/01-foundation.glb")) @@ -472,19 +472,199 @@ def stale_load_regression(browser, base_url, track, results, evidence): ej3 = held_for(run_j, "steps/03-cabin.glb") wait_text(page, "#status-label", "已完成") page.click("#replay-button") - page.wait_for_timeout(300) - page.click("#replay-button") + ehist = held_for(run_j, "steps/01-foundation.glb") + release(ehist) + wait_text(page, "#viewport-note", "历史几何 · steps/01-foundation.glb") page.wait_for_timeout(300) gj = page.evaluate(SCENE_JS) - results.append((gj["ready"] and is_frame(gj) and box_ok(gj), - f"stale-load: replay entry repaints frame view while GLB pending " - f"(kids={gj.get('kids')})")) + results.append((gj["ready"] and is_glb(gj) and "历史几何" in gj["note"], + f"stale-load: replay pins a real checkpoint with historical labels " + f"(note='{gj.get('note', '')[:40]}')")) page.screenshot(path=str(evidence / "stale-page-replay.png"), full_page=True) + page.click("#replay-button") + page.wait_for_timeout(300) + gj_pause = page.evaluate(SCENE_JS) + results.append((gj_pause["kids"] == gj["kids"] and gj_pause["note"] == gj["note"], + f"stale-load: historical playback pauses on the selected checkpoint " + f"(state='{page.inner_text('#replay-state')}')")) release(ej3) page.wait_for_timeout(600) gj2 = page.evaluate(SCENE_JS) results.append((gj2["ready"] and gj2["kids"] == gj["kids"] and gj2["note"] == gj["note"], - f"stale-load: live GLB after replay ignored (note='{gj2['note'][:40]}')")) + f"stale-load: live GLB after entering historical replay ignored " + f"(note='{gj2['note'][:40]}')")) + + for entry in held: + if not entry["done"]: + release(entry) + page.unroute("**/api/artifacts/steps*") + page.close() + + +def historical_replay_regression(browser, base_url, track, results, evidence): + """Regression for real historical checkpoint replay of a selected run. + + Proves the timeline offers a clearly labeled placeholder fallback while no + run history exists, that pinning a timeline step issues and serves the + selected run's own checkpoint artifact (byte-identical to the reference + GLB) under distinct historical labels, that the rendered geometry actually + changes when a different checkpoint is selected, that a late older + historical response cannot repaint a newer selection, that live snapshot + updates cannot unpin the chosen historical step, and that 跟随最新 returns + the viewport to live scope. All still-held responses are drained and the + route handler is removed before the page closes. + """ + page = browser.new_page(viewport={"width": 1280, "height": 900}) + + track(page) + stamp = int(time.time() * 1000) + held = [] + + def hold_route(route): + q = urllib.parse.parse_qs(urllib.parse.urlparse(route.request.url).query) + held.append({ + "route": route, + "project": (q.get("projectId") or [""])[0], + "run": (q.get("runId") or [""])[0], + "path": (q.get("path") or [""])[0], + "done": False, + }) + + page.route("**/api/artifacts/steps*", hold_route) + page.goto(f"{base_url}/?freeze=1", wait_until="domcontentloaded") + wait_text(page, "#status-label", "待机") + + def start_run(run_id): + page.fill("#run-id-input", run_id) + render_box = page.query_selector("#run-render-input") + if render_box and render_box.is_checked(): + render_box.set_checked(False) + page.click("#run-start-button") + wait_text(page, "#stream-state", "已连接") + + def held_for(run_id, path, timeout_s=120): + deadline = time.time() + timeout_s + while time.time() < deadline: + for entry in held: + if not entry["done"] and entry["run"] == run_id and entry["path"] == path: + return entry + page.wait_for_timeout(300) + raise AssertionError( + f"no held steps request for run={run_id} path={path}; " + f"held={[(e['run'], e['path'], e['done']) for e in held]}") + + def release(entry, status=200): + if status == 200: + q = urllib.parse.urlencode({ + "projectId": entry["project"], "runId": entry["run"], "path": entry["path"], + }) + resp_status, ctype, body = fetch_bytes(f"{base_url}/api/artifacts/steps?{q}") + if resp_status != 200: + raise AssertionError(f"artifact fetch for fulfill failed ({resp_status})") + else: + ctype, body = "text/plain", b"boom" + entry["route"].fulfill(status=status, content_type=ctype, body=body) + entry["done"] = True + + def is_glb(g): + return bool(g.get("kids")) and g["kids"][0]["type"] == "Group" + + def box_ok(g): + return g.get("boxSize") and all( + isinstance(s, (int, float)) and s > 0 for s in g["boxSize"]) + + # Idle fallback must be clearly labeled as placeholder, never as history. + idle_state = page.inner_text("#replay-state") + idle_button = page.inner_text("#replay-button") + results.append(("占位" in idle_state and "占位" in idle_button, + f"historical: idle fallback clearly labeled placeholder " + f"(state='{idle_state}', button='{idle_button}')")) + + # Phase I: pin a checkpoint mid-run; live snapshot updates must not unpin it. + run_id = f"run-replay-{stamp}" + start_run(run_id) + release(held_for(run_id, "steps/01-foundation.glb")) + wait_text(page, "#viewport-note", "管线几何 · steps/01-foundation.glb") + held_before = len(held) + page.locator(".timeline-step").nth(0).click() + e_pinned = held_for(run_id, "steps/01-foundation.glb") + results.append((e_pinned["run"] == run_id and e_pinned["path"] == "steps/01-foundation.glb", + f"historical: pin requests the run's own checkpoint " + f"(run={e_pinned['run']}, path={e_pinned['path']})")) + release(e_pinned) + wait_text(page, "#viewport-note", "历史几何 · steps/01-foundation.glb") + page.wait_for_timeout(400) + g_pinned = page.evaluate(SCENE_JS) + results.append((g_pinned["ready"] and is_glb(g_pinned) + and "历史" in page.inner_text("#replay-state"), + f"historical: pinned step carries historical labels " + f"(state='{page.inner_text('#replay-state')[:36]}')")) + + page.wait_for_function( + "() => document.querySelectorAll('.timeline-step').length === 3", timeout=150000) + page.wait_for_timeout(600) + g_unpin = page.evaluate(SCENE_JS) + stray = [e for e in held[held_before:] + if e["run"] == run_id and e["path"] == "steps/03-cabin.glb"] + results.append((g_unpin["kids"] == g_pinned["kids"] and g_unpin["note"] == g_pinned["note"] + and not stray, + f"historical: live snapshot update cannot unpin the pinned step " + f"(stray 03 requests={len(stray)})")) + + # Phase II: 跟随最新 returns the viewport to live scope with the newest step. + page.click("#follow-button") + e_live3 = held_for(run_id, "steps/03-cabin.glb") + release(e_live3) + wait_text(page, "#viewport-note", "管线几何 · steps/03-cabin.glb") + page.wait_for_timeout(500) + g_live3 = page.evaluate(SCENE_JS) + results.append((g_live3["ready"] and is_glb(g_live3) and box_ok(g_live3), + f"historical: 跟随最新 switches back to live scope and loads the " + f"newest checkpoint (kids={g_live3.get('kids')})")) + box_live3 = g_live3["boxSize"] + + # Phase III: a late older historical response cannot repaint the newer selection. + page.locator(".timeline-step").nth(0).click() + e_late1 = held_for(run_id, "steps/01-foundation.glb") + page.locator(".timeline-step").nth(2).click() + e_hist3 = held_for(run_id, "steps/03-cabin.glb") + release(e_late1) + page.wait_for_timeout(600) + g_late = page.evaluate(SCENE_JS) + results.append((g_late["kids"] == g_live3["kids"] + and "历史几何加载中 · steps/03-cabin.glb" in g_late["note"], + f"historical: late older historical response ignored after newer " + f"selection (note='{g_late['note'][:40]}')")) + release(e_hist3) + wait_text(page, "#viewport-note", "历史几何 · steps/03-cabin.glb") + page.wait_for_timeout(400) + g_hist3 = page.evaluate(SCENE_JS) + results.append((g_hist3["ready"] and is_glb(g_hist3) and g_hist3["note"] != g_late["note"], + f"historical: newer historical selection wins after late older response " + f"(note='{g_hist3['note'][:40]}')")) + + # Phase IV: selecting V01 changes both the served checkpoint identity and + # the actual on-screen geometry. + page.locator(".timeline-step").nth(0).click() + e_hist1 = held_for(run_id, "steps/01-foundation.glb") + q = urllib.parse.urlencode({ + "projectId": e_hist1["project"], "runId": e_hist1["run"], "path": e_hist1["path"], + }) + status, ctype, body = fetch_bytes(f"{base_url}/api/artifacts/steps?{q}") + results.append((status == 200 and sha256_bytes(body) == REFERENCE_SHA["steps/01-foundation.glb"], + f"historical: served checkpoint identity matches reference GLB " + f"(sha256={sha256_bytes(body)[:12]}…)")) + release(e_hist1) + wait_text(page, "#viewport-note", "历史几何 · steps/01-foundation.glb") + page.wait_for_timeout(500) + g_hist1 = page.evaluate(SCENE_JS) + diffs = [abs(a - b) / max(abs(b), 1e-6) for a, b in zip(g_hist1["boxSize"], box_live3)] + results.append((g_hist1["ready"] and is_glb(g_hist1) and max(diffs) >= 0.05, + f"historical: selection changes actual viewport geometry " + f"(max rel diff {max(diffs):.3f})")) + page.screenshot(path=str(evidence / "historical-replay-pinned.png"), full_page=True) + (evidence / "historical-replay-pinned.json").write_text( + json.dumps(g_hist1, indent=2, ensure_ascii=False)) for entry in held: if not entry["done"]: @@ -577,6 +757,7 @@ def main(): results.append((digest == expected, f"sha256 {step_path} -> {digest[:12]}… (expect {expected[:12]}…)")) stale_load_regression(browser, base_url, track, results, evidence) + historical_replay_regression(browser, base_url, track, results, evidence) fail_ms = int(time.time() * 1000) + 2 fail_run_id = f"run-acc-{fail_ms}-failure" -- cgit v1.2.3