@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
@@ -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)
@@ -1,4 +1,3 @@
1
-
2
1
  /**
3
2
  * Routing regression spec for the `broker` system-collection cap.
4
3
  *
@@ -98,7 +97,10 @@ function makeSelector(): (addonId?: string) => IBrokerProvider | null {
98
97
  return {
99
98
  ...first,
100
99
  list: concatCollection(providers, 'list') as IBrokerProvider['list'],
101
- listProviders: concatCollection(providers, 'listProviders') as IBrokerProvider['listProviders'],
100
+ listProviders: concatCollection(
101
+ providers,
102
+ 'listProviders',
103
+ ) as IBrokerProvider['listProviders'],
102
104
  }
103
105
  }
104
106
  }
@@ -113,7 +115,7 @@ describe('broker cap — addonId ownership routing', () => {
113
115
  expect(outcome.ok).toBe(true)
114
116
  if (!outcome.ok) return
115
117
  const rows = outcome.value as BrokerInfo[]
116
- const byId = new Map(rows.map(r => [r.id, r]))
118
+ const byId = new Map(rows.map((r) => [r.id, r]))
117
119
  expect(byId.get('mqtt_1')?.addonId).toBe('mqtt')
118
120
  expect(byId.get('ha_1')?.addonId).toBe('ha')
119
121
  })
@@ -122,7 +124,10 @@ describe('broker cap — addonId ownership routing', () => {
122
124
  const selector = makeSelector()
123
125
  const router = createCapRouter_broker((_ctx, addonId) => selector(addonId))
124
126
 
125
- const outcome = await invokeProcedure(router, 'get', makeCtx('admin'), { id: 'ha_1', addonId: 'ha' })
127
+ const outcome = await invokeProcedure(router, 'get', makeCtx('admin'), {
128
+ id: 'ha_1',
129
+ addonId: 'ha',
130
+ })
126
131
 
127
132
  expect(outcome.ok).toBe(true)
128
133
  if (!outcome.ok) return
@@ -147,7 +152,10 @@ describe('broker cap — addonId ownership routing', () => {
147
152
  const selector = makeSelector()
148
153
  const router = createCapRouter_broker((_ctx, addonId) => selector(addonId))
149
154
 
150
- const outcome = await invokeProcedure(router, 'testConnection', makeCtx('admin'), { id: 'ha_1', addonId: 'ha' })
155
+ const outcome = await invokeProcedure(router, 'testConnection', makeCtx('admin'), {
156
+ id: 'ha_1',
157
+ addonId: 'ha',
158
+ })
151
159
 
152
160
  expect(outcome.ok).toBe(true)
153
161
  if (!outcome.ok) return
@@ -163,7 +171,7 @@ describe('broker cap — addonId ownership routing', () => {
163
171
  expect(outcome.ok).toBe(true)
164
172
  if (!outcome.ok) return
165
173
  const entries = outcome.value as BrokerProviderInfo[]
166
- const addonIds = entries.map(e => e.addonId).sort()
174
+ const addonIds = entries.map((e) => e.addonId).toSorted()
167
175
  expect(addonIds).toEqual(['ha', 'mqtt'])
168
176
  })
169
177
  })
@@ -13,7 +13,9 @@ import { formatTrpcError } from '../../api/trpc/cap-route-error-formatter.js'
13
13
  // ── Helpers ──────────────────────────────────────────────────────────────────
14
14
 
15
15
  /** Minimal DefaultErrorShape stub. */
16
- function makeShape(overrides: Partial<{ message: string }> = {}): Parameters<typeof formatTrpcError>[0]['shape'] {
16
+ function makeShape(
17
+ overrides: Partial<{ message: string }> = {},
18
+ ): Parameters<typeof formatTrpcError>[0]['shape'] {
17
19
  return {
18
20
  message: overrides.message ?? 'Something went wrong',
19
21
  code: -32603, // INTERNAL_SERVER_ERROR code number
@@ -15,14 +15,25 @@ import { createCapabilitiesRouter } from '../../api/core/capabilities.router.js'
15
15
  import { makeCtx } from './harness.js'
16
16
 
17
17
  function harness() {
18
- const calls: { setActiveSingleton: unknown[][]; clear: unknown[][] } = { setActiveSingleton: [], clear: [] }
18
+ const calls: { setActiveSingleton: unknown[][]; clear: unknown[][] } = {
19
+ setActiveSingleton: [],
20
+ clear: [],
21
+ }
19
22
  const sets: Record<string, unknown> = {}
20
23
  const registry = {
21
- setActiveSingleton: vi.fn(async (...a: unknown[]) => { calls.setActiveSingleton.push(a) }),
22
- clearSingletonNodeOverride: vi.fn((...a: unknown[]) => { calls.clear.push(a) }),
24
+ setActiveSingleton: vi.fn(async (...a: unknown[]) => {
25
+ calls.setActiveSingleton.push(a)
26
+ }),
27
+ clearSingletonNodeOverride: vi.fn((...a: unknown[]) => {
28
+ calls.clear.push(a)
29
+ }),
23
30
  listCapabilities: () => [],
24
31
  } as unknown as CapabilityRegistry
25
- const config = { set: (k: string, v: unknown) => { sets[k] = v } } as unknown as ConfigService
32
+ const config = {
33
+ set: (k: string, v: unknown) => {
34
+ sets[k] = v
35
+ },
36
+ } as unknown as ConfigService
26
37
  const router = createCapabilitiesRouter(registry, config)
27
38
  return { router, calls, sets, registry }
28
39
  }
@@ -32,7 +43,9 @@ describe('capabilities.router — per-node singleton', () => {
32
43
  const { router, calls, sets } = harness()
33
44
  const caller = router.createCaller(makeCtx('admin'))
34
45
  await caller.setActiveSingleton({
35
- capability: 'webrtc-session', addonId: 'webrtc-native', nodeId: 'dev-agent-0',
46
+ capability: 'webrtc-session',
47
+ addonId: 'webrtc-native',
48
+ nodeId: 'dev-agent-0',
36
49
  })
37
50
  expect(calls.setActiveSingleton[0]).toEqual(['webrtc-session', 'webrtc-native', 'dev-agent-0'])
38
51
  expect(sets['capabilities.singletonNode.webrtc-session.dev-agent-0']).toBe('webrtc-native')
@@ -84,8 +84,9 @@ describe('requireDeviceScoped — getStatus overlay via resolveLinkedStatus', ()
84
84
  expect(dispatcher).not.toBeNull()
85
85
 
86
86
  // Call via the Proxy — method is resolved lazily
87
- const result = await (dispatcher as unknown as { getStatus: (i: { deviceId: number }) => Promise<unknown> })
88
- .getStatus({ deviceId: DEVICE_ID })
87
+ const result = await (
88
+ dispatcher as unknown as { getStatus: (i: { deviceId: number }) => Promise<unknown> }
89
+ ).getStatus({ deviceId: DEVICE_ID })
89
90
 
90
91
  expect(result).toEqual(overlaid)
91
92
  expect(result).not.toEqual(base)
@@ -102,8 +103,9 @@ describe('requireDeviceScoped — getStatus overlay via resolveLinkedStatus', ()
102
103
  const dispatcher = requireDeviceScoped(registry, CAP_NAME)
103
104
  expect(dispatcher).not.toBeNull()
104
105
 
105
- const result = await (dispatcher as unknown as { getStatus: (i: { deviceId: number }) => Promise<unknown> })
106
- .getStatus({ deviceId: DEVICE_ID })
106
+ const result = await (
107
+ dispatcher as unknown as { getStatus: (i: { deviceId: number }) => Promise<unknown> }
108
+ ).getStatus({ deviceId: DEVICE_ID })
107
109
 
108
110
  expect(result).toEqual(base)
109
111
  })
@@ -122,8 +124,11 @@ describe('requireDeviceScoped — getStatus overlay via resolveLinkedStatus', ()
122
124
  const dispatcher = requireDeviceScoped(registry, CAP_NAME)
123
125
  expect(dispatcher).not.toBeNull()
124
126
 
125
- await (dispatcher as unknown as { setFanSpeed: (i: { deviceId: number; speed: string }) => Promise<void> })
126
- .setFanSpeed({ deviceId: DEVICE_ID, speed: 'high' })
127
+ await (
128
+ dispatcher as unknown as {
129
+ setFanSpeed: (i: { deviceId: number; speed: string }) => Promise<void>
130
+ }
131
+ ).setFanSpeed({ deviceId: DEVICE_ID, speed: 'high' })
127
132
 
128
133
  expect(nativeSetFanSpeed).toHaveBeenCalledOnce()
129
134
  // The overlay path must NOT be consulted for mutations
@@ -24,25 +24,54 @@ function makeProvider(): IDeviceManagerProvider {
24
24
  title: 'Identity',
25
25
  tab: 'general',
26
26
  order: 0,
27
- fields: [{ type: 'text', key: '_stableId', label: 'Stable ID', readonlyField: true, value: String(deviceId) }],
27
+ fields: [
28
+ {
29
+ type: 'text',
30
+ key: '_stableId',
31
+ label: 'Stable ID',
32
+ readonlyField: true,
33
+ value: String(deviceId),
34
+ },
35
+ ],
28
36
  },
29
37
  {
30
38
  id: 'motion-tuning',
31
39
  title: 'Motion Tuning',
32
40
  tab: 'detection',
33
41
  order: 5,
34
- fields: [{ type: 'number', key: 'threshold', label: 'Threshold', writerCapName: 'motion-detection', writerAddonId: 'motion-wasm', source: 'settings', value: 20 }],
42
+ fields: [
43
+ {
44
+ type: 'number',
45
+ key: 'threshold',
46
+ label: 'Threshold',
47
+ writerCapName: 'motion-detection',
48
+ writerAddonId: 'motion-wasm',
49
+ source: 'settings',
50
+ value: 20,
51
+ },
52
+ ],
35
53
  },
36
54
  ],
37
55
  })),
38
56
  getDeviceLiveInfoAggregate: vi.fn(async ({ deviceId }) => ({
39
- sections: [{
40
- id: 'orchestrator-live',
41
- title: 'Pipeline Status',
42
- tab: 'detection',
43
- order: 100,
44
- fields: [{ type: 'text', key: 'assignedRunner', label: 'Assigned Runner', readonlyField: true, source: 'live', value: deviceId === 1 ? 'hub' : '' }],
45
- }],
57
+ sections: [
58
+ {
59
+ id: 'orchestrator-live',
60
+ title: 'Pipeline Status',
61
+ tab: 'detection',
62
+ order: 100,
63
+ fields: [
64
+ {
65
+ type: 'text',
66
+ key: 'assignedRunner',
67
+ label: 'Assigned Runner',
68
+ readonlyField: true,
69
+ source: 'live',
70
+ value: deviceId === 1 ? 'hub' : '',
71
+ },
72
+ ],
73
+ },
74
+ ],
46
75
  })),
47
76
  updateDeviceField: vi.fn(async () => ({ success: true as const })),
48
77
 
@@ -60,14 +89,21 @@ function makeProvider(): IDeviceManagerProvider {
60
89
  getStreamSources: vi.fn(async () => []) as IDeviceManagerProvider['getStreamSources'],
61
90
  getConfigSchema: vi.fn(async () => []) as IDeviceManagerProvider['getConfigSchema'],
62
91
  getSettingsSchema: vi.fn(async () => null) as IDeviceManagerProvider['getSettingsSchema'],
63
- updateConfig: vi.fn(async () => ({ success: true as const })) as IDeviceManagerProvider['updateConfig'],
92
+ updateConfig: vi.fn(async () => ({
93
+ success: true as const,
94
+ })) as IDeviceManagerProvider['updateConfig'],
64
95
  enable: vi.fn(async () => ({ success: true as const })) as IDeviceManagerProvider['enable'],
65
96
  disable: vi.fn(async () => ({ success: true as const })) as IDeviceManagerProvider['disable'],
66
97
  remove: vi.fn(async () => ({ success: true as const })) as IDeviceManagerProvider['remove'],
67
98
  getStreamProfileMap: vi.fn(async () => ({})) as IDeviceManagerProvider['getStreamProfileMap'],
68
- setStreamProfileMap: vi.fn(async () => ({ success: true as const })) as IDeviceManagerProvider['setStreamProfileMap'],
99
+ setStreamProfileMap: vi.fn(async () => ({
100
+ success: true as const,
101
+ })) as IDeviceManagerProvider['setStreamProfileMap'],
69
102
  probeStreams: vi.fn(async () => []) as IDeviceManagerProvider['probeStreams'],
70
- getBindings: vi.fn(async ({ deviceId }: { deviceId: number }) => ({ deviceId, entries: [] })) as IDeviceManagerProvider['getBindings'],
103
+ getBindings: vi.fn(async ({ deviceId }: { deviceId: number }) => ({
104
+ deviceId,
105
+ entries: [],
106
+ })) as IDeviceManagerProvider['getBindings'],
71
107
  setWrapperActive: vi.fn() as IDeviceManagerProvider['setWrapperActive'],
72
108
  }
73
109
  }
@@ -78,24 +114,32 @@ describe('device-manager aggregator via tRPC router', () => {
78
114
  it('getDeviceSettingsAggregate returns the merged shape for a known device', async () => {
79
115
  const provider = makeProvider()
80
116
  const router = createCapRouter_deviceManager(() => provider)
81
- const result = await invokeProcedure(router, 'getDeviceSettingsAggregate', makeCtx('admin'), { deviceId: DEVICE_ID })
117
+ const result = await invokeProcedure(router, 'getDeviceSettingsAggregate', makeCtx('admin'), {
118
+ deviceId: DEVICE_ID,
119
+ })
82
120
 
83
121
  expect(result.ok).toBe(true)
84
122
  if (!result.ok) return
85
- const value = result.value as { sections: readonly { id: string; tab?: string; fields: readonly unknown[] }[] }
86
- expect(value.sections.map(s => s.id)).toEqual(['identity', 'motion-tuning'])
123
+ const value = result.value as {
124
+ sections: readonly { id: string; tab?: string; fields: readonly unknown[] }[]
125
+ }
126
+ expect(value.sections.map((s) => s.id)).toEqual(['identity', 'motion-tuning'])
87
127
  expect(provider.getDeviceSettingsAggregate).toHaveBeenCalledWith({ deviceId: DEVICE_ID })
88
128
  })
89
129
 
90
130
  it('getDeviceLiveInfoAggregate returns live sections', async () => {
91
131
  const provider = makeProvider()
92
132
  const router = createCapRouter_deviceManager(() => provider)
93
- const result = await invokeProcedure(router, 'getDeviceLiveInfoAggregate', makeCtx('admin'), { deviceId: DEVICE_ID })
133
+ const result = await invokeProcedure(router, 'getDeviceLiveInfoAggregate', makeCtx('admin'), {
134
+ deviceId: DEVICE_ID,
135
+ })
94
136
 
95
137
  expect(result.ok).toBe(true)
96
138
  if (!result.ok) return
97
- const value = result.value as { sections: readonly { fields: readonly { key?: string; value?: unknown }[] }[] }
98
- const runner = value.sections[0]!.fields.find(f => f.key === 'assignedRunner')
139
+ const value = result.value as {
140
+ sections: readonly { fields: readonly { key?: string; value?: unknown }[] }[]
141
+ }
142
+ const runner = value.sections[0]!.fields.find((f) => f.key === 'assignedRunner')
99
143
  expect(runner?.value).toBe('hub')
100
144
  })
101
145
 
@@ -124,7 +168,13 @@ describe('device-manager aggregator via tRPC router', () => {
124
168
  const provider = makeProvider()
125
169
  const router = createCapRouter_deviceManager(() => provider)
126
170
 
127
- const payload = { deviceId: DEVICE_ID, writerCapName: 'motion-detection', writerAddonId: 'motion-wasm', key: 'threshold', value: 1 }
171
+ const payload = {
172
+ deviceId: DEVICE_ID,
173
+ writerCapName: 'motion-detection',
174
+ writerAddonId: 'motion-wasm',
175
+ key: 'threshold',
176
+ value: 1,
177
+ }
128
178
  const viewer = await invokeProcedure(router, 'updateDeviceField', makeCtx('user'), payload)
129
179
  expect(viewer.ok).toBe(false)
130
180
  if (!viewer.ok) expect(viewer.code).toBe('FORBIDDEN')
@@ -135,7 +185,9 @@ describe('device-manager aggregator via tRPC router', () => {
135
185
 
136
186
  it('router returns PRECONDITION_FAILED when the provider is missing', async () => {
137
187
  const router = createCapRouter_deviceManager(() => null)
138
- const result = await invokeProcedure(router, 'getDeviceSettingsAggregate', makeCtx('admin'), { deviceId: DEVICE_ID })
188
+ const result = await invokeProcedure(router, 'getDeviceSettingsAggregate', makeCtx('admin'), {
189
+ deviceId: DEVICE_ID,
190
+ })
139
191
  expect(result.ok).toBe(false)
140
192
  if (!result.ok) expect(result.code).toBe('PRECONDITION_FAILED')
141
193
  })
@@ -1,4 +1,3 @@
1
-
2
1
  /**
3
2
  * Test harness for codegen'd capability tRPC routers.
4
3
  *
@@ -24,7 +23,10 @@ export type AuthLevel = 'public' | 'protected' | 'admin'
24
23
  export type TestRole = 'anonymous' | 'user' | 'admin' | 'agent'
25
24
 
26
25
  /** Build an AuthenticatedAgent stub for a given synthetic identity. */
27
- export function makeUser(role: Exclude<TestRole, 'anonymous'>, overrides: Partial<AuthenticatedAgent> = {}): AuthenticatedAgent {
26
+ export function makeUser(
27
+ role: Exclude<TestRole, 'anonymous'>,
28
+ overrides: Partial<AuthenticatedAgent> = {},
29
+ ): AuthenticatedAgent {
28
30
  const isAdmin = role === 'admin' || role === 'agent'
29
31
  const base: AuthenticatedAgent = {
30
32
  id: `user-${role}`,
@@ -111,11 +113,13 @@ export async function checkAuthMatrix(
111
113
  procedure: string,
112
114
  auth: AuthLevel,
113
115
  input?: unknown,
114
- ): Promise<ReadonlyArray<{
115
- readonly role: TestRole
116
- readonly allowed: boolean
117
- readonly outcome: Awaited<ReturnType<typeof invokeProcedure>>
118
- }>> {
116
+ ): Promise<
117
+ ReadonlyArray<{
118
+ readonly role: TestRole
119
+ readonly allowed: boolean
120
+ readonly outcome: Awaited<ReturnType<typeof invokeProcedure>>
121
+ }>
122
+ > {
119
123
  const results: Array<{
120
124
  role: TestRole
121
125
  allowed: boolean
@@ -1,4 +1,3 @@
1
-
2
1
  /**
3
2
  * Example spec exercising the codegen'd metrics-provider router end-to-end:
4
3
  * - Wires a mock provider into `createCapRouter_metricsProvider`
@@ -14,9 +13,24 @@ function makeMockProvider(overrides: Partial<IMetricsProvider> = {}): IMetricsPr
14
13
  return {
15
14
  collectSnapshot: async () => ({
16
15
  cpu: { total: 0, user: 0, system: 0, irq: 0, nice: 0, loadAvg: [0, 0, 0], cores: 1 },
17
- memory: { percent: 0, totalBytes: 0, usedBytes: 0, availableBytes: 0, swapUsedBytes: 0, swapTotalBytes: 0 },
16
+ memory: {
17
+ percent: 0,
18
+ totalBytes: 0,
19
+ usedBytes: 0,
20
+ availableBytes: 0,
21
+ swapUsedBytes: 0,
22
+ swapTotalBytes: 0,
23
+ },
18
24
  gpu: null,
19
- network: { rxBytes: 0, txBytes: 0, rxPackets: 0, txPackets: 0, rxErrors: 0, txErrors: 0, timestampMs: 0 },
25
+ network: {
26
+ rxBytes: 0,
27
+ txBytes: 0,
28
+ rxPackets: 0,
29
+ txPackets: 0,
30
+ rxErrors: 0,
31
+ txErrors: 0,
32
+ timestampMs: 0,
33
+ },
20
34
  disk: { readBytes: 0, writeBytes: 0, readOps: 0, writeOps: 0, timestampMs: 0 },
21
35
  pressure: { cpu: null, memory: null, io: null },
22
36
  process: { openFds: 0, threadCount: 0, activeHandles: 0, activeRequests: 0 },
@@ -36,11 +36,12 @@ describe('null-provider guard — all capabilities', () => {
36
36
  const router = factory(() => null)
37
37
 
38
38
  // Find the first method on the router's caller
39
- const caller = (router as { createCaller: (ctx: unknown) => Record<string, unknown> })
40
- .createCaller(makeCtx('admin'))
39
+ const caller = (
40
+ router as { createCaller: (ctx: unknown) => Record<string, unknown> }
41
+ ).createCaller(makeCtx('admin'))
41
42
 
42
43
  const methods = Object.keys(caller).filter(
43
- k => typeof caller[k] === 'function' && !k.startsWith('_'),
44
+ (k) => typeof caller[k] === 'function' && !k.startsWith('_'),
44
45
  )
45
46
 
46
47
  if (methods.length === 0) return // No methods — skip
@@ -57,10 +58,7 @@ describe('null-provider guard — all capabilities', () => {
57
58
  // caught by codegen guard) or BAD_REQUEST (input validation before guard)
58
59
  // or INTERNAL_SERVER_ERROR (provider method call on null).
59
60
  // Any of these is acceptable — the important thing is ok !== true.
60
- expect(
61
- result.ok,
62
- `${name}.${firstMethod} returned success with null provider!`,
63
- ).toBe(false)
61
+ expect(result.ok, `${name}.${firstMethod} returned success with null provider!`).toBe(false)
64
62
  })
65
63
  }
66
64
  })