@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,427 +0,0 @@
1
- /**
2
- * Task 5 — TDD tests: device-scoped native cap routing through CapRouteResolver.
3
- *
4
- * Covers:
5
- * 1. A device-scoped native cap on a HUB-LOCAL child resolves to hub-local-uds
6
- * and dispatches via callCapOnChild with {capName, method, args:{...deviceId}, deviceId}.
7
- * 2. d9ba709 regression guard: a singleton cap on BOTH a hub-local child AND a remote
8
- * node, resolved WITHOUT explicit nodeId, classifies to hub-local (not remote).
9
- * 3. A remote device-scoped native cap builds the ${addonId}.native-provider.${cap}.${method}
10
- * action (NATIVE_PROVIDER_SERVICE_INFIX).
11
- * 4. nodeKnowsCap returns true for native caps (not just manifest caps).
12
- */
13
-
14
- import { describe, it, expect, vi } from 'vitest'
15
- import { CapRouteResolver, NATIVE_PROVIDER_SERVICE_INFIX } from '@camstack/kernel'
16
- import type {
17
- NodeCapAuthority,
18
- HubLocalChildDispatcher,
19
- CapRouteResolverDeps,
20
- InProcessProviderRef,
21
- } from '@camstack/kernel'
22
- import { CapRouteError } from '@camstack/kernel'
23
-
24
- const HUB_NODE_ID = 'hub'
25
-
26
- // ---------------------------------------------------------------------------
27
- // Fake HubLocalChildDispatcher with deviceId-aware resolveChildId
28
- // ---------------------------------------------------------------------------
29
-
30
- interface FakeHubLocalRegistry extends HubLocalChildDispatcher {
31
- readonly callCapOnChildSpy: ReturnType<typeof vi.fn>
32
- }
33
-
34
- /**
35
- * Build a fake registry that supports device-scoped cap resolution.
36
- * The caps map is {capName → {deviceId? → childId}} or {capName → childId} for singletons.
37
- */
38
- function makeDeviceAwareHubLocalRegistry(
39
- caps: ReadonlyMap<string, ReadonlyMap<number | 'singleton', string>>,
40
- ): FakeHubLocalRegistry {
41
- const callCapOnChildSpy = vi.fn(async (_childId: string, _input: unknown) => ({
42
- ok: true,
43
- from: 'uds',
44
- }))
45
- return {
46
- resolveChildId: (capName: string, deviceId?: number): string | null => {
47
- const capMap = caps.get(capName)
48
- if (capMap === undefined) return null
49
- if (deviceId !== undefined) {
50
- const deviceSpecific = capMap.get(deviceId)
51
- if (deviceSpecific !== undefined) return deviceSpecific
52
- }
53
- return capMap.get('singleton') ?? null
54
- },
55
- callCapOnChild: callCapOnChildSpy,
56
- callCapOnChildSpy,
57
- }
58
- }
59
-
60
- // ---------------------------------------------------------------------------
61
- // Fake NodeCapAuthority with native-cap awareness
62
- // ---------------------------------------------------------------------------
63
-
64
- interface NativeCapSpec {
65
- readonly nodeId: string
66
- readonly addonId: string
67
- readonly capName: string
68
- readonly deviceId: number
69
- }
70
-
71
- function makeNativeAwareNodeAuthority(
72
- systemCaps: ReadonlyMap<string, { addonId: string; nodeId: string }>,
73
- onlineNodes: ReadonlySet<string>,
74
- nativeCaps: readonly NativeCapSpec[],
75
- ): NodeCapAuthority {
76
- return {
77
- nodeKnowsCap: (nodeId: string, capName: string): boolean => {
78
- // Check system (manifest) caps
79
- const sys = systemCaps.get(capName)
80
- if (sys !== undefined && sys.nodeId === nodeId) return true
81
- // Check native caps (any deviceId for this node+capName)
82
- return nativeCaps.some((n) => n.nodeId === nodeId && n.capName === capName)
83
- },
84
-
85
- nodeIsAgent: (nodeId: string): boolean => nodeId !== HUB_NODE_ID && !nodeId.includes('/'),
86
-
87
- nodeOnline: (nodeId: string): boolean => onlineNodes.has(nodeId),
88
-
89
- listNodeIds: (): readonly string[] => {
90
- const ids = new Set<string>()
91
- for (const spec of systemCaps.values()) ids.add(spec.nodeId)
92
- for (const n of nativeCaps) ids.add(n.nodeId)
93
- return [...ids]
94
- },
95
-
96
- getAddonId: (nodeId: string, capName: string): string | null => {
97
- // Check system (manifest) caps
98
- const sys = systemCaps.get(capName)
99
- if (sys !== undefined && sys.nodeId === nodeId) return sys.addonId
100
- // Check native caps
101
- const nat = nativeCaps.find((n) => n.nodeId === nodeId && n.capName === capName)
102
- return nat?.addonId ?? null
103
- },
104
-
105
- getAgentChildId: (_agentNodeId: string, _capName: string): string | null => null,
106
-
107
- isNativeCap: (nodeId: string, capName: string, deviceId?: number): boolean => {
108
- if (deviceId !== undefined) {
109
- return nativeCaps.some(
110
- (n) => n.nodeId === nodeId && n.capName === capName && n.deviceId === deviceId,
111
- )
112
- }
113
- return nativeCaps.some((n) => n.nodeId === nodeId && n.capName === capName)
114
- },
115
- }
116
- }
117
-
118
- // ---------------------------------------------------------------------------
119
- // Test 1: device-scoped native cap on hub-local child → hub-local-uds dispatch
120
- // ---------------------------------------------------------------------------
121
-
122
- describe('Task5 – device-scoped native cap on hub-local child', () => {
123
- it('resolves to hub-local-uds and calls callCapOnChild with {capName, method, args+deviceId, deviceId}', async () => {
124
- // ptz is a device-scoped native cap owned by hub child 'provider-reolink'
125
- const hubLocalCaps = makeDeviceAwareHubLocalRegistry(
126
- new Map([['ptz', new Map([[7, 'provider-reolink']])]]),
127
- )
128
-
129
- const nativeCaps: NativeCapSpec[] = [
130
- {
131
- nodeId: 'hub/provider-reolink',
132
- addonId: 'addon-provider-reolink',
133
- capName: 'ptz',
134
- deviceId: 7,
135
- },
136
- ]
137
- const nodeAuthority = makeNativeAwareNodeAuthority(
138
- new Map(),
139
- new Set(['hub/provider-reolink']),
140
- nativeCaps,
141
- )
142
-
143
- const deps: CapRouteResolverDeps = {
144
- hubNodeId: HUB_NODE_ID,
145
- broker: { call: vi.fn(), waitForServices: vi.fn() },
146
- hubLocalRegistry: hubLocalCaps,
147
- nodeAuthority,
148
- inProcessProviders: () => null,
149
- }
150
-
151
- const resolver = new CapRouteResolver(deps)
152
-
153
- // Resolve with deviceId = 7 so the snapshot uses the device-aware accessor
154
- const route = resolver.resolveCapRoute('ptz', { nodeId: HUB_NODE_ID, deviceId: 7 })
155
- expect(route.kind).toBe('hub-local-uds')
156
- if (route.kind !== 'hub-local-uds') return
157
- expect(route.childId).toBe('provider-reolink')
158
-
159
- const result = await resolver.dispatch(route, 'move', { pan: 10, deviceId: 7 })
160
- expect(result).toEqual({ ok: true, from: 'uds' })
161
-
162
- expect(hubLocalCaps.callCapOnChildSpy).toHaveBeenCalledOnce()
163
- const [calledChildId, calledInput] = hubLocalCaps.callCapOnChildSpy.mock.calls[0] as [
164
- string,
165
- unknown,
166
- ]
167
- expect(calledChildId).toBe('provider-reolink')
168
- expect(calledInput).toMatchObject({
169
- capName: 'ptz',
170
- method: 'move',
171
- args: { pan: 10, deviceId: 7 },
172
- deviceId: 7,
173
- })
174
- })
175
-
176
- it('resolving ptz WITHOUT deviceId does NOT match the device-scoped child (no spurious route)', () => {
177
- // When no deviceId hint is given, the singleton fallback fires — no deviceId-less descriptor exists
178
- const hubLocalCaps = makeDeviceAwareHubLocalRegistry(
179
- new Map([
180
- ['ptz', new Map([[7, 'provider-reolink']])],
181
- // No 'singleton' entry for ptz — ptz is strictly device-scoped
182
- ]),
183
- )
184
- const nodeAuthority = makeNativeAwareNodeAuthority(new Map(), new Set(), [])
185
-
186
- const deps: CapRouteResolverDeps = {
187
- hubNodeId: HUB_NODE_ID,
188
- broker: { call: vi.fn(), waitForServices: vi.fn() },
189
- hubLocalRegistry: hubLocalCaps,
190
- nodeAuthority,
191
- inProcessProviders: () => null,
192
- }
193
-
194
- const resolver = new CapRouteResolver(deps)
195
- // Without deviceId, no hub-local-uds route → CapRouteError
196
- expect(() => resolver.resolveCapRoute('ptz', { nodeId: HUB_NODE_ID })).toThrow(CapRouteError)
197
- })
198
- })
199
-
200
- // ---------------------------------------------------------------------------
201
- // Test 2: d9ba709 regression guard — hub-local singleton beats remote
202
- // ---------------------------------------------------------------------------
203
-
204
- describe('Task5 – d9ba709 regression: singleton prefers hub-local over remote', () => {
205
- it('singleton cap provided by both hub-local child AND remote node classifies to hub-local-uds (no nodeId given)', () => {
206
- // 'pipeline-executor' is on BOTH local child addon-detection-pipeline AND remote-agent-0
207
- const hubLocalCaps = makeDeviceAwareHubLocalRegistry(
208
- new Map([['pipeline-executor', new Map([['singleton', 'addon-detection-pipeline']])]]),
209
- )
210
-
211
- const systemCaps = new Map([
212
- ['pipeline-executor', { addonId: 'addon-detection-pipeline', nodeId: 'remote-agent-0' }],
213
- ])
214
- const nodeAuthority = makeNativeAwareNodeAuthority(
215
- systemCaps,
216
- new Set(['hub/addon-detection-pipeline', 'remote-agent-0']),
217
- [],
218
- )
219
-
220
- const deps: CapRouteResolverDeps = {
221
- hubNodeId: HUB_NODE_ID,
222
- broker: { call: vi.fn(), waitForServices: vi.fn() },
223
- hubLocalRegistry: hubLocalCaps,
224
- nodeAuthority,
225
- inProcessProviders: () => null,
226
- }
227
-
228
- const resolver = new CapRouteResolver(deps)
229
- // Singleton resolution (no nodeId) — must prefer hub-local-uds
230
- const route = resolver.resolveCapRoute('pipeline-executor', {})
231
- expect(route.kind).toBe('hub-local-uds')
232
- if (route.kind === 'hub-local-uds') {
233
- expect(route.childId).toBe('addon-detection-pipeline')
234
- }
235
- })
236
- })
237
-
238
- // ---------------------------------------------------------------------------
239
- // Test 3: remote device-scoped native cap uses NATIVE_PROVIDER_SERVICE_INFIX
240
- // (Remote-moleculer path: non-agent remote node hosting a native device cap,
241
- // e.g. a secondary hub or a directly-wired remote runner)
242
- // ---------------------------------------------------------------------------
243
-
244
- describe('Task5 – remote native cap uses NATIVE_PROVIDER_SERVICE_INFIX in action name', () => {
245
- it('dispatches remote native cap (on non-agent remote node) with action ${addonId}.native-provider.${cap}.${method}', async () => {
246
- const callSpy = vi.fn(async () => ({ ok: true }))
247
- const broker = { call: callSpy, waitForServices: vi.fn(async () => {}) }
248
-
249
- // ptz is provided by a REMOTE non-agent node (contains '/' so nodeIsAgent=false)
250
- const remoteNodeId = 'hub-secondary/provider-reolink'
251
- const nativeCaps: NativeCapSpec[] = [
252
- { nodeId: remoteNodeId, addonId: 'addon-provider-reolink', capName: 'ptz', deviceId: 7 },
253
- ]
254
- const nodeAuthority = makeNativeAwareNodeAuthority(
255
- new Map(),
256
- new Set([remoteNodeId]),
257
- nativeCaps,
258
- )
259
-
260
- const deps: CapRouteResolverDeps = {
261
- hubNodeId: HUB_NODE_ID,
262
- broker,
263
- hubLocalRegistry: null,
264
- nodeAuthority,
265
- inProcessProviders: () => null,
266
- }
267
-
268
- const resolver = new CapRouteResolver(deps)
269
- const route = resolver.resolveCapRoute('ptz', { nodeId: remoteNodeId, deviceId: 7 })
270
- expect(route.kind).toBe('remote-moleculer')
271
-
272
- await resolver.dispatch(route, 'move', { pan: 10, deviceId: 7 })
273
-
274
- expect(callSpy).toHaveBeenCalledOnce()
275
- const [action, , opts] = callSpy.mock.calls[0] as [string, unknown, { nodeID?: string }]
276
- expect(action).toBe(`addon-provider-reolink${NATIVE_PROVIDER_SERVICE_INFIX}.ptz.move`)
277
- expect((opts as { nodeID?: string }).nodeID).toBe(remoteNodeId)
278
- })
279
-
280
- it('non-native remote cap still uses plain ${addonId}.${cap}.${method} action', async () => {
281
- const callSpy = vi.fn(async () => ({ ok: true }))
282
- const broker = { call: callSpy, waitForServices: vi.fn(async () => {}) }
283
-
284
- const remoteNodeId = 'hub-secondary/addon-analytics-suite'
285
- const systemCaps = new Map([
286
- ['analytics', { addonId: 'addon-analytics-suite', nodeId: remoteNodeId }],
287
- ])
288
- const nodeAuthority = makeNativeAwareNodeAuthority(
289
- systemCaps,
290
- new Set([remoteNodeId]),
291
- [], // no native caps
292
- )
293
-
294
- const deps: CapRouteResolverDeps = {
295
- hubNodeId: HUB_NODE_ID,
296
- broker,
297
- hubLocalRegistry: null,
298
- nodeAuthority,
299
- inProcessProviders: () => null,
300
- }
301
-
302
- const resolver = new CapRouteResolver(deps)
303
- const route = resolver.resolveCapRoute('analytics', { nodeId: remoteNodeId })
304
- expect(route.kind).toBe('remote-moleculer')
305
-
306
- await resolver.dispatch(route, 'getReport', { period: 'day' })
307
-
308
- const [action] = callSpy.mock.calls[0] as [string]
309
- expect(action).toBe('addon-analytics-suite.analytics.getReport')
310
- // Must NOT have the native infix
311
- expect(action).not.toContain(NATIVE_PROVIDER_SERVICE_INFIX)
312
- })
313
- })
314
-
315
- // ---------------------------------------------------------------------------
316
- // Test 4: nodeKnowsCap returns true for native caps (not just manifest)
317
- // ---------------------------------------------------------------------------
318
-
319
- describe('Task5 – nodeKnowsCap includes native caps', () => {
320
- it('nodeKnowsCap returns true when the node has a native cap entry (even with no manifest cap)', () => {
321
- const nativeCaps: NativeCapSpec[] = [
322
- {
323
- nodeId: 'hub/provider-reolink',
324
- addonId: 'addon-provider-reolink',
325
- capName: 'ptz',
326
- deviceId: 7,
327
- },
328
- ]
329
- const nodeAuthority = makeNativeAwareNodeAuthority(
330
- new Map(), // no system caps
331
- new Set(['hub/provider-reolink']),
332
- nativeCaps,
333
- )
334
-
335
- expect(nodeAuthority.nodeKnowsCap('hub/provider-reolink', 'ptz')).toBe(true)
336
- expect(nodeAuthority.nodeKnowsCap('hub/provider-reolink', 'stream-broker')).toBe(false)
337
- expect(nodeAuthority.nodeKnowsCap('other-node', 'ptz')).toBe(false)
338
- })
339
- })
340
-
341
- // ---------------------------------------------------------------------------
342
- // Test 5: native-fallback regression — an in-hub WRAPPER must not shadow the
343
- // hub-local-uds NATIVE child for the same cap name.
344
- //
345
- // Reproduces the snapshot bug: the `snapshot` cap has an in-hub wrapper
346
- // (SnapshotAddon, registered as a system singleton, so inProcessProviders
347
- // returns a ref) AND a per-device native provider in a forked vendor child
348
- // (provider-reolink). `resolveCapRoute` gives Priority 1 to hub-in-process,
349
- // so it returns the WRAPPER route — never reaching the hub-local-uds native.
350
- // The setNativeFallback resolver wants the NATIVE child specifically; it must
351
- // use `resolveHubLocalUdsRoute`, which skips the in-process branch.
352
- // ---------------------------------------------------------------------------
353
-
354
- describe('native-fallback – wrapper must not shadow the hub-local native child', () => {
355
- // snapshot has an in-hub wrapper AND a forked native child for device 7.
356
- const wrapperRef: InProcessProviderRef = { invoke: vi.fn(async () => ({ from: 'wrapper' })) }
357
-
358
- function makeSnapshotResolver(): {
359
- resolver: CapRouteResolver
360
- hubLocalCaps: FakeHubLocalRegistry
361
- } {
362
- const hubLocalCaps = makeDeviceAwareHubLocalRegistry(
363
- new Map([['snapshot', new Map([[7, 'provider-reolink']])]]),
364
- )
365
- const nativeCaps: NativeCapSpec[] = [
366
- {
367
- nodeId: 'hub/provider-reolink',
368
- addonId: 'addon-provider-reolink',
369
- capName: 'snapshot',
370
- deviceId: 7,
371
- },
372
- ]
373
- const nodeAuthority = makeNativeAwareNodeAuthority(
374
- new Map(),
375
- new Set(['hub/provider-reolink']),
376
- nativeCaps,
377
- )
378
- const deps: CapRouteResolverDeps = {
379
- hubNodeId: HUB_NODE_ID,
380
- broker: { call: vi.fn(), waitForServices: vi.fn() },
381
- hubLocalRegistry: hubLocalCaps,
382
- nodeAuthority,
383
- // The wrapper occupies the in-hub slot for 'snapshot'.
384
- inProcessProviders: (cap: string): InProcessProviderRef | null =>
385
- cap === 'snapshot' ? wrapperRef : null,
386
- }
387
- return { resolver: new CapRouteResolver(deps), hubLocalCaps }
388
- }
389
-
390
- it('documents the shadow: resolveCapRoute picks the in-hub wrapper (hub-in-process)', () => {
391
- const { resolver } = makeSnapshotResolver()
392
- const route = resolver.resolveCapRoute('snapshot', { nodeId: HUB_NODE_ID, deviceId: 7 })
393
- // The wrapper shadows the native — this is the existing (correct for the
394
- // generic dispatch path) behaviour the native fallback must NOT rely on.
395
- expect(route.kind).toBe('hub-in-process')
396
- })
397
-
398
- it('resolveHubLocalUdsRoute returns the native child route, bypassing the wrapper shadow', () => {
399
- const { resolver } = makeSnapshotResolver()
400
- const route = resolver.resolveHubLocalUdsRoute('snapshot', 7)
401
- expect(route).not.toBeNull()
402
- expect(route?.kind).toBe('hub-local-uds')
403
- expect(route?.childId).toBe('provider-reolink')
404
- })
405
-
406
- it('resolveHubLocalUdsRoute returns null when no hub-local child owns the cap (lets fallback try remote)', () => {
407
- const { resolver } = makeSnapshotResolver()
408
- // device 999 has no hub-local native child
409
- expect(resolver.resolveHubLocalUdsRoute('snapshot', 999)).toBeNull()
410
- // a cap with neither wrapper nor hub-local child
411
- expect(resolver.resolveHubLocalUdsRoute('nonexistent', 7)).toBeNull()
412
- })
413
-
414
- it('dispatching the resolved hub-local-uds route reaches the native child (not the wrapper)', async () => {
415
- const { resolver, hubLocalCaps } = makeSnapshotResolver()
416
- const route = resolver.resolveHubLocalUdsRoute('snapshot', 7)
417
- expect(route).not.toBeNull()
418
- if (route === null) return
419
- const result = await resolver.dispatch(route, 'getSnapshot', { deviceId: 7 })
420
- expect(result).toEqual({ ok: true, from: 'uds' })
421
- expect(hubLocalCaps.callCapOnChildSpy).toHaveBeenCalledOnce()
422
- const [childId] = hubLocalCaps.callCapOnChildSpy.mock.calls[0] as [string]
423
- expect(childId).toBe('provider-reolink')
424
- // The wrapper's invoke must NOT have been called.
425
- expect(wrapperRef.invoke).not.toHaveBeenCalled()
426
- })
427
- })