@camstack/server 0.2.2 → 1.0.1

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 (234) hide show
  1. package/{src/agent-status-page.ts → dist/agent-status-page.js} +30 -45
  2. package/dist/api/addon-upload.js +441 -0
  3. package/dist/api/addons-custom.router.js +91 -0
  4. package/dist/api/auth-whoami.js +55 -0
  5. package/dist/api/bridge-addons.router.js +109 -0
  6. package/dist/api/capabilities.router.js +229 -0
  7. package/dist/api/core/addon-settings.router.js +117 -0
  8. package/dist/api/core/agents.router.js +73 -0
  9. package/dist/api/core/auth.router.js +286 -0
  10. package/dist/api/core/bulk-update-coordinator.js +229 -0
  11. package/dist/api/core/cap-providers.js +1124 -0
  12. package/dist/api/core/capabilities.router.js +138 -0
  13. package/dist/api/core/collection-preference.js +17 -0
  14. package/dist/api/core/event-bus-proxy.router.js +45 -0
  15. package/dist/api/core/hwaccel.router.js +91 -0
  16. package/dist/api/core/live-events.router.js +61 -0
  17. package/dist/api/core/logs.router.js +172 -0
  18. package/dist/api/core/notifications.router.js +67 -0
  19. package/dist/api/core/repl.router.js +35 -0
  20. package/dist/api/core/settings-backend.router.js +121 -0
  21. package/dist/api/core/stream-probe.router.js +58 -0
  22. package/dist/api/core/system-events.router.js +100 -0
  23. package/dist/api/health/health.routes.js +68 -0
  24. package/{src/api/oauth2/consent-page.ts → dist/api/oauth2/consent-page.js} +11 -20
  25. package/dist/api/oauth2/oauth2-routes.js +219 -0
  26. package/dist/api/trpc/cap-mount-helpers.js +194 -0
  27. package/dist/api/trpc/cap-route-error-formatter.js +133 -0
  28. package/dist/api/trpc/client-ip.js +147 -0
  29. package/dist/api/trpc/core-cap-bridge.js +115 -0
  30. package/dist/api/trpc/generated-cap-mounts.js +388 -0
  31. package/dist/api/trpc/generated-cap-routers.js +7635 -0
  32. package/dist/api/trpc/scope-access.js +93 -0
  33. package/dist/api/trpc/trpc.context.js +184 -0
  34. package/dist/api/trpc/trpc.middleware.js +139 -0
  35. package/dist/api/trpc/trpc.router.js +188 -0
  36. package/dist/auth/session-cookie.js +47 -0
  37. package/dist/boot/boot-config.js +241 -0
  38. package/dist/boot/integration-id-backfill.js +76 -0
  39. package/dist/boot/post-boot.service.js +85 -0
  40. package/dist/core/addon/addon-call-gateway.js +99 -0
  41. package/dist/core/addon/addon-package.service.js +1560 -0
  42. package/dist/core/addon/addon-registry.service.js +2739 -0
  43. package/{src/core/addon/addon-row-manifest.ts → dist/core/addon/addon-row-manifest.js} +5 -5
  44. package/dist/core/addon/addon-search.service.js +62 -0
  45. package/dist/core/addon/addon-settings-provider.js +102 -0
  46. package/dist/core/addon/addon.tokens.js +5 -0
  47. package/dist/core/addon-bridge/addon-bridge.service.js +145 -0
  48. package/dist/core/addon-pages/addon-pages.service.js +107 -0
  49. package/dist/core/addon-widgets/addon-widgets.service.js +120 -0
  50. package/dist/core/agent/agent-registry.service.js +477 -0
  51. package/dist/core/auth/auth.service.js +10 -0
  52. package/dist/core/capability/capability.service.js +58 -0
  53. package/dist/core/config/config.schema.js +7 -0
  54. package/dist/core/config/config.service.js +10 -0
  55. package/dist/core/events/event-bus.service.js +83 -0
  56. package/dist/core/feature/feature.service.js +10 -0
  57. package/dist/core/lifecycle/lifecycle-state-machine.js +6 -0
  58. package/dist/core/logging/log-ring-buffer.js +6 -0
  59. package/dist/core/logging/logging.service.js +130 -0
  60. package/dist/core/logging/scoped-logger.js +6 -0
  61. package/dist/core/moleculer/cap-call-fn.js +50 -0
  62. package/dist/core/moleculer/cap-route-authority.js +122 -0
  63. package/dist/core/moleculer/moleculer.service.js +898 -0
  64. package/dist/core/network/network-quality.service.js +7 -0
  65. package/dist/core/notification/notification-wrapper.service.js +33 -0
  66. package/dist/core/notification/toast-wrapper.service.js +25 -0
  67. package/dist/core/provider/provider.tokens.js +4 -0
  68. package/dist/core/repl/repl-engine.service.js +140 -0
  69. package/dist/core/storage/fs-storage-backend.js +6 -0
  70. package/dist/core/storage/storage-location-manager.js +6 -0
  71. package/dist/core/storage/storage.service.js +7 -0
  72. package/dist/core/streaming/stream-probe.service.js +209 -0
  73. package/dist/core/topology/topology-emitter.service.js +106 -0
  74. package/dist/launcher.js +325 -0
  75. package/dist/main.js +1098 -0
  76. package/dist/manual-boot.js +227 -0
  77. package/package.json +5 -1
  78. package/src/__tests__/addon-install-e2e.test.ts +0 -74
  79. package/src/__tests__/addon-pages-e2e.test.ts +0 -200
  80. package/src/__tests__/addon-route-session.test.ts +0 -17
  81. package/src/__tests__/addon-settings-router.spec.ts +0 -67
  82. package/src/__tests__/addon-upload.spec.ts +0 -475
  83. package/src/__tests__/agent-registry.spec.ts +0 -179
  84. package/src/__tests__/agent-status-page.spec.ts +0 -82
  85. package/src/__tests__/auth-session-cookie.test.ts +0 -48
  86. package/src/__tests__/bulk-update-coordinator.spec.ts +0 -303
  87. package/src/__tests__/cap-ownership-authority.spec.ts +0 -431
  88. package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +0 -206
  89. package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +0 -37
  90. package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +0 -110
  91. package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +0 -292
  92. package/src/__tests__/cap-providers-bulk-update.spec.ts +0 -408
  93. package/src/__tests__/cap-route-adapter.spec.ts +0 -302
  94. package/src/__tests__/cap-routers/_meta.spec.ts +0 -199
  95. package/src/__tests__/cap-routers/addon-settings.router.spec.ts +0 -115
  96. package/src/__tests__/cap-routers/broker-routing.router.spec.ts +0 -177
  97. package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +0 -125
  98. package/src/__tests__/cap-routers/capabilities-node.spec.ts +0 -68
  99. package/src/__tests__/cap-routers/device-link-overlay.spec.ts +0 -137
  100. package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +0 -194
  101. package/src/__tests__/cap-routers/harness.ts +0 -163
  102. package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +0 -133
  103. package/src/__tests__/cap-routers/null-provider-guard.spec.ts +0 -64
  104. package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +0 -159
  105. package/src/__tests__/cap-routers/settings-store.router.spec.ts +0 -291
  106. package/src/__tests__/capability-e2e.test.ts +0 -384
  107. package/src/__tests__/cli-e2e.test.ts +0 -150
  108. package/src/__tests__/core-cap-bridge.spec.ts +0 -91
  109. package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +0 -40
  110. package/src/__tests__/device-settings-contribution-dispatch.spec.ts +0 -280
  111. package/src/__tests__/embedded-deps-e2e.test.ts +0 -125
  112. package/src/__tests__/event-bus-proxy-router.spec.ts +0 -75
  113. package/src/__tests__/fixtures/mock-analysis-addon-a.ts +0 -37
  114. package/src/__tests__/fixtures/mock-analysis-addon-b.ts +0 -37
  115. package/src/__tests__/fixtures/mock-log-addon.ts +0 -37
  116. package/src/__tests__/fixtures/mock-storage-addon.ts +0 -40
  117. package/src/__tests__/framework-allowlist.spec.ts +0 -96
  118. package/src/__tests__/framework-installer-defer-restart.spec.ts +0 -165
  119. package/src/__tests__/https-e2e.test.ts +0 -124
  120. package/src/__tests__/lifecycle-e2e.test.ts +0 -189
  121. package/src/__tests__/live-events-subscription.spec.ts +0 -149
  122. package/src/__tests__/moleculer/uds-readiness.spec.ts +0 -150
  123. package/src/__tests__/moleculer/uds-topology.spec.ts +0 -418
  124. package/src/__tests__/moleculer/uds-unowned-call.spec.ts +0 -383
  125. package/src/__tests__/moleculer-register-node-idempotency.spec.ts +0 -273
  126. package/src/__tests__/native-cap-route.spec.ts +0 -427
  127. package/src/__tests__/oauth2-account-linking.spec.ts +0 -867
  128. package/src/__tests__/post-boot-restart.spec.ts +0 -161
  129. package/src/__tests__/singleton-contention.test.ts +0 -499
  130. package/src/__tests__/streaming-diagnostic.test.ts +0 -615
  131. package/src/__tests__/streaming-scale.test.ts +0 -314
  132. package/src/__tests__/uds-addon-call-wiring.spec.ts +0 -242
  133. package/src/__tests__/uds-log-ingest.spec.ts +0 -183
  134. package/src/api/__tests__/addons-custom.spec.ts +0 -148
  135. package/src/api/__tests__/capabilities.router.test.ts +0 -56
  136. package/src/api/addon-upload.ts +0 -529
  137. package/src/api/addons-custom.router.ts +0 -101
  138. package/src/api/auth-whoami.ts +0 -101
  139. package/src/api/bridge-addons.router.ts +0 -122
  140. package/src/api/capabilities.router.ts +0 -265
  141. package/src/api/core/__tests__/auth-router-totp.spec.ts +0 -297
  142. package/src/api/core/__tests__/integration-markers.spec.ts +0 -10
  143. package/src/api/core/addon-settings.router.ts +0 -127
  144. package/src/api/core/agents.router.ts +0 -86
  145. package/src/api/core/auth.router.ts +0 -322
  146. package/src/api/core/bulk-update-coordinator.ts +0 -305
  147. package/src/api/core/cap-providers.ts +0 -1339
  148. package/src/api/core/capabilities.router.ts +0 -149
  149. package/src/api/core/collection-preference.ts +0 -40
  150. package/src/api/core/event-bus-proxy.router.ts +0 -45
  151. package/src/api/core/hwaccel.router.ts +0 -108
  152. package/src/api/core/live-events.router.ts +0 -67
  153. package/src/api/core/logs.router.ts +0 -195
  154. package/src/api/core/notifications.router.ts +0 -66
  155. package/src/api/core/repl.router.ts +0 -39
  156. package/src/api/core/settings-backend.router.ts +0 -140
  157. package/src/api/core/stream-probe.router.ts +0 -57
  158. package/src/api/core/system-events.router.ts +0 -125
  159. package/src/api/health/health.routes.ts +0 -117
  160. package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +0 -62
  161. package/src/api/oauth2/oauth2-routes.ts +0 -281
  162. package/src/api/trpc/__tests__/client-ip.spec.ts +0 -146
  163. package/src/api/trpc/__tests__/scope-access-device.spec.ts +0 -268
  164. package/src/api/trpc/__tests__/scope-access.spec.ts +0 -102
  165. package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +0 -136
  166. package/src/api/trpc/cap-mount-helpers.ts +0 -245
  167. package/src/api/trpc/cap-route-error-formatter.ts +0 -171
  168. package/src/api/trpc/client-ip.ts +0 -147
  169. package/src/api/trpc/core-cap-bridge.ts +0 -154
  170. package/src/api/trpc/generated-cap-mounts.ts +0 -1240
  171. package/src/api/trpc/generated-cap-routers.ts +0 -11523
  172. package/src/api/trpc/scope-access.ts +0 -110
  173. package/src/api/trpc/trpc.context.ts +0 -258
  174. package/src/api/trpc/trpc.middleware.ts +0 -146
  175. package/src/api/trpc/trpc.router.ts +0 -389
  176. package/src/auth/session-cookie.ts +0 -54
  177. package/src/boot/__tests__/integration-id-backfill.spec.ts +0 -131
  178. package/src/boot/boot-config.ts +0 -259
  179. package/src/boot/integration-id-backfill.ts +0 -109
  180. package/src/boot/post-boot.service.ts +0 -105
  181. package/src/core/addon/__tests__/addon-registry-capability.test.ts +0 -62
  182. package/src/core/addon/__tests__/addon-row-manifest.spec.ts +0 -62
  183. package/src/core/addon/addon-call-gateway.ts +0 -171
  184. package/src/core/addon/addon-package.service.ts +0 -1787
  185. package/src/core/addon/addon-registry.service.ts +0 -3130
  186. package/src/core/addon/addon-search.service.ts +0 -91
  187. package/src/core/addon/addon-settings-provider.ts +0 -220
  188. package/src/core/addon/addon.tokens.ts +0 -2
  189. package/src/core/addon-bridge/addon-bridge.service.ts +0 -130
  190. package/src/core/addon-pages/addon-pages.service.spec.ts +0 -117
  191. package/src/core/addon-pages/addon-pages.service.ts +0 -82
  192. package/src/core/addon-widgets/addon-widgets.service.ts +0 -95
  193. package/src/core/agent/agent-registry.service.ts +0 -529
  194. package/src/core/auth/auth.service.spec.ts +0 -86
  195. package/src/core/auth/auth.service.ts +0 -8
  196. package/src/core/capability/capability.service.ts +0 -66
  197. package/src/core/config/config.schema.ts +0 -3
  198. package/src/core/config/config.service.spec.ts +0 -175
  199. package/src/core/config/config.service.ts +0 -7
  200. package/src/core/events/event-bus.service.spec.ts +0 -235
  201. package/src/core/events/event-bus.service.ts +0 -89
  202. package/src/core/feature/feature.service.spec.ts +0 -99
  203. package/src/core/feature/feature.service.ts +0 -8
  204. package/src/core/lifecycle/lifecycle-state-machine.spec.ts +0 -166
  205. package/src/core/lifecycle/lifecycle-state-machine.ts +0 -3
  206. package/src/core/logging/log-ring-buffer.ts +0 -3
  207. package/src/core/logging/logging.service.spec.ts +0 -287
  208. package/src/core/logging/logging.service.ts +0 -143
  209. package/src/core/logging/scoped-logger.ts +0 -3
  210. package/src/core/moleculer/cap-call-fn.spec.ts +0 -173
  211. package/src/core/moleculer/cap-call-fn.ts +0 -107
  212. package/src/core/moleculer/cap-route-authority.ts +0 -194
  213. package/src/core/moleculer/moleculer.service.ts +0 -1072
  214. package/src/core/network/network-quality.service.spec.ts +0 -53
  215. package/src/core/network/network-quality.service.ts +0 -5
  216. package/src/core/notification/notification-wrapper.service.ts +0 -34
  217. package/src/core/notification/toast-wrapper.service.ts +0 -27
  218. package/src/core/provider/provider.tokens.ts +0 -1
  219. package/src/core/repl/repl-engine.service.spec.ts +0 -444
  220. package/src/core/repl/repl-engine.service.ts +0 -155
  221. package/src/core/storage/fs-storage-backend.spec.ts +0 -70
  222. package/src/core/storage/fs-storage-backend.ts +0 -3
  223. package/src/core/storage/storage-location-manager.spec.ts +0 -130
  224. package/src/core/storage/storage-location-manager.ts +0 -3
  225. package/src/core/storage/storage.service.spec.ts +0 -73
  226. package/src/core/storage/storage.service.ts +0 -3
  227. package/src/core/streaming/stream-probe.service.ts +0 -221
  228. package/src/core/topology/topology-emitter.service.ts +0 -105
  229. package/src/launcher.ts +0 -314
  230. package/src/main.ts +0 -1245
  231. package/src/manual-boot.ts +0 -301
  232. package/tsconfig.build.json +0 -8
  233. package/tsconfig.json +0 -33
  234. package/vitest.config.ts +0 -26
@@ -1,383 +0,0 @@
1
- /**
2
- * F0 (slice-5 outbound) — hub-side `onUnownedCall` wiring.
3
- *
4
- * When a forked hub child issues `ctx.api.<cap>.<method>` for a cap NO local
5
- * sibling owns, the hub's `LocalChildRegistry` must route it via the parent's
6
- * `onUnownedCall` handler and return the result over UDS — instead of falling
7
- * through `UDS_NO_ROUTE` to the child's own broker (which F1+F2 removes).
8
- *
9
- * The hub wires the handler in `onModuleInit` as:
10
- * createParentUnownedCallHandler({ getResolver: () => this.resolver, broker: this.broker, logger })
11
- *
12
- * This test stands up the REAL production handler against:
13
- * - a REAL `CapRouteResolver` configured with a hub-in-process provider, to
14
- * prove the resolver-first branch resolves a hub cap, AND
15
- * - a broker fake exposing a `$`-infra core service (`$core-caps`), to prove
16
- * the broker-fallback branch reaches a core service the resolver can't see.
17
- *
18
- * We do NOT boot the full MoleculerService (it requires a real broker + DI
19
- * graph); instead we exercise the exact handler the hub constructs, with the
20
- * same resolver shape the hub injects, so both branches are covered.
21
- */
22
- import { describe, it, expect, vi } from 'vitest'
23
- import {
24
- createParentUnownedCallHandler,
25
- CapRouteResolver,
26
- CapRouteError,
27
- HubNodeRegistry,
28
- } from '@camstack/kernel'
29
- import type {
30
- NodeCapAuthority,
31
- InProcessProviderLookup,
32
- CapRouteResolverDeps,
33
- NodeNativeCapEntry,
34
- HubLocalChildDispatcher,
35
- CapCallInput,
36
- } from '@camstack/kernel'
37
- import type { ServiceBroker } from 'moleculer'
38
-
39
- // A node authority that knows nothing — the only routable thing is the
40
- // hub-in-process provider injected via inProcessProviders.
41
- const emptyNodeAuthority: NodeCapAuthority = {
42
- nodeKnowsCap: () => false,
43
- nodeIsAgent: () => false,
44
- nodeOnline: () => false,
45
- listNodeIds: () => [],
46
- getAddonId: () => null,
47
- getAgentChildId: () => null,
48
- isNativeCap: () => false,
49
- }
50
-
51
- /**
52
- * Broker fake exposing the hub's `$core-caps` core service (the bridge for
53
- * `system`/`addons`/`capabilities`/`nodes` routers) so the broker-fallback
54
- * branch resolves it. `$core-caps` is NOT a CapabilityRegistry provider, so
55
- * the resolver returns `no-provider` for it and the fallback runs.
56
- */
57
- function hubBrokerFake(): ServiceBroker {
58
- const services = [{ name: '$core-caps', nodeID: 'hub', actions: { 'system.info': {} } }]
59
- const broker = {
60
- nodeID: 'hub',
61
- call: vi.fn(async (action: string, params: unknown) => {
62
- if (action === '$core-caps.system.info') return { uptimeSec: 99, params }
63
- throw Object.assign(new Error(`Service not found: ${action}`), { type: 'SERVICE_NOT_FOUND' })
64
- }),
65
- waitForServices: vi.fn(async () => undefined),
66
- registry: { getServiceList: () => services },
67
- }
68
- return broker as unknown as ServiceBroker
69
- }
70
-
71
- /**
72
- * Minimal `HubNodeRegistry`-shaped fake exposing only the lookup the unowned
73
- * handler uses: `listNativeCapEntriesForDevice`. The handler depends solely on
74
- * this method, so we don't need the full registry to exercise its routing.
75
- */
76
- function nodeRegistryFake(
77
- byDevice: Record<number, readonly NodeNativeCapEntry[]> = {},
78
- ): HubNodeRegistry {
79
- const fake = {
80
- listNativeCapEntriesForDevice: (deviceId: number): readonly NodeNativeCapEntry[] =>
81
- byDevice[deviceId] ?? [],
82
- }
83
- return fake as unknown as HubNodeRegistry
84
- }
85
-
86
- /**
87
- * Hub-local UDS dispatcher fake (LocalChildRegistry surface). `resolveChildId`
88
- * returns the configured childId on a (capName, deviceId) match else null;
89
- * `callCapOnChild` records the call and echoes a sentinel result.
90
- */
91
- function localDispatcherFake(owner?: { capName: string; deviceId: number; childId: string }): {
92
- dispatcher: HubLocalChildDispatcher
93
- resolveChildId: ReturnType<typeof vi.fn>
94
- callCapOnChild: ReturnType<typeof vi.fn>
95
- } {
96
- const resolveChildId = vi.fn((capName: string, deviceId?: number): string | null =>
97
- owner !== undefined && capName === owner.capName && deviceId === owner.deviceId
98
- ? owner.childId
99
- : null,
100
- )
101
- const callCapOnChild = vi.fn(async (childId: string, input: CapCallInput) => ({
102
- routedOverUds: childId,
103
- capName: input.capName,
104
- }))
105
- const dispatcher: HubLocalChildDispatcher = { resolveChildId, callCapOnChild }
106
- return { dispatcher, resolveChildId, callCapOnChild }
107
- }
108
-
109
- describe('hub onUnownedCall wiring (F0)', () => {
110
- it('resolver-first: resolves a hub-in-process cap through the real CapRouteResolver', async () => {
111
- const broker = hubBrokerFake()
112
-
113
- // Hub-in-process provider for `settings-store`.
114
- const settingsStore = { get: async (params: unknown) => ({ value: 'hub-resolved', params }) }
115
- const inProcessProviders: InProcessProviderLookup = (capName) =>
116
- capName === 'settings-store'
117
- ? {
118
- invoke: (method, args) =>
119
- Promise.resolve(
120
- (settingsStore as Record<string, (a: unknown) => unknown>)[method](args),
121
- ),
122
- }
123
- : null
124
-
125
- const resolverDeps: CapRouteResolverDeps = {
126
- hubNodeId: 'hub',
127
- broker,
128
- hubLocalRegistry: null,
129
- nodeAuthority: emptyNodeAuthority,
130
- inProcessProviders,
131
- }
132
- const resolver = new CapRouteResolver(resolverDeps)
133
-
134
- const handler = createParentUnownedCallHandler({
135
- getResolver: () => resolver,
136
- broker,
137
- nodeRegistry: nodeRegistryFake(),
138
- })
139
-
140
- const result = await handler({ capName: 'settings-store', method: 'get', args: { key: 'k' } })
141
- expect(result).toEqual({ value: 'hub-resolved', params: { key: 'k' } })
142
- // Resolver served it — broker untouched.
143
- expect(broker.call as ReturnType<typeof vi.fn>).not.toHaveBeenCalled()
144
- })
145
-
146
- it('broker-fallback: a `$`-infra core service (`$core-caps.system.info`) routes via the broker', async () => {
147
- const broker = hubBrokerFake()
148
-
149
- // No in-process providers and no known nodes → the resolver returns
150
- // no-provider for `system`, exactly as it does live for the core routers.
151
- const resolver = new CapRouteResolver({
152
- hubNodeId: 'hub',
153
- broker,
154
- hubLocalRegistry: null,
155
- nodeAuthority: emptyNodeAuthority,
156
- inProcessProviders: () => null,
157
- })
158
-
159
- const handler = createParentUnownedCallHandler({
160
- getResolver: () => resolver,
161
- broker,
162
- nodeRegistry: nodeRegistryFake(),
163
- })
164
-
165
- const result = await handler({ capName: 'system', method: 'info', args: undefined })
166
- expect(result).toEqual({ uptimeSec: 99, params: undefined })
167
- expect(broker.call).toHaveBeenCalledWith('$core-caps.system.info', undefined, undefined)
168
- })
169
-
170
- it('pre-init safety: getResolver returns null before onModuleInit → broker fallback still works', async () => {
171
- const broker = hubBrokerFake()
172
- // Mirrors the window before `this.resolver` is constructed: getResolver
173
- // returns null, so the handler goes straight to the broker fallback.
174
- const handler = createParentUnownedCallHandler({
175
- getResolver: () => null,
176
- broker,
177
- nodeRegistry: nodeRegistryFake(),
178
- })
179
-
180
- const result = await handler({ capName: 'system', method: 'info', args: undefined })
181
- expect(result).toEqual({ uptimeSec: 99, params: undefined })
182
- })
183
-
184
- it('deviceId-aware resolver: derives deviceId from args and routes via the resolver (no broker fallback)', async () => {
185
- const broker = hubBrokerFake()
186
-
187
- // A resolver-shaped fake that resolves a route ONLY when invoked with a
188
- // deviceId — mirroring the real resolver routing a device-scoped native cap
189
- // to its owning provider. Without a deviceId it returns no-provider.
190
- let resolvedWithDeviceId: number | undefined
191
- const resolver = {
192
- resolveCapRoute: (capName: string, opts: { deviceId?: number }) => {
193
- if (opts.deviceId === undefined) {
194
- throw new CapRouteError(capName, undefined, { reason: 'no-provider', rejected: [] })
195
- }
196
- resolvedWithDeviceId = opts.deviceId
197
- return { kind: 'hub-in-process', capName, deviceId: opts.deviceId }
198
- },
199
- dispatch: async () => ({ catalog: ['stream-a'] }),
200
- }
201
-
202
- const handler = createParentUnownedCallHandler({
203
- getResolver: () => resolver as unknown as CapRouteResolver,
204
- broker,
205
- nodeRegistry: nodeRegistryFake(),
206
- })
207
-
208
- // deviceId lives ONLY in args, NOT top-level — the handler must derive it.
209
- const result = await handler({
210
- capName: 'stream-catalog',
211
- method: 'getCatalog',
212
- args: { deviceId: 7 },
213
- })
214
- expect(result).toEqual({ catalog: ['stream-a'] })
215
- expect(resolvedWithDeviceId).toBe(7)
216
- // Resolver served it via the derived deviceId — broker untouched.
217
- expect(broker.call).not.toHaveBeenCalled()
218
- })
219
-
220
- it('pinned broker fallback: resolver finds nothing → call is pinned to the owning node', async () => {
221
- const broker = hubBrokerFake()
222
-
223
- // Resolver always returns no-provider for this device-scoped cap.
224
- const resolver = {
225
- resolveCapRoute: (capName: string) => {
226
- throw new CapRouteError(capName, undefined, { reason: 'no-provider', rejected: [] })
227
- },
228
- dispatch: async () => {
229
- throw new Error('dispatch should not be reached')
230
- },
231
- }
232
-
233
- const owner: NodeNativeCapEntry = {
234
- nodeId: 'agent',
235
- addonId: 'reolink',
236
- capName: 'stream-catalog',
237
- deviceId: 7,
238
- }
239
- const handler = createParentUnownedCallHandler({
240
- getResolver: () => resolver as unknown as CapRouteResolver,
241
- broker,
242
- nodeRegistry: nodeRegistryFake({ 7: [owner] }),
243
- })
244
-
245
- await handler({ capName: 'stream-catalog', method: 'getCatalog', args: { deviceId: 7 } }).catch(
246
- () => undefined,
247
- )
248
-
249
- // The broker call is pinned to the owning node via call-opts `{ nodeID }`.
250
- const callArgs = (broker.call as ReturnType<typeof vi.fn>).mock.calls[0]
251
- expect(callArgs[2]).toEqual({ nodeID: 'agent' })
252
- })
253
-
254
- it('back-compat: no resolvable device-owner → broker call stays UNPINNED (load-balanced)', async () => {
255
- const broker = hubBrokerFake()
256
-
257
- const resolver = {
258
- resolveCapRoute: (capName: string) => {
259
- throw new CapRouteError(capName, undefined, { reason: 'no-provider', rejected: [] })
260
- },
261
- dispatch: async () => {
262
- throw new Error('dispatch should not be reached')
263
- },
264
- }
265
-
266
- const handler = createParentUnownedCallHandler({
267
- getResolver: () => resolver as unknown as CapRouteResolver,
268
- broker,
269
- nodeRegistry: nodeRegistryFake(), // no owners for any device
270
- })
271
-
272
- const result = await handler({ capName: 'system', method: 'info', args: undefined })
273
- expect(result).toEqual({ uptimeSec: 99, params: undefined })
274
- // Unpinned: third arg (call-opts) is undefined — today's behavior preserved.
275
- const callArgs = (broker.call as ReturnType<typeof vi.fn>).mock.calls[0]
276
- expect(callArgs[2]).toBeUndefined()
277
- })
278
-
279
- it('hub-local owner: device-native cap owned by a hub-local UDS child routes over UDS (broker untouched)', async () => {
280
- const broker = hubBrokerFake()
281
- // Resolver misses the device-scoped native cap (mirrors live behavior).
282
- const resolver = {
283
- resolveCapRoute: (capName: string) => {
284
- throw new CapRouteError(capName, undefined, { reason: 'no-provider', rejected: [] })
285
- },
286
- dispatch: async () => {
287
- throw new Error('dispatch should not be reached')
288
- },
289
- }
290
- const { dispatcher, resolveChildId, callCapOnChild } = localDispatcherFake({
291
- capName: 'stream-catalog',
292
- deviceId: 7,
293
- childId: 'child-reolink',
294
- })
295
-
296
- const handler = createParentUnownedCallHandler({
297
- getResolver: () => resolver as unknown as CapRouteResolver,
298
- broker,
299
- nodeRegistry: nodeRegistryFake(), // empty — hub-local child is NOT in HubNodeRegistry
300
- getLocalDispatcher: () => dispatcher,
301
- })
302
-
303
- const result = await handler({
304
- capName: 'stream-catalog',
305
- method: 'getCatalog',
306
- args: { deviceId: 7 },
307
- })
308
- expect(result).toEqual({ routedOverUds: 'child-reolink', capName: 'stream-catalog' })
309
- expect(resolveChildId).toHaveBeenCalledWith('stream-catalog', 7)
310
- expect(callCapOnChild).toHaveBeenCalledWith('child-reolink', {
311
- capName: 'stream-catalog',
312
- method: 'getCatalog',
313
- args: { deviceId: 7 },
314
- deviceId: 7,
315
- })
316
- expect(broker.call).not.toHaveBeenCalled()
317
- })
318
-
319
- it('remote owner: hub-local dispatcher misses → pinned broker call', async () => {
320
- const broker = hubBrokerFake()
321
- const resolver = {
322
- resolveCapRoute: (capName: string) => {
323
- throw new CapRouteError(capName, undefined, { reason: 'no-provider', rejected: [] })
324
- },
325
- dispatch: async () => {
326
- throw new Error('dispatch should not be reached')
327
- },
328
- }
329
- const { dispatcher, callCapOnChild } = localDispatcherFake() // no local owner
330
- const owner: NodeNativeCapEntry = {
331
- nodeId: 'agent',
332
- addonId: 'reolink',
333
- capName: 'stream-catalog',
334
- deviceId: 7,
335
- }
336
-
337
- const handler = createParentUnownedCallHandler({
338
- getResolver: () => resolver as unknown as CapRouteResolver,
339
- broker,
340
- nodeRegistry: nodeRegistryFake({ 7: [owner] }),
341
- getLocalDispatcher: () => dispatcher,
342
- })
343
-
344
- await handler({ capName: 'stream-catalog', method: 'getCatalog', args: { deviceId: 7 } }).catch(
345
- () => undefined,
346
- )
347
-
348
- expect(callCapOnChild).not.toHaveBeenCalled()
349
- const callArgs = (broker.call as ReturnType<typeof vi.fn>).mock.calls[0]
350
- expect(callArgs[2]).toEqual({ nodeID: 'agent' })
351
- })
352
-
353
- it('no local dispatcher (getter returns null): behaves as before — HubNodeRegistry → broker', async () => {
354
- const broker = hubBrokerFake()
355
- const resolver = {
356
- resolveCapRoute: (capName: string) => {
357
- throw new CapRouteError(capName, undefined, { reason: 'no-provider', rejected: [] })
358
- },
359
- dispatch: async () => {
360
- throw new Error('dispatch should not be reached')
361
- },
362
- }
363
- const owner: NodeNativeCapEntry = {
364
- nodeId: 'agent',
365
- addonId: 'reolink',
366
- capName: 'stream-catalog',
367
- deviceId: 7,
368
- }
369
-
370
- const handler = createParentUnownedCallHandler({
371
- getResolver: () => resolver as unknown as CapRouteResolver,
372
- broker,
373
- nodeRegistry: nodeRegistryFake({ 7: [owner] }),
374
- getLocalDispatcher: () => null,
375
- })
376
-
377
- await handler({ capName: 'stream-catalog', method: 'getCatalog', args: { deviceId: 7 } }).catch(
378
- () => undefined,
379
- )
380
- const callArgs = (broker.call as ReturnType<typeof vi.fn>).mock.calls[0]
381
- expect(callArgs[2]).toEqual({ nodeID: 'agent' })
382
- })
383
- })
@@ -1,273 +0,0 @@
1
- /**
2
- * MoleculerService.applyNodeManifest re-handshake idempotency (D3).
3
- *
4
- * Pre-existing bug: `applyNodeManifest` ran on EVERY `$hub.registerNode`
5
- * and called `registry.registerProvider(cap, key, proxy)` unconditionally
6
- * for every cap in the manifest. The D3 protocol legitimately re-handshakes
7
- * (a node sends its COMPLETE manifest again — e.g. the post-device-restore
8
- * `nativeCaps` re-handshake). `CapabilityRegistry.registerProvider` throws
9
- * on a duplicate `(cap, addonId)` pair (the guard is CORRECT — it catches an
10
- * addon double-`initialize()`), so the second handshake threw, the
11
- * registering node's retry loop retried forever, and the cluster entered a
12
- * registration storm.
13
- *
14
- * The fix makes `applyNodeManifest` diff-based: it honours the CLAUDE.md
15
- * invariant "`registerNode` replaces the node's entire cap set atomically".
16
- * A re-handshake with the SAME manifest is a no-op; a re-handshake that
17
- * drops a cap unregisters exactly that cap; a re-handshake that adds a cap
18
- * registers only the new one. No throw, no churn.
19
- *
20
- * These specs drive the genuine `MoleculerService` registration path twice
21
- * for the same `nodeId` against a REAL `CapabilityRegistry` (with its real
22
- * duplicate guard) and assert idempotency.
23
- */
24
- import { describe, it, expect, beforeEach } from 'vitest'
25
- import { z } from 'zod'
26
- import { CapabilityRegistry } from '@camstack/kernel'
27
- import type { RegisterNodeParams } from '@camstack/kernel'
28
- import type { CapabilityDefinition, IScopedLogger, SystemEvent } from '@camstack/types'
29
- import { MoleculerService } from '../core/moleculer/moleculer.service.js'
30
- import type { EventBusService } from '../core/events/event-bus.service.js'
31
- import type { ConfigService } from '../core/config/config.service.js'
32
- import type { LoggingService } from '../core/logging/logging.service.js'
33
- import type { CapabilityService } from '../core/capability/capability.service.js'
34
- import type { StreamProbeService } from '../core/streaming/stream-probe.service.js'
35
-
36
- /**
37
- * Test-only view of `MoleculerService` exposing:
38
- * - the genuine private registration entrypoint `onRegisterNode` — the
39
- * same closure the `$hub.registerNode` Moleculer action invokes in
40
- * production. Driving it directly exercises the real
41
- * `nodeRegistry.registerNode` + `applyNodeManifest` path without
42
- * standing up a TCP broker.
43
- * - the public `createCapabilityProxy` method used to assert whether a
44
- * dropped capability's call-routing entry was correctly removed from
45
- * `nodeCallFns` after a reduced re-handshake.
46
- */
47
- interface RegisterNodeDriver {
48
- onRegisterNode: (params: RegisterNodeParams) => void
49
- createCapabilityProxy: (
50
- capabilityName: string,
51
- nodeId: string,
52
- ) => Record<string, (params: unknown) => Promise<unknown>> | null
53
- }
54
-
55
- /** Build a harness whose declared caps are SINGLETON, for active-provider preference tests. */
56
- function createSingletonHarness(capNames: readonly string[]): Harness {
57
- return createHarness(capNames, 'singleton')
58
- }
59
-
60
- /** A minimal real `CapabilityDefinition` so `getDefinition`/`expandCapMethods` resolve. */
61
- function makeCapDef(
62
- name: string,
63
- mode: 'collection' | 'singleton' = 'collection',
64
- ): CapabilityDefinition {
65
- return {
66
- name,
67
- scope: 'system',
68
- mode,
69
- methods: {
70
- ping: {
71
- input: z.object({}),
72
- output: z.object({}),
73
- kind: 'query',
74
- auth: 'protected',
75
- },
76
- },
77
- }
78
- }
79
-
80
- function makeLogger(): IScopedLogger {
81
- const logger = {
82
- info: () => undefined,
83
- warn: () => undefined,
84
- error: () => undefined,
85
- debug: () => undefined,
86
- trace: () => undefined,
87
- fatal: () => undefined,
88
- child: (() => logger) as IScopedLogger['child'],
89
- }
90
- return logger as unknown as IScopedLogger
91
- }
92
-
93
- interface Harness {
94
- readonly driver: RegisterNodeDriver
95
- readonly registry: CapabilityRegistry
96
- readonly capNames: readonly string[]
97
- }
98
-
99
- /**
100
- * Build a real `MoleculerService` (constructor only — no broker start) wired
101
- * to a real `CapabilityRegistry` pre-declaring `capNames`. Returns the
102
- * service viewed through `RegisterNodeDriver` plus the registry to assert on.
103
- *
104
- * The broker is never started, so there is nothing to stop in teardown —
105
- * no `afterEach` teardown is needed.
106
- */
107
- function createHarness(
108
- capNames: readonly string[],
109
- mode: 'collection' | 'singleton' = 'collection',
110
- ): Harness {
111
- const registry = new CapabilityRegistry(makeLogger())
112
- for (const name of capNames) {
113
- registry.declareCapability(makeCapDef(name, mode))
114
- }
115
- // Boot-complete state — `getAllProviders` returns [] until `ready()`.
116
- registry.ready()
117
-
118
- const fakeEventBus = {
119
- emit: (_event: SystemEvent) => undefined,
120
- // `ReadinessRegistry` (built in the MoleculerService constructor)
121
- // subscribes to `system.ready-state` + `agent.offline`. A no-op
122
- // subscription returning an unsubscribe fn keeps the constructor happy.
123
- subscribe: () => () => undefined,
124
- getRecent: () => [],
125
- } as unknown as EventBusService
126
-
127
- const fakeConfig = {
128
- get: () => undefined,
129
- getAddonConfig: () => ({}),
130
- } as unknown as ConfigService
131
-
132
- const fakeLogging = {
133
- createLogger: () => makeLogger(),
134
- writeFromWorker: () => undefined,
135
- } as unknown as LoggingService
136
-
137
- const fakeCapability = {
138
- getRegistry: () => registry,
139
- } as unknown as CapabilityService
140
-
141
- const fakeStreamProbe = {} as unknown as StreamProbeService
142
-
143
- const service = new MoleculerService(
144
- fakeEventBus,
145
- fakeConfig,
146
- fakeLogging,
147
- fakeCapability,
148
- fakeStreamProbe,
149
- )
150
-
151
- return {
152
- driver: service as unknown as RegisterNodeDriver,
153
- registry,
154
- capNames,
155
- }
156
- }
157
-
158
- /** Provider count for a cap on the registry — counts every registered key. */
159
- function providerCount(registry: CapabilityRegistry, capName: string): number {
160
- return registry.getAllProviders(capName).length
161
- }
162
-
163
- describe('MoleculerService.applyNodeManifest — re-handshake idempotency', () => {
164
- let harness: Harness
165
-
166
- beforeEach(() => {
167
- harness = createHarness(['cap-alpha', 'cap-beta'])
168
- })
169
-
170
- it('a re-handshake with the IDENTICAL manifest does not throw and leaves each provider registered exactly once', () => {
171
- const manifest: RegisterNodeParams = {
172
- nodeId: 'hub/reolink',
173
- addons: [{ addonId: 'reolink', capabilities: ['cap-alpha', 'cap-beta'] }],
174
- }
175
-
176
- // First handshake.
177
- harness.driver.onRegisterNode(manifest)
178
- expect(providerCount(harness.registry, 'cap-alpha')).toBe(1)
179
- expect(providerCount(harness.registry, 'cap-beta')).toBe(1)
180
-
181
- // Re-handshake — the documented post-device-restore re-handshake.
182
- // BEFORE the fix this threw `provider already registered for capability`.
183
- expect(() => harness.driver.onRegisterNode(manifest)).not.toThrow()
184
-
185
- // No duplicate, no loss — exactly one provider per cap.
186
- expect(providerCount(harness.registry, 'cap-alpha')).toBe(1)
187
- expect(providerCount(harness.registry, 'cap-beta')).toBe(1)
188
- expect(harness.registry.getProviderByAddon('cap-alpha', 'reolink')).not.toBeNull()
189
- expect(harness.registry.getProviderByAddon('cap-beta', 'reolink')).not.toBeNull()
190
- })
191
-
192
- it('three handshakes survive without throwing (registration storm regression guard)', () => {
193
- const manifest: RegisterNodeParams = {
194
- nodeId: 'hub/reolink',
195
- addons: [{ addonId: 'reolink', capabilities: ['cap-alpha', 'cap-beta'] }],
196
- }
197
-
198
- expect(() => {
199
- harness.driver.onRegisterNode(manifest)
200
- harness.driver.onRegisterNode(manifest)
201
- harness.driver.onRegisterNode(manifest)
202
- }).not.toThrow()
203
-
204
- expect(providerCount(harness.registry, 'cap-alpha')).toBe(1)
205
- expect(providerCount(harness.registry, 'cap-beta')).toBe(1)
206
- })
207
-
208
- it('a re-handshake that DROPS one capability unregisters that provider while the others remain', () => {
209
- const fullManifest: RegisterNodeParams = {
210
- nodeId: 'hub/reolink',
211
- addons: [{ addonId: 'reolink', capabilities: ['cap-alpha', 'cap-beta'] }],
212
- }
213
- const reducedManifest: RegisterNodeParams = {
214
- nodeId: 'hub/reolink',
215
- addons: [{ addonId: 'reolink', capabilities: ['cap-alpha'] }],
216
- }
217
-
218
- // First: full manifest — both caps registered.
219
- harness.driver.onRegisterNode(fullManifest)
220
- expect(providerCount(harness.registry, 'cap-alpha')).toBe(1)
221
- expect(providerCount(harness.registry, 'cap-beta')).toBe(1)
222
-
223
- // Second: identical full manifest — still idempotent, no throw.
224
- expect(() => harness.driver.onRegisterNode(fullManifest)).not.toThrow()
225
- expect(providerCount(harness.registry, 'cap-alpha')).toBe(1)
226
- expect(providerCount(harness.registry, 'cap-beta')).toBe(1)
227
-
228
- // Third: reduced manifest — cap-beta dropped, cap-alpha kept.
229
- expect(() => harness.driver.onRegisterNode(reducedManifest)).not.toThrow()
230
- expect(providerCount(harness.registry, 'cap-alpha')).toBe(1)
231
- expect(providerCount(harness.registry, 'cap-beta')).toBe(0)
232
- expect(harness.registry.getProviderByAddon('cap-alpha', 'reolink')).not.toBeNull()
233
- expect(harness.registry.getProviderByAddon('cap-beta', 'reolink')).toBeNull()
234
-
235
- // Verify that the call-routing entry for the dropped cap was removed
236
- // from `nodeCallFns`. `createCapabilityProxy` is the public seam that
237
- // delegates to `findCallFn` internally — a null return means no entry
238
- // exists for that (nodeId, cap) pair, confirming the delete ran.
239
- expect(harness.driver.createCapabilityProxy('cap-beta', 'hub/reolink')).toBeNull()
240
- // The still-present cap must remain routable.
241
- expect(harness.driver.createCapabilityProxy('cap-alpha', 'hub/reolink')).not.toBeNull()
242
- })
243
- })
244
-
245
- describe('MoleculerService.applyNodeManifest — singleton local-first preference (UDS regression)', () => {
246
- // A `placement: 'any-node'` singleton (e.g. `pipeline-executor`) can register
247
- // on BOTH the hub-local forked child AND a remote agent. The hub must resolve
248
- // its OWN local provider (reachable over UDS) — never the agent's proxy, whose
249
- // callFn routes over Moleculer to a UDS-only agent runner that no longer hosts
250
- // the service ("not found on <agent>"). First-registered-wins picked the agent.
251
- const LOCAL = 'hub/detection-pipeline'
252
- const REMOTE = 'dev-agent-0/detection-pipeline'
253
- const singleton: (nodeId: string) => RegisterNodeParams = (nodeId) => ({
254
- nodeId,
255
- addons: [{ addonId: 'detection-pipeline', capabilities: ['pipeline-executor'] }],
256
- })
257
-
258
- it('prefers the hub-local provider when the REMOTE one registered first', () => {
259
- const { driver, registry } = createSingletonHarness(['pipeline-executor'])
260
- driver.onRegisterNode(singleton(REMOTE)) // agent first → would win under first-wins
261
- driver.onRegisterNode(singleton(LOCAL)) // hub-local second
262
- // Active provider must be the hub-local key (bare addonId, no '@').
263
- expect(registry.getSingletonAddonId('pipeline-executor')).toBe('detection-pipeline')
264
- expect(registry.getSingletonAddonId('pipeline-executor')).not.toContain('@')
265
- })
266
-
267
- it('keeps the hub-local provider active when a REMOTE one registers afterwards', () => {
268
- const { driver, registry } = createSingletonHarness(['pipeline-executor'])
269
- driver.onRegisterNode(singleton(LOCAL)) // hub-local first
270
- driver.onRegisterNode(singleton(REMOTE)) // agent second must NOT steal active
271
- expect(registry.getSingletonAddonId('pipeline-executor')).toBe('detection-pipeline')
272
- })
273
- })