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