@camstack/server 0.1.8 → 0.2.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 (125) hide show
  1. package/package.json +9 -7
  2. package/src/__tests__/addon-install-e2e.test.ts +0 -1
  3. package/src/__tests__/addon-pages-e2e.test.ts +40 -18
  4. package/src/__tests__/addon-settings-router.spec.ts +6 -1
  5. package/src/__tests__/addon-upload.spec.ts +91 -29
  6. package/src/__tests__/agent-registry.spec.ts +26 -9
  7. package/src/__tests__/agent-status-page.spec.ts +1 -3
  8. package/src/__tests__/auth-session-cookie.test.ts +28 -1
  9. package/src/__tests__/bulk-update-coordinator.spec.ts +48 -31
  10. package/src/__tests__/cap-ownership-authority.spec.ts +39 -8
  11. package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +24 -4
  12. package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +17 -3
  13. package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +57 -11
  14. package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +64 -15
  15. package/src/__tests__/cap-providers-bulk-update.spec.ts +27 -7
  16. package/src/__tests__/cap-route-adapter.spec.ts +28 -15
  17. package/src/__tests__/cap-routers/_meta.spec.ts +6 -7
  18. package/src/__tests__/cap-routers/addon-settings.router.spec.ts +19 -10
  19. package/src/__tests__/cap-routers/broker-routing.router.spec.ts +14 -6
  20. package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +3 -1
  21. package/src/__tests__/cap-routers/capabilities-node.spec.ts +18 -5
  22. package/src/__tests__/cap-routers/device-link-overlay.spec.ts +11 -6
  23. package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +72 -20
  24. package/src/__tests__/cap-routers/harness.ts +11 -7
  25. package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +17 -3
  26. package/src/__tests__/cap-routers/null-provider-guard.spec.ts +5 -7
  27. package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +35 -11
  28. package/src/__tests__/cap-routers/settings-store.router.spec.ts +59 -15
  29. package/src/__tests__/capability-e2e.test.ts +9 -11
  30. package/src/__tests__/cli-e2e.test.ts +80 -59
  31. package/src/__tests__/core-cap-bridge.spec.ts +3 -1
  32. package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +12 -2
  33. package/src/__tests__/device-settings-contribution-dispatch.spec.ts +61 -30
  34. package/src/__tests__/embedded-deps-e2e.test.ts +35 -19
  35. package/src/__tests__/event-bus-proxy-router.spec.ts +3 -0
  36. package/src/__tests__/framework-allowlist.spec.ts +5 -4
  37. package/src/__tests__/https-e2e.test.ts +12 -6
  38. package/src/__tests__/lifecycle-e2e.test.ts +60 -11
  39. package/src/__tests__/live-events-subscription.spec.ts +17 -18
  40. package/src/__tests__/moleculer/uds-readiness.spec.ts +11 -4
  41. package/src/__tests__/moleculer/uds-topology.spec.ts +39 -11
  42. package/src/__tests__/moleculer/uds-unowned-call.spec.ts +71 -17
  43. package/src/__tests__/moleculer-register-node-idempotency.spec.ts +16 -7
  44. package/src/__tests__/native-cap-route.spec.ts +42 -19
  45. package/src/__tests__/oauth2-account-linking.spec.ts +63 -17
  46. package/src/__tests__/singleton-contention.test.ts +23 -11
  47. package/src/__tests__/streaming-diagnostic.test.ts +156 -53
  48. package/src/__tests__/streaming-scale.test.ts +69 -35
  49. package/src/__tests__/uds-addon-call-wiring.spec.ts +6 -1
  50. package/src/agent-status-page.ts +4 -3
  51. package/src/api/__tests__/addons-custom.spec.ts +22 -8
  52. package/src/api/__tests__/capabilities.router.test.ts +18 -9
  53. package/src/api/addon-upload.ts +46 -15
  54. package/src/api/addons-custom.router.ts +7 -6
  55. package/src/api/auth-whoami.ts +3 -1
  56. package/src/api/bridge-addons.router.ts +3 -1
  57. package/src/api/capabilities.router.ts +117 -78
  58. package/src/api/core/__tests__/auth-router-totp.spec.ts +57 -16
  59. package/src/api/core/addon-settings.router.ts +4 -1
  60. package/src/api/core/agents.router.ts +52 -53
  61. package/src/api/core/auth.router.ts +55 -36
  62. package/src/api/core/bulk-update-coordinator.ts +25 -22
  63. package/src/api/core/cap-providers.ts +346 -202
  64. package/src/api/core/capabilities.router.ts +30 -23
  65. package/src/api/core/hwaccel.router.ts +37 -10
  66. package/src/api/core/live-events.router.ts +16 -9
  67. package/src/api/core/logs.router.ts +54 -25
  68. package/src/api/core/notifications.router.ts +2 -1
  69. package/src/api/core/repl.router.ts +1 -3
  70. package/src/api/core/settings-backend.router.ts +68 -70
  71. package/src/api/core/system-events.router.ts +41 -32
  72. package/src/api/health/health.routes.ts +7 -13
  73. package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +12 -2
  74. package/src/api/oauth2/consent-page.ts +4 -3
  75. package/src/api/oauth2/oauth2-routes.ts +41 -12
  76. package/src/api/trpc/__tests__/scope-access-device.spec.ts +68 -23
  77. package/src/api/trpc/__tests__/scope-access.spec.ts +8 -13
  78. package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +10 -2
  79. package/src/api/trpc/cap-mount-helpers.ts +64 -55
  80. package/src/api/trpc/cap-route-error-formatter.ts +17 -9
  81. package/src/api/trpc/core-cap-bridge.ts +3 -1
  82. package/src/api/trpc/generated-cap-mounts.ts +593 -351
  83. package/src/api/trpc/generated-cap-routers.ts +3680 -579
  84. package/src/api/trpc/scope-access.ts +7 -7
  85. package/src/api/trpc/trpc.context.ts +7 -4
  86. package/src/api/trpc/trpc.middleware.ts +4 -2
  87. package/src/api/trpc/trpc.router.ts +79 -46
  88. package/src/auth/session-cookie.ts +10 -0
  89. package/src/boot/__tests__/integration-id-backfill.spec.ts +21 -6
  90. package/src/boot/boot-config.ts +103 -122
  91. package/src/boot/post-boot.service.ts +5 -3
  92. package/src/core/addon/__tests__/addon-registry-capability.test.ts +12 -3
  93. package/src/core/addon/addon-call-gateway.ts +20 -6
  94. package/src/core/addon/addon-package.service.ts +183 -89
  95. package/src/core/addon/addon-registry.service.ts +1163 -1305
  96. package/src/core/addon/addon-search.service.ts +2 -1
  97. package/src/core/addon/addon-settings-provider.ts +27 -7
  98. package/src/core/addon-bridge/addon-bridge.service.ts +11 -6
  99. package/src/core/addon-pages/addon-pages.service.ts +3 -1
  100. package/src/core/addon-widgets/addon-widgets.service.ts +5 -2
  101. package/src/core/agent/agent-registry.service.ts +60 -38
  102. package/src/core/auth/auth.service.spec.ts +6 -8
  103. package/src/core/config/config.service.spec.ts +1 -1
  104. package/src/core/events/event-bus.service.spec.ts +44 -21
  105. package/src/core/events/event-bus.service.ts +5 -1
  106. package/src/core/feature/feature.service.spec.ts +4 -1
  107. package/src/core/lifecycle/lifecycle-state-machine.spec.ts +8 -10
  108. package/src/core/logging/logging.service.spec.ts +61 -21
  109. package/src/core/logging/logging.service.ts +12 -3
  110. package/src/core/moleculer/cap-call-fn.spec.ts +17 -10
  111. package/src/core/moleculer/cap-call-fn.ts +5 -1
  112. package/src/core/moleculer/cap-route-authority.ts +18 -6
  113. package/src/core/moleculer/moleculer.service.ts +120 -32
  114. package/src/core/network/network-quality.service.spec.ts +6 -1
  115. package/src/core/notification/notification-wrapper.service.ts +1 -3
  116. package/src/core/notification/toast-wrapper.service.ts +1 -5
  117. package/src/core/repl/repl-engine.service.spec.ts +66 -39
  118. package/src/core/repl/repl-engine.service.ts +11 -12
  119. package/src/core/storage/storage-location-manager.spec.ts +12 -3
  120. package/src/core/streaming/stream-probe.service.ts +22 -13
  121. package/src/core/topology/topology-emitter.service.ts +5 -1
  122. package/src/launcher.ts +14 -9
  123. package/src/main.ts +602 -531
  124. package/src/manual-boot.ts +133 -154
  125. package/tsconfig.json +20 -8
@@ -62,7 +62,8 @@ export class AddonSearchService {
62
62
  }
63
63
 
64
64
  // npm registry v1 search: packages with BOTH "camstack" and "addon" keywords
65
- const url = 'https://registry.npmjs.org/-/v1/search?text=keywords:camstack+keywords:addon&size=250'
65
+ const url =
66
+ 'https://registry.npmjs.org/-/v1/search?text=keywords:camstack+keywords:addon&size=250'
66
67
 
67
68
  try {
68
69
  const response = await fetch(url, {
@@ -154,10 +154,15 @@ function createAddonSettingsProvider(deps: AddonSettingsProviderDeps): IAddonSet
154
154
  const result = await addon.getGlobalSettings(input.overlay, input.cap)
155
155
  return result ? reshapeForOutput(result) : null
156
156
  }
157
- return forkedGet(input.addonId, 'getGlobalSettings', {
158
- ...(input.overlay ? { overlay: input.overlay } : {}),
159
- ...(input.cap ? { cap: input.cap } : {}),
160
- }, input.nodeId)
157
+ return forkedGet(
158
+ input.addonId,
159
+ 'getGlobalSettings',
160
+ {
161
+ ...(input.overlay ? { overlay: input.overlay } : {}),
162
+ ...(input.cap ? { cap: input.cap } : {}),
163
+ },
164
+ input.nodeId,
165
+ )
161
166
  },
162
167
 
163
168
  async updateGlobalSettings(input: AddonPatchInput) {
@@ -169,7 +174,12 @@ function createAddonSettingsProvider(deps: AddonSettingsProviderDeps): IAddonSet
169
174
  await addon.updateGlobalSettings(input.patch)
170
175
  return { success: true as const }
171
176
  }
172
- return forkedUpdate(input.addonId, 'updateGlobalSettings', { patch: input.patch }, input.nodeId)
177
+ return forkedUpdate(
178
+ input.addonId,
179
+ 'updateGlobalSettings',
180
+ { patch: input.patch },
181
+ input.nodeId,
182
+ )
173
183
  },
174
184
 
175
185
  async getDeviceSettings(input: DeviceGetInput) {
@@ -179,7 +189,12 @@ function createAddonSettingsProvider(deps: AddonSettingsProviderDeps): IAddonSet
179
189
  const result = await addon.getDeviceSettings(input.deviceId)
180
190
  return result ? reshapeForOutput(result) : null
181
191
  }
182
- return forkedGet(input.addonId, 'getDeviceSettings', { deviceId: input.deviceId }, input.nodeId)
192
+ return forkedGet(
193
+ input.addonId,
194
+ 'getDeviceSettings',
195
+ { deviceId: input.deviceId },
196
+ input.nodeId,
197
+ )
183
198
  },
184
199
 
185
200
  async updateDeviceSettings(input: DeviceUpdateInput) {
@@ -191,7 +206,12 @@ function createAddonSettingsProvider(deps: AddonSettingsProviderDeps): IAddonSet
191
206
  await addon.updateDeviceSettings(input.deviceId, input.patch)
192
207
  return { success: true as const }
193
208
  }
194
- return forkedUpdate(input.addonId, 'updateDeviceSettings', { deviceId: input.deviceId, patch: input.patch }, input.nodeId)
209
+ return forkedUpdate(
210
+ input.addonId,
211
+ 'updateDeviceSettings',
212
+ { deviceId: input.deviceId, patch: input.patch },
213
+ input.nodeId,
214
+ )
195
215
  },
196
216
  }
197
217
  }
@@ -37,9 +37,7 @@ export class AddonBridgeService {
37
37
  /** Whether the bridge initialised successfully */
38
38
  private available = false
39
39
 
40
- constructor(
41
- private readonly loggingService: LoggingService,
42
- ) {
40
+ constructor(private readonly loggingService: LoggingService) {
43
41
  this.logger = this.loggingService.createLogger('AddonBridge')
44
42
 
45
43
  // Initialize installer eagerly in constructor (no async needed).
@@ -49,7 +47,10 @@ export class AddonBridgeService {
49
47
  const dataDir = process.env.CAMSTACK_DATA ?? 'camstack-data'
50
48
  const addonsDir = path.resolve(dataDir, 'addons')
51
49
  const workspacePackagesDir = kernel.detectWorkspacePackagesDir(__dirname)
52
- this.installer = new kernel.AddonInstaller({ addonsDir, workspacePackagesDir: workspacePackagesDir ?? undefined })
50
+ this.installer = new kernel.AddonInstaller({
51
+ addonsDir,
52
+ workspacePackagesDir: workspacePackagesDir ?? undefined,
53
+ })
53
54
  kernel.ensureDir(addonsDir)
54
55
  } catch (error: unknown) {
55
56
  const msg = errMsg(error)
@@ -70,10 +71,14 @@ export class AddonBridgeService {
70
71
  await this.loader.loadFromDirectory(addonsDir)
71
72
 
72
73
  this.available = true
73
- this.logger.info('Addon bridge initialized', { meta: { count: this.loader.listAddons().length } })
74
+ this.logger.info('Addon bridge initialized', {
75
+ meta: { count: this.loader.listAddons().length },
76
+ })
74
77
  } catch (error: unknown) {
75
78
  const msg = errMsg(error)
76
- this.logger.warn('Addon bridge loader failed — install/uninstall still available', { meta: { error: msg } })
79
+ this.logger.warn('Addon bridge loader failed — install/uninstall still available', {
80
+ meta: { error: msg },
81
+ })
77
82
  }
78
83
  }
79
84
 
@@ -47,7 +47,9 @@ export class AddonPagesService {
47
47
  const providers = this.caps.getCollection<IAddonPageProvider>('addon-pages-source')
48
48
  const isRegistered = providers.some((p) => p.id === addonId)
49
49
  if (!isRegistered) {
50
- this.logger.warn('Bundle resolve failed: addon not registered as page provider', { tags: { addonId } })
50
+ this.logger.warn('Bundle resolve failed: addon not registered as page provider', {
51
+ tags: { addonId },
52
+ })
51
53
  return null
52
54
  }
53
55
 
@@ -60,10 +60,13 @@ export class AddonWidgetsService {
60
60
  // the bare manifest id, so the route always carries the bare
61
61
  // `addonId` — match it against each registry key with the
62
62
  // `@<nodeId>` suffix stripped.
63
- const entries = this.caps.getCollectionEntries<IAddonWidgetsSourceProvider>('addon-widgets-source')
63
+ const entries =
64
+ this.caps.getCollectionEntries<IAddonWidgetsSourceProvider>('addon-widgets-source')
64
65
  const isRegistered = entries.some(([id]) => stripNodeSuffix(id) === addonId)
65
66
  if (!isRegistered) {
66
- this.logger.warn('Bundle resolve failed: addon not registered as widget provider', { tags: { addonId } })
67
+ this.logger.warn('Bundle resolve failed: addon not registered as widget provider', {
68
+ tags: { addonId },
69
+ })
67
70
  return null
68
71
  }
69
72
 
@@ -181,9 +181,7 @@ export class AgentRegistryService {
181
181
  | { getNodeList?: (opts: { onlyAvailable: boolean }) => readonly { id: string }[] }
182
182
  | undefined
183
183
  const nodes = registry?.getNodeList?.({ onlyAvailable: true }) ?? []
184
- const agentIds = nodes
185
- .map((n) => n.id)
186
- .filter((id) => id !== 'hub' && !id.includes('/'))
184
+ const agentIds = nodes.map((n) => n.id).filter((id) => id !== 'hub' && !id.includes('/'))
187
185
  if (agentIds.length === 0) return
188
186
  console.log(`[agent-registry] Boot reconcile: ${agentIds.length} connected agent(s)`)
189
187
  for (const agentId of agentIds) {
@@ -217,10 +215,14 @@ export class AgentRegistryService {
217
215
  }
218
216
  try {
219
217
  const broker = this.broker
220
- const statusRaw = await broker.call('$agent.status', {}, {
221
- nodeID: agentId,
222
- timeout: AGENT_RECONCILE_RPC_TIMEOUT_MS,
223
- })
218
+ const statusRaw = await broker.call(
219
+ '$agent.status',
220
+ {},
221
+ {
222
+ nodeID: agentId,
223
+ timeout: AGENT_RECONCILE_RPC_TIMEOUT_MS,
224
+ },
225
+ )
224
226
  const agentAddons = this.extractAgentAddons(statusRaw)
225
227
  if (agentAddons.length === 0) return
226
228
 
@@ -243,7 +245,9 @@ export class AgentRegistryService {
243
245
  })
244
246
 
245
247
  if (stale.length === 0) {
246
- console.log(`[agent-registry] Reconcile ${agentId}: no stale addons (${agentAddons.length} checked)`)
248
+ console.log(
249
+ `[agent-registry] Reconcile ${agentId}: no stale addons (${agentAddons.length} checked)`,
250
+ )
247
251
  return
248
252
  }
249
253
 
@@ -252,11 +256,17 @@ export class AgentRegistryService {
252
256
  ? 'placement is hub-only'
253
257
  : 'not installed on hub'
254
258
  try {
255
- await broker.call('$agent.undeploy', { addonId: addon.id }, {
256
- nodeID: agentId,
257
- timeout: AGENT_RECONCILE_RPC_TIMEOUT_MS,
258
- })
259
- console.log(`[agent-registry] Reconcile ${agentId}: undeployed stale addon "${addon.id}" (${reason})`)
259
+ await broker.call(
260
+ '$agent.undeploy',
261
+ { addonId: addon.id },
262
+ {
263
+ nodeID: agentId,
264
+ timeout: AGENT_RECONCILE_RPC_TIMEOUT_MS,
265
+ },
266
+ )
267
+ console.log(
268
+ `[agent-registry] Reconcile ${agentId}: undeployed stale addon "${addon.id}" (${reason})`,
269
+ )
260
270
  this.eventBus.emit({
261
271
  id: randomUUID(),
262
272
  timestamp: new Date(),
@@ -303,7 +313,10 @@ export class AgentRegistryService {
303
313
  // Get child processes for hub via $process.list
304
314
  let hubProcesses: readonly unknown[] = []
305
315
  try {
306
- const processes = await this.broker.call('$process.list') as readonly Record<string, unknown>[]
316
+ const processes = (await this.broker.call('$process.list')) as readonly Record<
317
+ string,
318
+ unknown
319
+ >[]
307
320
  hubProcesses = processes.map((p) => ({
308
321
  pid: (p.pid as number) ?? 0,
309
322
  name: (p.name as string) ?? '',
@@ -333,18 +346,26 @@ export class AgentRegistryService {
333
346
  if (nodeId === 'hub' || nodeId.includes('/')) continue
334
347
 
335
348
  try {
336
- const status = await this.broker.call('$agent.status', {}, {
337
- nodeID: nodeId,
338
- timeout: 5000,
339
- }) as Record<string, unknown>
349
+ const status = (await this.broker.call(
350
+ '$agent.status',
351
+ {},
352
+ {
353
+ nodeID: nodeId,
354
+ timeout: 5000,
355
+ },
356
+ )) as Record<string, unknown>
340
357
 
341
358
  // Get real sub-process stats from the agent's $process.list
342
359
  let subProcesses: readonly unknown[] = []
343
360
  try {
344
- const processes = await this.broker.call('$process.list', {}, {
345
- nodeID: nodeId,
346
- timeout: 5000,
347
- }) as readonly Record<string, unknown>[]
361
+ const processes = (await this.broker.call(
362
+ '$process.list',
363
+ {},
364
+ {
365
+ nodeID: nodeId,
366
+ timeout: 5000,
367
+ },
368
+ )) as readonly Record<string, unknown>[]
348
369
  subProcesses = processes.map((p) => ({
349
370
  pid: (p.pid as number) ?? 0,
350
371
  name: (p.name as string) ?? '',
@@ -358,20 +379,21 @@ export class AgentRegistryService {
358
379
  }))
359
380
  } catch {
360
381
  // Fall back to addon list from $agent.status (no stats)
361
- subProcesses = (status.addons as readonly Record<string, unknown>[] | undefined)?.map((a) => ({
362
- pid: 0,
363
- name: (a.id as string) ?? '',
364
- command: 'moleculer-service',
365
- state: ((a.status as string) ?? 'running') as 'running' | 'stopped' | 'crashed',
366
- cpuPercent: 0,
367
- memoryRss: 0,
368
- uptimeSeconds: 0,
369
- })) ?? []
382
+ subProcesses =
383
+ (status.addons as readonly Record<string, unknown>[] | undefined)?.map((a) => ({
384
+ pid: 0,
385
+ name: (a.id as string) ?? '',
386
+ command: 'moleculer-service',
387
+ state: ((a.status as string) ?? 'running') as 'running' | 'stopped' | 'crashed',
388
+ cpuPercent: 0,
389
+ memoryRss: 0,
390
+ uptimeSeconds: 0,
391
+ })) ?? []
370
392
  }
371
393
 
372
394
  // Extract addon IDs from $agent.status
373
- const agentAddons: readonly string[] = (status.addons as readonly { id: string }[] | undefined)
374
- ?.map(a => a.id) ?? []
395
+ const agentAddons: readonly string[] =
396
+ (status.addons as readonly { id: string }[] | undefined)?.map((a) => a.id) ?? []
375
397
 
376
398
  const hostname = typeof status.hostname === 'string' ? status.hostname : null
377
399
  const agentName = typeof status.name === 'string' ? status.name : nodeId
@@ -387,7 +409,7 @@ export class AgentRegistryService {
387
409
  memoryMB: (status.totalMemoryMB as number) ?? 0,
388
410
  cpuModel: status.cpuModel as string | undefined,
389
411
  },
390
- localIps: Array.isArray(status.localIps) ? status.localIps as string[] : [],
412
+ localIps: Array.isArray(status.localIps) ? (status.localIps as string[]) : [],
391
413
  status: {
392
414
  activeCameras: 0,
393
415
  cpuPercent: (status.cpuPercent as number) ?? 0,
@@ -395,14 +417,14 @@ export class AgentRegistryService {
395
417
  fps: {},
396
418
  errors: [],
397
419
  },
398
- connectedSince: typeof status.uptime === 'number'
399
- ? Date.now() - (status.uptime as number) * 1000
400
- : Date.now(),
420
+ connectedSince:
421
+ typeof status.uptime === 'number'
422
+ ? Date.now() - (status.uptime as number) * 1000
423
+ : Date.now(),
401
424
  isHub: false,
402
425
  subProcesses,
403
426
  agentAddons,
404
427
  })
405
-
406
428
  } catch {
407
429
  // Skip nodes without $agent service
408
430
  }
@@ -1,4 +1,3 @@
1
-
2
1
  import { describe, it, expect, beforeEach } from 'vitest'
3
2
  import { AuthService } from './auth.service'
4
3
  import type { ConfigService } from '../config/config.service'
@@ -28,20 +27,19 @@ describe('AuthService', () => {
28
27
  }
29
28
 
30
29
  const token = service.signToken(payload)
31
-
30
+
32
31
  const decoded = service.verifyToken(token)
33
32
 
34
-
35
33
  expect(decoded.userId).toBe('user-1')
36
-
34
+
37
35
  expect(decoded.username).toBe('admin')
38
-
36
+
39
37
  expect(decoded.role).toBe('admin')
40
-
38
+
41
39
  expect(decoded.allowedProviders).toBe('*')
42
-
40
+
43
41
  expect(decoded.iat).toBeDefined()
44
-
42
+
45
43
  expect(decoded.exp).toBeDefined()
46
44
  })
47
45
 
@@ -92,7 +92,7 @@ describe('ConfigService', () => {
92
92
  expect(raw.features.objectDetection).toBe(false)
93
93
  expect(raw.storage.provider).toBe('sqlite-storage')
94
94
  expect(raw.logging.level).toBe('info')
95
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access --
95
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access --
96
96
  expect((raw.logging as any).retentionDays).toBe(30)
97
97
  expect(raw.eventBus.ringBufferSize).toBe(10000)
98
98
  // addons.enabled removed — installed = active
@@ -10,7 +10,7 @@ import type { ISettingsStore } from '@camstack/kernel'
10
10
  import type { SystemEvent } from '@camstack/types'
11
11
 
12
12
  /** Flush pending queueMicrotask callbacks */
13
- const flush = () => new Promise<void>(resolve => queueMicrotask(resolve))
13
+ const flush = () => new Promise<void>((resolve) => queueMicrotask(resolve))
14
14
 
15
15
  const makeEvent = (category: string, overrides: Partial<SystemEvent> = {}): SystemEvent => ({
16
16
  id: `evt-${Math.random().toString(36).slice(2, 8)}`,
@@ -35,19 +35,43 @@ class InMemorySettingsStore implements ISettingsStore {
35
35
  this.system = { ...seed }
36
36
  }
37
37
 
38
- getSystem(key: string): unknown { return this.system[key] }
39
- setSystem(key: string, value: unknown): void { this.system[key] = value }
40
- getAllSystem(): Record<string, unknown> { return { ...this.system } }
41
-
42
- getAllAddon(_addonId: string): Record<string, unknown> { return {} }
43
- setAllAddon(_addonId: string, _config: Record<string, unknown>): void { /* no-op */ }
44
- getAllProvider(_providerId: string): Record<string, unknown> { return {} }
45
- setProvider(_providerId: string, _key: string, _value: unknown): void { /* no-op */ }
46
- getAllDevice(_deviceId: string): Record<string, unknown> { return {} }
47
- setDevice(_deviceId: string, _key: string, _value: unknown): void { /* no-op */ }
48
- getAddonDevice(_addonId: string, _deviceId: string): Record<string, unknown> { return {} }
49
- setAddonDevice(_addonId: string, _deviceId: string, _values: Record<string, unknown>): void { /* no-op */ }
50
- clearAddonDevice(_addonId: string, _deviceId: string): void { /* no-op */ }
38
+ getSystem(key: string): unknown {
39
+ return this.system[key]
40
+ }
41
+ setSystem(key: string, value: unknown): void {
42
+ this.system[key] = value
43
+ }
44
+ getAllSystem(): Record<string, unknown> {
45
+ return { ...this.system }
46
+ }
47
+
48
+ getAllAddon(_addonId: string): Record<string, unknown> {
49
+ return {}
50
+ }
51
+ setAllAddon(_addonId: string, _config: Record<string, unknown>): void {
52
+ /* no-op */
53
+ }
54
+ getAllProvider(_providerId: string): Record<string, unknown> {
55
+ return {}
56
+ }
57
+ setProvider(_providerId: string, _key: string, _value: unknown): void {
58
+ /* no-op */
59
+ }
60
+ getAllDevice(_deviceId: string): Record<string, unknown> {
61
+ return {}
62
+ }
63
+ setDevice(_deviceId: string, _key: string, _value: unknown): void {
64
+ /* no-op */
65
+ }
66
+ getAddonDevice(_addonId: string, _deviceId: string): Record<string, unknown> {
67
+ return {}
68
+ }
69
+ setAddonDevice(_addonId: string, _deviceId: string, _values: Record<string, unknown>): void {
70
+ /* no-op */
71
+ }
72
+ clearAddonDevice(_addonId: string, _deviceId: string): void {
73
+ /* no-op */
74
+ }
51
75
  }
52
76
 
53
77
  describe('EventBusService', () => {
@@ -80,9 +104,11 @@ describe('EventBusService', () => {
80
104
  'utf-8',
81
105
  )
82
106
  const configService = new ConfigService(configPath)
83
- configService.setSettingsStore(new InMemorySettingsStore({
84
- 'eventBus.ringBufferSize': bufferSize,
85
- }))
107
+ configService.setSettingsStore(
108
+ new InMemorySettingsStore({
109
+ 'eventBus.ringBufferSize': bufferSize,
110
+ }),
111
+ )
86
112
  const service = new EventBusService(configService)
87
113
  // EventBusService is a delegate that needs a broker to dispatch
88
114
  // through. Use a unique nodeID per call so each test gets its own
@@ -152,10 +178,7 @@ describe('EventBusService', () => {
152
178
  const service = await createService()
153
179
  const received: SystemEvent[] = []
154
180
 
155
- service.subscribe(
156
- { source: { type: 'addon', id: 'frigate' } },
157
- (event) => received.push(event),
158
- )
181
+ service.subscribe({ source: { type: 'addon', id: 'frigate' } }, (event) => received.push(event))
159
182
 
160
183
  service.emit(makeEvent('addon.started', { source: { type: 'addon', id: 'frigate' } }))
161
184
  service.emit(makeEvent('addon.started', { source: { type: 'addon', id: 'scrypted' } }))
@@ -27,7 +27,11 @@ export class EventBusService implements IEventBus {
27
27
  // before `attachBroker` runs (NestJS-era boot legacy). Drained into
28
28
  // the real bus on attach.
29
29
  private pending: SystemEvent[] = []
30
- private deferredSubs: Array<{ filter: EventFilter; handler: (event: SystemEvent) => void; unsub?: () => void }> = []
30
+ private deferredSubs: Array<{
31
+ filter: EventFilter
32
+ handler: (event: SystemEvent) => void
33
+ unsub?: () => void
34
+ }> = []
31
35
 
32
36
  constructor(_configService: ConfigService) {
33
37
  // The shared bus owns the ring-buffer size — hub-side config is
@@ -14,7 +14,10 @@ import type { FeatureManifest } from '@camstack/types'
14
14
  * ConfigManager constructor succeeds.
15
15
  */
16
16
  class StaticFeatureConfigService extends ConfigService {
17
- constructor(configPath: string, private readonly staticFeatures: FeatureManifest) {
17
+ constructor(
18
+ configPath: string,
19
+ private readonly staticFeatures: FeatureManifest,
20
+ ) {
18
21
  super(configPath)
19
22
  }
20
23
 
@@ -27,9 +27,8 @@ describe('LifecycleStateMachine', () => {
27
27
  let machine: LifecycleStateMachine
28
28
 
29
29
  beforeEach(() => {
30
-
31
30
  eventBus = createMockEventBus()
32
-
31
+
33
32
  logger = createMockLogger()
34
33
  machine = new LifecycleStateMachine('test-element', 'device', eventBus, logger)
35
34
  })
@@ -55,7 +54,7 @@ describe('LifecycleStateMachine', () => {
55
54
  const result = machine.transition('running')
56
55
  expect(result).toBe(false)
57
56
  expect(machine.state).toBe('stopped')
58
-
57
+
59
58
  expect(logger.warn).toHaveBeenCalledWith(
60
59
  expect.stringContaining('Invalid state transition'),
61
60
  expect.anything(),
@@ -66,21 +65,20 @@ describe('LifecycleStateMachine', () => {
66
65
  machine.transition('starting')
67
66
  machine.transition('running')
68
67
 
69
-
70
68
  expect(eventBus.emit).toHaveBeenCalledTimes(2)
71
-
69
+
72
70
  expect(eventBus.emitted[0]!.category).toBe('device.state.starting')
73
-
71
+
74
72
  expect(eventBus.emitted[0]!.source).toEqual({ type: 'device', id: 'test-element' })
75
-
73
+
76
74
  expect(eventBus.emitted[0]!.data).toMatchObject({
77
75
  from: 'stopped',
78
76
  to: 'starting',
79
77
  elementId: 'test-element',
80
78
  })
81
-
79
+
82
80
  expect(eventBus.emitted[1]!.category).toBe('device.state.running')
83
-
81
+
84
82
  expect(eventBus.emitted[1]!.data).toMatchObject({
85
83
  from: 'starting',
86
84
  to: 'running',
@@ -89,7 +87,7 @@ describe('LifecycleStateMachine', () => {
89
87
 
90
88
  it('does not emit event on invalid transition', () => {
91
89
  machine.transition('running')
92
-
90
+
93
91
  expect(eventBus.emit).not.toHaveBeenCalled()
94
92
  })
95
93
 
@@ -1,4 +1,3 @@
1
-
2
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest'
3
2
  import * as fs from 'node:fs'
4
3
  import * as path from 'node:path'
@@ -25,19 +24,43 @@ class InMemorySettingsStore implements ISettingsStore {
25
24
  this.system = { ...seed }
26
25
  }
27
26
 
28
- getSystem(key: string): unknown { return this.system[key] }
29
- setSystem(key: string, value: unknown): void { this.system[key] = value }
30
- getAllSystem(): Record<string, unknown> { return { ...this.system } }
31
-
32
- getAllAddon(_addonId: string): Record<string, unknown> { return {} }
33
- setAllAddon(_addonId: string, _config: Record<string, unknown>): void { /* no-op */ }
34
- getAllProvider(_providerId: string): Record<string, unknown> { return {} }
35
- setProvider(_providerId: string, _key: string, _value: unknown): void { /* no-op */ }
36
- getAllDevice(_deviceId: string): Record<string, unknown> { return {} }
37
- setDevice(_deviceId: string, _key: string, _value: unknown): void { /* no-op */ }
38
- getAddonDevice(_addonId: string, _deviceId: string): Record<string, unknown> { return {} }
39
- setAddonDevice(_addonId: string, _deviceId: string, _values: Record<string, unknown>): void { /* no-op */ }
40
- clearAddonDevice(_addonId: string, _deviceId: string): void { /* no-op */ }
27
+ getSystem(key: string): unknown {
28
+ return this.system[key]
29
+ }
30
+ setSystem(key: string, value: unknown): void {
31
+ this.system[key] = value
32
+ }
33
+ getAllSystem(): Record<string, unknown> {
34
+ return { ...this.system }
35
+ }
36
+
37
+ getAllAddon(_addonId: string): Record<string, unknown> {
38
+ return {}
39
+ }
40
+ setAllAddon(_addonId: string, _config: Record<string, unknown>): void {
41
+ /* no-op */
42
+ }
43
+ getAllProvider(_providerId: string): Record<string, unknown> {
44
+ return {}
45
+ }
46
+ setProvider(_providerId: string, _key: string, _value: unknown): void {
47
+ /* no-op */
48
+ }
49
+ getAllDevice(_deviceId: string): Record<string, unknown> {
50
+ return {}
51
+ }
52
+ setDevice(_deviceId: string, _key: string, _value: unknown): void {
53
+ /* no-op */
54
+ }
55
+ getAddonDevice(_addonId: string, _deviceId: string): Record<string, unknown> {
56
+ return {}
57
+ }
58
+ setAddonDevice(_addonId: string, _deviceId: string, _values: Record<string, unknown>): void {
59
+ /* no-op */
60
+ }
61
+ clearAddonDevice(_addonId: string, _deviceId: string): void {
62
+ /* no-op */
63
+ }
41
64
  }
42
65
 
43
66
  describe('ScopedLogger', () => {
@@ -147,13 +170,28 @@ describe('LogRingBuffer', () => {
147
170
  it('filters by tags (addonId exact match)', () => {
148
171
  const buffer = new LogRingBuffer(100)
149
172
 
150
- buffer.push({ timestamp: new Date(), level: 'info', message: 'a', tags: { addonId: 'stream-broker' } })
151
- buffer.push({ timestamp: new Date(), level: 'info', message: 'b', tags: { addonId: 'provider-rtsp' } })
152
- buffer.push({ timestamp: new Date(), level: 'info', message: 'c', tags: { addonId: 'stream-broker' } })
173
+ buffer.push({
174
+ timestamp: new Date(),
175
+ level: 'info',
176
+ message: 'a',
177
+ tags: { addonId: 'stream-broker' },
178
+ })
179
+ buffer.push({
180
+ timestamp: new Date(),
181
+ level: 'info',
182
+ message: 'b',
183
+ tags: { addonId: 'provider-rtsp' },
184
+ })
185
+ buffer.push({
186
+ timestamp: new Date(),
187
+ level: 'info',
188
+ message: 'c',
189
+ tags: { addonId: 'stream-broker' },
190
+ })
153
191
 
154
192
  const result = buffer.query({ tags: { addonId: 'stream-broker' } })
155
193
  expect(result).toHaveLength(2)
156
- expect(result.map((e) => e.message).sort()).toEqual(['a', 'c'])
194
+ expect(result.map((e) => e.message).toSorted()).toEqual(['a', 'c'])
157
195
  })
158
196
 
159
197
  it('respects limit', () => {
@@ -190,9 +228,11 @@ describe('LoggingService', () => {
190
228
  'utf-8',
191
229
  )
192
230
  const configService = new ConfigService(configPath)
193
- configService.setSettingsStore(new InMemorySettingsStore({
194
- 'eventBus.ringBufferSize': bufferSize,
195
- }))
231
+ configService.setSettingsStore(
232
+ new InMemorySettingsStore({
233
+ 'eventBus.ringBufferSize': bufferSize,
234
+ }),
235
+ )
196
236
  return new LoggingService(configService)
197
237
  }
198
238