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>
This commit is contained in:
Samraaj Bath
2026-07-04 13:49:14 -07:00
co-authored by Claude Fable 5
parent 3faf1ef043
commit 7e8863fc7e
7 changed files with 781 additions and 54 deletions
+117
View File
@@ -0,0 +1,117 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
WALL_RE,
isWallText,
isBotWall,
WALL_MAX_NODES,
classifyNavFailure,
isRetryableNavFailure,
} from "../src/util/captureFailure.js";
// ---------------------------------------------------------------------------
// Item 3: capture-side fast-fail predicates (pure — no browser).
// ---------------------------------------------------------------------------
describe("captureFailure: wall-text detection", () => {
it("matches common bot/auth-wall fingerprints", () => {
for (const t of [
"Just a moment...",
"Checking your browser before accessing",
"Please enable JavaScript to continue",
"Access to this page has been denied",
"Are you a human?",
"Verify you are human",
"Attention Required! | Cloudflare",
"Please complete the CAPTCHA",
"DDoS protection by Cloudflare",
]) {
assert.ok(isWallText(t), `expected wall text to match: ${t}`);
}
});
it("does not match ordinary marketing/body copy", () => {
for (const t of [
"Welcome to our store — shop the new collection",
"Our team of humans is here to help you 24/7",
"Enable notifications to get the latest deals",
"About us · Careers · Contact",
]) {
assert.equal(isWallText(t), false, `expected NOT to match: ${t}`);
}
});
it("WALL_RE is the same instance shared with the pollution gate (single source of truth)", () => {
// gates.ts imports WALL_RE from this module; assert the export is a real RegExp
// so a refactor that turns it into a function would fail here.
assert.ok(WALL_RE instanceof RegExp);
});
});
describe("captureFailure: isBotWall (matches the pollution gate's small+wall rule)", () => {
it("flags a small page with wall text", () => {
assert.ok(isBotWall({ text: "Just a moment...", nodes: 8 }));
assert.ok(isBotWall({ text: "checking your browser", nodes: WALL_MAX_NODES - 1 }));
});
it("does NOT flag a large page that merely mentions a wall phrase", () => {
// A real page can say "captcha" in its help docs — node count is the discriminator.
assert.equal(isBotWall({ text: "how our captcha works", nodes: WALL_MAX_NODES }), false);
assert.equal(isBotWall({ text: "how our captcha works", nodes: 5000 }), false);
});
it("does NOT flag a small page with no wall text", () => {
assert.equal(isBotWall({ text: "Home · About · Contact", nodes: 10 }), false);
});
it("is null-safe", () => {
assert.equal(isBotWall(null), false);
assert.equal(isBotWall(undefined), false);
});
});
describe("captureFailure: classifyNavFailure", () => {
it("classifies session-death / crash / transient network as retryable", () => {
for (const msg of [
"Target page, context or browser has been closed",
"Target closed",
"browser has been closed",
"page has crashed",
"net::ERR_CONNECTION_RESET at https://example.com",
"net::ERR_TIMED_OUT",
"Timeout 45000ms exceeded",
"Navigation timeout of 30000 ms exceeded",
]) {
assert.equal(classifyNavFailure(new Error(msg)), "retryable", msg);
assert.equal(isRetryableNavFailure(new Error(msg)), true, msg);
}
});
it("classifies wall fingerprints as wall (not retryable — a fresh context can't help)", () => {
const e = new Error("navigation failed: just a moment... checking your browser");
assert.equal(classifyNavFailure(e), "wall");
assert.equal(isRetryableNavFailure(e), false);
});
it("classifies hard failures as terminal (not retryable)", () => {
for (const msg of [
"net::ERR_NAME_NOT_RESOLVED",
"net::ERR_CERT_AUTHORITY_INVALID",
"net::ERR_CONNECTION_REFUSED",
"something totally unexpected",
]) {
assert.equal(classifyNavFailure(new Error(msg)), "terminal", msg);
assert.equal(isRetryableNavFailure(new Error(msg)), false, msg);
}
});
it("wall wins over a transient signature (a wall served over a reset is still a wall)", () => {
const e = new Error("net::ERR_CONNECTION_RESET — access denied");
assert.equal(classifyNavFailure(e), "wall");
});
it("accepts a bare string or non-Error value", () => {
assert.equal(classifyNavFailure("Target closed"), "retryable");
assert.equal(classifyNavFailure(undefined), "terminal");
});
});