1import { describe, expect, it, vi } from "vitest";23import {4 buildLocalSyncSnapshot,5 cancelSyncIfVaultLocked,6 checkSyncPreflight,7 planManifestSync,8} from "./sync-coordinator";9import { decodeSyncMessage } from "../shared/sync-wire";10import type { VaultEntry } from "../shared/types";1112function entry(overrides: Partial<VaultEntry> = {}): VaultEntry {13 return {14 id: "entry-1",15 type: "login",16 title: "GitHub",17 username: "june",18 password: "secret",19 url: "https://github.com",20 createdAt: 1_000,21 modifiedAt: 1_000,22 ...overrides,23 };24}2526describe("sync coordinator", () => {27 it("blocks sync for free users before inspecting the vault", async () => {28 const getEntries = vi.fn(() => [entry()]);2930 await expect(31 checkSyncPreflight({32 isProUser: async () => false,33 isUnlocked: () => true,34 })35 ).resolves.toEqual({ ok: false, error: "pro_required" });3637 expect(getEntries).not.toHaveBeenCalled();38 });3940 it("blocks sync when the vault is locked", async () => {41 await expect(42 checkSyncPreflight({43 isProUser: async () => true,44 isUnlocked: () => false,45 })46 ).resolves.toEqual({ ok: false, error: "vault_locked" });47 });4849 it("builds a local sync snapshot only after preflight passes", async () => {50 const snapshot = await buildLocalSyncSnapshot({51 isProUser: async () => true,52 isUnlocked: () => true,53 getEntries: () => [entry({ id: "b" }), entry({ id: "a", modifiedAt: 2_000 })],54 });5556 expect(snapshot.entries).toHaveLength(2);57 expect(snapshot.manifest.map((item) => item.id)).toEqual(["a", "b"]);58 });5960 it("plans manifest sync without sending entry payloads", async () => {61 const plan = await planManifestSync(62 [{ id: "remote-only", modifiedAt: 1_500, hash: "remote" }],63 {64 isProUser: async () => true,65 isUnlocked: () => true,66 getEntries: () => [entry({ id: "local-only", modifiedAt: 2_000 })],67 }68 );6970 expect(plan.diff).toEqual({71 requestFromRemote: ["remote-only"],72 offerToRemote: ["local-only"],73 conflicts: [],74 });75 });7677 it("sends a stable sync error when a peer asks while the vault is locked", async () => {78 const sendSyncData = vi.fn();7980 const cancelled = await cancelSyncIfVaultLocked("conn-1", {81 isUnlocked: () => false,82 sendSyncData,83 });8485 expect(cancelled).toBe(true);86 expect(sendSyncData).toHaveBeenCalledWith("conn-1", expect.any(String));87 expect(decodeSyncMessage(sendSyncData.mock.calls[0]![1])).toEqual({88 type: "SYNC_ERROR",89 code: "vault_locked",90 message: "Vault is locked; unlock Peach before syncing.",91 });92 });9394 it("does not cancel an unlocked vault sync", async () => {95 const sendSyncData = vi.fn();9697 expect(98 await cancelSyncIfVaultLocked("conn-1", {99 isUnlocked: () => true,100 sendSyncData,101 })102 ).toBe(false);103 expect(sendSyncData).not.toHaveBeenCalled();104 });105});