@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
|
@@ -22,9 +22,11 @@ function makeRig(opts?: { failItems?: ReadonlySet<string> }): TestRig {
|
|
|
22
22
|
const updateAddon = vi.fn(async (input: { name: string; version: string }) => {
|
|
23
23
|
if (fail.has(input.name)) throw new Error(`mock fail ${input.name}`)
|
|
24
24
|
})
|
|
25
|
-
const updateFrameworkPackage = vi.fn(
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
const updateFrameworkPackage = vi.fn(
|
|
26
|
+
async (input: { packageName: string; version: string; deferRestart: boolean }) => {
|
|
27
|
+
if (fail.has(input.packageName)) throw new Error(`mock fail fw ${input.packageName}`)
|
|
28
|
+
},
|
|
29
|
+
)
|
|
28
30
|
const restartServer = vi.fn(async () => {
|
|
29
31
|
// simulates the real restartServer: in a real run the process dies
|
|
30
32
|
})
|
|
@@ -40,7 +42,12 @@ function makeRig(opts?: { failItems?: ReadonlySet<string> }): TestRig {
|
|
|
40
42
|
updateAddon,
|
|
41
43
|
updateFrameworkPackage,
|
|
42
44
|
restartServer,
|
|
43
|
-
logger: {
|
|
45
|
+
logger: {
|
|
46
|
+
info: vi.fn(),
|
|
47
|
+
warn: vi.fn(),
|
|
48
|
+
error: vi.fn(),
|
|
49
|
+
debug: vi.fn(),
|
|
50
|
+
} as unknown as BulkUpdateCoordinatorDeps['logger'],
|
|
44
51
|
clock: () => clock++,
|
|
45
52
|
}
|
|
46
53
|
|
|
@@ -55,8 +62,12 @@ function makeRig(opts?: { failItems?: ReadonlySet<string> }): TestRig {
|
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
describe('BulkUpdateCoordinator', () => {
|
|
58
|
-
beforeEach(() => {
|
|
59
|
-
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
vi.useFakeTimers()
|
|
67
|
+
})
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
vi.useRealTimers()
|
|
70
|
+
})
|
|
60
71
|
|
|
61
72
|
it('regular-only happy path: processes 3 addons in order, no restart', async () => {
|
|
62
73
|
const rig = makeRig()
|
|
@@ -75,7 +86,7 @@ describe('BulkUpdateCoordinator', () => {
|
|
|
75
86
|
expect(final.completed).toBe(3)
|
|
76
87
|
expect(final.failed).toBe(0)
|
|
77
88
|
expect(final.phase).toBe('finalizing')
|
|
78
|
-
expect(final.items.every(i => i.status === 'done')).toBe(true)
|
|
89
|
+
expect(final.items.every((i) => i.status === 'done')).toBe(true)
|
|
79
90
|
expect(rig.restartServer).not.toHaveBeenCalled()
|
|
80
91
|
expect(rig.updateAddon).toHaveBeenCalledTimes(3)
|
|
81
92
|
})
|
|
@@ -98,15 +109,19 @@ describe('BulkUpdateCoordinator', () => {
|
|
|
98
109
|
expect(final.completed).toBe(4)
|
|
99
110
|
expect(rig.updateAddon).toHaveBeenCalledTimes(2)
|
|
100
111
|
expect(rig.updateFrameworkPackage).toHaveBeenCalledTimes(2)
|
|
101
|
-
expect(rig.updateFrameworkPackage).toHaveBeenCalledWith(
|
|
112
|
+
expect(rig.updateFrameworkPackage).toHaveBeenCalledWith(
|
|
113
|
+
expect.objectContaining({ deferRestart: true }),
|
|
114
|
+
)
|
|
102
115
|
expect(rig.restartServer).toHaveBeenCalledOnce()
|
|
103
116
|
|
|
104
117
|
// Verify ordering via event sequence: regular items reach 'updating' before any system item
|
|
105
|
-
const updatingNames = rig.events
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const lastRegularIdx = updatingNames.findLastIndex(
|
|
118
|
+
const updatingNames = rig.events.map((s) => s.current).filter((n): n is string => n !== null)
|
|
119
|
+
const firstSystemIdx = updatingNames.findIndex(
|
|
120
|
+
(n) => n === '@camstack/types' || n === '@camstack/kernel',
|
|
121
|
+
)
|
|
122
|
+
const lastRegularIdx = updatingNames.findLastIndex(
|
|
123
|
+
(n) => n === '@camstack/addon-a' || n === '@camstack/addon-b',
|
|
124
|
+
)
|
|
110
125
|
expect(firstSystemIdx).toBeGreaterThan(lastRegularIdx)
|
|
111
126
|
})
|
|
112
127
|
|
|
@@ -125,17 +140,19 @@ describe('BulkUpdateCoordinator', () => {
|
|
|
125
140
|
|
|
126
141
|
const final = rig.coordinator.get(id)!
|
|
127
142
|
expect(final.failed).toBe(1)
|
|
128
|
-
expect(final.items.find(i => i.name === '@camstack/addon-b')?.status).toBe('failed')
|
|
129
|
-
expect(final.items.find(i => i.name === '@camstack/addon-b')?.error).toContain('mock fail')
|
|
130
|
-
expect(final.items.find(i => i.name === '@camstack/addon-a')?.status).toBe('done')
|
|
131
|
-
expect(final.items.find(i => i.name === '@camstack/addon-c')?.status).toBe('done')
|
|
143
|
+
expect(final.items.find((i) => i.name === '@camstack/addon-b')?.status).toBe('failed')
|
|
144
|
+
expect(final.items.find((i) => i.name === '@camstack/addon-b')?.error).toContain('mock fail')
|
|
145
|
+
expect(final.items.find((i) => i.name === '@camstack/addon-a')?.status).toBe('done')
|
|
146
|
+
expect(final.items.find((i) => i.name === '@camstack/addon-c')?.status).toBe('done')
|
|
132
147
|
})
|
|
133
148
|
|
|
134
149
|
it('cancel pre-restart: loop exits, no restart, queued items remain queued', async () => {
|
|
135
150
|
const rig = makeRig()
|
|
136
151
|
rig.updateAddon.mockImplementation(async () => {
|
|
137
152
|
// Slow enough that cancel fires before item 2
|
|
138
|
-
await new Promise<void>(resolve => {
|
|
153
|
+
await new Promise<void>((resolve) => {
|
|
154
|
+
setTimeout(resolve, 50)
|
|
155
|
+
})
|
|
139
156
|
})
|
|
140
157
|
|
|
141
158
|
const { id } = rig.coordinator.start({
|
|
@@ -157,21 +174,21 @@ describe('BulkUpdateCoordinator', () => {
|
|
|
157
174
|
expect(final.cancelled).toBe(true)
|
|
158
175
|
expect(rig.restartServer).not.toHaveBeenCalled()
|
|
159
176
|
// At least one item should still be queued (we cancelled before item 2 ran)
|
|
160
|
-
expect(final.items.filter(i => i.status === 'queued').length).toBeGreaterThanOrEqual(1)
|
|
177
|
+
expect(final.items.filter((i) => i.status === 'queued').length).toBeGreaterThanOrEqual(1)
|
|
161
178
|
})
|
|
162
179
|
|
|
163
180
|
it('cancel ignored during restarting phase', async () => {
|
|
164
181
|
const rig = makeRig()
|
|
165
182
|
rig.restartServer.mockImplementation(async () => {
|
|
166
183
|
// Hang briefly so we can attempt cancel during restart
|
|
167
|
-
await new Promise<void>(resolve => {
|
|
184
|
+
await new Promise<void>((resolve) => {
|
|
185
|
+
setTimeout(resolve, 50)
|
|
186
|
+
})
|
|
168
187
|
})
|
|
169
188
|
|
|
170
189
|
const { id } = rig.coordinator.start({
|
|
171
190
|
nodeId: 'hub',
|
|
172
|
-
items: [
|
|
173
|
-
{ name: '@camstack/types', version: '0.1.40', isSystem: true },
|
|
174
|
-
],
|
|
191
|
+
items: [{ name: '@camstack/types', version: '0.1.40', isSystem: true }],
|
|
175
192
|
})
|
|
176
193
|
|
|
177
194
|
// Let it reach restarting
|
|
@@ -191,15 +208,13 @@ describe('BulkUpdateCoordinator', () => {
|
|
|
191
208
|
|
|
192
209
|
const { id } = rig.coordinator.start({
|
|
193
210
|
nodeId: 'hub',
|
|
194
|
-
items: [
|
|
195
|
-
{ name: '@camstack/types', version: '0.1.40', isSystem: true },
|
|
196
|
-
],
|
|
211
|
+
items: [{ name: '@camstack/types', version: '0.1.40', isSystem: true }],
|
|
197
212
|
})
|
|
198
213
|
|
|
199
214
|
await vi.runAllTimersAsync()
|
|
200
215
|
|
|
201
216
|
const final = rig.coordinator.get(id)!
|
|
202
|
-
const typesItem = final.items.find(i => i.name === '@camstack/types')!
|
|
217
|
+
const typesItem = final.items.find((i) => i.name === '@camstack/types')!
|
|
203
218
|
expect(typesItem.status).toBe('done')
|
|
204
219
|
expect(typesItem.error).toContain('Restart failed')
|
|
205
220
|
})
|
|
@@ -211,10 +226,12 @@ describe('BulkUpdateCoordinator', () => {
|
|
|
211
226
|
items: [{ name: '@camstack/addon-a', version: '1.0.1', isSystem: false }],
|
|
212
227
|
})
|
|
213
228
|
|
|
214
|
-
expect(() =>
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
229
|
+
expect(() =>
|
|
230
|
+
rig.coordinator.start({
|
|
231
|
+
nodeId: 'hub',
|
|
232
|
+
items: [{ name: '@camstack/addon-b', version: '2.0.1', isSystem: false }],
|
|
233
|
+
}),
|
|
234
|
+
).toThrow(/already in progress/i)
|
|
218
235
|
})
|
|
219
236
|
|
|
220
237
|
it('auto-cleanup: state purged 5 min after completedAt', async () => {
|
|
@@ -21,7 +21,11 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { describe, it, expect, vi } from 'vitest'
|
|
24
|
-
import type {
|
|
24
|
+
import type {
|
|
25
|
+
NodeCapAuthority,
|
|
26
|
+
HubLocalChildDispatcher,
|
|
27
|
+
CapRouteResolverDeps,
|
|
28
|
+
} from '@camstack/kernel'
|
|
25
29
|
import { CapRouteResolver, CapRouteError } from '@camstack/kernel'
|
|
26
30
|
|
|
27
31
|
// ---------------------------------------------------------------------------
|
|
@@ -40,8 +44,15 @@ import { CapRouteResolver, CapRouteError } from '@camstack/kernel'
|
|
|
40
44
|
function buildConsolidatedNativeFallback(opts: {
|
|
41
45
|
readonly resolver: CapRouteResolver | null
|
|
42
46
|
readonly hubNodeId: string
|
|
43
|
-
readonly resolveNativeCapOwnerSync: (
|
|
44
|
-
|
|
47
|
+
readonly resolveNativeCapOwnerSync: (
|
|
48
|
+
capName: string,
|
|
49
|
+
deviceId: number,
|
|
50
|
+
) => { addonId: string; nodeId: string } | null
|
|
51
|
+
readonly buildNativeCapProxy: (
|
|
52
|
+
addonId: string,
|
|
53
|
+
capName: string,
|
|
54
|
+
deviceId: number,
|
|
55
|
+
) => Record<string, unknown>
|
|
45
56
|
}): (capName: string, deviceId: number) => unknown | null {
|
|
46
57
|
const { resolver, hubNodeId, resolveNativeCapOwnerSync, buildNativeCapProxy } = opts
|
|
47
58
|
|
|
@@ -152,7 +163,9 @@ function makeNodeAuthority(
|
|
|
152
163
|
getAgentChildId: (): string | null => null,
|
|
153
164
|
isNativeCap: (nodeId: string, capName: string, deviceId?: number): boolean => {
|
|
154
165
|
if (deviceId !== undefined) {
|
|
155
|
-
return nativeCaps.some(
|
|
166
|
+
return nativeCaps.some(
|
|
167
|
+
(n) => n.nodeId === nodeId && n.capName === capName && n.deviceId === deviceId,
|
|
168
|
+
)
|
|
156
169
|
}
|
|
157
170
|
return nativeCaps.some((n) => n.nodeId === nodeId && n.capName === capName)
|
|
158
171
|
},
|
|
@@ -171,9 +184,18 @@ describe('G2 — (a) hub-local device-scoped native cap: resolver is consulted f
|
|
|
171
184
|
new Map([['ptz', new Map([[7, 'provider-reolink']])]]),
|
|
172
185
|
)
|
|
173
186
|
const nativeCaps: NativeCapSpec[] = [
|
|
174
|
-
{
|
|
187
|
+
{
|
|
188
|
+
nodeId: 'hub/provider-reolink',
|
|
189
|
+
addonId: 'addon-provider-reolink',
|
|
190
|
+
capName: 'ptz',
|
|
191
|
+
deviceId: 7,
|
|
192
|
+
},
|
|
175
193
|
]
|
|
176
|
-
const nodeAuthority = makeNodeAuthority(
|
|
194
|
+
const nodeAuthority = makeNodeAuthority(
|
|
195
|
+
new Map(),
|
|
196
|
+
new Set(['hub/provider-reolink']),
|
|
197
|
+
nativeCaps,
|
|
198
|
+
)
|
|
177
199
|
|
|
178
200
|
const deps: CapRouteResolverDeps = {
|
|
179
201
|
hubNodeId: HUB_NODE_ID,
|
|
@@ -211,9 +233,18 @@ describe('G2 — (a) hub-local device-scoped native cap: resolver is consulted f
|
|
|
211
233
|
new Map([['ptz', new Map([[7, 'provider-reolink']])]]),
|
|
212
234
|
)
|
|
213
235
|
const nativeCaps: NativeCapSpec[] = [
|
|
214
|
-
{
|
|
236
|
+
{
|
|
237
|
+
nodeId: 'hub/provider-reolink',
|
|
238
|
+
addonId: 'addon-provider-reolink',
|
|
239
|
+
capName: 'ptz',
|
|
240
|
+
deviceId: 7,
|
|
241
|
+
},
|
|
215
242
|
]
|
|
216
|
-
const nodeAuthority = makeNodeAuthority(
|
|
243
|
+
const nodeAuthority = makeNodeAuthority(
|
|
244
|
+
new Map(),
|
|
245
|
+
new Set(['hub/provider-reolink']),
|
|
246
|
+
nativeCaps,
|
|
247
|
+
)
|
|
217
248
|
|
|
218
249
|
const deps: CapRouteResolverDeps = {
|
|
219
250
|
hubNodeId: HUB_NODE_ID,
|
|
@@ -97,7 +97,12 @@ describe('getAvailableTypes — supportsLocationImport flag', () => {
|
|
|
97
97
|
},
|
|
98
98
|
}
|
|
99
99
|
const ar = makeAddonRegistry([addon])
|
|
100
|
-
const provider = buildIntegrationsProvider(
|
|
100
|
+
const provider = buildIntegrationsProvider(
|
|
101
|
+
ar as never,
|
|
102
|
+
makeEventBus() as never,
|
|
103
|
+
makeLoggingService() as never,
|
|
104
|
+
null,
|
|
105
|
+
)
|
|
101
106
|
const types = await provider.getAvailableTypes()
|
|
102
107
|
|
|
103
108
|
expect(types).toHaveLength(1)
|
|
@@ -120,7 +125,12 @@ describe('getAvailableTypes — supportsLocationImport flag', () => {
|
|
|
120
125
|
},
|
|
121
126
|
}
|
|
122
127
|
const ar = makeAddonRegistry([addon])
|
|
123
|
-
const provider = buildIntegrationsProvider(
|
|
128
|
+
const provider = buildIntegrationsProvider(
|
|
129
|
+
ar as never,
|
|
130
|
+
makeEventBus() as never,
|
|
131
|
+
makeLoggingService() as never,
|
|
132
|
+
null,
|
|
133
|
+
)
|
|
124
134
|
const types = await provider.getAvailableTypes()
|
|
125
135
|
|
|
126
136
|
expect(types).toHaveLength(1)
|
|
@@ -144,7 +154,12 @@ describe('getAvailableTypes — supportsLocationImport flag', () => {
|
|
|
144
154
|
},
|
|
145
155
|
}
|
|
146
156
|
const ar = makeAddonRegistry([addon])
|
|
147
|
-
const provider = buildIntegrationsProvider(
|
|
157
|
+
const provider = buildIntegrationsProvider(
|
|
158
|
+
ar as never,
|
|
159
|
+
makeEventBus() as never,
|
|
160
|
+
makeLoggingService() as never,
|
|
161
|
+
null,
|
|
162
|
+
)
|
|
148
163
|
const types = await provider.getAvailableTypes()
|
|
149
164
|
|
|
150
165
|
expect(types).toHaveLength(1)
|
|
@@ -173,7 +188,12 @@ describe('getAvailableTypes — supportsLocationImport flag', () => {
|
|
|
173
188
|
},
|
|
174
189
|
}
|
|
175
190
|
const ar = makeAddonRegistry([addon])
|
|
176
|
-
const provider = buildIntegrationsProvider(
|
|
191
|
+
const provider = buildIntegrationsProvider(
|
|
192
|
+
ar as never,
|
|
193
|
+
makeEventBus() as never,
|
|
194
|
+
makeLoggingService() as never,
|
|
195
|
+
null,
|
|
196
|
+
)
|
|
177
197
|
const types = await provider.getAvailableTypes()
|
|
178
198
|
|
|
179
199
|
expect(types).toHaveLength(1)
|
|
@@ -3,13 +3,27 @@ import { __resetCapUsageRegistryForTests, getCapUsageRegistry } from '@camstack/
|
|
|
3
3
|
import { buildNodesProvider } from '../../api/core/cap-providers'
|
|
4
4
|
|
|
5
5
|
describe('nodes.getCapUsageGraph provider', () => {
|
|
6
|
-
beforeEach(() => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
__resetCapUsageRegistryForTests()
|
|
8
|
+
})
|
|
7
9
|
|
|
8
10
|
it('returns recorded edges projected through the cap-usage registry', async () => {
|
|
9
11
|
const reg = getCapUsageRegistry()
|
|
10
12
|
const t0 = Date.now() - 5_000
|
|
11
|
-
reg.recordCall({
|
|
12
|
-
|
|
13
|
+
reg.recordCall({
|
|
14
|
+
callerAddonId: 'A',
|
|
15
|
+
providerAddonId: 'B',
|
|
16
|
+
capName: 'foo',
|
|
17
|
+
methodName: 'm',
|
|
18
|
+
atMs: t0,
|
|
19
|
+
})
|
|
20
|
+
reg.recordCall({
|
|
21
|
+
callerAddonId: 'A',
|
|
22
|
+
providerAddonId: 'B',
|
|
23
|
+
capName: 'foo',
|
|
24
|
+
methodName: 'm',
|
|
25
|
+
atMs: t0 + 1000,
|
|
26
|
+
})
|
|
13
27
|
|
|
14
28
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
15
29
|
const provider = buildNodesProvider({} as any, { broker: { call: vi.fn() } } as any)
|
|
@@ -6,7 +6,17 @@ function makeAgentRegistry(): { listNodes: () => Promise<unknown[]> } {
|
|
|
6
6
|
return {
|
|
7
7
|
listNodes: async () => [
|
|
8
8
|
{
|
|
9
|
-
info: {
|
|
9
|
+
info: {
|
|
10
|
+
id: 'hub',
|
|
11
|
+
name: 'hub',
|
|
12
|
+
hostname: 'hub',
|
|
13
|
+
platform: 'darwin',
|
|
14
|
+
arch: 'arm64',
|
|
15
|
+
cpuModel: 'M2',
|
|
16
|
+
cpuCores: 8,
|
|
17
|
+
memoryMB: 16384,
|
|
18
|
+
pythonRuntimes: [],
|
|
19
|
+
},
|
|
10
20
|
status: { cpuPercent: 12, memoryPercent: 30 },
|
|
11
21
|
isHub: true,
|
|
12
22
|
isOnline: true,
|
|
@@ -22,10 +32,34 @@ function makeAgentRegistry(): { listNodes: () => Promise<unknown[]> } {
|
|
|
22
32
|
function makeAddonRegistry(): { listAddons: () => readonly unknown[] } {
|
|
23
33
|
return {
|
|
24
34
|
listAddons: () => [
|
|
25
|
-
{
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
{
|
|
36
|
+
manifest: { id: 'provider-hikvision' },
|
|
37
|
+
declaration: {
|
|
38
|
+
id: 'provider-hikvision',
|
|
39
|
+
category: 'providers',
|
|
40
|
+
capabilities: [{ name: 'stream-params' }],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
manifest: { id: 'provider-onvif' },
|
|
45
|
+
declaration: {
|
|
46
|
+
id: 'provider-onvif',
|
|
47
|
+
category: 'providers',
|
|
48
|
+
capabilities: [{ name: 'device-provider' }],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
manifest: { id: 'stream-broker' },
|
|
53
|
+
declaration: {
|
|
54
|
+
id: 'stream-broker',
|
|
55
|
+
category: 'pipeline',
|
|
56
|
+
capabilities: [{ name: 'stream-broker' }],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
manifest: { id: 'sqlite-settings' },
|
|
61
|
+
declaration: { id: 'sqlite-settings', capabilities: [{ name: 'settings-store' }] },
|
|
62
|
+
},
|
|
29
63
|
],
|
|
30
64
|
}
|
|
31
65
|
}
|
|
@@ -36,12 +70,17 @@ describe('computeTopology categories[] aggregation', () => {
|
|
|
36
70
|
const nodes = await computeTopology(makeAgentRegistry() as any, makeAddonRegistry() as any)
|
|
37
71
|
expect(nodes).toHaveLength(1)
|
|
38
72
|
const cats = nodes[0]!.categories
|
|
39
|
-
const byId = new Map(cats.map(c => [c.category, c]))
|
|
73
|
+
const byId = new Map(cats.map((c) => [c.category, c]))
|
|
40
74
|
expect(byId.get('providers')?.total).toBe(2)
|
|
41
75
|
expect(byId.get('providers')?.healthy).toBe(2)
|
|
42
|
-
expect(
|
|
76
|
+
expect(
|
|
77
|
+
byId
|
|
78
|
+
.get('providers')
|
|
79
|
+
?.addons.map((a) => a.id)
|
|
80
|
+
.toSorted(),
|
|
81
|
+
).toEqual(['provider-hikvision', 'provider-onvif'])
|
|
43
82
|
expect(byId.get('pipeline')?.total).toBe(1)
|
|
44
|
-
expect(byId.get('system')?.addons.map(a => a.id)).toEqual(['sqlite-settings'])
|
|
83
|
+
expect(byId.get('system')?.addons.map((a) => a.id)).toEqual(['sqlite-settings'])
|
|
45
84
|
})
|
|
46
85
|
|
|
47
86
|
it('counts a non-running addon as not-healthy', async () => {
|
|
@@ -49,13 +88,20 @@ describe('computeTopology categories[] aggregation', () => {
|
|
|
49
88
|
// Force the addon registry to return one stopped + one running provider.
|
|
50
89
|
const addonReg = {
|
|
51
90
|
listAddons: () => [
|
|
52
|
-
{
|
|
53
|
-
|
|
91
|
+
{
|
|
92
|
+
manifest: { id: 'provider-rtsp' },
|
|
93
|
+
declaration: { id: 'provider-rtsp', category: 'providers', capabilities: [] },
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
manifest: { id: 'provider-hikvision' },
|
|
97
|
+
declaration: { id: 'provider-hikvision', category: 'providers', capabilities: [] },
|
|
98
|
+
status: 'failed',
|
|
99
|
+
},
|
|
54
100
|
],
|
|
55
101
|
}
|
|
56
102
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
57
103
|
const nodes = await computeTopology(agentReg as any, addonReg as any)
|
|
58
|
-
const providers = nodes[0]!.categories.find(c => c.category === 'providers')!
|
|
104
|
+
const providers = nodes[0]!.categories.find((c) => c.category === 'providers')!
|
|
59
105
|
expect(providers.total).toBe(2)
|
|
60
106
|
// The current TopologyNode `addons[].status` is always `'running'` per existing code;
|
|
61
107
|
// failed addons would surface through subProcesses[].state. Document the current contract:
|
|
@@ -62,12 +62,12 @@ function makeLoggingService(log = makeLogger()) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
function makeCapabilityRegistry(
|
|
65
|
-
removeByIntegration:
|
|
65
|
+
removeByIntegration:
|
|
66
|
+
| ((input: { integrationId: string }) => Promise<{ removed: number }>)
|
|
67
|
+
| null = vi.fn(async () => ({ removed: 2 })),
|
|
66
68
|
): CapabilityRegistry {
|
|
67
69
|
return {
|
|
68
|
-
getSingleton: vi.fn((_cap: string) =>
|
|
69
|
-
removeByIntegration ? { removeByIntegration } : null,
|
|
70
|
-
),
|
|
70
|
+
getSingleton: vi.fn((_cap: string) => (removeByIntegration ? { removeByIntegration } : null)),
|
|
71
71
|
} as unknown as CapabilityRegistry
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -92,7 +92,12 @@ describe('integrations.delete cascade-removes devices via removeByIntegration',
|
|
|
92
92
|
const removeByIntegration = vi.fn(async () => ({ removed: 3 }))
|
|
93
93
|
const capReg = makeCapabilityRegistry(removeByIntegration)
|
|
94
94
|
|
|
95
|
-
const provider = buildIntegrationsProvider(
|
|
95
|
+
const provider = buildIntegrationsProvider(
|
|
96
|
+
addonReg as never,
|
|
97
|
+
eb as never,
|
|
98
|
+
loggingService as never,
|
|
99
|
+
capReg,
|
|
100
|
+
)
|
|
96
101
|
await provider.delete({ id: INTEGRATION_ID })
|
|
97
102
|
|
|
98
103
|
expect(removeByIntegration).toHaveBeenCalledOnce()
|
|
@@ -102,7 +107,12 @@ describe('integrations.delete cascade-removes devices via removeByIntegration',
|
|
|
102
107
|
it('still deletes the integration record and emits integration.deleted after the cascade', async () => {
|
|
103
108
|
const capReg = makeCapabilityRegistry()
|
|
104
109
|
|
|
105
|
-
const provider = buildIntegrationsProvider(
|
|
110
|
+
const provider = buildIntegrationsProvider(
|
|
111
|
+
addonReg as never,
|
|
112
|
+
eb as never,
|
|
113
|
+
loggingService as never,
|
|
114
|
+
capReg,
|
|
115
|
+
)
|
|
106
116
|
const result = await provider.delete({ id: INTEGRATION_ID })
|
|
107
117
|
|
|
108
118
|
expect(integrationReg.deleteIntegration).toHaveBeenCalledOnce()
|
|
@@ -123,7 +133,12 @@ describe('integrations.delete cascade-removes devices via removeByIntegration',
|
|
|
123
133
|
const removeByIntegration = vi.fn(async () => ({ removed: 5 }))
|
|
124
134
|
const capReg = makeCapabilityRegistry(removeByIntegration)
|
|
125
135
|
|
|
126
|
-
const provider = buildIntegrationsProvider(
|
|
136
|
+
const provider = buildIntegrationsProvider(
|
|
137
|
+
addonReg as never,
|
|
138
|
+
eb as never,
|
|
139
|
+
loggingService as never,
|
|
140
|
+
capReg,
|
|
141
|
+
)
|
|
127
142
|
await provider.delete({ id: INTEGRATION_ID })
|
|
128
143
|
|
|
129
144
|
expect(log.info).toHaveBeenCalledWith(
|
|
@@ -138,7 +153,12 @@ describe('integrations.delete cascade-removes devices via removeByIntegration',
|
|
|
138
153
|
})
|
|
139
154
|
const capReg = makeCapabilityRegistry(removeByIntegration)
|
|
140
155
|
|
|
141
|
-
const provider = buildIntegrationsProvider(
|
|
156
|
+
const provider = buildIntegrationsProvider(
|
|
157
|
+
addonReg as never,
|
|
158
|
+
eb as never,
|
|
159
|
+
loggingService as never,
|
|
160
|
+
capReg,
|
|
161
|
+
)
|
|
142
162
|
|
|
143
163
|
// Should NOT throw
|
|
144
164
|
await expect(provider.delete({ id: INTEGRATION_ID })).resolves.toEqual({
|
|
@@ -158,7 +178,12 @@ describe('integrations.delete cascade-removes devices via removeByIntegration',
|
|
|
158
178
|
})
|
|
159
179
|
|
|
160
180
|
it('warns and skips cascade when capabilityRegistry is null', async () => {
|
|
161
|
-
const provider = buildIntegrationsProvider(
|
|
181
|
+
const provider = buildIntegrationsProvider(
|
|
182
|
+
addonReg as never,
|
|
183
|
+
eb as never,
|
|
184
|
+
loggingService as never,
|
|
185
|
+
null,
|
|
186
|
+
)
|
|
162
187
|
await provider.delete({ id: INTEGRATION_ID })
|
|
163
188
|
|
|
164
189
|
expect(integrationReg.deleteIntegration).toHaveBeenCalledOnce()
|
|
@@ -172,7 +197,12 @@ describe('integrations.delete cascade-removes devices via removeByIntegration',
|
|
|
172
197
|
it('warns and skips cascade when device-manager singleton is not registered', async () => {
|
|
173
198
|
const capReg = makeCapabilityRegistry(null)
|
|
174
199
|
|
|
175
|
-
const provider = buildIntegrationsProvider(
|
|
200
|
+
const provider = buildIntegrationsProvider(
|
|
201
|
+
addonReg as never,
|
|
202
|
+
eb as never,
|
|
203
|
+
loggingService as never,
|
|
204
|
+
capReg,
|
|
205
|
+
)
|
|
176
206
|
await provider.delete({ id: INTEGRATION_ID })
|
|
177
207
|
|
|
178
208
|
expect(integrationReg.deleteIntegration).toHaveBeenCalledOnce()
|
|
@@ -184,7 +214,9 @@ describe('integrations.delete cascade-removes devices via removeByIntegration',
|
|
|
184
214
|
|
|
185
215
|
it('stamps legacy un-tagged devices of the addon BEFORE cascade so they are removed too', async () => {
|
|
186
216
|
const removeByIntegration = vi.fn(async () => ({ removed: 1 }))
|
|
187
|
-
const setIntegrationId = vi.fn(
|
|
217
|
+
const setIntegrationId = vi.fn(
|
|
218
|
+
async (_input: { deviceId: number; integrationId: string }) => undefined,
|
|
219
|
+
)
|
|
188
220
|
// One un-tagged top-level device of the integration's addon (claimable),
|
|
189
221
|
// one already-tagged (skip), one child (skip), one other-addon (skip).
|
|
190
222
|
const listAll = vi.fn(async () => [
|
|
@@ -197,7 +229,12 @@ describe('integrations.delete cascade-removes devices via removeByIntegration',
|
|
|
197
229
|
getSingleton: vi.fn(() => ({ removeByIntegration, listAll, setIntegrationId })),
|
|
198
230
|
} as unknown as CapabilityRegistry
|
|
199
231
|
|
|
200
|
-
const provider = buildIntegrationsProvider(
|
|
232
|
+
const provider = buildIntegrationsProvider(
|
|
233
|
+
addonReg as never,
|
|
234
|
+
eb as never,
|
|
235
|
+
loggingService as never,
|
|
236
|
+
capReg,
|
|
237
|
+
)
|
|
201
238
|
await provider.delete({ id: INTEGRATION_ID })
|
|
202
239
|
|
|
203
240
|
// Only device 4 is claimed (untagged, top-level, this addon's single integration).
|
|
@@ -209,13 +246,20 @@ describe('integrations.delete cascade-removes devices via removeByIntegration',
|
|
|
209
246
|
|
|
210
247
|
it('claim failure does not abort the integration delete (best-effort)', async () => {
|
|
211
248
|
const removeByIntegration = vi.fn(async () => ({ removed: 0 }))
|
|
212
|
-
const setIntegrationId = vi.fn(async () => {
|
|
249
|
+
const setIntegrationId = vi.fn(async () => {
|
|
250
|
+
throw new Error('stamp boom')
|
|
251
|
+
})
|
|
213
252
|
const listAll = vi.fn(async () => [{ id: 4, addonId: ADDON_ID, parentDeviceId: null }])
|
|
214
253
|
const capReg = {
|
|
215
254
|
getSingleton: vi.fn(() => ({ removeByIntegration, listAll, setIntegrationId })),
|
|
216
255
|
} as unknown as CapabilityRegistry
|
|
217
256
|
|
|
218
|
-
const provider = buildIntegrationsProvider(
|
|
257
|
+
const provider = buildIntegrationsProvider(
|
|
258
|
+
addonReg as never,
|
|
259
|
+
eb as never,
|
|
260
|
+
loggingService as never,
|
|
261
|
+
capReg,
|
|
262
|
+
)
|
|
219
263
|
await expect(provider.delete({ id: INTEGRATION_ID })).resolves.toEqual({
|
|
220
264
|
success: true,
|
|
221
265
|
deletedId: INTEGRATION_ID,
|
|
@@ -232,7 +276,12 @@ describe('integrations.delete cascade-removes devices via removeByIntegration',
|
|
|
232
276
|
addonReg = makeAddonRegistry(integrationReg)
|
|
233
277
|
const capReg = makeCapabilityRegistry()
|
|
234
278
|
|
|
235
|
-
const provider = buildIntegrationsProvider(
|
|
279
|
+
const provider = buildIntegrationsProvider(
|
|
280
|
+
addonReg as never,
|
|
281
|
+
eb as never,
|
|
282
|
+
loggingService as never,
|
|
283
|
+
capReg,
|
|
284
|
+
)
|
|
236
285
|
|
|
237
286
|
await expect(provider.delete({ id: 'missing-id' })).rejects.toThrow('not found')
|
|
238
287
|
|
|
@@ -233,7 +233,18 @@ describe('buildAddonsProvider — BulkUpdateCoordinator delegation', () => {
|
|
|
233
233
|
|
|
234
234
|
it('listActiveBulkUpdates delegates to coordinator.list with nodeId and returns its value', async () => {
|
|
235
235
|
const mockList = [
|
|
236
|
-
{
|
|
236
|
+
{
|
|
237
|
+
id: 'bulk-1',
|
|
238
|
+
nodeId: 'hub',
|
|
239
|
+
startedAtMs: 1000,
|
|
240
|
+
total: 1,
|
|
241
|
+
completed: 0,
|
|
242
|
+
failed: 0,
|
|
243
|
+
current: 'pkg-a',
|
|
244
|
+
phase: 'regular' as const,
|
|
245
|
+
cancelled: false,
|
|
246
|
+
items: [],
|
|
247
|
+
},
|
|
237
248
|
]
|
|
238
249
|
stubs.list.mockReturnValue(mockList)
|
|
239
250
|
|
|
@@ -318,15 +329,15 @@ describe('buildAddonsProvider — listUpdates isSystem field', () => {
|
|
|
318
329
|
const result = await env.provider.listUpdates({ nodeId: 'hub' })
|
|
319
330
|
|
|
320
331
|
expect(result).toHaveLength(2)
|
|
321
|
-
const typesRow = result.find(r => r.name === '@camstack/types')
|
|
322
|
-
const fooRow = result.find(r => r.name === '@camstack/addon-foo')
|
|
332
|
+
const typesRow = result.find((r) => r.name === '@camstack/types')
|
|
333
|
+
const fooRow = result.find((r) => r.name === '@camstack/addon-foo')
|
|
323
334
|
expect(typesRow?.isSystem).toBe(true)
|
|
324
335
|
expect(fooRow?.isSystem).toBe(false)
|
|
325
336
|
})
|
|
326
337
|
|
|
327
338
|
it('all FRAMEWORK_PACKAGE_ALLOWLIST members get isSystem: true', async () => {
|
|
328
339
|
env.psMock['checkUpdates']!.mockResolvedValue(
|
|
329
|
-
FRAMEWORK_PACKAGE_ALLOWLIST.map(name => ({
|
|
340
|
+
FRAMEWORK_PACKAGE_ALLOWLIST.map((name) => ({
|
|
330
341
|
name,
|
|
331
342
|
currentVersion: '0.1.0',
|
|
332
343
|
latestVersion: '0.1.1',
|
|
@@ -360,7 +371,10 @@ describe('buildAddonsProvider — updateFrameworkPackage deferRestart propagatio
|
|
|
360
371
|
})
|
|
361
372
|
|
|
362
373
|
expect(env.psMock['updateFrameworkPackage']).toHaveBeenCalledOnce()
|
|
363
|
-
const callArg = env.psMock['updateFrameworkPackage']!.mock.calls[0]![0] as Record<
|
|
374
|
+
const callArg = env.psMock['updateFrameworkPackage']!.mock.calls[0]![0] as Record<
|
|
375
|
+
string,
|
|
376
|
+
unknown
|
|
377
|
+
>
|
|
364
378
|
expect(callArg['deferRestart']).toBe(true)
|
|
365
379
|
})
|
|
366
380
|
|
|
@@ -371,7 +385,10 @@ describe('buildAddonsProvider — updateFrameworkPackage deferRestart propagatio
|
|
|
371
385
|
deferRestart: false,
|
|
372
386
|
})
|
|
373
387
|
|
|
374
|
-
const callArg = env.psMock['updateFrameworkPackage']!.mock.calls[0]![0] as Record<
|
|
388
|
+
const callArg = env.psMock['updateFrameworkPackage']!.mock.calls[0]![0] as Record<
|
|
389
|
+
string,
|
|
390
|
+
unknown
|
|
391
|
+
>
|
|
375
392
|
expect(callArg['deferRestart']).toBe(false)
|
|
376
393
|
})
|
|
377
394
|
|
|
@@ -381,7 +398,10 @@ describe('buildAddonsProvider — updateFrameworkPackage deferRestart propagatio
|
|
|
381
398
|
version: '0.1.40',
|
|
382
399
|
})
|
|
383
400
|
|
|
384
|
-
const callArg = env.psMock['updateFrameworkPackage']!.mock.calls[0]![0] as Record<
|
|
401
|
+
const callArg = env.psMock['updateFrameworkPackage']!.mock.calls[0]![0] as Record<
|
|
402
|
+
string,
|
|
403
|
+
unknown
|
|
404
|
+
>
|
|
385
405
|
// Either undefined or not present — both are acceptable
|
|
386
406
|
expect(callArg['deferRestart']).toBeUndefined()
|
|
387
407
|
})
|