feat(03-04): Mock-Telegram runner page + Playwright capture harness — 7/7 captures + GEO-03 probe

- runner page: ?mock/?scheme/?pin/?locale profile block wholly inside if (__ARAHARAH_HARNESS__) — installer + /api/session + /api/location + /api/location-fallback fetch stubs dead-code-eliminated from production; main.ts import stays outside the guard
- harness-only ?sessiondelay/?fixdelay boot-stage photograph knobs (localhost resolves the chain in ms — staged BOOT-01/BOOT-02 cards need a hold)
- capture.mjs extends theme/scripts/screenshot.mjs: MIN_BYTES, WR-05 docroot guard, READY-signal race, dist-harness docroot, DENIED_LEG_TIMEOUT_MS=90000 for LINK 3's 60s pre-denial poll, timeout = hard failure with a signal diagnostic
- 7 captures: boot-1/boot-2 stages, live light+dark (theme-slot attribute asserted), Desktop denied card w/ ASK VIA CHAT, chat-pin relay LIVE, DSGN-02 side-by-side vs navigate_map_overview
- GEO-03 runtime probe: localStorage/sessionStorage/cookies/IndexedDB asserted empty after boot-to-live
- mock fixes (Rule 1): LocationManager delivery now uses the Telegram wire shape {latitude,longitude,horizontal_accuracy,course} (authoring shape mapped to undefined → NaN LngLat pageerror); languageCode→initData so localeFromInitData parses the real wire format

CAPTURE OK — 7/7 non-blank captures; GEO-03 probe PASSED
This commit is contained in:
araharah-executor
2026-10-23 00:37:58 +08:00
parent 8b39ea70c3
commit 5c155e393d
4 changed files with 483 additions and 4 deletions
+43 -3
View File
@@ -26,6 +26,17 @@ export interface MockOptions {
platform?: "android" | "ios" | "tdesktop" | "macos" | "windows" | "linux" | "web";
version?: string;
colorScheme?: "light" | "dark";
/** Telegram language_code carried in the fake initData's user JSON —
* lets main.ts's localeFromInitData exercise its real parsing path
* and lets the runner page switch locales via ?locale=. */
languageCode?: string;
/** Artificial delivery delay (ms) for LocationManager.init()/getLocation()
* callbacks — the runner page's ?fixdelay= knob. At localhost speeds the
* whole three-source chain resolves in a few ms, so the staged boot cards
* (BOOT-01 validating / BOOT-02 acquiring) cannot otherwise be photographed;
* a delayed fix delivery holds the earlier stage on screen (03-04 capture
* harness only — defaults to 0, the fallback dev install is unchanged). */
deliveryDelayMs?: number;
telegramFix?: { lat: number; lng: number; accuracyM?: number | null; headingDeg?: number | null } | null;
webviewFix?: { lat: number; lng: number; accuracyM?: number } | null;
grantLocation?: boolean;
@@ -112,9 +123,26 @@ function versionAtLeast(have: string, want: string): boolean {
let installed: MockWebApp | null = null;
/**
* Harness initData: a non-empty, correctly-SHAPED Telegram wire string
* (`user=<url-encoded JSON>&auth_date=1&query_id=h&hash=harness`). The
* capture runner stubs POST /api/session OUTSIDE any dead-code strip, so
* this never reaches an HMAC validator — it exists so main.ts's
* localeFromInitData parses the real wire format instead of falling
* back to "en" (locale = options.languageCode, defaulting to "en").
* Untouched when no languageCode is given (the plain-dev fallback mock
* keeps initData "" exactly as before).
*/
function buildMockInitData(languageCode?: string): string {
if (languageCode === undefined) return "";
const user = JSON.stringify({ language_code: languageCode, id: 1, first_name: "Harness" });
return `user=${encodeURIComponent(user)}&auth_date=1&query_id=h&hash=harness`;
}
export function installMockWebApp(options: MockOptions = {}): TelegramWebAppLike {
const version = options.version ?? "8.0";
const telegramFix = options.telegramFix ?? null;
const delayMs = options.deliveryDelayMs ?? 0;
const lm: MockLocationManager = {
isInited: false,
isLocationAvailable: options.grantLocation !== false,
@@ -124,13 +152,25 @@ export function installMockWebApp(options: MockOptions = {}): TelegramWebAppLike
setTimeout(() => {
lm.isInited = true;
cb();
}, 0);
}, delayMs);
},
getLocation(cb: (data: unknown | null) => void) {
// grantLocation true → delivers the configured fix on a macrotask so the
// callback is asynchronous (a wedged client is cb(null) or silence).
// The DELIVERY SHAPE is the Telegram LocationManager wire format —
// { latitude, longitude, horizontal_accuracy, course } — because the
// consumer is requestTelegramFix, which maps exactly those keys. The
// MockOptions.telegramFix keys (lat/lng/accuracyM/headingDeg) are the
// authoring shape; delivering them verbatim maps to undefined → a NaN
// LngLat pageerror in marker.update (found by the 03-04 capture harness,
// the first end-to-end consumer of this callback).
if (options.grantLocation !== false && telegramFix !== null) {
setTimeout(() => cb(telegramFix), 0);
setTimeout(() => cb({
latitude: telegramFix.lat,
longitude: telegramFix.lng,
horizontal_accuracy: telegramFix.accuracyM ?? undefined,
course: telegramFix.headingDeg ?? undefined,
}), delayMs);
} else {
setTimeout(() => cb(null), 0); // the denial signal — synchronous null would hide async bugs
}
@@ -139,7 +179,7 @@ export function installMockWebApp(options: MockOptions = {}): TelegramWebAppLike
const events = new Map<string, Array<() => void>>();
const backButtonClicks: Array<() => void> = [];
const fake: MockWebApp = {
initData: "",
initData: buildMockInitData(options.languageCode),
colorScheme: options.colorScheme ?? "light",
themeParams: {},
version,