Repository navigation
Expand file tree
/
Copy pathserver.test.js
More file actions
172 lines (145 loc) · 6.32 KB
/
Copy pathserver.test.js
File metadata and controls
172 lines (145 loc) · 6.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { test } from "node:test";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { createServer } from "./server.js";
const evaluationInput = {
state: { message: "Please refund this order.", order_id: "9007199254740993" },
questions: {
refund: { type: "noul", instructions: "Is a refund requested?" },
department: { type: "choice", instructions: "Which department?", criteria: { support: null, sales: "New sales" } },
urgency: { type: "score", instructions: "How urgent?", criteria: ["Routine", "Urgent"] },
},
};
const apiResponse = {
model: "jev-test",
answers: {
refund: { type: "noul", noul: 0.9 },
department: { type: "choice", choice: "support", probabilities: { support: 0.9, sales: 0.1 }, confidence: 0.8 },
urgency: { type: "score", score: 0.2, probabilities: { 0: 0.8, 1: 0.2 }, confidence: 0.6 },
},
usage: { input_tokens: 100, output_tokens: 20 },
};
async function connectClient(context, sendRequest) {
const server = createServer("test-key", sendRequest);
const client = new Client({ name: "test", version: "1" });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
context.after(async () => {
await client.close();
await server.close();
});
await server.connect(serverTransport);
await client.connect(clientTransport);
return client;
}
test("MCP forwards a mixed batch and preserves the API result", async (context) => {
const client = await connectClient(context, async (url, options) => {
assert.equal(url, "https://api.typesafe.ai/v1/systemone");
assert.equal(options.method, "POST");
assert.equal(options.headers.Authorization, "Bearer test-key");
assert.equal(options.redirect, "error");
assert.deepEqual(JSON.parse(options.body), { ...evaluationInput, model: "jev-latest" });
assert.ok(options.signal instanceof AbortSignal);
return Response.json(apiResponse);
});
const tools = await client.listTools();
const response = await client.callTool({ name: "evaluate", arguments: evaluationInput });
assert.deepEqual(tools.tools.map((tool) => tool.name), ["evaluate"]);
assert.ok(!response.isError);
assert.deepEqual(JSON.parse(response.content[0].text), apiResponse);
});
test("MCP rejects malformed questions before making a request", async (context) => {
let requestCount = 0;
const client = await connectClient(context, async () => {
requestCount += 1;
return Response.json(apiResponse);
});
const invalidQuestionBatches = [
{},
{ invalid: { type: "choice", instructions: "Choose", criteria: {} } },
{ invalid: { type: "score", instructions: "Rate", criteria: { low: "Low" } } },
];
for (const questions of invalidQuestionBatches) {
const response = await client.callTool({ name: "evaluate", arguments: { state: "Evidence", questions } });
assert.equal(response.isError, true);
}
assert.equal(requestCount, 0);
});
test("HTTP failures do not echo upstream bodies or retry", async (context) => {
let requestCount = 0;
const client = await connectClient(context, async () => {
requestCount += 1;
return new Response("sensitive upstream body test-key", { status: 429 });
});
const response = await client.callTool({ name: "evaluate", arguments: evaluationInput });
assert.equal(response.isError, true);
assert.equal(response.content[0].text, "TypeSafe returned HTTP 429.");
assert.equal(requestCount, 1);
});
test("missing answers and invalid JSON become tool errors", async (context) => {
const bodies = [
"not json",
"null",
'{"answers":{}}',
JSON.stringify({ answers: { ...apiResponse.answers, refund: { type: "score" } } }),
];
const responseCount = bodies.length;
const client = await connectClient(context, async () => new Response(bodies.shift()));
for (let index = 0; index < responseCount; index += 1) {
const response = await client.callTool({ name: "evaluate", arguments: evaluationInput });
assert.equal(response.isError, true);
}
});
test("request errors redact the configured key", async (context) => {
const client = await connectClient(context, async () => {
throw new Error("request failed: test-key");
});
const response = await client.callTool({ name: "evaluate", arguments: evaluationInput });
assert.equal(response.isError, true);
assert.equal(response.content[0].text, "request failed: [redacted]");
});
test("MCP cancellation aborts the provider request", { timeout: 2_000 }, async (context) => {
const started = Promise.withResolvers();
const cancelled = Promise.withResolvers();
const controller = new AbortController();
const client = await connectClient(context, async (url, { signal }) => {
started.resolve();
return new Promise((resolve, reject) => {
signal.addEventListener("abort", () => {
cancelled.resolve();
reject(signal.reason);
}, { once: true });
});
});
const pending = client.callTool({ name: "evaluate", arguments: evaluationInput }, undefined, { signal: controller.signal });
const rejected = assert.rejects(pending);
await started.promise;
controller.abort();
await rejected;
await cancelled.promise;
});
test("the actual stdio entry point advertises evaluate", async (context) => {
const client = new Client({ name: "stdio-test", version: "1" });
const transport = new StdioClientTransport({
command: process.execPath,
args: [fileURLToPath(new URL("./server.js", import.meta.url))],
env: { TYPESAFE_API_KEY: "test-key" },
stderr: "pipe",
});
context.after(() => client.close());
await client.connect(transport);
const tools = await client.listTools();
assert.deepEqual(tools.tools.map((tool) => tool.name), ["evaluate"]);
});
test("startup without a key exits cleanly without writing to stdout", () => {
const processResult = spawnSync(process.execPath, [fileURLToPath(new URL("./server.js", import.meta.url))], {
env: { TYPESAFE_API_KEY: "" },
encoding: "utf8",
});
assert.equal(processResult.status, 1);
assert.equal(processResult.stdout, "");
assert.match(processResult.stderr, /Set TYPESAFE_API_KEY/);
});