@positronic/spec 0.0.37 → 0.0.39

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 CHANGED
@@ -55,6 +55,19 @@ export declare const brains: {
55
55
  * Test GET /brains - List all brains
56
56
  */
57
57
  list(fetch: Fetch): Promise<boolean>;
58
+ /**
59
+ * Test GET /brains?q=<query> - Search brains by query string
60
+ * Returns brains matching the query (by title, filename, or description).
61
+ * The matching algorithm is implementation-defined; the spec only verifies
62
+ * the response structure and that results are relevant to the query.
63
+ */
64
+ search(fetch: Fetch, query: string): Promise<{
65
+ brains: Array<{
66
+ title: string;
67
+ description: string;
68
+ }>;
69
+ count: number;
70
+ } | null>;
58
71
  /**
59
72
  * Test GET /brains/:identifier - Get brain structure/definition
60
73
  * (For future brain exploration/info command)
@@ -89,6 +102,40 @@ export declare const brains: {
89
102
  * Test DELETE /brains/runs/:runId - Kill/cancel a running brain run
90
103
  */
91
104
  kill(fetch: Fetch, runId: string): Promise<boolean>;
105
+ /**
106
+ * Test DELETE /brains/runs/:runId for a brain suspended on a webhook.
107
+ * This tests that killing a webhook-suspended brain:
108
+ * 1. Returns 204 (not 409)
109
+ * 2. Updates status to CANCELLED
110
+ * 3. Clears webhook registrations (webhook no longer resumes the brain)
111
+ *
112
+ * Requires a brain with a loop step that will pause on a webhook.
113
+ */
114
+ killSuspended(fetch: Fetch, loopBrainIdentifier: string, webhookSlug: string, webhookPayload: Record<string, any>): Promise<boolean>;
115
+ /**
116
+ * Test that loop steps emit proper LOOP_* events in the SSE stream.
117
+ * Requires a brain with a loop step that will pause on a webhook.
118
+ *
119
+ * Expected events before webhook pause:
120
+ * - LOOP_START (with prompt and optional system)
121
+ * - LOOP_ITERATION
122
+ * - LOOP_TOOL_CALL
123
+ * - LOOP_WEBHOOK (before WEBHOOK event)
124
+ * - WEBHOOK
125
+ */
126
+ watchLoopEvents(fetch: Fetch, loopBrainIdentifier: string): Promise<boolean>;
127
+ /**
128
+ * Test full loop webhook resumption flow:
129
+ * 1. Start a loop brain that will pause on a webhook
130
+ * 2. Verify it pauses with WEBHOOK event
131
+ * 3. Trigger the webhook with a response
132
+ * 4. Verify the brain resumes and emits WEBHOOK_RESPONSE and LOOP_TOOL_RESULT
133
+ *
134
+ * Requires:
135
+ * - A brain with a loop step that calls a tool returning { waitFor: webhook(...) }
136
+ * - The webhook slug and identifier to trigger
137
+ */
138
+ loopWebhookResume(fetch: Fetch, loopBrainIdentifier: string, webhookSlug: string, webhookPayload: Record<string, any>): Promise<boolean>;
92
139
  };
93
140
  export declare const schedules: {
94
141
  /**
package/dist/api.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
package/dist/api.js CHANGED
@@ -1,4 +1,4 @@
1
- import { STATUS } from '@positronic/core';
1
+ import { STATUS, BRAIN_EVENTS } from '@positronic/core';
2
2
  export async function testStatus(fetch) {
3
3
  try {
4
4
  const request = new Request('http://example.com/status', {
@@ -570,6 +570,56 @@ export const brains = {
570
570
  return false;
571
571
  }
572
572
  },
573
+ /**
574
+ * Test GET /brains?q=<query> - Search brains by query string
575
+ * Returns brains matching the query (by title, filename, or description).
576
+ * The matching algorithm is implementation-defined; the spec only verifies
577
+ * the response structure and that results are relevant to the query.
578
+ */
579
+ async search(fetch, query) {
580
+ try {
581
+ const url = new URL('http://example.com/brains');
582
+ url.searchParams.set('q', query);
583
+ const request = new Request(url.toString(), {
584
+ method: 'GET',
585
+ });
586
+ const response = await fetch(request);
587
+ if (!response.ok) {
588
+ console.error(`GET /brains?q=${query} returned ${response.status}`);
589
+ return null;
590
+ }
591
+ const data = await response.json();
592
+ // Validate response structure
593
+ if (!Array.isArray(data.brains)) {
594
+ console.error(`Expected brains to be an array, got ${typeof data.brains}`);
595
+ return null;
596
+ }
597
+ if (typeof data.count !== 'number') {
598
+ console.error(`Expected count to be number, got ${typeof data.count}`);
599
+ return null;
600
+ }
601
+ // Validate each brain has required fields
602
+ for (const brain of data.brains) {
603
+ if (!brain.title ||
604
+ typeof brain.title !== 'string' ||
605
+ !brain.description ||
606
+ typeof brain.description !== 'string') {
607
+ console.error(`Brain missing required fields or has invalid types: ${JSON.stringify(brain)}`);
608
+ return null;
609
+ }
610
+ }
611
+ // Count should match array length
612
+ if (data.count !== data.brains.length) {
613
+ console.error(`Count (${data.count}) does not match brains array length (${data.brains.length})`);
614
+ return null;
615
+ }
616
+ return data;
617
+ }
618
+ catch (error) {
619
+ console.error(`Failed to test GET /brains?q=${query}:`, error);
620
+ return null;
621
+ }
622
+ },
573
623
  /**
574
624
  * Test GET /brains/:identifier - Get brain structure/definition
575
625
  * (For future brain exploration/info command)
@@ -867,6 +917,425 @@ export const brains = {
867
917
  return false;
868
918
  }
869
919
  },
920
+ /**
921
+ * Test DELETE /brains/runs/:runId for a brain suspended on a webhook.
922
+ * This tests that killing a webhook-suspended brain:
923
+ * 1. Returns 204 (not 409)
924
+ * 2. Updates status to CANCELLED
925
+ * 3. Clears webhook registrations (webhook no longer resumes the brain)
926
+ *
927
+ * Requires a brain with a loop step that will pause on a webhook.
928
+ */
929
+ async killSuspended(fetch, loopBrainIdentifier, webhookSlug, webhookPayload) {
930
+ try {
931
+ // Step 1: Start the loop brain
932
+ const runRequest = new Request('http://example.com/brains/runs', {
933
+ method: 'POST',
934
+ headers: { 'Content-Type': 'application/json' },
935
+ body: JSON.stringify({ identifier: loopBrainIdentifier }),
936
+ });
937
+ const runResponse = await fetch(runRequest);
938
+ if (runResponse.status !== 201) {
939
+ console.error(`POST /brains/runs returned ${runResponse.status}, expected 201`);
940
+ return false;
941
+ }
942
+ const { brainRunId } = (await runResponse.json());
943
+ // Step 2: Watch until WEBHOOK event (brain pauses)
944
+ const watchRequest = new Request(`http://example.com/brains/runs/${brainRunId}/watch`, { method: 'GET' });
945
+ const watchResponse = await fetch(watchRequest);
946
+ if (!watchResponse.ok) {
947
+ console.error(`GET /brains/runs/${brainRunId}/watch returned ${watchResponse.status}`);
948
+ return false;
949
+ }
950
+ let foundWebhookEvent = false;
951
+ if (watchResponse.body) {
952
+ const reader = watchResponse.body.getReader();
953
+ const decoder = new TextDecoder();
954
+ let buffer = '';
955
+ try {
956
+ while (!foundWebhookEvent) {
957
+ const { value, done } = await reader.read();
958
+ if (done)
959
+ break;
960
+ buffer += decoder.decode(value, { stream: true });
961
+ let eventEndIndex;
962
+ while ((eventEndIndex = buffer.indexOf('\n\n')) !== -1) {
963
+ const message = buffer.substring(0, eventEndIndex);
964
+ buffer = buffer.substring(eventEndIndex + 2);
965
+ if (message.startsWith('data: ')) {
966
+ try {
967
+ const event = JSON.parse(message.substring(6));
968
+ if (event.type === BRAIN_EVENTS.WEBHOOK) {
969
+ foundWebhookEvent = true;
970
+ break;
971
+ }
972
+ if (event.type === BRAIN_EVENTS.COMPLETE ||
973
+ event.type === BRAIN_EVENTS.ERROR) {
974
+ console.error(`Brain completed/errored before WEBHOOK event: ${event.type}`);
975
+ return false;
976
+ }
977
+ }
978
+ catch (e) {
979
+ // Ignore parse errors
980
+ }
981
+ }
982
+ }
983
+ }
984
+ }
985
+ finally {
986
+ await reader.cancel();
987
+ }
988
+ }
989
+ if (!foundWebhookEvent) {
990
+ console.error('Brain did not emit WEBHOOK event');
991
+ return false;
992
+ }
993
+ // Step 3: Kill the suspended brain
994
+ const killRequest = new Request(`http://example.com/brains/runs/${brainRunId}`, { method: 'DELETE' });
995
+ const killResponse = await fetch(killRequest);
996
+ if (killResponse.status !== 204) {
997
+ console.error(`DELETE /brains/runs/${brainRunId} returned ${killResponse.status}, expected 204`);
998
+ return false;
999
+ }
1000
+ // Step 4: Verify status is CANCELLED via getRun
1001
+ const getRunRequest = new Request(`http://example.com/brains/runs/${brainRunId}`, { method: 'GET' });
1002
+ const getRunResponse = await fetch(getRunRequest);
1003
+ if (!getRunResponse.ok) {
1004
+ console.error(`GET /brains/runs/${brainRunId} returned ${getRunResponse.status}`);
1005
+ return false;
1006
+ }
1007
+ const runData = (await getRunResponse.json());
1008
+ if (runData.status !== STATUS.CANCELLED) {
1009
+ console.error(`Expected status to be '${STATUS.CANCELLED}', got '${runData.status}'`);
1010
+ return false;
1011
+ }
1012
+ // Step 5: Verify webhook no longer resumes the brain
1013
+ // Send a webhook - it should return 'no-match' since registrations were cleared
1014
+ const webhookRequest = new Request(`http://example.com/webhooks/${encodeURIComponent(webhookSlug)}`, {
1015
+ method: 'POST',
1016
+ headers: { 'Content-Type': 'application/json' },
1017
+ body: JSON.stringify(webhookPayload),
1018
+ });
1019
+ const webhookResponse = await fetch(webhookRequest);
1020
+ // Accept 200/202 - the important thing is it doesn't resume the brain
1021
+ if (!webhookResponse.ok) {
1022
+ console.error(`POST /webhooks/${webhookSlug} returned ${webhookResponse.status}`);
1023
+ return false;
1024
+ }
1025
+ const webhookResult = (await webhookResponse.json());
1026
+ // The action should be 'no-match' since webhook registrations were cleared
1027
+ if (webhookResult.action === 'resumed') {
1028
+ console.error('Webhook resumed the brain after it was killed - webhook registrations were not cleared');
1029
+ return false;
1030
+ }
1031
+ // Verify the brain is still CANCELLED (didn't restart)
1032
+ const finalCheckRequest = new Request(`http://example.com/brains/runs/${brainRunId}`, { method: 'GET' });
1033
+ const finalCheckResponse = await fetch(finalCheckRequest);
1034
+ if (!finalCheckResponse.ok) {
1035
+ console.error(`Final GET /brains/runs/${brainRunId} returned ${finalCheckResponse.status}`);
1036
+ return false;
1037
+ }
1038
+ const finalRunData = (await finalCheckResponse.json());
1039
+ if (finalRunData.status !== STATUS.CANCELLED) {
1040
+ console.error(`Final status check: expected '${STATUS.CANCELLED}', got '${finalRunData.status}'`);
1041
+ return false;
1042
+ }
1043
+ return true;
1044
+ }
1045
+ catch (error) {
1046
+ console.error(`Failed to test kill suspended brain for ${loopBrainIdentifier}:`, error);
1047
+ return false;
1048
+ }
1049
+ },
1050
+ /**
1051
+ * Test that loop steps emit proper LOOP_* events in the SSE stream.
1052
+ * Requires a brain with a loop step that will pause on a webhook.
1053
+ *
1054
+ * Expected events before webhook pause:
1055
+ * - LOOP_START (with prompt and optional system)
1056
+ * - LOOP_ITERATION
1057
+ * - LOOP_TOOL_CALL
1058
+ * - LOOP_WEBHOOK (before WEBHOOK event)
1059
+ * - WEBHOOK
1060
+ */
1061
+ async watchLoopEvents(fetch, loopBrainIdentifier) {
1062
+ try {
1063
+ // Start the loop brain
1064
+ const runRequest = new Request('http://example.com/brains/runs', {
1065
+ method: 'POST',
1066
+ headers: { 'Content-Type': 'application/json' },
1067
+ body: JSON.stringify({ identifier: loopBrainIdentifier }),
1068
+ });
1069
+ const runResponse = await fetch(runRequest);
1070
+ if (runResponse.status !== 201) {
1071
+ console.error(`POST /brains/runs returned ${runResponse.status}, expected 201`);
1072
+ return false;
1073
+ }
1074
+ const { brainRunId } = (await runResponse.json());
1075
+ // Watch the brain run
1076
+ const watchRequest = new Request(`http://example.com/brains/runs/${brainRunId}/watch`, { method: 'GET' });
1077
+ const watchResponse = await fetch(watchRequest);
1078
+ if (!watchResponse.ok) {
1079
+ console.error(`GET /brains/runs/${brainRunId}/watch returned ${watchResponse.status}`);
1080
+ return false;
1081
+ }
1082
+ // Read SSE events until we get WEBHOOK or COMPLETE/ERROR
1083
+ const events = [];
1084
+ if (watchResponse.body) {
1085
+ const reader = watchResponse.body.getReader();
1086
+ const decoder = new TextDecoder();
1087
+ let buffer = '';
1088
+ let done = false;
1089
+ try {
1090
+ while (!done) {
1091
+ const { value, done: streamDone } = await reader.read();
1092
+ if (streamDone)
1093
+ break;
1094
+ buffer += decoder.decode(value, { stream: true });
1095
+ // Process complete SSE messages
1096
+ let eventEndIndex;
1097
+ while ((eventEndIndex = buffer.indexOf('\n\n')) !== -1) {
1098
+ const message = buffer.substring(0, eventEndIndex);
1099
+ buffer = buffer.substring(eventEndIndex + 2);
1100
+ if (message.startsWith('data: ')) {
1101
+ try {
1102
+ const event = JSON.parse(message.substring(6));
1103
+ events.push(event);
1104
+ // Stop on terminal events
1105
+ if (event.type === BRAIN_EVENTS.WEBHOOK ||
1106
+ event.type === BRAIN_EVENTS.COMPLETE ||
1107
+ event.type === BRAIN_EVENTS.ERROR) {
1108
+ done = true;
1109
+ break;
1110
+ }
1111
+ }
1112
+ catch (e) {
1113
+ // Ignore parse errors
1114
+ }
1115
+ }
1116
+ }
1117
+ }
1118
+ }
1119
+ finally {
1120
+ await reader.cancel();
1121
+ }
1122
+ }
1123
+ // Verify required loop events are present
1124
+ const hasLoopStart = events.some((e) => e.type === BRAIN_EVENTS.LOOP_START);
1125
+ if (!hasLoopStart) {
1126
+ console.error('Missing LOOP_START event in SSE stream');
1127
+ return false;
1128
+ }
1129
+ // Verify LOOP_START has prompt field
1130
+ const loopStartEvent = events.find((e) => e.type === BRAIN_EVENTS.LOOP_START);
1131
+ if (!loopStartEvent.prompt || typeof loopStartEvent.prompt !== 'string') {
1132
+ console.error('LOOP_START event missing prompt field');
1133
+ return false;
1134
+ }
1135
+ const hasLoopIteration = events.some((e) => e.type === BRAIN_EVENTS.LOOP_ITERATION);
1136
+ if (!hasLoopIteration) {
1137
+ console.error('Missing LOOP_ITERATION event in SSE stream');
1138
+ return false;
1139
+ }
1140
+ const hasLoopToolCall = events.some((e) => e.type === BRAIN_EVENTS.LOOP_TOOL_CALL);
1141
+ if (!hasLoopToolCall) {
1142
+ console.error('Missing LOOP_TOOL_CALL event in SSE stream');
1143
+ return false;
1144
+ }
1145
+ // If we got a WEBHOOK event, verify LOOP_WEBHOOK came before it
1146
+ const webhookIndex = events.findIndex((e) => e.type === BRAIN_EVENTS.WEBHOOK);
1147
+ if (webhookIndex !== -1) {
1148
+ const loopWebhookIndex = events.findIndex((e) => e.type === BRAIN_EVENTS.LOOP_WEBHOOK);
1149
+ if (loopWebhookIndex === -1) {
1150
+ console.error('Missing LOOP_WEBHOOK event before WEBHOOK event');
1151
+ return false;
1152
+ }
1153
+ if (loopWebhookIndex >= webhookIndex) {
1154
+ console.error('LOOP_WEBHOOK event must come before WEBHOOK event');
1155
+ return false;
1156
+ }
1157
+ // Verify LOOP_WEBHOOK has required fields
1158
+ const loopWebhookEvent = events[loopWebhookIndex];
1159
+ if (!loopWebhookEvent.toolCallId || !loopWebhookEvent.toolName) {
1160
+ console.error('LOOP_WEBHOOK event missing toolCallId or toolName fields');
1161
+ return false;
1162
+ }
1163
+ }
1164
+ return true;
1165
+ }
1166
+ catch (error) {
1167
+ console.error(`Failed to test loop events for ${loopBrainIdentifier}:`, error);
1168
+ return false;
1169
+ }
1170
+ },
1171
+ /**
1172
+ * Test full loop webhook resumption flow:
1173
+ * 1. Start a loop brain that will pause on a webhook
1174
+ * 2. Verify it pauses with WEBHOOK event
1175
+ * 3. Trigger the webhook with a response
1176
+ * 4. Verify the brain resumes and emits WEBHOOK_RESPONSE and LOOP_TOOL_RESULT
1177
+ *
1178
+ * Requires:
1179
+ * - A brain with a loop step that calls a tool returning { waitFor: webhook(...) }
1180
+ * - The webhook slug and identifier to trigger
1181
+ */
1182
+ async loopWebhookResume(fetch, loopBrainIdentifier, webhookSlug, webhookPayload) {
1183
+ try {
1184
+ // Step 1: Start the loop brain
1185
+ const runRequest = new Request('http://example.com/brains/runs', {
1186
+ method: 'POST',
1187
+ headers: { 'Content-Type': 'application/json' },
1188
+ body: JSON.stringify({ identifier: loopBrainIdentifier }),
1189
+ });
1190
+ const runResponse = await fetch(runRequest);
1191
+ if (runResponse.status !== 201) {
1192
+ console.error(`POST /brains/runs returned ${runResponse.status}, expected 201`);
1193
+ return false;
1194
+ }
1195
+ const { brainRunId } = (await runResponse.json());
1196
+ // Step 2: Watch until WEBHOOK event (brain pauses)
1197
+ const watchRequest = new Request(`http://example.com/brains/runs/${brainRunId}/watch`, { method: 'GET' });
1198
+ const watchResponse = await fetch(watchRequest);
1199
+ if (!watchResponse.ok) {
1200
+ console.error(`GET /brains/runs/${brainRunId}/watch returned ${watchResponse.status}`);
1201
+ return false;
1202
+ }
1203
+ let foundWebhookEvent = false;
1204
+ if (watchResponse.body) {
1205
+ const reader = watchResponse.body.getReader();
1206
+ const decoder = new TextDecoder();
1207
+ let buffer = '';
1208
+ try {
1209
+ while (!foundWebhookEvent) {
1210
+ const { value, done } = await reader.read();
1211
+ if (done)
1212
+ break;
1213
+ buffer += decoder.decode(value, { stream: true });
1214
+ let eventEndIndex;
1215
+ while ((eventEndIndex = buffer.indexOf('\n\n')) !== -1) {
1216
+ const message = buffer.substring(0, eventEndIndex);
1217
+ buffer = buffer.substring(eventEndIndex + 2);
1218
+ if (message.startsWith('data: ')) {
1219
+ try {
1220
+ const event = JSON.parse(message.substring(6));
1221
+ if (event.type === BRAIN_EVENTS.WEBHOOK) {
1222
+ foundWebhookEvent = true;
1223
+ break;
1224
+ }
1225
+ if (event.type === BRAIN_EVENTS.COMPLETE ||
1226
+ event.type === BRAIN_EVENTS.ERROR) {
1227
+ console.error(`Brain completed/errored before WEBHOOK event: ${event.type}`);
1228
+ return false;
1229
+ }
1230
+ }
1231
+ catch (e) {
1232
+ // Ignore parse errors
1233
+ }
1234
+ }
1235
+ }
1236
+ }
1237
+ }
1238
+ finally {
1239
+ await reader.cancel();
1240
+ }
1241
+ }
1242
+ if (!foundWebhookEvent) {
1243
+ console.error('Brain did not emit WEBHOOK event');
1244
+ return false;
1245
+ }
1246
+ // Step 3: Trigger the webhook
1247
+ const webhookRequest = new Request(`http://example.com/webhooks/${encodeURIComponent(webhookSlug)}`, {
1248
+ method: 'POST',
1249
+ headers: { 'Content-Type': 'application/json' },
1250
+ body: JSON.stringify(webhookPayload),
1251
+ });
1252
+ const webhookResponse = await fetch(webhookRequest);
1253
+ if (!webhookResponse.ok) {
1254
+ console.error(`POST /webhooks/${webhookSlug} returned ${webhookResponse.status}`);
1255
+ return false;
1256
+ }
1257
+ const webhookResult = (await webhookResponse.json());
1258
+ if (!webhookResult.received) {
1259
+ console.error('Webhook was not received');
1260
+ return false;
1261
+ }
1262
+ if (webhookResult.action !== 'resumed') {
1263
+ console.error(`Expected webhook action 'resumed', got '${webhookResult.action}'`);
1264
+ return false;
1265
+ }
1266
+ // Step 4: Watch again for resumed events
1267
+ const resumeWatchRequest = new Request(`http://example.com/brains/runs/${brainRunId}/watch`, { method: 'GET' });
1268
+ const resumeWatchResponse = await fetch(resumeWatchRequest);
1269
+ if (!resumeWatchResponse.ok) {
1270
+ console.error(`GET /brains/runs/${brainRunId}/watch (resume) returned ${resumeWatchResponse.status}`);
1271
+ return false;
1272
+ }
1273
+ const resumeEvents = [];
1274
+ if (resumeWatchResponse.body) {
1275
+ const reader = resumeWatchResponse.body.getReader();
1276
+ const decoder = new TextDecoder();
1277
+ let buffer = '';
1278
+ let done = false;
1279
+ try {
1280
+ while (!done) {
1281
+ const { value, done: streamDone } = await reader.read();
1282
+ if (streamDone)
1283
+ break;
1284
+ buffer += decoder.decode(value, { stream: true });
1285
+ let eventEndIndex;
1286
+ while ((eventEndIndex = buffer.indexOf('\n\n')) !== -1) {
1287
+ const message = buffer.substring(0, eventEndIndex);
1288
+ buffer = buffer.substring(eventEndIndex + 2);
1289
+ if (message.startsWith('data: ')) {
1290
+ try {
1291
+ const event = JSON.parse(message.substring(6));
1292
+ resumeEvents.push(event);
1293
+ if (event.type === BRAIN_EVENTS.COMPLETE ||
1294
+ event.type === BRAIN_EVENTS.ERROR) {
1295
+ done = true;
1296
+ break;
1297
+ }
1298
+ }
1299
+ catch (e) {
1300
+ // Ignore parse errors
1301
+ }
1302
+ }
1303
+ }
1304
+ }
1305
+ }
1306
+ finally {
1307
+ await reader.cancel();
1308
+ }
1309
+ }
1310
+ // Verify WEBHOOK_RESPONSE event is present
1311
+ const hasWebhookResponse = resumeEvents.some((e) => e.type === BRAIN_EVENTS.WEBHOOK_RESPONSE);
1312
+ if (!hasWebhookResponse) {
1313
+ console.error('Missing WEBHOOK_RESPONSE event after resume');
1314
+ return false;
1315
+ }
1316
+ // Verify LOOP_TOOL_RESULT event is present (with the webhook response as result)
1317
+ const hasLoopToolResult = resumeEvents.some((e) => e.type === BRAIN_EVENTS.LOOP_TOOL_RESULT);
1318
+ if (!hasLoopToolResult) {
1319
+ console.error('Missing LOOP_TOOL_RESULT event after resume');
1320
+ return false;
1321
+ }
1322
+ // Verify brain completed successfully
1323
+ const completeEvent = resumeEvents.find((e) => e.type === BRAIN_EVENTS.COMPLETE);
1324
+ if (!completeEvent) {
1325
+ console.error('Brain did not complete after resume');
1326
+ return false;
1327
+ }
1328
+ if (completeEvent.status !== STATUS.COMPLETE) {
1329
+ console.error(`Expected COMPLETE status, got ${completeEvent.status}`);
1330
+ return false;
1331
+ }
1332
+ return true;
1333
+ }
1334
+ catch (error) {
1335
+ console.error(`Failed to test loop webhook resume for ${loopBrainIdentifier}:`, error);
1336
+ return false;
1337
+ }
1338
+ },
870
1339
  };
871
1340
  export const schedules = {
872
1341
  /**