@friggframework/admin-scripts 2.0.0--canary.522.cbd3d5a.0 → 2.0.0--canary.517.21b69ac.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 (31) hide show
  1. package/PR_517_REVIEW_TRACKER.md +56 -0
  2. package/index.js +12 -3
  3. package/package.json +6 -9
  4. package/src/application/__tests__/admin-frigg-commands.test.js +19 -19
  5. package/src/application/__tests__/admin-script-base.test.js +100 -84
  6. package/src/application/__tests__/script-runner.test.js +146 -16
  7. package/src/application/admin-frigg-commands.js +20 -32
  8. package/src/application/admin-script-base.js +20 -99
  9. package/src/application/script-runner.js +131 -135
  10. package/src/application/use-cases/__tests__/delete-schedule-use-case.test.js +168 -0
  11. package/src/application/use-cases/__tests__/get-effective-schedule-use-case.test.js +114 -0
  12. package/src/application/use-cases/__tests__/upsert-schedule-use-case.test.js +201 -0
  13. package/src/application/use-cases/delete-schedule-use-case.js +108 -0
  14. package/src/application/use-cases/get-effective-schedule-use-case.js +78 -0
  15. package/src/application/use-cases/index.js +18 -0
  16. package/src/application/use-cases/upsert-schedule-use-case.js +127 -0
  17. package/src/builtins/__tests__/integration-health-check.test.js +67 -60
  18. package/src/builtins/__tests__/oauth-token-refresh.test.js +45 -37
  19. package/src/builtins/integration-health-check.js +23 -24
  20. package/src/builtins/oauth-token-refresh.js +19 -20
  21. package/src/infrastructure/__tests__/admin-auth-middleware.test.js +32 -95
  22. package/src/infrastructure/__tests__/admin-script-router.test.js +46 -47
  23. package/src/infrastructure/admin-auth-middleware.js +5 -43
  24. package/src/infrastructure/admin-script-router.js +38 -32
  25. package/src/infrastructure/script-executor-handler.js +2 -2
  26. package/src/application/__tests__/dry-run-http-interceptor.test.js +0 -313
  27. package/src/application/__tests__/dry-run-repository-wrapper.test.js +0 -257
  28. package/src/application/__tests__/schedule-management-use-case.test.js +0 -276
  29. package/src/application/dry-run-http-interceptor.js +0 -296
  30. package/src/application/dry-run-repository-wrapper.js +0 -261
  31. package/src/application/schedule-management-use-case.js +0 -230
@@ -0,0 +1,56 @@
1
+ # PR #517 Comment Tracker
2
+
3
+ ## ✅ ADDRESSED - Ready to Reply
4
+
5
+ | # | File:Line | Original Comment | What We Did | Reply |
6
+ |---|-----------|------------------|-------------|-------|
7
+ | 1 | `admin-frigg-commands.js:15` | "I don't think this should even exist, we're just duplicating methods" | Renamed to `AdminScriptContext`, clarified as facade pattern | Renamed to `AdminScriptContext`. It's a facade - wraps repositories so scripts have one API instead of multiple imports. Open to discussing alternatives. |
8
+ | 2 | `admin-script-base.js:94` | "Commands should come via constructor" | Changed to constructor injection | Done. Context now passed via constructor, scripts access via `this.context`. |
9
+ | 3 | `admin-script-base.js:109` | "logging does not belong here" | Removed logging from base class | Removed. Scripts use `this.context.log()` which persists to admin process record. |
10
+ | 4 | `admin-script-base.js:54` | "I would rename to requireIntegrationInstance" | Already renamed | Done - already renamed in current code. |
11
+ | 5 | `admin-script-base.js:56` | "This is just a duplication of the static Definition" | Cleaned up display object | Cleaned up. `display` now only holds UI overrides (category, icon). Label/description fall back to top-level via static methods. |
12
+ | 6 | `schedule-management-use-case.js:1` | "A use case should have single entry point" | Already split in previous session | Already split into `UpsertScheduleUseCase`, `DeleteScheduleUseCase`, `GetEffectiveScheduleUseCase`. |
13
+ | 7 | `package.json:20` | "chai and sinon should slowly be pushed away" | Sinon removed in previous session | Sinon already removed. Will remove chai too. |
14
+
15
+ ---
16
+
17
+ ## 📝 NEEDS RESPONSE ONLY - No Code Change Required
18
+
19
+ | # | File:Line | Original Comment | Reply |
20
+ |---|-----------|------------------|-------|
21
+ | 8 | `admin-frigg-commands.js:160` | "this does not belong here" (queueScript) | queueScript enables self-queuing pattern (fan-out, pagination, retries). It's here so scripts don't need queue internals. Could move to separate utility if preferred. |
22
+ | 9 | `admin-script-base.js:39` | "why do we need the source?" | Distinguishes builtin vs user-defined scripts. UI can filter differently, builtins could have special handling. Could remove if not needed. |
23
+ | 10 | `admin-script-base.js:42` | "what's the idea with these schemas?" | Optional JSON Schema for validation/documentation. Could wire to OpenAPI or dynamic UI forms. Not critical for v1 - could remove and add later. |
24
+ | 11 | `admin-script-base.js:46` | "enabled property confusion" | Agreed the matrix is confusing. Intent: `schedule.enabled` controls auto-trigger independent of registration. Could simplify to just use presence in appDefinition. |
25
+ | 12 | `admin-script-base.js:52` | "Do we have retry logic in place already?" | Not yet - placeholder for Phase 2. Could remove until we build it. |
26
+ | 13 | `admin-script-base.js:81` | "What is the executionId?" | ID of AdminProcess record tracking this execution. Used to persist logs and update status. Created before script runs, passed to constructor. |
27
+ | 14 | `schedule-management-use-case.js:89` | "why save to database if EventBridge is source of truth?" | Database stores user's config override. EventBridge is execution engine. On deploy, we sync DB to EventBridge. Tracks user config vs code default. |
28
+
29
+ ---
30
+
31
+ ## 🔧 OUTSTANDING - Needs Code Changes
32
+
33
+ | # | File:Line | Original Comment | Task |
34
+ |---|-----------|------------------|------|
35
+ | 15 | `docs/architecture-decisions/005-admin-script-runner.md:71` | "What is 'frigg' in this parameter?" | Update ADR - rename `frigg` to `context` throughout |
36
+ | 16 | `package.json:22` | "We already use nock for http request mocking" | Remove msw if present, use nock consistently |
37
+ | 17 | `package.json:12` | "why mongoose?" | Check if mongoose needed or can be removed |
38
+ | 18 | `schedule-management-use-case.js:109` | "leaking AWS specifics" | Abstract behind SchedulerAdapter, remove EventBridge references from use case |
39
+ | 19 | `schedule-management-use-case.js:138` | "should not mention EventBridge here" | Same as above |
40
+ | 20 | `adapters/aws-scheduler-adapter.js:30` | "should not infer/guess/default any variable" | Remove defaults, require explicit config |
41
+ | 21 | `adapters/scheduler-adapter-factory.js:48` | "env var confusion, prefer appDefinition" | Move scheduler config to appDefinition |
42
+ | 22 | `script-runner.js:36` | "we should not assume default values" | Remove defaults, require explicit values |
43
+ | 23 | `dry-run-http-interceptor.js:1` | "I don't understand why this is needed" | Explain or remove - was for intercepting HTTP in dry-run mode |
44
+ | 24 | `dry-run-repository-wrapper.js:1` | "This is smelly" | Review/remove - was for wrapping repos in dry-run mode |
45
+ | 25 | `.github/workflows/release.yml:11` | "why do we need those?" | Check release workflow changes |
46
+
47
+ ---
48
+
49
+ ## ❓ NEEDS DISCUSSION - Architectural Decisions
50
+
51
+ | # | File:Line | Original Comment | Decision Needed |
52
+ |---|-----------|------------------|-----------------|
53
+ | 26 | `admin-script-base.js:39` | source field | Keep BUILTIN/USER_DEFINED or remove? |
54
+ | 27 | `admin-script-base.js:42` | inputSchema/outputSchema | Keep for future or remove for now? |
55
+ | 28 | `admin-script-base.js:46` | schedule.enabled | Simplify to just appDefinition presence? |
56
+ | 29 | `admin-script-base.js:52` | maxRetries | Remove placeholder or keep? |
package/index.js CHANGED
@@ -8,11 +8,17 @@
8
8
  // Application Services
9
9
  const { ScriptFactory, getScriptFactory, createScriptFactory } = require('./src/application/script-factory');
10
10
  const { AdminScriptBase } = require('./src/application/admin-script-base');
11
- const { AdminFriggCommands, createAdminFriggCommands } = require('./src/application/admin-frigg-commands');
11
+ const {
12
+ AdminScriptContext,
13
+ createAdminScriptContext,
14
+ // Legacy aliases (deprecated)
15
+ AdminFriggCommands,
16
+ createAdminFriggCommands,
17
+ } = require('./src/application/admin-frigg-commands');
12
18
  const { ScriptRunner, createScriptRunner } = require('./src/application/script-runner');
13
19
 
14
20
  // Infrastructure
15
- const { adminAuthMiddleware } = require('./src/infrastructure/admin-auth-middleware');
21
+ const { validateAdminApiKey } = require('./src/infrastructure/admin-auth-middleware');
16
22
  const { router, app, handler: routerHandler } = require('./src/infrastructure/admin-script-router');
17
23
  const { handler: executorHandler } = require('./src/infrastructure/script-executor-handler');
18
24
 
@@ -39,13 +45,16 @@ module.exports = {
39
45
  ScriptFactory,
40
46
  getScriptFactory,
41
47
  createScriptFactory,
48
+ AdminScriptContext,
49
+ createAdminScriptContext,
50
+ // Legacy aliases (deprecated)
42
51
  AdminFriggCommands,
43
52
  createAdminFriggCommands,
44
53
  ScriptRunner,
45
54
  createScriptRunner,
46
55
 
47
56
  // Infrastructure layer
48
- adminAuthMiddleware,
57
+ validateAdminApiKey,
49
58
  router,
50
59
  app,
51
60
  routerHandler,
package/package.json CHANGED
@@ -1,27 +1,24 @@
1
1
  {
2
2
  "name": "@friggframework/admin-scripts",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.522.cbd3d5a.0",
4
+ "version": "2.0.0--canary.517.21b69ac.0",
5
5
  "description": "Admin Script Runner for Frigg - Execute maintenance and operational scripts in hosted environments",
6
6
  "dependencies": {
7
7
  "@aws-sdk/client-scheduler": "^3.588.0",
8
- "@friggframework/core": "2.0.0--canary.522.cbd3d5a.0",
8
+ "@friggframework/core": "2.0.0--canary.517.21b69ac.0",
9
9
  "bcryptjs": "^2.4.3",
10
10
  "express": "^4.18.2",
11
11
  "lodash": "4.17.21",
12
- "mongoose": "6.11.6",
13
12
  "serverless-http": "^3.2.0",
14
13
  "uuid": "^9.0.1"
15
14
  },
16
15
  "devDependencies": {
17
- "@friggframework/eslint-config": "2.0.0--canary.522.cbd3d5a.0",
18
- "@friggframework/prettier-config": "2.0.0--canary.522.cbd3d5a.0",
19
- "@friggframework/test": "2.0.0--canary.522.cbd3d5a.0",
20
- "chai": "^4.3.6",
16
+ "@friggframework/eslint-config": "2.0.0--canary.517.21b69ac.0",
17
+ "@friggframework/prettier-config": "2.0.0--canary.517.21b69ac.0",
18
+ "@friggframework/test": "2.0.0--canary.517.21b69ac.0",
21
19
  "eslint": "^8.22.0",
22
20
  "jest": "^29.7.0",
23
21
  "prettier": "^2.7.1",
24
- "sinon": "^16.1.1",
25
22
  "supertest": "^7.1.4"
26
23
  },
27
24
  "scripts": {
@@ -49,5 +46,5 @@
49
46
  "maintenance",
50
47
  "operations"
51
48
  ],
52
- "gitHead": "cbd3d5a81315519842f308fc0bd786a3f8786b79"
49
+ "gitHead": "21b69ac223278457b95dd8ba04c51d713a1c8f56"
53
50
  }
@@ -5,7 +5,7 @@ jest.mock('@friggframework/core/integrations/repositories/integration-repository
5
5
  jest.mock('@friggframework/core/user/repositories/user-repository-factory');
6
6
  jest.mock('@friggframework/core/modules/repositories/module-repository-factory');
7
7
  jest.mock('@friggframework/core/credential/repositories/credential-repository-factory');
8
- jest.mock('@friggframework/core/admin-scripts/repositories/script-execution-repository-factory');
8
+ jest.mock('@friggframework/core/admin-scripts/repositories/admin-process-repository-factory');
9
9
  jest.mock('@friggframework/core/queues');
10
10
 
11
11
  describe('AdminFriggCommands', () => {
@@ -13,7 +13,7 @@ describe('AdminFriggCommands', () => {
13
13
  let mockUserRepo;
14
14
  let mockModuleRepo;
15
15
  let mockCredentialRepo;
16
- let mockScriptExecutionRepo;
16
+ let mockAdminProcessRepo;
17
17
  let mockQueuerUtil;
18
18
 
19
19
  beforeEach(() => {
@@ -46,8 +46,8 @@ describe('AdminFriggCommands', () => {
46
46
  updateCredential: jest.fn(),
47
47
  };
48
48
 
49
- mockScriptExecutionRepo = {
50
- appendExecutionLog: jest.fn().mockResolvedValue(undefined),
49
+ mockAdminProcessRepo = {
50
+ appendProcessLog: jest.fn().mockResolvedValue(undefined),
51
51
  };
52
52
 
53
53
  mockQueuerUtil = {
@@ -60,14 +60,14 @@ describe('AdminFriggCommands', () => {
60
60
  const { createUserRepository } = require('@friggframework/core/user/repositories/user-repository-factory');
61
61
  const { createModuleRepository } = require('@friggframework/core/modules/repositories/module-repository-factory');
62
62
  const { createCredentialRepository } = require('@friggframework/core/credential/repositories/credential-repository-factory');
63
- const { createScriptExecutionRepository } = require('@friggframework/core/admin-scripts/repositories/script-execution-repository-factory');
63
+ const { createAdminProcessRepository } = require('@friggframework/core/admin-scripts/repositories/admin-process-repository-factory');
64
64
  const { QueuerUtil } = require('@friggframework/core/queues');
65
65
 
66
66
  createIntegrationRepository.mockReturnValue(mockIntegrationRepo);
67
67
  createUserRepository.mockReturnValue(mockUserRepo);
68
68
  createModuleRepository.mockReturnValue(mockModuleRepo);
69
69
  createCredentialRepository.mockReturnValue(mockCredentialRepo);
70
- createScriptExecutionRepository.mockReturnValue(mockScriptExecutionRepo);
70
+ createAdminProcessRepository.mockReturnValue(mockAdminProcessRepo);
71
71
 
72
72
  // Mock QueuerUtil methods
73
73
  QueuerUtil.send = mockQueuerUtil.send;
@@ -158,16 +158,16 @@ describe('AdminFriggCommands', () => {
158
158
  expect(repo).toBe(mockCredentialRepo);
159
159
  });
160
160
 
161
- it('creates scriptExecutionRepository on first access', () => {
161
+ it('creates adminProcessRepository on first access', () => {
162
162
  const commands = new AdminFriggCommands();
163
- const { createScriptExecutionRepository } = require('@friggframework/core/admin-scripts/repositories/script-execution-repository-factory');
163
+ const { createAdminProcessRepository } = require('@friggframework/core/admin-scripts/repositories/admin-process-repository-factory');
164
164
 
165
- expect(createScriptExecutionRepository).not.toHaveBeenCalled();
165
+ expect(createAdminProcessRepository).not.toHaveBeenCalled();
166
166
 
167
- const repo = commands.scriptExecutionRepository;
167
+ const repo = commands.adminProcessRepository;
168
168
 
169
- expect(createScriptExecutionRepository).toHaveBeenCalledTimes(1);
170
- expect(repo).toBe(mockScriptExecutionRepo);
169
+ expect(createAdminProcessRepository).toHaveBeenCalledTimes(1);
170
+ expect(repo).toBe(mockAdminProcessRepo);
171
171
  });
172
172
  });
173
173
 
@@ -341,7 +341,7 @@ describe('AdminFriggCommands', () => {
341
341
 
342
342
  await expect(commands.instantiate('int_123')).rejects.toThrow(
343
343
  'instantiate() requires integrationFactory. ' +
344
- 'Set Definition.config.requiresIntegrationFactory = true'
344
+ 'Set Definition.config.requireIntegrationInstance = true'
345
345
  );
346
346
  });
347
347
 
@@ -551,15 +551,15 @@ describe('AdminFriggCommands', () => {
551
551
  it('log() persists if executionId set', async () => {
552
552
  const commands = new AdminFriggCommands({ executionId: 'exec_123' });
553
553
  // Force repository creation
554
- commands.scriptExecutionRepository;
554
+ commands.adminProcessRepository;
555
555
 
556
556
  commands.log('warn', 'Warning message', { detail: 'xyz' });
557
557
 
558
558
  // Give async operation a chance to execute
559
559
  await new Promise(resolve => setImmediate(resolve));
560
560
 
561
- expect(mockScriptExecutionRepo.appendExecutionLog).toHaveBeenCalled();
562
- const callArgs = mockScriptExecutionRepo.appendExecutionLog.mock.calls[0];
561
+ expect(mockAdminProcessRepo.appendProcessLog).toHaveBeenCalled();
562
+ const callArgs = mockAdminProcessRepo.appendProcessLog.mock.calls[0];
563
563
  expect(callArgs[0]).toBe('exec_123');
564
564
  expect(callArgs[1].level).toBe('warn');
565
565
  expect(callArgs[1].message).toBe('Warning message');
@@ -572,14 +572,14 @@ describe('AdminFriggCommands', () => {
572
572
 
573
573
  await new Promise(resolve => setImmediate(resolve));
574
574
 
575
- expect(mockScriptExecutionRepo.appendExecutionLog).not.toHaveBeenCalled();
575
+ expect(mockAdminProcessRepo.appendProcessLog).not.toHaveBeenCalled();
576
576
  });
577
577
 
578
578
  it('log() handles persistence failure gracefully', async () => {
579
579
  const commands = new AdminFriggCommands({ executionId: 'exec_123' });
580
580
  // Force repository creation
581
- commands.scriptExecutionRepository;
582
- mockScriptExecutionRepo.appendExecutionLog.mockRejectedValue(new Error('DB Error'));
581
+ commands.adminProcessRepository;
582
+ mockAdminProcessRepo.appendProcessLog.mockRejectedValue(new Error('DB Error'));
583
583
 
584
584
  // Should not throw
585
585
  expect(() => commands.log('error', 'Test error')).not.toThrow();
@@ -28,12 +28,11 @@ describe('AdminScriptBase', () => {
28
28
  config: {
29
29
  timeout: 600000,
30
30
  maxRetries: 3,
31
- requiresIntegrationFactory: true,
31
+ requireIntegrationInstance: true,
32
32
  },
33
33
  display: {
34
- label: 'Test Script',
35
- description: 'For testing',
36
34
  category: 'testing',
35
+ icon: 'test-icon',
37
36
  },
38
37
  };
39
38
  }
@@ -45,6 +44,15 @@ describe('AdminScriptBase', () => {
45
44
  expect(TestScript.Definition.schedule.enabled).toBe(true);
46
45
  expect(TestScript.Definition.config.timeout).toBe(600000);
47
46
  });
47
+
48
+ it('should have clean display object without redundant fields', () => {
49
+ // Default display should only have UI-specific fields
50
+ expect(AdminScriptBase.Definition.display).toBeDefined();
51
+ expect(AdminScriptBase.Definition.display.category).toBe('maintenance');
52
+ // Should NOT have redundant label/description
53
+ expect(AdminScriptBase.Definition.display.label).toBeUndefined();
54
+ expect(AdminScriptBase.Definition.display.description).toBeUndefined();
55
+ });
48
56
  });
49
57
 
50
58
  describe('Static methods', () => {
@@ -90,18 +98,68 @@ describe('AdminScriptBase', () => {
90
98
  source: 'USER_DEFINED',
91
99
  });
92
100
  });
101
+
102
+ it('getDisplayLabel() should return display.label or fall back to name', () => {
103
+ class ScriptWithLabel extends AdminScriptBase {
104
+ static Definition = {
105
+ name: 'my-script',
106
+ version: '1.0.0',
107
+ description: 'test',
108
+ display: { label: 'My Custom Label' },
109
+ };
110
+ }
111
+
112
+ class ScriptWithoutLabel extends AdminScriptBase {
113
+ static Definition = {
114
+ name: 'another-script',
115
+ version: '1.0.0',
116
+ description: 'test',
117
+ };
118
+ }
119
+
120
+ expect(ScriptWithLabel.getDisplayLabel()).toBe('My Custom Label');
121
+ expect(ScriptWithoutLabel.getDisplayLabel()).toBe('another-script');
122
+ });
123
+
124
+ it('getDisplayDescription() should return display.description or fall back to description', () => {
125
+ class ScriptWithDisplayDesc extends AdminScriptBase {
126
+ static Definition = {
127
+ name: 'my-script',
128
+ version: '1.0.0',
129
+ description: 'Technical description',
130
+ display: { description: 'User-friendly description' },
131
+ };
132
+ }
133
+
134
+ class ScriptWithoutDisplayDesc extends AdminScriptBase {
135
+ static Definition = {
136
+ name: 'another-script',
137
+ version: '1.0.0',
138
+ description: 'Technical description',
139
+ };
140
+ }
141
+
142
+ expect(ScriptWithDisplayDesc.getDisplayDescription()).toBe('User-friendly description');
143
+ expect(ScriptWithoutDisplayDesc.getDisplayDescription()).toBe('Technical description');
144
+ });
93
145
  });
94
146
 
95
147
  describe('Constructor', () => {
96
148
  it('should initialize with default values', () => {
97
149
  const script = new AdminScriptBase();
98
150
 
151
+ expect(script.context).toBeNull();
99
152
  expect(script.executionId).toBeNull();
100
- expect(script.logs).toEqual([]);
101
- expect(script._startTime).toBeNull();
102
153
  expect(script.integrationFactory).toBeNull();
103
154
  });
104
155
 
156
+ it('should accept context parameter', () => {
157
+ const mockContext = { log: jest.fn() };
158
+ const script = new AdminScriptBase({ context: mockContext });
159
+
160
+ expect(script.context).toBe(mockContext);
161
+ });
162
+
105
163
  it('should accept executionId parameter', () => {
106
164
  const script = new AdminScriptBase({ executionId: 'exec_123' });
107
165
 
@@ -117,13 +175,16 @@ describe('AdminScriptBase', () => {
117
175
  expect(script.integrationFactory).toBe(mockFactory);
118
176
  });
119
177
 
120
- it('should accept both executionId and integrationFactory', () => {
178
+ it('should accept all parameters together', () => {
179
+ const mockContext = { log: jest.fn() };
121
180
  const mockFactory = { mock: true };
122
181
  const script = new AdminScriptBase({
182
+ context: mockContext,
123
183
  executionId: 'exec_456',
124
184
  integrationFactory: mockFactory,
125
185
  });
126
186
 
187
+ expect(script.context).toBe(mockContext);
127
188
  expect(script.executionId).toBe('exec_456');
128
189
  expect(script.integrationFactory).toBe(mockFactory);
129
190
  });
@@ -133,12 +194,12 @@ describe('AdminScriptBase', () => {
133
194
  it('should throw error when not implemented by subclass', async () => {
134
195
  const script = new AdminScriptBase();
135
196
 
136
- await expect(script.execute({}, {})).rejects.toThrow(
197
+ await expect(script.execute({})).rejects.toThrow(
137
198
  'AdminScriptBase.execute() must be implemented by subclass'
138
199
  );
139
200
  });
140
201
 
141
- it('should allow child classes to implement execute()', async () => {
202
+ it('should allow child classes to implement execute() with params only', async () => {
142
203
  class TestScript extends AdminScriptBase {
143
204
  static Definition = {
144
205
  name: 'test',
@@ -146,128 +207,83 @@ describe('AdminScriptBase', () => {
146
207
  description: 'test',
147
208
  };
148
209
 
149
- async execute(frigg, params) {
210
+ async execute(params) {
150
211
  return { result: 'success', params };
151
212
  }
152
213
  }
153
214
 
154
215
  const script = new TestScript();
155
- const frigg = {};
156
216
  const params = { foo: 'bar' };
157
217
 
158
- const result = await script.execute(frigg, params);
218
+ const result = await script.execute(params);
159
219
 
160
220
  expect(result.result).toBe('success');
161
221
  expect(result.params).toEqual({ foo: 'bar' });
162
222
  });
163
- });
164
-
165
- describe('Logging methods', () => {
166
- it('log() should create log entry with timestamp', () => {
167
- const script = new AdminScriptBase();
168
- const beforeTime = new Date().toISOString();
169
-
170
- const entry = script.log('info', 'Test message', { key: 'value' });
171
-
172
- const afterTime = new Date().toISOString();
173
-
174
- expect(entry.level).toBe('info');
175
- expect(entry.message).toBe('Test message');
176
- expect(entry.data).toEqual({ key: 'value' });
177
- expect(entry.timestamp).toBeDefined();
178
- expect(entry.timestamp >= beforeTime).toBe(true);
179
- expect(entry.timestamp <= afterTime).toBe(true);
180
- });
181
-
182
- it('log() should add entry to logs array', () => {
183
- const script = new AdminScriptBase();
184
-
185
- script.log('info', 'First');
186
- script.log('error', 'Second');
187
- script.log('warn', 'Third');
188
-
189
- const logs = script.getLogs();
190
-
191
- expect(logs).toHaveLength(3);
192
- expect(logs[0].message).toBe('First');
193
- expect(logs[1].message).toBe('Second');
194
- expect(logs[2].message).toBe('Third');
195
- });
196
-
197
- it('log() should default data to empty object', () => {
198
- const script = new AdminScriptBase();
199
-
200
- const entry = script.log('info', 'No data');
201
223
 
202
- expect(entry.data).toEqual({});
203
- });
204
-
205
- it('getLogs() should return logs array', () => {
206
- const script = new AdminScriptBase();
207
-
208
- script.log('info', 'Message 1');
209
- script.log('error', 'Message 2');
210
-
211
- const logs = script.getLogs();
212
-
213
- expect(logs).toHaveLength(2);
214
- expect(logs[0].level).toBe('info');
215
- expect(logs[1].level).toBe('error');
216
- });
224
+ it('should access context via this.context', async () => {
225
+ class TestScript extends AdminScriptBase {
226
+ static Definition = {
227
+ name: 'test',
228
+ version: '1.0.0',
229
+ description: 'test',
230
+ };
217
231
 
218
- it('clearLogs() should empty logs array', () => {
219
- const script = new AdminScriptBase();
232
+ async execute(params) {
233
+ this.context.log('info', 'Starting');
234
+ return { success: true };
235
+ }
236
+ }
220
237
 
221
- script.log('info', 'Message 1');
222
- script.log('info', 'Message 2');
223
- expect(script.getLogs()).toHaveLength(2);
238
+ const mockContext = { log: jest.fn() };
239
+ const script = new TestScript({ context: mockContext });
224
240
 
225
- script.clearLogs();
241
+ await script.execute({});
226
242
 
227
- expect(script.getLogs()).toHaveLength(0);
243
+ expect(mockContext.log).toHaveBeenCalledWith('info', 'Starting');
228
244
  });
229
245
  });
230
246
 
231
247
  describe('Integration with child classes', () => {
232
- it('should support full lifecycle', async () => {
248
+ it('should support full lifecycle with context injection', async () => {
233
249
  class MyScript extends AdminScriptBase {
234
250
  static Definition = {
235
251
  name: 'my-script',
236
252
  version: '1.0.0',
237
253
  description: 'My test script',
238
254
  config: {
239
- requiresIntegrationFactory: true,
255
+ requireIntegrationInstance: true,
240
256
  },
241
257
  };
242
258
 
243
- async execute(frigg, params) {
244
- this.log('info', 'Starting execution');
245
- this.log('debug', 'Processing', params);
259
+ async execute(params) {
260
+ this.context.log('info', 'Starting execution');
261
+ this.context.log('debug', 'Processing', params);
246
262
 
247
263
  if (this.integrationFactory) {
248
- this.log('info', 'Integration factory available');
264
+ this.context.log('info', 'Integration factory available');
249
265
  }
250
266
 
251
267
  return { processed: true };
252
268
  }
253
269
  }
254
270
 
271
+ const mockContext = { log: jest.fn() };
255
272
  const mockFactory = { getInstanceById: jest.fn() };
256
273
  const script = new MyScript({
274
+ context: mockContext,
257
275
  executionId: 'exec_789',
258
276
  integrationFactory: mockFactory,
259
277
  });
260
278
 
261
- const frigg = {};
262
- const result = await script.execute(frigg, { test: 'data' });
279
+ const result = await script.execute({ test: 'data' });
263
280
 
264
281
  expect(result).toEqual({ processed: true });
265
282
 
266
- const logs = script.getLogs();
267
- expect(logs).toHaveLength(3);
268
- expect(logs[0].message).toBe('Starting execution');
269
- expect(logs[1].message).toBe('Processing');
270
- expect(logs[2].message).toBe('Integration factory available');
283
+ expect(mockContext.log).toHaveBeenCalledTimes(3);
284
+ expect(mockContext.log).toHaveBeenCalledWith('info', 'Starting execution');
285
+ expect(mockContext.log).toHaveBeenCalledWith('debug', 'Processing', { test: 'data' });
286
+ expect(mockContext.log).toHaveBeenCalledWith('info', 'Integration factory available');
271
287
  });
272
288
  });
273
289
  });