@camstack/server 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
@@ -94,7 +94,9 @@ function localDispatcherFake(owner?: { capName: string; deviceId: number; childI
|
|
|
94
94
|
callCapOnChild: ReturnType<typeof vi.fn>
|
|
95
95
|
} {
|
|
96
96
|
const resolveChildId = vi.fn((capName: string, deviceId?: number): string | null =>
|
|
97
|
-
owner !== undefined && capName === owner.capName && deviceId === owner.deviceId
|
|
97
|
+
owner !== undefined && capName === owner.capName && deviceId === owner.deviceId
|
|
98
|
+
? owner.childId
|
|
99
|
+
: null,
|
|
98
100
|
)
|
|
99
101
|
const callCapOnChild = vi.fn(async (childId: string, input: CapCallInput) => ({
|
|
100
102
|
routedOverUds: childId,
|
|
@@ -112,7 +114,12 @@ describe('hub onUnownedCall wiring (F0)', () => {
|
|
|
112
114
|
const settingsStore = { get: async (params: unknown) => ({ value: 'hub-resolved', params }) }
|
|
113
115
|
const inProcessProviders: InProcessProviderLookup = (capName) =>
|
|
114
116
|
capName === 'settings-store'
|
|
115
|
-
? {
|
|
117
|
+
? {
|
|
118
|
+
invoke: (method, args) =>
|
|
119
|
+
Promise.resolve(
|
|
120
|
+
(settingsStore as Record<string, (a: unknown) => unknown>)[method](args),
|
|
121
|
+
),
|
|
122
|
+
}
|
|
116
123
|
: null
|
|
117
124
|
|
|
118
125
|
const resolverDeps: CapRouteResolverDeps = {
|
|
@@ -124,12 +131,16 @@ describe('hub onUnownedCall wiring (F0)', () => {
|
|
|
124
131
|
}
|
|
125
132
|
const resolver = new CapRouteResolver(resolverDeps)
|
|
126
133
|
|
|
127
|
-
const handler = createParentUnownedCallHandler({
|
|
134
|
+
const handler = createParentUnownedCallHandler({
|
|
135
|
+
getResolver: () => resolver,
|
|
136
|
+
broker,
|
|
137
|
+
nodeRegistry: nodeRegistryFake(),
|
|
138
|
+
})
|
|
128
139
|
|
|
129
140
|
const result = await handler({ capName: 'settings-store', method: 'get', args: { key: 'k' } })
|
|
130
141
|
expect(result).toEqual({ value: 'hub-resolved', params: { key: 'k' } })
|
|
131
142
|
// Resolver served it — broker untouched.
|
|
132
|
-
expect(
|
|
143
|
+
expect(broker.call as ReturnType<typeof vi.fn>).not.toHaveBeenCalled()
|
|
133
144
|
})
|
|
134
145
|
|
|
135
146
|
it('broker-fallback: a `$`-infra core service (`$core-caps.system.info`) routes via the broker', async () => {
|
|
@@ -145,7 +156,11 @@ describe('hub onUnownedCall wiring (F0)', () => {
|
|
|
145
156
|
inProcessProviders: () => null,
|
|
146
157
|
})
|
|
147
158
|
|
|
148
|
-
const handler = createParentUnownedCallHandler({
|
|
159
|
+
const handler = createParentUnownedCallHandler({
|
|
160
|
+
getResolver: () => resolver,
|
|
161
|
+
broker,
|
|
162
|
+
nodeRegistry: nodeRegistryFake(),
|
|
163
|
+
})
|
|
149
164
|
|
|
150
165
|
const result = await handler({ capName: 'system', method: 'info', args: undefined })
|
|
151
166
|
expect(result).toEqual({ uptimeSec: 99, params: undefined })
|
|
@@ -156,7 +171,11 @@ describe('hub onUnownedCall wiring (F0)', () => {
|
|
|
156
171
|
const broker = hubBrokerFake()
|
|
157
172
|
// Mirrors the window before `this.resolver` is constructed: getResolver
|
|
158
173
|
// returns null, so the handler goes straight to the broker fallback.
|
|
159
|
-
const handler = createParentUnownedCallHandler({
|
|
174
|
+
const handler = createParentUnownedCallHandler({
|
|
175
|
+
getResolver: () => null,
|
|
176
|
+
broker,
|
|
177
|
+
nodeRegistry: nodeRegistryFake(),
|
|
178
|
+
})
|
|
160
179
|
|
|
161
180
|
const result = await handler({ capName: 'system', method: 'info', args: undefined })
|
|
162
181
|
expect(result).toEqual({ uptimeSec: 99, params: undefined })
|
|
@@ -187,7 +206,11 @@ describe('hub onUnownedCall wiring (F0)', () => {
|
|
|
187
206
|
})
|
|
188
207
|
|
|
189
208
|
// deviceId lives ONLY in args, NOT top-level — the handler must derive it.
|
|
190
|
-
const result = await handler({
|
|
209
|
+
const result = await handler({
|
|
210
|
+
capName: 'stream-catalog',
|
|
211
|
+
method: 'getCatalog',
|
|
212
|
+
args: { deviceId: 7 },
|
|
213
|
+
})
|
|
191
214
|
expect(result).toEqual({ catalog: ['stream-a'] })
|
|
192
215
|
expect(resolvedWithDeviceId).toBe(7)
|
|
193
216
|
// Resolver served it via the derived deviceId — broker untouched.
|
|
@@ -207,14 +230,21 @@ describe('hub onUnownedCall wiring (F0)', () => {
|
|
|
207
230
|
},
|
|
208
231
|
}
|
|
209
232
|
|
|
210
|
-
const owner: NodeNativeCapEntry = {
|
|
233
|
+
const owner: NodeNativeCapEntry = {
|
|
234
|
+
nodeId: 'agent',
|
|
235
|
+
addonId: 'reolink',
|
|
236
|
+
capName: 'stream-catalog',
|
|
237
|
+
deviceId: 7,
|
|
238
|
+
}
|
|
211
239
|
const handler = createParentUnownedCallHandler({
|
|
212
240
|
getResolver: () => resolver as unknown as CapRouteResolver,
|
|
213
241
|
broker,
|
|
214
242
|
nodeRegistry: nodeRegistryFake({ 7: [owner] }),
|
|
215
243
|
})
|
|
216
244
|
|
|
217
|
-
await handler({ capName: 'stream-catalog', method: 'getCatalog', args: { deviceId: 7 } }).catch(
|
|
245
|
+
await handler({ capName: 'stream-catalog', method: 'getCatalog', args: { deviceId: 7 } }).catch(
|
|
246
|
+
() => undefined,
|
|
247
|
+
)
|
|
218
248
|
|
|
219
249
|
// The broker call is pinned to the owning node via call-opts `{ nodeID }`.
|
|
220
250
|
const callArgs = (broker.call as ReturnType<typeof vi.fn>).mock.calls[0]
|
|
@@ -253,7 +283,9 @@ describe('hub onUnownedCall wiring (F0)', () => {
|
|
|
253
283
|
resolveCapRoute: (capName: string) => {
|
|
254
284
|
throw new CapRouteError(capName, undefined, { reason: 'no-provider', rejected: [] })
|
|
255
285
|
},
|
|
256
|
-
dispatch: async () => {
|
|
286
|
+
dispatch: async () => {
|
|
287
|
+
throw new Error('dispatch should not be reached')
|
|
288
|
+
},
|
|
257
289
|
}
|
|
258
290
|
const { dispatcher, resolveChildId, callCapOnChild } = localDispatcherFake({
|
|
259
291
|
capName: 'stream-catalog',
|
|
@@ -268,7 +300,11 @@ describe('hub onUnownedCall wiring (F0)', () => {
|
|
|
268
300
|
getLocalDispatcher: () => dispatcher,
|
|
269
301
|
})
|
|
270
302
|
|
|
271
|
-
const result = await handler({
|
|
303
|
+
const result = await handler({
|
|
304
|
+
capName: 'stream-catalog',
|
|
305
|
+
method: 'getCatalog',
|
|
306
|
+
args: { deviceId: 7 },
|
|
307
|
+
})
|
|
272
308
|
expect(result).toEqual({ routedOverUds: 'child-reolink', capName: 'stream-catalog' })
|
|
273
309
|
expect(resolveChildId).toHaveBeenCalledWith('stream-catalog', 7)
|
|
274
310
|
expect(callCapOnChild).toHaveBeenCalledWith('child-reolink', {
|
|
@@ -286,10 +322,17 @@ describe('hub onUnownedCall wiring (F0)', () => {
|
|
|
286
322
|
resolveCapRoute: (capName: string) => {
|
|
287
323
|
throw new CapRouteError(capName, undefined, { reason: 'no-provider', rejected: [] })
|
|
288
324
|
},
|
|
289
|
-
dispatch: async () => {
|
|
325
|
+
dispatch: async () => {
|
|
326
|
+
throw new Error('dispatch should not be reached')
|
|
327
|
+
},
|
|
290
328
|
}
|
|
291
329
|
const { dispatcher, callCapOnChild } = localDispatcherFake() // no local owner
|
|
292
|
-
const owner: NodeNativeCapEntry = {
|
|
330
|
+
const owner: NodeNativeCapEntry = {
|
|
331
|
+
nodeId: 'agent',
|
|
332
|
+
addonId: 'reolink',
|
|
333
|
+
capName: 'stream-catalog',
|
|
334
|
+
deviceId: 7,
|
|
335
|
+
}
|
|
293
336
|
|
|
294
337
|
const handler = createParentUnownedCallHandler({
|
|
295
338
|
getResolver: () => resolver as unknown as CapRouteResolver,
|
|
@@ -298,7 +341,9 @@ describe('hub onUnownedCall wiring (F0)', () => {
|
|
|
298
341
|
getLocalDispatcher: () => dispatcher,
|
|
299
342
|
})
|
|
300
343
|
|
|
301
|
-
await handler({ capName: 'stream-catalog', method: 'getCatalog', args: { deviceId: 7 } }).catch(
|
|
344
|
+
await handler({ capName: 'stream-catalog', method: 'getCatalog', args: { deviceId: 7 } }).catch(
|
|
345
|
+
() => undefined,
|
|
346
|
+
)
|
|
302
347
|
|
|
303
348
|
expect(callCapOnChild).not.toHaveBeenCalled()
|
|
304
349
|
const callArgs = (broker.call as ReturnType<typeof vi.fn>).mock.calls[0]
|
|
@@ -311,9 +356,16 @@ describe('hub onUnownedCall wiring (F0)', () => {
|
|
|
311
356
|
resolveCapRoute: (capName: string) => {
|
|
312
357
|
throw new CapRouteError(capName, undefined, { reason: 'no-provider', rejected: [] })
|
|
313
358
|
},
|
|
314
|
-
dispatch: async () => {
|
|
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,
|
|
315
368
|
}
|
|
316
|
-
const owner: NodeNativeCapEntry = { nodeId: 'agent', addonId: 'reolink', capName: 'stream-catalog', deviceId: 7 }
|
|
317
369
|
|
|
318
370
|
const handler = createParentUnownedCallHandler({
|
|
319
371
|
getResolver: () => resolver as unknown as CapRouteResolver,
|
|
@@ -322,7 +374,9 @@ describe('hub onUnownedCall wiring (F0)', () => {
|
|
|
322
374
|
getLocalDispatcher: () => null,
|
|
323
375
|
})
|
|
324
376
|
|
|
325
|
-
await handler({ capName: 'stream-catalog', method: 'getCatalog', args: { deviceId: 7 } }).catch(
|
|
377
|
+
await handler({ capName: 'stream-catalog', method: 'getCatalog', args: { deviceId: 7 } }).catch(
|
|
378
|
+
() => undefined,
|
|
379
|
+
)
|
|
326
380
|
const callArgs = (broker.call as ReturnType<typeof vi.fn>).mock.calls[0]
|
|
327
381
|
expect(callArgs[2]).toEqual({ nodeID: 'agent' })
|
|
328
382
|
})
|
|
@@ -46,7 +46,10 @@ import type { StreamProbeService } from '../core/streaming/stream-probe.service.
|
|
|
46
46
|
*/
|
|
47
47
|
interface RegisterNodeDriver {
|
|
48
48
|
onRegisterNode: (params: RegisterNodeParams) => void
|
|
49
|
-
createCapabilityProxy: (
|
|
49
|
+
createCapabilityProxy: (
|
|
50
|
+
capabilityName: string,
|
|
51
|
+
nodeId: string,
|
|
52
|
+
) => Record<string, (params: unknown) => Promise<unknown>> | null
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
/** Build a harness whose declared caps are SINGLETON, for active-provider preference tests. */
|
|
@@ -55,7 +58,10 @@ function createSingletonHarness(capNames: readonly string[]): Harness {
|
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
/** A minimal real `CapabilityDefinition` so `getDefinition`/`expandCapMethods` resolve. */
|
|
58
|
-
function makeCapDef(
|
|
61
|
+
function makeCapDef(
|
|
62
|
+
name: string,
|
|
63
|
+
mode: 'collection' | 'singleton' = 'collection',
|
|
64
|
+
): CapabilityDefinition {
|
|
59
65
|
return {
|
|
60
66
|
name,
|
|
61
67
|
scope: 'system',
|
|
@@ -98,7 +104,10 @@ interface Harness {
|
|
|
98
104
|
* The broker is never started, so there is nothing to stop in teardown —
|
|
99
105
|
* no `afterEach` teardown is needed.
|
|
100
106
|
*/
|
|
101
|
-
function createHarness(
|
|
107
|
+
function createHarness(
|
|
108
|
+
capNames: readonly string[],
|
|
109
|
+
mode: 'collection' | 'singleton' = 'collection',
|
|
110
|
+
): Harness {
|
|
102
111
|
const registry = new CapabilityRegistry(makeLogger())
|
|
103
112
|
for (const name of capNames) {
|
|
104
113
|
registry.declareCapability(makeCapDef(name, mode))
|
|
@@ -248,8 +257,8 @@ describe('MoleculerService.applyNodeManifest — singleton local-first preferenc
|
|
|
248
257
|
|
|
249
258
|
it('prefers the hub-local provider when the REMOTE one registered first', () => {
|
|
250
259
|
const { driver, registry } = createSingletonHarness(['pipeline-executor'])
|
|
251
|
-
driver.onRegisterNode(singleton(REMOTE))
|
|
252
|
-
driver.onRegisterNode(singleton(LOCAL))
|
|
260
|
+
driver.onRegisterNode(singleton(REMOTE)) // agent first → would win under first-wins
|
|
261
|
+
driver.onRegisterNode(singleton(LOCAL)) // hub-local second
|
|
253
262
|
// Active provider must be the hub-local key (bare addonId, no '@').
|
|
254
263
|
expect(registry.getSingletonAddonId('pipeline-executor')).toBe('detection-pipeline')
|
|
255
264
|
expect(registry.getSingletonAddonId('pipeline-executor')).not.toContain('@')
|
|
@@ -257,8 +266,8 @@ describe('MoleculerService.applyNodeManifest — singleton local-first preferenc
|
|
|
257
266
|
|
|
258
267
|
it('keeps the hub-local provider active when a REMOTE one registers afterwards', () => {
|
|
259
268
|
const { driver, registry } = createSingletonHarness(['pipeline-executor'])
|
|
260
|
-
driver.onRegisterNode(singleton(LOCAL))
|
|
261
|
-
driver.onRegisterNode(singleton(REMOTE))
|
|
269
|
+
driver.onRegisterNode(singleton(LOCAL)) // hub-local first
|
|
270
|
+
driver.onRegisterNode(singleton(REMOTE)) // agent second must NOT steal active
|
|
262
271
|
expect(registry.getSingletonAddonId('pipeline-executor')).toBe('detection-pipeline')
|
|
263
272
|
})
|
|
264
273
|
})
|
|
@@ -38,7 +38,10 @@ interface FakeHubLocalRegistry extends HubLocalChildDispatcher {
|
|
|
38
38
|
function makeDeviceAwareHubLocalRegistry(
|
|
39
39
|
caps: ReadonlyMap<string, ReadonlyMap<number | 'singleton', string>>,
|
|
40
40
|
): FakeHubLocalRegistry {
|
|
41
|
-
const callCapOnChildSpy = vi.fn(async (_childId: string, _input: unknown) => ({
|
|
41
|
+
const callCapOnChildSpy = vi.fn(async (_childId: string, _input: unknown) => ({
|
|
42
|
+
ok: true,
|
|
43
|
+
from: 'uds',
|
|
44
|
+
}))
|
|
42
45
|
return {
|
|
43
46
|
resolveChildId: (capName: string, deviceId?: number): string | null => {
|
|
44
47
|
const capMap = caps.get(capName)
|
|
@@ -79,8 +82,7 @@ function makeNativeAwareNodeAuthority(
|
|
|
79
82
|
return nativeCaps.some((n) => n.nodeId === nodeId && n.capName === capName)
|
|
80
83
|
},
|
|
81
84
|
|
|
82
|
-
nodeIsAgent: (nodeId: string): boolean =>
|
|
83
|
-
nodeId !== HUB_NODE_ID && !nodeId.includes('/'),
|
|
85
|
+
nodeIsAgent: (nodeId: string): boolean => nodeId !== HUB_NODE_ID && !nodeId.includes('/'),
|
|
84
86
|
|
|
85
87
|
nodeOnline: (nodeId: string): boolean => onlineNodes.has(nodeId),
|
|
86
88
|
|
|
@@ -104,7 +106,9 @@ function makeNativeAwareNodeAuthority(
|
|
|
104
106
|
|
|
105
107
|
isNativeCap: (nodeId: string, capName: string, deviceId?: number): boolean => {
|
|
106
108
|
if (deviceId !== undefined) {
|
|
107
|
-
return nativeCaps.some(
|
|
109
|
+
return nativeCaps.some(
|
|
110
|
+
(n) => n.nodeId === nodeId && n.capName === capName && n.deviceId === deviceId,
|
|
111
|
+
)
|
|
108
112
|
}
|
|
109
113
|
return nativeCaps.some((n) => n.nodeId === nodeId && n.capName === capName)
|
|
110
114
|
},
|
|
@@ -119,15 +123,22 @@ describe('Task5 – device-scoped native cap on hub-local child', () => {
|
|
|
119
123
|
it('resolves to hub-local-uds and calls callCapOnChild with {capName, method, args+deviceId, deviceId}', async () => {
|
|
120
124
|
// ptz is a device-scoped native cap owned by hub child 'provider-reolink'
|
|
121
125
|
const hubLocalCaps = makeDeviceAwareHubLocalRegistry(
|
|
122
|
-
new Map([
|
|
123
|
-
['ptz', new Map([[7, 'provider-reolink']])],
|
|
124
|
-
]),
|
|
126
|
+
new Map([['ptz', new Map([[7, 'provider-reolink']])]]),
|
|
125
127
|
)
|
|
126
128
|
|
|
127
129
|
const nativeCaps: NativeCapSpec[] = [
|
|
128
|
-
{
|
|
130
|
+
{
|
|
131
|
+
nodeId: 'hub/provider-reolink',
|
|
132
|
+
addonId: 'addon-provider-reolink',
|
|
133
|
+
capName: 'ptz',
|
|
134
|
+
deviceId: 7,
|
|
135
|
+
},
|
|
129
136
|
]
|
|
130
|
-
const nodeAuthority = makeNativeAwareNodeAuthority(
|
|
137
|
+
const nodeAuthority = makeNativeAwareNodeAuthority(
|
|
138
|
+
new Map(),
|
|
139
|
+
new Set(['hub/provider-reolink']),
|
|
140
|
+
nativeCaps,
|
|
141
|
+
)
|
|
131
142
|
|
|
132
143
|
const deps: CapRouteResolverDeps = {
|
|
133
144
|
hubNodeId: HUB_NODE_ID,
|
|
@@ -149,7 +160,10 @@ describe('Task5 – device-scoped native cap on hub-local child', () => {
|
|
|
149
160
|
expect(result).toEqual({ ok: true, from: 'uds' })
|
|
150
161
|
|
|
151
162
|
expect(hubLocalCaps.callCapOnChildSpy).toHaveBeenCalledOnce()
|
|
152
|
-
const [calledChildId, calledInput] = hubLocalCaps.callCapOnChildSpy.mock.calls[0] as [
|
|
163
|
+
const [calledChildId, calledInput] = hubLocalCaps.callCapOnChildSpy.mock.calls[0] as [
|
|
164
|
+
string,
|
|
165
|
+
unknown,
|
|
166
|
+
]
|
|
153
167
|
expect(calledChildId).toBe('provider-reolink')
|
|
154
168
|
expect(calledInput).toMatchObject({
|
|
155
169
|
capName: 'ptz',
|
|
@@ -191,9 +205,7 @@ describe('Task5 – d9ba709 regression: singleton prefers hub-local over remote'
|
|
|
191
205
|
it('singleton cap provided by both hub-local child AND remote node classifies to hub-local-uds (no nodeId given)', () => {
|
|
192
206
|
// 'pipeline-executor' is on BOTH local child addon-detection-pipeline AND remote-agent-0
|
|
193
207
|
const hubLocalCaps = makeDeviceAwareHubLocalRegistry(
|
|
194
|
-
new Map([
|
|
195
|
-
['pipeline-executor', new Map([['singleton', 'addon-detection-pipeline']])],
|
|
196
|
-
]),
|
|
208
|
+
new Map([['pipeline-executor', new Map([['singleton', 'addon-detection-pipeline']])]]),
|
|
197
209
|
)
|
|
198
210
|
|
|
199
211
|
const systemCaps = new Map([
|
|
@@ -307,7 +319,12 @@ describe('Task5 – remote native cap uses NATIVE_PROVIDER_SERVICE_INFIX in acti
|
|
|
307
319
|
describe('Task5 – nodeKnowsCap includes native caps', () => {
|
|
308
320
|
it('nodeKnowsCap returns true when the node has a native cap entry (even with no manifest cap)', () => {
|
|
309
321
|
const nativeCaps: NativeCapSpec[] = [
|
|
310
|
-
{
|
|
322
|
+
{
|
|
323
|
+
nodeId: 'hub/provider-reolink',
|
|
324
|
+
addonId: 'addon-provider-reolink',
|
|
325
|
+
capName: 'ptz',
|
|
326
|
+
deviceId: 7,
|
|
327
|
+
},
|
|
311
328
|
]
|
|
312
329
|
const nodeAuthority = makeNativeAwareNodeAuthority(
|
|
313
330
|
new Map(), // no system caps
|
|
@@ -338,14 +355,20 @@ describe('native-fallback – wrapper must not shadow the hub-local native child
|
|
|
338
355
|
// snapshot has an in-hub wrapper AND a forked native child for device 7.
|
|
339
356
|
const wrapperRef: InProcessProviderRef = { invoke: vi.fn(async () => ({ from: 'wrapper' })) }
|
|
340
357
|
|
|
341
|
-
function makeSnapshotResolver(): {
|
|
358
|
+
function makeSnapshotResolver(): {
|
|
359
|
+
resolver: CapRouteResolver
|
|
360
|
+
hubLocalCaps: FakeHubLocalRegistry
|
|
361
|
+
} {
|
|
342
362
|
const hubLocalCaps = makeDeviceAwareHubLocalRegistry(
|
|
343
|
-
new Map([
|
|
344
|
-
['snapshot', new Map([[7, 'provider-reolink']])],
|
|
345
|
-
]),
|
|
363
|
+
new Map([['snapshot', new Map([[7, 'provider-reolink']])]]),
|
|
346
364
|
)
|
|
347
365
|
const nativeCaps: NativeCapSpec[] = [
|
|
348
|
-
{
|
|
366
|
+
{
|
|
367
|
+
nodeId: 'hub/provider-reolink',
|
|
368
|
+
addonId: 'addon-provider-reolink',
|
|
369
|
+
capName: 'snapshot',
|
|
370
|
+
deviceId: 7,
|
|
371
|
+
},
|
|
349
372
|
]
|
|
350
373
|
const nodeAuthority = makeNativeAwareNodeAuthority(
|
|
351
374
|
new Map(),
|
|
@@ -17,7 +17,11 @@ import * as crypto from 'node:crypto'
|
|
|
17
17
|
import { registerOauth2Routes } from '../api/oauth2/oauth2-routes.js'
|
|
18
18
|
import { createOauthGrants } from '../../../../packages/core/src/builtins/local-auth/oauth-grants.js'
|
|
19
19
|
import type { ISsoBridgeProvider } from '@camstack/types'
|
|
20
|
-
import type {
|
|
20
|
+
import type {
|
|
21
|
+
IOauthIntegrationProvider,
|
|
22
|
+
IUserManagementProvider,
|
|
23
|
+
TokenScope,
|
|
24
|
+
} from '@camstack/types'
|
|
21
25
|
import type { OauthSession } from '../../../../packages/core/src/builtins/local-auth/oauth-session-manager.js'
|
|
22
26
|
import { SESSION_COOKIE } from '../auth/session-cookie.js'
|
|
23
27
|
|
|
@@ -59,7 +63,10 @@ function verifyHmacJwt(token: string): Record<string, unknown> | null {
|
|
|
59
63
|
)
|
|
60
64
|
if (sig !== expected) return null
|
|
61
65
|
try {
|
|
62
|
-
const decoded = JSON.parse(Buffer.from(body, 'base64url').toString('utf8')) as Record<
|
|
66
|
+
const decoded = JSON.parse(Buffer.from(body, 'base64url').toString('utf8')) as Record<
|
|
67
|
+
string,
|
|
68
|
+
unknown
|
|
69
|
+
>
|
|
63
70
|
if (typeof decoded['exp'] === 'number' && decoded['exp'] < Math.floor(Date.now() / 1000)) {
|
|
64
71
|
return null
|
|
65
72
|
}
|
|
@@ -92,7 +99,12 @@ function fakeSessionManager() {
|
|
|
92
99
|
|
|
93
100
|
return {
|
|
94
101
|
store,
|
|
95
|
-
async create(input: {
|
|
102
|
+
async create(input: {
|
|
103
|
+
userId: string
|
|
104
|
+
username: string
|
|
105
|
+
integrationId: string
|
|
106
|
+
scopes: TokenScope[]
|
|
107
|
+
}): Promise<OauthSession> {
|
|
96
108
|
const now = Date.now()
|
|
97
109
|
const session: OauthSession = {
|
|
98
110
|
id: `session-${++seq}`,
|
|
@@ -139,7 +151,13 @@ const VALID_SESSION_TOKEN = 'valid-session-token-abc123'
|
|
|
139
151
|
const ALEXA_DESCRIPTOR = {
|
|
140
152
|
integrationId: 'export-alexa',
|
|
141
153
|
displayName: 'Alexa Smart Home',
|
|
142
|
-
requestedScopes: [
|
|
154
|
+
requestedScopes: [
|
|
155
|
+
{
|
|
156
|
+
type: 'category' as const,
|
|
157
|
+
target: 'device' as const,
|
|
158
|
+
access: ['view', 'create'] as ('view' | 'create' | 'delete')[],
|
|
159
|
+
},
|
|
160
|
+
],
|
|
143
161
|
allowedRedirectPrefixes: ['https://cb.example/'],
|
|
144
162
|
}
|
|
145
163
|
|
|
@@ -204,7 +222,10 @@ function buildFakeRegistry(
|
|
|
204
222
|
|
|
205
223
|
// ─── Test setup ───────────────────────────────────────────────────────────────
|
|
206
224
|
|
|
207
|
-
function buildApp(
|
|
225
|
+
function buildApp(
|
|
226
|
+
grants: ReturnType<typeof createOauthGrants>,
|
|
227
|
+
sessionManager: ReturnType<typeof fakeSessionManager>,
|
|
228
|
+
) {
|
|
208
229
|
const fastify = Fastify({ logger: false })
|
|
209
230
|
void fastify.register(cookie)
|
|
210
231
|
|
|
@@ -291,7 +312,11 @@ describe('OAuth2 account-linking flow', () => {
|
|
|
291
312
|
|
|
292
313
|
expect(res.statusCode).toBe(302)
|
|
293
314
|
const location = res.headers['location'] as string
|
|
294
|
-
expect(location).toMatch(
|
|
315
|
+
expect(location).toMatch(
|
|
316
|
+
new RegExp(
|
|
317
|
+
`^${REDIRECT_URI.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\?code=.+&state=${STATE}`,
|
|
318
|
+
),
|
|
319
|
+
)
|
|
295
320
|
})
|
|
296
321
|
|
|
297
322
|
// ── Case 4 ──────────────────────────────────────────────────────────────────
|
|
@@ -336,7 +361,12 @@ describe('OAuth2 account-linking flow', () => {
|
|
|
336
361
|
})
|
|
337
362
|
|
|
338
363
|
expect(tokenRes.statusCode).toBe(200)
|
|
339
|
-
const json = tokenRes.json<{
|
|
364
|
+
const json = tokenRes.json<{
|
|
365
|
+
access_token: string
|
|
366
|
+
refresh_token: string
|
|
367
|
+
expires_in: number
|
|
368
|
+
token_type: string
|
|
369
|
+
}>()
|
|
340
370
|
expect(json).toMatchObject({
|
|
341
371
|
expires_in: 3600,
|
|
342
372
|
token_type: 'Bearer',
|
|
@@ -550,7 +580,12 @@ describe('OAuth2 account-linking flow', () => {
|
|
|
550
580
|
})
|
|
551
581
|
|
|
552
582
|
expect(refreshRes.statusCode).toBe(200)
|
|
553
|
-
const refreshJson = refreshRes.json<{
|
|
583
|
+
const refreshJson = refreshRes.json<{
|
|
584
|
+
access_token: string
|
|
585
|
+
refresh_token: string
|
|
586
|
+
expires_in: number
|
|
587
|
+
token_type: string
|
|
588
|
+
}>()
|
|
554
589
|
expect(refreshJson).toMatchObject({ expires_in: 3600, token_type: 'Bearer' })
|
|
555
590
|
expect(typeof refreshJson.access_token).toBe('string')
|
|
556
591
|
expect(refreshJson.access_token.length).toBeGreaterThan(0)
|
|
@@ -604,7 +639,11 @@ describe('OAuth2 account-linking flow', () => {
|
|
|
604
639
|
describe('session registry lifecycle', () => {
|
|
605
640
|
// Shared helper: run the full authorize → consent → exchange flow and
|
|
606
641
|
// return the issued token pair plus the raw location URL.
|
|
607
|
-
async function doFullFlow(): Promise<{
|
|
642
|
+
async function doFullFlow(): Promise<{
|
|
643
|
+
accessToken: string
|
|
644
|
+
refreshToken: string
|
|
645
|
+
location: string
|
|
646
|
+
}> {
|
|
608
647
|
const authorizeBody = new URLSearchParams({
|
|
609
648
|
consent: 'allow',
|
|
610
649
|
integration: 'export-alexa',
|
|
@@ -661,9 +700,7 @@ describe('OAuth2 account-linking flow', () => {
|
|
|
661
700
|
|
|
662
701
|
// Scopes must contain the category/device entry declared in ALEXA_DESCRIPTOR.
|
|
663
702
|
expect(Array.isArray(session.scopes)).toBe(true)
|
|
664
|
-
const deviceScope = session.scopes.find(
|
|
665
|
-
(s) => s.type === 'category' && s.target === 'device',
|
|
666
|
-
)
|
|
703
|
+
const deviceScope = session.scopes.find((s) => s.type === 'category' && s.target === 'device')
|
|
667
704
|
expect(deviceScope).toBeDefined()
|
|
668
705
|
})
|
|
669
706
|
|
|
@@ -692,7 +729,9 @@ describe('OAuth2 account-linking flow', () => {
|
|
|
692
729
|
const sessionId = sessions[0]!.id
|
|
693
730
|
|
|
694
731
|
// Revoke via the fake user-management method (same as the real addon does).
|
|
695
|
-
const revokeResult = await sessionManager
|
|
732
|
+
const revokeResult = await sessionManager
|
|
733
|
+
.markRevoked(sessionId)
|
|
734
|
+
.then((ok) => ({ success: ok }))
|
|
696
735
|
expect(revokeResult).toEqual({ success: true })
|
|
697
736
|
|
|
698
737
|
// The session must now carry a non-null revokedAt.
|
|
@@ -729,7 +768,9 @@ describe('OAuth2 account-linking flow', () => {
|
|
|
729
768
|
|
|
730
769
|
// ── Case e ────────────────────────────────────────────────────────────────
|
|
731
770
|
it('e) revokeOauthSession with unknown id → {success: false}', async () => {
|
|
732
|
-
const result = await sessionManager
|
|
771
|
+
const result = await sessionManager
|
|
772
|
+
.markRevoked('non-existent-session-id')
|
|
773
|
+
.then((ok) => ({ success: ok }))
|
|
733
774
|
expect(result).toEqual({ success: false })
|
|
734
775
|
})
|
|
735
776
|
})
|
|
@@ -777,7 +818,8 @@ describe('integration-driven hubUrl claim (Phase A public-origin bridge)', () =>
|
|
|
777
818
|
registerOauth2Routes(fastify, {
|
|
778
819
|
getRegistry: () => reg as any,
|
|
779
820
|
verifyToken: (token: string) => {
|
|
780
|
-
if (token === VALID_SESSION_TOKEN)
|
|
821
|
+
if (token === VALID_SESSION_TOKEN)
|
|
822
|
+
return { userId: OPERATOR_USER_ID, username: OPERATOR_USERNAME }
|
|
781
823
|
throw new Error('invalid token')
|
|
782
824
|
},
|
|
783
825
|
publicHubUrl: () => GLOBAL_FALLBACK,
|
|
@@ -785,7 +827,9 @@ describe('integration-driven hubUrl claim (Phase A public-origin bridge)', () =>
|
|
|
785
827
|
return fastify
|
|
786
828
|
}
|
|
787
829
|
|
|
788
|
-
async function issuedCodeHubUrl(
|
|
830
|
+
async function issuedCodeHubUrl(
|
|
831
|
+
app: ReturnType<typeof buildAppWithDescriptor>,
|
|
832
|
+
): Promise<unknown> {
|
|
789
833
|
const body = new URLSearchParams({
|
|
790
834
|
consent: 'allow',
|
|
791
835
|
integration: 'export-alexa',
|
|
@@ -805,7 +849,9 @@ describe('integration-driven hubUrl claim (Phase A public-origin bridge)', () =>
|
|
|
805
849
|
expect(res.statusCode).toBe(302)
|
|
806
850
|
const loc = res.headers['location'] as string
|
|
807
851
|
const code = decodeURIComponent(loc.match(/[?&]code=([^&]+)/)![1]!)
|
|
808
|
-
const payload = JSON.parse(
|
|
852
|
+
const payload = JSON.parse(
|
|
853
|
+
Buffer.from(code.split('.')[1]!, 'base64url').toString('utf8'),
|
|
854
|
+
) as Record<string, unknown>
|
|
809
855
|
return payload['hubUrl']
|
|
810
856
|
}
|
|
811
857
|
|
|
@@ -86,12 +86,12 @@ class TestAddonHarness {
|
|
|
86
86
|
} as any
|
|
87
87
|
const result = await entry.addon.initialize(context)
|
|
88
88
|
if (result) {
|
|
89
|
-
const regs = Array.isArray(result) ? result : (result as any).providers ?? []
|
|
89
|
+
const regs = Array.isArray(result) ? result : ((result as any).providers ?? [])
|
|
90
90
|
for (const reg of regs) {
|
|
91
91
|
const capName: string =
|
|
92
92
|
typeof reg.capability === 'string'
|
|
93
93
|
? reg.capability
|
|
94
|
-
: (reg.capability as any)?.name ?? String(reg.capability)
|
|
94
|
+
: ((reg.capability as any)?.name ?? String(reg.capability))
|
|
95
95
|
self.registry.registerProvider(capName, id, reg.provider)
|
|
96
96
|
}
|
|
97
97
|
}
|
|
@@ -154,8 +154,12 @@ describe('Singleton contention E2E: two addons on the same singleton cap', () =>
|
|
|
154
154
|
expect(info.activeProvider).toBe('mock-analysis-a')
|
|
155
155
|
|
|
156
156
|
// Both providers individually addressable.
|
|
157
|
-
expect(harness.registry.getProviderByAddon('object-detector', 'mock-analysis-a')).toBe(
|
|
158
|
-
|
|
157
|
+
expect(harness.registry.getProviderByAddon('object-detector', 'mock-analysis-a')).toBe(
|
|
158
|
+
analysisA.provider,
|
|
159
|
+
)
|
|
160
|
+
expect(harness.registry.getProviderByAddon('object-detector', 'mock-analysis-b')).toBe(
|
|
161
|
+
analysisB.provider,
|
|
162
|
+
)
|
|
159
163
|
})
|
|
160
164
|
|
|
161
165
|
it('honours a configReader preference for the SECOND addon over first-registered', async () => {
|
|
@@ -191,7 +195,11 @@ describe('Singleton contention E2E: waitForProvider before registration', () =>
|
|
|
191
195
|
harness.declareCapabilities(analysisA)
|
|
192
196
|
|
|
193
197
|
// Consumer begins waiting BEFORE the addon initializes — no provider yet.
|
|
194
|
-
const waitPromise = harness.registry.waitForProvider(
|
|
198
|
+
const waitPromise = harness.registry.waitForProvider(
|
|
199
|
+
'object-detector',
|
|
200
|
+
'mock-analysis-a',
|
|
201
|
+
5_000,
|
|
202
|
+
)
|
|
195
203
|
|
|
196
204
|
// Addon initializes shortly after → registerProvider fulfils the waiter.
|
|
197
205
|
setTimeout(() => {
|
|
@@ -263,7 +271,9 @@ describe('Singleton contention E2E: active provider removed', () => {
|
|
|
263
271
|
const info = harness.registry.listCapabilities().find((c) => c.name === 'object-detector')!
|
|
264
272
|
expect(info.providers).toEqual(['mock-analysis-b'])
|
|
265
273
|
expect(info.activeProvider).toBe('mock-analysis-b')
|
|
266
|
-
expect(harness.registry.getProviderByAddon('object-detector', 'mock-analysis-b')).toBe(
|
|
274
|
+
expect(harness.registry.getProviderByAddon('object-detector', 'mock-analysis-b')).toBe(
|
|
275
|
+
analysisB.provider,
|
|
276
|
+
)
|
|
267
277
|
})
|
|
268
278
|
|
|
269
279
|
it('removing a NON-active provider keeps the active one untouched', async () => {
|
|
@@ -369,19 +379,19 @@ describe('Singleton contention E2E: setActiveSingleton switching', () => {
|
|
|
369
379
|
expect(harness.registry.getSingleton('object-detector')).toBe(analysisA.provider)
|
|
370
380
|
|
|
371
381
|
// Switch to B.
|
|
372
|
-
await harness.registry.setActiveSingleton('object-detector', 'mock-analysis-b'
|
|
382
|
+
await harness.registry.setActiveSingleton('object-detector', 'mock-analysis-b')
|
|
373
383
|
expect(harness.registry.getSingleton('object-detector')).toBe(analysisB.provider)
|
|
374
384
|
expect(harness.registry.getSingletonAddonId('object-detector')).toBe('mock-analysis-b')
|
|
375
385
|
|
|
376
386
|
// Switch back to A.
|
|
377
|
-
await harness.registry.setActiveSingleton('object-detector', 'mock-analysis-a'
|
|
387
|
+
await harness.registry.setActiveSingleton('object-detector', 'mock-analysis-a')
|
|
378
388
|
expect(harness.registry.getSingleton('object-detector')).toBe(analysisA.provider)
|
|
379
389
|
expect(harness.registry.getSingletonAddonId('object-detector')).toBe('mock-analysis-a')
|
|
380
390
|
})
|
|
381
391
|
|
|
382
392
|
it('setActiveSingleton throws when switching to an addon that never registered', async () => {
|
|
383
393
|
await expect(
|
|
384
|
-
harness.registry.setActiveSingleton('object-detector', 'mock-analysis-c'
|
|
394
|
+
harness.registry.setActiveSingleton('object-detector', 'mock-analysis-c'),
|
|
385
395
|
).rejects.toThrow(/[Nn]o provider/)
|
|
386
396
|
// Active pointer unchanged after the failed switch.
|
|
387
397
|
expect(harness.registry.getSingleton('object-detector')).toBe(analysisA.provider)
|
|
@@ -389,7 +399,7 @@ describe('Singleton contention E2E: setActiveSingleton switching', () => {
|
|
|
389
399
|
|
|
390
400
|
it('unregistering the explicitly-selected active provider promotes the remaining one', async () => {
|
|
391
401
|
// Operator explicitly selected B.
|
|
392
|
-
await harness.registry.setActiveSingleton('object-detector', 'mock-analysis-b'
|
|
402
|
+
await harness.registry.setActiveSingleton('object-detector', 'mock-analysis-b')
|
|
393
403
|
expect(harness.registry.getSingleton('object-detector')).toBe(analysisB.provider)
|
|
394
404
|
|
|
395
405
|
// B is removed. `unregisterProvider` promotes the remaining A rather
|
|
@@ -399,7 +409,9 @@ describe('Singleton contention E2E: setActiveSingleton switching', () => {
|
|
|
399
409
|
await harness.shutdownAddon('mock-analysis-b')
|
|
400
410
|
expect(harness.registry.getSingleton('object-detector')).toBe(analysisA.provider)
|
|
401
411
|
expect(harness.registry.getSingletonAddonId('object-detector')).toBe('mock-analysis-a')
|
|
402
|
-
expect(harness.registry.getProviderByAddon('object-detector', 'mock-analysis-a')).toBe(
|
|
412
|
+
expect(harness.registry.getProviderByAddon('object-detector', 'mock-analysis-a')).toBe(
|
|
413
|
+
analysisA.provider,
|
|
414
|
+
)
|
|
403
415
|
})
|
|
404
416
|
})
|
|
405
417
|
|