@camstack/server 0.1.8 → 0.2.0

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
@@ -22,9 +22,11 @@ function makeRig(opts?: { failItems?: ReadonlySet<string> }): TestRig {
22
22
  const updateAddon = vi.fn(async (input: { name: string; version: string }) => {
23
23
  if (fail.has(input.name)) throw new Error(`mock fail ${input.name}`)
24
24
  })
25
- const updateFrameworkPackage = vi.fn(async (input: { packageName: string; version: string; deferRestart: boolean }) => {
26
- if (fail.has(input.packageName)) throw new Error(`mock fail fw ${input.packageName}`)
27
- })
25
+ const updateFrameworkPackage = vi.fn(
26
+ async (input: { packageName: string; version: string; deferRestart: boolean }) => {
27
+ if (fail.has(input.packageName)) throw new Error(`mock fail fw ${input.packageName}`)
28
+ },
29
+ )
28
30
  const restartServer = vi.fn(async () => {
29
31
  // simulates the real restartServer: in a real run the process dies
30
32
  })
@@ -40,7 +42,12 @@ function makeRig(opts?: { failItems?: ReadonlySet<string> }): TestRig {
40
42
  updateAddon,
41
43
  updateFrameworkPackage,
42
44
  restartServer,
43
- logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() } as unknown as BulkUpdateCoordinatorDeps['logger'],
45
+ logger: {
46
+ info: vi.fn(),
47
+ warn: vi.fn(),
48
+ error: vi.fn(),
49
+ debug: vi.fn(),
50
+ } as unknown as BulkUpdateCoordinatorDeps['logger'],
44
51
  clock: () => clock++,
45
52
  }
46
53
 
@@ -55,8 +62,12 @@ function makeRig(opts?: { failItems?: ReadonlySet<string> }): TestRig {
55
62
  }
56
63
 
57
64
  describe('BulkUpdateCoordinator', () => {
58
- beforeEach(() => { vi.useFakeTimers() })
59
- afterEach(() => { vi.useRealTimers() })
65
+ beforeEach(() => {
66
+ vi.useFakeTimers()
67
+ })
68
+ afterEach(() => {
69
+ vi.useRealTimers()
70
+ })
60
71
 
61
72
  it('regular-only happy path: processes 3 addons in order, no restart', async () => {
62
73
  const rig = makeRig()
@@ -75,7 +86,7 @@ describe('BulkUpdateCoordinator', () => {
75
86
  expect(final.completed).toBe(3)
76
87
  expect(final.failed).toBe(0)
77
88
  expect(final.phase).toBe('finalizing')
78
- expect(final.items.every(i => i.status === 'done')).toBe(true)
89
+ expect(final.items.every((i) => i.status === 'done')).toBe(true)
79
90
  expect(rig.restartServer).not.toHaveBeenCalled()
80
91
  expect(rig.updateAddon).toHaveBeenCalledTimes(3)
81
92
  })
@@ -98,15 +109,19 @@ describe('BulkUpdateCoordinator', () => {
98
109
  expect(final.completed).toBe(4)
99
110
  expect(rig.updateAddon).toHaveBeenCalledTimes(2)
100
111
  expect(rig.updateFrameworkPackage).toHaveBeenCalledTimes(2)
101
- expect(rig.updateFrameworkPackage).toHaveBeenCalledWith(expect.objectContaining({ deferRestart: true }))
112
+ expect(rig.updateFrameworkPackage).toHaveBeenCalledWith(
113
+ expect.objectContaining({ deferRestart: true }),
114
+ )
102
115
  expect(rig.restartServer).toHaveBeenCalledOnce()
103
116
 
104
117
  // Verify ordering via event sequence: regular items reach 'updating' before any system item
105
- const updatingNames = rig.events
106
- .map(s => s.current)
107
- .filter((n): n is string => n !== null)
108
- const firstSystemIdx = updatingNames.findIndex(n => n === '@camstack/types' || n === '@camstack/kernel')
109
- const lastRegularIdx = updatingNames.findLastIndex(n => n === '@camstack/addon-a' || n === '@camstack/addon-b')
118
+ const updatingNames = rig.events.map((s) => s.current).filter((n): n is string => n !== null)
119
+ const firstSystemIdx = updatingNames.findIndex(
120
+ (n) => n === '@camstack/types' || n === '@camstack/kernel',
121
+ )
122
+ const lastRegularIdx = updatingNames.findLastIndex(
123
+ (n) => n === '@camstack/addon-a' || n === '@camstack/addon-b',
124
+ )
110
125
  expect(firstSystemIdx).toBeGreaterThan(lastRegularIdx)
111
126
  })
112
127
 
@@ -125,17 +140,19 @@ describe('BulkUpdateCoordinator', () => {
125
140
 
126
141
  const final = rig.coordinator.get(id)!
127
142
  expect(final.failed).toBe(1)
128
- expect(final.items.find(i => i.name === '@camstack/addon-b')?.status).toBe('failed')
129
- expect(final.items.find(i => i.name === '@camstack/addon-b')?.error).toContain('mock fail')
130
- expect(final.items.find(i => i.name === '@camstack/addon-a')?.status).toBe('done')
131
- expect(final.items.find(i => i.name === '@camstack/addon-c')?.status).toBe('done')
143
+ expect(final.items.find((i) => i.name === '@camstack/addon-b')?.status).toBe('failed')
144
+ expect(final.items.find((i) => i.name === '@camstack/addon-b')?.error).toContain('mock fail')
145
+ expect(final.items.find((i) => i.name === '@camstack/addon-a')?.status).toBe('done')
146
+ expect(final.items.find((i) => i.name === '@camstack/addon-c')?.status).toBe('done')
132
147
  })
133
148
 
134
149
  it('cancel pre-restart: loop exits, no restart, queued items remain queued', async () => {
135
150
  const rig = makeRig()
136
151
  rig.updateAddon.mockImplementation(async () => {
137
152
  // Slow enough that cancel fires before item 2
138
- await new Promise<void>(resolve => { setTimeout(resolve, 50) })
153
+ await new Promise<void>((resolve) => {
154
+ setTimeout(resolve, 50)
155
+ })
139
156
  })
140
157
 
141
158
  const { id } = rig.coordinator.start({
@@ -157,21 +174,21 @@ describe('BulkUpdateCoordinator', () => {
157
174
  expect(final.cancelled).toBe(true)
158
175
  expect(rig.restartServer).not.toHaveBeenCalled()
159
176
  // At least one item should still be queued (we cancelled before item 2 ran)
160
- expect(final.items.filter(i => i.status === 'queued').length).toBeGreaterThanOrEqual(1)
177
+ expect(final.items.filter((i) => i.status === 'queued').length).toBeGreaterThanOrEqual(1)
161
178
  })
162
179
 
163
180
  it('cancel ignored during restarting phase', async () => {
164
181
  const rig = makeRig()
165
182
  rig.restartServer.mockImplementation(async () => {
166
183
  // Hang briefly so we can attempt cancel during restart
167
- await new Promise<void>(resolve => { setTimeout(resolve, 50) })
184
+ await new Promise<void>((resolve) => {
185
+ setTimeout(resolve, 50)
186
+ })
168
187
  })
169
188
 
170
189
  const { id } = rig.coordinator.start({
171
190
  nodeId: 'hub',
172
- items: [
173
- { name: '@camstack/types', version: '0.1.40', isSystem: true },
174
- ],
191
+ items: [{ name: '@camstack/types', version: '0.1.40', isSystem: true }],
175
192
  })
176
193
 
177
194
  // Let it reach restarting
@@ -191,15 +208,13 @@ describe('BulkUpdateCoordinator', () => {
191
208
 
192
209
  const { id } = rig.coordinator.start({
193
210
  nodeId: 'hub',
194
- items: [
195
- { name: '@camstack/types', version: '0.1.40', isSystem: true },
196
- ],
211
+ items: [{ name: '@camstack/types', version: '0.1.40', isSystem: true }],
197
212
  })
198
213
 
199
214
  await vi.runAllTimersAsync()
200
215
 
201
216
  const final = rig.coordinator.get(id)!
202
- const typesItem = final.items.find(i => i.name === '@camstack/types')!
217
+ const typesItem = final.items.find((i) => i.name === '@camstack/types')!
203
218
  expect(typesItem.status).toBe('done')
204
219
  expect(typesItem.error).toContain('Restart failed')
205
220
  })
@@ -211,10 +226,12 @@ describe('BulkUpdateCoordinator', () => {
211
226
  items: [{ name: '@camstack/addon-a', version: '1.0.1', isSystem: false }],
212
227
  })
213
228
 
214
- expect(() => rig.coordinator.start({
215
- nodeId: 'hub',
216
- items: [{ name: '@camstack/addon-b', version: '2.0.1', isSystem: false }],
217
- })).toThrow(/already in progress/i)
229
+ expect(() =>
230
+ rig.coordinator.start({
231
+ nodeId: 'hub',
232
+ items: [{ name: '@camstack/addon-b', version: '2.0.1', isSystem: false }],
233
+ }),
234
+ ).toThrow(/already in progress/i)
218
235
  })
219
236
 
220
237
  it('auto-cleanup: state purged 5 min after completedAt', async () => {
@@ -21,7 +21,11 @@
21
21
  */
22
22
 
23
23
  import { describe, it, expect, vi } from 'vitest'
24
- import type { NodeCapAuthority, HubLocalChildDispatcher, CapRouteResolverDeps } from '@camstack/kernel'
24
+ import type {
25
+ NodeCapAuthority,
26
+ HubLocalChildDispatcher,
27
+ CapRouteResolverDeps,
28
+ } from '@camstack/kernel'
25
29
  import { CapRouteResolver, CapRouteError } from '@camstack/kernel'
26
30
 
27
31
  // ---------------------------------------------------------------------------
@@ -40,8 +44,15 @@ import { CapRouteResolver, CapRouteError } from '@camstack/kernel'
40
44
  function buildConsolidatedNativeFallback(opts: {
41
45
  readonly resolver: CapRouteResolver | null
42
46
  readonly hubNodeId: string
43
- readonly resolveNativeCapOwnerSync: (capName: string, deviceId: number) => { addonId: string; nodeId: string } | null
44
- readonly buildNativeCapProxy: (addonId: string, capName: string, deviceId: number) => Record<string, unknown>
47
+ readonly resolveNativeCapOwnerSync: (
48
+ capName: string,
49
+ deviceId: number,
50
+ ) => { addonId: string; nodeId: string } | null
51
+ readonly buildNativeCapProxy: (
52
+ addonId: string,
53
+ capName: string,
54
+ deviceId: number,
55
+ ) => Record<string, unknown>
45
56
  }): (capName: string, deviceId: number) => unknown | null {
46
57
  const { resolver, hubNodeId, resolveNativeCapOwnerSync, buildNativeCapProxy } = opts
47
58
 
@@ -152,7 +163,9 @@ function makeNodeAuthority(
152
163
  getAgentChildId: (): string | null => null,
153
164
  isNativeCap: (nodeId: string, capName: string, deviceId?: number): boolean => {
154
165
  if (deviceId !== undefined) {
155
- return nativeCaps.some((n) => n.nodeId === nodeId && n.capName === capName && n.deviceId === deviceId)
166
+ return nativeCaps.some(
167
+ (n) => n.nodeId === nodeId && n.capName === capName && n.deviceId === deviceId,
168
+ )
156
169
  }
157
170
  return nativeCaps.some((n) => n.nodeId === nodeId && n.capName === capName)
158
171
  },
@@ -171,9 +184,18 @@ describe('G2 — (a) hub-local device-scoped native cap: resolver is consulted f
171
184
  new Map([['ptz', new Map([[7, 'provider-reolink']])]]),
172
185
  )
173
186
  const nativeCaps: NativeCapSpec[] = [
174
- { nodeId: 'hub/provider-reolink', addonId: 'addon-provider-reolink', capName: 'ptz', deviceId: 7 },
187
+ {
188
+ nodeId: 'hub/provider-reolink',
189
+ addonId: 'addon-provider-reolink',
190
+ capName: 'ptz',
191
+ deviceId: 7,
192
+ },
175
193
  ]
176
- const nodeAuthority = makeNodeAuthority(new Map(), new Set(['hub/provider-reolink']), nativeCaps)
194
+ const nodeAuthority = makeNodeAuthority(
195
+ new Map(),
196
+ new Set(['hub/provider-reolink']),
197
+ nativeCaps,
198
+ )
177
199
 
178
200
  const deps: CapRouteResolverDeps = {
179
201
  hubNodeId: HUB_NODE_ID,
@@ -211,9 +233,18 @@ describe('G2 — (a) hub-local device-scoped native cap: resolver is consulted f
211
233
  new Map([['ptz', new Map([[7, 'provider-reolink']])]]),
212
234
  )
213
235
  const nativeCaps: NativeCapSpec[] = [
214
- { nodeId: 'hub/provider-reolink', addonId: 'addon-provider-reolink', capName: 'ptz', deviceId: 7 },
236
+ {
237
+ nodeId: 'hub/provider-reolink',
238
+ addonId: 'addon-provider-reolink',
239
+ capName: 'ptz',
240
+ deviceId: 7,
241
+ },
215
242
  ]
216
- const nodeAuthority = makeNodeAuthority(new Map(), new Set(['hub/provider-reolink']), nativeCaps)
243
+ const nodeAuthority = makeNodeAuthority(
244
+ new Map(),
245
+ new Set(['hub/provider-reolink']),
246
+ nativeCaps,
247
+ )
217
248
 
218
249
  const deps: CapRouteResolverDeps = {
219
250
  hubNodeId: HUB_NODE_ID,
@@ -97,7 +97,12 @@ describe('getAvailableTypes — supportsLocationImport flag', () => {
97
97
  },
98
98
  }
99
99
  const ar = makeAddonRegistry([addon])
100
- const provider = buildIntegrationsProvider(ar as never, makeEventBus() as never, makeLoggingService() as never, null)
100
+ const provider = buildIntegrationsProvider(
101
+ ar as never,
102
+ makeEventBus() as never,
103
+ makeLoggingService() as never,
104
+ null,
105
+ )
101
106
  const types = await provider.getAvailableTypes()
102
107
 
103
108
  expect(types).toHaveLength(1)
@@ -120,7 +125,12 @@ describe('getAvailableTypes — supportsLocationImport flag', () => {
120
125
  },
121
126
  }
122
127
  const ar = makeAddonRegistry([addon])
123
- const provider = buildIntegrationsProvider(ar as never, makeEventBus() as never, makeLoggingService() as never, null)
128
+ const provider = buildIntegrationsProvider(
129
+ ar as never,
130
+ makeEventBus() as never,
131
+ makeLoggingService() as never,
132
+ null,
133
+ )
124
134
  const types = await provider.getAvailableTypes()
125
135
 
126
136
  expect(types).toHaveLength(1)
@@ -144,7 +154,12 @@ describe('getAvailableTypes — supportsLocationImport flag', () => {
144
154
  },
145
155
  }
146
156
  const ar = makeAddonRegistry([addon])
147
- const provider = buildIntegrationsProvider(ar as never, makeEventBus() as never, makeLoggingService() as never, null)
157
+ const provider = buildIntegrationsProvider(
158
+ ar as never,
159
+ makeEventBus() as never,
160
+ makeLoggingService() as never,
161
+ null,
162
+ )
148
163
  const types = await provider.getAvailableTypes()
149
164
 
150
165
  expect(types).toHaveLength(1)
@@ -173,7 +188,12 @@ describe('getAvailableTypes — supportsLocationImport flag', () => {
173
188
  },
174
189
  }
175
190
  const ar = makeAddonRegistry([addon])
176
- const provider = buildIntegrationsProvider(ar as never, makeEventBus() as never, makeLoggingService() as never, null)
191
+ const provider = buildIntegrationsProvider(
192
+ ar as never,
193
+ makeEventBus() as never,
194
+ makeLoggingService() as never,
195
+ null,
196
+ )
177
197
  const types = await provider.getAvailableTypes()
178
198
 
179
199
  expect(types).toHaveLength(1)
@@ -3,13 +3,27 @@ import { __resetCapUsageRegistryForTests, getCapUsageRegistry } from '@camstack/
3
3
  import { buildNodesProvider } from '../../api/core/cap-providers'
4
4
 
5
5
  describe('nodes.getCapUsageGraph provider', () => {
6
- beforeEach(() => { __resetCapUsageRegistryForTests() })
6
+ beforeEach(() => {
7
+ __resetCapUsageRegistryForTests()
8
+ })
7
9
 
8
10
  it('returns recorded edges projected through the cap-usage registry', async () => {
9
11
  const reg = getCapUsageRegistry()
10
12
  const t0 = Date.now() - 5_000
11
- reg.recordCall({ callerAddonId: 'A', providerAddonId: 'B', capName: 'foo', methodName: 'm', atMs: t0 })
12
- reg.recordCall({ callerAddonId: 'A', providerAddonId: 'B', capName: 'foo', methodName: 'm', atMs: t0 + 1000 })
13
+ reg.recordCall({
14
+ callerAddonId: 'A',
15
+ providerAddonId: 'B',
16
+ capName: 'foo',
17
+ methodName: 'm',
18
+ atMs: t0,
19
+ })
20
+ reg.recordCall({
21
+ callerAddonId: 'A',
22
+ providerAddonId: 'B',
23
+ capName: 'foo',
24
+ methodName: 'm',
25
+ atMs: t0 + 1000,
26
+ })
13
27
 
14
28
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
29
  const provider = buildNodesProvider({} as any, { broker: { call: vi.fn() } } as any)
@@ -6,7 +6,17 @@ function makeAgentRegistry(): { listNodes: () => Promise<unknown[]> } {
6
6
  return {
7
7
  listNodes: async () => [
8
8
  {
9
- info: { id: 'hub', name: 'hub', hostname: 'hub', platform: 'darwin', arch: 'arm64', cpuModel: 'M2', cpuCores: 8, memoryMB: 16384, pythonRuntimes: [] },
9
+ info: {
10
+ id: 'hub',
11
+ name: 'hub',
12
+ hostname: 'hub',
13
+ platform: 'darwin',
14
+ arch: 'arm64',
15
+ cpuModel: 'M2',
16
+ cpuCores: 8,
17
+ memoryMB: 16384,
18
+ pythonRuntimes: [],
19
+ },
10
20
  status: { cpuPercent: 12, memoryPercent: 30 },
11
21
  isHub: true,
12
22
  isOnline: true,
@@ -22,10 +32,34 @@ function makeAgentRegistry(): { listNodes: () => Promise<unknown[]> } {
22
32
  function makeAddonRegistry(): { listAddons: () => readonly unknown[] } {
23
33
  return {
24
34
  listAddons: () => [
25
- { manifest: { id: 'provider-hikvision' }, declaration: { id: 'provider-hikvision', category: 'providers', capabilities: [{ name: 'stream-params' }] } },
26
- { manifest: { id: 'provider-onvif' }, declaration: { id: 'provider-onvif', category: 'providers', capabilities: [{ name: 'device-provider' }] } },
27
- { manifest: { id: 'stream-broker' }, declaration: { id: 'stream-broker', category: 'pipeline', capabilities: [{ name: 'stream-broker' }] } },
28
- { manifest: { id: 'sqlite-settings' }, declaration: { id: 'sqlite-settings', capabilities: [{ name: 'settings-store' }] } },
35
+ {
36
+ manifest: { id: 'provider-hikvision' },
37
+ declaration: {
38
+ id: 'provider-hikvision',
39
+ category: 'providers',
40
+ capabilities: [{ name: 'stream-params' }],
41
+ },
42
+ },
43
+ {
44
+ manifest: { id: 'provider-onvif' },
45
+ declaration: {
46
+ id: 'provider-onvif',
47
+ category: 'providers',
48
+ capabilities: [{ name: 'device-provider' }],
49
+ },
50
+ },
51
+ {
52
+ manifest: { id: 'stream-broker' },
53
+ declaration: {
54
+ id: 'stream-broker',
55
+ category: 'pipeline',
56
+ capabilities: [{ name: 'stream-broker' }],
57
+ },
58
+ },
59
+ {
60
+ manifest: { id: 'sqlite-settings' },
61
+ declaration: { id: 'sqlite-settings', capabilities: [{ name: 'settings-store' }] },
62
+ },
29
63
  ],
30
64
  }
31
65
  }
@@ -36,12 +70,17 @@ describe('computeTopology categories[] aggregation', () => {
36
70
  const nodes = await computeTopology(makeAgentRegistry() as any, makeAddonRegistry() as any)
37
71
  expect(nodes).toHaveLength(1)
38
72
  const cats = nodes[0]!.categories
39
- const byId = new Map(cats.map(c => [c.category, c]))
73
+ const byId = new Map(cats.map((c) => [c.category, c]))
40
74
  expect(byId.get('providers')?.total).toBe(2)
41
75
  expect(byId.get('providers')?.healthy).toBe(2)
42
- expect(byId.get('providers')?.addons.map(a => a.id).sort()).toEqual(['provider-hikvision', 'provider-onvif'])
76
+ expect(
77
+ byId
78
+ .get('providers')
79
+ ?.addons.map((a) => a.id)
80
+ .toSorted(),
81
+ ).toEqual(['provider-hikvision', 'provider-onvif'])
43
82
  expect(byId.get('pipeline')?.total).toBe(1)
44
- expect(byId.get('system')?.addons.map(a => a.id)).toEqual(['sqlite-settings'])
83
+ expect(byId.get('system')?.addons.map((a) => a.id)).toEqual(['sqlite-settings'])
45
84
  })
46
85
 
47
86
  it('counts a non-running addon as not-healthy', async () => {
@@ -49,13 +88,20 @@ describe('computeTopology categories[] aggregation', () => {
49
88
  // Force the addon registry to return one stopped + one running provider.
50
89
  const addonReg = {
51
90
  listAddons: () => [
52
- { manifest: { id: 'provider-rtsp' }, declaration: { id: 'provider-rtsp', category: 'providers', capabilities: [] } },
53
- { manifest: { id: 'provider-hikvision' }, declaration: { id: 'provider-hikvision', category: 'providers', capabilities: [] }, status: 'failed' },
91
+ {
92
+ manifest: { id: 'provider-rtsp' },
93
+ declaration: { id: 'provider-rtsp', category: 'providers', capabilities: [] },
94
+ },
95
+ {
96
+ manifest: { id: 'provider-hikvision' },
97
+ declaration: { id: 'provider-hikvision', category: 'providers', capabilities: [] },
98
+ status: 'failed',
99
+ },
54
100
  ],
55
101
  }
56
102
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
103
  const nodes = await computeTopology(agentReg as any, addonReg as any)
58
- const providers = nodes[0]!.categories.find(c => c.category === 'providers')!
104
+ const providers = nodes[0]!.categories.find((c) => c.category === 'providers')!
59
105
  expect(providers.total).toBe(2)
60
106
  // The current TopologyNode `addons[].status` is always `'running'` per existing code;
61
107
  // failed addons would surface through subProcesses[].state. Document the current contract:
@@ -62,12 +62,12 @@ function makeLoggingService(log = makeLogger()) {
62
62
  }
63
63
 
64
64
  function makeCapabilityRegistry(
65
- removeByIntegration: ((input: { integrationId: string }) => Promise<{ removed: number }>) | null = vi.fn(async () => ({ removed: 2 })),
65
+ removeByIntegration:
66
+ | ((input: { integrationId: string }) => Promise<{ removed: number }>)
67
+ | null = vi.fn(async () => ({ removed: 2 })),
66
68
  ): CapabilityRegistry {
67
69
  return {
68
- getSingleton: vi.fn((_cap: string) =>
69
- removeByIntegration ? { removeByIntegration } : null,
70
- ),
70
+ getSingleton: vi.fn((_cap: string) => (removeByIntegration ? { removeByIntegration } : null)),
71
71
  } as unknown as CapabilityRegistry
72
72
  }
73
73
 
@@ -92,7 +92,12 @@ describe('integrations.delete cascade-removes devices via removeByIntegration',
92
92
  const removeByIntegration = vi.fn(async () => ({ removed: 3 }))
93
93
  const capReg = makeCapabilityRegistry(removeByIntegration)
94
94
 
95
- const provider = buildIntegrationsProvider(addonReg as never, eb as never, loggingService as never, capReg)
95
+ const provider = buildIntegrationsProvider(
96
+ addonReg as never,
97
+ eb as never,
98
+ loggingService as never,
99
+ capReg,
100
+ )
96
101
  await provider.delete({ id: INTEGRATION_ID })
97
102
 
98
103
  expect(removeByIntegration).toHaveBeenCalledOnce()
@@ -102,7 +107,12 @@ describe('integrations.delete cascade-removes devices via removeByIntegration',
102
107
  it('still deletes the integration record and emits integration.deleted after the cascade', async () => {
103
108
  const capReg = makeCapabilityRegistry()
104
109
 
105
- const provider = buildIntegrationsProvider(addonReg as never, eb as never, loggingService as never, capReg)
110
+ const provider = buildIntegrationsProvider(
111
+ addonReg as never,
112
+ eb as never,
113
+ loggingService as never,
114
+ capReg,
115
+ )
106
116
  const result = await provider.delete({ id: INTEGRATION_ID })
107
117
 
108
118
  expect(integrationReg.deleteIntegration).toHaveBeenCalledOnce()
@@ -123,7 +133,12 @@ describe('integrations.delete cascade-removes devices via removeByIntegration',
123
133
  const removeByIntegration = vi.fn(async () => ({ removed: 5 }))
124
134
  const capReg = makeCapabilityRegistry(removeByIntegration)
125
135
 
126
- const provider = buildIntegrationsProvider(addonReg as never, eb as never, loggingService as never, capReg)
136
+ const provider = buildIntegrationsProvider(
137
+ addonReg as never,
138
+ eb as never,
139
+ loggingService as never,
140
+ capReg,
141
+ )
127
142
  await provider.delete({ id: INTEGRATION_ID })
128
143
 
129
144
  expect(log.info).toHaveBeenCalledWith(
@@ -138,7 +153,12 @@ describe('integrations.delete cascade-removes devices via removeByIntegration',
138
153
  })
139
154
  const capReg = makeCapabilityRegistry(removeByIntegration)
140
155
 
141
- const provider = buildIntegrationsProvider(addonReg as never, eb as never, loggingService as never, capReg)
156
+ const provider = buildIntegrationsProvider(
157
+ addonReg as never,
158
+ eb as never,
159
+ loggingService as never,
160
+ capReg,
161
+ )
142
162
 
143
163
  // Should NOT throw
144
164
  await expect(provider.delete({ id: INTEGRATION_ID })).resolves.toEqual({
@@ -158,7 +178,12 @@ describe('integrations.delete cascade-removes devices via removeByIntegration',
158
178
  })
159
179
 
160
180
  it('warns and skips cascade when capabilityRegistry is null', async () => {
161
- const provider = buildIntegrationsProvider(addonReg as never, eb as never, loggingService as never, null)
181
+ const provider = buildIntegrationsProvider(
182
+ addonReg as never,
183
+ eb as never,
184
+ loggingService as never,
185
+ null,
186
+ )
162
187
  await provider.delete({ id: INTEGRATION_ID })
163
188
 
164
189
  expect(integrationReg.deleteIntegration).toHaveBeenCalledOnce()
@@ -172,7 +197,12 @@ describe('integrations.delete cascade-removes devices via removeByIntegration',
172
197
  it('warns and skips cascade when device-manager singleton is not registered', async () => {
173
198
  const capReg = makeCapabilityRegistry(null)
174
199
 
175
- const provider = buildIntegrationsProvider(addonReg as never, eb as never, loggingService as never, capReg)
200
+ const provider = buildIntegrationsProvider(
201
+ addonReg as never,
202
+ eb as never,
203
+ loggingService as never,
204
+ capReg,
205
+ )
176
206
  await provider.delete({ id: INTEGRATION_ID })
177
207
 
178
208
  expect(integrationReg.deleteIntegration).toHaveBeenCalledOnce()
@@ -184,7 +214,9 @@ describe('integrations.delete cascade-removes devices via removeByIntegration',
184
214
 
185
215
  it('stamps legacy un-tagged devices of the addon BEFORE cascade so they are removed too', async () => {
186
216
  const removeByIntegration = vi.fn(async () => ({ removed: 1 }))
187
- const setIntegrationId = vi.fn(async (_input: { deviceId: number; integrationId: string }) => undefined)
217
+ const setIntegrationId = vi.fn(
218
+ async (_input: { deviceId: number; integrationId: string }) => undefined,
219
+ )
188
220
  // One un-tagged top-level device of the integration's addon (claimable),
189
221
  // one already-tagged (skip), one child (skip), one other-addon (skip).
190
222
  const listAll = vi.fn(async () => [
@@ -197,7 +229,12 @@ describe('integrations.delete cascade-removes devices via removeByIntegration',
197
229
  getSingleton: vi.fn(() => ({ removeByIntegration, listAll, setIntegrationId })),
198
230
  } as unknown as CapabilityRegistry
199
231
 
200
- const provider = buildIntegrationsProvider(addonReg as never, eb as never, loggingService as never, capReg)
232
+ const provider = buildIntegrationsProvider(
233
+ addonReg as never,
234
+ eb as never,
235
+ loggingService as never,
236
+ capReg,
237
+ )
201
238
  await provider.delete({ id: INTEGRATION_ID })
202
239
 
203
240
  // Only device 4 is claimed (untagged, top-level, this addon's single integration).
@@ -209,13 +246,20 @@ describe('integrations.delete cascade-removes devices via removeByIntegration',
209
246
 
210
247
  it('claim failure does not abort the integration delete (best-effort)', async () => {
211
248
  const removeByIntegration = vi.fn(async () => ({ removed: 0 }))
212
- const setIntegrationId = vi.fn(async () => { throw new Error('stamp boom') })
249
+ const setIntegrationId = vi.fn(async () => {
250
+ throw new Error('stamp boom')
251
+ })
213
252
  const listAll = vi.fn(async () => [{ id: 4, addonId: ADDON_ID, parentDeviceId: null }])
214
253
  const capReg = {
215
254
  getSingleton: vi.fn(() => ({ removeByIntegration, listAll, setIntegrationId })),
216
255
  } as unknown as CapabilityRegistry
217
256
 
218
- const provider = buildIntegrationsProvider(addonReg as never, eb as never, loggingService as never, capReg)
257
+ const provider = buildIntegrationsProvider(
258
+ addonReg as never,
259
+ eb as never,
260
+ loggingService as never,
261
+ capReg,
262
+ )
219
263
  await expect(provider.delete({ id: INTEGRATION_ID })).resolves.toEqual({
220
264
  success: true,
221
265
  deletedId: INTEGRATION_ID,
@@ -232,7 +276,12 @@ describe('integrations.delete cascade-removes devices via removeByIntegration',
232
276
  addonReg = makeAddonRegistry(integrationReg)
233
277
  const capReg = makeCapabilityRegistry()
234
278
 
235
- const provider = buildIntegrationsProvider(addonReg as never, eb as never, loggingService as never, capReg)
279
+ const provider = buildIntegrationsProvider(
280
+ addonReg as never,
281
+ eb as never,
282
+ loggingService as never,
283
+ capReg,
284
+ )
236
285
 
237
286
  await expect(provider.delete({ id: 'missing-id' })).rejects.toThrow('not found')
238
287
 
@@ -233,7 +233,18 @@ describe('buildAddonsProvider — BulkUpdateCoordinator delegation', () => {
233
233
 
234
234
  it('listActiveBulkUpdates delegates to coordinator.list with nodeId and returns its value', async () => {
235
235
  const mockList = [
236
- { id: 'bulk-1', nodeId: 'hub', startedAtMs: 1000, total: 1, completed: 0, failed: 0, current: 'pkg-a', phase: 'regular' as const, cancelled: false, items: [] },
236
+ {
237
+ id: 'bulk-1',
238
+ nodeId: 'hub',
239
+ startedAtMs: 1000,
240
+ total: 1,
241
+ completed: 0,
242
+ failed: 0,
243
+ current: 'pkg-a',
244
+ phase: 'regular' as const,
245
+ cancelled: false,
246
+ items: [],
247
+ },
237
248
  ]
238
249
  stubs.list.mockReturnValue(mockList)
239
250
 
@@ -318,15 +329,15 @@ describe('buildAddonsProvider — listUpdates isSystem field', () => {
318
329
  const result = await env.provider.listUpdates({ nodeId: 'hub' })
319
330
 
320
331
  expect(result).toHaveLength(2)
321
- const typesRow = result.find(r => r.name === '@camstack/types')
322
- const fooRow = result.find(r => r.name === '@camstack/addon-foo')
332
+ const typesRow = result.find((r) => r.name === '@camstack/types')
333
+ const fooRow = result.find((r) => r.name === '@camstack/addon-foo')
323
334
  expect(typesRow?.isSystem).toBe(true)
324
335
  expect(fooRow?.isSystem).toBe(false)
325
336
  })
326
337
 
327
338
  it('all FRAMEWORK_PACKAGE_ALLOWLIST members get isSystem: true', async () => {
328
339
  env.psMock['checkUpdates']!.mockResolvedValue(
329
- FRAMEWORK_PACKAGE_ALLOWLIST.map(name => ({
340
+ FRAMEWORK_PACKAGE_ALLOWLIST.map((name) => ({
330
341
  name,
331
342
  currentVersion: '0.1.0',
332
343
  latestVersion: '0.1.1',
@@ -360,7 +371,10 @@ describe('buildAddonsProvider — updateFrameworkPackage deferRestart propagatio
360
371
  })
361
372
 
362
373
  expect(env.psMock['updateFrameworkPackage']).toHaveBeenCalledOnce()
363
- const callArg = env.psMock['updateFrameworkPackage']!.mock.calls[0]![0] as Record<string, unknown>
374
+ const callArg = env.psMock['updateFrameworkPackage']!.mock.calls[0]![0] as Record<
375
+ string,
376
+ unknown
377
+ >
364
378
  expect(callArg['deferRestart']).toBe(true)
365
379
  })
366
380
 
@@ -371,7 +385,10 @@ describe('buildAddonsProvider — updateFrameworkPackage deferRestart propagatio
371
385
  deferRestart: false,
372
386
  })
373
387
 
374
- const callArg = env.psMock['updateFrameworkPackage']!.mock.calls[0]![0] as Record<string, unknown>
388
+ const callArg = env.psMock['updateFrameworkPackage']!.mock.calls[0]![0] as Record<
389
+ string,
390
+ unknown
391
+ >
375
392
  expect(callArg['deferRestart']).toBe(false)
376
393
  })
377
394
 
@@ -381,7 +398,10 @@ describe('buildAddonsProvider — updateFrameworkPackage deferRestart propagatio
381
398
  version: '0.1.40',
382
399
  })
383
400
 
384
- const callArg = env.psMock['updateFrameworkPackage']!.mock.calls[0]![0] as Record<string, unknown>
401
+ const callArg = env.psMock['updateFrameworkPackage']!.mock.calls[0]![0] as Record<
402
+ string,
403
+ unknown
404
+ >
385
405
  // Either undefined or not present — both are acceptable
386
406
  expect(callArg['deferRestart']).toBeUndefined()
387
407
  })