@friggframework/admin-scripts 2.0.0--canary.517.300ded3.0 → 2.0.0--canary.522.cbd3d5a.0

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.
@@ -1,6 +1,8 @@
1
1
  const { getScriptFactory } = require('./script-factory');
2
2
  const { createAdminFriggCommands } = require('./admin-frigg-commands');
3
3
  const { createAdminScriptCommands } = require('@friggframework/core/application/commands/admin-script-commands');
4
+ const { wrapAdminFriggCommandsForDryRun } = require('./dry-run-repository-wrapper');
5
+ const { createDryRunHttpClient, injectDryRunHttpClient } = require('./dry-run-http-interceptor');
4
6
 
5
7
  /**
6
8
  * Script Runner
@@ -28,7 +30,7 @@ class ScriptRunner {
28
30
  * @param {string} options.mode - 'sync' | 'async'
29
31
  * @param {Object} options.audit - Audit info { apiKeyName, apiKeyLast4, ipAddress }
30
32
  * @param {string} options.executionId - Reuse existing execution ID
31
- * @param {boolean} options.dryRun - Dry-run mode: validate and preview without executing
33
+ * @param {boolean} options.dryRun - Execute in dry-run mode (no writes, log operations)
32
34
  */
33
35
  async execute(scriptName, params = {}, options = {}) {
34
36
  const { trigger = 'MANUAL', audit = {}, executionId: existingExecutionId, dryRun = false } = options;
@@ -44,16 +46,11 @@ class ScriptRunner {
44
46
  );
45
47
  }
46
48
 
47
- // Dry-run mode: validate and return preview without executing
48
- if (dryRun) {
49
- return this.createDryRunPreview(scriptName, definition, params);
50
- }
51
-
52
49
  let executionId = existingExecutionId;
53
50
 
54
51
  // Create execution record if not provided
55
52
  if (!executionId) {
56
- const execution = await this.commands.createAdminProcess({
53
+ const execution = await this.commands.createScriptExecution({
57
54
  scriptName,
58
55
  scriptVersion: definition.version,
59
56
  trigger,
@@ -67,13 +64,25 @@ class ScriptRunner {
67
64
  const startTime = new Date();
68
65
 
69
66
  try {
70
- await this.commands.updateAdminProcessState(executionId, 'RUNNING');
67
+ // Update status to RUNNING (skip in dry-run)
68
+ if (!dryRun) {
69
+ await this.commands.updateScriptExecutionStatus(executionId, 'RUNNING');
70
+ }
71
71
 
72
72
  // Create frigg commands for the script
73
- const frigg = createAdminFriggCommands({
74
- executionId,
75
- integrationFactory: this.integrationFactory,
76
- });
73
+ let frigg;
74
+ let operationLog = [];
75
+
76
+ if (dryRun) {
77
+ // Dry-run mode: wrap commands to intercept writes
78
+ frigg = this.createDryRunFriggCommands(operationLog);
79
+ } else {
80
+ // Normal mode: create real commands
81
+ frigg = createAdminFriggCommands({
82
+ executionId,
83
+ integrationFactory: this.integrationFactory,
84
+ });
85
+ }
77
86
 
78
87
  // Create script instance
79
88
  const script = this.scriptFactory.createInstance(scriptName, {
@@ -88,15 +97,34 @@ class ScriptRunner {
88
97
  const endTime = new Date();
89
98
  const durationMs = endTime - startTime;
90
99
 
91
- await this.commands.completeAdminProcess(executionId, {
92
- state: 'COMPLETED',
93
- output,
94
- metrics: {
95
- startTime: startTime.toISOString(),
96
- endTime: endTime.toISOString(),
97
- durationMs,
98
- },
99
- });
100
+ // Complete execution (skip in dry-run)
101
+ if (!dryRun) {
102
+ await this.commands.completeScriptExecution(executionId, {
103
+ status: 'COMPLETED',
104
+ output,
105
+ metrics: {
106
+ startTime: startTime.toISOString(),
107
+ endTime: endTime.toISOString(),
108
+ durationMs,
109
+ },
110
+ });
111
+ }
112
+
113
+ // Return dry-run preview if in dry-run mode
114
+ if (dryRun) {
115
+ return {
116
+ executionId,
117
+ dryRun: true,
118
+ status: 'DRY_RUN_COMPLETED',
119
+ scriptName,
120
+ preview: {
121
+ operations: operationLog,
122
+ summary: this.summarizeOperations(operationLog),
123
+ scriptOutput: output,
124
+ },
125
+ metrics: { durationMs },
126
+ };
127
+ }
100
128
 
101
129
  return {
102
130
  executionId,
@@ -106,26 +134,31 @@ class ScriptRunner {
106
134
  metrics: { durationMs },
107
135
  };
108
136
  } catch (error) {
137
+ // Calculate metrics even on failure
109
138
  const endTime = new Date();
110
139
  const durationMs = endTime - startTime;
111
140
 
112
- await this.commands.completeAdminProcess(executionId, {
113
- state: 'FAILED',
114
- error: {
115
- name: error.name,
116
- message: error.message,
117
- stack: error.stack,
118
- },
119
- metrics: {
120
- startTime: startTime.toISOString(),
121
- endTime: endTime.toISOString(),
122
- durationMs,
123
- },
124
- });
141
+ // Record failure (skip in dry-run)
142
+ if (!dryRun) {
143
+ await this.commands.completeScriptExecution(executionId, {
144
+ status: 'FAILED',
145
+ error: {
146
+ name: error.name,
147
+ message: error.message,
148
+ stack: error.stack,
149
+ },
150
+ metrics: {
151
+ startTime: startTime.toISOString(),
152
+ endTime: endTime.toISOString(),
153
+ durationMs,
154
+ },
155
+ });
156
+ }
125
157
 
126
158
  return {
127
159
  executionId,
128
- status: 'FAILED',
160
+ dryRun,
161
+ status: dryRun ? 'DRY_RUN_FAILED' : 'FAILED',
129
162
  scriptName,
130
163
  error: {
131
164
  name: error.name,
@@ -137,107 +170,80 @@ class ScriptRunner {
137
170
  }
138
171
 
139
172
  /**
140
- * Create dry-run preview without executing the script
141
- * Validates inputs and shows what would be executed
173
+ * Create dry-run version of AdminFriggCommands
174
+ * Intercepts all write operations and logs them
142
175
  *
143
- * @param {string} scriptName - Script name
144
- * @param {Object} definition - Script definition
145
- * @param {Object} params - Input parameters
146
- * @returns {Object} Dry-run preview
176
+ * @param {Array} operationLog - Array to collect logged operations
177
+ * @returns {Object} Wrapped AdminFriggCommands
147
178
  */
148
- createDryRunPreview(scriptName, definition, params) {
149
- const validation = this.validateParams(definition, params);
150
-
151
- return {
152
- dryRun: true,
153
- status: validation.valid ? 'DRY_RUN_VALID' : 'DRY_RUN_INVALID',
154
- scriptName,
155
- preview: {
156
- script: {
157
- name: definition.name,
158
- version: definition.version,
159
- description: definition.description,
160
- requiresIntegrationFactory: definition.config?.requiresIntegrationFactory || false,
161
- },
162
- input: params,
163
- inputSchema: definition.inputSchema || null,
164
- validation,
165
- },
166
- message: validation.valid
167
- ? 'Dry-run validation passed. Script is ready to execute with provided parameters.'
168
- : `Dry-run validation failed: ${validation.errors.join(', ')}`,
169
- };
170
- }
179
+ createDryRunFriggCommands(operationLog) {
180
+ // Create real commands (for read operations)
181
+ const realCommands = createAdminFriggCommands({
182
+ executionId: null, // Don't persist logs in dry-run
183
+ integrationFactory: this.integrationFactory,
184
+ });
171
185
 
172
- /**
173
- * Validate parameters against script's input schema
174
- *
175
- * @param {Object} definition - Script definition
176
- * @param {Object} params - Input parameters
177
- * @returns {Object} Validation result { valid, errors }
178
- */
179
- validateParams(definition, params) {
180
- const errors = [];
181
- const schema = definition.inputSchema;
186
+ // Wrap commands to intercept writes
187
+ const wrappedCommands = wrapAdminFriggCommandsForDryRun(realCommands, operationLog);
182
188
 
183
- if (!schema) {
184
- return { valid: true, errors: [] };
185
- }
189
+ // Create dry-run HTTP client
190
+ const dryRunHttpClient = createDryRunHttpClient(operationLog);
186
191
 
187
- // Check required fields
188
- if (schema.required && Array.isArray(schema.required)) {
189
- for (const field of schema.required) {
190
- if (params[field] === undefined || params[field] === null) {
191
- errors.push(`Missing required parameter: ${field}`);
192
- }
193
- }
194
- }
192
+ // Override instantiate to inject dry-run HTTP client
193
+ const originalInstantiate = wrappedCommands.instantiate.bind(wrappedCommands);
194
+ wrappedCommands.instantiate = async (integrationId) => {
195
+ const instance = await originalInstantiate(integrationId);
195
196
 
196
- // Basic type validation for properties
197
- if (schema.properties) {
198
- for (const [key, prop] of Object.entries(schema.properties)) {
199
- const value = params[key];
200
- if (value !== undefined && value !== null) {
201
- const typeError = this.validateType(key, value, prop);
202
- if (typeError) {
203
- errors.push(typeError);
204
- }
205
- }
206
- }
207
- }
197
+ // Inject dry-run HTTP client into the integration instance
198
+ injectDryRunHttpClient(instance, dryRunHttpClient);
208
199
 
209
- return { valid: errors.length === 0, errors };
200
+ return instance;
201
+ };
202
+
203
+ return wrappedCommands;
210
204
  }
211
205
 
212
206
  /**
213
- * Validate a single parameter type
207
+ * Summarize operations from dry-run log
208
+ *
209
+ * @param {Array} log - Operation log
210
+ * @returns {Object} Summary statistics
214
211
  */
215
- validateType(key, value, schema) {
216
- const expectedType = schema.type;
217
- if (!expectedType) return null;
212
+ summarizeOperations(log) {
213
+ const summary = {
214
+ totalOperations: log.length,
215
+ databaseWrites: 0,
216
+ httpRequests: 0,
217
+ byOperation: {},
218
+ byModel: {},
219
+ byService: {},
220
+ };
218
221
 
219
- const actualType = Array.isArray(value) ? 'array' : typeof value;
222
+ for (const op of log) {
223
+ // Count by operation type
224
+ const operation = op.operation || op.method || 'UNKNOWN';
225
+ summary.byOperation[operation] = (summary.byOperation[operation] || 0) + 1;
226
+
227
+ // Database operations
228
+ if (op.model) {
229
+ summary.databaseWrites++;
230
+ summary.byModel[op.model] = summary.byModel[op.model] || [];
231
+ summary.byModel[op.model].push({
232
+ operation: op.operation,
233
+ method: op.method,
234
+ timestamp: op.timestamp,
235
+ });
236
+ }
220
237
 
221
- if (expectedType === 'integer' && (typeof value !== 'number' || !Number.isInteger(value))) {
222
- return `Parameter "${key}" must be an integer`;
223
- }
224
- if (expectedType === 'number' && typeof value !== 'number') {
225
- return `Parameter "${key}" must be a number`;
226
- }
227
- if (expectedType === 'string' && typeof value !== 'string') {
228
- return `Parameter "${key}" must be a string`;
229
- }
230
- if (expectedType === 'boolean' && typeof value !== 'boolean') {
231
- return `Parameter "${key}" must be a boolean`;
232
- }
233
- if (expectedType === 'array' && !Array.isArray(value)) {
234
- return `Parameter "${key}" must be an array`;
235
- }
236
- if (expectedType === 'object' && (typeof value !== 'object' || Array.isArray(value))) {
237
- return `Parameter "${key}" must be an object`;
238
+ // HTTP requests
239
+ if (op.operation === 'HTTP_REQUEST') {
240
+ summary.httpRequests++;
241
+ const service = op.service || 'unknown';
242
+ summary.byService[service] = (summary.byService[service] || 0) + 1;
243
+ }
238
244
  }
239
245
 
240
- return null;
246
+ return summary;
241
247
  }
242
248
  }
243
249
 
@@ -1,17 +1,22 @@
1
- const { validateAdminApiKey } = require('../admin-auth-middleware');
1
+ const { adminAuthMiddleware } = require('../admin-auth-middleware');
2
2
 
3
- describe('validateAdminApiKey', () => {
3
+ // Mock the admin script commands
4
+ jest.mock('@friggframework/core/application/commands/admin-script-commands', () => ({
5
+ createAdminScriptCommands: jest.fn(),
6
+ }));
7
+
8
+ const { createAdminScriptCommands } = require('@friggframework/core/application/commands/admin-script-commands');
9
+
10
+ describe('adminAuthMiddleware', () => {
4
11
  let mockReq;
5
12
  let mockRes;
6
13
  let mockNext;
7
- let originalEnv;
14
+ let mockCommands;
8
15
 
9
16
  beforeEach(() => {
10
- originalEnv = process.env.ADMIN_API_KEY;
11
- process.env.ADMIN_API_KEY = 'test-admin-key-123';
12
-
13
17
  mockReq = {
14
18
  headers: {},
19
+ ip: '127.0.0.1',
15
20
  };
16
21
 
17
22
  mockRes = {
@@ -20,66 +25,124 @@ describe('validateAdminApiKey', () => {
20
25
  };
21
26
 
22
27
  mockNext = jest.fn();
28
+
29
+ mockCommands = {
30
+ validateAdminApiKey: jest.fn(),
31
+ };
32
+
33
+ createAdminScriptCommands.mockReturnValue(mockCommands);
23
34
  });
24
35
 
25
36
  afterEach(() => {
26
- if (originalEnv) {
27
- process.env.ADMIN_API_KEY = originalEnv;
28
- } else {
29
- delete process.env.ADMIN_API_KEY;
30
- }
31
37
  jest.clearAllMocks();
32
38
  });
33
39
 
34
- describe('Environment configuration', () => {
35
- it('should reject when ADMIN_API_KEY not configured', () => {
36
- delete process.env.ADMIN_API_KEY;
37
-
38
- validateAdminApiKey(mockReq, mockRes, mockNext);
40
+ describe('Authorization header validation', () => {
41
+ it('should reject request without Authorization header', async () => {
42
+ await adminAuthMiddleware(mockReq, mockRes, mockNext);
39
43
 
40
44
  expect(mockRes.status).toHaveBeenCalledWith(401);
41
45
  expect(mockRes.json).toHaveBeenCalledWith({
42
- error: 'Unauthorized',
43
- message: 'Admin API key not configured',
46
+ error: 'Missing or invalid Authorization header',
47
+ code: 'MISSING_AUTH',
44
48
  });
45
49
  expect(mockNext).not.toHaveBeenCalled();
46
50
  });
47
- });
48
51
 
49
- describe('Header validation', () => {
50
- it('should reject request without x-frigg-admin-api-key header', () => {
51
- validateAdminApiKey(mockReq, mockRes, mockNext);
52
+ it('should reject request with invalid Authorization format', async () => {
53
+ mockReq.headers.authorization = 'InvalidFormat key123';
54
+
55
+ await adminAuthMiddleware(mockReq, mockRes, mockNext);
52
56
 
53
57
  expect(mockRes.status).toHaveBeenCalledWith(401);
54
58
  expect(mockRes.json).toHaveBeenCalledWith({
55
- error: 'Unauthorized',
56
- message: 'x-frigg-admin-api-key header required',
59
+ error: 'Missing or invalid Authorization header',
60
+ code: 'MISSING_AUTH',
57
61
  });
58
62
  expect(mockNext).not.toHaveBeenCalled();
59
63
  });
60
64
  });
61
65
 
62
66
  describe('API key validation', () => {
63
- it('should reject request with invalid API key', () => {
64
- mockReq.headers['x-frigg-admin-api-key'] = 'invalid-key';
67
+ it('should reject request with invalid API key', async () => {
68
+ mockReq.headers.authorization = 'Bearer invalid-key';
69
+ mockCommands.validateAdminApiKey.mockResolvedValue({
70
+ error: 401,
71
+ reason: 'Invalid API key',
72
+ code: 'INVALID_API_KEY',
73
+ });
74
+
75
+ await adminAuthMiddleware(mockReq, mockRes, mockNext);
76
+
77
+ expect(mockCommands.validateAdminApiKey).toHaveBeenCalledWith('invalid-key');
78
+ expect(mockRes.status).toHaveBeenCalledWith(401);
79
+ expect(mockRes.json).toHaveBeenCalledWith({
80
+ error: 'Invalid API key',
81
+ code: 'INVALID_API_KEY',
82
+ });
83
+ expect(mockNext).not.toHaveBeenCalled();
84
+ });
65
85
 
66
- validateAdminApiKey(mockReq, mockRes, mockNext);
86
+ it('should reject request with expired API key', async () => {
87
+ mockReq.headers.authorization = 'Bearer expired-key';
88
+ mockCommands.validateAdminApiKey.mockResolvedValue({
89
+ error: 401,
90
+ reason: 'API key has expired',
91
+ code: 'EXPIRED_API_KEY',
92
+ });
67
93
 
94
+ await adminAuthMiddleware(mockReq, mockRes, mockNext);
95
+
96
+ expect(mockCommands.validateAdminApiKey).toHaveBeenCalledWith('expired-key');
68
97
  expect(mockRes.status).toHaveBeenCalledWith(401);
69
98
  expect(mockRes.json).toHaveBeenCalledWith({
70
- error: 'Unauthorized',
71
- message: 'Invalid admin API key',
99
+ error: 'API key has expired',
100
+ code: 'EXPIRED_API_KEY',
72
101
  });
73
102
  expect(mockNext).not.toHaveBeenCalled();
74
103
  });
75
104
 
76
- it('should accept request with valid API key', () => {
77
- mockReq.headers['x-frigg-admin-api-key'] = 'test-admin-key-123';
105
+ it('should accept request with valid API key', async () => {
106
+ const validKey = 'valid-api-key-123';
107
+ mockReq.headers.authorization = `Bearer ${validKey}`;
108
+ mockCommands.validateAdminApiKey.mockResolvedValue({
109
+ valid: true,
110
+ apiKey: {
111
+ id: 'key-id-1',
112
+ name: 'test-key',
113
+ keyLast4: 'e123',
114
+ },
115
+ });
78
116
 
79
- validateAdminApiKey(mockReq, mockRes, mockNext);
117
+ await adminAuthMiddleware(mockReq, mockRes, mockNext);
80
118
 
119
+ expect(mockCommands.validateAdminApiKey).toHaveBeenCalledWith(validKey);
120
+ expect(mockReq.adminApiKey).toBeDefined();
121
+ expect(mockReq.adminApiKey.name).toBe('test-key');
122
+ expect(mockReq.adminAudit).toBeDefined();
123
+ expect(mockReq.adminAudit.apiKeyName).toBe('test-key');
124
+ expect(mockReq.adminAudit.apiKeyLast4).toBe('e123');
125
+ expect(mockReq.adminAudit.ipAddress).toBe('127.0.0.1');
81
126
  expect(mockNext).toHaveBeenCalled();
82
127
  expect(mockRes.status).not.toHaveBeenCalled();
83
128
  });
84
129
  });
130
+
131
+ describe('Error handling', () => {
132
+ it('should handle validation errors gracefully', async () => {
133
+ mockReq.headers.authorization = 'Bearer some-key';
134
+ mockCommands.validateAdminApiKey.mockRejectedValue(
135
+ new Error('Database error')
136
+ );
137
+
138
+ await adminAuthMiddleware(mockReq, mockRes, mockNext);
139
+
140
+ expect(mockRes.status).toHaveBeenCalledWith(500);
141
+ expect(mockRes.json).toHaveBeenCalledWith({
142
+ error: 'Authentication failed',
143
+ code: 'AUTH_ERROR',
144
+ });
145
+ expect(mockNext).not.toHaveBeenCalled();
146
+ });
147
+ });
85
148
  });
@@ -4,8 +4,13 @@ const { AdminScriptBase } = require('../../application/admin-script-base');
4
4
 
5
5
  // Mock dependencies
6
6
  jest.mock('../admin-auth-middleware', () => ({
7
- validateAdminApiKey: (req, res, next) => {
8
- // Mock auth - no audit trail with simplified auth
7
+ adminAuthMiddleware: (req, res, next) => {
8
+ // Mock auth - attach admin audit info
9
+ req.adminAudit = {
10
+ apiKeyName: 'test-key',
11
+ apiKeyLast4: '1234',
12
+ ipAddress: '127.0.0.1',
13
+ };
9
14
  next();
10
15
  },
11
16
  }));
@@ -54,8 +59,8 @@ describe('Admin Script Router', () => {
54
59
  };
55
60
 
56
61
  mockCommands = {
57
- createAdminProcess: jest.fn(),
58
- findAdminProcessById: jest.fn(),
62
+ createScriptExecution: jest.fn(),
63
+ findScriptExecutionById: jest.fn(),
59
64
  findRecentExecutions: jest.fn(),
60
65
  };
61
66
 
@@ -138,7 +143,7 @@ describe('Admin Script Router', () => {
138
143
  });
139
144
  });
140
145
 
141
- describe('POST /admin/scripts/:scriptName', () => {
146
+ describe('POST /admin/scripts/:scriptName/execute', () => {
142
147
  it('should execute script synchronously', async () => {
143
148
  mockRunner.execute.mockResolvedValue({
144
149
  executionId: 'exec-123',
@@ -149,7 +154,7 @@ describe('Admin Script Router', () => {
149
154
  });
150
155
 
151
156
  const response = await request(app)
152
- .post('/admin/scripts/test-script')
157
+ .post('/admin/scripts/test-script/execute')
153
158
  .send({
154
159
  params: { foo: 'bar' },
155
160
  mode: 'sync',
@@ -169,12 +174,12 @@ describe('Admin Script Router', () => {
169
174
  });
170
175
 
171
176
  it('should queue script for async execution', async () => {
172
- mockCommands.createAdminProcess.mockResolvedValue({
177
+ mockCommands.createScriptExecution.mockResolvedValue({
173
178
  id: 'exec-456',
174
179
  });
175
180
 
176
181
  const response = await request(app)
177
- .post('/admin/scripts/test-script')
182
+ .post('/admin/scripts/test-script/execute')
178
183
  .send({
179
184
  params: { foo: 'bar' },
180
185
  mode: 'async',
@@ -193,12 +198,12 @@ describe('Admin Script Router', () => {
193
198
  });
194
199
 
195
200
  it('should default to async mode', async () => {
196
- mockCommands.createAdminProcess.mockResolvedValue({
201
+ mockCommands.createScriptExecution.mockResolvedValue({
197
202
  id: 'exec-789',
198
203
  });
199
204
 
200
205
  const response = await request(app)
201
- .post('/admin/scripts/test-script')
206
+ .post('/admin/scripts/test-script/execute')
202
207
  .send({
203
208
  params: { foo: 'bar' },
204
209
  });
@@ -211,7 +216,7 @@ describe('Admin Script Router', () => {
211
216
  mockFactory.has.mockReturnValue(false);
212
217
 
213
218
  const response = await request(app)
214
- .post('/admin/scripts/non-existent')
219
+ .post('/admin/scripts/non-existent/execute')
215
220
  .send({
216
221
  params: {},
217
222
  });
@@ -221,15 +226,15 @@ describe('Admin Script Router', () => {
221
226
  });
222
227
  });
223
228
 
224
- describe('GET /admin/scripts/:scriptName/executions/:executionId', () => {
229
+ describe('GET /admin/executions/:executionId', () => {
225
230
  it('should return execution details', async () => {
226
- mockCommands.findAdminProcessById.mockResolvedValue({
231
+ mockCommands.findScriptExecutionById.mockResolvedValue({
227
232
  id: 'exec-123',
228
233
  scriptName: 'test-script',
229
234
  status: 'COMPLETED',
230
235
  });
231
236
 
232
- const response = await request(app).get('/admin/scripts/test-script/executions/exec-123');
237
+ const response = await request(app).get('/admin/executions/exec-123');
233
238
 
234
239
  expect(response.status).toBe(200);
235
240
  expect(response.body.id).toBe('exec-123');
@@ -237,14 +242,14 @@ describe('Admin Script Router', () => {
237
242
  });
238
243
 
239
244
  it('should return 404 for non-existent execution', async () => {
240
- mockCommands.findAdminProcessById.mockResolvedValue({
245
+ mockCommands.findScriptExecutionById.mockResolvedValue({
241
246
  error: 404,
242
247
  reason: 'Execution not found',
243
248
  code: 'EXECUTION_NOT_FOUND',
244
249
  });
245
250
 
246
251
  const response = await request(app).get(
247
- '/admin/scripts/test-script/executions/non-existent'
252
+ '/admin/executions/non-existent'
248
253
  );
249
254
 
250
255
  expect(response.status).toBe(404);
@@ -252,28 +257,24 @@ describe('Admin Script Router', () => {
252
257
  });
253
258
  });
254
259
 
255
- describe('GET /admin/scripts/:scriptName/executions', () => {
256
- it('should list executions for specific script', async () => {
260
+ describe('GET /admin/executions', () => {
261
+ it('should list recent executions', async () => {
257
262
  mockCommands.findRecentExecutions.mockResolvedValue([
258
263
  { id: 'exec-1', scriptName: 'test-script', status: 'COMPLETED' },
259
264
  { id: 'exec-2', scriptName: 'test-script', status: 'RUNNING' },
260
265
  ]);
261
266
 
262
- const response = await request(app).get('/admin/scripts/test-script/executions');
267
+ const response = await request(app).get('/admin/executions');
263
268
 
264
269
  expect(response.status).toBe(200);
265
270
  expect(response.body.executions).toHaveLength(2);
266
- expect(mockCommands.findRecentExecutions).toHaveBeenCalledWith({
267
- scriptName: 'test-script',
268
- limit: 50,
269
- });
270
271
  });
271
272
 
272
273
  it('should accept query parameters', async () => {
273
274
  mockCommands.findRecentExecutions.mockResolvedValue([]);
274
275
 
275
276
  await request(app).get(
276
- '/admin/scripts/test-script/executions?status=COMPLETED&limit=10'
277
+ '/admin/executions?scriptName=test-script&status=COMPLETED&limit=10'
277
278
  );
278
279
 
279
280
  expect(mockCommands.findRecentExecutions).toHaveBeenCalledWith({