@camstack/server 1.0.0 → 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,383 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* F0 (slice-5 outbound) — hub-side `onUnownedCall` wiring.
|
|
3
|
-
*
|
|
4
|
-
* When a forked hub child issues `ctx.api.<cap>.<method>` for a cap NO local
|
|
5
|
-
* sibling owns, the hub's `LocalChildRegistry` must route it via the parent's
|
|
6
|
-
* `onUnownedCall` handler and return the result over UDS — instead of falling
|
|
7
|
-
* through `UDS_NO_ROUTE` to the child's own broker (which F1+F2 removes).
|
|
8
|
-
*
|
|
9
|
-
* The hub wires the handler in `onModuleInit` as:
|
|
10
|
-
* createParentUnownedCallHandler({ getResolver: () => this.resolver, broker: this.broker, logger })
|
|
11
|
-
*
|
|
12
|
-
* This test stands up the REAL production handler against:
|
|
13
|
-
* - a REAL `CapRouteResolver` configured with a hub-in-process provider, to
|
|
14
|
-
* prove the resolver-first branch resolves a hub cap, AND
|
|
15
|
-
* - a broker fake exposing a `$`-infra core service (`$core-caps`), to prove
|
|
16
|
-
* the broker-fallback branch reaches a core service the resolver can't see.
|
|
17
|
-
*
|
|
18
|
-
* We do NOT boot the full MoleculerService (it requires a real broker + DI
|
|
19
|
-
* graph); instead we exercise the exact handler the hub constructs, with the
|
|
20
|
-
* same resolver shape the hub injects, so both branches are covered.
|
|
21
|
-
*/
|
|
22
|
-
import { describe, it, expect, vi } from 'vitest'
|
|
23
|
-
import {
|
|
24
|
-
createParentUnownedCallHandler,
|
|
25
|
-
CapRouteResolver,
|
|
26
|
-
CapRouteError,
|
|
27
|
-
HubNodeRegistry,
|
|
28
|
-
} from '@camstack/kernel'
|
|
29
|
-
import type {
|
|
30
|
-
NodeCapAuthority,
|
|
31
|
-
InProcessProviderLookup,
|
|
32
|
-
CapRouteResolverDeps,
|
|
33
|
-
NodeNativeCapEntry,
|
|
34
|
-
HubLocalChildDispatcher,
|
|
35
|
-
CapCallInput,
|
|
36
|
-
} from '@camstack/kernel'
|
|
37
|
-
import type { ServiceBroker } from 'moleculer'
|
|
38
|
-
|
|
39
|
-
// A node authority that knows nothing — the only routable thing is the
|
|
40
|
-
// hub-in-process provider injected via inProcessProviders.
|
|
41
|
-
const emptyNodeAuthority: NodeCapAuthority = {
|
|
42
|
-
nodeKnowsCap: () => false,
|
|
43
|
-
nodeIsAgent: () => false,
|
|
44
|
-
nodeOnline: () => false,
|
|
45
|
-
listNodeIds: () => [],
|
|
46
|
-
getAddonId: () => null,
|
|
47
|
-
getAgentChildId: () => null,
|
|
48
|
-
isNativeCap: () => false,
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Broker fake exposing the hub's `$core-caps` core service (the bridge for
|
|
53
|
-
* `system`/`addons`/`capabilities`/`nodes` routers) so the broker-fallback
|
|
54
|
-
* branch resolves it. `$core-caps` is NOT a CapabilityRegistry provider, so
|
|
55
|
-
* the resolver returns `no-provider` for it and the fallback runs.
|
|
56
|
-
*/
|
|
57
|
-
function hubBrokerFake(): ServiceBroker {
|
|
58
|
-
const services = [{ name: '$core-caps', nodeID: 'hub', actions: { 'system.info': {} } }]
|
|
59
|
-
const broker = {
|
|
60
|
-
nodeID: 'hub',
|
|
61
|
-
call: vi.fn(async (action: string, params: unknown) => {
|
|
62
|
-
if (action === '$core-caps.system.info') return { uptimeSec: 99, params }
|
|
63
|
-
throw Object.assign(new Error(`Service not found: ${action}`), { type: 'SERVICE_NOT_FOUND' })
|
|
64
|
-
}),
|
|
65
|
-
waitForServices: vi.fn(async () => undefined),
|
|
66
|
-
registry: { getServiceList: () => services },
|
|
67
|
-
}
|
|
68
|
-
return broker as unknown as ServiceBroker
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Minimal `HubNodeRegistry`-shaped fake exposing only the lookup the unowned
|
|
73
|
-
* handler uses: `listNativeCapEntriesForDevice`. The handler depends solely on
|
|
74
|
-
* this method, so we don't need the full registry to exercise its routing.
|
|
75
|
-
*/
|
|
76
|
-
function nodeRegistryFake(
|
|
77
|
-
byDevice: Record<number, readonly NodeNativeCapEntry[]> = {},
|
|
78
|
-
): HubNodeRegistry {
|
|
79
|
-
const fake = {
|
|
80
|
-
listNativeCapEntriesForDevice: (deviceId: number): readonly NodeNativeCapEntry[] =>
|
|
81
|
-
byDevice[deviceId] ?? [],
|
|
82
|
-
}
|
|
83
|
-
return fake as unknown as HubNodeRegistry
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Hub-local UDS dispatcher fake (LocalChildRegistry surface). `resolveChildId`
|
|
88
|
-
* returns the configured childId on a (capName, deviceId) match else null;
|
|
89
|
-
* `callCapOnChild` records the call and echoes a sentinel result.
|
|
90
|
-
*/
|
|
91
|
-
function localDispatcherFake(owner?: { capName: string; deviceId: number; childId: string }): {
|
|
92
|
-
dispatcher: HubLocalChildDispatcher
|
|
93
|
-
resolveChildId: ReturnType<typeof vi.fn>
|
|
94
|
-
callCapOnChild: ReturnType<typeof vi.fn>
|
|
95
|
-
} {
|
|
96
|
-
const resolveChildId = vi.fn((capName: string, deviceId?: number): string | null =>
|
|
97
|
-
owner !== undefined && capName === owner.capName && deviceId === owner.deviceId
|
|
98
|
-
? owner.childId
|
|
99
|
-
: null,
|
|
100
|
-
)
|
|
101
|
-
const callCapOnChild = vi.fn(async (childId: string, input: CapCallInput) => ({
|
|
102
|
-
routedOverUds: childId,
|
|
103
|
-
capName: input.capName,
|
|
104
|
-
}))
|
|
105
|
-
const dispatcher: HubLocalChildDispatcher = { resolveChildId, callCapOnChild }
|
|
106
|
-
return { dispatcher, resolveChildId, callCapOnChild }
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
describe('hub onUnownedCall wiring (F0)', () => {
|
|
110
|
-
it('resolver-first: resolves a hub-in-process cap through the real CapRouteResolver', async () => {
|
|
111
|
-
const broker = hubBrokerFake()
|
|
112
|
-
|
|
113
|
-
// Hub-in-process provider for `settings-store`.
|
|
114
|
-
const settingsStore = { get: async (params: unknown) => ({ value: 'hub-resolved', params }) }
|
|
115
|
-
const inProcessProviders: InProcessProviderLookup = (capName) =>
|
|
116
|
-
capName === 'settings-store'
|
|
117
|
-
? {
|
|
118
|
-
invoke: (method, args) =>
|
|
119
|
-
Promise.resolve(
|
|
120
|
-
(settingsStore as Record<string, (a: unknown) => unknown>)[method](args),
|
|
121
|
-
),
|
|
122
|
-
}
|
|
123
|
-
: null
|
|
124
|
-
|
|
125
|
-
const resolverDeps: CapRouteResolverDeps = {
|
|
126
|
-
hubNodeId: 'hub',
|
|
127
|
-
broker,
|
|
128
|
-
hubLocalRegistry: null,
|
|
129
|
-
nodeAuthority: emptyNodeAuthority,
|
|
130
|
-
inProcessProviders,
|
|
131
|
-
}
|
|
132
|
-
const resolver = new CapRouteResolver(resolverDeps)
|
|
133
|
-
|
|
134
|
-
const handler = createParentUnownedCallHandler({
|
|
135
|
-
getResolver: () => resolver,
|
|
136
|
-
broker,
|
|
137
|
-
nodeRegistry: nodeRegistryFake(),
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
const result = await handler({ capName: 'settings-store', method: 'get', args: { key: 'k' } })
|
|
141
|
-
expect(result).toEqual({ value: 'hub-resolved', params: { key: 'k' } })
|
|
142
|
-
// Resolver served it — broker untouched.
|
|
143
|
-
expect(broker.call as ReturnType<typeof vi.fn>).not.toHaveBeenCalled()
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
it('broker-fallback: a `$`-infra core service (`$core-caps.system.info`) routes via the broker', async () => {
|
|
147
|
-
const broker = hubBrokerFake()
|
|
148
|
-
|
|
149
|
-
// No in-process providers and no known nodes → the resolver returns
|
|
150
|
-
// no-provider for `system`, exactly as it does live for the core routers.
|
|
151
|
-
const resolver = new CapRouteResolver({
|
|
152
|
-
hubNodeId: 'hub',
|
|
153
|
-
broker,
|
|
154
|
-
hubLocalRegistry: null,
|
|
155
|
-
nodeAuthority: emptyNodeAuthority,
|
|
156
|
-
inProcessProviders: () => null,
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
const handler = createParentUnownedCallHandler({
|
|
160
|
-
getResolver: () => resolver,
|
|
161
|
-
broker,
|
|
162
|
-
nodeRegistry: nodeRegistryFake(),
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
const result = await handler({ capName: 'system', method: 'info', args: undefined })
|
|
166
|
-
expect(result).toEqual({ uptimeSec: 99, params: undefined })
|
|
167
|
-
expect(broker.call).toHaveBeenCalledWith('$core-caps.system.info', undefined, undefined)
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
it('pre-init safety: getResolver returns null before onModuleInit → broker fallback still works', async () => {
|
|
171
|
-
const broker = hubBrokerFake()
|
|
172
|
-
// Mirrors the window before `this.resolver` is constructed: getResolver
|
|
173
|
-
// returns null, so the handler goes straight to the broker fallback.
|
|
174
|
-
const handler = createParentUnownedCallHandler({
|
|
175
|
-
getResolver: () => null,
|
|
176
|
-
broker,
|
|
177
|
-
nodeRegistry: nodeRegistryFake(),
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
const result = await handler({ capName: 'system', method: 'info', args: undefined })
|
|
181
|
-
expect(result).toEqual({ uptimeSec: 99, params: undefined })
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
it('deviceId-aware resolver: derives deviceId from args and routes via the resolver (no broker fallback)', async () => {
|
|
185
|
-
const broker = hubBrokerFake()
|
|
186
|
-
|
|
187
|
-
// A resolver-shaped fake that resolves a route ONLY when invoked with a
|
|
188
|
-
// deviceId — mirroring the real resolver routing a device-scoped native cap
|
|
189
|
-
// to its owning provider. Without a deviceId it returns no-provider.
|
|
190
|
-
let resolvedWithDeviceId: number | undefined
|
|
191
|
-
const resolver = {
|
|
192
|
-
resolveCapRoute: (capName: string, opts: { deviceId?: number }) => {
|
|
193
|
-
if (opts.deviceId === undefined) {
|
|
194
|
-
throw new CapRouteError(capName, undefined, { reason: 'no-provider', rejected: [] })
|
|
195
|
-
}
|
|
196
|
-
resolvedWithDeviceId = opts.deviceId
|
|
197
|
-
return { kind: 'hub-in-process', capName, deviceId: opts.deviceId }
|
|
198
|
-
},
|
|
199
|
-
dispatch: async () => ({ catalog: ['stream-a'] }),
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const handler = createParentUnownedCallHandler({
|
|
203
|
-
getResolver: () => resolver as unknown as CapRouteResolver,
|
|
204
|
-
broker,
|
|
205
|
-
nodeRegistry: nodeRegistryFake(),
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
// deviceId lives ONLY in args, NOT top-level — the handler must derive it.
|
|
209
|
-
const result = await handler({
|
|
210
|
-
capName: 'stream-catalog',
|
|
211
|
-
method: 'getCatalog',
|
|
212
|
-
args: { deviceId: 7 },
|
|
213
|
-
})
|
|
214
|
-
expect(result).toEqual({ catalog: ['stream-a'] })
|
|
215
|
-
expect(resolvedWithDeviceId).toBe(7)
|
|
216
|
-
// Resolver served it via the derived deviceId — broker untouched.
|
|
217
|
-
expect(broker.call).not.toHaveBeenCalled()
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
it('pinned broker fallback: resolver finds nothing → call is pinned to the owning node', async () => {
|
|
221
|
-
const broker = hubBrokerFake()
|
|
222
|
-
|
|
223
|
-
// Resolver always returns no-provider for this device-scoped cap.
|
|
224
|
-
const resolver = {
|
|
225
|
-
resolveCapRoute: (capName: string) => {
|
|
226
|
-
throw new CapRouteError(capName, undefined, { reason: 'no-provider', rejected: [] })
|
|
227
|
-
},
|
|
228
|
-
dispatch: async () => {
|
|
229
|
-
throw new Error('dispatch should not be reached')
|
|
230
|
-
},
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const owner: NodeNativeCapEntry = {
|
|
234
|
-
nodeId: 'agent',
|
|
235
|
-
addonId: 'reolink',
|
|
236
|
-
capName: 'stream-catalog',
|
|
237
|
-
deviceId: 7,
|
|
238
|
-
}
|
|
239
|
-
const handler = createParentUnownedCallHandler({
|
|
240
|
-
getResolver: () => resolver as unknown as CapRouteResolver,
|
|
241
|
-
broker,
|
|
242
|
-
nodeRegistry: nodeRegistryFake({ 7: [owner] }),
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
await handler({ capName: 'stream-catalog', method: 'getCatalog', args: { deviceId: 7 } }).catch(
|
|
246
|
-
() => undefined,
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
// The broker call is pinned to the owning node via call-opts `{ nodeID }`.
|
|
250
|
-
const callArgs = (broker.call as ReturnType<typeof vi.fn>).mock.calls[0]
|
|
251
|
-
expect(callArgs[2]).toEqual({ nodeID: 'agent' })
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
it('back-compat: no resolvable device-owner → broker call stays UNPINNED (load-balanced)', async () => {
|
|
255
|
-
const broker = hubBrokerFake()
|
|
256
|
-
|
|
257
|
-
const resolver = {
|
|
258
|
-
resolveCapRoute: (capName: string) => {
|
|
259
|
-
throw new CapRouteError(capName, undefined, { reason: 'no-provider', rejected: [] })
|
|
260
|
-
},
|
|
261
|
-
dispatch: async () => {
|
|
262
|
-
throw new Error('dispatch should not be reached')
|
|
263
|
-
},
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const handler = createParentUnownedCallHandler({
|
|
267
|
-
getResolver: () => resolver as unknown as CapRouteResolver,
|
|
268
|
-
broker,
|
|
269
|
-
nodeRegistry: nodeRegistryFake(), // no owners for any device
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
const result = await handler({ capName: 'system', method: 'info', args: undefined })
|
|
273
|
-
expect(result).toEqual({ uptimeSec: 99, params: undefined })
|
|
274
|
-
// Unpinned: third arg (call-opts) is undefined — today's behavior preserved.
|
|
275
|
-
const callArgs = (broker.call as ReturnType<typeof vi.fn>).mock.calls[0]
|
|
276
|
-
expect(callArgs[2]).toBeUndefined()
|
|
277
|
-
})
|
|
278
|
-
|
|
279
|
-
it('hub-local owner: device-native cap owned by a hub-local UDS child routes over UDS (broker untouched)', async () => {
|
|
280
|
-
const broker = hubBrokerFake()
|
|
281
|
-
// Resolver misses the device-scoped native cap (mirrors live behavior).
|
|
282
|
-
const resolver = {
|
|
283
|
-
resolveCapRoute: (capName: string) => {
|
|
284
|
-
throw new CapRouteError(capName, undefined, { reason: 'no-provider', rejected: [] })
|
|
285
|
-
},
|
|
286
|
-
dispatch: async () => {
|
|
287
|
-
throw new Error('dispatch should not be reached')
|
|
288
|
-
},
|
|
289
|
-
}
|
|
290
|
-
const { dispatcher, resolveChildId, callCapOnChild } = localDispatcherFake({
|
|
291
|
-
capName: 'stream-catalog',
|
|
292
|
-
deviceId: 7,
|
|
293
|
-
childId: 'child-reolink',
|
|
294
|
-
})
|
|
295
|
-
|
|
296
|
-
const handler = createParentUnownedCallHandler({
|
|
297
|
-
getResolver: () => resolver as unknown as CapRouteResolver,
|
|
298
|
-
broker,
|
|
299
|
-
nodeRegistry: nodeRegistryFake(), // empty — hub-local child is NOT in HubNodeRegistry
|
|
300
|
-
getLocalDispatcher: () => dispatcher,
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
const result = await handler({
|
|
304
|
-
capName: 'stream-catalog',
|
|
305
|
-
method: 'getCatalog',
|
|
306
|
-
args: { deviceId: 7 },
|
|
307
|
-
})
|
|
308
|
-
expect(result).toEqual({ routedOverUds: 'child-reolink', capName: 'stream-catalog' })
|
|
309
|
-
expect(resolveChildId).toHaveBeenCalledWith('stream-catalog', 7)
|
|
310
|
-
expect(callCapOnChild).toHaveBeenCalledWith('child-reolink', {
|
|
311
|
-
capName: 'stream-catalog',
|
|
312
|
-
method: 'getCatalog',
|
|
313
|
-
args: { deviceId: 7 },
|
|
314
|
-
deviceId: 7,
|
|
315
|
-
})
|
|
316
|
-
expect(broker.call).not.toHaveBeenCalled()
|
|
317
|
-
})
|
|
318
|
-
|
|
319
|
-
it('remote owner: hub-local dispatcher misses → pinned broker call', async () => {
|
|
320
|
-
const broker = hubBrokerFake()
|
|
321
|
-
const resolver = {
|
|
322
|
-
resolveCapRoute: (capName: string) => {
|
|
323
|
-
throw new CapRouteError(capName, undefined, { reason: 'no-provider', rejected: [] })
|
|
324
|
-
},
|
|
325
|
-
dispatch: async () => {
|
|
326
|
-
throw new Error('dispatch should not be reached')
|
|
327
|
-
},
|
|
328
|
-
}
|
|
329
|
-
const { dispatcher, callCapOnChild } = localDispatcherFake() // no local owner
|
|
330
|
-
const owner: NodeNativeCapEntry = {
|
|
331
|
-
nodeId: 'agent',
|
|
332
|
-
addonId: 'reolink',
|
|
333
|
-
capName: 'stream-catalog',
|
|
334
|
-
deviceId: 7,
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const handler = createParentUnownedCallHandler({
|
|
338
|
-
getResolver: () => resolver as unknown as CapRouteResolver,
|
|
339
|
-
broker,
|
|
340
|
-
nodeRegistry: nodeRegistryFake({ 7: [owner] }),
|
|
341
|
-
getLocalDispatcher: () => dispatcher,
|
|
342
|
-
})
|
|
343
|
-
|
|
344
|
-
await handler({ capName: 'stream-catalog', method: 'getCatalog', args: { deviceId: 7 } }).catch(
|
|
345
|
-
() => undefined,
|
|
346
|
-
)
|
|
347
|
-
|
|
348
|
-
expect(callCapOnChild).not.toHaveBeenCalled()
|
|
349
|
-
const callArgs = (broker.call as ReturnType<typeof vi.fn>).mock.calls[0]
|
|
350
|
-
expect(callArgs[2]).toEqual({ nodeID: 'agent' })
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
it('no local dispatcher (getter returns null): behaves as before — HubNodeRegistry → broker', async () => {
|
|
354
|
-
const broker = hubBrokerFake()
|
|
355
|
-
const resolver = {
|
|
356
|
-
resolveCapRoute: (capName: string) => {
|
|
357
|
-
throw new CapRouteError(capName, undefined, { reason: 'no-provider', rejected: [] })
|
|
358
|
-
},
|
|
359
|
-
dispatch: async () => {
|
|
360
|
-
throw new Error('dispatch should not be reached')
|
|
361
|
-
},
|
|
362
|
-
}
|
|
363
|
-
const owner: NodeNativeCapEntry = {
|
|
364
|
-
nodeId: 'agent',
|
|
365
|
-
addonId: 'reolink',
|
|
366
|
-
capName: 'stream-catalog',
|
|
367
|
-
deviceId: 7,
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const handler = createParentUnownedCallHandler({
|
|
371
|
-
getResolver: () => resolver as unknown as CapRouteResolver,
|
|
372
|
-
broker,
|
|
373
|
-
nodeRegistry: nodeRegistryFake({ 7: [owner] }),
|
|
374
|
-
getLocalDispatcher: () => null,
|
|
375
|
-
})
|
|
376
|
-
|
|
377
|
-
await handler({ capName: 'stream-catalog', method: 'getCatalog', args: { deviceId: 7 } }).catch(
|
|
378
|
-
() => undefined,
|
|
379
|
-
)
|
|
380
|
-
const callArgs = (broker.call as ReturnType<typeof vi.fn>).mock.calls[0]
|
|
381
|
-
expect(callArgs[2]).toEqual({ nodeID: 'agent' })
|
|
382
|
-
})
|
|
383
|
-
})
|
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MoleculerService.applyNodeManifest re-handshake idempotency (D3).
|
|
3
|
-
*
|
|
4
|
-
* Pre-existing bug: `applyNodeManifest` ran on EVERY `$hub.registerNode`
|
|
5
|
-
* and called `registry.registerProvider(cap, key, proxy)` unconditionally
|
|
6
|
-
* for every cap in the manifest. The D3 protocol legitimately re-handshakes
|
|
7
|
-
* (a node sends its COMPLETE manifest again — e.g. the post-device-restore
|
|
8
|
-
* `nativeCaps` re-handshake). `CapabilityRegistry.registerProvider` throws
|
|
9
|
-
* on a duplicate `(cap, addonId)` pair (the guard is CORRECT — it catches an
|
|
10
|
-
* addon double-`initialize()`), so the second handshake threw, the
|
|
11
|
-
* registering node's retry loop retried forever, and the cluster entered a
|
|
12
|
-
* registration storm.
|
|
13
|
-
*
|
|
14
|
-
* The fix makes `applyNodeManifest` diff-based: it honours the CLAUDE.md
|
|
15
|
-
* invariant "`registerNode` replaces the node's entire cap set atomically".
|
|
16
|
-
* A re-handshake with the SAME manifest is a no-op; a re-handshake that
|
|
17
|
-
* drops a cap unregisters exactly that cap; a re-handshake that adds a cap
|
|
18
|
-
* registers only the new one. No throw, no churn.
|
|
19
|
-
*
|
|
20
|
-
* These specs drive the genuine `MoleculerService` registration path twice
|
|
21
|
-
* for the same `nodeId` against a REAL `CapabilityRegistry` (with its real
|
|
22
|
-
* duplicate guard) and assert idempotency.
|
|
23
|
-
*/
|
|
24
|
-
import { describe, it, expect, beforeEach } from 'vitest'
|
|
25
|
-
import { z } from 'zod'
|
|
26
|
-
import { CapabilityRegistry } from '@camstack/kernel'
|
|
27
|
-
import type { RegisterNodeParams } from '@camstack/kernel'
|
|
28
|
-
import type { CapabilityDefinition, IScopedLogger, SystemEvent } from '@camstack/types'
|
|
29
|
-
import { MoleculerService } from '../core/moleculer/moleculer.service.js'
|
|
30
|
-
import type { EventBusService } from '../core/events/event-bus.service.js'
|
|
31
|
-
import type { ConfigService } from '../core/config/config.service.js'
|
|
32
|
-
import type { LoggingService } from '../core/logging/logging.service.js'
|
|
33
|
-
import type { CapabilityService } from '../core/capability/capability.service.js'
|
|
34
|
-
import type { StreamProbeService } from '../core/streaming/stream-probe.service.js'
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Test-only view of `MoleculerService` exposing:
|
|
38
|
-
* - the genuine private registration entrypoint `onRegisterNode` — the
|
|
39
|
-
* same closure the `$hub.registerNode` Moleculer action invokes in
|
|
40
|
-
* production. Driving it directly exercises the real
|
|
41
|
-
* `nodeRegistry.registerNode` + `applyNodeManifest` path without
|
|
42
|
-
* standing up a TCP broker.
|
|
43
|
-
* - the public `createCapabilityProxy` method used to assert whether a
|
|
44
|
-
* dropped capability's call-routing entry was correctly removed from
|
|
45
|
-
* `nodeCallFns` after a reduced re-handshake.
|
|
46
|
-
*/
|
|
47
|
-
interface RegisterNodeDriver {
|
|
48
|
-
onRegisterNode: (params: RegisterNodeParams) => void
|
|
49
|
-
createCapabilityProxy: (
|
|
50
|
-
capabilityName: string,
|
|
51
|
-
nodeId: string,
|
|
52
|
-
) => Record<string, (params: unknown) => Promise<unknown>> | null
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Build a harness whose declared caps are SINGLETON, for active-provider preference tests. */
|
|
56
|
-
function createSingletonHarness(capNames: readonly string[]): Harness {
|
|
57
|
-
return createHarness(capNames, 'singleton')
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** A minimal real `CapabilityDefinition` so `getDefinition`/`expandCapMethods` resolve. */
|
|
61
|
-
function makeCapDef(
|
|
62
|
-
name: string,
|
|
63
|
-
mode: 'collection' | 'singleton' = 'collection',
|
|
64
|
-
): CapabilityDefinition {
|
|
65
|
-
return {
|
|
66
|
-
name,
|
|
67
|
-
scope: 'system',
|
|
68
|
-
mode,
|
|
69
|
-
methods: {
|
|
70
|
-
ping: {
|
|
71
|
-
input: z.object({}),
|
|
72
|
-
output: z.object({}),
|
|
73
|
-
kind: 'query',
|
|
74
|
-
auth: 'protected',
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function makeLogger(): IScopedLogger {
|
|
81
|
-
const logger = {
|
|
82
|
-
info: () => undefined,
|
|
83
|
-
warn: () => undefined,
|
|
84
|
-
error: () => undefined,
|
|
85
|
-
debug: () => undefined,
|
|
86
|
-
trace: () => undefined,
|
|
87
|
-
fatal: () => undefined,
|
|
88
|
-
child: (() => logger) as IScopedLogger['child'],
|
|
89
|
-
}
|
|
90
|
-
return logger as unknown as IScopedLogger
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
interface Harness {
|
|
94
|
-
readonly driver: RegisterNodeDriver
|
|
95
|
-
readonly registry: CapabilityRegistry
|
|
96
|
-
readonly capNames: readonly string[]
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Build a real `MoleculerService` (constructor only — no broker start) wired
|
|
101
|
-
* to a real `CapabilityRegistry` pre-declaring `capNames`. Returns the
|
|
102
|
-
* service viewed through `RegisterNodeDriver` plus the registry to assert on.
|
|
103
|
-
*
|
|
104
|
-
* The broker is never started, so there is nothing to stop in teardown —
|
|
105
|
-
* no `afterEach` teardown is needed.
|
|
106
|
-
*/
|
|
107
|
-
function createHarness(
|
|
108
|
-
capNames: readonly string[],
|
|
109
|
-
mode: 'collection' | 'singleton' = 'collection',
|
|
110
|
-
): Harness {
|
|
111
|
-
const registry = new CapabilityRegistry(makeLogger())
|
|
112
|
-
for (const name of capNames) {
|
|
113
|
-
registry.declareCapability(makeCapDef(name, mode))
|
|
114
|
-
}
|
|
115
|
-
// Boot-complete state — `getAllProviders` returns [] until `ready()`.
|
|
116
|
-
registry.ready()
|
|
117
|
-
|
|
118
|
-
const fakeEventBus = {
|
|
119
|
-
emit: (_event: SystemEvent) => undefined,
|
|
120
|
-
// `ReadinessRegistry` (built in the MoleculerService constructor)
|
|
121
|
-
// subscribes to `system.ready-state` + `agent.offline`. A no-op
|
|
122
|
-
// subscription returning an unsubscribe fn keeps the constructor happy.
|
|
123
|
-
subscribe: () => () => undefined,
|
|
124
|
-
getRecent: () => [],
|
|
125
|
-
} as unknown as EventBusService
|
|
126
|
-
|
|
127
|
-
const fakeConfig = {
|
|
128
|
-
get: () => undefined,
|
|
129
|
-
getAddonConfig: () => ({}),
|
|
130
|
-
} as unknown as ConfigService
|
|
131
|
-
|
|
132
|
-
const fakeLogging = {
|
|
133
|
-
createLogger: () => makeLogger(),
|
|
134
|
-
writeFromWorker: () => undefined,
|
|
135
|
-
} as unknown as LoggingService
|
|
136
|
-
|
|
137
|
-
const fakeCapability = {
|
|
138
|
-
getRegistry: () => registry,
|
|
139
|
-
} as unknown as CapabilityService
|
|
140
|
-
|
|
141
|
-
const fakeStreamProbe = {} as unknown as StreamProbeService
|
|
142
|
-
|
|
143
|
-
const service = new MoleculerService(
|
|
144
|
-
fakeEventBus,
|
|
145
|
-
fakeConfig,
|
|
146
|
-
fakeLogging,
|
|
147
|
-
fakeCapability,
|
|
148
|
-
fakeStreamProbe,
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
return {
|
|
152
|
-
driver: service as unknown as RegisterNodeDriver,
|
|
153
|
-
registry,
|
|
154
|
-
capNames,
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** Provider count for a cap on the registry — counts every registered key. */
|
|
159
|
-
function providerCount(registry: CapabilityRegistry, capName: string): number {
|
|
160
|
-
return registry.getAllProviders(capName).length
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
describe('MoleculerService.applyNodeManifest — re-handshake idempotency', () => {
|
|
164
|
-
let harness: Harness
|
|
165
|
-
|
|
166
|
-
beforeEach(() => {
|
|
167
|
-
harness = createHarness(['cap-alpha', 'cap-beta'])
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
it('a re-handshake with the IDENTICAL manifest does not throw and leaves each provider registered exactly once', () => {
|
|
171
|
-
const manifest: RegisterNodeParams = {
|
|
172
|
-
nodeId: 'hub/reolink',
|
|
173
|
-
addons: [{ addonId: 'reolink', capabilities: ['cap-alpha', 'cap-beta'] }],
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// First handshake.
|
|
177
|
-
harness.driver.onRegisterNode(manifest)
|
|
178
|
-
expect(providerCount(harness.registry, 'cap-alpha')).toBe(1)
|
|
179
|
-
expect(providerCount(harness.registry, 'cap-beta')).toBe(1)
|
|
180
|
-
|
|
181
|
-
// Re-handshake — the documented post-device-restore re-handshake.
|
|
182
|
-
// BEFORE the fix this threw `provider already registered for capability`.
|
|
183
|
-
expect(() => harness.driver.onRegisterNode(manifest)).not.toThrow()
|
|
184
|
-
|
|
185
|
-
// No duplicate, no loss — exactly one provider per cap.
|
|
186
|
-
expect(providerCount(harness.registry, 'cap-alpha')).toBe(1)
|
|
187
|
-
expect(providerCount(harness.registry, 'cap-beta')).toBe(1)
|
|
188
|
-
expect(harness.registry.getProviderByAddon('cap-alpha', 'reolink')).not.toBeNull()
|
|
189
|
-
expect(harness.registry.getProviderByAddon('cap-beta', 'reolink')).not.toBeNull()
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
it('three handshakes survive without throwing (registration storm regression guard)', () => {
|
|
193
|
-
const manifest: RegisterNodeParams = {
|
|
194
|
-
nodeId: 'hub/reolink',
|
|
195
|
-
addons: [{ addonId: 'reolink', capabilities: ['cap-alpha', 'cap-beta'] }],
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
expect(() => {
|
|
199
|
-
harness.driver.onRegisterNode(manifest)
|
|
200
|
-
harness.driver.onRegisterNode(manifest)
|
|
201
|
-
harness.driver.onRegisterNode(manifest)
|
|
202
|
-
}).not.toThrow()
|
|
203
|
-
|
|
204
|
-
expect(providerCount(harness.registry, 'cap-alpha')).toBe(1)
|
|
205
|
-
expect(providerCount(harness.registry, 'cap-beta')).toBe(1)
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
it('a re-handshake that DROPS one capability unregisters that provider while the others remain', () => {
|
|
209
|
-
const fullManifest: RegisterNodeParams = {
|
|
210
|
-
nodeId: 'hub/reolink',
|
|
211
|
-
addons: [{ addonId: 'reolink', capabilities: ['cap-alpha', 'cap-beta'] }],
|
|
212
|
-
}
|
|
213
|
-
const reducedManifest: RegisterNodeParams = {
|
|
214
|
-
nodeId: 'hub/reolink',
|
|
215
|
-
addons: [{ addonId: 'reolink', capabilities: ['cap-alpha'] }],
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// First: full manifest — both caps registered.
|
|
219
|
-
harness.driver.onRegisterNode(fullManifest)
|
|
220
|
-
expect(providerCount(harness.registry, 'cap-alpha')).toBe(1)
|
|
221
|
-
expect(providerCount(harness.registry, 'cap-beta')).toBe(1)
|
|
222
|
-
|
|
223
|
-
// Second: identical full manifest — still idempotent, no throw.
|
|
224
|
-
expect(() => harness.driver.onRegisterNode(fullManifest)).not.toThrow()
|
|
225
|
-
expect(providerCount(harness.registry, 'cap-alpha')).toBe(1)
|
|
226
|
-
expect(providerCount(harness.registry, 'cap-beta')).toBe(1)
|
|
227
|
-
|
|
228
|
-
// Third: reduced manifest — cap-beta dropped, cap-alpha kept.
|
|
229
|
-
expect(() => harness.driver.onRegisterNode(reducedManifest)).not.toThrow()
|
|
230
|
-
expect(providerCount(harness.registry, 'cap-alpha')).toBe(1)
|
|
231
|
-
expect(providerCount(harness.registry, 'cap-beta')).toBe(0)
|
|
232
|
-
expect(harness.registry.getProviderByAddon('cap-alpha', 'reolink')).not.toBeNull()
|
|
233
|
-
expect(harness.registry.getProviderByAddon('cap-beta', 'reolink')).toBeNull()
|
|
234
|
-
|
|
235
|
-
// Verify that the call-routing entry for the dropped cap was removed
|
|
236
|
-
// from `nodeCallFns`. `createCapabilityProxy` is the public seam that
|
|
237
|
-
// delegates to `findCallFn` internally — a null return means no entry
|
|
238
|
-
// exists for that (nodeId, cap) pair, confirming the delete ran.
|
|
239
|
-
expect(harness.driver.createCapabilityProxy('cap-beta', 'hub/reolink')).toBeNull()
|
|
240
|
-
// The still-present cap must remain routable.
|
|
241
|
-
expect(harness.driver.createCapabilityProxy('cap-alpha', 'hub/reolink')).not.toBeNull()
|
|
242
|
-
})
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
describe('MoleculerService.applyNodeManifest — singleton local-first preference (UDS regression)', () => {
|
|
246
|
-
// A `placement: 'any-node'` singleton (e.g. `pipeline-executor`) can register
|
|
247
|
-
// on BOTH the hub-local forked child AND a remote agent. The hub must resolve
|
|
248
|
-
// its OWN local provider (reachable over UDS) — never the agent's proxy, whose
|
|
249
|
-
// callFn routes over Moleculer to a UDS-only agent runner that no longer hosts
|
|
250
|
-
// the service ("not found on <agent>"). First-registered-wins picked the agent.
|
|
251
|
-
const LOCAL = 'hub/detection-pipeline'
|
|
252
|
-
const REMOTE = 'dev-agent-0/detection-pipeline'
|
|
253
|
-
const singleton: (nodeId: string) => RegisterNodeParams = (nodeId) => ({
|
|
254
|
-
nodeId,
|
|
255
|
-
addons: [{ addonId: 'detection-pipeline', capabilities: ['pipeline-executor'] }],
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
it('prefers the hub-local provider when the REMOTE one registered first', () => {
|
|
259
|
-
const { driver, registry } = createSingletonHarness(['pipeline-executor'])
|
|
260
|
-
driver.onRegisterNode(singleton(REMOTE)) // agent first → would win under first-wins
|
|
261
|
-
driver.onRegisterNode(singleton(LOCAL)) // hub-local second
|
|
262
|
-
// Active provider must be the hub-local key (bare addonId, no '@').
|
|
263
|
-
expect(registry.getSingletonAddonId('pipeline-executor')).toBe('detection-pipeline')
|
|
264
|
-
expect(registry.getSingletonAddonId('pipeline-executor')).not.toContain('@')
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
it('keeps the hub-local provider active when a REMOTE one registers afterwards', () => {
|
|
268
|
-
const { driver, registry } = createSingletonHarness(['pipeline-executor'])
|
|
269
|
-
driver.onRegisterNode(singleton(LOCAL)) // hub-local first
|
|
270
|
-
driver.onRegisterNode(singleton(REMOTE)) // agent second must NOT steal active
|
|
271
|
-
expect(registry.getSingletonAddonId('pipeline-executor')).toBe('detection-pipeline')
|
|
272
|
-
})
|
|
273
|
-
})
|