@positronic/cloudflare 0.0.2 → 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.
Files changed (52) hide show
  1. package/dist/src/api.js +1270 -0
  2. package/dist/src/brain-runner-do.js +654 -0
  3. package/dist/src/dev-server.js +1357 -0
  4. package/{src/index.ts → dist/src/index.js} +1 -6
  5. package/dist/src/manifest.js +278 -0
  6. package/dist/src/monitor-do.js +408 -0
  7. package/{src/node-index.ts → dist/src/node-index.js} +3 -7
  8. package/dist/src/r2-loader.js +207 -0
  9. package/dist/src/schedule-do.js +705 -0
  10. package/dist/src/sqlite-adapter.js +69 -0
  11. package/dist/types/api.d.ts +21 -0
  12. package/dist/types/api.d.ts.map +1 -0
  13. package/dist/types/brain-runner-do.d.ts +25 -0
  14. package/dist/types/brain-runner-do.d.ts.map +1 -0
  15. package/dist/types/dev-server.d.ts +45 -0
  16. package/dist/types/dev-server.d.ts.map +1 -0
  17. package/dist/types/index.d.ts +7 -0
  18. package/dist/types/index.d.ts.map +1 -0
  19. package/dist/types/manifest.d.ts +11 -0
  20. package/dist/types/manifest.d.ts.map +1 -0
  21. package/dist/types/monitor-do.d.ts +16 -0
  22. package/dist/types/monitor-do.d.ts.map +1 -0
  23. package/dist/types/node-index.d.ts +10 -0
  24. package/dist/types/node-index.d.ts.map +1 -0
  25. package/dist/types/r2-loader.d.ts +10 -0
  26. package/dist/types/r2-loader.d.ts.map +1 -0
  27. package/dist/types/schedule-do.d.ts +47 -0
  28. package/dist/types/schedule-do.d.ts.map +1 -0
  29. package/dist/types/sqlite-adapter.d.ts +10 -0
  30. package/dist/types/sqlite-adapter.d.ts.map +1 -0
  31. package/package.json +8 -4
  32. package/src/api.ts +0 -579
  33. package/src/brain-runner-do.ts +0 -309
  34. package/src/dev-server.ts +0 -776
  35. package/src/manifest.ts +0 -69
  36. package/src/monitor-do.ts +0 -268
  37. package/src/r2-loader.ts +0 -27
  38. package/src/schedule-do.ts +0 -377
  39. package/src/sqlite-adapter.ts +0 -50
  40. package/test-project/package-lock.json +0 -3010
  41. package/test-project/package.json +0 -21
  42. package/test-project/src/index.ts +0 -70
  43. package/test-project/src/runner.ts +0 -24
  44. package/test-project/tests/api.test.ts +0 -1005
  45. package/test-project/tests/r2loader.test.ts +0 -73
  46. package/test-project/tests/resources-api.test.ts +0 -671
  47. package/test-project/tests/spec.test.ts +0 -135
  48. package/test-project/tests/tsconfig.json +0 -7
  49. package/test-project/tsconfig.json +0 -20
  50. package/test-project/vitest.config.ts +0 -12
  51. package/test-project/wrangler.jsonc +0 -53
  52. package/tsconfig.json +0 -11
@@ -1,1005 +0,0 @@
1
- import {
2
- env,
3
- createExecutionContext,
4
- waitOnExecutionContext,
5
- } from 'cloudflare:test';
6
-
7
- import { describe, it, expect, beforeAll } from 'vitest';
8
- import worker from '../src/index';
9
- import { BRAIN_EVENTS, STATUS } from '@positronic/core';
10
- import type {
11
- BrainEvent,
12
- BrainStartEvent,
13
- BrainCompleteEvent,
14
- StepStatusEvent,
15
- StepCompletedEvent,
16
- StepStartedEvent,
17
- } from '@positronic/core';
18
- import type { BrainRunnerDO } from '../../src/brain-runner-do.js';
19
- import type { MonitorDO } from '../../src/monitor-do.js';
20
- import type { ScheduleDO } from '../../src/schedule-do.js';
21
-
22
- interface TestEnv {
23
- BRAIN_RUNNER_DO: DurableObjectNamespace<BrainRunnerDO>;
24
- MONITOR_DO: DurableObjectNamespace<MonitorDO>;
25
- SCHEDULE_DO: DurableObjectNamespace<ScheduleDO>;
26
- DB: D1Database;
27
- RESOURCES_BUCKET: R2Bucket;
28
- }
29
-
30
- describe('Hono API Tests', () => {
31
- // Helper to parse SSE data field
32
- function parseSseEvent(text: string): any | null {
33
- const lines = text.trim().split('\n');
34
- for (const line of lines) {
35
- if (line.startsWith('data: ')) {
36
- try {
37
- const jsonData = line.substring(6); // Length of "data: "
38
- const parsed = JSON.parse(jsonData);
39
- return parsed;
40
- } catch (e) {
41
- console.error(
42
- '[TEST_SSE_PARSE] Failed to parse SSE data:',
43
- line.substring(6),
44
- e
45
- );
46
- return null;
47
- }
48
- }
49
- }
50
- return null;
51
- }
52
-
53
- // Helper function to read the entire SSE stream and collect events
54
- async function readSseStream(
55
- stream: ReadableStream<Uint8Array>
56
- ): Promise<BrainEvent[]> {
57
- const reader = stream.getReader();
58
- const decoder = new TextDecoder();
59
- let buffer = '';
60
- const events: BrainEvent[] = [];
61
-
62
- while (true) {
63
- const { value, done } = await reader.read();
64
- if (done) {
65
- // Process any remaining buffer content
66
- if (buffer.trim().length > 0) {
67
- const event = parseSseEvent(buffer);
68
- if (event) {
69
- events.push(event);
70
- }
71
- }
72
- break; // Exit loop when stream is done
73
- }
74
-
75
- const decodedChunk = decoder.decode(value, { stream: true });
76
- buffer += decodedChunk;
77
- // Process buffer line by line, looking for complete messages (ending in \n\n)
78
- let eventEndIndex;
79
- while ((eventEndIndex = buffer.indexOf('\n\n')) !== -1) {
80
- const message = buffer.substring(0, eventEndIndex);
81
- buffer = buffer.substring(eventEndIndex + 2); // Consume message + \n\n
82
- if (message.startsWith('data:')) {
83
- const event = parseSseEvent(message);
84
- if (event) {
85
- events.push(event);
86
- if (event.type === BRAIN_EVENTS.COMPLETE) {
87
- reader.cancel(`Received terminal event: ${event.type}`);
88
- return events;
89
- }
90
- if (event.type === BRAIN_EVENTS.ERROR) {
91
- console.error(
92
- 'Received BRAIN_EVENTS.ERROR. Event details:',
93
- event
94
- );
95
- reader.cancel(`Received terminal event: ${event.type}`);
96
- throw new Error(
97
- `Received terminal event: ${
98
- event.type
99
- }. Details: ${JSON.stringify(event)}`
100
- );
101
- }
102
- }
103
- }
104
- }
105
- }
106
- return events;
107
- }
108
-
109
- it('POST /brains/runs without brainName should return 400', async () => {
110
- const testEnv = env as TestEnv;
111
-
112
- const request = new Request('http://example.com/brains/runs', {
113
- method: 'POST',
114
- headers: { 'Content-Type': 'application/json' },
115
- body: JSON.stringify({}), // Empty body, check for missing brainName
116
- });
117
- const context = createExecutionContext();
118
- const response = await worker.fetch(request, testEnv, context);
119
- await waitOnExecutionContext(context);
120
-
121
- expect(response.status).toBe(400);
122
- const responseBody = await response.json();
123
- expect(responseBody).toEqual({
124
- error: 'Missing brainName in request body',
125
- });
126
- });
127
-
128
- it('POST /brains/runs with non-existent brain should return 404', async () => {
129
- const testEnv = env as TestEnv;
130
- const request = new Request('http://example.com/brains/runs', {
131
- method: 'POST',
132
- headers: { 'Content-Type': 'application/json' },
133
- body: JSON.stringify({ brainName: 'non-existent-brain' }),
134
- });
135
- const context = createExecutionContext();
136
- const response = await worker.fetch(request, testEnv, context);
137
- await waitOnExecutionContext(context);
138
- expect(response.status).toBe(404);
139
- const responseBody = await response.json();
140
- expect(responseBody).toEqual({
141
- error: "Brain 'non-existent-brain' not found",
142
- });
143
- });
144
-
145
- it('Create and watch a brain run', async () => {
146
- const testEnv = env as TestEnv;
147
- const brainName = 'basic-brain';
148
-
149
- // --- Create the brain run ---
150
- const request = new Request('http://example.com/brains/runs', {
151
- method: 'POST',
152
- headers: { 'Content-Type': 'application/json' },
153
- body: JSON.stringify({ brainName }),
154
- });
155
- const context = createExecutionContext();
156
- const response = await worker.fetch(request, testEnv, context);
157
- expect(response.status).toBe(201);
158
- const responseBody = await response.json<{ brainRunId: string }>();
159
- const brainRunId = responseBody.brainRunId;
160
- await waitOnExecutionContext(context);
161
-
162
- // --- Watch the brain run via SSE ---
163
- const watchUrl = `http://example.com/brains/runs/${brainRunId}/watch`;
164
- const watchRequest = new Request(watchUrl);
165
- const watchContext = createExecutionContext();
166
- const watchResponse = await worker.fetch(
167
- watchRequest,
168
- testEnv,
169
- watchContext
170
- );
171
-
172
- expect(watchResponse.status).toBe(200);
173
- expect(watchResponse.headers.get('Content-Type')).toContain(
174
- 'text/event-stream'
175
- );
176
- if (!watchResponse.body) {
177
- throw new Error('Watch response body is null');
178
- }
179
-
180
- // --- Read all events from the SSE stream ---
181
- const allEvents = await readSseStream(watchResponse.body);
182
-
183
- // --- Assertions on the collected events ---
184
- // Check for start event
185
- const startEvent = allEvents.find(
186
- (e): e is BrainStartEvent => e.type === BRAIN_EVENTS.START
187
- );
188
- expect(startEvent).toBeDefined();
189
- expect(startEvent?.brainTitle).toBe(brainName);
190
- expect(startEvent?.status).toBe(STATUS.RUNNING);
191
-
192
- // Check for complete event
193
- const completeEvent = allEvents.find(
194
- (e): e is BrainCompleteEvent => e.type === BRAIN_EVENTS.COMPLETE
195
- );
196
- expect(completeEvent).toBeDefined();
197
- expect(completeEvent?.status).toBe(STATUS.COMPLETE);
198
-
199
- // Check the final step status event
200
- const stepStatusEvents = allEvents.filter(
201
- (e): e is StepStatusEvent => e.type === BRAIN_EVENTS.STEP_STATUS
202
- );
203
- expect(stepStatusEvents.length).toBeGreaterThan(0);
204
- const lastStepStatusEvent = stepStatusEvents[stepStatusEvents.length - 1];
205
- expect(
206
- lastStepStatusEvent.steps.every(
207
- (step: any) => step.status === STATUS.COMPLETE
208
- )
209
- ).toBe(true);
210
-
211
- // Check for specific step completion if needed (depends on basic-brain structure)
212
- const stepCompleteEvents = allEvents.filter(
213
- (e): e is StepCompletedEvent => e.type === BRAIN_EVENTS.STEP_COMPLETE
214
- );
215
- expect(stepCompleteEvents.length).toBeGreaterThanOrEqual(1); // Assuming basic-brain has at least one step
216
-
217
- await waitOnExecutionContext(watchContext);
218
- });
219
-
220
- it('Create and watch a delayed brain run', async () => {
221
- const testEnv = env as TestEnv;
222
- const brainName = 'delayed-brain';
223
-
224
- // Create the brain run
225
- const createRequest = new Request('http://example.com/brains/runs', {
226
- method: 'POST',
227
- headers: { 'Content-Type': 'application/json' },
228
- body: JSON.stringify({ brainName }),
229
- });
230
- const createContext = createExecutionContext();
231
- const createResponse = await worker.fetch(
232
- createRequest,
233
- testEnv,
234
- createContext
235
- );
236
- expect(createResponse.status).toBe(201);
237
- const createResponseBody = await createResponse.json<{
238
- brainRunId: string;
239
- }>();
240
- const brainRunId = createResponseBody.brainRunId;
241
- await waitOnExecutionContext(createContext);
242
-
243
- // Watch the brain run via SSE
244
- const watchUrl = `http://example.com/brains/runs/${brainRunId}/watch`;
245
- const watchRequest = new Request(watchUrl);
246
- const watchContext = createExecutionContext();
247
- const watchResponse = await worker.fetch(
248
- watchRequest,
249
- testEnv,
250
- watchContext
251
- );
252
-
253
- expect(watchResponse.status).toBe(200);
254
- expect(watchResponse.headers.get('Content-Type')).toContain(
255
- 'text/event-stream'
256
- );
257
- if (!watchResponse.body) {
258
- throw new Error('Watch response body is null');
259
- }
260
-
261
- // --- Read all events from the SSE stream ---
262
- const allEvents = await readSseStream(watchResponse.body);
263
-
264
- // --- Assertions on the collected events ---
265
- // Check for start event
266
- const startEvent = allEvents.find(
267
- (e): e is BrainStartEvent => e.type === BRAIN_EVENTS.START
268
- );
269
- expect(startEvent).toBeDefined();
270
- expect(startEvent?.brainTitle).toBe(brainName);
271
- expect(startEvent?.status).toBe(STATUS.RUNNING);
272
-
273
- // Check for step start/complete events for the delayed step
274
- const delayStepStart = allEvents.find(
275
- (e): e is StepStartedEvent =>
276
- e.type === BRAIN_EVENTS.STEP_START && e.stepTitle === 'Start Delay'
277
- );
278
- expect(delayStepStart).toBeDefined();
279
- const delayStepComplete = allEvents.find(
280
- (e): e is StepCompletedEvent =>
281
- e.type === BRAIN_EVENTS.STEP_COMPLETE && e.stepTitle === 'Start Delay'
282
- );
283
- expect(delayStepComplete).toBeDefined();
284
-
285
- // Check for the final complete event
286
- const completeEvent = allEvents.find(
287
- (e): e is BrainCompleteEvent => e.type === BRAIN_EVENTS.COMPLETE
288
- );
289
- expect(completeEvent).toBeDefined();
290
- expect(completeEvent?.status).toBe(STATUS.COMPLETE);
291
-
292
- // Check the final step status event shows completion
293
- const stepStatusEvents = allEvents.filter(
294
- (e): e is StepStatusEvent => e.type === BRAIN_EVENTS.STEP_STATUS
295
- );
296
- expect(stepStatusEvents.length).toBeGreaterThan(0);
297
- const lastStepStatusEvent = stepStatusEvents[stepStatusEvents.length - 1];
298
- expect(
299
- lastStepStatusEvent.steps.every(
300
- (step: any) => step.status === STATUS.COMPLETE
301
- )
302
- ).toBe(true);
303
-
304
- await waitOnExecutionContext(watchContext);
305
- });
306
-
307
- it('Asserts brainRunId is present in SSE events', async () => {
308
- const testEnv = env as TestEnv;
309
- const brainName = 'basic-brain';
310
-
311
- // Create brain run
312
- const createRequest = new Request('http://example.com/brains/runs', {
313
- method: 'POST',
314
- headers: { 'Content-Type': 'application/json' },
315
- body: JSON.stringify({ brainName }),
316
- });
317
- const createContext = createExecutionContext();
318
- const createResponse = await worker.fetch(
319
- createRequest,
320
- testEnv,
321
- createContext
322
- );
323
- const createResponseBody = await createResponse.json<{
324
- brainRunId: string;
325
- }>();
326
- const expectedBrainRunId = createResponseBody.brainRunId;
327
- await waitOnExecutionContext(createContext);
328
-
329
- // Watch brain run
330
- const watchUrl = `http://example.com/brains/runs/${expectedBrainRunId}/watch`;
331
- const watchRequest = new Request(watchUrl);
332
- const watchContext = createExecutionContext();
333
- const watchResponse = await worker.fetch(
334
- watchRequest,
335
- testEnv,
336
- watchContext
337
- );
338
-
339
- // Get first event from stream
340
- const reader = watchResponse.body?.getReader();
341
- if (!reader) throw new Error('Watch response body is null');
342
-
343
- const { value } = await reader.read();
344
- const chunk = new TextDecoder().decode(value);
345
- const event = parseSseEvent(chunk);
346
-
347
- // Cleanup
348
- reader.cancel();
349
- await waitOnExecutionContext(watchContext);
350
-
351
- // Assert
352
- expect(event.brainRunId).toBeDefined();
353
- expect(event.brainRunId).toBe(expectedBrainRunId);
354
- });
355
-
356
- it('Monitor receives brain events (checking brain run)', async () => {
357
- const testEnv = env as TestEnv;
358
- const brainName = 'basic-brain';
359
-
360
- // Start the brain run
361
- const createRequest = new Request('http://example.com/brains/runs', {
362
- method: 'POST',
363
- headers: { 'Content-Type': 'application/json' },
364
- body: JSON.stringify({ brainName }),
365
- });
366
- const createContext = createExecutionContext();
367
- const createResponse = await worker.fetch(
368
- createRequest,
369
- testEnv,
370
- createContext
371
- );
372
- const { brainRunId } = await createResponse.json<{ brainRunId: string }>();
373
- await waitOnExecutionContext(createContext);
374
-
375
- // Watch the brain run via SSE until completion
376
- const watchUrl = `http://example.com/brains/runs/${brainRunId}/watch`;
377
- const watchRequest = new Request(watchUrl);
378
- const watchContext = createExecutionContext();
379
- const watchResponse = await worker.fetch(
380
- watchRequest,
381
- testEnv,
382
- watchContext
383
- );
384
- await readSseStream(watchResponse.body!);
385
- await waitOnExecutionContext(watchContext);
386
-
387
- // Get the monitor singleton instance
388
- const monitorId = testEnv.MONITOR_DO.idFromName('singleton');
389
- const monitorStub = testEnv.MONITOR_DO.get(monitorId);
390
- const lastEvent = await monitorStub.getLastEvent(brainRunId);
391
-
392
- // The last event should be a brain complete event
393
- expect(lastEvent).toBeDefined();
394
- expect(lastEvent.type).toBe(BRAIN_EVENTS.COMPLETE);
395
- expect(lastEvent.status).toBe(STATUS.COMPLETE);
396
- });
397
-
398
- it('Watches brain run as it runs', async () => {
399
- const testEnv = env as TestEnv;
400
- const brainName = 'basic-brain';
401
-
402
- // Run the brain run twice
403
- for (let i = 0; i < 2; i++) {
404
- // Start the brain run
405
- const createRequest = new Request('http://example.com/brains/runs', {
406
- method: 'POST',
407
- headers: { 'Content-Type': 'application/json' },
408
- body: JSON.stringify({ brainName }),
409
- });
410
- const createContext = createExecutionContext();
411
- const createResponse = await worker.fetch(
412
- createRequest,
413
- testEnv,
414
- createContext
415
- );
416
- const { brainRunId } = await createResponse.json<{
417
- brainRunId: string;
418
- }>();
419
-
420
- // Watch the brain run via SSE until completion
421
- const watchUrl = `http://example.com/brains/runs/${brainRunId}/watch`;
422
- const watchRequest = new Request(watchUrl);
423
- const watchContext = createExecutionContext();
424
- const watchResponse = await worker.fetch(
425
- watchRequest,
426
- testEnv,
427
- watchContext
428
- );
429
- await readSseStream(watchResponse.body!);
430
- await waitOnExecutionContext(watchContext);
431
- await waitOnExecutionContext(createContext);
432
- }
433
-
434
- // Get brain run history
435
- const historyRequest = new Request(
436
- `http://example.com/brains/${brainName}/history?limit=5`
437
- );
438
- const historyContext = createExecutionContext();
439
- const historyResponse = await worker.fetch(
440
- historyRequest,
441
- testEnv,
442
- historyContext
443
- );
444
- await waitOnExecutionContext(historyContext);
445
-
446
- expect(historyResponse.status).toBe(200);
447
- const history = await historyResponse.json<{
448
- runs: Array<{
449
- brainRunId: string;
450
- brainTitle: string;
451
- brainDescription: string | null;
452
- type: string;
453
- status: string;
454
- options: string;
455
- error: string | null;
456
- createdAt: number;
457
- startedAt: number | null;
458
- completedAt: number | null;
459
- }>;
460
- }>();
461
- expect(history.runs.length).toBe(2);
462
-
463
- // Verify each run has the expected properties
464
- for (const run of history.runs) {
465
- expect(run).toHaveProperty('brainRunId');
466
- expect(run).toHaveProperty('brainTitle');
467
- expect(run).toHaveProperty('brainDescription');
468
- expect(run).toHaveProperty('type');
469
- expect(run).toHaveProperty('status');
470
- expect(run).toHaveProperty('options');
471
- expect(run).toHaveProperty('error');
472
- expect(run).toHaveProperty('createdAt');
473
- expect(run).toHaveProperty('startedAt');
474
- expect(run).toHaveProperty('completedAt');
475
- expect(run.status).toBe(STATUS.COMPLETE);
476
- expect(run.brainTitle).toBe(brainName);
477
- }
478
-
479
- // Verify runs are ordered by createdAt descending
480
- const timestamps = history.runs.map(
481
- (run: { createdAt: number }) => run.createdAt
482
- );
483
- expect(timestamps).toEqual([...timestamps].sort((a, b) => b - a));
484
- });
485
-
486
- it('Watch endpoint streams running brains', async () => {
487
- const testEnv = env as TestEnv;
488
- const brainName = 'delayed-brain';
489
- const brainRuns: string[] = [];
490
-
491
- // Start 3 delayed brains
492
- for (let i = 0; i < 3; i++) {
493
- const createRequest = new Request('http://example.com/brains/runs', {
494
- method: 'POST',
495
- headers: { 'Content-Type': 'application/json' },
496
- body: JSON.stringify({ brainName }),
497
- });
498
- const createContext = createExecutionContext();
499
- const createResponse = await worker.fetch(
500
- createRequest,
501
- testEnv,
502
- createContext
503
- );
504
- const { brainRunId } = await createResponse.json<{
505
- brainRunId: string;
506
- }>();
507
- brainRuns.push(brainRunId);
508
- await waitOnExecutionContext(createContext);
509
- }
510
-
511
- // Connect to watch endpoint
512
- const watchRequest = new Request('http://example.com/brains/watch');
513
- const watchContext = createExecutionContext();
514
- const watchResponse = await worker.fetch(
515
- watchRequest,
516
- testEnv,
517
- watchContext
518
- );
519
- expect(watchResponse.status).toBe(200);
520
- expect(watchResponse.headers.get('Content-Type')).toContain(
521
- 'text/event-stream'
522
- );
523
-
524
- if (!watchResponse.body) {
525
- throw new Error('Watch response body is null');
526
- }
527
-
528
- // Read the SSE stream
529
- const events: any[] = [];
530
- const reader = watchResponse.body.getReader();
531
- const decoder = new TextDecoder();
532
- let buffer = '';
533
-
534
- // Helper to process SSE messages
535
- const processBuffer = () => {
536
- const messages = buffer.split('\n\n');
537
- buffer = messages.pop() || ''; // Keep the incomplete message in the buffer
538
-
539
- for (const message of messages) {
540
- if (message.startsWith('data: ')) {
541
- const data = JSON.parse(message.slice(6));
542
- events.push(data);
543
- }
544
- }
545
- };
546
-
547
- // Read for a while to capture brain completions
548
- const startTime = Date.now();
549
- const TIMEOUT = 5000; // 5 seconds should be enough for our test brains
550
-
551
- try {
552
- while (Date.now() - startTime < TIMEOUT) {
553
- const { value, done } = await reader.read();
554
- if (done) break;
555
-
556
- buffer += decoder.decode(value, { stream: true });
557
- processBuffer();
558
-
559
- // If we've seen all brains complete, we can stop early
560
- const lastEvent = events[events.length - 1];
561
- if (lastEvent?.runningBrains?.length === 0) {
562
- break;
563
- }
564
- }
565
- } finally {
566
- reader.cancel();
567
- }
568
-
569
- // Verify the events
570
- expect(events.length).toBeGreaterThan(0);
571
-
572
- // First event should show all brains running
573
- const initialState = events[0];
574
- expect(initialState.runningBrains).toBeDefined();
575
- expect(initialState.runningBrains.length).toBe(3);
576
- expect(
577
- initialState.runningBrains.every((w: any) => w.status === STATUS.RUNNING)
578
- ).toBe(true);
579
-
580
- // Last event should show no running brains
581
- const finalState = events[events.length - 1];
582
- expect(finalState.runningBrains).toBeDefined();
583
- expect(finalState.runningBrains.length).toBe(0);
584
-
585
- await waitOnExecutionContext(watchContext);
586
- });
587
-
588
- it('Loads resources from the resource manifest', async () => {
589
- const testEnv = env as TestEnv;
590
- const brainName = 'resource-brain';
591
-
592
- // First, set up test resources in R2
593
- // Create testResource
594
- await testEnv.RESOURCES_BUCKET.put(
595
- 'testResource.txt',
596
- 'This is a test resource',
597
- {
598
- customMetadata: {
599
- type: 'text',
600
- path: 'testResource.txt',
601
- },
602
- }
603
- );
604
-
605
- // Create testResourceBinary
606
- await testEnv.RESOURCES_BUCKET.put(
607
- 'testResourceBinary.bin',
608
- 'This is a test resource binary',
609
- {
610
- customMetadata: {
611
- type: 'binary',
612
- path: 'testResourceBinary.bin',
613
- },
614
- }
615
- );
616
-
617
- // Create nested resource
618
- await testEnv.RESOURCES_BUCKET.put(
619
- 'nestedResource/testNestedResource.txt',
620
- 'This is a test resource',
621
- {
622
- customMetadata: {
623
- type: 'text',
624
- path: 'nestedResource/testNestedResource.txt',
625
- },
626
- }
627
- );
628
-
629
- const createRequest = new Request('http://example.com/brains/runs', {
630
- method: 'POST',
631
- headers: { 'Content-Type': 'application/json' },
632
- body: JSON.stringify({ brainName }),
633
- });
634
- const createContext = createExecutionContext();
635
- const createResponse = await worker.fetch(
636
- createRequest,
637
- testEnv,
638
- createContext
639
- );
640
- const { brainRunId } = await createResponse.json<{
641
- brainRunId: string;
642
- }>();
643
- await waitOnExecutionContext(createContext);
644
-
645
- // Watch the brain run via SSE until completion
646
- const watchUrl = `http://example.com/brains/runs/${brainRunId}/watch`;
647
- const watchRequest = new Request(watchUrl);
648
- const watchContext = createExecutionContext();
649
- const watchResponse = await worker.fetch(
650
- watchRequest,
651
- testEnv,
652
- watchContext
653
- );
654
- expect(watchResponse.status).toBe(200); // Ensure watch connection is OK
655
- if (!watchResponse.body) {
656
- throw new Error('Watch response body is null');
657
- }
658
-
659
- // --- Read all events from the SSE stream ---
660
- const allEvents = await readSseStream(watchResponse.body);
661
- await waitOnExecutionContext(watchContext); // Wait for SSE stream processing and DOs to settle
662
-
663
- // --- Assertions on the collected events ---
664
-
665
- // Check for overall brain completion
666
- const completeEvent = allEvents.find(
667
- (e): e is BrainCompleteEvent => e.type === BRAIN_EVENTS.COMPLETE
668
- );
669
- expect(completeEvent).toBeDefined();
670
- expect(completeEvent?.status).toBe(STATUS.COMPLETE);
671
-
672
- // Find the step completion events
673
- const stepCompleteEvents = allEvents.filter(
674
- (e): e is StepCompletedEvent => e.type === BRAIN_EVENTS.STEP_COMPLETE
675
- );
676
-
677
- const loadTextStepCompleteEvent = stepCompleteEvents.find(
678
- (e) => e.stepTitle === 'Load text resource'
679
- );
680
- expect(loadTextStepCompleteEvent).toBeDefined();
681
- expect(loadTextStepCompleteEvent?.patch).toBeDefined();
682
-
683
- const loadBinaryStepCompleteEvent = stepCompleteEvents.find(
684
- (e) => e.stepTitle === 'Load binary resource'
685
- );
686
- expect(loadBinaryStepCompleteEvent).toBeDefined();
687
- expect(loadBinaryStepCompleteEvent?.patch).toBeDefined();
688
-
689
- // Expected resource content from packages/cloudflare/test-project/src/runner.ts
690
- const expectedTextContent = 'This is a test resource';
691
- const expectedBinaryContentRaw = 'This is a test resource binary';
692
- const expectedBinaryContentBase64 = Buffer.from(
693
- expectedBinaryContentRaw
694
- ).toString('base64');
695
-
696
- // Verify the patch from 'Load text resource' step
697
- // This patch is relative to the state *before* this step
698
- const textPatch = loadTextStepCompleteEvent!.patch;
699
- const addTextOp = textPatch.find(
700
- (op) => op.op === 'add' && op.path === '/text'
701
- );
702
- expect(addTextOp).toBeDefined();
703
- expect(addTextOp?.value).toBe(expectedTextContent);
704
-
705
- // Verify the patch from 'Load binary resource' step
706
- // This patch is relative to the state *after* 'Load text resource' step
707
- const binaryPatch = loadBinaryStepCompleteEvent!.patch;
708
- const addBufferOp = binaryPatch.find(
709
- (op) => op.op === 'add' && op.path === '/buffer'
710
- );
711
- expect(addBufferOp).toBeDefined();
712
- expect(addBufferOp?.value).toBe(expectedBinaryContentBase64);
713
-
714
- // Verify the patch from 'Load nested resource' step
715
- const loadNestedStepCompleteEvent = stepCompleteEvents.find(
716
- (e) => e.stepTitle === 'Load nested resource'
717
- );
718
- expect(loadNestedStepCompleteEvent).toBeDefined();
719
- expect(loadNestedStepCompleteEvent?.patch).toBeDefined();
720
- const nestedTextPatch = loadNestedStepCompleteEvent!.patch;
721
- const addNestedTextOp = nestedTextPatch.find(
722
- (op) => op.op === 'add' && op.path === '/nestedText'
723
- );
724
- expect(addNestedTextOp).toBeDefined();
725
- // The mock loader will return 'This is a test resource' for any text request.
726
- expect(addNestedTextOp?.value).toBe(expectedTextContent);
727
-
728
- // Check that the steps themselves are marked as completed in the final status
729
- const stepStatusEvents = allEvents.filter(
730
- (e): e is StepStatusEvent => e.type === BRAIN_EVENTS.STEP_STATUS
731
- );
732
- expect(stepStatusEvents.length).toBeGreaterThan(0);
733
- const lastStepStatusEvent = stepStatusEvents[stepStatusEvents.length - 1];
734
-
735
- expect(lastStepStatusEvent.steps.length).toBe(3); // resource-brain has 3 steps
736
- const textStepFinalStatus = lastStepStatusEvent.steps.find(
737
- (s) => s.title === 'Load text resource'
738
- );
739
- const binaryStepFinalStatus = lastStepStatusEvent.steps.find(
740
- (s) => s.title === 'Load binary resource'
741
- );
742
- const nestedStepFinalStatus = lastStepStatusEvent.steps.find(
743
- (s) => s.title === 'Load nested resource'
744
- );
745
-
746
- expect(textStepFinalStatus?.status).toBe(STATUS.COMPLETE);
747
- expect(binaryStepFinalStatus?.status).toBe(STATUS.COMPLETE);
748
- expect(nestedStepFinalStatus?.status).toBe(STATUS.COMPLETE);
749
-
750
- // Clean up test resources
751
- await testEnv.RESOURCES_BUCKET.delete('testResource.txt');
752
- await testEnv.RESOURCES_BUCKET.delete('testResourceBinary.bin');
753
- await testEnv.RESOURCES_BUCKET.delete(
754
- 'nestedResource/testNestedResource.txt'
755
- );
756
- });
757
-
758
- describe('Brain Schedules API Tests', () => {
759
- it('POST /brains/schedules creates a new schedule', async () => {
760
- const testEnv = env as TestEnv;
761
- const brainName = 'basic-brain';
762
- const cronExpression = '0 3 * * *'; // Daily at 3am
763
-
764
- const request = new Request('http://example.com/brains/schedules', {
765
- method: 'POST',
766
- headers: { 'Content-Type': 'application/json' },
767
- body: JSON.stringify({ brainName, cronExpression }),
768
- });
769
- const context = createExecutionContext();
770
- const response = await worker.fetch(request, testEnv, context);
771
- await waitOnExecutionContext(context);
772
-
773
- expect(response.status).toBe(201);
774
- const responseBody = await response.json<{
775
- id: string;
776
- brainName: string;
777
- cronExpression: string;
778
- enabled: boolean;
779
- createdAt: number;
780
- }>();
781
-
782
- expect(responseBody.id).toBeDefined();
783
- expect(responseBody.brainName).toBe(brainName);
784
- expect(responseBody.cronExpression).toBe(cronExpression);
785
- expect(responseBody.enabled).toBe(true);
786
- expect(responseBody.createdAt).toBeDefined();
787
- });
788
-
789
- it('GET /brains/schedules lists all schedules', async () => {
790
- const testEnv = env as TestEnv;
791
-
792
- // Create a few schedules first
793
- for (let i = 0; i < 3; i++) {
794
- const request = new Request('http://example.com/brains/schedules', {
795
- method: 'POST',
796
- headers: { 'Content-Type': 'application/json' },
797
- body: JSON.stringify({
798
- brainName: `brain-${i}`,
799
- cronExpression: `${i} * * * *`,
800
- }),
801
- });
802
- const context = createExecutionContext();
803
- await worker.fetch(request, testEnv, context);
804
- await waitOnExecutionContext(context);
805
- }
806
-
807
- // List schedules
808
- const listRequest = new Request('http://example.com/brains/schedules');
809
- const listContext = createExecutionContext();
810
- const listResponse = await worker.fetch(
811
- listRequest,
812
- testEnv,
813
- listContext
814
- );
815
- await waitOnExecutionContext(listContext);
816
-
817
- expect(listResponse.status).toBe(200);
818
- const responseBody = await listResponse.json<{
819
- schedules: Array<{
820
- id: string;
821
- brainName: string;
822
- cronExpression: string;
823
- enabled: boolean;
824
- createdAt: number;
825
- }>;
826
- count: number;
827
- }>();
828
-
829
- expect(responseBody.schedules).toBeInstanceOf(Array);
830
- expect(responseBody.count).toBeGreaterThanOrEqual(3);
831
- });
832
-
833
- it('DELETE /brains/schedules/:scheduleId deletes a schedule', async () => {
834
- const testEnv = env as TestEnv;
835
-
836
- // Create a schedule
837
- const createRequest = new Request('http://example.com/brains/schedules', {
838
- method: 'POST',
839
- headers: { 'Content-Type': 'application/json' },
840
- body: JSON.stringify({
841
- brainName: 'delete-brain',
842
- cronExpression: '0 0 * * *',
843
- }),
844
- });
845
- const createContext = createExecutionContext();
846
- const createResponse = await worker.fetch(
847
- createRequest,
848
- testEnv,
849
- createContext
850
- );
851
- const { id } = await createResponse.json<{ id: string }>();
852
- await waitOnExecutionContext(createContext);
853
-
854
- // Delete the schedule
855
- const deleteRequest = new Request(
856
- `http://example.com/brains/schedules/${id}`,
857
- {
858
- method: 'DELETE',
859
- }
860
- );
861
- const deleteContext = createExecutionContext();
862
- const deleteResponse = await worker.fetch(
863
- deleteRequest,
864
- testEnv,
865
- deleteContext
866
- );
867
- await waitOnExecutionContext(deleteContext);
868
-
869
- expect(deleteResponse.status).toBe(204);
870
-
871
- // Verify it's deleted
872
- const getRequest = new Request(
873
- `http://example.com/brains/schedules/${id}`
874
- );
875
- const getContext = createExecutionContext();
876
- const getResponse = await worker.fetch(getRequest, testEnv, getContext);
877
- await waitOnExecutionContext(getContext);
878
-
879
- expect(getResponse.status).toBe(404);
880
- });
881
-
882
- it('GET /brains/schedules/runs lists scheduled run history', async () => {
883
- const testEnv = env as TestEnv;
884
-
885
- const request = new Request('http://example.com/brains/schedules/runs');
886
- const context = createExecutionContext();
887
- const response = await worker.fetch(request, testEnv, context);
888
- await waitOnExecutionContext(context);
889
-
890
- expect(response.status).toBe(200);
891
- const responseBody = await response.json<{
892
- runs: Array<{
893
- id: number;
894
- scheduleId: string;
895
- brainRunId?: string;
896
- status: 'triggered' | 'failed';
897
- ranAt: number;
898
- }>;
899
- count: number;
900
- }>();
901
-
902
- expect(responseBody.runs).toBeInstanceOf(Array);
903
- expect(typeof responseBody.count).toBe('number');
904
- });
905
-
906
- it('GET /brains/schedules/runs with scheduleId filter', async () => {
907
- const testEnv = env as TestEnv;
908
- const scheduleId = 'test-schedule-123';
909
-
910
- const request = new Request(
911
- `http://example.com/brains/schedules/runs?scheduleId=${scheduleId}`
912
- );
913
- const context = createExecutionContext();
914
- const response = await worker.fetch(request, testEnv, context);
915
- await waitOnExecutionContext(context);
916
-
917
- expect(response.status).toBe(200);
918
- const responseBody = await response.json<{
919
- runs: Array<{
920
- id: number;
921
- scheduleId: string;
922
- brainRunId?: string;
923
- status: 'triggered' | 'failed';
924
- ranAt: number;
925
- }>;
926
- count: number;
927
- }>();
928
-
929
- // All runs should belong to the specified schedule
930
- for (const run of responseBody.runs) {
931
- expect(run.scheduleId).toBe(scheduleId);
932
- }
933
- });
934
-
935
- it('POST /brains/schedules validates cron expression', async () => {
936
- const testEnv = env as TestEnv;
937
-
938
- const request = new Request('http://example.com/brains/schedules', {
939
- method: 'POST',
940
- headers: { 'Content-Type': 'application/json' },
941
- body: JSON.stringify({
942
- brainName: 'invalid-cron-brain',
943
- cronExpression: 'invalid cron',
944
- }),
945
- });
946
- const context = createExecutionContext();
947
- const response = await worker.fetch(request, testEnv, context);
948
- await waitOnExecutionContext(context);
949
-
950
- expect(response.status).toBe(400);
951
- const error = (await response.json()) as { error: string };
952
- expect(error.error).toContain('Invalid cron expression');
953
- });
954
-
955
- it('POST /brains/schedules allows multiple schedules per brain', async () => {
956
- const testEnv = env as TestEnv;
957
- const brainName = 'multi-schedule-brain';
958
-
959
- // Create first schedule
960
- const request1 = new Request('http://example.com/brains/schedules', {
961
- method: 'POST',
962
- headers: { 'Content-Type': 'application/json' },
963
- body: JSON.stringify({
964
- brainName,
965
- cronExpression: '0 9 * * *', // 9am daily
966
- }),
967
- });
968
- const context1 = createExecutionContext();
969
- const response1 = await worker.fetch(request1, testEnv, context1);
970
- await waitOnExecutionContext(context1);
971
- expect(response1.status).toBe(201);
972
-
973
- // Create second schedule for same brain
974
- const request2 = new Request('http://example.com/brains/schedules', {
975
- method: 'POST',
976
- headers: { 'Content-Type': 'application/json' },
977
- body: JSON.stringify({
978
- brainName,
979
- cronExpression: '0 17 * * *', // 5pm daily
980
- }),
981
- });
982
- const context2 = createExecutionContext();
983
- const response2 = await worker.fetch(request2, testEnv, context2);
984
- await waitOnExecutionContext(context2);
985
- expect(response2.status).toBe(201);
986
-
987
- // Verify both schedules exist
988
- const listRequest = new Request('http://example.com/brains/schedules');
989
- const listContext = createExecutionContext();
990
- const listResponse = await worker.fetch(
991
- listRequest,
992
- testEnv,
993
- listContext
994
- );
995
- await waitOnExecutionContext(listContext);
996
-
997
- const { schedules } = await listResponse.json<{
998
- schedules: Array<{ brainName: string }>;
999
- }>();
1000
-
1001
- const multiSchedules = schedules.filter((s) => s.brainName === brainName);
1002
- expect(multiSchedules.length).toBe(2);
1003
- });
1004
- });
1005
- });