diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8012b4774..2f68250ea 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -391,12 +391,10 @@ jobs: # → header=0x15 (route_type=1, payload_type=5) # → ComputeContentHash(...) = fae0c9e6d357a814 # - # The first_seen / observation timestamps are pinned to a date - # within retentionHours but outside the default 15-min UI - # window so the row is hidden in the default view (keeping - # test-e2e-playwright's first-10-rows hex-pane test - # unaffected) and reachable via the explicit ?timeWindow=0 - # deep-link the #1486 test uses. + # Fixed old timestamps keep the row behind the freshly shifted + # fixture observations and outside the default 15-min UI window. + # The #1486 and #2104 tests reach it via an explicit hash + + # ?timeWindow=0 deep-link. run: | sqlite3 test-fixtures/e2e-fixture.db <<'SQL' -- Sort the seeded row LAST in BOTH default packets views: @@ -404,16 +402,9 @@ jobs: -- • grouped view (#default for the packets page) sorts by -- MAX(observations.timestamp) DESC → we must keep our obs -- timestamps OLDER than every other fixture observation. - -- Fixture (after freshen) has obs timestamps spanning - -- 2026-05-17 16:01:39Z .. 2026-05-28 00:00:00Z (max). - -- Note: freshen only shifts transmissions.first_seen forward - -- to ~now; observation.timestamp is left alone except for - -- the timestamp=0 case. - -- Use 2026-05-15 (~2 days older than the oldest fixture obs) - -- so our row sorts LAST in the grouped view too, keeping - -- test-e2e-playwright's first-10-rows hex-pane test - -- unaffected. The #1486 test still reaches the row via the - -- explicit hash + ?timeWindow=0 deep-link. + -- freshen-fixture.sh shifts fixture observations forward. + -- Keep this row at 2026-05-15 so it sorts LAST in the grouped + -- view too; the #1486 and #2104 tests use the explicit deep-link. INSERT INTO transmissions(id,raw_hex,hash,first_seen,route_type,payload_type,payload_version,decoded_json,channel_hash,from_pubkey) VALUES (0,'15000102030405060708090a0b0c0d0e0f','fae0c9e6d357a814','2026-05-15T00:00:00Z',1,5,0,'{"type":"CHAN","channel":"#test","text":"#1486 fixture"}',NULL,NULL); INSERT INTO observations(transmission_id,observer_idx,direction,snr,rssi,score,path_json,timestamp,resolved_path) VALUES @@ -452,6 +443,18 @@ jobs: # required shape before the server boots. run: ./corescope-migrate -db test-fixtures/e2e-fixture.db + - name: 'Seed distinct observation bytes for #2104' + # raw_hex exists only after migration. Keep the #1486 payload/hash + # while encoding each observation's existing one-hop path. + run: | + sqlite3 test-fixtures/e2e-fixture.db <<'SQL' + UPDATE observations SET raw_hex = CASE observer_idx + WHEN 1 THEN '1501aa0102030405060708090a0b0c0d0e0f' + WHEN 2 THEN '1501bb0102030405060708090a0b0c0d0e0f' + WHEN 3 THEN '1501cc0102030405060708090a0b0c0d0e0f' + END WHERE transmission_id = 0 AND observer_idx IN (1,2,3); + SQL + - name: Seed deterministic Path Inspector routes (#2060) run: sqlite3 test-fixtures/e2e-fixture.db < test-fixtures/path-inspector.sql diff --git a/tests/e2e/test-e2e-playwright.js b/tests/e2e/test-e2e-playwright.js index d38f7a0b2..9d9fbf2ab 100644 --- a/tests/e2e/test-e2e-playwright.js +++ b/tests/e2e/test-e2e-playwright.js @@ -5,6 +5,7 @@ const REPO_ROOT = require('path').resolve(__dirname, '..', '..'); * Usage: node test-e2e-playwright.js */ const { chromium } = require('playwright'); +const { doesNotReject } = require('node:assert/strict'); const BASE = process.env.BASE_URL || 'http://localhost:3000'; const GO_BASE = process.env.GO_BASE_URL || ''; // e.g. https://analyzer.00id.net:82 @@ -2596,41 +2597,42 @@ async function run() { // Test: per-observation raw_hex — hex pane updates when switching observations (#881) await test('Packet detail hex pane updates per observation', async () => { - await gotoPackets(page); - await page.waitForTimeout(500); - - // Try clicking packet rows to find one with multiple observations - const rows = await page.$$('table tbody tr[data-action]'); - let obsRows = []; - for (let i = 0; i < Math.min(rows.length, 10); i++) { - await rows[i].click({ timeout: 3000 }).catch(() => null); - await page.waitForTimeout(600); - obsRows = await page.$$('.detail-obs-row'); - if (obsRows.length >= 2) break; - } - - if (obsRows.length < 2) { - console.log(' ⏭ Skipped: no packet with ≥2 observations found in first 10 rows'); - return; - } - - // Click first observation, capture hex dump - await obsRows[0].click({ timeout: 5000 }); - await page.waitForTimeout(500); - const hex1 = await page.$eval('.hex-dump', el => el.textContent).catch(() => ''); - - // Click second observation, capture hex dump - await obsRows[1].click({ timeout: 5000 }); - await page.waitForTimeout(500); - const hex2 = await page.$eval('.hex-dump', el => el.textContent).catch(() => ''); - - // If both have content and differ, the feature works - if (hex1 && hex2 && hex1 !== hex2) { - console.log(' ✓ Hex pane content differs between observations'); - } else if (hex1 && hex2 && hex1 === hex2) { - console.log(' ⏭ Hex same for both observations (likely historical NULL raw_hex — OK)'); - } else { - console.log(' ⏭ Could not capture hex content from both observations'); + // Reuse the real #1486 transmission, seeded with distinct raw bytes after + // migration in deploy.yml. Missing fixture data must fail, never skip. + const hash = 'fae0c9e6d357a814'; + const expectedHex = [ + '1501aa0102030405060708090a0b0c0d0e0f', + '1501bb0102030405060708090a0b0c0d0e0f' + ]; + const response = await context.request.get(BASE + '/api/packets/' + hash); + assert(response.ok(), '#2104 requires the #1486 grouped-packet fixture'); + const detail = await response.json(); + assert(detail.packet && detail.packet.hash === hash && Array.isArray(detail.observations), + 'fixture must expose the expected packet and observations'); + const observations = expectedHex.map(hex => detail.observations.find(o => o.raw_hex === hex)); + assert(observations.every(Boolean), 'fixture must include both distinct observation raw byte strings'); + assert(String(observations[0].id) !== String(observations[1].id), 'observation IDs must differ'); + + // Start on B so selecting A must complete a render before selecting B again. + await page.goto(BASE + '/#/packets/' + hash + '?obs=' + observations[1].id + '&timeWindow=0', + { waitUntil: 'domcontentloaded' }); + await page.reload({ waitUntil: 'load' }); + await page.waitForSelector('#pktRight .detail-obs-row.observation-current[data-obs-id="' + observations[1].id + '"]'); + const obsRows = observations.map(o => + page.locator('#pktRight .detail-obs-row[data-obs-id="' + o.id + '"]')); + + for (const index of [0, 1, 0]) { + const id = String(observations[index].id); + await doesNotReject(() => obsRows[index].click({ timeout: 5000 }), + 'Observation ' + id + ' must remain selectable after the detail rerenders'); + await page.waitForFunction(expectedId => { + const selected = document.querySelector('#pktRight .detail-obs-row.observation-current'); + return selected && selected.dataset.obsId === expectedId; + }, id); + const selectedId = await page.locator('#pktRight .detail-obs-row.observation-current').getAttribute('data-obs-id'); + assert(selectedId === id, 'expected selected observation ' + id + ', got ' + selectedId); + const hex = (await page.locator('#pktRight .hex-dump').textContent()).replace(/\s+/g, '').toLowerCase(); + assert(hex === expectedHex[index], 'observation ' + id + ': expected hex ' + expectedHex[index] + ', got ' + hex); } });