import { describe, expect, it } from "vitest"; import { MAX_SNAPSHOT_CHUNKS, isAdoptSnapshot, receiveFrame, receiveVaultSnapshot, requestAdoption, sendFrame, sendVaultSnapshot, type AdoptionReceiverDeps, } from "./vault-adopt"; import { memoryTransportPair, type PairingTransport } from "./pairing-handshake"; describe("inline vault snapshot", () => { it("round-trips an empty vault (0 entries)", async () => { const [source, receiver] = memoryTransportPair(); const [result] = await Promise.all([ receiveVaultSnapshot(receiver), sendVaultSnapshot(source, {}, {}), ]); expect(result.entries).toEqual({}); expect(result.tombstones).toEqual({}); }); it("round-trips 50 entries + 5 tombstones in a single chunk", async () => { const [source, receiver] = memoryTransportPair(); const entries: Record = {}; for (let i = 0; i < 50; i++) entries[`id-${i}`] = `cipher-${i}`; const tombstones: Record = {}; for (let i = 0; i < 5; i++) tombstones[`dead-${i}`] = 1000 + i; const [result] = await Promise.all([ receiveVaultSnapshot(receiver), sendVaultSnapshot(source, entries, tombstones), ]); expect(result.entries).toEqual(entries); expect(result.tombstones).toEqual(tombstones); }); it("splits a large vault into 3+ chunks and reassembles it", async () => { const [source, receiver] = memoryTransportPair(); let frames = 0; const countingSource: PairingTransport = { send: (msg) => { frames++; return source.send(msg); }, receive: () => source.receive(), }; const big = "x".repeat(200 * 1024); // 200 KiB per entry forces chunking const entries: Record = {}; for (let i = 0; i < 5; i++) entries[`id-${i}`] = big; const tombstones: Record = { "dead-0": 42 }; const [result] = await Promise.all([ receiveVaultSnapshot(receiver), sendVaultSnapshot(countingSource, entries, tombstones), ]); expect(frames).toBeGreaterThanOrEqual(3); expect(result.entries).toEqual(entries); expect(result.tombstones).toEqual(tombstones); }); it("rejects out-of-order chunks", async () => { const [source, receiver] = memoryTransportPair(); const send = sendFrame(source, { type: "ADOPT_SNAPSHOT", chunk_index: 1, chunk_count: 2, entries: { a: "x" }, tombstones: {}, }); await expect(receiveVaultSnapshot(receiver)).rejects.toThrow(/OUT_OF_ORDER/); await send; }); it("rejects a chunk_count exceeding MAX_SNAPSHOT_CHUNKS", async () => { const [source, receiver] = memoryTransportPair(); const send = sendFrame(source, { type: "ADOPT_SNAPSHOT", chunk_index: 0, chunk_count: MAX_SNAPSHOT_CHUNKS + 1, entries: {}, tombstones: {}, }); await expect(receiveVaultSnapshot(receiver)).rejects.toThrow(/TOO_MANY_CHUNKS/); await send; }); it("validates ADOPT_SNAPSHOT frames", () => { expect( isAdoptSnapshot({ type: "ADOPT_SNAPSHOT", chunk_index: 0, chunk_count: 1, entries: { a: "x" }, tombstones: {}, }), ).toBe(true); // Missing tombstones / chunk_index. expect( isAdoptSnapshot({ type: "ADOPT_SNAPSHOT", chunk_index: 0, chunk_count: 1, entries: {} }), ).toBe(false); expect( isAdoptSnapshot({ type: "ADOPT_SNAPSHOT", chunk_count: 1, entries: {}, tombstones: {} }), ).toBe(false); // Wrong type tag. expect( isAdoptSnapshot({ type: "ADOPT_GRANT", chunk_index: 0, chunk_count: 1, entries: {}, tombstones: {} }), ).toBe(false); // Invalid indices and value types. expect( isAdoptSnapshot({ type: "ADOPT_SNAPSHOT", chunk_index: -1, chunk_count: 1, entries: {}, tombstones: {} }), ).toBe(false); expect( isAdoptSnapshot({ type: "ADOPT_SNAPSHOT", chunk_index: 0, chunk_count: 0, entries: {}, tombstones: {} }), ).toBe(false); expect( isAdoptSnapshot({ type: "ADOPT_SNAPSHOT", chunk_index: 0, chunk_count: 1, entries: { a: 5 }, tombstones: {} }), ).toBe(false); expect(isAdoptSnapshot(null)).toBe(false); }); it("advertises snapshot_capable in the ADOPT_REQUEST frame", async () => { const [receiverChannel, sourceChannel] = memoryTransportPair(); const deps: AdoptionReceiverDeps = { deviceName: "recv", generateDeviceId: () => "recv-id", async confirmSas() { return true; }, async createVaultFromRootKey() { return { vaultId: "v" }; }, async deriveLocalIdentity() { return { deviceId: "recv-id", deviceName: "recv", deviceType: "extension", longTermPublicKeyHex: "ab".repeat(32), }; }, async upsertPairedDevice() {}, onError() {}, }; void requestAdoption(receiverChannel, deps); const first = await receiveFrame(sourceChannel); expect(first).toMatchObject({ type: "ADOPT_REQUEST", snapshot_capable: true }); }); });