@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.
@@ -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
- ```
@@ -1,5 +0,0 @@
1
- import type { BrainEvent } from '../dsl/brain.js';
2
-
3
- export interface Adapter<Options extends object = any> {
4
- dispatch(event: BrainEvent<Options>): void | Promise<void>;
5
- }
@@ -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
- }