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:
+39
-5
@@ -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);
|
||||||
|
|||||||
@@ -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)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -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`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user