1import { assert, describe, it } from "@effect/vitest";
2
3import { mergeLegacyLocalStorage } from "./legacyLocalStorageMerge.ts";
4
5const 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
14const stash = (...ids: string[]) =>
15 JSON.stringify({
16 version: 2,
17 state: { entries: ids.map((id) => ({ id, prompt: `prompt ${id}` })) },
18 });
19
20const 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
30describe("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});