@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 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.4",
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.0",
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
- let activeTrial;
90
- try {
91
- for (const trial of state.trials) {
92
- activeTrial = trial;
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
- } catch (error) {
153
- if (activeTrial) {
154
- activeTrial.record.error = safeModelFailure(error);
155
- if (Array.isArray(error?.attemptedToolCalls)) {
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
- for (const trial of trials) {
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
- const disclosureFailed = await judgeTrialMeaning(trial, trialIndex, expectation);
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
- const trial = trials[trialIndex];
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
- let failed = false;
387
- for (let judgment = 1; judgment <= 3; judgment += 1) {
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
- if (!verdict.pass) failed = true;
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 = true;
422
+ outcomes.push({ failed: true, record });
414
423
  }
415
- trial.evidence.judgeVerdicts.push(record);
416
424
  }
417
- return failed;
425
+ return {
426
+ failed: outcomes.some(({ failed }) => failed),
427
+ records: outcomes.map(({ record }) => record),
428
+ };
418
429
  }
419
430
 
420
431
  function safeModelFailure(error) {