sync-trigger.desktop.test.ts

4.9 KB · raw

1// Change 6 — desktop peers participate in sync sessions.2//3// Relocated from drafts/DESKTOP-PAIRING/06-sync-coordinator-desktop.md.4// Spec 1 asserts the host filter (sync-trigger.ts:104) includes "desktop".5// Spec 2 asserts the browser's SYNC_HELLO carries a vaultId equal to the6// desktop helper's stored vault_id (== computeVaultId of the same master key),7// which is what peach-sync compares byte-for-byte (sync_coordinator.zig:141).89import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";1011import {12  triggerSyncIfReachable,13  setSyncRunner,14  _resetSyncTriggerForTest,15} from "./sync-trigger";16import { upsertPairedDevice } from "./paired-devices";17import { computeVaultId } from "../shared/sync-identity";18import { runChirpSyncInitiator, type ChirpSyncVaultAdapter } from "./chirp-sync-coordinator";19import type { ChirpSyncSession } from "../shared/chirp-sync-transport";20import type { ChirpSyncMessage } from "../shared/sync-types";21import type { PairedDevice } from "../shared/sync-types";2223// Minimal chrome.storage.local fake — paired-devices and sync-trigger both read24// through it. Re-initialised each beforeEach so state never leaks across specs.25function makeStorageStub(): void {26  const store: Record<string, unknown> = {};27  (globalThis as unknown as { chrome: unknown }).chrome = {28    storage: {29      local: {30        get: async (keys?: string | string[]) => {31          if (!keys) return { ...store };32          const list = Array.isArray(keys) ? keys : [keys];33          const out: Record<string, unknown> = {};34          for (const k of list) if (k in store) out[k] = store[k];35          return out;36        },37        set: async (obj: Record<string, unknown>) => { Object.assign(store, obj); },38        remove: async (keys: string | string[]) => {39          for (const k of Array.isArray(keys) ? keys : [keys]) delete store[k];40        },41      },42    },43    runtime: { lastError: undefined },44  };45}4647const MASTER = Uint8Array.from(Array.from({ length: 32 }, (_, i) => (i * 7 + 1) & 0xff));4849const DESKTOP: PairedDevice = {50  deviceId: "desktop-001",51  deviceName: "June's iMac",52  deviceType: "desktop",53  longTermPublicKeyHex: "a".repeat(64),54  trustSource: "direct",55  wsPort: 8765,56  pairedAt: 1700000000000,57};5859beforeEach(() => {60  makeStorageStub();61  _resetSyncTriggerForTest();62});6364afterEach(() => {65  vi.restoreAllMocks();66});6768describe("Change 6 — desktop peers in sync", () => {69  it("desktop peer is included in sync session attempts", async () => {70    await upsertPairedDevice(DESKTOP);71    const seen: string[] = [];72    setSyncRunner(async (device) => {73      seen.push(device.deviceId);74      return { ok: true, entriesReceived: 0, entriesSent: 0 };75    });7677    await triggerSyncIfReachable({ reason: "manual" });7879    // Before Change 6, the filter (sync-trigger.ts:104) only matched80    // android|ios, so the runner was never called for a desktop host.81    expect(seen).toEqual([DESKTOP.deviceId]);82  });8384  it("SYNC_HELLO vaultId matches the desktop helper's stored vault_id", async () => {85    // The helper stores exactly the vault_id the browser sent at pairing86    // (peach-sync pairing.zig:212), which is computeVaultId(masterKey). At87    // sync time the browser sends SYNC_HELLO { vaultId: computeVaultId(...) }.88    // peach-sync compares the two byte-for-byte (sync_coordinator.zig:141).89    const ourVaultId = await computeVaultId(MASTER);9091    // Capture frames the coordinator sends; reply just enough to reach the92    // equal-version SYNC_DONE shortcut so the round completes cleanly.93    const sent: ChirpSyncMessage[] = [];94    const inbound: ChirpSyncMessage[] = [95      { type: "SYNC_HELLO", vaultId: ourVaultId, deviceId: "desktop-001", version: 7, deviceTime: Date.now() },96      { type: "SYNC_DONE", entriesReceived: 0, entriesSent: 0 },97    ];98    let rx = 0;99    const session: ChirpSyncSession = {100      send: (bytes: Uint8Array) => { sent.push(JSON.parse(new TextDecoder().decode(bytes))); },101      receive: async () => new TextEncoder().encode(JSON.stringify(inbound[rx++])),102      close: async () => {},103    } as unknown as ChirpSyncSession;104105    const adapter: ChirpSyncVaultAdapter = {106      computeVaultId: async () => ourVaultId,107      getOurDeviceId: async () => "ext-local",108      getLocalVersion: async () => 7, // equal version → SYNC_DONE shortcut109      buildEntryManifest: async () => ({}),110      buildTombstoneManifest: async () => ({}),111      getEncryptedEntries: async () => ({}),112      applyEntries: async () => {},113      applyTombstones: async () => {},114    };115116    const outcome = await runChirpSyncInitiator(session, adapter);117118    const hello = sent.find((m) => m.type === "SYNC_HELLO") as119      | { type: "SYNC_HELLO"; vaultId: string }120      | undefined;121    expect(hello).toBeTruthy();122    // This is the exact value peach-sync compares against opts.vault_id.123    expect(hello!.vaultId).toBe(ourVaultId);124    expect(hello!.vaultId).toMatch(/^[0-9a-f]{64}$/);125    expect(outcome.status).toBe("completed");126  });127});