// 🤖 Card schema contract tests: normalizeCard against every vector in // test/event-vectors/cards.json (the same file LChatCore's CardVectorTests // consumes). A failure here means the web renderer has drifted from the // contract — fix the implementation, never the vector, unless the schema // itself is deliberately changing on both platforms in one PR. import { readFileSync } from "node:fs"; import { normalizeCard, CARD_SCHEMA } from "../web/static/cards.js"; let failures = 0; function check(name, cond, detail) { if (cond) { console.log("PASS ", name); } else { failures++; console.error("FAIL ", name, detail ?? ""); } } // Canonical (sorted-key) stringify: the contract is about VALUES — JSON key // order is an artifact of each language's object representation. function canon(v) { if (Array.isArray(v)) return "[" + v.map(canon).join(",") + "]"; if (v && typeof v === "object") { return "{" + Object.keys(v).sort().map((k) => JSON.stringify(k) + ":" + canon(v[k])).join(",") + "}"; } return JSON.stringify(v); } const { schema, vectors } = JSON.parse(readFileSync(new URL("./event-vectors/cards.json", import.meta.url))); check("vector file schema matches CARD_SCHEMA", schema === CARD_SCHEMA); for (const v of vectors) { const got = normalizeCard(v.card); const ok = canon(got) === canon(v.want); check(v.name, ok, ok ? "" : `\n got ${canon(got)}\n want ${canon(v.want)}`); } // Bounds behave as bounds: 33 top-level blocks is one too many. const many = { schema: 1, blocks: Array.from({ length: 33 }, () => ({ b: "text", text: "x" })) }; check("block cap fails closed", normalizeCard(many) === null); const atCap = { schema: 1, blocks: Array.from({ length: 32 }, () => ({ b: "text", text: "x" })) }; check("block cap is inclusive", normalizeCard(atCap) !== null); // Normalizing is idempotent: normalize(normalize(c)) === normalize(c). for (const v of vectors) { if (!v.want) continue; const once = normalizeCard(v.card); check(`idempotent: ${v.name}`, canon(normalizeCard(once)) === canon(once)); } // The Swift test fixture must be a byte-identical copy of the canonical // vectors — the bundled copy exists only because SwiftPM tests can't reach // outside the package. Drift here means the native suite is pinning a stale // contract: re-copy test/event-vectors/cards.json over the fixture. const canonical = readFileSync(new URL("./event-vectors/cards.json", import.meta.url), "utf8"); const fixture = readFileSync(new URL("../ios/LChatCore/Tests/LChatCoreTests/Fixtures/cards-vector.json", import.meta.url), "utf8"); check("Swift fixture matches canonical vectors byte-for-byte", canonical === fixture); if (failures) { console.error(`\n${failures} failure(s)`); process.exit(1); } console.log("\nALL PASSED");