benjivers/jev-mcp Public Public: anyone can read this; nobody can change it.

workbenchthis machine
Files cbaceb505b32e2f74693377ae6d3e69cde6a98b6
372 lines · 14.5 KB · ~4.2k tok Raw History
1
// Contract and regression tests for the tool surface.
2
//
3
// These run the real built server over stdio against a local stand-in for the
4
// TypeSafe API, so each assertion is about what the server actually sends and
5
// returns. Every test named "regression" pins a defect that was confirmed on
6
// the wire before it was fixed.
7
8
import assert from "node:assert/strict";
9
import test from "node:test";
10
import { payload, startMock, withClient } from "./helpers.mjs";
11
12
const TOOLS = ["jev_ask", "jev_check", "jev_classify", "jev_models", "jev_score"];
13
14
// ── Contract ────────────────────────────────────────────────────────────────
15
16
test("advertises exactly the five judgment tools", async () => {
17
  await withClient({ withKey: false }, async (client) => {
18
    const { tools } = await client.listTools();
19
    assert.deepEqual(tools.map((t) => t.name).sort(), TOOLS);
20
  });
21
});
22
23
test("every tool documents itself and declares both input and output schemas", async () => {
24
  await withClient({ withKey: false }, async (client) => {
25
    const { tools } = await client.listTools();
26
    for (const tool of tools) {
27
      assert.ok(tool.description?.length > 40, `${tool.name} needs a real description`);
28
      assert.ok(tool.inputSchema, `${tool.name} has no inputSchema`);
29
      assert.equal(tool.inputSchema.type, "object");
30
      assert.ok(tool.outputSchema, `${tool.name} has no outputSchema, so callers get an opaque string`);
31
    }
32
  });
33
});
34
35
test("classify requires caller-supplied options, so the model cannot invent one", async () => {
36
  await withClient({ withKey: false }, async (client) => {
37
    const { tools } = await client.listTools();
38
    const classify = tools.find((t) => t.name === "jev_classify");
39
    for (const field of ["state", "question", "options"]) {
40
      assert.ok(classify.inputSchema.required.includes(field), `${field} must be required`);
41
    }
42
    const scoreTool = tools.find((t) => t.name === "jev_score");
43
    assert.ok(scoreTool.inputSchema.required.includes("levels"));
44
    assert.equal(scoreTool.inputSchema.properties.levels.minItems, 2);
45
  });
46
});
47
48
test("a missing API key fails loudly and says what to do about it", async () => {
49
  await withClient({ withKey: false }, async (client) => {
50
    const result = await client.callTool({
51
      name: "jev_check",
52
      arguments: { state: "anything", question: "Is this a greeting?" },
53
    });
54
    assert.equal(result.isError, true, "must report an error, not a fabricated answer");
55
    const { error } = payload(result);
56
    assert.equal(error.kind, "no_api_key");
57
    assert.equal(error.retryable, false);
58
    assert.match(error.hint, /environment/i);
59
  });
60
});
61
62
test("falls back to a key file when the environment carries no key", async () => {
63
  // An MCP server inherits the client's environment, not your shell's, so a key
64
  // exported from a shell profile never arrives. The file fallback is the only
65
  // route that reaches every spawn path, which makes it worth pinning here.
66
  const { mkdtempSync, writeFileSync } = await import("node:fs");
67
  const { tmpdir } = await import("node:os");
68
  const { join } = await import("node:path");
69
70
  const keyFile = join(mkdtempSync(join(tmpdir(), "jev-key-")), "key");
71
  writeFileSync(keyFile, "  value-for-the-local-stand-in\n", { mode: 0o600 });
72
73
  const mock = await startMock();
74
  try {
75
    await withClient({ baseUrl: mock.url, withKey: false, env: { JEV_KEY_FILE: keyFile } }, async (client) => {
76
      const result = await client.callTool({
77
        name: "jev_check",
78
        arguments: { state: "s", question: "Is this fine?" },
79
      });
80
      assert.notEqual(result.isError, true, "the key file should satisfy the credential check");
81
      assert.equal(typeof payload(result).probability_yes, "number");
82
      assert.equal(mock.requests.length, 1, "the request must actually reach the API");
83
    });
84
  } finally {
85
    await mock.close();
86
  }
87
});
88
89
// ── Regressions confirmed on the wire ───────────────────────────────────────
90
91
test("regression: an option the caller names 'none' is never overwritten", async () => {
92
  const mock = await startMock();
93
  const callerMeaning = "Already resolved; no action needed.";
94
  try {
95
    await withClient({ baseUrl: mock.url }, async (client) => {
96
      const result = await client.callTool({
97
        name: "jev_classify",
98
        arguments: {
99
          state: "s",
100
          question: "What should happen next?",
101
          options: { none: callerMeaning, escalate: "Send to a human" },
102
        },
103
      });
104
105
      const sent = mock.only().questions.classify.criteria;
106
      assert.equal(sent.none, callerMeaning, "the caller's meaning must reach the model intact");
107
      assert.equal(sent.none_of_these, "None of the other options fits.");
108
109
      const body = payload(result);
110
      assert.equal(body.none_option, "none_of_these", "the caller must be told which key means no-match");
111
    });
112
  } finally {
113
    await mock.close();
114
  }
115
});
116
117
test("regression: add_none false sends exactly the caller's options", async () => {
118
  const mock = await startMock();
119
  try {
120
    await withClient({ baseUrl: mock.url }, async (client) => {
121
      const result = await client.callTool({
122
        name: "jev_classify",
123
        arguments: { state: "s", question: "q", options: { a: "first", b: "second" }, add_none: false },
124
      });
125
      assert.deepEqual(Object.keys(mock.only().questions.classify.criteria).sort(), ["a", "b"]);
126
      assert.equal(payload(result).none_option, null);
127
    });
128
  } finally {
129
    await mock.close();
130
  }
131
});
132
133
test("regression: an empty or oversized option set fails before any request is sent", async () => {
134
  const mock = await startMock();
135
  try {
136
    await withClient({ baseUrl: mock.url }, async (client) => {
137
      const empty = await client.callTool({
138
        name: "jev_classify",
139
        arguments: { state: "s", question: "q", options: {}, add_none: false },
140
      });
141
      assert.equal(empty.isError, true);
142
      assert.match(payload(empty).error.message, /at least one option/);
143
144
      const tooMany = Object.fromEntries(Array.from({ length: 256 }, (_, i) => [`o${i}`, null]));
145
      const oversized = await client.callTool({
146
        name: "jev_classify",
147
        arguments: { state: "s", question: "q", options: tooMany, add_none: false },
148
      });
149
      assert.equal(oversized.isError, true);
150
      assert.match(payload(oversized).error.message, /at most 255 options/);
151
152
      assert.equal(mock.requests.length, 0, "neither request should reach the API");
153
    });
154
  } finally {
155
    await mock.close();
156
  }
157
});
158
159
test("regression: duplicate question ids fail instead of silently dropping a question", async () => {
160
  const mock = await startMock();
161
  try {
162
    await withClient({ baseUrl: mock.url }, async (client) => {
163
      const result = await client.callTool({
164
        name: "jev_ask",
165
        arguments: {
166
          state: "s",
167
          questions: [
168
            { id: "dup", type: "check", question: "first" },
169
            { id: "dup", type: "check", question: "second" },
170
          ],
171
        },
172
      });
173
      assert.equal(result.isError, true);
174
      assert.match(payload(result).error.message, /Duplicate question ids: "dup"/);
175
      assert.equal(mock.requests.length, 0, "no partial request should be sent");
176
    });
177
  } finally {
178
    await mock.close();
179
  }
180
});
181
182
test("regression: jev_ask honours add_none and passes yes/no meanings", async () => {
183
  const mock = await startMock();
184
  try {
185
    await withClient({ baseUrl: mock.url }, async (client) => {
186
      await client.callTool({
187
        name: "jev_ask",
188
        arguments: {
189
          state: "s",
190
          questions: [
191
            { id: "pick", type: "classify", question: "q", options: { a: "d", b: "d" }, add_none: false },
192
            { id: "flag", type: "check", question: "q", yes_means: "It is urgent", no_means: "It can wait" },
193
          ],
194
        },
195
      });
196
197
      const sent = mock.only().questions;
198
      assert.deepEqual(Object.keys(sent.pick.criteria).sort(), ["a", "b"], "add_none:false must be honoured");
199
      assert.deepEqual(sent.flag.criteria, { true: "It is urgent", false: "It can wait" });
200
    });
201
  } finally {
202
    await mock.close();
203
  }
204
});
205
206
test("regression: a verbose SDK log level does not corrupt the JSON-RPC stream", async () => {
207
  const mock = await startMock();
208
  try {
209
    await withClient({ baseUrl: mock.url, env: { TYPESAFE_LOG_LEVEL: "debug" } }, async (client) => {
210
      const result = await client.callTool({
211
        name: "jev_check",
212
        arguments: { state: "s", question: "Is this fine?" },
213
      });
214
      assert.notEqual(result.isError, true, "debug logging must not break the connection");
215
      assert.equal(typeof payload(result).probability_yes, "number");
216
    });
217
  } finally {
218
    await mock.close();
219
  }
220
});
221
222
// ── Behaviour ───────────────────────────────────────────────────────────────
223
224
test("results carry structured content matching the text block", async () => {
225
  const mock = await startMock();
226
  try {
227
    await withClient({ baseUrl: mock.url }, async (client) => {
228
      const result = await client.callTool({
229
        name: "jev_classify",
230
        arguments: { state: "s", question: "q", options: { a: "d", b: "d" } },
231
      });
232
      assert.ok(result.structuredContent, "clients should get typed data, not a JSON string");
233
      assert.deepEqual(result.structuredContent, payload(result));
234
      assert.equal(typeof result.structuredContent.probabilities.a, "number");
235
    });
236
  } finally {
237
    await mock.close();
238
  }
239
});
240
241
test("confidence drives the recommended action", async () => {
242
  const mock = await startMock();
243
  try {
244
    await withClient({ baseUrl: mock.url }, async (client) => {
245
      const args = { state: "s", question: "q", options: { a: "d", b: "d" } };
246
247
      mock.state.confidence = 0.95;
248
      assert.equal(payload(await client.callTool({ name: "jev_classify", arguments: args })).action, "act");
249
250
      mock.state.confidence = 0.6;
251
      assert.equal(payload(await client.callTool({ name: "jev_classify", arguments: args })).action, "review");
252
253
      mock.state.confidence = 0.2;
254
      assert.equal(payload(await client.callTool({ name: "jev_classify", arguments: args })).action, "abstain");
255
256
      mock.state.confidence = 0.6;
257
      const strict = await client.callTool({ name: "jev_classify", arguments: { ...args, act_above: 0.5 } });
258
      assert.equal(payload(strict).action, "act", "caller thresholds must override the defaults");
259
    });
260
  } finally {
261
    await mock.close();
262
  }
263
});
264
265
test("a near-even probability is reported as uncertain, not rounded to a yes", async () => {
266
  const mock = await startMock();
267
  try {
268
    await withClient({ baseUrl: mock.url }, async (client) => {
269
      mock.state.noul = 0.52;
270
      const result = await client.callTool({
271
        name: "jev_check",
272
        arguments: { state: "s", question: "Is this urgent?" },
273
      });
274
      const body = payload(result);
275
      assert.equal(body.probability_yes, 0.52);
276
      assert.equal(body.verdict, "uncertain");
277
    });
278
  } finally {
279
    await mock.close();
280
  }
281
});
282
283
test("oversized state is rejected before it costs anything", async () => {
284
  const mock = await startMock();
285
  try {
286
    await withClient({ baseUrl: mock.url, env: { JEV_MAX_STATE_CHARS: "50" } }, async (client) => {
287
      const result = await client.callTool({
288
        name: "jev_check",
289
        arguments: { state: "x".repeat(200), question: "q" },
290
      });
291
      assert.equal(result.isError, true);
292
      assert.match(payload(result).error.message, /above the 50 limit/);
293
      assert.equal(mock.requests.length, 0);
294
    });
295
  } finally {
296
    await mock.close();
297
  }
298
});
299
300
test("an unusable timeout falls back instead of disabling the timeout", async () => {
301
  const mock = await startMock();
302
  try {
303
    await withClient({ baseUrl: mock.url, env: { JEV_TIMEOUT_MS: "not-a-number" } }, async (client) => {
304
      const result = await client.callTool({
305
        name: "jev_check",
306
        arguments: { state: "s", question: "q" },
307
      });
308
      assert.notEqual(result.isError, true, "a malformed timeout must not break the server");
309
    });
310
  } finally {
311
    await mock.close();
312
  }
313
});
314
315
test("API failures are classified so a caller can tell retryable from terminal", async () => {
316
  const mock = await startMock();
317
  try {
318
    await withClient({ baseUrl: mock.url }, async (client) => {
319
      mock.state.status = 401;
320
      const unauthorized = await client.callTool({ name: "jev_check", arguments: { state: "s", question: "q" } });
321
      assert.equal(unauthorized.isError, true);
322
      const { error } = payload(unauthorized);
323
      assert.equal(error.kind, "authentication");
324
      assert.equal(error.retryable, false);
325
      assert.equal(error.requestId, "req_test_123", "the request id support needs must survive");
326
    });
327
  } finally {
328
    await mock.close();
329
  }
330
});
331
332
test("jev_models reports the active model and what the key can use", async () => {
333
  const mock = await startMock();
334
  try {
335
    await withClient({ baseUrl: mock.url }, async (client) => {
336
      const body = payload(await client.callTool({ name: "jev_models", arguments: {} }));
337
      assert.equal(body.active_model, "jev-latest");
338
      assert.equal(body.models[0].name, "jev-latest");
339
    });
340
  } finally {
341
    await mock.close();
342
  }
343
});
344
345
test("one jev_ask call batches every question into a single request", async () => {
346
  const mock = await startMock();
347
  try {
348
    await withClient({ baseUrl: mock.url }, async (client) => {
349
      const result = await client.callTool({
350
        name: "jev_ask",
351
        arguments: {
352
          state: { ticket: "My card was charged twice." },
353
          questions: [
354
            { id: "billing", type: "check", question: "Is this about billing?" },
355
            { id: "team", type: "classify", question: "Which team?", options: { billing: "d", tech: "d" } },
356
            { id: "anger", type: "score", question: "How angry?", levels: ["calm", "annoyed", "furious"] },
357
          ],
358
        },
359
      });
360
361
      assert.equal(mock.requests.length, 1, "three questions must cost one round trip");
362
      assert.deepEqual(Object.keys(mock.only().questions).sort(), ["anger", "billing", "team"]);
363
364
      const body = payload(result);
365
      assert.deepEqual(Object.keys(body.answers).sort(), ["anger", "billing", "team"]);
366
      assert.equal(body.answers.team.action, "act", "choice answers carry a gate");
367
      assert.equal(body.answers.billing.action, undefined, "a noul has no confidence to gate on");
368
    });
369
  } finally {
370
    await mock.close();
371
  }
372
});
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts