apcore-js 0.5.0 → 0.7.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/README.md +1 -1
- 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/approval.d.ts +85 -0
- package/dist/approval.d.ts.map +1 -0
- package/dist/approval.js +73 -0
- package/dist/approval.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 +204 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +364 -0
- package/dist/errors.js.map +1 -0
- package/dist/executor.d.ts +82 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +489 -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 +239 -0
- package/dist/extensions.js.map +1 -0
- package/{src/index.ts → dist/index.d.ts} +6 -63
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -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} +1 -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 +24 -3
- 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 -214
- 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/async-task.ts +0 -267
- package/src/bindings.ts +0 -207
- package/src/cancel.ts +0 -32
- package/src/config.ts +0 -24
- package/src/context.ts +0 -160
- package/src/decorator.ts +0 -110
- package/src/errors.ts +0 -429
- package/src/executor.ts +0 -493
- package/src/extensions.ts +0 -265
- 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 -252
- 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 -580
- 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/trace-context.ts +0 -102
- package/src/utils/index.ts +0 -5
- package/src/utils/pattern.ts +0 -30
- package/tests/async-task.test.ts +0 -335
- 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 -303
- 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 -1397
- 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-cancel.test.ts +0 -71
- package/tests/test-config.test.ts +0 -76
- package/tests/test-context.test.ts +0 -266
- 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-extensions.test.ts +0 -310
- 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/test-trace-context.test.ts +0 -251
- 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,1397 +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
|
-
});
|
|
1009
|
-
|
|
1010
|
-
/* -----------------------------------------------------------
|
|
1011
|
-
* describe() tests
|
|
1012
|
-
* --------------------------------------------------------- */
|
|
1013
|
-
|
|
1014
|
-
describe('Registry.describe()', () => {
|
|
1015
|
-
it('calls custom describe() method when module has one', () => {
|
|
1016
|
-
const registry = new Registry();
|
|
1017
|
-
const mod = {
|
|
1018
|
-
execute: async () => ({}),
|
|
1019
|
-
description: 'Module with custom describe',
|
|
1020
|
-
inputSchema: { type: 'object' },
|
|
1021
|
-
outputSchema: { type: 'object' },
|
|
1022
|
-
describe() {
|
|
1023
|
-
return 'Custom description from the module itself.';
|
|
1024
|
-
},
|
|
1025
|
-
};
|
|
1026
|
-
registry.register('test.custom', mod);
|
|
1027
|
-
expect(registry.describe('test.custom')).toBe('Custom description from the module itself.');
|
|
1028
|
-
});
|
|
1029
|
-
|
|
1030
|
-
it('auto-generates markdown when no custom describe method', () => {
|
|
1031
|
-
const registry = new Registry();
|
|
1032
|
-
const mod = {
|
|
1033
|
-
execute: async () => ({}),
|
|
1034
|
-
description: 'A valid test module',
|
|
1035
|
-
inputSchema: {
|
|
1036
|
-
type: 'object',
|
|
1037
|
-
properties: { value: { type: 'string', description: 'Input value' } },
|
|
1038
|
-
required: ['value'],
|
|
1039
|
-
},
|
|
1040
|
-
outputSchema: { type: 'object' },
|
|
1041
|
-
tags: ['test', 'sample'],
|
|
1042
|
-
};
|
|
1043
|
-
registry.register('test.auto', mod);
|
|
1044
|
-
const result = registry.describe('test.auto');
|
|
1045
|
-
expect(result).toContain('# test.auto');
|
|
1046
|
-
expect(result).toContain('A valid test module');
|
|
1047
|
-
expect(result).toContain('**Tags:** test, sample');
|
|
1048
|
-
expect(result).toContain('**Parameters:**');
|
|
1049
|
-
expect(result).toContain('`value`');
|
|
1050
|
-
expect(result).toContain('(required)');
|
|
1051
|
-
});
|
|
1052
|
-
|
|
1053
|
-
it('includes documentation section when available', () => {
|
|
1054
|
-
const registry = new Registry();
|
|
1055
|
-
const mod = {
|
|
1056
|
-
execute: async () => ({}),
|
|
1057
|
-
description: 'A documented module',
|
|
1058
|
-
documentation: 'This module does interesting things.',
|
|
1059
|
-
inputSchema: { type: 'object' },
|
|
1060
|
-
outputSchema: { type: 'object' },
|
|
1061
|
-
};
|
|
1062
|
-
registry.register('test.documented', mod);
|
|
1063
|
-
const result = registry.describe('test.documented');
|
|
1064
|
-
expect(result).toContain('**Documentation:**');
|
|
1065
|
-
expect(result).toContain('This module does interesting things.');
|
|
1066
|
-
});
|
|
1067
|
-
|
|
1068
|
-
it('throws ModuleNotFoundError for unregistered module', () => {
|
|
1069
|
-
const registry = new Registry();
|
|
1070
|
-
expect(() => registry.describe('nonexistent.module')).toThrow(ModuleNotFoundError);
|
|
1071
|
-
});
|
|
1072
|
-
});
|
|
1073
|
-
|
|
1074
|
-
/* -----------------------------------------------------------
|
|
1075
|
-
* Hot Reload (watch/unwatch)
|
|
1076
|
-
* --------------------------------------------------------- */
|
|
1077
|
-
|
|
1078
|
-
describe('Registry hot reload (watch/unwatch)', () => {
|
|
1079
|
-
let tempDir: string;
|
|
1080
|
-
|
|
1081
|
-
beforeEach(() => {
|
|
1082
|
-
tempDir = mkdtempSync(join(tmpdir(), 'apcore-registry-hotreload-'));
|
|
1083
|
-
});
|
|
1084
|
-
|
|
1085
|
-
afterEach(() => {
|
|
1086
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
1087
|
-
});
|
|
1088
|
-
|
|
1089
|
-
it('watch() does not throw when called with a valid directory', () => {
|
|
1090
|
-
const registry = new Registry({ extensionsDir: tempDir });
|
|
1091
|
-
expect(() => registry.watch()).not.toThrow();
|
|
1092
|
-
registry.unwatch();
|
|
1093
|
-
});
|
|
1094
|
-
|
|
1095
|
-
it('unwatch() is safe to call when not watching', () => {
|
|
1096
|
-
const registry = new Registry();
|
|
1097
|
-
expect(() => registry.unwatch()).not.toThrow();
|
|
1098
|
-
// Call again to verify idempotent
|
|
1099
|
-
expect(() => registry.unwatch()).not.toThrow();
|
|
1100
|
-
});
|
|
1101
|
-
|
|
1102
|
-
it('watch() is idempotent (calling twice does not throw)', () => {
|
|
1103
|
-
const registry = new Registry({ extensionsDir: tempDir });
|
|
1104
|
-
registry.watch();
|
|
1105
|
-
expect(() => registry.watch()).not.toThrow();
|
|
1106
|
-
registry.unwatch();
|
|
1107
|
-
});
|
|
1108
|
-
|
|
1109
|
-
it('_pathToModuleId maps a file path to a module ID correctly', () => {
|
|
1110
|
-
const registry = new Registry();
|
|
1111
|
-
const mod = {
|
|
1112
|
-
execute: async () => ({}),
|
|
1113
|
-
description: 'Test module',
|
|
1114
|
-
inputSchema: { type: 'object' },
|
|
1115
|
-
outputSchema: { type: 'object' },
|
|
1116
|
-
};
|
|
1117
|
-
registry.register('my_module', mod);
|
|
1118
|
-
const result = (registry as any)._pathToModuleId('/some/path/my_module.ts');
|
|
1119
|
-
expect(result).toBe('my_module');
|
|
1120
|
-
});
|
|
1121
|
-
|
|
1122
|
-
it('_pathToModuleId maps a namespaced module correctly', () => {
|
|
1123
|
-
const registry = new Registry();
|
|
1124
|
-
const mod = {
|
|
1125
|
-
execute: async () => ({}),
|
|
1126
|
-
description: 'Namespaced module',
|
|
1127
|
-
inputSchema: { type: 'object' },
|
|
1128
|
-
outputSchema: { type: 'object' },
|
|
1129
|
-
};
|
|
1130
|
-
registry.register('ns.my_module', mod);
|
|
1131
|
-
const result = (registry as any)._pathToModuleId('/some/path/my_module.js');
|
|
1132
|
-
expect(result).toBe('ns.my_module');
|
|
1133
|
-
});
|
|
1134
|
-
|
|
1135
|
-
it('_pathToModuleId returns null for an unknown file', () => {
|
|
1136
|
-
const registry = new Registry();
|
|
1137
|
-
const mod = {
|
|
1138
|
-
execute: async () => ({}),
|
|
1139
|
-
description: 'Test module',
|
|
1140
|
-
inputSchema: { type: 'object' },
|
|
1141
|
-
outputSchema: { type: 'object' },
|
|
1142
|
-
};
|
|
1143
|
-
registry.register('my_module', mod);
|
|
1144
|
-
const result = (registry as any)._pathToModuleId('/some/path/unknown_file.ts');
|
|
1145
|
-
expect(result).toBeNull();
|
|
1146
|
-
});
|
|
1147
|
-
|
|
1148
|
-
it('_handleFileDeletion unregisters a known module', () => {
|
|
1149
|
-
const registry = new Registry();
|
|
1150
|
-
let unloaded = false;
|
|
1151
|
-
const mod = {
|
|
1152
|
-
execute: async () => ({}),
|
|
1153
|
-
description: 'Deletable module',
|
|
1154
|
-
inputSchema: { type: 'object' },
|
|
1155
|
-
outputSchema: { type: 'object' },
|
|
1156
|
-
onUnload() { unloaded = true; },
|
|
1157
|
-
};
|
|
1158
|
-
registry.register('deletable', mod);
|
|
1159
|
-
expect(registry.has('deletable')).toBe(true);
|
|
1160
|
-
|
|
1161
|
-
(registry as any)._handleFileDeletion('/extensions/deletable.ts');
|
|
1162
|
-
|
|
1163
|
-
expect(registry.has('deletable')).toBe(false);
|
|
1164
|
-
expect(unloaded).toBe(true);
|
|
1165
|
-
});
|
|
1166
|
-
|
|
1167
|
-
it('_handleFileDeletion does nothing for an unknown file', () => {
|
|
1168
|
-
const registry = new Registry();
|
|
1169
|
-
const mod = {
|
|
1170
|
-
execute: async () => ({}),
|
|
1171
|
-
description: 'Existing module',
|
|
1172
|
-
inputSchema: { type: 'object' },
|
|
1173
|
-
outputSchema: { type: 'object' },
|
|
1174
|
-
};
|
|
1175
|
-
registry.register('existing', mod);
|
|
1176
|
-
// Should not throw, should not affect existing modules
|
|
1177
|
-
(registry as any)._handleFileDeletion('/some/path/unknown.ts');
|
|
1178
|
-
expect(registry.has('existing')).toBe(true);
|
|
1179
|
-
});
|
|
1180
|
-
});
|
|
1181
|
-
|
|
1182
|
-
/* -----------------------------------------------------------
|
|
1183
|
-
* Custom Discoverer
|
|
1184
|
-
* --------------------------------------------------------- */
|
|
1185
|
-
|
|
1186
|
-
describe('Registry custom discoverer', () => {
|
|
1187
|
-
it('uses custom discoverer when set', async () => {
|
|
1188
|
-
const modA = {
|
|
1189
|
-
execute: async () => ({}),
|
|
1190
|
-
description: 'Custom module A',
|
|
1191
|
-
inputSchema: { type: 'object' },
|
|
1192
|
-
outputSchema: { type: 'object' },
|
|
1193
|
-
};
|
|
1194
|
-
const modB = {
|
|
1195
|
-
execute: async () => ({}),
|
|
1196
|
-
description: 'Custom module B',
|
|
1197
|
-
inputSchema: { type: 'object' },
|
|
1198
|
-
outputSchema: { type: 'object' },
|
|
1199
|
-
};
|
|
1200
|
-
|
|
1201
|
-
let calledWithRoots: string[] | null = null;
|
|
1202
|
-
const discoverer = {
|
|
1203
|
-
discover(roots: string[]) {
|
|
1204
|
-
calledWithRoots = roots;
|
|
1205
|
-
return [
|
|
1206
|
-
{ moduleId: 'custom.a', module: modA },
|
|
1207
|
-
{ moduleId: 'custom.b', module: modB },
|
|
1208
|
-
];
|
|
1209
|
-
},
|
|
1210
|
-
};
|
|
1211
|
-
|
|
1212
|
-
const registry = new Registry();
|
|
1213
|
-
registry.setDiscoverer(discoverer);
|
|
1214
|
-
const count = await registry.discover();
|
|
1215
|
-
|
|
1216
|
-
expect(count).toBe(2);
|
|
1217
|
-
expect(registry.has('custom.a')).toBe(true);
|
|
1218
|
-
expect(registry.has('custom.b')).toBe(true);
|
|
1219
|
-
expect(registry.get('custom.a')).toBe(modA);
|
|
1220
|
-
expect(registry.get('custom.b')).toBe(modB);
|
|
1221
|
-
expect(calledWithRoots).toEqual(['./extensions']);
|
|
1222
|
-
});
|
|
1223
|
-
|
|
1224
|
-
it('uses default discoverer when none set', async () => {
|
|
1225
|
-
const tempDir = mkdtempSync(join(tmpdir(), 'apcore-registry-defdisc-'));
|
|
1226
|
-
try {
|
|
1227
|
-
writeModuleFile(
|
|
1228
|
-
tempDir,
|
|
1229
|
-
'default_mod.js',
|
|
1230
|
-
`export default {
|
|
1231
|
-
execute: async () => ({}),
|
|
1232
|
-
description: 'Default module',
|
|
1233
|
-
inputSchema: { type: 'object' },
|
|
1234
|
-
outputSchema: { type: 'object' },
|
|
1235
|
-
};`,
|
|
1236
|
-
);
|
|
1237
|
-
|
|
1238
|
-
const registry = new Registry({ extensionsDir: tempDir });
|
|
1239
|
-
const count = await registry.discover();
|
|
1240
|
-
|
|
1241
|
-
expect(count).toBe(1);
|
|
1242
|
-
expect(registry.has('default_mod')).toBe(true);
|
|
1243
|
-
} finally {
|
|
1244
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
1245
|
-
}
|
|
1246
|
-
});
|
|
1247
|
-
|
|
1248
|
-
it('supports async discoverer', async () => {
|
|
1249
|
-
const mod = {
|
|
1250
|
-
execute: async () => ({}),
|
|
1251
|
-
description: 'Async discovered module',
|
|
1252
|
-
inputSchema: { type: 'object' },
|
|
1253
|
-
outputSchema: { type: 'object' },
|
|
1254
|
-
};
|
|
1255
|
-
|
|
1256
|
-
const discoverer = {
|
|
1257
|
-
async discover(_roots: string[]) {
|
|
1258
|
-
return [{ moduleId: 'async.mod', module: mod }];
|
|
1259
|
-
},
|
|
1260
|
-
};
|
|
1261
|
-
|
|
1262
|
-
const registry = new Registry();
|
|
1263
|
-
registry.setDiscoverer(discoverer);
|
|
1264
|
-
const count = await registry.discover();
|
|
1265
|
-
|
|
1266
|
-
expect(count).toBe(1);
|
|
1267
|
-
expect(registry.has('async.mod')).toBe(true);
|
|
1268
|
-
});
|
|
1269
|
-
});
|
|
1270
|
-
|
|
1271
|
-
/* -----------------------------------------------------------
|
|
1272
|
-
* Custom Validator
|
|
1273
|
-
* --------------------------------------------------------- */
|
|
1274
|
-
|
|
1275
|
-
describe('Registry custom validator', () => {
|
|
1276
|
-
it('rejects modules when custom validator returns errors', async () => {
|
|
1277
|
-
const mod = {
|
|
1278
|
-
execute: async () => ({}),
|
|
1279
|
-
description: 'To be rejected',
|
|
1280
|
-
inputSchema: { type: 'object' },
|
|
1281
|
-
outputSchema: { type: 'object' },
|
|
1282
|
-
};
|
|
1283
|
-
|
|
1284
|
-
const discoverer = {
|
|
1285
|
-
discover(_roots: string[]) {
|
|
1286
|
-
return [{ moduleId: 'rejected.mod', module: mod }];
|
|
1287
|
-
},
|
|
1288
|
-
};
|
|
1289
|
-
|
|
1290
|
-
const validator = {
|
|
1291
|
-
validate(_module: unknown) {
|
|
1292
|
-
return ['rejected by custom validator'];
|
|
1293
|
-
},
|
|
1294
|
-
};
|
|
1295
|
-
|
|
1296
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
1297
|
-
const registry = new Registry();
|
|
1298
|
-
registry.setDiscoverer(discoverer);
|
|
1299
|
-
registry.setValidator(validator);
|
|
1300
|
-
const count = await registry.discover();
|
|
1301
|
-
|
|
1302
|
-
expect(count).toBe(0);
|
|
1303
|
-
expect(registry.has('rejected.mod')).toBe(false);
|
|
1304
|
-
warnSpy.mockRestore();
|
|
1305
|
-
});
|
|
1306
|
-
|
|
1307
|
-
it('accepts modules when custom validator returns empty list', async () => {
|
|
1308
|
-
const mod = {
|
|
1309
|
-
execute: async () => ({}),
|
|
1310
|
-
description: 'To be accepted',
|
|
1311
|
-
inputSchema: { type: 'object' },
|
|
1312
|
-
outputSchema: { type: 'object' },
|
|
1313
|
-
};
|
|
1314
|
-
|
|
1315
|
-
const discoverer = {
|
|
1316
|
-
discover(_roots: string[]) {
|
|
1317
|
-
return [{ moduleId: 'accepted.mod', module: mod }];
|
|
1318
|
-
},
|
|
1319
|
-
};
|
|
1320
|
-
|
|
1321
|
-
const validator = {
|
|
1322
|
-
validate(_module: unknown) {
|
|
1323
|
-
return [];
|
|
1324
|
-
},
|
|
1325
|
-
};
|
|
1326
|
-
|
|
1327
|
-
const registry = new Registry();
|
|
1328
|
-
registry.setDiscoverer(discoverer);
|
|
1329
|
-
registry.setValidator(validator);
|
|
1330
|
-
const count = await registry.discover();
|
|
1331
|
-
|
|
1332
|
-
expect(count).toBe(1);
|
|
1333
|
-
expect(registry.has('accepted.mod')).toBe(true);
|
|
1334
|
-
expect(registry.get('accepted.mod')).toBe(mod);
|
|
1335
|
-
});
|
|
1336
|
-
|
|
1337
|
-
it('custom validator works with default file-system discovery', async () => {
|
|
1338
|
-
const tempDir = mkdtempSync(join(tmpdir(), 'apcore-registry-customval-'));
|
|
1339
|
-
try {
|
|
1340
|
-
writeModuleFile(
|
|
1341
|
-
tempDir,
|
|
1342
|
-
'val_mod.js',
|
|
1343
|
-
`export default {
|
|
1344
|
-
execute: async () => ({}),
|
|
1345
|
-
description: 'Validated module',
|
|
1346
|
-
inputSchema: { type: 'object' },
|
|
1347
|
-
outputSchema: { type: 'object' },
|
|
1348
|
-
};`,
|
|
1349
|
-
);
|
|
1350
|
-
|
|
1351
|
-
const validator = {
|
|
1352
|
-
validate(_module: unknown) {
|
|
1353
|
-
return ['rejected by custom validator'];
|
|
1354
|
-
},
|
|
1355
|
-
};
|
|
1356
|
-
|
|
1357
|
-
const registry = new Registry({ extensionsDir: tempDir });
|
|
1358
|
-
registry.setValidator(validator);
|
|
1359
|
-
const count = await registry.discover();
|
|
1360
|
-
|
|
1361
|
-
// Custom validator rejects all, so nothing should be registered
|
|
1362
|
-
expect(count).toBe(0);
|
|
1363
|
-
expect(registry.has('val_mod')).toBe(false);
|
|
1364
|
-
} finally {
|
|
1365
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
1366
|
-
}
|
|
1367
|
-
});
|
|
1368
|
-
|
|
1369
|
-
it('supports async validator', async () => {
|
|
1370
|
-
const mod = {
|
|
1371
|
-
execute: async () => ({}),
|
|
1372
|
-
description: 'Async validated module',
|
|
1373
|
-
inputSchema: { type: 'object' },
|
|
1374
|
-
outputSchema: { type: 'object' },
|
|
1375
|
-
};
|
|
1376
|
-
|
|
1377
|
-
const discoverer = {
|
|
1378
|
-
discover(_roots: string[]) {
|
|
1379
|
-
return [{ moduleId: 'async.validated', module: mod }];
|
|
1380
|
-
},
|
|
1381
|
-
};
|
|
1382
|
-
|
|
1383
|
-
const validator = {
|
|
1384
|
-
async validate(_module: unknown) {
|
|
1385
|
-
return [];
|
|
1386
|
-
},
|
|
1387
|
-
};
|
|
1388
|
-
|
|
1389
|
-
const registry = new Registry();
|
|
1390
|
-
registry.setDiscoverer(discoverer);
|
|
1391
|
-
registry.setValidator(validator);
|
|
1392
|
-
const count = await registry.discover();
|
|
1393
|
-
|
|
1394
|
-
expect(count).toBe(1);
|
|
1395
|
-
expect(registry.has('async.validated')).toBe(true);
|
|
1396
|
-
});
|
|
1397
|
-
});
|