Files
ditto.site/compiler/test/envShim.test.ts
Samraaj BathandClaude Fable 5 7e8863fc7e Capture hardening: deterministic env shim, extended lazy sweep, nav budget with bot-wall fast-fail
Salvaged and re-implemented from PR #15:
- Deterministic environment shim (seeded Math.random, pinned-but-advancing
  Date consistent across constructor/now/getTime/valueOf/toISOString) so
  time/random-dependent pages capture reproducibly; epoch and seed are
  recorded run metadata, performance.now stays real for motion sampling
- Post-snapshot lazy-asset discovery sweep for channels the walker misses:
  noscript contents (DOMParser), inline style url(), data-background(-image),
  img[loading=lazy] currentSrc
- Navigation gets a total 90s budget, an early bot-wall probe that aborts
  with a pollution-style error instead of capturing garbage (WALL_RE now
  shared with the validator gate), and one fresh-context retry scoped to
  the initial nav on retryable failure classes only

505 tests pass (29 new), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 13:49:14 -07:00

163 lines
8.0 KiB
TypeScript

import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import { chromium, type Browser, type Page } from "playwright";
import {
buildDeterministicEnvShim,
captureEpochMs,
DEFAULT_CAPTURE_EPOCH_MS,
DEFAULT_PRNG_SEED,
} from "../src/util/envShim.js";
const ESBUILD_SHIM =
"globalThis.__name = globalThis.__name || ((fn) => fn);" +
"globalThis.__defProp = globalThis.__defProp || Object.defineProperty;";
// ---------------------------------------------------------------------------
// Item 1: deterministic-env shim — pure epoch resolution + in-browser behavior.
// ---------------------------------------------------------------------------
describe("envShim: captureEpochMs resolution", () => {
it("defaults to the fixed epoch when no override is given", () => {
assert.equal(captureEpochMs(), DEFAULT_CAPTURE_EPOCH_MS);
assert.equal(captureEpochMs(undefined), DEFAULT_CAPTURE_EPOCH_MS);
});
it("passes a finite numeric override through unchanged", () => {
assert.equal(captureEpochMs(1234567890), 1234567890);
});
it("parses a numeric string and an ISO date string", () => {
assert.equal(captureEpochMs("1234567890"), 1234567890);
assert.equal(captureEpochMs("2020-01-01T00:00:00Z"), Date.parse("2020-01-01T00:00:00Z"));
});
it("falls back to the default for garbage input", () => {
assert.equal(captureEpochMs("not-a-date"), DEFAULT_CAPTURE_EPOCH_MS);
assert.equal(captureEpochMs(NaN), DEFAULT_CAPTURE_EPOCH_MS);
});
});
describe("envShim: buildDeterministicEnvShim source safety", () => {
it("interpolates only numeric literals (no injection surface)", () => {
const src = buildDeterministicEnvShim({ seed: 42, epochMs: 1000 });
assert.match(src, /let __s = 42 \| 0;/);
assert.match(src, /const delta = 1000 - realNow\(\);/);
// performance.now must remain untouched — the shim only patches Math.random + Date,
// never assigns to performance.now (motion.ts velocity sampling depends on the real one).
assert.equal(/performance\s*\.\s*now\s*=/.test(src), false);
assert.equal(/defineProperty\s*\(\s*performance/.test(src), false);
});
it("truncates non-integer seed/epoch and uses defaults for non-finite", () => {
const src = buildDeterministicEnvShim({ seed: 3.9, epochMs: 5.9 });
assert.match(src, /let __s = 3 \| 0;/);
assert.match(src, /const delta = 5 - realNow\(\);/);
const dflt = buildDeterministicEnvShim({ seed: NaN, epochMs: Infinity });
assert.match(dflt, new RegExp(`let __s = ${DEFAULT_PRNG_SEED} \\| 0;`));
assert.match(dflt, new RegExp(`const delta = ${DEFAULT_CAPTURE_EPOCH_MS} - realNow\\(\\);`));
});
});
const VW = 1280, VH = 800;
describe("envShim: in-browser behavior", () => {
let browser: Browser;
before(async () => { browser = await chromium.launch(); });
after(async () => { await browser.close(); });
// Fresh page with the shim installed BEFORE any page script (via addInitScript),
// exactly as capture.ts wires it. We navigate to a data: URL rather than setContent
// so the document is CREATED via navigation — addInitScript fires on document
// creation, which setContent (a content swap on the existing about:blank) skips.
// The inline <script> then runs after the init scripts and samples the pinned env.
const loadWithShim = async (opts: { seed?: number; epochMs?: number }, bodyScript: string): Promise<Page> => {
const page = await browser.newPage();
await page.setViewportSize({ width: VW, height: VH });
await page.addInitScript(ESBUILD_SHIM);
await page.addInitScript(buildDeterministicEnvShim(opts));
const html = `<!doctype html><html><body><script>${bodyScript}</script></body></html>`;
await page.goto("data:text/html," + encodeURIComponent(html));
return page;
};
it("seeds Math.random reproducibly across two independent loads (same seed → same sequence)", async () => {
const script = "window.__draws = Array.from({length: 10}, () => Math.random());";
const p1 = await loadWithShim({ seed: 12345, epochMs: DEFAULT_CAPTURE_EPOCH_MS }, script);
const p2 = await loadWithShim({ seed: 12345, epochMs: DEFAULT_CAPTURE_EPOCH_MS }, script);
const [a, b] = await Promise.all([
p1.evaluate(() => (window as unknown as { __draws: number[] }).__draws),
p2.evaluate(() => (window as unknown as { __draws: number[] }).__draws),
]);
assert.deepEqual(a, b, "two loads with the same seed produce identical Math.random sequences");
assert.ok(a.every((n) => n >= 0 && n < 1), "draws are in [0,1)");
// Not a constant stream — a broken PRNG returning the same value would also be
// "stable"; assert genuine variation so the test can't pass trivially.
assert.ok(new Set(a).size > 1, "the sequence varies");
await p1.close(); await p2.close();
});
it("a different seed yields a different sequence", async () => {
const script = "window.__draws = Array.from({length: 10}, () => Math.random());";
const p1 = await loadWithShim({ seed: 1, epochMs: DEFAULT_CAPTURE_EPOCH_MS }, script);
const p2 = await loadWithShim({ seed: 2, epochMs: DEFAULT_CAPTURE_EPOCH_MS }, script);
const [a, b] = await Promise.all([
p1.evaluate(() => (window as unknown as { __draws: number[] }).__draws),
p2.evaluate(() => (window as unknown as { __draws: number[] }).__draws),
]);
assert.notDeepEqual(a, b);
await p1.close(); await p2.close();
});
it("pins the Date epoch consistently across constructor / now() / getTime() / valueOf() / toISOString()", async () => {
const epoch = Date.parse("2021-06-15T12:00:00Z");
const page = await loadWithShim(
{ seed: 1, epochMs: epoch },
`window.__t = {
now: Date.now(),
ctorGetTime: new Date().getTime(),
ctorValueOf: new Date().valueOf(),
iso: new Date().toISOString(),
};`,
);
const t = await page.evaluate(() => (window as unknown as { __t: { now: number; ctorGetTime: number; ctorValueOf: number; iso: string } }).__t);
// All reads land at the pinned epoch (within a few ms of real elapsed time between them).
assert.ok(Math.abs(t.now - epoch) < 5000, `Date.now near epoch: ${t.now} vs ${epoch}`);
assert.ok(Math.abs(t.ctorGetTime - epoch) < 5000, "new Date().getTime() near epoch");
assert.ok(Math.abs(t.ctorValueOf - epoch) < 5000, "new Date().valueOf() near epoch");
assert.match(t.iso, /^2021-06-15T12:00:0/, `toISOString pinned: ${t.iso}`);
// now() and getTime() must agree (both read the same shifted instant).
assert.ok(Math.abs(t.now - t.ctorGetTime) < 5000, "now() and getTime() agree");
await page.close();
});
it("keeps the clock ADVANCING from the pinned epoch (timers still behave)", async () => {
const epoch = DEFAULT_CAPTURE_EPOCH_MS;
const page = await loadWithShim(
{ seed: 1, epochMs: epoch },
`window.__adv = new Promise((res) => {
const t0 = Date.now();
setTimeout(() => res({ t0, t1: Date.now() }), 60);
});`,
);
const { t0, t1 } = await page.evaluate(() => (window as unknown as { __adv: Promise<{ t0: number; t1: number }> }).__adv);
assert.ok(t0 >= epoch - 5000 && t0 <= epoch + 5000, "start is pinned near epoch");
assert.ok(t1 > t0, "time advances across a real setTimeout delay");
await page.close();
});
it("leaves performance.now() REAL (motion sampling depends on it) and advancing", async () => {
const page = await loadWithShim(
{ seed: 1, epochMs: DEFAULT_CAPTURE_EPOCH_MS },
`window.__perf = new Promise((res) => {
const p0 = performance.now();
setTimeout(() => res({ p0, p1: performance.now() }), 40);
});`,
);
const { p0, p1 } = await page.evaluate(() => (window as unknown as { __perf: Promise<{ p0: number; p1: number }> }).__perf);
// performance.now() is a real monotonic clock from navigation start — small, not the epoch.
assert.ok(p0 >= 0 && p0 < 60_000, `performance.now() is real (not pinned to epoch): ${p0}`);
assert.ok(p1 > p0, "performance.now() advances");
await page.close();
});
});