@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.
- package/dist/fake-adapter-3T_w-IOY.js +246 -0
- package/dist/index.d.ts +1 -60
- package/dist/index.js +4 -1751
- package/dist/internal/cassettes.d.ts +62 -0
- package/dist/internal/cassettes.js +1508 -0
- package/package.json +2 -2
|
@@ -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
|
|
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 };
|