| 1 | import { assert, describe, it } from "@effect/vitest"; |
| 2 | |
| 3 | import { mergeLegacyLocalStorage } from "./legacyLocalStorageMerge.ts"; |
| 4 | |
| 5 | const memoryStorage = (initial: Record<string, string> = {}) => { |
| 6 | const items = new Map(Object.entries(initial)); |
| 7 | return { |
| 8 | items, |
| 9 | getItem: (key: string) => items.get(key) ?? null, |
| 10 | setItem: (key: string, value: string) => void items.set(key, value), |
| 11 | }; |
| 12 | }; |
| 13 | |
| 14 | const stash = (...ids: string[]) => |
| 15 | JSON.stringify({ |
| 16 | version: 2, |
| 17 | state: { entries: ids.map((id) => ({ id, prompt: `prompt ${id}` })) }, |
| 18 | }); |
| 19 | |
| 20 | const drafts = (version: number, threadKey: string, prompt: string) => |
| 21 | JSON.stringify({ |
| 22 | version, |
| 23 | state: { |
| 24 | draftsByThreadKey: { [threadKey]: { prompt } }, |
| 25 | draftThreadsByThreadKey: {}, |
| 26 | logicalProjectDraftThreadKeyByLogicalProjectKey: {}, |
| 27 | }, |
| 28 | }); |
| 29 | |
| 30 | describe("mergeLegacyLocalStorage", () => { |
| 31 | it("copies V1 keys into an empty profile, except the per-install client id", () => { |
| 32 | const storage = memoryStorage(); |
| 33 | mergeLegacyLocalStorage(storage, { |
| 34 | "t3code:theme": "dark", |
| 35 | "t3code:prompt-stash:v2": stash("a"), |
| 36 | "t3.backgroundActivity.clientId": "v1-client", |
| 37 | }); |
| 38 | assert.equal(storage.getItem("t3code:theme"), "dark"); |
| 39 | assert.equal(storage.getItem("t3code:prompt-stash:v2"), stash("a")); |
| 40 | assert.equal(storage.getItem("t3.backgroundActivity.clientId"), null); |
| 41 | }); |
| 42 | |
| 43 | it("keeps V2 values for plain keys", () => { |
| 44 | const storage = memoryStorage({ "t3code:theme": "light" }); |
| 45 | mergeLegacyLocalStorage(storage, { "t3code:theme": "dark" }); |
| 46 | assert.equal(storage.getItem("t3code:theme"), "light"); |
| 47 | }); |
| 48 | |
| 49 | it("appends V1 stash entries after V2's without duplicating ids", () => { |
| 50 | const storage = memoryStorage({ "t3code:prompt-stash:v2": stash("new", "shared") }); |
| 51 | mergeLegacyLocalStorage(storage, { "t3code:prompt-stash:v2": stash("shared", "old") }); |
| 52 | const ids = JSON.parse(storage.getItem("t3code:prompt-stash:v2")!).state.entries.map( |
| 53 | (entry: { id: string }) => entry.id, |
| 54 | ); |
| 55 | assert.deepStrictEqual(ids, ["new", "shared", "old"]); |
| 56 | }); |
| 57 | |
| 58 | it("adds V1 drafts for other threads and keeps V2's draft for the same thread", () => { |
| 59 | const storage = memoryStorage({ |
| 60 | "t3code:composer-drafts:v1": drafts(9, "env:shared", "v2 text"), |
| 61 | }); |
| 62 | const legacy = JSON.parse(drafts(9, "env:shared", "v1 text")); |
| 63 | legacy.state.draftsByThreadKey["env:old"] = { prompt: "v1 only" }; |
| 64 | mergeLegacyLocalStorage(storage, { "t3code:composer-drafts:v1": JSON.stringify(legacy) }); |
| 65 | const merged = JSON.parse(storage.getItem("t3code:composer-drafts:v1")!); |
| 66 | assert.equal(merged.state.draftsByThreadKey["env:shared"].prompt, "v2 text"); |
| 67 | assert.equal(merged.state.draftsByThreadKey["env:old"].prompt, "v1 only"); |
| 68 | }); |
| 69 | |
| 70 | it("leaves V2 drafts alone when the stored shapes differ in version", () => { |
| 71 | const current = drafts(10, "env:a", "v2"); |
| 72 | const storage = memoryStorage({ "t3code:composer-drafts:v1": current }); |
| 73 | mergeLegacyLocalStorage(storage, { "t3code:composer-drafts:v1": drafts(9, "env:b", "v1") }); |
| 74 | assert.equal(storage.getItem("t3code:composer-drafts:v1"), current); |
| 75 | }); |
| 76 | |
| 77 | it("caps the merged stash at the store's entry limit, dropping the oldest V1 entries", () => { |
| 78 | const v2Ids = Array.from({ length: 15 }, (_, index) => `v2-${index}`); |
| 79 | const v1Ids = Array.from({ length: 10 }, (_, index) => `v1-${index}`); |
| 80 | const storage = memoryStorage({ "t3code:prompt-stash:v2": stash(...v2Ids) }); |
| 81 | mergeLegacyLocalStorage(storage, { "t3code:prompt-stash:v2": stash(...v1Ids) }); |
| 82 | const ids = JSON.parse(storage.getItem("t3code:prompt-stash:v2")!).state.entries.map( |
| 83 | (entry: { id: string }) => entry.id, |
| 84 | ); |
| 85 | assert.deepStrictEqual(ids, [...v2Ids, ...v1Ids.slice(0, 5)]); |
| 86 | }); |
| 87 | |
| 88 | it("keeps importing other keys after one write fails, and reports the failure", () => { |
| 89 | const storage = memoryStorage(); |
| 90 | const setItem = storage.setItem; |
| 91 | storage.setItem = (key, value) => { |
| 92 | if (key === "t3code:theme") throw new DOMException("full", "QuotaExceededError"); |
| 93 | setItem(key, value); |
| 94 | }; |
| 95 | const complete = mergeLegacyLocalStorage(storage, { |
| 96 | "t3code:theme": "dark", |
| 97 | "t3code:last-editor": "zed", |
| 98 | }); |
| 99 | assert.equal(storage.getItem("t3code:last-editor"), "zed"); |
| 100 | assert.isFalse(complete); |
| 101 | }); |
| 102 | }); |