summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rwxr-xr-xtools/acceptance/e2e_acceptance.py197
1 files changed, 189 insertions, 8 deletions
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"