@emseepea/testing 0.16.4 → 0.16.6
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/README.md +3 -0
- package/package.json +2 -2
- package/semantic/test.mjs +94 -83
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# `@emseepea/testing`
|
|
2
2
|
|
|
3
|
+
Em See Pea is beta. [Maturity and support](https://github.com/emseepea/emseepea/blob/main/SUPPORT.md#maturity-and-support)
|
|
4
|
+
defines the limits of that label.
|
|
5
|
+
|
|
3
6
|
Test whether an AI selects the right MCP tool and understands its result, not
|
|
4
7
|
just whether the server returns valid data. Write JavaScript tests with ordinary
|
|
5
8
|
imports, setup hooks, loops, and assertions.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emseepea/testing",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.6",
|
|
4
4
|
"description": "MCP integration and semantic testing helpers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"test:built": "node --test test/*.test.mjs"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@emseepea/server": "0.18.
|
|
50
|
+
"@emseepea/server": "0.18.1",
|
|
51
51
|
"@modelcontextprotocol/client": "2.0.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
package/semantic/test.mjs
CHANGED
|
@@ -85,77 +85,79 @@ export async function createConversation(testContext, options) {
|
|
|
85
85
|
async send(prompt) {
|
|
86
86
|
if (typeof prompt !== "string" || !prompt.trim()) throw new Error("send needs a user prompt");
|
|
87
87
|
ensureOpen(state);
|
|
88
|
+
const results = await Promise.allSettled(state.trials.map(async (trial) => {
|
|
89
|
+
trial.model ??= startModelConversation(
|
|
90
|
+
provider,
|
|
91
|
+
trial.directory,
|
|
92
|
+
trial.running.url,
|
|
93
|
+
trial.tools,
|
|
94
|
+
semanticAuthToken(specification),
|
|
95
|
+
specification.context,
|
|
96
|
+
testContext.signal,
|
|
97
|
+
);
|
|
98
|
+
const answer = await trial.model.send(prompt);
|
|
99
|
+
const calls = answer.calls;
|
|
100
|
+
trial.history.push({
|
|
101
|
+
user: prompt,
|
|
102
|
+
toolCalls: calls.map((call, index) => ({
|
|
103
|
+
...call,
|
|
104
|
+
result: answer.toolResults[index].content,
|
|
105
|
+
isError: answer.toolResults[index].isError,
|
|
106
|
+
})),
|
|
107
|
+
assistant: answer.answer,
|
|
108
|
+
});
|
|
109
|
+
const record = {
|
|
110
|
+
turn: trial.record.turns.length + 1,
|
|
111
|
+
interactionMode: "native-mcp",
|
|
112
|
+
prompt,
|
|
113
|
+
response: answer.answer,
|
|
114
|
+
toolCalls: calls.map((call, index) => ({
|
|
115
|
+
...call,
|
|
116
|
+
result: answer.toolResults[index].content,
|
|
117
|
+
isError: answer.toolResults[index].isError,
|
|
118
|
+
})),
|
|
119
|
+
promptSha256: hash(prompt),
|
|
120
|
+
answerSha256: hash(answer.answer),
|
|
121
|
+
answerModels: answer.models,
|
|
122
|
+
answerTurnCount: answer.turnCount,
|
|
123
|
+
answerProviderTurnCount: answer.providerTurnCount,
|
|
124
|
+
answerProviderToolCount: answer.providerToolCount,
|
|
125
|
+
advertisedToolCount: trial.tools.length,
|
|
126
|
+
advertisedToolsSha256: hash(JSON.stringify(trial.tools)),
|
|
127
|
+
selectedCallsSha256: hash(JSON.stringify(calls)),
|
|
128
|
+
selectedTools: calls.map(({ name: toolName }) => toolName),
|
|
129
|
+
toolCallCount: calls.length,
|
|
130
|
+
materialSha256: hash(JSON.stringify(answer.pathEvidence)),
|
|
131
|
+
pathEvidence: answer.pathEvidence,
|
|
132
|
+
literalAssertionCount: 0,
|
|
133
|
+
meaningAssertionCount: 0,
|
|
134
|
+
};
|
|
135
|
+
trial.record.turns.push(record);
|
|
136
|
+
return {
|
|
137
|
+
answer: answer.answer,
|
|
138
|
+
calls,
|
|
139
|
+
prompt,
|
|
140
|
+
record,
|
|
141
|
+
state,
|
|
142
|
+
evidence,
|
|
143
|
+
provider,
|
|
144
|
+
signal: testContext.signal,
|
|
145
|
+
history: Object.freeze([...trial.history]),
|
|
146
|
+
};
|
|
147
|
+
}));
|
|
88
148
|
const trials = [];
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
trial.model ??= startModelConversation(
|
|
94
|
-
provider,
|
|
95
|
-
trial.directory,
|
|
96
|
-
trial.running.url,
|
|
97
|
-
trial.tools,
|
|
98
|
-
semanticAuthToken(specification),
|
|
99
|
-
specification.context,
|
|
100
|
-
testContext.signal,
|
|
101
|
-
);
|
|
102
|
-
const answer = await trial.model.send(prompt);
|
|
103
|
-
const calls = answer.calls;
|
|
104
|
-
trial.history.push({
|
|
105
|
-
user: prompt,
|
|
106
|
-
toolCalls: calls.map((call, index) => ({
|
|
107
|
-
...call,
|
|
108
|
-
result: answer.toolResults[index].content,
|
|
109
|
-
isError: answer.toolResults[index].isError,
|
|
110
|
-
})),
|
|
111
|
-
assistant: answer.answer,
|
|
112
|
-
});
|
|
113
|
-
const record = {
|
|
114
|
-
turn: trial.record.turns.length + 1,
|
|
115
|
-
interactionMode: "native-mcp",
|
|
116
|
-
prompt,
|
|
117
|
-
response: answer.answer,
|
|
118
|
-
toolCalls: calls.map((call, index) => ({
|
|
119
|
-
...call,
|
|
120
|
-
result: answer.toolResults[index].content,
|
|
121
|
-
isError: answer.toolResults[index].isError,
|
|
122
|
-
})),
|
|
123
|
-
promptSha256: hash(prompt),
|
|
124
|
-
answerSha256: hash(answer.answer),
|
|
125
|
-
answerModels: answer.models,
|
|
126
|
-
answerTurnCount: answer.turnCount,
|
|
127
|
-
answerProviderTurnCount: answer.providerTurnCount,
|
|
128
|
-
answerProviderToolCount: answer.providerToolCount,
|
|
129
|
-
advertisedToolCount: trial.tools.length,
|
|
130
|
-
advertisedToolsSha256: hash(JSON.stringify(trial.tools)),
|
|
131
|
-
selectedCallsSha256: hash(JSON.stringify(calls)),
|
|
132
|
-
selectedTools: calls.map(({ name: toolName }) => toolName),
|
|
133
|
-
toolCallCount: calls.length,
|
|
134
|
-
materialSha256: hash(JSON.stringify(answer.pathEvidence)),
|
|
135
|
-
pathEvidence: answer.pathEvidence,
|
|
136
|
-
literalAssertionCount: 0,
|
|
137
|
-
meaningAssertionCount: 0,
|
|
138
|
-
};
|
|
139
|
-
trial.record.turns.push(record);
|
|
140
|
-
trials.push({
|
|
141
|
-
answer: answer.answer,
|
|
142
|
-
calls,
|
|
143
|
-
prompt,
|
|
144
|
-
record,
|
|
145
|
-
state,
|
|
146
|
-
evidence,
|
|
147
|
-
provider,
|
|
148
|
-
signal: testContext.signal,
|
|
149
|
-
history: Object.freeze([...trial.history]),
|
|
150
|
-
});
|
|
149
|
+
results.forEach((result, index) => {
|
|
150
|
+
if (result.status === "fulfilled") {
|
|
151
|
+
trials.push(result.value);
|
|
152
|
+
return;
|
|
151
153
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
activeTrial.record.attemptedToolCalls = error.attemptedToolCalls;
|
|
157
|
-
}
|
|
154
|
+
const trial = state.trials[index];
|
|
155
|
+
trial.record.error = safeModelFailure(result.reason);
|
|
156
|
+
if (Array.isArray(result.reason?.attemptedToolCalls)) {
|
|
157
|
+
trial.record.attemptedToolCalls = result.reason.attemptedToolCalls;
|
|
158
158
|
}
|
|
159
|
+
});
|
|
160
|
+
if (results.some(({ status }) => status === "rejected")) {
|
|
159
161
|
state.failed = true;
|
|
160
162
|
evidence.failedPhase = "conversation turn";
|
|
161
163
|
throw new Error(`Semantic test failed during conversation turn: ${name}`);
|
|
@@ -188,7 +190,9 @@ export function assertToolCalls(turn, expected) {
|
|
|
188
190
|
export async function assertToolCallsWithOptionalFeedback(turn, expected) {
|
|
189
191
|
const trials = turnTrials(turn);
|
|
190
192
|
validateExpectedCalls(expected);
|
|
191
|
-
|
|
193
|
+
const judgments = [];
|
|
194
|
+
for (let trialIndex = 0; trialIndex < trials.length; trialIndex += 1) {
|
|
195
|
+
const trial = trials[trialIndex];
|
|
192
196
|
trial.record.expectedTools = expected.map(({ name }) => name);
|
|
193
197
|
trial.record.expectedCalls = expected;
|
|
194
198
|
trial.record.expectedOptionalFeedback = true;
|
|
@@ -221,9 +225,14 @@ export async function assertToolCallsWithOptionalFeedback(turn, expected) {
|
|
|
221
225
|
detail: feedback.arguments.detail,
|
|
222
226
|
expectationSha256: hash(expectation),
|
|
223
227
|
};
|
|
224
|
-
|
|
225
|
-
if (recordedCall.isError || disclosureFailed) failed = true;
|
|
228
|
+
judgments.push({ trial, trialIndex, expectation, recordedCall });
|
|
226
229
|
}
|
|
230
|
+
const outcomes = await Promise.all(judgments.map(({ trial, trialIndex, expectation }) =>
|
|
231
|
+
judgeTrialMeaning(trial, trialIndex, expectation)));
|
|
232
|
+
outcomes.forEach(({ records }, index) => {
|
|
233
|
+
trials[0].evidence.judgeVerdicts.push(...records);
|
|
234
|
+
if (judgments[index].recordedCall.isError || outcomes[index].failed) failed = true;
|
|
235
|
+
});
|
|
227
236
|
if (failed) {
|
|
228
237
|
failAssertion(trials, "feedback-disclosure assertion");
|
|
229
238
|
throw new Error("Optional feedback failed or was not openly described in the response");
|
|
@@ -368,23 +377,23 @@ export async function assertResponseMeaning(turn, expectation) {
|
|
|
368
377
|
|| Object.keys(expectation).join(",") !== "expected") {
|
|
369
378
|
throw new Error("Response meaning needs exactly one non-empty expected statement");
|
|
370
379
|
}
|
|
371
|
-
let failed = false;
|
|
372
380
|
for (let trialIndex = 0; trialIndex < trials.length; trialIndex += 1) {
|
|
373
|
-
|
|
374
|
-
trial.record.expectedMeaning = expectation.expected;
|
|
375
|
-
if (await judgeTrialMeaning(trial, trialIndex, expectation.expected)) failed = true;
|
|
376
|
-
trial.record.meaningAssertionCount += 1;
|
|
381
|
+
trials[trialIndex].record.expectedMeaning = expectation.expected;
|
|
377
382
|
}
|
|
383
|
+
const outcomes = await Promise.all(trials.map((trial, trialIndex) =>
|
|
384
|
+
judgeTrialMeaning(trial, trialIndex, expectation.expected)));
|
|
385
|
+
trials[0].evidence.judgeVerdicts.push(...outcomes.flatMap(({ records }) => records));
|
|
386
|
+
for (const trial of trials) trial.record.meaningAssertionCount += 1;
|
|
378
387
|
trials[0].state.meaningAssertions += 1;
|
|
379
|
-
if (failed) {
|
|
388
|
+
if (outcomes.some(({ failed }) => failed)) {
|
|
380
389
|
failAssertion(trials, "model judgment");
|
|
381
390
|
throw new Error("Response did not have the expected meaning");
|
|
382
391
|
}
|
|
383
392
|
}
|
|
384
393
|
|
|
385
394
|
async function judgeTrialMeaning(trial, trialIndex, expected) {
|
|
386
|
-
|
|
387
|
-
for (
|
|
395
|
+
const outcomes = [];
|
|
396
|
+
for (const judgment of [1, 2, 3]) {
|
|
388
397
|
const request = judgePrompt(trial.history, expected);
|
|
389
398
|
const record = {
|
|
390
399
|
trial: trialIndex + 1,
|
|
@@ -405,16 +414,18 @@ async function judgeTrialMeaning(trial, trialIndex, expected) {
|
|
|
405
414
|
});
|
|
406
415
|
const verdict = parseJudgeVerdict(response.answer.trim());
|
|
407
416
|
record.verdict = verdict;
|
|
408
|
-
|
|
417
|
+
outcomes.push({ failed: !verdict.pass, record });
|
|
409
418
|
} catch (error) {
|
|
410
419
|
record.error = error instanceof SyntaxError || error.message === "Judge returned an invalid verdict"
|
|
411
420
|
? "invalid judge verdict"
|
|
412
421
|
: safeModelFailure(error);
|
|
413
|
-
failed
|
|
422
|
+
outcomes.push({ failed: true, record });
|
|
414
423
|
}
|
|
415
|
-
trial.evidence.judgeVerdicts.push(record);
|
|
416
424
|
}
|
|
417
|
-
return
|
|
425
|
+
return {
|
|
426
|
+
failed: outcomes.some(({ failed }) => failed),
|
|
427
|
+
records: outcomes.map(({ record }) => record),
|
|
428
|
+
};
|
|
418
429
|
}
|
|
419
430
|
|
|
420
431
|
function safeModelFailure(error) {
|