@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.
- package/dist/src/cli.js +739 -0
- package/dist/src/commands/backend.js +199 -0
- package/dist/src/commands/brain.js +446 -0
- package/dist/src/commands/brain.test.js +2936 -0
- package/dist/src/commands/helpers.js +1315 -0
- package/dist/src/commands/helpers.test.js +832 -0
- package/dist/src/commands/project-config-manager.js +197 -0
- package/dist/src/commands/project.js +130 -0
- package/dist/src/commands/project.test.js +1201 -0
- package/dist/src/commands/resources.js +272 -0
- package/dist/src/commands/resources.test.js +2511 -0
- package/dist/src/commands/schedule.js +73 -0
- package/dist/src/commands/schedule.test.js +1235 -0
- package/dist/src/commands/secret.js +87 -0
- package/dist/src/commands/secret.test.d.js +1 -0
- package/dist/src/commands/secret.test.js +761 -0
- package/dist/src/commands/server.js +816 -0
- package/dist/src/commands/server.test.js +1237 -0
- package/dist/src/commands/test-utils.js +737 -0
- package/dist/src/components/brain-history.js +169 -0
- package/dist/src/components/brain-list.js +108 -0
- package/dist/src/components/brain-rerun.js +313 -0
- package/dist/src/components/brain-show.js +65 -0
- package/dist/src/components/error.js +19 -0
- package/dist/src/components/project-add.js +95 -0
- package/dist/src/components/project-create.js +276 -0
- package/dist/src/components/project-list.js +88 -0
- package/dist/src/components/project-remove.js +91 -0
- package/dist/src/components/project-select.js +224 -0
- package/dist/src/components/project-show.js +41 -0
- package/dist/src/components/resource-clear.js +152 -0
- package/dist/src/components/resource-delete.js +189 -0
- package/dist/src/components/resource-list.js +174 -0
- package/dist/src/components/resource-sync.js +386 -0
- package/dist/src/components/resource-types.js +243 -0
- package/dist/src/components/resource-upload.js +366 -0
- package/dist/src/components/schedule-create.js +259 -0
- package/dist/src/components/schedule-delete.js +161 -0
- package/dist/src/components/schedule-list.js +176 -0
- package/dist/src/components/schedule-runs.js +103 -0
- package/dist/src/components/secret-bulk.js +262 -0
- package/dist/src/components/secret-create.js +199 -0
- package/dist/src/components/secret-delete.js +190 -0
- package/dist/src/components/secret-list.js +190 -0
- package/dist/src/components/secret-sync.js +303 -0
- package/dist/src/components/watch.js +184 -0
- package/dist/src/hooks/useApi.js +512 -0
- package/dist/src/positronic.js +33 -0
- package/dist/src/test/mock-api-client.js +371 -0
- package/dist/src/test/test-dev-server.js +1376 -0
- package/dist/types/cli.d.ts +9 -0
- package/dist/types/cli.d.ts.map +1 -0
- package/dist/types/commands/backend.d.ts +6 -0
- package/dist/types/commands/backend.d.ts.map +1 -0
- package/dist/types/commands/brain.d.ts +35 -0
- package/dist/types/commands/brain.d.ts.map +1 -0
- package/dist/types/commands/helpers.d.ts +55 -0
- package/dist/types/commands/helpers.d.ts.map +1 -0
- package/dist/types/commands/project-config-manager.d.ts +37 -0
- package/dist/types/commands/project-config-manager.d.ts.map +1 -0
- package/dist/types/commands/project.d.ts +55 -0
- package/dist/types/commands/project.d.ts.map +1 -0
- package/dist/types/commands/resources.d.ts +13 -0
- package/dist/types/commands/resources.d.ts.map +1 -0
- package/dist/types/commands/schedule.d.ts +27 -0
- package/dist/types/commands/schedule.d.ts.map +1 -0
- package/dist/types/commands/secret.d.ts +23 -0
- package/dist/types/commands/secret.d.ts.map +1 -0
- package/dist/types/commands/server.d.ts +12 -0
- package/dist/types/commands/server.d.ts.map +1 -0
- package/dist/types/commands/test-utils.d.ts +45 -0
- package/dist/types/commands/test-utils.d.ts.map +1 -0
- package/dist/types/components/brain-history.d.ts +7 -0
- package/dist/types/components/brain-history.d.ts.map +1 -0
- package/dist/types/components/brain-list.d.ts +2 -0
- package/dist/types/components/brain-list.d.ts.map +1 -0
- package/dist/types/components/brain-rerun.d.ts +9 -0
- package/dist/types/components/brain-rerun.d.ts.map +1 -0
- package/dist/types/components/brain-show.d.ts +6 -0
- package/dist/types/components/brain-show.d.ts.map +1 -0
- package/dist/types/components/error.d.ts +10 -0
- package/dist/types/components/error.d.ts.map +1 -0
- package/dist/types/components/project-add.d.ts +9 -0
- package/dist/types/components/project-add.d.ts.map +1 -0
- package/dist/types/components/project-create.d.ts +6 -0
- package/dist/types/components/project-create.d.ts.map +1 -0
- package/dist/types/components/project-list.d.ts +7 -0
- package/dist/types/components/project-list.d.ts.map +1 -0
- package/dist/types/components/project-remove.d.ts +8 -0
- package/dist/types/components/project-remove.d.ts.map +1 -0
- package/dist/types/components/project-select.d.ts +8 -0
- package/dist/types/components/project-select.d.ts.map +1 -0
- package/dist/types/components/project-show.d.ts +7 -0
- package/dist/types/components/project-show.d.ts.map +1 -0
- package/dist/types/components/resource-clear.d.ts +2 -0
- package/dist/types/components/resource-clear.d.ts.map +1 -0
- package/dist/types/components/resource-delete.d.ts +9 -0
- package/dist/types/components/resource-delete.d.ts.map +1 -0
- package/dist/types/components/resource-list.d.ts +2 -0
- package/dist/types/components/resource-list.d.ts.map +1 -0
- package/dist/types/components/resource-sync.d.ts +8 -0
- package/dist/types/components/resource-sync.d.ts.map +1 -0
- package/dist/types/components/resource-types.d.ts +7 -0
- package/dist/types/components/resource-types.d.ts.map +1 -0
- package/dist/types/components/resource-upload.d.ts +8 -0
- package/dist/types/components/resource-upload.d.ts.map +1 -0
- package/dist/types/components/schedule-create.d.ts +7 -0
- package/dist/types/components/schedule-create.d.ts.map +1 -0
- package/dist/types/components/schedule-delete.d.ts +7 -0
- package/dist/types/components/schedule-delete.d.ts.map +1 -0
- package/dist/types/components/schedule-list.d.ts +6 -0
- package/dist/types/components/schedule-list.d.ts.map +1 -0
- package/dist/types/components/schedule-runs.d.ts +8 -0
- package/dist/types/components/schedule-runs.d.ts.map +1 -0
- package/dist/types/components/secret-bulk.d.ts +8 -0
- package/dist/types/components/secret-bulk.d.ts.map +1 -0
- package/dist/types/components/secret-create.d.ts +9 -0
- package/dist/types/components/secret-create.d.ts.map +1 -0
- package/dist/types/components/secret-delete.d.ts +8 -0
- package/dist/types/components/secret-delete.d.ts.map +1 -0
- package/dist/types/components/secret-list.d.ts +7 -0
- package/dist/types/components/secret-list.d.ts.map +1 -0
- package/dist/types/components/secret-sync.d.ts +9 -0
- package/dist/types/components/secret-sync.d.ts.map +1 -0
- package/dist/types/components/watch.d.ts +7 -0
- package/dist/types/components/watch.d.ts.map +1 -0
- package/dist/types/hooks/useApi.d.ts +29 -0
- package/dist/types/hooks/useApi.d.ts.map +1 -0
- package/dist/types/positronic.d.ts +3 -0
- package/dist/types/positronic.d.ts.map +1 -0
- package/dist/types/test/mock-api-client.d.ts +25 -0
- package/dist/types/test/mock-api-client.d.ts.map +1 -0
- package/dist/types/test/test-dev-server.d.ts +129 -0
- package/dist/types/test/test-dev-server.d.ts.map +1 -0
- package/package.json +37 -0
- package/src/cli.ts +981 -0
- package/src/commands/backend.ts +63 -0
- package/src/commands/brain.test.ts +1004 -0
- package/src/commands/brain.ts +215 -0
- package/src/commands/helpers.test.ts +487 -0
- package/src/commands/helpers.ts +870 -0
- package/src/commands/project-config-manager.ts +152 -0
- package/src/commands/project.test.ts +502 -0
- package/src/commands/project.ts +109 -0
- package/src/commands/resources.test.ts +1052 -0
- package/src/commands/resources.ts +97 -0
- package/src/commands/schedule.test.ts +481 -0
- package/src/commands/schedule.ts +65 -0
- package/src/commands/secret.test.ts +210 -0
- package/src/commands/secret.ts +50 -0
- package/src/commands/server.test.ts +493 -0
- package/src/commands/server.ts +353 -0
- package/src/commands/test-utils.ts +324 -0
- package/src/components/brain-history.tsx +198 -0
- package/src/components/brain-list.tsx +105 -0
- package/src/components/brain-rerun.tsx +111 -0
- package/src/components/brain-show.tsx +92 -0
- package/src/components/error.tsx +24 -0
- package/src/components/project-add.tsx +59 -0
- package/src/components/project-create.tsx +83 -0
- package/src/components/project-list.tsx +83 -0
- package/src/components/project-remove.tsx +55 -0
- package/src/components/project-select.tsx +200 -0
- package/src/components/project-show.tsx +58 -0
- package/src/components/resource-clear.tsx +127 -0
- package/src/components/resource-delete.tsx +160 -0
- package/src/components/resource-list.tsx +177 -0
- package/src/components/resource-sync.tsx +170 -0
- package/src/components/resource-types.tsx +55 -0
- package/src/components/resource-upload.tsx +182 -0
- package/src/components/schedule-create.tsx +90 -0
- package/src/components/schedule-delete.tsx +116 -0
- package/src/components/schedule-list.tsx +186 -0
- package/src/components/schedule-runs.tsx +151 -0
- package/src/components/secret-bulk.tsx +79 -0
- package/src/components/secret-create.tsx +49 -0
- package/src/components/secret-delete.tsx +41 -0
- package/src/components/secret-list.tsx +41 -0
- package/src/components/watch.tsx +155 -0
- package/src/hooks/useApi.ts +183 -0
- package/src/positronic.ts +40 -0
- package/src/test/data/resources/config.json +1 -0
- package/src/test/data/resources/data/config.json +1 -0
- package/src/test/data/resources/data/logo.png +2 -0
- package/src/test/data/resources/docs/api.md +3 -0
- package/src/test/data/resources/docs/readme.md +3 -0
- package/src/test/data/resources/example.md +3 -0
- package/src/test/data/resources/file with spaces.txt +1 -0
- package/src/test/data/resources/readme.md +3 -0
- package/src/test/data/resources/test.txt +1 -0
- package/src/test/mock-api-client.ts +145 -0
- package/src/test/test-dev-server.ts +1003 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,1003 @@
|
|
|
1
|
+
import type { PositronicDevServer, ServerHandle } from '@positronic/spec';
|
|
2
|
+
import nock from 'nock';
|
|
3
|
+
import { parse } from 'dotenv';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
|
|
6
|
+
interface MockResource {
|
|
7
|
+
key: string;
|
|
8
|
+
type: 'text' | 'binary';
|
|
9
|
+
size: number;
|
|
10
|
+
lastModified: string;
|
|
11
|
+
local?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface MethodCall {
|
|
15
|
+
method: string;
|
|
16
|
+
args: any[];
|
|
17
|
+
timestamp: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Extended ServerHandle interface for testing that includes test-specific methods
|
|
22
|
+
*/
|
|
23
|
+
export interface TestServerHandle extends ServerHandle {
|
|
24
|
+
/**
|
|
25
|
+
* Get the method call logs
|
|
26
|
+
*/
|
|
27
|
+
getLogs(): MethodCall[];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Clear the method call logs
|
|
31
|
+
*/
|
|
32
|
+
clearLogs(): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Mock implementation of TestServerHandle for testing
|
|
37
|
+
* Since we're using nock, there's no real process to manage
|
|
38
|
+
*/
|
|
39
|
+
class MockServerHandle implements TestServerHandle {
|
|
40
|
+
private _killed = false;
|
|
41
|
+
private _errorCallback?: (error: Error) => void;
|
|
42
|
+
private _closeCallback?: (code?: number | null) => void;
|
|
43
|
+
|
|
44
|
+
constructor(
|
|
45
|
+
private stopFn: () => void,
|
|
46
|
+
private getLogsFn: () => MethodCall[],
|
|
47
|
+
private clearLogsFn: () => void,
|
|
48
|
+
private port: number
|
|
49
|
+
) {}
|
|
50
|
+
|
|
51
|
+
onClose(callback: (code?: number | null) => void): void {
|
|
52
|
+
this._closeCallback = callback;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
onError(callback: (error: Error) => void): void {
|
|
56
|
+
this._errorCallback = callback;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
kill(signal?: string): boolean {
|
|
60
|
+
if (!this._killed) {
|
|
61
|
+
this.stopFn();
|
|
62
|
+
this._killed = true;
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get killed(): boolean {
|
|
69
|
+
return this._killed;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async waitUntilReady(maxWaitMs?: number): Promise<boolean> {
|
|
73
|
+
// Test server with nock is always ready immediately
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getLogs(): MethodCall[] {
|
|
78
|
+
return this.getLogsFn();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
clearLogs(): void {
|
|
82
|
+
this.clearLogsFn();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface MockSchedule {
|
|
87
|
+
id: string;
|
|
88
|
+
brainName: string;
|
|
89
|
+
cronExpression: string;
|
|
90
|
+
enabled: boolean;
|
|
91
|
+
createdAt: number;
|
|
92
|
+
nextRunAt?: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface MockScheduleRun {
|
|
96
|
+
id: string;
|
|
97
|
+
scheduleId: string;
|
|
98
|
+
status: 'triggered' | 'failed';
|
|
99
|
+
ranAt: number;
|
|
100
|
+
brainRunId?: string;
|
|
101
|
+
error?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface MockBrain {
|
|
105
|
+
name: string;
|
|
106
|
+
title: string;
|
|
107
|
+
description: string;
|
|
108
|
+
createdAt?: number;
|
|
109
|
+
lastModified?: number;
|
|
110
|
+
steps?: Array<{
|
|
111
|
+
type: 'step' | 'brain';
|
|
112
|
+
title: string;
|
|
113
|
+
innerBrain?: {
|
|
114
|
+
title: string;
|
|
115
|
+
description?: string;
|
|
116
|
+
steps: any[];
|
|
117
|
+
};
|
|
118
|
+
}>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface MockBrainRun {
|
|
122
|
+
brainRunId: string;
|
|
123
|
+
brainTitle: string;
|
|
124
|
+
brainDescription?: string;
|
|
125
|
+
type: string;
|
|
126
|
+
status: 'PENDING' | 'RUNNING' | 'COMPLETE' | 'ERROR';
|
|
127
|
+
options?: any;
|
|
128
|
+
error?: any;
|
|
129
|
+
createdAt: number;
|
|
130
|
+
startedAt?: number;
|
|
131
|
+
completedAt?: number;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface MockSecret {
|
|
135
|
+
name: string;
|
|
136
|
+
value: string;
|
|
137
|
+
createdAt: string;
|
|
138
|
+
updatedAt: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export class TestDevServer implements PositronicDevServer {
|
|
142
|
+
private resources: Map<string, MockResource> = new Map();
|
|
143
|
+
private schedules: Map<string, MockSchedule> = new Map();
|
|
144
|
+
private scheduleRuns: MockScheduleRun[] = [];
|
|
145
|
+
private brains: Map<string, MockBrain> = new Map();
|
|
146
|
+
private brainRuns: MockBrainRun[] = [];
|
|
147
|
+
private secrets: Map<string, MockSecret> = new Map();
|
|
148
|
+
public port: number = 0;
|
|
149
|
+
private callLog: MethodCall[] = [];
|
|
150
|
+
private nockScope: nock.Scope | null = null;
|
|
151
|
+
private logCallbacks: Array<(message: string) => void> = [];
|
|
152
|
+
private errorCallbacks: Array<(message: string) => void> = [];
|
|
153
|
+
private warningCallbacks: Array<(message: string) => void> = [];
|
|
154
|
+
|
|
155
|
+
constructor(public projectRootDir: string = '') {}
|
|
156
|
+
|
|
157
|
+
private logCall(method: string, args: any[]) {
|
|
158
|
+
const call: MethodCall = {
|
|
159
|
+
method,
|
|
160
|
+
args,
|
|
161
|
+
timestamp: Date.now(),
|
|
162
|
+
};
|
|
163
|
+
this.callLog.push(call);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async deploy(config?: any): Promise<void> {
|
|
167
|
+
this.logCall('deploy', [this.projectRootDir, config]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async setup(force?: boolean): Promise<void> {
|
|
171
|
+
this.logCall('setup', [force]);
|
|
172
|
+
// For tests, we don't need to set up .positronic directory
|
|
173
|
+
// Just ensure we're ready to serve
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
public getLogs(): MethodCall[] {
|
|
177
|
+
return this.callLog;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async start(port?: number): Promise<TestServerHandle> {
|
|
181
|
+
this.logCall('start', [port]);
|
|
182
|
+
|
|
183
|
+
this.port = port || 9000 + Math.floor(Math.random() * 1000);
|
|
184
|
+
|
|
185
|
+
process.env.POSITRONIC_PORT = this.port.toString();
|
|
186
|
+
|
|
187
|
+
// Set up nock interceptors for all endpoints
|
|
188
|
+
const nockInstance = nock(`http://localhost:${this.port}`).persist();
|
|
189
|
+
|
|
190
|
+
// GET /resources
|
|
191
|
+
nockInstance.get('/resources').reply(200, () => {
|
|
192
|
+
// Return the current in-memory resource list (populated by uploads)
|
|
193
|
+
const resources = Array.from(this.resources.values());
|
|
194
|
+
return {
|
|
195
|
+
resources,
|
|
196
|
+
truncated: false,
|
|
197
|
+
count: resources.length,
|
|
198
|
+
};
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// POST /resources
|
|
202
|
+
nockInstance.post('/resources').reply(201, (uri, requestBody) => {
|
|
203
|
+
// Convert request body to string using latin1 encoding to preserve binary data
|
|
204
|
+
let bodyString = Buffer.isBuffer(requestBody)
|
|
205
|
+
? requestBody.toString('latin1')
|
|
206
|
+
: typeof requestBody === 'string'
|
|
207
|
+
? requestBody
|
|
208
|
+
: JSON.stringify(requestBody);
|
|
209
|
+
|
|
210
|
+
// Check if the body string appears to be hex-encoded (all characters are hex)
|
|
211
|
+
const isHexEncoded = /^[0-9a-fA-F]+$/.test(bodyString);
|
|
212
|
+
if (isHexEncoded) {
|
|
213
|
+
// Convert hex string back to buffer and then to latin1 string
|
|
214
|
+
const hexBuffer = Buffer.from(bodyString, 'hex');
|
|
215
|
+
bodyString = hexBuffer.toString('latin1');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Attempt to extract the "key" (resource path) and "type" fields from the multipart data
|
|
219
|
+
const keyMatch = bodyString.match(/name="key"\s*\r?\n\r?\n([^\r\n]+)/);
|
|
220
|
+
const typeMatch = bodyString.match(/name="type"\s*\r?\n\r?\n([^\r\n]+)/);
|
|
221
|
+
|
|
222
|
+
const key = keyMatch ? keyMatch[1] : undefined;
|
|
223
|
+
const type = typeMatch ? (typeMatch[1] as 'text' | 'binary') : 'text';
|
|
224
|
+
|
|
225
|
+
if (key) {
|
|
226
|
+
this.resources.set(key, {
|
|
227
|
+
key,
|
|
228
|
+
type,
|
|
229
|
+
size: 0,
|
|
230
|
+
lastModified: new Date().toISOString(),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Log the upload so tests can verify resource sync behavior
|
|
235
|
+
this.logCall('upload', [bodyString]);
|
|
236
|
+
|
|
237
|
+
// Success response
|
|
238
|
+
return '';
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// DELETE /resources (bulk delete all)
|
|
242
|
+
nockInstance.delete('/resources').reply(204, () => {
|
|
243
|
+
this.resources.clear();
|
|
244
|
+
this.logCall('deleteAllResources', []);
|
|
245
|
+
return '';
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// DELETE /resources/:key
|
|
249
|
+
nockInstance.delete(/^\/resources\/(.+)$/).reply((uri) => {
|
|
250
|
+
const match = uri.match(/^\/resources\/(.+)$/);
|
|
251
|
+
if (match) {
|
|
252
|
+
const key = decodeURIComponent(match[1]);
|
|
253
|
+
if (this.resources.has(key)) {
|
|
254
|
+
this.resources.delete(key);
|
|
255
|
+
this.logCall('deleteResource', [key]);
|
|
256
|
+
return [204, ''];
|
|
257
|
+
} else {
|
|
258
|
+
// Check if it was already deleted (idempotent delete)
|
|
259
|
+
const wasDeleted = this.callLog.some(
|
|
260
|
+
(call) => call.method === 'deleteResource' && call.args[0] === key
|
|
261
|
+
);
|
|
262
|
+
if (wasDeleted) {
|
|
263
|
+
// Return success for idempotent delete
|
|
264
|
+
return [204, ''];
|
|
265
|
+
}
|
|
266
|
+
return [
|
|
267
|
+
404,
|
|
268
|
+
JSON.stringify({ error: `Resource "${key}" not found` }),
|
|
269
|
+
];
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return [404, 'Not Found'];
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// POST /brains/runs
|
|
276
|
+
nockInstance.post('/brains/runs').reply((uri, requestBody) => {
|
|
277
|
+
const body =
|
|
278
|
+
typeof requestBody === 'string' ? JSON.parse(requestBody) : requestBody;
|
|
279
|
+
|
|
280
|
+
// Check if brain exists (for testing brain not found scenario)
|
|
281
|
+
if (body.brainName === 'non-existent-brain') {
|
|
282
|
+
this.logCall('createBrainRun', [body.brainName]);
|
|
283
|
+
return [404, { error: `Brain '${body.brainName}' not found` }];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let brainRunId = `run-${Date.now()}`;
|
|
287
|
+
|
|
288
|
+
// Return specific runIds for specific test scenarios
|
|
289
|
+
if (body.brainName === 'error-brain') {
|
|
290
|
+
brainRunId = 'test-error-brain';
|
|
291
|
+
} else if (body.brainName === 'restart-brain') {
|
|
292
|
+
brainRunId = 'test-restart-brain';
|
|
293
|
+
} else if (body.brainName === 'multi-status-brain') {
|
|
294
|
+
brainRunId = 'test-multi-status';
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
this.logCall('createBrainRun', [brainRunId]);
|
|
298
|
+
return [201, { brainRunId }];
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// POST /brains/runs/rerun
|
|
302
|
+
nockInstance.post('/brains/runs/rerun').reply((uri, requestBody) => {
|
|
303
|
+
const body =
|
|
304
|
+
typeof requestBody === 'string' ? JSON.parse(requestBody) : requestBody;
|
|
305
|
+
|
|
306
|
+
// Check if brain exists
|
|
307
|
+
if (body.brainName === 'non-existent-brain') {
|
|
308
|
+
this.logCall('rerunBrain', [
|
|
309
|
+
body.brainName,
|
|
310
|
+
body.runId,
|
|
311
|
+
body.startsAt,
|
|
312
|
+
body.stopsAfter,
|
|
313
|
+
]);
|
|
314
|
+
return [404, { error: `Brain '${body.brainName}' not found` }];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Check if run ID exists (if provided)
|
|
318
|
+
if (body.runId === 'non-existent-run') {
|
|
319
|
+
this.logCall('rerunBrain', [
|
|
320
|
+
body.brainName,
|
|
321
|
+
body.runId,
|
|
322
|
+
body.startsAt,
|
|
323
|
+
body.stopsAfter,
|
|
324
|
+
]);
|
|
325
|
+
return [404, { error: `Brain run '${body.runId}' not found` }];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const newBrainRunId = `rerun-${Date.now()}`;
|
|
329
|
+
|
|
330
|
+
this.logCall('rerunBrain', [
|
|
331
|
+
body.brainName,
|
|
332
|
+
body.runId,
|
|
333
|
+
body.startsAt,
|
|
334
|
+
body.stopsAfter,
|
|
335
|
+
]);
|
|
336
|
+
return [201, { brainRunId: newBrainRunId }];
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// GET /brains/runs/:runId/watch (SSE endpoint)
|
|
340
|
+
nockInstance.get(/^\/brains\/runs\/(.+)\/watch$/).reply(
|
|
341
|
+
200,
|
|
342
|
+
function (uri) {
|
|
343
|
+
const match = uri.match(/^\/brains\/runs\/(.+)\/watch$/);
|
|
344
|
+
if (match) {
|
|
345
|
+
const runId = match[1];
|
|
346
|
+
|
|
347
|
+
// Different scenarios based on runId
|
|
348
|
+
if (runId === 'test-error-brain') {
|
|
349
|
+
// Error scenario
|
|
350
|
+
return [
|
|
351
|
+
`data: ${JSON.stringify({
|
|
352
|
+
type: 'brain:start',
|
|
353
|
+
brainTitle: 'Error Brain',
|
|
354
|
+
brainRunId: runId,
|
|
355
|
+
options: {},
|
|
356
|
+
status: 'running',
|
|
357
|
+
initialState: {},
|
|
358
|
+
})}\n\n`,
|
|
359
|
+
`data: ${JSON.stringify({
|
|
360
|
+
type: 'brain:error',
|
|
361
|
+
brainRunId: runId,
|
|
362
|
+
brainTitle: 'Error Brain',
|
|
363
|
+
options: {},
|
|
364
|
+
status: 'error',
|
|
365
|
+
error: {
|
|
366
|
+
name: 'TestError',
|
|
367
|
+
message: 'Something went wrong in the brain',
|
|
368
|
+
stack: 'Error: Something went wrong\n at test.js:1:1',
|
|
369
|
+
},
|
|
370
|
+
})}\n\n`,
|
|
371
|
+
].join('');
|
|
372
|
+
} else if (runId === 'test-restart-brain') {
|
|
373
|
+
// Restart scenario
|
|
374
|
+
return [
|
|
375
|
+
`data: ${JSON.stringify({
|
|
376
|
+
type: 'brain:restart',
|
|
377
|
+
brainTitle: 'Restarted Brain',
|
|
378
|
+
brainRunId: runId,
|
|
379
|
+
options: {},
|
|
380
|
+
status: 'running',
|
|
381
|
+
initialState: {},
|
|
382
|
+
})}\n\n`,
|
|
383
|
+
`data: ${JSON.stringify({
|
|
384
|
+
type: 'step:status',
|
|
385
|
+
brainRunId: runId,
|
|
386
|
+
options: {},
|
|
387
|
+
steps: [
|
|
388
|
+
{
|
|
389
|
+
id: 'restart-step-1',
|
|
390
|
+
title: 'Restart Step',
|
|
391
|
+
status: 'pending',
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
})}\n\n`,
|
|
395
|
+
].join('');
|
|
396
|
+
} else if (runId === 'test-multi-status') {
|
|
397
|
+
// Multiple step statuses
|
|
398
|
+
return [
|
|
399
|
+
`data: ${JSON.stringify({
|
|
400
|
+
type: 'brain:start',
|
|
401
|
+
brainTitle: 'Multi Status Brain',
|
|
402
|
+
brainRunId: runId,
|
|
403
|
+
options: {},
|
|
404
|
+
status: 'running',
|
|
405
|
+
initialState: {},
|
|
406
|
+
})}\n\n`,
|
|
407
|
+
`data: ${JSON.stringify({
|
|
408
|
+
type: 'step:status',
|
|
409
|
+
brainRunId: runId,
|
|
410
|
+
options: {},
|
|
411
|
+
steps: [
|
|
412
|
+
{ id: 'step-1', title: 'Complete Step', status: 'complete' },
|
|
413
|
+
{ id: 'step-2', title: 'Error Step', status: 'error' },
|
|
414
|
+
{ id: 'step-3', title: 'Running Step', status: 'running' },
|
|
415
|
+
{ id: 'step-4', title: 'Pending Step', status: 'pending' },
|
|
416
|
+
],
|
|
417
|
+
})}\n\n`,
|
|
418
|
+
].join('');
|
|
419
|
+
} else if (runId === 'test-complete-flow') {
|
|
420
|
+
// Full flow from start to complete
|
|
421
|
+
return [
|
|
422
|
+
`data: ${JSON.stringify({
|
|
423
|
+
type: 'brain:start',
|
|
424
|
+
brainTitle: 'Complete Flow Brain',
|
|
425
|
+
brainRunId: runId,
|
|
426
|
+
options: {},
|
|
427
|
+
status: 'running',
|
|
428
|
+
initialState: {},
|
|
429
|
+
})}\n\n`,
|
|
430
|
+
`data: ${JSON.stringify({
|
|
431
|
+
type: 'step:status',
|
|
432
|
+
brainRunId: runId,
|
|
433
|
+
options: {},
|
|
434
|
+
steps: [
|
|
435
|
+
{ id: 'step-1', title: 'First Step', status: 'complete' },
|
|
436
|
+
{ id: 'step-2', title: 'Second Step', status: 'complete' },
|
|
437
|
+
],
|
|
438
|
+
})}\n\n`,
|
|
439
|
+
`data: ${JSON.stringify({
|
|
440
|
+
type: 'brain:complete',
|
|
441
|
+
brainRunId: runId,
|
|
442
|
+
brainTitle: 'Complete Flow Brain',
|
|
443
|
+
options: {},
|
|
444
|
+
status: 'complete',
|
|
445
|
+
})}\n\n`,
|
|
446
|
+
].join('');
|
|
447
|
+
} else if (runId === 'test-brain-error') {
|
|
448
|
+
// Brain error scenario
|
|
449
|
+
return [
|
|
450
|
+
`data: ${JSON.stringify({
|
|
451
|
+
type: 'brain:start',
|
|
452
|
+
brainTitle: 'Error Brain',
|
|
453
|
+
brainRunId: runId,
|
|
454
|
+
options: {},
|
|
455
|
+
status: 'running',
|
|
456
|
+
initialState: {},
|
|
457
|
+
})}\n\n`,
|
|
458
|
+
`data: ${JSON.stringify({
|
|
459
|
+
type: 'brain:error',
|
|
460
|
+
brainRunId: runId,
|
|
461
|
+
brainTitle: 'Error Brain',
|
|
462
|
+
error: {
|
|
463
|
+
name: 'BrainExecutionError',
|
|
464
|
+
message: 'Something went wrong during brain execution',
|
|
465
|
+
stack:
|
|
466
|
+
'Error: Something went wrong during brain execution\n at BrainRunner.run',
|
|
467
|
+
},
|
|
468
|
+
})}\n\n`,
|
|
469
|
+
].join('');
|
|
470
|
+
} else if (runId === 'test-malformed-event') {
|
|
471
|
+
// Send malformed JSON
|
|
472
|
+
return 'data: {invalid json here}\n\n';
|
|
473
|
+
} else if (runId === 'test-no-steps') {
|
|
474
|
+
// Brain with no steps initially
|
|
475
|
+
return [
|
|
476
|
+
`data: ${JSON.stringify({
|
|
477
|
+
type: 'brain:start',
|
|
478
|
+
brainTitle: 'No Steps Brain',
|
|
479
|
+
brainRunId: runId,
|
|
480
|
+
options: {},
|
|
481
|
+
status: 'running',
|
|
482
|
+
initialState: {},
|
|
483
|
+
})}\n\n`,
|
|
484
|
+
`data: ${JSON.stringify({
|
|
485
|
+
type: 'step:status',
|
|
486
|
+
brainRunId: runId,
|
|
487
|
+
options: {},
|
|
488
|
+
steps: [],
|
|
489
|
+
})}\n\n`,
|
|
490
|
+
].join('');
|
|
491
|
+
} else if (runId === 'test-connection-error') {
|
|
492
|
+
// Simulate connection error by returning error
|
|
493
|
+
throw new Error('ECONNREFUSED');
|
|
494
|
+
} else {
|
|
495
|
+
// Default scenario
|
|
496
|
+
const mockEvents = [
|
|
497
|
+
`data: ${JSON.stringify({
|
|
498
|
+
type: 'brain:start',
|
|
499
|
+
brainTitle: 'test-brain',
|
|
500
|
+
brainRunId: runId,
|
|
501
|
+
options: {},
|
|
502
|
+
status: 'running',
|
|
503
|
+
initialState: {},
|
|
504
|
+
})}\n\n`,
|
|
505
|
+
`data: ${JSON.stringify({
|
|
506
|
+
type: 'step:status',
|
|
507
|
+
brainRunId: runId,
|
|
508
|
+
options: {},
|
|
509
|
+
steps: [
|
|
510
|
+
{
|
|
511
|
+
id: 'step-1',
|
|
512
|
+
title: 'Test Step 1',
|
|
513
|
+
status: 'running',
|
|
514
|
+
},
|
|
515
|
+
],
|
|
516
|
+
})}\n\n`,
|
|
517
|
+
`data: ${JSON.stringify({
|
|
518
|
+
type: 'brain:complete',
|
|
519
|
+
brainRunId: runId,
|
|
520
|
+
brainTitle: 'test-brain',
|
|
521
|
+
options: {},
|
|
522
|
+
status: 'complete',
|
|
523
|
+
})}\n\n`,
|
|
524
|
+
];
|
|
525
|
+
return mockEvents.join('');
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return 'data: {"type":"ERROR","error":{"message":"Invalid run ID"}}\n\n';
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
'Content-Type': 'text/event-stream',
|
|
532
|
+
'Cache-Control': 'no-cache',
|
|
533
|
+
Connection: 'keep-alive',
|
|
534
|
+
}
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
// GET /brains
|
|
538
|
+
nockInstance.get('/brains').reply(200, () => {
|
|
539
|
+
const brains = Array.from(this.brains.values());
|
|
540
|
+
this.logCall('getBrains', []);
|
|
541
|
+
return {
|
|
542
|
+
brains,
|
|
543
|
+
count: brains.length,
|
|
544
|
+
};
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// GET /brains/schedules
|
|
548
|
+
nockInstance.get('/brains/schedules').reply(200, () => {
|
|
549
|
+
const schedules = Array.from(this.schedules.values());
|
|
550
|
+
this.logCall('getSchedules', []);
|
|
551
|
+
return {
|
|
552
|
+
schedules,
|
|
553
|
+
count: schedules.length,
|
|
554
|
+
};
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// POST /brains/schedules
|
|
558
|
+
nockInstance.post('/brains/schedules').reply(201, (uri, requestBody) => {
|
|
559
|
+
const body =
|
|
560
|
+
typeof requestBody === 'string' ? JSON.parse(requestBody) : requestBody;
|
|
561
|
+
const scheduleId = `schedule-${Date.now()}`;
|
|
562
|
+
const schedule: MockSchedule = {
|
|
563
|
+
id: scheduleId,
|
|
564
|
+
brainName: body.brainName,
|
|
565
|
+
cronExpression: body.cronExpression,
|
|
566
|
+
enabled: true,
|
|
567
|
+
createdAt: Date.now(),
|
|
568
|
+
nextRunAt: Date.now() + 3600000, // 1 hour from now
|
|
569
|
+
};
|
|
570
|
+
this.schedules.set(scheduleId, schedule);
|
|
571
|
+
this.logCall('createSchedule', [body]);
|
|
572
|
+
return schedule;
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// GET /brains/schedules/runs
|
|
576
|
+
nockInstance
|
|
577
|
+
.get('/brains/schedules/runs')
|
|
578
|
+
.query(true)
|
|
579
|
+
.reply((uri) => {
|
|
580
|
+
const url = new URL(uri, 'http://example.com');
|
|
581
|
+
const scheduleId = url.searchParams.get('scheduleId');
|
|
582
|
+
const limit = parseInt(url.searchParams.get('limit') || '100', 10);
|
|
583
|
+
|
|
584
|
+
this.logCall('getScheduleRuns', [uri]);
|
|
585
|
+
|
|
586
|
+
let runs = this.scheduleRuns;
|
|
587
|
+
|
|
588
|
+
// Filter by scheduleId if provided
|
|
589
|
+
if (scheduleId) {
|
|
590
|
+
runs = runs.filter((run) => run.scheduleId === scheduleId);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Sort by ranAt descending (newest first)
|
|
594
|
+
runs = runs.sort((a, b) => b.ranAt - a.ranAt);
|
|
595
|
+
|
|
596
|
+
// Apply limit
|
|
597
|
+
runs = runs.slice(0, limit);
|
|
598
|
+
|
|
599
|
+
return [
|
|
600
|
+
200,
|
|
601
|
+
{
|
|
602
|
+
runs,
|
|
603
|
+
count: runs.length,
|
|
604
|
+
},
|
|
605
|
+
];
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// DELETE /brains/schedules/:id
|
|
609
|
+
nockInstance.delete(/^\/brains\/schedules\/(.+)$/).reply((uri) => {
|
|
610
|
+
const match = uri.match(/^\/brains\/schedules\/(.+)$/);
|
|
611
|
+
if (match) {
|
|
612
|
+
const scheduleId = decodeURIComponent(match[1]);
|
|
613
|
+
if (this.schedules.has(scheduleId)) {
|
|
614
|
+
this.schedules.delete(scheduleId);
|
|
615
|
+
this.logCall('deleteSchedule', [scheduleId]);
|
|
616
|
+
return [204, ''];
|
|
617
|
+
} else {
|
|
618
|
+
return [
|
|
619
|
+
404,
|
|
620
|
+
JSON.stringify({ error: `Schedule "${scheduleId}" not found` }),
|
|
621
|
+
];
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return [404, 'Not Found'];
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// GET /brains/:brainName/history
|
|
628
|
+
nockInstance
|
|
629
|
+
.get(/^\/brains\/(.+)\/history$/)
|
|
630
|
+
.query(true)
|
|
631
|
+
.reply((uri) => {
|
|
632
|
+
const parts = uri.split('/');
|
|
633
|
+
const brainName = decodeURIComponent(parts[2]);
|
|
634
|
+
const url = new URL(uri, 'http://example.com');
|
|
635
|
+
const limit = parseInt(url.searchParams.get('limit') || '10', 10);
|
|
636
|
+
|
|
637
|
+
this.logCall('getBrainHistory', [brainName, limit]);
|
|
638
|
+
|
|
639
|
+
// Filter runs by brain title
|
|
640
|
+
const runs = this.brainRuns
|
|
641
|
+
.filter(
|
|
642
|
+
(run) =>
|
|
643
|
+
run.brainTitle.toLowerCase() ===
|
|
644
|
+
brainName.toLowerCase().replace(/-/g, ' ')
|
|
645
|
+
)
|
|
646
|
+
.sort((a, b) => b.createdAt - a.createdAt)
|
|
647
|
+
.slice(0, limit);
|
|
648
|
+
|
|
649
|
+
return [200, { runs }];
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// GET /brains/:brainName/active-runs
|
|
653
|
+
nockInstance.get(/^\/brains\/(.+)\/active-runs$/).reply((uri) => {
|
|
654
|
+
const parts = uri.split('/');
|
|
655
|
+
const brainName = decodeURIComponent(parts[2]);
|
|
656
|
+
|
|
657
|
+
this.logCall('getBrainActiveRuns', [brainName]);
|
|
658
|
+
|
|
659
|
+
// Filter brain runs by brain title and status RUNNING
|
|
660
|
+
const activeRuns = this.brainRuns
|
|
661
|
+
.filter(
|
|
662
|
+
(run) =>
|
|
663
|
+
run.brainTitle.toLowerCase() ===
|
|
664
|
+
brainName.toLowerCase().replace(/-/g, ' ') &&
|
|
665
|
+
run.status === 'RUNNING'
|
|
666
|
+
)
|
|
667
|
+
.sort((a, b) => b.createdAt - a.createdAt);
|
|
668
|
+
|
|
669
|
+
return [200, { runs: activeRuns }];
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
// GET /brains/:brainName
|
|
673
|
+
nockInstance.get(/^\/brains\/(.+)$/).reply((uri) => {
|
|
674
|
+
const brainName = decodeURIComponent(uri.split('/')[2]);
|
|
675
|
+
const brain = this.brains.get(brainName);
|
|
676
|
+
this.logCall('getBrain', [brainName]);
|
|
677
|
+
|
|
678
|
+
if (!brain) {
|
|
679
|
+
return [404, { error: `Brain '${brainName}' not found` }];
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return [
|
|
683
|
+
200,
|
|
684
|
+
{
|
|
685
|
+
name: brain.name,
|
|
686
|
+
title: brain.title,
|
|
687
|
+
description: brain.description || `${brain.title} brain`,
|
|
688
|
+
steps: brain.steps || [],
|
|
689
|
+
},
|
|
690
|
+
];
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// Secret Management Endpoints
|
|
694
|
+
|
|
695
|
+
// POST /secrets
|
|
696
|
+
nockInstance.post('/secrets').reply(201, (uri, requestBody) => {
|
|
697
|
+
const body =
|
|
698
|
+
typeof requestBody === 'string' ? JSON.parse(requestBody) : requestBody;
|
|
699
|
+
|
|
700
|
+
const now = new Date().toISOString();
|
|
701
|
+
const secret: MockSecret = {
|
|
702
|
+
name: body.name,
|
|
703
|
+
value: body.value,
|
|
704
|
+
createdAt: now,
|
|
705
|
+
updatedAt: now,
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
this.secrets.set(body.name, secret);
|
|
709
|
+
this.logCall('createSecret', [body.name]);
|
|
710
|
+
|
|
711
|
+
// Return without the value for security
|
|
712
|
+
return {
|
|
713
|
+
name: secret.name,
|
|
714
|
+
createdAt: secret.createdAt,
|
|
715
|
+
updatedAt: secret.updatedAt,
|
|
716
|
+
};
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
// GET /secrets
|
|
720
|
+
nockInstance.get('/secrets').reply(200, () => {
|
|
721
|
+
this.logCall('listSecrets', []);
|
|
722
|
+
|
|
723
|
+
// Return secrets without values
|
|
724
|
+
const secrets = Array.from(this.secrets.values()).map((secret) => ({
|
|
725
|
+
name: secret.name,
|
|
726
|
+
createdAt: secret.createdAt,
|
|
727
|
+
updatedAt: secret.updatedAt,
|
|
728
|
+
}));
|
|
729
|
+
|
|
730
|
+
return {
|
|
731
|
+
secrets,
|
|
732
|
+
count: secrets.length,
|
|
733
|
+
};
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
// DELETE /secrets/:name
|
|
737
|
+
nockInstance.delete(/^\/secrets\/(.+)$/).reply((uri) => {
|
|
738
|
+
const match = uri.match(/^\/secrets\/(.+)$/);
|
|
739
|
+
if (match) {
|
|
740
|
+
const secretName = decodeURIComponent(match[1]);
|
|
741
|
+
|
|
742
|
+
if (this.secrets.has(secretName)) {
|
|
743
|
+
this.secrets.delete(secretName);
|
|
744
|
+
this.logCall('deleteSecret', [secretName]);
|
|
745
|
+
return [204, ''];
|
|
746
|
+
} else {
|
|
747
|
+
return [404, { error: `Secret "${secretName}" not found` }];
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return [404, 'Not Found'];
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
// GET /secrets/:name/exists
|
|
754
|
+
nockInstance.get(/^\/secrets\/(.+)\/exists$/).reply((uri) => {
|
|
755
|
+
const match = uri.match(/^\/secrets\/(.+)\/exists$/);
|
|
756
|
+
if (match) {
|
|
757
|
+
const secretName = decodeURIComponent(match[1]);
|
|
758
|
+
const exists = this.secrets.has(secretName);
|
|
759
|
+
|
|
760
|
+
this.logCall('secretExists', [secretName]);
|
|
761
|
+
|
|
762
|
+
return [200, { exists }];
|
|
763
|
+
}
|
|
764
|
+
return [404, 'Not Found'];
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
// POST /secrets/bulk
|
|
768
|
+
nockInstance.post('/secrets/bulk').reply(201, (uri, requestBody) => {
|
|
769
|
+
const body =
|
|
770
|
+
typeof requestBody === 'string' ? JSON.parse(requestBody) : requestBody;
|
|
771
|
+
|
|
772
|
+
let created = 0;
|
|
773
|
+
let updated = 0;
|
|
774
|
+
const now = new Date().toISOString();
|
|
775
|
+
|
|
776
|
+
for (const secretData of body.secrets) {
|
|
777
|
+
const existing = this.secrets.has(secretData.name);
|
|
778
|
+
|
|
779
|
+
const secret: MockSecret = {
|
|
780
|
+
name: secretData.name,
|
|
781
|
+
value: secretData.value,
|
|
782
|
+
createdAt: existing
|
|
783
|
+
? this.secrets.get(secretData.name)!.createdAt
|
|
784
|
+
: now,
|
|
785
|
+
updatedAt: now,
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
this.secrets.set(secretData.name, secret);
|
|
789
|
+
|
|
790
|
+
if (existing) {
|
|
791
|
+
updated++;
|
|
792
|
+
} else {
|
|
793
|
+
created++;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
this.logCall('bulkCreateSecrets', [body.secrets.length]);
|
|
798
|
+
|
|
799
|
+
return { created, updated };
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
this.nockScope = nockInstance;
|
|
803
|
+
|
|
804
|
+
// Simulate some initial log output after server starts
|
|
805
|
+
setTimeout(() => {
|
|
806
|
+
this.logCallbacks.forEach((cb) =>
|
|
807
|
+
cb('✅ Synced 3 resources (0 up to date, 0 deleted)\n')
|
|
808
|
+
);
|
|
809
|
+
this.logCallbacks.forEach((cb) =>
|
|
810
|
+
cb('🚀 Server started on port ' + this.port + '\n')
|
|
811
|
+
);
|
|
812
|
+
}, 100);
|
|
813
|
+
|
|
814
|
+
return new MockServerHandle(
|
|
815
|
+
() => {
|
|
816
|
+
this.stop();
|
|
817
|
+
},
|
|
818
|
+
() => this.callLog,
|
|
819
|
+
() => {
|
|
820
|
+
this.callLog = [];
|
|
821
|
+
},
|
|
822
|
+
this.port
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Add watch method to track calls
|
|
827
|
+
async watch(event: 'add' | 'change' | 'unlink') {
|
|
828
|
+
this.logCall('watch', [this.projectRootDir, event]);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Helper methods for tests to manipulate the server state
|
|
832
|
+
addResource(resource: MockResource) {
|
|
833
|
+
this.resources.set(resource.key, resource);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
clearResources() {
|
|
837
|
+
this.resources.clear();
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
getResources(): MockResource[] {
|
|
841
|
+
return Array.from(this.resources.values());
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Schedule helper methods
|
|
845
|
+
addSchedule(schedule: MockSchedule) {
|
|
846
|
+
this.schedules.set(schedule.id, schedule);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
clearSchedules() {
|
|
850
|
+
this.schedules.clear();
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
addScheduleRun(run: MockScheduleRun) {
|
|
854
|
+
this.scheduleRuns.push(run);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
clearScheduleRuns() {
|
|
858
|
+
this.scheduleRuns = [];
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
getSchedules(): MockSchedule[] {
|
|
862
|
+
return Array.from(this.schedules.values());
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Secret helper methods
|
|
866
|
+
addSecret(name: string, value: string) {
|
|
867
|
+
const now = new Date().toISOString();
|
|
868
|
+
this.secrets.set(name, {
|
|
869
|
+
name,
|
|
870
|
+
value,
|
|
871
|
+
createdAt: now,
|
|
872
|
+
updatedAt: now,
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
clearSecrets() {
|
|
877
|
+
this.secrets.clear();
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
getSecrets(): MockSecret[] {
|
|
881
|
+
return Array.from(this.secrets.values());
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
getSecret(name: string): MockSecret | undefined {
|
|
885
|
+
return this.secrets.get(name);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Brain helper methods
|
|
889
|
+
addBrain(brain: MockBrain) {
|
|
890
|
+
this.brains.set(brain.name, brain);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
addBrainRun(run: MockBrainRun) {
|
|
894
|
+
this.brainRuns.push(run);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
clearBrainRuns() {
|
|
898
|
+
this.brainRuns = [];
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
clearBrains() {
|
|
902
|
+
this.brains.clear();
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
getBrains(): MockBrain[] {
|
|
906
|
+
return Array.from(this.brains.values());
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
stop() {
|
|
910
|
+
if (this.nockScope) {
|
|
911
|
+
// Clean up all nock interceptors
|
|
912
|
+
nock.cleanAll();
|
|
913
|
+
this.nockScope = null;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
onLog(callback: (message: string) => void): void {
|
|
918
|
+
this.logCall('onLog', ['callback registered']);
|
|
919
|
+
this.logCallbacks.push(callback);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
onError(callback: (message: string) => void): void {
|
|
923
|
+
this.logCall('onError', ['callback registered']);
|
|
924
|
+
this.errorCallbacks.push(callback);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
onWarning(callback: (message: string) => void): void {
|
|
928
|
+
this.logCall('onWarning', ['callback registered']);
|
|
929
|
+
this.warningCallbacks.push(callback);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
async listSecrets(): Promise<
|
|
933
|
+
Array<{ name: string; createdAt?: Date; updatedAt?: Date }>
|
|
934
|
+
> {
|
|
935
|
+
this.logCall('listSecrets', []);
|
|
936
|
+
return Array.from(this.secrets.values()).map((secret) => ({
|
|
937
|
+
name: secret.name,
|
|
938
|
+
createdAt: new Date(secret.createdAt),
|
|
939
|
+
updatedAt: new Date(secret.updatedAt),
|
|
940
|
+
}));
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
async setSecret(name: string, value: string): Promise<void> {
|
|
944
|
+
this.logCall('setSecret', [name, value]);
|
|
945
|
+
const now = new Date().toISOString();
|
|
946
|
+
const existing = this.secrets.get(name);
|
|
947
|
+
|
|
948
|
+
this.secrets.set(name, {
|
|
949
|
+
name,
|
|
950
|
+
value,
|
|
951
|
+
createdAt: existing?.createdAt || now,
|
|
952
|
+
updatedAt: now,
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
async deleteSecret(name: string): Promise<boolean> {
|
|
957
|
+
this.logCall('deleteSecret', [name]);
|
|
958
|
+
if (this.secrets.has(name)) {
|
|
959
|
+
this.secrets.delete(name);
|
|
960
|
+
return true;
|
|
961
|
+
}
|
|
962
|
+
return false;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
async bulkSecrets(filePath: string): Promise<void> {
|
|
966
|
+
this.logCall('bulkSecrets', [filePath]);
|
|
967
|
+
|
|
968
|
+
try {
|
|
969
|
+
// Read and parse the .env file
|
|
970
|
+
if (!fs.existsSync(filePath)) {
|
|
971
|
+
throw new Error(`File not found: ${filePath}`);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const envContent = fs.readFileSync(filePath, 'utf8');
|
|
975
|
+
const secrets = parse(envContent);
|
|
976
|
+
|
|
977
|
+
const secretsArray = Object.entries(secrets);
|
|
978
|
+
|
|
979
|
+
if (secretsArray.length === 0) {
|
|
980
|
+
throw new Error('No secrets found in the .env file');
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Simulate the bulk upload - just store them
|
|
984
|
+
const now = new Date().toISOString();
|
|
985
|
+
for (const [name, value] of secretsArray) {
|
|
986
|
+
const existing = this.secrets.has(name);
|
|
987
|
+
this.secrets.set(name, {
|
|
988
|
+
name,
|
|
989
|
+
value,
|
|
990
|
+
createdAt: existing ? this.secrets.get(name)!.createdAt : now,
|
|
991
|
+
updatedAt: now,
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Simulate console output
|
|
996
|
+
this.logCallbacks.forEach((cb) =>
|
|
997
|
+
cb(`✨ Successfully uploaded ${secretsArray.length} secrets\n`)
|
|
998
|
+
);
|
|
999
|
+
} catch (error) {
|
|
1000
|
+
throw error;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|