@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
package/src/dsl/brain.test.ts
DELETED
|
@@ -1,1981 +0,0 @@
|
|
|
1
|
-
import { BRAIN_EVENTS, STATUS } from './constants.js';
|
|
2
|
-
import { applyPatches, type JsonPatch } from './json-patch.js';
|
|
3
|
-
import { State } from './types.js';
|
|
4
|
-
import {
|
|
5
|
-
brain,
|
|
6
|
-
type BrainEvent,
|
|
7
|
-
type BrainErrorEvent,
|
|
8
|
-
type SerializedStep,
|
|
9
|
-
type SerializedStepStatus,
|
|
10
|
-
} from './brain.js';
|
|
11
|
-
import { z } from 'zod';
|
|
12
|
-
import { jest } from '@jest/globals';
|
|
13
|
-
import { ObjectGenerator } from '../clients/types.js';
|
|
14
|
-
import { createResources, type Resources } from '../resources/resources.js';
|
|
15
|
-
import type { ResourceLoader } from '../resources/resource-loader.js';
|
|
16
|
-
|
|
17
|
-
// Helper function to get the next value from an AsyncIterator
|
|
18
|
-
const nextStep = async <T>(brainRun: AsyncIterator<T>): Promise<T> => {
|
|
19
|
-
const result = await brainRun.next();
|
|
20
|
-
if (result.done) throw new Error('Iterator is done');
|
|
21
|
-
return result.value;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
// Define a Logger interface for testing
|
|
25
|
-
interface Logger {
|
|
26
|
-
log: (message: string) => void;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Mock services for testing
|
|
30
|
-
const testLogger: Logger = {
|
|
31
|
-
log: jest.fn(),
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
type AssertEquals<T, U> = 0 extends 1 & T
|
|
35
|
-
? false // fails if T is any
|
|
36
|
-
: 0 extends 1 & U
|
|
37
|
-
? false // fails if U is any
|
|
38
|
-
: [T] extends [U]
|
|
39
|
-
? [U] extends [T]
|
|
40
|
-
? true
|
|
41
|
-
: false
|
|
42
|
-
: false;
|
|
43
|
-
|
|
44
|
-
// Mock ObjectGenerator for testing
|
|
45
|
-
const mockGenerateObject = jest.fn<ObjectGenerator['generateObject']>();
|
|
46
|
-
const mockClient: jest.Mocked<ObjectGenerator> = {
|
|
47
|
-
generateObject: mockGenerateObject,
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
// Mock Resources for testing
|
|
51
|
-
const mockResourceLoad = jest.fn(
|
|
52
|
-
async (
|
|
53
|
-
resourceName: string,
|
|
54
|
-
type?: 'text' | 'binary'
|
|
55
|
-
): Promise<string | Buffer> => {
|
|
56
|
-
if (type === 'binary')
|
|
57
|
-
return Buffer.from(`mock ${resourceName} binary content`);
|
|
58
|
-
return `mock ${resourceName} text content`;
|
|
59
|
-
}
|
|
60
|
-
) as jest.MockedFunction<ResourceLoader['load']>;
|
|
61
|
-
|
|
62
|
-
const mockResourceLoader: ResourceLoader = {
|
|
63
|
-
load: mockResourceLoad,
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const testManifest = {
|
|
67
|
-
myFile: {
|
|
68
|
-
type: 'text' as const,
|
|
69
|
-
key: 'myFile',
|
|
70
|
-
path: '/test/myFile.txt',
|
|
71
|
-
},
|
|
72
|
-
myBinaryFile: {
|
|
73
|
-
type: 'binary' as const,
|
|
74
|
-
key: 'myBinaryFile',
|
|
75
|
-
path: '/test/myBinaryFile.bin',
|
|
76
|
-
},
|
|
77
|
-
nested: {
|
|
78
|
-
anotherFile: {
|
|
79
|
-
type: 'text' as const,
|
|
80
|
-
key: 'anotherFile',
|
|
81
|
-
path: '/test/anotherFile.txt',
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
} as const;
|
|
85
|
-
const mockResources = createResources(mockResourceLoader, testManifest);
|
|
86
|
-
|
|
87
|
-
describe('brain creation', () => {
|
|
88
|
-
beforeEach(() => {
|
|
89
|
-
mockGenerateObject.mockClear();
|
|
90
|
-
mockResourceLoad.mockClear();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('should create a brain with steps and run through them', async () => {
|
|
94
|
-
const testBrain = brain('test brain')
|
|
95
|
-
.step('First step', () => {
|
|
96
|
-
return { count: 1 };
|
|
97
|
-
})
|
|
98
|
-
.step('Second step', ({ state }) => ({
|
|
99
|
-
...state,
|
|
100
|
-
doubled: state.count * 2,
|
|
101
|
-
}));
|
|
102
|
-
|
|
103
|
-
const brainRun = testBrain.run({
|
|
104
|
-
client: mockClient,
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// Check start event
|
|
108
|
-
const startResult = await brainRun.next();
|
|
109
|
-
expect(startResult.value).toEqual(
|
|
110
|
-
expect.objectContaining({
|
|
111
|
-
type: BRAIN_EVENTS.START,
|
|
112
|
-
status: STATUS.RUNNING,
|
|
113
|
-
brainTitle: 'test brain',
|
|
114
|
-
brainDescription: undefined,
|
|
115
|
-
})
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
// Skip initial step status event
|
|
119
|
-
await nextStep(brainRun);
|
|
120
|
-
|
|
121
|
-
// Check first step start
|
|
122
|
-
const firstStepStartResult = await nextStep(brainRun);
|
|
123
|
-
expect(firstStepStartResult).toEqual(
|
|
124
|
-
expect.objectContaining({
|
|
125
|
-
type: BRAIN_EVENTS.STEP_START,
|
|
126
|
-
status: STATUS.RUNNING,
|
|
127
|
-
stepTitle: 'First step',
|
|
128
|
-
stepId: expect.any(String),
|
|
129
|
-
})
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
// Check first step status (running)
|
|
133
|
-
const firstStepStatusRunning = await nextStep(brainRun);
|
|
134
|
-
expect(firstStepStatusRunning).toEqual(
|
|
135
|
-
expect.objectContaining({
|
|
136
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
137
|
-
steps: expect.any(Array),
|
|
138
|
-
})
|
|
139
|
-
);
|
|
140
|
-
if (firstStepStatusRunning.type === BRAIN_EVENTS.STEP_STATUS) {
|
|
141
|
-
expect(firstStepStatusRunning.steps[0].status).toBe(STATUS.RUNNING);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Check first step completion
|
|
145
|
-
const firstStepResult = await nextStep(brainRun);
|
|
146
|
-
expect(firstStepResult).toEqual(
|
|
147
|
-
expect.objectContaining({
|
|
148
|
-
type: BRAIN_EVENTS.STEP_COMPLETE,
|
|
149
|
-
status: STATUS.RUNNING,
|
|
150
|
-
stepTitle: 'First step',
|
|
151
|
-
stepId: expect.any(String),
|
|
152
|
-
patch: [
|
|
153
|
-
{
|
|
154
|
-
op: 'add',
|
|
155
|
-
path: '/count',
|
|
156
|
-
value: 1,
|
|
157
|
-
},
|
|
158
|
-
],
|
|
159
|
-
})
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
// Step Status Event
|
|
163
|
-
await nextStep(brainRun);
|
|
164
|
-
|
|
165
|
-
// Check second step start
|
|
166
|
-
const secondStepStartResult = await nextStep(brainRun);
|
|
167
|
-
expect(secondStepStartResult).toEqual(
|
|
168
|
-
expect.objectContaining({
|
|
169
|
-
type: BRAIN_EVENTS.STEP_START,
|
|
170
|
-
status: STATUS.RUNNING,
|
|
171
|
-
stepTitle: 'Second step',
|
|
172
|
-
stepId: expect.any(String),
|
|
173
|
-
})
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
// Check second step status (running)
|
|
177
|
-
const secondStepStatusRunning = await nextStep(brainRun);
|
|
178
|
-
expect(secondStepStatusRunning).toEqual(
|
|
179
|
-
expect.objectContaining({
|
|
180
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
181
|
-
steps: expect.any(Array),
|
|
182
|
-
})
|
|
183
|
-
);
|
|
184
|
-
if (secondStepStatusRunning.type === BRAIN_EVENTS.STEP_STATUS) {
|
|
185
|
-
expect(secondStepStatusRunning.steps[1].status).toBe(STATUS.RUNNING);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Check second step completion
|
|
189
|
-
const secondStepResult = await nextStep(brainRun);
|
|
190
|
-
expect(secondStepResult).toEqual(
|
|
191
|
-
expect.objectContaining({
|
|
192
|
-
type: BRAIN_EVENTS.STEP_COMPLETE,
|
|
193
|
-
stepTitle: 'Second step',
|
|
194
|
-
stepId: expect.any(String),
|
|
195
|
-
patch: [
|
|
196
|
-
{
|
|
197
|
-
op: 'add',
|
|
198
|
-
path: '/doubled',
|
|
199
|
-
value: 2,
|
|
200
|
-
},
|
|
201
|
-
],
|
|
202
|
-
})
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
// Step Status Event
|
|
206
|
-
const stepStatusResult = await nextStep(brainRun);
|
|
207
|
-
expect(stepStatusResult).toEqual(
|
|
208
|
-
expect.objectContaining({
|
|
209
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
210
|
-
steps: [
|
|
211
|
-
expect.objectContaining({
|
|
212
|
-
title: 'First step',
|
|
213
|
-
status: STATUS.COMPLETE,
|
|
214
|
-
id: expect.any(String),
|
|
215
|
-
}),
|
|
216
|
-
expect.objectContaining({
|
|
217
|
-
title: 'Second step',
|
|
218
|
-
status: STATUS.COMPLETE,
|
|
219
|
-
id: expect.any(String),
|
|
220
|
-
}),
|
|
221
|
-
],
|
|
222
|
-
})
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
// Check brain completion
|
|
226
|
-
const completeResult = await nextStep(brainRun);
|
|
227
|
-
expect(completeResult).toEqual(
|
|
228
|
-
expect.objectContaining({
|
|
229
|
-
type: BRAIN_EVENTS.COMPLETE,
|
|
230
|
-
status: STATUS.COMPLETE,
|
|
231
|
-
brainTitle: 'test brain',
|
|
232
|
-
brainDescription: undefined,
|
|
233
|
-
})
|
|
234
|
-
);
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it('should create a brain with a name and description when passed an object', async () => {
|
|
238
|
-
const testBrain = brain({
|
|
239
|
-
title: 'my named brain',
|
|
240
|
-
description: 'some description',
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
const brainRun = testBrain.run({
|
|
244
|
-
client: mockClient,
|
|
245
|
-
});
|
|
246
|
-
const startResult = await brainRun.next();
|
|
247
|
-
expect(startResult.value).toEqual(
|
|
248
|
-
expect.objectContaining({
|
|
249
|
-
type: BRAIN_EVENTS.START,
|
|
250
|
-
status: STATUS.RUNNING,
|
|
251
|
-
brainTitle: 'my named brain',
|
|
252
|
-
brainDescription: 'some description',
|
|
253
|
-
options: {},
|
|
254
|
-
})
|
|
255
|
-
);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it('should create a brain with just a name when passed a string', async () => {
|
|
259
|
-
const testBrain = brain('simple brain');
|
|
260
|
-
const brainRun = testBrain.run({
|
|
261
|
-
client: mockClient,
|
|
262
|
-
});
|
|
263
|
-
const startResult = await brainRun.next();
|
|
264
|
-
expect(startResult.value).toEqual(
|
|
265
|
-
expect.objectContaining({
|
|
266
|
-
type: BRAIN_EVENTS.START,
|
|
267
|
-
status: STATUS.RUNNING,
|
|
268
|
-
brainTitle: 'simple brain',
|
|
269
|
-
brainDescription: undefined,
|
|
270
|
-
options: {},
|
|
271
|
-
})
|
|
272
|
-
);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it('should allow overriding client per step', async () => {
|
|
276
|
-
const overrideClient: jest.Mocked<ObjectGenerator> = {
|
|
277
|
-
generateObject: jest
|
|
278
|
-
.fn<ObjectGenerator['generateObject']>()
|
|
279
|
-
.mockResolvedValue({ override: true }),
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
// Make sure that for the default prompt the default client returns a known value.
|
|
283
|
-
mockClient.generateObject.mockResolvedValueOnce({ override: false });
|
|
284
|
-
|
|
285
|
-
const testBrain = brain('Client Override Test')
|
|
286
|
-
.prompt('Use default client', {
|
|
287
|
-
template: () => 'prompt1',
|
|
288
|
-
outputSchema: {
|
|
289
|
-
schema: z.object({ override: z.boolean() }),
|
|
290
|
-
name: 'overrideResponse',
|
|
291
|
-
},
|
|
292
|
-
})
|
|
293
|
-
.prompt('Use override client', {
|
|
294
|
-
template: () => 'prompt2',
|
|
295
|
-
outputSchema: {
|
|
296
|
-
schema: z.object({ override: z.boolean() }),
|
|
297
|
-
name: 'overrideResponse',
|
|
298
|
-
},
|
|
299
|
-
client: overrideClient,
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
// Run the brain and capture all events
|
|
303
|
-
const events = [];
|
|
304
|
-
let finalState = {};
|
|
305
|
-
for await (const event of testBrain.run({
|
|
306
|
-
client: mockClient,
|
|
307
|
-
})) {
|
|
308
|
-
events.push(event);
|
|
309
|
-
if (event.type === BRAIN_EVENTS.STEP_COMPLETE) {
|
|
310
|
-
finalState = applyPatches(finalState, [event.patch]);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Final state should include both responses
|
|
315
|
-
expect(finalState).toEqual({
|
|
316
|
-
overrideResponse: { override: true },
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
// Verify that each client was used correctly based on the supplied prompt configuration.
|
|
320
|
-
expect(mockClient.generateObject).toHaveBeenCalledWith({
|
|
321
|
-
schema: expect.any(z.ZodObject),
|
|
322
|
-
schemaName: 'overrideResponse',
|
|
323
|
-
prompt: 'prompt1',
|
|
324
|
-
});
|
|
325
|
-
expect(overrideClient.generateObject).toHaveBeenCalledWith({
|
|
326
|
-
schema: expect.any(z.ZodObject),
|
|
327
|
-
schemaName: 'overrideResponse',
|
|
328
|
-
prompt: 'prompt2',
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
// Verify that the state was updated correctly with values from both clients.
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
it('should use the provided brainRunId for the initial run if supplied', async () => {
|
|
335
|
-
const testBrain = brain('Brain with Provided ID');
|
|
336
|
-
const providedId = 'my-custom-run-id-123';
|
|
337
|
-
|
|
338
|
-
const brainRun = testBrain.run({
|
|
339
|
-
client: mockClient,
|
|
340
|
-
brainRunId: providedId,
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
// Check start event
|
|
344
|
-
const startResult = await brainRun.next();
|
|
345
|
-
expect(startResult.value).toEqual(
|
|
346
|
-
expect.objectContaining({
|
|
347
|
-
type: BRAIN_EVENTS.START,
|
|
348
|
-
status: STATUS.RUNNING,
|
|
349
|
-
brainTitle: 'Brain with Provided ID',
|
|
350
|
-
brainRunId: providedId, // Expect the provided ID here
|
|
351
|
-
})
|
|
352
|
-
);
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
describe('Resource Availability in Steps', () => {
|
|
356
|
-
it('should make resources available in a simple step action', async () => {
|
|
357
|
-
let loadedText: string | undefined;
|
|
358
|
-
const testBrain = brain('Resource Step Test').step(
|
|
359
|
-
'Load My File',
|
|
360
|
-
async ({ resources }) => {
|
|
361
|
-
loadedText = await (resources.myFile as any).loadText();
|
|
362
|
-
return { loadedText };
|
|
363
|
-
}
|
|
364
|
-
);
|
|
365
|
-
|
|
366
|
-
const run = testBrain.run({
|
|
367
|
-
client: mockClient,
|
|
368
|
-
resources: mockResources,
|
|
369
|
-
});
|
|
370
|
-
// Iterate through to completion
|
|
371
|
-
for await (const _ of run) {
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
expect(mockResourceLoad).toHaveBeenCalledWith('myFile', 'text');
|
|
375
|
-
expect(loadedText).toBe('mock myFile text content');
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
it('should make resources available in a prompt action and reducer', async () => {
|
|
379
|
-
mockGenerateObject.mockResolvedValue({ summary: 'Generated summary' });
|
|
380
|
-
let promptTemplateString: string | undefined;
|
|
381
|
-
let reduceContent: string | undefined;
|
|
382
|
-
|
|
383
|
-
const testBrain = brain<
|
|
384
|
-
{ myOption: string },
|
|
385
|
-
{
|
|
386
|
-
promptResult?: any;
|
|
387
|
-
reducedResult?: string;
|
|
388
|
-
fileContentForPrompt?: string;
|
|
389
|
-
}
|
|
390
|
-
>('Resource Prompt Test')
|
|
391
|
-
.step('Load File For Prompt', async ({ resources }) => {
|
|
392
|
-
const fileContent = await (resources.myFile as any).loadText();
|
|
393
|
-
return { fileContentForPrompt: fileContent };
|
|
394
|
-
})
|
|
395
|
-
.prompt(
|
|
396
|
-
'Summarize File',
|
|
397
|
-
{
|
|
398
|
-
template: (state) => {
|
|
399
|
-
return 'Summarize: ' + state.fileContentForPrompt;
|
|
400
|
-
},
|
|
401
|
-
outputSchema: {
|
|
402
|
-
schema: z.object({ summary: z.string() }),
|
|
403
|
-
name: 'promptResult' as const,
|
|
404
|
-
},
|
|
405
|
-
},
|
|
406
|
-
async ({ state, response, resources }) => {
|
|
407
|
-
reduceContent = await (
|
|
408
|
-
(resources.nested as any).anotherFile as any
|
|
409
|
-
).loadText();
|
|
410
|
-
return {
|
|
411
|
-
...state,
|
|
412
|
-
reducedResult: response.summary + ' based on ' + reduceContent,
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
);
|
|
416
|
-
|
|
417
|
-
const run = testBrain.run({
|
|
418
|
-
client: mockClient,
|
|
419
|
-
resources: mockResources,
|
|
420
|
-
options: { myOption: 'test' },
|
|
421
|
-
});
|
|
422
|
-
let finalState: any = {};
|
|
423
|
-
const allPatches: JsonPatch[] = [];
|
|
424
|
-
const initialStateForReconstruction = {};
|
|
425
|
-
|
|
426
|
-
for await (const event of run) {
|
|
427
|
-
if (event.type === BRAIN_EVENTS.STEP_COMPLETE && event.patch) {
|
|
428
|
-
allPatches.push(event.patch);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
finalState = applyPatches(initialStateForReconstruction, allPatches);
|
|
432
|
-
|
|
433
|
-
expect(mockResourceLoad).toHaveBeenCalledWith('myFile', 'text');
|
|
434
|
-
expect(mockResourceLoad).toHaveBeenCalledWith('anotherFile', 'text');
|
|
435
|
-
expect(reduceContent).toBe('mock anotherFile text content');
|
|
436
|
-
expect(finalState.reducedResult).toBe(
|
|
437
|
-
'Generated summary based on mock anotherFile text content'
|
|
438
|
-
);
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
it('should pass resources to prompt template function', async () => {
|
|
442
|
-
const testBrain = brain('Resource Prompt Template Test').prompt(
|
|
443
|
-
'Generate Summary',
|
|
444
|
-
{
|
|
445
|
-
template: async (state, resources) => {
|
|
446
|
-
const templateContent = await (resources.myFile as any).loadText();
|
|
447
|
-
return `Generate a summary for: ${templateContent}`;
|
|
448
|
-
},
|
|
449
|
-
outputSchema: {
|
|
450
|
-
schema: z.object({ summary: z.string() }),
|
|
451
|
-
name: 'promptResult' as const,
|
|
452
|
-
},
|
|
453
|
-
}
|
|
454
|
-
);
|
|
455
|
-
|
|
456
|
-
mockGenerateObject.mockResolvedValue({ summary: 'Test summary' });
|
|
457
|
-
|
|
458
|
-
const run = testBrain.run({
|
|
459
|
-
client: mockClient,
|
|
460
|
-
resources: mockResources,
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
let finalState: any = {};
|
|
464
|
-
for await (const event of run) {
|
|
465
|
-
if (event.type === BRAIN_EVENTS.STEP_COMPLETE) {
|
|
466
|
-
finalState = applyPatches(finalState, [event.patch]);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Verify resource was loaded in template
|
|
471
|
-
expect(mockResourceLoad).toHaveBeenCalledWith('myFile', 'text');
|
|
472
|
-
|
|
473
|
-
// Verify the generated prompt included the resource content
|
|
474
|
-
expect(mockGenerateObject).toHaveBeenCalledWith(
|
|
475
|
-
expect.objectContaining({
|
|
476
|
-
prompt: 'Generate a summary for: mock myFile text content',
|
|
477
|
-
})
|
|
478
|
-
);
|
|
479
|
-
|
|
480
|
-
// Verify final state
|
|
481
|
-
expect(finalState).toEqual({
|
|
482
|
-
promptResult: { summary: 'Test summary' },
|
|
483
|
-
});
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
it('templates can use state', async () => {
|
|
487
|
-
const testBrain = brain('State Template Test')
|
|
488
|
-
.step('Set Data', () => ({ existingData: 'legacy data' }))
|
|
489
|
-
.prompt('Analyze Data', {
|
|
490
|
-
template: (state) => {
|
|
491
|
-
return `Analyze this: ${state.existingData}`;
|
|
492
|
-
},
|
|
493
|
-
outputSchema: {
|
|
494
|
-
schema: z.object({ analysis: z.string() }),
|
|
495
|
-
name: 'promptResult' as const,
|
|
496
|
-
},
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
mockGenerateObject.mockResolvedValue({
|
|
500
|
-
analysis: 'Analysis result',
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
const run = testBrain.run({
|
|
504
|
-
client: mockClient,
|
|
505
|
-
resources: mockResources,
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
let finalState: any = {};
|
|
509
|
-
for await (const event of run) {
|
|
510
|
-
if (event.type === BRAIN_EVENTS.STEP_COMPLETE) {
|
|
511
|
-
finalState = applyPatches(finalState, [event.patch]);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// Verify the prompt was generated correctly
|
|
516
|
-
expect(mockGenerateObject).toHaveBeenCalledWith(
|
|
517
|
-
expect.objectContaining({
|
|
518
|
-
prompt: 'Analyze this: legacy data',
|
|
519
|
-
})
|
|
520
|
-
);
|
|
521
|
-
|
|
522
|
-
// Verify final state
|
|
523
|
-
expect(finalState).toEqual({
|
|
524
|
-
existingData: 'legacy data',
|
|
525
|
-
promptResult: { analysis: 'Analysis result' },
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
// Verify no resources were loaded (since template didn't use them)
|
|
529
|
-
expect(mockResourceLoad).not.toHaveBeenCalled();
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
it('should make resources available in a nested brain step', async () => {
|
|
533
|
-
let nestedLoadedText: string | undefined;
|
|
534
|
-
|
|
535
|
-
const innerBrain = brain('Inner Resource Brain').step(
|
|
536
|
-
'Inner Load Step',
|
|
537
|
-
async ({ resources }) => {
|
|
538
|
-
nestedLoadedText = await (resources.myBinaryFile as any)
|
|
539
|
-
.loadBinary()
|
|
540
|
-
.then((b: Buffer) => b.toString());
|
|
541
|
-
return { nestedLoadedText };
|
|
542
|
-
}
|
|
543
|
-
);
|
|
544
|
-
|
|
545
|
-
const outerBrain = brain('Outer Resource Brain').brain(
|
|
546
|
-
'Run Inner',
|
|
547
|
-
innerBrain,
|
|
548
|
-
({ state, brainState }) => ({ ...state, ...brainState })
|
|
549
|
-
);
|
|
550
|
-
|
|
551
|
-
const run = outerBrain.run({
|
|
552
|
-
client: mockClient,
|
|
553
|
-
resources: mockResources,
|
|
554
|
-
});
|
|
555
|
-
for await (const _ of run) {
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
expect(mockResourceLoad).toHaveBeenCalledWith('myBinaryFile', 'binary');
|
|
559
|
-
expect(nestedLoadedText).toBe('mock myBinaryFile binary content');
|
|
560
|
-
});
|
|
561
|
-
});
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
describe('error handling', () => {
|
|
565
|
-
it('should handle errors in actions and maintain correct status', async () => {
|
|
566
|
-
const errorBrain = brain('Error Brain')
|
|
567
|
-
// Step 1: Normal step
|
|
568
|
-
.step('First step', () => ({
|
|
569
|
-
value: 1,
|
|
570
|
-
}))
|
|
571
|
-
// Step 2: Error step
|
|
572
|
-
.step('Error step', () => {
|
|
573
|
-
if (true) {
|
|
574
|
-
throw new Error('Test error');
|
|
575
|
-
}
|
|
576
|
-
return {
|
|
577
|
-
value: 1,
|
|
578
|
-
};
|
|
579
|
-
})
|
|
580
|
-
// Step 3: Should never execute
|
|
581
|
-
.step('Never reached', ({ state }) => ({
|
|
582
|
-
value: state.value + 1,
|
|
583
|
-
}));
|
|
584
|
-
|
|
585
|
-
let errorEvent, finalStepStatusEvent;
|
|
586
|
-
try {
|
|
587
|
-
for await (const event of errorBrain.run({
|
|
588
|
-
client: mockClient,
|
|
589
|
-
})) {
|
|
590
|
-
if (event.type === BRAIN_EVENTS.ERROR) {
|
|
591
|
-
errorEvent = event;
|
|
592
|
-
}
|
|
593
|
-
if (event.type === BRAIN_EVENTS.STEP_STATUS) {
|
|
594
|
-
finalStepStatusEvent = event;
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
} catch (error) {
|
|
598
|
-
// Error is expected to be thrown
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// Verify final state
|
|
602
|
-
expect(errorEvent?.status).toBe(STATUS.ERROR);
|
|
603
|
-
expect(errorEvent?.error?.message).toBe('Test error');
|
|
604
|
-
|
|
605
|
-
// Verify steps status
|
|
606
|
-
if (!finalStepStatusEvent?.steps) {
|
|
607
|
-
throw new Error('Steps not found');
|
|
608
|
-
}
|
|
609
|
-
expect(finalStepStatusEvent.steps[0].status).toBe(STATUS.COMPLETE);
|
|
610
|
-
expect(finalStepStatusEvent.steps[1].status).toBe(STATUS.ERROR);
|
|
611
|
-
expect(finalStepStatusEvent.steps[2].status).toBe(STATUS.PENDING);
|
|
612
|
-
|
|
613
|
-
// Verify error event structure
|
|
614
|
-
expect(errorEvent).toEqual(
|
|
615
|
-
expect.objectContaining({
|
|
616
|
-
type: BRAIN_EVENTS.ERROR,
|
|
617
|
-
status: STATUS.ERROR,
|
|
618
|
-
brainTitle: 'Error Brain',
|
|
619
|
-
error: expect.objectContaining({
|
|
620
|
-
name: expect.any(String),
|
|
621
|
-
message: expect.any(String),
|
|
622
|
-
}),
|
|
623
|
-
})
|
|
624
|
-
);
|
|
625
|
-
});
|
|
626
|
-
|
|
627
|
-
it('should handle errors in nested brains and propagate them up', async () => {
|
|
628
|
-
// Create an inner brain that will throw an error
|
|
629
|
-
const innerBrain = brain<{}, { inner?: boolean; value?: number }>(
|
|
630
|
-
'Failing Inner Brain'
|
|
631
|
-
).step('Throw error', (): { value: number } => {
|
|
632
|
-
throw new Error('Inner brain error');
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
// Create outer brain that uses the failing inner brain
|
|
636
|
-
const outerBrain = brain('Outer Brain')
|
|
637
|
-
.step('First step', () => ({ step: 'first' }))
|
|
638
|
-
.brain(
|
|
639
|
-
'Run inner brain',
|
|
640
|
-
innerBrain,
|
|
641
|
-
({ state, brainState }) => ({
|
|
642
|
-
...state,
|
|
643
|
-
step: 'second',
|
|
644
|
-
innerResult: brainState.value,
|
|
645
|
-
}),
|
|
646
|
-
() => ({ value: 5 })
|
|
647
|
-
);
|
|
648
|
-
|
|
649
|
-
const events: BrainEvent<any>[] = [];
|
|
650
|
-
let error: Error | undefined;
|
|
651
|
-
let mainBrainId: string | undefined;
|
|
652
|
-
|
|
653
|
-
try {
|
|
654
|
-
for await (const event of outerBrain.run({
|
|
655
|
-
client: mockClient,
|
|
656
|
-
})) {
|
|
657
|
-
events.push(event);
|
|
658
|
-
if (event.type === BRAIN_EVENTS.START && !mainBrainId) {
|
|
659
|
-
mainBrainId = event.brainRunId;
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
} catch (e) {
|
|
663
|
-
error = e as Error;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// Verify error was thrown
|
|
667
|
-
expect(error?.message).toBe('Inner brain error');
|
|
668
|
-
|
|
669
|
-
// Verify event sequence including error
|
|
670
|
-
expect(events).toEqual([
|
|
671
|
-
expect.objectContaining({
|
|
672
|
-
type: BRAIN_EVENTS.START,
|
|
673
|
-
brainTitle: 'Outer Brain',
|
|
674
|
-
status: STATUS.RUNNING,
|
|
675
|
-
brainRunId: mainBrainId,
|
|
676
|
-
}),
|
|
677
|
-
expect.objectContaining({
|
|
678
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
679
|
-
steps: expect.any(Array),
|
|
680
|
-
}),
|
|
681
|
-
expect.objectContaining({
|
|
682
|
-
type: BRAIN_EVENTS.STEP_START,
|
|
683
|
-
status: STATUS.RUNNING,
|
|
684
|
-
stepTitle: 'First step',
|
|
685
|
-
}),
|
|
686
|
-
expect.objectContaining({
|
|
687
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
688
|
-
steps: expect.any(Array),
|
|
689
|
-
}),
|
|
690
|
-
expect.objectContaining({
|
|
691
|
-
type: BRAIN_EVENTS.STEP_COMPLETE,
|
|
692
|
-
status: STATUS.RUNNING,
|
|
693
|
-
stepTitle: 'First step',
|
|
694
|
-
}),
|
|
695
|
-
expect.objectContaining({
|
|
696
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
697
|
-
steps: expect.any(Array),
|
|
698
|
-
}),
|
|
699
|
-
expect.objectContaining({
|
|
700
|
-
type: BRAIN_EVENTS.STEP_START,
|
|
701
|
-
status: STATUS.RUNNING,
|
|
702
|
-
stepTitle: 'Run inner brain',
|
|
703
|
-
}),
|
|
704
|
-
expect.objectContaining({
|
|
705
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
706
|
-
steps: expect.any(Array),
|
|
707
|
-
}),
|
|
708
|
-
expect.objectContaining({
|
|
709
|
-
type: BRAIN_EVENTS.START,
|
|
710
|
-
brainTitle: 'Failing Inner Brain',
|
|
711
|
-
status: STATUS.RUNNING,
|
|
712
|
-
}),
|
|
713
|
-
expect.objectContaining({
|
|
714
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
715
|
-
steps: expect.any(Array),
|
|
716
|
-
}),
|
|
717
|
-
expect.objectContaining({
|
|
718
|
-
type: BRAIN_EVENTS.STEP_START,
|
|
719
|
-
status: STATUS.RUNNING,
|
|
720
|
-
stepTitle: 'Throw error',
|
|
721
|
-
}),
|
|
722
|
-
expect.objectContaining({
|
|
723
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
724
|
-
steps: expect.any(Array),
|
|
725
|
-
}),
|
|
726
|
-
expect.objectContaining({
|
|
727
|
-
type: BRAIN_EVENTS.ERROR,
|
|
728
|
-
brainTitle: 'Failing Inner Brain',
|
|
729
|
-
status: STATUS.ERROR,
|
|
730
|
-
error: expect.objectContaining({
|
|
731
|
-
name: expect.any(String),
|
|
732
|
-
message: expect.any(String),
|
|
733
|
-
}),
|
|
734
|
-
}),
|
|
735
|
-
expect.objectContaining({
|
|
736
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
737
|
-
steps: expect.arrayContaining([
|
|
738
|
-
expect.objectContaining({
|
|
739
|
-
title: 'Throw error',
|
|
740
|
-
status: STATUS.ERROR,
|
|
741
|
-
}),
|
|
742
|
-
]),
|
|
743
|
-
}),
|
|
744
|
-
expect.objectContaining({
|
|
745
|
-
type: BRAIN_EVENTS.ERROR,
|
|
746
|
-
brainTitle: 'Outer Brain',
|
|
747
|
-
status: STATUS.ERROR,
|
|
748
|
-
error: expect.objectContaining({
|
|
749
|
-
name: expect.any(String),
|
|
750
|
-
message: expect.any(String),
|
|
751
|
-
}),
|
|
752
|
-
}),
|
|
753
|
-
expect.objectContaining({
|
|
754
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
755
|
-
steps: expect.arrayContaining([
|
|
756
|
-
expect.objectContaining({
|
|
757
|
-
title: 'Run inner brain',
|
|
758
|
-
status: STATUS.ERROR,
|
|
759
|
-
}),
|
|
760
|
-
]),
|
|
761
|
-
}),
|
|
762
|
-
]);
|
|
763
|
-
|
|
764
|
-
// Find inner and outer error events by brainRunId
|
|
765
|
-
const innerErrorEvent = events.find(
|
|
766
|
-
(e) => e.type === BRAIN_EVENTS.ERROR && e.brainRunId !== mainBrainId
|
|
767
|
-
) as BrainErrorEvent<any>;
|
|
768
|
-
|
|
769
|
-
const outerErrorEvent = events.find(
|
|
770
|
-
(e) => e.type === BRAIN_EVENTS.ERROR && e.brainRunId === mainBrainId
|
|
771
|
-
) as BrainErrorEvent<any>;
|
|
772
|
-
|
|
773
|
-
expect(innerErrorEvent.error).toEqual(
|
|
774
|
-
expect.objectContaining({
|
|
775
|
-
message: 'Inner brain error',
|
|
776
|
-
})
|
|
777
|
-
);
|
|
778
|
-
expect(outerErrorEvent.error).toEqual(
|
|
779
|
-
expect.objectContaining({
|
|
780
|
-
message: 'Inner brain error',
|
|
781
|
-
})
|
|
782
|
-
);
|
|
783
|
-
});
|
|
784
|
-
});
|
|
785
|
-
|
|
786
|
-
describe('step creation', () => {
|
|
787
|
-
it('should create a step that updates state', async () => {
|
|
788
|
-
const testBrain = brain('Simple Brain').step(
|
|
789
|
-
'Simple step',
|
|
790
|
-
({ state }) => ({
|
|
791
|
-
...state,
|
|
792
|
-
count: 1,
|
|
793
|
-
message: 'Count is now 1',
|
|
794
|
-
})
|
|
795
|
-
);
|
|
796
|
-
|
|
797
|
-
const events = [];
|
|
798
|
-
let finalState = {};
|
|
799
|
-
for await (const event of testBrain.run({
|
|
800
|
-
client: mockClient,
|
|
801
|
-
})) {
|
|
802
|
-
events.push(event);
|
|
803
|
-
if (event.type === BRAIN_EVENTS.STEP_COMPLETE) {
|
|
804
|
-
finalState = applyPatches(finalState, event.patch);
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
// Skip checking events[0] (brain:start)
|
|
809
|
-
// Skip checking events[1] (step:status)
|
|
810
|
-
|
|
811
|
-
// Verify the step start event
|
|
812
|
-
expect(events[2]).toEqual(
|
|
813
|
-
expect.objectContaining({
|
|
814
|
-
type: BRAIN_EVENTS.STEP_START,
|
|
815
|
-
status: STATUS.RUNNING,
|
|
816
|
-
stepTitle: 'Simple step',
|
|
817
|
-
stepId: expect.any(String),
|
|
818
|
-
options: {},
|
|
819
|
-
})
|
|
820
|
-
);
|
|
821
|
-
|
|
822
|
-
// Verify the step status event (running)
|
|
823
|
-
expect(events[3]).toEqual(
|
|
824
|
-
expect.objectContaining({
|
|
825
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
826
|
-
steps: expect.any(Array),
|
|
827
|
-
options: {},
|
|
828
|
-
})
|
|
829
|
-
);
|
|
830
|
-
if (events[3].type === BRAIN_EVENTS.STEP_STATUS) {
|
|
831
|
-
expect(events[3].steps[0].status).toBe(STATUS.RUNNING);
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
// Verify the step complete event
|
|
835
|
-
expect(events[4]).toEqual(
|
|
836
|
-
expect.objectContaining({
|
|
837
|
-
type: BRAIN_EVENTS.STEP_COMPLETE,
|
|
838
|
-
status: STATUS.RUNNING,
|
|
839
|
-
stepTitle: 'Simple step',
|
|
840
|
-
stepId: expect.any(String),
|
|
841
|
-
patch: [
|
|
842
|
-
{
|
|
843
|
-
op: 'add',
|
|
844
|
-
path: '/count',
|
|
845
|
-
value: 1,
|
|
846
|
-
},
|
|
847
|
-
{
|
|
848
|
-
op: 'add',
|
|
849
|
-
path: '/message',
|
|
850
|
-
value: 'Count is now 1',
|
|
851
|
-
},
|
|
852
|
-
],
|
|
853
|
-
options: {},
|
|
854
|
-
})
|
|
855
|
-
);
|
|
856
|
-
|
|
857
|
-
expect(events[5]).toEqual(
|
|
858
|
-
expect.objectContaining({
|
|
859
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
860
|
-
steps: [
|
|
861
|
-
expect.objectContaining({
|
|
862
|
-
title: 'Simple step',
|
|
863
|
-
status: STATUS.COMPLETE,
|
|
864
|
-
id: expect.any(String),
|
|
865
|
-
}),
|
|
866
|
-
],
|
|
867
|
-
options: {},
|
|
868
|
-
})
|
|
869
|
-
);
|
|
870
|
-
|
|
871
|
-
// Verify the brain complete event
|
|
872
|
-
expect(events[6]).toEqual(
|
|
873
|
-
expect.objectContaining({
|
|
874
|
-
type: BRAIN_EVENTS.COMPLETE,
|
|
875
|
-
status: STATUS.COMPLETE,
|
|
876
|
-
brainTitle: 'Simple Brain',
|
|
877
|
-
options: {},
|
|
878
|
-
})
|
|
879
|
-
);
|
|
880
|
-
// Verify the final state
|
|
881
|
-
expect(finalState).toEqual({
|
|
882
|
-
count: 1,
|
|
883
|
-
message: 'Count is now 1',
|
|
884
|
-
});
|
|
885
|
-
});
|
|
886
|
-
|
|
887
|
-
it('should maintain immutable results between steps', async () => {
|
|
888
|
-
const testBrain = brain('Immutable Steps Brain')
|
|
889
|
-
.step('First step', () => ({
|
|
890
|
-
value: 1,
|
|
891
|
-
}))
|
|
892
|
-
.step('Second step', ({ state }) => {
|
|
893
|
-
// Attempt to modify previous step's state
|
|
894
|
-
state.value = 99;
|
|
895
|
-
return {
|
|
896
|
-
value: 2,
|
|
897
|
-
};
|
|
898
|
-
});
|
|
899
|
-
|
|
900
|
-
let finalState = {};
|
|
901
|
-
const patches = [];
|
|
902
|
-
for await (const event of testBrain.run({
|
|
903
|
-
client: mockClient,
|
|
904
|
-
})) {
|
|
905
|
-
if (event.type === BRAIN_EVENTS.STEP_COMPLETE) {
|
|
906
|
-
patches.push(...event.patch);
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
// Apply all patches to the initial state
|
|
911
|
-
finalState = applyPatches(finalState, patches);
|
|
912
|
-
|
|
913
|
-
// Verify the final state
|
|
914
|
-
expect(finalState).toEqual({ value: 2 });
|
|
915
|
-
});
|
|
916
|
-
});
|
|
917
|
-
|
|
918
|
-
describe('brain resumption', () => {
|
|
919
|
-
const mockClient = {
|
|
920
|
-
generateObject: jest.fn(),
|
|
921
|
-
};
|
|
922
|
-
|
|
923
|
-
it('should resume brain from the correct step when given initialCompletedSteps', async () => {
|
|
924
|
-
const executedSteps: string[] = [];
|
|
925
|
-
const threeStepBrain = brain('Three Step Brain')
|
|
926
|
-
.step('Step 1', ({ state }) => {
|
|
927
|
-
executedSteps.push('Step 1');
|
|
928
|
-
return { ...state, value: 2 };
|
|
929
|
-
})
|
|
930
|
-
.step('Step 2', ({ state }) => {
|
|
931
|
-
executedSteps.push('Step 2');
|
|
932
|
-
return { ...state, value: state.value + 10 };
|
|
933
|
-
})
|
|
934
|
-
.step('Step 3', ({ state }) => {
|
|
935
|
-
executedSteps.push('Step 3');
|
|
936
|
-
return { ...state, value: state.value * 3 };
|
|
937
|
-
});
|
|
938
|
-
|
|
939
|
-
// First run to get the first step completed with initial state
|
|
940
|
-
let initialCompletedSteps: SerializedStep[] = []; // Use the correct type
|
|
941
|
-
const initialState = { initialValue: true };
|
|
942
|
-
let firstStepState: State = initialState;
|
|
943
|
-
let allStepsInfo: SerializedStepStatus[] = []; // Explicit type annotation needed
|
|
944
|
-
|
|
945
|
-
// Run brain until we get the first step completed
|
|
946
|
-
for await (const event of threeStepBrain.run({
|
|
947
|
-
client: mockClient as ObjectGenerator,
|
|
948
|
-
initialState,
|
|
949
|
-
})) {
|
|
950
|
-
// Capture the full step list from the first status event
|
|
951
|
-
if (event.type === BRAIN_EVENTS.STEP_STATUS) {
|
|
952
|
-
allStepsInfo = event.steps; // Direct assignment, type is SerializedStepStatus[]
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
if (
|
|
956
|
-
event.type === BRAIN_EVENTS.STEP_COMPLETE &&
|
|
957
|
-
event.stepTitle === 'Step 1'
|
|
958
|
-
) {
|
|
959
|
-
firstStepState = applyPatches(firstStepState, [event.patch]);
|
|
960
|
-
// Construct initialCompletedSteps with the full data for completed steps
|
|
961
|
-
initialCompletedSteps = allStepsInfo.map((stepInfo, index) => {
|
|
962
|
-
if (index === 0) {
|
|
963
|
-
// If it's Step 1
|
|
964
|
-
return { ...stepInfo, status: STATUS.COMPLETE, patch: event.patch };
|
|
965
|
-
} else {
|
|
966
|
-
return { ...stepInfo, status: STATUS.PENDING, patch: undefined };
|
|
967
|
-
}
|
|
968
|
-
});
|
|
969
|
-
break; // Stop after first step
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
// Clear executed steps array
|
|
974
|
-
executedSteps.length = 0;
|
|
975
|
-
|
|
976
|
-
// Resume brain with first step completed
|
|
977
|
-
let resumedState: State | undefined;
|
|
978
|
-
if (!initialCompletedSteps)
|
|
979
|
-
throw new Error('Expected initialCompletedSteps');
|
|
980
|
-
|
|
981
|
-
for await (const event of threeStepBrain.run({
|
|
982
|
-
client: mockClient as ObjectGenerator,
|
|
983
|
-
initialState,
|
|
984
|
-
initialCompletedSteps,
|
|
985
|
-
brainRunId: 'test-run-id',
|
|
986
|
-
})) {
|
|
987
|
-
if (event.type === BRAIN_EVENTS.RESTART) {
|
|
988
|
-
resumedState = event.initialState;
|
|
989
|
-
} else if (event.type === BRAIN_EVENTS.STEP_COMPLETE) {
|
|
990
|
-
resumedState = applyPatches(resumedState!, [event.patch]);
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
// Verify only steps 2 and 3 were executed
|
|
995
|
-
expect(executedSteps).toEqual(['Step 2', 'Step 3']);
|
|
996
|
-
expect(executedSteps).not.toContain('Step 1');
|
|
997
|
-
|
|
998
|
-
// Verify the final state after all steps complete
|
|
999
|
-
expect(resumedState).toEqual({
|
|
1000
|
-
value: 36,
|
|
1001
|
-
initialValue: true,
|
|
1002
|
-
});
|
|
1003
|
-
});
|
|
1004
|
-
});
|
|
1005
|
-
|
|
1006
|
-
describe('nested brains', () => {
|
|
1007
|
-
it('should execute nested brains and yield all inner brain events', async () => {
|
|
1008
|
-
// Create an inner brain that will be nested
|
|
1009
|
-
const innerBrain = brain<{}, { value: number }>('Inner Brain').step(
|
|
1010
|
-
'Double value',
|
|
1011
|
-
({ state }) => ({
|
|
1012
|
-
inner: true,
|
|
1013
|
-
value: state.value * 2,
|
|
1014
|
-
})
|
|
1015
|
-
);
|
|
1016
|
-
|
|
1017
|
-
// Create outer brain that uses the inner brain
|
|
1018
|
-
const outerBrain = brain('Outer Brain')
|
|
1019
|
-
.step('Set prefix', () => ({ prefix: 'test-' }))
|
|
1020
|
-
.brain(
|
|
1021
|
-
'Run inner brain',
|
|
1022
|
-
innerBrain,
|
|
1023
|
-
({ state, brainState }) => ({
|
|
1024
|
-
...state,
|
|
1025
|
-
innerResult: brainState.value,
|
|
1026
|
-
}),
|
|
1027
|
-
() => ({ value: 5 })
|
|
1028
|
-
);
|
|
1029
|
-
|
|
1030
|
-
const events: BrainEvent<any>[] = [];
|
|
1031
|
-
for await (const event of outerBrain.run({
|
|
1032
|
-
client: mockClient,
|
|
1033
|
-
})) {
|
|
1034
|
-
events.push(event);
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
// Verify all events are yielded in correct order
|
|
1038
|
-
expect(
|
|
1039
|
-
events.map((e) => ({
|
|
1040
|
-
type: e.type,
|
|
1041
|
-
brainTitle: 'brainTitle' in e ? e.brainTitle : undefined,
|
|
1042
|
-
status: 'status' in e ? e.status : undefined,
|
|
1043
|
-
stepTitle: 'stepTitle' in e ? e.stepTitle : undefined,
|
|
1044
|
-
}))
|
|
1045
|
-
).toEqual([
|
|
1046
|
-
// Outer brain start
|
|
1047
|
-
{
|
|
1048
|
-
type: BRAIN_EVENTS.START,
|
|
1049
|
-
brainTitle: 'Outer Brain',
|
|
1050
|
-
status: STATUS.RUNNING,
|
|
1051
|
-
stepTitle: undefined,
|
|
1052
|
-
},
|
|
1053
|
-
// Initial step status for outer brain
|
|
1054
|
-
{
|
|
1055
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
1056
|
-
brainTitle: undefined,
|
|
1057
|
-
status: undefined,
|
|
1058
|
-
stepTitle: undefined,
|
|
1059
|
-
},
|
|
1060
|
-
// First step of outer brain
|
|
1061
|
-
{
|
|
1062
|
-
type: BRAIN_EVENTS.STEP_START,
|
|
1063
|
-
brainTitle: undefined,
|
|
1064
|
-
status: STATUS.RUNNING,
|
|
1065
|
-
stepTitle: 'Set prefix',
|
|
1066
|
-
},
|
|
1067
|
-
// First step status (running)
|
|
1068
|
-
{
|
|
1069
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
1070
|
-
brainTitle: undefined,
|
|
1071
|
-
status: undefined,
|
|
1072
|
-
stepTitle: undefined,
|
|
1073
|
-
},
|
|
1074
|
-
{
|
|
1075
|
-
type: BRAIN_EVENTS.STEP_COMPLETE,
|
|
1076
|
-
brainTitle: undefined,
|
|
1077
|
-
status: STATUS.RUNNING,
|
|
1078
|
-
stepTitle: 'Set prefix',
|
|
1079
|
-
},
|
|
1080
|
-
{
|
|
1081
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
1082
|
-
brainTitle: undefined,
|
|
1083
|
-
status: undefined,
|
|
1084
|
-
stepTitle: undefined,
|
|
1085
|
-
},
|
|
1086
|
-
{
|
|
1087
|
-
type: BRAIN_EVENTS.STEP_START,
|
|
1088
|
-
brainTitle: undefined,
|
|
1089
|
-
status: STATUS.RUNNING,
|
|
1090
|
-
stepTitle: 'Run inner brain',
|
|
1091
|
-
},
|
|
1092
|
-
// Step status for inner brain (running)
|
|
1093
|
-
{
|
|
1094
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
1095
|
-
brainTitle: undefined,
|
|
1096
|
-
status: undefined,
|
|
1097
|
-
stepTitle: undefined,
|
|
1098
|
-
},
|
|
1099
|
-
// Inner brain start
|
|
1100
|
-
{
|
|
1101
|
-
type: BRAIN_EVENTS.START,
|
|
1102
|
-
brainTitle: 'Inner Brain',
|
|
1103
|
-
status: STATUS.RUNNING,
|
|
1104
|
-
stepTitle: undefined,
|
|
1105
|
-
},
|
|
1106
|
-
// Initial step status for inner brain
|
|
1107
|
-
{
|
|
1108
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
1109
|
-
brainTitle: undefined,
|
|
1110
|
-
status: undefined,
|
|
1111
|
-
stepTitle: undefined,
|
|
1112
|
-
},
|
|
1113
|
-
// Inner brain step
|
|
1114
|
-
{
|
|
1115
|
-
type: BRAIN_EVENTS.STEP_START,
|
|
1116
|
-
brainTitle: undefined,
|
|
1117
|
-
status: STATUS.RUNNING,
|
|
1118
|
-
stepTitle: 'Double value',
|
|
1119
|
-
},
|
|
1120
|
-
// Inner brain step status (running)
|
|
1121
|
-
{
|
|
1122
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
1123
|
-
brainTitle: undefined,
|
|
1124
|
-
status: undefined,
|
|
1125
|
-
stepTitle: undefined,
|
|
1126
|
-
},
|
|
1127
|
-
{
|
|
1128
|
-
type: BRAIN_EVENTS.STEP_COMPLETE,
|
|
1129
|
-
brainTitle: undefined,
|
|
1130
|
-
status: STATUS.RUNNING,
|
|
1131
|
-
stepTitle: 'Double value',
|
|
1132
|
-
},
|
|
1133
|
-
{
|
|
1134
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
1135
|
-
brainTitle: undefined,
|
|
1136
|
-
status: undefined,
|
|
1137
|
-
stepTitle: undefined,
|
|
1138
|
-
},
|
|
1139
|
-
{
|
|
1140
|
-
type: BRAIN_EVENTS.COMPLETE,
|
|
1141
|
-
brainTitle: 'Inner Brain',
|
|
1142
|
-
status: STATUS.COMPLETE,
|
|
1143
|
-
stepTitle: undefined,
|
|
1144
|
-
},
|
|
1145
|
-
// Outer brain nested step completion
|
|
1146
|
-
{
|
|
1147
|
-
type: BRAIN_EVENTS.STEP_COMPLETE,
|
|
1148
|
-
brainTitle: undefined,
|
|
1149
|
-
status: STATUS.RUNNING,
|
|
1150
|
-
stepTitle: 'Run inner brain',
|
|
1151
|
-
},
|
|
1152
|
-
{
|
|
1153
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
1154
|
-
brainTitle: undefined,
|
|
1155
|
-
status: undefined,
|
|
1156
|
-
stepTitle: undefined,
|
|
1157
|
-
},
|
|
1158
|
-
// Outer brain completion
|
|
1159
|
-
{
|
|
1160
|
-
type: BRAIN_EVENTS.COMPLETE,
|
|
1161
|
-
brainTitle: 'Outer Brain',
|
|
1162
|
-
status: STATUS.COMPLETE,
|
|
1163
|
-
stepTitle: undefined,
|
|
1164
|
-
},
|
|
1165
|
-
]);
|
|
1166
|
-
|
|
1167
|
-
// Verify states are passed correctly
|
|
1168
|
-
let innerState: State = { value: 5 }; // Match the initial state from the brain
|
|
1169
|
-
let outerState = {};
|
|
1170
|
-
|
|
1171
|
-
for (const event of events) {
|
|
1172
|
-
if (event.type === BRAIN_EVENTS.STEP_COMPLETE) {
|
|
1173
|
-
if (event.stepTitle === 'Double value') {
|
|
1174
|
-
innerState = applyPatches(innerState, [event.patch]);
|
|
1175
|
-
} else {
|
|
1176
|
-
outerState = applyPatches(outerState, [event.patch]);
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
// Verify final states
|
|
1182
|
-
expect(innerState).toEqual({
|
|
1183
|
-
inner: true,
|
|
1184
|
-
value: 10,
|
|
1185
|
-
});
|
|
1186
|
-
|
|
1187
|
-
expect(outerState).toEqual({
|
|
1188
|
-
prefix: 'test-',
|
|
1189
|
-
innerResult: 10,
|
|
1190
|
-
});
|
|
1191
|
-
});
|
|
1192
|
-
|
|
1193
|
-
it('should handle errors in nested brains and propagate them up', async () => {
|
|
1194
|
-
// Create an inner brain that will throw an error
|
|
1195
|
-
const innerBrain = brain<{}, { inner: boolean; value?: number }>(
|
|
1196
|
-
'Failing Inner Brain'
|
|
1197
|
-
).step('Throw error', (): { value: number } => {
|
|
1198
|
-
throw new Error('Inner brain error');
|
|
1199
|
-
});
|
|
1200
|
-
|
|
1201
|
-
// Create outer brain that uses the failing inner brain
|
|
1202
|
-
const outerBrain = brain('Outer Brain')
|
|
1203
|
-
.step('First step', () => ({ step: 'first' }))
|
|
1204
|
-
.brain(
|
|
1205
|
-
'Run inner brain',
|
|
1206
|
-
innerBrain,
|
|
1207
|
-
({ state, brainState }) => ({
|
|
1208
|
-
...state,
|
|
1209
|
-
step: 'second',
|
|
1210
|
-
innerResult: brainState.value,
|
|
1211
|
-
}),
|
|
1212
|
-
() => ({ value: 5 })
|
|
1213
|
-
);
|
|
1214
|
-
|
|
1215
|
-
const events: BrainEvent<any>[] = [];
|
|
1216
|
-
let error: Error | undefined;
|
|
1217
|
-
let mainBrainId: string | undefined;
|
|
1218
|
-
|
|
1219
|
-
try {
|
|
1220
|
-
for await (const event of outerBrain.run({
|
|
1221
|
-
client: mockClient,
|
|
1222
|
-
})) {
|
|
1223
|
-
events.push(event);
|
|
1224
|
-
if (event.type === BRAIN_EVENTS.START && !mainBrainId) {
|
|
1225
|
-
mainBrainId = event.brainRunId;
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
} catch (e) {
|
|
1229
|
-
error = e as Error;
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
// Verify error was thrown
|
|
1233
|
-
expect(error?.message).toBe('Inner brain error');
|
|
1234
|
-
|
|
1235
|
-
// Verify event sequence including error
|
|
1236
|
-
expect(events).toEqual([
|
|
1237
|
-
expect.objectContaining({
|
|
1238
|
-
type: BRAIN_EVENTS.START,
|
|
1239
|
-
brainTitle: 'Outer Brain',
|
|
1240
|
-
status: STATUS.RUNNING,
|
|
1241
|
-
brainRunId: mainBrainId,
|
|
1242
|
-
}),
|
|
1243
|
-
expect.objectContaining({
|
|
1244
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
1245
|
-
steps: expect.any(Array),
|
|
1246
|
-
}),
|
|
1247
|
-
expect.objectContaining({
|
|
1248
|
-
type: BRAIN_EVENTS.STEP_START,
|
|
1249
|
-
status: STATUS.RUNNING,
|
|
1250
|
-
stepTitle: 'First step',
|
|
1251
|
-
}),
|
|
1252
|
-
expect.objectContaining({
|
|
1253
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
1254
|
-
steps: expect.any(Array),
|
|
1255
|
-
}),
|
|
1256
|
-
expect.objectContaining({
|
|
1257
|
-
type: BRAIN_EVENTS.STEP_COMPLETE,
|
|
1258
|
-
status: STATUS.RUNNING,
|
|
1259
|
-
stepTitle: 'First step',
|
|
1260
|
-
}),
|
|
1261
|
-
expect.objectContaining({
|
|
1262
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
1263
|
-
steps: expect.any(Array),
|
|
1264
|
-
}),
|
|
1265
|
-
expect.objectContaining({
|
|
1266
|
-
type: BRAIN_EVENTS.STEP_START,
|
|
1267
|
-
status: STATUS.RUNNING,
|
|
1268
|
-
stepTitle: 'Run inner brain',
|
|
1269
|
-
}),
|
|
1270
|
-
expect.objectContaining({
|
|
1271
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
1272
|
-
steps: expect.any(Array),
|
|
1273
|
-
}),
|
|
1274
|
-
expect.objectContaining({
|
|
1275
|
-
type: BRAIN_EVENTS.START,
|
|
1276
|
-
brainTitle: 'Failing Inner Brain',
|
|
1277
|
-
status: STATUS.RUNNING,
|
|
1278
|
-
}),
|
|
1279
|
-
expect.objectContaining({
|
|
1280
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
1281
|
-
steps: expect.any(Array),
|
|
1282
|
-
}),
|
|
1283
|
-
expect.objectContaining({
|
|
1284
|
-
type: BRAIN_EVENTS.STEP_START,
|
|
1285
|
-
status: STATUS.RUNNING,
|
|
1286
|
-
stepTitle: 'Throw error',
|
|
1287
|
-
}),
|
|
1288
|
-
expect.objectContaining({
|
|
1289
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
1290
|
-
steps: expect.any(Array),
|
|
1291
|
-
}),
|
|
1292
|
-
expect.objectContaining({
|
|
1293
|
-
type: BRAIN_EVENTS.ERROR,
|
|
1294
|
-
brainTitle: 'Failing Inner Brain',
|
|
1295
|
-
status: STATUS.ERROR,
|
|
1296
|
-
error: expect.objectContaining({
|
|
1297
|
-
name: expect.any(String),
|
|
1298
|
-
message: expect.any(String),
|
|
1299
|
-
}),
|
|
1300
|
-
}),
|
|
1301
|
-
expect.objectContaining({
|
|
1302
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
1303
|
-
steps: expect.arrayContaining([
|
|
1304
|
-
expect.objectContaining({
|
|
1305
|
-
title: 'Throw error',
|
|
1306
|
-
status: STATUS.ERROR,
|
|
1307
|
-
}),
|
|
1308
|
-
]),
|
|
1309
|
-
}),
|
|
1310
|
-
expect.objectContaining({
|
|
1311
|
-
type: BRAIN_EVENTS.ERROR,
|
|
1312
|
-
brainTitle: 'Outer Brain',
|
|
1313
|
-
status: STATUS.ERROR,
|
|
1314
|
-
error: expect.objectContaining({
|
|
1315
|
-
name: expect.any(String),
|
|
1316
|
-
message: expect.any(String),
|
|
1317
|
-
}),
|
|
1318
|
-
}),
|
|
1319
|
-
expect.objectContaining({
|
|
1320
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
1321
|
-
steps: expect.arrayContaining([
|
|
1322
|
-
expect.objectContaining({
|
|
1323
|
-
title: 'Run inner brain',
|
|
1324
|
-
status: STATUS.ERROR,
|
|
1325
|
-
}),
|
|
1326
|
-
]),
|
|
1327
|
-
}),
|
|
1328
|
-
]);
|
|
1329
|
-
|
|
1330
|
-
// Find inner and outer error events by brainRunId
|
|
1331
|
-
const innerErrorEvent = events.find(
|
|
1332
|
-
(e) => e.type === BRAIN_EVENTS.ERROR && e.brainRunId !== mainBrainId
|
|
1333
|
-
) as BrainErrorEvent<any>;
|
|
1334
|
-
|
|
1335
|
-
const outerErrorEvent = events.find(
|
|
1336
|
-
(e) => e.type === BRAIN_EVENTS.ERROR && e.brainRunId === mainBrainId
|
|
1337
|
-
) as BrainErrorEvent<any>;
|
|
1338
|
-
|
|
1339
|
-
expect(innerErrorEvent.error).toEqual(
|
|
1340
|
-
expect.objectContaining({
|
|
1341
|
-
message: 'Inner brain error',
|
|
1342
|
-
})
|
|
1343
|
-
);
|
|
1344
|
-
expect(outerErrorEvent.error).toEqual(
|
|
1345
|
-
expect.objectContaining({
|
|
1346
|
-
message: 'Inner brain error',
|
|
1347
|
-
})
|
|
1348
|
-
);
|
|
1349
|
-
});
|
|
1350
|
-
|
|
1351
|
-
it('should include patches in step status events for inner brain steps', async () => {
|
|
1352
|
-
interface InnerState extends State {
|
|
1353
|
-
value: number;
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
interface OuterState extends State {
|
|
1357
|
-
value: number;
|
|
1358
|
-
result?: number;
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
// Create an inner brain that modifies state
|
|
1362
|
-
const innerBrain = brain<{}, InnerState>('Inner Brain').step(
|
|
1363
|
-
'Double value',
|
|
1364
|
-
({ state }) => ({
|
|
1365
|
-
...state,
|
|
1366
|
-
value: state.value * 2,
|
|
1367
|
-
})
|
|
1368
|
-
);
|
|
1369
|
-
|
|
1370
|
-
// Create outer brain that uses the inner brain
|
|
1371
|
-
const outerBrain = brain<{}, OuterState>('Outer Brain')
|
|
1372
|
-
.step('Set initial', () => ({
|
|
1373
|
-
value: 5,
|
|
1374
|
-
}))
|
|
1375
|
-
.brain(
|
|
1376
|
-
'Run inner brain',
|
|
1377
|
-
innerBrain,
|
|
1378
|
-
({ state, brainState }) => ({
|
|
1379
|
-
...state,
|
|
1380
|
-
result: brainState.value,
|
|
1381
|
-
}),
|
|
1382
|
-
(state) => ({ value: state.value })
|
|
1383
|
-
);
|
|
1384
|
-
|
|
1385
|
-
// Run brain and collect step status events
|
|
1386
|
-
let finalStepStatus;
|
|
1387
|
-
for await (const event of outerBrain.run({
|
|
1388
|
-
client: mockClient,
|
|
1389
|
-
})) {
|
|
1390
|
-
if (event.type === BRAIN_EVENTS.STEP_STATUS) {
|
|
1391
|
-
finalStepStatus = event;
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
// Verify step status contains patches for all steps including the inner brain step
|
|
1396
|
-
expect(finalStepStatus?.steps).toEqual([
|
|
1397
|
-
expect.objectContaining({
|
|
1398
|
-
title: 'Set initial',
|
|
1399
|
-
status: STATUS.COMPLETE,
|
|
1400
|
-
}),
|
|
1401
|
-
expect.objectContaining({
|
|
1402
|
-
title: 'Run inner brain',
|
|
1403
|
-
status: STATUS.COMPLETE,
|
|
1404
|
-
}),
|
|
1405
|
-
]);
|
|
1406
|
-
});
|
|
1407
|
-
});
|
|
1408
|
-
|
|
1409
|
-
describe('brain options', () => {
|
|
1410
|
-
it('should pass options through to brain events', async () => {
|
|
1411
|
-
const testBrain = brain<{ testOption: string }>('Options Brain').step(
|
|
1412
|
-
'Simple step',
|
|
1413
|
-
({ state, options }) => ({
|
|
1414
|
-
value: 1,
|
|
1415
|
-
passedOption: options.testOption,
|
|
1416
|
-
})
|
|
1417
|
-
);
|
|
1418
|
-
|
|
1419
|
-
const brainOptions = {
|
|
1420
|
-
testOption: 'test-value',
|
|
1421
|
-
};
|
|
1422
|
-
|
|
1423
|
-
let finalEvent, finalStepStatus;
|
|
1424
|
-
for await (const event of testBrain.run({
|
|
1425
|
-
client: mockClient,
|
|
1426
|
-
options: brainOptions,
|
|
1427
|
-
})) {
|
|
1428
|
-
if (event.type === BRAIN_EVENTS.STEP_STATUS) {
|
|
1429
|
-
finalStepStatus = event;
|
|
1430
|
-
} else {
|
|
1431
|
-
finalEvent = event;
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
expect(finalEvent).toEqual(
|
|
1436
|
-
expect.objectContaining({
|
|
1437
|
-
type: BRAIN_EVENTS.COMPLETE,
|
|
1438
|
-
status: STATUS.COMPLETE,
|
|
1439
|
-
brainTitle: 'Options Brain',
|
|
1440
|
-
brainDescription: undefined,
|
|
1441
|
-
options: brainOptions,
|
|
1442
|
-
})
|
|
1443
|
-
);
|
|
1444
|
-
expect(finalStepStatus).toEqual(
|
|
1445
|
-
expect.objectContaining({
|
|
1446
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
1447
|
-
steps: [
|
|
1448
|
-
expect.objectContaining({
|
|
1449
|
-
title: 'Simple step',
|
|
1450
|
-
status: STATUS.COMPLETE,
|
|
1451
|
-
}),
|
|
1452
|
-
],
|
|
1453
|
-
options: brainOptions,
|
|
1454
|
-
})
|
|
1455
|
-
);
|
|
1456
|
-
});
|
|
1457
|
-
|
|
1458
|
-
it('should provide empty object as default options', async () => {
|
|
1459
|
-
const testBrain = brain('Default Options Brain').step(
|
|
1460
|
-
'Simple step',
|
|
1461
|
-
({ options }) => ({
|
|
1462
|
-
hasOptions: Object.keys(options).length === 0,
|
|
1463
|
-
})
|
|
1464
|
-
);
|
|
1465
|
-
|
|
1466
|
-
const brainRun = testBrain.run({
|
|
1467
|
-
client: mockClient,
|
|
1468
|
-
});
|
|
1469
|
-
|
|
1470
|
-
// Skip start event
|
|
1471
|
-
await brainRun.next();
|
|
1472
|
-
|
|
1473
|
-
// Skip initial step status event
|
|
1474
|
-
await brainRun.next();
|
|
1475
|
-
|
|
1476
|
-
// Check step start
|
|
1477
|
-
const stepStartResult = await brainRun.next();
|
|
1478
|
-
expect(stepStartResult.value).toEqual(
|
|
1479
|
-
expect.objectContaining({
|
|
1480
|
-
options: {},
|
|
1481
|
-
type: BRAIN_EVENTS.STEP_START,
|
|
1482
|
-
})
|
|
1483
|
-
);
|
|
1484
|
-
|
|
1485
|
-
// Check step status (running)
|
|
1486
|
-
const stepStatusRunning = await brainRun.next();
|
|
1487
|
-
expect(stepStatusRunning.value).toEqual(
|
|
1488
|
-
expect.objectContaining({
|
|
1489
|
-
type: BRAIN_EVENTS.STEP_STATUS,
|
|
1490
|
-
steps: expect.any(Array),
|
|
1491
|
-
})
|
|
1492
|
-
);
|
|
1493
|
-
if (stepStatusRunning.value.type === BRAIN_EVENTS.STEP_STATUS) {
|
|
1494
|
-
expect(stepStatusRunning.value.steps[0].status).toBe(STATUS.RUNNING);
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
// Check step completion
|
|
1498
|
-
const stepResult = await brainRun.next();
|
|
1499
|
-
expect(stepResult.value).toEqual(
|
|
1500
|
-
expect.objectContaining({
|
|
1501
|
-
type: BRAIN_EVENTS.STEP_COMPLETE,
|
|
1502
|
-
stepTitle: 'Simple step',
|
|
1503
|
-
options: {},
|
|
1504
|
-
})
|
|
1505
|
-
);
|
|
1506
|
-
});
|
|
1507
|
-
});
|
|
1508
|
-
|
|
1509
|
-
describe('services support', () => {
|
|
1510
|
-
it('should allow adding custom services to brains', async () => {
|
|
1511
|
-
// Create a brain with services
|
|
1512
|
-
const testBrain = brain('Services Test')
|
|
1513
|
-
.withServices({
|
|
1514
|
-
logger: testLogger,
|
|
1515
|
-
})
|
|
1516
|
-
.step('Use service', ({ state, logger }) => {
|
|
1517
|
-
logger.log('Test service called');
|
|
1518
|
-
return { serviceUsed: true };
|
|
1519
|
-
});
|
|
1520
|
-
|
|
1521
|
-
// Run the brain and collect events
|
|
1522
|
-
let finalState = {};
|
|
1523
|
-
for await (const event of testBrain.run({
|
|
1524
|
-
client: mockClient,
|
|
1525
|
-
})) {
|
|
1526
|
-
if (event.type === BRAIN_EVENTS.STEP_COMPLETE) {
|
|
1527
|
-
finalState = applyPatches(finalState, [event.patch]);
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
// Verify the service was called
|
|
1532
|
-
expect(testLogger.log).toHaveBeenCalledWith('Test service called');
|
|
1533
|
-
|
|
1534
|
-
// Verify the state was updated
|
|
1535
|
-
expect(finalState).toEqual({ serviceUsed: true });
|
|
1536
|
-
});
|
|
1537
|
-
});
|
|
1538
|
-
|
|
1539
|
-
describe('type inference', () => {
|
|
1540
|
-
it('should correctly infer complex brain state types', async () => {
|
|
1541
|
-
// Create an inner brain that uses the shared options type
|
|
1542
|
-
const innerBrain = brain<{ features: string[] }>('Inner Type Test').step(
|
|
1543
|
-
'Process features',
|
|
1544
|
-
({ options }) => ({
|
|
1545
|
-
processedValue: options.features.includes('fast') ? 100 : 42,
|
|
1546
|
-
featureCount: options.features.length,
|
|
1547
|
-
})
|
|
1548
|
-
);
|
|
1549
|
-
|
|
1550
|
-
// Create a complex brain using multiple features
|
|
1551
|
-
const complexBrain = brain<{ features: string[] }>('Complex Type Test')
|
|
1552
|
-
.step('First step', ({ options }) => ({
|
|
1553
|
-
initialFeatures: options.features,
|
|
1554
|
-
value: 42,
|
|
1555
|
-
}))
|
|
1556
|
-
.brain(
|
|
1557
|
-
'Nested brain',
|
|
1558
|
-
innerBrain,
|
|
1559
|
-
({ state, brainState }) => ({
|
|
1560
|
-
...state,
|
|
1561
|
-
processedValue: brainState.processedValue,
|
|
1562
|
-
totalFeatures: brainState.featureCount,
|
|
1563
|
-
}),
|
|
1564
|
-
() => ({
|
|
1565
|
-
processedValue: 0,
|
|
1566
|
-
featureCount: 0,
|
|
1567
|
-
})
|
|
1568
|
-
)
|
|
1569
|
-
.step('Final step', ({ state }) => ({
|
|
1570
|
-
...state,
|
|
1571
|
-
completed: true,
|
|
1572
|
-
}));
|
|
1573
|
-
|
|
1574
|
-
// Type test setup
|
|
1575
|
-
type ExpectedState = {
|
|
1576
|
-
initialFeatures: string[];
|
|
1577
|
-
value: number;
|
|
1578
|
-
processedValue: number;
|
|
1579
|
-
totalFeatures: number;
|
|
1580
|
-
completed: true;
|
|
1581
|
-
};
|
|
1582
|
-
|
|
1583
|
-
type ActualState = Parameters<
|
|
1584
|
-
Parameters<(typeof complexBrain)['step']>[1]
|
|
1585
|
-
>[0]['state'];
|
|
1586
|
-
|
|
1587
|
-
type TypeTest = AssertEquals<ActualState, ExpectedState>;
|
|
1588
|
-
const _typeAssert: TypeTest = true;
|
|
1589
|
-
|
|
1590
|
-
// Collect all events
|
|
1591
|
-
const events = [];
|
|
1592
|
-
let finalStepStatus,
|
|
1593
|
-
finalState = {};
|
|
1594
|
-
let mainBrainId: string | undefined;
|
|
1595
|
-
|
|
1596
|
-
for await (const event of complexBrain.run({
|
|
1597
|
-
client: mockClient,
|
|
1598
|
-
options: { features: ['fast', 'secure'] },
|
|
1599
|
-
})) {
|
|
1600
|
-
events.push(event);
|
|
1601
|
-
|
|
1602
|
-
// Capture the main brain's ID from its start event
|
|
1603
|
-
if (event.type === BRAIN_EVENTS.START && !mainBrainId) {
|
|
1604
|
-
mainBrainId = event.brainRunId;
|
|
1605
|
-
}
|
|
1606
|
-
|
|
1607
|
-
if (event.type === BRAIN_EVENTS.STEP_STATUS) {
|
|
1608
|
-
finalStepStatus = event;
|
|
1609
|
-
} else if (
|
|
1610
|
-
event.type === BRAIN_EVENTS.STEP_COMPLETE &&
|
|
1611
|
-
event.brainRunId === mainBrainId // Only process events from main brain
|
|
1612
|
-
) {
|
|
1613
|
-
finalState = applyPatches(finalState, [event.patch]);
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
// Verify brain start event
|
|
1618
|
-
expect(events[0]).toEqual(
|
|
1619
|
-
expect.objectContaining({
|
|
1620
|
-
type: BRAIN_EVENTS.START,
|
|
1621
|
-
status: STATUS.RUNNING,
|
|
1622
|
-
brainTitle: 'Complex Type Test',
|
|
1623
|
-
brainDescription: undefined,
|
|
1624
|
-
options: { features: ['fast', 'secure'] },
|
|
1625
|
-
brainRunId: mainBrainId,
|
|
1626
|
-
})
|
|
1627
|
-
);
|
|
1628
|
-
|
|
1629
|
-
// Verify inner brain events are included
|
|
1630
|
-
const innerStartEvent = events.find(
|
|
1631
|
-
(e) =>
|
|
1632
|
-
e.type === BRAIN_EVENTS.START &&
|
|
1633
|
-
'brainRunId' in e &&
|
|
1634
|
-
e.brainRunId !== mainBrainId
|
|
1635
|
-
);
|
|
1636
|
-
expect(innerStartEvent).toEqual(
|
|
1637
|
-
expect.objectContaining({
|
|
1638
|
-
type: BRAIN_EVENTS.START,
|
|
1639
|
-
status: STATUS.RUNNING,
|
|
1640
|
-
brainTitle: 'Inner Type Test',
|
|
1641
|
-
options: { features: ['fast', 'secure'] },
|
|
1642
|
-
})
|
|
1643
|
-
);
|
|
1644
|
-
|
|
1645
|
-
// Verify the final step status
|
|
1646
|
-
if (!finalStepStatus) throw new Error('Expected final step status event');
|
|
1647
|
-
const lastStep = finalStepStatus.steps[finalStepStatus.steps.length - 1];
|
|
1648
|
-
expect(lastStep.status).toBe(STATUS.COMPLETE);
|
|
1649
|
-
expect(lastStep.title).toBe('Final step');
|
|
1650
|
-
|
|
1651
|
-
expect(finalState).toEqual({
|
|
1652
|
-
initialFeatures: ['fast', 'secure'],
|
|
1653
|
-
value: 42,
|
|
1654
|
-
processedValue: 100,
|
|
1655
|
-
totalFeatures: 2,
|
|
1656
|
-
completed: true,
|
|
1657
|
-
});
|
|
1658
|
-
});
|
|
1659
|
-
|
|
1660
|
-
it('should correctly infer brain reducer state types', async () => {
|
|
1661
|
-
// Create an inner brain with a specific state shape
|
|
1662
|
-
const innerBrain = brain('Inner State Test').step('Inner step', () => ({
|
|
1663
|
-
innerValue: 42,
|
|
1664
|
-
metadata: { processed: true },
|
|
1665
|
-
}));
|
|
1666
|
-
|
|
1667
|
-
// Create outer brain to test reducer type inference
|
|
1668
|
-
const outerBrain = brain('Outer State Test')
|
|
1669
|
-
.step('First step', () => ({
|
|
1670
|
-
outerValue: 100,
|
|
1671
|
-
status: 'ready',
|
|
1672
|
-
}))
|
|
1673
|
-
.brain(
|
|
1674
|
-
'Nested brain',
|
|
1675
|
-
innerBrain,
|
|
1676
|
-
({ state, brainState }) => {
|
|
1677
|
-
// Type assertion for outer state
|
|
1678
|
-
type ExpectedOuterState = {
|
|
1679
|
-
outerValue: number;
|
|
1680
|
-
status: string;
|
|
1681
|
-
};
|
|
1682
|
-
type ActualOuterState = typeof state;
|
|
1683
|
-
type OuterStateTest = AssertEquals<
|
|
1684
|
-
ActualOuterState,
|
|
1685
|
-
ExpectedOuterState
|
|
1686
|
-
>;
|
|
1687
|
-
const _outerAssert: OuterStateTest = true;
|
|
1688
|
-
|
|
1689
|
-
// Type assertion for inner brain state
|
|
1690
|
-
type ExpectedInnerState = {
|
|
1691
|
-
innerValue: number;
|
|
1692
|
-
metadata: { processed: true };
|
|
1693
|
-
};
|
|
1694
|
-
type ActualInnerState = typeof brainState;
|
|
1695
|
-
type InnerStateTest = AssertEquals<
|
|
1696
|
-
ActualInnerState,
|
|
1697
|
-
ExpectedInnerState
|
|
1698
|
-
>;
|
|
1699
|
-
const _innerAssert: InnerStateTest = true;
|
|
1700
|
-
|
|
1701
|
-
return {
|
|
1702
|
-
...state,
|
|
1703
|
-
innerResult: brainState.innerValue,
|
|
1704
|
-
processed: brainState.metadata.processed,
|
|
1705
|
-
};
|
|
1706
|
-
},
|
|
1707
|
-
() => ({} as { innerValue: number; metadata: { processed: boolean } })
|
|
1708
|
-
);
|
|
1709
|
-
|
|
1710
|
-
// Run the brain to verify runtime behavior
|
|
1711
|
-
let finalState = {};
|
|
1712
|
-
let mainBrainId: string | undefined;
|
|
1713
|
-
|
|
1714
|
-
for await (const event of outerBrain.run({
|
|
1715
|
-
client: mockClient,
|
|
1716
|
-
})) {
|
|
1717
|
-
if (event.type === BRAIN_EVENTS.START && !mainBrainId) {
|
|
1718
|
-
mainBrainId = event.brainRunId;
|
|
1719
|
-
}
|
|
1720
|
-
if (
|
|
1721
|
-
event.type === BRAIN_EVENTS.STEP_COMPLETE &&
|
|
1722
|
-
event.brainRunId === mainBrainId
|
|
1723
|
-
) {
|
|
1724
|
-
finalState = applyPatches(finalState, [event.patch]);
|
|
1725
|
-
}
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
expect(finalState).toEqual({
|
|
1729
|
-
outerValue: 100,
|
|
1730
|
-
status: 'ready',
|
|
1731
|
-
innerResult: 42,
|
|
1732
|
-
processed: true,
|
|
1733
|
-
});
|
|
1734
|
-
});
|
|
1735
|
-
|
|
1736
|
-
it('should correctly infer step action state types', async () => {
|
|
1737
|
-
const testBrain = brain('Action State Test')
|
|
1738
|
-
.step('First step', () => ({
|
|
1739
|
-
count: 1,
|
|
1740
|
-
metadata: { created: new Date().toISOString() },
|
|
1741
|
-
}))
|
|
1742
|
-
.step('Second step', ({ state }) => {
|
|
1743
|
-
// Type assertion for action state
|
|
1744
|
-
type ExpectedState = {
|
|
1745
|
-
count: number;
|
|
1746
|
-
metadata: { created: string };
|
|
1747
|
-
};
|
|
1748
|
-
type ActualState = typeof state;
|
|
1749
|
-
type StateTest = AssertEquals<ActualState, ExpectedState>;
|
|
1750
|
-
const _stateAssert: StateTest = true;
|
|
1751
|
-
|
|
1752
|
-
return {
|
|
1753
|
-
...state,
|
|
1754
|
-
count: state.count + 1,
|
|
1755
|
-
metadata: {
|
|
1756
|
-
...state.metadata,
|
|
1757
|
-
updated: new Date().toISOString(),
|
|
1758
|
-
},
|
|
1759
|
-
};
|
|
1760
|
-
});
|
|
1761
|
-
|
|
1762
|
-
// Run the brain to verify runtime behavior
|
|
1763
|
-
let finalState = {};
|
|
1764
|
-
let mainBrainId: string | undefined;
|
|
1765
|
-
|
|
1766
|
-
for await (const event of testBrain.run({
|
|
1767
|
-
client: mockClient,
|
|
1768
|
-
})) {
|
|
1769
|
-
if (event.type === BRAIN_EVENTS.START && !mainBrainId) {
|
|
1770
|
-
mainBrainId = event.brainRunId;
|
|
1771
|
-
}
|
|
1772
|
-
if (
|
|
1773
|
-
event.type === BRAIN_EVENTS.STEP_COMPLETE &&
|
|
1774
|
-
event.brainRunId === mainBrainId
|
|
1775
|
-
) {
|
|
1776
|
-
finalState = applyPatches(finalState, [event.patch]);
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
expect(finalState).toMatchObject({
|
|
1781
|
-
count: 2,
|
|
1782
|
-
metadata: {
|
|
1783
|
-
created: expect.any(String),
|
|
1784
|
-
updated: expect.any(String),
|
|
1785
|
-
},
|
|
1786
|
-
});
|
|
1787
|
-
});
|
|
1788
|
-
|
|
1789
|
-
it('should correctly infer prompt response types in subsequent steps', async () => {
|
|
1790
|
-
const testBrain = brain('Prompt Type Test')
|
|
1791
|
-
.prompt('Get user info', {
|
|
1792
|
-
template: () => "What is the user's info?",
|
|
1793
|
-
outputSchema: {
|
|
1794
|
-
schema: z.object({ name: z.string(), age: z.number() }),
|
|
1795
|
-
name: 'userInfo' as const, // Must be const or type inference breaks
|
|
1796
|
-
},
|
|
1797
|
-
})
|
|
1798
|
-
.step('Use response', ({ state }) => {
|
|
1799
|
-
// Type assertion to verify state includes userInfo
|
|
1800
|
-
type ExpectedState = {
|
|
1801
|
-
userInfo: {
|
|
1802
|
-
name: string;
|
|
1803
|
-
age: number;
|
|
1804
|
-
};
|
|
1805
|
-
};
|
|
1806
|
-
type ActualState = typeof state;
|
|
1807
|
-
type StateTest = AssertEquals<ActualState, ExpectedState>;
|
|
1808
|
-
const _stateAssert: StateTest = true;
|
|
1809
|
-
|
|
1810
|
-
return {
|
|
1811
|
-
...state,
|
|
1812
|
-
greeting: `Hello ${state.userInfo.name}, you are ${state.userInfo.age} years old`,
|
|
1813
|
-
};
|
|
1814
|
-
});
|
|
1815
|
-
|
|
1816
|
-
// Mock the client response
|
|
1817
|
-
mockClient.generateObject.mockResolvedValueOnce({
|
|
1818
|
-
name: 'Test User',
|
|
1819
|
-
age: 30,
|
|
1820
|
-
});
|
|
1821
|
-
|
|
1822
|
-
// Run brain and collect final state
|
|
1823
|
-
let finalState = {};
|
|
1824
|
-
for await (const event of testBrain.run({
|
|
1825
|
-
client: mockClient,
|
|
1826
|
-
})) {
|
|
1827
|
-
if (event.type === BRAIN_EVENTS.STEP_COMPLETE) {
|
|
1828
|
-
finalState = applyPatches(finalState, [event.patch]);
|
|
1829
|
-
}
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
// Verify the brain executed correctly
|
|
1833
|
-
expect(finalState).toEqual({
|
|
1834
|
-
userInfo: {
|
|
1835
|
-
name: 'Test User',
|
|
1836
|
-
age: 30,
|
|
1837
|
-
},
|
|
1838
|
-
greeting: 'Hello Test User, you are 30 years old',
|
|
1839
|
-
});
|
|
1840
|
-
});
|
|
1841
|
-
|
|
1842
|
-
it('should correctly handle prompt reduce function', async () => {
|
|
1843
|
-
const testBrain = brain('Prompt Reduce Test').prompt(
|
|
1844
|
-
'Get numbers',
|
|
1845
|
-
{
|
|
1846
|
-
template: () => 'Give me some numbers',
|
|
1847
|
-
outputSchema: {
|
|
1848
|
-
schema: z.object({ numbers: z.array(z.number()) }),
|
|
1849
|
-
name: 'numbersResponse' as const,
|
|
1850
|
-
},
|
|
1851
|
-
},
|
|
1852
|
-
({ state, response, options }) => ({
|
|
1853
|
-
...state,
|
|
1854
|
-
numbersResponse: response, // Include the response explicitly
|
|
1855
|
-
sum: response.numbers.reduce((a, b) => a + b, 0),
|
|
1856
|
-
count: response.numbers.length,
|
|
1857
|
-
})
|
|
1858
|
-
);
|
|
1859
|
-
|
|
1860
|
-
// Mock the client response
|
|
1861
|
-
mockClient.generateObject.mockResolvedValueOnce({
|
|
1862
|
-
numbers: [1, 2, 3, 4, 5],
|
|
1863
|
-
});
|
|
1864
|
-
|
|
1865
|
-
// Run brain and collect final state
|
|
1866
|
-
let finalState = {};
|
|
1867
|
-
for await (const event of testBrain.run({
|
|
1868
|
-
client: mockClient,
|
|
1869
|
-
})) {
|
|
1870
|
-
if (event.type === BRAIN_EVENTS.STEP_COMPLETE) {
|
|
1871
|
-
finalState = applyPatches(finalState, [event.patch]);
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
|
-
|
|
1875
|
-
// Verify the brain executed correctly with reduced state
|
|
1876
|
-
expect(finalState).toEqual({
|
|
1877
|
-
numbersResponse: {
|
|
1878
|
-
numbers: [1, 2, 3, 4, 5],
|
|
1879
|
-
},
|
|
1880
|
-
sum: 15,
|
|
1881
|
-
count: 5,
|
|
1882
|
-
});
|
|
1883
|
-
|
|
1884
|
-
// Verify type inference works correctly
|
|
1885
|
-
type ExpectedState = {
|
|
1886
|
-
numbersResponse: {
|
|
1887
|
-
numbers: number[];
|
|
1888
|
-
};
|
|
1889
|
-
sum: number;
|
|
1890
|
-
count: number;
|
|
1891
|
-
};
|
|
1892
|
-
|
|
1893
|
-
type ActualState = Parameters<
|
|
1894
|
-
Parameters<(typeof testBrain)['step']>[1]
|
|
1895
|
-
>[0]['state'];
|
|
1896
|
-
|
|
1897
|
-
type TypeTest = AssertEquals<ActualState, ExpectedState>;
|
|
1898
|
-
const _typeAssert: TypeTest = true;
|
|
1899
|
-
});
|
|
1900
|
-
});
|
|
1901
|
-
|
|
1902
|
-
describe('brain structure', () => {
|
|
1903
|
-
it('should expose brain structure with steps', () => {
|
|
1904
|
-
const testBrain = brain({
|
|
1905
|
-
title: 'Test Brain',
|
|
1906
|
-
description: 'A test brain description',
|
|
1907
|
-
})
|
|
1908
|
-
.step('First step', ({ state }) => ({ ...state, step1: true }))
|
|
1909
|
-
.step('Second step', ({ state }) => ({ ...state, step2: true }))
|
|
1910
|
-
.step('Third step', ({ state }) => ({ ...state, step3: true }));
|
|
1911
|
-
|
|
1912
|
-
const structure = testBrain.structure;
|
|
1913
|
-
|
|
1914
|
-
expect(structure).toEqual({
|
|
1915
|
-
title: 'Test Brain',
|
|
1916
|
-
description: 'A test brain description',
|
|
1917
|
-
steps: [
|
|
1918
|
-
{ type: 'step', title: 'First step' },
|
|
1919
|
-
{ type: 'step', title: 'Second step' },
|
|
1920
|
-
{ type: 'step', title: 'Third step' },
|
|
1921
|
-
],
|
|
1922
|
-
});
|
|
1923
|
-
});
|
|
1924
|
-
|
|
1925
|
-
it('should expose nested brain structure recursively', () => {
|
|
1926
|
-
const innerBrain = brain({
|
|
1927
|
-
title: 'Inner Brain',
|
|
1928
|
-
description: 'An inner brain',
|
|
1929
|
-
})
|
|
1930
|
-
.step('Inner step 1', ({ state }) => ({ ...state, inner1: true }))
|
|
1931
|
-
.step('Inner step 2', ({ state }) => ({ ...state, inner2: true }));
|
|
1932
|
-
|
|
1933
|
-
const outerBrain = brain({
|
|
1934
|
-
title: 'Outer Brain',
|
|
1935
|
-
description: 'An outer brain',
|
|
1936
|
-
})
|
|
1937
|
-
.step('Outer step 1', ({ state }) => ({ ...state, outer1: true }))
|
|
1938
|
-
.brain('Run inner brain', innerBrain, ({ brainState }) => ({
|
|
1939
|
-
result: brainState,
|
|
1940
|
-
}))
|
|
1941
|
-
.step('Outer step 2', ({ state }) => ({ ...state, outer2: true }));
|
|
1942
|
-
|
|
1943
|
-
const structure = outerBrain.structure;
|
|
1944
|
-
|
|
1945
|
-
expect(structure).toEqual({
|
|
1946
|
-
title: 'Outer Brain',
|
|
1947
|
-
description: 'An outer brain',
|
|
1948
|
-
steps: [
|
|
1949
|
-
{ type: 'step', title: 'Outer step 1' },
|
|
1950
|
-
{
|
|
1951
|
-
type: 'brain',
|
|
1952
|
-
title: 'Run inner brain',
|
|
1953
|
-
innerBrain: {
|
|
1954
|
-
title: 'Inner Brain',
|
|
1955
|
-
description: 'An inner brain',
|
|
1956
|
-
steps: [
|
|
1957
|
-
{ type: 'step', title: 'Inner step 1' },
|
|
1958
|
-
{ type: 'step', title: 'Inner step 2' },
|
|
1959
|
-
],
|
|
1960
|
-
},
|
|
1961
|
-
},
|
|
1962
|
-
{ type: 'step', title: 'Outer step 2' },
|
|
1963
|
-
],
|
|
1964
|
-
});
|
|
1965
|
-
});
|
|
1966
|
-
|
|
1967
|
-
it('should handle brain without description', () => {
|
|
1968
|
-
const testBrain = brain('No Description Brain').step(
|
|
1969
|
-
'Only step',
|
|
1970
|
-
({ state }) => state
|
|
1971
|
-
);
|
|
1972
|
-
|
|
1973
|
-
const structure = testBrain.structure;
|
|
1974
|
-
|
|
1975
|
-
expect(structure).toEqual({
|
|
1976
|
-
title: 'No Description Brain',
|
|
1977
|
-
description: undefined,
|
|
1978
|
-
steps: [{ type: 'step', title: 'Only step' }],
|
|
1979
|
-
});
|
|
1980
|
-
});
|
|
1981
|
-
});
|