clone

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

main
lofi / package / runtime / store-status_test.ts
store-status_test.ts 5.4K
1// Package contract tests for the boot store-status diagnostic: when the2// preflight runs at all, how every node answer maps onto the runtime3// diagnostic, and that failures and timeouts degrade to a diagnostic value4// instead of gating or failing boot.5import type { TicketStoreStatus } from "../schema/store.ts";6import { isTicketServerUrl } from "./data-sink.ts";7import {8 describeStoreStatus,9 resolveStoreStatus,10 type RuntimeStoreStatus,11} from "./store-status.ts";12import { assert } from "./test-assert.ts";1314const ticketUrl = `http://192.168.1.10:4802/t/${"s".repeat(43)}`;1516Deno.test("the diagnostic stays unchecked without managed sync or a ticket-gated sink", async () => {17 let calls = 0;18 const preflight = (): Promise<TicketStoreStatus> => {19 calls += 1;20 return Promise.resolve({ state: "no_schema", appId: "a-app" });21 };22 const notConnected = await resolveStoreStatus({23 connect: false,24 sink: { serverUrl: ticketUrl },25 preflight,26 });27 assert(28 notConnected.state === "unchecked" && notConnected.reason === "sync-not-connected",29 `a non-connecting runtime resolved to ${JSON.stringify(notConnected)}`,30 );31 const localOnly = await resolveStoreStatus({ connect: true, sink: null, preflight });32 assert(33 localOnly.state === "unchecked" && localOnly.reason === "sync-not-connected",34 `a local-only device resolved to ${JSON.stringify(localOnly)}`,35 );36 const firstParty = await resolveStoreStatus({37 connect: true,38 sink: { serverUrl: "https://sync.example.com" },39 preflight,40 });41 assert(42 firstParty.state === "unchecked" && firstParty.reason === "sink-not-ticket-gated",43 `a first-party server sink resolved to ${JSON.stringify(firstParty)}`,44 );45 assert(calls === 0, "an unchecked diagnostic must never touch the network");46});4748Deno.test("every preflight answer maps onto the runtime diagnostic", async () => {49 const resolve = (answer: TicketStoreStatus): Promise<RuntimeStoreStatus> =>50 resolveStoreStatus({51 connect: true,52 sink: { serverUrl: ticketUrl },53 preflight: (url) => {54 assert(url === ticketUrl, `the preflight received ${url}`);55 return Promise.resolve(answer);56 },57 });58 const deployed = await resolve({ state: "deployed", appId: "a-app", headHash: "ff85" });59 assert(60 deployed.state === "deployed" && deployed.headHash === "ff85",61 `a deployed store resolved to ${JSON.stringify(deployed)} — the head hash must surface ` +62 "verbatim so drift against this app's deployment stays a human judgement",63 );64 const fresh = await resolve({ state: "no_schema", appId: "a-app" });65 assert(fresh.state === "no_schema", `a schema-less store resolved to ${JSON.stringify(fresh)}`);66 assert(67 fresh.state === "no_schema" && fresh.message.includes("hang") &&68 fresh.message.includes("Provision"),69 "no_schema must carry the actionable message naming the hanging-write consequence and the " +70 "provisioning remediation",71 );72 for (const state of ["store_unavailable", "ticket_rejected", "unsupported"] as const) {73 const mapped = await resolve({ state });74 assert(mapped.state === state, `${state} resolved to ${JSON.stringify(mapped)}`);75 }76});7778Deno.test("a hung or failing preflight degrades to store_unavailable", async () => {79 const hung = await resolveStoreStatus({80 connect: true,81 sink: { serverUrl: ticketUrl },82 preflight: () => new Promise<TicketStoreStatus>(() => {}),83 timeoutMs: 20,84 });85 assert(86 hung.state === "store_unavailable",87 `a black-holed preflight resolved to ${JSON.stringify(hung)}`,88 );89 const failed = await resolveStoreStatus({90 connect: true,91 sink: { serverUrl: ticketUrl },92 preflight: () => Promise.reject(new Error("network down")),93 });94 assert(95 failed.state === "store_unavailable",96 `a rejecting preflight resolved to ${JSON.stringify(failed)} instead of a diagnostic value`,97 );98});99100Deno.test("only ticket and PoP connect paths count as ticket-gated server URLs", () => {101 assert(isTicketServerUrl(ticketUrl), "a valid ticket URL must be recognized");102 assert(103 isTicketServerUrl(`${ticketUrl}/c/${"c".repeat(43)}`),104 "a valid PoP connect URL must be recognized",105 );106 assert(107 !isTicketServerUrl("https://sync.example.com"),108 "a first-party server URL must not be probed",109 );110 assert(111 !isTicketServerUrl("https://node.example/t/short"),112 "a short secret segment must not be treated as a ticket path",113 );114 assert(115 !isTicketServerUrl(`${ticketUrl}/c/short`),116 "a short connect token must not be treated as a ticket path",117 );118 assert(!isTicketServerUrl("not a url"), "a malformed URL must not be treated as a ticket path");119});120121Deno.test("describeStoreStatus gives every state a distinct label", () => {122 const statuses: RuntimeStoreStatus[] = [123 { state: "unchecked", reason: "sync-not-connected" },124 { state: "unchecked", reason: "sink-not-ticket-gated" },125 { state: "deployed", headHash: "ff85" },126 { state: "no_schema", message: "provision" },127 { state: "store_unavailable" },128 { state: "ticket_rejected" },129 { state: "unsupported" },130 ];131 const labels = statuses.map(describeStoreStatus);132 assert(new Set(labels).size === labels.length, `labels collide: ${labels.join(" | ")}`);133 assert(labels[2].includes("ff85"), "the deployed label must show the head hash");134 assert(135 labels[0].startsWith("not checked") && labels[1].startsWith("not checked"),136 "unchecked labels must read as not-a-problem, never as an alarm",137 );138});