@rulvar/testing 1.23.0 → 1.24.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.
@@ -0,0 +1,246 @@
1
+ import { createCanonicalIdMinter } from "@rulvar/core";
2
+ //#region src/fake-adapter.ts
3
+ /**
4
+ * FakeAdapter (M1-T14): a REAL ProviderAdapter that resolves calls from
5
+ * declared patterns instead of the network, behind the same seam as live
6
+ * adapters, so unit tests run through the full engine: journal, scheduler,
7
+ * budget layers, and event stream. Calls cost zero USD. Honors the
8
+ * caller's AbortSignal exactly like a live adapter: an abort ends the
9
+ * stream promptly with no terminal event, so cancellation, deadline, and
10
+ * budget tests observe the same journal shapes as production adapters.
11
+ */
12
+ /** Scripts a tool-calling turn from a responder. */
13
+ function fakeToolCalls(...calls) {
14
+ return {
15
+ __fake: "tool-calls",
16
+ calls
17
+ };
18
+ }
19
+ /** Scripts a typed wire failure (e.g. a retryable rate limit). */
20
+ function fakeWireError(error) {
21
+ return {
22
+ __fake: "wire-error",
23
+ error
24
+ };
25
+ }
26
+ function isFakeToolCalls(value) {
27
+ return typeof value === "object" && value !== null && value.__fake === "tool-calls";
28
+ }
29
+ function isFakeWireError(value) {
30
+ return typeof value === "object" && value !== null && value.__fake === "wire-error";
31
+ }
32
+ const FAKE_MODEL = "fake-model";
33
+ const FAKE_MODEL_REF = "fake:fake-model";
34
+ const FAKE_CAPS = {
35
+ structuredOutput: "native",
36
+ supportsTemperature: true,
37
+ supportsParallelTools: true,
38
+ reasoningEfforts: [
39
+ "low",
40
+ "medium",
41
+ "high",
42
+ "xhigh",
43
+ "max"
44
+ ],
45
+ contextWindow: 1e6,
46
+ maxOutputTokens: 64e3,
47
+ pricing: {
48
+ inputUsdPerMTok: 0,
49
+ outputUsdPerMTok: 0
50
+ }
51
+ };
52
+ /**
53
+ * Races a responder promise against the caller's abort. On abort the
54
+ * pending responder is detached: its eventual value is discarded and an
55
+ * eventual rejection is swallowed (the caller cancelled; a late failure
56
+ * of the abandoned work must not become an unhandled rejection). A
57
+ * rejection that settles first propagates to the caller unchanged.
58
+ */
59
+ async function raceAbort(pending, signal) {
60
+ if (signal === void 0) return {
61
+ aborted: false,
62
+ value: await pending
63
+ };
64
+ if (signal.aborted) {
65
+ pending.catch(() => void 0);
66
+ return { aborted: true };
67
+ }
68
+ let onAbort = () => void 0;
69
+ const abortPromise = new Promise((resolve) => {
70
+ onAbort = () => resolve({ aborted: true });
71
+ signal.addEventListener("abort", onAbort, { once: true });
72
+ });
73
+ try {
74
+ const raced = await Promise.race([pending.then((value) => ({
75
+ aborted: false,
76
+ value
77
+ })), abortPromise]);
78
+ if (raced.aborted) pending.catch(() => void 0);
79
+ return raced;
80
+ } finally {
81
+ signal.removeEventListener("abort", onAbort);
82
+ }
83
+ }
84
+ function lastUserText(req) {
85
+ for (let i = req.messages.length - 1; i >= 0; i -= 1) {
86
+ const msg = req.messages[i];
87
+ if (msg?.role !== "user") continue;
88
+ return msg.parts.filter((part) => part.type === "text").map((part) => part.text).join("\n");
89
+ }
90
+ return "";
91
+ }
92
+ var FakeAdapter = class {
93
+ id = "fake";
94
+ agents;
95
+ mintId = createCanonicalIdMinter();
96
+ /**
97
+ * Every request this adapter served, in order. A request whose signal
98
+ * was already aborted on arrival was never served and is not recorded.
99
+ */
100
+ calls = [];
101
+ constructor(options) {
102
+ this.agents = options.agents;
103
+ }
104
+ caps() {
105
+ return FAKE_CAPS;
106
+ }
107
+ match(call) {
108
+ let fallback;
109
+ for (const [pattern, responder] of Object.entries(this.agents)) {
110
+ if (pattern === "*") {
111
+ fallback = responder;
112
+ continue;
113
+ }
114
+ if (call.agentType === pattern || call.label === pattern) return responder;
115
+ try {
116
+ if (new RegExp(pattern).test(call.prompt)) return responder;
117
+ } catch {}
118
+ }
119
+ return fallback;
120
+ }
121
+ async *stream(req, signal) {
122
+ const aborted = () => signal?.aborted === true;
123
+ if (aborted()) return;
124
+ const telemetry = req.providerOptions?.rulvar ?? {};
125
+ const call = {
126
+ prompt: lastUserText(req),
127
+ req,
128
+ ...telemetry.agentType === void 0 || telemetry.agentType === "" ? {} : { agentType: telemetry.agentType },
129
+ ...telemetry.label === void 0 ? {} : { label: telemetry.label }
130
+ };
131
+ this.calls.push(call);
132
+ const responder = this.match(call);
133
+ if (responder === void 0) {
134
+ yield {
135
+ type: "error",
136
+ error: {
137
+ code: "agent",
138
+ message: `FakeAdapter: no pattern matches agentType='${call.agentType ?? ""}' label='${call.label ?? ""}' prompt='${call.prompt.slice(0, 80)}'; add a '*' fallback`,
139
+ retryable: false,
140
+ data: { kind: "terminal" }
141
+ }
142
+ };
143
+ return;
144
+ }
145
+ let value;
146
+ try {
147
+ if (typeof responder === "function") {
148
+ const raced = await raceAbort(Promise.resolve(responder(call)), signal);
149
+ if (raced.aborted) return;
150
+ value = raced.value;
151
+ } else value = responder;
152
+ } catch (thrown) {
153
+ if (aborted()) return;
154
+ yield {
155
+ type: "error",
156
+ error: {
157
+ code: "agent",
158
+ message: thrown instanceof Error ? thrown.message : String(thrown),
159
+ retryable: false,
160
+ data: { kind: "terminal" }
161
+ }
162
+ };
163
+ return;
164
+ }
165
+ const events = [];
166
+ if (isFakeWireError(value)) events.push({
167
+ type: "error",
168
+ error: value.error
169
+ });
170
+ else if (isFakeToolCalls(value)) {
171
+ const usage = {
172
+ inputTokens: Math.max(1, Math.ceil(call.prompt.length / 4)),
173
+ outputTokens: Math.max(1, value.calls.length * 8),
174
+ cacheReadTokens: 0,
175
+ cacheWriteTokens: 0
176
+ };
177
+ for (const toolCall of value.calls) {
178
+ const id = this.mintId();
179
+ events.push({
180
+ type: "tool-call-start",
181
+ id,
182
+ name: toolCall.name
183
+ });
184
+ events.push({
185
+ type: "tool-call-end",
186
+ id,
187
+ args: toolCall.args
188
+ });
189
+ }
190
+ events.push({
191
+ type: "finish",
192
+ finish: { reason: "tool-calls" },
193
+ usage
194
+ });
195
+ } else {
196
+ const text = typeof value === "string" ? value : JSON.stringify(value);
197
+ const usage = {
198
+ inputTokens: Math.max(1, Math.ceil(call.prompt.length / 4)),
199
+ outputTokens: Math.max(1, Math.ceil(text.length / 4)),
200
+ cacheReadTokens: 0,
201
+ cacheWriteTokens: 0
202
+ };
203
+ const forcedName = typeof req.toolChoice === "object" ? req.toolChoice.name : void 0;
204
+ if (forcedName !== void 0) {
205
+ let args = value;
206
+ if (typeof value === "string") try {
207
+ args = JSON.parse(value);
208
+ } catch {
209
+ args = { text: value };
210
+ }
211
+ const id = this.mintId();
212
+ events.push({
213
+ type: "tool-call-start",
214
+ id,
215
+ name: forcedName
216
+ });
217
+ events.push({
218
+ type: "tool-call-end",
219
+ id,
220
+ args
221
+ });
222
+ events.push({
223
+ type: "finish",
224
+ finish: { reason: "tool-calls" },
225
+ usage
226
+ });
227
+ } else {
228
+ events.push({
229
+ type: "text-delta",
230
+ text
231
+ });
232
+ events.push({
233
+ type: "finish",
234
+ finish: { reason: "stop" },
235
+ usage
236
+ });
237
+ }
238
+ }
239
+ for (const event of events) {
240
+ if (aborted()) return;
241
+ yield event;
242
+ }
243
+ }
244
+ };
245
+ //#endregion
246
+ export { fakeWireError as a, fakeToolCalls as i, FAKE_MODEL_REF as n, FakeAdapter as r, FAKE_MODEL as t };
package/dist/index.d.ts CHANGED
@@ -125,65 +125,6 @@ type LiveSmokeOutcome = {
125
125
  */
126
126
  declare function runLiveSmoke(adapter: Pick<ProviderAdapter, "stream">, req: ChatRequest, options?: RunLiveSmokeOptions): Promise<LiveSmokeOutcome>;
127
127
  //#endregion
128
- //#region src/cassettes/build-fixtures.d.ts
129
- /** One cassette fixture file: id, provenance note, and the journal. */
130
- /** @internal */
131
- interface CassetteFixture {
132
- id: string;
133
- note: string;
134
- entries: JournalEntry[];
135
- }
136
- /** @internal */
137
- declare function buildM2CassetteFixtures(): CassetteFixture[];
138
- /**
139
- * The frozen v1 journal: a
140
- * round-1 JSONL file with kinds agent, step, rand, external, approval and
141
- * the legacy `v: 1` field (no hashVersion member). Returned as raw
142
- * JSON-ready objects, one per line.
143
- * @internal
144
- */
145
- declare function buildFrozenV1JournalRaw(): Array<Record<string, unknown>>;
146
- /**
147
- * v2 golden identity fixtures: worked examples per spawn kind (M2-T12).
148
- * The keys freeze the hashVersion 2 profile; the v1 members freeze the
149
- * effort-insensitive projection and the incomparable domain.
150
- * @internal
151
- */
152
- declare function buildV2GoldenIdentity(): Record<string, unknown>;
153
- //#endregion
154
- //#region src/cassettes/record-live.d.ts
155
- /** @internal */
156
- declare function recordLiveCassettes(): Promise<CassetteFixture[]>;
157
- //#endregion
158
- //#region src/cassettes/m6-orchestrator.d.ts
159
- /** @internal */
160
- declare const M6_ORCH_RUN_ID = "m6-orchestrator-crash";
161
- /** @internal */
162
- declare const M6_ORCH_GOAL = "m6 cassette: gather two facts";
163
- /** @internal */
164
- declare const M6_ORCH_PROFILES: {
165
- worker: {
166
- description: string;
167
- };
168
- };
169
- /** Extracts spawn handles from the tool results the model saw. */
170
- /** @internal */
171
- declare function handlesInRequest(req: ChatRequest): number[];
172
- /** Fixes wall clock and spans; everything else is deterministic already. */
173
- /** @internal */
174
- declare function normalizeM6Entries(entries: readonly JournalEntry[]): JournalEntry[];
175
- /**
176
- * Phase 1: record the pre-crash journal. The transcripts store carries
177
- * the boundary checkpoint the resume restores from; the recorder keeps
178
- * it in memory because the cassette pins only journal bytes (checkpoint
179
- * blobs are engine-internal at-least-once state).
180
- * @internal
181
- */
182
- declare function recordOrchestratorCrash(): Promise<{
183
- entries: JournalEntry[]; /** Boundary checkpoint blobs by ref, base64: the resume restores from them. */
184
- checkpoints: Record<string, string>;
185
- }>;
186
- //#endregion
187
128
  //#region src/vcr.d.ts
188
129
  /** One recorded exchange; a cassette is one JSON header line plus rows. */
189
130
  interface VcrRow {
@@ -250,4 +191,4 @@ declare function replay(options: {
250
191
  adapters?: ProviderAdapter[];
251
192
  }): ProviderAdapter[];
252
193
  //#endregion
253
- export { type CassetteFixture, type CreateTestEngineOptions, DEFAULT_LIVE_SMOKE_ATTEMPTS, FAKE_MODEL, FAKE_MODEL_REF, FakeAdapter, type FakeAdapterOptions, type FakeCall, type FakeResponder, type FakeToolCallsValue, type FakeWireErrorValue, type LiveSmokeOutcome, M6_ORCH_GOAL, M6_ORCH_PROFILES, M6_ORCH_RUN_ID, MAX_LIVE_SMOKE_ATTEMPTS, MAX_LIVE_SMOKE_DELAY_MS, RedactFn, type ReplayRunOptions, type RunLiveSmokeOptions, type TestEngine, type TestRunHandle, VcrCassette, VcrMissError, VcrRow, buildFrozenV1JournalRaw, buildM2CassetteFixtures, buildV2GoldenIdentity, createTestEngine, defaultRedact, fakeToolCalls, fakeWireError, handlesInRequest, liveTestEnabled, normalizeM6Entries, readCassette, record, recordLiveCassettes, recordOrchestratorCrash, replay, replayRun, requestHash, runLiveSmoke };
194
+ export { type CreateTestEngineOptions, DEFAULT_LIVE_SMOKE_ATTEMPTS, FAKE_MODEL, FAKE_MODEL_REF, FakeAdapter, type FakeAdapterOptions, type FakeCall, type FakeResponder, type FakeToolCallsValue, type FakeWireErrorValue, type LiveSmokeOutcome, MAX_LIVE_SMOKE_ATTEMPTS, MAX_LIVE_SMOKE_DELAY_MS, RedactFn, type ReplayRunOptions, type RunLiveSmokeOptions, type TestEngine, type TestRunHandle, VcrCassette, VcrMissError, VcrRow, createTestEngine, defaultRedact, fakeToolCalls, fakeWireError, liveTestEnabled, readCassette, record, replay, replayRun, requestHash, runLiveSmoke };