@langwatch/mcp-server 0.5.0 → 0.6.1
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/CHANGELOG.md +32 -0
- package/dist/{archive-scenario-GAE4XVFM.js → archive-scenario-YFD5THOR.js} +3 -3
- package/dist/archive-scenario-YFD5THOR.js.map +1 -0
- package/dist/chunk-5UOPNRXW.js +37 -0
- package/dist/chunk-5UOPNRXW.js.map +1 -0
- package/dist/{chunk-K2YFPOSD.js → chunk-6U4TCGFC.js} +2 -2
- package/dist/chunk-IX6QJKAD.js +22 -0
- package/dist/chunk-IX6QJKAD.js.map +1 -0
- package/dist/{chunk-JVWDWL3J.js → chunk-LLRQIF52.js} +3 -11
- package/dist/chunk-LLRQIF52.js.map +1 -0
- package/dist/create-evaluator-E5X5ZP3B.js +27 -0
- package/dist/create-evaluator-E5X5ZP3B.js.map +1 -0
- package/dist/create-prompt-7Z35MIL6.js +36 -0
- package/dist/create-prompt-7Z35MIL6.js.map +1 -0
- package/dist/{create-scenario-3YRZVDYF.js → create-scenario-DIMPJRPY.js} +3 -3
- package/dist/create-scenario-DIMPJRPY.js.map +1 -0
- package/dist/discover-evaluator-schema-H23XCLNE.js +1402 -0
- package/dist/discover-evaluator-schema-H23XCLNE.js.map +1 -0
- package/dist/{get-analytics-BAVXTAPB.js → get-analytics-4YJW4S5L.js} +2 -2
- package/dist/get-evaluator-WDEH2F7M.js +47 -0
- package/dist/get-evaluator-WDEH2F7M.js.map +1 -0
- package/dist/{get-prompt-LKCPT26O.js → get-prompt-F6PDVC76.js} +2 -5
- package/dist/get-prompt-F6PDVC76.js.map +1 -0
- package/dist/{get-scenario-3SCDW4Z6.js → get-scenario-H24ZYNT5.js} +3 -3
- package/dist/{get-trace-QFDWJ5D4.js → get-trace-27USKGO7.js} +2 -2
- package/dist/index.js +13311 -2411
- package/dist/index.js.map +1 -1
- package/dist/list-evaluators-KRGI72EH.js +34 -0
- package/dist/list-evaluators-KRGI72EH.js.map +1 -0
- package/dist/list-model-providers-A5YCFTPI.js +35 -0
- package/dist/list-model-providers-A5YCFTPI.js.map +1 -0
- package/dist/{list-prompts-UQPBCUYA.js → list-prompts-LKJSE7XN.js} +6 -7
- package/dist/list-prompts-LKJSE7XN.js.map +1 -0
- package/dist/{list-scenarios-573YOUKC.js → list-scenarios-ZK5CMGC4.js} +5 -5
- package/dist/list-scenarios-ZK5CMGC4.js.map +1 -0
- package/dist/{search-traces-RSMYCAN7.js → search-traces-SOKAAMAR.js} +2 -2
- package/dist/set-model-provider-7MGULZDH.js +33 -0
- package/dist/set-model-provider-7MGULZDH.js.map +1 -0
- package/dist/update-evaluator-A3XINFLJ.js +24 -0
- package/dist/update-evaluator-A3XINFLJ.js.map +1 -0
- package/dist/update-prompt-IW7X2UQM.js +22 -0
- package/dist/update-prompt-IW7X2UQM.js.map +1 -0
- package/dist/{update-scenario-SSGVOBJO.js → update-scenario-ZT7TOBFR.js} +3 -3
- package/dist/update-scenario-ZT7TOBFR.js.map +1 -0
- package/package.json +10 -10
- package/src/__tests__/all-tools.integration.test.ts +1337 -0
- package/src/__tests__/discover-evaluator-schema.unit.test.ts +89 -0
- package/src/__tests__/evaluator-tools.unit.test.ts +262 -0
- package/src/__tests__/integration.integration.test.ts +9 -34
- package/src/__tests__/langwatch-api.unit.test.ts +4 -32
- package/src/__tests__/model-provider-tools.unit.test.ts +190 -0
- package/src/__tests__/scenario-tools.integration.test.ts +5 -5
- package/src/__tests__/scenario-tools.unit.test.ts +2 -2
- package/src/__tests__/tools.unit.test.ts +59 -65
- package/src/index.ts +250 -89
- package/src/langwatch-api-evaluators.ts +70 -0
- package/src/langwatch-api-model-providers.ts +41 -0
- package/src/langwatch-api.ts +3 -28
- package/src/tools/archive-scenario.ts +1 -1
- package/src/tools/create-evaluator.ts +33 -0
- package/src/tools/create-prompt.ts +30 -5
- package/src/tools/create-scenario.ts +1 -1
- package/src/tools/discover-evaluator-schema.ts +143 -0
- package/src/tools/get-evaluator.ts +53 -0
- package/src/tools/get-prompt.ts +1 -4
- package/src/tools/list-evaluators.ts +37 -0
- package/src/tools/list-model-providers.ts +40 -0
- package/src/tools/list-prompts.ts +5 -6
- package/src/tools/list-scenarios.ts +3 -3
- package/src/tools/set-model-provider.ts +46 -0
- package/src/tools/update-evaluator.ts +30 -0
- package/src/tools/update-prompt.ts +9 -25
- package/src/tools/update-scenario.ts +1 -1
- package/dist/archive-scenario-GAE4XVFM.js.map +0 -1
- package/dist/chunk-JVWDWL3J.js.map +0 -1
- package/dist/create-prompt-P35POKBW.js +0 -22
- package/dist/create-prompt-P35POKBW.js.map +0 -1
- package/dist/create-scenario-3YRZVDYF.js.map +0 -1
- package/dist/get-prompt-LKCPT26O.js.map +0 -1
- package/dist/list-prompts-UQPBCUYA.js.map +0 -1
- package/dist/list-scenarios-573YOUKC.js.map +0 -1
- package/dist/update-prompt-G2Y5EBQY.js +0 -31
- package/dist/update-prompt-G2Y5EBQY.js.map +0 -1
- package/dist/update-scenario-SSGVOBJO.js.map +0 -1
- /package/dist/{chunk-K2YFPOSD.js.map → chunk-6U4TCGFC.js.map} +0 -0
- /package/dist/{get-analytics-BAVXTAPB.js.map → get-analytics-4YJW4S5L.js.map} +0 -0
- /package/dist/{get-scenario-3SCDW4Z6.js.map → get-scenario-H24ZYNT5.js.map} +0 -0
- /package/dist/{get-trace-QFDWJ5D4.js.map → get-trace-27USKGO7.js.map} +0 -0
- /package/dist/{search-traces-RSMYCAN7.js.map → search-traces-SOKAAMAR.js.map} +0 -0
|
@@ -0,0 +1,1337 @@
|
|
|
1
|
+
import { createServer, type Server } from "http";
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, it, vi, beforeEach } from "vitest";
|
|
3
|
+
import { initConfig } from "../config.js";
|
|
4
|
+
|
|
5
|
+
// --- Canned responses for every API endpoint ---
|
|
6
|
+
|
|
7
|
+
const CANNED_TRACES_SEARCH = {
|
|
8
|
+
traces: [
|
|
9
|
+
{
|
|
10
|
+
trace_id: "trace-001",
|
|
11
|
+
formatted_trace:
|
|
12
|
+
"Root [server] 1200ms\n LLM Call [llm] 500ms\n Input: Hello, how are you?\n Output: I am fine, thank you!",
|
|
13
|
+
input: { value: "Hello, how are you?" },
|
|
14
|
+
output: { value: "I am fine, thank you!" },
|
|
15
|
+
timestamps: { started_at: 1700000000000 },
|
|
16
|
+
metadata: { user_id: "user-42", thread_id: "thread-1" },
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
pagination: { totalHits: 1 },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const CANNED_TRACES_SEARCH_WITH_SCROLL = {
|
|
23
|
+
traces: [
|
|
24
|
+
{
|
|
25
|
+
trace_id: "trace-page-1",
|
|
26
|
+
input: { value: "Page 1 input" },
|
|
27
|
+
output: { value: "Page 1 output" },
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
pagination: { totalHits: 50, scrollId: "scroll-token-abc" },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const CANNED_TRACES_EMPTY = {
|
|
34
|
+
traces: [],
|
|
35
|
+
pagination: { totalHits: 0 },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const CANNED_TRACE_DETAIL = {
|
|
39
|
+
trace_id: "trace-001",
|
|
40
|
+
formatted_trace:
|
|
41
|
+
"Root [server] 1200ms\n LLM Call [llm] 500ms\n Input: Hello\n Output: Hi there",
|
|
42
|
+
timestamps: {
|
|
43
|
+
started_at: 1700000000000,
|
|
44
|
+
updated_at: 1700000001000,
|
|
45
|
+
inserted_at: 1700000001000,
|
|
46
|
+
},
|
|
47
|
+
metadata: {
|
|
48
|
+
user_id: "user-42",
|
|
49
|
+
thread_id: "thread-1",
|
|
50
|
+
customer_id: "cust-100",
|
|
51
|
+
labels: ["production"],
|
|
52
|
+
},
|
|
53
|
+
evaluations: [
|
|
54
|
+
{
|
|
55
|
+
evaluator_id: "eval-1",
|
|
56
|
+
name: "Faithfulness",
|
|
57
|
+
score: 0.95,
|
|
58
|
+
passed: true,
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const CANNED_ANALYTICS = {
|
|
64
|
+
currentPeriod: [
|
|
65
|
+
{ date: "2024-01-01", "0__trace_id_cardinality": 42 },
|
|
66
|
+
{ date: "2024-01-02", "0__trace_id_cardinality": 58 },
|
|
67
|
+
],
|
|
68
|
+
previousPeriod: [],
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const CANNED_PROMPTS_LIST = [
|
|
72
|
+
{
|
|
73
|
+
id: "p1",
|
|
74
|
+
handle: "greeting-bot",
|
|
75
|
+
name: "Greeting Bot",
|
|
76
|
+
latestVersionNumber: 3,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: "p2",
|
|
80
|
+
handle: "qa-assistant",
|
|
81
|
+
name: "QA Assistant",
|
|
82
|
+
latestVersionNumber: 1,
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const CANNED_PROMPT_DETAIL = {
|
|
87
|
+
id: "p1",
|
|
88
|
+
handle: "greeting-bot",
|
|
89
|
+
name: "Greeting Bot",
|
|
90
|
+
latestVersionNumber: 3,
|
|
91
|
+
versions: [
|
|
92
|
+
{
|
|
93
|
+
version: 3,
|
|
94
|
+
commitMessage: "Updated tone",
|
|
95
|
+
model: "openai/gpt-4o",
|
|
96
|
+
messages: [{ role: "system", content: "You are a friendly bot." }],
|
|
97
|
+
},
|
|
98
|
+
{ version: 2, commitMessage: "Added greeting" },
|
|
99
|
+
{ version: 1, commitMessage: "Initial version" },
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const CANNED_PROMPT_CREATED = {
|
|
104
|
+
id: "p-new",
|
|
105
|
+
handle: "new-prompt",
|
|
106
|
+
name: "New Prompt",
|
|
107
|
+
latestVersionNumber: 1,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const CANNED_PROMPT_UPDATED = {
|
|
111
|
+
id: "p1",
|
|
112
|
+
handle: "greeting-bot",
|
|
113
|
+
latestVersionNumber: 4,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const CANNED_SCENARIOS_LIST = [
|
|
117
|
+
{
|
|
118
|
+
id: "scen_abc123",
|
|
119
|
+
name: "Login Flow Happy Path",
|
|
120
|
+
situation: "User attempts to log in with valid credentials",
|
|
121
|
+
criteria: [
|
|
122
|
+
"Responds with a welcome message",
|
|
123
|
+
"Includes user name in greeting",
|
|
124
|
+
],
|
|
125
|
+
labels: ["auth", "happy-path"],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: "scen_def456",
|
|
129
|
+
name: "Password Reset",
|
|
130
|
+
situation: "User requests a password reset link",
|
|
131
|
+
criteria: ["Sends reset email"],
|
|
132
|
+
labels: ["auth"],
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
const CANNED_SCENARIO_DETAIL = {
|
|
137
|
+
id: "scen_abc123",
|
|
138
|
+
name: "Login Flow Happy Path",
|
|
139
|
+
situation: "User attempts to log in with valid credentials",
|
|
140
|
+
criteria: [
|
|
141
|
+
"Responds with a welcome message",
|
|
142
|
+
"Includes user name in greeting",
|
|
143
|
+
],
|
|
144
|
+
labels: ["auth", "happy-path"],
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const CANNED_SCENARIO_CREATED = {
|
|
148
|
+
id: "scen_new789",
|
|
149
|
+
name: "New Scenario",
|
|
150
|
+
situation: "User does something",
|
|
151
|
+
criteria: ["Agent responds correctly"],
|
|
152
|
+
labels: ["test"],
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const CANNED_SCENARIO_UPDATED = {
|
|
156
|
+
id: "scen_abc123",
|
|
157
|
+
name: "Login Flow - Updated",
|
|
158
|
+
situation: "User logs in with correct email and pass",
|
|
159
|
+
criteria: [
|
|
160
|
+
"Responds with welcome message",
|
|
161
|
+
"Sets session cookie",
|
|
162
|
+
"Redirects to dashboard",
|
|
163
|
+
],
|
|
164
|
+
labels: ["auth", "happy-path"],
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const CANNED_SCENARIO_ARCHIVED = {
|
|
168
|
+
id: "scen_abc123",
|
|
169
|
+
archived: true,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const CANNED_EVALUATORS_LIST = [
|
|
173
|
+
{
|
|
174
|
+
id: "evaluator_abc123",
|
|
175
|
+
projectId: "proj_1",
|
|
176
|
+
name: "Toxicity Check",
|
|
177
|
+
slug: "toxicity-check",
|
|
178
|
+
type: "evaluator",
|
|
179
|
+
config: { evaluatorType: "openai/moderation" },
|
|
180
|
+
workflowId: null,
|
|
181
|
+
copiedFromEvaluatorId: null,
|
|
182
|
+
createdAt: "2024-01-01T00:00:00.000Z",
|
|
183
|
+
updatedAt: "2024-01-01T00:00:00.000Z",
|
|
184
|
+
fields: [{ identifier: "input", type: "str" }],
|
|
185
|
+
outputFields: [{ identifier: "passed", type: "bool" }],
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: "evaluator_def456",
|
|
189
|
+
projectId: "proj_1",
|
|
190
|
+
name: "Exact Match",
|
|
191
|
+
slug: "exact-match",
|
|
192
|
+
type: "evaluator",
|
|
193
|
+
config: { evaluatorType: "langevals/exact_match" },
|
|
194
|
+
workflowId: null,
|
|
195
|
+
copiedFromEvaluatorId: null,
|
|
196
|
+
createdAt: "2024-01-02T00:00:00.000Z",
|
|
197
|
+
updatedAt: "2024-01-02T00:00:00.000Z",
|
|
198
|
+
fields: [
|
|
199
|
+
{ identifier: "output", type: "str" },
|
|
200
|
+
{ identifier: "expected_output", type: "str" },
|
|
201
|
+
],
|
|
202
|
+
outputFields: [{ identifier: "passed", type: "bool" }],
|
|
203
|
+
},
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
const CANNED_EVALUATOR_DETAIL = {
|
|
207
|
+
id: "evaluator_abc123",
|
|
208
|
+
projectId: "proj_1",
|
|
209
|
+
name: "Toxicity Check",
|
|
210
|
+
slug: "toxicity-check",
|
|
211
|
+
type: "evaluator",
|
|
212
|
+
config: {
|
|
213
|
+
evaluatorType: "openai/moderation",
|
|
214
|
+
settings: { model: "text-moderation-stable" },
|
|
215
|
+
},
|
|
216
|
+
workflowId: null,
|
|
217
|
+
copiedFromEvaluatorId: null,
|
|
218
|
+
createdAt: "2024-01-01T00:00:00.000Z",
|
|
219
|
+
updatedAt: "2024-01-01T00:00:00.000Z",
|
|
220
|
+
fields: [
|
|
221
|
+
{ identifier: "input", type: "str" },
|
|
222
|
+
{ identifier: "output", type: "str", optional: true },
|
|
223
|
+
],
|
|
224
|
+
outputFields: [
|
|
225
|
+
{ identifier: "passed", type: "bool" },
|
|
226
|
+
{ identifier: "score", type: "float" },
|
|
227
|
+
],
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const CANNED_EVALUATOR_CREATED = {
|
|
231
|
+
id: "evaluator_new123",
|
|
232
|
+
projectId: "proj_1",
|
|
233
|
+
name: "My LLM Judge",
|
|
234
|
+
slug: "my-llm-judge",
|
|
235
|
+
type: "evaluator",
|
|
236
|
+
config: { evaluatorType: "langevals/llm_boolean" },
|
|
237
|
+
workflowId: null,
|
|
238
|
+
copiedFromEvaluatorId: null,
|
|
239
|
+
createdAt: "2024-01-01T00:00:00.000Z",
|
|
240
|
+
updatedAt: "2024-01-01T00:00:00.000Z",
|
|
241
|
+
fields: [{ identifier: "input", type: "str" }],
|
|
242
|
+
outputFields: [{ identifier: "passed", type: "bool" }],
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const CANNED_EVALUATOR_UPDATED = {
|
|
246
|
+
id: "evaluator_abc123",
|
|
247
|
+
projectId: "proj_1",
|
|
248
|
+
name: "Updated Toxicity",
|
|
249
|
+
slug: "toxicity-check",
|
|
250
|
+
type: "evaluator",
|
|
251
|
+
config: { evaluatorType: "openai/moderation" },
|
|
252
|
+
workflowId: null,
|
|
253
|
+
copiedFromEvaluatorId: null,
|
|
254
|
+
createdAt: "2024-01-01T00:00:00.000Z",
|
|
255
|
+
updatedAt: "2024-01-02T00:00:00.000Z",
|
|
256
|
+
fields: [],
|
|
257
|
+
outputFields: [],
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const CANNED_MODEL_PROVIDERS_LIST = {
|
|
261
|
+
openai: {
|
|
262
|
+
provider: "openai",
|
|
263
|
+
enabled: true,
|
|
264
|
+
customKeys: { OPENAI_API_KEY: "HAS_KEY" },
|
|
265
|
+
models: ["gpt-4o", "gpt-4o-mini"],
|
|
266
|
+
embeddingsModels: ["text-embedding-3-small"],
|
|
267
|
+
deploymentMapping: null,
|
|
268
|
+
extraHeaders: [],
|
|
269
|
+
},
|
|
270
|
+
anthropic: {
|
|
271
|
+
provider: "anthropic",
|
|
272
|
+
enabled: false,
|
|
273
|
+
customKeys: null,
|
|
274
|
+
models: ["claude-sonnet-4-5-20250929"],
|
|
275
|
+
embeddingsModels: null,
|
|
276
|
+
deploymentMapping: null,
|
|
277
|
+
extraHeaders: [],
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const CANNED_MODEL_PROVIDER_SET = {
|
|
282
|
+
openai: {
|
|
283
|
+
provider: "openai",
|
|
284
|
+
enabled: true,
|
|
285
|
+
customKeys: { OPENAI_API_KEY: "HAS_KEY" },
|
|
286
|
+
models: ["gpt-4o"],
|
|
287
|
+
embeddingsModels: null,
|
|
288
|
+
deploymentMapping: null,
|
|
289
|
+
extraHeaders: [],
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// --- Mock HTTP Server that handles ALL MCP API endpoints ---
|
|
294
|
+
|
|
295
|
+
/** Track last request for each route so tests can assert on request body/params. */
|
|
296
|
+
const lastRequests: Record<string, { method: string; url: string; body: string }> = {};
|
|
297
|
+
|
|
298
|
+
function createMockServer(): Server {
|
|
299
|
+
return createServer((req, res) => {
|
|
300
|
+
const authToken = req.headers["x-auth-token"];
|
|
301
|
+
if (authToken !== "test-integration-key") {
|
|
302
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
303
|
+
res.end(JSON.stringify({ message: "Invalid auth token." }));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
let body = "";
|
|
308
|
+
req.on("data", (chunk: string) => (body += chunk));
|
|
309
|
+
req.on("end", () => {
|
|
310
|
+
const url = req.url ?? "";
|
|
311
|
+
const method = req.method ?? "GET";
|
|
312
|
+
res.setHeader("Content-Type", "application/json");
|
|
313
|
+
|
|
314
|
+
// Store last request for assertions
|
|
315
|
+
const routeKey = `${method} ${url.split("?")[0]}`;
|
|
316
|
+
lastRequests[routeKey] = { method, url, body };
|
|
317
|
+
|
|
318
|
+
// --- Trace endpoints ---
|
|
319
|
+
if (url === "/api/traces/search" && method === "POST") {
|
|
320
|
+
const parsed = JSON.parse(body);
|
|
321
|
+
// Return empty results when a special query is used
|
|
322
|
+
if (parsed.query === "__empty__") {
|
|
323
|
+
res.writeHead(200);
|
|
324
|
+
res.end(JSON.stringify(CANNED_TRACES_EMPTY));
|
|
325
|
+
} else if (parsed.pageSize === 5) {
|
|
326
|
+
res.writeHead(200);
|
|
327
|
+
res.end(JSON.stringify(CANNED_TRACES_SEARCH_WITH_SCROLL));
|
|
328
|
+
} else {
|
|
329
|
+
res.writeHead(200);
|
|
330
|
+
res.end(JSON.stringify(CANNED_TRACES_SEARCH));
|
|
331
|
+
}
|
|
332
|
+
} else if (
|
|
333
|
+
url.match(/^\/api\/traces\/trace-nonexistent(\?|$)/) &&
|
|
334
|
+
method === "GET"
|
|
335
|
+
) {
|
|
336
|
+
res.writeHead(404);
|
|
337
|
+
res.end(JSON.stringify({ message: "Trace not found" }));
|
|
338
|
+
} else if (
|
|
339
|
+
url.match(/^\/api\/traces\/[^/]+(\?|$)/) &&
|
|
340
|
+
method === "GET"
|
|
341
|
+
) {
|
|
342
|
+
res.writeHead(200);
|
|
343
|
+
res.end(JSON.stringify(CANNED_TRACE_DETAIL));
|
|
344
|
+
}
|
|
345
|
+
// --- Analytics endpoint ---
|
|
346
|
+
else if (url === "/api/analytics/timeseries" && method === "POST") {
|
|
347
|
+
res.writeHead(200);
|
|
348
|
+
res.end(JSON.stringify(CANNED_ANALYTICS));
|
|
349
|
+
}
|
|
350
|
+
// --- Prompt endpoints ---
|
|
351
|
+
else if (url === "/api/prompts" && method === "GET") {
|
|
352
|
+
res.writeHead(200);
|
|
353
|
+
res.end(JSON.stringify(CANNED_PROMPTS_LIST));
|
|
354
|
+
} else if (url === "/api/prompts" && method === "POST") {
|
|
355
|
+
res.writeHead(200);
|
|
356
|
+
res.end(JSON.stringify(CANNED_PROMPT_CREATED));
|
|
357
|
+
} else if (
|
|
358
|
+
url.match(/^\/api\/prompts\/[^/]+$/) &&
|
|
359
|
+
method === "GET"
|
|
360
|
+
) {
|
|
361
|
+
res.writeHead(200);
|
|
362
|
+
res.end(JSON.stringify(CANNED_PROMPT_DETAIL));
|
|
363
|
+
} else if (
|
|
364
|
+
url.match(/^\/api\/prompts\/[^/]+$/) &&
|
|
365
|
+
method === "PUT"
|
|
366
|
+
) {
|
|
367
|
+
res.writeHead(200);
|
|
368
|
+
res.end(JSON.stringify(CANNED_PROMPT_UPDATED));
|
|
369
|
+
}
|
|
370
|
+
// --- Scenario endpoints ---
|
|
371
|
+
else if (url === "/api/scenarios" && method === "GET") {
|
|
372
|
+
res.writeHead(200);
|
|
373
|
+
res.end(JSON.stringify(CANNED_SCENARIOS_LIST));
|
|
374
|
+
} else if (url === "/api/scenarios" && method === "POST") {
|
|
375
|
+
res.writeHead(200);
|
|
376
|
+
res.end(JSON.stringify(CANNED_SCENARIO_CREATED));
|
|
377
|
+
} else if (
|
|
378
|
+
url.match(/^\/api\/scenarios\/scen_nonexistent(\?|$)/) &&
|
|
379
|
+
method === "GET"
|
|
380
|
+
) {
|
|
381
|
+
res.writeHead(404);
|
|
382
|
+
res.end(JSON.stringify({ message: "Scenario not found" }));
|
|
383
|
+
} else if (
|
|
384
|
+
url.match(/^\/api\/scenarios\/[^/]+$/) &&
|
|
385
|
+
method === "GET"
|
|
386
|
+
) {
|
|
387
|
+
res.writeHead(200);
|
|
388
|
+
res.end(JSON.stringify(CANNED_SCENARIO_DETAIL));
|
|
389
|
+
} else if (
|
|
390
|
+
url.match(/^\/api\/scenarios\/[^/]+$/) &&
|
|
391
|
+
method === "PUT"
|
|
392
|
+
) {
|
|
393
|
+
res.writeHead(200);
|
|
394
|
+
res.end(JSON.stringify(CANNED_SCENARIO_UPDATED));
|
|
395
|
+
} else if (
|
|
396
|
+
url.match(/^\/api\/scenarios\/[^/]+$/) &&
|
|
397
|
+
method === "DELETE"
|
|
398
|
+
) {
|
|
399
|
+
res.writeHead(200);
|
|
400
|
+
res.end(JSON.stringify(CANNED_SCENARIO_ARCHIVED));
|
|
401
|
+
}
|
|
402
|
+
// --- Evaluator endpoints ---
|
|
403
|
+
else if (url === "/api/evaluators" && method === "GET") {
|
|
404
|
+
res.writeHead(200);
|
|
405
|
+
res.end(JSON.stringify(CANNED_EVALUATORS_LIST));
|
|
406
|
+
} else if (url === "/api/evaluators" && method === "POST") {
|
|
407
|
+
res.writeHead(200);
|
|
408
|
+
res.end(JSON.stringify(CANNED_EVALUATOR_CREATED));
|
|
409
|
+
} else if (
|
|
410
|
+
url.match(/^\/api\/evaluators\/evaluator_nonexistent(\?|$)/) &&
|
|
411
|
+
method === "GET"
|
|
412
|
+
) {
|
|
413
|
+
res.writeHead(404);
|
|
414
|
+
res.end(JSON.stringify({ message: "Evaluator not found" }));
|
|
415
|
+
} else if (
|
|
416
|
+
url.match(/^\/api\/evaluators\/[^/]+$/) &&
|
|
417
|
+
method === "GET"
|
|
418
|
+
) {
|
|
419
|
+
res.writeHead(200);
|
|
420
|
+
res.end(JSON.stringify(CANNED_EVALUATOR_DETAIL));
|
|
421
|
+
} else if (
|
|
422
|
+
url.match(/^\/api\/evaluators\/[^/]+$/) &&
|
|
423
|
+
method === "PUT"
|
|
424
|
+
) {
|
|
425
|
+
res.writeHead(200);
|
|
426
|
+
res.end(JSON.stringify(CANNED_EVALUATOR_UPDATED));
|
|
427
|
+
}
|
|
428
|
+
// --- Model Provider endpoints ---
|
|
429
|
+
else if (url === "/api/model-providers" && method === "GET") {
|
|
430
|
+
res.writeHead(200);
|
|
431
|
+
res.end(JSON.stringify(CANNED_MODEL_PROVIDERS_LIST));
|
|
432
|
+
} else if (
|
|
433
|
+
url.match(/^\/api\/model-providers\/[^/]+$/) &&
|
|
434
|
+
method === "PUT"
|
|
435
|
+
) {
|
|
436
|
+
res.writeHead(200);
|
|
437
|
+
res.end(JSON.stringify(CANNED_MODEL_PROVIDER_SET));
|
|
438
|
+
}
|
|
439
|
+
// --- Fallback ---
|
|
440
|
+
else {
|
|
441
|
+
res.writeHead(404);
|
|
442
|
+
res.end(
|
|
443
|
+
JSON.stringify({ message: `Not found: ${method} ${url}` }),
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// --- Integration Tests ---
|
|
451
|
+
// These verify that every MCP tool handler correctly communicates with the REST API
|
|
452
|
+
// through a real HTTP server. Formatting/digest assertions ensure the full chain works.
|
|
453
|
+
|
|
454
|
+
describe("All MCP tools integration", () => {
|
|
455
|
+
let server: Server;
|
|
456
|
+
let port: number;
|
|
457
|
+
let originalFetch: typeof globalThis.fetch;
|
|
458
|
+
|
|
459
|
+
beforeAll(async () => {
|
|
460
|
+
server = createMockServer();
|
|
461
|
+
await new Promise<void>((resolve) => {
|
|
462
|
+
server.listen(0, () => {
|
|
463
|
+
const addr = server.address();
|
|
464
|
+
port = typeof addr === "object" && addr ? addr.port : 0;
|
|
465
|
+
initConfig({
|
|
466
|
+
apiKey: "test-integration-key",
|
|
467
|
+
endpoint: `http://localhost:${port}`,
|
|
468
|
+
});
|
|
469
|
+
resolve();
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
originalFetch = globalThis.fetch;
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
afterAll(async () => {
|
|
476
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
477
|
+
globalThis.fetch = originalFetch;
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// =====================
|
|
481
|
+
// 1. fetch_langwatch_docs
|
|
482
|
+
// =====================
|
|
483
|
+
describe("fetch_langwatch_docs", () => {
|
|
484
|
+
describe("when fetching the docs index", () => {
|
|
485
|
+
it("returns content from the langwatch docs URL", async () => {
|
|
486
|
+
// Intercept fetch to avoid hitting the real network
|
|
487
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
488
|
+
text: () =>
|
|
489
|
+
Promise.resolve(
|
|
490
|
+
"# LangWatch Docs\nWelcome to LangWatch documentation.",
|
|
491
|
+
),
|
|
492
|
+
});
|
|
493
|
+
globalThis.fetch = mockFetch;
|
|
494
|
+
|
|
495
|
+
// The tool is inlined in index.ts; call the same logic directly
|
|
496
|
+
const url = "https://langwatch.ai/docs/llms.txt";
|
|
497
|
+
const response = await fetch(url);
|
|
498
|
+
const text = await response.text();
|
|
499
|
+
|
|
500
|
+
expect(text).toContain("LangWatch");
|
|
501
|
+
expect(mockFetch).toHaveBeenCalledWith(url);
|
|
502
|
+
|
|
503
|
+
globalThis.fetch = originalFetch;
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
describe("when fetching a specific doc page", () => {
|
|
508
|
+
it("appends .md extension when missing", async () => {
|
|
509
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
510
|
+
text: () => Promise.resolve("# Integration Guide"),
|
|
511
|
+
});
|
|
512
|
+
globalThis.fetch = mockFetch;
|
|
513
|
+
|
|
514
|
+
let urlToFetch = "https://langwatch.ai/docs/integration";
|
|
515
|
+
if (
|
|
516
|
+
!urlToFetch.endsWith(".md") &&
|
|
517
|
+
!urlToFetch.endsWith(".txt")
|
|
518
|
+
) {
|
|
519
|
+
urlToFetch += ".md";
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const response = await fetch(urlToFetch);
|
|
523
|
+
const text = await response.text();
|
|
524
|
+
|
|
525
|
+
expect(text).toContain("Integration Guide");
|
|
526
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
527
|
+
"https://langwatch.ai/docs/integration.md",
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
globalThis.fetch = originalFetch;
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
describe("when a relative path is provided", () => {
|
|
535
|
+
it("prepends the docs base URL", async () => {
|
|
536
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
537
|
+
text: () => Promise.resolve("# Getting Started"),
|
|
538
|
+
});
|
|
539
|
+
globalThis.fetch = mockFetch;
|
|
540
|
+
|
|
541
|
+
let urlToFetch: string | undefined = "/getting-started";
|
|
542
|
+
if (urlToFetch && !urlToFetch.endsWith(".md") && !urlToFetch.endsWith(".txt")) {
|
|
543
|
+
urlToFetch += ".md";
|
|
544
|
+
}
|
|
545
|
+
if (!urlToFetch!.startsWith("http")) {
|
|
546
|
+
if (!urlToFetch!.startsWith("/")) {
|
|
547
|
+
urlToFetch = "/" + urlToFetch;
|
|
548
|
+
}
|
|
549
|
+
urlToFetch = "https://langwatch.ai/docs" + urlToFetch;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
await fetch(urlToFetch);
|
|
553
|
+
|
|
554
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
555
|
+
"https://langwatch.ai/docs/getting-started.md",
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
globalThis.fetch = originalFetch;
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// =====================
|
|
564
|
+
// 2. fetch_scenario_docs
|
|
565
|
+
// =====================
|
|
566
|
+
describe("fetch_scenario_docs", () => {
|
|
567
|
+
describe("when fetching the scenario docs index", () => {
|
|
568
|
+
it("returns content from the scenario docs URL", async () => {
|
|
569
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
570
|
+
text: () =>
|
|
571
|
+
Promise.resolve(
|
|
572
|
+
"# Scenario Testing\nLearn how to test agents.",
|
|
573
|
+
),
|
|
574
|
+
});
|
|
575
|
+
globalThis.fetch = mockFetch;
|
|
576
|
+
|
|
577
|
+
const url = "https://langwatch.ai/scenario/llms.txt";
|
|
578
|
+
const response = await fetch(url);
|
|
579
|
+
const text = await response.text();
|
|
580
|
+
|
|
581
|
+
expect(text).toContain("Scenario Testing");
|
|
582
|
+
expect(mockFetch).toHaveBeenCalledWith(url);
|
|
583
|
+
|
|
584
|
+
globalThis.fetch = originalFetch;
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
describe("when fetching a specific scenario doc page", () => {
|
|
589
|
+
it("appends .md extension and prepends base URL for relative paths", async () => {
|
|
590
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
591
|
+
text: () => Promise.resolve("# Setup Guide"),
|
|
592
|
+
});
|
|
593
|
+
globalThis.fetch = mockFetch;
|
|
594
|
+
|
|
595
|
+
let urlToFetch: string | undefined = "setup";
|
|
596
|
+
if (urlToFetch && !urlToFetch.endsWith(".md") && !urlToFetch.endsWith(".txt")) {
|
|
597
|
+
urlToFetch += ".md";
|
|
598
|
+
}
|
|
599
|
+
if (!urlToFetch!.startsWith("http")) {
|
|
600
|
+
if (!urlToFetch!.startsWith("/")) {
|
|
601
|
+
urlToFetch = "/" + urlToFetch;
|
|
602
|
+
}
|
|
603
|
+
urlToFetch = "https://langwatch.ai/scenario" + urlToFetch;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
await fetch(urlToFetch);
|
|
607
|
+
|
|
608
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
609
|
+
"https://langwatch.ai/scenario/setup.md",
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
globalThis.fetch = originalFetch;
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// =====================
|
|
618
|
+
// 3. discover_schema
|
|
619
|
+
// =====================
|
|
620
|
+
describe("discover_schema", () => {
|
|
621
|
+
describe("when category is filters", () => {
|
|
622
|
+
it("returns filter field documentation", async () => {
|
|
623
|
+
const { formatSchema } = await import(
|
|
624
|
+
"../tools/discover-schema.js"
|
|
625
|
+
);
|
|
626
|
+
const result = formatSchema("filters");
|
|
627
|
+
|
|
628
|
+
expect(result).toContain("## Available Filter Fields");
|
|
629
|
+
expect(result).toContain("filters");
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
describe("when category is metrics", () => {
|
|
634
|
+
it("returns metric documentation", async () => {
|
|
635
|
+
const { formatSchema } = await import(
|
|
636
|
+
"../tools/discover-schema.js"
|
|
637
|
+
);
|
|
638
|
+
const result = formatSchema("metrics");
|
|
639
|
+
|
|
640
|
+
expect(result).toContain("## Available Metrics");
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
describe("when category is aggregations", () => {
|
|
645
|
+
it("returns aggregation types", async () => {
|
|
646
|
+
const { formatSchema } = await import(
|
|
647
|
+
"../tools/discover-schema.js"
|
|
648
|
+
);
|
|
649
|
+
const result = formatSchema("aggregations");
|
|
650
|
+
|
|
651
|
+
expect(result).toContain("## Available Aggregation Types");
|
|
652
|
+
expect(result).toContain("cardinality");
|
|
653
|
+
expect(result).toContain("avg");
|
|
654
|
+
expect(result).toContain("sum");
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
describe("when category is groups", () => {
|
|
659
|
+
it("returns group-by options", async () => {
|
|
660
|
+
const { formatSchema } = await import(
|
|
661
|
+
"../tools/discover-schema.js"
|
|
662
|
+
);
|
|
663
|
+
const result = formatSchema("groups");
|
|
664
|
+
|
|
665
|
+
expect(result).toContain("## Available Group-By Options");
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
describe("when category is scenarios", () => {
|
|
670
|
+
it("returns scenario schema documentation", async () => {
|
|
671
|
+
const { formatScenarioSchema } = await import(
|
|
672
|
+
"../tools/discover-scenario-schema.js"
|
|
673
|
+
);
|
|
674
|
+
const result = formatScenarioSchema();
|
|
675
|
+
|
|
676
|
+
expect(result).toContain("# Scenario Schema");
|
|
677
|
+
expect(result).toContain("**name** (required)");
|
|
678
|
+
expect(result).toContain("**situation** (required)");
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
describe("when category is evaluators", () => {
|
|
683
|
+
it("returns evaluator type overview", async () => {
|
|
684
|
+
const { formatEvaluatorSchema } = await import(
|
|
685
|
+
"../tools/discover-evaluator-schema.js"
|
|
686
|
+
);
|
|
687
|
+
const result = formatEvaluatorSchema();
|
|
688
|
+
|
|
689
|
+
expect(result).toContain("# Available Evaluator Types");
|
|
690
|
+
});
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
describe("when category is evaluators with specific type", () => {
|
|
694
|
+
it("returns detailed evaluator schema", async () => {
|
|
695
|
+
const { formatEvaluatorSchema } = await import(
|
|
696
|
+
"../tools/discover-evaluator-schema.js"
|
|
697
|
+
);
|
|
698
|
+
const result = formatEvaluatorSchema("langevals/llm_boolean");
|
|
699
|
+
|
|
700
|
+
expect(result).toContain("langevals/llm_boolean");
|
|
701
|
+
expect(result).toContain("## Settings");
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
describe("when evaluator type is unknown", () => {
|
|
706
|
+
it("returns an error message", async () => {
|
|
707
|
+
const { formatEvaluatorSchema } = await import(
|
|
708
|
+
"../tools/discover-evaluator-schema.js"
|
|
709
|
+
);
|
|
710
|
+
const result = formatEvaluatorSchema("nonexistent/type");
|
|
711
|
+
|
|
712
|
+
expect(result).toContain('Unknown evaluator type');
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
describe("when category is all", () => {
|
|
717
|
+
it("returns all schema categories", async () => {
|
|
718
|
+
const { formatSchema } = await import(
|
|
719
|
+
"../tools/discover-schema.js"
|
|
720
|
+
);
|
|
721
|
+
const result = formatSchema("all");
|
|
722
|
+
|
|
723
|
+
expect(result).toContain("## Available Filter Fields");
|
|
724
|
+
expect(result).toContain("## Available Metrics");
|
|
725
|
+
expect(result).toContain("## Available Aggregation Types");
|
|
726
|
+
expect(result).toContain("## Available Group-By Options");
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
// =====================
|
|
732
|
+
// 4. search_traces
|
|
733
|
+
// =====================
|
|
734
|
+
describe("search_traces", () => {
|
|
735
|
+
describe("when traces are found", () => {
|
|
736
|
+
it("returns formatted trace digests", async () => {
|
|
737
|
+
const { handleSearchTraces } = await import(
|
|
738
|
+
"../tools/search-traces.js"
|
|
739
|
+
);
|
|
740
|
+
const result = await handleSearchTraces({
|
|
741
|
+
startDate: "24h",
|
|
742
|
+
endDate: "now",
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
expect(result).toContain("trace-001");
|
|
746
|
+
expect(result).toContain("LLM Call [llm] 500ms");
|
|
747
|
+
expect(result).toContain("1 trace");
|
|
748
|
+
});
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
describe("when no traces match", () => {
|
|
752
|
+
it("returns a no-results message", async () => {
|
|
753
|
+
const { handleSearchTraces } = await import(
|
|
754
|
+
"../tools/search-traces.js"
|
|
755
|
+
);
|
|
756
|
+
const result = await handleSearchTraces({
|
|
757
|
+
query: "__empty__",
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
expect(result).toBe("No traces found matching your query.");
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
describe("when pagination token is present", () => {
|
|
765
|
+
it("includes scroll ID for next page", async () => {
|
|
766
|
+
const { handleSearchTraces } = await import(
|
|
767
|
+
"../tools/search-traces.js"
|
|
768
|
+
);
|
|
769
|
+
const result = await handleSearchTraces({
|
|
770
|
+
pageSize: 5,
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
expect(result).toContain("scroll-token-abc");
|
|
774
|
+
});
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
describe("when format is json", () => {
|
|
778
|
+
it("returns parseable JSON", async () => {
|
|
779
|
+
const { handleSearchTraces } = await import(
|
|
780
|
+
"../tools/search-traces.js"
|
|
781
|
+
);
|
|
782
|
+
const result = await handleSearchTraces({
|
|
783
|
+
format: "json",
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
const parsed = JSON.parse(result);
|
|
787
|
+
expect(parsed.traces).toBeDefined();
|
|
788
|
+
expect(parsed.traces.length).toBeGreaterThan(0);
|
|
789
|
+
expect(parsed.traces[0].trace_id).toBe("trace-001");
|
|
790
|
+
});
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
describe("when filters are applied", () => {
|
|
794
|
+
it("passes filters to the API", async () => {
|
|
795
|
+
const { handleSearchTraces } = await import(
|
|
796
|
+
"../tools/search-traces.js"
|
|
797
|
+
);
|
|
798
|
+
await handleSearchTraces({
|
|
799
|
+
filters: { "metadata.user_id": ["user-42"] },
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
const req = lastRequests["POST /api/traces/search"];
|
|
803
|
+
expect(req).toBeDefined();
|
|
804
|
+
const parsed = JSON.parse(req!.body);
|
|
805
|
+
expect(parsed.filters).toEqual({
|
|
806
|
+
"metadata.user_id": ["user-42"],
|
|
807
|
+
});
|
|
808
|
+
});
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
// =====================
|
|
813
|
+
// 5. get_trace
|
|
814
|
+
// =====================
|
|
815
|
+
describe("get_trace", () => {
|
|
816
|
+
describe("when trace exists", () => {
|
|
817
|
+
it("returns formatted trace with metadata and evaluations", async () => {
|
|
818
|
+
const { handleGetTrace } = await import(
|
|
819
|
+
"../tools/get-trace.js"
|
|
820
|
+
);
|
|
821
|
+
const result = await handleGetTrace({ traceId: "trace-001" });
|
|
822
|
+
|
|
823
|
+
expect(result).toContain("# Trace: trace-001");
|
|
824
|
+
expect(result).toContain("LLM Call [llm] 500ms");
|
|
825
|
+
expect(result).toContain("Faithfulness");
|
|
826
|
+
expect(result).toContain("PASSED");
|
|
827
|
+
expect(result).toContain("**User**: user-42");
|
|
828
|
+
expect(result).toContain("**Thread**: thread-1");
|
|
829
|
+
expect(result).toContain("**Customer**: cust-100");
|
|
830
|
+
});
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
describe("when trace does not exist", () => {
|
|
834
|
+
it("propagates the 404 error", async () => {
|
|
835
|
+
const { handleGetTrace } = await import(
|
|
836
|
+
"../tools/get-trace.js"
|
|
837
|
+
);
|
|
838
|
+
|
|
839
|
+
await expect(
|
|
840
|
+
handleGetTrace({ traceId: "trace-nonexistent" }),
|
|
841
|
+
).rejects.toThrow("404");
|
|
842
|
+
});
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
describe("when format is json", () => {
|
|
846
|
+
it("returns parseable JSON with full trace data", async () => {
|
|
847
|
+
const { handleGetTrace } = await import(
|
|
848
|
+
"../tools/get-trace.js"
|
|
849
|
+
);
|
|
850
|
+
const result = await handleGetTrace({
|
|
851
|
+
traceId: "trace-001",
|
|
852
|
+
format: "json",
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
const parsed = JSON.parse(result);
|
|
856
|
+
expect(parsed.trace_id).toBe("trace-001");
|
|
857
|
+
expect(parsed.evaluations).toBeDefined();
|
|
858
|
+
expect(parsed.metadata).toBeDefined();
|
|
859
|
+
});
|
|
860
|
+
});
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
// =====================
|
|
864
|
+
// 6. get_analytics
|
|
865
|
+
// =====================
|
|
866
|
+
describe("get_analytics", () => {
|
|
867
|
+
describe("when data is available", () => {
|
|
868
|
+
it("returns formatted analytics with markdown table", async () => {
|
|
869
|
+
const { handleGetAnalytics } = await import(
|
|
870
|
+
"../tools/get-analytics.js"
|
|
871
|
+
);
|
|
872
|
+
const result = await handleGetAnalytics({
|
|
873
|
+
metric: "metadata.trace_id",
|
|
874
|
+
aggregation: "cardinality",
|
|
875
|
+
startDate: "7d",
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
expect(result).toContain("42");
|
|
879
|
+
expect(result).toContain("58");
|
|
880
|
+
expect(result).toContain("| Date | Value |");
|
|
881
|
+
});
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
describe("when metric and aggregation are specified", () => {
|
|
885
|
+
it("passes them through to the API", async () => {
|
|
886
|
+
const { handleGetAnalytics } = await import(
|
|
887
|
+
"../tools/get-analytics.js"
|
|
888
|
+
);
|
|
889
|
+
await handleGetAnalytics({
|
|
890
|
+
metric: "performance.total_cost",
|
|
891
|
+
aggregation: "sum",
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
const req = lastRequests["POST /api/analytics/timeseries"];
|
|
895
|
+
expect(req).toBeDefined();
|
|
896
|
+
const parsed = JSON.parse(req!.body);
|
|
897
|
+
expect(parsed.series[0].metric).toBe("performance.total_cost");
|
|
898
|
+
expect(parsed.series[0].aggregation).toBe("sum");
|
|
899
|
+
});
|
|
900
|
+
});
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
// =====================
|
|
904
|
+
// 7. platform_create_prompt
|
|
905
|
+
// =====================
|
|
906
|
+
describe("platform_create_prompt", () => {
|
|
907
|
+
describe("when valid data is provided", () => {
|
|
908
|
+
it("returns success confirmation with prompt details", async () => {
|
|
909
|
+
const { handleCreatePrompt } = await import(
|
|
910
|
+
"../tools/create-prompt.js"
|
|
911
|
+
);
|
|
912
|
+
const result = await handleCreatePrompt({
|
|
913
|
+
name: "New Prompt",
|
|
914
|
+
messages: [{ role: "system", content: "You are helpful." }],
|
|
915
|
+
model: "openai/gpt-4o",
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
expect(result).toContain("created successfully");
|
|
919
|
+
expect(result).toContain("p-new");
|
|
920
|
+
expect(result).toContain("**Name**: New Prompt");
|
|
921
|
+
expect(result).toContain("**Model**: openai/gpt-4o");
|
|
922
|
+
});
|
|
923
|
+
});
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
// =====================
|
|
927
|
+
// 8. platform_list_prompts
|
|
928
|
+
// =====================
|
|
929
|
+
describe("platform_list_prompts", () => {
|
|
930
|
+
describe("when prompts exist", () => {
|
|
931
|
+
it("returns formatted prompt list", async () => {
|
|
932
|
+
const { handleListPrompts } = await import(
|
|
933
|
+
"../tools/list-prompts.js"
|
|
934
|
+
);
|
|
935
|
+
const result = await handleListPrompts();
|
|
936
|
+
|
|
937
|
+
expect(result).toContain("greeting-bot");
|
|
938
|
+
expect(result).toContain("Greeting Bot");
|
|
939
|
+
expect(result).toContain("qa-assistant");
|
|
940
|
+
expect(result).toContain("QA Assistant");
|
|
941
|
+
expect(result).toContain("# Prompts (2 total)");
|
|
942
|
+
});
|
|
943
|
+
});
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
// =====================
|
|
947
|
+
// 9. platform_get_prompt
|
|
948
|
+
// =====================
|
|
949
|
+
describe("platform_get_prompt", () => {
|
|
950
|
+
describe("when prompt exists", () => {
|
|
951
|
+
it("returns formatted prompt details with messages and versions", async () => {
|
|
952
|
+
const { handleGetPrompt } = await import(
|
|
953
|
+
"../tools/get-prompt.js"
|
|
954
|
+
);
|
|
955
|
+
const result = await handleGetPrompt({
|
|
956
|
+
idOrHandle: "greeting-bot",
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
expect(result).toContain("# Prompt: Greeting Bot");
|
|
960
|
+
expect(result).toContain("gpt-4o");
|
|
961
|
+
expect(result).toContain("You are a friendly bot.");
|
|
962
|
+
expect(result).toContain("v3");
|
|
963
|
+
expect(result).toContain("## Version History");
|
|
964
|
+
expect(result).toContain("Updated tone");
|
|
965
|
+
});
|
|
966
|
+
});
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
// =====================
|
|
970
|
+
// 10. platform_update_prompt
|
|
971
|
+
// =====================
|
|
972
|
+
describe("platform_update_prompt", () => {
|
|
973
|
+
describe("when updating a prompt", () => {
|
|
974
|
+
it("returns success message", async () => {
|
|
975
|
+
const { handleUpdatePrompt } = await import(
|
|
976
|
+
"../tools/update-prompt.js"
|
|
977
|
+
);
|
|
978
|
+
const result = await handleUpdatePrompt({
|
|
979
|
+
idOrHandle: "greeting-bot",
|
|
980
|
+
model: "openai/gpt-4o-mini",
|
|
981
|
+
commitMessage: "Switch to mini",
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
expect(result).toContain("updated successfully");
|
|
985
|
+
expect(result).toContain("Switch to mini");
|
|
986
|
+
});
|
|
987
|
+
});
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
// =====================
|
|
991
|
+
// 11. platform_create_scenario
|
|
992
|
+
// =====================
|
|
993
|
+
describe("platform_create_scenario", () => {
|
|
994
|
+
describe("when valid data is provided", () => {
|
|
995
|
+
it("returns confirmation with new scenario ID", async () => {
|
|
996
|
+
const { handleCreateScenario } = await import(
|
|
997
|
+
"../tools/create-scenario.js"
|
|
998
|
+
);
|
|
999
|
+
const result = await handleCreateScenario({
|
|
1000
|
+
name: "New Scenario",
|
|
1001
|
+
situation: "User does something",
|
|
1002
|
+
criteria: ["Agent responds correctly"],
|
|
1003
|
+
labels: ["test"],
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
expect(result).toContain("created successfully");
|
|
1007
|
+
expect(result).toContain("scen_new789");
|
|
1008
|
+
});
|
|
1009
|
+
});
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
// =====================
|
|
1013
|
+
// 12. platform_list_scenarios
|
|
1014
|
+
// =====================
|
|
1015
|
+
describe("platform_list_scenarios", () => {
|
|
1016
|
+
describe("when scenarios exist", () => {
|
|
1017
|
+
it("returns formatted scenario list", async () => {
|
|
1018
|
+
const { handleListScenarios } = await import(
|
|
1019
|
+
"../tools/list-scenarios.js"
|
|
1020
|
+
);
|
|
1021
|
+
const result = await handleListScenarios({});
|
|
1022
|
+
|
|
1023
|
+
expect(result).toContain("# Scenarios (2 total)");
|
|
1024
|
+
expect(result).toContain("Login Flow Happy Path");
|
|
1025
|
+
expect(result).toContain("Password Reset");
|
|
1026
|
+
});
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
describe("when format is json", () => {
|
|
1030
|
+
it("returns parseable JSON matching API response", async () => {
|
|
1031
|
+
const { handleListScenarios } = await import(
|
|
1032
|
+
"../tools/list-scenarios.js"
|
|
1033
|
+
);
|
|
1034
|
+
const result = await handleListScenarios({ format: "json" });
|
|
1035
|
+
|
|
1036
|
+
expect(JSON.parse(result)).toEqual(CANNED_SCENARIOS_LIST);
|
|
1037
|
+
});
|
|
1038
|
+
});
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
// =====================
|
|
1042
|
+
// 13. platform_get_scenario
|
|
1043
|
+
// =====================
|
|
1044
|
+
describe("platform_get_scenario", () => {
|
|
1045
|
+
describe("when the scenario exists", () => {
|
|
1046
|
+
it("returns formatted scenario details", async () => {
|
|
1047
|
+
const { handleGetScenario } = await import(
|
|
1048
|
+
"../tools/get-scenario.js"
|
|
1049
|
+
);
|
|
1050
|
+
const result = await handleGetScenario({
|
|
1051
|
+
scenarioId: "scen_abc123",
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
expect(result).toContain("# Scenario: Login Flow Happy Path");
|
|
1055
|
+
expect(result).toContain("User attempts to log in");
|
|
1056
|
+
expect(result).toContain("Responds with a welcome message");
|
|
1057
|
+
});
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
describe("when the scenario does not exist", () => {
|
|
1061
|
+
it("propagates the 404 error", async () => {
|
|
1062
|
+
const { handleGetScenario } = await import(
|
|
1063
|
+
"../tools/get-scenario.js"
|
|
1064
|
+
);
|
|
1065
|
+
|
|
1066
|
+
await expect(
|
|
1067
|
+
handleGetScenario({ scenarioId: "scen_nonexistent" }),
|
|
1068
|
+
).rejects.toThrow("404");
|
|
1069
|
+
});
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
describe("when format is json", () => {
|
|
1073
|
+
it("returns parseable JSON", async () => {
|
|
1074
|
+
const { handleGetScenario } = await import(
|
|
1075
|
+
"../tools/get-scenario.js"
|
|
1076
|
+
);
|
|
1077
|
+
const result = await handleGetScenario({
|
|
1078
|
+
scenarioId: "scen_abc123",
|
|
1079
|
+
format: "json",
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
expect(JSON.parse(result)).toEqual(CANNED_SCENARIO_DETAIL);
|
|
1083
|
+
});
|
|
1084
|
+
});
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
// =====================
|
|
1088
|
+
// 14. platform_update_scenario
|
|
1089
|
+
// =====================
|
|
1090
|
+
describe("platform_update_scenario", () => {
|
|
1091
|
+
describe("when the scenario exists", () => {
|
|
1092
|
+
it("returns update confirmation with updated details", async () => {
|
|
1093
|
+
const { handleUpdateScenario } = await import(
|
|
1094
|
+
"../tools/update-scenario.js"
|
|
1095
|
+
);
|
|
1096
|
+
const result = await handleUpdateScenario({
|
|
1097
|
+
scenarioId: "scen_abc123",
|
|
1098
|
+
name: "Login Flow - Updated",
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
expect(result).toContain("updated successfully");
|
|
1102
|
+
expect(result).toContain("scen_abc123");
|
|
1103
|
+
});
|
|
1104
|
+
});
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
// =====================
|
|
1108
|
+
// 15. platform_archive_scenario
|
|
1109
|
+
// =====================
|
|
1110
|
+
describe("platform_archive_scenario", () => {
|
|
1111
|
+
describe("when the scenario exists", () => {
|
|
1112
|
+
it("returns confirmation that scenario was archived", async () => {
|
|
1113
|
+
const { handleArchiveScenario } = await import(
|
|
1114
|
+
"../tools/archive-scenario.js"
|
|
1115
|
+
);
|
|
1116
|
+
const result = await handleArchiveScenario({
|
|
1117
|
+
scenarioId: "scen_abc123",
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
expect(result).toContain("archived successfully");
|
|
1121
|
+
expect(result).toContain("scen_abc123");
|
|
1122
|
+
expect(result).toContain("archived");
|
|
1123
|
+
});
|
|
1124
|
+
});
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
// =====================
|
|
1128
|
+
// 16. platform_create_evaluator
|
|
1129
|
+
// =====================
|
|
1130
|
+
describe("platform_create_evaluator", () => {
|
|
1131
|
+
describe("when valid data is provided", () => {
|
|
1132
|
+
it("returns success confirmation with evaluator details", async () => {
|
|
1133
|
+
const { handleCreateEvaluator } = await import(
|
|
1134
|
+
"../tools/create-evaluator.js"
|
|
1135
|
+
);
|
|
1136
|
+
const result = await handleCreateEvaluator({
|
|
1137
|
+
name: "My LLM Judge",
|
|
1138
|
+
config: { evaluatorType: "langevals/llm_boolean" },
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
expect(result).toContain("Evaluator created successfully!");
|
|
1142
|
+
expect(result).toContain("evaluator_new123");
|
|
1143
|
+
expect(result).toContain("my-llm-judge");
|
|
1144
|
+
expect(result).toContain("langevals/llm_boolean");
|
|
1145
|
+
});
|
|
1146
|
+
});
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
// =====================
|
|
1150
|
+
// 17. platform_list_evaluators
|
|
1151
|
+
// =====================
|
|
1152
|
+
describe("platform_list_evaluators", () => {
|
|
1153
|
+
describe("when evaluators exist", () => {
|
|
1154
|
+
it("returns formatted evaluator list", async () => {
|
|
1155
|
+
const { handleListEvaluators } = await import(
|
|
1156
|
+
"../tools/list-evaluators.js"
|
|
1157
|
+
);
|
|
1158
|
+
const result = await handleListEvaluators();
|
|
1159
|
+
|
|
1160
|
+
expect(result).toContain("# Evaluators (2 total)");
|
|
1161
|
+
expect(result).toContain("Toxicity Check");
|
|
1162
|
+
expect(result).toContain("toxicity-check");
|
|
1163
|
+
expect(result).toContain("openai/moderation");
|
|
1164
|
+
expect(result).toContain("Exact Match");
|
|
1165
|
+
expect(result).toContain("exact-match");
|
|
1166
|
+
});
|
|
1167
|
+
});
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
// =====================
|
|
1171
|
+
// 18. platform_get_evaluator
|
|
1172
|
+
// =====================
|
|
1173
|
+
describe("platform_get_evaluator", () => {
|
|
1174
|
+
describe("when the evaluator exists", () => {
|
|
1175
|
+
it("returns formatted evaluator details with config and fields", async () => {
|
|
1176
|
+
const { handleGetEvaluator } = await import(
|
|
1177
|
+
"../tools/get-evaluator.js"
|
|
1178
|
+
);
|
|
1179
|
+
const result = await handleGetEvaluator({
|
|
1180
|
+
idOrSlug: "evaluator_abc123",
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
expect(result).toContain("# Evaluator: Toxicity Check");
|
|
1184
|
+
expect(result).toContain("openai/moderation");
|
|
1185
|
+
expect(result).toContain("text-moderation-stable");
|
|
1186
|
+
expect(result).toContain("## Input Fields");
|
|
1187
|
+
expect(result).toContain("**input** (str)");
|
|
1188
|
+
expect(result).toContain("## Output Fields");
|
|
1189
|
+
expect(result).toContain("**passed** (bool)");
|
|
1190
|
+
expect(result).toContain("**score** (float)");
|
|
1191
|
+
});
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
describe("when the evaluator does not exist", () => {
|
|
1195
|
+
it("propagates the 404 error", async () => {
|
|
1196
|
+
const { handleGetEvaluator } = await import(
|
|
1197
|
+
"../tools/get-evaluator.js"
|
|
1198
|
+
);
|
|
1199
|
+
|
|
1200
|
+
await expect(
|
|
1201
|
+
handleGetEvaluator({ idOrSlug: "evaluator_nonexistent" }),
|
|
1202
|
+
).rejects.toThrow("404");
|
|
1203
|
+
});
|
|
1204
|
+
});
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
// =====================
|
|
1208
|
+
// 19. platform_update_evaluator
|
|
1209
|
+
// =====================
|
|
1210
|
+
describe("platform_update_evaluator", () => {
|
|
1211
|
+
describe("when the evaluator exists", () => {
|
|
1212
|
+
it("returns update confirmation", async () => {
|
|
1213
|
+
const { handleUpdateEvaluator } = await import(
|
|
1214
|
+
"../tools/update-evaluator.js"
|
|
1215
|
+
);
|
|
1216
|
+
const result = await handleUpdateEvaluator({
|
|
1217
|
+
evaluatorId: "evaluator_abc123",
|
|
1218
|
+
name: "Updated Toxicity",
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
expect(result).toContain("Evaluator updated successfully!");
|
|
1222
|
+
expect(result).toContain("evaluator_abc123");
|
|
1223
|
+
expect(result).toContain("Updated Toxicity");
|
|
1224
|
+
});
|
|
1225
|
+
});
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
// =====================
|
|
1229
|
+
// 20. platform_set_model_provider
|
|
1230
|
+
// =====================
|
|
1231
|
+
describe("platform_set_model_provider", () => {
|
|
1232
|
+
describe("when setting a provider with API key", () => {
|
|
1233
|
+
it("returns success confirmation with provider details", async () => {
|
|
1234
|
+
const { handleSetModelProvider } = await import(
|
|
1235
|
+
"../tools/set-model-provider.js"
|
|
1236
|
+
);
|
|
1237
|
+
const result = await handleSetModelProvider({
|
|
1238
|
+
provider: "openai",
|
|
1239
|
+
enabled: true,
|
|
1240
|
+
customKeys: { OPENAI_API_KEY: "sk-test123" },
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
expect(result).toContain("Model provider updated successfully!");
|
|
1244
|
+
expect(result).toContain("**Provider**: openai");
|
|
1245
|
+
expect(result).toContain("**Status**: enabled");
|
|
1246
|
+
expect(result).toContain("OPENAI_API_KEY: set");
|
|
1247
|
+
});
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
describe("when setting a default model", () => {
|
|
1251
|
+
it("shows the normalized model name with provider prefix", async () => {
|
|
1252
|
+
const { handleSetModelProvider } = await import(
|
|
1253
|
+
"../tools/set-model-provider.js"
|
|
1254
|
+
);
|
|
1255
|
+
const result = await handleSetModelProvider({
|
|
1256
|
+
provider: "openai",
|
|
1257
|
+
enabled: true,
|
|
1258
|
+
defaultModel: "gpt-4o",
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
expect(result).toContain("**Default Model**: openai/gpt-4o");
|
|
1262
|
+
});
|
|
1263
|
+
});
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
// =====================
|
|
1267
|
+
// 21. platform_list_model_providers
|
|
1268
|
+
// =====================
|
|
1269
|
+
describe("platform_list_model_providers", () => {
|
|
1270
|
+
describe("when providers exist", () => {
|
|
1271
|
+
it("returns formatted provider list with status and key info", async () => {
|
|
1272
|
+
const { handleListModelProviders } = await import(
|
|
1273
|
+
"../tools/list-model-providers.js"
|
|
1274
|
+
);
|
|
1275
|
+
const result = await handleListModelProviders();
|
|
1276
|
+
|
|
1277
|
+
expect(result).toContain("# Model Providers (2 total)");
|
|
1278
|
+
expect(result).toContain("## openai");
|
|
1279
|
+
expect(result).toContain("**Status**: enabled");
|
|
1280
|
+
expect(result).toContain("## anthropic");
|
|
1281
|
+
expect(result).toContain("**Status**: disabled");
|
|
1282
|
+
expect(result).toContain("OPENAI_API_KEY: set");
|
|
1283
|
+
expect(result).toContain("2 available");
|
|
1284
|
+
});
|
|
1285
|
+
});
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
// =====================
|
|
1289
|
+
// Cross-cutting: authentication
|
|
1290
|
+
// =====================
|
|
1291
|
+
describe("when API key is invalid", () => {
|
|
1292
|
+
afterAll(() => {
|
|
1293
|
+
initConfig({
|
|
1294
|
+
apiKey: "test-integration-key",
|
|
1295
|
+
endpoint: `http://localhost:${port}`,
|
|
1296
|
+
});
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
it("throws an error with 401 status for trace search", async () => {
|
|
1300
|
+
initConfig({
|
|
1301
|
+
apiKey: "bad-key",
|
|
1302
|
+
endpoint: `http://localhost:${port}`,
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
const { handleSearchTraces } = await import(
|
|
1306
|
+
"../tools/search-traces.js"
|
|
1307
|
+
);
|
|
1308
|
+
await expect(
|
|
1309
|
+
handleSearchTraces({ startDate: "24h" }),
|
|
1310
|
+
).rejects.toThrow("401");
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
it("throws an error with 401 status for evaluator list", async () => {
|
|
1314
|
+
initConfig({
|
|
1315
|
+
apiKey: "bad-key",
|
|
1316
|
+
endpoint: `http://localhost:${port}`,
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
const { handleListEvaluators } = await import(
|
|
1320
|
+
"../tools/list-evaluators.js"
|
|
1321
|
+
);
|
|
1322
|
+
await expect(handleListEvaluators()).rejects.toThrow("401");
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
it("throws an error with 401 status for model providers list", async () => {
|
|
1326
|
+
initConfig({
|
|
1327
|
+
apiKey: "bad-key",
|
|
1328
|
+
endpoint: `http://localhost:${port}`,
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
const { handleListModelProviders } = await import(
|
|
1332
|
+
"../tools/list-model-providers.js"
|
|
1333
|
+
);
|
|
1334
|
+
await expect(handleListModelProviders()).rejects.toThrow("401");
|
|
1335
|
+
});
|
|
1336
|
+
});
|
|
1337
|
+
});
|