clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

feat/141-effect-library
lofi / package / schema / compat_test.ts
compat_test.ts 4.9K
1import {2 buildSchemaVersionRange,3 classifySchemaCompat,4 computeSchemaFingerprint,5 parseLocalSchemaVersion,6 parseSchemaCompatManifest,7 resolveAppWasmSchema,8 serializeLocalSchemaVersion,9} from "./compat.ts";10import { s } from "./mod.ts";1112function assert(condition: unknown, message: string): asserts condition {13 if (!condition) throw new Error(message);14}1516const appV1 = s.defineApp({17 todos: s.table({ title: s.string(), done: s.boolean() }),18});19const appV2 = s.defineApp({20 todos: s.table({ title: s.string(), done: s.boolean(), note: s.string() }),21});2223Deno.test("schema fingerprints are stable, versioned, and column-order insensitive", async () => {24 const first = await computeSchemaFingerprint(resolveAppWasmSchema(appV1));25 const again = await computeSchemaFingerprint(resolveAppWasmSchema(appV1));26 assert(first === again, "the same schema produced two fingerprints");27 assert(/^v1:[0-9a-f]{16}$/.test(first), `fingerprint is not versioned hex: ${first}`);2829 const reordered = {30 todos: {31 columns: [32 { name: "title", column_type: "Text" },33 { name: "done", column_type: "Boolean" },34 ],35 },36 };37 const sorted = {38 todos: {39 columns: [40 { name: "done", column_type: "Boolean" },41 { name: "title", column_type: "Text" },42 ],43 },44 };45 assert(46 await computeSchemaFingerprint(reordered) === await computeSchemaFingerprint(sorted),47 "column order changed the fingerprint",48 );49 assert(50 first !== await computeSchemaFingerprint(resolveAppWasmSchema(appV2)),51 "two different schemas collided",52 );53 const roundTripped = JSON.parse(54 JSON.stringify(resolveAppWasmSchema(appV1)),55 ) as Record<string, unknown>;56 assert(57 first === await computeSchemaFingerprint(roundTripped),58 "a schema and its JSON snapshot fingerprint differently",59 );60});6162Deno.test("nested apps fingerprint through their deploy target", async () => {63 const nested = s.defineNestedApp({64 taskapp: { tasks: s.table({ text: s.string() }) },65 });66 const fingerprint = await computeSchemaFingerprint(resolveAppWasmSchema(nested));67 assert(/^v1:[0-9a-f]{16}$/.test(fingerprint), "nested app produced no fingerprint");68});6970Deno.test("a schema version range carries its head and every ancestor", async () => {71 const ancestor = resolveAppWasmSchema(appV1);72 const range = await buildSchemaVersionRange(appV2, [ancestor]);73 const ancestorFingerprint = await computeSchemaFingerprint(ancestor);74 const head = await computeSchemaFingerprint(resolveAppWasmSchema(appV2));75 assert(range.head === head, "range head is not the app's fingerprint");76 assert(range.lineage.includes(head), "range lineage omitted the head");77 assert(range.lineage.includes(ancestorFingerprint), "range lineage omitted the ancestor");78 const noAncestors = await buildSchemaVersionRange(appV1);79 assert(80 noAncestors.lineage.length === 1 && noAncestors.lineage[0] === noAncestors.head,81 "an unmigrated app's lineage is not exactly its head",82 );83});8485Deno.test("gate classification covers first-boot, equal, code-ahead, data-ahead, unrelated", () => {86 const older = { head: "v1:a", lineage: ["v1:a"] };87 const newer = { head: "v1:b", lineage: ["v1:a", "v1:b"] };88 const foreign = { head: "v1:z", lineage: ["v1:z"] };89 assert(classifySchemaCompat(newer, null) === "first-boot", "no record must be first-boot");90 assert(classifySchemaCompat(newer, newer) === "equal", "same head must be equal");91 assert(92 classifySchemaCompat(newer, older) === "code-ahead",93 "a bundle whose lineage contains the data's head must be code-ahead",94 );95 assert(96 classifySchemaCompat(older, newer) === "data-ahead",97 "data stamped by a newer bundle must be data-ahead",98 );99 assert(100 classifySchemaCompat(older, foreign) === "unrelated",101 "disconnected histories must be unrelated",102 );103});104105Deno.test("manifest and local-record parsing reject unusable shapes", () => {106 const manifest = parseSchemaCompatManifest({107 v: 1,108 revision: "abc",109 head: "v1:a",110 lineage: ["v1:a"],111 });112 assert(manifest?.head === "v1:a", "a valid manifest was rejected");113 assert(parseSchemaCompatManifest(null) === null, "null passed manifest parsing");114 assert(115 parseSchemaCompatManifest({ v: 2, revision: "abc", head: "v1:a", lineage: ["v1:a"] }) === null,116 "an unknown manifest version was accepted",117 );118 assert(119 parseSchemaCompatManifest({ v: 1, revision: "abc", head: "v1:a", lineage: ["v1:b"] }) === null,120 "a manifest whose lineage omits its head was accepted",121 );122123 const record = serializeLocalSchemaVersion({ head: "v1:a", lineage: ["v1:a"] });124 assert(125 parseLocalSchemaVersion(record)?.head === "v1:a",126 "a round-tripped local record was rejected",127 );128 assert(parseLocalSchemaVersion("not json") === null, "malformed JSON passed record parsing");129 assert(parseLocalSchemaVersion(null) === null, "a missing record did not parse to null");130});