import { describe, expect, it } from "vitest"; import { runChirpSyncInitiator, type ChirpSyncVaultAdapter } from "./chirp-sync-coordinator"; import type { ChirpSyncMessage, SignedDeviceManifest } from "../shared/sync-types"; import type { ChirpSyncSession } from "../shared/chirp-sync-transport"; import { buildSignedDeviceManifest } from "../shared/device-manifest"; import { deriveSyncSigningKeypair } from "../shared/sync-identity"; function inMemoryPair(): [ChirpSyncSession, FakePeer] { const a: Uint8Array[] = []; const b: Uint8Array[] = []; const aR: Array<(v: Uint8Array) => void> = []; const bR: Array<(v: Uint8Array) => void> = []; const ourSession: ChirpSyncSession = { send(bytes) { const r = bR.shift(); if (r) r(bytes); else b.push(bytes); }, receive(): Promise { const v = a.shift(); if (v) return Promise.resolve(v); return new Promise((res) => aR.push(res)); }, async close() { /* no-op */ }, remoteStaticPublicKey: new Uint8Array(32), }; const peer: FakePeer = { async send(msg: ChirpSyncMessage) { const bytes = new TextEncoder().encode(JSON.stringify(msg)); const r = aR.shift(); if (r) r(bytes); else a.push(bytes); }, async receive(): Promise { const v = b.shift(); const bytes = v ?? await new Promise((res) => bR.push(res)); return JSON.parse(new TextDecoder().decode(bytes)) as ChirpSyncMessage; }, }; return [ourSession, peer]; } interface FakePeer { send(msg: ChirpSyncMessage): Promise; receive(): Promise; } function makeVaultAdapter(opts: { vaultId?: string; deviceId?: string; localVersion?: number; entries?: Record; tombstones?: Record; ciphertext?: Record; }): ChirpSyncVaultAdapter & { appliedEntries: Record[]; appliedTombstones: Record[]; } { const appliedEntries: Record[] = []; const appliedTombstones: Record[] = []; return { async computeVaultId() { return opts.vaultId ?? "vault-abc"; }, async getOurDeviceId() { return opts.deviceId ?? "device-init"; }, // Use a non-zero default so the equal-version short circuit doesn't fire in // tests that aren't explicitly testing that path. async getLocalVersion() { return opts.localVersion ?? 5; }, async buildEntryManifest() { return { ...(opts.entries ?? {}) }; }, async buildTombstoneManifest() { return { ...(opts.tombstones ?? {}) }; }, async getEncryptedEntries(ids) { const out: Record = {}; const cipher = opts.ciphertext ?? {}; for (const id of ids) if (cipher[id] !== undefined) out[id] = cipher[id]; return out; }, async applyEntries(entries) { appliedEntries.push(entries); }, async applyTombstones(t) { appliedTombstones.push(t); }, appliedEntries, appliedTombstones, }; } describe("runChirpSyncInitiator", () => { it("completes a full sync with bi-directional updates", async () => { const [session, peer] = inMemoryPair(); const vault = makeVaultAdapter({ entries: { a: 100, b: 50 }, // we have 'a' at t=100, 'b' at t=50 tombstones: { x: 70 }, ciphertext: { a: "ct-a", b: "ct-b" }, }); const initPromise = runChirpSyncInitiator(session, vault); const peerProtocol = (async () => { const hello = await peer.receive(); expect(hello.type).toBe("SYNC_HELLO"); await peer.send({ type: "SYNC_HELLO", vaultId: "vault-abc", deviceId: "device-peer", version: 3, deviceTime: Date.now() }); const ourManifest = await peer.receive(); expect(ourManifest.type).toBe("SYNC_MANIFEST"); await peer.send({ type: "SYNC_MANIFEST", entries: { a: 80, b: 90, c: 200 }, // we have newer 'a'; peer has newer 'b' and 'c' tombstones: { x: 60, y: 99 }, // peer has newer tombstone for 'y' }); const ourRequest = await peer.receive(); expect(ourRequest.type).toBe("SYNC_REQUEST"); // We should request the entries the peer reported newer if (ourRequest.type === "SYNC_REQUEST") { expect(ourRequest.entryIds.sort()).toEqual(["b", "c"]); } // Peer asks us for entries the peer claimed it had older versions of await peer.send({ type: "SYNC_REQUEST", entryIds: ["a"] }); const ourEntries = await peer.receive(); expect(ourEntries.type).toBe("SYNC_ENTRIES"); if (ourEntries.type === "SYNC_ENTRIES") { expect(ourEntries.entries).toEqual({ a: "ct-a" }); } await peer.send({ type: "SYNC_ENTRIES", entries: { b: "ct-b-new", c: "ct-c" } }); const ourTombs = await peer.receive(); expect(ourTombs.type).toBe("SYNC_TOMBSTONES"); if (ourTombs.type === "SYNC_TOMBSTONES") { // We had x@70, peer had x@60, so we ship x@70 expect(ourTombs.tombstones).toEqual({ x: 70 }); } await peer.send({ type: "SYNC_TOMBSTONES", tombstones: { y: 99 } }); const ourDone = await peer.receive(); expect(ourDone.type).toBe("SYNC_DONE"); await peer.send({ type: "SYNC_DONE" }); })(); const outcome = await initPromise; await peerProtocol; expect(outcome.status).toBe("completed"); expect(outcome.entriesSent).toBe(1); expect(outcome.entriesReceived).toBe(2); expect(outcome.tombstonesSent).toBe(1); expect(outcome.tombstonesReceived).toBe(1); expect(vault.appliedEntries).toEqual([{ b: "ct-b-new", c: "ct-c" }]); expect(vault.appliedTombstones).toEqual([{ y: 99 }]); }); it("aborts with vault_mismatch on a peer with a different vault_id", async () => { const [session, peer] = inMemoryPair(); const vault = makeVaultAdapter({ vaultId: "vault-A" }); const initPromise = runChirpSyncInitiator(session, vault); const peerProtocol = (async () => { const hello = await peer.receive(); expect(hello.type).toBe("SYNC_HELLO"); await peer.send({ type: "SYNC_HELLO", vaultId: "vault-B", deviceId: "evil", version: 3, deviceTime: Date.now() }); // Peer should receive a SYNC_ERROR and we should stop. const err = await peer.receive(); expect(err.type).toBe("SYNC_ERROR"); })(); const outcome = await initPromise; await peerProtocol; expect(outcome.status).toBe("vault_mismatch"); }); it("exchanges device manifests when both adapters opt in", async () => { const [session, peer] = inMemoryPair(); const ourKp = await deriveSyncSigningKeypair(new Uint8Array(32).fill(0x11), "us"); const peerKp = await deriveSyncSigningKeypair(new Uint8Array(32).fill(0x22), "them"); const outgoing = await buildSignedDeviceManifest("us", [], ourKp.privateKey); const incomingFromPeer = await buildSignedDeviceManifest("them", [], peerKp.privateKey); const applied: SignedDeviceManifest[] = []; const base = makeVaultAdapter({}); const vault: ChirpSyncVaultAdapter = { ...base, async buildOutgoingDeviceManifest() { return outgoing; }, async applyIncomingDeviceManifest(m) { applied.push(m); }, }; const initPromise = runChirpSyncInitiator(session, vault); const peerProtocol = (async () => { await peer.receive(); // hello await peer.send({ type: "SYNC_HELLO", vaultId: "vault-abc", deviceId: "p", version: 3, deviceTime: Date.now() }); await peer.receive(); // manifest await peer.send({ type: "SYNC_MANIFEST", entries: {}, tombstones: {} }); await peer.receive(); // request await peer.send({ type: "SYNC_REQUEST", entryIds: [] }); await peer.receive(); // entries await peer.receive(); // tombstones await peer.send({ type: "SYNC_TOMBSTONES", tombstones: {} }); const ourManifest = await peer.receive(); expect(ourManifest.type).toBe("SYNC_DEVICE_MANIFEST"); if (ourManifest.type === "SYNC_DEVICE_MANIFEST") { expect(ourManifest.manifest.body.issuerDeviceId).toBe("us"); } await peer.send({ type: "SYNC_DEVICE_MANIFEST", manifest: incomingFromPeer }); await peer.receive(); // done await peer.send({ type: "SYNC_DONE" }); })(); const outcome = await initPromise; await peerProtocol; expect(outcome.status).toBe("completed"); expect(applied).toHaveLength(1); expect(applied[0].body.issuerDeviceId).toBe("them"); }); it("returns errored when peer sends a wrong frame", async () => { const [session, peer] = inMemoryPair(); const vault = makeVaultAdapter({ vaultId: "v" }); const initPromise = runChirpSyncInitiator(session, vault); void (async () => { await peer.receive(); // hello await peer.send({ type: "SYNC_HELLO", vaultId: "v", deviceId: "p", version: 3, deviceTime: Date.now() }); await peer.receive(); // our manifest await peer.send({ type: "SYNC_DONE" }); // wrong frame })(); const outcome = await initPromise; expect(outcome.status).toBe("errored"); expect(outcome.error).toMatch(/SYNC_MANIFEST/); }); });