@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.
- package/dist/__tests__/base-tool.test.d.ts +2 -0
- package/dist/__tests__/base-tool.test.d.ts.map +1 -0
- package/dist/__tests__/base-tool.test.js +193 -0
- package/dist/__tests__/base-tool.test.js.map +1 -0
- package/dist/__tests__/contract.test.d.ts +24 -0
- package/dist/__tests__/contract.test.d.ts.map +1 -0
- package/dist/__tests__/contract.test.js +919 -0
- package/dist/__tests__/contract.test.js.map +1 -0
- package/dist/__tests__/fixtures/mock-client.d.ts +11 -0
- package/dist/__tests__/fixtures/mock-client.d.ts.map +1 -0
- package/dist/__tests__/fixtures/mock-client.js +33 -0
- package/dist/__tests__/fixtures/mock-client.js.map +1 -0
- package/dist/__tests__/helpers.d.ts +10 -0
- package/dist/__tests__/helpers.d.ts.map +1 -0
- package/dist/__tests__/helpers.js +27 -0
- package/dist/__tests__/helpers.js.map +1 -0
- package/dist/__tests__/schema-alignment.test.d.ts +15 -0
- package/dist/__tests__/schema-alignment.test.d.ts.map +1 -0
- package/dist/__tests__/schema-alignment.test.js +1011 -0
- package/dist/__tests__/schema-alignment.test.js.map +1 -0
- package/dist/__tests__/server-dispatch.test.d.ts +10 -0
- package/dist/__tests__/server-dispatch.test.d.ts.map +1 -0
- package/dist/__tests__/server-dispatch.test.js +202 -0
- package/dist/__tests__/server-dispatch.test.js.map +1 -0
- package/dist/tools/__tests__/createKeys.test.d.ts +2 -0
- package/dist/tools/__tests__/createKeys.test.d.ts.map +1 -0
- package/dist/tools/__tests__/createKeys.test.js +139 -0
- package/dist/tools/__tests__/createKeys.test.js.map +1 -0
- package/dist/tools/__tests__/deleteKeys.test.d.ts +2 -0
- package/dist/tools/__tests__/deleteKeys.test.d.ts.map +1 -0
- package/dist/tools/__tests__/deleteKeys.test.js +91 -0
- package/dist/tools/__tests__/deleteKeys.test.js.map +1 -0
- package/dist/tools/__tests__/getPendingChanges.test.d.ts +2 -0
- package/dist/tools/__tests__/getPendingChanges.test.d.ts.map +1 -0
- package/dist/tools/__tests__/getPendingChanges.test.js +50 -0
- package/dist/tools/__tests__/getPendingChanges.test.js.map +1 -0
- package/dist/tools/__tests__/getProject.test.d.ts +2 -0
- package/dist/tools/__tests__/getProject.test.d.ts.map +1 -0
- package/dist/tools/__tests__/getProject.test.js +55 -0
- package/dist/tools/__tests__/getProject.test.js.map +1 -0
- package/dist/tools/__tests__/getSync.test.d.ts +2 -0
- package/dist/tools/__tests__/getSync.test.d.ts.map +1 -0
- package/dist/tools/__tests__/getSync.test.js +42 -0
- package/dist/tools/__tests__/getSync.test.js.map +1 -0
- package/dist/tools/__tests__/getSyncs.test.d.ts +2 -0
- package/dist/tools/__tests__/getSyncs.test.d.ts.map +1 -0
- package/dist/tools/__tests__/getSyncs.test.js +66 -0
- package/dist/tools/__tests__/getSyncs.test.js.map +1 -0
- package/dist/tools/__tests__/getTranslations.test.d.ts +2 -0
- package/dist/tools/__tests__/getTranslations.test.d.ts.map +1 -0
- package/dist/tools/__tests__/getTranslations.test.js +114 -0
- package/dist/tools/__tests__/getTranslations.test.js.map +1 -0
- package/dist/tools/__tests__/listKeys.test.d.ts +2 -0
- package/dist/tools/__tests__/listKeys.test.d.ts.map +1 -0
- package/dist/tools/__tests__/listKeys.test.js +98 -0
- package/dist/tools/__tests__/listKeys.test.js.map +1 -0
- package/dist/tools/__tests__/listProjects.test.d.ts +2 -0
- package/dist/tools/__tests__/listProjects.test.d.ts.map +1 -0
- package/dist/tools/__tests__/listProjects.test.js +45 -0
- package/dist/tools/__tests__/listProjects.test.js.map +1 -0
- package/dist/tools/__tests__/proposeLanguageEdits.test.d.ts +2 -0
- package/dist/tools/__tests__/proposeLanguageEdits.test.d.ts.map +1 -0
- package/dist/tools/__tests__/proposeLanguageEdits.test.js +87 -0
- package/dist/tools/__tests__/proposeLanguageEdits.test.js.map +1 -0
- package/dist/tools/__tests__/proposeLanguages.test.d.ts +2 -0
- package/dist/tools/__tests__/proposeLanguages.test.d.ts.map +1 -0
- package/dist/tools/__tests__/proposeLanguages.test.js +109 -0
- package/dist/tools/__tests__/proposeLanguages.test.js.map +1 -0
- package/dist/tools/__tests__/publishTranslations.test.d.ts +2 -0
- package/dist/tools/__tests__/publishTranslations.test.d.ts.map +1 -0
- package/dist/tools/__tests__/publishTranslations.test.js +127 -0
- package/dist/tools/__tests__/publishTranslations.test.js.map +1 -0
- package/dist/tools/__tests__/updateKeys.test.d.ts +2 -0
- package/dist/tools/__tests__/updateKeys.test.d.ts.map +1 -0
- package/dist/tools/__tests__/updateKeys.test.js +122 -0
- package/dist/tools/__tests__/updateKeys.test.js.map +1 -0
- package/dist/tools/createKeys.d.ts.map +1 -1
- package/dist/tools/createKeys.js +19 -7
- package/dist/tools/createKeys.js.map +1 -1
- package/dist/tools/deleteKeys.d.ts.map +1 -1
- package/dist/tools/deleteKeys.js +9 -1
- package/dist/tools/deleteKeys.js.map +1 -1
- package/dist/tools/getProject.d.ts.map +1 -1
- package/dist/tools/getProject.js +2 -1
- package/dist/tools/getProject.js.map +1 -1
- package/dist/tools/getTranslations.d.ts +1 -1
- package/dist/tools/getTranslations.js +4 -4
- package/dist/tools/getTranslations.js.map +1 -1
- package/dist/tools/publishTranslations.d.ts.map +1 -1
- package/dist/tools/publishTranslations.js +12 -3
- package/dist/tools/publishTranslations.js.map +1 -1
- package/dist/tools/updateKeys.d.ts.map +1 -1
- package/dist/tools/updateKeys.js +8 -3
- package/dist/tools/updateKeys.js.map +1 -1
- package/dist/worker.test.d.ts +16 -0
- package/dist/worker.test.d.ts.map +1 -0
- package/dist/worker.test.js +244 -0
- package/dist/worker.test.js.map +1 -0
- 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
|