@positronic/core 0.0.3 → 0.0.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.
- package/package.json +5 -1
- package/CLAUDE.md +0 -141
- package/dist/src/dsl/brain-runner.test.js +0 -733
- package/dist/src/dsl/brain.test.js +0 -4225
- package/dist/src/test-utils.js +0 -474
- package/dist/src/testing.js +0 -3
- package/dist/types/test-utils.d.ts +0 -94
- package/dist/types/test-utils.d.ts.map +0 -1
- package/dist/types/testing.d.ts +0 -2
- package/dist/types/testing.d.ts.map +0 -1
- package/docs/core-testing-guide.md +0 -289
- package/src/adapters/types.ts +0 -5
- package/src/clients/types.ts +0 -54
- package/src/dsl/brain-runner.test.ts +0 -384
- package/src/dsl/brain-runner.ts +0 -111
- package/src/dsl/brain.test.ts +0 -1981
- package/src/dsl/brain.ts +0 -740
- package/src/dsl/constants.ts +0 -16
- package/src/dsl/json-patch.ts +0 -42
- package/src/dsl/types.ts +0 -13
- package/src/index.ts +0 -36
- package/src/resources/resource-loader.ts +0 -8
- package/src/resources/resources.ts +0 -267
- package/src/test-utils.ts +0 -254
- package/test/resources.test.ts +0 -248
- package/tsconfig.json +0 -10
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
# Core Testing Guide
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
The Core package tests focus on Brain DSL workflows, event sequences, and state management. This guide helps you write effective tests while avoiding common pitfalls.
|
|
6
|
-
|
|
7
|
-
## Key Testing Patterns
|
|
8
|
-
|
|
9
|
-
### 1. Brain Event Collection Pattern
|
|
10
|
-
|
|
11
|
-
**The Challenge**: Brains emit async events that need to be collected and verified.
|
|
12
|
-
|
|
13
|
-
**Solution**: Always collect ALL events before making assertions:
|
|
14
|
-
|
|
15
|
-
```typescript
|
|
16
|
-
// CORRECT: Collect all events first
|
|
17
|
-
const events = [];
|
|
18
|
-
for await (const event of brain.run({ client: mockClient })) {
|
|
19
|
-
events.push(event);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Now make assertions
|
|
23
|
-
expect(events.map(e => e.type)).toContain(BRAIN_EVENTS.COMPLETE);
|
|
24
|
-
|
|
25
|
-
// WRONG: Don't try to assert while iterating
|
|
26
|
-
for await (const event of brain.run()) {
|
|
27
|
-
expect(event).toBeDefined(); // This can miss events!
|
|
28
|
-
}
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
### 2. State Reconstruction from Patches
|
|
32
|
-
|
|
33
|
-
**The Challenge**: Brain state is represented as JSON patches, not direct state objects.
|
|
34
|
-
|
|
35
|
-
**Solution**: Apply patches to reconstruct final state:
|
|
36
|
-
|
|
37
|
-
```typescript
|
|
38
|
-
import { applyPatches } from '@positronic/core';
|
|
39
|
-
|
|
40
|
-
// Helper to reconstruct state
|
|
41
|
-
function reconstructState(events: BrainEvent[]) {
|
|
42
|
-
let state = {};
|
|
43
|
-
for (const event of events) {
|
|
44
|
-
if (event.type === BRAIN_EVENTS.STEP_COMPLETE) {
|
|
45
|
-
state = applyPatches(state, [event.patch]);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return state;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Usage
|
|
52
|
-
const events = await collectAllEvents(brain.run());
|
|
53
|
-
const finalState = reconstructState(events);
|
|
54
|
-
expect(finalState).toEqual({ expected: 'state' });
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
### 3. Mock Setup for AI Clients
|
|
58
|
-
|
|
59
|
-
**The Challenge**: Brains often depend on AI client calls that need specific mock setup.
|
|
60
|
-
|
|
61
|
-
**Solution**: Create properly typed mocks:
|
|
62
|
-
|
|
63
|
-
```typescript
|
|
64
|
-
// Setup mock client with proper typing
|
|
65
|
-
const mockGenerateObject = jest.fn<ObjectGenerator['generateObject']>();
|
|
66
|
-
const mockClient: jest.Mocked<ObjectGenerator> = {
|
|
67
|
-
generateObject: mockGenerateObject,
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
// Configure response BEFORE running brain
|
|
71
|
-
mockGenerateObject.mockResolvedValue({ result: 'test data' });
|
|
72
|
-
|
|
73
|
-
// Now run the brain
|
|
74
|
-
const brain = brain('test').step('Gen', async ({ client }) => {
|
|
75
|
-
const res = await client.generateObject({ prompt: 'test' });
|
|
76
|
-
return { data: res.result };
|
|
77
|
-
});
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
### 4. Resource Loading Mocks
|
|
81
|
-
|
|
82
|
-
**The Challenge**: Resources use a proxy API that's tricky to mock.
|
|
83
|
-
|
|
84
|
-
**Solution**: Mock the underlying loader, not the proxy:
|
|
85
|
-
|
|
86
|
-
```typescript
|
|
87
|
-
const mockResourceLoad = jest.fn();
|
|
88
|
-
const mockResourceLoader: ResourceLoader = {
|
|
89
|
-
load: mockResourceLoad,
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
// Setup resource responses
|
|
93
|
-
const mockResources = {
|
|
94
|
-
'example.txt': { type: 'text', content: 'Hello' },
|
|
95
|
-
'data.json': { type: 'text', content: '{"key": "value"}' },
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
mockResourceLoad.mockImplementation(async (path) => {
|
|
99
|
-
const resource = mockResources[path];
|
|
100
|
-
if (!resource) throw new Error(`Resource not found: ${path}`);
|
|
101
|
-
return resource;
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
// Use in brain - the proxy API will call your mock
|
|
105
|
-
const brain = brain('test').step('Load', async ({ resources }) => {
|
|
106
|
-
const text = await resources.example.loadText(); // Proxy API
|
|
107
|
-
return { content: text };
|
|
108
|
-
});
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### 5. Testing Error Events
|
|
112
|
-
|
|
113
|
-
**The Challenge**: Errors emit special events but brain execution continues.
|
|
114
|
-
|
|
115
|
-
**Solution**: Look for ERROR events, not exceptions:
|
|
116
|
-
|
|
117
|
-
```typescript
|
|
118
|
-
const errorBrain = brain('test').step('Fail', () => {
|
|
119
|
-
throw new Error('Step failed');
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
const events = [];
|
|
123
|
-
for await (const event of errorBrain.run()) {
|
|
124
|
-
events.push(event);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Find the error event
|
|
128
|
-
const errorEvent = events.find(e => e.type === BRAIN_EVENTS.ERROR);
|
|
129
|
-
expect(errorEvent).toBeDefined();
|
|
130
|
-
expect(errorEvent?.error.message).toBe('Step failed');
|
|
131
|
-
|
|
132
|
-
// Brain still completes!
|
|
133
|
-
expect(events.some(e => e.type === BRAIN_EVENTS.COMPLETE)).toBe(true);
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
### 6. Type Inference Testing
|
|
137
|
-
|
|
138
|
-
**The Challenge**: Brain DSL uses complex TypeScript inference that needs testing.
|
|
139
|
-
|
|
140
|
-
**Solution**: Use compile-time type assertions:
|
|
141
|
-
|
|
142
|
-
```typescript
|
|
143
|
-
// Define a type equality helper
|
|
144
|
-
type AssertEquals<T, U> = T extends U ? (U extends T ? true : false) : false;
|
|
145
|
-
|
|
146
|
-
// Test that state types are inferred correctly
|
|
147
|
-
const typedBrain = brain('test')
|
|
148
|
-
.step('Init', () => ({ count: 0 }))
|
|
149
|
-
.step('Inc', ({ state }) => ({ count: state.count + 1 }));
|
|
150
|
-
|
|
151
|
-
// Extract the inferred state type
|
|
152
|
-
type InferredState = typeof typedBrain extends Brain<infer S> ? S : never;
|
|
153
|
-
|
|
154
|
-
// This line will fail compilation if types don't match
|
|
155
|
-
type Test = AssertEquals<InferredState, { count: number }>;
|
|
156
|
-
const _: Test = true;
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
## Common Pitfalls & Solutions
|
|
160
|
-
|
|
161
|
-
### Pitfall 1: Forgetting to Mock Client Methods
|
|
162
|
-
|
|
163
|
-
```typescript
|
|
164
|
-
// WRONG: Forgot to mock generateObject
|
|
165
|
-
const brain = brain('test').step('Gen', async ({ client }) => {
|
|
166
|
-
const res = await client.generateObject({ prompt: 'test' });
|
|
167
|
-
return res;
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
// This will fail with "mockGenerateObject is not a function"
|
|
171
|
-
await brain.run({ client: mockClient });
|
|
172
|
-
|
|
173
|
-
// CORRECT: Always mock methods before use
|
|
174
|
-
mockGenerateObject.mockResolvedValue({ data: 'test' });
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
### Pitfall 2: Testing During Event Iteration
|
|
178
|
-
|
|
179
|
-
```typescript
|
|
180
|
-
// WRONG: Testing while iterating can miss events
|
|
181
|
-
let foundComplete = false;
|
|
182
|
-
for await (const event of brain.run()) {
|
|
183
|
-
if (event.type === BRAIN_EVENTS.COMPLETE) {
|
|
184
|
-
foundComplete = true;
|
|
185
|
-
break; // Stops iteration early!
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// CORRECT: Collect all events first
|
|
190
|
-
const events = await collectAllEvents(brain.run());
|
|
191
|
-
const foundComplete = events.some(e => e.type === BRAIN_EVENTS.COMPLETE);
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
### Pitfall 3: Not Handling Async Steps
|
|
195
|
-
|
|
196
|
-
```typescript
|
|
197
|
-
// WRONG: Synchronous step function when async is needed
|
|
198
|
-
const brain = brain('test').step('Async', ({ client }) => {
|
|
199
|
-
// This won't wait for the promise!
|
|
200
|
-
client.generateObject({ prompt: 'test' });
|
|
201
|
-
return { done: true };
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
// CORRECT: Use async/await
|
|
205
|
-
const brain = brain('test').step('Async', async ({ client }) => {
|
|
206
|
-
const result = await client.generateObject({ prompt: 'test' });
|
|
207
|
-
return { done: true, result };
|
|
208
|
-
});
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
### Pitfall 4: Incorrect Event Sequence Expectations
|
|
212
|
-
|
|
213
|
-
```typescript
|
|
214
|
-
// WRONG: Expecting only main events
|
|
215
|
-
expect(events.map(e => e.type)).toEqual([
|
|
216
|
-
BRAIN_EVENTS.START,
|
|
217
|
-
BRAIN_EVENTS.COMPLETE
|
|
218
|
-
]);
|
|
219
|
-
|
|
220
|
-
// CORRECT: Include all events in sequence
|
|
221
|
-
expect(events.map(e => e.type)).toEqual([
|
|
222
|
-
BRAIN_EVENTS.START,
|
|
223
|
-
BRAIN_EVENTS.STEP_STATUS, // Don't forget status events!
|
|
224
|
-
BRAIN_EVENTS.STEP_START,
|
|
225
|
-
BRAIN_EVENTS.STEP_COMPLETE,
|
|
226
|
-
BRAIN_EVENTS.STEP_STATUS,
|
|
227
|
-
BRAIN_EVENTS.COMPLETE
|
|
228
|
-
]);
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
## Quick Test Template
|
|
232
|
-
|
|
233
|
-
```typescript
|
|
234
|
-
import { brain, BRAIN_EVENTS, applyPatches } from '@positronic/core';
|
|
235
|
-
import type { ObjectGenerator, ResourceLoader } from '@positronic/core';
|
|
236
|
-
|
|
237
|
-
describe('my brain feature', () => {
|
|
238
|
-
// Setup mocks
|
|
239
|
-
const mockGenerateObject = jest.fn<ObjectGenerator['generateObject']>();
|
|
240
|
-
const mockClient: jest.Mocked<ObjectGenerator> = {
|
|
241
|
-
generateObject: mockGenerateObject,
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
beforeEach(() => {
|
|
245
|
-
jest.clearAllMocks();
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it('should do something', async () => {
|
|
249
|
-
// Configure mocks
|
|
250
|
-
mockGenerateObject.mockResolvedValue({ result: 'test' });
|
|
251
|
-
|
|
252
|
-
// Define brain
|
|
253
|
-
const testBrain = brain('test')
|
|
254
|
-
.step('Process', async ({ client }) => {
|
|
255
|
-
const res = await client.generateObject({ prompt: 'test' });
|
|
256
|
-
return { processed: res.result };
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
// Collect all events
|
|
260
|
-
const events = [];
|
|
261
|
-
for await (const event of testBrain.run({ client: mockClient })) {
|
|
262
|
-
events.push(event);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Verify final state
|
|
266
|
-
let finalState = {};
|
|
267
|
-
for (const event of events) {
|
|
268
|
-
if (event.type === BRAIN_EVENTS.STEP_COMPLETE) {
|
|
269
|
-
finalState = applyPatches(finalState, [event.patch]);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
expect(finalState).toEqual({ processed: 'test' });
|
|
273
|
-
|
|
274
|
-
// Verify completion
|
|
275
|
-
expect(events.some(e => e.type === BRAIN_EVENTS.COMPLETE)).toBe(true);
|
|
276
|
-
});
|
|
277
|
-
});
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
## Running Tests
|
|
281
|
-
|
|
282
|
-
```bash
|
|
283
|
-
# From monorepo root (required!)
|
|
284
|
-
npm test -- packages/core # Run all core tests
|
|
285
|
-
npm test -- brain.test.ts # Run specific test file
|
|
286
|
-
npm test -- -t "should process" # Run tests matching pattern
|
|
287
|
-
npm run test:watch # Watch mode
|
|
288
|
-
npm run build:workspaces # Ensure TypeScript compiles
|
|
289
|
-
```
|
package/src/adapters/types.ts
DELETED
package/src/clients/types.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Represents a message in a conversation, used as input for the Generator.
|
|
5
|
-
*/
|
|
6
|
-
export type Message = {
|
|
7
|
-
role: 'user' | 'assistant' | 'system';
|
|
8
|
-
content: string;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Interface for AI model interactions, focused on generating structured objects
|
|
13
|
-
* and potentially other types of content in the future.
|
|
14
|
-
*/
|
|
15
|
-
export interface ObjectGenerator {
|
|
16
|
-
/**
|
|
17
|
-
* Generates a structured JSON object that conforms to the provided Zod schema.
|
|
18
|
-
*
|
|
19
|
-
* This method supports both simple single-string prompts and more complex
|
|
20
|
-
* multi-turn conversations via the `messages` array.
|
|
21
|
-
*/
|
|
22
|
-
generateObject<T extends z.AnyZodObject>(params: {
|
|
23
|
-
/**
|
|
24
|
-
* The definition of the expected output object, including its Zod schema
|
|
25
|
-
* and a name for state management within the brain.
|
|
26
|
-
*/
|
|
27
|
-
schema: T;
|
|
28
|
-
schemaName: string;
|
|
29
|
-
schemaDescription?: string;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* A simple prompt string for single-turn requests.
|
|
33
|
-
* If provided, this will typically be treated as the latest user input.
|
|
34
|
-
* If `messages` are also provided, this `prompt` is usually appended
|
|
35
|
-
* as a new user message to the existing `messages` array.
|
|
36
|
-
*/
|
|
37
|
-
prompt?: string;
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* An array of messages forming the conversation history.
|
|
41
|
-
* Use this for multi-turn conversations or when you need to provide
|
|
42
|
-
* a sequence of interactions (e.g., user, assistant, tool calls).
|
|
43
|
-
* If `prompt` is also provided, it's typically added to this history.
|
|
44
|
-
*/
|
|
45
|
-
messages?: Message[];
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* An optional system-level instruction or context to guide the model's
|
|
49
|
-
* behavior for the entire interaction. Implementations will typically
|
|
50
|
-
* prepend this as a `system` role message to the full message list.
|
|
51
|
-
*/
|
|
52
|
-
system?: string;
|
|
53
|
-
}): Promise<z.infer<T>>;
|
|
54
|
-
}
|