mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-07-02 21:31:38 +00:00
## Summary Partial fix for #1599 — replay from packets sidebar no longer freezes the live map. Clicking **Replay** on a packets-page row wrote the packet to `sessionStorage['replay-packet']` and navigated to `/#/live`. On init, `live.js` called `vcrPause()` to silence live WS traffic during the replay. But `vcrPause()` sets `VCR.mode = 'PAUSED'`, and `renderAnimations()` gates `anim.progress` advancement on `!isPaused` — so the replayed animation never advanced and the map appeared frozen. ## Fix Introduce a module-level `suppressLive` flag dedicated to muting live WS traffic without entering `PAUSED`. The WS handler's `LIVE` branch honors the flag (still ticking `updateTimeline` so the UI keeps reflecting traffic). The replay handoff sets the flag for ~12 s — long enough for the animation to play out — then clears it. Files changed: - `public/live.js` — module flag (`~145`), replay handoff (`~1502`), WS LIVE branch (`~897`) - `test-issue-1599-replay-freeze-e2e.js` — new Playwright E2E (seeds `sessionStorage['replay-packet']`, asserts `activeAnimations` drains after the handoff) - `.github/workflows/deploy.yml` — wire the new E2E into the deploy E2E block ## TDD trail | Commit | Role | | --- | --- | | `8a0add00` | Red — failing E2E (asserts the queued animation drains; pre-fix it never does → `FAIL: activeAnimations did NOT drain after replay handoff (count=1) — replay freeze regression`) | | `8069210d` | Green — `suppressLive` flag replaces `vcrPause()` in the handoff | | `c2a84a3e` | CI wiring | Locally reproduced both states against the e2e-fixture DB (Chromium via `CHROMIUM_PATH=/usr/bin/chromium`): - HEAD red commit: `2 pass, 1 fail` (assertion-shaped, not compile) - HEAD green commit: `3 pass, 0 fail` Browser verified: local Chromium against `corescope-server -port 13581 -db /tmp/e2e-fixture.db -public public` — `replay-packet` key is consumed by the init path, animation queues, and drains post-fix. E2E assertion added: `test-issue-1599-replay-freeze-e2e.js:111` (`activeAnimations drained to 0`). ## What this PR does NOT do The reporter explicitly called out a second, separable problem on the same issue: `renderPacketTree(packets, true)` runs with `isReplay = true`, which skips `addFeedItem` (`public/live.js:3155`), so the bottom-left feed shows "Waiting for packets…" even once the map animates. That is a UX decision (should the replayed packet appear in the feed?) and is intentionally **not** addressed here. Leaving #1599 open so the operator can decide. Hence: **"Partial fix for #1599"** — no `Fixes #` keyword. ## Preflight `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` → all hard gates ✅, no warnings. --------- Co-authored-by: corescope-bot <bot@corescope>
This commit is contained in:
@@ -392,6 +392,7 @@ jobs:
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1244-live-vcr-row-hints-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1510-live-nav-pin-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-live-fullscreen-1572-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1599-replay-freeze-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
BASE_URL=http://localhost:13581 node test-issue-1224-channels-mobile-ux-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
BASE_URL=http://localhost:13581 node test-issue-1367-channels-chat-app-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
BASE_URL=http://localhost:13581 node test-issue-1236-map-mobile-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
|
||||
+17
-1
@@ -137,6 +137,11 @@
|
||||
timelineFetchedScope: 0, // last fetched scope to avoid redundant fetches
|
||||
replayGen: 0, // generation counter — incremented on each replay/rewind to discard stale async results
|
||||
};
|
||||
// #1599 — drop live WS packets during a manual replay handoff without
|
||||
// entering VCR PAUSED (which freezes the canvas engine and kills the very
|
||||
// animation we want to play). The handoff sets this true, then clears it
|
||||
// after the replay window has elapsed.
|
||||
let suppressLive = false;
|
||||
|
||||
// ROLE_COLORS loaded from shared roles.js (includes 'unknown')
|
||||
|
||||
@@ -899,6 +904,10 @@
|
||||
if (_tabHidden) {
|
||||
return;
|
||||
}
|
||||
// #1599 — manual replay handoff sets suppressLive=true so incoming live
|
||||
// WS packets don't clutter the replay animation. We still want the
|
||||
// timeline to tick so the UI shows traffic continuing.
|
||||
if (suppressLive) { updateTimeline(); return; }
|
||||
if (realisticPropagation && pkt.hash) {
|
||||
const hash = pkt.hash;
|
||||
if (propagationBuffer.has(hash)) {
|
||||
@@ -1499,8 +1508,15 @@
|
||||
try {
|
||||
const parsed = JSON.parse(replayData);
|
||||
const packets = Array.isArray(parsed) ? parsed : [parsed];
|
||||
vcrPause(); // suppress live packets
|
||||
// #1599 — mute live WS traffic but keep VCR in LIVE so the canvas
|
||||
// engine keeps advancing animation progress. Using vcrPause() here
|
||||
// set VCR.mode='PAUSED' which froze anim.progress in
|
||||
// renderAnimations(), turning the replay into a blank, motionless map.
|
||||
suppressLive = true;
|
||||
setTimeout(() => renderPacketTree(packets, true), 1500);
|
||||
// Clear the suppression after the replay window has elapsed (the
|
||||
// longest animation duration plus a margin) so live traffic resumes.
|
||||
setTimeout(() => { suppressLive = false; }, 12000);
|
||||
} catch { }
|
||||
} else {
|
||||
// replayRecent(); // disabled — live page starts empty, fills from WS
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env node
|
||||
/* Issue #1599 — Replay from packets sidebar must not freeze the live map.
|
||||
*
|
||||
* Reproduces the freeze caused by the replay handoff calling vcrPause() to
|
||||
* silence live WS traffic. vcrPause() sets VCR.mode='PAUSED', and
|
||||
* renderAnimations() gates `anim.progress` advancement on `!isPaused`, so the
|
||||
* replayed animation never advances and the map appears frozen.
|
||||
*
|
||||
* Repro:
|
||||
* 1. Seed sessionStorage['replay-packet'] with a synthetic packet (the
|
||||
* packets-sidebar "Replay" button does this before navigating to /#/live).
|
||||
* 2. Load /#/live. live.js init reads the key and (currently) calls
|
||||
* vcrPause().
|
||||
* 3. Push an animation via the existing _liveDrawAnimatedLine test seam
|
||||
* AFTER the replay handoff has run.
|
||||
* 4. Wait 2x the canvas-engine duration (660ms base → 1500ms total).
|
||||
*
|
||||
* Expected behaviour after fix: the animation drains to 0 (engine still
|
||||
* running because VCR stays in LIVE mode; live WS suppression is handled by a
|
||||
* dedicated flag, not by entering PAUSED).
|
||||
*
|
||||
* Pre-fix behaviour: VCR.mode is 'PAUSED' → animation progress stays 0 →
|
||||
* activeAnimations never drains → test fails on the drain assertion.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
const BASE = process.env.BASE_URL || 'http://localhost:13581';
|
||||
|
||||
async function main() {
|
||||
const requireChromium = process.env.CHROMIUM_REQUIRE === '1';
|
||||
let browser;
|
||||
try {
|
||||
browser = await chromium.launch({
|
||||
headless: true,
|
||||
executablePath: process.env.CHROMIUM_PATH || undefined,
|
||||
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
|
||||
});
|
||||
} catch (err) {
|
||||
if (requireChromium) {
|
||||
console.error(`test-issue-1599-replay-freeze-e2e.js: FAIL — Chromium required but unavailable: ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`test-issue-1599-replay-freeze-e2e.js: SKIP (Chromium unavailable: ${err.message.split('\n')[0]})`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let failures = 0, passes = 0;
|
||||
const fail = (m) => { failures++; console.error(' FAIL: ' + m); };
|
||||
const pass = (m) => { passes++; console.log(' PASS: ' + m); };
|
||||
|
||||
const ctx = await browser.newContext();
|
||||
const page = await ctx.newPage();
|
||||
page.setDefaultTimeout(15000);
|
||||
|
||||
// Seed sessionStorage BEFORE the page script runs (mirrors the packets
|
||||
// sidebar "Replay" button which sets the key then navigates to /#/live).
|
||||
await page.addInitScript(() => {
|
||||
try {
|
||||
const syntheticPacket = {
|
||||
hash: 'test1599deadbeef',
|
||||
decoded: {
|
||||
header: { payloadTypeName: 'ADVERT' },
|
||||
payload: {},
|
||||
path: { hops: [] },
|
||||
},
|
||||
};
|
||||
sessionStorage.setItem('replay-packet', JSON.stringify([syntheticPacket]));
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
try {
|
||||
await page.goto(`${BASE}/#/live`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForSelector('#liveMap', { timeout: 15000 });
|
||||
await page.waitForFunction(() => !!(window._liveTestSeams && window._liveDrawAnimatedLine), null, { timeout: 15000 });
|
||||
|
||||
// Wait past the 1500ms setTimeout in the replay handoff so VCR mode is
|
||||
// settled into whatever state the handoff leaves it in.
|
||||
await page.waitForTimeout(1700);
|
||||
|
||||
// Sanity: confirm the handoff actually consumed the sessionStorage key
|
||||
// (proves the init path ran the replay branch).
|
||||
const replayKeyConsumed = await page.evaluate(() => sessionStorage.getItem('replay-packet') === null);
|
||||
if (replayKeyConsumed) pass('replay-packet sessionStorage key was consumed by live.js init');
|
||||
else fail('replay-packet sessionStorage key still present — handoff did not run');
|
||||
|
||||
// Push a synthetic animation onto the canvas engine post-handoff.
|
||||
await page.evaluate(() => {
|
||||
window._liveDrawAnimatedLine(
|
||||
[37.4, -122.0],
|
||||
[37.5, -122.1],
|
||||
'#00ff00',
|
||||
null,
|
||||
null,
|
||||
'00AA',
|
||||
'test-1599-anim'
|
||||
);
|
||||
});
|
||||
|
||||
const initialCount = await page.evaluate(() => window._liveTestSeams.getAnimCount());
|
||||
if (initialCount >= 1) pass(`animation queued (count=${initialCount})`);
|
||||
else fail(`animation did not queue (count=${initialCount})`);
|
||||
|
||||
// Core assertion: with the replay handoff leaving VCR in LIVE mode
|
||||
// (post-fix), the canvas engine advances progress and the animation
|
||||
// drains within ~2× the 660ms base duration. Pre-fix the handoff sets
|
||||
// VCR.mode='PAUSED' which freezes progress, so the engine never drains.
|
||||
//
|
||||
// Headless Chromium throttles requestAnimationFrame when no compositing
|
||||
// is happening, so we pump rAF callbacks from inside the page to give
|
||||
// the engine a deterministic chance to advance.
|
||||
const drainedTo = await page.evaluate(async () => {
|
||||
const seam = window._liveTestSeams;
|
||||
// ~30 frames at ~16ms each ≈ 480ms of simulated time, well over the
|
||||
// 660ms / 60fps it takes one animation to drain, but the rAF callback
|
||||
// bridge advances by real timestamps so we keep pumping until either
|
||||
// the queue drains or we hit a hard cap (90 frames ≈ 1.5s wall-clock).
|
||||
for (let i = 0; i < 90; i++) {
|
||||
if (seam.getAnimCount() === 0) break;
|
||||
await new Promise((r) => requestAnimationFrame(r));
|
||||
}
|
||||
return seam.getAnimCount();
|
||||
});
|
||||
|
||||
if (drainedTo === 0) {
|
||||
pass('activeAnimations drained to 0 — canvas engine advanced progress during replay');
|
||||
} else {
|
||||
fail(`activeAnimations did NOT drain after replay handoff (count=${drainedTo}) — replay freeze regression`);
|
||||
}
|
||||
} catch (e) {
|
||||
fail(`unexpected error: ${e && e.stack || e}`);
|
||||
} finally {
|
||||
try { await browser.close(); } catch (_) {}
|
||||
}
|
||||
|
||||
console.log(`\ntest-issue-1599-replay-freeze-e2e.js: ${passes} pass, ${failures} fail`);
|
||||
process.exit(failures === 0 ? 0 : 1);
|
||||
}
|
||||
|
||||
main().catch((e) => { console.error(e); process.exit(1); });
|
||||
Reference in New Issue
Block a user