fix: harden favorites storage recovery

This commit is contained in:
wxs
2026-07-17 12:04:56 +08:00
parent a7d62c7ec0
commit ac8e86ab7b
2 changed files with 73 additions and 5 deletions
+8
View File
@@ -245,6 +245,14 @@ function getChromeLocalStorage(): ChromeStorageLocalArea {
function normalizeState(value: unknown, now: () => string): FavoritesStateV1 { function normalizeState(value: unknown, now: () => string): FavoritesStateV1 {
const source = isRecord(value) ? value : {}; const source = isRecord(value) ? value : {};
if (source.version !== 1) {
return {
creators: [],
folders: [],
memberships: [],
version: 1
};
}
const folders = normalizeFolders(source.folders, now); const folders = normalizeFolders(source.folders, now);
const creators = normalizeCreators(source.creators, now); const creators = normalizeCreators(source.creators, now);
const knownFolderIds = new Set(folders.map((folder) => folder.id)); const knownFolderIds = new Set(folders.map((folder) => folder.id));
+64 -4
View File
@@ -154,7 +154,7 @@ describe("favorites-store", () => {
test("recovers from malformed, duplicate, and dangling stored values", async () => { test("recovers from malformed, duplicate, and dangling stored values", async () => {
const { repository } = createRepository( const { repository } = createRepository(
createMemoryStorage({ createMemoryStorage({
version: 42, version: 1,
folders: [ folders: [
{ {
id: "folder-1", 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 () => { test("serializes concurrent mutations so neither folder is overwritten", async () => {
const { repository } = createRepository(); 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 = { const storage: FavoritesStorage = {
async get() { async get() {
return undefined; return storedState;
}, },
async set() { async set(value) {
writeAttempts += 1;
if (writeAttempts === 1) {
throw new Error("write failed"); throw new Error("write failed");
} }
storedState = value;
}
}; };
const { repository } = createRepository(storage); const { repository } = createRepository(storage);
await expect(repository.createFolder("合作")).rejects.toThrow("write failed"); 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 () => { test("uses the exact Chrome storage key and reports unavailable storage", async () => {