@rulvar/testing 1.11.0 → 1.13.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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as FAKE_MODEL, c as FakeAdapterOptions, d as FakeToolCallsValue, f as FakeWireErrorValue, i as createTestEngine, l as FakeCall, m as fakeWireError, n as TestEngine, o as FAKE_MODEL_REF, p as fakeToolCalls, r as TestRunHandle, s as FakeAdapter, t as CreateTestEngineOptions, u as FakeResponder } from "./test-engine-2Md1gvT4.js";
1
+ import { a as FAKE_MODEL, c as FakeAdapterOptions, d as FakeToolCallsValue, f as FakeWireErrorValue, i as createTestEngine, l as FakeCall, m as fakeWireError, n as TestEngine, o as FAKE_MODEL_REF, p as fakeToolCalls, r as TestRunHandle, s as FakeAdapter, t as CreateTestEngineOptions, u as FakeResponder } from "./test-engine-DAXwdoOQ.js";
2
2
  import { AgentProfile, ChatEvent, ChatRequest, InvocationRole, JournalEntry, JournalStore, ModelCaps, ModelSpec, ProviderAdapter, ResumePreview, RunOutcome, WireError, Workflow, createEngine } from "@rulvar/core";
3
3
 
4
4
  //#region src/replay-strict.d.ts
package/dist/index.js CHANGED
@@ -7,7 +7,10 @@ import { createHash } from "node:crypto";
7
7
  * FakeAdapter (M1-T14): a REAL ProviderAdapter that resolves calls from
8
8
  * declared patterns instead of the network, behind the same seam as live
9
9
  * adapters, so unit tests run through the full engine: journal, scheduler,
10
- * budget layers, and event stream. Calls cost zero USD.
10
+ * budget layers, and event stream. Calls cost zero USD. Honors the
11
+ * caller's AbortSignal exactly like a live adapter: an abort ends the
12
+ * stream promptly with no terminal event, so cancellation, deadline, and
13
+ * budget tests observe the same journal shapes as production adapters.
11
14
  */
12
15
  /** Scripts a tool-calling turn from a responder. */
13
16
  function fakeToolCalls(...calls) {
@@ -49,6 +52,38 @@ const FAKE_CAPS = {
49
52
  outputUsdPerMTok: 0
50
53
  }
51
54
  };
55
+ /**
56
+ * Races a responder promise against the caller's abort. On abort the
57
+ * pending responder is detached: its eventual value is discarded and an
58
+ * eventual rejection is swallowed (the caller cancelled; a late failure
59
+ * of the abandoned work must not become an unhandled rejection). A
60
+ * rejection that settles first propagates to the caller unchanged.
61
+ */
62
+ async function raceAbort(pending, signal) {
63
+ if (signal === void 0) return {
64
+ aborted: false,
65
+ value: await pending
66
+ };
67
+ if (signal.aborted) {
68
+ pending.catch(() => void 0);
69
+ return { aborted: true };
70
+ }
71
+ let onAbort = () => void 0;
72
+ const abortPromise = new Promise((resolve) => {
73
+ onAbort = () => resolve({ aborted: true });
74
+ signal.addEventListener("abort", onAbort, { once: true });
75
+ });
76
+ try {
77
+ const raced = await Promise.race([pending.then((value) => ({
78
+ aborted: false,
79
+ value
80
+ })), abortPromise]);
81
+ if (raced.aborted) pending.catch(() => void 0);
82
+ return raced;
83
+ } finally {
84
+ signal.removeEventListener("abort", onAbort);
85
+ }
86
+ }
52
87
  function lastUserText(req) {
53
88
  for (let i = req.messages.length - 1; i >= 0; i -= 1) {
54
89
  const msg = req.messages[i];
@@ -61,7 +96,10 @@ var FakeAdapter = class {
61
96
  id = "fake";
62
97
  agents;
63
98
  mintId = createCanonicalIdMinter();
64
- /** Every request this adapter served, in order. */
99
+ /**
100
+ * Every request this adapter served, in order. A request whose signal
101
+ * was already aborted on arrival was never served and is not recorded.
102
+ */
65
103
  calls = [];
66
104
  constructor(options) {
67
105
  this.agents = options.agents;
@@ -83,7 +121,9 @@ var FakeAdapter = class {
83
121
  }
84
122
  return fallback;
85
123
  }
86
- async *stream(req) {
124
+ async *stream(req, signal) {
125
+ const aborted = () => signal?.aborted === true;
126
+ if (aborted()) return;
87
127
  const telemetry = req.providerOptions?.rulvar ?? {};
88
128
  const call = {
89
129
  prompt: lastUserText(req),
@@ -107,8 +147,13 @@ var FakeAdapter = class {
107
147
  }
108
148
  let value;
109
149
  try {
110
- value = typeof responder === "function" ? await responder(call) : responder;
150
+ if (typeof responder === "function") {
151
+ const raced = await raceAbort(Promise.resolve(responder(call)), signal);
152
+ if (raced.aborted) return;
153
+ value = raced.value;
154
+ } else value = responder;
111
155
  } catch (thrown) {
156
+ if (aborted()) return;
112
157
  yield {
113
158
  type: "error",
114
159
  error: {
@@ -120,14 +165,12 @@ var FakeAdapter = class {
120
165
  };
121
166
  return;
122
167
  }
123
- if (isFakeWireError(value)) {
124
- yield {
125
- type: "error",
126
- error: value.error
127
- };
128
- return;
129
- }
130
- if (isFakeToolCalls(value)) {
168
+ const events = [];
169
+ if (isFakeWireError(value)) events.push({
170
+ type: "error",
171
+ error: value.error
172
+ });
173
+ else if (isFakeToolCalls(value)) {
131
174
  const usage = {
132
175
  inputTokens: Math.max(1, Math.ceil(call.prompt.length / 4)),
133
176
  outputTokens: Math.max(1, value.calls.length * 8),
@@ -136,66 +179,70 @@ var FakeAdapter = class {
136
179
  };
137
180
  for (const toolCall of value.calls) {
138
181
  const id = this.mintId();
139
- yield {
182
+ events.push({
140
183
  type: "tool-call-start",
141
184
  id,
142
185
  name: toolCall.name
143
- };
144
- yield {
186
+ });
187
+ events.push({
145
188
  type: "tool-call-end",
146
189
  id,
147
190
  args: toolCall.args
148
- };
191
+ });
149
192
  }
150
- yield {
193
+ events.push({
151
194
  type: "finish",
152
195
  finish: { reason: "tool-calls" },
153
196
  usage
197
+ });
198
+ } else {
199
+ const text = typeof value === "string" ? value : JSON.stringify(value);
200
+ const usage = {
201
+ inputTokens: Math.max(1, Math.ceil(call.prompt.length / 4)),
202
+ outputTokens: Math.max(1, Math.ceil(text.length / 4)),
203
+ cacheReadTokens: 0,
204
+ cacheWriteTokens: 0
154
205
  };
155
- return;
156
- }
157
- const text = typeof value === "string" ? value : JSON.stringify(value);
158
- const usage = {
159
- inputTokens: Math.max(1, Math.ceil(call.prompt.length / 4)),
160
- outputTokens: Math.max(1, Math.ceil(text.length / 4)),
161
- cacheReadTokens: 0,
162
- cacheWriteTokens: 0
163
- };
164
- const forcedName = typeof req.toolChoice === "object" ? req.toolChoice.name : void 0;
165
- if (forcedName !== void 0) {
166
- let args = value;
167
- if (typeof value === "string") try {
168
- args = JSON.parse(value);
169
- } catch {
170
- args = { text: value };
206
+ const forcedName = typeof req.toolChoice === "object" ? req.toolChoice.name : void 0;
207
+ if (forcedName !== void 0) {
208
+ let args = value;
209
+ if (typeof value === "string") try {
210
+ args = JSON.parse(value);
211
+ } catch {
212
+ args = { text: value };
213
+ }
214
+ const id = this.mintId();
215
+ events.push({
216
+ type: "tool-call-start",
217
+ id,
218
+ name: forcedName
219
+ });
220
+ events.push({
221
+ type: "tool-call-end",
222
+ id,
223
+ args
224
+ });
225
+ events.push({
226
+ type: "finish",
227
+ finish: { reason: "tool-calls" },
228
+ usage
229
+ });
230
+ } else {
231
+ events.push({
232
+ type: "text-delta",
233
+ text
234
+ });
235
+ events.push({
236
+ type: "finish",
237
+ finish: { reason: "stop" },
238
+ usage
239
+ });
171
240
  }
172
- const id = this.mintId();
173
- yield {
174
- type: "tool-call-start",
175
- id,
176
- name: forcedName
177
- };
178
- yield {
179
- type: "tool-call-end",
180
- id,
181
- args
182
- };
183
- yield {
184
- type: "finish",
185
- finish: { reason: "tool-calls" },
186
- usage
187
- };
188
- return;
189
241
  }
190
- yield {
191
- type: "text-delta",
192
- text
193
- };
194
- yield {
195
- type: "finish",
196
- finish: { reason: "stop" },
197
- usage
198
- };
242
+ for (const event of events) {
243
+ if (aborted()) return;
244
+ yield event;
245
+ }
199
246
  }
200
247
  };
201
248
  //#endregion
@@ -1,4 +1,4 @@
1
- import { r as TestRunHandle } from "./test-engine-2Md1gvT4.js";
1
+ import { r as TestRunHandle } from "./test-engine-DAXwdoOQ.js";
2
2
 
3
3
  //#region src/matchers.d.ts
4
4
  interface MatcherResult {
@@ -48,12 +48,15 @@ declare class FakeAdapter implements ProviderAdapter {
48
48
  readonly id = "fake";
49
49
  private readonly agents;
50
50
  private readonly mintId;
51
- /** Every request this adapter served, in order. */
51
+ /**
52
+ * Every request this adapter served, in order. A request whose signal
53
+ * was already aborted on arrival was never served and is not recorded.
54
+ */
52
55
  readonly calls: FakeCall[];
53
56
  constructor(options: FakeAdapterOptions);
54
57
  caps(this: void): ModelCaps;
55
58
  private match;
56
- stream(req: ChatRequest): AsyncIterable<ChatEvent>;
59
+ stream(req: ChatRequest, signal?: AbortSignal): AsyncIterable<ChatEvent>;
57
60
  }
58
61
  //#endregion
59
62
  //#region src/test-engine.d.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/testing",
3
- "version": "1.11.0",
3
+ "version": "1.13.0",
4
4
  "description": "Rulvar test harness: createTestEngine, FakeAdapter, VCR cassettes, replay-strict runs, matchers.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -26,7 +26,7 @@
26
26
  "access": "public"
27
27
  },
28
28
  "dependencies": {
29
- "@rulvar/core": "1.11.0"
29
+ "@rulvar/core": "1.13.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^22.20.0",