Files
ditto.site/compiler/test/patternCatalog.test.ts
Samraaj BathandClaude Fable 5 f991f469cf Pattern catalog: pinned library fingerprints as hint-only recipe evidence
Ported from PR #15: a SHA256-pinned catalog of 49 generic library and
platform fingerprints (carousel/consent/chat/commerce vendors) with an
O(1) signature index. Catalog hits strengthen recipe recognition (small
confidence deltas, marquee->logo-cloud, carousel false-grid skip,
lightbox->gallery, commerce prior) and are recorded in the manifest as
pattern evidence with node references - they never override captured
geometry, which remains gated by the existing plan-agreement checks.
The integrity pin is unconditional: a catalog/lock mismatch always
throws (the fork's env-var soft-warning mode is removed).

515 tests pass (10 new), typecheck clean.

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

151 lines
6.4 KiB
TypeScript

import { describe, it, afterEach } from "node:test";
import assert from "node:assert/strict";
import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import {
loadPatternIndex,
assertPinnedCatalog,
resolvePatternHints,
matchCatalogNode,
} from "../src/knowledge/patternIndex.js";
import { planForFrameUrl } from "../src/capture/graft.js";
import type { IR, IRNode, IRChild } from "../src/normalize/ir.js";
const CW = 1280;
const LOCK_PATH = fileURLToPath(new URL("../data/pattern-catalog.lock", import.meta.url));
/** Element node at the canonical viewport (ids assigned in pre-order by `page`). */
function el(
tag: string,
opts: { srcClass?: string; attrs?: Record<string, string>; children?: IRChild[] } = {},
): IRNode {
return {
id: "",
tag,
attrs: opts.attrs ?? {},
srcClass: opts.srcClass,
visibleByVp: { [CW]: true },
bboxByVp: { [CW]: { x: 0, y: 0, width: CW, height: 100 } },
computedByVp: { [CW]: { display: "block" } },
children: opts.children ?? [],
};
}
/** Wrap children in a <body> root and assign stable pre-order ids (n0, n1, …). */
function page(children: IRNode[]): IR {
const root = el("body", { children });
let i = 0;
const assign = (n: IRNode): void => {
n.id = `n${i++}`;
for (const c of n.children) if ((c as IRNode).tag) assign(c as IRNode);
};
assign(root);
return {
doc: {
sourceUrl: "https://example.test/", title: "Fixture", lang: "en", charset: "UTF-8",
metaViewport: "width=device-width, initial-scale=1",
viewports: [CW], sampleViewports: [CW], canonicalViewport: CW,
perViewport: { [CW]: { scrollHeight: 100, scrollWidth: CW, htmlBg: "", bodyBg: "", bodyColor: "", bodyFont: "" } },
nodeCount: i, keyframes: [],
},
root,
};
}
describe("pattern catalog: load + pin", () => {
it("loads the frozen catalog and pins by sha256 (lock verifies)", () => {
const idx = loadPatternIndex();
assert.equal(idx.catalog.version, 1);
assert.ok(idx.catalog.patterns.length > 0, "catalog has patterns");
// The on-disk lock must equal the catalog's live hash — no throw.
assert.doesNotThrow(() => assertPinnedCatalog());
const pinned = readFileSync(LOCK_PATH, "utf8").trim();
assert.equal(pinned, idx.hash, "lock matches catalog hash");
});
});
describe("pattern catalog: integrity check throws unconditionally", () => {
const original = readFileSync(LOCK_PATH, "utf8");
afterEach(() => {
// Always restore the real lock so later tests / runs see the pinned hash.
writeFileSync(LOCK_PATH, original);
});
it("throws on a lock/catalog hash mismatch (no env-var escape hatch)", () => {
writeFileSync(LOCK_PATH, "deadbeef".repeat(8) + "\n");
assert.throws(() => assertPinnedCatalog(), /hash mismatch/);
// Restoring must make it pass again.
writeFileSync(LOCK_PATH, original);
assert.doesNotThrow(() => assertPinnedCatalog());
});
});
describe("pattern catalog: fingerprint fixtures hit", () => {
it("matchCatalogNode identifies a generic carousel fingerprint by class token", () => {
const hits = matchCatalogNode({ tag: "div", attrs: {}, srcClass: "swiper swiper-wrapper" });
assert.deepEqual(hits.map((h) => h.id), ["carousel_swiper"]);
assert.equal(hits[0].kind, "carousel");
});
it("matchCatalogNode identifies a platform fingerprint by class prefix", () => {
const hits = matchCatalogNode({ tag: "div", attrs: {}, srcClass: "elementor-widget-container" });
assert.ok(hits.some((h) => h.id === "platform_elementor"), "elementor prefix hit");
});
it("matchCatalogNode identifies a scroll-animation fingerprint by attribute presence", () => {
const hits = matchCatalogNode({ tag: "div", attrs: { "data-aos": "fade-up" }, srcClass: undefined });
assert.ok(hits.some((h) => h.id === "anim_aos"), "data-aos attr hit");
});
it("resolvePatternHints walks the IR deterministically and records id + cid evidence", () => {
const ir = page([
el("div", { srcClass: "swiper", children: [el("div", { srcClass: "swiper-slide" })] }),
el("div", { srcClass: "swiper-slide" }),
]);
const hints = resolvePatternHints(ir);
const swiper = hints.matches.find((m) => m.id === "carousel_swiper");
assert.ok(swiper, "swiper detected");
// Three nodes carried a swiper token → count 3, cids are pre-order node ids.
assert.equal(swiper!.count, 3);
assert.deepEqual(swiper!.cids, ["n1", "n2", "n3"]);
// matches are sorted by id → deterministic evidence ordering.
const ids = hints.matches.map((m) => m.id);
assert.deepEqual(ids, [...ids].sort((a, b) => a.localeCompare(b)));
// Same catalog + same IR ⇒ byte-identical hints.
assert.deepEqual(resolvePatternHints(page([
el("div", { srcClass: "swiper", children: [el("div", { srcClass: "swiper-slide" })] }),
el("div", { srcClass: "swiper-slide" }),
])), hints);
});
it("resolvePatternHints exposes platform flags (stripped) and simpleStatic", () => {
const ir = page([el("section", { srcClass: "shopify-section" })]);
const hints = resolvePatternHints(ir);
assert.ok(hints.platforms.includes("shopify"), "shopify platform surfaced");
assert.ok(hints.flags.includes("platform_shopify"));
});
});
describe("popup-vendor skip list: FRAME_SKIP semantics preserved (single source of truth)", () => {
// The catalog's consent/chat entries are DOM class/id fingerprints; the capture
// pipeline's popup-vendor skip list (graft.ts FRAME_SKIP_RE) matches iframe host
// URLs. They are orthogonal axes — the catalog carries no URL hosts, so there is
// nothing to consolidate; capture's list stays the single source of truth for
// frame-graft decisions. Assert its semantics are unchanged.
it("skips promo/consent creative iframe hosts", () => {
assert.equal(planForFrameUrl("https://widget.privy.com/assets/popup.html"), "skip");
assert.equal(planForFrameUrl("https://app.recart.com/creative"), "skip");
assert.equal(planForFrameUrl("https://www.googletagmanager.com/ns.html"), "skip");
});
it("still grafts inline form embeds and same-origin blank frames", () => {
assert.equal(planForFrameUrl("https://static-forms.klaviyo.com/form"), "graft");
assert.equal(planForFrameUrl("about:blank"), "graft");
assert.equal(planForFrameUrl(""), "graft");
});
it("still renders media players as stills", () => {
assert.equal(planForFrameUrl("https://www.youtube.com/embed/abc"), "still");
});
});