@jmoak/vibrissa 0.1.0 → 2.0.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/LICENSE +21 -21
- package/README.md +268 -254
- package/bin/vib-test.js +1 -1
- package/dist/chunk-AMDRQ2RV.js +2329 -0
- package/dist/chunk-AMDRQ2RV.js.map +1 -0
- package/dist/chunk-TZK5KIGQ.js +10706 -0
- package/dist/chunk-TZK5KIGQ.js.map +1 -0
- package/dist/cli.d.ts +57 -0
- package/dist/cli.js +5896 -613
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +837 -58
- package/dist/index.js +211 -10
- package/dist/index.js.map +1 -1
- package/dist/runner-J_V7pgax.d.ts +1194 -0
- package/dist/vitest.d.ts +44 -0
- package/dist/vitest.js +240 -0
- package/dist/vitest.js.map +1 -0
- package/package.json +27 -9
- package/schema/case.schema.json +405 -14
- package/schema/vibrissa.schema.json +528 -12
- package/dist/chunk-FTVETYUK.js +0 -656
- package/dist/chunk-FTVETYUK.js.map +0 -1
package/dist/chunk-FTVETYUK.js
DELETED
|
@@ -1,656 +0,0 @@
|
|
|
1
|
-
// src/mcp-session.ts
|
|
2
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
-
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
4
|
-
import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
-
var CLIENT_INFO = { name: "vibrissa", version: "0.1.0" };
|
|
6
|
-
function inheritedEnv(overrides) {
|
|
7
|
-
const env = {};
|
|
8
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
9
|
-
if (typeof value === "string") env[key] = value;
|
|
10
|
-
}
|
|
11
|
-
return { ...env, ...overrides };
|
|
12
|
-
}
|
|
13
|
-
function errorCodeName(code) {
|
|
14
|
-
const name = ErrorCode[code];
|
|
15
|
-
return typeof name === "string" ? name : void 0;
|
|
16
|
-
}
|
|
17
|
-
function textFromContent(content) {
|
|
18
|
-
if (!Array.isArray(content)) return "";
|
|
19
|
-
return content.filter((item) => {
|
|
20
|
-
return typeof item === "object" && item !== null && item.type === "text" && typeof item.text === "string";
|
|
21
|
-
}).map((item) => item.text).join("\n");
|
|
22
|
-
}
|
|
23
|
-
function stripMcpErrorPrefix(message) {
|
|
24
|
-
return message.replace(/^MCP error -?\d+:\s*/, "");
|
|
25
|
-
}
|
|
26
|
-
async function withTimeout(promise, timeoutMs, message) {
|
|
27
|
-
let handle;
|
|
28
|
-
const timeout = new Promise((_, reject) => {
|
|
29
|
-
handle = setTimeout(() => reject(new Error(message)), timeoutMs);
|
|
30
|
-
});
|
|
31
|
-
try {
|
|
32
|
-
return await Promise.race([promise, timeout]);
|
|
33
|
-
} finally {
|
|
34
|
-
if (handle) clearTimeout(handle);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
var McpSession = class _McpSession {
|
|
38
|
-
constructor(client) {
|
|
39
|
-
this.client = client;
|
|
40
|
-
}
|
|
41
|
-
static async start(server, timeoutMs) {
|
|
42
|
-
const transport = new StdioClientTransport({
|
|
43
|
-
command: server.cmd,
|
|
44
|
-
args: server.args ?? [],
|
|
45
|
-
cwd: server.cwd ?? ".",
|
|
46
|
-
env: inheritedEnv(server.env ?? {}),
|
|
47
|
-
stderr: "inherit"
|
|
48
|
-
});
|
|
49
|
-
const client = new Client(CLIENT_INFO, { capabilities: {} });
|
|
50
|
-
try {
|
|
51
|
-
await withTimeout(client.connect(transport), timeoutMs, "Server spawn timeout");
|
|
52
|
-
} catch (err) {
|
|
53
|
-
await transport.close().catch(() => {
|
|
54
|
-
});
|
|
55
|
-
if (err instanceof McpError && err.code === ErrorCode.ConnectionClosed) {
|
|
56
|
-
throw new Error(`Server exited early before completing MCP initialization: ${err.message}`);
|
|
57
|
-
}
|
|
58
|
-
throw err;
|
|
59
|
-
}
|
|
60
|
-
return new _McpSession(client);
|
|
61
|
-
}
|
|
62
|
-
async callTool(name, args, timeoutMs) {
|
|
63
|
-
try {
|
|
64
|
-
const result = await this.client.callTool({ name, arguments: args }, void 0, {
|
|
65
|
-
timeout: timeoutMs
|
|
66
|
-
});
|
|
67
|
-
if (result.isError) {
|
|
68
|
-
return {
|
|
69
|
-
kind: "error",
|
|
70
|
-
error: { kind: "tool", message: textFromContent(result.content) }
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
return { kind: "result", result };
|
|
74
|
-
} catch (err) {
|
|
75
|
-
if (err instanceof McpError) {
|
|
76
|
-
return {
|
|
77
|
-
kind: "error",
|
|
78
|
-
error: {
|
|
79
|
-
kind: "protocol",
|
|
80
|
-
code: err.code,
|
|
81
|
-
codeName: errorCodeName(err.code),
|
|
82
|
-
message: stripMcpErrorPrefix(err.message)
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
throw err;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
async listTools() {
|
|
90
|
-
const { tools } = await this.client.listTools();
|
|
91
|
-
return tools.map((tool) => ({
|
|
92
|
-
name: tool.name,
|
|
93
|
-
description: tool.description,
|
|
94
|
-
inputSchema: tool.inputSchema
|
|
95
|
-
}));
|
|
96
|
-
}
|
|
97
|
-
async close() {
|
|
98
|
-
await this.client.close().catch(() => {
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
// src/defaults.ts
|
|
104
|
-
var defaultRunCasesOptions = {
|
|
105
|
-
server: { cmd: "node", args: ["dist/index.js"], cwd: ".", env: {} },
|
|
106
|
-
globs: ["tests/integration/**/*.json"],
|
|
107
|
-
timeoutMs: 15e3,
|
|
108
|
-
failFast: false,
|
|
109
|
-
display: "pretty",
|
|
110
|
-
allowEmpty: false
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
// src/display.ts
|
|
114
|
-
import { diffStringsUnified } from "jest-diff";
|
|
115
|
-
var ConsoleResultsDisplay = class {
|
|
116
|
-
onStart(_total) {
|
|
117
|
-
}
|
|
118
|
-
onCasePass(_name) {
|
|
119
|
-
}
|
|
120
|
-
onCaseFail(name, error) {
|
|
121
|
-
console.error(`Case failed: ${name}${error ? ` - ${error}` : ""}`);
|
|
122
|
-
}
|
|
123
|
-
onComplete(_summary) {
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
var RESET = "\x1B[0m";
|
|
127
|
-
var BOLD = "\x1B[1m";
|
|
128
|
-
var FG_RED = "\x1B[31m";
|
|
129
|
-
var FG_GREEN = "\x1B[32m";
|
|
130
|
-
var FG_CYAN = "\x1B[36m";
|
|
131
|
-
var FG_GRAY = "\x1B[90m";
|
|
132
|
-
function color(text, code) {
|
|
133
|
-
return `${code}${text}${RESET}`;
|
|
134
|
-
}
|
|
135
|
-
function visibleLength(text) {
|
|
136
|
-
return text.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
137
|
-
}
|
|
138
|
-
function boxLine(content, width) {
|
|
139
|
-
const pad = Math.max(0, width - 2 - visibleLength(content));
|
|
140
|
-
return `|${content}${" ".repeat(pad)}|`;
|
|
141
|
-
}
|
|
142
|
-
function padCenter(text, width) {
|
|
143
|
-
if (text.length >= width) return text.slice(0, width);
|
|
144
|
-
const totalPad = width - text.length;
|
|
145
|
-
const left = Math.floor(totalPad / 2);
|
|
146
|
-
const right = totalPad - left;
|
|
147
|
-
return `${" ".repeat(left)}${text}${" ".repeat(right)}`;
|
|
148
|
-
}
|
|
149
|
-
function formatDuration(ms) {
|
|
150
|
-
if (ms < 1e3) return `${ms}ms`;
|
|
151
|
-
return `${(ms / 1e3).toFixed(2)}s`;
|
|
152
|
-
}
|
|
153
|
-
var PrettyConsoleResultsDisplay = class {
|
|
154
|
-
total = 0;
|
|
155
|
-
startedAt = 0;
|
|
156
|
-
passed = [];
|
|
157
|
-
failed = [];
|
|
158
|
-
cases = [];
|
|
159
|
-
onStart(total) {
|
|
160
|
-
this.total = total;
|
|
161
|
-
this.startedAt = Date.now();
|
|
162
|
-
}
|
|
163
|
-
onCasePass(name) {
|
|
164
|
-
this.passed.push(name);
|
|
165
|
-
this.cases.push({ name, status: "pass" });
|
|
166
|
-
const badge = color("[OK]", FG_GREEN);
|
|
167
|
-
const nameColored = color(name, FG_CYAN);
|
|
168
|
-
console.log(`${badge} ${nameColored}`);
|
|
169
|
-
}
|
|
170
|
-
onCaseFail(name, error) {
|
|
171
|
-
this.failed.push({ name, error });
|
|
172
|
-
this.cases.push({ name, status: "fail", error });
|
|
173
|
-
const badge = color("[!!]", FG_RED);
|
|
174
|
-
const nameColored = color(name, FG_CYAN);
|
|
175
|
-
console.log(`${badge} ${nameColored}`);
|
|
176
|
-
}
|
|
177
|
-
onComplete(summary) {
|
|
178
|
-
const width = 64;
|
|
179
|
-
const top = `+${"=".repeat(width - 2)}+`;
|
|
180
|
-
const mid = `+${"-".repeat(width - 2)}+`;
|
|
181
|
-
const title = boxLine(color(padCenter("vib-test results", width - 2), BOLD + FG_CYAN), width);
|
|
182
|
-
const totals = [
|
|
183
|
-
` Total: ${color(String(summary.total), FG_CYAN)} `,
|
|
184
|
-
` Passed: ${color(String(summary.passed), FG_GREEN)} `,
|
|
185
|
-
` Failed: ${color(String(summary.failed), FG_RED)} `,
|
|
186
|
-
` Duration: ${color(formatDuration(summary.durationMs), FG_GRAY)}`
|
|
187
|
-
].join(" |");
|
|
188
|
-
console.log(top);
|
|
189
|
-
console.log(title);
|
|
190
|
-
console.log(mid);
|
|
191
|
-
console.log(boxLine(totals, width));
|
|
192
|
-
if (this.failed.length > 0) {
|
|
193
|
-
console.log(mid);
|
|
194
|
-
console.log(color(boxLine(` Failures (${this.failed.length}):`, width), FG_RED));
|
|
195
|
-
const maxList = 10;
|
|
196
|
-
for (let i = 0; i < Math.min(this.failed.length, maxList); i++) {
|
|
197
|
-
const f = this.failed[i];
|
|
198
|
-
const lineRaw = ` - ${f.name}`;
|
|
199
|
-
const maxContent = width - 3;
|
|
200
|
-
const trimmed = lineRaw.length > maxContent ? `${lineRaw.slice(0, maxContent - 3)}...` : lineRaw;
|
|
201
|
-
console.log(color(boxLine(` ${trimmed}`, width), FG_RED));
|
|
202
|
-
if (f.error?.includes("::")) {
|
|
203
|
-
const idx = f.error.indexOf("::");
|
|
204
|
-
try {
|
|
205
|
-
const json = f.error.slice(idx + 2).trim();
|
|
206
|
-
const parsed = JSON.parse(json);
|
|
207
|
-
const expectedStr = JSON.stringify(parsed.expected, null, 2);
|
|
208
|
-
const actualStr = JSON.stringify(parsed.actual, null, 2);
|
|
209
|
-
const unified = diffStringsUnified(expectedStr, actualStr, {
|
|
210
|
-
aAnnotation: "expected",
|
|
211
|
-
bAnnotation: "actual",
|
|
212
|
-
expand: false,
|
|
213
|
-
contextLines: 3
|
|
214
|
-
});
|
|
215
|
-
const diffLines = (unified ?? "").split(/\r?\n/);
|
|
216
|
-
for (const d of diffLines) console.log(`|${d}`);
|
|
217
|
-
} catch {
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
if (this.failed.length > maxList) {
|
|
222
|
-
console.log(color(boxLine(` ...and ${this.failed.length - maxList} more`, width), FG_RED));
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
console.log(top);
|
|
226
|
-
}
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
// src/match.ts
|
|
230
|
-
var MAX_MISMATCHES = 25;
|
|
231
|
-
function matchValue(expected, actual) {
|
|
232
|
-
const ctx = { partial: false, mismatches: [] };
|
|
233
|
-
matchNode(expected, actual, "", ctx);
|
|
234
|
-
return ctx.mismatches;
|
|
235
|
-
}
|
|
236
|
-
function matches(expected, actual) {
|
|
237
|
-
return matchValue(expected, actual).length === 0;
|
|
238
|
-
}
|
|
239
|
-
function report(ctx, mismatch) {
|
|
240
|
-
if (ctx.mismatches.length < MAX_MISMATCHES) ctx.mismatches.push(mismatch);
|
|
241
|
-
}
|
|
242
|
-
function displayPath(path3) {
|
|
243
|
-
return path3 === "" ? "(root)" : path3;
|
|
244
|
-
}
|
|
245
|
-
function describeType(value) {
|
|
246
|
-
if (value === null) return "null";
|
|
247
|
-
if (Array.isArray(value)) return "array";
|
|
248
|
-
return typeof value;
|
|
249
|
-
}
|
|
250
|
-
function isPlainObject(value) {
|
|
251
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
252
|
-
}
|
|
253
|
-
function matchNode(expected, actual, path3, ctx) {
|
|
254
|
-
if (isPlainObject(expected)) {
|
|
255
|
-
if ("$regex" in expected) {
|
|
256
|
-
matchRegexDirective(expected, actual, path3, ctx);
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
if ("$wildcard" in expected) {
|
|
260
|
-
matchWildcardDirective(expected, actual, path3, ctx);
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
if (expected.$unordered === true && Array.isArray(expected.value)) {
|
|
264
|
-
matchUnordered(expected.value, actual, path3, ctx);
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
matchObject(expected, actual, path3, ctx);
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
if (Array.isArray(expected)) {
|
|
271
|
-
matchArray(expected, actual, path3, ctx);
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
if (!Object.is(expected, actual)) {
|
|
275
|
-
report(ctx, {
|
|
276
|
-
path: displayPath(path3),
|
|
277
|
-
message: `expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
|
|
278
|
-
expected,
|
|
279
|
-
actual
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
function matchRegexDirective(directive, actual, path3, ctx) {
|
|
284
|
-
const pattern = directive.$regex;
|
|
285
|
-
const flags = directive.flags;
|
|
286
|
-
if (typeof pattern !== "string" || flags !== void 0 && typeof flags !== "string") {
|
|
287
|
-
report(ctx, {
|
|
288
|
-
path: displayPath(path3),
|
|
289
|
-
message: `invalid $regex directive: ${JSON.stringify(directive)}`,
|
|
290
|
-
expected: directive,
|
|
291
|
-
actual
|
|
292
|
-
});
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
if (typeof actual !== "string") {
|
|
296
|
-
report(ctx, {
|
|
297
|
-
path: displayPath(path3),
|
|
298
|
-
message: `expected a string matching /${pattern}/${flags ?? ""}, got ${describeType(actual)}`,
|
|
299
|
-
expected: directive,
|
|
300
|
-
actual
|
|
301
|
-
});
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
let regex;
|
|
305
|
-
try {
|
|
306
|
-
regex = new RegExp(pattern, flags);
|
|
307
|
-
} catch (err) {
|
|
308
|
-
report(ctx, {
|
|
309
|
-
path: displayPath(path3),
|
|
310
|
-
message: `invalid $regex pattern /${pattern}/${flags ?? ""}: ${err instanceof Error ? err.message : String(err)}`,
|
|
311
|
-
expected: directive,
|
|
312
|
-
actual
|
|
313
|
-
});
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
if (!regex.test(actual)) {
|
|
317
|
-
report(ctx, {
|
|
318
|
-
path: displayPath(path3),
|
|
319
|
-
message: `expected string matching /${pattern}/${flags ?? ""}, got ${JSON.stringify(actual)}`,
|
|
320
|
-
expected: directive,
|
|
321
|
-
actual
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
function wildcardToRegex(pattern) {
|
|
326
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
327
|
-
const body = escaped.replace(/\*/g, "[\\s\\S]*").replace(/\?/g, "[\\s\\S]");
|
|
328
|
-
return new RegExp(`^${body}$`);
|
|
329
|
-
}
|
|
330
|
-
function matchWildcardDirective(directive, actual, path3, ctx) {
|
|
331
|
-
const pattern = directive.$wildcard;
|
|
332
|
-
if (typeof pattern !== "string") {
|
|
333
|
-
report(ctx, {
|
|
334
|
-
path: displayPath(path3),
|
|
335
|
-
message: `invalid $wildcard directive: ${JSON.stringify(directive)}`,
|
|
336
|
-
expected: directive,
|
|
337
|
-
actual
|
|
338
|
-
});
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
if (typeof actual !== "string") {
|
|
342
|
-
report(ctx, {
|
|
343
|
-
path: displayPath(path3),
|
|
344
|
-
message: `expected a string matching wildcard "${pattern}", got ${describeType(actual)}`,
|
|
345
|
-
expected: directive,
|
|
346
|
-
actual
|
|
347
|
-
});
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
if (!wildcardToRegex(pattern).test(actual)) {
|
|
351
|
-
report(ctx, {
|
|
352
|
-
path: displayPath(path3),
|
|
353
|
-
message: `expected string matching wildcard "${pattern}", got ${JSON.stringify(actual)}`,
|
|
354
|
-
expected: directive,
|
|
355
|
-
actual
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
function matchObject(expected, actual, path3, ctx) {
|
|
360
|
-
if (!isPlainObject(actual)) {
|
|
361
|
-
report(ctx, {
|
|
362
|
-
path: displayPath(path3),
|
|
363
|
-
message: `expected an object, got ${describeType(actual)}`,
|
|
364
|
-
expected,
|
|
365
|
-
actual
|
|
366
|
-
});
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
const partial = ctx.partial || expected.$partial === true;
|
|
370
|
-
const expectedKeys = Object.keys(expected).filter((key) => key !== "$partial");
|
|
371
|
-
if (!partial) {
|
|
372
|
-
const actualKeys = Object.keys(actual);
|
|
373
|
-
const extra = actualKeys.filter((key) => !expectedKeys.includes(key));
|
|
374
|
-
if (extra.length > 0) {
|
|
375
|
-
report(ctx, {
|
|
376
|
-
path: displayPath(path3),
|
|
377
|
-
message: `unexpected key${extra.length > 1 ? "s" : ""} ${extra.map((k) => JSON.stringify(k)).join(", ")} (use "$partial": true to allow extra fields)`,
|
|
378
|
-
expected,
|
|
379
|
-
actual
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
for (const key of expectedKeys) {
|
|
384
|
-
const childPath = path3 === "" ? key : `${path3}.${key}`;
|
|
385
|
-
if (!(key in actual)) {
|
|
386
|
-
report(ctx, {
|
|
387
|
-
path: displayPath(childPath),
|
|
388
|
-
message: `missing expected key (expected ${JSON.stringify(expected[key])})`,
|
|
389
|
-
expected: expected[key],
|
|
390
|
-
actual: void 0
|
|
391
|
-
});
|
|
392
|
-
continue;
|
|
393
|
-
}
|
|
394
|
-
const childCtx = { partial, mismatches: ctx.mismatches };
|
|
395
|
-
matchNode(expected[key], actual[key], childPath, childCtx);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
function matchArray(expected, actual, path3, ctx) {
|
|
399
|
-
if (!Array.isArray(actual)) {
|
|
400
|
-
report(ctx, {
|
|
401
|
-
path: displayPath(path3),
|
|
402
|
-
message: `expected an array, got ${describeType(actual)}`,
|
|
403
|
-
expected,
|
|
404
|
-
actual
|
|
405
|
-
});
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
const lengthOk = ctx.partial ? actual.length >= expected.length : actual.length === expected.length;
|
|
409
|
-
if (!lengthOk) {
|
|
410
|
-
report(ctx, {
|
|
411
|
-
path: displayPath(path3),
|
|
412
|
-
message: `expected array of length ${ctx.partial ? `>= ${expected.length}` : expected.length}, got ${actual.length}`,
|
|
413
|
-
expected,
|
|
414
|
-
actual
|
|
415
|
-
});
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
for (let i = 0; i < expected.length; i++) {
|
|
419
|
-
matchNode(expected[i], actual[i], `${path3}[${i}]`, ctx);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
function matchesQuietly(expected, actual, partial) {
|
|
423
|
-
const scratch = { partial, mismatches: [] };
|
|
424
|
-
matchNode(expected, actual, "", scratch);
|
|
425
|
-
return scratch.mismatches.length === 0;
|
|
426
|
-
}
|
|
427
|
-
function matchUnordered(expected, actual, path3, ctx) {
|
|
428
|
-
if (!Array.isArray(actual)) {
|
|
429
|
-
report(ctx, {
|
|
430
|
-
path: displayPath(path3),
|
|
431
|
-
message: `expected an array (unordered), got ${describeType(actual)}`,
|
|
432
|
-
expected,
|
|
433
|
-
actual
|
|
434
|
-
});
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
if (ctx.partial ? actual.length < expected.length : actual.length !== expected.length) {
|
|
438
|
-
report(ctx, {
|
|
439
|
-
path: displayPath(path3),
|
|
440
|
-
message: `expected unordered array of length ${ctx.partial ? `>= ${expected.length}` : expected.length}, got ${actual.length}`,
|
|
441
|
-
expected,
|
|
442
|
-
actual
|
|
443
|
-
});
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
const used = new Array(actual.length).fill(false);
|
|
447
|
-
const assign = (index) => {
|
|
448
|
-
if (index === expected.length) return true;
|
|
449
|
-
for (let j = 0; j < actual.length; j++) {
|
|
450
|
-
if (used[j]) continue;
|
|
451
|
-
if (matchesQuietly(expected[index], actual[j], ctx.partial)) {
|
|
452
|
-
used[j] = true;
|
|
453
|
-
if (assign(index + 1)) return true;
|
|
454
|
-
used[j] = false;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
return false;
|
|
458
|
-
};
|
|
459
|
-
if (!assign(0)) {
|
|
460
|
-
report(ctx, {
|
|
461
|
-
path: displayPath(path3),
|
|
462
|
-
message: `no arrangement of the actual array satisfies the expected elements (unordered${ctx.partial ? ", partial" : ""})`,
|
|
463
|
-
expected,
|
|
464
|
-
actual
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// src/runner.ts
|
|
470
|
-
import fs from "fs";
|
|
471
|
-
import path2 from "path";
|
|
472
|
-
import fg from "fast-glob";
|
|
473
|
-
|
|
474
|
-
// src/paths.ts
|
|
475
|
-
import path from "path";
|
|
476
|
-
function resolveRootDir(options) {
|
|
477
|
-
return options.rootDir ? path.resolve(options.rootDir) : process.cwd();
|
|
478
|
-
}
|
|
479
|
-
function resolveServerConfig(options) {
|
|
480
|
-
const rootDir = resolveRootDir(options);
|
|
481
|
-
const cwd = options.server.cwd ?? ".";
|
|
482
|
-
return {
|
|
483
|
-
...options.server,
|
|
484
|
-
cwd: path.isAbsolute(cwd) ? cwd : path.resolve(rootDir, cwd)
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// src/runner.ts
|
|
489
|
-
async function resolveCaseFiles(globs, cwd) {
|
|
490
|
-
const patterns = globs.map((g) => {
|
|
491
|
-
const isNegative = g.startsWith("!");
|
|
492
|
-
const body = isNegative ? g.slice(1) : g;
|
|
493
|
-
const resolved = (path2.isAbsolute(body) ? body : path2.join(cwd, body)).replace(/\\/g, "/");
|
|
494
|
-
return isNegative ? `!${resolved}` : resolved;
|
|
495
|
-
});
|
|
496
|
-
return await fg(patterns, { dot: false, onlyFiles: true, unique: true });
|
|
497
|
-
}
|
|
498
|
-
function summarizeMismatches(mismatches, label) {
|
|
499
|
-
const first = mismatches[0];
|
|
500
|
-
const suffix = mismatches.length > 1 ? ` (+${mismatches.length - 1} more)` : "";
|
|
501
|
-
return `${label} at ${first.path}: ${first.message}${suffix}`;
|
|
502
|
-
}
|
|
503
|
-
function matchExpectedError(expected, actual) {
|
|
504
|
-
const observed = {
|
|
505
|
-
code: actual.codeName ?? (actual.code !== void 0 ? String(actual.code) : void 0),
|
|
506
|
-
message: actual.message
|
|
507
|
-
};
|
|
508
|
-
if (expected.code !== void 0) {
|
|
509
|
-
const codeMatches = expected.code === actual.codeName || expected.code === String(actual.code ?? "");
|
|
510
|
-
if (!codeMatches) {
|
|
511
|
-
return {
|
|
512
|
-
ok: false,
|
|
513
|
-
error: `Error code mismatch: expected ${JSON.stringify(expected.code)}, got ${JSON.stringify(observed.code)}`,
|
|
514
|
-
expected,
|
|
515
|
-
actual: observed
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
if (expected.message !== void 0) {
|
|
520
|
-
const mismatches = matchValue(expected.message, actual.message);
|
|
521
|
-
if (mismatches.length > 0) {
|
|
522
|
-
return {
|
|
523
|
-
ok: false,
|
|
524
|
-
error: summarizeMismatches(mismatches, "Error message mismatch"),
|
|
525
|
-
expected,
|
|
526
|
-
actual: observed
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
return { ok: true };
|
|
531
|
-
}
|
|
532
|
-
async function executeCase(session, testCase, timeoutMs) {
|
|
533
|
-
if (!testCase.tool) {
|
|
534
|
-
return { ok: false, error: 'Case is missing required "tool" field' };
|
|
535
|
-
}
|
|
536
|
-
const outcome = await session.callTool(testCase.tool, testCase.args ?? {}, timeoutMs);
|
|
537
|
-
if (testCase.expectError !== void 0) {
|
|
538
|
-
if (outcome.kind === "result") {
|
|
539
|
-
return {
|
|
540
|
-
ok: false,
|
|
541
|
-
error: "Expected an error but tool call succeeded",
|
|
542
|
-
expected: testCase.expectError,
|
|
543
|
-
actual: outcome.result
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
return matchExpectedError(testCase.expectError, outcome.error);
|
|
547
|
-
}
|
|
548
|
-
if (outcome.kind === "error") {
|
|
549
|
-
const label = outcome.error.codeName ? ` [${outcome.error.codeName}]` : "";
|
|
550
|
-
return { ok: false, error: `Tool call failed${label}: ${outcome.error.message}` };
|
|
551
|
-
}
|
|
552
|
-
if (testCase.expect !== void 0) {
|
|
553
|
-
const mismatches = matchValue(testCase.expect, outcome.result);
|
|
554
|
-
if (mismatches.length > 0) {
|
|
555
|
-
return {
|
|
556
|
-
ok: false,
|
|
557
|
-
error: summarizeMismatches(mismatches, "Expectation failed"),
|
|
558
|
-
expected: testCase.expect,
|
|
559
|
-
actual: outcome.result
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
return { ok: true };
|
|
564
|
-
}
|
|
565
|
-
async function runWithTimeout(fn, timeoutMs) {
|
|
566
|
-
let timeoutHandle;
|
|
567
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
568
|
-
timeoutHandle = setTimeout(() => reject(new Error("Case timeout exceeded")), timeoutMs);
|
|
569
|
-
});
|
|
570
|
-
try {
|
|
571
|
-
return await Promise.race([fn(), timeoutPromise]);
|
|
572
|
-
} finally {
|
|
573
|
-
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
var Runner = class {
|
|
577
|
-
options;
|
|
578
|
-
display;
|
|
579
|
-
constructor(options, display = new ConsoleResultsDisplay()) {
|
|
580
|
-
this.options = options;
|
|
581
|
-
this.display = display;
|
|
582
|
-
}
|
|
583
|
-
async run() {
|
|
584
|
-
const rootDir = resolveRootDir(this.options);
|
|
585
|
-
const files = await resolveCaseFiles(this.options.globs, rootDir);
|
|
586
|
-
if (files.length === 0) {
|
|
587
|
-
const pattern = this.options.globs.join(", ");
|
|
588
|
-
const message = `No test cases matched globs: ${pattern} (from ${rootDir})`;
|
|
589
|
-
if (this.options.allowEmpty) {
|
|
590
|
-
console.warn(message);
|
|
591
|
-
return 0;
|
|
592
|
-
}
|
|
593
|
-
console.error(message);
|
|
594
|
-
return 1;
|
|
595
|
-
}
|
|
596
|
-
this.display.onStart(files.length);
|
|
597
|
-
const spawnTimeoutMs = Math.max(2e3, Math.min(1e4, this.options.timeoutMs));
|
|
598
|
-
const server = resolveServerConfig(this.options);
|
|
599
|
-
const session = await McpSession.start(server, spawnTimeoutMs);
|
|
600
|
-
try {
|
|
601
|
-
let failures = 0;
|
|
602
|
-
const startedAt = Date.now();
|
|
603
|
-
let processed = 0;
|
|
604
|
-
for (const file of files) {
|
|
605
|
-
let data;
|
|
606
|
-
try {
|
|
607
|
-
data = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
608
|
-
} catch (err) {
|
|
609
|
-
failures++;
|
|
610
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
611
|
-
this.display.onCaseFail(path2.basename(file), `Failed to load case file: ${reason}`);
|
|
612
|
-
processed++;
|
|
613
|
-
if (this.options.failFast) break;
|
|
614
|
-
continue;
|
|
615
|
-
}
|
|
616
|
-
const { ok, error, expected, actual } = await runWithTimeout(
|
|
617
|
-
() => executeCase(session, data, this.options.timeoutMs),
|
|
618
|
-
this.options.timeoutMs
|
|
619
|
-
);
|
|
620
|
-
if (!ok) {
|
|
621
|
-
failures++;
|
|
622
|
-
const caseName = data.name ?? path2.basename(file);
|
|
623
|
-
const details = expected !== void 0 ? ` :: ${JSON.stringify({ expected, actual })}` : "";
|
|
624
|
-
this.display.onCaseFail(caseName, `${error ?? "Error"}${details}`);
|
|
625
|
-
if (this.options.failFast) break;
|
|
626
|
-
} else {
|
|
627
|
-
const caseName = data.name ?? path2.basename(file);
|
|
628
|
-
this.display.onCasePass(caseName);
|
|
629
|
-
}
|
|
630
|
-
processed++;
|
|
631
|
-
}
|
|
632
|
-
this.display.onComplete({
|
|
633
|
-
total: processed,
|
|
634
|
-
passed: processed - failures,
|
|
635
|
-
failed: failures,
|
|
636
|
-
durationMs: Date.now() - startedAt
|
|
637
|
-
});
|
|
638
|
-
return failures === 0 ? 0 : 1;
|
|
639
|
-
} finally {
|
|
640
|
-
await session.close();
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
};
|
|
644
|
-
|
|
645
|
-
export {
|
|
646
|
-
McpSession,
|
|
647
|
-
defaultRunCasesOptions,
|
|
648
|
-
resolveRootDir,
|
|
649
|
-
resolveServerConfig,
|
|
650
|
-
ConsoleResultsDisplay,
|
|
651
|
-
PrettyConsoleResultsDisplay,
|
|
652
|
-
matchValue,
|
|
653
|
-
matches,
|
|
654
|
-
Runner
|
|
655
|
-
};
|
|
656
|
-
//# sourceMappingURL=chunk-FTVETYUK.js.map
|