@node-llm/testing 0.1.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/CHANGELOG.md +8 -0
- package/README.md +541 -0
- package/dist/Mocker.d.ts +58 -0
- package/dist/Mocker.d.ts.map +1 -0
- package/dist/Mocker.js +247 -0
- package/dist/Scrubber.d.ts +18 -0
- package/dist/Scrubber.d.ts.map +1 -0
- package/dist/Scrubber.js +68 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/vcr.d.ts +57 -0
- package/dist/vcr.d.ts.map +1 -0
- package/dist/vcr.js +291 -0
- package/package.json +19 -0
- package/src/Mocker.ts +311 -0
- package/src/Scrubber.ts +85 -0
- package/src/index.ts +2 -0
- package/src/vcr.ts +377 -0
- package/test/cassettes/custom-scrub-config.json +33 -0
- package/test/cassettes/defaults-plus-custom.json +33 -0
- package/test/cassettes/explicit-sugar-test.json +33 -0
- package/test/cassettes/feature-1-vcr.json +33 -0
- package/test/cassettes/global-config-keys.json +33 -0
- package/test/cassettes/global-config-merge.json +33 -0
- package/test/cassettes/global-config-patterns.json +33 -0
- package/test/cassettes/global-config-reset.json +33 -0
- package/test/cassettes/global-config-test.json +33 -0
- package/test/cassettes/streaming-chunks.json +18 -0
- package/test/cassettes/testunitdxtestts-vcr-feature-5-6-dx-sugar-auto-naming-automatically-names-and-records-cassettes.json +33 -0
- package/test/cassettes/vcr-feature-5-6-dx-sugar-auto-naming-automatically-names-and-records-cassettes.json +28 -0
- package/test/cassettes/vcr-streaming.json +17 -0
- package/test/helpers/MockProvider.ts +75 -0
- package/test/unit/ci.test.ts +36 -0
- package/test/unit/dx.test.ts +86 -0
- package/test/unit/mocker-debug.test.ts +68 -0
- package/test/unit/mocker.test.ts +46 -0
- package/test/unit/multimodal.test.ts +46 -0
- package/test/unit/scoping.test.ts +54 -0
- package/test/unit/scrubbing.test.ts +110 -0
- package/test/unit/streaming.test.ts +51 -0
- package/test/unit/strict-mode.test.ts +112 -0
- package/test/unit/tools.test.ts +58 -0
- package/test/unit/vcr-global-config.test.ts +87 -0
- package/test/unit/vcr-mismatch.test.ts +172 -0
- package/test/unit/vcr-passthrough.test.ts +68 -0
- package/test/unit/vcr-streaming.test.ts +86 -0
- package/test/unit/vcr.test.ts +34 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +12 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
# @node-llm/testing ๐๐ข๐งช
|
|
2
|
+
|
|
3
|
+
Deterministic testing infrastructure for NodeLLM-powered AI systems. Built for engineers who prioritize **Boring Solutions**, **Security**, and **High-Fidelity Feedback Loops**.
|
|
4
|
+
|
|
5
|
+
> ๐ก **What is High-Fidelity?**
|
|
6
|
+
> Your tests exercise the same execution path, provider behavior, and tool orchestration as production โ without live network calls.
|
|
7
|
+
|
|
8
|
+
**Framework Support**: โ
Vitest (native) | โ
Jest (compatible) | โ
Any test framework (core APIs)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## ๐งญ The Philosophy: Two-Tier Testing
|
|
13
|
+
|
|
14
|
+
We believe AI testing should never be flaky or expensive. We provide two distinct strategies:
|
|
15
|
+
|
|
16
|
+
### 1. VCR (Integration Testing) ๐ผ
|
|
17
|
+
|
|
18
|
+
**When to use**: To verify your system works with real LLM responses without paying for every test run.
|
|
19
|
+
|
|
20
|
+
- **High Fidelity**: Captures the exact raw response from the provider.
|
|
21
|
+
- **Security First**: Automatically scrubs API Keys and sensitive PII from "cassettes".
|
|
22
|
+
- **CI Safe**: Fails-fast in CI if a cassette is missing, preventing accidental live API calls.
|
|
23
|
+
|
|
24
|
+
### 2. Mocker (Unit Testing) ๐ญ
|
|
25
|
+
|
|
26
|
+
**When to use**: To test application logic, edge cases (errors, rate limits), and rare tool-calling paths.
|
|
27
|
+
|
|
28
|
+
- **Declarative**: Fluent API to define expected prompts and responses.
|
|
29
|
+
- **Multimodal**: Native support for `chat`, `embed`, `paint`, `transcribe`, and `moderate`.
|
|
30
|
+
- **Streaming**: Simulate token-by-token delivery to test real-time UI logic.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## ๐ผ VCR Usage
|
|
35
|
+
|
|
36
|
+
### Basic Interaction
|
|
37
|
+
|
|
38
|
+
Wrap your tests in `withVCR` to automatically record interactions the first time they run.
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { withVCR } from "@node-llm/testing";
|
|
42
|
+
|
|
43
|
+
it(
|
|
44
|
+
"calculates sentiment correctly",
|
|
45
|
+
withVCR(async () => {
|
|
46
|
+
const result = await mySentimentAgent.run("I love NodeLLM!");
|
|
47
|
+
expect(result.sentiment).toBe("positive");
|
|
48
|
+
})
|
|
49
|
+
);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Hierarchical Organization (Rails Mode) ๐
|
|
53
|
+
|
|
54
|
+
Organize your cassettes into nested subfolders to match your test suite structure.
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { describeVCR, withVCR } from "@node-llm/testing";
|
|
58
|
+
|
|
59
|
+
describeVCR("Authentication", () => {
|
|
60
|
+
describeVCR("Login", () => {
|
|
61
|
+
it(
|
|
62
|
+
"logs in successfully",
|
|
63
|
+
withVCR(async () => {
|
|
64
|
+
// Cassette saved to: .llm-cassettes/authentication/login/logs-in-successfully.json
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Security & Scrubbing ๐ก๏ธ
|
|
72
|
+
|
|
73
|
+
The VCR automatically redacts `api_key`, `authorization`, and other sensitive headers. You can add custom redaction:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
withVCR({
|
|
77
|
+
scrub: (data) => data.replace(/SSN: \d+/g, "[REDACTED_SSN]")
|
|
78
|
+
}, async () => { ... });
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## ๐ญ Mocker Usage
|
|
84
|
+
|
|
85
|
+
### Fluent, Explicit Mocking
|
|
86
|
+
|
|
87
|
+
Define lightning-fast, zero-network tests for your agents.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { mockLLM } from "@node-llm/testing";
|
|
91
|
+
|
|
92
|
+
const mocker = mockLLM();
|
|
93
|
+
|
|
94
|
+
// Exact match
|
|
95
|
+
mocker.chat("Ping").respond("Pong");
|
|
96
|
+
|
|
97
|
+
// Regex match
|
|
98
|
+
mocker.chat(/hello/i).respond("Greetings!");
|
|
99
|
+
|
|
100
|
+
// Simulate a Tool Call
|
|
101
|
+
mocker.chat("What's the weather?").callsTool("get_weather", { city: "London" });
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Streaming Mocks ๐
|
|
105
|
+
|
|
106
|
+
Test your streaming logic by simulating token delivery.
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
mocker.chat("Tell a story").stream(["Once ", "upon ", "a ", "time."]);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Multimodal Mocks ๐จ
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
mocker.paint(/a cat/i).respond({ url: "https://mock.com/cat.png" });
|
|
116
|
+
mocker.embed("text").respond({ vectors: [[0.1, 0.2, 0.3]] });
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## ๐ฃ๏ธ Decision Tree: VCR vs Mocker
|
|
122
|
+
|
|
123
|
+
Choose the right tool for your test:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
Does your test need to verify behavior against REAL LLM responses?
|
|
127
|
+
โโ YES โ Use VCR (integration testing)
|
|
128
|
+
โ โโ Do you need to record the first time and replay afterward?
|
|
129
|
+
โ โ โโ YES โ Use VCR in "record" or "auto" mode
|
|
130
|
+
โ โโ Are you testing in CI/CD? (No live API calls allowed)
|
|
131
|
+
โ โ โโ YES โ Set VCR_MODE=replay in CI
|
|
132
|
+
โ โโ Need custom scrubbing for sensitive data?
|
|
133
|
+
โ โโ YES โ Use withVCR({ scrub: ... })
|
|
134
|
+
โ
|
|
135
|
+
โโ NO โ Use Mocker (unit testing)
|
|
136
|
+
โโ Testing error handling, edge cases, or rare paths?
|
|
137
|
+
โ โโ YES โ Mock the error with mocker.chat(...).respond({ error: ... })
|
|
138
|
+
โโ Testing streaming token delivery?
|
|
139
|
+
โ โโ YES โ Use mocker.chat(...).stream([...])
|
|
140
|
+
โโ Testing tool-calling paths without real tools?
|
|
141
|
+
โโ YES โ Use mocker.chat(...).callsTool(name, params)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Quick Reference**:
|
|
145
|
+
|
|
146
|
+
- **VCR**: Database queries, API calls, real provider behavior, network latency
|
|
147
|
+
- **Mocker**: Business logic, UI interactions, error scenarios, tool orchestration
|
|
148
|
+
|
|
149
|
+
### At-a-Glance Comparison
|
|
150
|
+
|
|
151
|
+
| Use Case | VCR | Mocker |
|
|
152
|
+
| ----------------------- | ----------------- | ------ |
|
|
153
|
+
| Real provider behavior | โ
| โ |
|
|
154
|
+
| CI-safe (no live calls) | โ
(after record) | โ
|
|
|
155
|
+
| Zero network overhead | โ (first run) | โ
|
|
|
156
|
+
| Error simulation | โ ๏ธ (record real) | โ
|
|
|
157
|
+
| Tool orchestration | โ
| โ
|
|
|
158
|
+
| Streaming tokens | โ
| โ
|
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## โ๏ธ Configuration
|
|
163
|
+
|
|
164
|
+
### Environment Variables
|
|
165
|
+
|
|
166
|
+
| Env Variable | Description | Default |
|
|
167
|
+
| ------------------ | ---------------------------------------------------------- | ---------------- |
|
|
168
|
+
| `VCR_MODE` | `record`, `replay`, `auto`, or `passthrough` | `auto` |
|
|
169
|
+
| `VCR_CASSETTE_DIR` | Base directory for cassettes | `test/cassettes` |
|
|
170
|
+
| `CI` | When true, VCR prevents recording and forces exact matches | (Auto-detected) |
|
|
171
|
+
|
|
172
|
+
### Programmatic Configuration
|
|
173
|
+
|
|
174
|
+
Configure VCR globally for all instances in your test suite:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
import { configureVCR, resetVCRConfig } from "@node-llm/testing";
|
|
178
|
+
|
|
179
|
+
// Before all tests
|
|
180
|
+
beforeAll(() => {
|
|
181
|
+
configureVCR({
|
|
182
|
+
// Custom keys to redact in cassettes
|
|
183
|
+
sensitiveKeys: ["api_key", "bearer_token", "custom_secret"],
|
|
184
|
+
|
|
185
|
+
// Custom regex patterns to redact
|
|
186
|
+
sensitivePatterns: [/api_key=[\w]+/g, /Bearer ([\w.-]+)/g]
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// After all tests
|
|
191
|
+
afterAll(() => {
|
|
192
|
+
resetVCRConfig();
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Per-Instance Configuration
|
|
197
|
+
|
|
198
|
+
Override global settings for a specific VCR instance:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
withVCR(
|
|
202
|
+
{
|
|
203
|
+
mode: "replay",
|
|
204
|
+
cassettesDir: "./test/fixtures",
|
|
205
|
+
scrub: (data) => data.replace(/email=\S+@/, "email=[REDACTED]@"),
|
|
206
|
+
sensitiveKeys: ["session_token"]
|
|
207
|
+
},
|
|
208
|
+
async () => {
|
|
209
|
+
// Test runs here
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
````
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## ๐งช Framework Integration
|
|
221
|
+
|
|
222
|
+
### Vitest (Native Support)
|
|
223
|
+
|
|
224
|
+
Vitest is the primary test framework with optimized helpers:
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
import { it, describe } from "vitest";
|
|
228
|
+
import { mockLLM, withVCR, describeVCR } from "@node-llm/testing";
|
|
229
|
+
|
|
230
|
+
describeVCR("Payments", () => {
|
|
231
|
+
it(
|
|
232
|
+
"processes successfully",
|
|
233
|
+
withVCR(async () => {
|
|
234
|
+
// โจ withVCR auto-detects test name ("processes successfully")
|
|
235
|
+
// โจ describeVCR auto-manages scopes
|
|
236
|
+
})
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Jest Compatibility
|
|
242
|
+
|
|
243
|
+
All core APIs work with Jest. The only difference: `withVCR()` can't auto-detect test names, so provide it manually:
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
import { describe, it } from "@jest/globals";
|
|
247
|
+
import { mockLLM, setupVCR, describeVCR } from "@node-llm/testing";
|
|
248
|
+
|
|
249
|
+
describeVCR("Payments", () => {
|
|
250
|
+
it("processes successfully", async () => {
|
|
251
|
+
// โ
describeVCR works with Jest (framework-agnostic)
|
|
252
|
+
// โ ๏ธ withVCR doesn't work here (needs Vitest's expect.getState())
|
|
253
|
+
// โ
Use setupVCR instead:
|
|
254
|
+
const vcr = setupVCR("processes", { mode: "record" });
|
|
255
|
+
|
|
256
|
+
const mocker = mockLLM(); // โ
works with Jest
|
|
257
|
+
mocker.chat("pay").respond("done");
|
|
258
|
+
|
|
259
|
+
// Test logic here
|
|
260
|
+
|
|
261
|
+
await vcr.stop();
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Framework Support Matrix
|
|
267
|
+
|
|
268
|
+
| API | Vitest | Jest | Any Framework |
|
|
269
|
+
|-----|--------|------|---------------|
|
|
270
|
+
| `mockLLM()` | โ
| โ
| โ
|
|
|
271
|
+
| `describeVCR()` | โ
| โ
| โ
|
|
|
272
|
+
| `setupVCR()` | โ
| โ
| โ
|
|
|
273
|
+
| `withVCR()` | โ
(auto name) | โ ๏ธ (manual name) | โ ๏ธ (manual name) |
|
|
274
|
+
| Mocker class | โ
| โ
| โ
|
|
|
275
|
+
| VCR class | โ
| โ
| โ
|
|
|
276
|
+
|
|
277
|
+
**Only `withVCR()` is Vitest-specific** because it auto-detects test names. All other APIs are framework-agnostic.
|
|
278
|
+
|
|
279
|
+
### Any Test Framework
|
|
280
|
+
|
|
281
|
+
Using raw classes for maximum portability:
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
import { Mocker, VCR } from "@node-llm/testing";
|
|
285
|
+
|
|
286
|
+
// Mocker - works everywhere
|
|
287
|
+
const mocker = new Mocker();
|
|
288
|
+
mocker.chat("hello").respond("hi");
|
|
289
|
+
|
|
290
|
+
// VCR - works everywhere
|
|
291
|
+
const vcr = new VCR("test-name", { mode: "record" });
|
|
292
|
+
// ... run test ...
|
|
293
|
+
await vcr.stop();
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## ๐จ Error Handling & Debugging
|
|
299
|
+
|
|
300
|
+
### VCR Common Issues
|
|
301
|
+
|
|
302
|
+
#### Missing Cassette Error
|
|
303
|
+
|
|
304
|
+
**Error**: `Error: Cassette file not found`
|
|
305
|
+
|
|
306
|
+
**Cause**: VCR is in `replay` mode but the cassette doesn't exist yet.
|
|
307
|
+
|
|
308
|
+
**Solution**:
|
|
309
|
+
```typescript
|
|
310
|
+
// Either: Record it first
|
|
311
|
+
VCR_MODE=record npm test
|
|
312
|
+
|
|
313
|
+
// Or: Use auto mode (records if missing, replays if exists)
|
|
314
|
+
VCR_MODE=auto npm test
|
|
315
|
+
|
|
316
|
+
// Or: Explicitly set mode
|
|
317
|
+
withVCR({ mode: "record" }, async () => { ... });
|
|
318
|
+
````
|
|
319
|
+
|
|
320
|
+
#### Cassette Mismatch Error
|
|
321
|
+
|
|
322
|
+
**Error**: `AssertionError: No interaction matched the request`
|
|
323
|
+
|
|
324
|
+
**Cause**: Your code is making a request that doesn't match any recorded interaction.
|
|
325
|
+
|
|
326
|
+
**Solution**:
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
// 1. Debug what request was made
|
|
330
|
+
const mocker = mockLLM();
|
|
331
|
+
mocker.onAnyRequest((req) => {
|
|
332
|
+
console.log("Unexpected request:", req.prompt);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// 2. Re-record the cassette
|
|
336
|
+
rm -rf .llm-cassettes/your-test
|
|
337
|
+
VCR_MODE=record npm test -- your-test
|
|
338
|
+
|
|
339
|
+
// 3. Commit the updated cassette to git
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
#### Sensitive Data Not Scrubbed
|
|
343
|
+
|
|
344
|
+
**Error**: API keys appear in cassette JSON
|
|
345
|
+
|
|
346
|
+
**Solution**: Add custom scrubbing rules
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
import { configureVCR } from "@node-llm/testing";
|
|
350
|
+
|
|
351
|
+
configureVCR({
|
|
352
|
+
sensitiveKeys: ["x-api-key", "authorization", "custom_token"],
|
|
353
|
+
sensitivePatterns: [/Bearer ([\w.-]+)/g]
|
|
354
|
+
});
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Mocker Common Issues
|
|
358
|
+
|
|
359
|
+
#### Strict Mode Enforcement
|
|
360
|
+
|
|
361
|
+
**Error**: `Error: No mock defined for prompt: "unexpected question"`
|
|
362
|
+
|
|
363
|
+
**Cause**: Your code asked a question you didn't mock in strict mode.
|
|
364
|
+
|
|
365
|
+
**Solution**:
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
// Either: Add the missing mock
|
|
369
|
+
mocker.chat("unexpected question").respond("mocked response");
|
|
370
|
+
|
|
371
|
+
// Or: Disable strict mode
|
|
372
|
+
const mocker = mockLLM({ strict: false });
|
|
373
|
+
// Now unmocked requests return generic "I don't have a response" message
|
|
374
|
+
|
|
375
|
+
// Or: Debug what's being asked
|
|
376
|
+
mocker.onAnyRequest((req) => {
|
|
377
|
+
console.error("Unmatched request:", req.prompt);
|
|
378
|
+
throw new Error(`Add mock for: mocker.chat("${req.prompt}").respond(...)`);
|
|
379
|
+
});
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
#### Stream Simulation Issues
|
|
383
|
+
|
|
384
|
+
**Error**: `TypeError: Cannot read property 'Symbol(Symbol.iterator)' of undefined`
|
|
385
|
+
|
|
386
|
+
**Cause**: Stream mock not properly yielding tokens.
|
|
387
|
+
|
|
388
|
+
**Solution**:
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
// Correct: Array of tokens
|
|
392
|
+
mocker.chat("story").stream(["Once ", "upon ", "a ", "time."]);
|
|
393
|
+
|
|
394
|
+
// Incorrect: String instead of array
|
|
395
|
+
mocker.chat("story").stream("Once upon a time."); // โ Wrong!
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Debug Information
|
|
399
|
+
|
|
400
|
+
Get detailed insight into what mocks are registered:
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
const mocker = mockLLM();
|
|
404
|
+
mocker.chat("hello").respond("hi");
|
|
405
|
+
mocker.embed("text").respond({ vectors: [[0.1, 0.2]] });
|
|
406
|
+
|
|
407
|
+
const debug = mocker.getDebugInfo();
|
|
408
|
+
console.log(debug);
|
|
409
|
+
// Output:
|
|
410
|
+
// {
|
|
411
|
+
// totalMocks: 2,
|
|
412
|
+
// methods: ["chat", "embed"]
|
|
413
|
+
// }
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## ๐ Type Documentation
|
|
419
|
+
|
|
420
|
+
### VCROptions
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
interface VCROptions {
|
|
424
|
+
// Recording/Replay behavior
|
|
425
|
+
mode?: "record" | "replay" | "auto" | "passthrough";
|
|
426
|
+
cassettesDir?: string;
|
|
427
|
+
|
|
428
|
+
// Security & Scrubbing
|
|
429
|
+
sensitiveKeys?: string[];
|
|
430
|
+
sensitivePatterns?: RegExp[];
|
|
431
|
+
scrub?: (data: string) => string;
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### MockerOptions
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
interface MockerOptions {
|
|
439
|
+
// Enforce exact matching
|
|
440
|
+
strict?: boolean;
|
|
441
|
+
|
|
442
|
+
// Enable verbose logging
|
|
443
|
+
debug?: boolean;
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### MockResponse
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
interface MockResponse {
|
|
451
|
+
// Simple text response
|
|
452
|
+
content?: string;
|
|
453
|
+
|
|
454
|
+
// Tool calling
|
|
455
|
+
toolName?: string;
|
|
456
|
+
toolParams?: Record<string, unknown>;
|
|
457
|
+
|
|
458
|
+
// Error simulation
|
|
459
|
+
error?: Error | string;
|
|
460
|
+
|
|
461
|
+
// Streaming tokens
|
|
462
|
+
tokens?: string[];
|
|
463
|
+
|
|
464
|
+
// Generation metadata
|
|
465
|
+
metadata?: {
|
|
466
|
+
tokensUsed?: number;
|
|
467
|
+
model?: string;
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### MockerDebugInfo
|
|
473
|
+
|
|
474
|
+
```typescript
|
|
475
|
+
interface MockerDebugInfo {
|
|
476
|
+
// Total number of mocks defined
|
|
477
|
+
totalMocks: number;
|
|
478
|
+
|
|
479
|
+
// Array of unique method names used ("chat", "embed", etc.)
|
|
480
|
+
methods: string[];
|
|
481
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
486
|
+
## ๐๏ธ Integration with @node-llm/orm
|
|
487
|
+
|
|
488
|
+
The testing tools operate at the `providerRegistry` level. This means they **automatically** intercept LLM calls made by the ORM layer.
|
|
489
|
+
|
|
490
|
+
### Pattern: Testing Database Persistence
|
|
491
|
+
|
|
492
|
+
When using `@node-llm/orm`, you can verify both the database state and the LLM response in a single test.
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
import { withVCR } from "@node-llm/testing";
|
|
496
|
+
import { createChat } from "@node-llm/orm/prisma";
|
|
497
|
+
|
|
498
|
+
it(
|
|
499
|
+
"saves the LLM response to the database",
|
|
500
|
+
withVCR(async () => {
|
|
501
|
+
// 1. Setup ORM Chat
|
|
502
|
+
const chat = await createChat(prisma, llm, { model: "gpt-4" });
|
|
503
|
+
|
|
504
|
+
// 2. Interaction (VCR intercepts the LLM call)
|
|
505
|
+
await chat.ask("Hello ORM!");
|
|
506
|
+
|
|
507
|
+
// 3. Verify DB state (standard Prisma/ORM assertions)
|
|
508
|
+
const messages = await prisma.assistantMessage.findMany({
|
|
509
|
+
where: { chatId: chat.id }
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
expect(messages).toHaveLength(2); // User + Assistant
|
|
513
|
+
expect(messages[1].content).toBeDefined();
|
|
514
|
+
})
|
|
515
|
+
);
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### Pattern: Mocking Rare Logic
|
|
519
|
+
|
|
520
|
+
Use the `Mocker` to test how your application handles complex tool results or errors without setting up a real LLM.
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
import { mockLLM } from "@node-llm/testing";
|
|
524
|
+
|
|
525
|
+
it("handles tool errors in ORM sessions", async () => {
|
|
526
|
+
const mocker = mockLLM();
|
|
527
|
+
mocker.chat("Search docs").respond({ error: new Error("DB Timeout") });
|
|
528
|
+
|
|
529
|
+
const chat = await loadChat(prisma, llm, "existing-id");
|
|
530
|
+
|
|
531
|
+
await expect(chat.ask("Search docs")).rejects.toThrow("DB Timeout");
|
|
532
|
+
});
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
## ๐๏ธ Architecture Contract
|
|
538
|
+
|
|
539
|
+
- **No Side Effects**: Mocks and VCR interceptors are automatically cleared after each test turn.
|
|
540
|
+
- **Deterministic**: The same input MUST always yield the same output in Replay mode.
|
|
541
|
+
- **Explicit > Implicit**: We prefer explicit mock definitions over complex global state.
|
package/dist/Mocker.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ChatChunk, ToolCall, ModerationResult } from "@node-llm/core";
|
|
2
|
+
export interface MockResponse {
|
|
3
|
+
content?: string | null;
|
|
4
|
+
tool_calls?: ToolCall[];
|
|
5
|
+
usage?: {
|
|
6
|
+
input_tokens: number;
|
|
7
|
+
output_tokens: number;
|
|
8
|
+
total_tokens: number;
|
|
9
|
+
};
|
|
10
|
+
error?: Error;
|
|
11
|
+
finish_reason?: string | null;
|
|
12
|
+
chunks?: string[] | ChatChunk[];
|
|
13
|
+
vectors?: number[][];
|
|
14
|
+
url?: string;
|
|
15
|
+
data?: string;
|
|
16
|
+
text?: string;
|
|
17
|
+
results?: ModerationResult[];
|
|
18
|
+
revised_prompt?: string;
|
|
19
|
+
id?: string;
|
|
20
|
+
}
|
|
21
|
+
export type MockMatcher = (request: unknown) => boolean;
|
|
22
|
+
export interface MockDefinition {
|
|
23
|
+
method: string;
|
|
24
|
+
match: MockMatcher;
|
|
25
|
+
response: MockResponse | ((request: unknown) => MockResponse);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Debug information about defined mocks.
|
|
29
|
+
*/
|
|
30
|
+
export interface MockerDebugInfo {
|
|
31
|
+
totalMocks: number;
|
|
32
|
+
methods: string[];
|
|
33
|
+
}
|
|
34
|
+
export declare class Mocker {
|
|
35
|
+
private mocks;
|
|
36
|
+
strict: boolean;
|
|
37
|
+
constructor();
|
|
38
|
+
chat(query?: string | RegExp): this;
|
|
39
|
+
stream(chunks: string[] | ChatChunk[]): this;
|
|
40
|
+
placeholder(query: string | RegExp): this;
|
|
41
|
+
callsTool(name: string, args?: Record<string, unknown>): this;
|
|
42
|
+
embed(input?: string | string[]): this;
|
|
43
|
+
paint(prompt?: string | RegExp): this;
|
|
44
|
+
transcribe(file?: string | RegExp): this;
|
|
45
|
+
moderate(input?: string | string[] | RegExp): this;
|
|
46
|
+
respond(response: string | MockResponse | ((req: unknown) => MockResponse)): this;
|
|
47
|
+
/**
|
|
48
|
+
* Returns debug information about defined mocks.
|
|
49
|
+
* Useful for troubleshooting what mocks are defined.
|
|
50
|
+
*/
|
|
51
|
+
getDebugInfo(): MockerDebugInfo;
|
|
52
|
+
clear(): void;
|
|
53
|
+
private addMock;
|
|
54
|
+
private getContentString;
|
|
55
|
+
private setupInterceptor;
|
|
56
|
+
}
|
|
57
|
+
export declare function mockLLM(): Mocker;
|
|
58
|
+
//# sourceMappingURL=Mocker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Mocker.d.ts","sourceRoot":"","sources":["../src/Mocker.ts"],"names":[],"mappings":"AAAA,OAAO,EAaL,SAAS,EACT,QAAQ,EACR,gBAAgB,EAEjB,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,CAAC,EAAE,QAAQ,EAAE,CAAC;IACxB,KAAK,CAAC,EAAE;QACN,YAAY,EAAE,MAAM,CAAC;QACrB,aAAa,EAAE,MAAM,CAAC;QACtB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,EAAE,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC;AAExD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,WAAW,CAAC;IACnB,QAAQ,EAAE,YAAY,GAAG,CAAC,CAAC,OAAO,EAAE,OAAO,KAAK,YAAY,CAAC,CAAC;CAC/D;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAID,qBAAa,MAAM;IACjB,OAAO,CAAC,KAAK,CAAwB;IAC9B,MAAM,UAAS;;IAMf,IAAI,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAcnC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS,EAAE,GAAG,IAAI;IAU5C,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAWzC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,IAAI;IAmBjE,KAAK,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI;IAQtC,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IASrC,UAAU,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IASxC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI;IAUlD,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,GAAG,CAAC,CAAC,GAAG,EAAE,OAAO,KAAK,YAAY,CAAC,GAAG,IAAI;IAWxF;;;OAGG;IACI,YAAY,IAAI,eAAe;IAQ/B,KAAK,IAAI,IAAI;IAKpB,OAAO,CAAC,OAAO;IAKf,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,gBAAgB;CA6GzB;AAED,wBAAgB,OAAO,WAEtB"}
|