@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.
- package/PR_517_REVIEW_TRACKER.md +56 -0
- package/index.js +12 -3
- package/package.json +6 -9
- package/src/application/__tests__/admin-frigg-commands.test.js +19 -19
- package/src/application/__tests__/admin-script-base.test.js +100 -84
- package/src/application/__tests__/script-runner.test.js +146 -16
- package/src/application/admin-frigg-commands.js +20 -32
- package/src/application/admin-script-base.js +20 -99
- package/src/application/script-runner.js +131 -135
- package/src/application/use-cases/__tests__/delete-schedule-use-case.test.js +168 -0
- package/src/application/use-cases/__tests__/get-effective-schedule-use-case.test.js +114 -0
- package/src/application/use-cases/__tests__/upsert-schedule-use-case.test.js +201 -0
- package/src/application/use-cases/delete-schedule-use-case.js +108 -0
- package/src/application/use-cases/get-effective-schedule-use-case.js +78 -0
- package/src/application/use-cases/index.js +18 -0
- package/src/application/use-cases/upsert-schedule-use-case.js +127 -0
- package/src/builtins/__tests__/integration-health-check.test.js +67 -60
- package/src/builtins/__tests__/oauth-token-refresh.test.js +45 -37
- package/src/builtins/integration-health-check.js +23 -24
- package/src/builtins/oauth-token-refresh.js +19 -20
- package/src/infrastructure/__tests__/admin-auth-middleware.test.js +32 -95
- package/src/infrastructure/__tests__/admin-script-router.test.js +46 -47
- package/src/infrastructure/admin-auth-middleware.js +5 -43
- package/src/infrastructure/admin-script-router.js +38 -32
- package/src/infrastructure/script-executor-handler.js +2 -2
- package/src/application/__tests__/dry-run-http-interceptor.test.js +0 -313
- package/src/application/__tests__/dry-run-repository-wrapper.test.js +0 -257
- package/src/application/__tests__/schedule-management-use-case.test.js +0 -276
- package/src/application/dry-run-http-interceptor.js +0 -296
- package/src/application/dry-run-repository-wrapper.js +0 -261
- 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 {
|
|
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 {
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
18
|
-
"@friggframework/prettier-config": "2.0.0--canary.
|
|
19
|
-
"@friggframework/test": "2.0.0--canary.
|
|
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": "
|
|
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/
|
|
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
|
|
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
|
-
|
|
50
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
161
|
+
it('creates adminProcessRepository on first access', () => {
|
|
162
162
|
const commands = new AdminFriggCommands();
|
|
163
|
-
const {
|
|
163
|
+
const { createAdminProcessRepository } = require('@friggframework/core/admin-scripts/repositories/admin-process-repository-factory');
|
|
164
164
|
|
|
165
|
-
expect(
|
|
165
|
+
expect(createAdminProcessRepository).not.toHaveBeenCalled();
|
|
166
166
|
|
|
167
|
-
const repo = commands.
|
|
167
|
+
const repo = commands.adminProcessRepository;
|
|
168
168
|
|
|
169
|
-
expect(
|
|
170
|
-
expect(repo).toBe(
|
|
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.
|
|
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.
|
|
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(
|
|
562
|
-
const callArgs =
|
|
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(
|
|
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.
|
|
582
|
-
|
|
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
|
-
|
|
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
|
|
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({}
|
|
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(
|
|
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(
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
219
|
-
|
|
232
|
+
async execute(params) {
|
|
233
|
+
this.context.log('info', 'Starting');
|
|
234
|
+
return { success: true };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
220
237
|
|
|
221
|
-
|
|
222
|
-
script
|
|
223
|
-
expect(script.getLogs()).toHaveLength(2);
|
|
238
|
+
const mockContext = { log: jest.fn() };
|
|
239
|
+
const script = new TestScript({ context: mockContext });
|
|
224
240
|
|
|
225
|
-
script.
|
|
241
|
+
await script.execute({});
|
|
226
242
|
|
|
227
|
-
expect(
|
|
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
|
-
|
|
255
|
+
requireIntegrationInstance: true,
|
|
240
256
|
},
|
|
241
257
|
};
|
|
242
258
|
|
|
243
|
-
async execute(
|
|
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
|
|
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
|
-
|
|
267
|
-
expect(
|
|
268
|
-
expect(
|
|
269
|
-
expect(
|
|
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
|
});
|