test(05-05): add five route capture legs racing ARAHARAH-ROUTE (RED)

- SIGNALS + seen-map + diagnostics gain ARAHARAH-ROUTE; waitMode 'route'
  races the state token, asserts expectState + expectText DOM text, and
  applies the PX-02 theme-slot honesty check on themed legs
- captureLeg gains an interact(page) pre-shot hook (the alt leg's chip tap)
- five legs data-driven from the committed corpus (demoPair picked at
  runtime — never a hardcoded pair literal): fixture / alt / pending /
  dark / route-gallery-compare (composer refactored into composeCompare)
- expected list grows 8 → 13
- RED by construction: the ?fixpair knob does not exist yet, so the
  default fix sits outside the demo pair's 200m tolerance → the drawn
  legs must fail with state=pending
This commit is contained in:
araharah-executor
2026-10-24 05:47:40 +08:00
parent 29ad451c27
commit c9b1128a0f
+183 -12
View File
@@ -27,7 +27,12 @@
* shell-denied.png denied card + ASK VIA CHAT (GEO-02 entry point) * shell-denied.png denied card + ASK VIA CHAT (GEO-02 entry point)
* shell-chat-pin.png LIVE via the chat-pin relay (?pin=1, stubbed /api/location) * shell-chat-pin.png LIVE via the chat-pin relay (?pin=1, stubbed /api/location)
* shell-dest-preload.png the Mini App booted WITH a preloaded destination (?dest=<id>) * shell-dest-preload.png the Mini App booted WITH a preloaded destination (?dest=<id>)
* shell-route-fixture.png route drawn from the demo pair's pinned origin (?fixpair=, 05-05)
* shell-route-alt.png alternative selected via chip tap (ROUTE {n}/{N} re-render, 05-05)
* shell-route-pending.png the honest ROUTE // ROUTER pending card (D-03, 05-05)
* shell-route-dark.png route drawn, Telegram dark theme (PX-02 route half, 05-05)
* gallery-compare.png DSGN-02 side-by-side vs navigate_map_overview/screen.png * gallery-compare.png DSGN-02 side-by-side vs navigate_map_overview/screen.png
* route-gallery-compare.png DSGN-02 route fidelity artifact (05-05, same composer)
* *
* Also runs the GEO-03 runtime probe in-page after boot-to-live and fails * Also runs the GEO-03 runtime probe in-page after boot-to-live and fails
* the run on any non-empty storage surface. * the run on any non-empty storage surface.
@@ -163,7 +168,24 @@ log.push(`serving web/dist-harness/ → ${base}/`);
const browser = await chromium.launch(); const browser = await chromium.launch();
const SIGNALS = ['RAMS-BASEMAP-READY', 'ARAHARAH-LIVE', 'ARAHARAH-DEST']; const SIGNALS = ['RAMS-BASEMAP-READY', 'ARAHARAH-LIVE', 'ARAHARAH-DEST', 'ARAHARAH-ROUTE'];
/* The five route legs (05-05) are DATA-DRIVEN from the committed corpus:
* the demo pair is picked AT RUNTIME — never a hardcoded pair literal.
* RT10 guarantees >= 4 multi-route pairs, so the find succeeds; the
* fallback keeps the legs runnable in the degenerate single-route case. */
const routePairs = JSON.parse(
readFileSync(resolve(REPOROOT, 'routing/src/data/route-fixtures.json'), 'utf8'),
).pairs;
const demoPair = routePairs.find((p) => p.routes.length >= 2) ?? routePairs[0];
const routeGazetteer = JSON.parse(
readFileSync(resolve(REPOROOT, 'search/src/data/kl-gazetteer.json'), 'utf8'),
);
const routeDestEntry = routeGazetteer.entries.find((e) => e.id === demoPair.destinationId);
if (routeDestEntry === undefined) {
console.error(`[capture] FAILED demo pair destination "${demoPair.destinationId}" is not a gazetteer id`);
process.exit(1);
}
/* /*
* One capture leg = one context + one screenshot. The Phase-1 READY-race * One capture leg = one context + one screenshot. The Phase-1 READY-race
@@ -174,8 +196,15 @@ const SIGNALS = ['RAMS-BASEMAP-READY', 'ARAHARAH-LIVE', 'ARAHARAH-DEST'];
* 'settle400' → shot after 400ms (boot card 2) * 'settle400' → shot after 400ms (boot card 2)
* 'live' → ARAHARAH-LIVE race + 2000ms tile settle * 'live' → ARAHARAH-LIVE race + 2000ms tile settle
* 'denied' → .lamp[data-state="denied"] race (LINK 3's 60s poll first) * 'denied' → .lamp[data-state="denied"] race (LINK 3's 60s poll first)
* 'dest' → ARAHARAH-DEST race + state=resolved + card-text poll
* 'route' → ARAHARAH-ROUTE race + expectState + expectText DOM poll
* (05-05; expectState = 'drawn'|'pending', expectText =
* body-text substrings polled up to 15s; a themed leg
* gets the same PX-02 honesty check as 'live';
* `interact(page)` runs after the waits, pre-shot —
* the alt leg's chip tap)
*/ */
async function captureLeg({ name, query, colorScheme, grantGeo, waitMode }) { async function captureLeg({ name, query, colorScheme, grantGeo, waitMode, expectState, expectText, interact }) {
const target = join(SHOTS, name); const target = join(SHOTS, name);
const context = await browser.newContext({ const context = await browser.newContext({
viewport: { width: 390, height: 844 }, viewport: { width: 390, height: 844 },
@@ -187,7 +216,7 @@ async function captureLeg({ name, query, colorScheme, grantGeo, waitMode }) {
await context.setGeolocation({ latitude: 3.1513, longitude: 101.6958, accuracy: 12 }); await context.setGeolocation({ latitude: 3.1513, longitude: 101.6958, accuracy: 12 });
} }
const page = await context.newPage(); const page = await context.newPage();
const seen = { 'RAMS-BASEMAP-READY': false, 'ARAHARAH-LIVE': false, 'ARAHARAH-DEST': false, live: 'no' }; const seen = { 'RAMS-BASEMAP-READY': false, 'ARAHARAH-LIVE': false, 'ARAHARAH-DEST': false, 'ARAHARAH-ROUTE': false, live: 'no' };
const consoleLines = []; const consoleLines = [];
page.on('console', (m) => { page.on('console', (m) => {
for (const s of SIGNALS) if (m.text().startsWith(s)) seen[s] = true; for (const s of SIGNALS) if (m.text().startsWith(s)) seen[s] = true;
@@ -195,7 +224,7 @@ async function captureLeg({ name, query, colorScheme, grantGeo, waitMode }) {
if (consoleLines.length > 20) consoleLines.shift(); if (consoleLines.length > 20) consoleLines.shift();
}); });
const signalDiagnostics = () => const signalDiagnostics = () =>
`${seen['RAMS-BASEMAP-READY'] ? 'RAMS-BASEMAP-READY yes' : 'RAMS-BASEMAP-READY no'}; ${seen['ARAHARAH-LIVE'] ? 'ARAHARAH-LIVE yes' : 'ARAHARAH-LIVE no'}; ${seen['ARAHARAH-DEST'] ? 'ARAHARAH-DEST yes' : 'ARAHARAH-DEST no'}; last console: ${JSON.stringify(consoleLines.slice(-5))}`; `${seen['RAMS-BASEMAP-READY'] ? 'RAMS-BASEMAP-READY yes' : 'RAMS-BASEMAP-READY no'}; ${seen['ARAHARAH-LIVE'] ? 'ARAHARAH-LIVE yes' : 'ARAHARAH-LIVE no'}; ${seen['ARAHARAH-DEST'] ? 'ARAHARAH-DEST yes' : 'ARAHARAH-DEST no'}; ${seen['ARAHARAH-ROUTE'] ? 'ARAHARAH-ROUTE yes' : 'ARAHARAH-ROUTE no'}; last console: ${JSON.stringify(consoleLines.slice(-5))}`;
try { try {
await page.goto(`${base}/?${query}`, { waitUntil: 'domcontentloaded', timeout: 60000 }); await page.goto(`${base}/?${query}`, { waitUntil: 'domcontentloaded', timeout: 60000 });
@@ -244,6 +273,40 @@ async function captureLeg({ name, query, colorScheme, grantGeo, waitMode }) {
{ timeout: 15000, polling: 250 }, { timeout: 15000, polling: 250 },
); );
await page.waitForTimeout(500); await page.waitForTimeout(500);
} else if (waitMode === 'route') {
/* 05-05: race the ARAHARAH-ROUTE console event — a STATE TOKEN only
* (state=drawn|pending, never coordinates; D-15/T-05-13: RS8 pins the
* source, this race pins the runtime). The leg then asserts its
* expected state and DOM text, and themed legs get the same PX-02
* honesty check as the live branch. */
const signal = await Promise.race([
page.waitForEvent('console', {
predicate: (m) => m.text().startsWith('ARAHARAH-ROUTE'),
timeout: SIGNAL_TIMEOUT_MS,
}).then(() => 'signal'),
new Promise((r) => setTimeout(() => r('timeout'), SIGNAL_TIMEOUT_MS)),
]);
if (signal === 'timeout') {
throw new Error(`ARAHARAH-ROUTE race timed out after ${SIGNAL_TIMEOUT_MS}ms — ${signalDiagnostics()}`);
}
const routeLine = consoleLines.filter((l) => l.startsWith('ARAHARAH-ROUTE')).pop() ?? '';
if (!routeLine.includes(`state=${expectState}`)) {
throw new Error(`route state mismatch: "${routeLine}" — expected state=${expectState}`);
}
for (const text of expectText ?? []) {
await page.waitForFunction(
(t) => (document.body.textContent ?? '').includes(t),
text,
{ timeout: 15000, polling: 250 },
);
}
/* PX-02 honesty check (anti-fake, T-03-30): the theme slot ACTUALLY
* flipped to the requested scheme before shooting. */
const themed = await page.evaluate(() => document.documentElement.dataset.tgTheme ?? null);
if (themed !== colorScheme) {
throw new Error(`theme slot mismatch: expected ${colorScheme}, documentElement[data-tg-theme]=${String(themed)}`);
}
await page.waitForTimeout(2000); // tile render settle (route + camera refit at 400ms)
} else if (waitMode === 'denied') { } else if (waitMode === 'denied') {
// Wait on the denied strip lamp specifically — the awaiting-pin variant of // Wait on the denied strip lamp specifically — the awaiting-pin variant of
// the card also carries a CTA during LINK 3's 60s poll, so the CARD alone // the card also carries a CTA during LINK 3's 60s poll, so the CARD alone
@@ -262,6 +325,8 @@ async function captureLeg({ name, query, colorScheme, grantGeo, waitMode }) {
await page.waitForTimeout(400); await page.waitForTimeout(400);
} // 'immediate' → no wait beyond goto } // 'immediate' → no wait beyond goto
if (interact) await interact(page);
await page.screenshot({ path: target }); await page.screenshot({ path: target });
log.push(`captured ${name} (${sizeOf(target)}B, ?${query || '—'} | wait=${waitMode})`); log.push(`captured ${name} (${sizeOf(target)}B, ?${query || '—'} | wait=${waitMode})`);
} catch (err) { } catch (err) {
@@ -349,13 +414,11 @@ try {
} }
} }
/* 8 — DSGN-02 fidelity artifact: the live-light capture beside the committed /* ————— DSGN-02 composer (shared by legs 8 and 13) ————— */
* navigate_map_overview mockup, each column labeled with its source path. */ async function composeCompare(target, implementedFile, implementedCaption) {
{
const target = join(SHOTS, 'gallery-compare.png');
try {
if (!existsSync(MOCKUP_COMPARE)) throw new Error(`committed mockup missing: ${MOCKUP_COMPARE}`); if (!existsSync(MOCKUP_COMPARE)) throw new Error(`committed mockup missing: ${MOCKUP_COMPARE}`);
const liveB64 = readFileSync(join(SHOTS, 'shell-live-light.png')).toString('base64'); if (!existsSync(implementedFile)) throw new Error(`implemented capture missing: ${implementedFile}`);
const liveB64 = readFileSync(implementedFile).toString('base64');
const mockB64 = readFileSync(MOCKUP_COMPARE).toString('base64'); const mockB64 = readFileSync(MOCKUP_COMPARE).toString('base64');
const context = await browser.newContext({ viewport: { width: 880, height: 960 }, deviceScaleFactor: 1 }); const context = await browser.newContext({ viewport: { width: 880, height: 960 }, deviceScaleFactor: 1 });
const page = await context.newPage(); const page = await context.newPage();
@@ -367,7 +430,7 @@ try {
.cell { width: 100%; height: 800px; object-fit: contain; display: block; background: #d9d2c6; } .cell { width: 100%; height: 800px; object-fit: contain; display: block; background: #d9d2c6; }
.mockimg { background: #ffffff; } .mockimg { background: #ffffff; }
</style></head><body><div class="row"> </style></head><body><div class="row">
<figure><figcaption>IMPLEMENTED — web/screenshots/shell-live-light.png (?mock=android, harness build)</figcaption> <figure><figcaption>IMPLEMENTED — ${implementedCaption}</figcaption>
<img class="cell" src="data:image/png;base64,${liveB64}"></figure> <img class="cell" src="data:image/png;base64,${liveB64}"></figure>
<figure><figcaption>MOCKUP REFERENCE — stitch_minimalist_route_navigator/navigate_map_overview/screen.png</figcaption> <figure><figcaption>MOCKUP REFERENCE — stitch_minimalist_route_navigator/navigate_map_overview/screen.png</figcaption>
<img class="cell mockimg" src="data:image/png;base64,${mockB64}"></figure> <img class="cell mockimg" src="data:image/png;base64,${mockB64}"></figure>
@@ -378,11 +441,114 @@ try {
await page.screenshot({ path: target, fullPage: true }); await page.screenshot({ path: target, fullPage: true });
await context.close(); await context.close();
if (sizeOf(target) < MIN_BYTES) throw new Error(`capture undersized (${sizeOf(target)}B < ${MIN_BYTES}B)`); if (sizeOf(target) < MIN_BYTES) throw new Error(`capture undersized (${sizeOf(target)}B < ${MIN_BYTES}B)`);
}
/* 8 — DSGN-02 fidelity artifact: the live-light capture beside the committed
* navigate_map_overview mockup, each column labeled with its source path. */
{
const target = join(SHOTS, 'gallery-compare.png');
try {
await composeCompare(
target,
join(SHOTS, 'shell-live-light.png'),
'web/screenshots/shell-live-light.png (?mock=android, harness build)',
);
log.push(`captured gallery-compare.png (${sizeOf(target)}B, DSGN-02 side-by-side)`); log.push(`captured gallery-compare.png (${sizeOf(target)}B, DSGN-02 side-by-side)`);
} catch (err) { } catch (err) {
failures.push(`gallery-compare.png: ${err.message}`); failures.push(`gallery-compare.png: ${err.message}`);
} }
} }
/* 9–12 — the route experience (05-05). Every leg rides the ?fixpair knob:
* the mock's Telegram fix is the demo pair's PINNED PUBLIC origin resolved
* from the committed fixture corpus (D-12 — the harness introduces no user
* coordinate), paired with the ?dest carrier. All raced on the
* ARAHARAH-ROUTE state token + DOM text assertions. */
const routeQuery = `mock=android&dest=${demoPair.destinationId}&fixpair=${demoPair.pairId}`;
/* 9 — route drawn: orange casing stack + bottom card + endpoints, light. */
await captureLeg({
name: 'shell-route-fixture.png',
query: routeQuery,
colorScheme: 'light',
grantGeo: true,
waitMode: 'route',
expectState: 'drawn',
expectText: [routeDestEntry.name, 'MIN'],
});
/* 10 — alternative selection: tap the first chip → instant geometry swap +
* card re-render + camera refit (D-10 ALWAYS-refit). The demo pair is
* multi-route by selection above (RT10 floor 4); the degenerate arm proves
* the single-route law instead (single-route draws render NO chip row). */
await captureLeg({
name: 'shell-route-alt.png',
query: routeQuery,
colorScheme: 'light',
grantGeo: true,
waitMode: 'route',
expectState: 'drawn',
expectText: [routeDestEntry.name, 'MIN'],
interact: async (page) => {
const chip = page.locator('.route-chip').first();
if ((await chip.count()) > 0) {
await chip.click();
await page.waitForTimeout(800); // instant swap + 400ms refit + margin
const bodyText = await page.evaluate(() => document.body.textContent ?? '');
if (!/ROUTE \d\/\d/.test(bodyText)) {
throw new Error(`chip tap did not re-render the ROUTE {n}/{N} register — body text: ${JSON.stringify(bodyText.slice(0, 300))}`);
}
} else {
const chipCount = await page.locator('.route-chip').count();
if (chipCount !== 0) {
throw new Error(`single-route draw rendered ${chipCount} chips — the single-route law says none`);
}
log.push('shell-route-alt.png: degenerate single-route pair — no-chip law asserted');
}
},
});
/* 11 — router pending (D-03): an out-of-corpus destination renders the
* honest card — never a blank map, never a fabricated geometry (T-05-14).
* national-museum is RT4-guaranteed absent from corpus destinationIds
* (05-02's pinned probe carrier). */
await captureLeg({
name: 'shell-route-pending.png',
query: `mock=android&dest=national-museum&fixpair=${demoPair.pairId}`,
colorScheme: 'light',
grantGeo: true,
waitMode: 'route',
expectState: 'pending',
expectText: ['not wired up yet'],
});
/* 12 — dark theme (PX-02 cold-boot half for the route surface): chrome
* re-slots, route + basemap stay Rams-constant. */
await captureLeg({
name: 'shell-route-dark.png',
query: `${routeQuery}&scheme=dark`,
colorScheme: 'dark',
grantGeo: true,
waitMode: 'route',
expectState: 'drawn',
expectText: [routeDestEntry.name, 'MIN'],
});
/* 13 — DSGN-02 route fidelity artifact: the drawn-route capture beside the
* navigate_map_overview mockup (the same composer as leg 8). */
{
const target = join(SHOTS, 'route-gallery-compare.png');
try {
await composeCompare(
target,
join(SHOTS, 'shell-route-fixture.png'),
`web/screenshots/shell-route-fixture.png (?dest=${demoPair.destinationId}&fixpair=${demoPair.pairId}, harness build)`,
);
log.push(`captured route-gallery-compare.png (${sizeOf(target)}B, DSGN-02 route side-by-side)`);
} catch (err) {
failures.push(`route-gallery-compare.png: ${err.message}`);
}
}
} finally { } finally {
await browser.close(); await browser.close();
server.close(); server.close();
@@ -399,7 +565,12 @@ const expected = [
'shell-denied.png', 'shell-denied.png',
'shell-chat-pin.png', 'shell-chat-pin.png',
'shell-dest-preload.png', 'shell-dest-preload.png',
'shell-route-fixture.png',
'shell-route-alt.png',
'shell-route-pending.png',
'shell-route-dark.png',
'gallery-compare.png', 'gallery-compare.png',
'route-gallery-compare.png',
]; ];
const missing = expected.filter((f) => !existsSync(join(SHOTS, f))); const missing = expected.filter((f) => !existsSync(join(SHOTS, f)));
const undersized = expected.filter((f) => existsSync(join(SHOTS, f)) && sizeOf(join(SHOTS, f)) < MIN_BYTES); const undersized = expected.filter((f) => existsSync(join(SHOTS, f)) && sizeOf(join(SHOTS, f)) < MIN_BYTES);
@@ -412,5 +583,5 @@ if (missing.length || undersized.length || failures.length) {
process.exit(1); process.exit(1);
} }
console.log(`CAPTURE OK — 8/8 non-blank captures under web/screenshots/`); console.log(`CAPTURE OK — ${expected.length}/${expected.length} non-blank captures under web/screenshots/`);
process.exit(0); process.exit(0);