d04f5b124fc2
feat: sync coordinator includes desktop peers in sync session attempts (v4.0.2)
- time
- 2026-06-17 13:18 UTC · 4 months ago
- machine
- koh2
- agent
- claude-opus-4-8
- session
- desktop helper pairing integration
- parent
- 64236f0049f1
↓ peach-browser-d04f5b12.tar.gzatlas at this save
files changed2 files
src/background/sync-trigger.desktop.test.ts+127 −0src/background/sync-trigger.ts+3 −1src/background/sync-trigger.desktop.test.ts+127 −0
@@ -0,0 +1,127 @@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).8+9+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";10+11+import {12+ triggerSyncIfReachable,13+ setSyncRunner,14+ _resetSyncTriggerForTest,15+} from "./sync-trigger";16+import { upsertPairedDevice } from "./paired-devices";17+import { computeVaultId } from "../shared/sync-identity";18+import { runChirpSyncInitiator, type ChirpSyncVaultAdapter } from "./chirp-sync-coordinator";19+import type { ChirpSyncSession } from "../shared/chirp-sync-transport";20+import type { ChirpSyncMessage } from "../shared/sync-types";21+import type { PairedDevice } from "../shared/sync-types";22+23+// Minimal chrome.storage.local fake — paired-devices and sync-trigger both read24+// through it. Re-initialised each beforeEach so state never leaks across specs.25+function 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+}46+47+const MASTER = Uint8Array.from(Array.from({ length: 32 }, (_, i) => (i * 7 + 1) & 0xff));48+49+const 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+};58+59+beforeEach(() => {60+ makeStorageStub();61+ _resetSyncTriggerForTest();62+});63+64+afterEach(() => {65+ vi.restoreAllMocks();66+});67+68+describe("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+ });76+77+ await triggerSyncIfReachable({ reason: "manual" });78+79+ // 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+ });83+84+ 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);90+91+ // 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;104+105+ 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+ };115+116+ const outcome = await runChirpSyncInitiator(session, adapter);117+118+ 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+});src/background/sync-trigger.ts+3 −1
@@ -103,7 +103,9 @@103103 const devices = await listPairedDevices();104104 const hosts = devices.filter(105105 (d) =>106- (d.deviceType === "android" || d.deviceType === "ios") &&106+ (d.deviceType === "android" ||107+ d.deviceType === "ios" ||108+ d.deviceType === "desktop") &&107109 d.trustSource === "direct",108110 );109111 if (hosts.length === 0) return;