@cogitator-ai/test-utils 0.3.3 → 0.3.4

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.
Files changed (2) hide show
  1. package/README.md +457 -0
  2. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,457 @@
1
+ # @cogitator-ai/test-utils
2
+
3
+ Testing utilities for Cogitator to test your own agents, tools, workflows, and backends without calling real LLMs or databases.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add -D @cogitator-ai/test-utils
9
+ ```
10
+
11
+ `@cogitator-ai/test-utils` gives you deterministic stand-ins and helpers to test your own agents and applications without network calls, paid API keys, or external databases. See [Test Utilities](https://cogitator.app/docs/testing/test-utils) on the website.
12
+
13
+ ## Features
14
+
15
+ - **Mock LLM Backend** - Scriptable `MockLLMBackend` for deterministic `chat()` responses and `chatStream()` chunks with request recording and inspection
16
+ - **Mock Memory Adapter** - In-process `MockMemoryAdapter` recording method calls for threads and entries
17
+ - **Test Fixtures** - Preconfigured agents, messages, tool calls, tool results, JSON Schema tools, and run results
18
+ - **Stream Helpers** - Drain and collect stream content, chunks, and tool calls, or create mock byte streams
19
+ - **Async Utilities** - Polling (`waitFor`, `eventually`), delay, and promise timeout helpers
20
+ - **Backend Conformance Kit** - Test custom backends against wire protocols (OpenAI, Anthropic, Gemini, Ollama, Bedrock) with `startFakeProvider`
21
+
22
+ ---
23
+
24
+ ## Quick Start
25
+
26
+ ### Testing with MockLLMBackend
27
+
28
+ A short, valid Vitest example using `MockLLMBackend`:
29
+
30
+ ```typescript
31
+ import { describe, it, expect } from 'vitest';
32
+ import { MockLLMBackend, createToolCall } from '@cogitator-ai/test-utils';
33
+
34
+ describe('MockLLMBackend', () => {
35
+ it('returns scripted responses and inspects requests', async () => {
36
+ const backend = new MockLLMBackend();
37
+ backend.setResponses([
38
+ { toolCalls: [createToolCall('get_weather', { city: 'Tokyo' })] },
39
+ { content: 'The weather in Tokyo is sunny and 22°C.' },
40
+ ]);
41
+
42
+ const first = await backend.chat({
43
+ model: 'test-model',
44
+ messages: [{ role: 'user', content: 'What is the weather in Tokyo?' }],
45
+ });
46
+
47
+ expect(first.finishReason).toBe('tool_calls');
48
+ expect(first.toolCalls?.[0].name).toBe('get_weather');
49
+
50
+ const second = await backend.chat({
51
+ model: 'test-model',
52
+ messages: [
53
+ { role: 'user', content: 'What is the weather in Tokyo?' },
54
+ { role: 'assistant', content: 'Checking weather...' },
55
+ {
56
+ role: 'tool',
57
+ content: '{"temperature": 22}',
58
+ toolCallId: first.toolCalls![0].id,
59
+ name: 'get_weather',
60
+ },
61
+ ],
62
+ });
63
+
64
+ expect(second.content).toBe('The weather in Tokyo is sunny and 22°C.');
65
+ expect(second.finishReason).toBe('stop');
66
+
67
+ expect(backend.getCallCount()).toBe(2);
68
+ expect(backend.wasCalledWith((req) => req.model === 'test-model')).toBe(true);
69
+ expect(backend.getLastCall()?.messages).toHaveLength(3);
70
+ });
71
+ });
72
+ ```
73
+
74
+ ### Testing with MockMemoryAdapter
75
+
76
+ A short, valid Vitest example using `MockMemoryAdapter`:
77
+
78
+ ```typescript
79
+ import { describe, it, expect } from 'vitest';
80
+ import { MockMemoryAdapter, createUserMessage } from '@cogitator-ai/test-utils';
81
+
82
+ describe('MockMemoryAdapter', () => {
83
+ it('stores conversation history and records calls', async () => {
84
+ const memory = new MockMemoryAdapter();
85
+ await memory.connect();
86
+ expect(memory.isConnected()).toBe(true);
87
+
88
+ const threadResult = await memory.createThread('agent-1', { topic: 'testing' }, 'thread-123');
89
+ expect(threadResult.success).toBe(true);
90
+
91
+ const addResult = await memory.addEntry({
92
+ threadId: 'thread-123',
93
+ message: createUserMessage('Remember this note'),
94
+ tokenCount: 4,
95
+ });
96
+ expect(addResult.success).toBe(true);
97
+
98
+ const entriesResult = await memory.getEntries({ threadId: 'thread-123' });
99
+ expect(entriesResult.success).toBe(true);
100
+ if (entriesResult.success) {
101
+ expect(entriesResult.data).toHaveLength(1);
102
+ expect(entriesResult.data[0].message.content).toBe('Remember this note');
103
+ }
104
+
105
+ expect(memory.getCallsFor('createThread')).toHaveLength(1);
106
+ expect(memory.getCallsFor('addEntry')).toHaveLength(1);
107
+
108
+ await memory.disconnect();
109
+ expect(memory.isConnected()).toBe(false);
110
+ });
111
+ });
112
+ ```
113
+
114
+ ---
115
+
116
+ ## Mocks
117
+
118
+ ### MockLLMBackend
119
+
120
+ `MockLLMBackend` implements `LLMBackend` (`provider: 'openai'`) and answers calls with scripted responses or stream chunks while recording all incoming requests.
121
+
122
+ #### Scripting Chat Responses
123
+
124
+ ```typescript
125
+ import { MockLLMBackend, createMockLLMBackend, createToolCall } from '@cogitator-ai/test-utils';
126
+
127
+ const backend = createMockLLMBackend();
128
+
129
+ // Set a single response for all chat() calls
130
+ backend.setResponse({ content: 'Hello, world!' });
131
+
132
+ // Set multiple responses returned in sequence (the last response repeats)
133
+ backend.setResponses([{ content: 'First response' }, { content: 'Second response' }]);
134
+
135
+ // Shortcut for scripted tool call responses
136
+ backend.setToolCallResponse([createToolCall('calculator', { expression: '2 + 2' })]);
137
+
138
+ // Set default delay (in ms) before answering
139
+ backend.setDefaultDelay(50);
140
+ ```
141
+
142
+ #### Streaming
143
+
144
+ ```typescript
145
+ import { MockLLMBackend, collectStreamContent } from '@cogitator-ai/test-utils';
146
+
147
+ const backend = new MockLLMBackend();
148
+
149
+ // Script a stream for chatStream()
150
+ backend.setStreamChunks([{ content: 'Hello' }, { content: ' world' }, { content: '!' }]);
151
+
152
+ // Or script multiple distinct streams in sequence
153
+ backend.setMultipleStreamChunks([[{ content: 'First stream' }], [{ content: 'Second stream' }]]);
154
+
155
+ const stream = backend.chatStream({
156
+ model: 'test-model',
157
+ messages: [{ role: 'user', content: 'Hi' }],
158
+ });
159
+
160
+ const text = await collectStreamContent(stream);
161
+ ```
162
+
163
+ #### Request Inspection
164
+
165
+ ```typescript
166
+ // Inspect recorded requests
167
+ const count = backend.getCallCount();
168
+ const allCalls = backend.getCalls();
169
+ const lastCall = backend.getLastCall();
170
+
171
+ // Predicate match
172
+ const wasCalled = backend.wasCalledWith((req) => req.model === 'test-model');
173
+
174
+ // Reset calls, responses, and stream chunks
175
+ backend.reset();
176
+ ```
177
+
178
+ ### MockMemoryAdapter
179
+
180
+ `MockMemoryAdapter` implements `MemoryAdapter` (`provider: 'memory'`) in memory using native Maps and records every invocation.
181
+
182
+ ```typescript
183
+ import { MockMemoryAdapter, createMockMemoryAdapter } from '@cogitator-ai/test-utils';
184
+
185
+ const memory = createMockMemoryAdapter();
186
+
187
+ // Lifecycle
188
+ await memory.connect();
189
+ const connected = memory.isConnected(); // true
190
+
191
+ // Threads
192
+ const thread = await memory.createThread('agent-id', { tag: 'session' }, 'custom-id');
193
+ const fetchedThread = await memory.getThread('custom-id');
194
+ await memory.updateThread('custom-id', { tag: 'updated' });
195
+
196
+ // Entries
197
+ await memory.addEntry({
198
+ threadId: 'custom-id',
199
+ message: { role: 'user', content: 'Hello' },
200
+ tokenCount: 1,
201
+ });
202
+ const entries = await memory.getEntries({ threadId: 'custom-id', limit: 10 });
203
+ const singleEntry = await memory.getEntry(
204
+ entries.success && entries.data[0] ? entries.data[0].id : ''
205
+ );
206
+ await memory.deleteEntry('entry-id');
207
+ await memory.clearThread('custom-id');
208
+ await memory.deleteThread('custom-id');
209
+
210
+ // Inspection
211
+ const calls = memory.getCalls(); // Array<{ method: string; args: unknown[] }>
212
+ const threadCalls = memory.getCallsFor('createThread');
213
+
214
+ // Reset all internal state and calls
215
+ memory.reset();
216
+ await memory.disconnect();
217
+ ```
218
+
219
+ ---
220
+
221
+ ## Fixtures
222
+
223
+ ### Agents
224
+
225
+ Generate test agent configs or agent-shaped objects with test defaults:
226
+
227
+ ```typescript
228
+ import { createTestAgentConfig, createTestAgent } from '@cogitator-ai/test-utils';
229
+
230
+ // AgentConfig with default name 'test-agent' and model 'mock/test-model'
231
+ const config = createTestAgentConfig({
232
+ name: 'custom-agent',
233
+ instructions: 'Custom instructions',
234
+ });
235
+
236
+ // Agent-shaped object implementing the Agent interface
237
+ const agent = createTestAgent({
238
+ name: 'test-agent',
239
+ });
240
+ ```
241
+
242
+ ### Messages and Tool Calls
243
+
244
+ Construct strongly typed conversation messages, tool calls, and tool results:
245
+
246
+ ```typescript
247
+ import {
248
+ createUserMessage,
249
+ createAssistantMessage,
250
+ createSystemMessage,
251
+ createToolMessage,
252
+ createMultimodalMessage,
253
+ createImageUrlMessage,
254
+ createToolCall,
255
+ createToolResult,
256
+ createConversation,
257
+ } from '@cogitator-ai/test-utils';
258
+
259
+ const userMsg = createUserMessage('Hello');
260
+ const assistantMsg = createAssistantMessage('Hi there');
261
+ const systemMsg = createSystemMessage('You are a test assistant');
262
+ const toolMsg = createToolMessage('call_1', '{"result": 42}', 'calculator');
263
+
264
+ const multimodalMsg = createMultimodalMessage([
265
+ { type: 'text', text: 'Describe this' },
266
+ { type: 'image_url', image_url: { url: 'https://example.com/image.png' } },
267
+ ]);
268
+ const imgMsg = createImageUrlMessage('Look at this', 'https://example.com/image.png');
269
+
270
+ const call = createToolCall('calculator', { expression: '2 + 2' });
271
+ const result = createToolResult(call, { result: 4 });
272
+
273
+ const turns = createConversation(
274
+ { role: 'user', content: 'Hello' },
275
+ { role: 'assistant', content: 'Hi' }
276
+ );
277
+ ```
278
+
279
+ ### Tools
280
+
281
+ Predefined JSON Schema tool fixtures for testing tool execution and schema passing:
282
+
283
+ ```typescript
284
+ import {
285
+ createTestTool,
286
+ createCalculatorTool,
287
+ createWeatherTool,
288
+ createFailingTool,
289
+ createSlowTool,
290
+ createToolSchema,
291
+ } from '@cogitator-ai/test-utils';
292
+
293
+ // Customizable SimpleTool
294
+ const customTool = createTestTool({
295
+ name: 'custom_tool',
296
+ description: 'A custom tool',
297
+ execute: async (args) => ({ echo: args }),
298
+ });
299
+
300
+ // Built-in tools
301
+ const calc = createCalculatorTool(); // evaluates arithmetic expressions ('2 + 2')
302
+ const weather = createWeatherTool(); // returns 22°C sunny weather for city
303
+ const failing = createFailingTool('Custom error'); // always throws
304
+ const slow = createSlowTool(50); // delays for specified ms before returning
305
+
306
+ // ToolSchema fixture
307
+ const schema = createToolSchema('my_tool', 'Tool description');
308
+ ```
309
+
310
+ ### Run Results
311
+
312
+ Mock `RunResult` structures for code consuming agent execution outcomes:
313
+
314
+ ```typescript
315
+ import {
316
+ createMockRunResult,
317
+ createRunResultWithToolCalls,
318
+ createRunResultWithMessages,
319
+ } from '@cogitator-ai/test-utils';
320
+
321
+ const baseResult = createMockRunResult('Final answer');
322
+ const withTools = createRunResultWithToolCalls('Output', [call]);
323
+ const withHistory = createRunResultWithMessages('Output', [userMsg, assistantMsg]);
324
+ ```
325
+
326
+ ---
327
+
328
+ ## Helpers
329
+
330
+ ### Stream Helpers
331
+
332
+ Helpers to consume and test asynchronous generator streams (`AsyncGenerator<ChatStreamChunk>`):
333
+
334
+ ```typescript
335
+ import {
336
+ collectStreamContent,
337
+ collectStreamChunks,
338
+ collectStreamToolCalls,
339
+ createMockReadableStream,
340
+ } from '@cogitator-ai/test-utils';
341
+
342
+ // Drains stream and returns concatenated text
343
+ const content = await collectStreamContent(stream);
344
+
345
+ // Drains stream and returns all chunks as an array
346
+ const chunks = await collectStreamChunks(stream);
347
+
348
+ // Drains stream and returns parsed tool calls
349
+ const toolCalls = await collectStreamToolCalls(stream);
350
+
351
+ // Creates a ReadableStream<Uint8Array> from string chunks (e.g. for SSE testing)
352
+ const byteStream = createMockReadableStream(['data: {"text":"hello"}\n\n']);
353
+ ```
354
+
355
+ ### Async Helpers
356
+
357
+ Utilities for handling asynchronous timing, polling, and timeouts:
358
+
359
+ ```typescript
360
+ import { waitFor, eventually, delay, withTimeout } from '@cogitator-ai/test-utils';
361
+
362
+ // Sleep for ms
363
+ await delay(100);
364
+
365
+ // Poll condition until truthy or timeout (defaults: timeout 5000ms, interval 50ms)
366
+ const ready = await waitFor(() => checkReady(), { timeout: 2000, interval: 20 });
367
+
368
+ // Retry assertion until it passes or timeout
369
+ await eventually(() => expect(isDone()).toBe(true), { timeout: 3000, interval: 50 });
370
+
371
+ // Wrap promise with timeout
372
+ const value = await withTimeout(fetchData(), 1000);
373
+ ```
374
+
375
+ ---
376
+
377
+ ## Conformance
378
+
379
+ A complete kit for verifying that custom or third-party `LLMBackend` implementations correctly adhere to provider protocols, error handling, cancellation, and streaming conventions.
380
+
381
+ ### Backend Conformance Suite
382
+
383
+ Run the full suite of conformance test cases against an `LLMBackend`:
384
+
385
+ ```typescript
386
+ import { describe, it } from 'vitest';
387
+ import {
388
+ backendConformanceCases,
389
+ openAIChatWire,
390
+ type BackendConformanceTarget,
391
+ } from '@cogitator-ai/test-utils';
392
+
393
+ const target: BackendConformanceTarget = {
394
+ name: 'MyCustomBackend',
395
+ model: 'gpt-4o',
396
+ wire: openAIChatWire,
397
+ createBackend: (url) => new MyCustomBackend({ baseUrl: url }),
398
+ };
399
+
400
+ describe('MyCustomBackend conformance', () => {
401
+ for (const c of backendConformanceCases(target)) {
402
+ (c.skip ? it.skip : it)(c.name, () => c.run());
403
+ }
404
+ });
405
+ ```
406
+
407
+ ### Fake Provider Server
408
+
409
+ Run a lightweight HTTP server emulating provider APIs:
410
+
411
+ ```typescript
412
+ import { startFakeProvider, sse } from '@cogitator-ai/test-utils';
413
+
414
+ const server = await startFakeProvider();
415
+
416
+ server.reply((req) => {
417
+ if (req.path === '/v1/chat/completions') {
418
+ return {
419
+ type: 'stream',
420
+ contentType: 'text/event-stream',
421
+ chunks: [sse({ choices: [{ delta: { content: 'Hi' } }] })],
422
+ };
423
+ }
424
+ return { type: 'json', status: 404, body: { error: 'Not found' } };
425
+ });
426
+
427
+ console.log(`Fake provider listening at: ${server.url}`);
428
+
429
+ await server.close();
430
+ ```
431
+
432
+ ### Wire Protocols & Encodings
433
+
434
+ Wire specifications and formatting helpers for popular LLM provider protocols:
435
+
436
+ - **Wires**: `openAIChatWire`, `openAIResponsesWire`, `anthropicWire`, `geminiWire`, `ollamaWire`, `bedrockConverseWire`
437
+ - **Encodings**: `sse(data, event?)`, `ndjson(data)`, `awsEventStreamMessage(headers, payload)`
438
+ - **Fixture**: `conformanceRequest`, `CONFORMANCE_FIXTURE`
439
+
440
+ ---
441
+
442
+ ## Public Exports
443
+
444
+ Public exports are grouped in `src/index.ts` as:
445
+
446
+ | Group | Module | Description |
447
+ | --------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
448
+ | **Mocks** | `@cogitator-ai/test-utils` (`./mocks`) | `MockLLMBackend`, `createMockLLMBackend`, `MockMemoryAdapter`, `createMockMemoryAdapter`, `MockChatResponse`, `MockStreamChunk` |
449
+ | **Fixtures** | `@cogitator-ai/test-utils` (`./fixtures`) | `createTestAgent`, `createTestAgentConfig`, `TestAgentConfig`, `createUserMessage`, `createAssistantMessage`, `createSystemMessage`, `createToolMessage`, `createMultimodalMessage`, `createImageUrlMessage`, `createToolCall`, `createToolResult`, `createConversation`, `createTestTool`, `createCalculatorTool`, `createWeatherTool`, `createFailingTool`, `createSlowTool`, `createToolSchema`, `TestToolOptions`, `SimpleTool`, `createMockRunResult`, `createRunResultWithToolCalls`, `createRunResultWithMessages`, `TestRunResultOptions` |
450
+ | **Helpers** | `@cogitator-ai/test-utils` (`./helpers`) | `collectStreamContent`, `collectStreamChunks`, `collectStreamToolCalls`, `createMockReadableStream`, `waitFor`, `eventually`, `delay`, `withTimeout` |
451
+ | **Conformance** | `@cogitator-ai/test-utils` (`./conformance`) | `startFakeProvider`, `FakeProvider`, `FakeProviderHandler`, `FakeProviderOptions`, `FakeProviderReply`, `FakeProviderRequest`, `backendConformanceCases`, `conformanceRequest`, `CONFORMANCE_FIXTURE`, `BackendConformanceCase`, `BackendConformanceTarget`, `ConformanceMode`, `ConformanceScenario`, `ProviderWire`, `openAIChatWire`, `openAIResponsesWire`, `anthropicWire`, `geminiWire`, `ollamaWire`, `bedrockConverseWire`, `sse`, `ndjson`, `awsEventStreamMessage` |
452
+
453
+ ---
454
+
455
+ ## License
456
+
457
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cogitator-ai/test-utils",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Shared testing utilities for Cogitator packages",
5
5
  "keywords": [
6
6
  "cogitator",