@better-i18n/mcp 0.15.6 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tools/createKeys.d.ts.map +1 -1
- package/dist/tools/createKeys.js +14 -0
- package/dist/tools/createKeys.js.map +1 -1
- package/dist/tools/proposeLanguageEdits.d.ts.map +1 -1
- package/dist/tools/proposeLanguageEdits.js +10 -0
- package/dist/tools/proposeLanguageEdits.js.map +1 -1
- package/dist/tools/proposeLanguages.d.ts.map +1 -1
- package/dist/tools/proposeLanguages.js +4 -0
- package/dist/tools/proposeLanguages.js.map +1 -1
- package/package.json +3 -2
- package/dist/__tests__/base-tool.test.d.ts +0 -2
- package/dist/__tests__/base-tool.test.d.ts.map +0 -1
- package/dist/__tests__/base-tool.test.js +0 -193
- package/dist/__tests__/base-tool.test.js.map +0 -1
- package/dist/__tests__/contract.test.d.ts +0 -24
- package/dist/__tests__/contract.test.d.ts.map +0 -1
- package/dist/__tests__/contract.test.js +0 -919
- package/dist/__tests__/contract.test.js.map +0 -1
- package/dist/__tests__/fixtures/mock-client.d.ts +0 -11
- package/dist/__tests__/fixtures/mock-client.d.ts.map +0 -1
- package/dist/__tests__/fixtures/mock-client.js +0 -33
- package/dist/__tests__/fixtures/mock-client.js.map +0 -1
- package/dist/__tests__/helpers.d.ts +0 -10
- package/dist/__tests__/helpers.d.ts.map +0 -1
- package/dist/__tests__/helpers.js +0 -27
- package/dist/__tests__/helpers.js.map +0 -1
- package/dist/__tests__/schema-alignment.test.d.ts +0 -15
- package/dist/__tests__/schema-alignment.test.d.ts.map +0 -1
- package/dist/__tests__/schema-alignment.test.js +0 -1011
- package/dist/__tests__/schema-alignment.test.js.map +0 -1
- package/dist/__tests__/server-dispatch.test.d.ts +0 -10
- package/dist/__tests__/server-dispatch.test.d.ts.map +0 -1
- package/dist/__tests__/server-dispatch.test.js +0 -202
- package/dist/__tests__/server-dispatch.test.js.map +0 -1
- package/dist/__tests__/version-check.test.d.ts +0 -2
- package/dist/__tests__/version-check.test.d.ts.map +0 -1
- package/dist/__tests__/version-check.test.js +0 -89
- package/dist/__tests__/version-check.test.js.map +0 -1
- package/dist/tools/__tests__/createKeys.test.d.ts +0 -2
- package/dist/tools/__tests__/createKeys.test.d.ts.map +0 -1
- package/dist/tools/__tests__/createKeys.test.js +0 -139
- package/dist/tools/__tests__/createKeys.test.js.map +0 -1
- package/dist/tools/__tests__/deleteKeys.test.d.ts +0 -2
- package/dist/tools/__tests__/deleteKeys.test.d.ts.map +0 -1
- package/dist/tools/__tests__/deleteKeys.test.js +0 -91
- package/dist/tools/__tests__/deleteKeys.test.js.map +0 -1
- package/dist/tools/__tests__/getPendingChanges.test.d.ts +0 -2
- package/dist/tools/__tests__/getPendingChanges.test.d.ts.map +0 -1
- package/dist/tools/__tests__/getPendingChanges.test.js +0 -50
- package/dist/tools/__tests__/getPendingChanges.test.js.map +0 -1
- package/dist/tools/__tests__/getProject.test.d.ts +0 -2
- package/dist/tools/__tests__/getProject.test.d.ts.map +0 -1
- package/dist/tools/__tests__/getProject.test.js +0 -55
- package/dist/tools/__tests__/getProject.test.js.map +0 -1
- package/dist/tools/__tests__/getSync.test.d.ts +0 -2
- package/dist/tools/__tests__/getSync.test.d.ts.map +0 -1
- package/dist/tools/__tests__/getSync.test.js +0 -42
- package/dist/tools/__tests__/getSync.test.js.map +0 -1
- package/dist/tools/__tests__/getSyncs.test.d.ts +0 -2
- package/dist/tools/__tests__/getSyncs.test.d.ts.map +0 -1
- package/dist/tools/__tests__/getSyncs.test.js +0 -66
- package/dist/tools/__tests__/getSyncs.test.js.map +0 -1
- package/dist/tools/__tests__/getTranslations.test.d.ts +0 -2
- package/dist/tools/__tests__/getTranslations.test.d.ts.map +0 -1
- package/dist/tools/__tests__/getTranslations.test.js +0 -114
- package/dist/tools/__tests__/getTranslations.test.js.map +0 -1
- package/dist/tools/__tests__/listKeys.test.d.ts +0 -2
- package/dist/tools/__tests__/listKeys.test.d.ts.map +0 -1
- package/dist/tools/__tests__/listKeys.test.js +0 -98
- package/dist/tools/__tests__/listKeys.test.js.map +0 -1
- package/dist/tools/__tests__/listProjects.test.d.ts +0 -2
- package/dist/tools/__tests__/listProjects.test.d.ts.map +0 -1
- package/dist/tools/__tests__/listProjects.test.js +0 -45
- package/dist/tools/__tests__/listProjects.test.js.map +0 -1
- package/dist/tools/__tests__/proposeLanguageEdits.test.d.ts +0 -2
- package/dist/tools/__tests__/proposeLanguageEdits.test.d.ts.map +0 -1
- package/dist/tools/__tests__/proposeLanguageEdits.test.js +0 -87
- package/dist/tools/__tests__/proposeLanguageEdits.test.js.map +0 -1
- package/dist/tools/__tests__/proposeLanguages.test.d.ts +0 -2
- package/dist/tools/__tests__/proposeLanguages.test.d.ts.map +0 -1
- package/dist/tools/__tests__/proposeLanguages.test.js +0 -109
- package/dist/tools/__tests__/proposeLanguages.test.js.map +0 -1
- package/dist/tools/__tests__/publishTranslations.test.d.ts +0 -2
- package/dist/tools/__tests__/publishTranslations.test.d.ts.map +0 -1
- package/dist/tools/__tests__/publishTranslations.test.js +0 -127
- package/dist/tools/__tests__/publishTranslations.test.js.map +0 -1
- package/dist/tools/__tests__/updateKeys.test.d.ts +0 -2
- package/dist/tools/__tests__/updateKeys.test.d.ts.map +0 -1
- package/dist/tools/__tests__/updateKeys.test.js +0 -122
- package/dist/tools/__tests__/updateKeys.test.js.map +0 -1
|
@@ -1,919 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* contract.test.ts
|
|
3
|
-
*
|
|
4
|
-
* End-to-end contract tests that bridge the gap between tool logic and the real API.
|
|
5
|
-
*
|
|
6
|
-
* THREE test sections:
|
|
7
|
-
*
|
|
8
|
-
* 1. FORWARD CONTRACT — tool output → API schema validation
|
|
9
|
-
* Execute each tool with a mock client, capture what it ACTUALLY sends to the
|
|
10
|
-
* API (orgSlug, projectSlug, payload), and validate those args against the
|
|
11
|
-
* mcp-types Zod schemas (the real API contract). If the API schema rejects it,
|
|
12
|
-
* the tool has a normalization or mapping bug.
|
|
13
|
-
*
|
|
14
|
-
* 2. REVERSE CONTRACT — LLM-plausible args → tool acceptance
|
|
15
|
-
* Simulate inputs an LLM would plausibly generate from reading inputSchema,
|
|
16
|
-
* and assert the tool accepts them without isError. Catches regressions where
|
|
17
|
-
* a schema tightening breaks something an LLM was expected to send.
|
|
18
|
-
*
|
|
19
|
-
* 3. NEGATIVE CONTRACT — invalid args MUST be rejected
|
|
20
|
-
* Guard rails: assert that clearly invalid inputs are rejected (isError=true)
|
|
21
|
-
* so we never silently swallow bad data from an LLM.
|
|
22
|
-
*/
|
|
23
|
-
import { describe, it, expect, vi } from "vitest";
|
|
24
|
-
import { createMockClient } from "./fixtures/mock-client.js";
|
|
25
|
-
// ── Tool imports ──────────────────────────────────────────────────────────────
|
|
26
|
-
import { createKeys } from "../tools/createKeys.js";
|
|
27
|
-
import { updateKeys } from "../tools/updateKeys.js";
|
|
28
|
-
import { deleteKeys } from "../tools/deleteKeys.js";
|
|
29
|
-
import { publishTranslations } from "../tools/publishTranslations.js";
|
|
30
|
-
import { listKeys } from "../tools/listKeys.js";
|
|
31
|
-
import { getTranslations } from "../tools/getTranslations.js";
|
|
32
|
-
import { listProjects } from "../tools/listProjects.js";
|
|
33
|
-
import { getProject } from "../tools/getProject.js";
|
|
34
|
-
import { getPendingChanges } from "../tools/getPendingChanges.js";
|
|
35
|
-
import { proposeLanguages } from "../tools/proposeLanguages.js";
|
|
36
|
-
import { proposeLanguageEdits } from "../tools/proposeLanguageEdits.js";
|
|
37
|
-
import { getSyncs } from "../tools/getSyncs.js";
|
|
38
|
-
import { getSync } from "../tools/getSync.js";
|
|
39
|
-
// ── mcp-types API schemas (the real API contract) ────────────────────────────
|
|
40
|
-
import { createKeysInput, updateKeysInput, deleteKeysInput, listKeysInput, getTranslationsInput, addLanguagesInput, updateLanguagesInput, getSyncsInput, getSyncInput, publishInput, getProjectInput, getPendingChangesInput, } from "@better-i18n/mcp-types/schemas";
|
|
41
|
-
// ── Stub responses (minimal valid shapes, tools don't care about their content) ──
|
|
42
|
-
const STUBS = {
|
|
43
|
-
createKeys: { ok: true, cnt: 1, new: 1, ren: 0, dup: 0, k: [] },
|
|
44
|
-
updateKeys: { ok: true, cnt: 1, upd: [] },
|
|
45
|
-
deleteKeys: { ok: true, cnt: 1, mk: [] },
|
|
46
|
-
publishTranslations: { success: true },
|
|
47
|
-
listKeys: { tot: 0, ret: 0, pg: 1, lim: 20, has_more: false, nss: [], k: [] },
|
|
48
|
-
getAllTranslations: { prj: "org/proj", sl: "en", ret: 0, tot: 0, has_more: false, keys: [] },
|
|
49
|
-
getProject: { prj: "org/proj", sl: "en", nss: [], lng: [], tk: 0, cov: {} },
|
|
50
|
-
getPendingChanges: { prj: "org/proj", has_chg: false, sum: { tr: 0, del_k: 0, lng_chg: 0, tot: 0 }, by_lng: {}, del_k: [], pub_dst: "cdn" },
|
|
51
|
-
addLanguages: { success: true, added: 1, skipped: 0, results: [] },
|
|
52
|
-
updateLanguages: { success: true, results: [], notFound: [] },
|
|
53
|
-
getSyncs: { prj: "org/proj", tot: 0, sy: [] },
|
|
54
|
-
getSync: { id: "sync-1", tp: "source_sync", st: "completed", st_at: "2024-01-01", log: [], aff_k: [] },
|
|
55
|
-
listProjects: [],
|
|
56
|
-
};
|
|
57
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
58
|
-
// 1. FORWARD CONTRACT: Tool output → API schema validation
|
|
59
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
60
|
-
describe("forward contract: tool output → API schema", () => {
|
|
61
|
-
// ── createKeys ──────────────────────────────────────────────────────────────
|
|
62
|
-
describe("createKeys", () => {
|
|
63
|
-
it("API schema accepts what tool sends after normalization", async () => {
|
|
64
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.createKeys);
|
|
65
|
-
const client = createMockClient({ mcp: { createKeys: { mutate: mutateMock } } });
|
|
66
|
-
await createKeys.execute(client, {
|
|
67
|
-
project: "my-org/my-proj",
|
|
68
|
-
k: [{ n: "greeting", v: "Hello", t: { TR: "Merhaba", DE: "Hallo" } }],
|
|
69
|
-
});
|
|
70
|
-
const apiArgs = mutateMock.mock.calls[0][0];
|
|
71
|
-
const result = createKeysInput.safeParse(apiArgs);
|
|
72
|
-
expect(result.success, result.error?.message).toBe(true);
|
|
73
|
-
});
|
|
74
|
-
it("language codes are lowercased before API call", async () => {
|
|
75
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.createKeys);
|
|
76
|
-
const client = createMockClient({ mcp: { createKeys: { mutate: mutateMock } } });
|
|
77
|
-
await createKeys.execute(client, {
|
|
78
|
-
project: "org/proj",
|
|
79
|
-
k: [{ n: "test", t: { TR: "val", DE: "val" } }],
|
|
80
|
-
});
|
|
81
|
-
const apiArgs = mutateMock.mock.calls[0][0];
|
|
82
|
-
expect(apiArgs.k[0].t).toEqual({ tr: "val", de: "val" });
|
|
83
|
-
const result = createKeysInput.safeParse(apiArgs);
|
|
84
|
-
expect(result.success, result.error?.message).toBe(true);
|
|
85
|
-
});
|
|
86
|
-
it("default namespace is included in API call", async () => {
|
|
87
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.createKeys);
|
|
88
|
-
const client = createMockClient({ mcp: { createKeys: { mutate: mutateMock } } });
|
|
89
|
-
await createKeys.execute(client, {
|
|
90
|
-
project: "org/proj",
|
|
91
|
-
k: [{ n: "test" }],
|
|
92
|
-
});
|
|
93
|
-
const apiArgs = mutateMock.mock.calls[0][0];
|
|
94
|
-
expect(apiArgs.k[0].ns).toBe("default");
|
|
95
|
-
const result = createKeysInput.safeParse(apiArgs);
|
|
96
|
-
expect(result.success, result.error?.message).toBe(true);
|
|
97
|
-
});
|
|
98
|
-
it("namespace context passes through to API", async () => {
|
|
99
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.createKeys);
|
|
100
|
-
const client = createMockClient({ mcp: { createKeys: { mutate: mutateMock } } });
|
|
101
|
-
await createKeys.execute(client, {
|
|
102
|
-
project: "org/proj",
|
|
103
|
-
k: [{
|
|
104
|
-
n: "test",
|
|
105
|
-
nc: {
|
|
106
|
-
description: "Auth flow",
|
|
107
|
-
team: "auth-team",
|
|
108
|
-
domain: "auth",
|
|
109
|
-
aiPrompt: "Use formal tone",
|
|
110
|
-
tags: ["critical"],
|
|
111
|
-
},
|
|
112
|
-
}],
|
|
113
|
-
});
|
|
114
|
-
const apiArgs = mutateMock.mock.calls[0][0];
|
|
115
|
-
const result = createKeysInput.safeParse(apiArgs);
|
|
116
|
-
expect(result.success, result.error?.message).toBe(true);
|
|
117
|
-
});
|
|
118
|
-
it("orgSlug and projectSlug are correctly split from project string", async () => {
|
|
119
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.createKeys);
|
|
120
|
-
const client = createMockClient({ mcp: { createKeys: { mutate: mutateMock } } });
|
|
121
|
-
await createKeys.execute(client, {
|
|
122
|
-
project: "my-org/my-proj",
|
|
123
|
-
k: [{ n: "key" }],
|
|
124
|
-
});
|
|
125
|
-
const apiArgs = mutateMock.mock.calls[0][0];
|
|
126
|
-
expect(apiArgs.orgSlug).toBe("my-org");
|
|
127
|
-
expect(apiArgs.projectSlug).toBe("my-proj");
|
|
128
|
-
const result = createKeysInput.safeParse(apiArgs);
|
|
129
|
-
expect(result.success, result.error?.message).toBe(true);
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
// ── updateKeys ──────────────────────────────────────────────────────────────
|
|
133
|
-
describe("updateKeys", () => {
|
|
134
|
-
it("API schema accepts what tool sends after normalization", async () => {
|
|
135
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.updateKeys);
|
|
136
|
-
const client = createMockClient({ mcp: { updateKeys: { mutate: mutateMock } } });
|
|
137
|
-
await updateKeys.execute(client, {
|
|
138
|
-
project: "org/proj",
|
|
139
|
-
t: [{ id: "550e8400-e29b-41d4-a716-446655440000", l: "TR", t: "Merhaba" }],
|
|
140
|
-
});
|
|
141
|
-
const apiArgs = mutateMock.mock.calls[0][0];
|
|
142
|
-
expect(apiArgs.t[0].l).toBe("tr");
|
|
143
|
-
const result = updateKeysInput.safeParse(apiArgs);
|
|
144
|
-
expect(result.success, result.error?.message).toBe(true);
|
|
145
|
-
});
|
|
146
|
-
it("source update flag and status pass through correctly", async () => {
|
|
147
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.updateKeys);
|
|
148
|
-
const client = createMockClient({ mcp: { updateKeys: { mutate: mutateMock } } });
|
|
149
|
-
await updateKeys.execute(client, {
|
|
150
|
-
project: "org/proj",
|
|
151
|
-
t: [{ id: "550e8400-e29b-41d4-a716-446655440000", l: "en", t: "Updated", s: true, st: "published" }],
|
|
152
|
-
});
|
|
153
|
-
const apiArgs = mutateMock.mock.calls[0][0];
|
|
154
|
-
const result = updateKeysInput.safeParse(apiArgs);
|
|
155
|
-
expect(result.success, result.error?.message).toBe(true);
|
|
156
|
-
});
|
|
157
|
-
it("batch updates accepted by API", async () => {
|
|
158
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.updateKeys);
|
|
159
|
-
const client = createMockClient({ mcp: { updateKeys: { mutate: mutateMock } } });
|
|
160
|
-
await updateKeys.execute(client, {
|
|
161
|
-
project: "org/proj",
|
|
162
|
-
t: [
|
|
163
|
-
{ id: "550e8400-e29b-41d4-a716-446655440000", l: "TR", t: "Merhaba" },
|
|
164
|
-
{ id: "550e8400-e29b-41d4-a716-446655440001", l: "DE", t: "Hallo" },
|
|
165
|
-
],
|
|
166
|
-
});
|
|
167
|
-
const apiArgs = mutateMock.mock.calls[0][0];
|
|
168
|
-
expect(apiArgs.t[0].l).toBe("tr");
|
|
169
|
-
expect(apiArgs.t[1].l).toBe("de");
|
|
170
|
-
const result = updateKeysInput.safeParse(apiArgs);
|
|
171
|
-
expect(result.success, result.error?.message).toBe(true);
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
// ── deleteKeys ──────────────────────────────────────────────────────────────
|
|
175
|
-
describe("deleteKeys", () => {
|
|
176
|
-
it("API schema accepts what tool sends", async () => {
|
|
177
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.deleteKeys);
|
|
178
|
-
const client = createMockClient({ mcp: { deleteKeys: { mutate: mutateMock } } });
|
|
179
|
-
await deleteKeys.execute(client, {
|
|
180
|
-
project: "org/proj",
|
|
181
|
-
keyIds: ["550e8400-e29b-41d4-a716-446655440000"],
|
|
182
|
-
});
|
|
183
|
-
const apiArgs = mutateMock.mock.calls[0][0];
|
|
184
|
-
const result = deleteKeysInput.safeParse(apiArgs);
|
|
185
|
-
expect(result.success, result.error?.message).toBe(true);
|
|
186
|
-
});
|
|
187
|
-
it("multiple UUIDs accepted by API", async () => {
|
|
188
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.deleteKeys);
|
|
189
|
-
const client = createMockClient({ mcp: { deleteKeys: { mutate: mutateMock } } });
|
|
190
|
-
await deleteKeys.execute(client, {
|
|
191
|
-
project: "org/proj",
|
|
192
|
-
keyIds: [
|
|
193
|
-
"550e8400-e29b-41d4-a716-446655440000",
|
|
194
|
-
"550e8400-e29b-41d4-a716-446655440001",
|
|
195
|
-
"550e8400-e29b-41d4-a716-446655440002",
|
|
196
|
-
],
|
|
197
|
-
});
|
|
198
|
-
const apiArgs = mutateMock.mock.calls[0][0];
|
|
199
|
-
const result = deleteKeysInput.safeParse(apiArgs);
|
|
200
|
-
expect(result.success, result.error?.message).toBe(true);
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
// ── publishTranslations ─────────────────────────────────────────────────────
|
|
204
|
-
describe("publishTranslations", () => {
|
|
205
|
-
it("full publish (no translations) accepted by API", async () => {
|
|
206
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.publishTranslations);
|
|
207
|
-
const client = createMockClient({ mcp: { publishTranslations: { mutate: mutateMock } } });
|
|
208
|
-
await publishTranslations.execute(client, { project: "org/proj" });
|
|
209
|
-
const apiArgs = mutateMock.mock.calls[0][0];
|
|
210
|
-
const result = publishInput.safeParse(apiArgs);
|
|
211
|
-
expect(result.success, result.error?.message).toBe(true);
|
|
212
|
-
});
|
|
213
|
-
it("selective publish with normalized language codes accepted by API", async () => {
|
|
214
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.publishTranslations);
|
|
215
|
-
const client = createMockClient({ mcp: { publishTranslations: { mutate: mutateMock } } });
|
|
216
|
-
await publishTranslations.execute(client, {
|
|
217
|
-
project: "org/proj",
|
|
218
|
-
translations: [{ keyId: "550e8400-e29b-41d4-a716-446655440000", languageCode: "TR" }],
|
|
219
|
-
});
|
|
220
|
-
const apiArgs = mutateMock.mock.calls[0][0];
|
|
221
|
-
expect(apiArgs.translations[0].languageCode).toBe("tr");
|
|
222
|
-
const result = publishInput.safeParse(apiArgs);
|
|
223
|
-
expect(result.success, result.error?.message).toBe(true);
|
|
224
|
-
});
|
|
225
|
-
it("multiple selective translations accepted by API", async () => {
|
|
226
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.publishTranslations);
|
|
227
|
-
const client = createMockClient({ mcp: { publishTranslations: { mutate: mutateMock } } });
|
|
228
|
-
await publishTranslations.execute(client, {
|
|
229
|
-
project: "org/proj",
|
|
230
|
-
translations: [
|
|
231
|
-
{ keyId: "550e8400-e29b-41d4-a716-446655440000", languageCode: "tr" },
|
|
232
|
-
{ keyId: "550e8400-e29b-41d4-a716-446655440001", languageCode: "DE" },
|
|
233
|
-
],
|
|
234
|
-
});
|
|
235
|
-
const apiArgs = mutateMock.mock.calls[0][0];
|
|
236
|
-
expect(apiArgs.translations[1].languageCode).toBe("de");
|
|
237
|
-
const result = publishInput.safeParse(apiArgs);
|
|
238
|
-
expect(result.success, result.error?.message).toBe(true);
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
// ── listKeys ────────────────────────────────────────────────────────────────
|
|
242
|
-
describe("listKeys", () => {
|
|
243
|
-
it("default pagination accepted by API", async () => {
|
|
244
|
-
const queryMock = vi.fn().mockResolvedValue(STUBS.listKeys);
|
|
245
|
-
const client = createMockClient({ mcp: { listKeys: { query: queryMock } } });
|
|
246
|
-
await listKeys.execute(client, { project: "org/proj" });
|
|
247
|
-
const apiArgs = queryMock.mock.calls[0][0];
|
|
248
|
-
const result = listKeysInput.safeParse(apiArgs);
|
|
249
|
-
expect(result.success, result.error?.message).toBe(true);
|
|
250
|
-
});
|
|
251
|
-
it("all filters accepted by API", async () => {
|
|
252
|
-
const queryMock = vi.fn().mockResolvedValue(STUBS.listKeys);
|
|
253
|
-
const client = createMockClient({ mcp: { listKeys: { query: queryMock } } });
|
|
254
|
-
await listKeys.execute(client, {
|
|
255
|
-
project: "org/proj",
|
|
256
|
-
search: "login",
|
|
257
|
-
namespaces: ["auth"],
|
|
258
|
-
missingLanguage: "tr",
|
|
259
|
-
fields: ["id", "sourceText"],
|
|
260
|
-
page: 2,
|
|
261
|
-
limit: 50,
|
|
262
|
-
});
|
|
263
|
-
const apiArgs = queryMock.mock.calls[0][0];
|
|
264
|
-
const result = listKeysInput.safeParse(apiArgs);
|
|
265
|
-
expect(result.success, result.error?.message).toBe(true);
|
|
266
|
-
});
|
|
267
|
-
it("array search accepted by API", async () => {
|
|
268
|
-
const queryMock = vi.fn().mockResolvedValue(STUBS.listKeys);
|
|
269
|
-
const client = createMockClient({ mcp: { listKeys: { query: queryMock } } });
|
|
270
|
-
await listKeys.execute(client, {
|
|
271
|
-
project: "org/proj",
|
|
272
|
-
search: ["login", "signup"],
|
|
273
|
-
});
|
|
274
|
-
const apiArgs = queryMock.mock.calls[0][0];
|
|
275
|
-
const result = listKeysInput.safeParse(apiArgs);
|
|
276
|
-
expect(result.success, result.error?.message).toBe(true);
|
|
277
|
-
});
|
|
278
|
-
});
|
|
279
|
-
// ── getTranslations ─────────────────────────────────────────────────────────
|
|
280
|
-
describe("getTranslations", () => {
|
|
281
|
-
it("minimal args (project only) accepted by API", async () => {
|
|
282
|
-
const queryMock = vi.fn().mockResolvedValue(STUBS.getAllTranslations);
|
|
283
|
-
const client = createMockClient({ mcp: { getAllTranslations: { query: queryMock } } });
|
|
284
|
-
await getTranslations.execute(client, { project: "org/proj" });
|
|
285
|
-
const apiArgs = queryMock.mock.calls[0][0];
|
|
286
|
-
const result = getTranslationsInput.safeParse(apiArgs);
|
|
287
|
-
expect(result.success, result.error?.message).toBe(true);
|
|
288
|
-
});
|
|
289
|
-
it("all filters accepted by API", async () => {
|
|
290
|
-
const queryMock = vi.fn().mockResolvedValue(STUBS.getAllTranslations);
|
|
291
|
-
const client = createMockClient({ mcp: { getAllTranslations: { query: queryMock } } });
|
|
292
|
-
await getTranslations.execute(client, {
|
|
293
|
-
project: "org/proj",
|
|
294
|
-
search: "login",
|
|
295
|
-
languages: ["tr", "de"],
|
|
296
|
-
namespaces: ["auth"],
|
|
297
|
-
keys: ["auth.login.title"],
|
|
298
|
-
status: "missing",
|
|
299
|
-
limit: 50,
|
|
300
|
-
});
|
|
301
|
-
const apiArgs = queryMock.mock.calls[0][0];
|
|
302
|
-
const result = getTranslationsInput.safeParse(apiArgs);
|
|
303
|
-
expect(result.success, result.error?.message).toBe(true);
|
|
304
|
-
});
|
|
305
|
-
});
|
|
306
|
-
// ── getProject ──────────────────────────────────────────────────────────────
|
|
307
|
-
describe("getProject", () => {
|
|
308
|
-
it("API schema accepts what tool sends", async () => {
|
|
309
|
-
const queryMock = vi.fn().mockResolvedValue(STUBS.getProject);
|
|
310
|
-
const client = createMockClient({ mcp: { getProject: { query: queryMock } } });
|
|
311
|
-
await getProject.execute(client, { project: "org/proj" });
|
|
312
|
-
const apiArgs = queryMock.mock.calls[0][0];
|
|
313
|
-
const result = getProjectInput.safeParse(apiArgs);
|
|
314
|
-
expect(result.success, result.error?.message).toBe(true);
|
|
315
|
-
});
|
|
316
|
-
});
|
|
317
|
-
// ── getPendingChanges ────────────────────────────────────────────────────────
|
|
318
|
-
describe("getPendingChanges", () => {
|
|
319
|
-
it("API schema accepts what tool sends", async () => {
|
|
320
|
-
const queryMock = vi.fn().mockResolvedValue(STUBS.getPendingChanges);
|
|
321
|
-
const client = createMockClient({ mcp: { getPendingChanges: { query: queryMock } } });
|
|
322
|
-
await getPendingChanges.execute(client, { project: "org/proj" });
|
|
323
|
-
const apiArgs = queryMock.mock.calls[0][0];
|
|
324
|
-
const result = getPendingChangesInput.safeParse(apiArgs);
|
|
325
|
-
expect(result.success, result.error?.message).toBe(true);
|
|
326
|
-
});
|
|
327
|
-
});
|
|
328
|
-
// ── proposeLanguages ────────────────────────────────────────────────────────
|
|
329
|
-
describe("proposeLanguages", () => {
|
|
330
|
-
it("API schema accepts normalized language codes", async () => {
|
|
331
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.addLanguages);
|
|
332
|
-
const client = createMockClient({ mcp: { addLanguages: { mutate: mutateMock } } });
|
|
333
|
-
await proposeLanguages.execute(client, {
|
|
334
|
-
project: "org/proj",
|
|
335
|
-
languages: [{ languageCode: "FR" }, { languageCode: "DE", status: "draft" }],
|
|
336
|
-
});
|
|
337
|
-
const apiArgs = mutateMock.mock.calls[0][0];
|
|
338
|
-
expect(apiArgs.languages[0].languageCode).toBe("fr");
|
|
339
|
-
expect(apiArgs.languages[1].languageCode).toBe("de");
|
|
340
|
-
const result = addLanguagesInput.safeParse(apiArgs);
|
|
341
|
-
expect(result.success, result.error?.message).toBe(true);
|
|
342
|
-
});
|
|
343
|
-
});
|
|
344
|
-
// ── proposeLanguageEdits ────────────────────────────────────────────────────
|
|
345
|
-
describe("proposeLanguageEdits", () => {
|
|
346
|
-
it("API schema accepts renamed fields (edits→updates, newStatus→status)", async () => {
|
|
347
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.updateLanguages);
|
|
348
|
-
const client = createMockClient({ mcp: { updateLanguages: { mutate: mutateMock } } });
|
|
349
|
-
await proposeLanguageEdits.execute(client, {
|
|
350
|
-
project: "org/proj",
|
|
351
|
-
edits: [{ languageCode: "FR", newStatus: "archived" }],
|
|
352
|
-
});
|
|
353
|
-
const apiArgs = mutateMock.mock.calls[0][0];
|
|
354
|
-
// Tool maps edits → updates and newStatus → status
|
|
355
|
-
expect(apiArgs.updates[0].status).toBe("archived");
|
|
356
|
-
expect(apiArgs.updates[0].languageCode).toBe("fr");
|
|
357
|
-
const result = updateLanguagesInput.safeParse(apiArgs);
|
|
358
|
-
expect(result.success, result.error?.message).toBe(true);
|
|
359
|
-
});
|
|
360
|
-
it("multiple language edits accepted by API", async () => {
|
|
361
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.updateLanguages);
|
|
362
|
-
const client = createMockClient({ mcp: { updateLanguages: { mutate: mutateMock } } });
|
|
363
|
-
await proposeLanguageEdits.execute(client, {
|
|
364
|
-
project: "org/proj",
|
|
365
|
-
edits: [
|
|
366
|
-
{ languageCode: "FR", newStatus: "archived" },
|
|
367
|
-
{ languageCode: "DE", newStatus: "active" },
|
|
368
|
-
{ languageCode: "JA", newStatus: "draft" },
|
|
369
|
-
],
|
|
370
|
-
});
|
|
371
|
-
const apiArgs = mutateMock.mock.calls[0][0];
|
|
372
|
-
expect(apiArgs.updates).toHaveLength(3);
|
|
373
|
-
expect(apiArgs.updates[0].languageCode).toBe("fr");
|
|
374
|
-
expect(apiArgs.updates[1].languageCode).toBe("de");
|
|
375
|
-
expect(apiArgs.updates[2].languageCode).toBe("ja");
|
|
376
|
-
const result = updateLanguagesInput.safeParse(apiArgs);
|
|
377
|
-
expect(result.success, result.error?.message).toBe(true);
|
|
378
|
-
});
|
|
379
|
-
});
|
|
380
|
-
// ── getSyncs ────────────────────────────────────────────────────────────────
|
|
381
|
-
describe("getSyncs", () => {
|
|
382
|
-
it("API schema accepts what tool sends with all filters", async () => {
|
|
383
|
-
const queryMock = vi.fn().mockResolvedValue(STUBS.getSyncs);
|
|
384
|
-
const client = createMockClient({ mcp: { getSyncs: { query: queryMock } } });
|
|
385
|
-
await getSyncs.execute(client, {
|
|
386
|
-
project: "org/proj",
|
|
387
|
-
limit: 10,
|
|
388
|
-
status: "completed",
|
|
389
|
-
type: "source_sync",
|
|
390
|
-
});
|
|
391
|
-
const apiArgs = queryMock.mock.calls[0][0];
|
|
392
|
-
const result = getSyncsInput.safeParse(apiArgs);
|
|
393
|
-
expect(result.success, result.error?.message).toBe(true);
|
|
394
|
-
});
|
|
395
|
-
it("minimal args (project only) accepted by API", async () => {
|
|
396
|
-
const queryMock = vi.fn().mockResolvedValue(STUBS.getSyncs);
|
|
397
|
-
const client = createMockClient({ mcp: { getSyncs: { query: queryMock } } });
|
|
398
|
-
await getSyncs.execute(client, { project: "org/proj" });
|
|
399
|
-
const apiArgs = queryMock.mock.calls[0][0];
|
|
400
|
-
const result = getSyncsInput.safeParse(apiArgs);
|
|
401
|
-
expect(result.success, result.error?.message).toBe(true);
|
|
402
|
-
});
|
|
403
|
-
});
|
|
404
|
-
// ── getSync ─────────────────────────────────────────────────────────────────
|
|
405
|
-
describe("getSync", () => {
|
|
406
|
-
it("API schema accepts what tool sends", async () => {
|
|
407
|
-
const queryMock = vi.fn().mockResolvedValue(STUBS.getSync);
|
|
408
|
-
const client = createMockClient({ mcp: { getSync: { query: queryMock } } });
|
|
409
|
-
await getSync.execute(client, { syncId: "sync-123" });
|
|
410
|
-
const apiArgs = queryMock.mock.calls[0][0];
|
|
411
|
-
const result = getSyncInput.safeParse(apiArgs);
|
|
412
|
-
expect(result.success, result.error?.message).toBe(true);
|
|
413
|
-
});
|
|
414
|
-
});
|
|
415
|
-
});
|
|
416
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
417
|
-
// 2. REVERSE CONTRACT: LLM-plausible args → tool acceptance
|
|
418
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
419
|
-
describe("reverse contract: LLM-plausible args → tool acceptance", () => {
|
|
420
|
-
// ── createKeys ──────────────────────────────────────────────────────────────
|
|
421
|
-
describe("createKeys — LLM perspective", () => {
|
|
422
|
-
it("accepts minimal args (only required fields from inputSchema)", async () => {
|
|
423
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.createKeys);
|
|
424
|
-
const client = createMockClient({ mcp: { createKeys: { mutate: mutateMock } } });
|
|
425
|
-
const result = await createKeys.execute(client, {
|
|
426
|
-
project: "org/proj",
|
|
427
|
-
k: [{ n: "hello" }],
|
|
428
|
-
});
|
|
429
|
-
expect(result.isError).toBeUndefined();
|
|
430
|
-
});
|
|
431
|
-
it("accepts full args with all optional fields", async () => {
|
|
432
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.createKeys);
|
|
433
|
-
const client = createMockClient({ mcp: { createKeys: { mutate: mutateMock } } });
|
|
434
|
-
const result = await createKeys.execute(client, {
|
|
435
|
-
project: "org/proj",
|
|
436
|
-
k: [{
|
|
437
|
-
n: "auth.login.title",
|
|
438
|
-
ns: "auth",
|
|
439
|
-
v: "Log In",
|
|
440
|
-
t: { tr: "Giriş Yap", de: "Anmelden" },
|
|
441
|
-
nc: {
|
|
442
|
-
description: "Auth namespace",
|
|
443
|
-
team: "auth",
|
|
444
|
-
domain: "auth",
|
|
445
|
-
aiPrompt: "Formal tone",
|
|
446
|
-
tags: ["user-facing"],
|
|
447
|
-
},
|
|
448
|
-
}],
|
|
449
|
-
});
|
|
450
|
-
expect(result.isError).toBeUndefined();
|
|
451
|
-
});
|
|
452
|
-
it("accepts multiple keys in single call", async () => {
|
|
453
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.createKeys);
|
|
454
|
-
const client = createMockClient({ mcp: { createKeys: { mutate: mutateMock } } });
|
|
455
|
-
const result = await createKeys.execute(client, {
|
|
456
|
-
project: "org/proj",
|
|
457
|
-
k: [
|
|
458
|
-
{ n: "btn.submit", v: "Submit" },
|
|
459
|
-
{ n: "btn.cancel", v: "Cancel" },
|
|
460
|
-
{ n: "btn.save", ns: "common", v: "Save" },
|
|
461
|
-
],
|
|
462
|
-
});
|
|
463
|
-
expect(result.isError).toBeUndefined();
|
|
464
|
-
});
|
|
465
|
-
it("LLM might send uppercase language codes (common mistake) — normalized correctly", async () => {
|
|
466
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.createKeys);
|
|
467
|
-
const client = createMockClient({ mcp: { createKeys: { mutate: mutateMock } } });
|
|
468
|
-
const result = await createKeys.execute(client, {
|
|
469
|
-
project: "org/proj",
|
|
470
|
-
k: [{ n: "test", t: { TR: "Merhaba", DE: "Hallo", FR: "Bonjour" } }],
|
|
471
|
-
});
|
|
472
|
-
expect(result.isError).toBeUndefined();
|
|
473
|
-
// AND verify the API would accept the normalized version
|
|
474
|
-
const apiArgs = mutateMock.mock.calls[0][0];
|
|
475
|
-
expect(Object.keys(apiArgs.k[0].t)).toEqual(["tr", "de", "fr"]);
|
|
476
|
-
});
|
|
477
|
-
it("accepts BCP 47 locale codes in translations", async () => {
|
|
478
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.createKeys);
|
|
479
|
-
const client = createMockClient({ mcp: { createKeys: { mutate: mutateMock } } });
|
|
480
|
-
const result = await createKeys.execute(client, {
|
|
481
|
-
project: "org/proj",
|
|
482
|
-
k: [{ n: "greeting", t: { "zh-Hans": "你好", "pt-BR": "Olá" } }],
|
|
483
|
-
});
|
|
484
|
-
expect(result.isError).toBeUndefined();
|
|
485
|
-
});
|
|
486
|
-
});
|
|
487
|
-
// ── updateKeys ──────────────────────────────────────────────────────────────
|
|
488
|
-
describe("updateKeys — LLM perspective", () => {
|
|
489
|
-
it("accepts minimal single update", async () => {
|
|
490
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.updateKeys);
|
|
491
|
-
const client = createMockClient({ mcp: { updateKeys: { mutate: mutateMock } } });
|
|
492
|
-
const result = await updateKeys.execute(client, {
|
|
493
|
-
project: "org/proj",
|
|
494
|
-
t: [{ id: "550e8400-e29b-41d4-a716-446655440000", l: "tr", t: "Merhaba" }],
|
|
495
|
-
});
|
|
496
|
-
expect(result.isError).toBeUndefined();
|
|
497
|
-
});
|
|
498
|
-
it("accepts batch update with source flag and status", async () => {
|
|
499
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.updateKeys);
|
|
500
|
-
const client = createMockClient({ mcp: { updateKeys: { mutate: mutateMock } } });
|
|
501
|
-
const result = await updateKeys.execute(client, {
|
|
502
|
-
project: "org/proj",
|
|
503
|
-
t: [
|
|
504
|
-
{ id: "550e8400-e29b-41d4-a716-446655440000", l: "en", t: "Updated", s: true },
|
|
505
|
-
{ id: "550e8400-e29b-41d4-a716-446655440001", l: "tr", t: "Güncellendi", st: "published" },
|
|
506
|
-
],
|
|
507
|
-
});
|
|
508
|
-
expect(result.isError).toBeUndefined();
|
|
509
|
-
});
|
|
510
|
-
it("LLM might send uppercase language (common mistake) — normalized correctly", async () => {
|
|
511
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.updateKeys);
|
|
512
|
-
const client = createMockClient({ mcp: { updateKeys: { mutate: mutateMock } } });
|
|
513
|
-
const result = await updateKeys.execute(client, {
|
|
514
|
-
project: "org/proj",
|
|
515
|
-
t: [{ id: "550e8400-e29b-41d4-a716-446655440000", l: "TR", t: "Merhaba" }],
|
|
516
|
-
});
|
|
517
|
-
expect(result.isError).toBeUndefined();
|
|
518
|
-
expect(mutateMock.mock.calls[0][0].t[0].l).toBe("tr");
|
|
519
|
-
});
|
|
520
|
-
it("accepts non-UUID id (API uses plain string for id)", async () => {
|
|
521
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.updateKeys);
|
|
522
|
-
const client = createMockClient({ mcp: { updateKeys: { mutate: mutateMock } } });
|
|
523
|
-
// The API schema for updateKeys uses z.string() (not uuid()) for id
|
|
524
|
-
const result = await updateKeys.execute(client, {
|
|
525
|
-
project: "org/proj",
|
|
526
|
-
t: [{ id: "some-plain-string-id", l: "tr", t: "text" }],
|
|
527
|
-
});
|
|
528
|
-
expect(result.isError).toBeUndefined();
|
|
529
|
-
});
|
|
530
|
-
});
|
|
531
|
-
// ── deleteKeys ──────────────────────────────────────────────────────────────
|
|
532
|
-
describe("deleteKeys — LLM perspective", () => {
|
|
533
|
-
it("accepts single UUID", async () => {
|
|
534
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.deleteKeys);
|
|
535
|
-
const client = createMockClient({ mcp: { deleteKeys: { mutate: mutateMock } } });
|
|
536
|
-
const result = await deleteKeys.execute(client, {
|
|
537
|
-
project: "org/proj",
|
|
538
|
-
keyIds: ["550e8400-e29b-41d4-a716-446655440000"],
|
|
539
|
-
});
|
|
540
|
-
expect(result.isError).toBeUndefined();
|
|
541
|
-
});
|
|
542
|
-
it("accepts batch UUIDs", async () => {
|
|
543
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.deleteKeys);
|
|
544
|
-
const client = createMockClient({ mcp: { deleteKeys: { mutate: mutateMock } } });
|
|
545
|
-
const uuids = Array.from({ length: 5 }, (_, i) => `550e8400-e29b-41d4-a716-${String(i).padStart(12, "0")}`);
|
|
546
|
-
const result = await deleteKeys.execute(client, {
|
|
547
|
-
project: "org/proj",
|
|
548
|
-
keyIds: uuids,
|
|
549
|
-
});
|
|
550
|
-
expect(result.isError).toBeUndefined();
|
|
551
|
-
});
|
|
552
|
-
});
|
|
553
|
-
// ── listKeys ────────────────────────────────────────────────────────────────
|
|
554
|
-
describe("listKeys — LLM perspective", () => {
|
|
555
|
-
it("accepts project-only (minimal)", async () => {
|
|
556
|
-
const queryMock = vi.fn().mockResolvedValue(STUBS.listKeys);
|
|
557
|
-
const client = createMockClient({ mcp: { listKeys: { query: queryMock } } });
|
|
558
|
-
const result = await listKeys.execute(client, { project: "org/proj" });
|
|
559
|
-
expect(result.isError).toBeUndefined();
|
|
560
|
-
});
|
|
561
|
-
it("accepts all filter combinations", async () => {
|
|
562
|
-
const queryMock = vi.fn().mockResolvedValue(STUBS.listKeys);
|
|
563
|
-
const client = createMockClient({ mcp: { listKeys: { query: queryMock } } });
|
|
564
|
-
const result = await listKeys.execute(client, {
|
|
565
|
-
project: "org/proj",
|
|
566
|
-
search: ["login", "signup"],
|
|
567
|
-
namespaces: ["auth", "common"],
|
|
568
|
-
missingLanguage: "tr",
|
|
569
|
-
fields: ["id", "sourceText", "translatedLanguages"],
|
|
570
|
-
page: 2,
|
|
571
|
-
limit: 50,
|
|
572
|
-
});
|
|
573
|
-
expect(result.isError).toBeUndefined();
|
|
574
|
-
});
|
|
575
|
-
it("accepts translatedLanguageCount field", async () => {
|
|
576
|
-
const queryMock = vi.fn().mockResolvedValue(STUBS.listKeys);
|
|
577
|
-
const client = createMockClient({ mcp: { listKeys: { query: queryMock } } });
|
|
578
|
-
const result = await listKeys.execute(client, {
|
|
579
|
-
project: "org/proj",
|
|
580
|
-
fields: ["id", "translatedLanguageCount"],
|
|
581
|
-
});
|
|
582
|
-
expect(result.isError).toBeUndefined();
|
|
583
|
-
});
|
|
584
|
-
});
|
|
585
|
-
// ── getTranslations ─────────────────────────────────────────────────────────
|
|
586
|
-
describe("getTranslations — LLM perspective", () => {
|
|
587
|
-
it("accepts status filter with languages (correct usage)", async () => {
|
|
588
|
-
const queryMock = vi.fn().mockResolvedValue(STUBS.getAllTranslations);
|
|
589
|
-
const client = createMockClient({ mcp: { getAllTranslations: { query: queryMock } } });
|
|
590
|
-
const result = await getTranslations.execute(client, {
|
|
591
|
-
project: "org/proj",
|
|
592
|
-
languages: ["tr"],
|
|
593
|
-
status: "missing",
|
|
594
|
-
limit: 50,
|
|
595
|
-
});
|
|
596
|
-
expect(result.isError).toBeUndefined();
|
|
597
|
-
});
|
|
598
|
-
it("accepts multi-term search array", async () => {
|
|
599
|
-
const queryMock = vi.fn().mockResolvedValue(STUBS.getAllTranslations);
|
|
600
|
-
const client = createMockClient({ mcp: { getAllTranslations: { query: queryMock } } });
|
|
601
|
-
const result = await getTranslations.execute(client, {
|
|
602
|
-
project: "org/proj",
|
|
603
|
-
search: ["login", "signup", "forgot_password"],
|
|
604
|
-
});
|
|
605
|
-
expect(result.isError).toBeUndefined();
|
|
606
|
-
});
|
|
607
|
-
it("accepts specific keys lookup", async () => {
|
|
608
|
-
const queryMock = vi.fn().mockResolvedValue(STUBS.getAllTranslations);
|
|
609
|
-
const client = createMockClient({ mcp: { getAllTranslations: { query: queryMock } } });
|
|
610
|
-
const result = await getTranslations.execute(client, {
|
|
611
|
-
project: "org/proj",
|
|
612
|
-
keys: ["auth.login.title", "auth.login.button"],
|
|
613
|
-
});
|
|
614
|
-
expect(result.isError).toBeUndefined();
|
|
615
|
-
});
|
|
616
|
-
it("accepts all status values", async () => {
|
|
617
|
-
const queryMock = vi.fn().mockResolvedValue(STUBS.getAllTranslations);
|
|
618
|
-
const client = createMockClient({ mcp: { getAllTranslations: { query: queryMock } } });
|
|
619
|
-
for (const status of ["missing", "draft", "published", "all"]) {
|
|
620
|
-
queryMock.mockClear();
|
|
621
|
-
const result = await getTranslations.execute(client, {
|
|
622
|
-
project: "org/proj",
|
|
623
|
-
languages: ["tr"],
|
|
624
|
-
status,
|
|
625
|
-
});
|
|
626
|
-
expect(result.isError, `status="${status}" should be accepted`).toBeUndefined();
|
|
627
|
-
}
|
|
628
|
-
});
|
|
629
|
-
});
|
|
630
|
-
// ── publishTranslations ─────────────────────────────────────────────────────
|
|
631
|
-
describe("publishTranslations — LLM perspective", () => {
|
|
632
|
-
it("accepts project-only for full publish", async () => {
|
|
633
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.publishTranslations);
|
|
634
|
-
const client = createMockClient({ mcp: { publishTranslations: { mutate: mutateMock } } });
|
|
635
|
-
const result = await publishTranslations.execute(client, { project: "org/proj" });
|
|
636
|
-
expect(result.isError).toBeUndefined();
|
|
637
|
-
});
|
|
638
|
-
it("accepts selective publish", async () => {
|
|
639
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.publishTranslations);
|
|
640
|
-
const client = createMockClient({ mcp: { publishTranslations: { mutate: mutateMock } } });
|
|
641
|
-
const result = await publishTranslations.execute(client, {
|
|
642
|
-
project: "org/proj",
|
|
643
|
-
translations: [
|
|
644
|
-
{ keyId: "550e8400-e29b-41d4-a716-446655440000", languageCode: "tr" },
|
|
645
|
-
{ keyId: "550e8400-e29b-41d4-a716-446655440001", languageCode: "de" },
|
|
646
|
-
],
|
|
647
|
-
});
|
|
648
|
-
expect(result.isError).toBeUndefined();
|
|
649
|
-
});
|
|
650
|
-
});
|
|
651
|
-
// ── proposeLanguages ────────────────────────────────────────────────────────
|
|
652
|
-
describe("proposeLanguages — LLM perspective", () => {
|
|
653
|
-
it("accepts multiple languages", async () => {
|
|
654
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.addLanguages);
|
|
655
|
-
const client = createMockClient({ mcp: { addLanguages: { mutate: mutateMock } } });
|
|
656
|
-
const result = await proposeLanguages.execute(client, {
|
|
657
|
-
project: "org/proj",
|
|
658
|
-
languages: [
|
|
659
|
-
{ languageCode: "fr" },
|
|
660
|
-
{ languageCode: "de", status: "draft" },
|
|
661
|
-
{ languageCode: "ja" },
|
|
662
|
-
],
|
|
663
|
-
});
|
|
664
|
-
expect(result.isError).toBeUndefined();
|
|
665
|
-
});
|
|
666
|
-
it("accepts BCP 47 locale codes", async () => {
|
|
667
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.addLanguages);
|
|
668
|
-
const client = createMockClient({ mcp: { addLanguages: { mutate: mutateMock } } });
|
|
669
|
-
const result = await proposeLanguages.execute(client, {
|
|
670
|
-
project: "org/proj",
|
|
671
|
-
languages: [
|
|
672
|
-
{ languageCode: "zh-Hans" },
|
|
673
|
-
{ languageCode: "pt-BR" },
|
|
674
|
-
],
|
|
675
|
-
});
|
|
676
|
-
expect(result.isError).toBeUndefined();
|
|
677
|
-
});
|
|
678
|
-
});
|
|
679
|
-
// ── proposeLanguageEdits ────────────────────────────────────────────────────
|
|
680
|
-
describe("proposeLanguageEdits — LLM perspective", () => {
|
|
681
|
-
it("accepts status changes", async () => {
|
|
682
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.updateLanguages);
|
|
683
|
-
const client = createMockClient({ mcp: { updateLanguages: { mutate: mutateMock } } });
|
|
684
|
-
const result = await proposeLanguageEdits.execute(client, {
|
|
685
|
-
project: "org/proj",
|
|
686
|
-
edits: [
|
|
687
|
-
{ languageCode: "fr", newStatus: "archived" },
|
|
688
|
-
{ languageCode: "de", newStatus: "active" },
|
|
689
|
-
],
|
|
690
|
-
});
|
|
691
|
-
expect(result.isError).toBeUndefined();
|
|
692
|
-
});
|
|
693
|
-
it("accepts all valid status values", async () => {
|
|
694
|
-
const mutateMock = vi.fn().mockResolvedValue(STUBS.updateLanguages);
|
|
695
|
-
const client = createMockClient({ mcp: { updateLanguages: { mutate: mutateMock } } });
|
|
696
|
-
for (const newStatus of ["active", "draft", "archived"]) {
|
|
697
|
-
mutateMock.mockClear();
|
|
698
|
-
const result = await proposeLanguageEdits.execute(client, {
|
|
699
|
-
project: "org/proj",
|
|
700
|
-
edits: [{ languageCode: "fr", newStatus }],
|
|
701
|
-
});
|
|
702
|
-
expect(result.isError, `newStatus="${newStatus}" should be accepted`).toBeUndefined();
|
|
703
|
-
}
|
|
704
|
-
});
|
|
705
|
-
});
|
|
706
|
-
// ── getSyncs ────────────────────────────────────────────────────────────────
|
|
707
|
-
describe("getSyncs — LLM perspective", () => {
|
|
708
|
-
it("accepts all filter combinations", async () => {
|
|
709
|
-
const queryMock = vi.fn().mockResolvedValue(STUBS.getSyncs);
|
|
710
|
-
const client = createMockClient({ mcp: { getSyncs: { query: queryMock } } });
|
|
711
|
-
const result = await getSyncs.execute(client, {
|
|
712
|
-
project: "org/proj",
|
|
713
|
-
limit: 10,
|
|
714
|
-
status: "failed",
|
|
715
|
-
type: "cdn_upload",
|
|
716
|
-
});
|
|
717
|
-
expect(result.isError).toBeUndefined();
|
|
718
|
-
});
|
|
719
|
-
it("accepts project-only (minimal)", async () => {
|
|
720
|
-
const queryMock = vi.fn().mockResolvedValue(STUBS.getSyncs);
|
|
721
|
-
const client = createMockClient({ mcp: { getSyncs: { query: queryMock } } });
|
|
722
|
-
const result = await getSyncs.execute(client, { project: "org/proj" });
|
|
723
|
-
expect(result.isError).toBeUndefined();
|
|
724
|
-
});
|
|
725
|
-
});
|
|
726
|
-
// ── getSync ─────────────────────────────────────────────────────────────────
|
|
727
|
-
describe("getSync — LLM perspective", () => {
|
|
728
|
-
it("accepts syncId", async () => {
|
|
729
|
-
const queryMock = vi.fn().mockResolvedValue(STUBS.getSync);
|
|
730
|
-
const client = createMockClient({ mcp: { getSync: { query: queryMock } } });
|
|
731
|
-
const result = await getSync.execute(client, { syncId: "abc-123" });
|
|
732
|
-
expect(result.isError).toBeUndefined();
|
|
733
|
-
});
|
|
734
|
-
});
|
|
735
|
-
// ── listProjects ────────────────────────────────────────────────────────────
|
|
736
|
-
describe("listProjects — LLM perspective", () => {
|
|
737
|
-
it("accepts empty args", async () => {
|
|
738
|
-
const queryMock = vi.fn().mockResolvedValue(STUBS.listProjects);
|
|
739
|
-
const client = createMockClient({ mcp: { listProjects: { query: queryMock } } });
|
|
740
|
-
const result = await listProjects.execute(client, {});
|
|
741
|
-
expect(result.isError).toBeUndefined();
|
|
742
|
-
});
|
|
743
|
-
it("accepts args with no recognized properties (extra fields ignored)", async () => {
|
|
744
|
-
const queryMock = vi.fn().mockResolvedValue(STUBS.listProjects);
|
|
745
|
-
const client = createMockClient({ mcp: { listProjects: { query: queryMock } } });
|
|
746
|
-
// LLMs sometimes add extra fields — should be harmless (stripped by Zod)
|
|
747
|
-
const result = await listProjects.execute(client, {});
|
|
748
|
-
expect(result.isError).toBeUndefined();
|
|
749
|
-
});
|
|
750
|
-
});
|
|
751
|
-
});
|
|
752
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
753
|
-
// 3. NEGATIVE CONTRACT: invalid args MUST be rejected
|
|
754
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
755
|
-
describe("negative contract: invalid args rejected", () => {
|
|
756
|
-
it("rejects project without slash", async () => {
|
|
757
|
-
const client = createMockClient();
|
|
758
|
-
const result = await createKeys.execute(client, {
|
|
759
|
-
project: "no-slash",
|
|
760
|
-
k: [{ n: "test" }],
|
|
761
|
-
});
|
|
762
|
-
expect(result.isError).toBe(true);
|
|
763
|
-
});
|
|
764
|
-
it("rejects project as empty string", async () => {
|
|
765
|
-
const client = createMockClient();
|
|
766
|
-
const result = await createKeys.execute(client, {
|
|
767
|
-
project: "",
|
|
768
|
-
k: [{ n: "test" }],
|
|
769
|
-
});
|
|
770
|
-
expect(result.isError).toBe(true);
|
|
771
|
-
});
|
|
772
|
-
it("rejects empty key name in createKeys", async () => {
|
|
773
|
-
const client = createMockClient();
|
|
774
|
-
const result = await createKeys.execute(client, {
|
|
775
|
-
project: "org/proj",
|
|
776
|
-
k: [{ n: "" }],
|
|
777
|
-
});
|
|
778
|
-
expect(result.isError).toBe(true);
|
|
779
|
-
});
|
|
780
|
-
it("rejects missing k array in createKeys", async () => {
|
|
781
|
-
const client = createMockClient();
|
|
782
|
-
const result = await createKeys.execute(client, { project: "org/proj" });
|
|
783
|
-
expect(result.isError).toBe(true);
|
|
784
|
-
});
|
|
785
|
-
it("rejects empty k array in createKeys", async () => {
|
|
786
|
-
const client = createMockClient();
|
|
787
|
-
const result = await createKeys.execute(client, {
|
|
788
|
-
project: "org/proj",
|
|
789
|
-
k: [],
|
|
790
|
-
});
|
|
791
|
-
expect(result.isError).toBe(true);
|
|
792
|
-
});
|
|
793
|
-
it("rejects non-UUID keyIds in deleteKeys", async () => {
|
|
794
|
-
const client = createMockClient();
|
|
795
|
-
const result = await deleteKeys.execute(client, {
|
|
796
|
-
project: "org/proj",
|
|
797
|
-
keyIds: ["not-a-uuid"],
|
|
798
|
-
});
|
|
799
|
-
expect(result.isError).toBe(true);
|
|
800
|
-
});
|
|
801
|
-
it("rejects empty keyIds array in deleteKeys", async () => {
|
|
802
|
-
const client = createMockClient();
|
|
803
|
-
const result = await deleteKeys.execute(client, {
|
|
804
|
-
project: "org/proj",
|
|
805
|
-
keyIds: [],
|
|
806
|
-
});
|
|
807
|
-
expect(result.isError).toBe(true);
|
|
808
|
-
});
|
|
809
|
-
it("rejects > 100 keyIds in deleteKeys", async () => {
|
|
810
|
-
const client = createMockClient();
|
|
811
|
-
const uuids = Array.from({ length: 101 }, (_, i) => `550e8400-e29b-41d4-a716-${String(i).padStart(12, "0")}`);
|
|
812
|
-
const result = await deleteKeys.execute(client, {
|
|
813
|
-
project: "org/proj",
|
|
814
|
-
keyIds: uuids,
|
|
815
|
-
});
|
|
816
|
-
expect(result.isError).toBe(true);
|
|
817
|
-
});
|
|
818
|
-
it("rejects limit > 100 in listKeys", async () => {
|
|
819
|
-
const client = createMockClient();
|
|
820
|
-
const result = await listKeys.execute(client, {
|
|
821
|
-
project: "org/proj",
|
|
822
|
-
limit: 101,
|
|
823
|
-
});
|
|
824
|
-
expect(result.isError).toBe(true);
|
|
825
|
-
});
|
|
826
|
-
it("rejects limit > 200 in getTranslations", async () => {
|
|
827
|
-
const client = createMockClient();
|
|
828
|
-
const result = await getTranslations.execute(client, {
|
|
829
|
-
project: "org/proj",
|
|
830
|
-
limit: 201,
|
|
831
|
-
});
|
|
832
|
-
expect(result.isError).toBe(true);
|
|
833
|
-
});
|
|
834
|
-
it("rejects invalid status in getTranslations", async () => {
|
|
835
|
-
const client = createMockClient();
|
|
836
|
-
const result = await getTranslations.execute(client, {
|
|
837
|
-
project: "org/proj",
|
|
838
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
839
|
-
status: "invalid",
|
|
840
|
-
});
|
|
841
|
-
expect(result.isError).toBe(true);
|
|
842
|
-
});
|
|
843
|
-
it("rejects empty t array in updateKeys", async () => {
|
|
844
|
-
const client = createMockClient();
|
|
845
|
-
const result = await updateKeys.execute(client, {
|
|
846
|
-
project: "org/proj",
|
|
847
|
-
t: [],
|
|
848
|
-
});
|
|
849
|
-
expect(result.isError).toBe(true);
|
|
850
|
-
});
|
|
851
|
-
it("rejects missing t field in updateKeys item", async () => {
|
|
852
|
-
const client = createMockClient();
|
|
853
|
-
const result = await updateKeys.execute(client, {
|
|
854
|
-
project: "org/proj",
|
|
855
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
856
|
-
t: [{ id: "550e8400-e29b-41d4-a716-446655440000", l: "tr" }],
|
|
857
|
-
});
|
|
858
|
-
expect(result.isError).toBe(true);
|
|
859
|
-
});
|
|
860
|
-
it("rejects empty languages array in proposeLanguages", async () => {
|
|
861
|
-
const client = createMockClient();
|
|
862
|
-
const result = await proposeLanguages.execute(client, {
|
|
863
|
-
project: "org/proj",
|
|
864
|
-
languages: [],
|
|
865
|
-
});
|
|
866
|
-
expect(result.isError).toBe(true);
|
|
867
|
-
});
|
|
868
|
-
it("rejects invalid status in proposeLanguageEdits", async () => {
|
|
869
|
-
const client = createMockClient();
|
|
870
|
-
const result = await proposeLanguageEdits.execute(client, {
|
|
871
|
-
project: "org/proj",
|
|
872
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
873
|
-
edits: [{ languageCode: "fr", newStatus: "unknown-status" }],
|
|
874
|
-
});
|
|
875
|
-
expect(result.isError).toBe(true);
|
|
876
|
-
});
|
|
877
|
-
it("rejects empty edits array in proposeLanguageEdits", async () => {
|
|
878
|
-
const client = createMockClient();
|
|
879
|
-
const result = await proposeLanguageEdits.execute(client, {
|
|
880
|
-
project: "org/proj",
|
|
881
|
-
edits: [],
|
|
882
|
-
});
|
|
883
|
-
expect(result.isError).toBe(true);
|
|
884
|
-
});
|
|
885
|
-
it("rejects limit > 50 in getSyncs", async () => {
|
|
886
|
-
const client = createMockClient();
|
|
887
|
-
const result = await getSyncs.execute(client, {
|
|
888
|
-
project: "org/proj",
|
|
889
|
-
limit: 51,
|
|
890
|
-
});
|
|
891
|
-
expect(result.isError).toBe(true);
|
|
892
|
-
});
|
|
893
|
-
it("rejects invalid type in getSyncs", async () => {
|
|
894
|
-
const client = createMockClient();
|
|
895
|
-
const result = await getSyncs.execute(client, {
|
|
896
|
-
project: "org/proj",
|
|
897
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
898
|
-
type: "not_a_valid_type",
|
|
899
|
-
});
|
|
900
|
-
expect(result.isError).toBe(true);
|
|
901
|
-
});
|
|
902
|
-
it("rejects page < 1 in listKeys", async () => {
|
|
903
|
-
const client = createMockClient();
|
|
904
|
-
const result = await listKeys.execute(client, {
|
|
905
|
-
project: "org/proj",
|
|
906
|
-
page: 0,
|
|
907
|
-
});
|
|
908
|
-
expect(result.isError).toBe(true);
|
|
909
|
-
});
|
|
910
|
-
it("rejects limit < 1 in getTranslations", async () => {
|
|
911
|
-
const client = createMockClient();
|
|
912
|
-
const result = await getTranslations.execute(client, {
|
|
913
|
-
project: "org/proj",
|
|
914
|
-
limit: 0,
|
|
915
|
-
});
|
|
916
|
-
expect(result.isError).toBe(true);
|
|
917
|
-
});
|
|
918
|
-
});
|
|
919
|
-
//# sourceMappingURL=contract.test.js.map
|