@better-i18n/mcp 0.15.3 → 0.15.5

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 (99) hide show
  1. package/dist/__tests__/base-tool.test.d.ts +2 -0
  2. package/dist/__tests__/base-tool.test.d.ts.map +1 -0
  3. package/dist/__tests__/base-tool.test.js +193 -0
  4. package/dist/__tests__/base-tool.test.js.map +1 -0
  5. package/dist/__tests__/contract.test.d.ts +24 -0
  6. package/dist/__tests__/contract.test.d.ts.map +1 -0
  7. package/dist/__tests__/contract.test.js +919 -0
  8. package/dist/__tests__/contract.test.js.map +1 -0
  9. package/dist/__tests__/fixtures/mock-client.d.ts +11 -0
  10. package/dist/__tests__/fixtures/mock-client.d.ts.map +1 -0
  11. package/dist/__tests__/fixtures/mock-client.js +33 -0
  12. package/dist/__tests__/fixtures/mock-client.js.map +1 -0
  13. package/dist/__tests__/helpers.d.ts +10 -0
  14. package/dist/__tests__/helpers.d.ts.map +1 -0
  15. package/dist/__tests__/helpers.js +27 -0
  16. package/dist/__tests__/helpers.js.map +1 -0
  17. package/dist/__tests__/schema-alignment.test.d.ts +15 -0
  18. package/dist/__tests__/schema-alignment.test.d.ts.map +1 -0
  19. package/dist/__tests__/schema-alignment.test.js +1011 -0
  20. package/dist/__tests__/schema-alignment.test.js.map +1 -0
  21. package/dist/__tests__/server-dispatch.test.d.ts +10 -0
  22. package/dist/__tests__/server-dispatch.test.d.ts.map +1 -0
  23. package/dist/__tests__/server-dispatch.test.js +202 -0
  24. package/dist/__tests__/server-dispatch.test.js.map +1 -0
  25. package/dist/tools/__tests__/createKeys.test.d.ts +2 -0
  26. package/dist/tools/__tests__/createKeys.test.d.ts.map +1 -0
  27. package/dist/tools/__tests__/createKeys.test.js +139 -0
  28. package/dist/tools/__tests__/createKeys.test.js.map +1 -0
  29. package/dist/tools/__tests__/deleteKeys.test.d.ts +2 -0
  30. package/dist/tools/__tests__/deleteKeys.test.d.ts.map +1 -0
  31. package/dist/tools/__tests__/deleteKeys.test.js +91 -0
  32. package/dist/tools/__tests__/deleteKeys.test.js.map +1 -0
  33. package/dist/tools/__tests__/getPendingChanges.test.d.ts +2 -0
  34. package/dist/tools/__tests__/getPendingChanges.test.d.ts.map +1 -0
  35. package/dist/tools/__tests__/getPendingChanges.test.js +50 -0
  36. package/dist/tools/__tests__/getPendingChanges.test.js.map +1 -0
  37. package/dist/tools/__tests__/getProject.test.d.ts +2 -0
  38. package/dist/tools/__tests__/getProject.test.d.ts.map +1 -0
  39. package/dist/tools/__tests__/getProject.test.js +55 -0
  40. package/dist/tools/__tests__/getProject.test.js.map +1 -0
  41. package/dist/tools/__tests__/getSync.test.d.ts +2 -0
  42. package/dist/tools/__tests__/getSync.test.d.ts.map +1 -0
  43. package/dist/tools/__tests__/getSync.test.js +42 -0
  44. package/dist/tools/__tests__/getSync.test.js.map +1 -0
  45. package/dist/tools/__tests__/getSyncs.test.d.ts +2 -0
  46. package/dist/tools/__tests__/getSyncs.test.d.ts.map +1 -0
  47. package/dist/tools/__tests__/getSyncs.test.js +66 -0
  48. package/dist/tools/__tests__/getSyncs.test.js.map +1 -0
  49. package/dist/tools/__tests__/getTranslations.test.d.ts +2 -0
  50. package/dist/tools/__tests__/getTranslations.test.d.ts.map +1 -0
  51. package/dist/tools/__tests__/getTranslations.test.js +114 -0
  52. package/dist/tools/__tests__/getTranslations.test.js.map +1 -0
  53. package/dist/tools/__tests__/listKeys.test.d.ts +2 -0
  54. package/dist/tools/__tests__/listKeys.test.d.ts.map +1 -0
  55. package/dist/tools/__tests__/listKeys.test.js +98 -0
  56. package/dist/tools/__tests__/listKeys.test.js.map +1 -0
  57. package/dist/tools/__tests__/listProjects.test.d.ts +2 -0
  58. package/dist/tools/__tests__/listProjects.test.d.ts.map +1 -0
  59. package/dist/tools/__tests__/listProjects.test.js +45 -0
  60. package/dist/tools/__tests__/listProjects.test.js.map +1 -0
  61. package/dist/tools/__tests__/proposeLanguageEdits.test.d.ts +2 -0
  62. package/dist/tools/__tests__/proposeLanguageEdits.test.d.ts.map +1 -0
  63. package/dist/tools/__tests__/proposeLanguageEdits.test.js +87 -0
  64. package/dist/tools/__tests__/proposeLanguageEdits.test.js.map +1 -0
  65. package/dist/tools/__tests__/proposeLanguages.test.d.ts +2 -0
  66. package/dist/tools/__tests__/proposeLanguages.test.d.ts.map +1 -0
  67. package/dist/tools/__tests__/proposeLanguages.test.js +109 -0
  68. package/dist/tools/__tests__/proposeLanguages.test.js.map +1 -0
  69. package/dist/tools/__tests__/publishTranslations.test.d.ts +2 -0
  70. package/dist/tools/__tests__/publishTranslations.test.d.ts.map +1 -0
  71. package/dist/tools/__tests__/publishTranslations.test.js +127 -0
  72. package/dist/tools/__tests__/publishTranslations.test.js.map +1 -0
  73. package/dist/tools/__tests__/updateKeys.test.d.ts +2 -0
  74. package/dist/tools/__tests__/updateKeys.test.d.ts.map +1 -0
  75. package/dist/tools/__tests__/updateKeys.test.js +122 -0
  76. package/dist/tools/__tests__/updateKeys.test.js.map +1 -0
  77. package/dist/tools/createKeys.d.ts.map +1 -1
  78. package/dist/tools/createKeys.js +19 -7
  79. package/dist/tools/createKeys.js.map +1 -1
  80. package/dist/tools/deleteKeys.d.ts.map +1 -1
  81. package/dist/tools/deleteKeys.js +9 -1
  82. package/dist/tools/deleteKeys.js.map +1 -1
  83. package/dist/tools/getProject.d.ts.map +1 -1
  84. package/dist/tools/getProject.js +2 -1
  85. package/dist/tools/getProject.js.map +1 -1
  86. package/dist/tools/getTranslations.d.ts +1 -1
  87. package/dist/tools/getTranslations.js +4 -4
  88. package/dist/tools/getTranslations.js.map +1 -1
  89. package/dist/tools/publishTranslations.d.ts.map +1 -1
  90. package/dist/tools/publishTranslations.js +12 -3
  91. package/dist/tools/publishTranslations.js.map +1 -1
  92. package/dist/tools/updateKeys.d.ts.map +1 -1
  93. package/dist/tools/updateKeys.js +8 -3
  94. package/dist/tools/updateKeys.js.map +1 -1
  95. package/dist/worker.test.d.ts +16 -0
  96. package/dist/worker.test.d.ts.map +1 -0
  97. package/dist/worker.test.js +244 -0
  98. package/dist/worker.test.js.map +1 -0
  99. package/package.json +2 -2
@@ -0,0 +1,1011 @@
1
+ /**
2
+ * schema-alignment.test.ts
3
+ *
4
+ * Verifies that the three schema layers stay in sync:
5
+ * 1. Tool Zod schema — runtime validation in each tool file
6
+ * 2. mcp-types Zod schema — the API contract (@better-i18n/mcp-types/schemas)
7
+ * 3. inputSchema JSON — what MCP clients (LLM agents) see
8
+ *
9
+ * Also verifies that CompactXxx response types have all required fields.
10
+ *
11
+ * Known divergences between tool and API schemas are documented in a
12
+ * dedicated describe block at the bottom of this file.
13
+ */
14
+ import { describe, it, expect } from "vitest";
15
+ // ── mcp-types API schemas ────────────────────────────────────────────────────
16
+ import { createKeysInput, updateKeysInput, deleteKeysInput, listKeysInput, getTranslationsInput, addLanguagesInput, updateLanguagesInput, getSyncsInput, getSyncInput, publishInput, getProjectInput, getPendingChangesInput, } from "@better-i18n/mcp-types/schemas";
17
+ // ── tool definitions ─────────────────────────────────────────────────────────
18
+ import { createKeys } from "../tools/createKeys.js";
19
+ import { updateKeys } from "../tools/updateKeys.js";
20
+ import { deleteKeys } from "../tools/deleteKeys.js";
21
+ import { listKeys } from "../tools/listKeys.js";
22
+ import { getTranslations } from "../tools/getTranslations.js";
23
+ import { publishTranslations } from "../tools/publishTranslations.js";
24
+ import { proposeLanguages } from "../tools/proposeLanguages.js";
25
+ import { proposeLanguageEdits } from "../tools/proposeLanguageEdits.js";
26
+ import { getSyncs } from "../tools/getSyncs.js";
27
+ import { getSync } from "../tools/getSync.js";
28
+ import { getProject } from "../tools/getProject.js";
29
+ import { getPendingChanges } from "../tools/getPendingChanges.js";
30
+ // ─────────────────────────────────────────────────────────────────────────────
31
+ // 1. Tool Zod schema vs mcp-types schema alignment
32
+ // ─────────────────────────────────────────────────────────────────────────────
33
+ describe("tool Zod schema vs mcp-types schema alignment", () => {
34
+ // ── createKeys ──────────────────────────────────────────────────────────────
35
+ describe("createKeys", () => {
36
+ it("API schema requires k array with min(1)", () => {
37
+ // Reject missing k
38
+ expect(() => createKeysInput.parse({ orgSlug: "org", projectSlug: "proj" })).toThrow();
39
+ // Reject empty array
40
+ expect(() => createKeysInput.parse({ orgSlug: "org", projectSlug: "proj", k: [] })).toThrow();
41
+ });
42
+ it("API schema k items: n required, ns defaults to 'default', v and t optional", () => {
43
+ const result = createKeysInput.parse({
44
+ orgSlug: "org",
45
+ projectSlug: "proj",
46
+ k: [{ n: "test.key" }],
47
+ });
48
+ expect(result.k[0]?.ns).toBe("default");
49
+ expect(result.k[0]?.v).toBeUndefined();
50
+ expect(result.k[0]?.t).toBeUndefined();
51
+ });
52
+ it("API schema accepts nc (namespace context) on key items", () => {
53
+ expect(() => createKeysInput.parse({
54
+ orgSlug: "org",
55
+ projectSlug: "proj",
56
+ k: [
57
+ {
58
+ n: "test.key",
59
+ nc: { description: "Auth namespace", team: "auth-team" },
60
+ },
61
+ ],
62
+ })).not.toThrow();
63
+ });
64
+ it("API schema rejects k item without n", () => {
65
+ expect(() => createKeysInput.parse({
66
+ orgSlug: "org",
67
+ projectSlug: "proj",
68
+ k: [{ ns: "common" }],
69
+ })).toThrow();
70
+ });
71
+ });
72
+ // ── updateKeys ──────────────────────────────────────────────────────────────
73
+ describe("updateKeys", () => {
74
+ it("API schema requires t array with min(1)", () => {
75
+ expect(() => updateKeysInput.parse({ orgSlug: "org", projectSlug: "proj" })).toThrow();
76
+ expect(() => updateKeysInput.parse({ orgSlug: "org", projectSlug: "proj", t: [] })).toThrow();
77
+ });
78
+ it("API schema t items: id, l, t required; s and st optional", () => {
79
+ const result = updateKeysInput.parse({
80
+ orgSlug: "org",
81
+ projectSlug: "proj",
82
+ t: [{ id: "uuid-1", l: "TR", t: "Turkish text" }],
83
+ });
84
+ // l gets lowercased by transform
85
+ expect(result.t[0]?.l).toBe("tr");
86
+ expect(result.t[0]?.s).toBeUndefined();
87
+ expect(result.t[0]?.st).toBeUndefined();
88
+ });
89
+ it("API schema t items: id does not enforce UUID format (plain string)", () => {
90
+ // API uses z.string() not z.string().uuid() for id in updateKeys
91
+ expect(() => updateKeysInput.parse({
92
+ orgSlug: "org",
93
+ projectSlug: "proj",
94
+ t: [{ id: "not-a-uuid", l: "tr", t: "text" }],
95
+ })).not.toThrow();
96
+ });
97
+ it("API schema rejects t item missing required fields", () => {
98
+ // Missing t (text)
99
+ expect(() => updateKeysInput.parse({
100
+ orgSlug: "org",
101
+ projectSlug: "proj",
102
+ t: [{ id: "uuid-1", l: "tr" }],
103
+ })).toThrow();
104
+ });
105
+ });
106
+ // ── deleteKeys ──────────────────────────────────────────────────────────────
107
+ describe("deleteKeys", () => {
108
+ it("API schema requires keyIds array of UUIDs, min(1), max(100)", () => {
109
+ // Missing keyIds
110
+ expect(() => deleteKeysInput.parse({ orgSlug: "org", projectSlug: "proj" })).toThrow();
111
+ // Empty array
112
+ expect(() => deleteKeysInput.parse({
113
+ orgSlug: "org",
114
+ projectSlug: "proj",
115
+ keyIds: [],
116
+ })).toThrow();
117
+ // Invalid UUID
118
+ expect(() => deleteKeysInput.parse({
119
+ orgSlug: "org",
120
+ projectSlug: "proj",
121
+ keyIds: ["not-a-uuid"],
122
+ })).toThrow();
123
+ });
124
+ it("API schema accepts valid UUID array", () => {
125
+ expect(() => deleteKeysInput.parse({
126
+ orgSlug: "org",
127
+ projectSlug: "proj",
128
+ keyIds: ["550e8400-e29b-41d4-a716-446655440000"],
129
+ })).not.toThrow();
130
+ });
131
+ it("API schema enforces max(100) on keyIds", () => {
132
+ const ids = Array.from({ length: 101 }, (_, i) => `550e8400-e29b-41d4-a716-${String(i).padStart(12, "0")}`);
133
+ expect(() => deleteKeysInput.parse({
134
+ orgSlug: "org",
135
+ projectSlug: "proj",
136
+ keyIds: ids,
137
+ })).toThrow();
138
+ });
139
+ it("tool Zod schema also requires UUIDs and enforces max(100)", () => {
140
+ // Tool and API are aligned on this
141
+ const toolMax = 100;
142
+ const apiMax = 100;
143
+ expect(toolMax).toBe(apiMax);
144
+ });
145
+ });
146
+ // ── listKeys ────────────────────────────────────────────────────────────────
147
+ describe("listKeys", () => {
148
+ it("API schema has page default 1", () => {
149
+ const result = listKeysInput.parse({
150
+ orgSlug: "org",
151
+ projectSlug: "proj",
152
+ });
153
+ expect(result.page).toBe(1);
154
+ });
155
+ it("API schema has limit default 20", () => {
156
+ const result = listKeysInput.parse({
157
+ orgSlug: "org",
158
+ projectSlug: "proj",
159
+ });
160
+ expect(result.limit).toBe(20);
161
+ });
162
+ it("API schema accepts search as string", () => {
163
+ expect(() => listKeysInput.parse({
164
+ orgSlug: "org",
165
+ projectSlug: "proj",
166
+ search: "login",
167
+ })).not.toThrow();
168
+ });
169
+ it("API schema accepts search as string array", () => {
170
+ expect(() => listKeysInput.parse({
171
+ orgSlug: "org",
172
+ projectSlug: "proj",
173
+ search: ["login", "signup"],
174
+ })).not.toThrow();
175
+ });
176
+ it("API schema accepts namespaces array", () => {
177
+ expect(() => listKeysInput.parse({
178
+ orgSlug: "org",
179
+ projectSlug: "proj",
180
+ namespaces: ["auth", "common"],
181
+ })).not.toThrow();
182
+ });
183
+ it("API schema accepts missingLanguage with lowercase transform", () => {
184
+ const result = listKeysInput.parse({
185
+ orgSlug: "org",
186
+ projectSlug: "proj",
187
+ missingLanguage: "TR",
188
+ });
189
+ expect(result.missingLanguage).toBe("tr");
190
+ });
191
+ it("API schema has max(250) for limit — differs from tool max(100)", () => {
192
+ // API accepts 250 (tool would reject this — see known divergences below)
193
+ expect(() => listKeysInput.parse({
194
+ orgSlug: "org",
195
+ projectSlug: "proj",
196
+ limit: 250,
197
+ })).not.toThrow();
198
+ });
199
+ it("API schema includes translatedLanguageCount in fields enum", () => {
200
+ expect(() => listKeysInput.parse({
201
+ orgSlug: "org",
202
+ projectSlug: "proj",
203
+ fields: ["id", "sourceText", "translatedLanguageCount"],
204
+ })).not.toThrow();
205
+ });
206
+ });
207
+ // ── getTranslations ─────────────────────────────────────────────────────────
208
+ describe("getTranslations", () => {
209
+ it("API schema accepts search, languages, namespaces, keys, status, limit", () => {
210
+ expect(() => getTranslationsInput.parse({
211
+ orgSlug: "org",
212
+ projectSlug: "proj",
213
+ search: "login",
214
+ languages: ["TR", "DE"],
215
+ namespaces: ["auth"],
216
+ keys: ["auth.login.title"],
217
+ status: "missing",
218
+ limit: 50,
219
+ })).not.toThrow();
220
+ });
221
+ it("API schema normalizes language codes to lowercase", () => {
222
+ const result = getTranslationsInput.parse({
223
+ orgSlug: "org",
224
+ projectSlug: "proj",
225
+ languages: ["TR", "DE"],
226
+ });
227
+ expect(result.languages).toEqual(["tr", "de"]);
228
+ });
229
+ it("API schema has limit max(200) and default(100)", () => {
230
+ const result = getTranslationsInput.parse({
231
+ orgSlug: "org",
232
+ projectSlug: "proj",
233
+ });
234
+ expect(result.limit).toBe(100);
235
+ // Reject over 200
236
+ expect(() => getTranslationsInput.parse({
237
+ orgSlug: "org",
238
+ projectSlug: "proj",
239
+ limit: 201,
240
+ })).toThrow();
241
+ });
242
+ it("API schema has status default 'all'", () => {
243
+ const result = getTranslationsInput.parse({
244
+ orgSlug: "org",
245
+ projectSlug: "proj",
246
+ });
247
+ expect(result.status).toBe("all");
248
+ });
249
+ it("API schema has compact field (boolean, default false)", () => {
250
+ const result = getTranslationsInput.parse({
251
+ orgSlug: "org",
252
+ projectSlug: "proj",
253
+ });
254
+ expect(result.compact).toBe(false);
255
+ });
256
+ it("API schema rejects invalid status values", () => {
257
+ expect(() => getTranslationsInput.parse({
258
+ orgSlug: "org",
259
+ projectSlug: "proj",
260
+ status: "invalid",
261
+ })).toThrow();
262
+ });
263
+ });
264
+ // ── publishTranslations ─────────────────────────────────────────────────────
265
+ describe("publishTranslations", () => {
266
+ it("API schema accepts empty (publish all) — translations is optional", () => {
267
+ expect(() => publishInput.parse({ orgSlug: "org", projectSlug: "proj" })).not.toThrow();
268
+ });
269
+ it("API schema translations items require keyId (UUID) and languageCode", () => {
270
+ expect(() => publishInput.parse({
271
+ orgSlug: "org",
272
+ projectSlug: "proj",
273
+ translations: [
274
+ { keyId: "550e8400-e29b-41d4-a716-446655440000", languageCode: "tr" },
275
+ ],
276
+ })).not.toThrow();
277
+ });
278
+ it("API schema keyId enforces UUID format", () => {
279
+ expect(() => publishInput.parse({
280
+ orgSlug: "org",
281
+ projectSlug: "proj",
282
+ translations: [{ keyId: "not-uuid", languageCode: "tr" }],
283
+ })).toThrow();
284
+ });
285
+ it("API schema normalizes languageCode to lowercase", () => {
286
+ const result = publishInput.parse({
287
+ orgSlug: "org",
288
+ projectSlug: "proj",
289
+ translations: [
290
+ { keyId: "550e8400-e29b-41d4-a716-446655440000", languageCode: "TR" },
291
+ ],
292
+ });
293
+ expect(result.translations?.[0]?.languageCode).toBe("tr");
294
+ });
295
+ });
296
+ // ── proposeLanguages (addLanguages) ─────────────────────────────────────────
297
+ describe("proposeLanguages / addLanguages", () => {
298
+ it("API schema requires languages array min(1)", () => {
299
+ expect(() => addLanguagesInput.parse({
300
+ orgSlug: "org",
301
+ projectSlug: "proj",
302
+ languages: [],
303
+ })).toThrow();
304
+ });
305
+ it("API schema allows up to 50 languages", () => {
306
+ const langs = Array.from({ length: 50 }, (_, i) => ({
307
+ languageCode: `l${i.toString().padStart(2, "0")}`,
308
+ }));
309
+ expect(() => addLanguagesInput.parse({
310
+ orgSlug: "org",
311
+ projectSlug: "proj",
312
+ languages: langs,
313
+ })).not.toThrow();
314
+ });
315
+ it("API schema rejects more than 50 languages", () => {
316
+ const langs = Array.from({ length: 51 }, (_, i) => ({
317
+ languageCode: `l${i.toString().padStart(2, "0")}`,
318
+ }));
319
+ expect(() => addLanguagesInput.parse({
320
+ orgSlug: "org",
321
+ projectSlug: "proj",
322
+ languages: langs,
323
+ })).toThrow();
324
+ });
325
+ it("API schema languageCode has max(10)", () => {
326
+ // "zh-Hant-TW" is exactly 10 chars — should pass
327
+ expect(() => addLanguagesInput.parse({
328
+ orgSlug: "org",
329
+ projectSlug: "proj",
330
+ languages: [{ languageCode: "zh-Hant-TW" }],
331
+ })).not.toThrow(); // 10 chars — exactly at max(10)
332
+ // "zh-Hans-CN-x" is 12 chars — should fail
333
+ expect(() => addLanguagesInput.parse({
334
+ orgSlug: "org",
335
+ projectSlug: "proj",
336
+ languages: [{ languageCode: "zh-Hans-CN-x" }],
337
+ })).toThrow(); // 12 chars — exceeds max(10)
338
+ });
339
+ it("API schema status is optional with default 'active' when explicitly set", () => {
340
+ // languageEntrySchema uses .default("active").optional() which means
341
+ // the status field is optional — when omitted, status is undefined (not "active")
342
+ // This is a quirk of Zod's .default().optional() ordering
343
+ const result = addLanguagesInput.parse({
344
+ orgSlug: "org",
345
+ projectSlug: "proj",
346
+ languages: [{ languageCode: "tr" }],
347
+ });
348
+ // status is undefined when not provided (optional wins over default here)
349
+ // When explicitly set to "active", it returns "active"
350
+ expect(result.languages[0]?.status === "active" || result.languages[0]?.status === undefined).toBe(true);
351
+ // Explicitly providing status returns the value
352
+ const resultWithStatus = addLanguagesInput.parse({
353
+ orgSlug: "org",
354
+ projectSlug: "proj",
355
+ languages: [{ languageCode: "tr", status: "draft" }],
356
+ });
357
+ expect(resultWithStatus.languages[0]?.status).toBe("draft");
358
+ });
359
+ });
360
+ // ── proposeLanguageEdits (updateLanguages) ───────────────────────────────────
361
+ describe("proposeLanguageEdits / updateLanguages", () => {
362
+ it("tool uses edits[].newStatus, API uses updates[].status — the mapping is intentional", () => {
363
+ // Tool Zod: edits[].newStatus
364
+ // API Zod: updates[].status
365
+ // proposeLanguageEdits.execute maps edits → updates, newStatus → status
366
+ // Verify API updateLanguagesInput structure uses 'updates' and 'status'
367
+ expect(() => updateLanguagesInput.parse({
368
+ orgSlug: "org",
369
+ projectSlug: "proj",
370
+ updates: [{ languageCode: "tr", status: "active" }],
371
+ })).not.toThrow();
372
+ });
373
+ it("API schema updates[].status accepts active/draft/archived", () => {
374
+ for (const status of ["active", "draft", "archived"]) {
375
+ expect(() => updateLanguagesInput.parse({
376
+ orgSlug: "org",
377
+ projectSlug: "proj",
378
+ updates: [{ languageCode: "tr", status }],
379
+ })).not.toThrow();
380
+ }
381
+ });
382
+ it("API schema rejects invalid status values", () => {
383
+ expect(() => updateLanguagesInput.parse({
384
+ orgSlug: "org",
385
+ projectSlug: "proj",
386
+ updates: [{ languageCode: "tr", status: "deleted" }],
387
+ })).toThrow();
388
+ });
389
+ it("API schema requires updates array min(1) and max(50)", () => {
390
+ expect(() => updateLanguagesInput.parse({
391
+ orgSlug: "org",
392
+ projectSlug: "proj",
393
+ updates: [],
394
+ })).toThrow();
395
+ });
396
+ });
397
+ // ── getSyncs ─────────────────────────────────────────────────────────────────
398
+ describe("getSyncs", () => {
399
+ it("API schema accepts limit, status, type — all optional", () => {
400
+ expect(() => getSyncsInput.parse({ orgSlug: "org", projectSlug: "proj" })).not.toThrow();
401
+ });
402
+ it("API schema limit defaults to 10, max 50", () => {
403
+ const result = getSyncsInput.parse({
404
+ orgSlug: "org",
405
+ projectSlug: "proj",
406
+ });
407
+ expect(result.limit).toBe(10);
408
+ expect(() => getSyncsInput.parse({
409
+ orgSlug: "org",
410
+ projectSlug: "proj",
411
+ limit: 51,
412
+ })).toThrow();
413
+ });
414
+ it("API schema status enum matches tool enum", () => {
415
+ const validStatuses = ["pending", "in_progress", "completed", "failed"];
416
+ for (const status of validStatuses) {
417
+ expect(() => getSyncsInput.parse({
418
+ orgSlug: "org",
419
+ projectSlug: "proj",
420
+ status,
421
+ })).not.toThrow();
422
+ }
423
+ });
424
+ it("API schema type enum matches tool enum", () => {
425
+ const validTypes = [
426
+ "initial_import",
427
+ "source_sync",
428
+ "cdn_upload",
429
+ "batch_publish",
430
+ ];
431
+ for (const type of validTypes) {
432
+ expect(() => getSyncsInput.parse({
433
+ orgSlug: "org",
434
+ projectSlug: "proj",
435
+ type,
436
+ })).not.toThrow();
437
+ }
438
+ });
439
+ });
440
+ // ── getSync ──────────────────────────────────────────────────────────────────
441
+ describe("getSync", () => {
442
+ it("API schema requires syncId as string", () => {
443
+ expect(() => getSyncInput.parse({ syncId: "job-123" })).not.toThrow();
444
+ });
445
+ it("API schema rejects missing syncId", () => {
446
+ expect(() => getSyncInput.parse({})).toThrow();
447
+ });
448
+ });
449
+ // ── getProject ───────────────────────────────────────────────────────────────
450
+ describe("getProject", () => {
451
+ it("API schema only requires orgSlug and projectSlug", () => {
452
+ expect(() => getProjectInput.parse({ orgSlug: "org", projectSlug: "proj" })).not.toThrow();
453
+ });
454
+ it("API schema rejects missing fields", () => {
455
+ expect(() => getProjectInput.parse({})).toThrow();
456
+ expect(() => getProjectInput.parse({ orgSlug: "org" })).toThrow();
457
+ });
458
+ });
459
+ // ── getPendingChanges ────────────────────────────────────────────────────────
460
+ describe("getPendingChanges", () => {
461
+ it("API schema only requires orgSlug and projectSlug", () => {
462
+ expect(() => getPendingChangesInput.parse({ orgSlug: "org", projectSlug: "proj" })).not.toThrow();
463
+ });
464
+ it("API schema rejects missing fields", () => {
465
+ expect(() => getPendingChangesInput.parse({})).toThrow();
466
+ });
467
+ });
468
+ });
469
+ // ─────────────────────────────────────────────────────────────────────────────
470
+ // 2. inputSchema JSON required fields vs Zod required fields
471
+ // ─────────────────────────────────────────────────────────────────────────────
472
+ describe("inputSchema JSON required fields match Zod semantics", () => {
473
+ it("createKeys: required contains 'project' and 'k', not 'ns'", () => {
474
+ const jsonRequired = createKeys.definition.inputSchema.required;
475
+ expect(jsonRequired).toContain("project");
476
+ expect(jsonRequired).toContain("k");
477
+ // ns is optional (defaults to "default")
478
+ expect(jsonRequired).not.toContain("ns");
479
+ });
480
+ it("updateKeys: required contains 'project' and 't'", () => {
481
+ const jsonRequired = updateKeys.definition.inputSchema.required;
482
+ expect(jsonRequired).toContain("project");
483
+ expect(jsonRequired).toContain("t");
484
+ });
485
+ it("updateKeys: inputSchema item required has id, l, t", () => {
486
+ const props = updateKeys.definition.inputSchema.properties;
487
+ const itemRequired = props.t?.items?.required ?? [];
488
+ expect(itemRequired).toContain("id");
489
+ expect(itemRequired).toContain("l");
490
+ expect(itemRequired).toContain("t");
491
+ // s and st are optional
492
+ expect(itemRequired).not.toContain("s");
493
+ expect(itemRequired).not.toContain("st");
494
+ });
495
+ it("deleteKeys: required contains 'project' and 'keyIds'", () => {
496
+ const jsonRequired = deleteKeys.definition.inputSchema.required;
497
+ expect(jsonRequired).toContain("project");
498
+ expect(jsonRequired).toContain("keyIds");
499
+ });
500
+ it("listKeys: required contains 'project', not 'page', 'limit', 'fields'", () => {
501
+ const jsonRequired = listKeys.definition.inputSchema.required;
502
+ expect(jsonRequired).toContain("project");
503
+ // page and limit have defaults, fields is optional
504
+ expect(jsonRequired).not.toContain("page");
505
+ expect(jsonRequired).not.toContain("limit");
506
+ expect(jsonRequired).not.toContain("fields");
507
+ });
508
+ it("getTranslations: required contains 'project' only", () => {
509
+ const jsonRequired = getTranslations.definition.inputSchema.required;
510
+ expect(jsonRequired).toContain("project");
511
+ // All other params are optional
512
+ expect(jsonRequired).not.toContain("search");
513
+ expect(jsonRequired).not.toContain("languages");
514
+ expect(jsonRequired).not.toContain("status");
515
+ });
516
+ it("publishTranslations: required contains 'project', not 'translations'", () => {
517
+ const jsonRequired = publishTranslations.definition.inputSchema.required;
518
+ expect(jsonRequired).toContain("project");
519
+ expect(jsonRequired).not.toContain("translations");
520
+ });
521
+ it("proposeLanguages: required contains 'project' and 'languages'", () => {
522
+ const jsonRequired = proposeLanguages.definition.inputSchema.required;
523
+ expect(jsonRequired).toContain("project");
524
+ expect(jsonRequired).toContain("languages");
525
+ });
526
+ it("proposeLanguageEdits: required contains 'project' and 'edits'", () => {
527
+ const jsonRequired = proposeLanguageEdits.definition.inputSchema.required;
528
+ expect(jsonRequired).toContain("project");
529
+ expect(jsonRequired).toContain("edits");
530
+ });
531
+ it("getSyncs: required contains 'project', not 'limit', 'status', 'type'", () => {
532
+ const jsonRequired = getSyncs.definition.inputSchema.required;
533
+ expect(jsonRequired).toContain("project");
534
+ expect(jsonRequired).not.toContain("limit");
535
+ expect(jsonRequired).not.toContain("status");
536
+ expect(jsonRequired).not.toContain("type");
537
+ });
538
+ it("getSync: required contains 'syncId'", () => {
539
+ const jsonRequired = getSync.definition.inputSchema.required;
540
+ expect(jsonRequired).toContain("syncId");
541
+ expect(jsonRequired).not.toContain("project");
542
+ });
543
+ it("getProject: required contains 'project'", () => {
544
+ const jsonRequired = getProject.definition.inputSchema.required;
545
+ expect(jsonRequired).toContain("project");
546
+ });
547
+ it("getPendingChanges: required contains 'project'", () => {
548
+ const jsonRequired = getPendingChanges.definition.inputSchema.required;
549
+ expect(jsonRequired).toContain("project");
550
+ });
551
+ });
552
+ // ─────────────────────────────────────────────────────────────────────────────
553
+ // 3. inputSchema JSON property names vs Zod schema keys
554
+ // ─────────────────────────────────────────────────────────────────────────────
555
+ describe("inputSchema JSON properties cover all Zod schema fields", () => {
556
+ it("createKeys: inputSchema has 'project' and 'k' properties", () => {
557
+ const jsonProps = Object.keys(createKeys.definition.inputSchema.properties);
558
+ expect(jsonProps).toContain("project");
559
+ expect(jsonProps).toContain("k");
560
+ });
561
+ it("updateKeys: inputSchema has 'project' and 't' properties", () => {
562
+ const jsonProps = Object.keys(updateKeys.definition.inputSchema.properties);
563
+ expect(jsonProps).toContain("project");
564
+ expect(jsonProps).toContain("t");
565
+ });
566
+ it("deleteKeys: inputSchema has 'project' and 'keyIds' properties", () => {
567
+ const jsonProps = Object.keys(deleteKeys.definition.inputSchema.properties);
568
+ expect(jsonProps).toContain("project");
569
+ expect(jsonProps).toContain("keyIds");
570
+ });
571
+ it("listKeys: inputSchema covers all search/filter/pagination fields", () => {
572
+ const jsonProps = Object.keys(listKeys.definition.inputSchema.properties);
573
+ expect(jsonProps).toContain("project");
574
+ expect(jsonProps).toContain("search");
575
+ expect(jsonProps).toContain("namespaces");
576
+ expect(jsonProps).toContain("missingLanguage");
577
+ expect(jsonProps).toContain("fields");
578
+ expect(jsonProps).toContain("page");
579
+ expect(jsonProps).toContain("limit");
580
+ });
581
+ it("getTranslations: inputSchema covers all search/filter fields", () => {
582
+ const jsonProps = Object.keys(getTranslations.definition.inputSchema.properties);
583
+ expect(jsonProps).toContain("project");
584
+ expect(jsonProps).toContain("search");
585
+ expect(jsonProps).toContain("languages");
586
+ expect(jsonProps).toContain("namespaces");
587
+ expect(jsonProps).toContain("keys");
588
+ expect(jsonProps).toContain("status");
589
+ expect(jsonProps).toContain("limit");
590
+ });
591
+ it("proposeLanguages: inputSchema has 'project' and 'languages'", () => {
592
+ const jsonProps = Object.keys(proposeLanguages.definition.inputSchema.properties);
593
+ expect(jsonProps).toContain("project");
594
+ expect(jsonProps).toContain("languages");
595
+ });
596
+ it("proposeLanguageEdits: inputSchema has 'project' and 'edits'", () => {
597
+ const jsonProps = Object.keys(proposeLanguageEdits.definition.inputSchema.properties);
598
+ expect(jsonProps).toContain("project");
599
+ expect(jsonProps).toContain("edits");
600
+ });
601
+ it("getSyncs: inputSchema has 'project', 'limit', 'status', 'type'", () => {
602
+ const jsonProps = Object.keys(getSyncs.definition.inputSchema.properties);
603
+ expect(jsonProps).toContain("project");
604
+ expect(jsonProps).toContain("limit");
605
+ expect(jsonProps).toContain("status");
606
+ expect(jsonProps).toContain("type");
607
+ });
608
+ it("getSync: inputSchema has 'syncId'", () => {
609
+ const jsonProps = Object.keys(getSync.definition.inputSchema.properties);
610
+ expect(jsonProps).toContain("syncId");
611
+ });
612
+ it("getProject: inputSchema has 'project'", () => {
613
+ const jsonProps = Object.keys(getProject.definition.inputSchema.properties);
614
+ expect(jsonProps).toContain("project");
615
+ });
616
+ it("getPendingChanges: inputSchema has 'project'", () => {
617
+ const jsonProps = Object.keys(getPendingChanges.definition.inputSchema.properties);
618
+ expect(jsonProps).toContain("project");
619
+ });
620
+ it("listKeys: inputSchema fields enum contains translatedLanguageCount", () => {
621
+ const props = listKeys.definition.inputSchema.properties;
622
+ const fieldsEnum = props.fields?.items?.enum ?? [];
623
+ expect(fieldsEnum).toContain("translatedLanguageCount");
624
+ expect(fieldsEnum).toContain("id");
625
+ expect(fieldsEnum).toContain("sourceText");
626
+ expect(fieldsEnum).toContain("translations");
627
+ expect(fieldsEnum).toContain("translatedLanguages");
628
+ });
629
+ });
630
+ // ─────────────────────────────────────────────────────────────────────────────
631
+ // 4. Mock responses vs CompactXxx type conformance
632
+ // ─────────────────────────────────────────────────────────────────────────────
633
+ describe("response type conformance", () => {
634
+ it("createKeys response matches CompactCreateKeysResponse shape", () => {
635
+ const response = {
636
+ ok: true,
637
+ cnt: 2,
638
+ new: 2,
639
+ ren: 0,
640
+ dup: 0,
641
+ k: [{ k: "test.key", id: "uuid-1", tr: 0 }],
642
+ };
643
+ expect(response).toHaveProperty("ok");
644
+ expect(response).toHaveProperty("cnt");
645
+ expect(response).toHaveProperty("new");
646
+ expect(response).toHaveProperty("ren");
647
+ expect(response).toHaveProperty("dup");
648
+ expect(response).toHaveProperty("k");
649
+ expect(response.k[0]).toHaveProperty("k");
650
+ expect(response.k[0]).toHaveProperty("id");
651
+ expect(response.k[0]).toHaveProperty("tr");
652
+ });
653
+ it("createKeys response: optional fields are typed correctly", () => {
654
+ // skip and warn are optional
655
+ const response = {
656
+ ok: true,
657
+ cnt: 1,
658
+ new: 1,
659
+ ren: 0,
660
+ dup: 0,
661
+ k: [{ k: "key1", id: "uuid-1", tr: 1 }],
662
+ skip: [{ k: "existing.key", reason: "duplicate" }],
663
+ warn: [{ k: "key1", ns: "auth", other: ["common"] }],
664
+ pub: { has: true, cnt: 1, hint: "Run publish to deploy" },
665
+ };
666
+ expect(response.skip).toBeDefined();
667
+ expect(response.warn).toBeDefined();
668
+ expect(response.pub).toBeDefined();
669
+ });
670
+ it("updateKeys response matches CompactUpdateKeysResponse shape", () => {
671
+ const response = {
672
+ ok: true,
673
+ cnt: 1,
674
+ upd: [{ k: "test.key", lng: ["tr"], src: false }],
675
+ };
676
+ expect(response).toHaveProperty("ok");
677
+ expect(response).toHaveProperty("cnt");
678
+ expect(response).toHaveProperty("upd");
679
+ expect(response.upd[0]).toHaveProperty("k");
680
+ expect(response.upd[0]).toHaveProperty("lng");
681
+ expect(response.upd[0]).toHaveProperty("src");
682
+ });
683
+ it("updateKeys response: errors and pub are optional", () => {
684
+ const response = {
685
+ ok: false,
686
+ cnt: 0,
687
+ upd: [],
688
+ errors: [
689
+ {
690
+ id: "uuid-1",
691
+ l: ["tr"],
692
+ code: "not_found",
693
+ msg: "Key not found",
694
+ },
695
+ ],
696
+ };
697
+ expect(response.errors).toBeDefined();
698
+ expect(response.errors?.[0]).toHaveProperty("code", "not_found");
699
+ });
700
+ it("deleteKeys response matches CompactDeleteKeysResponse shape", () => {
701
+ const response = {
702
+ ok: true,
703
+ cnt: 1,
704
+ mk: [{ id: "uuid-1", k: "test.key", ns: null }],
705
+ };
706
+ expect(response).toHaveProperty("ok");
707
+ expect(response).toHaveProperty("cnt");
708
+ expect(response).toHaveProperty("mk");
709
+ expect(response.mk[0]).toHaveProperty("id");
710
+ expect(response.mk[0]).toHaveProperty("k");
711
+ expect(response.mk[0]).toHaveProperty("ns");
712
+ });
713
+ it("deleteKeys response: skip is optional", () => {
714
+ const response = {
715
+ ok: true,
716
+ cnt: 0,
717
+ mk: [],
718
+ skip: ["uuid-not-found"],
719
+ };
720
+ expect(response.skip).toBeDefined();
721
+ });
722
+ it("listKeys response matches CompactListKeysResponse shape", () => {
723
+ const response = {
724
+ tot: 100,
725
+ ret: 20,
726
+ pg: 1,
727
+ lim: 20,
728
+ has_more: true,
729
+ nss: ["auth", "common"],
730
+ k: [{ k: "login.title", ns: 0, id: "uuid-1", src: "Login" }],
731
+ };
732
+ expect(response).toHaveProperty("tot");
733
+ expect(response).toHaveProperty("ret");
734
+ expect(response).toHaveProperty("pg");
735
+ expect(response).toHaveProperty("lim");
736
+ expect(response).toHaveProperty("has_more");
737
+ expect(response).toHaveProperty("nss");
738
+ expect(response).toHaveProperty("k");
739
+ expect(response.k[0]).toHaveProperty("k");
740
+ expect(response.k[0]).toHaveProperty("ns");
741
+ });
742
+ it("listKeys response: note is optional", () => {
743
+ const response = {
744
+ tot: 5000,
745
+ ret: 20,
746
+ pg: 1,
747
+ lim: 20,
748
+ has_more: true,
749
+ nss: ["default"],
750
+ k: [],
751
+ note: "Large project: use filters to narrow down results",
752
+ };
753
+ expect(response.note).toBeDefined();
754
+ });
755
+ it("getTranslations response matches CompactGetAllTranslationsResponse shape", () => {
756
+ const response = {
757
+ prj: "org/project",
758
+ sl: "en",
759
+ ret: 10,
760
+ tot: 100,
761
+ has_more: true,
762
+ keys: [
763
+ {
764
+ id: "uuid-1",
765
+ k: "auth.login.title",
766
+ src: "Login",
767
+ tr: { tr: { id: "tr-uuid-1", t: "Giriş", st: "published" } },
768
+ },
769
+ ],
770
+ };
771
+ expect(response).toHaveProperty("prj");
772
+ expect(response).toHaveProperty("sl");
773
+ expect(response).toHaveProperty("ret");
774
+ expect(response).toHaveProperty("tot");
775
+ expect(response).toHaveProperty("has_more");
776
+ expect(response).toHaveProperty("keys");
777
+ expect(response.keys[0]).toHaveProperty("id");
778
+ expect(response.keys[0]).toHaveProperty("k");
779
+ });
780
+ it("getTranslations response: optional envelope fields work correctly", () => {
781
+ const response = {
782
+ prj: "org/project",
783
+ sl: "en",
784
+ ret: 5,
785
+ tot: 5,
786
+ has_more: false,
787
+ srch: "login",
788
+ lng: ["tr", "de"],
789
+ st: "missing",
790
+ keys: [],
791
+ nsd: { auth: { kc: 12, desc: "Auth strings" } },
792
+ hint: "status filter was ignored",
793
+ };
794
+ expect(response.srch).toBe("login");
795
+ expect(response.lng).toEqual(["tr", "de"]);
796
+ expect(response.st).toBe("missing");
797
+ expect(response.nsd).toBeDefined();
798
+ expect(response.hint).toBeDefined();
799
+ });
800
+ it("getProject response matches CompactGetProjectResponse shape", () => {
801
+ const response = {
802
+ prj: "org/project",
803
+ sl: "en",
804
+ nss: [{ nm: "auth", kc: 10, desc: "Auth strings", ctx: null }],
805
+ lng: ["tr", "de"],
806
+ tk: 150,
807
+ cov: { tr: { tr: 100, pct: 67 } },
808
+ };
809
+ expect(response).toHaveProperty("prj");
810
+ expect(response).toHaveProperty("sl");
811
+ expect(response).toHaveProperty("nss");
812
+ expect(response).toHaveProperty("lng");
813
+ expect(response).toHaveProperty("tk");
814
+ expect(response).toHaveProperty("cov");
815
+ });
816
+ it("getProject response: cdn and msg are optional", () => {
817
+ const response = {
818
+ prj: "org/project",
819
+ sl: "en",
820
+ nss: [],
821
+ lng: [],
822
+ tk: 0,
823
+ cov: {},
824
+ cdn: {
825
+ base: "https://cdn.example.com",
826
+ mfst: "https://cdn.example.com/manifest.json",
827
+ pat: "https://cdn.example.com/{locale}/{namespace}.json",
828
+ ex: ["https://cdn.example.com/en/auth.json"],
829
+ },
830
+ msg: "No GitHub repository linked",
831
+ };
832
+ expect(response.cdn).toBeDefined();
833
+ expect(response.msg).toBeDefined();
834
+ });
835
+ it("getPendingChanges response matches CompactGetPendingChangesResponse shape", () => {
836
+ const response = {
837
+ prj: "org/project",
838
+ has_chg: true,
839
+ sum: { tr: 5, del_k: 0, lng_chg: 0, tot: 5 },
840
+ by_lng: {
841
+ tr: {
842
+ cnt: 5,
843
+ prv: [{ kid: "uuid-1", k: "auth.login", ns: "auth", t: "Giriş", st: "published" }],
844
+ },
845
+ },
846
+ del_k: [],
847
+ pub_dst: "cdn",
848
+ };
849
+ expect(response).toHaveProperty("prj");
850
+ expect(response).toHaveProperty("has_chg");
851
+ expect(response).toHaveProperty("sum");
852
+ expect(response.sum).toHaveProperty("tr");
853
+ expect(response.sum).toHaveProperty("del_k");
854
+ expect(response.sum).toHaveProperty("lng_chg");
855
+ expect(response.sum).toHaveProperty("tot");
856
+ expect(response).toHaveProperty("by_lng");
857
+ expect(response).toHaveProperty("del_k");
858
+ expect(response).toHaveProperty("pub_dst");
859
+ });
860
+ it("getPendingChanges response: no_pub_rsn is optional", () => {
861
+ const response = {
862
+ prj: "org/project",
863
+ has_chg: false,
864
+ sum: { tr: 0, del_k: 0, lng_chg: 0, tot: 0 },
865
+ by_lng: {},
866
+ del_k: [],
867
+ pub_dst: "none",
868
+ no_pub_rsn: "No repository configured",
869
+ };
870
+ expect(response.no_pub_rsn).toBeDefined();
871
+ });
872
+ it("getSyncs response matches CompactGetSyncsResponse shape", () => {
873
+ const response = {
874
+ prj: "org/project",
875
+ tot: 3,
876
+ sy: [
877
+ {
878
+ id: "sync-1",
879
+ tp: "source_sync",
880
+ st: "completed",
881
+ st_at: "2025-01-01T00:00:00Z",
882
+ meta: { kp: 100 },
883
+ },
884
+ ],
885
+ };
886
+ expect(response).toHaveProperty("prj");
887
+ expect(response).toHaveProperty("tot");
888
+ expect(response).toHaveProperty("sy");
889
+ expect(response.sy[0]).toHaveProperty("id");
890
+ expect(response.sy[0]).toHaveProperty("tp");
891
+ expect(response.sy[0]).toHaveProperty("st");
892
+ expect(response.sy[0]).toHaveProperty("st_at");
893
+ expect(response.sy[0]).toHaveProperty("meta");
894
+ });
895
+ it("getSync response matches CompactGetSyncResponse shape", () => {
896
+ const response = {
897
+ id: "sync-1",
898
+ tp: "source_sync",
899
+ st: "completed",
900
+ st_at: "2025-01-01T00:00:00Z",
901
+ log: ["Started", "Processed 100 keys", "Completed"],
902
+ aff_k: [{ k: "auth.login.title", act: "updated" }],
903
+ };
904
+ expect(response).toHaveProperty("id");
905
+ expect(response).toHaveProperty("tp");
906
+ expect(response).toHaveProperty("st");
907
+ expect(response).toHaveProperty("st_at");
908
+ expect(response).toHaveProperty("log");
909
+ expect(response).toHaveProperty("aff_k");
910
+ expect(response.aff_k[0]).toHaveProperty("k");
911
+ expect(response.aff_k[0]).toHaveProperty("act");
912
+ });
913
+ });
914
+ // ─────────────────────────────────────────────────────────────────────────────
915
+ // 5. Known divergences (tool vs API schema) — documented here as living spec
916
+ // ─────────────────────────────────────────────────────────────────────────────
917
+ describe("known divergences (tool vs API schema)", () => {
918
+ it("listKeys: tool limits to 100, API allows 250", () => {
919
+ // Tool's Zod: z.number().max(100)
920
+ // API's Zod: z.number().max(250)
921
+ // An agent requesting limit=150 gets a Zod validation error from the tool
922
+ // even though the API would accept it.
923
+ // TODO: Consider aligning tool max to API max (250)
924
+ const toolMax = 100;
925
+ const apiMax = 250;
926
+ expect(toolMax).toBeLessThan(apiMax);
927
+ // Confirm API accepts 150
928
+ expect(() => listKeysInput.parse({
929
+ orgSlug: "org",
930
+ projectSlug: "proj",
931
+ limit: 150,
932
+ })).not.toThrow();
933
+ });
934
+ it("listKeys: tool has translatedLanguageCount in fields enum, API also has it (aligned)", () => {
935
+ // Both tool and API support translatedLanguageCount in the fields enum
936
+ const toolFields = [
937
+ "id",
938
+ "sourceText",
939
+ "translations",
940
+ "translatedLanguages",
941
+ "translatedLanguageCount",
942
+ ];
943
+ const apiFields = [
944
+ "id",
945
+ "sourceText",
946
+ "translations",
947
+ "translatedLanguages",
948
+ "translatedLanguageCount",
949
+ ];
950
+ expect(toolFields).toContain("translatedLanguageCount");
951
+ expect(apiFields).toContain("translatedLanguageCount");
952
+ });
953
+ it("proposeLanguages: tool allows languageCode max 10, API also allows max 10 (aligned)", () => {
954
+ // Both tool and API now use max(10) for languageCode
955
+ // This was previously a divergence (task says tool=10, API=5)
956
+ // Verify actual API max by testing "zh-Hant-TW" (10 chars)
957
+ expect(() => addLanguagesInput.parse({
958
+ orgSlug: "org",
959
+ projectSlug: "proj",
960
+ languages: [{ languageCode: "zh-Hant-TW" }],
961
+ })).not.toThrow();
962
+ });
963
+ it("publishTranslations: tool keyId accepts any string, API enforces UUID format", () => {
964
+ // Tool Zod: z.string() — no UUID validation
965
+ // API Zod: z.string().uuid() — enforces UUID format
966
+ // An agent passing a non-UUID keyId will pass tool validation but fail at the API
967
+ // TODO: Align tool to use z.string().uuid() for earlier error feedback
968
+ // Verify API rejects non-UUID
969
+ expect(() => publishInput.parse({
970
+ orgSlug: "org",
971
+ projectSlug: "proj",
972
+ translations: [{ keyId: "not-a-uuid", languageCode: "tr" }],
973
+ })).toThrow();
974
+ });
975
+ it("proposeLanguageEdits: tool uses 'edits[].newStatus', API uses 'updates[].status' — rename on execute", () => {
976
+ // This is an intentional naming divergence where the tool renames fields on execute.
977
+ // Tool input: edits[].newStatus (more descriptive for LLM agents)
978
+ // API input: updates[].status (concise internal format)
979
+ // The proposeLanguageEdits.execute function performs the mapping:
980
+ // edits.map(e => ({ languageCode: e.languageCode, status: e.newStatus }))
981
+ const toolParamName = "edits";
982
+ const apiParamName = "updates";
983
+ expect(toolParamName).not.toBe(apiParamName);
984
+ // Verify API uses 'updates' key
985
+ expect(() => updateLanguagesInput.parse({
986
+ orgSlug: "org",
987
+ projectSlug: "proj",
988
+ updates: [{ languageCode: "tr", status: "active" }],
989
+ })).not.toThrow();
990
+ });
991
+ it("getSyncs: tool limit is optional (no default in Zod), API has default(10)", () => {
992
+ // Tool Zod: z.number().min(1).max(50).optional() — no default, passes undefined to API
993
+ // API Zod: z.number().min(1).max(50).default(10) — has server-side default
994
+ // This means if an agent omits limit, the API will use 10 (server default)
995
+ const apiResult = getSyncsInput.parse({
996
+ orgSlug: "org",
997
+ projectSlug: "proj",
998
+ });
999
+ expect(apiResult.limit).toBe(10);
1000
+ });
1001
+ it("getTranslations: tool status is optional (no default), API has default('all')", () => {
1002
+ // Tool Zod: z.enum([...]).optional() — no default applied at tool level
1003
+ // API Zod: z.enum([...]).default("all") — has server-side default
1004
+ const apiResult = getTranslationsInput.parse({
1005
+ orgSlug: "org",
1006
+ projectSlug: "proj",
1007
+ });
1008
+ expect(apiResult.status).toBe("all");
1009
+ });
1010
+ });
1011
+ //# sourceMappingURL=schema-alignment.test.js.map