@camstack/server 0.2.2 → 1.0.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/{src/agent-status-page.ts → dist/agent-status-page.js} +30 -45
- package/dist/api/addon-upload.js +441 -0
- package/dist/api/addons-custom.router.js +91 -0
- package/dist/api/auth-whoami.js +55 -0
- package/dist/api/bridge-addons.router.js +109 -0
- package/dist/api/capabilities.router.js +229 -0
- package/dist/api/core/addon-settings.router.js +117 -0
- package/dist/api/core/agents.router.js +73 -0
- package/dist/api/core/auth.router.js +286 -0
- package/dist/api/core/bulk-update-coordinator.js +229 -0
- package/dist/api/core/cap-providers.js +1124 -0
- package/dist/api/core/capabilities.router.js +138 -0
- package/dist/api/core/collection-preference.js +17 -0
- package/dist/api/core/event-bus-proxy.router.js +45 -0
- package/dist/api/core/hwaccel.router.js +91 -0
- package/dist/api/core/live-events.router.js +61 -0
- package/dist/api/core/logs.router.js +172 -0
- package/dist/api/core/notifications.router.js +67 -0
- package/dist/api/core/repl.router.js +35 -0
- package/dist/api/core/settings-backend.router.js +121 -0
- package/dist/api/core/stream-probe.router.js +58 -0
- package/dist/api/core/system-events.router.js +100 -0
- package/dist/api/health/health.routes.js +68 -0
- package/{src/api/oauth2/consent-page.ts → dist/api/oauth2/consent-page.js} +11 -20
- package/dist/api/oauth2/oauth2-routes.js +219 -0
- package/dist/api/trpc/cap-mount-helpers.js +194 -0
- package/dist/api/trpc/cap-route-error-formatter.js +133 -0
- package/dist/api/trpc/client-ip.js +147 -0
- package/dist/api/trpc/core-cap-bridge.js +115 -0
- package/dist/api/trpc/generated-cap-mounts.js +388 -0
- package/dist/api/trpc/generated-cap-routers.js +7635 -0
- package/dist/api/trpc/scope-access.js +93 -0
- package/dist/api/trpc/trpc.context.js +184 -0
- package/dist/api/trpc/trpc.middleware.js +139 -0
- package/dist/api/trpc/trpc.router.js +188 -0
- package/dist/auth/session-cookie.js +47 -0
- package/dist/boot/boot-config.js +241 -0
- package/dist/boot/integration-id-backfill.js +76 -0
- package/dist/boot/post-boot.service.js +85 -0
- package/dist/core/addon/addon-call-gateway.js +99 -0
- package/dist/core/addon/addon-package.service.js +1560 -0
- package/dist/core/addon/addon-registry.service.js +2739 -0
- package/{src/core/addon/addon-row-manifest.ts → dist/core/addon/addon-row-manifest.js} +5 -5
- package/dist/core/addon/addon-search.service.js +62 -0
- package/dist/core/addon/addon-settings-provider.js +102 -0
- package/dist/core/addon/addon.tokens.js +5 -0
- package/dist/core/addon-bridge/addon-bridge.service.js +145 -0
- package/dist/core/addon-pages/addon-pages.service.js +107 -0
- package/dist/core/addon-widgets/addon-widgets.service.js +120 -0
- package/dist/core/agent/agent-registry.service.js +477 -0
- package/dist/core/auth/auth.service.js +10 -0
- package/dist/core/capability/capability.service.js +58 -0
- package/dist/core/config/config.schema.js +7 -0
- package/dist/core/config/config.service.js +10 -0
- package/dist/core/events/event-bus.service.js +83 -0
- package/dist/core/feature/feature.service.js +10 -0
- package/dist/core/lifecycle/lifecycle-state-machine.js +6 -0
- package/dist/core/logging/log-ring-buffer.js +6 -0
- package/dist/core/logging/logging.service.js +130 -0
- package/dist/core/logging/scoped-logger.js +6 -0
- package/dist/core/moleculer/cap-call-fn.js +50 -0
- package/dist/core/moleculer/cap-route-authority.js +122 -0
- package/dist/core/moleculer/moleculer.service.js +898 -0
- package/dist/core/network/network-quality.service.js +7 -0
- package/dist/core/notification/notification-wrapper.service.js +33 -0
- package/dist/core/notification/toast-wrapper.service.js +25 -0
- package/dist/core/provider/provider.tokens.js +4 -0
- package/dist/core/repl/repl-engine.service.js +140 -0
- package/dist/core/storage/fs-storage-backend.js +6 -0
- package/dist/core/storage/storage-location-manager.js +6 -0
- package/dist/core/storage/storage.service.js +7 -0
- package/dist/core/streaming/stream-probe.service.js +209 -0
- package/dist/core/topology/topology-emitter.service.js +106 -0
- package/dist/launcher.js +325 -0
- package/dist/main.js +1098 -0
- package/dist/manual-boot.js +227 -0
- package/package.json +5 -1
- package/src/__tests__/addon-install-e2e.test.ts +0 -74
- package/src/__tests__/addon-pages-e2e.test.ts +0 -200
- package/src/__tests__/addon-route-session.test.ts +0 -17
- package/src/__tests__/addon-settings-router.spec.ts +0 -67
- package/src/__tests__/addon-upload.spec.ts +0 -475
- package/src/__tests__/agent-registry.spec.ts +0 -179
- package/src/__tests__/agent-status-page.spec.ts +0 -82
- package/src/__tests__/auth-session-cookie.test.ts +0 -48
- package/src/__tests__/bulk-update-coordinator.spec.ts +0 -303
- package/src/__tests__/cap-ownership-authority.spec.ts +0 -431
- package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +0 -206
- package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +0 -37
- package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +0 -110
- package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +0 -292
- package/src/__tests__/cap-providers-bulk-update.spec.ts +0 -408
- package/src/__tests__/cap-route-adapter.spec.ts +0 -302
- package/src/__tests__/cap-routers/_meta.spec.ts +0 -199
- package/src/__tests__/cap-routers/addon-settings.router.spec.ts +0 -115
- package/src/__tests__/cap-routers/broker-routing.router.spec.ts +0 -177
- package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +0 -125
- package/src/__tests__/cap-routers/capabilities-node.spec.ts +0 -68
- package/src/__tests__/cap-routers/device-link-overlay.spec.ts +0 -137
- package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +0 -194
- package/src/__tests__/cap-routers/harness.ts +0 -163
- package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +0 -133
- package/src/__tests__/cap-routers/null-provider-guard.spec.ts +0 -64
- package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +0 -159
- package/src/__tests__/cap-routers/settings-store.router.spec.ts +0 -291
- package/src/__tests__/capability-e2e.test.ts +0 -384
- package/src/__tests__/cli-e2e.test.ts +0 -150
- package/src/__tests__/core-cap-bridge.spec.ts +0 -91
- package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +0 -40
- package/src/__tests__/device-settings-contribution-dispatch.spec.ts +0 -280
- package/src/__tests__/embedded-deps-e2e.test.ts +0 -125
- package/src/__tests__/event-bus-proxy-router.spec.ts +0 -75
- package/src/__tests__/fixtures/mock-analysis-addon-a.ts +0 -37
- package/src/__tests__/fixtures/mock-analysis-addon-b.ts +0 -37
- package/src/__tests__/fixtures/mock-log-addon.ts +0 -37
- package/src/__tests__/fixtures/mock-storage-addon.ts +0 -40
- package/src/__tests__/framework-allowlist.spec.ts +0 -96
- package/src/__tests__/framework-installer-defer-restart.spec.ts +0 -165
- package/src/__tests__/https-e2e.test.ts +0 -124
- package/src/__tests__/lifecycle-e2e.test.ts +0 -189
- package/src/__tests__/live-events-subscription.spec.ts +0 -149
- package/src/__tests__/moleculer/uds-readiness.spec.ts +0 -150
- package/src/__tests__/moleculer/uds-topology.spec.ts +0 -418
- package/src/__tests__/moleculer/uds-unowned-call.spec.ts +0 -383
- package/src/__tests__/moleculer-register-node-idempotency.spec.ts +0 -273
- package/src/__tests__/native-cap-route.spec.ts +0 -427
- package/src/__tests__/oauth2-account-linking.spec.ts +0 -867
- package/src/__tests__/post-boot-restart.spec.ts +0 -161
- package/src/__tests__/singleton-contention.test.ts +0 -499
- package/src/__tests__/streaming-diagnostic.test.ts +0 -615
- package/src/__tests__/streaming-scale.test.ts +0 -314
- package/src/__tests__/uds-addon-call-wiring.spec.ts +0 -242
- package/src/__tests__/uds-log-ingest.spec.ts +0 -183
- package/src/api/__tests__/addons-custom.spec.ts +0 -148
- package/src/api/__tests__/capabilities.router.test.ts +0 -56
- package/src/api/addon-upload.ts +0 -529
- package/src/api/addons-custom.router.ts +0 -101
- package/src/api/auth-whoami.ts +0 -101
- package/src/api/bridge-addons.router.ts +0 -122
- package/src/api/capabilities.router.ts +0 -265
- package/src/api/core/__tests__/auth-router-totp.spec.ts +0 -297
- package/src/api/core/__tests__/integration-markers.spec.ts +0 -10
- package/src/api/core/addon-settings.router.ts +0 -127
- package/src/api/core/agents.router.ts +0 -86
- package/src/api/core/auth.router.ts +0 -322
- package/src/api/core/bulk-update-coordinator.ts +0 -305
- package/src/api/core/cap-providers.ts +0 -1339
- package/src/api/core/capabilities.router.ts +0 -149
- package/src/api/core/collection-preference.ts +0 -40
- package/src/api/core/event-bus-proxy.router.ts +0 -45
- package/src/api/core/hwaccel.router.ts +0 -108
- package/src/api/core/live-events.router.ts +0 -67
- package/src/api/core/logs.router.ts +0 -195
- package/src/api/core/notifications.router.ts +0 -66
- package/src/api/core/repl.router.ts +0 -39
- package/src/api/core/settings-backend.router.ts +0 -140
- package/src/api/core/stream-probe.router.ts +0 -57
- package/src/api/core/system-events.router.ts +0 -125
- package/src/api/health/health.routes.ts +0 -117
- package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +0 -62
- package/src/api/oauth2/oauth2-routes.ts +0 -281
- package/src/api/trpc/__tests__/client-ip.spec.ts +0 -146
- package/src/api/trpc/__tests__/scope-access-device.spec.ts +0 -268
- package/src/api/trpc/__tests__/scope-access.spec.ts +0 -102
- package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +0 -136
- package/src/api/trpc/cap-mount-helpers.ts +0 -245
- package/src/api/trpc/cap-route-error-formatter.ts +0 -171
- package/src/api/trpc/client-ip.ts +0 -147
- package/src/api/trpc/core-cap-bridge.ts +0 -154
- package/src/api/trpc/generated-cap-mounts.ts +0 -1240
- package/src/api/trpc/generated-cap-routers.ts +0 -11523
- package/src/api/trpc/scope-access.ts +0 -110
- package/src/api/trpc/trpc.context.ts +0 -258
- package/src/api/trpc/trpc.middleware.ts +0 -146
- package/src/api/trpc/trpc.router.ts +0 -389
- package/src/auth/session-cookie.ts +0 -54
- package/src/boot/__tests__/integration-id-backfill.spec.ts +0 -131
- package/src/boot/boot-config.ts +0 -259
- package/src/boot/integration-id-backfill.ts +0 -109
- package/src/boot/post-boot.service.ts +0 -105
- package/src/core/addon/__tests__/addon-registry-capability.test.ts +0 -62
- package/src/core/addon/__tests__/addon-row-manifest.spec.ts +0 -62
- package/src/core/addon/addon-call-gateway.ts +0 -171
- package/src/core/addon/addon-package.service.ts +0 -1787
- package/src/core/addon/addon-registry.service.ts +0 -3130
- package/src/core/addon/addon-search.service.ts +0 -91
- package/src/core/addon/addon-settings-provider.ts +0 -220
- package/src/core/addon/addon.tokens.ts +0 -2
- package/src/core/addon-bridge/addon-bridge.service.ts +0 -130
- package/src/core/addon-pages/addon-pages.service.spec.ts +0 -117
- package/src/core/addon-pages/addon-pages.service.ts +0 -82
- package/src/core/addon-widgets/addon-widgets.service.ts +0 -95
- package/src/core/agent/agent-registry.service.ts +0 -529
- package/src/core/auth/auth.service.spec.ts +0 -86
- package/src/core/auth/auth.service.ts +0 -8
- package/src/core/capability/capability.service.ts +0 -66
- package/src/core/config/config.schema.ts +0 -3
- package/src/core/config/config.service.spec.ts +0 -175
- package/src/core/config/config.service.ts +0 -7
- package/src/core/events/event-bus.service.spec.ts +0 -235
- package/src/core/events/event-bus.service.ts +0 -89
- package/src/core/feature/feature.service.spec.ts +0 -99
- package/src/core/feature/feature.service.ts +0 -8
- package/src/core/lifecycle/lifecycle-state-machine.spec.ts +0 -166
- package/src/core/lifecycle/lifecycle-state-machine.ts +0 -3
- package/src/core/logging/log-ring-buffer.ts +0 -3
- package/src/core/logging/logging.service.spec.ts +0 -287
- package/src/core/logging/logging.service.ts +0 -143
- package/src/core/logging/scoped-logger.ts +0 -3
- package/src/core/moleculer/cap-call-fn.spec.ts +0 -173
- package/src/core/moleculer/cap-call-fn.ts +0 -107
- package/src/core/moleculer/cap-route-authority.ts +0 -194
- package/src/core/moleculer/moleculer.service.ts +0 -1072
- package/src/core/network/network-quality.service.spec.ts +0 -53
- package/src/core/network/network-quality.service.ts +0 -5
- package/src/core/notification/notification-wrapper.service.ts +0 -34
- package/src/core/notification/toast-wrapper.service.ts +0 -27
- package/src/core/provider/provider.tokens.ts +0 -1
- package/src/core/repl/repl-engine.service.spec.ts +0 -444
- package/src/core/repl/repl-engine.service.ts +0 -155
- package/src/core/storage/fs-storage-backend.spec.ts +0 -70
- package/src/core/storage/fs-storage-backend.ts +0 -3
- package/src/core/storage/storage-location-manager.spec.ts +0 -130
- package/src/core/storage/storage-location-manager.ts +0 -3
- package/src/core/storage/storage.service.spec.ts +0 -73
- package/src/core/storage/storage.service.ts +0 -3
- package/src/core/streaming/stream-probe.service.ts +0 -221
- package/src/core/topology/topology-emitter.service.ts +0 -105
- package/src/launcher.ts +0 -314
- package/src/main.ts +0 -1245
- package/src/manual-boot.ts +0 -301
- package/tsconfig.build.json +0 -8
- package/tsconfig.json +0 -33
- package/vitest.config.ts +0 -26
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Routing regression spec for the `broker` system-collection cap.
|
|
3
|
-
*
|
|
4
|
-
* The bug: `broker` is `scope:'system', mode:'collection'`. Two addons
|
|
5
|
-
* register a `broker` provider, each owning a DISJOINT id set
|
|
6
|
-
* (mqtt-broker owns `mqtt_*`, provider-homeassistant owns `ha_*`). The
|
|
7
|
-
* generated collection mount only fans out the array-output methods
|
|
8
|
-
* (`list` / `listProviders`); every id-keyed method (`get` / `getSettings`
|
|
9
|
-
* / `remove` / `testConnection` / …) falls through to `providers[0]`
|
|
10
|
-
* (the FIRST-registered provider = mqtt-broker). So operating on an HA
|
|
11
|
-
* broker `ha_1` hit mqtt-broker → "broker 'ha_1' not found".
|
|
12
|
-
*
|
|
13
|
-
* The fix: each broker carries its owning `addonId`; the admin UI threads
|
|
14
|
-
* it back as the `{ addonId }` system-collection selector so the call
|
|
15
|
-
* routes to the OWNING provider via `getProviderByAddonId`. Providers
|
|
16
|
-
* also return `null` (not throw) for ids they don't own, so the
|
|
17
|
-
* no-addonId fallback degrades gracefully.
|
|
18
|
-
*
|
|
19
|
-
* This spec exercises `createCapRouter_broker` with the SAME selector
|
|
20
|
-
* the generated mount builds (registry-backed: addonId → provider, else
|
|
21
|
-
* first-provider aggregate with array methods fanned out). It registers
|
|
22
|
-
* two mock providers and asserts:
|
|
23
|
-
* - `list()` returns both, each tagged with its `addonId`.
|
|
24
|
-
* - `get({id:'ha_1'}, addonId:'ha')` routes to the HA provider.
|
|
25
|
-
* - `get({id:'ha_1'})` WITHOUT addonId returns null (graceful) — the
|
|
26
|
-
* HA-owned id isn't in providers[0]'s registry, so it doesn't throw.
|
|
27
|
-
* - `listProviders()` aggregates both providers' entries.
|
|
28
|
-
*/
|
|
29
|
-
import { describe, it, expect } from 'vitest'
|
|
30
|
-
import { createCapRouter_broker } from '../../api/trpc/generated-cap-routers.js'
|
|
31
|
-
import { type IBrokerProvider, type BrokerInfo, type BrokerProviderInfo } from '@camstack/types'
|
|
32
|
-
import { concatCollection } from '../../api/trpc/cap-mount-helpers.js'
|
|
33
|
-
import { makeCtx, invokeProcedure } from './harness.js'
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* A self-contained mock `broker` provider that owns a fixed set of
|
|
37
|
-
* broker ids and self-tags every returned record with its `addonId`.
|
|
38
|
-
* Returns `null` / no-op for ids it does NOT own (never throws) — the
|
|
39
|
-
* exact graceful-degradation contract the real providers must honour.
|
|
40
|
-
*/
|
|
41
|
-
function makeProvider(addonId: string, ownedIds: readonly string[], kind: string): IBrokerProvider {
|
|
42
|
-
const owns = (id: string): boolean => ownedIds.includes(id)
|
|
43
|
-
const infoFor = (id: string): BrokerInfo => ({
|
|
44
|
-
id,
|
|
45
|
-
addonId,
|
|
46
|
-
name: `${kind}-${id}`,
|
|
47
|
-
kind,
|
|
48
|
-
status: 'connected',
|
|
49
|
-
info: {},
|
|
50
|
-
lastCheckedAt: null,
|
|
51
|
-
error: null,
|
|
52
|
-
})
|
|
53
|
-
return {
|
|
54
|
-
list: async () => ownedIds.map(infoFor),
|
|
55
|
-
get: async ({ id }) => (owns(id) ? infoFor(id) : null),
|
|
56
|
-
listProviders: async (): Promise<BrokerProviderInfo[]> => [
|
|
57
|
-
{ addonId, kinds: [{ kind, label: kind }] },
|
|
58
|
-
],
|
|
59
|
-
add: async () => ({ id: 'new' }),
|
|
60
|
-
remove: async () => {
|
|
61
|
-
// no-op for foreign ids (and owned ids in this mock)
|
|
62
|
-
},
|
|
63
|
-
testConnection: async ({ id }) =>
|
|
64
|
-
owns(id) ? { ok: true, latencyMs: 1 } : { ok: false, error: 'unknown broker' },
|
|
65
|
-
getSettings: async ({ id }) => (owns(id) ? { secret: id } : null),
|
|
66
|
-
setSettings: async () => {
|
|
67
|
-
// no-op
|
|
68
|
-
},
|
|
69
|
-
getBrokerConfig: async ({ id }) => (owns(id) ? { url: id } : null),
|
|
70
|
-
getSettingsSchema: async ({ kind: k }) => (k === kind ? { form: kind } : null),
|
|
71
|
-
testSettings: async () => ({ ok: true }),
|
|
72
|
-
publish: async () => null,
|
|
73
|
-
subscribe: async () => ({ subscriptionId: 's' }),
|
|
74
|
-
unsubscribe: async () => {
|
|
75
|
-
// no-op
|
|
76
|
-
},
|
|
77
|
-
getState: async () => null,
|
|
78
|
-
getStatus: async () => ({ brokerCount: ownedIds.length, connectedCount: ownedIds.length }),
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Mirror of the generated collection mount selector for `broker`
|
|
84
|
-
* (`generated-cap-mounts.ts`): addonId → that provider directly; else a
|
|
85
|
-
* first-provider aggregate whose array-output methods are
|
|
86
|
-
* `concatCollection`-fanned. Two providers registered in order:
|
|
87
|
-
* mqtt (owns `mqtt_1`) first, ha (owns `ha_1`) second.
|
|
88
|
-
*/
|
|
89
|
-
function makeSelector(): (addonId?: string) => IBrokerProvider | null {
|
|
90
|
-
const mqtt = makeProvider('mqtt', ['mqtt_1'], 'mqtt')
|
|
91
|
-
const ha = makeProvider('ha', ['ha_1'], 'home-assistant')
|
|
92
|
-
const byAddonId: Record<string, IBrokerProvider> = { mqtt, ha }
|
|
93
|
-
const providers: readonly IBrokerProvider[] = [mqtt, ha]
|
|
94
|
-
return (addonId?: string): IBrokerProvider | null => {
|
|
95
|
-
if (addonId !== undefined) return byAddonId[addonId] ?? null
|
|
96
|
-
const first = providers[0]!
|
|
97
|
-
return {
|
|
98
|
-
...first,
|
|
99
|
-
list: concatCollection(providers, 'list') as IBrokerProvider['list'],
|
|
100
|
-
listProviders: concatCollection(
|
|
101
|
-
providers,
|
|
102
|
-
'listProviders',
|
|
103
|
-
) as IBrokerProvider['listProviders'],
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
describe('broker cap — addonId ownership routing', () => {
|
|
109
|
-
it('list() aggregates both providers, each broker tagged with its addonId', async () => {
|
|
110
|
-
const selector = makeSelector()
|
|
111
|
-
const router = createCapRouter_broker((_ctx, addonId) => selector(addonId))
|
|
112
|
-
|
|
113
|
-
const outcome = await invokeProcedure(router, 'list', makeCtx('admin'), {})
|
|
114
|
-
|
|
115
|
-
expect(outcome.ok).toBe(true)
|
|
116
|
-
if (!outcome.ok) return
|
|
117
|
-
const rows = outcome.value as BrokerInfo[]
|
|
118
|
-
const byId = new Map(rows.map((r) => [r.id, r]))
|
|
119
|
-
expect(byId.get('mqtt_1')?.addonId).toBe('mqtt')
|
|
120
|
-
expect(byId.get('ha_1')?.addonId).toBe('ha')
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
it('get({id:ha_1}, addonId:ha) routes to the HA provider (not mqtt-broker)', async () => {
|
|
124
|
-
const selector = makeSelector()
|
|
125
|
-
const router = createCapRouter_broker((_ctx, addonId) => selector(addonId))
|
|
126
|
-
|
|
127
|
-
const outcome = await invokeProcedure(router, 'get', makeCtx('admin'), {
|
|
128
|
-
id: 'ha_1',
|
|
129
|
-
addonId: 'ha',
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
expect(outcome.ok).toBe(true)
|
|
133
|
-
if (!outcome.ok) return
|
|
134
|
-
const row = outcome.value as BrokerInfo | null
|
|
135
|
-
expect(row).not.toBeNull()
|
|
136
|
-
expect(row?.id).toBe('ha_1')
|
|
137
|
-
expect(row?.addonId).toBe('ha')
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
it('get({id:ha_1}) WITHOUT addonId returns null gracefully (first provider does not own it)', async () => {
|
|
141
|
-
const selector = makeSelector()
|
|
142
|
-
const router = createCapRouter_broker((_ctx, addonId) => selector(addonId))
|
|
143
|
-
|
|
144
|
-
const outcome = await invokeProcedure(router, 'get', makeCtx('admin'), { id: 'ha_1' })
|
|
145
|
-
|
|
146
|
-
expect(outcome.ok).toBe(true)
|
|
147
|
-
if (!outcome.ok) return
|
|
148
|
-
expect(outcome.value).toBeNull()
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
it('testConnection({id:ha_1}, addonId:ha) routes to HA provider and succeeds', async () => {
|
|
152
|
-
const selector = makeSelector()
|
|
153
|
-
const router = createCapRouter_broker((_ctx, addonId) => selector(addonId))
|
|
154
|
-
|
|
155
|
-
const outcome = await invokeProcedure(router, 'testConnection', makeCtx('admin'), {
|
|
156
|
-
id: 'ha_1',
|
|
157
|
-
addonId: 'ha',
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
expect(outcome.ok).toBe(true)
|
|
161
|
-
if (!outcome.ok) return
|
|
162
|
-
expect(outcome.value).toEqual({ ok: true, latencyMs: 1 })
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
it('listProviders() aggregates both providers entries', async () => {
|
|
166
|
-
const selector = makeSelector()
|
|
167
|
-
const router = createCapRouter_broker((_ctx, addonId) => selector(addonId))
|
|
168
|
-
|
|
169
|
-
const outcome = await invokeProcedure(router, 'listProviders', makeCtx('admin'))
|
|
170
|
-
|
|
171
|
-
expect(outcome.ok).toBe(true)
|
|
172
|
-
if (!outcome.ok) return
|
|
173
|
-
const entries = outcome.value as BrokerProviderInfo[]
|
|
174
|
-
const addonIds = entries.map((e) => e.addonId).toSorted()
|
|
175
|
-
expect(addonIds).toEqual(['ha', 'mqtt'])
|
|
176
|
-
})
|
|
177
|
-
})
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for the cap-route-error-formatter.
|
|
3
|
-
*
|
|
4
|
-
* `formatTrpcError` is a pure function — no tRPC plumbing required.
|
|
5
|
-
* We hand-craft minimal TRPCError-like / DefaultErrorShape-like objects
|
|
6
|
-
* to verify the augmentation logic.
|
|
7
|
-
*/
|
|
8
|
-
import { describe, it, expect } from 'vitest'
|
|
9
|
-
import { TRPCError } from '@trpc/server'
|
|
10
|
-
import { CapRouteError } from '@camstack/kernel'
|
|
11
|
-
import { formatTrpcError } from '../../api/trpc/cap-route-error-formatter.js'
|
|
12
|
-
|
|
13
|
-
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
/** Minimal DefaultErrorShape stub. */
|
|
16
|
-
function makeShape(
|
|
17
|
-
overrides: Partial<{ message: string }> = {},
|
|
18
|
-
): Parameters<typeof formatTrpcError>[0]['shape'] {
|
|
19
|
-
return {
|
|
20
|
-
message: overrides.message ?? 'Something went wrong',
|
|
21
|
-
code: -32603, // INTERNAL_SERVER_ERROR code number
|
|
22
|
-
data: {
|
|
23
|
-
code: 'INTERNAL_SERVER_ERROR',
|
|
24
|
-
httpStatus: 500,
|
|
25
|
-
},
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Build a TRPCError-like object that formatTrpcError accepts. */
|
|
30
|
-
function makeTrpcError(
|
|
31
|
-
message: string,
|
|
32
|
-
cause?: Error,
|
|
33
|
-
): Parameters<typeof formatTrpcError>[0]['error'] {
|
|
34
|
-
return new TRPCError({
|
|
35
|
-
code: 'PRECONDITION_FAILED',
|
|
36
|
-
message,
|
|
37
|
-
cause,
|
|
38
|
-
})
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
42
|
-
|
|
43
|
-
describe('formatTrpcError', () => {
|
|
44
|
-
it('returns shape unchanged when error is not a CapRouteError', () => {
|
|
45
|
-
const shape = makeShape({ message: 'Boom' })
|
|
46
|
-
const error = makeTrpcError('Some generic error')
|
|
47
|
-
const result = formatTrpcError({ error, shape })
|
|
48
|
-
expect(result).toStrictEqual(shape)
|
|
49
|
-
expect(result.data).not.toHaveProperty('capRouteReason')
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('augments shape with capRouteReason when error.cause is a CapRouteError', () => {
|
|
53
|
-
const capRouteErr = new CapRouteError('my-cap', undefined, {
|
|
54
|
-
reason: 'no-provider',
|
|
55
|
-
rejected: [{ kind: 'hub-in-process', why: 'no provider bound' }],
|
|
56
|
-
})
|
|
57
|
-
const shape = makeShape()
|
|
58
|
-
const error = makeTrpcError('Capability "my-cap" provider not available', capRouteErr)
|
|
59
|
-
const result = formatTrpcError({ error, shape })
|
|
60
|
-
|
|
61
|
-
expect(result.data.capRouteReason).toBe('no-provider')
|
|
62
|
-
expect(result.data.capRouteRejected).toStrictEqual([
|
|
63
|
-
{ kind: 'hub-in-process', why: 'no provider bound' },
|
|
64
|
-
])
|
|
65
|
-
expect(result.data.capRouteNodeId).toBeUndefined()
|
|
66
|
-
// Original shape fields preserved
|
|
67
|
-
expect(result.message).toBe(shape.message)
|
|
68
|
-
expect(result.code).toBe(shape.code)
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it('augments shape with capRouteReason when error itself is a CapRouteError', () => {
|
|
72
|
-
const capRouteErr = new CapRouteError('some-cap', undefined, {
|
|
73
|
-
reason: 'node-offline',
|
|
74
|
-
nodeId: 'node-abc',
|
|
75
|
-
rejected: [{ kind: 'remote-moleculer', why: 'node node-abc is offline' }],
|
|
76
|
-
})
|
|
77
|
-
const shape = makeShape()
|
|
78
|
-
// Wrap as TRPCError with the CapRouteError AS the cause
|
|
79
|
-
const error = makeTrpcError('Transport failed', capRouteErr)
|
|
80
|
-
const result = formatTrpcError({ error, shape })
|
|
81
|
-
|
|
82
|
-
expect(result.data.capRouteReason).toBe('node-offline')
|
|
83
|
-
expect(result.data.capRouteNodeId).toBe('node-abc')
|
|
84
|
-
expect(result.data.capRouteRejected).toHaveLength(1)
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
it('surfaces transport-failed reason correctly (not absent provider)', () => {
|
|
88
|
-
const capRouteErr = new CapRouteError('stream-params', 'getStatus', {
|
|
89
|
-
reason: 'transport-failed',
|
|
90
|
-
rejected: [{ kind: 'hub-local-uds', why: 'socket closed' }],
|
|
91
|
-
})
|
|
92
|
-
const shape = makeShape()
|
|
93
|
-
const error = makeTrpcError('Transport failed', capRouteErr)
|
|
94
|
-
const result = formatTrpcError({ error, shape })
|
|
95
|
-
|
|
96
|
-
expect(result.data.capRouteReason).toBe('transport-failed')
|
|
97
|
-
expect(result.data.capRouteRejected).toStrictEqual([
|
|
98
|
-
{ kind: 'hub-local-uds', why: 'socket closed' },
|
|
99
|
-
])
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
it('walks a nested cause chain to find a CapRouteError', () => {
|
|
103
|
-
const capRouteErr = new CapRouteError('ptz', undefined, {
|
|
104
|
-
reason: 'cap-unknown',
|
|
105
|
-
rejected: [],
|
|
106
|
-
})
|
|
107
|
-
// Wrap two levels deep
|
|
108
|
-
const innerError = new Error('dispatch failed', { cause: capRouteErr })
|
|
109
|
-
const shape = makeShape()
|
|
110
|
-
const error = makeTrpcError('Outer error', innerError)
|
|
111
|
-
const result = formatTrpcError({ error, shape })
|
|
112
|
-
|
|
113
|
-
expect(result.data.capRouteReason).toBe('cap-unknown')
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
it('returns shape unchanged for a plain Error (non-CapRouteError cause)', () => {
|
|
117
|
-
const plainErr = new Error('plain cause')
|
|
118
|
-
const shape = makeShape()
|
|
119
|
-
const error = makeTrpcError('Wrapper', plainErr)
|
|
120
|
-
const result = formatTrpcError({ error, shape })
|
|
121
|
-
|
|
122
|
-
expect(result.data).not.toHaveProperty('capRouteReason')
|
|
123
|
-
expect(result).toStrictEqual(shape)
|
|
124
|
-
})
|
|
125
|
-
})
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Spec for the per-node singleton extension in capabilities.router.
|
|
3
|
-
*
|
|
4
|
-
* Exercises:
|
|
5
|
-
* - setActiveSingleton with nodeId → calls registry with (cap, addonId, nodeId)
|
|
6
|
-
* + persists `capabilities.singletonNode.<cap>.<nodeId>`
|
|
7
|
-
* - setActiveSingleton without nodeId → persists `capabilities.singleton.<cap>` (global key)
|
|
8
|
-
* - clearSingletonNodeOverride → calls registry.clearSingletonNodeOverride(cap, nodeId)
|
|
9
|
-
* + persists the per-node key as null
|
|
10
|
-
*/
|
|
11
|
-
import { describe, it, expect, vi } from 'vitest'
|
|
12
|
-
import type { CapabilityRegistry } from '@camstack/kernel'
|
|
13
|
-
import type { ConfigService } from '../../core/config/config.service.js'
|
|
14
|
-
import { createCapabilitiesRouter } from '../../api/core/capabilities.router.js'
|
|
15
|
-
import { makeCtx } from './harness.js'
|
|
16
|
-
|
|
17
|
-
function harness() {
|
|
18
|
-
const calls: { setActiveSingleton: unknown[][]; clear: unknown[][] } = {
|
|
19
|
-
setActiveSingleton: [],
|
|
20
|
-
clear: [],
|
|
21
|
-
}
|
|
22
|
-
const sets: Record<string, unknown> = {}
|
|
23
|
-
const registry = {
|
|
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
|
-
}),
|
|
30
|
-
listCapabilities: () => [],
|
|
31
|
-
} as unknown as CapabilityRegistry
|
|
32
|
-
const config = {
|
|
33
|
-
set: (k: string, v: unknown) => {
|
|
34
|
-
sets[k] = v
|
|
35
|
-
},
|
|
36
|
-
} as unknown as ConfigService
|
|
37
|
-
const router = createCapabilitiesRouter(registry, config)
|
|
38
|
-
return { router, calls, sets, registry }
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
describe('capabilities.router — per-node singleton', () => {
|
|
42
|
-
it('setActiveSingleton with nodeId persists the per-node key', async () => {
|
|
43
|
-
const { router, calls, sets } = harness()
|
|
44
|
-
const caller = router.createCaller(makeCtx('admin'))
|
|
45
|
-
await caller.setActiveSingleton({
|
|
46
|
-
capability: 'webrtc-session',
|
|
47
|
-
addonId: 'webrtc-native',
|
|
48
|
-
nodeId: 'dev-agent-0',
|
|
49
|
-
})
|
|
50
|
-
expect(calls.setActiveSingleton[0]).toEqual(['webrtc-session', 'webrtc-native', 'dev-agent-0'])
|
|
51
|
-
expect(sets['capabilities.singletonNode.webrtc-session.dev-agent-0']).toBe('webrtc-native')
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('setActiveSingleton without nodeId persists the global key', async () => {
|
|
55
|
-
const { router, sets } = harness()
|
|
56
|
-
const caller = router.createCaller(makeCtx('admin'))
|
|
57
|
-
await caller.setActiveSingleton({ capability: 'webrtc-session', addonId: 'stream-broker' })
|
|
58
|
-
expect(sets['capabilities.singleton.webrtc-session']).toBe('stream-broker')
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('clearSingletonNodeOverride clears the per-node key', async () => {
|
|
62
|
-
const { router, calls, sets } = harness()
|
|
63
|
-
const caller = router.createCaller(makeCtx('admin'))
|
|
64
|
-
await caller.clearSingletonNodeOverride({ capability: 'webrtc-session', nodeId: 'dev-agent-0' })
|
|
65
|
-
expect(calls.clear[0]).toEqual(['webrtc-session', 'dev-agent-0'])
|
|
66
|
-
expect(sets['capabilities.singletonNode.webrtc-session.dev-agent-0']).toBe(null)
|
|
67
|
-
})
|
|
68
|
-
})
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests that `requireDeviceScoped` overlays `getStatus` results via
|
|
3
|
-
* `device-manager`'s `resolveLinkedStatus` when the device has linked
|
|
4
|
-
* properties for the requested cap.
|
|
5
|
-
*
|
|
6
|
-
* The overlay is transparent (null return = no-op) for the vast majority
|
|
7
|
-
* of reads; only devices with active links for the cap get the merged
|
|
8
|
-
* status back.
|
|
9
|
-
*/
|
|
10
|
-
import { describe, it, expect, vi } from 'vitest'
|
|
11
|
-
import type { CapabilityRegistry } from '@camstack/kernel'
|
|
12
|
-
import { requireDeviceScoped } from '../../api/trpc/cap-mount-helpers.js'
|
|
13
|
-
|
|
14
|
-
// ── Fake registry ────────────────────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
type ResolveLinkedStatus = (i: {
|
|
17
|
-
deviceId: number
|
|
18
|
-
cap: string
|
|
19
|
-
baseStatus: unknown
|
|
20
|
-
}) => Promise<Record<string, unknown> | null>
|
|
21
|
-
|
|
22
|
-
interface FakeDeviceManager {
|
|
23
|
-
resolveLinkedStatus: ResolveLinkedStatus
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function makeRegistry(opts: {
|
|
27
|
-
nativeGetStatus: () => Promise<Record<string, unknown>>
|
|
28
|
-
nativeSetFanSpeed?: (i: unknown) => Promise<void>
|
|
29
|
-
resolveLinkedStatus: ResolveLinkedStatus
|
|
30
|
-
}): CapabilityRegistry {
|
|
31
|
-
const nativeProvider = {
|
|
32
|
-
getStatus: opts.nativeGetStatus,
|
|
33
|
-
setFanSpeed: opts.nativeSetFanSpeed ?? vi.fn(async () => undefined),
|
|
34
|
-
}
|
|
35
|
-
const deviceManager: FakeDeviceManager = {
|
|
36
|
-
resolveLinkedStatus: opts.resolveLinkedStatus,
|
|
37
|
-
}
|
|
38
|
-
return {
|
|
39
|
-
getNativeProvider<T>(_capName: string, _deviceId: number): T | null {
|
|
40
|
-
return nativeProvider as unknown as T
|
|
41
|
-
},
|
|
42
|
-
getSingleton<T>(capability: string): T | null {
|
|
43
|
-
if (capability === 'device-manager') {
|
|
44
|
-
return deviceManager as unknown as T
|
|
45
|
-
}
|
|
46
|
-
return null
|
|
47
|
-
},
|
|
48
|
-
// Minimal no-op stubs for the rest of the CapabilityRegistry surface
|
|
49
|
-
// so TypeScript is satisfied without pulling in the real kernel class.
|
|
50
|
-
listCapabilities: vi.fn(() => []),
|
|
51
|
-
registerProvider: vi.fn(),
|
|
52
|
-
unregisterProvider: vi.fn(),
|
|
53
|
-
getCollection: vi.fn(() => []),
|
|
54
|
-
getCollectionEntries: vi.fn(() => []),
|
|
55
|
-
registerNativeProvider: vi.fn(),
|
|
56
|
-
unregisterNativeProvider: vi.fn(),
|
|
57
|
-
getProviderForDevice: vi.fn(() => null),
|
|
58
|
-
getBindings: vi.fn(() => ({ entries: [] })),
|
|
59
|
-
setActiveSingleton: vi.fn(),
|
|
60
|
-
getSingletonAddonId: vi.fn(() => null),
|
|
61
|
-
getAddonIdForProvider: vi.fn(() => null),
|
|
62
|
-
on: vi.fn(),
|
|
63
|
-
off: vi.fn(),
|
|
64
|
-
dispose: vi.fn(),
|
|
65
|
-
} as unknown as CapabilityRegistry
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
69
|
-
|
|
70
|
-
const DEVICE_ID = 668
|
|
71
|
-
const CAP_NAME = 'vacuum-control' as Parameters<typeof requireDeviceScoped>[1]
|
|
72
|
-
|
|
73
|
-
describe('requireDeviceScoped — getStatus overlay via resolveLinkedStatus', () => {
|
|
74
|
-
it('returns the overlaid status when resolveLinkedStatus returns a non-null object', async () => {
|
|
75
|
-
const base = { state: 'idle', battery: 80 }
|
|
76
|
-
const overlaid = { state: 'paused', cleanWater: { status: 'low', level: null } }
|
|
77
|
-
|
|
78
|
-
const registry = makeRegistry({
|
|
79
|
-
nativeGetStatus: vi.fn(async () => base),
|
|
80
|
-
resolveLinkedStatus: vi.fn(async () => overlaid),
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
const dispatcher = requireDeviceScoped(registry, CAP_NAME)
|
|
84
|
-
expect(dispatcher).not.toBeNull()
|
|
85
|
-
|
|
86
|
-
// Call via the Proxy — method is resolved lazily
|
|
87
|
-
const result = await (
|
|
88
|
-
dispatcher as unknown as { getStatus: (i: { deviceId: number }) => Promise<unknown> }
|
|
89
|
-
).getStatus({ deviceId: DEVICE_ID })
|
|
90
|
-
|
|
91
|
-
expect(result).toEqual(overlaid)
|
|
92
|
-
expect(result).not.toEqual(base)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('returns the base provider result unchanged when resolveLinkedStatus returns null (no links)', async () => {
|
|
96
|
-
const base = { state: 'cleaning', battery: 60 }
|
|
97
|
-
|
|
98
|
-
const registry = makeRegistry({
|
|
99
|
-
nativeGetStatus: vi.fn(async () => base),
|
|
100
|
-
resolveLinkedStatus: vi.fn(async () => null),
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
const dispatcher = requireDeviceScoped(registry, CAP_NAME)
|
|
104
|
-
expect(dispatcher).not.toBeNull()
|
|
105
|
-
|
|
106
|
-
const result = await (
|
|
107
|
-
dispatcher as unknown as { getStatus: (i: { deviceId: number }) => Promise<unknown> }
|
|
108
|
-
).getStatus({ deviceId: DEVICE_ID })
|
|
109
|
-
|
|
110
|
-
expect(result).toEqual(base)
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
it('does NOT call resolveLinkedStatus for non-getStatus methods', async () => {
|
|
114
|
-
const base = { state: 'idle', battery: 90 }
|
|
115
|
-
const resolveLinkedStatus = vi.fn(async () => null)
|
|
116
|
-
const nativeSetFanSpeed = vi.fn(async () => undefined)
|
|
117
|
-
|
|
118
|
-
const registry = makeRegistry({
|
|
119
|
-
nativeGetStatus: vi.fn(async () => base),
|
|
120
|
-
nativeSetFanSpeed,
|
|
121
|
-
resolveLinkedStatus,
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
const dispatcher = requireDeviceScoped(registry, CAP_NAME)
|
|
125
|
-
expect(dispatcher).not.toBeNull()
|
|
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' })
|
|
132
|
-
|
|
133
|
-
expect(nativeSetFanSpeed).toHaveBeenCalledOnce()
|
|
134
|
-
// The overlay path must NOT be consulted for mutations
|
|
135
|
-
expect(resolveLinkedStatus).not.toHaveBeenCalled()
|
|
136
|
-
})
|
|
137
|
-
})
|