apcore-js 0.4.0 → 0.6.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/dist/acl.d.ts +27 -0
- package/dist/acl.d.ts.map +1 -0
- package/dist/acl.js +175 -0
- package/dist/acl.js.map +1 -0
- package/dist/async-task.d.ts +90 -0
- package/dist/async-task.d.ts.map +1 -0
- package/dist/async-task.js +215 -0
- package/dist/async-task.js.map +1 -0
- package/dist/bindings.d.ts +12 -0
- package/dist/bindings.d.ts.map +1 -0
- package/dist/bindings.js +185 -0
- package/dist/bindings.js.map +1 -0
- package/dist/cancel.d.ts +14 -0
- package/dist/cancel.d.ts.map +1 -0
- package/dist/cancel.js +27 -0
- package/dist/cancel.js.map +1 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +23 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +50 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +87 -0
- package/dist/context.js.map +1 -0
- package/dist/decorator.d.ts +57 -0
- package/dist/decorator.d.ts.map +1 -0
- package/dist/decorator.js +74 -0
- package/dist/decorator.js.map +1 -0
- package/dist/errors.d.ts +215 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +246 -0
- package/dist/errors.js.map +1 -0
- package/dist/executor.d.ts +67 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +372 -0
- package/dist/executor.js.map +1 -0
- package/dist/extensions.d.ts +58 -0
- package/dist/extensions.d.ts.map +1 -0
- package/dist/extensions.js +220 -0
- package/dist/extensions.js.map +1 -0
- package/{src/index.ts → dist/index.d.ts} +14 -59
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/adapters.d.ts +18 -0
- package/dist/middleware/adapters.d.ts.map +1 -0
- package/dist/middleware/adapters.js +25 -0
- package/dist/middleware/adapters.js.map +1 -0
- package/dist/middleware/base.d.ts +10 -0
- package/dist/middleware/base.d.ts.map +1 -0
- package/dist/middleware/base.js +15 -0
- package/dist/middleware/base.js.map +1 -0
- package/{src/middleware/index.ts → dist/middleware/index.d.ts} +1 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +5 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/logging.d.ts +25 -0
- package/dist/middleware/logging.d.ts.map +1 -0
- package/dist/middleware/logging.js +64 -0
- package/dist/middleware/logging.js.map +1 -0
- package/dist/middleware/manager.d.ts +21 -0
- package/dist/middleware/manager.d.ts.map +1 -0
- package/dist/middleware/manager.js +77 -0
- package/dist/middleware/manager.js.map +1 -0
- package/dist/module.d.ts +31 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +12 -0
- package/dist/module.js.map +1 -0
- package/dist/observability/context-logger.d.ts +54 -0
- package/dist/observability/context-logger.d.ts.map +1 -0
- package/dist/observability/context-logger.js +151 -0
- package/dist/observability/context-logger.js.map +1 -0
- package/{src/observability/index.ts → dist/observability/index.d.ts} +1 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/observability/index.js +4 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/metrics.d.ts +30 -0
- package/dist/observability/metrics.d.ts.map +1 -0
- package/dist/observability/metrics.js +177 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/observability/tracing.d.ts +62 -0
- package/dist/observability/tracing.d.ts.map +1 -0
- package/dist/observability/tracing.js +184 -0
- package/dist/observability/tracing.js.map +1 -0
- package/dist/registry/dependencies.d.ts +6 -0
- package/dist/registry/dependencies.d.ts.map +1 -0
- package/dist/registry/dependencies.js +83 -0
- package/dist/registry/dependencies.js.map +1 -0
- package/dist/registry/entry-point.d.ts +6 -0
- package/dist/registry/entry-point.d.ts.map +1 -0
- package/dist/registry/entry-point.js +55 -0
- package/dist/registry/entry-point.js.map +1 -0
- package/{src/registry/index.ts → dist/registry/index.d.ts} +2 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +8 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/registry/metadata.d.ts +9 -0
- package/dist/registry/metadata.d.ts.map +1 -0
- package/dist/registry/metadata.js +105 -0
- package/dist/registry/metadata.js.map +1 -0
- package/dist/registry/registry.d.ts +102 -0
- package/dist/registry/registry.d.ts.map +1 -0
- package/dist/registry/registry.js +534 -0
- package/dist/registry/registry.js.map +1 -0
- package/dist/registry/scanner.d.ts +7 -0
- package/dist/registry/scanner.d.ts.map +1 -0
- package/dist/registry/scanner.js +164 -0
- package/dist/registry/scanner.js.map +1 -0
- package/dist/registry/schema-export.d.ts +9 -0
- package/dist/registry/schema-export.d.ts.map +1 -0
- package/dist/registry/schema-export.js +132 -0
- package/dist/registry/schema-export.js.map +1 -0
- package/dist/registry/types.d.ts +29 -0
- package/dist/registry/types.d.ts.map +1 -0
- package/dist/registry/types.js +5 -0
- package/dist/registry/types.js.map +1 -0
- package/dist/registry/validation.d.ts +9 -0
- package/dist/registry/validation.d.ts.map +1 -0
- package/dist/registry/validation.js +33 -0
- package/dist/registry/validation.js.map +1 -0
- package/dist/schema/annotations.d.ts +8 -0
- package/dist/schema/annotations.d.ts.map +1 -0
- package/dist/schema/annotations.js +52 -0
- package/dist/schema/annotations.js.map +1 -0
- package/dist/schema/exporter.d.ts +13 -0
- package/dist/schema/exporter.d.ts.map +1 -0
- package/dist/schema/exporter.js +71 -0
- package/dist/schema/exporter.js.map +1 -0
- package/dist/schema/index.d.ts +9 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/{src/schema/index.ts → dist/schema/index.js} +1 -7
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/loader.d.ts +30 -0
- package/dist/schema/loader.d.ts.map +1 -0
- package/dist/schema/loader.js +260 -0
- package/dist/schema/loader.js.map +1 -0
- package/dist/schema/ref-resolver.d.ts +19 -0
- package/dist/schema/ref-resolver.d.ts.map +1 -0
- package/dist/schema/ref-resolver.js +212 -0
- package/dist/schema/ref-resolver.js.map +1 -0
- package/dist/schema/strict.d.ts +7 -0
- package/dist/schema/strict.d.ts.map +1 -0
- package/dist/schema/strict.js +127 -0
- package/dist/schema/strict.js.map +1 -0
- package/dist/schema/types.d.ts +53 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/types.js +31 -0
- package/dist/schema/types.js.map +1 -0
- package/dist/schema/validator.d.ts +16 -0
- package/dist/schema/validator.d.ts.map +1 -0
- package/dist/schema/validator.js +71 -0
- package/dist/schema/validator.js.map +1 -0
- package/dist/trace-context.d.ts +35 -0
- package/dist/trace-context.d.ts.map +1 -0
- package/dist/trace-context.js +86 -0
- package/dist/trace-context.js.map +1 -0
- package/dist/utils/index.d.ts +11 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +32 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/pattern.d.ts +5 -0
- package/dist/utils/pattern.d.ts.map +1 -0
- package/dist/utils/pattern.js +31 -0
- package/dist/utils/pattern.js.map +1 -0
- package/package.json +8 -2
- package/.claude/settings.local.json +0 -12
- package/.github/workflows/ci.yml +0 -39
- package/.gitmessage +0 -60
- package/.pre-commit-config.yaml +0 -28
- package/CHANGELOG.md +0 -183
- package/CLAUDE.md +0 -68
- package/apcore-logo.svg +0 -79
- package/planning/acl-system/overview.md +0 -54
- package/planning/acl-system/plan.md +0 -92
- package/planning/acl-system/state.json +0 -76
- package/planning/acl-system/tasks/acl-core.md +0 -226
- package/planning/acl-system/tasks/acl-rule.md +0 -92
- package/planning/acl-system/tasks/conditional-rules.md +0 -259
- package/planning/acl-system/tasks/pattern-matching.md +0 -152
- package/planning/acl-system/tasks/yaml-loading.md +0 -271
- package/planning/core-executor/overview.md +0 -53
- package/planning/core-executor/plan.md +0 -88
- package/planning/core-executor/state.json +0 -76
- package/planning/core-executor/tasks/async-support.md +0 -106
- package/planning/core-executor/tasks/execution-pipeline.md +0 -113
- package/planning/core-executor/tasks/redaction.md +0 -85
- package/planning/core-executor/tasks/safety-checks.md +0 -65
- package/planning/core-executor/tasks/setup.md +0 -75
- package/planning/decorator-bindings/overview.md +0 -62
- package/planning/decorator-bindings/plan.md +0 -104
- package/planning/decorator-bindings/state.json +0 -87
- package/planning/decorator-bindings/tasks/binding-directory.md +0 -79
- package/planning/decorator-bindings/tasks/binding-loader.md +0 -148
- package/planning/decorator-bindings/tasks/explicit-schemas.md +0 -85
- package/planning/decorator-bindings/tasks/function-module.md +0 -127
- package/planning/decorator-bindings/tasks/module-factory.md +0 -89
- package/planning/decorator-bindings/tasks/schema-modes.md +0 -142
- package/planning/middleware-system/overview.md +0 -48
- package/planning/middleware-system/plan.md +0 -102
- package/planning/middleware-system/state.json +0 -65
- package/planning/middleware-system/tasks/adapters.md +0 -170
- package/planning/middleware-system/tasks/base.md +0 -115
- package/planning/middleware-system/tasks/logging-middleware.md +0 -304
- package/planning/middleware-system/tasks/manager.md +0 -313
- package/planning/observability/overview.md +0 -53
- package/planning/observability/plan.md +0 -119
- package/planning/observability/state.json +0 -98
- package/planning/observability/tasks/context-logger.md +0 -201
- package/planning/observability/tasks/exporters.md +0 -121
- package/planning/observability/tasks/metrics-collector.md +0 -162
- package/planning/observability/tasks/metrics-middleware.md +0 -141
- package/planning/observability/tasks/obs-logging-middleware.md +0 -179
- package/planning/observability/tasks/span-model.md +0 -120
- package/planning/observability/tasks/tracing-middleware.md +0 -179
- package/planning/overview.md +0 -81
- package/planning/registry-system/overview.md +0 -57
- package/planning/registry-system/plan.md +0 -114
- package/planning/registry-system/state.json +0 -109
- package/planning/registry-system/tasks/dependencies.md +0 -157
- package/planning/registry-system/tasks/entry-point.md +0 -148
- package/planning/registry-system/tasks/metadata.md +0 -198
- package/planning/registry-system/tasks/registry-core.md +0 -323
- package/planning/registry-system/tasks/scanner.md +0 -172
- package/planning/registry-system/tasks/schema-export.md +0 -261
- package/planning/registry-system/tasks/types.md +0 -124
- package/planning/registry-system/tasks/validation.md +0 -177
- package/planning/schema-system/overview.md +0 -56
- package/planning/schema-system/plan.md +0 -121
- package/planning/schema-system/state.json +0 -98
- package/planning/schema-system/tasks/exporter.md +0 -153
- package/planning/schema-system/tasks/loader.md +0 -106
- package/planning/schema-system/tasks/ref-resolver.md +0 -133
- package/planning/schema-system/tasks/strict-mode.md +0 -140
- package/planning/schema-system/tasks/typebox-generation.md +0 -133
- package/planning/schema-system/tasks/types-and-annotations.md +0 -160
- package/planning/schema-system/tasks/validator.md +0 -149
- package/src/acl.ts +0 -200
- package/src/bindings.ts +0 -207
- package/src/config.ts +0 -24
- package/src/context.ts +0 -78
- package/src/decorator.ts +0 -110
- package/src/errors.ts +0 -425
- package/src/executor.ts +0 -475
- package/src/middleware/adapters.ts +0 -54
- package/src/middleware/base.ts +0 -33
- package/src/middleware/logging.ts +0 -103
- package/src/middleware/manager.ts +0 -105
- package/src/module.ts +0 -43
- package/src/observability/context-logger.ts +0 -203
- package/src/observability/metrics.ts +0 -214
- package/src/observability/tracing.ts +0 -188
- package/src/registry/dependencies.ts +0 -99
- package/src/registry/entry-point.ts +0 -64
- package/src/registry/metadata.ts +0 -111
- package/src/registry/registry.ts +0 -360
- package/src/registry/scanner.ts +0 -168
- package/src/registry/schema-export.ts +0 -181
- package/src/registry/types.ts +0 -32
- package/src/registry/validation.ts +0 -38
- package/src/schema/annotations.ts +0 -68
- package/src/schema/exporter.ts +0 -90
- package/src/schema/loader.ts +0 -273
- package/src/schema/ref-resolver.ts +0 -244
- package/src/schema/strict.ts +0 -136
- package/src/schema/types.ts +0 -73
- package/src/schema/validator.ts +0 -82
- package/src/utils/index.ts +0 -5
- package/src/utils/pattern.ts +0 -30
- package/tests/helpers.ts +0 -30
- package/tests/integration/test-acl-safety.test.ts +0 -269
- package/tests/integration/test-binding-executor.test.ts +0 -194
- package/tests/integration/test-e2e-flow.test.ts +0 -117
- package/tests/integration/test-error-propagation.test.ts +0 -259
- package/tests/integration/test-middleware-chain.test.ts +0 -120
- package/tests/integration/test-observability-integration.test.ts +0 -438
- package/tests/observability/test-context-logger.test.ts +0 -123
- package/tests/observability/test-metrics.test.ts +0 -186
- package/tests/observability/test-tracing.test.ts +0 -131
- package/tests/registry/test-dependencies.test.ts +0 -70
- package/tests/registry/test-entry-point.test.ts +0 -133
- package/tests/registry/test-metadata.test.ts +0 -265
- package/tests/registry/test-registry.test.ts +0 -1008
- package/tests/registry/test-scanner.test.ts +0 -257
- package/tests/registry/test-schema-export.test.ts +0 -355
- package/tests/registry/test-validation.test.ts +0 -75
- package/tests/schema/test-annotations.test.ts +0 -137
- package/tests/schema/test-exporter.test.ts +0 -172
- package/tests/schema/test-loader.test.ts +0 -461
- package/tests/schema/test-ref-resolver.test.ts +0 -530
- package/tests/schema/test-strict.test.ts +0 -348
- package/tests/schema/test-validator.test.ts +0 -64
- package/tests/test-acl.test.ts +0 -423
- package/tests/test-bindings.test.ts +0 -227
- package/tests/test-config.test.ts +0 -76
- package/tests/test-context.test.ts +0 -151
- package/tests/test-decorator.test.ts +0 -173
- package/tests/test-errors.test.ts +0 -647
- package/tests/test-executor-stream.test.ts +0 -208
- package/tests/test-executor.test.ts +0 -252
- package/tests/test-logging-middleware.test.ts +0 -150
- package/tests/test-middleware-manager.test.ts +0 -185
- package/tests/test-middleware.test.ts +0 -86
- package/tests/utils/test-pattern.test.ts +0 -109
- package/tsconfig.build.json +0 -8
- package/tsconfig.json +0 -20
- package/vitest.config.ts +0 -18
|
@@ -1,1008 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { Type } from '@sinclair/typebox';
|
|
6
|
-
import { Registry } from '../../src/registry/registry.js';
|
|
7
|
-
import { FunctionModule } from '../../src/decorator.js';
|
|
8
|
-
import { InvalidInputError, ModuleNotFoundError } from '../../src/errors.js';
|
|
9
|
-
import { Config } from '../../src/config.js';
|
|
10
|
-
|
|
11
|
-
function createMod(id: string): FunctionModule {
|
|
12
|
-
return new FunctionModule({
|
|
13
|
-
execute: () => ({ ok: true }),
|
|
14
|
-
moduleId: id,
|
|
15
|
-
inputSchema: Type.Object({}),
|
|
16
|
-
outputSchema: Type.Object({ ok: Type.Boolean() }),
|
|
17
|
-
description: `Module ${id}`,
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
describe('Registry', () => {
|
|
22
|
-
it('creates empty registry', () => {
|
|
23
|
-
const registry = new Registry();
|
|
24
|
-
expect(registry.count).toBe(0);
|
|
25
|
-
expect(registry.list()).toEqual([]);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('register and get module', () => {
|
|
29
|
-
const registry = new Registry();
|
|
30
|
-
const mod = createMod('test.a');
|
|
31
|
-
registry.register('test.a', mod);
|
|
32
|
-
expect(registry.get('test.a')).toBe(mod);
|
|
33
|
-
expect(registry.has('test.a')).toBe(true);
|
|
34
|
-
expect(registry.count).toBe(1);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('get returns null for unknown module', () => {
|
|
38
|
-
const registry = new Registry();
|
|
39
|
-
expect(registry.get('unknown')).toBeNull();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('get throws for empty string', () => {
|
|
43
|
-
const registry = new Registry();
|
|
44
|
-
expect(() => registry.get('')).toThrow(ModuleNotFoundError);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('register throws for empty moduleId', () => {
|
|
48
|
-
const registry = new Registry();
|
|
49
|
-
expect(() => registry.register('', createMod('x'))).toThrow(InvalidInputError);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('register throws for duplicate moduleId', () => {
|
|
53
|
-
const registry = new Registry();
|
|
54
|
-
registry.register('test.a', createMod('test.a'));
|
|
55
|
-
expect(() => registry.register('test.a', createMod('test.a'))).toThrow(InvalidInputError);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('unregister removes module', () => {
|
|
59
|
-
const registry = new Registry();
|
|
60
|
-
registry.register('test.a', createMod('test.a'));
|
|
61
|
-
const removed = registry.unregister('test.a');
|
|
62
|
-
expect(removed).toBe(true);
|
|
63
|
-
expect(registry.has('test.a')).toBe(false);
|
|
64
|
-
expect(registry.count).toBe(0);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('unregister returns false for unknown module', () => {
|
|
68
|
-
const registry = new Registry();
|
|
69
|
-
expect(registry.unregister('nonexistent')).toBe(false);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('list returns sorted module IDs', () => {
|
|
73
|
-
const registry = new Registry();
|
|
74
|
-
registry.register('b.mod', createMod('b.mod'));
|
|
75
|
-
registry.register('a.mod', createMod('a.mod'));
|
|
76
|
-
registry.register('c.mod', createMod('c.mod'));
|
|
77
|
-
expect(registry.list()).toEqual(['a.mod', 'b.mod', 'c.mod']);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('list filters by prefix', () => {
|
|
81
|
-
const registry = new Registry();
|
|
82
|
-
registry.register('foo.a', createMod('foo.a'));
|
|
83
|
-
registry.register('foo.b', createMod('foo.b'));
|
|
84
|
-
registry.register('bar.a', createMod('bar.a'));
|
|
85
|
-
expect(registry.list({ prefix: 'foo.' })).toEqual(['foo.a', 'foo.b']);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('moduleIds returns sorted IDs', () => {
|
|
89
|
-
const registry = new Registry();
|
|
90
|
-
registry.register('z.mod', createMod('z.mod'));
|
|
91
|
-
registry.register('a.mod', createMod('a.mod'));
|
|
92
|
-
expect(registry.moduleIds).toEqual(['a.mod', 'z.mod']);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('iter returns entries', () => {
|
|
96
|
-
const registry = new Registry();
|
|
97
|
-
registry.register('test.a', createMod('test.a'));
|
|
98
|
-
const entries = [...registry.iter()];
|
|
99
|
-
expect(entries).toHaveLength(1);
|
|
100
|
-
expect(entries[0][0]).toBe('test.a');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('on register event fires', () => {
|
|
104
|
-
const registry = new Registry();
|
|
105
|
-
const events: string[] = [];
|
|
106
|
-
registry.on('register', (id) => events.push(id));
|
|
107
|
-
registry.register('test.a', createMod('test.a'));
|
|
108
|
-
expect(events).toEqual(['test.a']);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('on unregister event fires', () => {
|
|
112
|
-
const registry = new Registry();
|
|
113
|
-
const events: string[] = [];
|
|
114
|
-
registry.on('unregister', (id) => events.push(id));
|
|
115
|
-
registry.register('test.a', createMod('test.a'));
|
|
116
|
-
registry.unregister('test.a');
|
|
117
|
-
expect(events).toEqual(['test.a']);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('on throws for invalid event', () => {
|
|
121
|
-
const registry = new Registry();
|
|
122
|
-
expect(() => registry.on('invalid', () => {})).toThrow(InvalidInputError);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('getDefinition returns descriptor', () => {
|
|
126
|
-
const registry = new Registry();
|
|
127
|
-
const mod = createMod('test.a');
|
|
128
|
-
registry.register('test.a', mod);
|
|
129
|
-
const def = registry.getDefinition('test.a');
|
|
130
|
-
expect(def).not.toBeNull();
|
|
131
|
-
expect(def!.moduleId).toBe('test.a');
|
|
132
|
-
expect(def!.description).toBe('Module test.a');
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('getDefinition returns null for unknown module', () => {
|
|
136
|
-
const registry = new Registry();
|
|
137
|
-
expect(registry.getDefinition('nonexistent')).toBeNull();
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('clearCache does not throw', () => {
|
|
141
|
-
const registry = new Registry();
|
|
142
|
-
registry.clearCache();
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
/* -----------------------------------------------------------
|
|
147
|
-
* Integration tests for Registry.discover() and related APIs
|
|
148
|
-
* --------------------------------------------------------- */
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Helper: write a valid ESM module file (.js) that the scanner and
|
|
152
|
-
* entry-point resolver can dynamically import.
|
|
153
|
-
*/
|
|
154
|
-
function writeModuleFile(
|
|
155
|
-
dir: string,
|
|
156
|
-
filename: string,
|
|
157
|
-
content: string,
|
|
158
|
-
): string {
|
|
159
|
-
const filePath = join(dir, filename);
|
|
160
|
-
writeFileSync(filePath, content, 'utf-8');
|
|
161
|
-
return filePath;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
describe('Registry.discover()', () => {
|
|
165
|
-
let tempDir: string;
|
|
166
|
-
|
|
167
|
-
beforeEach(() => {
|
|
168
|
-
tempDir = mkdtempSync(join(tmpdir(), 'apcore-registry-test-'));
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
afterEach(() => {
|
|
172
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('discovers valid .js module files and registers them', async () => {
|
|
176
|
-
writeModuleFile(
|
|
177
|
-
tempDir,
|
|
178
|
-
'greeter.js',
|
|
179
|
-
`export default {
|
|
180
|
-
execute: async (inputs) => ({ greeting: 'Hello ' + inputs.name }),
|
|
181
|
-
description: 'A greeter module',
|
|
182
|
-
inputSchema: { type: 'object', properties: { name: { type: 'string' } } },
|
|
183
|
-
outputSchema: { type: 'object', properties: { greeting: { type: 'string' } } },
|
|
184
|
-
};`,
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
const registry = new Registry({ extensionsDir: tempDir });
|
|
188
|
-
const count = await registry.discover();
|
|
189
|
-
|
|
190
|
-
expect(count).toBe(1);
|
|
191
|
-
expect(registry.has('greeter')).toBe(true);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('discovers multiple modules in nested directories', async () => {
|
|
195
|
-
writeModuleFile(
|
|
196
|
-
tempDir,
|
|
197
|
-
'alpha.js',
|
|
198
|
-
`export default {
|
|
199
|
-
execute: async () => ({}),
|
|
200
|
-
description: 'Alpha module',
|
|
201
|
-
inputSchema: { type: 'object' },
|
|
202
|
-
outputSchema: { type: 'object' },
|
|
203
|
-
};`,
|
|
204
|
-
);
|
|
205
|
-
|
|
206
|
-
const subDir = join(tempDir, 'sub');
|
|
207
|
-
mkdirSync(subDir, { recursive: true });
|
|
208
|
-
writeModuleFile(
|
|
209
|
-
subDir,
|
|
210
|
-
'beta.js',
|
|
211
|
-
`export default {
|
|
212
|
-
execute: async () => ({}),
|
|
213
|
-
description: 'Beta module',
|
|
214
|
-
inputSchema: { type: 'object' },
|
|
215
|
-
outputSchema: { type: 'object' },
|
|
216
|
-
};`,
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
const registry = new Registry({ extensionsDir: tempDir });
|
|
220
|
-
const count = await registry.discover();
|
|
221
|
-
|
|
222
|
-
expect(count).toBe(2);
|
|
223
|
-
expect(registry.has('alpha')).toBe(true);
|
|
224
|
-
expect(registry.has('sub.beta')).toBe(true);
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it('calls onLoad during discover when module exports onLoad', async () => {
|
|
228
|
-
writeModuleFile(
|
|
229
|
-
tempDir,
|
|
230
|
-
'withload.js',
|
|
231
|
-
`let loaded = false;
|
|
232
|
-
export default {
|
|
233
|
-
execute: async () => ({}),
|
|
234
|
-
description: 'Module with onLoad',
|
|
235
|
-
inputSchema: { type: 'object' },
|
|
236
|
-
outputSchema: { type: 'object' },
|
|
237
|
-
onLoad() { loaded = true; },
|
|
238
|
-
isLoaded() { return loaded; },
|
|
239
|
-
};`,
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
const registry = new Registry({ extensionsDir: tempDir });
|
|
243
|
-
const count = await registry.discover();
|
|
244
|
-
|
|
245
|
-
expect(count).toBe(1);
|
|
246
|
-
expect(registry.has('withload')).toBe(true);
|
|
247
|
-
|
|
248
|
-
const mod = registry.get('withload') as Record<string, unknown>;
|
|
249
|
-
const isLoaded = (mod['isLoaded'] as () => boolean)();
|
|
250
|
-
expect(isLoaded).toBe(true);
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('skips modules that fail validation (no execute method)', async () => {
|
|
254
|
-
writeModuleFile(
|
|
255
|
-
tempDir,
|
|
256
|
-
'valid.js',
|
|
257
|
-
`export default {
|
|
258
|
-
execute: async () => ({}),
|
|
259
|
-
description: 'Valid module',
|
|
260
|
-
inputSchema: { type: 'object' },
|
|
261
|
-
outputSchema: { type: 'object' },
|
|
262
|
-
};`,
|
|
263
|
-
);
|
|
264
|
-
|
|
265
|
-
// Invalid module: no default export that passes isModuleClass
|
|
266
|
-
writeModuleFile(
|
|
267
|
-
tempDir,
|
|
268
|
-
'invalid.js',
|
|
269
|
-
`export const someData = 42;
|
|
270
|
-
export const description = 'Invalid module - no execute';`,
|
|
271
|
-
);
|
|
272
|
-
|
|
273
|
-
const registry = new Registry({ extensionsDir: tempDir });
|
|
274
|
-
const count = await registry.discover();
|
|
275
|
-
|
|
276
|
-
expect(count).toBe(1);
|
|
277
|
-
expect(registry.has('valid')).toBe(true);
|
|
278
|
-
expect(registry.has('invalid')).toBe(false);
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it('merges companion _meta.yaml metadata into discovered module', async () => {
|
|
282
|
-
writeModuleFile(
|
|
283
|
-
tempDir,
|
|
284
|
-
'tagged.js',
|
|
285
|
-
`export default {
|
|
286
|
-
execute: async () => ({}),
|
|
287
|
-
description: 'Tagged module from code',
|
|
288
|
-
inputSchema: { type: 'object' },
|
|
289
|
-
outputSchema: { type: 'object' },
|
|
290
|
-
};`,
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
writeFileSync(
|
|
294
|
-
join(tempDir, 'tagged_meta.yaml'),
|
|
295
|
-
[
|
|
296
|
-
'description: "Overridden description from YAML"',
|
|
297
|
-
'version: "2.0.0"',
|
|
298
|
-
'tags:',
|
|
299
|
-
' - yaml_tag',
|
|
300
|
-
' - production',
|
|
301
|
-
].join('\n'),
|
|
302
|
-
'utf-8',
|
|
303
|
-
);
|
|
304
|
-
|
|
305
|
-
const registry = new Registry({ extensionsDir: tempDir });
|
|
306
|
-
const count = await registry.discover();
|
|
307
|
-
|
|
308
|
-
expect(count).toBe(1);
|
|
309
|
-
expect(registry.has('tagged')).toBe(true);
|
|
310
|
-
|
|
311
|
-
const def = registry.getDefinition('tagged');
|
|
312
|
-
expect(def).not.toBeNull();
|
|
313
|
-
expect(def!.description).toBe('Overridden description from YAML');
|
|
314
|
-
expect(def!.version).toBe('2.0.0');
|
|
315
|
-
expect(def!.tags).toEqual(['yaml_tag', 'production']);
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
it('returns 0 when extensions directory contains no valid modules', async () => {
|
|
319
|
-
// Write a file that is not a module (plain text)
|
|
320
|
-
writeFileSync(join(tempDir, 'readme.txt'), 'Not a module', 'utf-8');
|
|
321
|
-
|
|
322
|
-
const registry = new Registry({ extensionsDir: tempDir });
|
|
323
|
-
const count = await registry.discover();
|
|
324
|
-
|
|
325
|
-
expect(count).toBe(0);
|
|
326
|
-
expect(registry.count).toBe(0);
|
|
327
|
-
});
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
describe('Registry.getDefinition() with discover', () => {
|
|
331
|
-
let tempDir: string;
|
|
332
|
-
|
|
333
|
-
beforeEach(() => {
|
|
334
|
-
tempDir = mkdtempSync(join(tmpdir(), 'apcore-registry-def-'));
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
afterEach(() => {
|
|
338
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
it('returns full ModuleDescriptor for a discovered module', async () => {
|
|
342
|
-
writeModuleFile(
|
|
343
|
-
tempDir,
|
|
344
|
-
'detailed.js',
|
|
345
|
-
`export default {
|
|
346
|
-
execute: async (inputs) => ({ result: inputs.x * 2 }),
|
|
347
|
-
description: 'A detailed test module',
|
|
348
|
-
version: '3.5.0',
|
|
349
|
-
tags: ['math', 'utility'],
|
|
350
|
-
inputSchema: { type: 'object', properties: { x: { type: 'number' } } },
|
|
351
|
-
outputSchema: { type: 'object', properties: { result: { type: 'number' } } },
|
|
352
|
-
};`,
|
|
353
|
-
);
|
|
354
|
-
|
|
355
|
-
const registry = new Registry({ extensionsDir: tempDir });
|
|
356
|
-
await registry.discover();
|
|
357
|
-
|
|
358
|
-
const def = registry.getDefinition('detailed');
|
|
359
|
-
expect(def).not.toBeNull();
|
|
360
|
-
expect(def!.moduleId).toBe('detailed');
|
|
361
|
-
expect(def!.description).toBe('A detailed test module');
|
|
362
|
-
expect(def!.version).toBe('3.5.0');
|
|
363
|
-
expect(def!.tags).toEqual(['math', 'utility']);
|
|
364
|
-
expect(def!.inputSchema).toEqual({
|
|
365
|
-
type: 'object',
|
|
366
|
-
properties: { x: { type: 'number' } },
|
|
367
|
-
});
|
|
368
|
-
expect(def!.outputSchema).toEqual({
|
|
369
|
-
type: 'object',
|
|
370
|
-
properties: { result: { type: 'number' } },
|
|
371
|
-
});
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
it('returns null for a module ID that was not discovered', async () => {
|
|
375
|
-
const registry = new Registry({ extensionsDir: tempDir });
|
|
376
|
-
await registry.discover();
|
|
377
|
-
|
|
378
|
-
expect(registry.getDefinition('nonexistent')).toBeNull();
|
|
379
|
-
});
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
describe('Registry.list() with tag filtering', () => {
|
|
383
|
-
it('filters modules by tags on registered plain objects', () => {
|
|
384
|
-
const registry = new Registry();
|
|
385
|
-
|
|
386
|
-
const modA = {
|
|
387
|
-
execute: async () => ({}),
|
|
388
|
-
description: 'Module A',
|
|
389
|
-
inputSchema: { type: 'object' },
|
|
390
|
-
outputSchema: { type: 'object' },
|
|
391
|
-
tags: ['web', 'api'],
|
|
392
|
-
};
|
|
393
|
-
const modB = {
|
|
394
|
-
execute: async () => ({}),
|
|
395
|
-
description: 'Module B',
|
|
396
|
-
inputSchema: { type: 'object' },
|
|
397
|
-
outputSchema: { type: 'object' },
|
|
398
|
-
tags: ['cli', 'api'],
|
|
399
|
-
};
|
|
400
|
-
const modC = {
|
|
401
|
-
execute: async () => ({}),
|
|
402
|
-
description: 'Module C',
|
|
403
|
-
inputSchema: { type: 'object' },
|
|
404
|
-
outputSchema: { type: 'object' },
|
|
405
|
-
tags: ['web'],
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
registry.register('mod.a', modA);
|
|
409
|
-
registry.register('mod.b', modB);
|
|
410
|
-
registry.register('mod.c', modC);
|
|
411
|
-
|
|
412
|
-
expect(registry.list({ tags: ['api'] })).toEqual(['mod.a', 'mod.b']);
|
|
413
|
-
expect(registry.list({ tags: ['web'] })).toEqual(['mod.a', 'mod.c']);
|
|
414
|
-
expect(registry.list({ tags: ['cli'] })).toEqual(['mod.b']);
|
|
415
|
-
expect(registry.list({ tags: ['web', 'api'] })).toEqual(['mod.a']);
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
it('returns empty array when no modules match the tag', () => {
|
|
419
|
-
const registry = new Registry();
|
|
420
|
-
|
|
421
|
-
const modA = {
|
|
422
|
-
execute: async () => ({}),
|
|
423
|
-
description: 'Module A',
|
|
424
|
-
inputSchema: { type: 'object' },
|
|
425
|
-
outputSchema: { type: 'object' },
|
|
426
|
-
tags: ['web'],
|
|
427
|
-
};
|
|
428
|
-
registry.register('mod.a', modA);
|
|
429
|
-
|
|
430
|
-
expect(registry.list({ tags: ['nonexistent'] })).toEqual([]);
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
it('combines tag and prefix filtering', () => {
|
|
434
|
-
const registry = new Registry();
|
|
435
|
-
|
|
436
|
-
const modA = {
|
|
437
|
-
execute: async () => ({}),
|
|
438
|
-
description: 'Module A',
|
|
439
|
-
inputSchema: { type: 'object' },
|
|
440
|
-
outputSchema: { type: 'object' },
|
|
441
|
-
tags: ['api'],
|
|
442
|
-
};
|
|
443
|
-
const modB = {
|
|
444
|
-
execute: async () => ({}),
|
|
445
|
-
description: 'Module B',
|
|
446
|
-
inputSchema: { type: 'object' },
|
|
447
|
-
outputSchema: { type: 'object' },
|
|
448
|
-
tags: ['api'],
|
|
449
|
-
};
|
|
450
|
-
|
|
451
|
-
registry.register('svc.alpha', modA);
|
|
452
|
-
registry.register('lib.beta', modB);
|
|
453
|
-
|
|
454
|
-
expect(registry.list({ prefix: 'svc.', tags: ['api'] })).toEqual(['svc.alpha']);
|
|
455
|
-
expect(registry.list({ prefix: 'lib.', tags: ['api'] })).toEqual(['lib.beta']);
|
|
456
|
-
expect(registry.list({ prefix: 'unknown.', tags: ['api'] })).toEqual([]);
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
let tempDir: string;
|
|
460
|
-
|
|
461
|
-
beforeEach(() => {
|
|
462
|
-
tempDir = mkdtempSync(join(tmpdir(), 'apcore-registry-tags-'));
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
afterEach(() => {
|
|
466
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
it('filters discovered modules by tags from code exports', async () => {
|
|
470
|
-
writeModuleFile(
|
|
471
|
-
tempDir,
|
|
472
|
-
'svcone.js',
|
|
473
|
-
`export default {
|
|
474
|
-
execute: async () => ({}),
|
|
475
|
-
description: 'Service one',
|
|
476
|
-
tags: ['backend', 'grpc'],
|
|
477
|
-
inputSchema: { type: 'object' },
|
|
478
|
-
outputSchema: { type: 'object' },
|
|
479
|
-
};`,
|
|
480
|
-
);
|
|
481
|
-
|
|
482
|
-
writeModuleFile(
|
|
483
|
-
tempDir,
|
|
484
|
-
'svctwo.js',
|
|
485
|
-
`export default {
|
|
486
|
-
execute: async () => ({}),
|
|
487
|
-
description: 'Service two',
|
|
488
|
-
tags: ['frontend', 'rest'],
|
|
489
|
-
inputSchema: { type: 'object' },
|
|
490
|
-
outputSchema: { type: 'object' },
|
|
491
|
-
};`,
|
|
492
|
-
);
|
|
493
|
-
|
|
494
|
-
const registry = new Registry({ extensionsDir: tempDir });
|
|
495
|
-
await registry.discover();
|
|
496
|
-
|
|
497
|
-
expect(registry.list({ tags: ['backend'] })).toEqual(['svcone']);
|
|
498
|
-
expect(registry.list({ tags: ['frontend'] })).toEqual(['svctwo']);
|
|
499
|
-
expect(registry.list({ tags: ['grpc'] })).toEqual(['svcone']);
|
|
500
|
-
expect(registry.list({ tags: ['rest'] })).toEqual(['svctwo']);
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
it('filters discovered modules by tags from companion YAML metadata', async () => {
|
|
504
|
-
writeModuleFile(
|
|
505
|
-
tempDir,
|
|
506
|
-
'yamlmod.js',
|
|
507
|
-
`export default {
|
|
508
|
-
execute: async () => ({}),
|
|
509
|
-
description: 'YAML tagged module',
|
|
510
|
-
inputSchema: { type: 'object' },
|
|
511
|
-
outputSchema: { type: 'object' },
|
|
512
|
-
};`,
|
|
513
|
-
);
|
|
514
|
-
|
|
515
|
-
writeFileSync(
|
|
516
|
-
join(tempDir, 'yamlmod_meta.yaml'),
|
|
517
|
-
['tags:', ' - infra', ' - deploy'].join('\n'),
|
|
518
|
-
'utf-8',
|
|
519
|
-
);
|
|
520
|
-
|
|
521
|
-
const registry = new Registry({ extensionsDir: tempDir });
|
|
522
|
-
await registry.discover();
|
|
523
|
-
|
|
524
|
-
expect(registry.list({ tags: ['infra'] })).toEqual(['yamlmod']);
|
|
525
|
-
expect(registry.list({ tags: ['deploy'] })).toEqual(['yamlmod']);
|
|
526
|
-
expect(registry.list({ tags: ['web'] })).toEqual([]);
|
|
527
|
-
});
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
/* -----------------------------------------------------------
|
|
531
|
-
* Constructor branch coverage
|
|
532
|
-
* --------------------------------------------------------- */
|
|
533
|
-
|
|
534
|
-
describe('Registry constructor branches', () => {
|
|
535
|
-
it('accepts extensionsDirs with string entries', () => {
|
|
536
|
-
const registry = new Registry({ extensionsDirs: ['/tmp/ext-a', '/tmp/ext-b'] });
|
|
537
|
-
expect(registry.count).toBe(0);
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
it('accepts extensionsDirs with object entries', () => {
|
|
541
|
-
const registry = new Registry({
|
|
542
|
-
extensionsDirs: [{ root: '/tmp/ext-a', namespace: 'ns' }, '/tmp/ext-b'],
|
|
543
|
-
});
|
|
544
|
-
expect(registry.count).toBe(0);
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
it('throws when both extensionsDir and extensionsDirs are provided', () => {
|
|
548
|
-
expect(
|
|
549
|
-
() => new Registry({ extensionsDir: '/tmp/ext-a', extensionsDirs: ['/tmp/ext-b'] }),
|
|
550
|
-
).toThrow(InvalidInputError);
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
it('uses extensions.root from config when no extensionsDir is provided', () => {
|
|
554
|
-
const config = new Config({ extensions: { root: '/tmp/from-config' } });
|
|
555
|
-
const registry = new Registry({ config });
|
|
556
|
-
expect(registry.count).toBe(0);
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
it('falls back to ./extensions when config has no extensions.root key', () => {
|
|
560
|
-
const config = new Config({});
|
|
561
|
-
const registry = new Registry({ config });
|
|
562
|
-
expect(registry.count).toBe(0);
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
it('falls back to ./extensions when no options are provided', () => {
|
|
566
|
-
const registry = new Registry();
|
|
567
|
-
expect(registry.count).toBe(0);
|
|
568
|
-
});
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
/* -----------------------------------------------------------
|
|
572
|
-
* register() with onLoad callback
|
|
573
|
-
* --------------------------------------------------------- */
|
|
574
|
-
|
|
575
|
-
describe('Registry register() onLoad callback', () => {
|
|
576
|
-
it('calls onLoad when module has an onLoad function', () => {
|
|
577
|
-
const registry = new Registry();
|
|
578
|
-
let loaded = false;
|
|
579
|
-
const mod = {
|
|
580
|
-
execute: async () => ({}),
|
|
581
|
-
description: 'Module with onLoad',
|
|
582
|
-
inputSchema: Type.Object({}),
|
|
583
|
-
outputSchema: Type.Object({}),
|
|
584
|
-
onLoad() {
|
|
585
|
-
loaded = true;
|
|
586
|
-
},
|
|
587
|
-
};
|
|
588
|
-
registry.register('with.load', mod);
|
|
589
|
-
expect(loaded).toBe(true);
|
|
590
|
-
expect(registry.has('with.load')).toBe(true);
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
it('re-deletes module and re-throws when onLoad throws', () => {
|
|
594
|
-
const registry = new Registry();
|
|
595
|
-
const loadError = new Error('onLoad failed');
|
|
596
|
-
const mod = {
|
|
597
|
-
execute: async () => ({}),
|
|
598
|
-
description: 'Failing onLoad module',
|
|
599
|
-
inputSchema: Type.Object({}),
|
|
600
|
-
outputSchema: Type.Object({}),
|
|
601
|
-
onLoad() {
|
|
602
|
-
throw loadError;
|
|
603
|
-
},
|
|
604
|
-
};
|
|
605
|
-
expect(() => registry.register('bad.load', mod)).toThrow(loadError);
|
|
606
|
-
expect(registry.has('bad.load')).toBe(false);
|
|
607
|
-
expect(registry.count).toBe(0);
|
|
608
|
-
});
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
/* -----------------------------------------------------------
|
|
612
|
-
* unregister() with onUnload callback
|
|
613
|
-
* --------------------------------------------------------- */
|
|
614
|
-
|
|
615
|
-
describe('Registry unregister() onUnload callback', () => {
|
|
616
|
-
it('calls onUnload when module has an onUnload function', () => {
|
|
617
|
-
const registry = new Registry();
|
|
618
|
-
let unloaded = false;
|
|
619
|
-
const mod = {
|
|
620
|
-
execute: async () => ({}),
|
|
621
|
-
description: 'Module with onUnload',
|
|
622
|
-
inputSchema: Type.Object({}),
|
|
623
|
-
outputSchema: Type.Object({}),
|
|
624
|
-
onUnload() {
|
|
625
|
-
unloaded = true;
|
|
626
|
-
},
|
|
627
|
-
};
|
|
628
|
-
registry.register('with.unload', mod);
|
|
629
|
-
const result = registry.unregister('with.unload');
|
|
630
|
-
expect(result).toBe(true);
|
|
631
|
-
expect(unloaded).toBe(true);
|
|
632
|
-
expect(registry.has('with.unload')).toBe(false);
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
it('still unregisters and warns when onUnload throws', () => {
|
|
636
|
-
const registry = new Registry();
|
|
637
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
638
|
-
const unloadError = new Error('onUnload failed');
|
|
639
|
-
const mod = {
|
|
640
|
-
execute: async () => ({}),
|
|
641
|
-
description: 'Module with failing onUnload',
|
|
642
|
-
inputSchema: Type.Object({}),
|
|
643
|
-
outputSchema: Type.Object({}),
|
|
644
|
-
onUnload() {
|
|
645
|
-
throw unloadError;
|
|
646
|
-
},
|
|
647
|
-
};
|
|
648
|
-
registry.register('bad.unload', mod);
|
|
649
|
-
const result = registry.unregister('bad.unload');
|
|
650
|
-
expect(result).toBe(true);
|
|
651
|
-
expect(registry.has('bad.unload')).toBe(false);
|
|
652
|
-
expect(warnSpy).toHaveBeenCalledWith(
|
|
653
|
-
expect.stringContaining('[apcore:registry]'),
|
|
654
|
-
unloadError,
|
|
655
|
-
);
|
|
656
|
-
warnSpy.mockRestore();
|
|
657
|
-
});
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
/* -----------------------------------------------------------
|
|
661
|
-
* _triggerEvent error handling
|
|
662
|
-
* --------------------------------------------------------- */
|
|
663
|
-
|
|
664
|
-
describe('Registry _triggerEvent error handling', () => {
|
|
665
|
-
it('warns and continues when a registered event callback throws', () => {
|
|
666
|
-
const registry = new Registry();
|
|
667
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
668
|
-
const callbackError = new Error('callback exploded');
|
|
669
|
-
|
|
670
|
-
registry.on('register', () => {
|
|
671
|
-
throw callbackError;
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
// register should complete normally despite the callback throwing
|
|
675
|
-
expect(() => registry.register('trigger.test', createMod('trigger.test'))).not.toThrow();
|
|
676
|
-
expect(registry.has('trigger.test')).toBe(true);
|
|
677
|
-
expect(warnSpy).toHaveBeenCalledWith(
|
|
678
|
-
expect.stringContaining('[apcore:registry]'),
|
|
679
|
-
callbackError,
|
|
680
|
-
);
|
|
681
|
-
warnSpy.mockRestore();
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
it('warns and continues for unregister event callbacks that throw', () => {
|
|
685
|
-
const registry = new Registry();
|
|
686
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
687
|
-
|
|
688
|
-
registry.register('trigger.unreg', createMod('trigger.unreg'));
|
|
689
|
-
|
|
690
|
-
const callbackError = new Error('unregister callback exploded');
|
|
691
|
-
registry.on('unregister', () => {
|
|
692
|
-
throw callbackError;
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
const result = registry.unregister('trigger.unreg');
|
|
696
|
-
expect(result).toBe(true);
|
|
697
|
-
expect(registry.has('trigger.unreg')).toBe(false);
|
|
698
|
-
expect(warnSpy).toHaveBeenCalledWith(
|
|
699
|
-
expect.stringContaining('[apcore:registry]'),
|
|
700
|
-
callbackError,
|
|
701
|
-
);
|
|
702
|
-
warnSpy.mockRestore();
|
|
703
|
-
});
|
|
704
|
-
});
|
|
705
|
-
|
|
706
|
-
/* -----------------------------------------------------------
|
|
707
|
-
* list() with metaTags from _moduleMeta
|
|
708
|
-
* --------------------------------------------------------- */
|
|
709
|
-
|
|
710
|
-
/* -----------------------------------------------------------
|
|
711
|
-
* register() invalid pattern (MODULE_ID_PATTERN check)
|
|
712
|
-
* --------------------------------------------------------- */
|
|
713
|
-
|
|
714
|
-
describe('Registry register() invalid module ID pattern', () => {
|
|
715
|
-
it('throws InvalidInputError when moduleId contains a hyphen', () => {
|
|
716
|
-
const registry = new Registry();
|
|
717
|
-
expect(() => registry.register('bad-id', createMod('test.a'))).toThrow(InvalidInputError);
|
|
718
|
-
});
|
|
719
|
-
|
|
720
|
-
it('throws InvalidInputError when moduleId starts with a digit', () => {
|
|
721
|
-
const registry = new Registry();
|
|
722
|
-
expect(() => registry.register('1invalid', createMod('test.a'))).toThrow(InvalidInputError);
|
|
723
|
-
});
|
|
724
|
-
|
|
725
|
-
it('throws InvalidInputError when moduleId contains uppercase letters', () => {
|
|
726
|
-
const registry = new Registry();
|
|
727
|
-
expect(() => registry.register('Bad.Id', createMod('test.a'))).toThrow(InvalidInputError);
|
|
728
|
-
});
|
|
729
|
-
});
|
|
730
|
-
|
|
731
|
-
/* -----------------------------------------------------------
|
|
732
|
-
* discover() with config: _scanRoots uses config values
|
|
733
|
-
* --------------------------------------------------------- */
|
|
734
|
-
|
|
735
|
-
describe('Registry discover() with Config', () => {
|
|
736
|
-
let tempDir: string;
|
|
737
|
-
|
|
738
|
-
beforeEach(() => {
|
|
739
|
-
tempDir = mkdtempSync(join(tmpdir(), 'apcore-registry-config-'));
|
|
740
|
-
});
|
|
741
|
-
|
|
742
|
-
afterEach(() => {
|
|
743
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
744
|
-
});
|
|
745
|
-
|
|
746
|
-
it('uses extensions.root from config and reads max_depth and follow_symlinks during discover()', async () => {
|
|
747
|
-
writeFileSync(
|
|
748
|
-
join(tempDir, 'cfgmod.js'),
|
|
749
|
-
`export default {
|
|
750
|
-
execute: async () => ({}),
|
|
751
|
-
description: 'Config-driven module',
|
|
752
|
-
inputSchema: { type: 'object' },
|
|
753
|
-
outputSchema: { type: 'object' },
|
|
754
|
-
};`,
|
|
755
|
-
'utf-8',
|
|
756
|
-
);
|
|
757
|
-
|
|
758
|
-
const config = new Config({
|
|
759
|
-
extensions: { root: tempDir, max_depth: 3, follow_symlinks: false },
|
|
760
|
-
});
|
|
761
|
-
const registry = new Registry({ config });
|
|
762
|
-
const count = await registry.discover();
|
|
763
|
-
|
|
764
|
-
expect(count).toBe(1);
|
|
765
|
-
expect(registry.has('cfgmod')).toBe(true);
|
|
766
|
-
});
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
/* -----------------------------------------------------------
|
|
770
|
-
* discover() onLoad failure in _registerInOrder
|
|
771
|
-
* --------------------------------------------------------- */
|
|
772
|
-
|
|
773
|
-
describe('Registry discover() onLoad failure during _registerInOrder', () => {
|
|
774
|
-
let tempDir: string;
|
|
775
|
-
|
|
776
|
-
beforeEach(() => {
|
|
777
|
-
tempDir = mkdtempSync(join(tmpdir(), 'apcore-registry-onloadfail-'));
|
|
778
|
-
});
|
|
779
|
-
|
|
780
|
-
afterEach(() => {
|
|
781
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
782
|
-
});
|
|
783
|
-
|
|
784
|
-
it('skips module and warns when onLoad throws during discover()', async () => {
|
|
785
|
-
writeFileSync(
|
|
786
|
-
join(tempDir, 'failload.js'),
|
|
787
|
-
`export default {
|
|
788
|
-
execute: async () => ({}),
|
|
789
|
-
description: 'Module with failing onLoad',
|
|
790
|
-
inputSchema: { type: 'object' },
|
|
791
|
-
outputSchema: { type: 'object' },
|
|
792
|
-
onLoad() { throw new Error('onLoad exploded'); },
|
|
793
|
-
};`,
|
|
794
|
-
'utf-8',
|
|
795
|
-
);
|
|
796
|
-
|
|
797
|
-
writeFileSync(
|
|
798
|
-
join(tempDir, 'goodmod.js'),
|
|
799
|
-
`export default {
|
|
800
|
-
execute: async () => ({}),
|
|
801
|
-
description: 'Good module',
|
|
802
|
-
inputSchema: { type: 'object' },
|
|
803
|
-
outputSchema: { type: 'object' },
|
|
804
|
-
};`,
|
|
805
|
-
'utf-8',
|
|
806
|
-
);
|
|
807
|
-
|
|
808
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
809
|
-
const registry = new Registry({ extensionsDir: tempDir });
|
|
810
|
-
const count = await registry.discover();
|
|
811
|
-
|
|
812
|
-
// Only the good module should be registered; failload is skipped
|
|
813
|
-
expect(count).toBe(1);
|
|
814
|
-
expect(registry.has('goodmod')).toBe(true);
|
|
815
|
-
expect(registry.has('failload')).toBe(false);
|
|
816
|
-
expect(warnSpy).toHaveBeenCalledWith(
|
|
817
|
-
expect.stringContaining('[apcore:registry]'),
|
|
818
|
-
expect.any(Error),
|
|
819
|
-
);
|
|
820
|
-
warnSpy.mockRestore();
|
|
821
|
-
});
|
|
822
|
-
});
|
|
823
|
-
|
|
824
|
-
/* -----------------------------------------------------------
|
|
825
|
-
* discover() with extensionsDirs (multi-root / scanMultiRoot path)
|
|
826
|
-
* --------------------------------------------------------- */
|
|
827
|
-
|
|
828
|
-
describe('Registry discover() with extensionsDirs (multi-root)', () => {
|
|
829
|
-
let tempDirA: string;
|
|
830
|
-
let tempDirB: string;
|
|
831
|
-
|
|
832
|
-
beforeEach(() => {
|
|
833
|
-
tempDirA = mkdtempSync(join(tmpdir(), 'apcore-registry-multiroot-a-'));
|
|
834
|
-
tempDirB = mkdtempSync(join(tmpdir(), 'apcore-registry-multiroot-b-'));
|
|
835
|
-
});
|
|
836
|
-
|
|
837
|
-
afterEach(() => {
|
|
838
|
-
rmSync(tempDirA, { recursive: true, force: true });
|
|
839
|
-
rmSync(tempDirB, { recursive: true, force: true });
|
|
840
|
-
});
|
|
841
|
-
|
|
842
|
-
it('discovers modules across multiple extension directories', async () => {
|
|
843
|
-
writeFileSync(
|
|
844
|
-
join(tempDirA, 'alpha.js'),
|
|
845
|
-
`export default {
|
|
846
|
-
execute: async () => ({}),
|
|
847
|
-
description: 'Alpha module',
|
|
848
|
-
inputSchema: { type: 'object' },
|
|
849
|
-
outputSchema: { type: 'object' },
|
|
850
|
-
};`,
|
|
851
|
-
'utf-8',
|
|
852
|
-
);
|
|
853
|
-
|
|
854
|
-
writeFileSync(
|
|
855
|
-
join(tempDirB, 'beta.js'),
|
|
856
|
-
`export default {
|
|
857
|
-
execute: async () => ({}),
|
|
858
|
-
description: 'Beta module',
|
|
859
|
-
inputSchema: { type: 'object' },
|
|
860
|
-
outputSchema: { type: 'object' },
|
|
861
|
-
};`,
|
|
862
|
-
'utf-8',
|
|
863
|
-
);
|
|
864
|
-
|
|
865
|
-
// When using extensionsDirs with multiple roots, scanMultiRoot prefixes
|
|
866
|
-
// each module ID with the namespace (basename of the root dir by default).
|
|
867
|
-
const registry = new Registry({ extensionsDirs: [tempDirA, tempDirB] });
|
|
868
|
-
const count = await registry.discover();
|
|
869
|
-
|
|
870
|
-
expect(count).toBe(2);
|
|
871
|
-
expect(registry.count).toBe(2);
|
|
872
|
-
});
|
|
873
|
-
|
|
874
|
-
it('discovers modules from an extensionsDirs object entry with namespace', async () => {
|
|
875
|
-
writeFileSync(
|
|
876
|
-
join(tempDirA, 'nsmod.js'),
|
|
877
|
-
`export default {
|
|
878
|
-
execute: async () => ({}),
|
|
879
|
-
description: 'Namespaced module',
|
|
880
|
-
inputSchema: { type: 'object' },
|
|
881
|
-
outputSchema: { type: 'object' },
|
|
882
|
-
};`,
|
|
883
|
-
'utf-8',
|
|
884
|
-
);
|
|
885
|
-
|
|
886
|
-
const registry = new Registry({
|
|
887
|
-
extensionsDirs: [{ root: tempDirA, namespace: 'myns' }],
|
|
888
|
-
});
|
|
889
|
-
const count = await registry.discover();
|
|
890
|
-
|
|
891
|
-
expect(count).toBe(1);
|
|
892
|
-
});
|
|
893
|
-
});
|
|
894
|
-
|
|
895
|
-
/* -----------------------------------------------------------
|
|
896
|
-
* discover() with idMapPath (_applyIdMapOverrides path)
|
|
897
|
-
* --------------------------------------------------------- */
|
|
898
|
-
|
|
899
|
-
describe('Registry discover() with idMapPath', () => {
|
|
900
|
-
let tempDir: string;
|
|
901
|
-
|
|
902
|
-
beforeEach(() => {
|
|
903
|
-
tempDir = mkdtempSync(join(tmpdir(), 'apcore-registry-idmap-'));
|
|
904
|
-
});
|
|
905
|
-
|
|
906
|
-
afterEach(() => {
|
|
907
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
908
|
-
});
|
|
909
|
-
|
|
910
|
-
it('applies ID map overrides to discovered modules', async () => {
|
|
911
|
-
writeFileSync(
|
|
912
|
-
join(tempDir, 'mymod.js'),
|
|
913
|
-
`export default {
|
|
914
|
-
execute: async () => ({}),
|
|
915
|
-
description: 'ID map overridden module',
|
|
916
|
-
inputSchema: { type: 'object' },
|
|
917
|
-
outputSchema: { type: 'object' },
|
|
918
|
-
};`,
|
|
919
|
-
'utf-8',
|
|
920
|
-
);
|
|
921
|
-
|
|
922
|
-
const idMapPath = join(tempDir, 'idmap.yaml');
|
|
923
|
-
writeFileSync(
|
|
924
|
-
idMapPath,
|
|
925
|
-
[
|
|
926
|
-
'mappings:',
|
|
927
|
-
' - file: mymod.js',
|
|
928
|
-
' id: custom.mapped.id',
|
|
929
|
-
].join('\n'),
|
|
930
|
-
'utf-8',
|
|
931
|
-
);
|
|
932
|
-
|
|
933
|
-
const registry = new Registry({ extensionsDir: tempDir, idMapPath });
|
|
934
|
-
const count = await registry.discover();
|
|
935
|
-
|
|
936
|
-
expect(count).toBe(1);
|
|
937
|
-
expect(registry.has('custom.mapped.id')).toBe(true);
|
|
938
|
-
expect(registry.has('mymod')).toBe(false);
|
|
939
|
-
});
|
|
940
|
-
|
|
941
|
-
it('discovers normally when ID map has no matching entry for a file', async () => {
|
|
942
|
-
writeFileSync(
|
|
943
|
-
join(tempDir, 'unmapped.js'),
|
|
944
|
-
`export default {
|
|
945
|
-
execute: async () => ({}),
|
|
946
|
-
description: 'Unmapped module',
|
|
947
|
-
inputSchema: { type: 'object' },
|
|
948
|
-
outputSchema: { type: 'object' },
|
|
949
|
-
};`,
|
|
950
|
-
'utf-8',
|
|
951
|
-
);
|
|
952
|
-
|
|
953
|
-
const idMapPath = join(tempDir, 'idmap.yaml');
|
|
954
|
-
writeFileSync(
|
|
955
|
-
idMapPath,
|
|
956
|
-
['mappings:', ' - file: other.js', ' id: other.id'].join('\n'),
|
|
957
|
-
'utf-8',
|
|
958
|
-
);
|
|
959
|
-
|
|
960
|
-
const registry = new Registry({ extensionsDir: tempDir, idMapPath });
|
|
961
|
-
const count = await registry.discover();
|
|
962
|
-
|
|
963
|
-
expect(count).toBe(1);
|
|
964
|
-
expect(registry.has('unmapped')).toBe(true);
|
|
965
|
-
});
|
|
966
|
-
});
|
|
967
|
-
|
|
968
|
-
/* -----------------------------------------------------------
|
|
969
|
-
* list() metaTags from companion metadata
|
|
970
|
-
* --------------------------------------------------------- */
|
|
971
|
-
|
|
972
|
-
describe('Registry list() metaTags from companion metadata', () => {
|
|
973
|
-
let tempDir: string;
|
|
974
|
-
|
|
975
|
-
beforeEach(() => {
|
|
976
|
-
tempDir = mkdtempSync(join(tmpdir(), 'apcore-registry-metatags-'));
|
|
977
|
-
});
|
|
978
|
-
|
|
979
|
-
afterEach(() => {
|
|
980
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
981
|
-
});
|
|
982
|
-
|
|
983
|
-
it('filters using tags stored in _moduleMeta when module object has no tags', async () => {
|
|
984
|
-
writeFileSync(
|
|
985
|
-
join(tempDir, 'notagmod.js'),
|
|
986
|
-
`export default {
|
|
987
|
-
execute: async () => ({}),
|
|
988
|
-
description: 'No tags on module object',
|
|
989
|
-
inputSchema: { type: 'object' },
|
|
990
|
-
outputSchema: { type: 'object' },
|
|
991
|
-
};`,
|
|
992
|
-
'utf-8',
|
|
993
|
-
);
|
|
994
|
-
|
|
995
|
-
writeFileSync(
|
|
996
|
-
join(tempDir, 'notagmod_meta.yaml'),
|
|
997
|
-
['tags:', ' - alpha', ' - beta'].join('\n'),
|
|
998
|
-
'utf-8',
|
|
999
|
-
);
|
|
1000
|
-
|
|
1001
|
-
const registry = new Registry({ extensionsDir: tempDir });
|
|
1002
|
-
await registry.discover();
|
|
1003
|
-
|
|
1004
|
-
expect(registry.list({ tags: ['alpha'] })).toEqual(['notagmod']);
|
|
1005
|
-
expect(registry.list({ tags: ['beta'] })).toEqual(['notagmod']);
|
|
1006
|
-
expect(registry.list({ tags: ['gamma'] })).toEqual([]);
|
|
1007
|
-
});
|
|
1008
|
-
});
|