@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,53 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest'
2
- import { NetworkQualityService } from './network-quality.service'
3
-
4
- describe('NetworkQualityService', () => {
5
- let service: NetworkQualityService
6
-
7
- beforeEach(() => {
8
- service = new NetworkQualityService()
9
- })
10
-
11
- it('should return null for unknown device', () => {
12
- expect(service.getDeviceStats(999)).toBeNull()
13
- })
14
-
15
- it('should track stream bitrate with rolling average', () => {
16
- service.reportStreamStats(1, 'main', 8000)
17
- service.reportStreamStats(1, 'main', 10000)
18
- service.reportStreamStats(1, 'main', 9000)
19
-
20
- const stats = service.getDeviceStats(1)
21
- expect(stats).not.toBeNull()
22
- expect(stats!.streams['main']!.observedBitrateKbps).toBe(9000)
23
- expect(stats!.streams['main']!.peakBitrateKbps).toBe(10000)
24
- })
25
-
26
- it('should track packet loss', () => {
27
- service.reportStreamStats(1, 'main', 8000, 0.5)
28
- service.reportStreamStats(1, 'main', 8000, 1.5)
29
-
30
- const stats = service.getDeviceStats(1)
31
- expect(stats!.streams['main']!.packetLossPercent).toBe(1.0)
32
- })
33
-
34
- it('should track client stats', () => {
35
- service.reportClientStats(1, {
36
- rttMs: 50,
37
- jitterMs: 5,
38
- estimatedBandwidthKbps: 20000,
39
- packetLossPercent: 3,
40
- })
41
- const stats = service.getDeviceStats(1)
42
- expect(stats!.client?.rttMs).toBe(50)
43
- expect(stats!.client?.estimatedBandwidthKbps).toBe(20000)
44
- expect(stats!.client?.packetLossPercent).toBe(3)
45
- })
46
-
47
- it('should list all device stats', () => {
48
- service.reportStreamStats(1, 'main', 8000)
49
- service.reportStreamStats(2, 'sub', 2000)
50
- const all = service.getAllStats()
51
- expect(all).toHaveLength(2)
52
- })
53
- })
@@ -1,5 +0,0 @@
1
- import { NetworkQualityTracker } from '@camstack/core'
2
-
3
- export class NetworkQualityService extends NetworkQualityTracker {
4
- // NestJS DI wrapper — delegates entirely to core NetworkQualityTracker
5
- }
@@ -1,34 +0,0 @@
1
- import { NotificationService } from '@camstack/core'
2
- import type { Notification } from '@camstack/types'
3
- import { CapabilityService } from '../capability/capability.service'
4
- import { LoggingService } from '../logging/logging.service'
5
-
6
- /**
7
- * NestJS-injectable wrapper around the core NotificationService.
8
- *
9
- * Lazily creates the NotificationService on first use, wiring in the
10
- * CapabilityRegistry for proxy-based output resolution.
11
- */
12
- export class NotificationServiceWrapper {
13
- private _service: NotificationService | null = null
14
-
15
- constructor(
16
- private readonly caps: CapabilityService,
17
- private readonly logging: LoggingService,
18
- ) {}
19
-
20
- get service(): NotificationService {
21
- if (!this._service) {
22
- this._service = new NotificationService(this.logging.createLogger('notifications'))
23
- const registry = this.caps.getRegistry()
24
- if (registry) {
25
- this._service.setRegistry(registry)
26
- }
27
- }
28
- return this._service
29
- }
30
-
31
- async notify(notification: Notification): Promise<void> {
32
- return this.service.notify(notification)
33
- }
34
- }
@@ -1,27 +0,0 @@
1
- import { ToastService } from '@camstack/core'
2
- import type { Toast } from '@camstack/types'
3
-
4
- /**
5
- * NestJS-injectable wrapper around the core ToastService.
6
- *
7
- * Provides toast broadcasting to connected UI clients.
8
- */
9
- export class ToastServiceWrapper {
10
- private readonly _service = new ToastService()
11
-
12
- get service(): ToastService {
13
- return this._service
14
- }
15
-
16
- broadcast(toast: Toast): void {
17
- this._service.broadcast(toast)
18
- }
19
-
20
- sendToUser(userId: string, toast: Toast): void {
21
- this._service.sendToUser(userId, toast)
22
- }
23
-
24
- subscribe(connectionId: string, userId: string, callback: (toast: Toast) => void): () => void {
25
- return this._service.subscribe(connectionId, userId, callback)
26
- }
27
- }
@@ -1 +0,0 @@
1
- export const PROVIDER_MANAGER = Symbol('PROVIDER_MANAGER')
@@ -1,444 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return -- test file: mock typing crosses generic boundaries */
2
- /* eslint-disable @typescript-eslint/no-explicit-any */
3
- /**
4
- * ReplEngineService specs — exercises the actual current shape of the
5
- * REPL service, including the SystemManager warm-boot path that
6
- * recently hung the entire REPL when the broker tried to route
7
- * `live.onEvent` through the Moleculer service registry.
8
- *
9
- * Mocks at the AddonRegistry boundary (the dependency of the service)
10
- * — the rest is real: ReplEngine, SystemManager, EventBus adapter.
11
- */
12
- import { describe, it, expect, vi, beforeEach } from 'vitest'
13
- import { ReplEngineService } from './repl-engine.service'
14
- import { SystemEventBus } from '@camstack/core'
15
- import type { ReplSessionContext } from '@camstack/core'
16
- import type { DeviceBinding } from '@camstack/types'
17
- import { DeviceType, EventCategory } from '@camstack/types'
18
-
19
- // ── In-process broker api fixture ─────────────────────────────────────
20
- //
21
- // Mimics what `addonRegistry.getBrokerApi()` returns: a plain object
22
- // with `<cap>.<method>.{query|mutate}` shape that resolves locally.
23
- // The REPL service merges `live.onEvent` via the EventBus adapter on
24
- // top of this base.
25
-
26
- interface BrokerApiState {
27
- bindings: Map<number, DeviceBinding>
28
- snapshots: Record<string, Record<string, Record<string, unknown>>>
29
- devices: Map<
30
- number,
31
- {
32
- id: number
33
- stableId: string
34
- addonId: string
35
- type: DeviceType
36
- name: string
37
- parentDeviceId: number | null
38
- role: string | null
39
- online: boolean
40
- features: string[]
41
- isCamera: boolean
42
- config: Record<string, unknown>
43
- }
44
- >
45
- }
46
-
47
- function makeBrokerApi(state: BrokerApiState): unknown {
48
- return {
49
- deviceManager: {
50
- getAllBindings: {
51
- query: vi.fn(async () => Array.from(state.bindings.values())),
52
- },
53
- listAll: {
54
- query: vi.fn(async () => Array.from(state.devices.values())),
55
- },
56
- getBindings: {
57
- query: vi.fn(
58
- async ({ deviceId }: { deviceId: number }) =>
59
- state.bindings.get(deviceId) ?? { deviceId, entries: [] },
60
- ),
61
- },
62
- },
63
- deviceState: {
64
- getCapSlice: {
65
- query: vi.fn(
66
- async ({ deviceId, capName }: { deviceId: number; capName: string }) =>
67
- state.snapshots[String(deviceId)]?.[capName] ?? null,
68
- ),
69
- },
70
- getAllSnapshots: {
71
- query: vi.fn(async () => state.snapshots),
72
- },
73
- },
74
- }
75
- }
76
-
77
- // ── Harness ───────────────────────────────────────────────────────────
78
-
79
- interface Harness {
80
- service: ReplEngineService
81
- state: BrokerApiState
82
- eventBus: SystemEventBus
83
- /** Re-fetch the broker api spies for assertion. */
84
- brokerApi: any
85
- }
86
-
87
- function makeHarness(seed?: Partial<BrokerApiState>): Harness {
88
- // Reset the static SystemMirror singleton so each test gets a fresh
89
- // warm-boot. The service caches it in static class fields — accept
90
- // the test-only reach-in to keep tests isolated.
91
- ;(ReplEngineService as any).systemMirror = null
92
- ;(ReplEngineService as any).systemMirrorInit = null
93
-
94
- const state: BrokerApiState = {
95
- bindings: seed?.bindings ?? new Map(),
96
- snapshots: seed?.snapshots ?? {},
97
- devices: seed?.devices ?? new Map(),
98
- }
99
-
100
- const brokerApi = makeBrokerApi(state)
101
- const eventBus = new SystemEventBus(1000)
102
-
103
- // Real EventBusService stub — the service only calls .subscribe.
104
- const eventBusService = {
105
- subscribe: (filter: any, handler: any) => eventBus.subscribe(filter, handler),
106
- emit: (evt: any) => eventBus.emit(evt),
107
- getRecent: () => [],
108
- } as any
109
-
110
- const deviceRegistry = {
111
- getAll: () => Array.from(state.devices.values()),
112
- getById: (id: number) => state.devices.get(id) ?? null,
113
- getAllForAddon: (addonId: string) =>
114
- Array.from(state.devices.values()).filter((d) => d.addonId === addonId),
115
- getAllWithAddonId: () =>
116
- Array.from(state.devices.values()).map((d) => ({ addonId: d.addonId, device: d })),
117
- getAddonId: (id: number) => state.devices.get(id)?.addonId ?? null,
118
- }
119
-
120
- const integrationRegistry = {
121
- listIntegrations: vi.fn(async () => []),
122
- getIntegration: vi.fn(async () => null),
123
- }
124
-
125
- const addonRegistry = {
126
- getBrokerApi: () => brokerApi,
127
- getDeviceRegistry: () => deviceRegistry,
128
- getIntegrationRegistry: () => integrationRegistry,
129
- listAddons: () => [],
130
- } as any
131
-
132
- const loggingService = {
133
- createLogger: () => ({
134
- info: vi.fn(),
135
- child: vi.fn(),
136
- debug: vi.fn(),
137
- warn: vi.fn(),
138
- error: vi.fn(),
139
- }),
140
- } as any
141
-
142
- const service = new ReplEngineService(addonRegistry, eventBusService, loggingService)
143
- return { service, state, eventBus, brokerApi }
144
- }
145
-
146
- const mkBinding = (deviceId: number, capNames: string[]): DeviceBinding => ({
147
- deviceId,
148
- entries: capNames.map((capName) => ({
149
- capName,
150
- kind: 'native' as const,
151
- providerAddonId: 'test',
152
- providerNodeId: 'hub',
153
- nativeAddonId: 'test',
154
- })),
155
- })
156
-
157
- const mkDevice = (
158
- id: number,
159
- overrides: Partial<BrokerApiState['devices'] extends Map<number, infer V> ? V : never> = {},
160
- ): BrokerApiState['devices'] extends Map<number, infer V> ? V : never =>
161
- ({
162
- id,
163
- stableId: `stable-${id}`,
164
- addonId: 'addon-test',
165
- type: DeviceType.Camera,
166
- name: `Device ${id}`,
167
- parentDeviceId: null,
168
- role: null,
169
- online: true,
170
- features: [],
171
- isCamera: true,
172
- config: {},
173
- ...overrides,
174
- }) as any
175
-
176
- const SYSTEM_CTX: ReplSessionContext = { scope: { type: 'system' }, variables: {} }
177
-
178
- // ── Tests ─────────────────────────────────────────────────────────────
179
-
180
- describe('ReplEngineService — basic eval', () => {
181
- let h: Harness
182
- beforeEach(() => {
183
- h = makeHarness()
184
- })
185
-
186
- it('evaluates simple arithmetic expressions', async () => {
187
- const r = await h.service.execute('1 + 1', SYSTEM_CTX)
188
- expect(r.type).toBe('value')
189
- expect(r.output).toBe('2')
190
- })
191
-
192
- it('returns void for variable declarations', async () => {
193
- const r = await h.service.execute('const x = 42', SYSTEM_CTX)
194
- expect(r.type).toBe('void')
195
- })
196
-
197
- it('returns error type when user code throws', async () => {
198
- const r = await h.service.execute("throw new Error('boom')", SYSTEM_CTX)
199
- expect(r.type).toBe('error')
200
- expect(r.output).toContain('boom')
201
- })
202
-
203
- it('reports duration for every eval', async () => {
204
- const r = await h.service.execute('1 + 1', SYSTEM_CTX)
205
- expect(r.duration).toBeGreaterThanOrEqual(0)
206
- expect(r.duration).toBeLessThan(5_000)
207
- })
208
-
209
- it('isolates blocked globals — fetch / setTimeout / require unavailable', async () => {
210
- const fetchR = await h.service.execute('typeof fetch', SYSTEM_CTX)
211
- expect(fetchR.output).toBe("'undefined'")
212
- const reqR = await h.service.execute('typeof require', SYSTEM_CTX)
213
- expect(reqR.output).toBe("'undefined'")
214
- const setTimeoutR = await h.service.execute('typeof setTimeout', SYSTEM_CTX)
215
- expect(setTimeoutR.output).toBe("'undefined'")
216
- })
217
-
218
- it('exposes JSON / Math / Date as standard globals', async () => {
219
- const json = await h.service.execute('JSON.stringify({a:1})', SYSTEM_CTX)
220
- expect(json.output).toContain('"a":1')
221
- const math = await h.service.execute('Math.max(1, 5, 3)', SYSTEM_CTX)
222
- expect(math.output).toBe('5')
223
- })
224
- })
225
-
226
- describe('ReplEngineService — system scope sandbox', () => {
227
- let h: Harness
228
- beforeEach(() => {
229
- h = makeHarness({
230
- bindings: new Map([[1, mkBinding(1, ['battery'])]]),
231
- devices: new Map([[1, mkDevice(1, { name: 'Test Cam' })]]),
232
- snapshots: { '1': { battery: { sleeping: true, percentage: 50 } } },
233
- })
234
- })
235
-
236
- it('binds `sm` to the warm-booted SystemManager', async () => {
237
- const r = await h.service.execute('typeof sm', SYSTEM_CTX)
238
- expect(r.output).toBe("'object'")
239
- })
240
-
241
- it('sm.getDeviceById returns a typed proxy with sync state', async () => {
242
- const r = await h.service.execute(
243
- 'sm.getDeviceById(1).state.battery.value.sleeping',
244
- SYSTEM_CTX,
245
- )
246
- expect(r.type).toBe('value')
247
- expect(r.output).toBe('true')
248
- })
249
-
250
- it('sm.getDeviceByName resolves the metadata mirror', async () => {
251
- const r = await h.service.execute("sm.getDeviceByName('Test Cam').deviceId", SYSTEM_CTX)
252
- expect(r.output).toBe('1')
253
- })
254
-
255
- it('sm.summary returns counts', async () => {
256
- const r = await h.service.execute('sm.summary().totalDevices', SYSTEM_CTX)
257
- expect(r.output).toBe('1')
258
- })
259
-
260
- it('sm.query filters by addonId + caps + online', async () => {
261
- h.state.bindings.set(2, mkBinding(2, ['snapshot']))
262
- h.state.devices.set(2, mkDevice(2, { name: 'Cam 2', addonId: 'reolink', online: true }))
263
- h.state.devices.set(1, mkDevice(1, { name: 'Cam 1', addonId: 'reolink', online: false }))
264
- // Re-warm-boot the SM so the new devices show up.
265
- ;(ReplEngineService as any).systemMirror = null
266
- ;(ReplEngineService as any).systemMirrorInit = null
267
-
268
- const r = await h.service.execute(
269
- "sm.query({ addonId: 'reolink', online: true }).map(d => d.deviceId)",
270
- SYSTEM_CTX,
271
- )
272
- expect(r.output).toContain('2')
273
- expect(r.output).not.toContain('1,')
274
- })
275
-
276
- it('legacy variables remain accessible (backward-compat)', async () => {
277
- const r1 = await h.service.execute('typeof addonRegistry', SYSTEM_CTX)
278
- expect(r1.output).toBe("'object'")
279
- const r2 = await h.service.execute('typeof eventBus', SYSTEM_CTX)
280
- expect(r2.output).toBe("'object'")
281
- const r3 = await h.service.execute('typeof getDevice', SYSTEM_CTX)
282
- expect(r3.output).toBe("'function'")
283
- })
284
- })
285
-
286
- describe('ReplEngineService — device scope sandbox', () => {
287
- const DEVICE_CTX: ReplSessionContext = {
288
- scope: { type: 'device', deviceId: 8 },
289
- variables: {},
290
- }
291
-
292
- let h: Harness
293
- beforeEach(() => {
294
- h = makeHarness({
295
- bindings: new Map([[8, mkBinding(8, ['battery', 'snapshot'])]]),
296
- devices: new Map([[8, mkDevice(8, { name: 'Sala', addonId: 'reolink' })]]),
297
- snapshots: { '8': { battery: { sleeping: false, percentage: 88 } } },
298
- })
299
- })
300
-
301
- it('pre-binds `device` as a DeviceProxy with sync state', async () => {
302
- const r = await h.service.execute('device.state.battery.value.percentage', DEVICE_CTX)
303
- expect(r.type).toBe('value')
304
- expect(r.output).toBe('88')
305
- })
306
-
307
- it('exposes deviceId, info, sm, rawDevice', async () => {
308
- const a = await h.service.execute('deviceId', DEVICE_CTX)
309
- expect(a.output).toBe('8')
310
- const b = await h.service.execute('info.name', DEVICE_CTX)
311
- expect(b.output).toBe("'Sala'")
312
- const c = await h.service.execute('typeof sm', DEVICE_CTX)
313
- expect(c.output).toBe("'object'")
314
- const d = await h.service.execute('typeof rawDevice', DEVICE_CTX)
315
- // rawDevice is the raw IDevice from registry — null in our harness for device 8 (we use plain objects)
316
- // but the type check ensures the binding is in scope.
317
- expect(['undefined', "'object'"]).toContain(d.output)
318
- })
319
-
320
- it('device proxy exposes .state for every cap with runtimeState', async () => {
321
- const r = await h.service.execute('typeof device.state.battery.subscribe', DEVICE_CTX)
322
- expect(r.output).toBe("'function'")
323
- const r2 = await h.service.execute('typeof device.state.motion.value', DEVICE_CTX)
324
- expect(r2.output).toBe("'undefined'")
325
- })
326
-
327
- it('device proxy exposes cap method dispatchers', async () => {
328
- const r = await h.service.execute('typeof device.snapshot.getSnapshot', DEVICE_CTX)
329
- expect(r.output).toBe("'function'")
330
- })
331
- })
332
-
333
- describe('ReplEngineService — SystemManager warm-boot resilience', () => {
334
- it("warm-boot does NOT hang on `live.onEvent` (regression — broker can't route)", async () => {
335
- // The bug: `getBrokerApi().live.onEvent.subscribe(...)` polls
336
- // forever for a Moleculer service that doesn't exist. The fix
337
- // injects a direct EventBus adapter for `live` so SM init never
338
- // touches the broker for subscriptions.
339
- const h = makeHarness({
340
- bindings: new Map([[1, mkBinding(1, ['battery'])]]),
341
- devices: new Map([[1, mkDevice(1)]]),
342
- snapshots: {},
343
- })
344
-
345
- // 5 second hard ceiling — way above the 15s SM timeout but below
346
- // the broker's infinite poll. If the bug regresses this test
347
- // hangs the runner, not silently passes.
348
- const result = await Promise.race([
349
- h.service.execute('typeof sm', SYSTEM_CTX),
350
- new Promise<never>((_, reject) =>
351
- setTimeout(() => reject(new Error('test ceiling — REPL eval hung')), 5_000),
352
- ),
353
- ])
354
- expect(result.type).toBe('value')
355
- expect(result.output).toBe("'object'")
356
- })
357
-
358
- it('first eval triggers warm-boot; subsequent evals reuse the cached SystemManager', async () => {
359
- const h = makeHarness({
360
- bindings: new Map([[1, mkBinding(1, ['battery'])]]),
361
- devices: new Map([[1, mkDevice(1)]]),
362
- snapshots: {},
363
- })
364
-
365
- await h.service.execute('typeof sm', SYSTEM_CTX)
366
- await h.service.execute('typeof sm', SYSTEM_CTX)
367
- await h.service.execute('typeof sm', SYSTEM_CTX)
368
-
369
- expect(h.brokerApi.deviceManager.getAllBindings.query).toHaveBeenCalledTimes(1)
370
- expect(h.brokerApi.deviceState.getAllSnapshots.query).toHaveBeenCalledTimes(1)
371
- expect(h.brokerApi.deviceManager.listAll.query).toHaveBeenCalledTimes(1)
372
- })
373
-
374
- it('live.onEvent uses the in-process EventBus, not the broker', async () => {
375
- const h = makeHarness({
376
- bindings: new Map([[1, mkBinding(1, ['battery'])]]),
377
- devices: new Map([[1, mkDevice(1)]]),
378
- snapshots: { '1': { battery: { sleeping: true, percentage: 50 } } },
379
- })
380
-
381
- // Boot SM via first eval.
382
- await h.service.execute('typeof sm', SYSTEM_CTX)
383
-
384
- // Fire an in-process state-change event — SM mirror should pick it up.
385
- h.eventBus.emit({
386
- id: 'test-1',
387
- timestamp: Date.now(),
388
- category: EventCategory.DeviceStateChanged,
389
- source: { type: 'device', id: 1, deviceId: 1 },
390
- data: { deviceId: 1, capName: 'battery', slice: { sleeping: false, percentage: 90 } },
391
- })
392
- // Microtask flush.
393
- await new Promise((r) => setTimeout(r, 10))
394
-
395
- const r = await h.service.execute(
396
- 'sm.getDeviceById(1).state.battery.value.sleeping',
397
- SYSTEM_CTX,
398
- )
399
- expect(r.output).toBe('false')
400
- })
401
- })
402
-
403
- describe('ReplEngineService — error paths', () => {
404
- let h: Harness
405
- beforeEach(() => {
406
- h = makeHarness()
407
- })
408
-
409
- it('returns error when accessing undefined variables', async () => {
410
- const r = await h.service.execute('undefinedVariable.foo', SYSTEM_CTX)
411
- expect(r.type).toBe('error')
412
- })
413
-
414
- it('error output does not leak the engine internals', async () => {
415
- const r = await h.service.execute('throw new TypeError("user error")', SYSTEM_CTX)
416
- expect(r.type).toBe('error')
417
- expect(r.output).toContain('user error')
418
- // Should NOT contain stack frames from repl-engine.ts itself.
419
- expect(r.output).not.toContain('repl-engine.ts')
420
- })
421
- })
422
-
423
- describe('ReplEngineService — completions', () => {
424
- let h: Harness
425
- beforeEach(() => {
426
- h = makeHarness({
427
- bindings: new Map([[1, mkBinding(1, ['battery'])]]),
428
- devices: new Map([[1, mkDevice(1)]]),
429
- snapshots: {},
430
- })
431
- })
432
-
433
- it('returns sandbox keys when partial is empty', async () => {
434
- const completions = await h.service.getCompletions('', SYSTEM_CTX)
435
- expect(completions).toContain('sm')
436
- expect(completions).toContain('JSON')
437
- expect(completions).toContain('Math')
438
- })
439
-
440
- it('filters by partial prefix', async () => {
441
- const completions = await h.service.getCompletions('sm', SYSTEM_CTX)
442
- expect(completions).toContain('sm')
443
- })
444
- })