@camstack/server 0.1.7 → 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 +11 -9
- 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 +206 -0
- 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 +292 -0
- 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 +177 -0
- 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 +137 -0
- 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 +265 -5
- 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/__tests__/integration-markers.spec.ts +10 -0
- 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 +459 -166
- 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 +58 -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__/client-ip.spec.ts +27 -1
- 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 +136 -0
- package/src/api/trpc/cap-mount-helpers.ts +64 -44
- package/src/api/trpc/cap-route-error-formatter.ts +17 -9
- package/src/api/trpc/client-ip.ts +17 -0
- package/src/api/trpc/core-cap-bridge.ts +3 -1
- package/src/api/trpc/generated-cap-mounts.ts +801 -286
- package/src/api/trpc/generated-cap-routers.ts +5723 -719
- 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 +117 -48
- package/src/auth/session-cookie.ts +10 -0
- package/src/boot/__tests__/integration-id-backfill.spec.ts +131 -0
- package/src/boot/boot-config.ts +103 -122
- package/src/boot/integration-id-backfill.ts +109 -0
- 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/__tests__/addon-row-manifest.spec.ts +62 -0
- 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 +1212 -1267
- package/src/core/addon/addon-row-manifest.ts +29 -0
- 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 +19 -5
- 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 +145 -29
- package/src/core/network/network-quality.service.spec.ts +7 -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 +658 -495
- package/src/manual-boot.ts +133 -154
- package/tsconfig.json +20 -8
- package/src/core/storage/settings-store.spec.ts +0 -213
- package/src/core/storage/settings-store.ts +0 -2
- package/src/core/storage/sql-schema.spec.ts +0 -140
- package/src/core/storage/sql-schema.ts +0 -3
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
import { buildIntegrationsProvider } from '../../api/core/cap-providers'
|
|
3
|
+
import type { CapabilityRegistry } from '@camstack/kernel'
|
|
4
|
+
import type { Integration } from '@camstack/types'
|
|
5
|
+
|
|
6
|
+
// ── Minimal stubs ────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
const INTEGRATION_ID = 'integ-abc-123'
|
|
9
|
+
const ADDON_ID = 'provider-test'
|
|
10
|
+
|
|
11
|
+
function makeIntegration(id = INTEGRATION_ID): Integration {
|
|
12
|
+
return {
|
|
13
|
+
id,
|
|
14
|
+
addonId: ADDON_ID,
|
|
15
|
+
name: 'Test Integration',
|
|
16
|
+
enabled: true,
|
|
17
|
+
info: null,
|
|
18
|
+
settings: null,
|
|
19
|
+
createdAt: new Date().toISOString(),
|
|
20
|
+
updatedAt: new Date().toISOString(),
|
|
21
|
+
} as unknown as Integration
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function makeIntegrationRegistry(integration: Integration | null = makeIntegration()) {
|
|
25
|
+
return {
|
|
26
|
+
getIntegration: vi.fn(async (_id: string) => integration),
|
|
27
|
+
deleteIntegration: vi.fn(async (_id: string) => undefined),
|
|
28
|
+
listIntegrations: vi.fn(async () => (integration ? [integration] : [])),
|
|
29
|
+
createIntegration: vi.fn(),
|
|
30
|
+
updateIntegration: vi.fn(),
|
|
31
|
+
getIntegrationByAddonId: vi.fn(),
|
|
32
|
+
getIntegrationSettings: vi.fn(),
|
|
33
|
+
setIntegrationSettings: vi.fn(),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function makeAddonRegistry(reg = makeIntegrationRegistry()) {
|
|
38
|
+
return {
|
|
39
|
+
getIntegrationRegistry: vi.fn(() => reg),
|
|
40
|
+
listAddons: vi.fn(() => []),
|
|
41
|
+
restartAddon: vi.fn(),
|
|
42
|
+
getCapabilityRegistry: vi.fn(() => ({
|
|
43
|
+
getProviderByAddon: vi.fn(() => null),
|
|
44
|
+
})),
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function makeEventBus() {
|
|
49
|
+
return { emit: vi.fn() }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function makeLogger() {
|
|
53
|
+
return {
|
|
54
|
+
info: vi.fn(),
|
|
55
|
+
warn: vi.fn(),
|
|
56
|
+
error: vi.fn(),
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function makeLoggingService(log = makeLogger()) {
|
|
61
|
+
return { createLogger: vi.fn(() => log) }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function makeCapabilityRegistry(
|
|
65
|
+
removeByIntegration:
|
|
66
|
+
| ((input: { integrationId: string }) => Promise<{ removed: number }>)
|
|
67
|
+
| null = vi.fn(async () => ({ removed: 2 })),
|
|
68
|
+
): CapabilityRegistry {
|
|
69
|
+
return {
|
|
70
|
+
getSingleton: vi.fn((_cap: string) => (removeByIntegration ? { removeByIntegration } : null)),
|
|
71
|
+
} as unknown as CapabilityRegistry
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
describe('integrations.delete cascade-removes devices via removeByIntegration', () => {
|
|
77
|
+
let integrationReg: ReturnType<typeof makeIntegrationRegistry>
|
|
78
|
+
let addonReg: ReturnType<typeof makeAddonRegistry>
|
|
79
|
+
let eb: ReturnType<typeof makeEventBus>
|
|
80
|
+
let log: ReturnType<typeof makeLogger>
|
|
81
|
+
let loggingService: ReturnType<typeof makeLoggingService>
|
|
82
|
+
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
integrationReg = makeIntegrationRegistry()
|
|
85
|
+
addonReg = makeAddonRegistry(integrationReg)
|
|
86
|
+
eb = makeEventBus()
|
|
87
|
+
log = makeLogger()
|
|
88
|
+
loggingService = makeLoggingService(log)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('calls removeByIntegration with the integration id before deleteIntegration', async () => {
|
|
92
|
+
const removeByIntegration = vi.fn(async () => ({ removed: 3 }))
|
|
93
|
+
const capReg = makeCapabilityRegistry(removeByIntegration)
|
|
94
|
+
|
|
95
|
+
const provider = buildIntegrationsProvider(
|
|
96
|
+
addonReg as never,
|
|
97
|
+
eb as never,
|
|
98
|
+
loggingService as never,
|
|
99
|
+
capReg,
|
|
100
|
+
)
|
|
101
|
+
await provider.delete({ id: INTEGRATION_ID })
|
|
102
|
+
|
|
103
|
+
expect(removeByIntegration).toHaveBeenCalledOnce()
|
|
104
|
+
expect(removeByIntegration).toHaveBeenCalledWith({ integrationId: INTEGRATION_ID })
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('still deletes the integration record and emits integration.deleted after the cascade', async () => {
|
|
108
|
+
const capReg = makeCapabilityRegistry()
|
|
109
|
+
|
|
110
|
+
const provider = buildIntegrationsProvider(
|
|
111
|
+
addonReg as never,
|
|
112
|
+
eb as never,
|
|
113
|
+
loggingService as never,
|
|
114
|
+
capReg,
|
|
115
|
+
)
|
|
116
|
+
const result = await provider.delete({ id: INTEGRATION_ID })
|
|
117
|
+
|
|
118
|
+
expect(integrationReg.deleteIntegration).toHaveBeenCalledOnce()
|
|
119
|
+
expect(integrationReg.deleteIntegration).toHaveBeenCalledWith(INTEGRATION_ID)
|
|
120
|
+
|
|
121
|
+
expect(eb.emit).toHaveBeenCalledOnce()
|
|
122
|
+
expect(eb.emit).toHaveBeenCalledWith(
|
|
123
|
+
expect.objectContaining({
|
|
124
|
+
category: 'integration.deleted',
|
|
125
|
+
data: expect.objectContaining({ integrationId: INTEGRATION_ID }),
|
|
126
|
+
}),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
expect(result).toEqual({ success: true, deletedId: INTEGRATION_ID })
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('logs the removed count from removeByIntegration', async () => {
|
|
133
|
+
const removeByIntegration = vi.fn(async () => ({ removed: 5 }))
|
|
134
|
+
const capReg = makeCapabilityRegistry(removeByIntegration)
|
|
135
|
+
|
|
136
|
+
const provider = buildIntegrationsProvider(
|
|
137
|
+
addonReg as never,
|
|
138
|
+
eb as never,
|
|
139
|
+
loggingService as never,
|
|
140
|
+
capReg,
|
|
141
|
+
)
|
|
142
|
+
await provider.delete({ id: INTEGRATION_ID })
|
|
143
|
+
|
|
144
|
+
expect(log.info).toHaveBeenCalledWith(
|
|
145
|
+
'cascade-removed devices',
|
|
146
|
+
expect.objectContaining({ meta: expect.objectContaining({ removed: 5 }) }),
|
|
147
|
+
)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('does NOT abort the integration delete when removeByIntegration throws (best-effort)', async () => {
|
|
151
|
+
const removeByIntegration = vi.fn(async () => {
|
|
152
|
+
throw new Error('device-manager transient error')
|
|
153
|
+
})
|
|
154
|
+
const capReg = makeCapabilityRegistry(removeByIntegration)
|
|
155
|
+
|
|
156
|
+
const provider = buildIntegrationsProvider(
|
|
157
|
+
addonReg as never,
|
|
158
|
+
eb as never,
|
|
159
|
+
loggingService as never,
|
|
160
|
+
capReg,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
// Should NOT throw
|
|
164
|
+
await expect(provider.delete({ id: INTEGRATION_ID })).resolves.toEqual({
|
|
165
|
+
success: true,
|
|
166
|
+
deletedId: INTEGRATION_ID,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
// Integration record deletion and event still fire
|
|
170
|
+
expect(integrationReg.deleteIntegration).toHaveBeenCalledOnce()
|
|
171
|
+
expect(eb.emit).toHaveBeenCalledOnce()
|
|
172
|
+
|
|
173
|
+
// A warning is logged
|
|
174
|
+
expect(log.warn).toHaveBeenCalledWith(
|
|
175
|
+
'device cascade-remove failed (best-effort — continuing)',
|
|
176
|
+
expect.anything(),
|
|
177
|
+
)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('warns and skips cascade when capabilityRegistry is null', async () => {
|
|
181
|
+
const provider = buildIntegrationsProvider(
|
|
182
|
+
addonReg as never,
|
|
183
|
+
eb as never,
|
|
184
|
+
loggingService as never,
|
|
185
|
+
null,
|
|
186
|
+
)
|
|
187
|
+
await provider.delete({ id: INTEGRATION_ID })
|
|
188
|
+
|
|
189
|
+
expect(integrationReg.deleteIntegration).toHaveBeenCalledOnce()
|
|
190
|
+
expect(eb.emit).toHaveBeenCalledOnce()
|
|
191
|
+
expect(log.warn).toHaveBeenCalledWith(
|
|
192
|
+
'device-manager not available — skipping cascade device removal',
|
|
193
|
+
expect.anything(),
|
|
194
|
+
)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('warns and skips cascade when device-manager singleton is not registered', async () => {
|
|
198
|
+
const capReg = makeCapabilityRegistry(null)
|
|
199
|
+
|
|
200
|
+
const provider = buildIntegrationsProvider(
|
|
201
|
+
addonReg as never,
|
|
202
|
+
eb as never,
|
|
203
|
+
loggingService as never,
|
|
204
|
+
capReg,
|
|
205
|
+
)
|
|
206
|
+
await provider.delete({ id: INTEGRATION_ID })
|
|
207
|
+
|
|
208
|
+
expect(integrationReg.deleteIntegration).toHaveBeenCalledOnce()
|
|
209
|
+
expect(log.warn).toHaveBeenCalledWith(
|
|
210
|
+
'device-manager not available — skipping cascade device removal',
|
|
211
|
+
expect.anything(),
|
|
212
|
+
)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('stamps legacy un-tagged devices of the addon BEFORE cascade so they are removed too', async () => {
|
|
216
|
+
const removeByIntegration = vi.fn(async () => ({ removed: 1 }))
|
|
217
|
+
const setIntegrationId = vi.fn(
|
|
218
|
+
async (_input: { deviceId: number; integrationId: string }) => undefined,
|
|
219
|
+
)
|
|
220
|
+
// One un-tagged top-level device of the integration's addon (claimable),
|
|
221
|
+
// one already-tagged (skip), one child (skip), one other-addon (skip).
|
|
222
|
+
const listAll = vi.fn(async () => [
|
|
223
|
+
{ id: 4, addonId: ADDON_ID, parentDeviceId: null },
|
|
224
|
+
{ id: 5, addonId: ADDON_ID, parentDeviceId: null, integrationId: INTEGRATION_ID },
|
|
225
|
+
{ id: 6, addonId: ADDON_ID, parentDeviceId: 4 },
|
|
226
|
+
{ id: 7, addonId: 'provider-other', parentDeviceId: null },
|
|
227
|
+
])
|
|
228
|
+
const capReg = {
|
|
229
|
+
getSingleton: vi.fn(() => ({ removeByIntegration, listAll, setIntegrationId })),
|
|
230
|
+
} as unknown as CapabilityRegistry
|
|
231
|
+
|
|
232
|
+
const provider = buildIntegrationsProvider(
|
|
233
|
+
addonReg as never,
|
|
234
|
+
eb as never,
|
|
235
|
+
loggingService as never,
|
|
236
|
+
capReg,
|
|
237
|
+
)
|
|
238
|
+
await provider.delete({ id: INTEGRATION_ID })
|
|
239
|
+
|
|
240
|
+
// Only device 4 is claimed (untagged, top-level, this addon's single integration).
|
|
241
|
+
expect(setIntegrationId).toHaveBeenCalledOnce()
|
|
242
|
+
expect(setIntegrationId).toHaveBeenCalledWith({ deviceId: 4, integrationId: INTEGRATION_ID })
|
|
243
|
+
// Cascade still runs after the claim.
|
|
244
|
+
expect(removeByIntegration).toHaveBeenCalledWith({ integrationId: INTEGRATION_ID })
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('claim failure does not abort the integration delete (best-effort)', async () => {
|
|
248
|
+
const removeByIntegration = vi.fn(async () => ({ removed: 0 }))
|
|
249
|
+
const setIntegrationId = vi.fn(async () => {
|
|
250
|
+
throw new Error('stamp boom')
|
|
251
|
+
})
|
|
252
|
+
const listAll = vi.fn(async () => [{ id: 4, addonId: ADDON_ID, parentDeviceId: null }])
|
|
253
|
+
const capReg = {
|
|
254
|
+
getSingleton: vi.fn(() => ({ removeByIntegration, listAll, setIntegrationId })),
|
|
255
|
+
} as unknown as CapabilityRegistry
|
|
256
|
+
|
|
257
|
+
const provider = buildIntegrationsProvider(
|
|
258
|
+
addonReg as never,
|
|
259
|
+
eb as never,
|
|
260
|
+
loggingService as never,
|
|
261
|
+
capReg,
|
|
262
|
+
)
|
|
263
|
+
await expect(provider.delete({ id: INTEGRATION_ID })).resolves.toEqual({
|
|
264
|
+
success: true,
|
|
265
|
+
deletedId: INTEGRATION_ID,
|
|
266
|
+
})
|
|
267
|
+
expect(integrationReg.deleteIntegration).toHaveBeenCalledOnce()
|
|
268
|
+
expect(log.warn).toHaveBeenCalledWith(
|
|
269
|
+
'legacy device claim failed (best-effort — continuing)',
|
|
270
|
+
expect.anything(),
|
|
271
|
+
)
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('throws when the integration is not found (guard still fires before cascade)', async () => {
|
|
275
|
+
integrationReg = makeIntegrationRegistry(null)
|
|
276
|
+
addonReg = makeAddonRegistry(integrationReg)
|
|
277
|
+
const capReg = makeCapabilityRegistry()
|
|
278
|
+
|
|
279
|
+
const provider = buildIntegrationsProvider(
|
|
280
|
+
addonReg as never,
|
|
281
|
+
eb as never,
|
|
282
|
+
loggingService as never,
|
|
283
|
+
capReg,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
await expect(provider.delete({ id: 'missing-id' })).rejects.toThrow('not found')
|
|
287
|
+
|
|
288
|
+
// Nothing should have been cleaned up
|
|
289
|
+
expect(integrationReg.deleteIntegration).not.toHaveBeenCalled()
|
|
290
|
+
expect(eb.emit).not.toHaveBeenCalled()
|
|
291
|
+
})
|
|
292
|
+
})
|
|
@@ -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
|
})
|
|
@@ -52,9 +52,7 @@ function makeNodeRegistry(nodes: ReadonlyMap<string, readonly StubNodeEntry[]>)
|
|
|
52
52
|
// Helpers for stub CapabilityService
|
|
53
53
|
// ---------------------------------------------------------------------------
|
|
54
54
|
|
|
55
|
-
function makeCapabilityService(
|
|
56
|
-
providers: ReadonlyMap<string, Record<string, unknown>>,
|
|
57
|
-
) {
|
|
55
|
+
function makeCapabilityService(providers: ReadonlyMap<string, Record<string, unknown>>) {
|
|
58
56
|
return {
|
|
59
57
|
getSingleton<T>(capability: string): T | null {
|
|
60
58
|
return (providers.get(capability) as T) ?? null
|
|
@@ -68,7 +66,10 @@ function makeCapabilityService(
|
|
|
68
66
|
|
|
69
67
|
describe('createNodeCapAuthority', () => {
|
|
70
68
|
const nodes = new Map([
|
|
71
|
-
[
|
|
69
|
+
[
|
|
70
|
+
'hub/stream-broker',
|
|
71
|
+
[{ addonId: 'addon-stream-broker', capabilities: ['stream-broker', 'stream-params'] }],
|
|
72
|
+
],
|
|
72
73
|
['dev-agent-0', [{ addonId: 'addon-detection-pipeline', capabilities: ['pipeline-executor'] }]],
|
|
73
74
|
])
|
|
74
75
|
const registry = makeNodeRegistry(nodes)
|
|
@@ -86,7 +87,9 @@ describe('createNodeCapAuthority', () => {
|
|
|
86
87
|
|
|
87
88
|
it('getAddonId returns the addonId for a known cap', () => {
|
|
88
89
|
expect(authority.getAddonId('hub/stream-broker', 'stream-broker')).toBe('addon-stream-broker')
|
|
89
|
-
expect(authority.getAddonId('dev-agent-0', 'pipeline-executor')).toBe(
|
|
90
|
+
expect(authority.getAddonId('dev-agent-0', 'pipeline-executor')).toBe(
|
|
91
|
+
'addon-detection-pipeline',
|
|
92
|
+
)
|
|
90
93
|
})
|
|
91
94
|
|
|
92
95
|
it('getAddonId returns null for missing nodes or caps', () => {
|
|
@@ -126,10 +129,13 @@ describe('createNodeCapAuthority', () => {
|
|
|
126
129
|
describe('createNodeCapAuthority — per-node singleton override', () => {
|
|
127
130
|
it('getAddonId honors the per-node singleton override when available', () => {
|
|
128
131
|
const nodeRegistry = {
|
|
129
|
-
getNodeManifest: (id: string) =>
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
132
|
+
getNodeManifest: (id: string) =>
|
|
133
|
+
id === 'dev-agent-0'
|
|
134
|
+
? [
|
|
135
|
+
{ addonId: 'webrtc-native', capabilities: ['webrtc-session'] },
|
|
136
|
+
{ addonId: 'stream-broker', capabilities: ['webrtc-session'] },
|
|
137
|
+
]
|
|
138
|
+
: undefined,
|
|
133
139
|
listNodeIds: () => ['hub', 'dev-agent-0'],
|
|
134
140
|
}
|
|
135
141
|
const authority = createNodeCapAuthority(nodeRegistry, {
|
|
@@ -141,9 +147,10 @@ describe('createNodeCapAuthority — per-node singleton override', () => {
|
|
|
141
147
|
|
|
142
148
|
it('getAddonId falls back to first manifest match without an override', () => {
|
|
143
149
|
const nodeRegistry = {
|
|
144
|
-
getNodeManifest: (id: string) =>
|
|
145
|
-
|
|
146
|
-
|
|
150
|
+
getNodeManifest: (id: string) =>
|
|
151
|
+
id === 'dev-agent-0'
|
|
152
|
+
? [{ addonId: 'webrtc-native', capabilities: ['webrtc-session'] }]
|
|
153
|
+
: undefined,
|
|
147
154
|
listNodeIds: () => ['hub', 'dev-agent-0'],
|
|
148
155
|
}
|
|
149
156
|
const authority = createNodeCapAuthority(nodeRegistry, { resolveSingleton: () => null })
|
|
@@ -186,7 +193,9 @@ describe('createInProcessProviderLookup', () => {
|
|
|
186
193
|
expect(ref).not.toBeNull()
|
|
187
194
|
|
|
188
195
|
await expect(ref!.invoke('notAFn', {})).rejects.toThrow(/method "notAFn" not found/)
|
|
189
|
-
await expect(ref!.invoke('missingMethod', {})).rejects.toThrow(
|
|
196
|
+
await expect(ref!.invoke('missingMethod', {})).rejects.toThrow(
|
|
197
|
+
/method "missingMethod" not found/,
|
|
198
|
+
)
|
|
190
199
|
})
|
|
191
200
|
})
|
|
192
201
|
|
|
@@ -201,7 +210,9 @@ describe('Resolver + adapters — end-to-end dispatch', () => {
|
|
|
201
210
|
return vi.fn(async (_childId: string, _input: unknown) => ({ ok: true, from: 'uds' }))
|
|
202
211
|
}
|
|
203
212
|
|
|
204
|
-
function makeHubLocalRegistry(
|
|
213
|
+
function makeHubLocalRegistry(
|
|
214
|
+
caps: ReadonlyMap<string, string>,
|
|
215
|
+
): HubLocalChildDispatcher & { callSpy: ReturnType<typeof vi.fn> } {
|
|
205
216
|
const callSpy = makeCallCapOnChildSpy()
|
|
206
217
|
return {
|
|
207
218
|
resolveChildId: (capName: string) => caps.get(capName) ?? null,
|
|
@@ -284,6 +295,8 @@ describe('Resolver + adapters — end-to-end dispatch', () => {
|
|
|
284
295
|
expect(thrown).toBeInstanceOf(CapRouteError)
|
|
285
296
|
expect((thrown as CapRouteError).reason).toBe('no-provider')
|
|
286
297
|
// Must NOT be the old opaque string
|
|
287
|
-
expect((thrown as CapRouteError).message).not.toContain(
|
|
298
|
+
expect((thrown as CapRouteError).message).not.toContain(
|
|
299
|
+
'Capability "ghost-cap" not available on node',
|
|
300
|
+
)
|
|
288
301
|
})
|
|
289
302
|
})
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
/**
|
|
3
2
|
* Meta-test: ensures that every capability with methods has a corresponding
|
|
4
3
|
* `<cap-name>.router.spec.ts` file in this directory. This prevents new caps
|
|
@@ -40,7 +39,7 @@ function collectCapabilitiesWithMethods(): readonly CapabilityDefinition[] {
|
|
|
40
39
|
if (Object.keys(value.methods).length === 0) continue
|
|
41
40
|
out.push(value)
|
|
42
41
|
}
|
|
43
|
-
return out.
|
|
42
|
+
return out.toSorted((a, b) => a.name.localeCompare(b.name))
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
function specFileNameFor(capName: string): string {
|
|
@@ -51,7 +50,7 @@ describe('cap-routers meta', () => {
|
|
|
51
50
|
const caps = collectCapabilitiesWithMethods()
|
|
52
51
|
const specDir = path.dirname(new URL(import.meta.url).pathname)
|
|
53
52
|
const existingSpecs = new Set(
|
|
54
|
-
fs.readdirSync(specDir).filter(f => f.endsWith('.router.spec.ts')),
|
|
53
|
+
fs.readdirSync(specDir).filter((f) => f.endsWith('.router.spec.ts')),
|
|
55
54
|
)
|
|
56
55
|
|
|
57
56
|
it('discovers at least one capability with methods', () => {
|
|
@@ -79,7 +78,7 @@ describe('cap-routers meta', () => {
|
|
|
79
78
|
})
|
|
80
79
|
|
|
81
80
|
it('ALLOWED_MISSING only references real capabilities', () => {
|
|
82
|
-
const names = new Set(caps.map(c => c.name))
|
|
81
|
+
const names = new Set(caps.map((c) => c.name))
|
|
83
82
|
for (const name of ALLOWED_MISSING) {
|
|
84
83
|
expect(names, `ALLOWED_MISSING references unknown cap "${name}"`).toContain(name)
|
|
85
84
|
}
|
|
@@ -180,8 +179,8 @@ describe('cap-routers meta', () => {
|
|
|
180
179
|
expect(
|
|
181
180
|
outputCount,
|
|
182
181
|
`output-validation codegen drift — expected at least ${procedureCount} .output() ` +
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
`calls (one per query/mutation), found ${outputCount}. ` +
|
|
183
|
+
`Re-run: npx tsx scripts/generate-cap-routers.ts`,
|
|
185
184
|
).toBeGreaterThanOrEqual(procedureCount)
|
|
186
185
|
})
|
|
187
186
|
|
|
@@ -193,7 +192,7 @@ describe('cap-routers meta', () => {
|
|
|
193
192
|
const outputCount = (generatedSource.match(/\.output\(/g) ?? []).length
|
|
194
193
|
console.log(
|
|
195
194
|
`[output-validation codegen] queries=${queryCount} mutations=${mutationCount} ` +
|
|
196
|
-
|
|
195
|
+
`subscriptions=${subscriptionCount} outputs=${outputCount}`,
|
|
197
196
|
)
|
|
198
197
|
})
|
|
199
198
|
})
|
|
@@ -31,9 +31,12 @@ describe('addon-settings cap router', () => {
|
|
|
31
31
|
it('getGlobalSettings returns schema', async () => {
|
|
32
32
|
const provider = makeMockProvider()
|
|
33
33
|
const router = createCapRouter_addonSettings(() => provider)
|
|
34
|
-
const result = await invokeProcedure(router, 'getGlobalSettings', makeCtx('admin'), {
|
|
34
|
+
const result = await invokeProcedure(router, 'getGlobalSettings', makeCtx('admin'), {
|
|
35
|
+
addonId: 'test',
|
|
36
|
+
})
|
|
35
37
|
expect(result.ok).toBe(true)
|
|
36
|
-
if (result.ok)
|
|
38
|
+
if (result.ok)
|
|
39
|
+
expect(result.value).toEqual({ sections: [{ id: 'g', title: 'Global', fields: [] }] })
|
|
37
40
|
expect(provider.getGlobalSettings).toHaveBeenCalledWith({ addonId: 'test' })
|
|
38
41
|
})
|
|
39
42
|
|
|
@@ -41,7 +44,8 @@ describe('addon-settings cap router', () => {
|
|
|
41
44
|
const provider = makeMockProvider()
|
|
42
45
|
const router = createCapRouter_addonSettings(() => provider)
|
|
43
46
|
const result = await invokeProcedure(router, 'updateGlobalSettings', makeCtx('admin'), {
|
|
44
|
-
addonId: 'test',
|
|
47
|
+
addonId: 'test',
|
|
48
|
+
patch: { volume: 50 },
|
|
45
49
|
})
|
|
46
50
|
expect(result.ok).toBe(true)
|
|
47
51
|
if (result.ok) expect(result.value).toEqual({ success: true })
|
|
@@ -51,7 +55,8 @@ describe('addon-settings cap router', () => {
|
|
|
51
55
|
const provider = makeMockProvider()
|
|
52
56
|
const router = createCapRouter_addonSettings(() => provider)
|
|
53
57
|
await invokeProcedure(router, 'getDeviceSettings', makeCtx('admin'), {
|
|
54
|
-
addonId: 'test',
|
|
58
|
+
addonId: 'test',
|
|
59
|
+
deviceId: 1,
|
|
55
60
|
})
|
|
56
61
|
expect(provider.getDeviceSettings).toHaveBeenCalledWith({ addonId: 'test', deviceId: 1 })
|
|
57
62
|
})
|
|
@@ -60,16 +65,22 @@ describe('addon-settings cap router', () => {
|
|
|
60
65
|
const provider = makeMockProvider()
|
|
61
66
|
const router = createCapRouter_addonSettings(() => provider)
|
|
62
67
|
await invokeProcedure(router, 'updateDeviceSettings', makeCtx('admin'), {
|
|
63
|
-
addonId: 'test',
|
|
68
|
+
addonId: 'test',
|
|
69
|
+
deviceId: 1,
|
|
70
|
+
patch: { enabled: false },
|
|
64
71
|
})
|
|
65
72
|
expect(provider.updateDeviceSettings).toHaveBeenCalledWith({
|
|
66
|
-
addonId: 'test',
|
|
73
|
+
addonId: 'test',
|
|
74
|
+
deviceId: 1,
|
|
75
|
+
patch: { enabled: false },
|
|
67
76
|
})
|
|
68
77
|
})
|
|
69
78
|
|
|
70
79
|
it('returns PRECONDITION_FAILED when provider is null', async () => {
|
|
71
80
|
const router = createCapRouter_addonSettings(() => null)
|
|
72
|
-
const result = await invokeProcedure(router, 'getGlobalSettings', makeCtx('admin'), {
|
|
81
|
+
const result = await invokeProcedure(router, 'getGlobalSettings', makeCtx('admin'), {
|
|
82
|
+
addonId: 'x',
|
|
83
|
+
})
|
|
73
84
|
expect(result.ok).toBe(false)
|
|
74
85
|
if (!result.ok) expect(result.code).toBe('PRECONDITION_FAILED')
|
|
75
86
|
})
|
|
@@ -78,9 +89,7 @@ describe('addon-settings cap router', () => {
|
|
|
78
89
|
const provider = makeMockProvider()
|
|
79
90
|
const router = createCapRouter_addonSettings(() => provider)
|
|
80
91
|
for (const method of ['getGlobalSettings', 'getDeviceSettings']) {
|
|
81
|
-
const input = method.includes('Device')
|
|
82
|
-
? { addonId: 'x', deviceId: 1 }
|
|
83
|
-
: { addonId: 'x' }
|
|
92
|
+
const input = method.includes('Device') ? { addonId: 'x', deviceId: 1 } : { addonId: 'x' }
|
|
84
93
|
const results = await checkAuthMatrix(router, method, 'protected', input)
|
|
85
94
|
for (const r of results) {
|
|
86
95
|
if (r.allowed) expect(r.outcome.ok).toBe(true)
|