123456789import { 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 { runSyncInitiator, type SyncVaultAdapter } from "./sync-coordinator";19import type { SyncSession } from "../shared/sync-transport";20import type { SyncMessage } from "../shared/sync-types";21import type { PairedDevice } from "../shared/sync-types";22232425function 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 80 81 expect(seen).toEqual([DESKTOP.deviceId]);82 });8384 it("SYNC_HELLO vaultId matches the desktop helper's stored vault_id", async () => {85 86 87 88 89 const ourVaultId = await computeVaultId(MASTER);9091 92 93 const sent: SyncMessage[] = [];94 const inbound: SyncMessage[] = [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: SyncSession = {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 SyncSession;104105 const adapter: SyncVaultAdapter = {106 computeVaultId: async () => ourVaultId,107 getOurDeviceId: async () => "ext-local",108 getLocalVersion: async () => 7, 109 buildEntryManifest: async () => ({}),110 buildTombstoneManifest: async () => ({}),111 getEncryptedEntries: async () => ({}),112 applyEntries: async () => {},113 applyTombstones: async () => {},114 };115116 const outcome = await runSyncInitiator(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 123 expect(hello!.vaultId).toBe(ourVaultId);124 expect(hello!.vaultId).toMatch(/^[0-9a-f]{64}$/);125 expect(outcome.status).toBe("completed");126 });127});