sync-coordinator.test.ts

2.9 KB · raw

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});