clone

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

feat/fix-sync-enrollment-startup
lofi / package / runtime / namespace-state_test.ts
namespace-state_test.ts 3.9K
1// Package contract tests for the durable namespace election record.2import {3 completeLocalRowMigration,4 electManagedNamespace,5 readNamespaceState,6} from "./namespace-state.ts";7import { appId } from "./config.ts";8import { assert } from "./test-assert.ts";910const test = (globalThis as unknown as {11 Deno: { test(name: string, body: () => void | Promise<void>): void };12}).Deno.test;1314const stateKey = `lofi:namespace-state:${appId}`;15const legacyManagedKey = `lofi:managed-runtime:${appId}`;16const legacyMigrateKey = `lofi:migrate-local-rows:${appId}`;17const allKeys = [stateKey, legacyManagedKey, legacyMigrateKey];1819// Deno persists localStorage across test files and runs, so every test starts20// from explicitly cleared election keys and leaves none behind.21function withCleanStorage(body: () => void): () => void {22 return () => {23 for (const key of allKeys) localStorage.removeItem(key);24 try {25 body();26 } finally {27 for (const key of allKeys) localStorage.removeItem(key);28 }29 };30}3132test(33 "first boot elects the local namespace with no pending migration",34 withCleanStorage(() => {35 const state = readNamespaceState();36 assert(state.mode === "local", "a device without an election must boot local");37 assert(!state.migrateLocalRows, "a local device has nothing to migrate");38 }),39);4041test(42 "electing managed records mode and migration in one atomic record",43 withCleanStorage(() => {44 electManagedNamespace({ migrateLocalRows: true });45 const state = readNamespaceState();46 assert(state.mode === "managed", "election must switch the namespace to managed");47 assert(state.migrateLocalRows, "a local-to-managed transition must schedule migration");48 assert(49 localStorage.getItem(stateKey) !== null && localStorage.getItem(legacyManagedKey) === null,50 "election must write the single record, never the legacy split flags",51 );52 }),53);5455test(56 "completing migration clears only the pending flag",57 withCleanStorage(() => {58 electManagedNamespace({ migrateLocalRows: true });59 completeLocalRowMigration();60 const state = readNamespaceState();61 assert(state.mode === "managed", "completion must not change the elected namespace");62 assert(!state.migrateLocalRows, "completion must clear the pending migration");63 }),64);6566test(67 "re-election preserves migration progress in both directions",68 withCleanStorage(() => {69 electManagedNamespace({ migrateLocalRows: true });70 electManagedNamespace({ migrateLocalRows: false });71 assert(72 readNamespaceState().migrateLocalRows,73 "re-election must never clear a pending migration",74 );75 completeLocalRowMigration();76 electManagedNamespace({ migrateLocalRows: true });77 assert(78 !readNamespaceState().migrateLocalRows,79 "re-election must never resurrect a completed migration",80 );81 }),82);8384test(85 "legacy split flags upgrade to the record and are removed",86 withCleanStorage(() => {87 localStorage.setItem(legacyManagedKey, "1");88 localStorage.setItem(legacyMigrateKey, "1");89 const state = readNamespaceState();90 assert(state.mode === "managed", "a legacy managed election must survive the upgrade");91 assert(state.migrateLocalRows, "a legacy pending migration must survive the upgrade");92 assert(93 localStorage.getItem(legacyManagedKey) === null &&94 localStorage.getItem(legacyMigrateKey) === null,95 "the upgrade must retire the legacy keys",96 );97 assert(98 localStorage.getItem(stateKey) !== null,99 "the upgrade must persist the consolidated record",100 );101 }),102);103104test(105 "a corrupted record falls back to the local first-boot state",106 withCleanStorage(() => {107 localStorage.setItem(stateKey, "{not json");108 const state = readNamespaceState();109 assert(state.mode === "local", "an unreadable record must never elect managed");110 assert(!state.migrateLocalRows, "an unreadable record must never schedule migration");111 }),112);