From ac8e86ab7b09fe5efb2d247a6e279c0aeb8d2eaa Mon Sep 17 00:00:00 2001 From: wxs Date: Fri, 17 Jul 2026 12:04:56 +0800 Subject: [PATCH] fix: harden favorites storage recovery --- src/content/market/favorites-store.ts | 8 +++ tests/favorites-store.test.ts | 70 +++++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/src/content/market/favorites-store.ts b/src/content/market/favorites-store.ts index 009f3ba..446da2f 100644 --- a/src/content/market/favorites-store.ts +++ b/src/content/market/favorites-store.ts @@ -245,6 +245,14 @@ function getChromeLocalStorage(): ChromeStorageLocalArea { function normalizeState(value: unknown, now: () => string): FavoritesStateV1 { const source = isRecord(value) ? value : {}; + if (source.version !== 1) { + return { + creators: [], + folders: [], + memberships: [], + version: 1 + }; + } const folders = normalizeFolders(source.folders, now); const creators = normalizeCreators(source.creators, now); const knownFolderIds = new Set(folders.map((folder) => folder.id)); diff --git a/tests/favorites-store.test.ts b/tests/favorites-store.test.ts index d61a512..b364a57 100644 --- a/tests/favorites-store.test.ts +++ b/tests/favorites-store.test.ts @@ -154,7 +154,7 @@ describe("favorites-store", () => { test("recovers from malformed, duplicate, and dangling stored values", async () => { const { repository } = createRepository( createMemoryStorage({ - version: 42, + version: 1, folders: [ { id: "folder-1", @@ -214,6 +214,44 @@ describe("favorites-store", () => { }); }); + test("treats a future stored version as an empty V1 state", async () => { + const { repository } = createRepository( + createMemoryStorage({ + version: 2, + folders: [ + { + id: "future-folder", + name: "未来收藏夹", + createdAt: "future", + updatedAt: "future" + } + ], + creators: [ + { + authorId: "future-author", + authorName: "Future", + savedAt: "future" + } + ], + memberships: [ + { + folderId: "future-folder", + authorId: "future-author", + addedAt: "future" + } + ], + futureOnlyField: "do not retain" + }) + ); + + await expect(repository.read()).resolves.toEqual({ + creators: [], + folders: [], + memberships: [], + version: 1 + }); + }); + test("serializes concurrent mutations so neither folder is overwritten", async () => { const { repository } = createRepository(); @@ -230,18 +268,40 @@ describe("favorites-store", () => { }); }); - test("propagates rejected storage writes", async () => { + test("recovers the mutation queue after one rejected storage write", async () => { + let writeAttempts = 0; + let storedState: FavoritesStateV1 | undefined; const storage: FavoritesStorage = { async get() { - return undefined; + return storedState; }, - async set() { - throw new Error("write failed"); + async set(value) { + writeAttempts += 1; + if (writeAttempts === 1) { + throw new Error("write failed"); + } + storedState = value; } }; const { repository } = createRepository(storage); await expect(repository.createFolder("合作")).rejects.toThrow("write failed"); + await expect(repository.createFolder("待联系")).resolves.toMatchObject({ + name: "待联系" + }); + await expect(repository.read()).resolves.toEqual({ + creators: [], + folders: [ + { + id: "folder-2", + name: "待联系", + createdAt: "2026-07-17T00:00:00.000Z", + updatedAt: "2026-07-17T00:00:00.000Z" + } + ], + memberships: [], + version: 1 + }); }); test("uses the exact Chrome storage key and reports unavailable storage", async () => {