@friggframework/admin-scripts 2.0.0--canary.522.cbd3d5a.0 → 2.0.0--canary.517.35ee143.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.
Files changed (30) hide show
  1. package/index.js +2 -2
  2. package/package.json +6 -9
  3. package/src/application/__tests__/admin-frigg-commands.test.js +19 -19
  4. package/src/application/__tests__/admin-script-base.test.js +2 -2
  5. package/src/application/__tests__/script-runner.test.js +146 -16
  6. package/src/application/admin-frigg-commands.js +8 -8
  7. package/src/application/admin-script-base.js +3 -5
  8. package/src/application/script-runner.js +125 -129
  9. package/src/application/use-cases/__tests__/delete-schedule-use-case.test.js +168 -0
  10. package/src/application/use-cases/__tests__/get-effective-schedule-use-case.test.js +114 -0
  11. package/src/application/use-cases/__tests__/upsert-schedule-use-case.test.js +201 -0
  12. package/src/application/use-cases/delete-schedule-use-case.js +108 -0
  13. package/src/application/use-cases/get-effective-schedule-use-case.js +78 -0
  14. package/src/application/use-cases/index.js +18 -0
  15. package/src/application/use-cases/upsert-schedule-use-case.js +127 -0
  16. package/src/builtins/__tests__/integration-health-check.test.js +1 -1
  17. package/src/builtins/__tests__/oauth-token-refresh.test.js +1 -1
  18. package/src/builtins/integration-health-check.js +1 -1
  19. package/src/builtins/oauth-token-refresh.js +1 -1
  20. package/src/infrastructure/__tests__/admin-auth-middleware.test.js +32 -95
  21. package/src/infrastructure/__tests__/admin-script-router.test.js +46 -47
  22. package/src/infrastructure/admin-auth-middleware.js +5 -43
  23. package/src/infrastructure/admin-script-router.js +38 -32
  24. package/src/infrastructure/script-executor-handler.js +2 -2
  25. package/src/application/__tests__/dry-run-http-interceptor.test.js +0 -313
  26. package/src/application/__tests__/dry-run-repository-wrapper.test.js +0 -257
  27. package/src/application/__tests__/schedule-management-use-case.test.js +0 -276
  28. package/src/application/dry-run-http-interceptor.js +0 -296
  29. package/src/application/dry-run-repository-wrapper.js +0 -261
  30. package/src/application/schedule-management-use-case.js +0 -230
@@ -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
@@ -29,8 +27,10 @@ class ScriptRunner {
29
27
  * @param {string} options.trigger - 'MANUAL' | 'SCHEDULED' | 'QUEUE'
30
28
  * @param {string} options.mode - 'sync' | 'async'
31
29
  * @param {Object} options.audit - Audit info { apiKeyName, apiKeyLast4, ipAddress }
32
- * @param {string} options.executionId - Reuse existing execution ID
33
- * @param {boolean} options.dryRun - Execute in dry-run mode (no writes, log operations)
30
+ * @param {string} options.executionId - Reuse existing AdminProcess record ID (NOT the Lambda execution ID).
31
+ * This is the database ID from the AdminProcess collection/table that tracks script executions.
32
+ * Pass this when resuming a queued execution to continue using the same execution record.
33
+ * @param {boolean} options.dryRun - Dry-run mode: validate and preview without executing
34
34
  */
35
35
  async execute(scriptName, params = {}, options = {}) {
36
36
  const { trigger = 'MANUAL', audit = {}, executionId: existingExecutionId, dryRun = false } = options;
@@ -40,17 +40,22 @@ class ScriptRunner {
40
40
  const definition = scriptClass.Definition;
41
41
 
42
42
  // Validate integrationFactory requirement
43
- if (definition.config?.requiresIntegrationFactory && !this.integrationFactory) {
43
+ if (definition.config?.requireIntegrationInstance && !this.integrationFactory) {
44
44
  throw new Error(
45
45
  `Script "${scriptName}" requires integrationFactory but none was provided`
46
46
  );
47
47
  }
48
48
 
49
+ // Dry-run mode: validate and return preview without executing
50
+ if (dryRun) {
51
+ return this.createDryRunPreview(scriptName, definition, params);
52
+ }
53
+
49
54
  let executionId = existingExecutionId;
50
55
 
51
56
  // Create execution record if not provided
52
57
  if (!executionId) {
53
- const execution = await this.commands.createScriptExecution({
58
+ const execution = await this.commands.createAdminProcess({
54
59
  scriptName,
55
60
  scriptVersion: definition.version,
56
61
  trigger,
@@ -64,25 +69,13 @@ class ScriptRunner {
64
69
  const startTime = new Date();
65
70
 
66
71
  try {
67
- // Update status to RUNNING (skip in dry-run)
68
- if (!dryRun) {
69
- await this.commands.updateScriptExecutionStatus(executionId, 'RUNNING');
70
- }
72
+ await this.commands.updateAdminProcessState(executionId, 'RUNNING');
71
73
 
72
74
  // 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
- }
75
+ const frigg = createAdminFriggCommands({
76
+ executionId,
77
+ integrationFactory: this.integrationFactory,
78
+ });
86
79
 
87
80
  // Create script instance
88
81
  const script = this.scriptFactory.createInstance(scriptName, {
@@ -97,34 +90,15 @@ class ScriptRunner {
97
90
  const endTime = new Date();
98
91
  const durationMs = endTime - startTime;
99
92
 
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
- }
93
+ await this.commands.completeAdminProcess(executionId, {
94
+ state: 'COMPLETED',
95
+ output,
96
+ metrics: {
97
+ startTime: startTime.toISOString(),
98
+ endTime: endTime.toISOString(),
99
+ durationMs,
100
+ },
101
+ });
128
102
 
129
103
  return {
130
104
  executionId,
@@ -134,31 +108,26 @@ class ScriptRunner {
134
108
  metrics: { durationMs },
135
109
  };
136
110
  } catch (error) {
137
- // Calculate metrics even on failure
138
111
  const endTime = new Date();
139
112
  const durationMs = endTime - startTime;
140
113
 
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
- }
114
+ await this.commands.completeAdminProcess(executionId, {
115
+ state: 'FAILED',
116
+ error: {
117
+ name: error.name,
118
+ message: error.message,
119
+ stack: error.stack,
120
+ },
121
+ metrics: {
122
+ startTime: startTime.toISOString(),
123
+ endTime: endTime.toISOString(),
124
+ durationMs,
125
+ },
126
+ });
157
127
 
158
128
  return {
159
129
  executionId,
160
- dryRun,
161
- status: dryRun ? 'DRY_RUN_FAILED' : 'FAILED',
130
+ status: 'FAILED',
162
131
  scriptName,
163
132
  error: {
164
133
  name: error.name,
@@ -170,80 +139,107 @@ class ScriptRunner {
170
139
  }
171
140
 
172
141
  /**
173
- * Create dry-run version of AdminFriggCommands
174
- * Intercepts all write operations and logs them
142
+ * Create dry-run preview without executing the script
143
+ * Validates inputs and shows what would be executed
175
144
  *
176
- * @param {Array} operationLog - Array to collect logged operations
177
- * @returns {Object} Wrapped AdminFriggCommands
145
+ * @param {string} scriptName - Script name
146
+ * @param {Object} definition - Script definition
147
+ * @param {Object} params - Input parameters
148
+ * @returns {Object} Dry-run preview
178
149
  */
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);
150
+ createDryRunPreview(scriptName, definition, params) {
151
+ const validation = this.validateParams(definition, params);
152
+
153
+ return {
154
+ dryRun: true,
155
+ status: validation.valid ? 'DRY_RUN_VALID' : 'DRY_RUN_INVALID',
156
+ scriptName,
157
+ preview: {
158
+ script: {
159
+ name: definition.name,
160
+ version: definition.version,
161
+ description: definition.description,
162
+ requireIntegrationInstance: definition.config?.requireIntegrationInstance || false,
163
+ },
164
+ input: params,
165
+ inputSchema: definition.inputSchema || null,
166
+ validation,
167
+ },
168
+ message: validation.valid
169
+ ? 'Dry-run validation passed. Script is ready to execute with provided parameters.'
170
+ : `Dry-run validation failed: ${validation.errors.join(', ')}`,
171
+ };
172
+ }
188
173
 
189
- // Create dry-run HTTP client
190
- const dryRunHttpClient = createDryRunHttpClient(operationLog);
174
+ /**
175
+ * Validate parameters against script's input schema
176
+ *
177
+ * @param {Object} definition - Script definition
178
+ * @param {Object} params - Input parameters
179
+ * @returns {Object} Validation result { valid, errors }
180
+ */
181
+ validateParams(definition, params) {
182
+ const errors = [];
183
+ const schema = definition.inputSchema;
191
184
 
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);
185
+ if (!schema) {
186
+ return { valid: true, errors: [] };
187
+ }
196
188
 
197
- // Inject dry-run HTTP client into the integration instance
198
- injectDryRunHttpClient(instance, dryRunHttpClient);
189
+ // Check required fields
190
+ if (schema.required && Array.isArray(schema.required)) {
191
+ for (const field of schema.required) {
192
+ if (params[field] === undefined || params[field] === null) {
193
+ errors.push(`Missing required parameter: ${field}`);
194
+ }
195
+ }
196
+ }
199
197
 
200
- return instance;
201
- };
198
+ // Basic type validation for properties
199
+ if (schema.properties) {
200
+ for (const [key, prop] of Object.entries(schema.properties)) {
201
+ const value = params[key];
202
+ if (value !== undefined && value !== null) {
203
+ const typeError = this.validateType(key, value, prop);
204
+ if (typeError) {
205
+ errors.push(typeError);
206
+ }
207
+ }
208
+ }
209
+ }
202
210
 
203
- return wrappedCommands;
211
+ return { valid: errors.length === 0, errors };
204
212
  }
205
213
 
206
214
  /**
207
- * Summarize operations from dry-run log
208
- *
209
- * @param {Array} log - Operation log
210
- * @returns {Object} Summary statistics
215
+ * Validate a single parameter type
211
216
  */
212
- summarizeOperations(log) {
213
- const summary = {
214
- totalOperations: log.length,
215
- databaseWrites: 0,
216
- httpRequests: 0,
217
- byOperation: {},
218
- byModel: {},
219
- byService: {},
220
- };
217
+ validateType(key, value, schema) {
218
+ const expectedType = schema.type;
219
+ if (!expectedType) return null;
221
220
 
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
- }
221
+ const actualType = Array.isArray(value) ? 'array' : typeof value;
237
222
 
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
- }
223
+ if (expectedType === 'integer' && (typeof value !== 'number' || !Number.isInteger(value))) {
224
+ return `Parameter "${key}" must be an integer`;
225
+ }
226
+ if (expectedType === 'number' && typeof value !== 'number') {
227
+ return `Parameter "${key}" must be a number`;
228
+ }
229
+ if (expectedType === 'string' && typeof value !== 'string') {
230
+ return `Parameter "${key}" must be a string`;
231
+ }
232
+ if (expectedType === 'boolean' && typeof value !== 'boolean') {
233
+ return `Parameter "${key}" must be a boolean`;
234
+ }
235
+ if (expectedType === 'array' && !Array.isArray(value)) {
236
+ return `Parameter "${key}" must be an array`;
237
+ }
238
+ if (expectedType === 'object' && (typeof value !== 'object' || Array.isArray(value))) {
239
+ return `Parameter "${key}" must be an object`;
244
240
  }
245
241
 
246
- return summary;
242
+ return null;
247
243
  }
248
244
  }
249
245
 
@@ -0,0 +1,168 @@
1
+ const { DeleteScheduleUseCase } = require('../delete-schedule-use-case');
2
+
3
+ describe('DeleteScheduleUseCase', () => {
4
+ let useCase;
5
+ let mockCommands;
6
+ let mockSchedulerAdapter;
7
+ let mockScriptFactory;
8
+
9
+ beforeEach(() => {
10
+ mockCommands = {
11
+ deleteSchedule: jest.fn(),
12
+ };
13
+
14
+ mockSchedulerAdapter = {
15
+ deleteSchedule: jest.fn(),
16
+ };
17
+
18
+ mockScriptFactory = {
19
+ has: jest.fn(),
20
+ get: jest.fn(),
21
+ };
22
+
23
+ useCase = new DeleteScheduleUseCase({
24
+ commands: mockCommands,
25
+ schedulerAdapter: mockSchedulerAdapter,
26
+ scriptFactory: mockScriptFactory,
27
+ });
28
+ });
29
+
30
+ describe('execute', () => {
31
+ it('should delete schedule and cleanup external scheduler', async () => {
32
+ const deletedSchedule = {
33
+ scriptName: 'test-script',
34
+ externalScheduleId: 'arn:aws:scheduler:us-east-1:123:schedule/test',
35
+ };
36
+
37
+ mockScriptFactory.has.mockReturnValue(true);
38
+ mockScriptFactory.get.mockReturnValue({ Definition: {} });
39
+ mockCommands.deleteSchedule.mockResolvedValue({
40
+ deletedCount: 1,
41
+ deleted: deletedSchedule,
42
+ });
43
+ mockSchedulerAdapter.deleteSchedule.mockResolvedValue();
44
+
45
+ const result = await useCase.execute('test-script');
46
+
47
+ expect(result.success).toBe(true);
48
+ expect(result.deletedCount).toBe(1);
49
+ expect(result.message).toBe('Schedule override removed');
50
+ expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith('test-script');
51
+ });
52
+
53
+ it('should not call scheduler when no external rule exists', async () => {
54
+ mockScriptFactory.has.mockReturnValue(true);
55
+ mockScriptFactory.get.mockReturnValue({ Definition: {} });
56
+ mockCommands.deleteSchedule.mockResolvedValue({
57
+ deletedCount: 1,
58
+ deleted: { scriptName: 'test-script' }, // No externalScheduleId
59
+ });
60
+
61
+ const result = await useCase.execute('test-script');
62
+
63
+ expect(result.success).toBe(true);
64
+ expect(mockSchedulerAdapter.deleteSchedule).not.toHaveBeenCalled();
65
+ });
66
+
67
+ it('should handle scheduler delete errors gracefully with warning', async () => {
68
+ mockScriptFactory.has.mockReturnValue(true);
69
+ mockScriptFactory.get.mockReturnValue({ Definition: {} });
70
+ mockCommands.deleteSchedule.mockResolvedValue({
71
+ deletedCount: 1,
72
+ deleted: {
73
+ scriptName: 'test-script',
74
+ externalScheduleId: 'arn:aws:scheduler:us-east-1:123:schedule/test',
75
+ },
76
+ });
77
+ mockSchedulerAdapter.deleteSchedule.mockRejectedValue(
78
+ new Error('Scheduler delete failed')
79
+ );
80
+
81
+ const result = await useCase.execute('test-script');
82
+
83
+ expect(result.success).toBe(true);
84
+ expect(result.schedulerWarning).toBe('Scheduler delete failed');
85
+ });
86
+
87
+ it('should return definition schedule as effective after deletion', async () => {
88
+ const definitionSchedule = {
89
+ enabled: true,
90
+ cronExpression: '0 6 * * *',
91
+ timezone: 'America/Los_Angeles',
92
+ };
93
+
94
+ mockScriptFactory.has.mockReturnValue(true);
95
+ mockScriptFactory.get.mockReturnValue({
96
+ Definition: { schedule: definitionSchedule },
97
+ });
98
+ mockCommands.deleteSchedule.mockResolvedValue({
99
+ deletedCount: 1,
100
+ deleted: { scriptName: 'test-script' },
101
+ });
102
+
103
+ const result = await useCase.execute('test-script');
104
+
105
+ expect(result.effectiveSchedule.source).toBe('definition');
106
+ expect(result.effectiveSchedule.enabled).toBe(true);
107
+ expect(result.effectiveSchedule.cronExpression).toBe('0 6 * * *');
108
+ expect(result.effectiveSchedule.timezone).toBe('America/Los_Angeles');
109
+ });
110
+
111
+ it('should default timezone to UTC when not in definition', async () => {
112
+ mockScriptFactory.has.mockReturnValue(true);
113
+ mockScriptFactory.get.mockReturnValue({
114
+ Definition: { schedule: { enabled: true, cronExpression: '0 6 * * *' } },
115
+ });
116
+ mockCommands.deleteSchedule.mockResolvedValue({
117
+ deletedCount: 1,
118
+ deleted: { scriptName: 'test-script' },
119
+ });
120
+
121
+ const result = await useCase.execute('test-script');
122
+
123
+ expect(result.effectiveSchedule.timezone).toBe('UTC');
124
+ });
125
+
126
+ it('should return none as effective when no definition schedule', async () => {
127
+ mockScriptFactory.has.mockReturnValue(true);
128
+ mockScriptFactory.get.mockReturnValue({ Definition: {} });
129
+ mockCommands.deleteSchedule.mockResolvedValue({
130
+ deletedCount: 1,
131
+ deleted: { scriptName: 'test-script' },
132
+ });
133
+
134
+ const result = await useCase.execute('test-script');
135
+
136
+ expect(result.effectiveSchedule.source).toBe('none');
137
+ expect(result.effectiveSchedule.enabled).toBe(false);
138
+ });
139
+
140
+ it('should return correct message when no schedule found', async () => {
141
+ mockScriptFactory.has.mockReturnValue(true);
142
+ mockScriptFactory.get.mockReturnValue({ Definition: {} });
143
+ mockCommands.deleteSchedule.mockResolvedValue({
144
+ deletedCount: 0,
145
+ deleted: null,
146
+ });
147
+
148
+ const result = await useCase.execute('test-script');
149
+
150
+ expect(result.success).toBe(true);
151
+ expect(result.deletedCount).toBe(0);
152
+ expect(result.message).toBe('No schedule override found');
153
+ });
154
+
155
+ it('should throw SCRIPT_NOT_FOUND error when script does not exist', async () => {
156
+ mockScriptFactory.has.mockReturnValue(false);
157
+
158
+ await expect(useCase.execute('non-existent'))
159
+ .rejects.toThrow('Script "non-existent" not found');
160
+
161
+ try {
162
+ await useCase.execute('non-existent');
163
+ } catch (error) {
164
+ expect(error.code).toBe('SCRIPT_NOT_FOUND');
165
+ }
166
+ });
167
+ });
168
+ });
@@ -0,0 +1,114 @@
1
+ const { GetEffectiveScheduleUseCase } = require('../get-effective-schedule-use-case');
2
+
3
+ describe('GetEffectiveScheduleUseCase', () => {
4
+ let useCase;
5
+ let mockCommands;
6
+ let mockScriptFactory;
7
+
8
+ beforeEach(() => {
9
+ mockCommands = {
10
+ getScheduleByScriptName: jest.fn(),
11
+ };
12
+
13
+ mockScriptFactory = {
14
+ has: jest.fn(),
15
+ get: jest.fn(),
16
+ };
17
+
18
+ useCase = new GetEffectiveScheduleUseCase({
19
+ commands: mockCommands,
20
+ scriptFactory: mockScriptFactory,
21
+ });
22
+ });
23
+
24
+ describe('execute', () => {
25
+ it('should return database schedule when override exists', async () => {
26
+ const dbSchedule = {
27
+ scriptName: 'test-script',
28
+ enabled: true,
29
+ cronExpression: '0 9 * * *',
30
+ timezone: 'UTC',
31
+ };
32
+
33
+ mockScriptFactory.has.mockReturnValue(true);
34
+ mockScriptFactory.get.mockReturnValue({ Definition: {} });
35
+ mockCommands.getScheduleByScriptName.mockResolvedValue(dbSchedule);
36
+
37
+ const result = await useCase.execute('test-script');
38
+
39
+ expect(result.source).toBe('database');
40
+ expect(result.schedule).toEqual(dbSchedule);
41
+ });
42
+
43
+ it('should return definition schedule when no database override', async () => {
44
+ const definitionSchedule = {
45
+ enabled: true,
46
+ cronExpression: '0 12 * * *',
47
+ timezone: 'America/New_York',
48
+ };
49
+
50
+ mockScriptFactory.has.mockReturnValue(true);
51
+ mockScriptFactory.get.mockReturnValue({
52
+ Definition: { schedule: definitionSchedule },
53
+ });
54
+ mockCommands.getScheduleByScriptName.mockResolvedValue(null);
55
+
56
+ const result = await useCase.execute('test-script');
57
+
58
+ expect(result.source).toBe('definition');
59
+ expect(result.schedule.enabled).toBe(true);
60
+ expect(result.schedule.cronExpression).toBe('0 12 * * *');
61
+ expect(result.schedule.timezone).toBe('America/New_York');
62
+ });
63
+
64
+ it('should default timezone to UTC when not specified in definition', async () => {
65
+ mockScriptFactory.has.mockReturnValue(true);
66
+ mockScriptFactory.get.mockReturnValue({
67
+ Definition: { schedule: { enabled: true, cronExpression: '0 12 * * *' } },
68
+ });
69
+ mockCommands.getScheduleByScriptName.mockResolvedValue(null);
70
+
71
+ const result = await useCase.execute('test-script');
72
+
73
+ expect(result.schedule.timezone).toBe('UTC');
74
+ });
75
+
76
+ it('should return none when no schedule configured', async () => {
77
+ mockScriptFactory.has.mockReturnValue(true);
78
+ mockScriptFactory.get.mockReturnValue({ Definition: {} });
79
+ mockCommands.getScheduleByScriptName.mockResolvedValue(null);
80
+
81
+ const result = await useCase.execute('test-script');
82
+
83
+ expect(result.source).toBe('none');
84
+ expect(result.schedule.enabled).toBe(false);
85
+ expect(result.schedule.scriptName).toBe('test-script');
86
+ });
87
+
88
+ it('should return none when definition schedule is disabled', async () => {
89
+ mockScriptFactory.has.mockReturnValue(true);
90
+ mockScriptFactory.get.mockReturnValue({
91
+ Definition: { schedule: { enabled: false } },
92
+ });
93
+ mockCommands.getScheduleByScriptName.mockResolvedValue(null);
94
+
95
+ const result = await useCase.execute('test-script');
96
+
97
+ expect(result.source).toBe('none');
98
+ expect(result.schedule.enabled).toBe(false);
99
+ });
100
+
101
+ it('should throw SCRIPT_NOT_FOUND error when script does not exist', async () => {
102
+ mockScriptFactory.has.mockReturnValue(false);
103
+
104
+ await expect(useCase.execute('non-existent'))
105
+ .rejects.toThrow('Script "non-existent" not found');
106
+
107
+ try {
108
+ await useCase.execute('non-existent');
109
+ } catch (error) {
110
+ expect(error.code).toBe('SCRIPT_NOT_FOUND');
111
+ }
112
+ });
113
+ });
114
+ });