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