@camstack/server 0.1.8 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
/**
|
|
3
2
|
* Router-level spec for `pipelineExecutor` — covers the reference-media
|
|
4
3
|
* and audio-capabilities endpoints that remain on the cap after the
|
|
@@ -11,7 +10,9 @@ import { createCapRouter_pipelineExecutor } from '../../api/trpc/generated-cap-r
|
|
|
11
10
|
import type { IPipelineExecutorProvider } from '@camstack/types'
|
|
12
11
|
import { makeCtx, invokeProcedure } from './harness.js'
|
|
13
12
|
|
|
14
|
-
function makeMockProvider(
|
|
13
|
+
function makeMockProvider(
|
|
14
|
+
overrides: Partial<IPipelineExecutorProvider> = {},
|
|
15
|
+
): IPipelineExecutorProvider {
|
|
15
16
|
const defaultEngine = { runtime: 'node' as const, backend: 'cpu', format: 'onnx' as const }
|
|
16
17
|
|
|
17
18
|
return {
|
|
@@ -20,7 +21,9 @@ function makeMockProvider(overrides: Partial<IPipelineExecutorProvider> = {}): I
|
|
|
20
21
|
setEngine: async () => ({ steps: [] }),
|
|
21
22
|
getDefaultSteps: async () => [],
|
|
22
23
|
getSchema: async () => ({
|
|
23
|
-
availableEngines: [
|
|
24
|
+
availableEngines: [
|
|
25
|
+
{ engine: defaultEngine, devices: [{ id: 'cpu', label: 'CPU' }], defaultDevice: 'cpu' },
|
|
26
|
+
],
|
|
24
27
|
selectedEngine: defaultEngine,
|
|
25
28
|
slots: [],
|
|
26
29
|
}),
|
|
@@ -29,15 +32,28 @@ function makeMockProvider(overrides: Partial<IPipelineExecutorProvider> = {}): I
|
|
|
29
32
|
getGlobalPipelineConfig: () => null,
|
|
30
33
|
getOrchestratorConfigSchema: () => ({ sections: [] }),
|
|
31
34
|
getOrchestratorSettings: () => ({
|
|
32
|
-
motionFps: 4,
|
|
35
|
+
motionFps: 4,
|
|
36
|
+
detectionFps: 10,
|
|
37
|
+
cooldownMs: 10000,
|
|
38
|
+
maxConcurrentInferences: null,
|
|
33
39
|
}),
|
|
34
40
|
setOrchestratorSettings: () => {},
|
|
35
41
|
listTemplates: () => [],
|
|
36
42
|
saveTemplate: () => ({
|
|
37
|
-
id: 'tpl-1',
|
|
43
|
+
id: 'tpl-1',
|
|
44
|
+
name: 'x',
|
|
45
|
+
createdAt: '',
|
|
46
|
+
updatedAt: '',
|
|
47
|
+
engine: defaultEngine,
|
|
48
|
+
steps: [],
|
|
38
49
|
}),
|
|
39
50
|
updateTemplate: () => ({
|
|
40
|
-
id: 'tpl-1',
|
|
51
|
+
id: 'tpl-1',
|
|
52
|
+
name: 'x',
|
|
53
|
+
createdAt: '',
|
|
54
|
+
updatedAt: '',
|
|
55
|
+
engine: defaultEngine,
|
|
56
|
+
steps: [],
|
|
41
57
|
}),
|
|
42
58
|
deleteTemplate: () => {},
|
|
43
59
|
getCapabilities: async () => {
|
|
@@ -64,11 +80,18 @@ function makeMockProvider(overrides: Partial<IPipelineExecutorProvider> = {}): I
|
|
|
64
80
|
getReferenceAudio: () => ({ base64: 'AAAA' }),
|
|
65
81
|
getAudioCapabilities: () => ({
|
|
66
82
|
activeBackend: 'yamnet-onnx',
|
|
67
|
-
availableBackends: [
|
|
83
|
+
availableBackends: [
|
|
84
|
+
{ id: 'yamnet-onnx', name: 'YAMNet ONNX', description: '', available: true },
|
|
85
|
+
],
|
|
68
86
|
sampleRate: 16000,
|
|
69
87
|
chunkDurationMs: 500,
|
|
70
88
|
}),
|
|
71
|
-
getAddonResolver: () => ({
|
|
89
|
+
getAddonResolver: () => ({
|
|
90
|
+
resolve: async () => {
|
|
91
|
+
throw new Error('nop')
|
|
92
|
+
},
|
|
93
|
+
shutdownAll: async () => {},
|
|
94
|
+
}),
|
|
72
95
|
orchestratorStatus: () => null,
|
|
73
96
|
cameraDetectionStatus: () => null,
|
|
74
97
|
getDevicePipelineSteps: () => null,
|
|
@@ -83,9 +106,10 @@ describe('createCapRouter_pipelineExecutor — reference + audio endpoints', ()
|
|
|
83
106
|
it('delegates to provider.listReferenceImages', async () => {
|
|
84
107
|
const router = createCapRouter_pipelineExecutor(() =>
|
|
85
108
|
makeMockProvider({
|
|
86
|
-
listReferenceImages: () =>
|
|
87
|
-
|
|
88
|
-
|
|
109
|
+
listReferenceImages: () =>
|
|
110
|
+
[
|
|
111
|
+
{ id: 'persons-cars-animal.jpg', filename: 'persons-cars-animal.jpg', sizeKB: 120 },
|
|
112
|
+
] as never,
|
|
89
113
|
}),
|
|
90
114
|
)
|
|
91
115
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
/**
|
|
3
2
|
* Spec for the codegen'd `settings-store` capability router.
|
|
4
3
|
*
|
|
@@ -34,7 +33,10 @@ function makeMockProvider(): ISettingsStoreProvider & { _store: MockStore } {
|
|
|
34
33
|
},
|
|
35
34
|
|
|
36
35
|
async set(input) {
|
|
37
|
-
store.records.set(input.key, {
|
|
36
|
+
store.records.set(input.key, {
|
|
37
|
+
id: input.key,
|
|
38
|
+
data: (input.value as Record<string, unknown>) ?? {},
|
|
39
|
+
})
|
|
38
40
|
},
|
|
39
41
|
|
|
40
42
|
async query(input) {
|
|
@@ -81,14 +83,20 @@ describe('createCapRouter_settingsStore', () => {
|
|
|
81
83
|
it('returns stored value by key', async () => {
|
|
82
84
|
provider._store.records.set('myKey', { id: 'myKey', data: { foo: 'bar' } })
|
|
83
85
|
const router = createCapRouter_settingsStore(() => provider)
|
|
84
|
-
const result = await invokeProcedure(router, 'get', makeCtx('admin'), {
|
|
86
|
+
const result = await invokeProcedure(router, 'get', makeCtx('admin'), {
|
|
87
|
+
collection: 'test',
|
|
88
|
+
key: 'myKey',
|
|
89
|
+
})
|
|
85
90
|
expect(result.ok).toBe(true)
|
|
86
91
|
if (result.ok) expect(result.value).toMatchObject({ foo: 'bar' })
|
|
87
92
|
})
|
|
88
93
|
|
|
89
94
|
it('returns null for missing key', async () => {
|
|
90
95
|
const router = createCapRouter_settingsStore(() => provider)
|
|
91
|
-
const result = await invokeProcedure(router, 'get', makeCtx('admin'), {
|
|
96
|
+
const result = await invokeProcedure(router, 'get', makeCtx('admin'), {
|
|
97
|
+
collection: 'test',
|
|
98
|
+
key: 'nope',
|
|
99
|
+
})
|
|
92
100
|
expect(result.ok).toBe(true)
|
|
93
101
|
if (result.ok) expect(result.value).toBeNull()
|
|
94
102
|
})
|
|
@@ -97,7 +105,11 @@ describe('createCapRouter_settingsStore', () => {
|
|
|
97
105
|
describe('set', () => {
|
|
98
106
|
it('upserts a value', async () => {
|
|
99
107
|
const router = createCapRouter_settingsStore(() => provider)
|
|
100
|
-
const result = await invokeProcedure(router, 'set', makeCtx('admin'), {
|
|
108
|
+
const result = await invokeProcedure(router, 'set', makeCtx('admin'), {
|
|
109
|
+
collection: 'test',
|
|
110
|
+
key: 'k1',
|
|
111
|
+
value: { x: 1 },
|
|
112
|
+
})
|
|
101
113
|
expect(result.ok).toBe(true)
|
|
102
114
|
expect(provider._store.records.has('k1')).toBe(true)
|
|
103
115
|
})
|
|
@@ -108,7 +120,9 @@ describe('createCapRouter_settingsStore', () => {
|
|
|
108
120
|
provider._store.records.set('a', { id: 'a', data: { v: 1 } })
|
|
109
121
|
provider._store.records.set('b', { id: 'b', data: { v: 2 } })
|
|
110
122
|
const router = createCapRouter_settingsStore(() => provider)
|
|
111
|
-
const result = await invokeProcedure(router, 'query', makeCtx('admin'), {
|
|
123
|
+
const result = await invokeProcedure(router, 'query', makeCtx('admin'), {
|
|
124
|
+
collection: 'test',
|
|
125
|
+
})
|
|
112
126
|
expect(result.ok).toBe(true)
|
|
113
127
|
if (result.ok) expect(result.value).toHaveLength(2)
|
|
114
128
|
})
|
|
@@ -118,7 +132,10 @@ describe('createCapRouter_settingsStore', () => {
|
|
|
118
132
|
provider._store.records.set('b', { id: 'b', data: {} })
|
|
119
133
|
provider._store.records.set('c', { id: 'c', data: {} })
|
|
120
134
|
const router = createCapRouter_settingsStore(() => provider)
|
|
121
|
-
const result = await invokeProcedure(router, 'query', makeCtx('admin'), {
|
|
135
|
+
const result = await invokeProcedure(router, 'query', makeCtx('admin'), {
|
|
136
|
+
collection: 'test',
|
|
137
|
+
filter: { limit: 2 },
|
|
138
|
+
})
|
|
122
139
|
expect(result.ok).toBe(true)
|
|
123
140
|
if (result.ok) expect(result.value).toHaveLength(2)
|
|
124
141
|
})
|
|
@@ -154,7 +171,10 @@ describe('createCapRouter_settingsStore', () => {
|
|
|
154
171
|
it('removes a record', async () => {
|
|
155
172
|
provider._store.records.set('r1', { id: 'r1', data: {} })
|
|
156
173
|
const router = createCapRouter_settingsStore(() => provider)
|
|
157
|
-
const result = await invokeProcedure(router, 'delete', makeCtx('admin'), {
|
|
174
|
+
const result = await invokeProcedure(router, 'delete', makeCtx('admin'), {
|
|
175
|
+
collection: 'test',
|
|
176
|
+
key: 'r1',
|
|
177
|
+
})
|
|
158
178
|
expect(result.ok).toBe(true)
|
|
159
179
|
expect(provider._store.records.has('r1')).toBe(false)
|
|
160
180
|
})
|
|
@@ -165,7 +185,9 @@ describe('createCapRouter_settingsStore', () => {
|
|
|
165
185
|
provider._store.records.set('a', { id: 'a', data: {} })
|
|
166
186
|
provider._store.records.set('b', { id: 'b', data: {} })
|
|
167
187
|
const router = createCapRouter_settingsStore(() => provider)
|
|
168
|
-
const result = await invokeProcedure(router, 'count', makeCtx('admin'), {
|
|
188
|
+
const result = await invokeProcedure(router, 'count', makeCtx('admin'), {
|
|
189
|
+
collection: 'test',
|
|
190
|
+
})
|
|
169
191
|
expect(result.ok).toBe(true)
|
|
170
192
|
if (result.ok) expect(result.value).toBe(2)
|
|
171
193
|
})
|
|
@@ -174,7 +196,9 @@ describe('createCapRouter_settingsStore', () => {
|
|
|
174
196
|
describe('isEmpty', () => {
|
|
175
197
|
it('returns true when empty', async () => {
|
|
176
198
|
const router = createCapRouter_settingsStore(() => provider)
|
|
177
|
-
const result = await invokeProcedure(router, 'isEmpty', makeCtx('admin'), {
|
|
199
|
+
const result = await invokeProcedure(router, 'isEmpty', makeCtx('admin'), {
|
|
200
|
+
collection: 'test',
|
|
201
|
+
})
|
|
178
202
|
expect(result.ok).toBe(true)
|
|
179
203
|
if (result.ok) expect(result.value).toBe(true)
|
|
180
204
|
})
|
|
@@ -182,7 +206,9 @@ describe('createCapRouter_settingsStore', () => {
|
|
|
182
206
|
it('returns false when records exist', async () => {
|
|
183
207
|
provider._store.records.set('x', { id: 'x', data: {} })
|
|
184
208
|
const router = createCapRouter_settingsStore(() => provider)
|
|
185
|
-
const result = await invokeProcedure(router, 'isEmpty', makeCtx('admin'), {
|
|
209
|
+
const result = await invokeProcedure(router, 'isEmpty', makeCtx('admin'), {
|
|
210
|
+
collection: 'test',
|
|
211
|
+
})
|
|
186
212
|
expect(result.ok).toBe(true)
|
|
187
213
|
if (result.ok) expect(result.value).toBe(false)
|
|
188
214
|
})
|
|
@@ -215,8 +241,20 @@ describe('createCapRouter_settingsStore', () => {
|
|
|
215
241
|
describe('auth', () => {
|
|
216
242
|
it('rejects anonymous on all methods', async () => {
|
|
217
243
|
const router = createCapRouter_settingsStore(() => provider)
|
|
218
|
-
for (const method of [
|
|
219
|
-
|
|
244
|
+
for (const method of [
|
|
245
|
+
'get',
|
|
246
|
+
'set',
|
|
247
|
+
'query',
|
|
248
|
+
'insert',
|
|
249
|
+
'update',
|
|
250
|
+
'delete',
|
|
251
|
+
'count',
|
|
252
|
+
'isEmpty',
|
|
253
|
+
]) {
|
|
254
|
+
const result = await invokeProcedure(router, method, makeCtx('anonymous'), {
|
|
255
|
+
collection: 'c',
|
|
256
|
+
key: 'k',
|
|
257
|
+
})
|
|
220
258
|
expect(result.ok).toBe(false)
|
|
221
259
|
if (!result.ok) expect(result.code).toBe('UNAUTHORIZED')
|
|
222
260
|
}
|
|
@@ -225,7 +263,10 @@ describe('createCapRouter_settingsStore', () => {
|
|
|
225
263
|
it('allows viewer on query methods (protected)', async () => {
|
|
226
264
|
const router = createCapRouter_settingsStore(() => provider)
|
|
227
265
|
for (const method of ['get', 'query', 'count', 'isEmpty']) {
|
|
228
|
-
const result = await invokeProcedure(router, method, makeCtx('user'), {
|
|
266
|
+
const result = await invokeProcedure(router, method, makeCtx('user'), {
|
|
267
|
+
collection: 'c',
|
|
268
|
+
key: 'k',
|
|
269
|
+
})
|
|
229
270
|
expect(result.ok).toBe(true)
|
|
230
271
|
}
|
|
231
272
|
})
|
|
@@ -236,7 +277,10 @@ describe('createCapRouter_settingsStore', () => {
|
|
|
236
277
|
describe('missing provider', () => {
|
|
237
278
|
it('throws PRECONDITION_FAILED when provider is null', async () => {
|
|
238
279
|
const router = createCapRouter_settingsStore(() => null)
|
|
239
|
-
const result = await invokeProcedure(router, 'get', makeCtx('admin'), {
|
|
280
|
+
const result = await invokeProcedure(router, 'get', makeCtx('admin'), {
|
|
281
|
+
collection: 'c',
|
|
282
|
+
key: 'k',
|
|
283
|
+
})
|
|
240
284
|
expect(result.ok).toBe(false)
|
|
241
285
|
if (!result.ok) {
|
|
242
286
|
expect(result.code).toBe('PRECONDITION_FAILED')
|
|
@@ -46,7 +46,6 @@ class TestAddonHarness {
|
|
|
46
46
|
|
|
47
47
|
/** Register an addon (mimics AddonRegistryService.registerAddon) */
|
|
48
48
|
registerAddon(addon: ICamstackAddon): void {
|
|
49
|
-
|
|
50
49
|
this.addonEntries.set(addon.manifest.id, { addon, initialized: false })
|
|
51
50
|
}
|
|
52
51
|
|
|
@@ -60,7 +59,7 @@ class TestAddonHarness {
|
|
|
60
59
|
for (const cap of caps) {
|
|
61
60
|
// Create a full definition so the registry has a CapabilityState.
|
|
62
61
|
// Mode is inferred from the cap name for test convenience.
|
|
63
|
-
const mode = cap.name === 'log-destination' ? 'collection' as const : 'singleton' as const
|
|
62
|
+
const mode = cap.name === 'log-destination' ? ('collection' as const) : ('singleton' as const)
|
|
64
63
|
this.registry.declareCapability({ name: cap.name, scope: 'system', mode, methods: {} })
|
|
65
64
|
this.registry.declareFromManifest(cap)
|
|
66
65
|
}
|
|
@@ -84,11 +83,12 @@ class TestAddonHarness {
|
|
|
84
83
|
// Mirror the real addon-registry.service behaviour: process the return
|
|
85
84
|
// value which may be ProviderRegistration[] or AddonInitResult.
|
|
86
85
|
if (result) {
|
|
87
|
-
const regs = Array.isArray(result) ? result : (result as any).providers ?? []
|
|
86
|
+
const regs = Array.isArray(result) ? result : ((result as any).providers ?? [])
|
|
88
87
|
for (const reg of regs) {
|
|
89
|
-
const capName: string =
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
const capName: string =
|
|
89
|
+
typeof reg.capability === 'string'
|
|
90
|
+
? reg.capability
|
|
91
|
+
: ((reg.capability as any)?.name ?? String(reg.capability))
|
|
92
92
|
self.registry.registerProvider(capName, id, reg.provider)
|
|
93
93
|
}
|
|
94
94
|
}
|
|
@@ -105,7 +105,6 @@ class TestAddonHarness {
|
|
|
105
105
|
this.registry.unregisterProvider(cap.name, id)
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
|
|
109
108
|
await entry.addon.shutdown()
|
|
110
109
|
entry.initialized = false
|
|
111
110
|
}
|
|
@@ -153,11 +152,10 @@ class TestAddonHarness {
|
|
|
153
152
|
}
|
|
154
153
|
|
|
155
154
|
private getAddonCapabilities(addon: ICamstackAddon): CapabilityDeclaration[] {
|
|
156
|
-
|
|
157
155
|
const manifest = addon.manifest as any
|
|
158
|
-
|
|
156
|
+
|
|
159
157
|
if (!manifest.capabilities) return []
|
|
160
|
-
|
|
158
|
+
|
|
161
159
|
return manifest.capabilities.map((cap: string | CapabilityDeclaration) => {
|
|
162
160
|
if (typeof cap === 'string') {
|
|
163
161
|
const decl: CapabilityDeclaration = { name: cap }
|
|
@@ -268,7 +266,7 @@ describe('Server E2E: Collection addon add/remove', () => {
|
|
|
268
266
|
|
|
269
267
|
// Dynamically add a second log destination
|
|
270
268
|
const logAddon2 = new MockLogAddon()
|
|
271
|
-
|
|
269
|
+
|
|
272
270
|
;(logAddon2 as any).manifest = {
|
|
273
271
|
...logAddon2.manifest,
|
|
274
272
|
id: 'mock-log-addon-2',
|
|
@@ -33,7 +33,10 @@ function waitForPort(port: number, timeoutMs: number = 15000): Promise<void> {
|
|
|
33
33
|
const check = () => {
|
|
34
34
|
const req = https.request(
|
|
35
35
|
{ hostname: '127.0.0.1', port, path: '/', method: 'GET', rejectUnauthorized: false },
|
|
36
|
-
(res) => {
|
|
36
|
+
(res) => {
|
|
37
|
+
res.resume()
|
|
38
|
+
resolve()
|
|
39
|
+
},
|
|
37
40
|
)
|
|
38
41
|
req.on('error', () => {
|
|
39
42
|
if (Date.now() > deadline) {
|
|
@@ -63,67 +66,85 @@ describe('CLI E2E', () => {
|
|
|
63
66
|
// Full server boot tests require the entire build chain
|
|
64
67
|
const fullTest = process.env.CAMSTACK_TEST_CLI_FULL === 'true' && CLI_EXISTS ? it : it.skip
|
|
65
68
|
|
|
66
|
-
fullTest(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
fullTest(
|
|
70
|
+
'camstack serve boots and responds on HTTPS',
|
|
71
|
+
async () => {
|
|
72
|
+
const port = randomPort()
|
|
73
|
+
const proc = fork(CLI_PATH, ['serve', '--port', String(port), '--data', TEST_DATA_DIR], {
|
|
74
|
+
stdio: 'pipe',
|
|
75
|
+
env: { ...process.env, CAMSTACK_TLS_ENABLED: 'true' },
|
|
76
|
+
})
|
|
77
|
+
processes.push(proc)
|
|
78
|
+
|
|
79
|
+
await waitForPort(port)
|
|
80
|
+
|
|
81
|
+
// Verify HTTPS response
|
|
82
|
+
const body = await new Promise<string>((resolve, reject) => {
|
|
83
|
+
const req = https.request(
|
|
84
|
+
{ hostname: '127.0.0.1', port, path: '/', method: 'GET', rejectUnauthorized: false },
|
|
85
|
+
(res) => {
|
|
86
|
+
let data = ''
|
|
87
|
+
res.on('data', (chunk) => {
|
|
88
|
+
data += chunk
|
|
89
|
+
})
|
|
90
|
+
res.on('end', () => resolve(data))
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
req.on('error', reject)
|
|
94
|
+
req.end()
|
|
95
|
+
})
|
|
75
96
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
97
|
+
// Should return something (admin UI or health endpoint)
|
|
98
|
+
expect(body.length).toBeGreaterThan(0)
|
|
99
|
+
},
|
|
100
|
+
30000,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
fullTest(
|
|
104
|
+
'camstack agent boots and stays alive',
|
|
105
|
+
async () => {
|
|
106
|
+
const proc = fork(
|
|
107
|
+
CLI_PATH,
|
|
108
|
+
['agent', '--hub', '127.0.0.1', '--token', 'test_token', '--port', String(randomPort())],
|
|
109
|
+
{
|
|
110
|
+
stdio: 'pipe',
|
|
111
|
+
env: {
|
|
112
|
+
...process.env,
|
|
113
|
+
CAMSTACK_AGENT_NAME: 'test-agent',
|
|
114
|
+
CAMSTACK_DATA: TEST_DATA_DIR,
|
|
115
|
+
},
|
|
84
116
|
},
|
|
85
117
|
)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// Process should still be alive (not crashed)
|
|
111
|
-
expect(proc.exitCode).toBeNull()
|
|
112
|
-
}, 15000)
|
|
113
|
-
|
|
114
|
-
it.skipIf(!CLI_EXISTS)('camstack info prints version and platform', async () => {
|
|
115
|
-
const output = await new Promise<string>((resolve, reject) => {
|
|
116
|
-
let stdout = ''
|
|
117
|
-
const proc = fork(CLI_PATH, ['info'], { stdio: 'pipe' })
|
|
118
|
-
proc.stdout?.on('data', (chunk) => { stdout += chunk })
|
|
119
|
-
proc.on('exit', (code) => {
|
|
120
|
-
if (code === 0) resolve(stdout)
|
|
121
|
-
else reject(new Error(`Exit code ${code}`))
|
|
118
|
+
processes.push(proc)
|
|
119
|
+
|
|
120
|
+
// Agent should start even if hub is unreachable (retries in background)
|
|
121
|
+
await new Promise((r) => setTimeout(r, 5000))
|
|
122
|
+
|
|
123
|
+
// Process should still be alive (not crashed)
|
|
124
|
+
expect(proc.exitCode).toBeNull()
|
|
125
|
+
},
|
|
126
|
+
15000,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
it.skipIf(!CLI_EXISTS)(
|
|
130
|
+
'camstack info prints version and platform',
|
|
131
|
+
async () => {
|
|
132
|
+
const output = await new Promise<string>((resolve, reject) => {
|
|
133
|
+
let stdout = ''
|
|
134
|
+
const proc = fork(CLI_PATH, ['info'], { stdio: 'pipe' })
|
|
135
|
+
proc.stdout?.on('data', (chunk) => {
|
|
136
|
+
stdout += chunk
|
|
137
|
+
})
|
|
138
|
+
proc.on('exit', (code) => {
|
|
139
|
+
if (code === 0) resolve(stdout)
|
|
140
|
+
else reject(new Error(`Exit code ${code}`))
|
|
141
|
+
})
|
|
122
142
|
})
|
|
123
|
-
})
|
|
124
143
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
144
|
+
expect(output).toContain('camstack v')
|
|
145
|
+
expect(output).toContain('Platform:')
|
|
146
|
+
expect(output).toContain('Node.js:')
|
|
147
|
+
},
|
|
148
|
+
10000,
|
|
149
|
+
)
|
|
129
150
|
})
|
|
@@ -83,7 +83,9 @@ describe('buildCoreCapService', () => {
|
|
|
83
83
|
broker.createService(buildCoreCapService(asAppRouter(makeRouter())))
|
|
84
84
|
await broker.start()
|
|
85
85
|
|
|
86
|
-
const result = await broker.call(`${CORE_CAP_SERVICE_NAME}.system.setRetentionConfig`, {
|
|
86
|
+
const result = await broker.call(`${CORE_CAP_SERVICE_NAME}.system.setRetentionConfig`, {
|
|
87
|
+
days: 30,
|
|
88
|
+
})
|
|
87
89
|
expect(result).toEqual({ days: 30 })
|
|
88
90
|
})
|
|
89
91
|
})
|
|
@@ -7,10 +7,20 @@ describe('dev bootstrap — shm-ring prebuild availability', () => {
|
|
|
7
7
|
const platform = process.platform
|
|
8
8
|
const arch = process.arch
|
|
9
9
|
const triple = `${platform}-${arch}`
|
|
10
|
-
const prebuildsDir = join(
|
|
10
|
+
const prebuildsDir = join(
|
|
11
|
+
__dirname,
|
|
12
|
+
'..',
|
|
13
|
+
'..',
|
|
14
|
+
'..',
|
|
15
|
+
'..',
|
|
16
|
+
'packages',
|
|
17
|
+
'shm-ring',
|
|
18
|
+
'prebuilds',
|
|
19
|
+
triple,
|
|
20
|
+
)
|
|
11
21
|
expect(existsSync(prebuildsDir)).toBe(true)
|
|
12
22
|
const files = readdirSync(prebuildsDir)
|
|
13
|
-
const nodeBinaries = files.filter(f => f.endsWith('.node'))
|
|
23
|
+
const nodeBinaries = files.filter((f) => f.endsWith('.node'))
|
|
14
24
|
expect(nodeBinaries.length).toBeGreaterThan(0)
|
|
15
25
|
})
|
|
16
26
|
|
|
@@ -23,8 +23,11 @@ import { DeviceManagerAddon } from '@camstack/core'
|
|
|
23
23
|
import { CapabilityRegistry, DeviceRegistry } from '@camstack/kernel'
|
|
24
24
|
import { snapshotCapability } from '@camstack/types'
|
|
25
25
|
import type {
|
|
26
|
-
AddonContext,
|
|
27
|
-
|
|
26
|
+
AddonContext,
|
|
27
|
+
ConfigUISchemaWithValues,
|
|
28
|
+
IScopedLogger,
|
|
29
|
+
IEventBus,
|
|
30
|
+
ProviderRegistration,
|
|
28
31
|
IDeviceManagerProvider,
|
|
29
32
|
} from '@camstack/types'
|
|
30
33
|
|
|
@@ -32,8 +35,12 @@ import type {
|
|
|
32
35
|
|
|
33
36
|
function makeLogger(): IScopedLogger {
|
|
34
37
|
const l: IScopedLogger = {
|
|
35
|
-
info: vi.fn(),
|
|
36
|
-
|
|
38
|
+
info: vi.fn(),
|
|
39
|
+
warn: vi.fn(),
|
|
40
|
+
error: vi.fn(),
|
|
41
|
+
debug: vi.fn(),
|
|
42
|
+
child: vi.fn(() => l),
|
|
43
|
+
withTags: vi.fn(() => l),
|
|
37
44
|
}
|
|
38
45
|
return l
|
|
39
46
|
}
|
|
@@ -63,18 +70,30 @@ function makeSnapshotWrapperProvider() {
|
|
|
63
70
|
invalidateCache: vi.fn(async () => undefined),
|
|
64
71
|
|
|
65
72
|
// Contribution methods — owned by the wrapper
|
|
66
|
-
getDeviceSettingsContribution: vi.fn(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
getDeviceSettingsContribution: vi.fn(
|
|
74
|
+
async (_input: { deviceId: number }): Promise<ConfigUISchemaWithValues | null> => ({
|
|
75
|
+
sections: [
|
|
76
|
+
{
|
|
77
|
+
id: 'snapshot-settings',
|
|
78
|
+
title: 'Snapshot',
|
|
79
|
+
tab: 'general',
|
|
80
|
+
order: 99,
|
|
81
|
+
fields: [
|
|
82
|
+
{
|
|
83
|
+
type: 'text' as const,
|
|
84
|
+
key: 'preferredStreamId',
|
|
85
|
+
label: 'Preferred Stream',
|
|
86
|
+
default: '',
|
|
87
|
+
value: '',
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
},
|
|
74
91
|
],
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
getDeviceLiveContribution: vi.fn(
|
|
92
|
+
}),
|
|
93
|
+
),
|
|
94
|
+
getDeviceLiveContribution: vi.fn(
|
|
95
|
+
async (_input: { deviceId: number }): Promise<ConfigUISchemaWithValues | null> => null,
|
|
96
|
+
),
|
|
78
97
|
applyDeviceSettingsPatch: vi.fn(async () => ({ success: true as const })),
|
|
79
98
|
}
|
|
80
99
|
}
|
|
@@ -118,7 +137,9 @@ async function setupBug3Scenario() {
|
|
|
118
137
|
capabilityRegistry.registerProvider(snapshotCapability.name, 'snapshot-addon', wrapperProvider)
|
|
119
138
|
|
|
120
139
|
// Register the default wrapper so auto-bind picks it up in getBindings step 2.
|
|
121
|
-
capabilityRegistry.registerWrapper(snapshotCapability.name, 'snapshot-addon', {
|
|
140
|
+
capabilityRegistry.registerWrapper(snapshotCapability.name, 'snapshot-addon', {
|
|
141
|
+
defaultActive: true,
|
|
142
|
+
})
|
|
122
143
|
|
|
123
144
|
const ctx = {
|
|
124
145
|
id: 'device-manager',
|
|
@@ -140,15 +161,28 @@ async function setupBug3Scenario() {
|
|
|
140
161
|
const provider = providers[0]!.provider as IDeviceManagerProvider
|
|
141
162
|
|
|
142
163
|
// Allocate and register the device.
|
|
143
|
-
const { id: deviceId } = await provider.allocateDeviceId({
|
|
164
|
+
const { id: deviceId } = await provider.allocateDeviceId({
|
|
165
|
+
addonId: 'reolink-addon',
|
|
166
|
+
stableId: 'cam-reolink-1',
|
|
167
|
+
})
|
|
144
168
|
await provider.registerDevice({
|
|
145
|
-
addonId: 'reolink-addon',
|
|
146
|
-
|
|
169
|
+
addonId: 'reolink-addon',
|
|
170
|
+
stableId: 'cam-reolink-1',
|
|
171
|
+
id: deviceId,
|
|
172
|
+
type: 'camera',
|
|
173
|
+
name: 'Reolink Camera',
|
|
174
|
+
parentDeviceId: null,
|
|
175
|
+
config: {},
|
|
147
176
|
})
|
|
148
177
|
|
|
149
178
|
// Register the NATIVE provider for the device — this is what a Reolink/ONVIF
|
|
150
179
|
// driver does. The native does NOT implement contribution methods.
|
|
151
|
-
capabilityRegistry.registerNativeProvider(
|
|
180
|
+
capabilityRegistry.registerNativeProvider(
|
|
181
|
+
snapshotCapability.name,
|
|
182
|
+
deviceId,
|
|
183
|
+
'reolink-addon',
|
|
184
|
+
nativeProvider,
|
|
185
|
+
)
|
|
152
186
|
|
|
153
187
|
capabilityRegistry.ready()
|
|
154
188
|
|
|
@@ -166,7 +200,7 @@ describe('device-manager contribution dispatch — Bug-3 regression (snapshot wr
|
|
|
166
200
|
|
|
167
201
|
expect(aggregate).not.toBeNull()
|
|
168
202
|
// The wrapper's contribution section must be present.
|
|
169
|
-
const snapshotSection = aggregate!.sections.find(s => s.id === 'snapshot-settings')
|
|
203
|
+
const snapshotSection = aggregate!.sections.find((s) => s.id === 'snapshot-settings')
|
|
170
204
|
expect(snapshotSection).toBeDefined()
|
|
171
205
|
expect(snapshotSection!.title).toBe('Snapshot')
|
|
172
206
|
|
|
@@ -204,9 +238,10 @@ describe('device-manager contribution dispatch — Bug-3 regression (snapshot wr
|
|
|
204
238
|
expect(aggregate).not.toBeNull()
|
|
205
239
|
|
|
206
240
|
const field = aggregate!.sections
|
|
207
|
-
.find(s => s.id === 'snapshot-settings')
|
|
208
|
-
?.fields
|
|
209
|
-
|
|
241
|
+
.find((s) => s.id === 'snapshot-settings')
|
|
242
|
+
?.fields.find((f) => (f as Record<string, unknown>)['key'] === 'preferredStreamId') as
|
|
243
|
+
| Record<string, unknown>
|
|
244
|
+
| undefined
|
|
210
245
|
|
|
211
246
|
expect(field).toBeDefined()
|
|
212
247
|
expect(field!['writerCapName']).toBe('snapshot')
|
|
@@ -238,12 +273,8 @@ describe('device-manager contribution dispatch — Bug-3 regression (snapshot wr
|
|
|
238
273
|
const { provider, deviceId } = await setupBug3Scenario()
|
|
239
274
|
|
|
240
275
|
// Must complete without throwing.
|
|
241
|
-
await expect(
|
|
242
|
-
provider.getDeviceSettingsAggregate({ deviceId }),
|
|
243
|
-
).resolves.not.toBeNull()
|
|
276
|
+
await expect(provider.getDeviceSettingsAggregate({ deviceId })).resolves.not.toBeNull()
|
|
244
277
|
|
|
245
|
-
await expect(
|
|
246
|
-
provider.getDeviceLiveInfoAggregate({ deviceId }),
|
|
247
|
-
).resolves.toBeDefined()
|
|
278
|
+
await expect(provider.getDeviceLiveInfoAggregate({ deviceId })).resolves.toBeDefined()
|
|
248
279
|
})
|
|
249
280
|
})
|