@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,280 +0,0 @@
1
- /**
2
- * device-settings-contribution-dispatch.spec.ts
3
- *
4
- * Regression tests for Bug-3: snapshot `getDeviceSettingsContribution` not
5
- * found on native provider.
6
- *
7
- * Root cause: device-manager.addon.ts used `getProviderForDevice` for the
8
- * three `exposesDeviceSettings` contribution methods. `getProviderForDevice`
9
- * returns the native first when present (e.g. Reolink/ONVIF); the native
10
- * only implements camera-specific operations (getSnapshot, invalidateCache),
11
- * NOT the contribution methods. The cross-process dispatch then throws
12
- * "native-provider: method 'getDeviceSettingsContribution' not found on
13
- * 'snapshot'".
14
- *
15
- * Fix: device-manager now uses `getProvider(capName)` — the active system
16
- * singleton (the wrapper, e.g. SnapshotAddon) — for all contribution method
17
- * calls. The native is never touched for these methods.
18
- *
19
- * Spec: docs/superpowers/plans/2026-05-21-addons-bulk-update-progress.md (Bug-3)
20
- */
21
- import { describe, it, expect, vi } from 'vitest'
22
- import { DeviceManagerAddon } from '@camstack/core'
23
- import { CapabilityRegistry, DeviceRegistry } from '@camstack/kernel'
24
- import { snapshotCapability } from '@camstack/types'
25
- import type {
26
- AddonContext,
27
- ConfigUISchemaWithValues,
28
- IScopedLogger,
29
- IEventBus,
30
- ProviderRegistration,
31
- IDeviceManagerProvider,
32
- } from '@camstack/types'
33
-
34
- // ── Fakes ────────────────────────────────────────────────────────────────────
35
-
36
- function makeLogger(): IScopedLogger {
37
- const l: IScopedLogger = {
38
- info: vi.fn(),
39
- warn: vi.fn(),
40
- error: vi.fn(),
41
- debug: vi.fn(),
42
- child: vi.fn(() => l),
43
- withTags: vi.fn(() => l),
44
- }
45
- return l
46
- }
47
-
48
- function makeEventBus(): IEventBus {
49
- return { emit: vi.fn(), subscribe: () => () => {}, getRecent: () => [] } as unknown as IEventBus
50
- }
51
-
52
- function makeSettings() {
53
- return {
54
- readAddonStore: vi.fn(async () => ({})),
55
- writeAddonStore: vi.fn(async () => {}),
56
- readDeviceStore: vi.fn(async () => ({})),
57
- writeDeviceStore: vi.fn(async () => {}),
58
- }
59
- }
60
-
61
- /**
62
- * Wrapper provider (SnapshotAddon equivalent): registered as a system
63
- * singleton via `registerProvider`. Implements ALL three contribution
64
- * methods plus the cap-specific operations (getSnapshot, invalidateCache).
65
- */
66
- function makeSnapshotWrapperProvider() {
67
- return {
68
- // Cap-specific methods
69
- getSnapshot: vi.fn(async () => null),
70
- invalidateCache: vi.fn(async () => undefined),
71
-
72
- // Contribution methods — owned by the wrapper
73
- getDeviceSettingsContribution: vi.fn(
74
- async (_input: { deviceId: number }): Promise<ConfigUISchemaWithValues | null> => ({
75
- sections: [
76
- {
77
- id: 'snapshot-settings',
78
- title: 'Snapshot',
79
- tab: 'general',
80
- order: 99,
81
- fields: [
82
- {
83
- type: 'text' as const,
84
- key: 'preferredStreamId',
85
- label: 'Preferred Stream',
86
- default: '',
87
- value: '',
88
- },
89
- ],
90
- },
91
- ],
92
- }),
93
- ),
94
- getDeviceLiveContribution: vi.fn(
95
- async (_input: { deviceId: number }): Promise<ConfigUISchemaWithValues | null> => null,
96
- ),
97
- applyDeviceSettingsPatch: vi.fn(async () => ({ success: true as const })),
98
- }
99
- }
100
-
101
- /**
102
- * Native provider (Reolink/ONVIF/Hikvision equivalent): registered per-device
103
- * via `registerNativeProvider`. Implements ONLY the camera-specific operations
104
- * (getSnapshot, invalidateCache). Does NOT implement contribution methods.
105
- *
106
- * Calling any of the three contribution methods on this object simulates the
107
- * "method not found" scenario that was Bug-3.
108
- */
109
- function makeSnapshotNativeProvider() {
110
- return {
111
- getSnapshot: vi.fn(async () => ({
112
- base64: 'abc123',
113
- contentType: 'image/jpeg',
114
- })),
115
- invalidateCache: vi.fn(async () => undefined),
116
- // Deliberately omit getDeviceSettingsContribution / getDeviceLiveContribution /
117
- // applyDeviceSettingsPatch — these don't exist on native providers.
118
- }
119
- }
120
-
121
- // ── Scenario setup ────────────────────────────────────────────────────────────
122
-
123
- async function setupBug3Scenario() {
124
- const addon = new DeviceManagerAddon()
125
- const settings = makeSettings()
126
- const deviceRegistry = new DeviceRegistry()
127
- const capabilityRegistry = new CapabilityRegistry(makeLogger())
128
-
129
- const wrapperProvider = makeSnapshotWrapperProvider()
130
- const nativeProvider = makeSnapshotNativeProvider()
131
-
132
- // Declare the snapshot capability (kind:'wrapper', defaultActive:true,
133
- // exposesDeviceSettings:true, mode:'singleton').
134
- capabilityRegistry.declareCapability(snapshotCapability)
135
-
136
- // Register the WRAPPER as the system singleton — this is what SnapshotAddon does.
137
- capabilityRegistry.registerProvider(snapshotCapability.name, 'snapshot-addon', wrapperProvider)
138
-
139
- // Register the default wrapper so auto-bind picks it up in getBindings step 2.
140
- capabilityRegistry.registerWrapper(snapshotCapability.name, 'snapshot-addon', {
141
- defaultActive: true,
142
- })
143
-
144
- const ctx = {
145
- id: 'device-manager',
146
- logger: makeLogger(),
147
- eventBus: makeEventBus(),
148
- addonConfig: {},
149
- dataDir: '/tmp',
150
- settings,
151
- kernel: { deviceRegistry, capabilityRegistry },
152
- }
153
-
154
- const initResult = await addon.initialize(ctx as unknown as AddonContext)
155
- let providers: readonly ProviderRegistration[] = []
156
- if (Array.isArray(initResult)) {
157
- providers = initResult as readonly ProviderRegistration[]
158
- } else if (initResult && typeof initResult === 'object' && 'providers' in initResult) {
159
- providers = (initResult.providers ?? []) as readonly ProviderRegistration[]
160
- }
161
- const provider = providers[0]!.provider as IDeviceManagerProvider
162
-
163
- // Allocate and register the device.
164
- const { id: deviceId } = await provider.allocateDeviceId({
165
- addonId: 'reolink-addon',
166
- stableId: 'cam-reolink-1',
167
- })
168
- await provider.registerDevice({
169
- addonId: 'reolink-addon',
170
- stableId: 'cam-reolink-1',
171
- id: deviceId,
172
- type: 'camera',
173
- name: 'Reolink Camera',
174
- parentDeviceId: null,
175
- config: {},
176
- })
177
-
178
- // Register the NATIVE provider for the device — this is what a Reolink/ONVIF
179
- // driver does. The native does NOT implement contribution methods.
180
- capabilityRegistry.registerNativeProvider(
181
- snapshotCapability.name,
182
- deviceId,
183
- 'reolink-addon',
184
- nativeProvider,
185
- )
186
-
187
- capabilityRegistry.ready()
188
-
189
- return { provider, wrapperProvider, nativeProvider, deviceId }
190
- }
191
-
192
- // ── Tests ────────────────────────────────────────────────────────────────────
193
-
194
- describe('device-manager contribution dispatch — Bug-3 regression (snapshot wrapper vs native)', () => {
195
- it('routes getDeviceSettingsContribution to the wrapper, never to the native', async () => {
196
- const { provider, wrapperProvider, nativeProvider, deviceId } = await setupBug3Scenario()
197
-
198
- // This MUST NOT throw "method not found on native" — the fix routes to the wrapper.
199
- const aggregate = await provider.getDeviceSettingsAggregate({ deviceId })
200
-
201
- expect(aggregate).not.toBeNull()
202
- // The wrapper's contribution section must be present.
203
- const snapshotSection = aggregate!.sections.find((s) => s.id === 'snapshot-settings')
204
- expect(snapshotSection).toBeDefined()
205
- expect(snapshotSection!.title).toBe('Snapshot')
206
-
207
- // Wrapper was called exactly once.
208
- expect(wrapperProvider.getDeviceSettingsContribution).toHaveBeenCalledTimes(1)
209
- expect(wrapperProvider.getDeviceSettingsContribution).toHaveBeenCalledWith({ deviceId })
210
-
211
- // Native was NOT called — it doesn't implement contribution methods.
212
- expect(nativeProvider.getSnapshot).not.toHaveBeenCalled()
213
- })
214
-
215
- it('routes getDeviceLiveContribution to the wrapper, never to the native', async () => {
216
- const { provider, wrapperProvider, nativeProvider, deviceId } = await setupBug3Scenario()
217
-
218
- // This MUST NOT throw "method not found on native".
219
- const liveAggregate = await provider.getDeviceLiveInfoAggregate({ deviceId })
220
-
221
- // The wrapper returns null for live contributions in our fake (acceptable).
222
- // Key assertion: the wrapper was invoked, and no error was thrown.
223
- expect(wrapperProvider.getDeviceLiveContribution).toHaveBeenCalledTimes(1)
224
- expect(wrapperProvider.getDeviceLiveContribution).toHaveBeenCalledWith({ deviceId })
225
-
226
- // Native was NOT called for live contributions.
227
- expect(nativeProvider.getSnapshot).not.toHaveBeenCalled()
228
- // liveAggregate may be null (wrapper returned null for live) — that's fine.
229
- // The important thing is no throw occurred.
230
- expect(() => liveAggregate).not.toThrow()
231
- })
232
-
233
- it('routes applyDeviceSettingsPatch to the wrapper, never to the native', async () => {
234
- const { provider, wrapperProvider, nativeProvider, deviceId } = await setupBug3Scenario()
235
-
236
- // First get the aggregate to confirm the contribution fields are tagged correctly.
237
- const aggregate = await provider.getDeviceSettingsAggregate({ deviceId })
238
- expect(aggregate).not.toBeNull()
239
-
240
- const field = aggregate!.sections
241
- .find((s) => s.id === 'snapshot-settings')
242
- ?.fields.find((f) => (f as Record<string, unknown>)['key'] === 'preferredStreamId') as
243
- | Record<string, unknown>
244
- | undefined
245
-
246
- expect(field).toBeDefined()
247
- expect(field!['writerCapName']).toBe('snapshot')
248
-
249
- // Now apply a settings patch — must go to the wrapper, NOT the native.
250
- await provider.updateDeviceField({
251
- deviceId,
252
- writerCapName: 'snapshot',
253
- writerAddonId: field!['writerAddonId'] as string,
254
- key: 'preferredStreamId',
255
- value: 'high',
256
- })
257
-
258
- // Wrapper's applyDeviceSettingsPatch was called with the correct patch.
259
- expect(wrapperProvider.applyDeviceSettingsPatch).toHaveBeenCalledWith({
260
- deviceId,
261
- patch: { preferredStreamId: 'high' },
262
- })
263
-
264
- // Native was NOT called for the settings write.
265
- expect(nativeProvider.getSnapshot).not.toHaveBeenCalled()
266
- })
267
-
268
- it('does not throw when both a native and a wrapper provider are registered for the same cap', async () => {
269
- // This is the exact Bug-3 scenario: native registered for device, wrapper
270
- // registered as system singleton. Before the fix, calling
271
- // getDeviceSettingsAggregate would throw because getProviderForDevice
272
- // returned the native which lacks contribution methods.
273
- const { provider, deviceId } = await setupBug3Scenario()
274
-
275
- // Must complete without throwing.
276
- await expect(provider.getDeviceSettingsAggregate({ deviceId })).resolves.not.toBeNull()
277
-
278
- await expect(provider.getDeviceLiveInfoAggregate({ deviceId })).resolves.toBeDefined()
279
- })
280
- })
@@ -1,125 +0,0 @@
1
- /**
2
- * Embedded dependencies E2E — verify ffmpeg/python resolution and download.
3
- *
4
- * These tests check the resolution logic (PATH detection, embedded binary check)
5
- * without actually downloading large binaries (unless CAMSTACK_TEST_DOWNLOAD=true).
6
- */
7
- import { describe, it, expect, beforeAll, afterAll } from 'vitest'
8
- import * as fs from 'node:fs'
9
- import * as path from 'node:path'
10
- import * as os from 'node:os'
11
- // Import directly from source submodules because vitest+swc doesn't resolve
12
- // `export * from './deps/index.js'` barrel re-exports in the @camstack/core index.
13
- import {
14
- findInPath,
15
- getPlatformInfo,
16
- buildBinaryPath,
17
- } from '../../../../packages/core/src/deps/binary-downloader'
18
- import {
19
- getFfmpegDownloadUrl,
20
- ensureFfmpeg,
21
- } from '../../../../packages/core/src/deps/ffmpeg-downloader'
22
- import {
23
- getPythonDownloadUrl,
24
- ensurePython,
25
- } from '../../../../packages/core/src/deps/python-downloader'
26
- import type { IScopedLogger } from '@camstack/types'
27
-
28
- function createMockLogger(): IScopedLogger {
29
- const logger: IScopedLogger = {
30
- info: () => {},
31
- warn: () => {},
32
- error: () => {},
33
- debug: () => {},
34
- child: () => createMockLogger(),
35
- }
36
- return logger
37
- }
38
-
39
- const mockLogger = createMockLogger()
40
-
41
- describe('Embedded Dependencies E2E', () => {
42
- let tmpDir: string
43
-
44
- beforeAll(() => {
45
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'camstack-deps-e2e-'))
46
- })
47
-
48
- afterAll(() => {
49
- fs.rmSync(tmpDir, { recursive: true, force: true })
50
- })
51
-
52
- describe('Platform detection', () => {
53
- it('returns valid platform and arch', () => {
54
- const info = getPlatformInfo()
55
- expect(['darwin', 'linux', 'win32']).toContain(info.platform)
56
- expect(['x64', 'arm64', 'arm', 'ia32']).toContain(info.arch)
57
- })
58
- })
59
-
60
- describe('Binary path resolution', () => {
61
- it('builds correct path for deps directory', () => {
62
- const p = buildBinaryPath('/data', 'ffmpeg')
63
- expect(p).toBe('/data/deps/ffmpeg')
64
- })
65
- })
66
-
67
- describe('FFmpeg', () => {
68
- it('generates valid download URL for current platform', () => {
69
- const url = getFfmpegDownloadUrl(process.platform, process.arch)
70
- expect(url).toMatch(/^https:\/\//)
71
- expect(url.length).toBeGreaterThan(20)
72
- })
73
-
74
- it('finds ffmpeg in PATH or reports not found', () => {
75
- const result = findInPath('ffmpeg')
76
- // Either found (returns the name) or null — both valid
77
- expect(result === null || typeof result === 'string').toBe(true)
78
- })
79
-
80
- // Only run download test if explicitly enabled (downloads ~80MB)
81
- const downloadTest = process.env.CAMSTACK_TEST_DOWNLOAD === 'true' ? it : it.skip
82
- downloadTest(
83
- 'downloads ffmpeg binary',
84
- async () => {
85
- const ffmpegPath = await ensureFfmpeg(tmpDir, mockLogger)
86
- expect(ffmpegPath).toBeTruthy()
87
- expect(fs.existsSync(ffmpegPath)).toBe(true)
88
- },
89
- 120000,
90
- )
91
- })
92
-
93
- describe('Python', () => {
94
- it('generates valid download URL for current platform', () => {
95
- const url = getPythonDownloadUrl(process.platform, process.arch)
96
- expect(url).toContain('python-headless')
97
- if (process.platform === 'darwin') {
98
- expect(url).toContain('universal2')
99
- }
100
- })
101
-
102
- it('finds python in PATH or reports not found', () => {
103
- const result = findInPath('python3') ?? findInPath('python')
104
- // Either found or null — both valid
105
- expect(result === null || typeof result === 'string').toBe(true)
106
- })
107
-
108
- // Only run download test if explicitly enabled (downloads ~25MB)
109
- const downloadTest = process.env.CAMSTACK_TEST_DOWNLOAD === 'true' ? it : it.skip
110
- downloadTest(
111
- 'downloads portable Python',
112
- async () => {
113
- const pythonPath = await ensurePython(tmpDir, mockLogger)
114
- expect(pythonPath).toBeTruthy()
115
- expect(fs.existsSync(pythonPath!)).toBe(true)
116
-
117
- // Verify it actually runs
118
- const { execFileSync } = await import('node:child_process')
119
- const version = execFileSync(pythonPath!, ['--version'], { encoding: 'utf8' }).trim()
120
- expect(version).toMatch(/Python 3\.12/)
121
- },
122
- 120000,
123
- )
124
- })
125
- })
@@ -1,75 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-unsafe-assignment -- test mock typing */
2
- import { describe, it, expect, vi } from 'vitest'
3
- import { createEventBusProxyRouter } from '../api/core/event-bus-proxy.router.js'
4
- import { makeCtx } from './cap-routers/harness.js'
5
- import type { IEventBus } from '@camstack/types'
6
-
7
- function createMockEventBus() {
8
- return {
9
- emit: vi.fn(),
10
- } as unknown as IEventBus & { emit: ReturnType<typeof vi.fn> }
11
- }
12
-
13
- describe('event-bus-proxy router', () => {
14
- it('emit calls eventBus.emit with the correct shape', async () => {
15
- const bus = createMockEventBus()
16
- const router = createEventBusProxyRouter(bus)
17
- const caller = router.createCaller(makeCtx('admin'))
18
-
19
- await caller.emit({
20
- id: 'evt-1',
21
- timestamp: '2026-01-15T10:00:00.000Z',
22
- source: { type: 'addon', id: 'my-addon' },
23
- // biome-ignore lint/plugin: synthetic test-only event category
24
- category: 'motion',
25
- data: { zone: 'front' },
26
- })
27
-
28
- expect(bus.emit).toHaveBeenCalledOnce()
29
- expect(bus.emit).toHaveBeenCalledWith({
30
- id: 'evt-1',
31
- timestamp: expect.any(Date),
32
- source: { type: 'addon', id: 'my-addon' },
33
- category: 'motion',
34
- data: { zone: 'front' },
35
- })
36
- })
37
-
38
- it('emit returns { ok: true }', async () => {
39
- const bus = createMockEventBus()
40
- const router = createEventBusProxyRouter(bus)
41
- const caller = router.createCaller(makeCtx('admin'))
42
-
43
- const result = await caller.emit({
44
- id: 'evt-2',
45
- timestamp: '2026-01-15T10:00:00.000Z',
46
- source: { type: 'device', id: 'cam-1' },
47
- // biome-ignore lint/plugin: synthetic test-only event category
48
- category: 'alert',
49
- data: {},
50
- })
51
-
52
- expect(result).toEqual({ ok: true })
53
- })
54
-
55
- it('timestamp string is converted to a Date object', async () => {
56
- const bus = createMockEventBus()
57
- const router = createEventBusProxyRouter(bus)
58
- const caller = router.createCaller(makeCtx('admin'))
59
-
60
- const isoString = '2026-06-20T14:30:00.000Z'
61
-
62
- await caller.emit({
63
- id: 'evt-3',
64
- timestamp: isoString,
65
- source: { type: 'addon', id: 'test' },
66
- // biome-ignore lint/plugin: synthetic test-only event category
67
- category: 'info',
68
- data: {},
69
- })
70
-
71
- const emittedEvent = bus.emit.mock.calls[0][0] as { timestamp: unknown }
72
- expect(emittedEvent.timestamp).toBeInstanceOf(Date)
73
- expect((emittedEvent.timestamp as Date).toISOString()).toBe(isoString)
74
- })
75
- })
@@ -1,37 +0,0 @@
1
- // server/backend/src/__tests__/fixtures/mock-analysis-addon-a.ts
2
- import type { ICamstackAddon, AddonDeclaration, AddonContext } from '@camstack/types'
3
-
4
- export class MockAnalysisAddonA implements ICamstackAddon {
5
- readonly manifest: AddonDeclaration = {
6
- id: 'mock-analysis-a',
7
- name: 'Mock Analysis A',
8
- version: '1.0.0',
9
- capabilities: ['object-detector'],
10
- }
11
-
12
- private initialized = false
13
- readonly provider = { id: 'analysis-a', processFrame: async () => [] }
14
-
15
- async initialize(_context: AddonContext) {
16
- this.initialized = true
17
- return [{ capability: 'object-detector', provider: this.provider }]
18
- }
19
-
20
- async shutdown(): Promise<void> {
21
- this.initialized = false
22
- }
23
-
24
- isInitialized(): boolean {
25
- return this.initialized
26
- }
27
-
28
- getConfigSchema() {
29
- return { sections: [] }
30
- }
31
-
32
- getConfig() {
33
- return {}
34
- }
35
-
36
- async onConfigChange() {}
37
- }
@@ -1,37 +0,0 @@
1
- // server/backend/src/__tests__/fixtures/mock-analysis-addon-b.ts
2
- import type { ICamstackAddon, AddonDeclaration, AddonContext } from '@camstack/types'
3
-
4
- export class MockAnalysisAddonB implements ICamstackAddon {
5
- readonly manifest: AddonDeclaration = {
6
- id: 'mock-analysis-b',
7
- name: 'Mock Analysis B',
8
- version: '1.0.0',
9
- capabilities: ['object-detector'],
10
- }
11
-
12
- private initialized = false
13
- readonly provider = { id: 'analysis-b', processFrame: async () => [] }
14
-
15
- async initialize(_context: AddonContext) {
16
- this.initialized = true
17
- return [{ capability: 'object-detector', provider: this.provider }]
18
- }
19
-
20
- async shutdown(): Promise<void> {
21
- this.initialized = false
22
- }
23
-
24
- isInitialized(): boolean {
25
- return this.initialized
26
- }
27
-
28
- getConfigSchema() {
29
- return { sections: [] }
30
- }
31
-
32
- getConfig() {
33
- return {}
34
- }
35
-
36
- async onConfigChange() {}
37
- }
@@ -1,37 +0,0 @@
1
- // server/backend/src/__tests__/fixtures/mock-log-addon.ts
2
- import type { ICamstackAddon, AddonDeclaration, AddonContext } from '@camstack/types'
3
-
4
- export class MockLogAddon implements ICamstackAddon {
5
- readonly manifest: AddonDeclaration = {
6
- id: 'mock-log-addon',
7
- name: 'Mock Log Destination',
8
- version: '1.0.0',
9
- capabilities: ['log-destination'],
10
- }
11
-
12
- private initialized = false
13
- readonly provider = { id: 'mock-log', write: async () => {} }
14
-
15
- async initialize(_context: AddonContext) {
16
- this.initialized = true
17
- return [{ capability: 'log-destination', provider: this.provider }]
18
- }
19
-
20
- async shutdown(): Promise<void> {
21
- this.initialized = false
22
- }
23
-
24
- isInitialized(): boolean {
25
- return this.initialized
26
- }
27
-
28
- getConfigSchema() {
29
- return { sections: [] }
30
- }
31
-
32
- getConfig() {
33
- return {}
34
- }
35
-
36
- async onConfigChange() {}
37
- }
@@ -1,40 +0,0 @@
1
- // server/backend/src/__tests__/fixtures/mock-storage-addon.ts
2
- import type { ICamstackAddon, AddonDeclaration, AddonContext } from '@camstack/types'
3
-
4
- export class MockStorageAddon implements ICamstackAddon {
5
- readonly manifest: AddonDeclaration = {
6
- id: 'mock-storage',
7
- name: 'Mock Storage',
8
- version: '1.0.0',
9
- capabilities: ['storage', 'settings-store'],
10
- }
11
-
12
- private initialized = false
13
- readonly provider = { id: 'mock-storage', type: 'mock' }
14
-
15
- async initialize(_context: AddonContext) {
16
- this.initialized = true
17
- return [
18
- { capability: 'storage', provider: this.provider },
19
- { capability: 'settings-store', provider: this.provider },
20
- ]
21
- }
22
-
23
- async shutdown(): Promise<void> {
24
- this.initialized = false
25
- }
26
-
27
- isInitialized(): boolean {
28
- return this.initialized
29
- }
30
-
31
- getConfigSchema() {
32
- return { sections: [] }
33
- }
34
-
35
- getConfig() {
36
- return {}
37
- }
38
-
39
- async onConfigChange() {}
40
- }