@camstack/server 0.1.7 → 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 (135) hide show
  1. package/package.json +11 -9
  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 +206 -0
  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 +292 -0
  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 +177 -0
  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 +137 -0
  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 +265 -5
  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/__tests__/integration-markers.spec.ts +10 -0
  60. package/src/api/core/addon-settings.router.ts +4 -1
  61. package/src/api/core/agents.router.ts +52 -53
  62. package/src/api/core/auth.router.ts +55 -36
  63. package/src/api/core/bulk-update-coordinator.ts +25 -22
  64. package/src/api/core/cap-providers.ts +459 -166
  65. package/src/api/core/capabilities.router.ts +30 -23
  66. package/src/api/core/hwaccel.router.ts +37 -10
  67. package/src/api/core/live-events.router.ts +16 -9
  68. package/src/api/core/logs.router.ts +58 -25
  69. package/src/api/core/notifications.router.ts +2 -1
  70. package/src/api/core/repl.router.ts +1 -3
  71. package/src/api/core/settings-backend.router.ts +68 -70
  72. package/src/api/core/system-events.router.ts +41 -32
  73. package/src/api/health/health.routes.ts +7 -13
  74. package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +12 -2
  75. package/src/api/oauth2/consent-page.ts +4 -3
  76. package/src/api/oauth2/oauth2-routes.ts +41 -12
  77. package/src/api/trpc/__tests__/client-ip.spec.ts +27 -1
  78. package/src/api/trpc/__tests__/scope-access-device.spec.ts +68 -23
  79. package/src/api/trpc/__tests__/scope-access.spec.ts +8 -13
  80. package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +136 -0
  81. package/src/api/trpc/cap-mount-helpers.ts +64 -44
  82. package/src/api/trpc/cap-route-error-formatter.ts +17 -9
  83. package/src/api/trpc/client-ip.ts +17 -0
  84. package/src/api/trpc/core-cap-bridge.ts +3 -1
  85. package/src/api/trpc/generated-cap-mounts.ts +801 -286
  86. package/src/api/trpc/generated-cap-routers.ts +5723 -719
  87. package/src/api/trpc/scope-access.ts +7 -7
  88. package/src/api/trpc/trpc.context.ts +7 -4
  89. package/src/api/trpc/trpc.middleware.ts +4 -2
  90. package/src/api/trpc/trpc.router.ts +117 -48
  91. package/src/auth/session-cookie.ts +10 -0
  92. package/src/boot/__tests__/integration-id-backfill.spec.ts +131 -0
  93. package/src/boot/boot-config.ts +103 -122
  94. package/src/boot/integration-id-backfill.ts +109 -0
  95. package/src/boot/post-boot.service.ts +5 -3
  96. package/src/core/addon/__tests__/addon-registry-capability.test.ts +12 -3
  97. package/src/core/addon/__tests__/addon-row-manifest.spec.ts +62 -0
  98. package/src/core/addon/addon-call-gateway.ts +20 -6
  99. package/src/core/addon/addon-package.service.ts +183 -89
  100. package/src/core/addon/addon-registry.service.ts +1212 -1267
  101. package/src/core/addon/addon-row-manifest.ts +29 -0
  102. package/src/core/addon/addon-search.service.ts +2 -1
  103. package/src/core/addon/addon-settings-provider.ts +27 -7
  104. package/src/core/addon-bridge/addon-bridge.service.ts +11 -6
  105. package/src/core/addon-pages/addon-pages.service.ts +3 -1
  106. package/src/core/addon-widgets/addon-widgets.service.ts +5 -2
  107. package/src/core/agent/agent-registry.service.ts +60 -38
  108. package/src/core/auth/auth.service.spec.ts +6 -8
  109. package/src/core/config/config.service.spec.ts +1 -1
  110. package/src/core/events/event-bus.service.spec.ts +44 -21
  111. package/src/core/events/event-bus.service.ts +5 -1
  112. package/src/core/feature/feature.service.spec.ts +4 -1
  113. package/src/core/lifecycle/lifecycle-state-machine.spec.ts +8 -10
  114. package/src/core/logging/logging.service.spec.ts +61 -21
  115. package/src/core/logging/logging.service.ts +19 -5
  116. package/src/core/moleculer/cap-call-fn.spec.ts +17 -10
  117. package/src/core/moleculer/cap-call-fn.ts +5 -1
  118. package/src/core/moleculer/cap-route-authority.ts +18 -6
  119. package/src/core/moleculer/moleculer.service.ts +145 -29
  120. package/src/core/network/network-quality.service.spec.ts +7 -1
  121. package/src/core/notification/notification-wrapper.service.ts +1 -3
  122. package/src/core/notification/toast-wrapper.service.ts +1 -5
  123. package/src/core/repl/repl-engine.service.spec.ts +66 -39
  124. package/src/core/repl/repl-engine.service.ts +11 -12
  125. package/src/core/storage/storage-location-manager.spec.ts +12 -3
  126. package/src/core/streaming/stream-probe.service.ts +22 -13
  127. package/src/core/topology/topology-emitter.service.ts +5 -1
  128. package/src/launcher.ts +14 -9
  129. package/src/main.ts +658 -495
  130. package/src/manual-boot.ts +133 -154
  131. package/tsconfig.json +20 -8
  132. package/src/core/storage/settings-store.spec.ts +0 -213
  133. package/src/core/storage/settings-store.ts +0 -2
  134. package/src/core/storage/sql-schema.spec.ts +0 -140
  135. package/src/core/storage/sql-schema.ts +0 -3
@@ -0,0 +1,292 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import { buildIntegrationsProvider } from '../../api/core/cap-providers'
3
+ import type { CapabilityRegistry } from '@camstack/kernel'
4
+ import type { Integration } from '@camstack/types'
5
+
6
+ // ── Minimal stubs ────────────────────────────────────────────────────────────
7
+
8
+ const INTEGRATION_ID = 'integ-abc-123'
9
+ const ADDON_ID = 'provider-test'
10
+
11
+ function makeIntegration(id = INTEGRATION_ID): Integration {
12
+ return {
13
+ id,
14
+ addonId: ADDON_ID,
15
+ name: 'Test Integration',
16
+ enabled: true,
17
+ info: null,
18
+ settings: null,
19
+ createdAt: new Date().toISOString(),
20
+ updatedAt: new Date().toISOString(),
21
+ } as unknown as Integration
22
+ }
23
+
24
+ function makeIntegrationRegistry(integration: Integration | null = makeIntegration()) {
25
+ return {
26
+ getIntegration: vi.fn(async (_id: string) => integration),
27
+ deleteIntegration: vi.fn(async (_id: string) => undefined),
28
+ listIntegrations: vi.fn(async () => (integration ? [integration] : [])),
29
+ createIntegration: vi.fn(),
30
+ updateIntegration: vi.fn(),
31
+ getIntegrationByAddonId: vi.fn(),
32
+ getIntegrationSettings: vi.fn(),
33
+ setIntegrationSettings: vi.fn(),
34
+ }
35
+ }
36
+
37
+ function makeAddonRegistry(reg = makeIntegrationRegistry()) {
38
+ return {
39
+ getIntegrationRegistry: vi.fn(() => reg),
40
+ listAddons: vi.fn(() => []),
41
+ restartAddon: vi.fn(),
42
+ getCapabilityRegistry: vi.fn(() => ({
43
+ getProviderByAddon: vi.fn(() => null),
44
+ })),
45
+ }
46
+ }
47
+
48
+ function makeEventBus() {
49
+ return { emit: vi.fn() }
50
+ }
51
+
52
+ function makeLogger() {
53
+ return {
54
+ info: vi.fn(),
55
+ warn: vi.fn(),
56
+ error: vi.fn(),
57
+ }
58
+ }
59
+
60
+ function makeLoggingService(log = makeLogger()) {
61
+ return { createLogger: vi.fn(() => log) }
62
+ }
63
+
64
+ function makeCapabilityRegistry(
65
+ removeByIntegration:
66
+ | ((input: { integrationId: string }) => Promise<{ removed: number }>)
67
+ | null = vi.fn(async () => ({ removed: 2 })),
68
+ ): CapabilityRegistry {
69
+ return {
70
+ getSingleton: vi.fn((_cap: string) => (removeByIntegration ? { removeByIntegration } : null)),
71
+ } as unknown as CapabilityRegistry
72
+ }
73
+
74
+ // ── Tests ────────────────────────────────────────────────────────────────────
75
+
76
+ describe('integrations.delete cascade-removes devices via removeByIntegration', () => {
77
+ let integrationReg: ReturnType<typeof makeIntegrationRegistry>
78
+ let addonReg: ReturnType<typeof makeAddonRegistry>
79
+ let eb: ReturnType<typeof makeEventBus>
80
+ let log: ReturnType<typeof makeLogger>
81
+ let loggingService: ReturnType<typeof makeLoggingService>
82
+
83
+ beforeEach(() => {
84
+ integrationReg = makeIntegrationRegistry()
85
+ addonReg = makeAddonRegistry(integrationReg)
86
+ eb = makeEventBus()
87
+ log = makeLogger()
88
+ loggingService = makeLoggingService(log)
89
+ })
90
+
91
+ it('calls removeByIntegration with the integration id before deleteIntegration', async () => {
92
+ const removeByIntegration = vi.fn(async () => ({ removed: 3 }))
93
+ const capReg = makeCapabilityRegistry(removeByIntegration)
94
+
95
+ const provider = buildIntegrationsProvider(
96
+ addonReg as never,
97
+ eb as never,
98
+ loggingService as never,
99
+ capReg,
100
+ )
101
+ await provider.delete({ id: INTEGRATION_ID })
102
+
103
+ expect(removeByIntegration).toHaveBeenCalledOnce()
104
+ expect(removeByIntegration).toHaveBeenCalledWith({ integrationId: INTEGRATION_ID })
105
+ })
106
+
107
+ it('still deletes the integration record and emits integration.deleted after the cascade', async () => {
108
+ const capReg = makeCapabilityRegistry()
109
+
110
+ const provider = buildIntegrationsProvider(
111
+ addonReg as never,
112
+ eb as never,
113
+ loggingService as never,
114
+ capReg,
115
+ )
116
+ const result = await provider.delete({ id: INTEGRATION_ID })
117
+
118
+ expect(integrationReg.deleteIntegration).toHaveBeenCalledOnce()
119
+ expect(integrationReg.deleteIntegration).toHaveBeenCalledWith(INTEGRATION_ID)
120
+
121
+ expect(eb.emit).toHaveBeenCalledOnce()
122
+ expect(eb.emit).toHaveBeenCalledWith(
123
+ expect.objectContaining({
124
+ category: 'integration.deleted',
125
+ data: expect.objectContaining({ integrationId: INTEGRATION_ID }),
126
+ }),
127
+ )
128
+
129
+ expect(result).toEqual({ success: true, deletedId: INTEGRATION_ID })
130
+ })
131
+
132
+ it('logs the removed count from removeByIntegration', async () => {
133
+ const removeByIntegration = vi.fn(async () => ({ removed: 5 }))
134
+ const capReg = makeCapabilityRegistry(removeByIntegration)
135
+
136
+ const provider = buildIntegrationsProvider(
137
+ addonReg as never,
138
+ eb as never,
139
+ loggingService as never,
140
+ capReg,
141
+ )
142
+ await provider.delete({ id: INTEGRATION_ID })
143
+
144
+ expect(log.info).toHaveBeenCalledWith(
145
+ 'cascade-removed devices',
146
+ expect.objectContaining({ meta: expect.objectContaining({ removed: 5 }) }),
147
+ )
148
+ })
149
+
150
+ it('does NOT abort the integration delete when removeByIntegration throws (best-effort)', async () => {
151
+ const removeByIntegration = vi.fn(async () => {
152
+ throw new Error('device-manager transient error')
153
+ })
154
+ const capReg = makeCapabilityRegistry(removeByIntegration)
155
+
156
+ const provider = buildIntegrationsProvider(
157
+ addonReg as never,
158
+ eb as never,
159
+ loggingService as never,
160
+ capReg,
161
+ )
162
+
163
+ // Should NOT throw
164
+ await expect(provider.delete({ id: INTEGRATION_ID })).resolves.toEqual({
165
+ success: true,
166
+ deletedId: INTEGRATION_ID,
167
+ })
168
+
169
+ // Integration record deletion and event still fire
170
+ expect(integrationReg.deleteIntegration).toHaveBeenCalledOnce()
171
+ expect(eb.emit).toHaveBeenCalledOnce()
172
+
173
+ // A warning is logged
174
+ expect(log.warn).toHaveBeenCalledWith(
175
+ 'device cascade-remove failed (best-effort — continuing)',
176
+ expect.anything(),
177
+ )
178
+ })
179
+
180
+ it('warns and skips cascade when capabilityRegistry is null', async () => {
181
+ const provider = buildIntegrationsProvider(
182
+ addonReg as never,
183
+ eb as never,
184
+ loggingService as never,
185
+ null,
186
+ )
187
+ await provider.delete({ id: INTEGRATION_ID })
188
+
189
+ expect(integrationReg.deleteIntegration).toHaveBeenCalledOnce()
190
+ expect(eb.emit).toHaveBeenCalledOnce()
191
+ expect(log.warn).toHaveBeenCalledWith(
192
+ 'device-manager not available — skipping cascade device removal',
193
+ expect.anything(),
194
+ )
195
+ })
196
+
197
+ it('warns and skips cascade when device-manager singleton is not registered', async () => {
198
+ const capReg = makeCapabilityRegistry(null)
199
+
200
+ const provider = buildIntegrationsProvider(
201
+ addonReg as never,
202
+ eb as never,
203
+ loggingService as never,
204
+ capReg,
205
+ )
206
+ await provider.delete({ id: INTEGRATION_ID })
207
+
208
+ expect(integrationReg.deleteIntegration).toHaveBeenCalledOnce()
209
+ expect(log.warn).toHaveBeenCalledWith(
210
+ 'device-manager not available — skipping cascade device removal',
211
+ expect.anything(),
212
+ )
213
+ })
214
+
215
+ it('stamps legacy un-tagged devices of the addon BEFORE cascade so they are removed too', async () => {
216
+ const removeByIntegration = vi.fn(async () => ({ removed: 1 }))
217
+ const setIntegrationId = vi.fn(
218
+ async (_input: { deviceId: number; integrationId: string }) => undefined,
219
+ )
220
+ // One un-tagged top-level device of the integration's addon (claimable),
221
+ // one already-tagged (skip), one child (skip), one other-addon (skip).
222
+ const listAll = vi.fn(async () => [
223
+ { id: 4, addonId: ADDON_ID, parentDeviceId: null },
224
+ { id: 5, addonId: ADDON_ID, parentDeviceId: null, integrationId: INTEGRATION_ID },
225
+ { id: 6, addonId: ADDON_ID, parentDeviceId: 4 },
226
+ { id: 7, addonId: 'provider-other', parentDeviceId: null },
227
+ ])
228
+ const capReg = {
229
+ getSingleton: vi.fn(() => ({ removeByIntegration, listAll, setIntegrationId })),
230
+ } as unknown as CapabilityRegistry
231
+
232
+ const provider = buildIntegrationsProvider(
233
+ addonReg as never,
234
+ eb as never,
235
+ loggingService as never,
236
+ capReg,
237
+ )
238
+ await provider.delete({ id: INTEGRATION_ID })
239
+
240
+ // Only device 4 is claimed (untagged, top-level, this addon's single integration).
241
+ expect(setIntegrationId).toHaveBeenCalledOnce()
242
+ expect(setIntegrationId).toHaveBeenCalledWith({ deviceId: 4, integrationId: INTEGRATION_ID })
243
+ // Cascade still runs after the claim.
244
+ expect(removeByIntegration).toHaveBeenCalledWith({ integrationId: INTEGRATION_ID })
245
+ })
246
+
247
+ it('claim failure does not abort the integration delete (best-effort)', async () => {
248
+ const removeByIntegration = vi.fn(async () => ({ removed: 0 }))
249
+ const setIntegrationId = vi.fn(async () => {
250
+ throw new Error('stamp boom')
251
+ })
252
+ const listAll = vi.fn(async () => [{ id: 4, addonId: ADDON_ID, parentDeviceId: null }])
253
+ const capReg = {
254
+ getSingleton: vi.fn(() => ({ removeByIntegration, listAll, setIntegrationId })),
255
+ } as unknown as CapabilityRegistry
256
+
257
+ const provider = buildIntegrationsProvider(
258
+ addonReg as never,
259
+ eb as never,
260
+ loggingService as never,
261
+ capReg,
262
+ )
263
+ await expect(provider.delete({ id: INTEGRATION_ID })).resolves.toEqual({
264
+ success: true,
265
+ deletedId: INTEGRATION_ID,
266
+ })
267
+ expect(integrationReg.deleteIntegration).toHaveBeenCalledOnce()
268
+ expect(log.warn).toHaveBeenCalledWith(
269
+ 'legacy device claim failed (best-effort — continuing)',
270
+ expect.anything(),
271
+ )
272
+ })
273
+
274
+ it('throws when the integration is not found (guard still fires before cascade)', async () => {
275
+ integrationReg = makeIntegrationRegistry(null)
276
+ addonReg = makeAddonRegistry(integrationReg)
277
+ const capReg = makeCapabilityRegistry()
278
+
279
+ const provider = buildIntegrationsProvider(
280
+ addonReg as never,
281
+ eb as never,
282
+ loggingService as never,
283
+ capReg,
284
+ )
285
+
286
+ await expect(provider.delete({ id: 'missing-id' })).rejects.toThrow('not found')
287
+
288
+ // Nothing should have been cleaned up
289
+ expect(integrationReg.deleteIntegration).not.toHaveBeenCalled()
290
+ expect(eb.emit).not.toHaveBeenCalled()
291
+ })
292
+ })
@@ -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
  })
@@ -52,9 +52,7 @@ function makeNodeRegistry(nodes: ReadonlyMap<string, readonly StubNodeEntry[]>)
52
52
  // Helpers for stub CapabilityService
53
53
  // ---------------------------------------------------------------------------
54
54
 
55
- function makeCapabilityService(
56
- providers: ReadonlyMap<string, Record<string, unknown>>,
57
- ) {
55
+ function makeCapabilityService(providers: ReadonlyMap<string, Record<string, unknown>>) {
58
56
  return {
59
57
  getSingleton<T>(capability: string): T | null {
60
58
  return (providers.get(capability) as T) ?? null
@@ -68,7 +66,10 @@ function makeCapabilityService(
68
66
 
69
67
  describe('createNodeCapAuthority', () => {
70
68
  const nodes = new Map([
71
- ['hub/stream-broker', [{ addonId: 'addon-stream-broker', capabilities: ['stream-broker', 'stream-params'] }]],
69
+ [
70
+ 'hub/stream-broker',
71
+ [{ addonId: 'addon-stream-broker', capabilities: ['stream-broker', 'stream-params'] }],
72
+ ],
72
73
  ['dev-agent-0', [{ addonId: 'addon-detection-pipeline', capabilities: ['pipeline-executor'] }]],
73
74
  ])
74
75
  const registry = makeNodeRegistry(nodes)
@@ -86,7 +87,9 @@ describe('createNodeCapAuthority', () => {
86
87
 
87
88
  it('getAddonId returns the addonId for a known cap', () => {
88
89
  expect(authority.getAddonId('hub/stream-broker', 'stream-broker')).toBe('addon-stream-broker')
89
- expect(authority.getAddonId('dev-agent-0', 'pipeline-executor')).toBe('addon-detection-pipeline')
90
+ expect(authority.getAddonId('dev-agent-0', 'pipeline-executor')).toBe(
91
+ 'addon-detection-pipeline',
92
+ )
90
93
  })
91
94
 
92
95
  it('getAddonId returns null for missing nodes or caps', () => {
@@ -126,10 +129,13 @@ describe('createNodeCapAuthority', () => {
126
129
  describe('createNodeCapAuthority — per-node singleton override', () => {
127
130
  it('getAddonId honors the per-node singleton override when available', () => {
128
131
  const nodeRegistry = {
129
- getNodeManifest: (id: string) => id === 'dev-agent-0'
130
- ? [{ addonId: 'webrtc-native', capabilities: ['webrtc-session'] },
131
- { addonId: 'stream-broker', capabilities: ['webrtc-session'] }]
132
- : undefined,
132
+ getNodeManifest: (id: string) =>
133
+ id === 'dev-agent-0'
134
+ ? [
135
+ { addonId: 'webrtc-native', capabilities: ['webrtc-session'] },
136
+ { addonId: 'stream-broker', capabilities: ['webrtc-session'] },
137
+ ]
138
+ : undefined,
133
139
  listNodeIds: () => ['hub', 'dev-agent-0'],
134
140
  }
135
141
  const authority = createNodeCapAuthority(nodeRegistry, {
@@ -141,9 +147,10 @@ describe('createNodeCapAuthority — per-node singleton override', () => {
141
147
 
142
148
  it('getAddonId falls back to first manifest match without an override', () => {
143
149
  const nodeRegistry = {
144
- getNodeManifest: (id: string) => id === 'dev-agent-0'
145
- ? [{ addonId: 'webrtc-native', capabilities: ['webrtc-session'] }]
146
- : undefined,
150
+ getNodeManifest: (id: string) =>
151
+ id === 'dev-agent-0'
152
+ ? [{ addonId: 'webrtc-native', capabilities: ['webrtc-session'] }]
153
+ : undefined,
147
154
  listNodeIds: () => ['hub', 'dev-agent-0'],
148
155
  }
149
156
  const authority = createNodeCapAuthority(nodeRegistry, { resolveSingleton: () => null })
@@ -186,7 +193,9 @@ describe('createInProcessProviderLookup', () => {
186
193
  expect(ref).not.toBeNull()
187
194
 
188
195
  await expect(ref!.invoke('notAFn', {})).rejects.toThrow(/method "notAFn" not found/)
189
- await expect(ref!.invoke('missingMethod', {})).rejects.toThrow(/method "missingMethod" not found/)
196
+ await expect(ref!.invoke('missingMethod', {})).rejects.toThrow(
197
+ /method "missingMethod" not found/,
198
+ )
190
199
  })
191
200
  })
192
201
 
@@ -201,7 +210,9 @@ describe('Resolver + adapters — end-to-end dispatch', () => {
201
210
  return vi.fn(async (_childId: string, _input: unknown) => ({ ok: true, from: 'uds' }))
202
211
  }
203
212
 
204
- function makeHubLocalRegistry(caps: ReadonlyMap<string, string>): HubLocalChildDispatcher & { callSpy: ReturnType<typeof vi.fn> } {
213
+ function makeHubLocalRegistry(
214
+ caps: ReadonlyMap<string, string>,
215
+ ): HubLocalChildDispatcher & { callSpy: ReturnType<typeof vi.fn> } {
205
216
  const callSpy = makeCallCapOnChildSpy()
206
217
  return {
207
218
  resolveChildId: (capName: string) => caps.get(capName) ?? null,
@@ -284,6 +295,8 @@ describe('Resolver + adapters — end-to-end dispatch', () => {
284
295
  expect(thrown).toBeInstanceOf(CapRouteError)
285
296
  expect((thrown as CapRouteError).reason).toBe('no-provider')
286
297
  // Must NOT be the old opaque string
287
- expect((thrown as CapRouteError).message).not.toContain('Capability "ghost-cap" not available on node')
298
+ expect((thrown as CapRouteError).message).not.toContain(
299
+ 'Capability "ghost-cap" not available on node',
300
+ )
288
301
  })
289
302
  })
@@ -1,4 +1,3 @@
1
-
2
1
  /**
3
2
  * Meta-test: ensures that every capability with methods has a corresponding
4
3
  * `<cap-name>.router.spec.ts` file in this directory. This prevents new caps
@@ -40,7 +39,7 @@ function collectCapabilitiesWithMethods(): readonly CapabilityDefinition[] {
40
39
  if (Object.keys(value.methods).length === 0) continue
41
40
  out.push(value)
42
41
  }
43
- return out.sort((a, b) => a.name.localeCompare(b.name))
42
+ return out.toSorted((a, b) => a.name.localeCompare(b.name))
44
43
  }
45
44
 
46
45
  function specFileNameFor(capName: string): string {
@@ -51,7 +50,7 @@ describe('cap-routers meta', () => {
51
50
  const caps = collectCapabilitiesWithMethods()
52
51
  const specDir = path.dirname(new URL(import.meta.url).pathname)
53
52
  const existingSpecs = new Set(
54
- fs.readdirSync(specDir).filter(f => f.endsWith('.router.spec.ts')),
53
+ fs.readdirSync(specDir).filter((f) => f.endsWith('.router.spec.ts')),
55
54
  )
56
55
 
57
56
  it('discovers at least one capability with methods', () => {
@@ -79,7 +78,7 @@ describe('cap-routers meta', () => {
79
78
  })
80
79
 
81
80
  it('ALLOWED_MISSING only references real capabilities', () => {
82
- const names = new Set(caps.map(c => c.name))
81
+ const names = new Set(caps.map((c) => c.name))
83
82
  for (const name of ALLOWED_MISSING) {
84
83
  expect(names, `ALLOWED_MISSING references unknown cap "${name}"`).toContain(name)
85
84
  }
@@ -180,8 +179,8 @@ describe('cap-routers meta', () => {
180
179
  expect(
181
180
  outputCount,
182
181
  `output-validation codegen drift — expected at least ${procedureCount} .output() ` +
183
- `calls (one per query/mutation), found ${outputCount}. ` +
184
- `Re-run: npx tsx scripts/generate-cap-routers.ts`,
182
+ `calls (one per query/mutation), found ${outputCount}. ` +
183
+ `Re-run: npx tsx scripts/generate-cap-routers.ts`,
185
184
  ).toBeGreaterThanOrEqual(procedureCount)
186
185
  })
187
186
 
@@ -193,7 +192,7 @@ describe('cap-routers meta', () => {
193
192
  const outputCount = (generatedSource.match(/\.output\(/g) ?? []).length
194
193
  console.log(
195
194
  `[output-validation codegen] queries=${queryCount} mutations=${mutationCount} ` +
196
- `subscriptions=${subscriptionCount} outputs=${outputCount}`,
195
+ `subscriptions=${subscriptionCount} outputs=${outputCount}`,
197
196
  )
198
197
  })
199
198
  })
@@ -31,9 +31,12 @@ describe('addon-settings cap router', () => {
31
31
  it('getGlobalSettings returns schema', async () => {
32
32
  const provider = makeMockProvider()
33
33
  const router = createCapRouter_addonSettings(() => provider)
34
- const result = await invokeProcedure(router, 'getGlobalSettings', makeCtx('admin'), { addonId: 'test' })
34
+ const result = await invokeProcedure(router, 'getGlobalSettings', makeCtx('admin'), {
35
+ addonId: 'test',
36
+ })
35
37
  expect(result.ok).toBe(true)
36
- if (result.ok) expect(result.value).toEqual({ sections: [{ id: 'g', title: 'Global', fields: [] }] })
38
+ if (result.ok)
39
+ expect(result.value).toEqual({ sections: [{ id: 'g', title: 'Global', fields: [] }] })
37
40
  expect(provider.getGlobalSettings).toHaveBeenCalledWith({ addonId: 'test' })
38
41
  })
39
42
 
@@ -41,7 +44,8 @@ describe('addon-settings cap router', () => {
41
44
  const provider = makeMockProvider()
42
45
  const router = createCapRouter_addonSettings(() => provider)
43
46
  const result = await invokeProcedure(router, 'updateGlobalSettings', makeCtx('admin'), {
44
- addonId: 'test', patch: { volume: 50 },
47
+ addonId: 'test',
48
+ patch: { volume: 50 },
45
49
  })
46
50
  expect(result.ok).toBe(true)
47
51
  if (result.ok) expect(result.value).toEqual({ success: true })
@@ -51,7 +55,8 @@ describe('addon-settings cap router', () => {
51
55
  const provider = makeMockProvider()
52
56
  const router = createCapRouter_addonSettings(() => provider)
53
57
  await invokeProcedure(router, 'getDeviceSettings', makeCtx('admin'), {
54
- addonId: 'test', deviceId: 1,
58
+ addonId: 'test',
59
+ deviceId: 1,
55
60
  })
56
61
  expect(provider.getDeviceSettings).toHaveBeenCalledWith({ addonId: 'test', deviceId: 1 })
57
62
  })
@@ -60,16 +65,22 @@ describe('addon-settings cap router', () => {
60
65
  const provider = makeMockProvider()
61
66
  const router = createCapRouter_addonSettings(() => provider)
62
67
  await invokeProcedure(router, 'updateDeviceSettings', makeCtx('admin'), {
63
- addonId: 'test', deviceId: 1, patch: { enabled: false },
68
+ addonId: 'test',
69
+ deviceId: 1,
70
+ patch: { enabled: false },
64
71
  })
65
72
  expect(provider.updateDeviceSettings).toHaveBeenCalledWith({
66
- addonId: 'test', deviceId: 1, patch: { enabled: false },
73
+ addonId: 'test',
74
+ deviceId: 1,
75
+ patch: { enabled: false },
67
76
  })
68
77
  })
69
78
 
70
79
  it('returns PRECONDITION_FAILED when provider is null', async () => {
71
80
  const router = createCapRouter_addonSettings(() => null)
72
- const result = await invokeProcedure(router, 'getGlobalSettings', makeCtx('admin'), { addonId: 'x' })
81
+ const result = await invokeProcedure(router, 'getGlobalSettings', makeCtx('admin'), {
82
+ addonId: 'x',
83
+ })
73
84
  expect(result.ok).toBe(false)
74
85
  if (!result.ok) expect(result.code).toBe('PRECONDITION_FAILED')
75
86
  })
@@ -78,9 +89,7 @@ describe('addon-settings cap router', () => {
78
89
  const provider = makeMockProvider()
79
90
  const router = createCapRouter_addonSettings(() => provider)
80
91
  for (const method of ['getGlobalSettings', 'getDeviceSettings']) {
81
- const input = method.includes('Device')
82
- ? { addonId: 'x', deviceId: 1 }
83
- : { addonId: 'x' }
92
+ const input = method.includes('Device') ? { addonId: 'x', deviceId: 1 } : { addonId: 'x' }
84
93
  const results = await checkAuthMatrix(router, method, 'protected', input)
85
94
  for (const r of results) {
86
95
  if (r.allowed) expect(r.outcome.ok).toBe(true)