@camstack/server 1.0.0 → 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,302 +0,0 @@
1
- /**
2
- * Unit tests for the two adapter factories and the resolver wired with them.
3
- *
4
- * Exercises:
5
- * - createNodeCapAuthority: delegates to HubNodeRegistry correctly
6
- * - createInProcessProviderLookup: invokes provider methods cast-free
7
- * - Resolver wired with both adapters:
8
- * hub-local UDS child cap resolves via callCapOnChild
9
- * hub-resident singleton resolves in-process (invoke called)
10
- * absent cap throws CapRouteError (NOT the old opaque string)
11
- *
12
- * We test the adapter factories directly + a resolver wired with them —
13
- * standing up a full MoleculerService is too heavy for a unit test, and
14
- * testing the adapters + resolver in isolation exercises the meaningful unit.
15
- */
16
-
17
- import { describe, it, expect, vi } from 'vitest'
18
- import { CapRouteError, CapRouteResolver } from '@camstack/kernel'
19
- import type {
20
- NodeCapAuthority,
21
- InProcessProviderLookup,
22
- HubLocalChildDispatcher,
23
- CapRouteResolverDeps,
24
- } from '@camstack/kernel'
25
- import type { InProcessProviderRef } from '@camstack/kernel'
26
- import {
27
- createNodeCapAuthority,
28
- createInProcessProviderLookup,
29
- } from '../core/moleculer/cap-route-authority.js'
30
-
31
- // ---------------------------------------------------------------------------
32
- // Helpers for stub HubNodeRegistry
33
- // ---------------------------------------------------------------------------
34
-
35
- interface StubNodeEntry {
36
- readonly addonId: string
37
- readonly capabilities: readonly string[]
38
- }
39
-
40
- function makeNodeRegistry(nodes: ReadonlyMap<string, readonly StubNodeEntry[]>) {
41
- return {
42
- getNodeManifest(nodeId: string) {
43
- return nodes.get(nodeId)
44
- },
45
- listNodeIds(): readonly string[] {
46
- return [...nodes.keys()]
47
- },
48
- }
49
- }
50
-
51
- // ---------------------------------------------------------------------------
52
- // Helpers for stub CapabilityService
53
- // ---------------------------------------------------------------------------
54
-
55
- function makeCapabilityService(providers: ReadonlyMap<string, Record<string, unknown>>) {
56
- return {
57
- getSingleton<T>(capability: string): T | null {
58
- return (providers.get(capability) as T) ?? null
59
- },
60
- }
61
- }
62
-
63
- // ---------------------------------------------------------------------------
64
- // createNodeCapAuthority
65
- // ---------------------------------------------------------------------------
66
-
67
- describe('createNodeCapAuthority', () => {
68
- const nodes = new Map([
69
- [
70
- 'hub/stream-broker',
71
- [{ addonId: 'addon-stream-broker', capabilities: ['stream-broker', 'stream-params'] }],
72
- ],
73
- ['dev-agent-0', [{ addonId: 'addon-detection-pipeline', capabilities: ['pipeline-executor'] }]],
74
- ])
75
- const registry = makeNodeRegistry(nodes)
76
- const authority = createNodeCapAuthority(registry)
77
-
78
- it('nodeKnowsCap returns true when the node manifest includes the cap', () => {
79
- expect(authority.nodeKnowsCap('hub/stream-broker', 'stream-broker')).toBe(true)
80
- expect(authority.nodeKnowsCap('hub/stream-broker', 'stream-params')).toBe(true)
81
- expect(authority.nodeKnowsCap('hub/stream-broker', 'ghost-cap')).toBe(false)
82
- })
83
-
84
- it('nodeKnowsCap returns false for unknown nodes', () => {
85
- expect(authority.nodeKnowsCap('not-a-node', 'stream-broker')).toBe(false)
86
- })
87
-
88
- it('getAddonId returns the addonId for a known cap', () => {
89
- expect(authority.getAddonId('hub/stream-broker', 'stream-broker')).toBe('addon-stream-broker')
90
- expect(authority.getAddonId('dev-agent-0', 'pipeline-executor')).toBe(
91
- 'addon-detection-pipeline',
92
- )
93
- })
94
-
95
- it('getAddonId returns null for missing nodes or caps', () => {
96
- expect(authority.getAddonId('not-a-node', 'stream-broker')).toBeNull()
97
- expect(authority.getAddonId('hub/stream-broker', 'ghost-cap')).toBeNull()
98
- })
99
-
100
- it('nodeIsAgent: hub and hub-children are NOT agents; bare-id non-hub nodes are agents', () => {
101
- expect(authority.nodeIsAgent('hub')).toBe(false)
102
- expect(authority.nodeIsAgent('hub/stream-broker')).toBe(false)
103
- expect(authority.nodeIsAgent('dev-agent-0')).toBe(true)
104
- expect(authority.nodeIsAgent('some-remote-agent')).toBe(true)
105
- })
106
-
107
- it('nodeOnline: nodes in the registry are online; absent ones are not', () => {
108
- expect(authority.nodeOnline('hub/stream-broker')).toBe(true)
109
- expect(authority.nodeOnline('dev-agent-0')).toBe(true)
110
- expect(authority.nodeOnline('not-registered')).toBe(false)
111
- })
112
-
113
- it('listNodeIds returns all registered node ids', () => {
114
- const ids = authority.listNodeIds()
115
- expect(ids).toContain('hub/stream-broker')
116
- expect(ids).toContain('dev-agent-0')
117
- expect(ids).toHaveLength(2)
118
- })
119
-
120
- it('getAgentChildId always returns null (hub cannot resolve; Task 6 handles it)', () => {
121
- expect(authority.getAgentChildId('dev-agent-0', 'pipeline-executor')).toBeNull()
122
- })
123
- })
124
-
125
- // ---------------------------------------------------------------------------
126
- // createNodeCapAuthority — per-node singleton override
127
- // ---------------------------------------------------------------------------
128
-
129
- describe('createNodeCapAuthority — per-node singleton override', () => {
130
- it('getAddonId honors the per-node singleton override when available', () => {
131
- const nodeRegistry = {
132
- getNodeManifest: (id: string) =>
133
- id === 'dev-agent-0'
134
- ? [
135
- { addonId: 'webrtc-native', capabilities: ['webrtc-session'] },
136
- { addonId: 'stream-broker', capabilities: ['webrtc-session'] },
137
- ]
138
- : undefined,
139
- listNodeIds: () => ['hub', 'dev-agent-0'],
140
- }
141
- const authority = createNodeCapAuthority(nodeRegistry, {
142
- resolveSingleton: (cap, nodeId) =>
143
- cap === 'webrtc-session' && nodeId === 'dev-agent-0' ? 'stream-broker' : null,
144
- })
145
- expect(authority.getAddonId('dev-agent-0', 'webrtc-session')).toBe('stream-broker')
146
- })
147
-
148
- it('getAddonId falls back to first manifest match without an override', () => {
149
- const nodeRegistry = {
150
- getNodeManifest: (id: string) =>
151
- id === 'dev-agent-0'
152
- ? [{ addonId: 'webrtc-native', capabilities: ['webrtc-session'] }]
153
- : undefined,
154
- listNodeIds: () => ['hub', 'dev-agent-0'],
155
- }
156
- const authority = createNodeCapAuthority(nodeRegistry, { resolveSingleton: () => null })
157
- expect(authority.getAddonId('dev-agent-0', 'webrtc-session')).toBe('webrtc-native')
158
- })
159
- })
160
-
161
- // ---------------------------------------------------------------------------
162
- // createInProcessProviderLookup
163
- // ---------------------------------------------------------------------------
164
-
165
- describe('createInProcessProviderLookup', () => {
166
- it('returns null for a cap not hosted in-process', () => {
167
- const lookup = createInProcessProviderLookup(makeCapabilityService(new Map()))
168
- expect(lookup('device-manager')).toBeNull()
169
- })
170
-
171
- it('returns an InProcessProviderRef whose invoke delegates to the provider method', async () => {
172
- const getStatusImpl = vi.fn().mockReturnValue({ ok: true })
173
- const providers = new Map<string, Record<string, unknown>>([
174
- ['device-manager', { getStatus: getStatusImpl }],
175
- ])
176
- const lookup = createInProcessProviderLookup(makeCapabilityService(providers))
177
-
178
- const ref = lookup('device-manager')
179
- expect(ref).not.toBeNull()
180
-
181
- const result = await ref!.invoke('getStatus', { deviceId: 1 })
182
- expect(result).toEqual({ ok: true })
183
- expect(getStatusImpl).toHaveBeenCalledOnce()
184
- expect(getStatusImpl).toHaveBeenCalledWith({ deviceId: 1 })
185
- })
186
-
187
- it('invoke throws a typed Error when the method is not a function (never casts)', async () => {
188
- const providers = new Map<string, Record<string, unknown>>([
189
- ['device-manager', { notAFn: 'some-string' }],
190
- ])
191
- const lookup = createInProcessProviderLookup(makeCapabilityService(providers))
192
- const ref = lookup('device-manager')
193
- expect(ref).not.toBeNull()
194
-
195
- await expect(ref!.invoke('notAFn', {})).rejects.toThrow(/method "notAFn" not found/)
196
- await expect(ref!.invoke('missingMethod', {})).rejects.toThrow(
197
- /method "missingMethod" not found/,
198
- )
199
- })
200
- })
201
-
202
- // ---------------------------------------------------------------------------
203
- // Resolver wired with both adapters
204
- // ---------------------------------------------------------------------------
205
-
206
- describe('Resolver + adapters — end-to-end dispatch', () => {
207
- const HUB_NODE_ID = 'hub'
208
-
209
- function makeCallCapOnChildSpy() {
210
- return vi.fn(async (_childId: string, _input: unknown) => ({ ok: true, from: 'uds' }))
211
- }
212
-
213
- function makeHubLocalRegistry(
214
- caps: ReadonlyMap<string, string>,
215
- ): HubLocalChildDispatcher & { callSpy: ReturnType<typeof vi.fn> } {
216
- const callSpy = makeCallCapOnChildSpy()
217
- return {
218
- resolveChildId: (capName: string) => caps.get(capName) ?? null,
219
- callCapOnChild: callSpy,
220
- callSpy,
221
- }
222
- }
223
-
224
- it('hub-local UDS child cap resolves via callCapOnChild', async () => {
225
- // Registry: stream-broker is served by a hub-local child.
226
- const nodes = new Map([
227
- ['hub/stream-broker', [{ addonId: 'addon-stream-broker', capabilities: ['stream-broker'] }]],
228
- ])
229
- const nodeRegistry = makeNodeRegistry(nodes)
230
- const authority = createNodeCapAuthority(nodeRegistry)
231
-
232
- const hubLocalCaps = new Map([['stream-broker', 'addon-stream-broker']])
233
- const hubLocalRegistry = makeHubLocalRegistry(hubLocalCaps)
234
-
235
- const lookup = createInProcessProviderLookup(makeCapabilityService(new Map()))
236
-
237
- const deps: CapRouteResolverDeps = {
238
- hubNodeId: HUB_NODE_ID,
239
- broker: { call: vi.fn(), waitForServices: vi.fn() },
240
- hubLocalRegistry,
241
- nodeAuthority: authority,
242
- inProcessProviders: lookup,
243
- }
244
-
245
- const resolver = new CapRouteResolver(deps)
246
- const route = resolver.resolveCapRoute('stream-broker', { nodeId: HUB_NODE_ID })
247
- expect(route.kind).toBe('hub-local-uds')
248
-
249
- const result = await resolver.dispatch(route, 'attachCamera', { deviceId: 5 })
250
- expect(result).toEqual({ ok: true, from: 'uds' })
251
- expect(hubLocalRegistry.callSpy).toHaveBeenCalledOnce()
252
- })
253
-
254
- it('hub-resident singleton resolves in-process via invoke', async () => {
255
- const invokeSpy = vi.fn().mockResolvedValue({ devices: [] })
256
- const providers = new Map<string, Record<string, unknown>>([
257
- ['device-manager', { listAll: invokeSpy }],
258
- ])
259
- const lookup = createInProcessProviderLookup(makeCapabilityService(providers))
260
-
261
- const deps: CapRouteResolverDeps = {
262
- hubNodeId: HUB_NODE_ID,
263
- broker: { call: vi.fn(), waitForServices: vi.fn() },
264
- hubLocalRegistry: null,
265
- nodeAuthority: createNodeCapAuthority(makeNodeRegistry(new Map())),
266
- inProcessProviders: lookup,
267
- }
268
-
269
- const resolver = new CapRouteResolver(deps)
270
- const route = resolver.resolveCapRoute('device-manager', { nodeId: HUB_NODE_ID })
271
- expect(route.kind).toBe('hub-in-process')
272
-
273
- const result = await resolver.dispatch(route, 'listAll', {})
274
- expect(result).toEqual({ devices: [] })
275
- expect(invokeSpy).toHaveBeenCalledOnce()
276
- })
277
-
278
- it('genuinely-absent cap throws CapRouteError, NOT the old opaque string', () => {
279
- const deps: CapRouteResolverDeps = {
280
- hubNodeId: HUB_NODE_ID,
281
- broker: { call: vi.fn(), waitForServices: vi.fn() },
282
- hubLocalRegistry: null,
283
- nodeAuthority: createNodeCapAuthority(makeNodeRegistry(new Map())),
284
- inProcessProviders: createInProcessProviderLookup(makeCapabilityService(new Map())),
285
- }
286
- const resolver = new CapRouteResolver(deps)
287
-
288
- let thrown: unknown
289
- try {
290
- resolver.resolveCapRoute('ghost-cap', {})
291
- } catch (e) {
292
- thrown = e
293
- }
294
-
295
- expect(thrown).toBeInstanceOf(CapRouteError)
296
- expect((thrown as CapRouteError).reason).toBe('no-provider')
297
- // Must NOT be the old opaque string
298
- expect((thrown as CapRouteError).message).not.toContain(
299
- 'Capability "ghost-cap" not available on node',
300
- )
301
- })
302
- })
@@ -1,199 +0,0 @@
1
- /**
2
- * Meta-test: ensures that every capability with methods has a corresponding
3
- * `<cap-name>.router.spec.ts` file in this directory. This prevents new caps
4
- * from being added without a router-level test.
5
- *
6
- * Caps listed in `ALLOWED_MISSING` are explicitly exempted (document why).
7
- */
8
- import { describe, it, expect } from 'vitest'
9
- import * as fs from 'node:fs'
10
- import * as path from 'node:path'
11
- import * as capsModule from '@camstack/types'
12
- import type { CapabilityDefinition } from '@camstack/types'
13
-
14
- /**
15
- * Caps that are knowingly untested at the router level.
16
- * Keep this list empty whenever possible — every entry is tech debt.
17
- */
18
- const ALLOWED_MISSING: ReadonlySet<string> = new Set<string>([
19
- // Populated during migration. Goal: drain to empty.
20
- ])
21
-
22
- function isCapabilityDefinition(value: unknown): value is CapabilityDefinition {
23
- if (value === null || typeof value !== 'object') return false
24
- const v = value as Record<string, unknown>
25
- return (
26
- typeof v['name'] === 'string' &&
27
- typeof v['scope'] === 'string' &&
28
- typeof v['mode'] === 'string' &&
29
- typeof v['methods'] === 'object' &&
30
- v['methods'] !== null
31
- )
32
- }
33
-
34
- function collectCapabilitiesWithMethods(): readonly CapabilityDefinition[] {
35
- const out: CapabilityDefinition[] = []
36
- for (const [key, value] of Object.entries(capsModule)) {
37
- if (!key.endsWith('Capability')) continue
38
- if (!isCapabilityDefinition(value)) continue
39
- if (Object.keys(value.methods).length === 0) continue
40
- out.push(value)
41
- }
42
- return out.toSorted((a, b) => a.name.localeCompare(b.name))
43
- }
44
-
45
- function specFileNameFor(capName: string): string {
46
- return `${capName}.router.spec.ts`
47
- }
48
-
49
- describe('cap-routers meta', () => {
50
- const caps = collectCapabilitiesWithMethods()
51
- const specDir = path.dirname(new URL(import.meta.url).pathname)
52
- const existingSpecs = new Set(
53
- fs.readdirSync(specDir).filter((f) => f.endsWith('.router.spec.ts')),
54
- )
55
-
56
- it('discovers at least one capability with methods', () => {
57
- expect(caps.length).toBeGreaterThan(0)
58
- })
59
-
60
- it('every capability has a <cap>.router.spec.ts (or is in ALLOWED_MISSING)', () => {
61
- const missing: string[] = []
62
- for (const cap of caps) {
63
- const expected = specFileNameFor(cap.name)
64
- if (!existingSpecs.has(expected) && !ALLOWED_MISSING.has(cap.name)) {
65
- missing.push(cap.name)
66
- }
67
- }
68
-
69
- // Soft-fail during migration: list missing but warn loudly.
70
- // Flip to `expect(missing).toEqual([])` once coverage is complete.
71
- if (missing.length > 0) {
72
- console.warn(
73
- `\n[cap-routers meta] ${missing.length} capabilities lack a router spec:\n - ${missing.join('\n - ')}\n`,
74
- )
75
- }
76
- // Hard gate: at least one real spec must exist so the harness stays exercised.
77
- expect(existingSpecs.size).toBeGreaterThanOrEqual(1)
78
- })
79
-
80
- it('ALLOWED_MISSING only references real capabilities', () => {
81
- const names = new Set(caps.map((c) => c.name))
82
- for (const name of ALLOWED_MISSING) {
83
- expect(names, `ALLOWED_MISSING references unknown cap "${name}"`).toContain(name)
84
- }
85
- })
86
-
87
- // ── Subscription codegen guard ──────────────────────────────────────
88
- //
89
- // Enumerates every capability method and, for each method marked
90
- // `kind: 'subscription'`, verifies that the generated cap-router file
91
- // includes the corresponding `.subscription(` wiring and the
92
- // `iterableSubscription` import. This catches codegen drift when new
93
- // subscription methods are added to caps.
94
- describe('subscription codegen', () => {
95
- const generatedPath = path.resolve(specDir, '../../api/trpc/generated-cap-routers.ts')
96
- const generatedSource = fs.existsSync(generatedPath)
97
- ? fs.readFileSync(generatedPath, 'utf-8')
98
- : null
99
-
100
- it('generated-cap-routers.ts exists', () => {
101
- expect(generatedSource, `expected generated file at ${generatedPath}`).not.toBeNull()
102
- })
103
-
104
- const subscriptionMethods: Array<{ capName: string; methodName: string }> = []
105
- for (const cap of caps) {
106
- for (const [methodName, schema] of Object.entries(cap.methods)) {
107
- if (schema.kind === 'subscription') {
108
- subscriptionMethods.push({ capName: cap.name, methodName })
109
- }
110
- }
111
- }
112
-
113
- it('if any cap has subscriptions, iterableSubscription is imported', () => {
114
- if (subscriptionMethods.length === 0 || generatedSource === null) return
115
- expect(
116
- generatedSource,
117
- 'generated-cap-routers.ts must import iterableSubscription when any cap has kind: "subscription"',
118
- ).toContain('iterableSubscription')
119
- })
120
-
121
- it('every subscription method is wired in the generated router', () => {
122
- if (generatedSource === null) return
123
- const missing: string[] = []
124
- for (const { capName, methodName } of subscriptionMethods) {
125
- // The generator emits `${methodName}: procedure\n .input(...)\n .subscription(`.
126
- // We check for the method-name line followed by `.subscription(` within a small window.
127
- const nameIdx = generatedSource.indexOf(`${methodName}:`)
128
- if (nameIdx === -1) {
129
- missing.push(`${capName}.${methodName} (method name not found)`)
130
- continue
131
- }
132
- const window = generatedSource.slice(nameIdx, nameIdx + 300)
133
- if (!window.includes('.subscription(')) {
134
- missing.push(`${capName}.${methodName} (no .subscription( call within 300 chars)`)
135
- }
136
- }
137
- expect(
138
- missing,
139
- `Subscription codegen drift — re-run: npx tsx scripts/generate-cap-routers.ts\n ${missing.join('\n ')}`,
140
- ).toEqual([])
141
- })
142
-
143
- it('reports the subscription method inventory (informational)', () => {
144
- // Not an assertion — this keeps the metric visible in test output.
145
- console.log(
146
- `[subscription codegen] ${subscriptionMethods.length} subscription method(s) across ${caps.length} caps`,
147
- )
148
- if (subscriptionMethods.length > 0) {
149
- for (const { capName, methodName } of subscriptionMethods) {
150
- console.log(` ${capName}.${methodName}`)
151
- }
152
- }
153
- })
154
- })
155
-
156
- // ── Output-validation codegen guard ────────────────────────────────
157
- //
158
- // Per device-proxy redesign §6.8 / Task 4.1, every non-subscription
159
- // procedure in the generated cap-router file must declare an `.output()`
160
- // call so that tRPC validates response shapes at runtime. Subscriptions
161
- // are exempt — tRPC v11 does not support `.output()` on subscriptions.
162
- describe('output-validation codegen', () => {
163
- const generatedPath = path.resolve(specDir, '../../api/trpc/generated-cap-routers.ts')
164
- const generatedSource = fs.existsSync(generatedPath)
165
- ? fs.readFileSync(generatedPath, 'utf-8')
166
- : null
167
-
168
- it('every non-subscription procedure declares .output()', () => {
169
- expect(generatedSource, `expected generated file at ${generatedPath}`).not.toBeNull()
170
- if (generatedSource === null) return
171
-
172
- // tRPC procedure call sites are spelled `.query(async` / `.mutation(async`
173
- // when they're emitted as router endpoints — the few in-body `p.query(...)`
174
- // / `p.mutation(...)` calls don't include `(async`, so this filter is exact.
175
- const procedureCallRegex = /\.(query|mutation)\(async/g
176
- const procedureCount = (generatedSource.match(procedureCallRegex) ?? []).length
177
- const outputCount = (generatedSource.match(/\.output\(/g) ?? []).length
178
-
179
- expect(
180
- outputCount,
181
- `output-validation codegen drift — expected at least ${procedureCount} .output() ` +
182
- `calls (one per query/mutation), found ${outputCount}. ` +
183
- `Re-run: npx tsx scripts/generate-cap-routers.ts`,
184
- ).toBeGreaterThanOrEqual(procedureCount)
185
- })
186
-
187
- it('reports the procedure / output-call inventory (informational)', () => {
188
- if (generatedSource === null) return
189
- const queryCount = (generatedSource.match(/\.query\(async/g) ?? []).length
190
- const mutationCount = (generatedSource.match(/\.mutation\(async/g) ?? []).length
191
- const subscriptionCount = (generatedSource.match(/\.subscription\(/g) ?? []).length
192
- const outputCount = (generatedSource.match(/\.output\(/g) ?? []).length
193
- console.log(
194
- `[output-validation codegen] queries=${queryCount} mutations=${mutationCount} ` +
195
- `subscriptions=${subscriptionCount} outputs=${outputCount}`,
196
- )
197
- })
198
- })
199
- })
@@ -1,115 +0,0 @@
1
- /**
2
- * Spec for the codegen'd `addon-settings` capability router.
3
- *
4
- * Exercises:
5
- * - All 4 methods (get/update × global/device) — `getAddonSettings` /
6
- * `updateAddonSettings` were collapsed into the global pair when the
7
- * three-level settings API was simplified.
8
- * - Auth enforcement (getters=protected, updaters=admin)
9
- * - Missing provider → PRECONDITION_FAILED
10
- * - Device settings routing with deviceId
11
- */
12
- import { describe, it, expect, vi } from 'vitest'
13
- import { createCapRouter_addonSettings } from '../../api/trpc/generated-cap-routers.js'
14
- import type { IAddonSettingsProvider } from '@camstack/types'
15
- import { makeCtx, invokeProcedure, checkAuthMatrix } from './harness.js'
16
-
17
- function makeMockProvider(): IAddonSettingsProvider {
18
- return {
19
- getGlobalSettings: vi.fn(async () => ({
20
- sections: [{ id: 'g', title: 'Global', fields: [] }],
21
- })),
22
- updateGlobalSettings: vi.fn(async () => ({ success: true as const })),
23
- getDeviceSettings: vi.fn(async () => ({
24
- sections: [{ id: 'd', title: 'Device', fields: [] }],
25
- })),
26
- updateDeviceSettings: vi.fn(async () => ({ success: true as const })),
27
- } as unknown as IAddonSettingsProvider
28
- }
29
-
30
- describe('addon-settings cap router', () => {
31
- it('getGlobalSettings returns schema', async () => {
32
- const provider = makeMockProvider()
33
- const router = createCapRouter_addonSettings(() => provider)
34
- const result = await invokeProcedure(router, 'getGlobalSettings', makeCtx('admin'), {
35
- addonId: 'test',
36
- })
37
- expect(result.ok).toBe(true)
38
- if (result.ok)
39
- expect(result.value).toEqual({ sections: [{ id: 'g', title: 'Global', fields: [] }] })
40
- expect(provider.getGlobalSettings).toHaveBeenCalledWith({ addonId: 'test' })
41
- })
42
-
43
- it('updateGlobalSettings returns success', async () => {
44
- const provider = makeMockProvider()
45
- const router = createCapRouter_addonSettings(() => provider)
46
- const result = await invokeProcedure(router, 'updateGlobalSettings', makeCtx('admin'), {
47
- addonId: 'test',
48
- patch: { volume: 50 },
49
- })
50
- expect(result.ok).toBe(true)
51
- if (result.ok) expect(result.value).toEqual({ success: true })
52
- })
53
-
54
- it('getDeviceSettings passes deviceId', async () => {
55
- const provider = makeMockProvider()
56
- const router = createCapRouter_addonSettings(() => provider)
57
- await invokeProcedure(router, 'getDeviceSettings', makeCtx('admin'), {
58
- addonId: 'test',
59
- deviceId: 1,
60
- })
61
- expect(provider.getDeviceSettings).toHaveBeenCalledWith({ addonId: 'test', deviceId: 1 })
62
- })
63
-
64
- it('updateDeviceSettings passes deviceId + patch', async () => {
65
- const provider = makeMockProvider()
66
- const router = createCapRouter_addonSettings(() => provider)
67
- await invokeProcedure(router, 'updateDeviceSettings', makeCtx('admin'), {
68
- addonId: 'test',
69
- deviceId: 1,
70
- patch: { enabled: false },
71
- })
72
- expect(provider.updateDeviceSettings).toHaveBeenCalledWith({
73
- addonId: 'test',
74
- deviceId: 1,
75
- patch: { enabled: false },
76
- })
77
- })
78
-
79
- it('returns PRECONDITION_FAILED when provider is null', async () => {
80
- const router = createCapRouter_addonSettings(() => null)
81
- const result = await invokeProcedure(router, 'getGlobalSettings', makeCtx('admin'), {
82
- addonId: 'x',
83
- })
84
- expect(result.ok).toBe(false)
85
- if (!result.ok) expect(result.code).toBe('PRECONDITION_FAILED')
86
- })
87
-
88
- it('getters enforce protected auth', async () => {
89
- const provider = makeMockProvider()
90
- const router = createCapRouter_addonSettings(() => provider)
91
- for (const method of ['getGlobalSettings', 'getDeviceSettings']) {
92
- const input = method.includes('Device') ? { addonId: 'x', deviceId: 1 } : { addonId: 'x' }
93
- const results = await checkAuthMatrix(router, method, 'protected', input)
94
- for (const r of results) {
95
- if (r.allowed) expect(r.outcome.ok).toBe(true)
96
- else expect(r.outcome.ok).toBe(false)
97
- }
98
- }
99
- })
100
-
101
- it('updaters enforce admin auth', async () => {
102
- const provider = makeMockProvider()
103
- const router = createCapRouter_addonSettings(() => provider)
104
- for (const method of ['updateGlobalSettings', 'updateDeviceSettings']) {
105
- const input = method.includes('Device')
106
- ? { addonId: 'x', deviceId: 1, patch: {} }
107
- : { addonId: 'x', patch: {} }
108
- const results = await checkAuthMatrix(router, method, 'admin', input)
109
- for (const r of results) {
110
- if (r.allowed) expect(r.outcome.ok).toBe(true)
111
- else expect(r.outcome.ok).toBe(false)
112
- }
113
- }
114
- })
115
- })