apcore-js 0.5.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.
Files changed (314) hide show
  1. package/dist/acl.d.ts +27 -0
  2. package/dist/acl.d.ts.map +1 -0
  3. package/dist/acl.js +175 -0
  4. package/dist/acl.js.map +1 -0
  5. package/dist/async-task.d.ts +90 -0
  6. package/dist/async-task.d.ts.map +1 -0
  7. package/dist/async-task.js +215 -0
  8. package/dist/async-task.js.map +1 -0
  9. package/dist/bindings.d.ts +12 -0
  10. package/dist/bindings.d.ts.map +1 -0
  11. package/dist/bindings.js +185 -0
  12. package/dist/bindings.js.map +1 -0
  13. package/dist/cancel.d.ts +14 -0
  14. package/dist/cancel.d.ts.map +1 -0
  15. package/dist/cancel.js +27 -0
  16. package/dist/cancel.js.map +1 -0
  17. package/dist/config.d.ts +9 -0
  18. package/dist/config.d.ts.map +1 -0
  19. package/dist/config.js +23 -0
  20. package/dist/config.js.map +1 -0
  21. package/dist/context.d.ts +50 -0
  22. package/dist/context.d.ts.map +1 -0
  23. package/dist/context.js +87 -0
  24. package/dist/context.js.map +1 -0
  25. package/dist/decorator.d.ts +57 -0
  26. package/dist/decorator.d.ts.map +1 -0
  27. package/dist/decorator.js +74 -0
  28. package/dist/decorator.js.map +1 -0
  29. package/dist/errors.d.ts +215 -0
  30. package/dist/errors.d.ts.map +1 -0
  31. package/dist/errors.js +246 -0
  32. package/dist/errors.js.map +1 -0
  33. package/dist/executor.d.ts +67 -0
  34. package/dist/executor.d.ts.map +1 -0
  35. package/dist/executor.js +372 -0
  36. package/dist/executor.js.map +1 -0
  37. package/dist/extensions.d.ts +58 -0
  38. package/dist/extensions.d.ts.map +1 -0
  39. package/dist/extensions.js +220 -0
  40. package/dist/extensions.js.map +1 -0
  41. package/{src/index.ts → dist/index.d.ts} +3 -62
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +43 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/middleware/adapters.d.ts +18 -0
  46. package/dist/middleware/adapters.d.ts.map +1 -0
  47. package/dist/middleware/adapters.js +25 -0
  48. package/dist/middleware/adapters.js.map +1 -0
  49. package/dist/middleware/base.d.ts +10 -0
  50. package/dist/middleware/base.d.ts.map +1 -0
  51. package/dist/middleware/base.js +15 -0
  52. package/dist/middleware/base.js.map +1 -0
  53. package/{src/middleware/index.ts → dist/middleware/index.d.ts} +1 -0
  54. package/dist/middleware/index.d.ts.map +1 -0
  55. package/dist/middleware/index.js +5 -0
  56. package/dist/middleware/index.js.map +1 -0
  57. package/dist/middleware/logging.d.ts +25 -0
  58. package/dist/middleware/logging.d.ts.map +1 -0
  59. package/dist/middleware/logging.js +64 -0
  60. package/dist/middleware/logging.js.map +1 -0
  61. package/dist/middleware/manager.d.ts +21 -0
  62. package/dist/middleware/manager.d.ts.map +1 -0
  63. package/dist/middleware/manager.js +77 -0
  64. package/dist/middleware/manager.js.map +1 -0
  65. package/dist/module.d.ts +31 -0
  66. package/dist/module.d.ts.map +1 -0
  67. package/dist/module.js +12 -0
  68. package/dist/module.js.map +1 -0
  69. package/dist/observability/context-logger.d.ts +54 -0
  70. package/dist/observability/context-logger.d.ts.map +1 -0
  71. package/dist/observability/context-logger.js +151 -0
  72. package/dist/observability/context-logger.js.map +1 -0
  73. package/{src/observability/index.ts → dist/observability/index.d.ts} +1 -0
  74. package/dist/observability/index.d.ts.map +1 -0
  75. package/dist/observability/index.js +4 -0
  76. package/dist/observability/index.js.map +1 -0
  77. package/dist/observability/metrics.d.ts +30 -0
  78. package/dist/observability/metrics.d.ts.map +1 -0
  79. package/dist/observability/metrics.js +177 -0
  80. package/dist/observability/metrics.js.map +1 -0
  81. package/dist/observability/tracing.d.ts +62 -0
  82. package/dist/observability/tracing.d.ts.map +1 -0
  83. package/dist/observability/tracing.js +184 -0
  84. package/dist/observability/tracing.js.map +1 -0
  85. package/dist/registry/dependencies.d.ts +6 -0
  86. package/dist/registry/dependencies.d.ts.map +1 -0
  87. package/dist/registry/dependencies.js +83 -0
  88. package/dist/registry/dependencies.js.map +1 -0
  89. package/dist/registry/entry-point.d.ts +6 -0
  90. package/dist/registry/entry-point.d.ts.map +1 -0
  91. package/dist/registry/entry-point.js +55 -0
  92. package/dist/registry/entry-point.js.map +1 -0
  93. package/{src/registry/index.ts → dist/registry/index.d.ts} +1 -0
  94. package/dist/registry/index.d.ts.map +1 -0
  95. package/dist/registry/index.js +8 -0
  96. package/dist/registry/index.js.map +1 -0
  97. package/dist/registry/metadata.d.ts +9 -0
  98. package/dist/registry/metadata.d.ts.map +1 -0
  99. package/dist/registry/metadata.js +105 -0
  100. package/dist/registry/metadata.js.map +1 -0
  101. package/dist/registry/registry.d.ts +102 -0
  102. package/dist/registry/registry.d.ts.map +1 -0
  103. package/dist/registry/registry.js +534 -0
  104. package/dist/registry/registry.js.map +1 -0
  105. package/dist/registry/scanner.d.ts +7 -0
  106. package/dist/registry/scanner.d.ts.map +1 -0
  107. package/dist/registry/scanner.js +164 -0
  108. package/dist/registry/scanner.js.map +1 -0
  109. package/dist/registry/schema-export.d.ts +9 -0
  110. package/dist/registry/schema-export.d.ts.map +1 -0
  111. package/dist/registry/schema-export.js +132 -0
  112. package/dist/registry/schema-export.js.map +1 -0
  113. package/dist/registry/types.d.ts +29 -0
  114. package/dist/registry/types.d.ts.map +1 -0
  115. package/dist/registry/types.js +5 -0
  116. package/dist/registry/types.js.map +1 -0
  117. package/dist/registry/validation.d.ts +9 -0
  118. package/dist/registry/validation.d.ts.map +1 -0
  119. package/dist/registry/validation.js +33 -0
  120. package/dist/registry/validation.js.map +1 -0
  121. package/dist/schema/annotations.d.ts +8 -0
  122. package/dist/schema/annotations.d.ts.map +1 -0
  123. package/dist/schema/annotations.js +52 -0
  124. package/dist/schema/annotations.js.map +1 -0
  125. package/dist/schema/exporter.d.ts +13 -0
  126. package/dist/schema/exporter.d.ts.map +1 -0
  127. package/dist/schema/exporter.js +71 -0
  128. package/dist/schema/exporter.js.map +1 -0
  129. package/dist/schema/index.d.ts +9 -0
  130. package/dist/schema/index.d.ts.map +1 -0
  131. package/{src/schema/index.ts → dist/schema/index.js} +1 -7
  132. package/dist/schema/index.js.map +1 -0
  133. package/dist/schema/loader.d.ts +30 -0
  134. package/dist/schema/loader.d.ts.map +1 -0
  135. package/dist/schema/loader.js +260 -0
  136. package/dist/schema/loader.js.map +1 -0
  137. package/dist/schema/ref-resolver.d.ts +19 -0
  138. package/dist/schema/ref-resolver.d.ts.map +1 -0
  139. package/dist/schema/ref-resolver.js +212 -0
  140. package/dist/schema/ref-resolver.js.map +1 -0
  141. package/dist/schema/strict.d.ts +7 -0
  142. package/dist/schema/strict.d.ts.map +1 -0
  143. package/dist/schema/strict.js +127 -0
  144. package/dist/schema/strict.js.map +1 -0
  145. package/dist/schema/types.d.ts +53 -0
  146. package/dist/schema/types.d.ts.map +1 -0
  147. package/dist/schema/types.js +31 -0
  148. package/dist/schema/types.js.map +1 -0
  149. package/dist/schema/validator.d.ts +16 -0
  150. package/dist/schema/validator.d.ts.map +1 -0
  151. package/dist/schema/validator.js +71 -0
  152. package/dist/schema/validator.js.map +1 -0
  153. package/dist/trace-context.d.ts +35 -0
  154. package/dist/trace-context.d.ts.map +1 -0
  155. package/dist/trace-context.js +86 -0
  156. package/dist/trace-context.js.map +1 -0
  157. package/dist/utils/index.d.ts +11 -0
  158. package/dist/utils/index.d.ts.map +1 -0
  159. package/dist/utils/index.js +32 -0
  160. package/dist/utils/index.js.map +1 -0
  161. package/dist/utils/pattern.d.ts +5 -0
  162. package/dist/utils/pattern.d.ts.map +1 -0
  163. package/dist/utils/pattern.js +31 -0
  164. package/dist/utils/pattern.js.map +1 -0
  165. package/package.json +8 -2
  166. package/.claude/settings.local.json +0 -12
  167. package/.github/workflows/ci.yml +0 -39
  168. package/.gitmessage +0 -60
  169. package/.pre-commit-config.yaml +0 -28
  170. package/CHANGELOG.md +0 -214
  171. package/CLAUDE.md +0 -68
  172. package/apcore-logo.svg +0 -79
  173. package/planning/acl-system/overview.md +0 -54
  174. package/planning/acl-system/plan.md +0 -92
  175. package/planning/acl-system/state.json +0 -76
  176. package/planning/acl-system/tasks/acl-core.md +0 -226
  177. package/planning/acl-system/tasks/acl-rule.md +0 -92
  178. package/planning/acl-system/tasks/conditional-rules.md +0 -259
  179. package/planning/acl-system/tasks/pattern-matching.md +0 -152
  180. package/planning/acl-system/tasks/yaml-loading.md +0 -271
  181. package/planning/core-executor/overview.md +0 -53
  182. package/planning/core-executor/plan.md +0 -88
  183. package/planning/core-executor/state.json +0 -76
  184. package/planning/core-executor/tasks/async-support.md +0 -106
  185. package/planning/core-executor/tasks/execution-pipeline.md +0 -113
  186. package/planning/core-executor/tasks/redaction.md +0 -85
  187. package/planning/core-executor/tasks/safety-checks.md +0 -65
  188. package/planning/core-executor/tasks/setup.md +0 -75
  189. package/planning/decorator-bindings/overview.md +0 -62
  190. package/planning/decorator-bindings/plan.md +0 -104
  191. package/planning/decorator-bindings/state.json +0 -87
  192. package/planning/decorator-bindings/tasks/binding-directory.md +0 -79
  193. package/planning/decorator-bindings/tasks/binding-loader.md +0 -148
  194. package/planning/decorator-bindings/tasks/explicit-schemas.md +0 -85
  195. package/planning/decorator-bindings/tasks/function-module.md +0 -127
  196. package/planning/decorator-bindings/tasks/module-factory.md +0 -89
  197. package/planning/decorator-bindings/tasks/schema-modes.md +0 -142
  198. package/planning/middleware-system/overview.md +0 -48
  199. package/planning/middleware-system/plan.md +0 -102
  200. package/planning/middleware-system/state.json +0 -65
  201. package/planning/middleware-system/tasks/adapters.md +0 -170
  202. package/planning/middleware-system/tasks/base.md +0 -115
  203. package/planning/middleware-system/tasks/logging-middleware.md +0 -304
  204. package/planning/middleware-system/tasks/manager.md +0 -313
  205. package/planning/observability/overview.md +0 -53
  206. package/planning/observability/plan.md +0 -119
  207. package/planning/observability/state.json +0 -98
  208. package/planning/observability/tasks/context-logger.md +0 -201
  209. package/planning/observability/tasks/exporters.md +0 -121
  210. package/planning/observability/tasks/metrics-collector.md +0 -162
  211. package/planning/observability/tasks/metrics-middleware.md +0 -141
  212. package/planning/observability/tasks/obs-logging-middleware.md +0 -179
  213. package/planning/observability/tasks/span-model.md +0 -120
  214. package/planning/observability/tasks/tracing-middleware.md +0 -179
  215. package/planning/overview.md +0 -81
  216. package/planning/registry-system/overview.md +0 -57
  217. package/planning/registry-system/plan.md +0 -114
  218. package/planning/registry-system/state.json +0 -109
  219. package/planning/registry-system/tasks/dependencies.md +0 -157
  220. package/planning/registry-system/tasks/entry-point.md +0 -148
  221. package/planning/registry-system/tasks/metadata.md +0 -198
  222. package/planning/registry-system/tasks/registry-core.md +0 -323
  223. package/planning/registry-system/tasks/scanner.md +0 -172
  224. package/planning/registry-system/tasks/schema-export.md +0 -261
  225. package/planning/registry-system/tasks/types.md +0 -124
  226. package/planning/registry-system/tasks/validation.md +0 -177
  227. package/planning/schema-system/overview.md +0 -56
  228. package/planning/schema-system/plan.md +0 -121
  229. package/planning/schema-system/state.json +0 -98
  230. package/planning/schema-system/tasks/exporter.md +0 -153
  231. package/planning/schema-system/tasks/loader.md +0 -106
  232. package/planning/schema-system/tasks/ref-resolver.md +0 -133
  233. package/planning/schema-system/tasks/strict-mode.md +0 -140
  234. package/planning/schema-system/tasks/typebox-generation.md +0 -133
  235. package/planning/schema-system/tasks/types-and-annotations.md +0 -160
  236. package/planning/schema-system/tasks/validator.md +0 -149
  237. package/src/acl.ts +0 -200
  238. package/src/async-task.ts +0 -267
  239. package/src/bindings.ts +0 -207
  240. package/src/cancel.ts +0 -32
  241. package/src/config.ts +0 -24
  242. package/src/context.ts +0 -160
  243. package/src/decorator.ts +0 -110
  244. package/src/errors.ts +0 -429
  245. package/src/executor.ts +0 -493
  246. package/src/extensions.ts +0 -265
  247. package/src/middleware/adapters.ts +0 -54
  248. package/src/middleware/base.ts +0 -33
  249. package/src/middleware/logging.ts +0 -103
  250. package/src/middleware/manager.ts +0 -105
  251. package/src/module.ts +0 -43
  252. package/src/observability/context-logger.ts +0 -203
  253. package/src/observability/metrics.ts +0 -214
  254. package/src/observability/tracing.ts +0 -252
  255. package/src/registry/dependencies.ts +0 -99
  256. package/src/registry/entry-point.ts +0 -64
  257. package/src/registry/metadata.ts +0 -111
  258. package/src/registry/registry.ts +0 -580
  259. package/src/registry/scanner.ts +0 -168
  260. package/src/registry/schema-export.ts +0 -181
  261. package/src/registry/types.ts +0 -32
  262. package/src/registry/validation.ts +0 -38
  263. package/src/schema/annotations.ts +0 -68
  264. package/src/schema/exporter.ts +0 -90
  265. package/src/schema/loader.ts +0 -273
  266. package/src/schema/ref-resolver.ts +0 -244
  267. package/src/schema/strict.ts +0 -136
  268. package/src/schema/types.ts +0 -73
  269. package/src/schema/validator.ts +0 -82
  270. package/src/trace-context.ts +0 -102
  271. package/src/utils/index.ts +0 -5
  272. package/src/utils/pattern.ts +0 -30
  273. package/tests/async-task.test.ts +0 -335
  274. package/tests/helpers.ts +0 -30
  275. package/tests/integration/test-acl-safety.test.ts +0 -269
  276. package/tests/integration/test-binding-executor.test.ts +0 -194
  277. package/tests/integration/test-e2e-flow.test.ts +0 -117
  278. package/tests/integration/test-error-propagation.test.ts +0 -259
  279. package/tests/integration/test-middleware-chain.test.ts +0 -120
  280. package/tests/integration/test-observability-integration.test.ts +0 -438
  281. package/tests/observability/test-context-logger.test.ts +0 -123
  282. package/tests/observability/test-metrics.test.ts +0 -186
  283. package/tests/observability/test-tracing.test.ts +0 -303
  284. package/tests/registry/test-dependencies.test.ts +0 -70
  285. package/tests/registry/test-entry-point.test.ts +0 -133
  286. package/tests/registry/test-metadata.test.ts +0 -265
  287. package/tests/registry/test-registry.test.ts +0 -1397
  288. package/tests/registry/test-scanner.test.ts +0 -257
  289. package/tests/registry/test-schema-export.test.ts +0 -355
  290. package/tests/registry/test-validation.test.ts +0 -75
  291. package/tests/schema/test-annotations.test.ts +0 -137
  292. package/tests/schema/test-exporter.test.ts +0 -172
  293. package/tests/schema/test-loader.test.ts +0 -461
  294. package/tests/schema/test-ref-resolver.test.ts +0 -530
  295. package/tests/schema/test-strict.test.ts +0 -348
  296. package/tests/schema/test-validator.test.ts +0 -64
  297. package/tests/test-acl.test.ts +0 -423
  298. package/tests/test-bindings.test.ts +0 -227
  299. package/tests/test-cancel.test.ts +0 -71
  300. package/tests/test-config.test.ts +0 -76
  301. package/tests/test-context.test.ts +0 -266
  302. package/tests/test-decorator.test.ts +0 -173
  303. package/tests/test-errors.test.ts +0 -647
  304. package/tests/test-executor-stream.test.ts +0 -208
  305. package/tests/test-executor.test.ts +0 -252
  306. package/tests/test-extensions.test.ts +0 -310
  307. package/tests/test-logging-middleware.test.ts +0 -150
  308. package/tests/test-middleware-manager.test.ts +0 -185
  309. package/tests/test-middleware.test.ts +0 -86
  310. package/tests/test-trace-context.test.ts +0 -251
  311. package/tests/utils/test-pattern.test.ts +0 -109
  312. package/tsconfig.build.json +0 -8
  313. package/tsconfig.json +0 -20
  314. 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
- });