@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,177 +0,0 @@
1
- /**
2
- * Routing regression spec for the `broker` system-collection cap.
3
- *
4
- * The bug: `broker` is `scope:'system', mode:'collection'`. Two addons
5
- * register a `broker` provider, each owning a DISJOINT id set
6
- * (mqtt-broker owns `mqtt_*`, provider-homeassistant owns `ha_*`). The
7
- * generated collection mount only fans out the array-output methods
8
- * (`list` / `listProviders`); every id-keyed method (`get` / `getSettings`
9
- * / `remove` / `testConnection` / …) falls through to `providers[0]`
10
- * (the FIRST-registered provider = mqtt-broker). So operating on an HA
11
- * broker `ha_1` hit mqtt-broker → "broker 'ha_1' not found".
12
- *
13
- * The fix: each broker carries its owning `addonId`; the admin UI threads
14
- * it back as the `{ addonId }` system-collection selector so the call
15
- * routes to the OWNING provider via `getProviderByAddonId`. Providers
16
- * also return `null` (not throw) for ids they don't own, so the
17
- * no-addonId fallback degrades gracefully.
18
- *
19
- * This spec exercises `createCapRouter_broker` with the SAME selector
20
- * the generated mount builds (registry-backed: addonId → provider, else
21
- * first-provider aggregate with array methods fanned out). It registers
22
- * two mock providers and asserts:
23
- * - `list()` returns both, each tagged with its `addonId`.
24
- * - `get({id:'ha_1'}, addonId:'ha')` routes to the HA provider.
25
- * - `get({id:'ha_1'})` WITHOUT addonId returns null (graceful) — the
26
- * HA-owned id isn't in providers[0]'s registry, so it doesn't throw.
27
- * - `listProviders()` aggregates both providers' entries.
28
- */
29
- import { describe, it, expect } from 'vitest'
30
- import { createCapRouter_broker } from '../../api/trpc/generated-cap-routers.js'
31
- import { type IBrokerProvider, type BrokerInfo, type BrokerProviderInfo } from '@camstack/types'
32
- import { concatCollection } from '../../api/trpc/cap-mount-helpers.js'
33
- import { makeCtx, invokeProcedure } from './harness.js'
34
-
35
- /**
36
- * A self-contained mock `broker` provider that owns a fixed set of
37
- * broker ids and self-tags every returned record with its `addonId`.
38
- * Returns `null` / no-op for ids it does NOT own (never throws) — the
39
- * exact graceful-degradation contract the real providers must honour.
40
- */
41
- function makeProvider(addonId: string, ownedIds: readonly string[], kind: string): IBrokerProvider {
42
- const owns = (id: string): boolean => ownedIds.includes(id)
43
- const infoFor = (id: string): BrokerInfo => ({
44
- id,
45
- addonId,
46
- name: `${kind}-${id}`,
47
- kind,
48
- status: 'connected',
49
- info: {},
50
- lastCheckedAt: null,
51
- error: null,
52
- })
53
- return {
54
- list: async () => ownedIds.map(infoFor),
55
- get: async ({ id }) => (owns(id) ? infoFor(id) : null),
56
- listProviders: async (): Promise<BrokerProviderInfo[]> => [
57
- { addonId, kinds: [{ kind, label: kind }] },
58
- ],
59
- add: async () => ({ id: 'new' }),
60
- remove: async () => {
61
- // no-op for foreign ids (and owned ids in this mock)
62
- },
63
- testConnection: async ({ id }) =>
64
- owns(id) ? { ok: true, latencyMs: 1 } : { ok: false, error: 'unknown broker' },
65
- getSettings: async ({ id }) => (owns(id) ? { secret: id } : null),
66
- setSettings: async () => {
67
- // no-op
68
- },
69
- getBrokerConfig: async ({ id }) => (owns(id) ? { url: id } : null),
70
- getSettingsSchema: async ({ kind: k }) => (k === kind ? { form: kind } : null),
71
- testSettings: async () => ({ ok: true }),
72
- publish: async () => null,
73
- subscribe: async () => ({ subscriptionId: 's' }),
74
- unsubscribe: async () => {
75
- // no-op
76
- },
77
- getState: async () => null,
78
- getStatus: async () => ({ brokerCount: ownedIds.length, connectedCount: ownedIds.length }),
79
- }
80
- }
81
-
82
- /**
83
- * Mirror of the generated collection mount selector for `broker`
84
- * (`generated-cap-mounts.ts`): addonId → that provider directly; else a
85
- * first-provider aggregate whose array-output methods are
86
- * `concatCollection`-fanned. Two providers registered in order:
87
- * mqtt (owns `mqtt_1`) first, ha (owns `ha_1`) second.
88
- */
89
- function makeSelector(): (addonId?: string) => IBrokerProvider | null {
90
- const mqtt = makeProvider('mqtt', ['mqtt_1'], 'mqtt')
91
- const ha = makeProvider('ha', ['ha_1'], 'home-assistant')
92
- const byAddonId: Record<string, IBrokerProvider> = { mqtt, ha }
93
- const providers: readonly IBrokerProvider[] = [mqtt, ha]
94
- return (addonId?: string): IBrokerProvider | null => {
95
- if (addonId !== undefined) return byAddonId[addonId] ?? null
96
- const first = providers[0]!
97
- return {
98
- ...first,
99
- list: concatCollection(providers, 'list') as IBrokerProvider['list'],
100
- listProviders: concatCollection(
101
- providers,
102
- 'listProviders',
103
- ) as IBrokerProvider['listProviders'],
104
- }
105
- }
106
- }
107
-
108
- describe('broker cap — addonId ownership routing', () => {
109
- it('list() aggregates both providers, each broker tagged with its addonId', async () => {
110
- const selector = makeSelector()
111
- const router = createCapRouter_broker((_ctx, addonId) => selector(addonId))
112
-
113
- const outcome = await invokeProcedure(router, 'list', makeCtx('admin'), {})
114
-
115
- expect(outcome.ok).toBe(true)
116
- if (!outcome.ok) return
117
- const rows = outcome.value as BrokerInfo[]
118
- const byId = new Map(rows.map((r) => [r.id, r]))
119
- expect(byId.get('mqtt_1')?.addonId).toBe('mqtt')
120
- expect(byId.get('ha_1')?.addonId).toBe('ha')
121
- })
122
-
123
- it('get({id:ha_1}, addonId:ha) routes to the HA provider (not mqtt-broker)', async () => {
124
- const selector = makeSelector()
125
- const router = createCapRouter_broker((_ctx, addonId) => selector(addonId))
126
-
127
- const outcome = await invokeProcedure(router, 'get', makeCtx('admin'), {
128
- id: 'ha_1',
129
- addonId: 'ha',
130
- })
131
-
132
- expect(outcome.ok).toBe(true)
133
- if (!outcome.ok) return
134
- const row = outcome.value as BrokerInfo | null
135
- expect(row).not.toBeNull()
136
- expect(row?.id).toBe('ha_1')
137
- expect(row?.addonId).toBe('ha')
138
- })
139
-
140
- it('get({id:ha_1}) WITHOUT addonId returns null gracefully (first provider does not own it)', async () => {
141
- const selector = makeSelector()
142
- const router = createCapRouter_broker((_ctx, addonId) => selector(addonId))
143
-
144
- const outcome = await invokeProcedure(router, 'get', makeCtx('admin'), { id: 'ha_1' })
145
-
146
- expect(outcome.ok).toBe(true)
147
- if (!outcome.ok) return
148
- expect(outcome.value).toBeNull()
149
- })
150
-
151
- it('testConnection({id:ha_1}, addonId:ha) routes to HA provider and succeeds', async () => {
152
- const selector = makeSelector()
153
- const router = createCapRouter_broker((_ctx, addonId) => selector(addonId))
154
-
155
- const outcome = await invokeProcedure(router, 'testConnection', makeCtx('admin'), {
156
- id: 'ha_1',
157
- addonId: 'ha',
158
- })
159
-
160
- expect(outcome.ok).toBe(true)
161
- if (!outcome.ok) return
162
- expect(outcome.value).toEqual({ ok: true, latencyMs: 1 })
163
- })
164
-
165
- it('listProviders() aggregates both providers entries', async () => {
166
- const selector = makeSelector()
167
- const router = createCapRouter_broker((_ctx, addonId) => selector(addonId))
168
-
169
- const outcome = await invokeProcedure(router, 'listProviders', makeCtx('admin'))
170
-
171
- expect(outcome.ok).toBe(true)
172
- if (!outcome.ok) return
173
- const entries = outcome.value as BrokerProviderInfo[]
174
- const addonIds = entries.map((e) => e.addonId).toSorted()
175
- expect(addonIds).toEqual(['ha', 'mqtt'])
176
- })
177
- })
@@ -1,125 +0,0 @@
1
- /**
2
- * Unit tests for the cap-route-error-formatter.
3
- *
4
- * `formatTrpcError` is a pure function — no tRPC plumbing required.
5
- * We hand-craft minimal TRPCError-like / DefaultErrorShape-like objects
6
- * to verify the augmentation logic.
7
- */
8
- import { describe, it, expect } from 'vitest'
9
- import { TRPCError } from '@trpc/server'
10
- import { CapRouteError } from '@camstack/kernel'
11
- import { formatTrpcError } from '../../api/trpc/cap-route-error-formatter.js'
12
-
13
- // ── Helpers ──────────────────────────────────────────────────────────────────
14
-
15
- /** Minimal DefaultErrorShape stub. */
16
- function makeShape(
17
- overrides: Partial<{ message: string }> = {},
18
- ): Parameters<typeof formatTrpcError>[0]['shape'] {
19
- return {
20
- message: overrides.message ?? 'Something went wrong',
21
- code: -32603, // INTERNAL_SERVER_ERROR code number
22
- data: {
23
- code: 'INTERNAL_SERVER_ERROR',
24
- httpStatus: 500,
25
- },
26
- }
27
- }
28
-
29
- /** Build a TRPCError-like object that formatTrpcError accepts. */
30
- function makeTrpcError(
31
- message: string,
32
- cause?: Error,
33
- ): Parameters<typeof formatTrpcError>[0]['error'] {
34
- return new TRPCError({
35
- code: 'PRECONDITION_FAILED',
36
- message,
37
- cause,
38
- })
39
- }
40
-
41
- // ── Tests ─────────────────────────────────────────────────────────────────────
42
-
43
- describe('formatTrpcError', () => {
44
- it('returns shape unchanged when error is not a CapRouteError', () => {
45
- const shape = makeShape({ message: 'Boom' })
46
- const error = makeTrpcError('Some generic error')
47
- const result = formatTrpcError({ error, shape })
48
- expect(result).toStrictEqual(shape)
49
- expect(result.data).not.toHaveProperty('capRouteReason')
50
- })
51
-
52
- it('augments shape with capRouteReason when error.cause is a CapRouteError', () => {
53
- const capRouteErr = new CapRouteError('my-cap', undefined, {
54
- reason: 'no-provider',
55
- rejected: [{ kind: 'hub-in-process', why: 'no provider bound' }],
56
- })
57
- const shape = makeShape()
58
- const error = makeTrpcError('Capability "my-cap" provider not available', capRouteErr)
59
- const result = formatTrpcError({ error, shape })
60
-
61
- expect(result.data.capRouteReason).toBe('no-provider')
62
- expect(result.data.capRouteRejected).toStrictEqual([
63
- { kind: 'hub-in-process', why: 'no provider bound' },
64
- ])
65
- expect(result.data.capRouteNodeId).toBeUndefined()
66
- // Original shape fields preserved
67
- expect(result.message).toBe(shape.message)
68
- expect(result.code).toBe(shape.code)
69
- })
70
-
71
- it('augments shape with capRouteReason when error itself is a CapRouteError', () => {
72
- const capRouteErr = new CapRouteError('some-cap', undefined, {
73
- reason: 'node-offline',
74
- nodeId: 'node-abc',
75
- rejected: [{ kind: 'remote-moleculer', why: 'node node-abc is offline' }],
76
- })
77
- const shape = makeShape()
78
- // Wrap as TRPCError with the CapRouteError AS the cause
79
- const error = makeTrpcError('Transport failed', capRouteErr)
80
- const result = formatTrpcError({ error, shape })
81
-
82
- expect(result.data.capRouteReason).toBe('node-offline')
83
- expect(result.data.capRouteNodeId).toBe('node-abc')
84
- expect(result.data.capRouteRejected).toHaveLength(1)
85
- })
86
-
87
- it('surfaces transport-failed reason correctly (not absent provider)', () => {
88
- const capRouteErr = new CapRouteError('stream-params', 'getStatus', {
89
- reason: 'transport-failed',
90
- rejected: [{ kind: 'hub-local-uds', why: 'socket closed' }],
91
- })
92
- const shape = makeShape()
93
- const error = makeTrpcError('Transport failed', capRouteErr)
94
- const result = formatTrpcError({ error, shape })
95
-
96
- expect(result.data.capRouteReason).toBe('transport-failed')
97
- expect(result.data.capRouteRejected).toStrictEqual([
98
- { kind: 'hub-local-uds', why: 'socket closed' },
99
- ])
100
- })
101
-
102
- it('walks a nested cause chain to find a CapRouteError', () => {
103
- const capRouteErr = new CapRouteError('ptz', undefined, {
104
- reason: 'cap-unknown',
105
- rejected: [],
106
- })
107
- // Wrap two levels deep
108
- const innerError = new Error('dispatch failed', { cause: capRouteErr })
109
- const shape = makeShape()
110
- const error = makeTrpcError('Outer error', innerError)
111
- const result = formatTrpcError({ error, shape })
112
-
113
- expect(result.data.capRouteReason).toBe('cap-unknown')
114
- })
115
-
116
- it('returns shape unchanged for a plain Error (non-CapRouteError cause)', () => {
117
- const plainErr = new Error('plain cause')
118
- const shape = makeShape()
119
- const error = makeTrpcError('Wrapper', plainErr)
120
- const result = formatTrpcError({ error, shape })
121
-
122
- expect(result.data).not.toHaveProperty('capRouteReason')
123
- expect(result).toStrictEqual(shape)
124
- })
125
- })
@@ -1,68 +0,0 @@
1
- /**
2
- * Spec for the per-node singleton extension in capabilities.router.
3
- *
4
- * Exercises:
5
- * - setActiveSingleton with nodeId → calls registry with (cap, addonId, nodeId)
6
- * + persists `capabilities.singletonNode.<cap>.<nodeId>`
7
- * - setActiveSingleton without nodeId → persists `capabilities.singleton.<cap>` (global key)
8
- * - clearSingletonNodeOverride → calls registry.clearSingletonNodeOverride(cap, nodeId)
9
- * + persists the per-node key as null
10
- */
11
- import { describe, it, expect, vi } from 'vitest'
12
- import type { CapabilityRegistry } from '@camstack/kernel'
13
- import type { ConfigService } from '../../core/config/config.service.js'
14
- import { createCapabilitiesRouter } from '../../api/core/capabilities.router.js'
15
- import { makeCtx } from './harness.js'
16
-
17
- function harness() {
18
- const calls: { setActiveSingleton: unknown[][]; clear: unknown[][] } = {
19
- setActiveSingleton: [],
20
- clear: [],
21
- }
22
- const sets: Record<string, unknown> = {}
23
- const registry = {
24
- setActiveSingleton: vi.fn(async (...a: unknown[]) => {
25
- calls.setActiveSingleton.push(a)
26
- }),
27
- clearSingletonNodeOverride: vi.fn((...a: unknown[]) => {
28
- calls.clear.push(a)
29
- }),
30
- listCapabilities: () => [],
31
- } as unknown as CapabilityRegistry
32
- const config = {
33
- set: (k: string, v: unknown) => {
34
- sets[k] = v
35
- },
36
- } as unknown as ConfigService
37
- const router = createCapabilitiesRouter(registry, config)
38
- return { router, calls, sets, registry }
39
- }
40
-
41
- describe('capabilities.router — per-node singleton', () => {
42
- it('setActiveSingleton with nodeId persists the per-node key', async () => {
43
- const { router, calls, sets } = harness()
44
- const caller = router.createCaller(makeCtx('admin'))
45
- await caller.setActiveSingleton({
46
- capability: 'webrtc-session',
47
- addonId: 'webrtc-native',
48
- nodeId: 'dev-agent-0',
49
- })
50
- expect(calls.setActiveSingleton[0]).toEqual(['webrtc-session', 'webrtc-native', 'dev-agent-0'])
51
- expect(sets['capabilities.singletonNode.webrtc-session.dev-agent-0']).toBe('webrtc-native')
52
- })
53
-
54
- it('setActiveSingleton without nodeId persists the global key', async () => {
55
- const { router, sets } = harness()
56
- const caller = router.createCaller(makeCtx('admin'))
57
- await caller.setActiveSingleton({ capability: 'webrtc-session', addonId: 'stream-broker' })
58
- expect(sets['capabilities.singleton.webrtc-session']).toBe('stream-broker')
59
- })
60
-
61
- it('clearSingletonNodeOverride clears the per-node key', async () => {
62
- const { router, calls, sets } = harness()
63
- const caller = router.createCaller(makeCtx('admin'))
64
- await caller.clearSingletonNodeOverride({ capability: 'webrtc-session', nodeId: 'dev-agent-0' })
65
- expect(calls.clear[0]).toEqual(['webrtc-session', 'dev-agent-0'])
66
- expect(sets['capabilities.singletonNode.webrtc-session.dev-agent-0']).toBe(null)
67
- })
68
- })
@@ -1,137 +0,0 @@
1
- /**
2
- * Tests that `requireDeviceScoped` overlays `getStatus` results via
3
- * `device-manager`'s `resolveLinkedStatus` when the device has linked
4
- * properties for the requested cap.
5
- *
6
- * The overlay is transparent (null return = no-op) for the vast majority
7
- * of reads; only devices with active links for the cap get the merged
8
- * status back.
9
- */
10
- import { describe, it, expect, vi } from 'vitest'
11
- import type { CapabilityRegistry } from '@camstack/kernel'
12
- import { requireDeviceScoped } from '../../api/trpc/cap-mount-helpers.js'
13
-
14
- // ── Fake registry ────────────────────────────────────────────────────────────
15
-
16
- type ResolveLinkedStatus = (i: {
17
- deviceId: number
18
- cap: string
19
- baseStatus: unknown
20
- }) => Promise<Record<string, unknown> | null>
21
-
22
- interface FakeDeviceManager {
23
- resolveLinkedStatus: ResolveLinkedStatus
24
- }
25
-
26
- function makeRegistry(opts: {
27
- nativeGetStatus: () => Promise<Record<string, unknown>>
28
- nativeSetFanSpeed?: (i: unknown) => Promise<void>
29
- resolveLinkedStatus: ResolveLinkedStatus
30
- }): CapabilityRegistry {
31
- const nativeProvider = {
32
- getStatus: opts.nativeGetStatus,
33
- setFanSpeed: opts.nativeSetFanSpeed ?? vi.fn(async () => undefined),
34
- }
35
- const deviceManager: FakeDeviceManager = {
36
- resolveLinkedStatus: opts.resolveLinkedStatus,
37
- }
38
- return {
39
- getNativeProvider<T>(_capName: string, _deviceId: number): T | null {
40
- return nativeProvider as unknown as T
41
- },
42
- getSingleton<T>(capability: string): T | null {
43
- if (capability === 'device-manager') {
44
- return deviceManager as unknown as T
45
- }
46
- return null
47
- },
48
- // Minimal no-op stubs for the rest of the CapabilityRegistry surface
49
- // so TypeScript is satisfied without pulling in the real kernel class.
50
- listCapabilities: vi.fn(() => []),
51
- registerProvider: vi.fn(),
52
- unregisterProvider: vi.fn(),
53
- getCollection: vi.fn(() => []),
54
- getCollectionEntries: vi.fn(() => []),
55
- registerNativeProvider: vi.fn(),
56
- unregisterNativeProvider: vi.fn(),
57
- getProviderForDevice: vi.fn(() => null),
58
- getBindings: vi.fn(() => ({ entries: [] })),
59
- setActiveSingleton: vi.fn(),
60
- getSingletonAddonId: vi.fn(() => null),
61
- getAddonIdForProvider: vi.fn(() => null),
62
- on: vi.fn(),
63
- off: vi.fn(),
64
- dispose: vi.fn(),
65
- } as unknown as CapabilityRegistry
66
- }
67
-
68
- // ── Tests ────────────────────────────────────────────────────────────────────
69
-
70
- const DEVICE_ID = 668
71
- const CAP_NAME = 'vacuum-control' as Parameters<typeof requireDeviceScoped>[1]
72
-
73
- describe('requireDeviceScoped — getStatus overlay via resolveLinkedStatus', () => {
74
- it('returns the overlaid status when resolveLinkedStatus returns a non-null object', async () => {
75
- const base = { state: 'idle', battery: 80 }
76
- const overlaid = { state: 'paused', cleanWater: { status: 'low', level: null } }
77
-
78
- const registry = makeRegistry({
79
- nativeGetStatus: vi.fn(async () => base),
80
- resolveLinkedStatus: vi.fn(async () => overlaid),
81
- })
82
-
83
- const dispatcher = requireDeviceScoped(registry, CAP_NAME)
84
- expect(dispatcher).not.toBeNull()
85
-
86
- // Call via the Proxy — method is resolved lazily
87
- const result = await (
88
- dispatcher as unknown as { getStatus: (i: { deviceId: number }) => Promise<unknown> }
89
- ).getStatus({ deviceId: DEVICE_ID })
90
-
91
- expect(result).toEqual(overlaid)
92
- expect(result).not.toEqual(base)
93
- })
94
-
95
- it('returns the base provider result unchanged when resolveLinkedStatus returns null (no links)', async () => {
96
- const base = { state: 'cleaning', battery: 60 }
97
-
98
- const registry = makeRegistry({
99
- nativeGetStatus: vi.fn(async () => base),
100
- resolveLinkedStatus: vi.fn(async () => null),
101
- })
102
-
103
- const dispatcher = requireDeviceScoped(registry, CAP_NAME)
104
- expect(dispatcher).not.toBeNull()
105
-
106
- const result = await (
107
- dispatcher as unknown as { getStatus: (i: { deviceId: number }) => Promise<unknown> }
108
- ).getStatus({ deviceId: DEVICE_ID })
109
-
110
- expect(result).toEqual(base)
111
- })
112
-
113
- it('does NOT call resolveLinkedStatus for non-getStatus methods', async () => {
114
- const base = { state: 'idle', battery: 90 }
115
- const resolveLinkedStatus = vi.fn(async () => null)
116
- const nativeSetFanSpeed = vi.fn(async () => undefined)
117
-
118
- const registry = makeRegistry({
119
- nativeGetStatus: vi.fn(async () => base),
120
- nativeSetFanSpeed,
121
- resolveLinkedStatus,
122
- })
123
-
124
- const dispatcher = requireDeviceScoped(registry, CAP_NAME)
125
- expect(dispatcher).not.toBeNull()
126
-
127
- await (
128
- dispatcher as unknown as {
129
- setFanSpeed: (i: { deviceId: number; speed: string }) => Promise<void>
130
- }
131
- ).setFanSpeed({ deviceId: DEVICE_ID, speed: 'high' })
132
-
133
- expect(nativeSetFanSpeed).toHaveBeenCalledOnce()
134
- // The overlay path must NOT be consulted for mutations
135
- expect(resolveLinkedStatus).not.toHaveBeenCalled()
136
- })
137
- })