@tsctl/cli 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +735 -0
  2. package/bin/tsctl.js +2 -0
  3. package/package.json +65 -0
  4. package/src/__tests__/analyticsrules.test.ts +303 -0
  5. package/src/__tests__/apikeys.test.ts +223 -0
  6. package/src/__tests__/apply.test.ts +245 -0
  7. package/src/__tests__/client.test.ts +48 -0
  8. package/src/__tests__/collection-advanced.test.ts +274 -0
  9. package/src/__tests__/config-loader.test.ts +217 -0
  10. package/src/__tests__/curationsets.test.ts +190 -0
  11. package/src/__tests__/helpers.ts +17 -0
  12. package/src/__tests__/import-drift.test.ts +231 -0
  13. package/src/__tests__/migrate-advanced.test.ts +197 -0
  14. package/src/__tests__/migrate.test.ts +220 -0
  15. package/src/__tests__/plan-new-resources.test.ts +258 -0
  16. package/src/__tests__/plan.test.ts +337 -0
  17. package/src/__tests__/presets.test.ts +97 -0
  18. package/src/__tests__/resources.test.ts +592 -0
  19. package/src/__tests__/setup.ts +77 -0
  20. package/src/__tests__/state.test.ts +312 -0
  21. package/src/__tests__/stemmingdictionaries.test.ts +111 -0
  22. package/src/__tests__/stopwords.test.ts +109 -0
  23. package/src/__tests__/synonymsets.test.ts +170 -0
  24. package/src/__tests__/types.test.ts +416 -0
  25. package/src/apply/index.ts +336 -0
  26. package/src/cli/index.ts +1106 -0
  27. package/src/client/index.ts +55 -0
  28. package/src/config/loader.ts +158 -0
  29. package/src/index.ts +45 -0
  30. package/src/migrate/index.ts +220 -0
  31. package/src/plan/index.ts +1333 -0
  32. package/src/resources/alias.ts +59 -0
  33. package/src/resources/analyticsrule.ts +134 -0
  34. package/src/resources/apikey.ts +203 -0
  35. package/src/resources/collection.ts +424 -0
  36. package/src/resources/curationset.ts +155 -0
  37. package/src/resources/index.ts +11 -0
  38. package/src/resources/override.ts +174 -0
  39. package/src/resources/preset.ts +83 -0
  40. package/src/resources/stemmingdictionary.ts +103 -0
  41. package/src/resources/stopword.ts +100 -0
  42. package/src/resources/synonym.ts +152 -0
  43. package/src/resources/synonymset.ts +144 -0
  44. package/src/state/index.ts +206 -0
  45. package/src/types/index.ts +451 -0
@@ -0,0 +1,312 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
+ import { setupClient, cleanupTypesense } from "./setup.js";
3
+ import {
4
+ ensureStateCollection,
5
+ loadState,
6
+ saveState,
7
+ computeChecksum,
8
+ formatResourceId,
9
+ parseResourceId,
10
+ findResource,
11
+ upsertResource,
12
+ removeResource,
13
+ } from "../state/index.js";
14
+ import type { State, ManagedResource, ResourceIdentifier } from "../types/index.js";
15
+
16
+ describe("state", () => {
17
+ beforeEach(async () => {
18
+ setupClient();
19
+ await cleanupTypesense();
20
+ });
21
+
22
+ afterEach(async () => {
23
+ await cleanupTypesense();
24
+ });
25
+
26
+ describe("ensureStateCollection", () => {
27
+ test("creates state collection if it doesn't exist", async () => {
28
+ await ensureStateCollection();
29
+ const client = (await import("../client/index.js")).getClient();
30
+ const collection = await client.collections("_tsctl_state").retrieve();
31
+ expect(collection.name).toBe("_tsctl_state");
32
+ });
33
+
34
+ test("does not fail if state collection already exists", async () => {
35
+ await ensureStateCollection();
36
+ await ensureStateCollection(); // second call should not throw
37
+ });
38
+ });
39
+
40
+ describe("loadState / saveState", () => {
41
+ test("returns empty state when no state exists", async () => {
42
+ const state = await loadState();
43
+ expect(state.version).toBe("1.0");
44
+ expect(state.resources).toEqual([]);
45
+ });
46
+
47
+ test("saves and loads state", async () => {
48
+ const state: State = {
49
+ version: "1.0",
50
+ resources: [
51
+ {
52
+ identifier: { type: "collection", name: "products" },
53
+ config: { name: "products", fields: [{ name: "title", type: "string" }] },
54
+ checksum: "abc123",
55
+ lastUpdated: new Date().toISOString(),
56
+ },
57
+ ],
58
+ };
59
+
60
+ await saveState(state);
61
+ const loaded = await loadState();
62
+
63
+ expect(loaded.version).toBe("1.0");
64
+ expect(loaded.resources).toHaveLength(1);
65
+ expect(loaded.resources[0]!.identifier.name).toBe("products");
66
+ });
67
+
68
+ test("overwrites previous state on save", async () => {
69
+ const state1: State = {
70
+ version: "1.0",
71
+ resources: [
72
+ {
73
+ identifier: { type: "collection", name: "old" },
74
+ config: { name: "old", fields: [{ name: "x", type: "string" }] },
75
+ checksum: "aaa",
76
+ lastUpdated: new Date().toISOString(),
77
+ },
78
+ ],
79
+ };
80
+
81
+ const state2: State = {
82
+ version: "1.0",
83
+ resources: [
84
+ {
85
+ identifier: { type: "collection", name: "new" },
86
+ config: { name: "new", fields: [{ name: "y", type: "string" }] },
87
+ checksum: "bbb",
88
+ lastUpdated: new Date().toISOString(),
89
+ },
90
+ ],
91
+ };
92
+
93
+ await saveState(state1);
94
+ await saveState(state2);
95
+ const loaded = await loadState();
96
+
97
+ expect(loaded.resources).toHaveLength(1);
98
+ expect(loaded.resources[0]!.identifier.name).toBe("new");
99
+ });
100
+ });
101
+
102
+ describe("computeChecksum", () => {
103
+ test("returns consistent checksum for same config", () => {
104
+ const config = { name: "products", fields: [{ name: "title", type: "string" as const }] };
105
+ const checksum1 = computeChecksum(config);
106
+ const checksum2 = computeChecksum(config);
107
+ expect(checksum1).toBe(checksum2);
108
+ });
109
+
110
+ test("returns different checksum for different configs", () => {
111
+ const config1 = { name: "a", fields: [{ name: "x", type: "string" as const }] };
112
+ const config2 = { name: "b", fields: [{ name: "y", type: "string" as const }] };
113
+ expect(computeChecksum(config1)).not.toBe(computeChecksum(config2));
114
+ });
115
+
116
+ test("returns 16-character hex string", () => {
117
+ const checksum = computeChecksum({ name: "test", collection: "x" });
118
+ expect(checksum).toMatch(/^[a-f0-9]{16}$/);
119
+ });
120
+ });
121
+
122
+ describe("formatResourceId / parseResourceId", () => {
123
+ test("formats collection identifier", () => {
124
+ expect(formatResourceId({ type: "collection", name: "products" })).toBe(
125
+ "collection.products"
126
+ );
127
+ });
128
+
129
+ test("formats scoped identifier", () => {
130
+ expect(
131
+ formatResourceId({ type: "synonym", name: "syn1", collection: "products" })
132
+ ).toBe("synonym.products.syn1");
133
+ });
134
+
135
+ test("parses simple identifier", () => {
136
+ const id = parseResourceId("collection.products");
137
+ expect(id.type).toBe("collection");
138
+ expect(id.name).toBe("products");
139
+ expect(id.collection).toBeUndefined();
140
+ });
141
+
142
+ test("parses scoped identifier", () => {
143
+ const id = parseResourceId("synonym.products.syn1");
144
+ expect(id.type).toBe("synonym");
145
+ expect(id.collection).toBe("products");
146
+ expect(id.name).toBe("syn1");
147
+ });
148
+
149
+ test("roundtrips correctly", () => {
150
+ const identifier: ResourceIdentifier = {
151
+ type: "override",
152
+ name: "ov1",
153
+ collection: "products",
154
+ };
155
+ const formatted = formatResourceId(identifier);
156
+ const parsed = parseResourceId(formatted);
157
+ expect(parsed).toEqual(identifier);
158
+ });
159
+ });
160
+
161
+ describe("findResource", () => {
162
+ test("finds existing resource", () => {
163
+ const state: State = {
164
+ version: "1.0",
165
+ resources: [
166
+ {
167
+ identifier: { type: "collection", name: "products" },
168
+ config: { name: "products", fields: [] },
169
+ checksum: "abc",
170
+ lastUpdated: "",
171
+ },
172
+ ],
173
+ };
174
+ const found = findResource(state, { type: "collection", name: "products" });
175
+ expect(found).toBeDefined();
176
+ expect(found!.identifier.name).toBe("products");
177
+ });
178
+
179
+ test("returns undefined for missing resource", () => {
180
+ const state: State = { version: "1.0", resources: [] };
181
+ const found = findResource(state, { type: "collection", name: "missing" });
182
+ expect(found).toBeUndefined();
183
+ });
184
+
185
+ test("matches by collection scope", () => {
186
+ const state: State = {
187
+ version: "1.0",
188
+ resources: [
189
+ {
190
+ identifier: { type: "synonym", name: "syn1", collection: "products" },
191
+ config: { id: "syn1", collection: "products", synonyms: ["a", "b"] },
192
+ checksum: "abc",
193
+ lastUpdated: "",
194
+ },
195
+ {
196
+ identifier: { type: "synonym", name: "syn1", collection: "users" },
197
+ config: { id: "syn1", collection: "users", synonyms: ["c", "d"] },
198
+ checksum: "def",
199
+ lastUpdated: "",
200
+ },
201
+ ],
202
+ };
203
+
204
+ const found = findResource(state, {
205
+ type: "synonym",
206
+ name: "syn1",
207
+ collection: "products",
208
+ });
209
+ expect(found).toBeDefined();
210
+ expect((found!.config as any).collection).toBe("products");
211
+ });
212
+ });
213
+
214
+ describe("upsertResource", () => {
215
+ test("adds new resource", () => {
216
+ const state: State = { version: "1.0", resources: [] };
217
+ const resource: ManagedResource = {
218
+ identifier: { type: "collection", name: "products" },
219
+ config: { name: "products", fields: [] },
220
+ checksum: "abc",
221
+ lastUpdated: "",
222
+ };
223
+ const newState = upsertResource(state, resource);
224
+ expect(newState.resources).toHaveLength(1);
225
+ });
226
+
227
+ test("updates existing resource", () => {
228
+ const state: State = {
229
+ version: "1.0",
230
+ resources: [
231
+ {
232
+ identifier: { type: "collection", name: "products" },
233
+ config: { name: "products", fields: [] },
234
+ checksum: "old",
235
+ lastUpdated: "",
236
+ },
237
+ ],
238
+ };
239
+ const resource: ManagedResource = {
240
+ identifier: { type: "collection", name: "products" },
241
+ config: { name: "products", fields: [{ name: "title", type: "string" }] },
242
+ checksum: "new",
243
+ lastUpdated: "",
244
+ };
245
+ const newState = upsertResource(state, resource);
246
+ expect(newState.resources).toHaveLength(1);
247
+ expect(newState.resources[0]!.checksum).toBe("new");
248
+ });
249
+
250
+ test("does not mutate original state", () => {
251
+ const state: State = { version: "1.0", resources: [] };
252
+ const resource: ManagedResource = {
253
+ identifier: { type: "collection", name: "products" },
254
+ config: { name: "products", fields: [] },
255
+ checksum: "abc",
256
+ lastUpdated: "",
257
+ };
258
+ upsertResource(state, resource);
259
+ expect(state.resources).toHaveLength(0);
260
+ });
261
+ });
262
+
263
+ describe("removeResource", () => {
264
+ test("removes existing resource", () => {
265
+ const state: State = {
266
+ version: "1.0",
267
+ resources: [
268
+ {
269
+ identifier: { type: "collection", name: "products" },
270
+ config: { name: "products", fields: [] },
271
+ checksum: "abc",
272
+ lastUpdated: "",
273
+ },
274
+ ],
275
+ };
276
+ const newState = removeResource(state, { type: "collection", name: "products" });
277
+ expect(newState.resources).toHaveLength(0);
278
+ });
279
+
280
+ test("does nothing for non-existent resource", () => {
281
+ const state: State = {
282
+ version: "1.0",
283
+ resources: [
284
+ {
285
+ identifier: { type: "collection", name: "products" },
286
+ config: { name: "products", fields: [] },
287
+ checksum: "abc",
288
+ lastUpdated: "",
289
+ },
290
+ ],
291
+ };
292
+ const newState = removeResource(state, { type: "collection", name: "missing" });
293
+ expect(newState.resources).toHaveLength(1);
294
+ });
295
+
296
+ test("does not mutate original state", () => {
297
+ const state: State = {
298
+ version: "1.0",
299
+ resources: [
300
+ {
301
+ identifier: { type: "collection", name: "products" },
302
+ config: { name: "products", fields: [] },
303
+ checksum: "abc",
304
+ lastUpdated: "",
305
+ },
306
+ ],
307
+ };
308
+ removeResource(state, { type: "collection", name: "products" });
309
+ expect(state.resources).toHaveLength(1);
310
+ });
311
+ });
312
+ });
@@ -0,0 +1,111 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
+ import { setupClient, cleanupTypesense } from "./setup.js";
3
+ import { getTypesenseVersion } from "./helpers.js";
4
+ import {
5
+ upsertStemmingDictionary,
6
+ getStemmingDictionary,
7
+ listStemmingDictionaries,
8
+ stemmingDictionaryConfigsEqual,
9
+ } from "../resources/stemmingdictionary.js";
10
+
11
+ describe("stemming dictionaries", () => {
12
+ let version: number;
13
+
14
+ beforeEach(async () => {
15
+ setupClient();
16
+ await cleanupTypesense();
17
+ version = await getTypesenseVersion();
18
+ });
19
+
20
+ afterEach(async () => {
21
+ await cleanupTypesense();
22
+ });
23
+
24
+ test("upsertStemmingDictionary creates a dictionary", async () => {
25
+ if (version < 28) return; // Stemming API requires v28+
26
+ await upsertStemmingDictionary({
27
+ id: "english-plurals",
28
+ words: [
29
+ { word: "dogs", root: "dog" },
30
+ { word: "cats", root: "cat" },
31
+ { word: "mice", root: "mouse" },
32
+ ],
33
+ });
34
+
35
+ const result = await getStemmingDictionary("english-plurals");
36
+ expect(result).not.toBeNull();
37
+ expect(result!.id).toBe("english-plurals");
38
+ expect(result!.words).toHaveLength(3);
39
+ });
40
+
41
+ test("getStemmingDictionary returns null for non-existent", async () => {
42
+ if (version < 28) return;
43
+ const result = await getStemmingDictionary("nonexistent");
44
+ expect(result).toBeNull();
45
+ });
46
+
47
+ test("listStemmingDictionaries returns all dictionaries", async () => {
48
+ if (version < 28) return;
49
+ await upsertStemmingDictionary({
50
+ id: "dict1",
51
+ words: [{ word: "running", root: "run" }],
52
+ });
53
+ await upsertStemmingDictionary({
54
+ id: "dict2",
55
+ words: [{ word: "jumping", root: "jump" }],
56
+ });
57
+
58
+ const dicts = await listStemmingDictionaries();
59
+ const ids = dicts.map((d) => d.id);
60
+ expect(ids).toContain("dict1");
61
+ expect(ids).toContain("dict2");
62
+ });
63
+
64
+ test("upsertStemmingDictionary updates existing dictionary", async () => {
65
+ if (version < 28) return;
66
+ await upsertStemmingDictionary({
67
+ id: "my-dict",
68
+ words: [{ word: "dogs", root: "dog" }],
69
+ });
70
+ await upsertStemmingDictionary({
71
+ id: "my-dict",
72
+ words: [
73
+ { word: "dogs", root: "dog" },
74
+ { word: "cats", root: "cat" },
75
+ ],
76
+ });
77
+
78
+ const result = await getStemmingDictionary("my-dict");
79
+ expect(result!.words).toHaveLength(2);
80
+ });
81
+
82
+ test("stemmingDictionaryConfigsEqual compares correctly", () => {
83
+ const a = {
84
+ id: "test",
85
+ words: [
86
+ { word: "dogs", root: "dog" },
87
+ { word: "cats", root: "cat" },
88
+ ],
89
+ };
90
+ const b = {
91
+ id: "test",
92
+ words: [
93
+ { word: "cats", root: "cat" },
94
+ { word: "dogs", root: "dog" },
95
+ ],
96
+ };
97
+ expect(stemmingDictionaryConfigsEqual(a, b)).toBe(true);
98
+ });
99
+
100
+ test("stemmingDictionaryConfigsEqual detects differences", () => {
101
+ const a = {
102
+ id: "test",
103
+ words: [{ word: "dogs", root: "dog" }],
104
+ };
105
+ const b = {
106
+ id: "test",
107
+ words: [{ word: "cats", root: "cat" }],
108
+ };
109
+ expect(stemmingDictionaryConfigsEqual(a, b)).toBe(false);
110
+ });
111
+ });
@@ -0,0 +1,109 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
+ import { setupClient, cleanupTypesense } from "./setup.js";
3
+ import {
4
+ upsertStopwordSet,
5
+ getStopwordSet,
6
+ listStopwordSets,
7
+ deleteStopwordSet,
8
+ stopwordSetConfigsEqual,
9
+ } from "../resources/stopword.js";
10
+
11
+ describe("stopwords", () => {
12
+ beforeEach(async () => {
13
+ setupClient();
14
+ await cleanupTypesense();
15
+ });
16
+
17
+ afterEach(async () => {
18
+ await cleanupTypesense();
19
+ });
20
+
21
+ test("upsertStopwordSet creates a stopword set", async () => {
22
+ await upsertStopwordSet({
23
+ id: "english-stopwords",
24
+ stopwords: ["the", "a", "an", "is", "are"],
25
+ });
26
+
27
+ const result = await getStopwordSet("english-stopwords");
28
+ expect(result).not.toBeNull();
29
+ expect(result!.id).toBe("english-stopwords");
30
+ expect(result!.stopwords).toContain("the");
31
+ expect(result!.stopwords).toContain("a");
32
+ });
33
+
34
+ test("upsertStopwordSet creates with locale", async () => {
35
+ await upsertStopwordSet({
36
+ id: "german-stopwords",
37
+ stopwords: ["der", "die", "das"],
38
+ locale: "de",
39
+ });
40
+
41
+ const result = await getStopwordSet("german-stopwords");
42
+ expect(result).not.toBeNull();
43
+ expect(result!.locale).toBe("de");
44
+ });
45
+
46
+ test("getStopwordSet returns null for non-existent", async () => {
47
+ const result = await getStopwordSet("nonexistent");
48
+ expect(result).toBeNull();
49
+ });
50
+
51
+ test("listStopwordSets returns all stopword sets", async () => {
52
+ await upsertStopwordSet({
53
+ id: "english",
54
+ stopwords: ["the", "a"],
55
+ });
56
+ await upsertStopwordSet({
57
+ id: "german",
58
+ stopwords: ["der", "die"],
59
+ });
60
+
61
+ const sets = await listStopwordSets();
62
+ expect(sets.length).toBeGreaterThanOrEqual(2);
63
+ const ids = sets.map((s) => s.id);
64
+ expect(ids).toContain("english");
65
+ expect(ids).toContain("german");
66
+ });
67
+
68
+ test("upsertStopwordSet updates existing set", async () => {
69
+ await upsertStopwordSet({
70
+ id: "english",
71
+ stopwords: ["the", "a"],
72
+ });
73
+ await upsertStopwordSet({
74
+ id: "english",
75
+ stopwords: ["the", "a", "an"],
76
+ });
77
+
78
+ const result = await getStopwordSet("english");
79
+ expect(result!.stopwords).toHaveLength(3);
80
+ });
81
+
82
+ test("deleteStopwordSet removes set", async () => {
83
+ await upsertStopwordSet({
84
+ id: "to-delete",
85
+ stopwords: ["the"],
86
+ });
87
+ await deleteStopwordSet("to-delete");
88
+ const result = await getStopwordSet("to-delete");
89
+ expect(result).toBeNull();
90
+ });
91
+
92
+ test("stopwordSetConfigsEqual compares correctly", () => {
93
+ const a = { id: "test", stopwords: ["a", "b", "c"] };
94
+ const b = { id: "test", stopwords: ["c", "b", "a"] };
95
+ expect(stopwordSetConfigsEqual(a, b)).toBe(true);
96
+ });
97
+
98
+ test("stopwordSetConfigsEqual detects differences", () => {
99
+ const a = { id: "test", stopwords: ["a", "b"] };
100
+ const b = { id: "test", stopwords: ["a", "c"] };
101
+ expect(stopwordSetConfigsEqual(a, b)).toBe(false);
102
+ });
103
+
104
+ test("stopwordSetConfigsEqual considers locale", () => {
105
+ const a = { id: "test", stopwords: ["a"], locale: "en" };
106
+ const b = { id: "test", stopwords: ["a"], locale: "de" };
107
+ expect(stopwordSetConfigsEqual(a, b)).toBe(false);
108
+ });
109
+ });
@@ -0,0 +1,170 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
+ import { setupClient, cleanupTypesense } from "./setup.js";
3
+ import { getTypesenseVersion } from "./helpers.js";
4
+ import {
5
+ createSynonymSet,
6
+ getSynonymSet,
7
+ listSynonymSets,
8
+ updateSynonymSet,
9
+ deleteSynonymSet,
10
+ synonymSetConfigsEqual,
11
+ } from "../resources/synonymset.js";
12
+ import type { SynonymSetConfig } from "../types/index.js";
13
+
14
+ describe("synonym sets (global)", () => {
15
+ let version: number;
16
+
17
+ beforeEach(async () => {
18
+ setupClient();
19
+ await cleanupTypesense();
20
+ version = await getTypesenseVersion();
21
+ });
22
+
23
+ afterEach(async () => {
24
+ await cleanupTypesense();
25
+ });
26
+
27
+ test("createSynonymSet creates a set", async () => {
28
+ if (version < 28) return; // Synonym sets require v28+
29
+ const config: SynonymSetConfig = {
30
+ name: "clothing-synonyms",
31
+ items: [
32
+ { id: "pants", synonyms: ["pants", "trousers", "slacks"] },
33
+ { id: "shirt", synonyms: ["shirt", "top", "blouse"] },
34
+ ],
35
+ };
36
+
37
+ await createSynonymSet(config);
38
+ const result = await getSynonymSet("clothing-synonyms");
39
+ expect(result).not.toBeNull();
40
+ expect(result!.name).toBe("clothing-synonyms");
41
+ expect(result!.items).toHaveLength(2);
42
+ });
43
+
44
+ test("getSynonymSet returns null for non-existent", async () => {
45
+ if (version < 28) return;
46
+ const result = await getSynonymSet("nonexistent");
47
+ expect(result).toBeNull();
48
+ });
49
+
50
+ test("createSynonymSet with one-way synonyms", async () => {
51
+ if (version < 28) return;
52
+ const config: SynonymSetConfig = {
53
+ name: "tv-synonyms",
54
+ items: [
55
+ { id: "tv", root: "television", synonyms: ["tv", "telly"] },
56
+ ],
57
+ };
58
+
59
+ await createSynonymSet(config);
60
+ const result = await getSynonymSet("tv-synonyms");
61
+ expect(result).not.toBeNull();
62
+ expect(result!.items[0]!.root).toBe("television");
63
+ });
64
+
65
+ test("listSynonymSets returns all sets", async () => {
66
+ if (version < 28) return;
67
+ await createSynonymSet({
68
+ name: "set1",
69
+ items: [{ id: "a", synonyms: ["x", "y"] }],
70
+ });
71
+ await createSynonymSet({
72
+ name: "set2",
73
+ items: [{ id: "b", synonyms: ["m", "n"] }],
74
+ });
75
+
76
+ const sets = await listSynonymSets();
77
+ const names = sets.map((s) => s.name);
78
+ expect(names).toContain("set1");
79
+ expect(names).toContain("set2");
80
+ });
81
+
82
+ test("updateSynonymSet modifies items", async () => {
83
+ if (version < 28) return;
84
+ const initial: SynonymSetConfig = {
85
+ name: "my-synonyms",
86
+ items: [{ id: "a", synonyms: ["x", "y"] }],
87
+ };
88
+ await createSynonymSet(initial);
89
+
90
+ const updated: SynonymSetConfig = {
91
+ name: "my-synonyms",
92
+ items: [
93
+ { id: "a", synonyms: ["x", "y"] },
94
+ { id: "b", synonyms: ["m", "n"] },
95
+ ],
96
+ };
97
+ await updateSynonymSet(updated, initial);
98
+
99
+ const result = await getSynonymSet("my-synonyms");
100
+ expect(result!.items).toHaveLength(2);
101
+ });
102
+
103
+ test("deleteSynonymSet removes set", async () => {
104
+ if (version < 28) return;
105
+ await createSynonymSet({
106
+ name: "to-delete",
107
+ items: [{ id: "a", synonyms: ["x", "y"] }],
108
+ });
109
+ await deleteSynonymSet("to-delete");
110
+ const result = await getSynonymSet("to-delete");
111
+ expect(result).toBeNull();
112
+ });
113
+
114
+ test("synonymSetConfigsEqual compares correctly", () => {
115
+ const a: SynonymSetConfig = {
116
+ name: "test",
117
+ items: [
118
+ { id: "a", synonyms: ["x", "y"] },
119
+ { id: "b", synonyms: ["m", "n"] },
120
+ ],
121
+ };
122
+ const b: SynonymSetConfig = {
123
+ name: "test",
124
+ items: [
125
+ { id: "b", synonyms: ["m", "n"] },
126
+ { id: "a", synonyms: ["x", "y"] },
127
+ ],
128
+ };
129
+ expect(synonymSetConfigsEqual(a, b)).toBe(true);
130
+ });
131
+
132
+ test("synonymSetConfigsEqual detects differences", () => {
133
+ const a: SynonymSetConfig = {
134
+ name: "test",
135
+ items: [{ id: "a", synonyms: ["x", "y"] }],
136
+ };
137
+ const b: SynonymSetConfig = {
138
+ name: "test",
139
+ items: [{ id: "a", synonyms: ["x", "z"] }],
140
+ };
141
+ expect(synonymSetConfigsEqual(a, b)).toBe(false);
142
+ });
143
+
144
+ test("synonymSetConfigsEqual detects name difference", () => {
145
+ const a: SynonymSetConfig = {
146
+ name: "test1",
147
+ items: [{ id: "a", synonyms: ["x"] }],
148
+ };
149
+ const b: SynonymSetConfig = {
150
+ name: "test2",
151
+ items: [{ id: "a", synonyms: ["x"] }],
152
+ };
153
+ expect(synonymSetConfigsEqual(a, b)).toBe(false);
154
+ });
155
+
156
+ test("synonymSetConfigsEqual detects length difference", () => {
157
+ const a: SynonymSetConfig = {
158
+ name: "test",
159
+ items: [{ id: "a", synonyms: ["x"] }],
160
+ };
161
+ const b: SynonymSetConfig = {
162
+ name: "test",
163
+ items: [
164
+ { id: "a", synonyms: ["x"] },
165
+ { id: "b", synonyms: ["y"] },
166
+ ],
167
+ };
168
+ expect(synonymSetConfigsEqual(a, b)).toBe(false);
169
+ });
170
+ });