@positronic/cli 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/dist/src/cli.js +739 -0
  2. package/dist/src/commands/backend.js +199 -0
  3. package/dist/src/commands/brain.js +446 -0
  4. package/dist/src/commands/brain.test.js +2936 -0
  5. package/dist/src/commands/helpers.js +1315 -0
  6. package/dist/src/commands/helpers.test.js +832 -0
  7. package/dist/src/commands/project-config-manager.js +197 -0
  8. package/dist/src/commands/project.js +130 -0
  9. package/dist/src/commands/project.test.js +1201 -0
  10. package/dist/src/commands/resources.js +272 -0
  11. package/dist/src/commands/resources.test.js +2511 -0
  12. package/dist/src/commands/schedule.js +73 -0
  13. package/dist/src/commands/schedule.test.js +1235 -0
  14. package/dist/src/commands/secret.js +87 -0
  15. package/dist/src/commands/secret.test.d.js +1 -0
  16. package/dist/src/commands/secret.test.js +761 -0
  17. package/dist/src/commands/server.js +816 -0
  18. package/dist/src/commands/server.test.js +1237 -0
  19. package/dist/src/commands/test-utils.js +737 -0
  20. package/dist/src/components/brain-history.js +169 -0
  21. package/dist/src/components/brain-list.js +108 -0
  22. package/dist/src/components/brain-rerun.js +313 -0
  23. package/dist/src/components/brain-show.js +65 -0
  24. package/dist/src/components/error.js +19 -0
  25. package/dist/src/components/project-add.js +95 -0
  26. package/dist/src/components/project-create.js +276 -0
  27. package/dist/src/components/project-list.js +88 -0
  28. package/dist/src/components/project-remove.js +91 -0
  29. package/dist/src/components/project-select.js +224 -0
  30. package/dist/src/components/project-show.js +41 -0
  31. package/dist/src/components/resource-clear.js +152 -0
  32. package/dist/src/components/resource-delete.js +189 -0
  33. package/dist/src/components/resource-list.js +174 -0
  34. package/dist/src/components/resource-sync.js +386 -0
  35. package/dist/src/components/resource-types.js +243 -0
  36. package/dist/src/components/resource-upload.js +366 -0
  37. package/dist/src/components/schedule-create.js +259 -0
  38. package/dist/src/components/schedule-delete.js +161 -0
  39. package/dist/src/components/schedule-list.js +176 -0
  40. package/dist/src/components/schedule-runs.js +103 -0
  41. package/dist/src/components/secret-bulk.js +262 -0
  42. package/dist/src/components/secret-create.js +199 -0
  43. package/dist/src/components/secret-delete.js +190 -0
  44. package/dist/src/components/secret-list.js +190 -0
  45. package/dist/src/components/secret-sync.js +303 -0
  46. package/dist/src/components/watch.js +184 -0
  47. package/dist/src/hooks/useApi.js +512 -0
  48. package/dist/src/positronic.js +33 -0
  49. package/dist/src/test/mock-api-client.js +371 -0
  50. package/dist/src/test/test-dev-server.js +1376 -0
  51. package/dist/types/cli.d.ts +9 -0
  52. package/dist/types/cli.d.ts.map +1 -0
  53. package/dist/types/commands/backend.d.ts +6 -0
  54. package/dist/types/commands/backend.d.ts.map +1 -0
  55. package/dist/types/commands/brain.d.ts +35 -0
  56. package/dist/types/commands/brain.d.ts.map +1 -0
  57. package/dist/types/commands/helpers.d.ts +55 -0
  58. package/dist/types/commands/helpers.d.ts.map +1 -0
  59. package/dist/types/commands/project-config-manager.d.ts +37 -0
  60. package/dist/types/commands/project-config-manager.d.ts.map +1 -0
  61. package/dist/types/commands/project.d.ts +55 -0
  62. package/dist/types/commands/project.d.ts.map +1 -0
  63. package/dist/types/commands/resources.d.ts +13 -0
  64. package/dist/types/commands/resources.d.ts.map +1 -0
  65. package/dist/types/commands/schedule.d.ts +27 -0
  66. package/dist/types/commands/schedule.d.ts.map +1 -0
  67. package/dist/types/commands/secret.d.ts +23 -0
  68. package/dist/types/commands/secret.d.ts.map +1 -0
  69. package/dist/types/commands/server.d.ts +12 -0
  70. package/dist/types/commands/server.d.ts.map +1 -0
  71. package/dist/types/commands/test-utils.d.ts +45 -0
  72. package/dist/types/commands/test-utils.d.ts.map +1 -0
  73. package/dist/types/components/brain-history.d.ts +7 -0
  74. package/dist/types/components/brain-history.d.ts.map +1 -0
  75. package/dist/types/components/brain-list.d.ts +2 -0
  76. package/dist/types/components/brain-list.d.ts.map +1 -0
  77. package/dist/types/components/brain-rerun.d.ts +9 -0
  78. package/dist/types/components/brain-rerun.d.ts.map +1 -0
  79. package/dist/types/components/brain-show.d.ts +6 -0
  80. package/dist/types/components/brain-show.d.ts.map +1 -0
  81. package/dist/types/components/error.d.ts +10 -0
  82. package/dist/types/components/error.d.ts.map +1 -0
  83. package/dist/types/components/project-add.d.ts +9 -0
  84. package/dist/types/components/project-add.d.ts.map +1 -0
  85. package/dist/types/components/project-create.d.ts +6 -0
  86. package/dist/types/components/project-create.d.ts.map +1 -0
  87. package/dist/types/components/project-list.d.ts +7 -0
  88. package/dist/types/components/project-list.d.ts.map +1 -0
  89. package/dist/types/components/project-remove.d.ts +8 -0
  90. package/dist/types/components/project-remove.d.ts.map +1 -0
  91. package/dist/types/components/project-select.d.ts +8 -0
  92. package/dist/types/components/project-select.d.ts.map +1 -0
  93. package/dist/types/components/project-show.d.ts +7 -0
  94. package/dist/types/components/project-show.d.ts.map +1 -0
  95. package/dist/types/components/resource-clear.d.ts +2 -0
  96. package/dist/types/components/resource-clear.d.ts.map +1 -0
  97. package/dist/types/components/resource-delete.d.ts +9 -0
  98. package/dist/types/components/resource-delete.d.ts.map +1 -0
  99. package/dist/types/components/resource-list.d.ts +2 -0
  100. package/dist/types/components/resource-list.d.ts.map +1 -0
  101. package/dist/types/components/resource-sync.d.ts +8 -0
  102. package/dist/types/components/resource-sync.d.ts.map +1 -0
  103. package/dist/types/components/resource-types.d.ts +7 -0
  104. package/dist/types/components/resource-types.d.ts.map +1 -0
  105. package/dist/types/components/resource-upload.d.ts +8 -0
  106. package/dist/types/components/resource-upload.d.ts.map +1 -0
  107. package/dist/types/components/schedule-create.d.ts +7 -0
  108. package/dist/types/components/schedule-create.d.ts.map +1 -0
  109. package/dist/types/components/schedule-delete.d.ts +7 -0
  110. package/dist/types/components/schedule-delete.d.ts.map +1 -0
  111. package/dist/types/components/schedule-list.d.ts +6 -0
  112. package/dist/types/components/schedule-list.d.ts.map +1 -0
  113. package/dist/types/components/schedule-runs.d.ts +8 -0
  114. package/dist/types/components/schedule-runs.d.ts.map +1 -0
  115. package/dist/types/components/secret-bulk.d.ts +8 -0
  116. package/dist/types/components/secret-bulk.d.ts.map +1 -0
  117. package/dist/types/components/secret-create.d.ts +9 -0
  118. package/dist/types/components/secret-create.d.ts.map +1 -0
  119. package/dist/types/components/secret-delete.d.ts +8 -0
  120. package/dist/types/components/secret-delete.d.ts.map +1 -0
  121. package/dist/types/components/secret-list.d.ts +7 -0
  122. package/dist/types/components/secret-list.d.ts.map +1 -0
  123. package/dist/types/components/secret-sync.d.ts +9 -0
  124. package/dist/types/components/secret-sync.d.ts.map +1 -0
  125. package/dist/types/components/watch.d.ts +7 -0
  126. package/dist/types/components/watch.d.ts.map +1 -0
  127. package/dist/types/hooks/useApi.d.ts +29 -0
  128. package/dist/types/hooks/useApi.d.ts.map +1 -0
  129. package/dist/types/positronic.d.ts +3 -0
  130. package/dist/types/positronic.d.ts.map +1 -0
  131. package/dist/types/test/mock-api-client.d.ts +25 -0
  132. package/dist/types/test/mock-api-client.d.ts.map +1 -0
  133. package/dist/types/test/test-dev-server.d.ts +129 -0
  134. package/dist/types/test/test-dev-server.d.ts.map +1 -0
  135. package/package.json +37 -0
  136. package/src/cli.ts +981 -0
  137. package/src/commands/backend.ts +63 -0
  138. package/src/commands/brain.test.ts +1004 -0
  139. package/src/commands/brain.ts +215 -0
  140. package/src/commands/helpers.test.ts +487 -0
  141. package/src/commands/helpers.ts +870 -0
  142. package/src/commands/project-config-manager.ts +152 -0
  143. package/src/commands/project.test.ts +502 -0
  144. package/src/commands/project.ts +109 -0
  145. package/src/commands/resources.test.ts +1052 -0
  146. package/src/commands/resources.ts +97 -0
  147. package/src/commands/schedule.test.ts +481 -0
  148. package/src/commands/schedule.ts +65 -0
  149. package/src/commands/secret.test.ts +210 -0
  150. package/src/commands/secret.ts +50 -0
  151. package/src/commands/server.test.ts +493 -0
  152. package/src/commands/server.ts +353 -0
  153. package/src/commands/test-utils.ts +324 -0
  154. package/src/components/brain-history.tsx +198 -0
  155. package/src/components/brain-list.tsx +105 -0
  156. package/src/components/brain-rerun.tsx +111 -0
  157. package/src/components/brain-show.tsx +92 -0
  158. package/src/components/error.tsx +24 -0
  159. package/src/components/project-add.tsx +59 -0
  160. package/src/components/project-create.tsx +83 -0
  161. package/src/components/project-list.tsx +83 -0
  162. package/src/components/project-remove.tsx +55 -0
  163. package/src/components/project-select.tsx +200 -0
  164. package/src/components/project-show.tsx +58 -0
  165. package/src/components/resource-clear.tsx +127 -0
  166. package/src/components/resource-delete.tsx +160 -0
  167. package/src/components/resource-list.tsx +177 -0
  168. package/src/components/resource-sync.tsx +170 -0
  169. package/src/components/resource-types.tsx +55 -0
  170. package/src/components/resource-upload.tsx +182 -0
  171. package/src/components/schedule-create.tsx +90 -0
  172. package/src/components/schedule-delete.tsx +116 -0
  173. package/src/components/schedule-list.tsx +186 -0
  174. package/src/components/schedule-runs.tsx +151 -0
  175. package/src/components/secret-bulk.tsx +79 -0
  176. package/src/components/secret-create.tsx +49 -0
  177. package/src/components/secret-delete.tsx +41 -0
  178. package/src/components/secret-list.tsx +41 -0
  179. package/src/components/watch.tsx +155 -0
  180. package/src/hooks/useApi.ts +183 -0
  181. package/src/positronic.ts +40 -0
  182. package/src/test/data/resources/config.json +1 -0
  183. package/src/test/data/resources/data/config.json +1 -0
  184. package/src/test/data/resources/data/logo.png +2 -0
  185. package/src/test/data/resources/docs/api.md +3 -0
  186. package/src/test/data/resources/docs/readme.md +3 -0
  187. package/src/test/data/resources/example.md +3 -0
  188. package/src/test/data/resources/file with spaces.txt +1 -0
  189. package/src/test/data/resources/readme.md +3 -0
  190. package/src/test/data/resources/test.txt +1 -0
  191. package/src/test/mock-api-client.ts +145 -0
  192. package/src/test/test-dev-server.ts +1003 -0
  193. package/tsconfig.json +11 -0
@@ -0,0 +1,1004 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { createTestEnv, px } from './test-utils.js';
3
+ import nock from 'nock';
4
+
5
+ describe('CLI Integration: positronic brain commands', () => {
6
+ describe('brain run command', () => {
7
+ it('should successfully run a brain and return run ID', async () => {
8
+ const env = await createTestEnv();
9
+ const px = await env.start();
10
+
11
+ try {
12
+ const { waitForOutput } = await px(['run', 'test-brain']);
13
+ const isOutputRendered = await waitForOutput(/Run ID: run-\d+/);
14
+ expect(isOutputRendered).toBe(true);
15
+ } finally {
16
+ await env.stopAndCleanup();
17
+ }
18
+ });
19
+
20
+ it('should run a brain with watch option', async () => {
21
+ const env = await createTestEnv();
22
+ const px = await env.start();
23
+
24
+ try {
25
+ const { waitForOutput, instance } = await px(['run', 'test-brain', '--watch']);
26
+ // The watch component should be rendered - first shows connecting message
27
+ const isOutputRendered = await waitForOutput(
28
+ /Connecting to watch service|Brain: test-brain/
29
+ );
30
+ expect(isOutputRendered).toBe(true);
31
+
32
+ // Unmount the component to trigger EventSource cleanup
33
+ instance.unmount();
34
+ } finally {
35
+ await env.stopAndCleanup();
36
+ }
37
+ });
38
+
39
+ it('should run a brain with short watch flag', async () => {
40
+ const env = await createTestEnv();
41
+ const px = await env.start();
42
+
43
+ try {
44
+ const { waitForOutput, instance } = await px(['run', 'test-brain', '-w']);
45
+ // The watch component should be rendered - first shows connecting message
46
+ const isOutputRendered = await waitForOutput(
47
+ /Connecting to watch service|Brain: test-brain/
48
+ );
49
+ expect(isOutputRendered).toBe(true);
50
+
51
+ // Unmount the component to trigger EventSource cleanup
52
+ instance.unmount();
53
+ } finally {
54
+ await env.stopAndCleanup();
55
+ }
56
+ });
57
+
58
+
59
+ it('should handle brain not found (404) error with helpful message', async () => {
60
+ const env = await createTestEnv();
61
+ const px = await env.start();
62
+
63
+ try {
64
+ const { waitForOutput } = await px(['run', 'non-existent-brain']);
65
+
66
+ // Check for error component output
67
+ const foundErrorTitle = await waitForOutput(/Brain Not Found/i, 30);
68
+ expect(foundErrorTitle).toBe(true);
69
+
70
+ const foundErrorMessage = await waitForOutput(/Brain 'non-existent-brain' not found/i, 30);
71
+ expect(foundErrorMessage).toBe(true);
72
+
73
+ const foundHelpText = await waitForOutput(/brain name is spelled correctly/i, 30);
74
+ expect(foundHelpText).toBe(true);
75
+
76
+ // Verify the API was called
77
+ const calls = env.server.getLogs();
78
+ const runCall = calls.find(c => c.method === 'createBrainRun');
79
+ expect(runCall).toBeDefined();
80
+ expect(runCall?.args[0]).toBe('non-existent-brain');
81
+ } finally {
82
+ await env.stopAndCleanup();
83
+ }
84
+ });
85
+
86
+ it('should handle API server error responses', async () => {
87
+ const env = await createTestEnv();
88
+ const px = await env.start();
89
+
90
+ try {
91
+ // Clear all existing nock interceptors to avoid conflicts
92
+ nock.cleanAll();
93
+
94
+ // Mock the server to return a 500 error
95
+ const port = env.server.port;
96
+ nock(`http://localhost:${port}`)
97
+ .post('/brains/runs')
98
+ .reply(500, 'Internal Server Error');
99
+
100
+ // Mock process.exit to prevent test from exiting
101
+ const originalExit = process.exit;
102
+ let exitCalled = false;
103
+ process.exit = ((code?: number) => {
104
+ exitCalled = true;
105
+ throw new Error(`process.exit called with code ${code}`);
106
+ }) as any;
107
+
108
+ try {
109
+ await expect(px(['run', 'test-brain'])).rejects.toThrow(
110
+ 'process.exit called with code 1'
111
+ );
112
+ expect(exitCalled).toBe(true);
113
+ } finally {
114
+ process.exit = originalExit;
115
+ }
116
+ } finally {
117
+ await env.stopAndCleanup();
118
+ }
119
+ });
120
+
121
+ it('should handle network connection errors (ECONNREFUSED)', async () => {
122
+ const env = await createTestEnv();
123
+ const px = await env.start();
124
+
125
+ try {
126
+ // Clear all existing nock interceptors to avoid conflicts
127
+ nock.cleanAll();
128
+
129
+ // Mock a connection refused error
130
+ const port = env.server.port;
131
+ nock(`http://localhost:${port}`).post('/brains/runs').replyWithError({
132
+ message: 'connect ECONNREFUSED',
133
+ code: 'ECONNREFUSED',
134
+ });
135
+
136
+ // Mock process.exit to prevent test from exiting
137
+ const originalExit = process.exit;
138
+ let exitCalled = false;
139
+ process.exit = ((code?: number) => {
140
+ exitCalled = true;
141
+ throw new Error(`process.exit called with code ${code}`);
142
+ }) as any;
143
+
144
+ try {
145
+ await expect(px(['run', 'test-brain'])).rejects.toThrow(
146
+ 'process.exit called with code 1'
147
+ );
148
+ expect(exitCalled).toBe(true);
149
+ } finally {
150
+ process.exit = originalExit;
151
+ }
152
+ } finally {
153
+ await env.stopAndCleanup();
154
+ }
155
+ });
156
+
157
+ it('should handle other network errors', async () => {
158
+ const env = await createTestEnv();
159
+ const px = await env.start();
160
+
161
+ try {
162
+ // Clear all existing nock interceptors to avoid conflicts
163
+ nock.cleanAll();
164
+
165
+ // Mock a different network error (without ECONNREFUSED code)
166
+ const port = env.server.port;
167
+ nock(`http://localhost:${port}`)
168
+ .post('/brains/runs')
169
+ .replyWithError({
170
+ message: 'Network timeout error occurred',
171
+ code: 'TIMEOUT',
172
+ });
173
+
174
+ // Mock process.exit to prevent test from exiting
175
+ const originalExit = process.exit;
176
+ let exitCalled = false;
177
+ process.exit = ((code?: number) => {
178
+ exitCalled = true;
179
+ throw new Error(`process.exit called with code ${code}`);
180
+ }) as any;
181
+
182
+ try {
183
+ await expect(px(['run', 'test-brain'])).rejects.toThrow(
184
+ 'process.exit called with code 1'
185
+ );
186
+ expect(exitCalled).toBe(true);
187
+ } finally {
188
+ process.exit = originalExit;
189
+ }
190
+ } finally {
191
+ await env.stopAndCleanup();
192
+ }
193
+ });
194
+
195
+ it('should handle network errors with specific error message', async () => {
196
+ const env = await createTestEnv();
197
+ const px = await env.start();
198
+
199
+ try {
200
+ // Clear all existing nock interceptors to avoid conflicts
201
+ nock.cleanAll();
202
+
203
+ // Mock a network error with a specific message (not ECONNREFUSED)
204
+ const port = env.server.port;
205
+ nock(`http://localhost:${port}`)
206
+ .post('/brains/runs')
207
+ .replyWithError(new Error('DNS resolution failed'));
208
+
209
+ // Mock process.exit to prevent test from exiting
210
+ const originalExit = process.exit;
211
+ let exitCalled = false;
212
+ process.exit = ((code?: number) => {
213
+ exitCalled = true;
214
+ throw new Error(`process.exit called with code ${code}`);
215
+ }) as any;
216
+
217
+ try {
218
+ await expect(px(['run', 'test-brain'])).rejects.toThrow(
219
+ 'process.exit called with code 1'
220
+ );
221
+ expect(exitCalled).toBe(true);
222
+ } finally {
223
+ process.exit = originalExit;
224
+ }
225
+ } finally {
226
+ await env.stopAndCleanup();
227
+ }
228
+ });
229
+ });
230
+
231
+ describe('brain watch command', () => {
232
+ it('should watch a brain run by run ID', async () => {
233
+ const env = await createTestEnv();
234
+ const px = await env.start();
235
+
236
+ try {
237
+ const { waitForOutput, instance } = await px([
238
+ 'watch',
239
+ '--run-id',
240
+ 'test-run-123',
241
+ ]);
242
+ const isOutputRendered = await waitForOutput(
243
+ /Connecting to watch service|Brain: test-brain/
244
+ );
245
+ expect(isOutputRendered).toBe(true);
246
+
247
+ // Unmount the component to trigger EventSource cleanup
248
+ instance.unmount();
249
+ } finally {
250
+ await env.stopAndCleanup();
251
+ }
252
+ });
253
+
254
+ it('should watch a brain run by run ID using short flag', async () => {
255
+ const env = await createTestEnv();
256
+ const px = await env.start();
257
+
258
+ try {
259
+ const { waitForOutput, instance } = await px(['watch', '--id', 'test-run-456']);
260
+ const isOutputRendered = await waitForOutput(
261
+ /Connecting to watch service|Brain: test-brain/
262
+ );
263
+ expect(isOutputRendered).toBe(true);
264
+
265
+ // Unmount the component to trigger EventSource cleanup
266
+ instance.unmount();
267
+ } finally {
268
+ await env.stopAndCleanup();
269
+ }
270
+ });
271
+
272
+ it('should watch the single active run when watching by brain name', async () => {
273
+ const env = await createTestEnv();
274
+ const { server } = env;
275
+
276
+ // Add a running brain run
277
+ server.addBrainRun({
278
+ brainRunId: 'run-active-123',
279
+ brainTitle: 'test brain',
280
+ type: 'START',
281
+ status: 'RUNNING',
282
+ createdAt: Date.now() - 60000, // 1 minute ago
283
+ startedAt: Date.now() - 60000,
284
+ });
285
+
286
+ const px = await env.start();
287
+
288
+ try {
289
+ const { waitForOutput, instance } = await px(['watch', 'test-brain']);
290
+
291
+ // Should connect to watch the active run
292
+ const isOutputRendered = await waitForOutput(
293
+ /Connecting to watch service|Brain: test-brain/
294
+ );
295
+ expect(isOutputRendered).toBe(true);
296
+
297
+ // Verify API was called to get active runs
298
+ const calls = server.getLogs();
299
+ const activeRunsCall = calls.find(c => c.method === 'getBrainActiveRuns');
300
+ expect(activeRunsCall).toBeDefined();
301
+ expect(activeRunsCall?.args[0]).toBe('test-brain');
302
+
303
+ // Unmount the component to trigger EventSource cleanup
304
+ instance.unmount();
305
+ } finally {
306
+ await env.stopAndCleanup();
307
+ }
308
+ });
309
+
310
+ it('should show error when no active runs found for brain name', async () => {
311
+ const env = await createTestEnv();
312
+ const px = await env.start();
313
+
314
+ try {
315
+ const { waitForOutput } = await px(['watch', 'test-brain']);
316
+
317
+ // Should show no active runs error
318
+ const foundTitle = await waitForOutput(/No Active Runs/i, 30);
319
+ expect(foundTitle).toBe(true);
320
+
321
+ const foundMessage = await waitForOutput(/No currently running brain runs found for brain "test-brain"/i, 30);
322
+ expect(foundMessage).toBe(true);
323
+
324
+ const foundDetails = await waitForOutput(/positronic run test-brain/i, 30);
325
+ expect(foundDetails).toBe(true);
326
+
327
+ // Verify API was called
328
+ const calls = env.server.getLogs();
329
+ const activeRunsCall = calls.find(c => c.method === 'getBrainActiveRuns');
330
+ expect(activeRunsCall).toBeDefined();
331
+ } finally {
332
+ await env.stopAndCleanup();
333
+ }
334
+ });
335
+
336
+ it('should show error when multiple active runs found for brain name', async () => {
337
+ const env = await createTestEnv();
338
+ const { server } = env;
339
+
340
+ // Add multiple running brain runs
341
+ server.addBrainRun({
342
+ brainRunId: 'run-active-1',
343
+ brainTitle: 'test brain',
344
+ type: 'START',
345
+ status: 'RUNNING',
346
+ createdAt: Date.now() - 120000, // 2 minutes ago
347
+ startedAt: Date.now() - 120000,
348
+ });
349
+
350
+ server.addBrainRun({
351
+ brainRunId: 'run-active-2',
352
+ brainTitle: 'test brain',
353
+ type: 'START',
354
+ status: 'RUNNING',
355
+ createdAt: Date.now() - 60000, // 1 minute ago
356
+ startedAt: Date.now() - 60000,
357
+ });
358
+
359
+ const px = await env.start();
360
+
361
+ try {
362
+ const { waitForOutput } = await px(['watch', 'test-brain']);
363
+
364
+ // Should show multiple active runs error
365
+ const foundTitle = await waitForOutput(/Multiple Active Runs/i, 30);
366
+ expect(foundTitle).toBe(true);
367
+
368
+ const foundMessage = await waitForOutput(/Found 2 active runs for brain "test-brain"/i, 30);
369
+ expect(foundMessage).toBe(true);
370
+
371
+ const foundDetails = await waitForOutput(/positronic watch --run-id run-active-/i, 30);
372
+ expect(foundDetails).toBe(true);
373
+ } finally {
374
+ await env.stopAndCleanup();
375
+ }
376
+ });
377
+
378
+ it('should handle API errors when looking up active runs', async () => {
379
+ const env = await createTestEnv();
380
+ const px = await env.start();
381
+
382
+ try {
383
+ // Clear all existing nock interceptors to avoid conflicts
384
+ nock.cleanAll();
385
+
386
+ // Mock the server to return a 500 error for active-runs endpoint
387
+ const port = env.server.port;
388
+ nock(`http://localhost:${port}`)
389
+ .get(/^\/brains\/(.+)\/active-runs$/)
390
+ .reply(500, 'Internal Server Error');
391
+
392
+ const { waitForOutput } = await px(['watch', 'test-brain']);
393
+
394
+ // Should show API error
395
+ const foundTitle = await waitForOutput(/API Error/i, 30);
396
+ expect(foundTitle).toBe(true);
397
+
398
+ const foundMessage = await waitForOutput(/Failed to get active runs for brain "test-brain"/i, 30);
399
+ expect(foundMessage).toBe(true);
400
+
401
+ const foundDetails = await waitForOutput(/Server returned 500/i, 30);
402
+ expect(foundDetails).toBe(true);
403
+ } finally {
404
+ await env.stopAndCleanup();
405
+ }
406
+ });
407
+
408
+ it('should handle connection errors when looking up active runs', async () => {
409
+ const env = await createTestEnv();
410
+ // Don't start the server to simulate connection error
411
+
412
+ try {
413
+ const { waitForOutput } = await px(['watch', 'test-brain'], { server: env.server });
414
+
415
+ // Should show connection error
416
+ const foundTitle = await waitForOutput(/Connection Error/i, 30);
417
+ expect(foundTitle).toBe(true);
418
+
419
+ const foundMessage = await waitForOutput(/Error connecting to the local development server/i, 30);
420
+ expect(foundMessage).toBe(true);
421
+
422
+ const foundDetails = await waitForOutput(/positronic server/i, 30);
423
+ expect(foundDetails).toBe(true);
424
+ } finally {
425
+ env.cleanup();
426
+ }
427
+ });
428
+
429
+ it('should show error when no run ID or brain name provided', async () => {
430
+ const env = await createTestEnv();
431
+ const px = await env.start();
432
+
433
+ try {
434
+ // This will throw an error during yargs validation
435
+ await expect(px(['watch'])).rejects.toThrow(
436
+ 'You must provide either a brain name or a --run-id'
437
+ );
438
+ } finally {
439
+ await env.stopAndCleanup();
440
+ }
441
+ });
442
+
443
+
444
+ it('should display all step statuses correctly', async () => {
445
+ const env = await createTestEnv();
446
+ const px = await env.start();
447
+
448
+ try {
449
+ const { waitForOutput, instance } = await px([
450
+ 'watch',
451
+ '--run-id',
452
+ 'test-multi-status',
453
+ ]);
454
+
455
+ // Check for all different step statuses
456
+ const foundComplete = await waitForOutput(/✔.*Complete Step/);
457
+ expect(foundComplete).toBe(true);
458
+
459
+ const foundError = await waitForOutput(/•.*Error Step/);
460
+ expect(foundError).toBe(true);
461
+
462
+ const foundRunning = await waitForOutput(/•.*Running Step/);
463
+ expect(foundRunning).toBe(true);
464
+
465
+ const foundPending = await waitForOutput(/•.*Pending Step/);
466
+ expect(foundPending).toBe(true);
467
+
468
+ // Unmount the component to trigger EventSource cleanup
469
+ instance.unmount();
470
+ } finally {
471
+ await env.stopAndCleanup();
472
+ }
473
+ });
474
+
475
+ });
476
+
477
+ describe('brain list command', () => {
478
+ it('should list brains when no brains exist', async () => {
479
+ const env = await createTestEnv();
480
+ const px = await env.start();
481
+
482
+ try {
483
+ const { waitForOutput } = await px(['list']);
484
+
485
+ // Wait for the empty state message
486
+ const foundEmpty = await waitForOutput(/No brains found/i, 30);
487
+ expect(foundEmpty).toBe(true);
488
+
489
+ // Verify API call was made
490
+ const calls = env.server.getLogs();
491
+ const listCall = calls.find(c => c.method === 'getBrains');
492
+ expect(listCall).toBeDefined();
493
+ } finally {
494
+ await env.stopAndCleanup();
495
+ }
496
+ });
497
+
498
+ it('should list brains when brains exist', async () => {
499
+ const env = await createTestEnv();
500
+ const { server } = env;
501
+
502
+ // Add test brains before starting
503
+ server.addBrain({
504
+ name: 'daily-report',
505
+ title: 'Daily Report Generator',
506
+ description: 'Generates daily reports from various data sources',
507
+ createdAt: Date.now() - 86400000,
508
+ lastModified: Date.now() - 3600000,
509
+ });
510
+
511
+ server.addBrain({
512
+ name: 'data-processor',
513
+ title: 'Data Processing Pipeline',
514
+ description: 'Processes incoming data and transforms it',
515
+ createdAt: Date.now() - 172800000,
516
+ lastModified: Date.now() - 7200000,
517
+ });
518
+
519
+ const px = await env.start();
520
+
521
+ try {
522
+ const { waitForOutput, instance } = await px(['list']);
523
+
524
+ // Wait for brains to appear
525
+ const foundBrains = await waitForOutput(/daily-report/i, 30);
526
+ expect(foundBrains).toBe(true);
527
+
528
+ // Check that all data is shown
529
+ const output = instance.lastFrame() || '';
530
+ expect(output).toContain('daily-report');
531
+ expect(output).toContain('Daily Report Generator');
532
+ expect(output).toContain('data-processor');
533
+ expect(output).toContain('Data Processing Pipeline');
534
+
535
+ // Verify API call
536
+ const calls = server.getLogs();
537
+ const listCall = calls.find(c => c.method === 'getBrains');
538
+ expect(listCall).toBeDefined();
539
+ } finally {
540
+ await env.stopAndCleanup();
541
+ }
542
+ });
543
+
544
+ it('should handle API errors gracefully', async () => {
545
+ const env = await createTestEnv();
546
+ // Don't start the server to simulate connection error
547
+ const { waitForOutput } = await px(['list'], { server: env.server });
548
+
549
+ try {
550
+ const foundError = await waitForOutput(/Error connecting to the local development server/i, 30);
551
+ expect(foundError).toBe(true);
552
+ } finally {
553
+ env.cleanup();
554
+ }
555
+ });
556
+ });
557
+
558
+ describe('brain history command', () => {
559
+ it('should show empty history when no runs exist', async () => {
560
+ const env = await createTestEnv();
561
+ const px = await env.start();
562
+
563
+ try {
564
+ const { waitForOutput } = await px(['history', 'test-brain']);
565
+ const foundMessage = await waitForOutput(/No run history found for brain: test-brain/i, 30);
566
+ expect(foundMessage).toBe(true);
567
+
568
+ // Verify API call was made
569
+ const calls = env.server.getLogs();
570
+ const historyCall = calls.find(c => c.method === 'getBrainHistory');
571
+ expect(historyCall).toBeDefined();
572
+ expect(historyCall?.args[0]).toBe('test-brain');
573
+ } finally {
574
+ await env.stopAndCleanup();
575
+ }
576
+ });
577
+
578
+ it('should show brain run history when runs exist', async () => {
579
+ const env = await createTestEnv();
580
+ const { server } = env;
581
+
582
+ // Add some brain runs to history
583
+ server.addBrainRun({
584
+ brainRunId: 'run-123',
585
+ brainTitle: 'Test Brain',
586
+ brainDescription: 'A test brain',
587
+ type: 'START',
588
+ status: 'COMPLETE',
589
+ createdAt: Date.now() - 3600000, // 1 hour ago
590
+ startedAt: Date.now() - 3600000,
591
+ completedAt: Date.now() - 3540000, // 1 minute duration
592
+ });
593
+
594
+ server.addBrainRun({
595
+ brainRunId: 'run-456',
596
+ brainTitle: 'Test Brain',
597
+ type: 'START',
598
+ status: 'ERROR',
599
+ error: { message: 'Connection failed' },
600
+ createdAt: Date.now() - 7200000, // 2 hours ago
601
+ startedAt: Date.now() - 7200000,
602
+ });
603
+
604
+ const px = await env.start();
605
+
606
+ try {
607
+ const { waitForOutput } = await px(['history', 'test-brain']);
608
+
609
+ // Check for header
610
+ const foundHeader = await waitForOutput(/Recent runs for brain "test-brain"/i, 30);
611
+ expect(foundHeader).toBe(true);
612
+
613
+ // Check for run IDs
614
+ const foundRun1 = await waitForOutput(/run-123/i, 30);
615
+ expect(foundRun1).toBe(true);
616
+
617
+ const foundRun2 = await waitForOutput(/run-456/i, 30);
618
+ expect(foundRun2).toBe(true);
619
+
620
+ // Check for statuses
621
+ const foundComplete = await waitForOutput(/COMPLETE/i, 30);
622
+ expect(foundComplete).toBe(true);
623
+
624
+ const foundError = await waitForOutput(/ERROR/i, 30);
625
+ expect(foundError).toBe(true);
626
+ } finally {
627
+ await env.stopAndCleanup();
628
+ }
629
+ });
630
+
631
+ it('should respect custom limit parameter', async () => {
632
+ const env = await createTestEnv();
633
+ const px = await env.start();
634
+
635
+ try {
636
+ const { waitForOutput } = await px([
637
+ 'history',
638
+ 'test-brain',
639
+ '--limit',
640
+ '20',
641
+ ]);
642
+
643
+ // Wait for the component to render
644
+ await waitForOutput(/run history|Recent runs/i, 30);
645
+
646
+ // Verify API was called with correct limit
647
+ const calls = env.server.getLogs();
648
+ const historyCall = calls.find(c => c.method === 'getBrainHistory');
649
+ expect(historyCall).toBeDefined();
650
+ expect(historyCall?.args[1]).toBe(20);
651
+ } finally {
652
+ await env.stopAndCleanup();
653
+ }
654
+ });
655
+
656
+ it('should show error details for failed runs', async () => {
657
+ const env = await createTestEnv();
658
+ const { server } = env;
659
+
660
+ // Add a failed brain run
661
+ server.addBrainRun({
662
+ brainRunId: 'run-error',
663
+ brainTitle: 'Test Brain',
664
+ type: 'START',
665
+ status: 'ERROR',
666
+ error: 'Connection timeout',
667
+ createdAt: Date.now() - 1800000,
668
+ startedAt: Date.now() - 1800000,
669
+ });
670
+
671
+ const px = await env.start();
672
+
673
+ try {
674
+ const { waitForOutput } = await px(['history', 'test-brain']);
675
+
676
+ // Check for error section
677
+ const foundErrors = await waitForOutput(/Errors:/i, 30);
678
+ expect(foundErrors).toBe(true);
679
+
680
+ // Check for error message
681
+ const foundErrorMsg = await waitForOutput(/Connection timeout/i, 30);
682
+ expect(foundErrorMsg).toBe(true);
683
+ } finally {
684
+ await env.stopAndCleanup();
685
+ }
686
+ });
687
+
688
+ it('should handle server connection errors', async () => {
689
+ const env = await createTestEnv();
690
+ // Don't start the server to simulate connection error
691
+
692
+ try {
693
+ const { waitForOutput } = await px(['history', 'test-brain'], { server: env.server });
694
+
695
+ const foundError = await waitForOutput(/Error connecting to the local development server/i, 30);
696
+ expect(foundError).toBe(true);
697
+ } finally {
698
+ env.cleanup();
699
+ }
700
+ });
701
+
702
+ it('should handle API server errors', async () => {
703
+ const env = await createTestEnv();
704
+ const px = await env.start();
705
+
706
+ try {
707
+ // Clear all existing nock interceptors to avoid conflicts
708
+ nock.cleanAll();
709
+
710
+ // Mock the server to return a 500 error
711
+ const port = env.server.port;
712
+ nock(`http://localhost:${port}`)
713
+ .get(/^\/brains\/(.+)\/history/)
714
+ .reply(500, 'Internal Server Error');
715
+
716
+ const { waitForOutput } = await px(['history', 'test-brain']);
717
+
718
+ // The ErrorComponent will display the error
719
+ const foundError = await waitForOutput(/Error:|Failed|500/i, 30);
720
+ expect(foundError).toBe(true);
721
+ } finally {
722
+ await env.stopAndCleanup();
723
+ }
724
+ });
725
+ });
726
+
727
+ describe('brain show command', () => {
728
+ it('should show brain structure when brain exists', async () => {
729
+ const env = await createTestEnv();
730
+ const { server } = env;
731
+
732
+ // Add a brain to the test server with structure
733
+ server.addBrain({
734
+ name: 'test-brain',
735
+ title: 'Test Brain',
736
+ description: 'A test brain for unit testing',
737
+ steps: [
738
+ {
739
+ type: 'step',
740
+ title: 'Initialize',
741
+ },
742
+ {
743
+ type: 'step',
744
+ title: 'Process Data',
745
+ },
746
+ {
747
+ type: 'brain',
748
+ title: 'Nested Analysis',
749
+ innerBrain: {
750
+ title: 'Inner Brain',
751
+ description: 'Performs nested analysis',
752
+ steps: [
753
+ {
754
+ type: 'step',
755
+ title: 'Analyze Subset',
756
+ },
757
+ ],
758
+ },
759
+ },
760
+ ],
761
+ });
762
+
763
+ const px = await env.start();
764
+
765
+ try {
766
+ const { waitForOutput } = await px(['show', 'test-brain']);
767
+
768
+ // Check for brain title
769
+ const foundTitle = await waitForOutput(/Test Brain/, 30);
770
+ expect(foundTitle).toBe(true);
771
+
772
+ // Check for description
773
+ const foundDescription = await waitForOutput(/A test brain for unit testing/, 30);
774
+ expect(foundDescription).toBe(true);
775
+
776
+ // Check for steps
777
+ const foundSteps = await waitForOutput(/• Initialize/, 30);
778
+ expect(foundSteps).toBe(true);
779
+ } finally {
780
+ await env.stopAndCleanup();
781
+ }
782
+ });
783
+
784
+ it('should show error when brain does not exist', async () => {
785
+ const env = await createTestEnv();
786
+ const px = await env.start();
787
+
788
+ try {
789
+ const { waitForOutput } = await px(['show', 'non-existent-brain']);
790
+ const foundError = await waitForOutput(/Brain 'non-existent-brain' not found/, 30);
791
+ expect(foundError).toBe(true);
792
+ } finally {
793
+ await env.stopAndCleanup();
794
+ }
795
+ });
796
+ });
797
+
798
+ describe('brain rerun command', () => {
799
+ it('should successfully rerun a brain without specific run ID', async () => {
800
+ const env = await createTestEnv();
801
+ const px = await env.start();
802
+
803
+ try {
804
+ const { waitForOutput } = await px(['rerun', 'test-brain']);
805
+
806
+ // Check for success message
807
+ const foundSuccess = await waitForOutput(/Brain rerun started successfully/i, 30);
808
+ expect(foundSuccess).toBe(true);
809
+
810
+ // Check for new run ID
811
+ const foundRunId = await waitForOutput(/New run ID:.*rerun-/i, 30);
812
+ expect(foundRunId).toBe(true);
813
+
814
+ // Check for descriptive text
815
+ const foundDescription = await waitForOutput(/Rerunning brain "test-brain"/i, 30);
816
+ expect(foundDescription).toBe(true);
817
+
818
+ // Check for watch command suggestion
819
+ const foundWatchSuggestion = await waitForOutput(/Watch the run with: positronic watch --run-id/i, 30);
820
+ expect(foundWatchSuggestion).toBe(true);
821
+
822
+ // Verify API call
823
+ const calls = env.server.getLogs();
824
+ const rerunCall = calls.find(c => c.method === 'rerunBrain');
825
+ expect(rerunCall).toBeDefined();
826
+ expect(rerunCall?.args[0]).toBe('test-brain');
827
+ expect(rerunCall?.args[1]).toBeUndefined(); // no runId
828
+ } finally {
829
+ await env.stopAndCleanup();
830
+ }
831
+ });
832
+
833
+ it('should successfully rerun a brain with specific run ID', async () => {
834
+ const env = await createTestEnv();
835
+ const px = await env.start();
836
+
837
+ try {
838
+ const { waitForOutput } = await px(['rerun', 'test-brain', 'run-123']);
839
+
840
+ // Check for success message
841
+ const foundSuccess = await waitForOutput(/Brain rerun started successfully/i, 30);
842
+ expect(foundSuccess).toBe(true);
843
+
844
+ // Check for run details
845
+ const foundRunDetails = await waitForOutput(/from run run-123/i, 30);
846
+ expect(foundRunDetails).toBe(true);
847
+
848
+ // Verify API call
849
+ const calls = env.server.getLogs();
850
+ const rerunCall = calls.find(c => c.method === 'rerunBrain');
851
+ expect(rerunCall).toBeDefined();
852
+ expect(rerunCall?.args[0]).toBe('test-brain');
853
+ expect(rerunCall?.args[1]).toBe('run-123');
854
+ } finally {
855
+ await env.stopAndCleanup();
856
+ }
857
+ });
858
+
859
+ it('should successfully rerun a brain with step range options', async () => {
860
+ const env = await createTestEnv();
861
+ const px = await env.start();
862
+
863
+ try {
864
+ const { waitForOutput } = await px([
865
+ 'rerun',
866
+ 'test-brain',
867
+ '--starts-at',
868
+ '3',
869
+ '--stops-after',
870
+ '5',
871
+ ]);
872
+
873
+ // Check for success message
874
+ const foundSuccess = await waitForOutput(/Brain rerun started successfully/i, 30);
875
+ expect(foundSuccess).toBe(true);
876
+
877
+ // Check for step range details
878
+ const foundStepRange = await waitForOutput(/starting at step 3, stopping after step 5/i, 30);
879
+ expect(foundStepRange).toBe(true);
880
+
881
+ // Verify API call
882
+ const calls = env.server.getLogs();
883
+ const rerunCall = calls.find(c => c.method === 'rerunBrain');
884
+ expect(rerunCall).toBeDefined();
885
+ expect(rerunCall?.args[0]).toBe('test-brain');
886
+ expect(rerunCall?.args[1]).toBeUndefined(); // no runId
887
+ expect(rerunCall?.args[2]).toBe(3); // startsAt
888
+ expect(rerunCall?.args[3]).toBe(5); // stopsAfter
889
+ } finally {
890
+ await env.stopAndCleanup();
891
+ }
892
+ });
893
+
894
+ it('should handle brain not found error', async () => {
895
+ const env = await createTestEnv();
896
+ const px = await env.start();
897
+
898
+ try {
899
+ const { waitForOutput } = await px(['rerun', 'non-existent-brain']);
900
+
901
+ // Check for error title
902
+ const foundErrorTitle = await waitForOutput(/Brain Rerun Failed/i, 30);
903
+ expect(foundErrorTitle).toBe(true);
904
+
905
+ // Check for error message
906
+ const foundErrorMessage = await waitForOutput(/Brain 'non-existent-brain' not found/i, 30);
907
+ expect(foundErrorMessage).toBe(true);
908
+
909
+ // Check for helpful details
910
+ const foundDetails = await waitForOutput(/positronic brain list/i, 30);
911
+ expect(foundDetails).toBe(true);
912
+ } finally {
913
+ await env.stopAndCleanup();
914
+ }
915
+ });
916
+
917
+ it('should handle run ID not found error', async () => {
918
+ const env = await createTestEnv();
919
+ const px = await env.start();
920
+
921
+ try {
922
+ const { waitForOutput } = await px(['rerun', 'test-brain', 'non-existent-run']);
923
+
924
+ // Check for error title
925
+ const foundErrorTitle = await waitForOutput(/Brain Rerun Failed/i, 30);
926
+ expect(foundErrorTitle).toBe(true);
927
+
928
+ // Check for error message
929
+ const foundErrorMessage = await waitForOutput(/Brain run 'non-existent-run' not found/i, 30);
930
+ expect(foundErrorMessage).toBe(true);
931
+
932
+ // Check for helpful details with runId
933
+ const foundDetails = await waitForOutput(/positronic brain history test-brain/i, 30);
934
+ expect(foundDetails).toBe(true);
935
+ } finally {
936
+ await env.stopAndCleanup();
937
+ }
938
+ });
939
+
940
+ it('should handle server connection errors', async () => {
941
+ const env = await createTestEnv();
942
+ // Don't start the server to simulate connection error
943
+
944
+ try {
945
+ const { waitForOutput } = await px(['rerun', 'test-brain'], { server: env.server });
946
+
947
+ // Check for error title
948
+ const foundErrorTitle = await waitForOutput(/Brain Rerun Failed/i, 30);
949
+ expect(foundErrorTitle).toBe(true);
950
+
951
+ // Check for connection error
952
+ const foundConnectionError = await waitForOutput(/Connection error/i, 30);
953
+ expect(foundConnectionError).toBe(true);
954
+ } finally {
955
+ env.cleanup();
956
+ }
957
+ });
958
+
959
+ it('should handle API server errors', async () => {
960
+ const env = await createTestEnv();
961
+ const px = await env.start();
962
+
963
+ try {
964
+ // Clear all existing nock interceptors to avoid conflicts
965
+ nock.cleanAll();
966
+
967
+ // Mock the server to return a 500 error
968
+ const port = env.server.port;
969
+ nock(`http://localhost:${port}`)
970
+ .post('/brains/runs/rerun')
971
+ .reply(500, 'Internal Server Error');
972
+
973
+ const { waitForOutput } = await px(['rerun', 'test-brain']);
974
+
975
+ // Check for error title
976
+ const foundErrorTitle = await waitForOutput(/Brain Rerun Failed/i, 30);
977
+ expect(foundErrorTitle).toBe(true);
978
+
979
+ // Check for server error
980
+ const foundServerError = await waitForOutput(/Server returned 500/i, 30);
981
+ expect(foundServerError).toBe(true);
982
+ } finally {
983
+ await env.stopAndCleanup();
984
+ }
985
+ });
986
+ });
987
+
988
+
989
+ describe('error handling', () => {
990
+ it('should handle missing brain name for run command', async () => {
991
+ const env = await createTestEnv();
992
+ const px = await env.start();
993
+
994
+ try {
995
+ // This will throw an error during yargs validation
996
+ await expect(px(['run'])).rejects.toThrow(
997
+ 'Not enough non-option arguments: got 0, need at least 1'
998
+ );
999
+ } finally {
1000
+ await env.stopAndCleanup();
1001
+ }
1002
+ });
1003
+ });
1004
+ });