test(04-04): the BA7/BA8 drift gates + the search-tree GEO3 pin + the destination-preload capture leg

- BA7: the shipped JS must carry EVERY committed gazetteer id (the
  web half of bot/web id-set parity, T-04-39) — RED against the
  pre-gazetteer bundle, green after the resolver lands in the build
- BA8: the shipped JS carries NO deep-link crypto (JS assets only —
  the official Telegram SDK is external, D-11/T-04-40)
- no-storage: explicit web/src/search memory-only + no-crypto pin
- capture.mjs: the ARAHARAH-DEST signal race + the 'dest' wait mode
  (state=resolved asserted, then the rendered card's text is waited
  on — the card mounts at live, T-04-42) + the shell-dest-preload
  leg; 8/8 expected captures
This commit is contained in:
araharah-executor
2026-10-24 01:38:24 +08:00
parent c2d78b0c8f
commit 747eeb7b34
3 changed files with 106 additions and 6 deletions
+39 -5
View File
@@ -26,6 +26,7 @@
* shell-live-dark.png LIVE state, Telegram dark theme (PX-02) * shell-live-dark.png LIVE state, Telegram dark theme (PX-02)
* 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>)
* 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
* *
* 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
@@ -162,7 +163,7 @@ log.push(`serving web/dist-harness/ → ${base}/`);
const browser = await chromium.launch(); const browser = await chromium.launch();
const SIGNALS = ['RAMS-BASEMAP-READY', 'ARAHARAH-LIVE']; const SIGNALS = ['RAMS-BASEMAP-READY', 'ARAHARAH-LIVE', 'ARAHARAH-DEST'];
/* /*
* One capture leg = one context + one screenshot. The Phase-1 READY-race * One capture leg = one context + one screenshot. The Phase-1 READY-race
@@ -186,7 +187,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, live: 'no' }; const seen = { 'RAMS-BASEMAP-READY': false, 'ARAHARAH-LIVE': false, 'ARAHARAH-DEST': 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;
@@ -194,7 +195,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'}; 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'}; 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 });
@@ -216,6 +217,33 @@ async function captureLeg({ name, query, colorScheme, grantGeo, waitMode }) {
throw new Error(`theme slot mismatch: expected ${colorScheme}, documentElement[data-tg-theme]=${String(themed)}`); throw new Error(`theme slot mismatch: expected ${colorScheme}, documentElement[data-tg-theme]=${String(themed)}`);
} }
await page.waitForTimeout(2000); // tile render settle (plan: first render + 2s) await page.waitForTimeout(2000); // tile render settle (plan: first render + 2s)
} else if (waitMode === 'dest') {
const signal = await Promise.race([
page.waitForEvent('console', {
predicate: (m) => m.text().startsWith('ARAHARAH-DEST'),
timeout: SIGNAL_TIMEOUT_MS,
}).then(() => 'signal'),
new Promise((r) => setTimeout(() => r('timeout'), SIGNAL_TIMEOUT_MS)),
]);
if (signal === 'timeout') {
throw new Error(`ARAHARAH-DEST race timed out after ${SIGNAL_TIMEOUT_MS}ms — ${signalDiagnostics()}`);
}
/* The destination must actually be the RESOLVED one — a signal that fired
* with state=absent means the payload never reached the resolver (T-04-42).
* The signal is a STATE TOKEN (no id — D-15 hygiene), so the leg then
* proves WHICH destination resolved via the rendered card's text. The card
* mounts when the chain reaches live (the boot stages replace the stack
* transiently), so the DOM check waits rather than sampling once. */
const destLine = consoleLines.filter((l) => l.startsWith('ARAHARAH-DEST')).pop() ?? '';
if (!destLine.includes('state=resolved')) {
throw new Error(`destination did not resolve: "${destLine}" — expected state=resolved`);
}
await page.waitForFunction(
() => (document.body.textContent ?? '').includes('Suria KLCC'),
undefined,
{ timeout: 15000, polling: 250 },
);
await page.waitForTimeout(500);
} 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
@@ -268,6 +296,11 @@ try {
* a pin; the chain reaches LIVE offline — the automated Desktop-leg counterpart. */ * a pin; the chain reaches LIVE offline — the automated Desktop-leg counterpart. */
await captureLeg({ name: 'shell-chat-pin.png', query: 'mock=tdesktop&pin=1', colorScheme: 'light', grantGeo: false, waitMode: 'live' }); await captureLeg({ name: 'shell-chat-pin.png', query: 'mock=tdesktop&pin=1', colorScheme: 'light', grantGeo: false, waitMode: 'live' });
/* 7 — ONBD-02: the destination-preload boot. ?dest=<id> carries the plain
* gazetteer id in the URL's own query string (the inline-button carrier —
* RESEARCH Pitfall 1). The ARAHARAH-DEST signal resolves the race. */
await captureLeg({ name: 'shell-dest-preload.png', query: 'mock=android&dest=suria-klcc', colorScheme: 'light', grantGeo: true, waitMode: 'dest' });
/* GEO-03 runtime probe (its own live session) — GEO-03's "spot-checkable at /* GEO-03 runtime probe (its own live session) — GEO-03's "spot-checkable at
* phase exit" criterion made mechanical. Counts and key names only (T-03-24). */ * phase exit" criterion made mechanical. Counts and key names only (T-03-24). */
{ {
@@ -316,7 +349,7 @@ try {
} }
} }
/* 7 — DSGN-02 fidelity artifact: the live-light capture beside the committed /* 8 — DSGN-02 fidelity artifact: the live-light capture beside the committed
* navigate_map_overview mockup, each column labeled with its source path. */ * navigate_map_overview mockup, each column labeled with its source path. */
{ {
const target = join(SHOTS, 'gallery-compare.png'); const target = join(SHOTS, 'gallery-compare.png');
@@ -365,6 +398,7 @@ const expected = [
'shell-live-dark.png', 'shell-live-dark.png',
'shell-denied.png', 'shell-denied.png',
'shell-chat-pin.png', 'shell-chat-pin.png',
'shell-dest-preload.png',
'gallery-compare.png', 'gallery-compare.png',
]; ];
const missing = expected.filter((f) => !existsSync(join(SHOTS, f))); const missing = expected.filter((f) => !existsSync(join(SHOTS, f)));
@@ -378,5 +412,5 @@ if (missing.length || undersized.length || failures.length) {
process.exit(1); process.exit(1);
} }
console.log(`CAPTURE OK — 7/7 non-blank captures under web/screenshots/`); console.log(`CAPTURE OK — 8/8 non-blank captures under web/screenshots/`);
process.exit(0); process.exit(0);
+41
View File
@@ -24,6 +24,17 @@
* BA5 pointed there would invert the gate it exists to enforce. * BA5 pointed there would invert the gate it exists to enforce.
* BA6 no emitted HTML carries the runner page's /api/session or * BA6 no emitted HTML carries the runner page's /api/session or
* /api/location-fallback fetch stubs (same dead-code-elimination proof) * /api/location-fallback fetch stubs (same dead-code-elimination proof)
* BA7 the shipped JS carries EVERY gazetteer id from the committed
* dataset (search/src/data/kl-gazetteer.json) — the WEB half of the
* bot/web id-set parity gate (T-04-39): a stale web bundle ships
* without an id the bot just deep-linked, and the webview could
* never resolve it
* BA8 the shipped JS carries NO deep-link crypto (createHmac,
* crypto.subtle, sha256, HMAC() — the deep link is a plain public id
* (D-11, T-04-40). Scans the JS ASSETS only, never the HTML: the
* OFFICIAL Telegram SDK script is loaded EXTERNALLY from
* telegram.org (not bundled) and may contain crypto internals of
* its own.
* *
* If web/dist does not exist, every case fails with a build-first message. * If web/dist does not exist, every case fails with a build-first message.
*/ */
@@ -33,6 +44,7 @@ import { join } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import path from "node:path"; import path from "node:path";
import { test } from "./harness.mjs"; import { test } from "./harness.mjs";
import gazetteer from "../../search/src/data/kl-gazetteer.json" with { type: "json" };
const WEB = path.join(fileURLToPath(new URL(import.meta.url)), "..", ".."); const WEB = path.join(fileURLToPath(new URL(import.meta.url)), "..", "..");
const DIST = join(WEB, "dist"); const DIST = join(WEB, "dist");
@@ -133,3 +145,32 @@ test("BA6 production HTML carries NO runner-page fetch stub endpoints", () => {
); );
} }
}); });
test("BA7 the shipped bundle carries EVERY gazetteer id (the bot/web id-set parity gate, T-04-39)", () => {
assert.ok(existsSync(DIST_ASSETS), claimBuildFirst().message);
const bundle = jsFiles
.filter((f) => f.startsWith(DIST_ASSETS))
.map((f) => readFileSync(f, "utf8"))
.join("");
assert.ok(gazetteer.entries.length >= 30, `scanned a real dataset (found ${gazetteer.entries.length} entries)`);
for (const entry of gazetteer.entries) {
assert.ok(
bundle.includes(entry.id),
`gazetteer id "${entry.id}" is MISSING from the shipped web bundle — a stale bundle the webview cannot resolve against (T-04-39); ids are never renamed, only added`,
);
}
});
test("BA8 the shipped bundle carries NO deep-link crypto (D-11, T-04-40)", () => {
assert.ok(existsSync(DIST_ASSETS), claimBuildFirst().message);
const bundle = jsFiles
.filter((f) => f.startsWith(DIST_ASSETS))
.map((f) => readFileSync(f, "utf8"))
.join("");
for (const token of ["createHmac", "crypto.subtle", "sha256", "HMAC("]) {
assert.ok(
!bundle.includes(token),
`"${token}" found in a web/dist JS asset — the deep link is a PLAIN public id, no crypto anywhere in the path (D-11)`,
);
}
});
+26 -1
View File
@@ -12,7 +12,7 @@
* tokens appear — code occurrences only. * tokens appear — code occurrences only.
*/ */
import { strict as assert } from "node:assert"; import { strict as assert } from "node:assert";
import { readFileSync, readdirSync, statSync } from "node:fs"; import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
import path from "node:path"; import path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { test } from "./harness.mjs"; import { test } from "./harness.mjs";
@@ -77,3 +77,28 @@ test("GEO3 web/src/session/store.ts assigns nothing to any storage object (the D
// ...and keeps the named TTL constant the client revalidates against (D-03). // ...and keeps the named TTL constant the client revalidates against (D-03).
assert.ok(code.includes("SESSION_TTL_SECONDS = 900"), "the 900s TTL mirror by name is present"); assert.ok(code.includes("SESSION_TTL_SECONDS = 900"), "the 900s TTL mirror by name is present");
}); });
test("GEO3 web/src/search/ holds no persistence surface and no deep-link crypto", () => {
// The ONBD-02 tree (04-04): the resolved destination is user context and
// stays MEMORY-ONLY (GEO-03/D-07), and the deep-link path is a plain
// public id with NO crypto (D-11). The whole-tree GEO3 gate above already
// walks this directory; this explicit case pins the new tree + the
// no-crypto law so a future file there fails HERE, by name.
const searchDir = path.join(SRC, "search");
assert.ok(existsSync(searchDir), "web/src/search exists (the ONBD-02 resolver tree)");
for (const file of listFiles(searchDir, [".ts"])) {
const code = codeText(file);
for (const banned of BANNED) {
assert.ok(
!code.includes(banned),
`${path.relative(process.cwd(), file)} references ${banned} — GEO-03 violated (web/src/search must stay memory-only)`,
);
}
for (const token of ["createHmac", "crypto.subtle", "sha256"]) {
assert.ok(
!code.includes(token),
`${path.relative(process.cwd(), file)} references ${token} — D-11 forbids crypto in the deep-link path`,
);
}
}
});