123456import { beforeEach, describe, expect, it } from "vitest";7import {8 MAX_CLOCK_SKEW_MS,9 clampSyncTimestamp,10} from "../shared/sync-limits";11import {12 applyMergedRemoteEntries,13 addEntry,14 createVault,15 getEntries,16 getEntryById,17} from "./vault";18import { getPendingSyncAlerts, clearPendingSyncAlerts } from "./sync-alerts";1920describe("clampSyncTimestamp", () => {21 it("clamps a far-future timestamp to now + MAX_CLOCK_SKEW_MS", () => {22 const now = 1_750_000_000_000;23 const farFuture = now + 10_000_000;24 expect(clampSyncTimestamp(farFuture, now)).toBe(now + MAX_CLOCK_SKEW_MS);25 });2627 it("passes through timestamps within the skew window", () => {28 const now = 1_750_000_000_000;29 expect(clampSyncTimestamp(now - 1000, now)).toBe(now - 1000);30 expect(clampSyncTimestamp(now + MAX_CLOCK_SKEW_MS - 1, now)).toBe(31 now + MAX_CLOCK_SKEW_MS - 1,32 );33 });34});3536describe("applyMergedRemoteEntries — HV-1", () => {37 beforeEach(async () => {38 39 await createVault("correct horse battery staple");40 await clearPendingSyncAlerts();41 });4243 it("clamps a far-future remote modifiedAt so a local edit can win", async () => {44 const local = await addEntry({45 title: "Bank",46 username: "june",47 password: "strong-local-password",48 url: "https://bank.example.com",49 type: "login",50 });5152 const farFuture = Date.now() + 10_000_000;53 await applyMergedRemoteEntries(54 [{ ...local, password: "attacker-password", modifiedAt: farFuture }],55 {},56 );5758 const merged = getEntryById(local.id)!;59 60 61 expect(merged.modifiedAt).toBeLessThanOrEqual(Date.now() + MAX_CLOCK_SKEW_MS);6263 64 const { updateEntry } = await import("./vault");65 await updateEntry(local.id, { password: "honest-newer-password" });66 67 await applyMergedRemoteEntries(68 [{ ...merged, password: "attacker-password", modifiedAt: farFuture }],69 {},70 );71 expect(getEntryById(local.id)!.password).toBe("honest-newer-password");72 });7374 it("clamps a far-future tombstone ts before applying", async () => {75 const local = await addEntry({76 title: "Forum",77 username: "june",78 password: "pw",79 url: "https://forum.example.com",80 type: "login",81 });82 const farFuture = Date.now() + 10_000_000;83 await applyMergedRemoteEntries([], { [local.id]: farFuture });8485 const deleted = getEntries().length === 086 ? undefined87 : undefined; 88 void deleted;89 const { getDeletedEntries } = await import("./vault");90 const tombstoned = getDeletedEntries().find((e) => e.id === local.id)!;91 expect(tombstoned.deletedAt).toBeLessThanOrEqual(92 Date.now() + MAX_CLOCK_SKEW_MS,93 );94 });9596 it("records a REMOTE_PASSWORD_CHANGE alert when a remote replaces a local password", async () => {97 const local = await addEntry({98 title: "Email",99 username: "june",100 password: "old-password",101 url: "https://mail.example.com",102 type: "login",103 });104 await applyMergedRemoteEntries(105 [{ ...local, password: "replaced-by-peer", modifiedAt: Date.now() + 1000 }],106 {},107 { deviceName: "Pixel 9" },108 );109 const alerts = await getPendingSyncAlerts();110 expect(alerts).toHaveLength(1);111 expect(alerts[0]).toMatchObject({112 type: "REMOTE_PASSWORD_CHANGE",113 entryId: local.id,114 entryTitle: "Email",115 deviceName: "Pixel 9",116 });117 });118119 it("records a REMOTE_DELETE alert when a remote tombstone deletes a live entry", async () => {120 const local = await addEntry({121 title: "Shop",122 username: "june",123 password: "pw",124 url: "https://shop.example.com",125 type: "login",126 });127 await applyMergedRemoteEntries([], { [local.id]: Date.now() + 1000 }, {128 deviceName: "Pixel 9",129 });130 const alerts = await getPendingSyncAlerts();131 expect(alerts).toHaveLength(1);132 expect(alerts[0]).toMatchObject({133 type: "REMOTE_DELETE",134 entryId: local.id,135 entryTitle: "Shop",136 deviceName: "Pixel 9",137 });138 });139});