@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/dist/api.d.ts +107 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +948 -0
- package/dist/api.js.map +1 -0
- package/dist/index.d.ts +123 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/src/api.js +2246 -0
- package/dist/src/index.js +16 -0
- package/package.json +5 -1
- package/src/api.ts +0 -1404
- package/src/index.ts +0 -136
- package/tsconfig.json +0 -11
package/dist/api.js
ADDED
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
export async function testStatus(fetch) {
|
|
2
|
+
try {
|
|
3
|
+
const request = new Request('http://example.com/status', {
|
|
4
|
+
method: 'GET',
|
|
5
|
+
});
|
|
6
|
+
const response = await fetch(request);
|
|
7
|
+
if (!response.ok) {
|
|
8
|
+
console.error(`Status endpoint returned ${response.status}`);
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
const data = await response.json();
|
|
12
|
+
if (data.ready !== true) {
|
|
13
|
+
console.error(`Expected { ready: true }, got ${JSON.stringify(data)}`);
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
console.error(`Failed to test status endpoint:`, error);
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export const resources = {
|
|
24
|
+
/**
|
|
25
|
+
* Test GET /resources - List all resources
|
|
26
|
+
*/
|
|
27
|
+
async list(fetch) {
|
|
28
|
+
try {
|
|
29
|
+
const request = new Request('http://example.com/resources', {
|
|
30
|
+
method: 'GET',
|
|
31
|
+
});
|
|
32
|
+
const response = await fetch(request);
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
console.error(`GET /resources returned ${response.status}`);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
const data = await response.json();
|
|
38
|
+
// Validate response structure
|
|
39
|
+
if (!Array.isArray(data.resources)) {
|
|
40
|
+
console.error(`Expected resources to be an array, got ${typeof data.resources}`);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (typeof data.truncated !== 'boolean') {
|
|
44
|
+
console.error(`Expected truncated to be boolean, got ${typeof data.truncated}`);
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if (typeof data.count !== 'number') {
|
|
48
|
+
console.error(`Expected count to be number, got ${typeof data.count}`);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
// Validate each resource has required fields
|
|
52
|
+
for (const resource of data.resources) {
|
|
53
|
+
if (!resource.key ||
|
|
54
|
+
!resource.type ||
|
|
55
|
+
typeof resource.size !== 'number' ||
|
|
56
|
+
!resource.lastModified ||
|
|
57
|
+
typeof resource.local !== 'boolean') {
|
|
58
|
+
console.error(`Resource missing required fields: ${JSON.stringify(resource)}`);
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (!['text', 'binary'].includes(resource.type)) {
|
|
62
|
+
console.error(`Invalid resource type: ${resource.type}`);
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
console.error(`Failed to test GET /resources:`, error);
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
/**
|
|
74
|
+
* Test POST /resources - Upload a resource
|
|
75
|
+
*/
|
|
76
|
+
async upload(fetch) {
|
|
77
|
+
try {
|
|
78
|
+
const formData = new FormData();
|
|
79
|
+
formData.append('file', new Blob(['test content'], { type: 'text/plain' }), 'test.txt');
|
|
80
|
+
formData.append('type', 'text');
|
|
81
|
+
formData.append('key', 'test-resource.txt');
|
|
82
|
+
formData.append('local', 'false');
|
|
83
|
+
const request = new Request('http://example.com/resources', {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
body: formData,
|
|
86
|
+
});
|
|
87
|
+
const response = await fetch(request);
|
|
88
|
+
if (response.status !== 201) {
|
|
89
|
+
console.error(`POST /resources returned ${response.status}, expected 201`);
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const data = await response.json();
|
|
93
|
+
// Validate response has required fields
|
|
94
|
+
if (!data.key ||
|
|
95
|
+
!data.type ||
|
|
96
|
+
typeof data.size !== 'number' ||
|
|
97
|
+
!data.lastModified ||
|
|
98
|
+
typeof data.local !== 'boolean') {
|
|
99
|
+
console.error(`Response missing required fields: ${JSON.stringify(data)}`);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
if (data.key !== 'test-resource.txt') {
|
|
103
|
+
console.error(`Expected key to be 'test-resource.txt', got ${data.key}`);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
if (data.type !== 'text') {
|
|
107
|
+
console.error(`Expected type to be 'text', got ${data.type}`);
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
if (data.local !== false) {
|
|
111
|
+
console.error(`Expected local to be false, got ${data.local}`);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.error(`Failed to test POST /resources:`, error);
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
/**
|
|
122
|
+
* Test DELETE /resources/:key - Delete a specific resource
|
|
123
|
+
*/
|
|
124
|
+
async delete(fetch, key) {
|
|
125
|
+
try {
|
|
126
|
+
const request = new Request(`http://example.com/resources/${encodeURIComponent(key)}`, {
|
|
127
|
+
method: 'DELETE',
|
|
128
|
+
});
|
|
129
|
+
const response = await fetch(request);
|
|
130
|
+
if (response.status !== 204) {
|
|
131
|
+
console.error(`DELETE /resources/${key} returned ${response.status}, expected 204`);
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
console.error(`Failed to test DELETE /resources/${key}:`, error);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
/**
|
|
142
|
+
* Test DELETE /resources - Bulk delete all resources (dev mode only)
|
|
143
|
+
*/
|
|
144
|
+
async deleteAll(fetch) {
|
|
145
|
+
try {
|
|
146
|
+
const request = new Request('http://example.com/resources', {
|
|
147
|
+
method: 'DELETE',
|
|
148
|
+
});
|
|
149
|
+
const response = await fetch(request);
|
|
150
|
+
// In production mode, this should return 403
|
|
151
|
+
if (response.status === 403) {
|
|
152
|
+
const data = await response.json();
|
|
153
|
+
if (data.error === 'Bulk delete is only available in development mode') {
|
|
154
|
+
// This is expected behavior in production
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (response.status !== 200) {
|
|
159
|
+
console.error(`DELETE /resources returned ${response.status}, expected 200 or 403`);
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
const data = await response.json();
|
|
163
|
+
if (typeof data.deletedCount !== 'number') {
|
|
164
|
+
console.error(`Expected deletedCount to be number, got ${typeof data.deletedCount}`);
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
console.error(`Failed to test DELETE /resources:`, error);
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
/**
|
|
175
|
+
* Test POST /resources/presigned-link - Generate presigned URL for upload
|
|
176
|
+
*/
|
|
177
|
+
async generatePresignedLink(fetch) {
|
|
178
|
+
try {
|
|
179
|
+
const request = new Request('http://example.com/resources/presigned-link', {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
headers: {
|
|
182
|
+
'Content-Type': 'application/json',
|
|
183
|
+
},
|
|
184
|
+
body: JSON.stringify({
|
|
185
|
+
key: 'test-files/large-video.mp4',
|
|
186
|
+
type: 'binary',
|
|
187
|
+
size: 150 * 1024 * 1024, // 150MB - larger than Worker limit
|
|
188
|
+
}),
|
|
189
|
+
});
|
|
190
|
+
const response = await fetch(request);
|
|
191
|
+
// If credentials are not configured, expect 400
|
|
192
|
+
if (response.status === 400) {
|
|
193
|
+
const data = await response.json();
|
|
194
|
+
// This is acceptable - implementation may not have credentials configured
|
|
195
|
+
console.log('Presigned URL generation not available - this is acceptable');
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
if (response.status !== 200) {
|
|
199
|
+
console.error(`POST /resources/presigned-link returned ${response.status}, expected 200 or 400`);
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
const data = await response.json();
|
|
203
|
+
// Validate response structure (backend-agnostic)
|
|
204
|
+
if (!data.url || typeof data.url !== 'string') {
|
|
205
|
+
console.error(`Expected url to be string, got ${typeof data.url}`);
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
if (!data.method || data.method !== 'PUT') {
|
|
209
|
+
console.error(`Expected method to be 'PUT', got ${data.method}`);
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
if (typeof data.expiresIn !== 'number' || data.expiresIn <= 0) {
|
|
213
|
+
console.error(`Expected expiresIn to be positive number, got ${data.expiresIn}`);
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
// Basic URL validation - just ensure it's a valid URL
|
|
217
|
+
try {
|
|
218
|
+
new URL(data.url);
|
|
219
|
+
console.log('Presigned URL structure validated successfully');
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
console.error(`Invalid URL returned: ${data.url}`);
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
console.error(`Failed to test POST /resources/presigned-link:`, error);
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
export const brains = {
|
|
234
|
+
/**
|
|
235
|
+
* Test POST /brains/runs - Create a new brain run
|
|
236
|
+
*/
|
|
237
|
+
async run(fetch, brainName) {
|
|
238
|
+
try {
|
|
239
|
+
const request = new Request('http://example.com/brains/runs', {
|
|
240
|
+
method: 'POST',
|
|
241
|
+
headers: {
|
|
242
|
+
'Content-Type': 'application/json',
|
|
243
|
+
},
|
|
244
|
+
body: JSON.stringify({ brainName }),
|
|
245
|
+
});
|
|
246
|
+
const response = await fetch(request);
|
|
247
|
+
if (response.status !== 201) {
|
|
248
|
+
console.error(`POST /brains/runs returned ${response.status}, expected 201`);
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
const data = await response.json();
|
|
252
|
+
if (!data.brainRunId || typeof data.brainRunId !== 'string') {
|
|
253
|
+
console.error(`Expected brainRunId to be string, got ${typeof data.brainRunId}`);
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
return data.brainRunId;
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
console.error(`Failed to test POST /brains/runs:`, error);
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
/**
|
|
264
|
+
* Test POST /brains/runs with non-existent brain - Should return 404
|
|
265
|
+
*/
|
|
266
|
+
async runNotFound(fetch, nonExistentBrainName) {
|
|
267
|
+
try {
|
|
268
|
+
const request = new Request('http://example.com/brains/runs', {
|
|
269
|
+
method: 'POST',
|
|
270
|
+
headers: {
|
|
271
|
+
'Content-Type': 'application/json',
|
|
272
|
+
},
|
|
273
|
+
body: JSON.stringify({ brainName: nonExistentBrainName }),
|
|
274
|
+
});
|
|
275
|
+
const response = await fetch(request);
|
|
276
|
+
if (response.status !== 404) {
|
|
277
|
+
console.error(`POST /brains/runs with non-existent brain returned ${response.status}, expected 404`);
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
const data = await response.json();
|
|
281
|
+
if (!data.error || typeof data.error !== 'string') {
|
|
282
|
+
console.error(`Expected error to be string, got ${typeof data.error}`);
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
// Check that the error message mentions the brain name
|
|
286
|
+
if (!data.error.includes(nonExistentBrainName)) {
|
|
287
|
+
console.error(`Expected error to mention brain name '${nonExistentBrainName}', got: ${data.error}`);
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
// Check that the error message follows expected format
|
|
291
|
+
const expectedPattern = new RegExp(`Brain '${nonExistentBrainName}' not found`);
|
|
292
|
+
if (!expectedPattern.test(data.error)) {
|
|
293
|
+
console.error(`Expected error message to match pattern "Brain '${nonExistentBrainName}' not found", got: ${data.error}`);
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
console.error(`Failed to test POST /brains/runs with non-existent brain:`, error);
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
/**
|
|
304
|
+
* Test GET /brains/runs/:runId/watch - Watch a brain run via SSE
|
|
305
|
+
*/
|
|
306
|
+
async watch(fetch, runId) {
|
|
307
|
+
try {
|
|
308
|
+
const request = new Request(`http://example.com/brains/runs/${runId}/watch`, {
|
|
309
|
+
method: 'GET',
|
|
310
|
+
});
|
|
311
|
+
const response = await fetch(request);
|
|
312
|
+
if (!response.ok) {
|
|
313
|
+
console.error(`GET /brains/runs/${runId}/watch returned ${response.status}`);
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
// Check that it's an event stream
|
|
317
|
+
const contentType = response.headers.get('content-type');
|
|
318
|
+
if (!contentType || !contentType.includes('text/event-stream')) {
|
|
319
|
+
console.error(`Expected content-type to be text/event-stream, got ${contentType}`);
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
// Read a bit of the stream to verify it's actually SSE format
|
|
323
|
+
if (response.body) {
|
|
324
|
+
const reader = response.body.getReader();
|
|
325
|
+
try {
|
|
326
|
+
// Read first chunk
|
|
327
|
+
const { value } = await reader.read();
|
|
328
|
+
if (value) {
|
|
329
|
+
const text = new TextDecoder().decode(value);
|
|
330
|
+
// SSE data should contain "data: " lines
|
|
331
|
+
if (!text.includes('data: ')) {
|
|
332
|
+
console.error(`Expected SSE format with "data: " prefix, got: ${text.substring(0, 100)}`);
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
finally {
|
|
338
|
+
// Always cancel the reader to clean up
|
|
339
|
+
await reader.cancel();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
console.error(`Failed to test GET /brains/runs/${runId}/watch:`, error);
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
/**
|
|
350
|
+
* Test GET /brains/:brainName/history - Get history of brain runs
|
|
351
|
+
*/
|
|
352
|
+
async history(fetch, brainName, limit) {
|
|
353
|
+
try {
|
|
354
|
+
const url = new URL('http://example.com/brains/' + brainName + '/history');
|
|
355
|
+
if (limit !== undefined) {
|
|
356
|
+
url.searchParams.set('limit', limit.toString());
|
|
357
|
+
}
|
|
358
|
+
const request = new Request(url.toString(), {
|
|
359
|
+
method: 'GET',
|
|
360
|
+
});
|
|
361
|
+
const response = await fetch(request);
|
|
362
|
+
if (!response.ok) {
|
|
363
|
+
console.error(`GET /brains/${brainName}/history returned ${response.status}`);
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
const data = await response.json();
|
|
367
|
+
// Validate response structure
|
|
368
|
+
if (!data.runs || !Array.isArray(data.runs)) {
|
|
369
|
+
console.error(`Expected runs to be an array, got ${typeof data.runs}`);
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
// Validate each run has required fields
|
|
373
|
+
for (const run of data.runs) {
|
|
374
|
+
if (!run.brainRunId ||
|
|
375
|
+
!run.brainTitle ||
|
|
376
|
+
!run.type ||
|
|
377
|
+
!run.status ||
|
|
378
|
+
typeof run.createdAt !== 'number') {
|
|
379
|
+
console.error(`Run missing required fields: ${JSON.stringify(run)}`);
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
console.error(`Failed to test GET /brains/${brainName}/history:`, error);
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
/**
|
|
391
|
+
* Test GET /brains/watch - Watch all running brains
|
|
392
|
+
*/
|
|
393
|
+
async watchAll(fetch) {
|
|
394
|
+
try {
|
|
395
|
+
const request = new Request('http://example.com/brains/watch', {
|
|
396
|
+
method: 'GET',
|
|
397
|
+
});
|
|
398
|
+
const response = await fetch(request);
|
|
399
|
+
if (!response.ok) {
|
|
400
|
+
console.error(`GET /brains/watch returned ${response.status}`);
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
// Check that it's an event stream
|
|
404
|
+
const contentType = response.headers.get('content-type');
|
|
405
|
+
if (!contentType || !contentType.includes('text/event-stream')) {
|
|
406
|
+
console.error(`Expected content-type to be text/event-stream, got ${contentType}`);
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
// Read a bit of the stream to verify it's actually SSE format
|
|
410
|
+
if (response.body) {
|
|
411
|
+
const reader = response.body.getReader();
|
|
412
|
+
try {
|
|
413
|
+
// Read first chunk
|
|
414
|
+
const { value } = await reader.read();
|
|
415
|
+
if (value) {
|
|
416
|
+
const text = new TextDecoder().decode(value);
|
|
417
|
+
// SSE data should contain "data: " lines
|
|
418
|
+
if (!text.includes('data: ')) {
|
|
419
|
+
console.error(`Expected SSE format with "data: " prefix, got: ${text.substring(0, 100)}`);
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
425
|
+
// Always cancel the reader to clean up
|
|
426
|
+
await reader.cancel();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
console.error(`Failed to test GET /brains/watch:`, error);
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
/**
|
|
437
|
+
* Test GET /brains - List all brains
|
|
438
|
+
*/
|
|
439
|
+
async list(fetch) {
|
|
440
|
+
try {
|
|
441
|
+
const request = new Request('http://example.com/brains', {
|
|
442
|
+
method: 'GET',
|
|
443
|
+
});
|
|
444
|
+
const response = await fetch(request);
|
|
445
|
+
if (!response.ok) {
|
|
446
|
+
console.error(`GET /brains returned ${response.status}`);
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
const data = await response.json();
|
|
450
|
+
// Validate response structure
|
|
451
|
+
if (!Array.isArray(data.brains)) {
|
|
452
|
+
console.error(`Expected brains to be an array, got ${typeof data.brains}`);
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
if (typeof data.count !== 'number') {
|
|
456
|
+
console.error(`Expected count to be number, got ${typeof data.count}`);
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
// Validate each brain has required fields
|
|
460
|
+
for (const brain of data.brains) {
|
|
461
|
+
if (!brain.name ||
|
|
462
|
+
typeof brain.name !== 'string' ||
|
|
463
|
+
!brain.title ||
|
|
464
|
+
typeof brain.title !== 'string' ||
|
|
465
|
+
!brain.description ||
|
|
466
|
+
typeof brain.description !== 'string') {
|
|
467
|
+
console.error(`Brain missing required fields or has invalid types: ${JSON.stringify(brain)}`);
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return true;
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
console.error(`Failed to test GET /brains:`, error);
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
/**
|
|
479
|
+
* Test GET /brains/:brainName - Get brain structure
|
|
480
|
+
*/
|
|
481
|
+
async show(fetch, brainName) {
|
|
482
|
+
try {
|
|
483
|
+
const request = new Request(`http://example.com/brains/${encodeURIComponent(brainName)}`, {
|
|
484
|
+
method: 'GET',
|
|
485
|
+
});
|
|
486
|
+
const response = await fetch(request);
|
|
487
|
+
if (!response.ok) {
|
|
488
|
+
console.error(`GET /brains/${brainName} returned ${response.status}`);
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
const data = await response.json();
|
|
492
|
+
// Validate response structure
|
|
493
|
+
if (!data.name || typeof data.name !== 'string') {
|
|
494
|
+
console.error(`Expected name to be string, got ${typeof data.name}`);
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
if (!data.title || typeof data.title !== 'string') {
|
|
498
|
+
console.error(`Expected title to be string, got ${typeof data.title}`);
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
if (!Array.isArray(data.steps)) {
|
|
502
|
+
console.error(`Expected steps to be an array, got ${typeof data.steps}`);
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
// Validate each step
|
|
506
|
+
for (const step of data.steps) {
|
|
507
|
+
if (!step.type || !['step', 'brain'].includes(step.type)) {
|
|
508
|
+
console.error(`Invalid step type: ${step.type}`);
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
if (!step.title || typeof step.title !== 'string') {
|
|
512
|
+
console.error(`Step missing title or has invalid type: ${JSON.stringify(step)}`);
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
// If it's a brain step, validate the inner brain recursively
|
|
516
|
+
if (step.type === 'brain' && step.innerBrain) {
|
|
517
|
+
if (!step.innerBrain.title ||
|
|
518
|
+
typeof step.innerBrain.title !== 'string') {
|
|
519
|
+
console.error(`Inner brain missing title: ${JSON.stringify(step.innerBrain)}`);
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
if (!Array.isArray(step.innerBrain.steps)) {
|
|
523
|
+
console.error(`Inner brain missing steps array: ${JSON.stringify(step.innerBrain)}`);
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
console.error(`Failed to test GET /brains/${brainName}:`, error);
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
},
|
|
535
|
+
/**
|
|
536
|
+
* Test GET /brains/:brainName/active-runs - Get active/running brain runs
|
|
537
|
+
*/
|
|
538
|
+
async activeRuns(fetch, brainName) {
|
|
539
|
+
try {
|
|
540
|
+
const request = new Request(`http://example.com/brains/${encodeURIComponent(brainName)}/active-runs`, {
|
|
541
|
+
method: 'GET',
|
|
542
|
+
});
|
|
543
|
+
const response = await fetch(request);
|
|
544
|
+
if (!response.ok) {
|
|
545
|
+
console.error(`GET /brains/${brainName}/active-runs returned ${response.status}`);
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
const data = await response.json();
|
|
549
|
+
// Validate response structure
|
|
550
|
+
if (!data.runs || !Array.isArray(data.runs)) {
|
|
551
|
+
console.error(`Expected runs to be an array, got ${typeof data.runs}`);
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
// Validate each run has required fields
|
|
555
|
+
for (const run of data.runs) {
|
|
556
|
+
if (!run.brainRunId ||
|
|
557
|
+
!run.brainTitle ||
|
|
558
|
+
!run.type ||
|
|
559
|
+
!run.status ||
|
|
560
|
+
typeof run.createdAt !== 'number') {
|
|
561
|
+
console.error(`Active run missing required fields: ${JSON.stringify(run)}`);
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
// All active runs should have status 'RUNNING'
|
|
565
|
+
if (run.status !== 'RUNNING') {
|
|
566
|
+
console.error(`Expected active run status to be 'RUNNING', got ${run.status}`);
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
console.error(`Failed to test GET /brains/${brainName}/active-runs:`, error);
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
/**
|
|
578
|
+
* Test POST /brains/runs/rerun - Rerun an existing brain run
|
|
579
|
+
*/
|
|
580
|
+
async rerun(fetch, brainName, runId, startsAt, stopsAfter) {
|
|
581
|
+
try {
|
|
582
|
+
const body = { brainName };
|
|
583
|
+
if (runId)
|
|
584
|
+
body.runId = runId;
|
|
585
|
+
if (startsAt !== undefined)
|
|
586
|
+
body.startsAt = startsAt;
|
|
587
|
+
if (stopsAfter !== undefined)
|
|
588
|
+
body.stopsAfter = stopsAfter;
|
|
589
|
+
const request = new Request('http://example.com/brains/runs/rerun', {
|
|
590
|
+
method: 'POST',
|
|
591
|
+
headers: {
|
|
592
|
+
'Content-Type': 'application/json',
|
|
593
|
+
},
|
|
594
|
+
body: JSON.stringify(body),
|
|
595
|
+
});
|
|
596
|
+
const response = await fetch(request);
|
|
597
|
+
if (response.status !== 201) {
|
|
598
|
+
console.error(`POST /brains/runs/rerun returned ${response.status}, expected 201`);
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
const data = await response.json();
|
|
602
|
+
if (!data.brainRunId || typeof data.brainRunId !== 'string') {
|
|
603
|
+
console.error(`Expected brainRunId to be string, got ${typeof data.brainRunId}`);
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
return data.brainRunId;
|
|
607
|
+
}
|
|
608
|
+
catch (error) {
|
|
609
|
+
console.error(`Failed to test POST /brains/runs/rerun:`, error);
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
};
|
|
614
|
+
export const schedules = {
|
|
615
|
+
/**
|
|
616
|
+
* Test POST /brains/schedules - Create a new schedule
|
|
617
|
+
*/
|
|
618
|
+
async create(fetch, brainName, cronExpression) {
|
|
619
|
+
try {
|
|
620
|
+
const request = new Request('http://example.com/brains/schedules', {
|
|
621
|
+
method: 'POST',
|
|
622
|
+
headers: {
|
|
623
|
+
'Content-Type': 'application/json',
|
|
624
|
+
},
|
|
625
|
+
body: JSON.stringify({ brainName, cronExpression }),
|
|
626
|
+
});
|
|
627
|
+
const response = await fetch(request);
|
|
628
|
+
if (response.status !== 201) {
|
|
629
|
+
console.error(`POST /brains/schedules returned ${response.status}, expected 201`);
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
const data = await response.json();
|
|
633
|
+
// Validate response structure
|
|
634
|
+
if (!data.id || typeof data.id !== 'string') {
|
|
635
|
+
console.error(`Expected id to be string, got ${typeof data.id}`);
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
if (data.brainName !== brainName) {
|
|
639
|
+
console.error(`Expected brainName to be '${brainName}', got ${data.brainName}`);
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
if (data.cronExpression !== cronExpression) {
|
|
643
|
+
console.error(`Expected cronExpression to be '${cronExpression}', got ${data.cronExpression}`);
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
if (typeof data.enabled !== 'boolean') {
|
|
647
|
+
console.error(`Expected enabled to be boolean, got ${typeof data.enabled}`);
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
if (typeof data.createdAt !== 'number') {
|
|
651
|
+
console.error(`Expected createdAt to be number, got ${typeof data.createdAt}`);
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
return data.id;
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
console.error(`Failed to test POST /brains/schedules:`, error);
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
},
|
|
661
|
+
/**
|
|
662
|
+
* Test GET /brains/schedules - List all schedules
|
|
663
|
+
*/
|
|
664
|
+
async list(fetch) {
|
|
665
|
+
try {
|
|
666
|
+
const request = new Request('http://example.com/brains/schedules', {
|
|
667
|
+
method: 'GET',
|
|
668
|
+
});
|
|
669
|
+
const response = await fetch(request);
|
|
670
|
+
if (!response.ok) {
|
|
671
|
+
console.error(`GET /brains/schedules returned ${response.status}`);
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
674
|
+
const data = await response.json();
|
|
675
|
+
// Validate response structure
|
|
676
|
+
if (!Array.isArray(data.schedules)) {
|
|
677
|
+
console.error(`Expected schedules to be an array, got ${typeof data.schedules}`);
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
if (typeof data.count !== 'number') {
|
|
681
|
+
console.error(`Expected count to be number, got ${typeof data.count}`);
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
// Validate each schedule has required fields
|
|
685
|
+
for (const schedule of data.schedules) {
|
|
686
|
+
if (!schedule.id ||
|
|
687
|
+
!schedule.brainName ||
|
|
688
|
+
!schedule.cronExpression ||
|
|
689
|
+
typeof schedule.enabled !== 'boolean' ||
|
|
690
|
+
typeof schedule.createdAt !== 'number') {
|
|
691
|
+
console.error(`Schedule missing required fields: ${JSON.stringify(schedule)}`);
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
697
|
+
catch (error) {
|
|
698
|
+
console.error(`Failed to test GET /brains/schedules:`, error);
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
},
|
|
702
|
+
/**
|
|
703
|
+
* Test DELETE /brains/schedules/:scheduleId - Delete a schedule
|
|
704
|
+
*/
|
|
705
|
+
async delete(fetch, scheduleId) {
|
|
706
|
+
try {
|
|
707
|
+
const request = new Request(`http://example.com/brains/schedules/${scheduleId}`, {
|
|
708
|
+
method: 'DELETE',
|
|
709
|
+
});
|
|
710
|
+
const response = await fetch(request);
|
|
711
|
+
if (response.status !== 204) {
|
|
712
|
+
console.error(`DELETE /brains/schedules/${scheduleId} returned ${response.status}, expected 204`);
|
|
713
|
+
return false;
|
|
714
|
+
}
|
|
715
|
+
return true;
|
|
716
|
+
}
|
|
717
|
+
catch (error) {
|
|
718
|
+
console.error(`Failed to test DELETE /brains/schedules/${scheduleId}:`, error);
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
721
|
+
},
|
|
722
|
+
/**
|
|
723
|
+
* Test GET /brains/schedules/runs - Get history of scheduled runs
|
|
724
|
+
*/
|
|
725
|
+
async runs(fetch, scheduleId, limit) {
|
|
726
|
+
try {
|
|
727
|
+
const url = new URL('http://example.com/brains/schedules/runs');
|
|
728
|
+
if (scheduleId !== undefined) {
|
|
729
|
+
url.searchParams.set('scheduleId', scheduleId);
|
|
730
|
+
}
|
|
731
|
+
if (limit !== undefined) {
|
|
732
|
+
url.searchParams.set('limit', limit.toString());
|
|
733
|
+
}
|
|
734
|
+
const request = new Request(url.toString(), {
|
|
735
|
+
method: 'GET',
|
|
736
|
+
});
|
|
737
|
+
const response = await fetch(request);
|
|
738
|
+
if (!response.ok) {
|
|
739
|
+
console.error(`GET /brains/schedules/runs returned ${response.status}`);
|
|
740
|
+
return false;
|
|
741
|
+
}
|
|
742
|
+
const data = await response.json();
|
|
743
|
+
// Validate response structure
|
|
744
|
+
if (!Array.isArray(data.runs)) {
|
|
745
|
+
console.error(`Expected runs to be an array, got ${typeof data.runs}`);
|
|
746
|
+
return false;
|
|
747
|
+
}
|
|
748
|
+
if (typeof data.count !== 'number') {
|
|
749
|
+
console.error(`Expected count to be number, got ${typeof data.count}`);
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
// Validate each run has required fields
|
|
753
|
+
for (const run of data.runs) {
|
|
754
|
+
if (!run.id ||
|
|
755
|
+
!run.scheduleId ||
|
|
756
|
+
!run.status ||
|
|
757
|
+
typeof run.ranAt !== 'number') {
|
|
758
|
+
console.error(`Scheduled run missing required fields: ${JSON.stringify(run)}`);
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
if (!['triggered', 'failed'].includes(run.status)) {
|
|
762
|
+
console.error(`Invalid run status: ${run.status}`);
|
|
763
|
+
return false;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
return true;
|
|
767
|
+
}
|
|
768
|
+
catch (error) {
|
|
769
|
+
console.error(`Failed to test GET /brains/schedules/runs:`, error);
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
},
|
|
773
|
+
};
|
|
774
|
+
export const secrets = {
|
|
775
|
+
/**
|
|
776
|
+
* Test POST /secrets - Create or update a secret
|
|
777
|
+
*/
|
|
778
|
+
async create(fetch, name, value) {
|
|
779
|
+
try {
|
|
780
|
+
const request = new Request('http://example.com/secrets', {
|
|
781
|
+
method: 'POST',
|
|
782
|
+
headers: {
|
|
783
|
+
'Content-Type': 'application/json',
|
|
784
|
+
},
|
|
785
|
+
body: JSON.stringify({ name, value }),
|
|
786
|
+
});
|
|
787
|
+
const response = await fetch(request);
|
|
788
|
+
if (response.status !== 201) {
|
|
789
|
+
console.error(`POST /secrets returned ${response.status}, expected 201`);
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
const data = await response.json();
|
|
793
|
+
// Validate response structure
|
|
794
|
+
if (!data.name || typeof data.name !== 'string') {
|
|
795
|
+
console.error(`Expected name to be string, got ${typeof data.name}`);
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
if (data.name !== name) {
|
|
799
|
+
console.error(`Expected name to be '${name}', got ${data.name}`);
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
if (typeof data.createdAt !== 'string') {
|
|
803
|
+
console.error(`Expected createdAt to be string, got ${typeof data.createdAt}`);
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
if (typeof data.updatedAt !== 'string') {
|
|
807
|
+
console.error(`Expected updatedAt to be string, got ${typeof data.updatedAt}`);
|
|
808
|
+
return false;
|
|
809
|
+
}
|
|
810
|
+
return true;
|
|
811
|
+
}
|
|
812
|
+
catch (error) {
|
|
813
|
+
console.error(`Failed to test POST /secrets:`, error);
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
},
|
|
817
|
+
/**
|
|
818
|
+
* Test GET /secrets - List all secrets (names only, not values)
|
|
819
|
+
*/
|
|
820
|
+
async list(fetch) {
|
|
821
|
+
try {
|
|
822
|
+
const request = new Request('http://example.com/secrets', {
|
|
823
|
+
method: 'GET',
|
|
824
|
+
});
|
|
825
|
+
const response = await fetch(request);
|
|
826
|
+
if (!response.ok) {
|
|
827
|
+
console.error(`GET /secrets returned ${response.status}`);
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
const data = await response.json();
|
|
831
|
+
// Validate response structure
|
|
832
|
+
if (!Array.isArray(data.secrets)) {
|
|
833
|
+
console.error(`Expected secrets to be an array, got ${typeof data.secrets}`);
|
|
834
|
+
return false;
|
|
835
|
+
}
|
|
836
|
+
if (typeof data.count !== 'number') {
|
|
837
|
+
console.error(`Expected count to be number, got ${typeof data.count}`);
|
|
838
|
+
return false;
|
|
839
|
+
}
|
|
840
|
+
// Validate each secret has required fields (but NOT the value)
|
|
841
|
+
for (const secret of data.secrets) {
|
|
842
|
+
if (!secret.name ||
|
|
843
|
+
typeof secret.name !== 'string' ||
|
|
844
|
+
typeof secret.createdAt !== 'string' ||
|
|
845
|
+
typeof secret.updatedAt !== 'string') {
|
|
846
|
+
console.error(`Secret missing required fields: ${JSON.stringify(secret)}`);
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
// Ensure value is NOT included
|
|
850
|
+
if ('value' in secret) {
|
|
851
|
+
console.error(`Secret should not include value field: ${JSON.stringify(secret)}`);
|
|
852
|
+
return false;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return true;
|
|
856
|
+
}
|
|
857
|
+
catch (error) {
|
|
858
|
+
console.error(`Failed to test GET /secrets:`, error);
|
|
859
|
+
return false;
|
|
860
|
+
}
|
|
861
|
+
},
|
|
862
|
+
/**
|
|
863
|
+
* Test DELETE /secrets/:name - Delete a specific secret
|
|
864
|
+
*/
|
|
865
|
+
async delete(fetch, name) {
|
|
866
|
+
try {
|
|
867
|
+
const request = new Request(`http://example.com/secrets/${encodeURIComponent(name)}`, {
|
|
868
|
+
method: 'DELETE',
|
|
869
|
+
});
|
|
870
|
+
const response = await fetch(request);
|
|
871
|
+
if (response.status !== 204) {
|
|
872
|
+
console.error(`DELETE /secrets/${name} returned ${response.status}, expected 204`);
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
return true;
|
|
876
|
+
}
|
|
877
|
+
catch (error) {
|
|
878
|
+
console.error(`Failed to test DELETE /secrets/${name}:`, error);
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
},
|
|
882
|
+
/**
|
|
883
|
+
* Test GET /secrets/:name/exists - Check if a secret exists
|
|
884
|
+
*/
|
|
885
|
+
async exists(fetch, name) {
|
|
886
|
+
try {
|
|
887
|
+
const request = new Request(`http://example.com/secrets/${encodeURIComponent(name)}/exists`, {
|
|
888
|
+
method: 'GET',
|
|
889
|
+
});
|
|
890
|
+
const response = await fetch(request);
|
|
891
|
+
if (!response.ok) {
|
|
892
|
+
console.error(`GET /secrets/${name}/exists returned ${response.status}`);
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
const data = await response.json();
|
|
896
|
+
// Validate response structure
|
|
897
|
+
if (typeof data.exists !== 'boolean') {
|
|
898
|
+
console.error(`Expected exists to be boolean, got ${typeof data.exists}`);
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
901
|
+
return true;
|
|
902
|
+
}
|
|
903
|
+
catch (error) {
|
|
904
|
+
console.error(`Failed to test GET /secrets/${name}/exists:`, error);
|
|
905
|
+
return false;
|
|
906
|
+
}
|
|
907
|
+
},
|
|
908
|
+
/**
|
|
909
|
+
* Test POST /secrets/bulk - Create multiple secrets
|
|
910
|
+
*/
|
|
911
|
+
async bulk(fetch, secrets) {
|
|
912
|
+
try {
|
|
913
|
+
const request = new Request('http://example.com/secrets/bulk', {
|
|
914
|
+
method: 'POST',
|
|
915
|
+
headers: {
|
|
916
|
+
'Content-Type': 'application/json',
|
|
917
|
+
},
|
|
918
|
+
body: JSON.stringify({ secrets }),
|
|
919
|
+
});
|
|
920
|
+
const response = await fetch(request);
|
|
921
|
+
if (response.status !== 201) {
|
|
922
|
+
console.error(`POST /secrets/bulk returned ${response.status}, expected 201`);
|
|
923
|
+
return false;
|
|
924
|
+
}
|
|
925
|
+
const data = await response.json();
|
|
926
|
+
// Validate response structure
|
|
927
|
+
if (typeof data.created !== 'number') {
|
|
928
|
+
console.error(`Expected created to be number, got ${typeof data.created}`);
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
931
|
+
if (typeof data.updated !== 'number') {
|
|
932
|
+
console.error(`Expected updated to be number, got ${typeof data.updated}`);
|
|
933
|
+
return false;
|
|
934
|
+
}
|
|
935
|
+
// Total should match input
|
|
936
|
+
if (data.created + data.updated !== secrets.length) {
|
|
937
|
+
console.error(`Expected total (${data.created + data.updated}) to match input length (${secrets.length})`);
|
|
938
|
+
return false;
|
|
939
|
+
}
|
|
940
|
+
return true;
|
|
941
|
+
}
|
|
942
|
+
catch (error) {
|
|
943
|
+
console.error(`Failed to test POST /secrets/bulk:`, error);
|
|
944
|
+
return false;
|
|
945
|
+
}
|
|
946
|
+
},
|
|
947
|
+
};
|
|
948
|
+
//# sourceMappingURL=api.js.map
|