@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.
- package/package.json +9 -7
- package/src/__tests__/addon-install-e2e.test.ts +0 -1
- package/src/__tests__/addon-pages-e2e.test.ts +40 -18
- package/src/__tests__/addon-settings-router.spec.ts +6 -1
- package/src/__tests__/addon-upload.spec.ts +91 -29
- package/src/__tests__/agent-registry.spec.ts +26 -9
- package/src/__tests__/agent-status-page.spec.ts +1 -3
- package/src/__tests__/auth-session-cookie.test.ts +28 -1
- package/src/__tests__/bulk-update-coordinator.spec.ts +48 -31
- package/src/__tests__/cap-ownership-authority.spec.ts +39 -8
- package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +24 -4
- package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +17 -3
- package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +57 -11
- package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +64 -15
- package/src/__tests__/cap-providers-bulk-update.spec.ts +27 -7
- package/src/__tests__/cap-route-adapter.spec.ts +28 -15
- package/src/__tests__/cap-routers/_meta.spec.ts +6 -7
- package/src/__tests__/cap-routers/addon-settings.router.spec.ts +19 -10
- package/src/__tests__/cap-routers/broker-routing.router.spec.ts +14 -6
- package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +3 -1
- package/src/__tests__/cap-routers/capabilities-node.spec.ts +18 -5
- package/src/__tests__/cap-routers/device-link-overlay.spec.ts +11 -6
- package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +72 -20
- package/src/__tests__/cap-routers/harness.ts +11 -7
- package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +17 -3
- package/src/__tests__/cap-routers/null-provider-guard.spec.ts +5 -7
- package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +35 -11
- package/src/__tests__/cap-routers/settings-store.router.spec.ts +59 -15
- package/src/__tests__/capability-e2e.test.ts +9 -11
- package/src/__tests__/cli-e2e.test.ts +80 -59
- package/src/__tests__/core-cap-bridge.spec.ts +3 -1
- package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +12 -2
- package/src/__tests__/device-settings-contribution-dispatch.spec.ts +61 -30
- package/src/__tests__/embedded-deps-e2e.test.ts +35 -19
- package/src/__tests__/event-bus-proxy-router.spec.ts +3 -0
- package/src/__tests__/framework-allowlist.spec.ts +5 -4
- package/src/__tests__/https-e2e.test.ts +12 -6
- package/src/__tests__/lifecycle-e2e.test.ts +60 -11
- package/src/__tests__/live-events-subscription.spec.ts +17 -18
- package/src/__tests__/moleculer/uds-readiness.spec.ts +11 -4
- package/src/__tests__/moleculer/uds-topology.spec.ts +39 -11
- package/src/__tests__/moleculer/uds-unowned-call.spec.ts +71 -17
- package/src/__tests__/moleculer-register-node-idempotency.spec.ts +16 -7
- package/src/__tests__/native-cap-route.spec.ts +42 -19
- package/src/__tests__/oauth2-account-linking.spec.ts +63 -17
- package/src/__tests__/singleton-contention.test.ts +23 -11
- package/src/__tests__/streaming-diagnostic.test.ts +156 -53
- package/src/__tests__/streaming-scale.test.ts +69 -35
- package/src/__tests__/uds-addon-call-wiring.spec.ts +6 -1
- package/src/agent-status-page.ts +4 -3
- package/src/api/__tests__/addons-custom.spec.ts +22 -8
- package/src/api/__tests__/capabilities.router.test.ts +18 -9
- package/src/api/addon-upload.ts +46 -15
- package/src/api/addons-custom.router.ts +7 -6
- package/src/api/auth-whoami.ts +3 -1
- package/src/api/bridge-addons.router.ts +3 -1
- package/src/api/capabilities.router.ts +117 -78
- package/src/api/core/__tests__/auth-router-totp.spec.ts +57 -16
- package/src/api/core/addon-settings.router.ts +4 -1
- package/src/api/core/agents.router.ts +52 -53
- package/src/api/core/auth.router.ts +55 -36
- package/src/api/core/bulk-update-coordinator.ts +25 -22
- package/src/api/core/cap-providers.ts +346 -202
- package/src/api/core/capabilities.router.ts +30 -23
- package/src/api/core/hwaccel.router.ts +37 -10
- package/src/api/core/live-events.router.ts +16 -9
- package/src/api/core/logs.router.ts +54 -25
- package/src/api/core/notifications.router.ts +2 -1
- package/src/api/core/repl.router.ts +1 -3
- package/src/api/core/settings-backend.router.ts +68 -70
- package/src/api/core/system-events.router.ts +41 -32
- package/src/api/health/health.routes.ts +7 -13
- package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +12 -2
- package/src/api/oauth2/consent-page.ts +4 -3
- package/src/api/oauth2/oauth2-routes.ts +41 -12
- package/src/api/trpc/__tests__/scope-access-device.spec.ts +68 -23
- package/src/api/trpc/__tests__/scope-access.spec.ts +8 -13
- package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +10 -2
- package/src/api/trpc/cap-mount-helpers.ts +64 -55
- package/src/api/trpc/cap-route-error-formatter.ts +17 -9
- package/src/api/trpc/core-cap-bridge.ts +3 -1
- package/src/api/trpc/generated-cap-mounts.ts +593 -351
- package/src/api/trpc/generated-cap-routers.ts +3680 -579
- package/src/api/trpc/scope-access.ts +7 -7
- package/src/api/trpc/trpc.context.ts +7 -4
- package/src/api/trpc/trpc.middleware.ts +4 -2
- package/src/api/trpc/trpc.router.ts +79 -46
- package/src/auth/session-cookie.ts +10 -0
- package/src/boot/__tests__/integration-id-backfill.spec.ts +21 -6
- package/src/boot/boot-config.ts +103 -122
- package/src/boot/post-boot.service.ts +5 -3
- package/src/core/addon/__tests__/addon-registry-capability.test.ts +12 -3
- package/src/core/addon/addon-call-gateway.ts +20 -6
- package/src/core/addon/addon-package.service.ts +183 -89
- package/src/core/addon/addon-registry.service.ts +1163 -1305
- package/src/core/addon/addon-search.service.ts +2 -1
- package/src/core/addon/addon-settings-provider.ts +27 -7
- package/src/core/addon-bridge/addon-bridge.service.ts +11 -6
- package/src/core/addon-pages/addon-pages.service.ts +3 -1
- package/src/core/addon-widgets/addon-widgets.service.ts +5 -2
- package/src/core/agent/agent-registry.service.ts +60 -38
- package/src/core/auth/auth.service.spec.ts +6 -8
- package/src/core/config/config.service.spec.ts +1 -1
- package/src/core/events/event-bus.service.spec.ts +44 -21
- package/src/core/events/event-bus.service.ts +5 -1
- package/src/core/feature/feature.service.spec.ts +4 -1
- package/src/core/lifecycle/lifecycle-state-machine.spec.ts +8 -10
- package/src/core/logging/logging.service.spec.ts +61 -21
- package/src/core/logging/logging.service.ts +12 -3
- package/src/core/moleculer/cap-call-fn.spec.ts +17 -10
- package/src/core/moleculer/cap-call-fn.ts +5 -1
- package/src/core/moleculer/cap-route-authority.ts +18 -6
- package/src/core/moleculer/moleculer.service.ts +120 -32
- package/src/core/network/network-quality.service.spec.ts +6 -1
- package/src/core/notification/notification-wrapper.service.ts +1 -3
- package/src/core/notification/toast-wrapper.service.ts +1 -5
- package/src/core/repl/repl-engine.service.spec.ts +66 -39
- package/src/core/repl/repl-engine.service.ts +11 -12
- package/src/core/storage/storage-location-manager.spec.ts +12 -3
- package/src/core/streaming/stream-probe.service.ts +22 -13
- package/src/core/topology/topology-emitter.service.ts +5 -1
- package/src/launcher.ts +14 -9
- package/src/main.ts +602 -531
- package/src/manual-boot.ts +133 -154
- 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
|
-
[
|
|
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(
|
|
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) =>
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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) =>
|
|
145
|
-
|
|
146
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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'), {
|
|
34
|
+
const result = await invokeProcedure(router, 'getGlobalSettings', makeCtx('admin'), {
|
|
35
|
+
addonId: 'test',
|
|
36
|
+
})
|
|
35
37
|
expect(result.ok).toBe(true)
|
|
36
|
-
if (result.ok)
|
|
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',
|
|
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',
|
|
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',
|
|
68
|
+
addonId: 'test',
|
|
69
|
+
deviceId: 1,
|
|
70
|
+
patch: { enabled: false },
|
|
64
71
|
})
|
|
65
72
|
expect(provider.updateDeviceSettings).toHaveBeenCalledWith({
|
|
66
|
-
addonId: 'test',
|
|
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'), {
|
|
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(
|
|
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'), {
|
|
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'), {
|
|
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).
|
|
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(
|
|
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[][] } = {
|
|
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[]) => {
|
|
22
|
-
|
|
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 = {
|
|
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',
|
|
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 (
|
|
88
|
-
|
|
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 (
|
|
106
|
-
|
|
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 (
|
|
126
|
-
|
|
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: [
|
|
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: [
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 () => ({
|
|
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 () => ({
|
|
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 }) => ({
|
|
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'), {
|
|
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 {
|
|
86
|
-
|
|
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'), {
|
|
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 {
|
|
98
|
-
|
|
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 = {
|
|
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'), {
|
|
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(
|
|
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<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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: {
|
|
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: {
|
|
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 = (
|
|
40
|
-
|
|
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
|
})
|