// Change 6 — desktop peers participate in sync sessions. // // Relocated from drafts/DESKTOP-PAIRING/06-sync-coordinator-desktop.md. // Spec 1 asserts the host filter (sync-trigger.ts:104) includes "desktop". // Spec 2 asserts the browser's SYNC_HELLO carries a vaultId equal to the // desktop helper's stored vault_id (== computeVaultId of the same master key), // which is what peach-sync compares byte-for-byte (sync_coordinator.zig:141). import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { triggerSyncIfReachable, setSyncRunner, _resetSyncTriggerForTest, } from "./sync-trigger"; import { upsertPairedDevice } from "./paired-devices"; import { computeVaultId } from "../shared/sync-identity"; import { runSyncInitiator, type SyncVaultAdapter } from "./sync-coordinator"; import type { SyncSession } from "../shared/sync-transport"; import type { SyncMessage } from "../shared/sync-types"; import type { PairedDevice } from "../shared/sync-types"; // Minimal chrome.storage.local fake — paired-devices and sync-trigger both read // through it. Re-initialised each beforeEach so state never leaks across specs. function makeStorageStub(): void { const store: Record = {}; (globalThis as unknown as { chrome: unknown }).chrome = { storage: { local: { get: async (keys?: string | string[]) => { if (!keys) return { ...store }; const list = Array.isArray(keys) ? keys : [keys]; const out: Record = {}; for (const k of list) if (k in store) out[k] = store[k]; return out; }, set: async (obj: Record) => { Object.assign(store, obj); }, remove: async (keys: string | string[]) => { for (const k of Array.isArray(keys) ? keys : [keys]) delete store[k]; }, }, }, runtime: { lastError: undefined }, }; } const MASTER = Uint8Array.from(Array.from({ length: 32 }, (_, i) => (i * 7 + 1) & 0xff)); const DESKTOP: PairedDevice = { deviceId: "desktop-001", deviceName: "June's iMac", deviceType: "desktop", longTermPublicKeyHex: "a".repeat(64), trustSource: "direct", wsPort: 8765, pairedAt: 1700000000000, }; beforeEach(() => { makeStorageStub(); _resetSyncTriggerForTest(); }); afterEach(() => { vi.restoreAllMocks(); }); describe("Change 6 — desktop peers in sync", () => { it("desktop peer is included in sync session attempts", async () => { await upsertPairedDevice(DESKTOP); const seen: string[] = []; setSyncRunner(async (device) => { seen.push(device.deviceId); return { ok: true, entriesReceived: 0, entriesSent: 0 }; }); await triggerSyncIfReachable({ reason: "manual" }); // Before Change 6, the filter (sync-trigger.ts:104) only matched // android|ios, so the runner was never called for a desktop host. expect(seen).toEqual([DESKTOP.deviceId]); }); it("SYNC_HELLO vaultId matches the desktop helper's stored vault_id", async () => { // The helper stores exactly the vault_id the browser sent at pairing // (peach-sync pairing.zig:212), which is computeVaultId(masterKey). At // sync time the browser sends SYNC_HELLO { vaultId: computeVaultId(...) }. // peach-sync compares the two byte-for-byte (sync_coordinator.zig:141). const ourVaultId = await computeVaultId(MASTER); // Capture frames the coordinator sends; reply just enough to reach the // equal-version SYNC_DONE shortcut so the round completes cleanly. const sent: SyncMessage[] = []; const inbound: SyncMessage[] = [ { type: "SYNC_HELLO", vaultId: ourVaultId, deviceId: "desktop-001", version: 7, deviceTime: Date.now() }, { type: "SYNC_DONE", entriesReceived: 0, entriesSent: 0 }, ]; let rx = 0; const session: SyncSession = { send: (bytes: Uint8Array) => { sent.push(JSON.parse(new TextDecoder().decode(bytes))); }, receive: async () => new TextEncoder().encode(JSON.stringify(inbound[rx++])), close: async () => {}, } as unknown as SyncSession; const adapter: SyncVaultAdapter = { computeVaultId: async () => ourVaultId, getOurDeviceId: async () => "ext-local", getLocalVersion: async () => 7, // equal version → SYNC_DONE shortcut buildEntryManifest: async () => ({}), buildTombstoneManifest: async () => ({}), getEncryptedEntries: async () => ({}), applyEntries: async () => {}, applyTombstones: async () => {}, }; const outcome = await runSyncInitiator(session, adapter); const hello = sent.find((m) => m.type === "SYNC_HELLO") as | { type: "SYNC_HELLO"; vaultId: string } | undefined; expect(hello).toBeTruthy(); // This is the exact value peach-sync compares against opts.vault_id. expect(hello!.vaultId).toBe(ourVaultId); expect(hello!.vaultId).toMatch(/^[0-9a-f]{64}$/); expect(outcome.status).toBe("completed"); }); });