@positronic/spec 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/src/api.ts DELETED
@@ -1,1404 +0,0 @@
1
- type Fetch = (request: Request) => Promise<Response>;
2
-
3
- export async function testStatus(fetch: Fetch): Promise<boolean> {
4
- try {
5
- const request = new Request('http://example.com/status', {
6
- method: 'GET',
7
- });
8
-
9
- const response = await fetch(request);
10
-
11
- if (!response.ok) {
12
- console.error(`Status endpoint returned ${response.status}`);
13
- return false;
14
- }
15
-
16
- const data = await response.json() as { ready: boolean };
17
-
18
- if (data.ready !== true) {
19
- console.error(`Expected { ready: true }, got ${JSON.stringify(data)}`);
20
- return false;
21
- }
22
-
23
- return true;
24
- } catch (error) {
25
- console.error(`Failed to test status endpoint:`, error);
26
- return false;
27
- }
28
- }
29
-
30
- export const resources = {
31
- /**
32
- * Test GET /resources - List all resources
33
- */
34
- async list(fetch: Fetch): Promise<boolean> {
35
- try {
36
- const request = new Request('http://example.com/resources', {
37
- method: 'GET',
38
- });
39
-
40
- const response = await fetch(request);
41
-
42
- if (!response.ok) {
43
- console.error(`GET /resources returned ${response.status}`);
44
- return false;
45
- }
46
-
47
- const data = await response.json() as {
48
- resources: Array<{
49
- key: string;
50
- type: string;
51
- size: number;
52
- lastModified: string;
53
- local: boolean;
54
- }>;
55
- truncated: boolean;
56
- count: number;
57
- };
58
-
59
- // Validate response structure
60
- if (!Array.isArray(data.resources)) {
61
- console.error(
62
- `Expected resources to be an array, got ${typeof data.resources}`
63
- );
64
- return false;
65
- }
66
-
67
- if (typeof data.truncated !== 'boolean') {
68
- console.error(
69
- `Expected truncated to be boolean, got ${typeof data.truncated}`
70
- );
71
- return false;
72
- }
73
-
74
- if (typeof data.count !== 'number') {
75
- console.error(`Expected count to be number, got ${typeof data.count}`);
76
- return false;
77
- }
78
-
79
- // Validate each resource has required fields
80
- for (const resource of data.resources) {
81
- if (
82
- !resource.key ||
83
- !resource.type ||
84
- typeof resource.size !== 'number' ||
85
- !resource.lastModified ||
86
- typeof resource.local !== 'boolean'
87
- ) {
88
- console.error(
89
- `Resource missing required fields: ${JSON.stringify(resource)}`
90
- );
91
- return false;
92
- }
93
-
94
- if (!['text', 'binary'].includes(resource.type)) {
95
- console.error(`Invalid resource type: ${resource.type}`);
96
- return false;
97
- }
98
- }
99
-
100
- return true;
101
- } catch (error) {
102
- console.error(`Failed to test GET /resources:`, error);
103
- return false;
104
- }
105
- },
106
-
107
- /**
108
- * Test POST /resources - Upload a resource
109
- */
110
- async upload(fetch: Fetch): Promise<boolean> {
111
- try {
112
- const formData = new FormData();
113
- formData.append(
114
- 'file',
115
- new Blob(['test content'], { type: 'text/plain' }),
116
- 'test.txt'
117
- );
118
- formData.append('type', 'text');
119
- formData.append('key', 'test-resource.txt');
120
- formData.append('local', 'false');
121
-
122
- const request = new Request('http://example.com/resources', {
123
- method: 'POST',
124
- body: formData,
125
- });
126
-
127
- const response = await fetch(request);
128
-
129
- if (response.status !== 201) {
130
- console.error(
131
- `POST /resources returned ${response.status}, expected 201`
132
- );
133
- return false;
134
- }
135
-
136
- const data = await response.json() as {
137
- key: string;
138
- type: string;
139
- size: number;
140
- lastModified: string;
141
- local: boolean;
142
- };
143
-
144
- // Validate response has required fields
145
- if (
146
- !data.key ||
147
- !data.type ||
148
- typeof data.size !== 'number' ||
149
- !data.lastModified ||
150
- typeof data.local !== 'boolean'
151
- ) {
152
- console.error(
153
- `Response missing required fields: ${JSON.stringify(data)}`
154
- );
155
- return false;
156
- }
157
-
158
- if (data.key !== 'test-resource.txt') {
159
- console.error(
160
- `Expected key to be 'test-resource.txt', got ${data.key}`
161
- );
162
- return false;
163
- }
164
-
165
- if (data.type !== 'text') {
166
- console.error(`Expected type to be 'text', got ${data.type}`);
167
- return false;
168
- }
169
-
170
- if (data.local !== false) {
171
- console.error(`Expected local to be false, got ${data.local}`);
172
- return false;
173
- }
174
-
175
- return true;
176
- } catch (error) {
177
- console.error(`Failed to test POST /resources:`, error);
178
- return false;
179
- }
180
- },
181
-
182
- /**
183
- * Test DELETE /resources/:key - Delete a specific resource
184
- */
185
- async delete(fetch: Fetch, key: string): Promise<boolean> {
186
- try {
187
- const request = new Request(
188
- `http://example.com/resources/${encodeURIComponent(key)}`,
189
- {
190
- method: 'DELETE',
191
- }
192
- );
193
-
194
- const response = await fetch(request);
195
-
196
- if (response.status !== 204) {
197
- console.error(
198
- `DELETE /resources/${key} returned ${response.status}, expected 204`
199
- );
200
- return false;
201
- }
202
-
203
- return true;
204
- } catch (error) {
205
- console.error(`Failed to test DELETE /resources/${key}:`, error);
206
- return false;
207
- }
208
- },
209
-
210
- /**
211
- * Test DELETE /resources - Bulk delete all resources (dev mode only)
212
- */
213
- async deleteAll(fetch: Fetch): Promise<boolean> {
214
- try {
215
- const request = new Request('http://example.com/resources', {
216
- method: 'DELETE',
217
- });
218
-
219
- const response = await fetch(request);
220
-
221
- // In production mode, this should return 403
222
- if (response.status === 403) {
223
- const data = await response.json() as { error: string };
224
- if (
225
- data.error === 'Bulk delete is only available in development mode'
226
- ) {
227
- // This is expected behavior in production
228
- return true;
229
- }
230
- }
231
-
232
- if (response.status !== 200) {
233
- console.error(
234
- `DELETE /resources returned ${response.status}, expected 200 or 403`
235
- );
236
- return false;
237
- }
238
-
239
- const data = await response.json() as { deletedCount: number };
240
-
241
- if (typeof data.deletedCount !== 'number') {
242
- console.error(
243
- `Expected deletedCount to be number, got ${typeof data.deletedCount}`
244
- );
245
- return false;
246
- }
247
-
248
- return true;
249
- } catch (error) {
250
- console.error(`Failed to test DELETE /resources:`, error);
251
- return false;
252
- }
253
- },
254
-
255
- /**
256
- * Test POST /resources/presigned-link - Generate presigned URL for upload
257
- */
258
- async generatePresignedLink(fetch: Fetch): Promise<boolean> {
259
- try {
260
- const request = new Request(
261
- 'http://example.com/resources/presigned-link',
262
- {
263
- method: 'POST',
264
- headers: {
265
- 'Content-Type': 'application/json',
266
- },
267
- body: JSON.stringify({
268
- key: 'test-files/large-video.mp4',
269
- type: 'binary',
270
- size: 150 * 1024 * 1024, // 150MB - larger than Worker limit
271
- }),
272
- }
273
- );
274
-
275
- const response = await fetch(request);
276
-
277
- // If credentials are not configured, expect 400
278
- if (response.status === 400) {
279
- const data = await response.json() as { error?: string };
280
- // This is acceptable - implementation may not have credentials configured
281
- console.log(
282
- 'Presigned URL generation not available - this is acceptable'
283
- );
284
- return true;
285
- }
286
-
287
- if (response.status !== 200) {
288
- console.error(
289
- `POST /resources/presigned-link returned ${response.status}, expected 200 or 400`
290
- );
291
- return false;
292
- }
293
-
294
- const data = await response.json() as {
295
- url: string;
296
- method: string;
297
- expiresIn: number;
298
- };
299
-
300
- // Validate response structure (backend-agnostic)
301
- if (!data.url || typeof data.url !== 'string') {
302
- console.error(`Expected url to be string, got ${typeof data.url}`);
303
- return false;
304
- }
305
-
306
- if (!data.method || data.method !== 'PUT') {
307
- console.error(`Expected method to be 'PUT', got ${data.method}`);
308
- return false;
309
- }
310
-
311
- if (typeof data.expiresIn !== 'number' || data.expiresIn <= 0) {
312
- console.error(
313
- `Expected expiresIn to be positive number, got ${data.expiresIn}`
314
- );
315
- return false;
316
- }
317
-
318
- // Basic URL validation - just ensure it's a valid URL
319
- try {
320
- new URL(data.url);
321
- console.log('Presigned URL structure validated successfully');
322
- } catch (error) {
323
- console.error(`Invalid URL returned: ${data.url}`);
324
- return false;
325
- }
326
-
327
- return true;
328
- } catch (error) {
329
- console.error(`Failed to test POST /resources/presigned-link:`, error);
330
- return false;
331
- }
332
- },
333
- };
334
-
335
- export const brains = {
336
- /**
337
- * Test POST /brains/runs - Create a new brain run
338
- */
339
- async run(fetch: Fetch, brainName: string): Promise<string | null> {
340
- try {
341
- const request = new Request('http://example.com/brains/runs', {
342
- method: 'POST',
343
- headers: {
344
- 'Content-Type': 'application/json',
345
- },
346
- body: JSON.stringify({ brainName }),
347
- });
348
-
349
- const response = await fetch(request);
350
-
351
- if (response.status !== 201) {
352
- console.error(
353
- `POST /brains/runs returned ${response.status}, expected 201`
354
- );
355
- return null;
356
- }
357
-
358
- const data = await response.json() as { brainRunId: string };
359
-
360
- if (!data.brainRunId || typeof data.brainRunId !== 'string') {
361
- console.error(
362
- `Expected brainRunId to be string, got ${typeof data.brainRunId}`
363
- );
364
- return null;
365
- }
366
-
367
- return data.brainRunId;
368
- } catch (error) {
369
- console.error(`Failed to test POST /brains/runs:`, error);
370
- return null;
371
- }
372
- },
373
-
374
- /**
375
- * Test POST /brains/runs with non-existent brain - Should return 404
376
- */
377
- async runNotFound(
378
- fetch: Fetch,
379
- nonExistentBrainName: string
380
- ): Promise<boolean> {
381
- try {
382
- const request = new Request('http://example.com/brains/runs', {
383
- method: 'POST',
384
- headers: {
385
- 'Content-Type': 'application/json',
386
- },
387
- body: JSON.stringify({ brainName: nonExistentBrainName }),
388
- });
389
-
390
- const response = await fetch(request);
391
-
392
- if (response.status !== 404) {
393
- console.error(
394
- `POST /brains/runs with non-existent brain returned ${response.status}, expected 404`
395
- );
396
- return false;
397
- }
398
-
399
- const data = await response.json() as { error: string };
400
-
401
- if (!data.error || typeof data.error !== 'string') {
402
- console.error(`Expected error to be string, got ${typeof data.error}`);
403
- return false;
404
- }
405
-
406
- // Check that the error message mentions the brain name
407
- if (!data.error.includes(nonExistentBrainName)) {
408
- console.error(
409
- `Expected error to mention brain name '${nonExistentBrainName}', got: ${data.error}`
410
- );
411
- return false;
412
- }
413
-
414
- // Check that the error message follows expected format
415
- const expectedPattern = new RegExp(
416
- `Brain '${nonExistentBrainName}' not found`
417
- );
418
- if (!expectedPattern.test(data.error)) {
419
- console.error(
420
- `Expected error message to match pattern "Brain '${nonExistentBrainName}' not found", got: ${data.error}`
421
- );
422
- return false;
423
- }
424
-
425
- return true;
426
- } catch (error) {
427
- console.error(
428
- `Failed to test POST /brains/runs with non-existent brain:`,
429
- error
430
- );
431
- return false;
432
- }
433
- },
434
-
435
- /**
436
- * Test GET /brains/runs/:runId/watch - Watch a brain run via SSE
437
- */
438
- async watch(fetch: Fetch, runId: string): Promise<boolean> {
439
- try {
440
- const request = new Request(
441
- `http://example.com/brains/runs/${runId}/watch`,
442
- {
443
- method: 'GET',
444
- }
445
- );
446
-
447
- const response = await fetch(request);
448
-
449
- if (!response.ok) {
450
- console.error(
451
- `GET /brains/runs/${runId}/watch returned ${response.status}`
452
- );
453
- return false;
454
- }
455
-
456
- // Check that it's an event stream
457
- const contentType = response.headers.get('content-type');
458
- if (!contentType || !contentType.includes('text/event-stream')) {
459
- console.error(
460
- `Expected content-type to be text/event-stream, got ${contentType}`
461
- );
462
- return false;
463
- }
464
-
465
- // Read a bit of the stream to verify it's actually SSE format
466
- if (response.body) {
467
- const reader = response.body.getReader();
468
- try {
469
- // Read first chunk
470
- const { value } = await reader.read();
471
- if (value) {
472
- const text = new TextDecoder().decode(value);
473
- // SSE data should contain "data: " lines
474
- if (!text.includes('data: ')) {
475
- console.error(
476
- `Expected SSE format with "data: " prefix, got: ${text.substring(
477
- 0,
478
- 100
479
- )}`
480
- );
481
- return false;
482
- }
483
- }
484
- } finally {
485
- // Always cancel the reader to clean up
486
- await reader.cancel();
487
- }
488
- }
489
-
490
- return true;
491
- } catch (error) {
492
- console.error(`Failed to test GET /brains/runs/${runId}/watch:`, error);
493
- return false;
494
- }
495
- },
496
-
497
- /**
498
- * Test GET /brains/:brainName/history - Get history of brain runs
499
- */
500
- async history(
501
- fetch: Fetch,
502
- brainName: string,
503
- limit?: number
504
- ): Promise<boolean> {
505
- try {
506
- const url = new URL(
507
- 'http://example.com/brains/' + brainName + '/history'
508
- );
509
- if (limit !== undefined) {
510
- url.searchParams.set('limit', limit.toString());
511
- }
512
-
513
- const request = new Request(url.toString(), {
514
- method: 'GET',
515
- });
516
-
517
- const response = await fetch(request);
518
-
519
- if (!response.ok) {
520
- console.error(
521
- `GET /brains/${brainName}/history returned ${response.status}`
522
- );
523
- return false;
524
- }
525
-
526
- const data = await response.json() as {
527
- runs: Array<{
528
- brainRunId: string;
529
- brainTitle: string;
530
- type: string;
531
- status: string;
532
- createdAt: number;
533
- }>;
534
- };
535
-
536
- // Validate response structure
537
- if (!data.runs || !Array.isArray(data.runs)) {
538
- console.error(`Expected runs to be an array, got ${typeof data.runs}`);
539
- return false;
540
- }
541
-
542
- // Validate each run has required fields
543
- for (const run of data.runs) {
544
- if (
545
- !run.brainRunId ||
546
- !run.brainTitle ||
547
- !run.type ||
548
- !run.status ||
549
- typeof run.createdAt !== 'number'
550
- ) {
551
- console.error(`Run missing required fields: ${JSON.stringify(run)}`);
552
- return false;
553
- }
554
- }
555
-
556
- return true;
557
- } catch (error) {
558
- console.error(`Failed to test GET /brains/${brainName}/history:`, error);
559
- return false;
560
- }
561
- },
562
-
563
- /**
564
- * Test GET /brains/watch - Watch all running brains
565
- */
566
- async watchAll(fetch: Fetch): Promise<boolean> {
567
- try {
568
- const request = new Request('http://example.com/brains/watch', {
569
- method: 'GET',
570
- });
571
-
572
- const response = await fetch(request);
573
-
574
- if (!response.ok) {
575
- console.error(`GET /brains/watch returned ${response.status}`);
576
- return false;
577
- }
578
-
579
- // Check that it's an event stream
580
- const contentType = response.headers.get('content-type');
581
- if (!contentType || !contentType.includes('text/event-stream')) {
582
- console.error(
583
- `Expected content-type to be text/event-stream, got ${contentType}`
584
- );
585
- return false;
586
- }
587
-
588
- // Read a bit of the stream to verify it's actually SSE format
589
- if (response.body) {
590
- const reader = response.body.getReader();
591
- try {
592
- // Read first chunk
593
- const { value } = await reader.read();
594
- if (value) {
595
- const text = new TextDecoder().decode(value);
596
- // SSE data should contain "data: " lines
597
- if (!text.includes('data: ')) {
598
- console.error(
599
- `Expected SSE format with "data: " prefix, got: ${text.substring(
600
- 0,
601
- 100
602
- )}`
603
- );
604
- return false;
605
- }
606
- }
607
- } finally {
608
- // Always cancel the reader to clean up
609
- await reader.cancel();
610
- }
611
- }
612
-
613
- return true;
614
- } catch (error) {
615
- console.error(`Failed to test GET /brains/watch:`, error);
616
- return false;
617
- }
618
- },
619
-
620
- /**
621
- * Test GET /brains - List all brains
622
- */
623
- async list(fetch: Fetch): Promise<boolean> {
624
- try {
625
- const request = new Request('http://example.com/brains', {
626
- method: 'GET',
627
- });
628
-
629
- const response = await fetch(request);
630
-
631
- if (!response.ok) {
632
- console.error(`GET /brains returned ${response.status}`);
633
- return false;
634
- }
635
-
636
- const data = await response.json() as {
637
- brains: Array<{
638
- name: string;
639
- title: string;
640
- description: string;
641
- }>;
642
- count: number;
643
- };
644
-
645
- // Validate response structure
646
- if (!Array.isArray(data.brains)) {
647
- console.error(
648
- `Expected brains to be an array, got ${typeof data.brains}`
649
- );
650
- return false;
651
- }
652
-
653
- if (typeof data.count !== 'number') {
654
- console.error(`Expected count to be number, got ${typeof data.count}`);
655
- return false;
656
- }
657
-
658
- // Validate each brain has required fields
659
- for (const brain of data.brains) {
660
- if (
661
- !brain.name ||
662
- typeof brain.name !== 'string' ||
663
- !brain.title ||
664
- typeof brain.title !== 'string' ||
665
- !brain.description ||
666
- typeof brain.description !== 'string'
667
- ) {
668
- console.error(
669
- `Brain missing required fields or has invalid types: ${JSON.stringify(
670
- brain
671
- )}`
672
- );
673
- return false;
674
- }
675
- }
676
-
677
- return true;
678
- } catch (error) {
679
- console.error(`Failed to test GET /brains:`, error);
680
- return false;
681
- }
682
- },
683
-
684
- /**
685
- * Test GET /brains/:brainName - Get brain structure
686
- */
687
- async show(fetch: Fetch, brainName: string): Promise<boolean> {
688
- try {
689
- const request = new Request(
690
- `http://example.com/brains/${encodeURIComponent(brainName)}`,
691
- {
692
- method: 'GET',
693
- }
694
- );
695
-
696
- const response = await fetch(request);
697
-
698
- if (!response.ok) {
699
- console.error(`GET /brains/${brainName} returned ${response.status}`);
700
- return false;
701
- }
702
-
703
- const data = await response.json() as {
704
- name: string;
705
- title: string;
706
- steps: Array<{
707
- type: string;
708
- title: string;
709
- innerBrain?: {
710
- title: string;
711
- steps: any[];
712
- };
713
- }>;
714
- };
715
-
716
- // Validate response structure
717
- if (!data.name || typeof data.name !== 'string') {
718
- console.error(`Expected name to be string, got ${typeof data.name}`);
719
- return false;
720
- }
721
-
722
- if (!data.title || typeof data.title !== 'string') {
723
- console.error(`Expected title to be string, got ${typeof data.title}`);
724
- return false;
725
- }
726
-
727
- if (!Array.isArray(data.steps)) {
728
- console.error(
729
- `Expected steps to be an array, got ${typeof data.steps}`
730
- );
731
- return false;
732
- }
733
-
734
- // Validate each step
735
- for (const step of data.steps) {
736
- if (!step.type || !['step', 'brain'].includes(step.type)) {
737
- console.error(`Invalid step type: ${step.type}`);
738
- return false;
739
- }
740
-
741
- if (!step.title || typeof step.title !== 'string') {
742
- console.error(
743
- `Step missing title or has invalid type: ${JSON.stringify(step)}`
744
- );
745
- return false;
746
- }
747
-
748
- // If it's a brain step, validate the inner brain recursively
749
- if (step.type === 'brain' && step.innerBrain) {
750
- if (
751
- !step.innerBrain.title ||
752
- typeof step.innerBrain.title !== 'string'
753
- ) {
754
- console.error(
755
- `Inner brain missing title: ${JSON.stringify(step.innerBrain)}`
756
- );
757
- return false;
758
- }
759
-
760
- if (!Array.isArray(step.innerBrain.steps)) {
761
- console.error(
762
- `Inner brain missing steps array: ${JSON.stringify(
763
- step.innerBrain
764
- )}`
765
- );
766
- return false;
767
- }
768
- }
769
- }
770
-
771
- return true;
772
- } catch (error) {
773
- console.error(`Failed to test GET /brains/${brainName}:`, error);
774
- return false;
775
- }
776
- },
777
-
778
- /**
779
- * Test GET /brains/:brainName/active-runs - Get active/running brain runs
780
- */
781
- async activeRuns(fetch: Fetch, brainName: string): Promise<boolean> {
782
- try {
783
- const request = new Request(
784
- `http://example.com/brains/${encodeURIComponent(
785
- brainName
786
- )}/active-runs`,
787
- {
788
- method: 'GET',
789
- }
790
- );
791
-
792
- const response = await fetch(request);
793
-
794
- if (!response.ok) {
795
- console.error(
796
- `GET /brains/${brainName}/active-runs returned ${response.status}`
797
- );
798
- return false;
799
- }
800
-
801
- const data = await response.json() as {
802
- runs: Array<{
803
- brainRunId: string;
804
- brainTitle: string;
805
- type: string;
806
- status: string;
807
- createdAt: number;
808
- }>;
809
- };
810
-
811
- // Validate response structure
812
- if (!data.runs || !Array.isArray(data.runs)) {
813
- console.error(`Expected runs to be an array, got ${typeof data.runs}`);
814
- return false;
815
- }
816
-
817
- // Validate each run has required fields
818
- for (const run of data.runs) {
819
- if (
820
- !run.brainRunId ||
821
- !run.brainTitle ||
822
- !run.type ||
823
- !run.status ||
824
- typeof run.createdAt !== 'number'
825
- ) {
826
- console.error(
827
- `Active run missing required fields: ${JSON.stringify(run)}`
828
- );
829
- return false;
830
- }
831
-
832
- // All active runs should have status 'RUNNING'
833
- if (run.status !== 'RUNNING') {
834
- console.error(
835
- `Expected active run status to be 'RUNNING', got ${run.status}`
836
- );
837
- return false;
838
- }
839
- }
840
-
841
- return true;
842
- } catch (error) {
843
- console.error(
844
- `Failed to test GET /brains/${brainName}/active-runs:`,
845
- error
846
- );
847
- return false;
848
- }
849
- },
850
-
851
- /**
852
- * Test POST /brains/runs/rerun - Rerun an existing brain run
853
- */
854
- async rerun(
855
- fetch: Fetch,
856
- brainName: string,
857
- runId?: string,
858
- startsAt?: number,
859
- stopsAfter?: number
860
- ): Promise<string | null> {
861
- try {
862
- const body: any = { brainName };
863
- if (runId) body.runId = runId;
864
- if (startsAt !== undefined) body.startsAt = startsAt;
865
- if (stopsAfter !== undefined) body.stopsAfter = stopsAfter;
866
-
867
- const request = new Request('http://example.com/brains/runs/rerun', {
868
- method: 'POST',
869
- headers: {
870
- 'Content-Type': 'application/json',
871
- },
872
- body: JSON.stringify(body),
873
- });
874
-
875
- const response = await fetch(request);
876
-
877
- if (response.status !== 201) {
878
- console.error(
879
- `POST /brains/runs/rerun returned ${response.status}, expected 201`
880
- );
881
- return null;
882
- }
883
-
884
- const data = await response.json() as { brainRunId: string };
885
-
886
- if (!data.brainRunId || typeof data.brainRunId !== 'string') {
887
- console.error(
888
- `Expected brainRunId to be string, got ${typeof data.brainRunId}`
889
- );
890
- return null;
891
- }
892
-
893
- return data.brainRunId;
894
- } catch (error) {
895
- console.error(`Failed to test POST /brains/runs/rerun:`, error);
896
- return null;
897
- }
898
- },
899
- };
900
-
901
- export const schedules = {
902
- /**
903
- * Test POST /brains/schedules - Create a new schedule
904
- */
905
- async create(
906
- fetch: Fetch,
907
- brainName: string,
908
- cronExpression: string
909
- ): Promise<string | null> {
910
- try {
911
- const request = new Request('http://example.com/brains/schedules', {
912
- method: 'POST',
913
- headers: {
914
- 'Content-Type': 'application/json',
915
- },
916
- body: JSON.stringify({ brainName, cronExpression }),
917
- });
918
-
919
- const response = await fetch(request);
920
-
921
- if (response.status !== 201) {
922
- console.error(
923
- `POST /brains/schedules returned ${response.status}, expected 201`
924
- );
925
- return null;
926
- }
927
-
928
- const data = await response.json() as {
929
- id: string;
930
- brainName: string;
931
- cronExpression: string;
932
- enabled: boolean;
933
- createdAt: number;
934
- };
935
-
936
- // Validate response structure
937
- if (!data.id || typeof data.id !== 'string') {
938
- console.error(`Expected id to be string, got ${typeof data.id}`);
939
- return null;
940
- }
941
-
942
- if (data.brainName !== brainName) {
943
- console.error(
944
- `Expected brainName to be '${brainName}', got ${data.brainName}`
945
- );
946
- return null;
947
- }
948
-
949
- if (data.cronExpression !== cronExpression) {
950
- console.error(
951
- `Expected cronExpression to be '${cronExpression}', got ${data.cronExpression}`
952
- );
953
- return null;
954
- }
955
-
956
- if (typeof data.enabled !== 'boolean') {
957
- console.error(
958
- `Expected enabled to be boolean, got ${typeof data.enabled}`
959
- );
960
- return null;
961
- }
962
-
963
- if (typeof data.createdAt !== 'number') {
964
- console.error(
965
- `Expected createdAt to be number, got ${typeof data.createdAt}`
966
- );
967
- return null;
968
- }
969
-
970
- return data.id;
971
- } catch (error) {
972
- console.error(`Failed to test POST /brains/schedules:`, error);
973
- return null;
974
- }
975
- },
976
-
977
- /**
978
- * Test GET /brains/schedules - List all schedules
979
- */
980
- async list(fetch: Fetch): Promise<boolean> {
981
- try {
982
- const request = new Request('http://example.com/brains/schedules', {
983
- method: 'GET',
984
- });
985
-
986
- const response = await fetch(request);
987
-
988
- if (!response.ok) {
989
- console.error(`GET /brains/schedules returned ${response.status}`);
990
- return false;
991
- }
992
-
993
- const data = await response.json() as {
994
- schedules: Array<{
995
- id: string;
996
- brainName: string;
997
- cronExpression: string;
998
- enabled: boolean;
999
- createdAt: number;
1000
- }>;
1001
- count: number;
1002
- };
1003
-
1004
- // Validate response structure
1005
- if (!Array.isArray(data.schedules)) {
1006
- console.error(
1007
- `Expected schedules to be an array, got ${typeof data.schedules}`
1008
- );
1009
- return false;
1010
- }
1011
-
1012
- if (typeof data.count !== 'number') {
1013
- console.error(`Expected count to be number, got ${typeof data.count}`);
1014
- return false;
1015
- }
1016
-
1017
- // Validate each schedule has required fields
1018
- for (const schedule of data.schedules) {
1019
- if (
1020
- !schedule.id ||
1021
- !schedule.brainName ||
1022
- !schedule.cronExpression ||
1023
- typeof schedule.enabled !== 'boolean' ||
1024
- typeof schedule.createdAt !== 'number'
1025
- ) {
1026
- console.error(
1027
- `Schedule missing required fields: ${JSON.stringify(schedule)}`
1028
- );
1029
- return false;
1030
- }
1031
- }
1032
-
1033
- return true;
1034
- } catch (error) {
1035
- console.error(`Failed to test GET /brains/schedules:`, error);
1036
- return false;
1037
- }
1038
- },
1039
-
1040
- /**
1041
- * Test DELETE /brains/schedules/:scheduleId - Delete a schedule
1042
- */
1043
- async delete(fetch: Fetch, scheduleId: string): Promise<boolean> {
1044
- try {
1045
- const request = new Request(
1046
- `http://example.com/brains/schedules/${scheduleId}`,
1047
- {
1048
- method: 'DELETE',
1049
- }
1050
- );
1051
-
1052
- const response = await fetch(request);
1053
-
1054
- if (response.status !== 204) {
1055
- console.error(
1056
- `DELETE /brains/schedules/${scheduleId} returned ${response.status}, expected 204`
1057
- );
1058
- return false;
1059
- }
1060
-
1061
- return true;
1062
- } catch (error) {
1063
- console.error(
1064
- `Failed to test DELETE /brains/schedules/${scheduleId}:`,
1065
- error
1066
- );
1067
- return false;
1068
- }
1069
- },
1070
-
1071
- /**
1072
- * Test GET /brains/schedules/runs - Get history of scheduled runs
1073
- */
1074
- async runs(
1075
- fetch: Fetch,
1076
- scheduleId?: string,
1077
- limit?: number
1078
- ): Promise<boolean> {
1079
- try {
1080
- const url = new URL('http://example.com/brains/schedules/runs');
1081
- if (scheduleId !== undefined) {
1082
- url.searchParams.set('scheduleId', scheduleId);
1083
- }
1084
- if (limit !== undefined) {
1085
- url.searchParams.set('limit', limit.toString());
1086
- }
1087
-
1088
- const request = new Request(url.toString(), {
1089
- method: 'GET',
1090
- });
1091
-
1092
- const response = await fetch(request);
1093
-
1094
- if (!response.ok) {
1095
- console.error(`GET /brains/schedules/runs returned ${response.status}`);
1096
- return false;
1097
- }
1098
-
1099
- const data = await response.json() as {
1100
- runs: Array<{
1101
- id: string;
1102
- scheduleId: string;
1103
- status: string;
1104
- ranAt: number;
1105
- }>;
1106
- count: number;
1107
- };
1108
-
1109
- // Validate response structure
1110
- if (!Array.isArray(data.runs)) {
1111
- console.error(`Expected runs to be an array, got ${typeof data.runs}`);
1112
- return false;
1113
- }
1114
-
1115
- if (typeof data.count !== 'number') {
1116
- console.error(`Expected count to be number, got ${typeof data.count}`);
1117
- return false;
1118
- }
1119
-
1120
- // Validate each run has required fields
1121
- for (const run of data.runs) {
1122
- if (
1123
- !run.id ||
1124
- !run.scheduleId ||
1125
- !run.status ||
1126
- typeof run.ranAt !== 'number'
1127
- ) {
1128
- console.error(
1129
- `Scheduled run missing required fields: ${JSON.stringify(run)}`
1130
- );
1131
- return false;
1132
- }
1133
-
1134
- if (!['triggered', 'failed'].includes(run.status)) {
1135
- console.error(`Invalid run status: ${run.status}`);
1136
- return false;
1137
- }
1138
- }
1139
-
1140
- return true;
1141
- } catch (error) {
1142
- console.error(`Failed to test GET /brains/schedules/runs:`, error);
1143
- return false;
1144
- }
1145
- },
1146
- };
1147
-
1148
- export const secrets = {
1149
- /**
1150
- * Test POST /secrets - Create or update a secret
1151
- */
1152
- async create(fetch: Fetch, name: string, value: string): Promise<boolean> {
1153
- try {
1154
- const request = new Request('http://example.com/secrets', {
1155
- method: 'POST',
1156
- headers: {
1157
- 'Content-Type': 'application/json',
1158
- },
1159
- body: JSON.stringify({ name, value }),
1160
- });
1161
-
1162
- const response = await fetch(request);
1163
-
1164
- if (response.status !== 201) {
1165
- console.error(
1166
- `POST /secrets returned ${response.status}, expected 201`
1167
- );
1168
- return false;
1169
- }
1170
-
1171
- const data = await response.json() as {
1172
- name: string;
1173
- createdAt: string;
1174
- updatedAt: string;
1175
- };
1176
-
1177
- // Validate response structure
1178
- if (!data.name || typeof data.name !== 'string') {
1179
- console.error(`Expected name to be string, got ${typeof data.name}`);
1180
- return false;
1181
- }
1182
-
1183
- if (data.name !== name) {
1184
- console.error(`Expected name to be '${name}', got ${data.name}`);
1185
- return false;
1186
- }
1187
-
1188
- if (typeof data.createdAt !== 'string') {
1189
- console.error(
1190
- `Expected createdAt to be string, got ${typeof data.createdAt}`
1191
- );
1192
- return false;
1193
- }
1194
-
1195
- if (typeof data.updatedAt !== 'string') {
1196
- console.error(
1197
- `Expected updatedAt to be string, got ${typeof data.updatedAt}`
1198
- );
1199
- return false;
1200
- }
1201
-
1202
- return true;
1203
- } catch (error) {
1204
- console.error(`Failed to test POST /secrets:`, error);
1205
- return false;
1206
- }
1207
- },
1208
-
1209
- /**
1210
- * Test GET /secrets - List all secrets (names only, not values)
1211
- */
1212
- async list(fetch: Fetch): Promise<boolean> {
1213
- try {
1214
- const request = new Request('http://example.com/secrets', {
1215
- method: 'GET',
1216
- });
1217
-
1218
- const response = await fetch(request);
1219
-
1220
- if (!response.ok) {
1221
- console.error(`GET /secrets returned ${response.status}`);
1222
- return false;
1223
- }
1224
-
1225
- const data = await response.json() as {
1226
- secrets: Array<{
1227
- name: string;
1228
- createdAt: string;
1229
- updatedAt: string;
1230
- }>;
1231
- count: number;
1232
- };
1233
-
1234
- // Validate response structure
1235
- if (!Array.isArray(data.secrets)) {
1236
- console.error(
1237
- `Expected secrets to be an array, got ${typeof data.secrets}`
1238
- );
1239
- return false;
1240
- }
1241
-
1242
- if (typeof data.count !== 'number') {
1243
- console.error(`Expected count to be number, got ${typeof data.count}`);
1244
- return false;
1245
- }
1246
-
1247
- // Validate each secret has required fields (but NOT the value)
1248
- for (const secret of data.secrets) {
1249
- if (
1250
- !secret.name ||
1251
- typeof secret.name !== 'string' ||
1252
- typeof secret.createdAt !== 'string' ||
1253
- typeof secret.updatedAt !== 'string'
1254
- ) {
1255
- console.error(
1256
- `Secret missing required fields: ${JSON.stringify(secret)}`
1257
- );
1258
- return false;
1259
- }
1260
-
1261
- // Ensure value is NOT included
1262
- if ('value' in secret) {
1263
- console.error(
1264
- `Secret should not include value field: ${JSON.stringify(secret)}`
1265
- );
1266
- return false;
1267
- }
1268
- }
1269
-
1270
- return true;
1271
- } catch (error) {
1272
- console.error(`Failed to test GET /secrets:`, error);
1273
- return false;
1274
- }
1275
- },
1276
-
1277
- /**
1278
- * Test DELETE /secrets/:name - Delete a specific secret
1279
- */
1280
- async delete(fetch: Fetch, name: string): Promise<boolean> {
1281
- try {
1282
- const request = new Request(
1283
- `http://example.com/secrets/${encodeURIComponent(name)}`,
1284
- {
1285
- method: 'DELETE',
1286
- }
1287
- );
1288
-
1289
- const response = await fetch(request);
1290
-
1291
- if (response.status !== 204) {
1292
- console.error(
1293
- `DELETE /secrets/${name} returned ${response.status}, expected 204`
1294
- );
1295
- return false;
1296
- }
1297
-
1298
- return true;
1299
- } catch (error) {
1300
- console.error(`Failed to test DELETE /secrets/${name}:`, error);
1301
- return false;
1302
- }
1303
- },
1304
-
1305
- /**
1306
- * Test GET /secrets/:name/exists - Check if a secret exists
1307
- */
1308
- async exists(fetch: Fetch, name: string): Promise<boolean> {
1309
- try {
1310
- const request = new Request(
1311
- `http://example.com/secrets/${encodeURIComponent(name)}/exists`,
1312
- {
1313
- method: 'GET',
1314
- }
1315
- );
1316
-
1317
- const response = await fetch(request);
1318
-
1319
- if (!response.ok) {
1320
- console.error(
1321
- `GET /secrets/${name}/exists returned ${response.status}`
1322
- );
1323
- return false;
1324
- }
1325
-
1326
- const data = await response.json() as { exists: boolean };
1327
-
1328
- // Validate response structure
1329
- if (typeof data.exists !== 'boolean') {
1330
- console.error(
1331
- `Expected exists to be boolean, got ${typeof data.exists}`
1332
- );
1333
- return false;
1334
- }
1335
-
1336
- return true;
1337
- } catch (error) {
1338
- console.error(`Failed to test GET /secrets/${name}/exists:`, error);
1339
- return false;
1340
- }
1341
- },
1342
-
1343
- /**
1344
- * Test POST /secrets/bulk - Create multiple secrets
1345
- */
1346
- async bulk(
1347
- fetch: Fetch,
1348
- secrets: Array<{ name: string; value: string }>
1349
- ): Promise<boolean> {
1350
- try {
1351
- const request = new Request('http://example.com/secrets/bulk', {
1352
- method: 'POST',
1353
- headers: {
1354
- 'Content-Type': 'application/json',
1355
- },
1356
- body: JSON.stringify({ secrets }),
1357
- });
1358
-
1359
- const response = await fetch(request);
1360
-
1361
- if (response.status !== 201) {
1362
- console.error(
1363
- `POST /secrets/bulk returned ${response.status}, expected 201`
1364
- );
1365
- return false;
1366
- }
1367
-
1368
- const data = await response.json() as {
1369
- created: number;
1370
- updated: number;
1371
- };
1372
-
1373
- // Validate response structure
1374
- if (typeof data.created !== 'number') {
1375
- console.error(
1376
- `Expected created to be number, got ${typeof data.created}`
1377
- );
1378
- return false;
1379
- }
1380
-
1381
- if (typeof data.updated !== 'number') {
1382
- console.error(
1383
- `Expected updated to be number, got ${typeof data.updated}`
1384
- );
1385
- return false;
1386
- }
1387
-
1388
- // Total should match input
1389
- if (data.created + data.updated !== secrets.length) {
1390
- console.error(
1391
- `Expected total (${
1392
- data.created + data.updated
1393
- }) to match input length (${secrets.length})`
1394
- );
1395
- return false;
1396
- }
1397
-
1398
- return true;
1399
- } catch (error) {
1400
- console.error(`Failed to test POST /secrets/bulk:`, error);
1401
- return false;
1402
- }
1403
- },
1404
- };