12345678910import { beforeEach, describe, expect, it, vi } from "vitest";11import type { VaultHistory } from "../shared/types";1213type StorageShape = Record<string, unknown>;1415describe("vault history", () => {16 let storage: StorageShape;1718 beforeEach(() => {19 vi.resetModules();20 storage = {};2122 const chromeMock = {23 storage: {24 local: {25 get: vi.fn(async (key?: string | string[]) => {26 if (!key) return { ...storage };27 if (typeof key === "string") return { [key]: storage[key] };28 return key.reduce<Record<string, unknown>>((acc, k) => {29 acc[k] = storage[k];30 return acc;31 }, {});32 }),33 set: vi.fn(async (next: Record<string, unknown>) => {34 Object.assign(storage, next);35 }),36 remove: vi.fn(async (keys: string | string[]) => {37 for (const k of Array.isArray(keys) ? keys : [keys]) delete storage[k];38 }),39 },40 },41 alarms: {42 clear: vi.fn(),43 create: vi.fn(),44 onAlarm: { addListener: vi.fn() },45 },46 idle: {47 setDetectionInterval: vi.fn(),48 onStateChanged: { addListener: vi.fn() },49 },50 } as unknown as typeof chrome;51 (globalThis as { chrome?: typeof chrome }).chrome = chromeMock;5253 vi.stubGlobal("fetch", vi.fn(async () => {54 throw new Error("network disabled in tests");55 }));56 });5758 function readHistory(): VaultHistory {59 const raw = storage.peach_vault_history as VaultHistory | undefined;60 return raw ?? { snapshots: [] };61 }6263 it("first vault write produces no snapshot — nothing to lose yet", async () => {64 const vault = await import("./vault");65 await vault.createVault("history-pass-12345");66 expect(readHistory().snapshots).toHaveLength(0);67 });6869 it("second write snapshots the first state with reason 'local_save'", async () => {70 const vault = await import("./vault");71 await vault.createVault("history-pass-12345");72 await vault.addEntry({ title: "A", username: "", password: "p1", url: "" });7374 const history = readHistory();75 expect(history.snapshots).toHaveLength(1);76 expect(history.snapshots[0]!.reason).toBe("local_save");77 expect(history.snapshots[0]!.id).toBeTruthy();78 expect(history.snapshots[0]!.savedAt).toBeGreaterThan(0);79 });8081 it("snapshots are newest-first", async () => {82 const vault = await import("./vault");83 await vault.createVault("history-pass-67890");84 await vault.addEntry({ title: "First", username: "", password: "p1", url: "" });85 await vault.addEntry({ title: "Second", username: "", password: "p2", url: "" });8687 const snaps = readHistory().snapshots;88 expect(snaps).toHaveLength(2);89 expect(snaps[0]!.savedAt).toBeGreaterThanOrEqual(snaps[1]!.savedAt);90 });9192 it("history is capped at 5 — oldest dropped", async () => {93 const vault = await import("./vault");94 await vault.createVault("cap-pass-abcde");95 for (let i = 0; i < 15; i += 1) {96 await vault.addEntry({ title: `E${i}`, username: "", password: `p${i}`, url: "" });97 }98 expect(readHistory().snapshots).toHaveLength(5);99 });100101 it("entryCount reflects current in-memory active count at snapshot time", async () => {102 const vault = await import("./vault");103 await vault.createVault("count-pass-12345");104 await vault.addEntry({ title: "A", username: "", password: "p1", url: "" });105 106 107 108 await vault.addEntry({ title: "B", username: "", password: "p2", url: "" });109110 const snaps = readHistory().snapshots;111 expect(snaps).toHaveLength(2);112 113 114 expect(snaps[0]!.entryCount).toBe(2);115 expect(snaps[1]!.entryCount).toBe(1);116 });117118 it("dedup: takeVaultSnapshot with no intervening slot write is a no-op the second time", async () => {119 const vault = await import("./vault");120 await vault.createVault("dedup-pass-12345");121 await vault.addEntry({ title: "A", username: "", password: "p1", url: "" });122123 124 125 126 127 await vault.takeVaultSnapshot("local_save");128 const afterFirst = readHistory().snapshots.length;129 await vault.takeVaultSnapshot("local_save");130 expect(readHistory().snapshots.length).toBe(afterFirst);131 });132133 it("getVaultHistory returns the stored snapshots in array form", async () => {134 const vault = await import("./vault");135 await vault.createVault("get-pass-12345");136 await vault.addEntry({ title: "X", username: "", password: "p1", url: "" });137 const list = await vault.getVaultHistory();138 expect(Array.isArray(list)).toBe(true);139 expect(list).toHaveLength(1);140 });141142 it("restoreVaultSnapshot rolls slot A back to a saved blob and locks the vault", async () => {143 const vault = await import("./vault");144 await vault.createVault("restore-pass-12345");145 await vault.addEntry({ title: "first", username: "", password: "p1", url: "" });146 const slotABeforeSecond = JSON.stringify(storage.peach_vault_a);147 await vault.addEntry({ title: "second", username: "", password: "p2", url: "" });148149 const list = await vault.getVaultHistory();150 151 const target = list[0]!;152 expect(target.vault_a).toBe(slotABeforeSecond);153154 expect(vault.isUnlocked()).toBe(true);155 const result = await vault.restoreVaultSnapshot(target.id);156 expect(result).toEqual({ ok: true });157 expect(vault.isUnlocked()).toBe(false);158 expect(JSON.stringify(storage.peach_vault_a)).toBe(slotABeforeSecond);159 });160161 it("restore is itself snapshotted with reason 'restore' so it is reversible", async () => {162 const vault = await import("./vault");163 await vault.createVault("undo-pass-12345");164 await vault.addEntry({ title: "first", username: "", password: "p1", url: "" });165 await vault.addEntry({ title: "second", username: "", password: "p2", url: "" });166167 const list = await vault.getVaultHistory();168 const target = list[0]!;169170 const slotABeforeRestore = JSON.stringify(storage.peach_vault_a);171 await vault.restoreVaultSnapshot(target.id);172173 const after = await vault.getVaultHistory();174 const restoreEntry = after.find((s) => s.reason === "restore");175 expect(restoreEntry).toBeTruthy();176 expect(restoreEntry!.vault_a).toBe(slotABeforeRestore);177 });178179 it("restoreVaultSnapshot returns snapshot_not_found for unknown id", async () => {180 const vault = await import("./vault");181 await vault.createVault("unknown-pass-12345");182 await vault.addEntry({ title: "X", username: "", password: "p1", url: "" });183 const result = await vault.restoreVaultSnapshot("does-not-exist");184 expect(result).toEqual({ ok: false, error: "snapshot_not_found" });185 });186187 it("clearVaultHistory wipes the array", async () => {188 const vault = await import("./vault");189 await vault.createVault("clear-pass-12345");190 await vault.addEntry({ title: "X", username: "", password: "p1", url: "" });191 expect(readHistory().snapshots).toHaveLength(1);192193 await vault.clearVaultHistory();194 expect(readHistory().snapshots).toHaveLength(0);195 });196197 it("import flow uses reason 'import' on its snapshot", async () => {198 const vault = await import("./vault");199 await vault.createVault("import-pass-12345");200 await vault.addEntry({ title: "existing", username: "", password: "p1", url: "" });201202 await vault.restoreEntries(203 [204 {205 id: "imported-1",206 type: "login" as const,207 title: "imported",208 username: "u",209 password: "ip",210 url: "https://imported.test",211 createdAt: 100,212 modifiedAt: 100,213 },214 ],215 "merge",216 );217218 const list = await vault.getVaultHistory();219 expect(list[0]!.reason).toBe("import");220 });221});