@camstack/server 0.1.3
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/.env.example +17 -0
- package/package.json +55 -0
- package/src/__tests__/addon-install-e2e.test.ts +75 -0
- package/src/__tests__/addon-pages-e2e.test.ts +178 -0
- package/src/__tests__/addon-route-session.test.ts +17 -0
- package/src/__tests__/addon-settings-router.spec.ts +62 -0
- package/src/__tests__/addon-upload.spec.ts +355 -0
- package/src/__tests__/agent-registry.spec.ts +162 -0
- package/src/__tests__/agent-status-page.spec.ts +84 -0
- package/src/__tests__/auth-session-cookie.test.ts +21 -0
- package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +23 -0
- package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +64 -0
- package/src/__tests__/cap-routers/_meta.spec.ts +200 -0
- package/src/__tests__/cap-routers/addon-settings.router.spec.ts +106 -0
- package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +142 -0
- package/src/__tests__/cap-routers/harness.ts +159 -0
- package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +119 -0
- package/src/__tests__/cap-routers/null-provider-guard.spec.ts +66 -0
- package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +135 -0
- package/src/__tests__/cap-routers/settings-store.router.spec.ts +247 -0
- package/src/__tests__/capability-e2e.test.ts +386 -0
- package/src/__tests__/cli-e2e.test.ts +129 -0
- package/src/__tests__/core-cap-bridge.spec.ts +89 -0
- package/src/__tests__/embedded-deps-e2e.test.ts +109 -0
- package/src/__tests__/event-bus-proxy-router.spec.ts +72 -0
- package/src/__tests__/fixtures/mock-analysis-addon-a.ts +37 -0
- package/src/__tests__/fixtures/mock-analysis-addon-b.ts +37 -0
- package/src/__tests__/fixtures/mock-log-addon.ts +37 -0
- package/src/__tests__/fixtures/mock-storage-addon.ts +40 -0
- package/src/__tests__/framework-allowlist.spec.ts +95 -0
- package/src/__tests__/https-e2e.test.ts +118 -0
- package/src/__tests__/lifecycle-e2e.test.ts +140 -0
- package/src/__tests__/live-events-subscription.spec.ts +150 -0
- package/src/__tests__/moleculer-register-node-idempotency.spec.ts +229 -0
- package/src/__tests__/oauth2-account-linking.spec.ts +736 -0
- package/src/__tests__/post-boot-restart.spec.ts +161 -0
- package/src/__tests__/singleton-contention.test.ts +487 -0
- package/src/__tests__/streaming-diagnostic.test.ts +512 -0
- package/src/__tests__/streaming-scale.test.ts +280 -0
- package/src/agent-status-page.ts +121 -0
- package/src/api/__tests__/addons-custom.spec.ts +134 -0
- package/src/api/__tests__/capabilities.router.test.ts +47 -0
- package/src/api/addon-upload.ts +472 -0
- package/src/api/addons-custom.router.ts +100 -0
- package/src/api/auth-whoami.ts +99 -0
- package/src/api/bridge-addons.router.ts +120 -0
- package/src/api/capabilities.router.ts +226 -0
- package/src/api/core/__tests__/auth-router-totp.spec.ts +256 -0
- package/src/api/core/addon-settings.router.ts +124 -0
- package/src/api/core/agents.router.ts +87 -0
- package/src/api/core/auth.router.ts +303 -0
- package/src/api/core/cap-providers.ts +993 -0
- package/src/api/core/capabilities.router.ts +119 -0
- package/src/api/core/collection-preference.ts +40 -0
- package/src/api/core/event-bus-proxy.router.ts +45 -0
- package/src/api/core/hwaccel.router.ts +81 -0
- package/src/api/core/live-events.router.ts +60 -0
- package/src/api/core/logs.router.ts +162 -0
- package/src/api/core/notifications.router.ts +65 -0
- package/src/api/core/repl.router.ts +41 -0
- package/src/api/core/settings-backend.router.ts +142 -0
- package/src/api/core/stream-probe.router.ts +57 -0
- package/src/api/core/system-events.router.ts +116 -0
- package/src/api/health/health.routes.ts +123 -0
- package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +52 -0
- package/src/api/oauth2/consent-page.ts +42 -0
- package/src/api/oauth2/oauth2-routes.ts +248 -0
- package/src/api/trpc/__tests__/scope-access-device.spec.ts +223 -0
- package/src/api/trpc/__tests__/scope-access.spec.ts +107 -0
- package/src/api/trpc/cap-mount-helpers.ts +225 -0
- package/src/api/trpc/core-cap-bridge.ts +152 -0
- package/src/api/trpc/generated-cap-mounts.ts +707 -0
- package/src/api/trpc/generated-cap-routers.ts +6340 -0
- package/src/api/trpc/scope-access.ts +110 -0
- package/src/api/trpc/trpc.context.ts +255 -0
- package/src/api/trpc/trpc.middleware.ts +140 -0
- package/src/api/trpc/trpc.router.ts +275 -0
- package/src/auth/session-cookie.ts +44 -0
- package/src/boot/boot-config.ts +278 -0
- package/src/boot/post-boot.service.ts +103 -0
- package/src/core/addon/__tests__/addon-registry-capability.test.ts +53 -0
- package/src/core/addon/addon-package.service.ts +1684 -0
- package/src/core/addon/addon-registry.service.ts +2926 -0
- package/src/core/addon/addon-search.service.ts +90 -0
- package/src/core/addon/addon-settings-provider.ts +276 -0
- package/src/core/addon/addon.tokens.ts +2 -0
- package/src/core/addon-bridge/addon-bridge.service.ts +125 -0
- package/src/core/addon-pages/addon-pages.service.spec.ts +117 -0
- package/src/core/addon-pages/addon-pages.service.ts +80 -0
- package/src/core/addon-widgets/addon-widgets.service.ts +92 -0
- package/src/core/agent/agent-registry.service.ts +507 -0
- package/src/core/auth/auth.service.spec.ts +88 -0
- package/src/core/auth/auth.service.ts +8 -0
- package/src/core/capability/capability.service.ts +57 -0
- package/src/core/config/config.schema.ts +3 -0
- package/src/core/config/config.service.spec.ts +175 -0
- package/src/core/config/config.service.ts +7 -0
- package/src/core/events/event-bus.service.spec.ts +212 -0
- package/src/core/events/event-bus.service.ts +85 -0
- package/src/core/feature/feature.service.spec.ts +96 -0
- package/src/core/feature/feature.service.ts +8 -0
- package/src/core/lifecycle/lifecycle-state-machine.spec.ts +168 -0
- package/src/core/lifecycle/lifecycle-state-machine.ts +3 -0
- package/src/core/logging/log-ring-buffer.ts +3 -0
- package/src/core/logging/logging.service.spec.ts +247 -0
- package/src/core/logging/logging.service.ts +129 -0
- package/src/core/logging/scoped-logger.ts +3 -0
- package/src/core/moleculer/moleculer.service.ts +612 -0
- package/src/core/network/network-quality.service.spec.ts +47 -0
- package/src/core/network/network-quality.service.ts +5 -0
- package/src/core/notification/notification-wrapper.service.ts +36 -0
- package/src/core/notification/toast-wrapper.service.ts +31 -0
- package/src/core/provider/provider.tokens.ts +1 -0
- package/src/core/repl/repl-engine.service.spec.ts +417 -0
- package/src/core/repl/repl-engine.service.ts +156 -0
- package/src/core/storage/fs-storage-backend.spec.ts +70 -0
- package/src/core/storage/fs-storage-backend.ts +3 -0
- package/src/core/storage/settings-store.spec.ts +213 -0
- package/src/core/storage/settings-store.ts +2 -0
- package/src/core/storage/sql-schema.spec.ts +140 -0
- package/src/core/storage/sql-schema.ts +3 -0
- package/src/core/storage/storage-location-manager.spec.ts +121 -0
- package/src/core/storage/storage-location-manager.ts +3 -0
- package/src/core/storage/storage.service.spec.ts +73 -0
- package/src/core/storage/storage.service.ts +3 -0
- package/src/core/streaming/stream-probe.service.ts +212 -0
- package/src/core/topology/topology-emitter.service.ts +101 -0
- package/src/launcher.ts +309 -0
- package/src/main.ts +1049 -0
- package/src/manual-boot.ts +322 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +26 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Example spec exercising the codegen'd metrics-provider router end-to-end:
|
|
4
|
+
* - Wires a mock provider into `createCapRouter_metricsProvider`
|
|
5
|
+
* - Verifies happy-path query dispatch + output validation for getCurrent()
|
|
6
|
+
* - Verifies provider-missing → PRECONDITION_FAILED
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect } from 'vitest'
|
|
9
|
+
import { createCapRouter_metricsProvider } from '../../api/trpc/generated-cap-routers.js'
|
|
10
|
+
import { type IMetricsProvider } from '@camstack/types'
|
|
11
|
+
import { makeCtx, invokeProcedure } from './harness.js'
|
|
12
|
+
|
|
13
|
+
function makeMockProvider(overrides: Partial<IMetricsProvider> = {}): IMetricsProvider {
|
|
14
|
+
return {
|
|
15
|
+
collectSnapshot: async () => ({
|
|
16
|
+
cpu: { total: 0, user: 0, system: 0, irq: 0, nice: 0, loadAvg: [0, 0, 0], cores: 1 },
|
|
17
|
+
memory: { percent: 0, totalBytes: 0, usedBytes: 0, availableBytes: 0, swapUsedBytes: 0, swapTotalBytes: 0 },
|
|
18
|
+
gpu: null,
|
|
19
|
+
network: { rxBytes: 0, txBytes: 0, rxPackets: 0, txPackets: 0, rxErrors: 0, txErrors: 0, timestampMs: 0 },
|
|
20
|
+
disk: { readBytes: 0, writeBytes: 0, readOps: 0, writeOps: 0, timestampMs: 0 },
|
|
21
|
+
pressure: { cpu: null, memory: null, io: null },
|
|
22
|
+
process: { openFds: 0, threadCount: 0, activeHandles: 0, activeRequests: 0 },
|
|
23
|
+
cpuTemperature: null,
|
|
24
|
+
timestampMs: 0,
|
|
25
|
+
}),
|
|
26
|
+
getCached: async () => null,
|
|
27
|
+
getCurrent: async () => ({
|
|
28
|
+
cpuPercent: 42.5,
|
|
29
|
+
memoryPercent: 68.2,
|
|
30
|
+
memoryUsedMB: 8192,
|
|
31
|
+
memoryTotalMB: 16384,
|
|
32
|
+
diskPercent: 55.1,
|
|
33
|
+
temperature: 48.0,
|
|
34
|
+
}),
|
|
35
|
+
getDiskSpace: async ({ dirPath }) => ({
|
|
36
|
+
path: dirPath,
|
|
37
|
+
totalBytes: 0,
|
|
38
|
+
usedBytes: 0,
|
|
39
|
+
availableBytes: 0,
|
|
40
|
+
percent: 0,
|
|
41
|
+
}),
|
|
42
|
+
getGpuInfo: async () => null,
|
|
43
|
+
getCpuTemperature: async () => null,
|
|
44
|
+
getProcessStats: async () => [],
|
|
45
|
+
...overrides,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('createCapRouter_metricsProvider', () => {
|
|
50
|
+
describe('happy path', () => {
|
|
51
|
+
it('dispatches getCurrent() to the provider and returns typed metrics', async () => {
|
|
52
|
+
const provider = makeMockProvider()
|
|
53
|
+
const router = createCapRouter_metricsProvider(() => provider)
|
|
54
|
+
|
|
55
|
+
const outcome = await invokeProcedure(router, 'getCurrent', makeCtx('admin'))
|
|
56
|
+
|
|
57
|
+
expect(outcome.ok).toBe(true)
|
|
58
|
+
if (outcome.ok) {
|
|
59
|
+
expect(outcome.value).toMatchObject({
|
|
60
|
+
cpuPercent: 42.5,
|
|
61
|
+
memoryPercent: 68.2,
|
|
62
|
+
memoryUsedMB: 8192,
|
|
63
|
+
memoryTotalMB: 16384,
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('propagates optional fields (temperature, gpuPercent)', async () => {
|
|
69
|
+
const router = createCapRouter_metricsProvider(() =>
|
|
70
|
+
makeMockProvider({
|
|
71
|
+
getCurrent: async () => ({
|
|
72
|
+
cpuPercent: 10,
|
|
73
|
+
memoryPercent: 20,
|
|
74
|
+
memoryUsedMB: 1024,
|
|
75
|
+
memoryTotalMB: 8192,
|
|
76
|
+
gpuPercent: 75,
|
|
77
|
+
gpuMemoryPercent: 60,
|
|
78
|
+
}),
|
|
79
|
+
}),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
const outcome = await invokeProcedure(router, 'getCurrent', makeCtx('admin'))
|
|
83
|
+
|
|
84
|
+
expect(outcome.ok).toBe(true)
|
|
85
|
+
if (outcome.ok) {
|
|
86
|
+
const value = outcome.value as { gpuPercent?: number; gpuMemoryPercent?: number }
|
|
87
|
+
expect(value.gpuPercent).toBe(75)
|
|
88
|
+
expect(value.gpuMemoryPercent).toBe(60)
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('auth', () => {
|
|
94
|
+
it('rejects anonymous (protected endpoint)', async () => {
|
|
95
|
+
const router = createCapRouter_metricsProvider(() => makeMockProvider())
|
|
96
|
+
const outcome = await invokeProcedure(router, 'getCurrent', makeCtx('anonymous'))
|
|
97
|
+
expect(outcome.ok).toBe(false)
|
|
98
|
+
if (!outcome.ok) expect(outcome.code).toBe('UNAUTHORIZED')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('allows viewer (protected endpoint)', async () => {
|
|
102
|
+
const router = createCapRouter_metricsProvider(() => makeMockProvider())
|
|
103
|
+
const outcome = await invokeProcedure(router, 'getCurrent', makeCtx('user'))
|
|
104
|
+
expect(outcome.ok).toBe(true)
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
describe('missing provider', () => {
|
|
109
|
+
it('throws PRECONDITION_FAILED when getProvider returns null', async () => {
|
|
110
|
+
const router = createCapRouter_metricsProvider(() => null)
|
|
111
|
+
const outcome = await invokeProcedure(router, 'getCurrent', makeCtx('admin'))
|
|
112
|
+
expect(outcome.ok).toBe(false)
|
|
113
|
+
if (!outcome.ok) {
|
|
114
|
+
expect(outcome.code).toBe('PRECONDITION_FAILED')
|
|
115
|
+
expect(outcome.message).toContain('metrics-provider')
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comprehensive null-provider spec — verifies that EVERY codegen'd cap
|
|
3
|
+
* router returns PRECONDITION_FAILED when the provider is null.
|
|
4
|
+
*
|
|
5
|
+
* This is a safety net: if a new cap is added and the codegen'd router
|
|
6
|
+
* doesn't guard against a null provider, this test catches it.
|
|
7
|
+
*
|
|
8
|
+
* NOTE: some caps have methods that require non-void input (z.object({...})).
|
|
9
|
+
* For those, Zod input validation may reject our empty input before the
|
|
10
|
+
* null-provider check runs — in which case we expect BAD_REQUEST or
|
|
11
|
+
* PRECONDITION_FAILED (both are acceptable: the point is that the call
|
|
12
|
+
* does NOT succeed and return garbage data).
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, expect } from 'vitest'
|
|
15
|
+
import * as generatedModule from '../../api/trpc/generated-cap-routers.js'
|
|
16
|
+
import { makeCtx, invokeProcedure } from './harness.js'
|
|
17
|
+
|
|
18
|
+
// Discover all createCapRouter_* functions from the generated module
|
|
19
|
+
const routerFactories: Array<{ name: string; factory: (getProvider: () => null) => unknown }> = []
|
|
20
|
+
for (const [key, value] of Object.entries(generatedModule)) {
|
|
21
|
+
if (key.startsWith('createCapRouter_') && typeof value === 'function') {
|
|
22
|
+
routerFactories.push({
|
|
23
|
+
name: key.replace('createCapRouter_', ''),
|
|
24
|
+
factory: value as (getProvider: () => null) => unknown,
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('null-provider guard — all capabilities', () => {
|
|
30
|
+
it(`discovers ${routerFactories.length} cap router factories`, () => {
|
|
31
|
+
expect(routerFactories.length).toBeGreaterThanOrEqual(30)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
for (const { name, factory } of routerFactories) {
|
|
35
|
+
it(`${name}: null provider does not return success`, async () => {
|
|
36
|
+
const router = factory(() => null)
|
|
37
|
+
|
|
38
|
+
// Find the first method on the router's caller
|
|
39
|
+
const caller = (router as { createCaller: (ctx: unknown) => Record<string, unknown> })
|
|
40
|
+
.createCaller(makeCtx('admin'))
|
|
41
|
+
|
|
42
|
+
const methods = Object.keys(caller).filter(
|
|
43
|
+
k => typeof caller[k] === 'function' && !k.startsWith('_'),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if (methods.length === 0) return // No methods — skip
|
|
47
|
+
|
|
48
|
+
// Try the first method with no input (will work for void-input methods)
|
|
49
|
+
const firstMethod = methods[0]!
|
|
50
|
+
const result = await invokeProcedure(
|
|
51
|
+
router as { createCaller: (ctx: unknown) => Record<string, (input?: unknown) => unknown> },
|
|
52
|
+
firstMethod,
|
|
53
|
+
makeCtx('admin'),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
// The call should NOT succeed — either PRECONDITION_FAILED (null provider
|
|
57
|
+
// caught by codegen guard) or BAD_REQUEST (input validation before guard)
|
|
58
|
+
// or INTERNAL_SERVER_ERROR (provider method call on null).
|
|
59
|
+
// Any of these is acceptable — the important thing is ok !== true.
|
|
60
|
+
expect(
|
|
61
|
+
result.ok,
|
|
62
|
+
`${name}.${firstMethod} returned success with null provider!`,
|
|
63
|
+
).toBe(false)
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
})
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Router-level spec for `pipelineExecutor` — covers the reference-media
|
|
4
|
+
* and audio-capabilities endpoints that remain on the cap after the
|
|
5
|
+
* benchmark methods moved to `@camstack/addon-benchmark` (2026-04-14,
|
|
6
|
+
* dispatched via `api.addons.custom`). The full benchmark E2E surface
|
|
7
|
+
* is now tested inside the benchmark addon's own spec files.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect } from 'vitest'
|
|
10
|
+
import { createCapRouter_pipelineExecutor } from '../../api/trpc/generated-cap-routers.js'
|
|
11
|
+
import type { IPipelineExecutorProvider } from '@camstack/types'
|
|
12
|
+
import { makeCtx, invokeProcedure } from './harness.js'
|
|
13
|
+
|
|
14
|
+
function makeMockProvider(overrides: Partial<IPipelineExecutorProvider> = {}): IPipelineExecutorProvider {
|
|
15
|
+
const defaultEngine = { runtime: 'node' as const, backend: 'cpu', format: 'onnx' as const }
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
getAvailableEngines: () => [defaultEngine],
|
|
19
|
+
getSelectedEngine: () => defaultEngine,
|
|
20
|
+
setEngine: async () => ({ steps: [] }),
|
|
21
|
+
getDefaultSteps: async () => [],
|
|
22
|
+
getSchema: async () => ({
|
|
23
|
+
availableEngines: [{ engine: defaultEngine, devices: [{ id: 'cpu', label: 'CPU' }], defaultDevice: 'cpu' }],
|
|
24
|
+
selectedEngine: defaultEngine,
|
|
25
|
+
slots: [],
|
|
26
|
+
}),
|
|
27
|
+
getGlobalSteps: async () => null,
|
|
28
|
+
setGlobalSteps: () => {},
|
|
29
|
+
getGlobalPipelineConfig: () => null,
|
|
30
|
+
getOrchestratorConfigSchema: () => ({ sections: [] }),
|
|
31
|
+
getOrchestratorSettings: () => ({
|
|
32
|
+
motionFps: 4, detectionFps: 10, cooldownMs: 10000, maxConcurrentInferences: null,
|
|
33
|
+
}),
|
|
34
|
+
setOrchestratorSettings: () => {},
|
|
35
|
+
listTemplates: () => [],
|
|
36
|
+
saveTemplate: () => ({
|
|
37
|
+
id: 'tpl-1', name: 'x', createdAt: '', updatedAt: '', engine: defaultEngine, steps: [],
|
|
38
|
+
}),
|
|
39
|
+
updateTemplate: () => ({
|
|
40
|
+
id: 'tpl-1', name: 'x', createdAt: '', updatedAt: '', engine: defaultEngine, steps: [],
|
|
41
|
+
}),
|
|
42
|
+
deleteTemplate: () => {},
|
|
43
|
+
getCapabilities: async () => {
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- test mock: `never` is uninhabitable so `as never` is the only option
|
|
45
|
+
const platform: never = { name: 'test', cpuCores: 4 } as never
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- test mock: `never` is uninhabitable so `as never` is the only option
|
|
47
|
+
const runtimes: never = {} as never
|
|
48
|
+
return { platform, addons: [], runtimes }
|
|
49
|
+
},
|
|
50
|
+
detect: async () => ({ detections: [], inferenceMs: 0, modelId: 'yolov9t' }),
|
|
51
|
+
downloadModel: async () => ({ filePath: '', sizeMB: 0, durationMs: 0 }),
|
|
52
|
+
deleteModel: async () => ({ success: true as const }),
|
|
53
|
+
runPipeline: async () => ({
|
|
54
|
+
detections: [],
|
|
55
|
+
stepTimings: [],
|
|
56
|
+
totalMs: 42,
|
|
57
|
+
imageWidth: 640,
|
|
58
|
+
imageHeight: 480,
|
|
59
|
+
}),
|
|
60
|
+
runFrame: async () => null,
|
|
61
|
+
listReferenceImages: () => [],
|
|
62
|
+
getReferenceImage: () => null,
|
|
63
|
+
getReferenceAudioFiles: () => [{ filename: 'car.wav', sizeKb: 12 }],
|
|
64
|
+
getReferenceAudio: () => ({ base64: 'AAAA' }),
|
|
65
|
+
getAudioCapabilities: () => ({
|
|
66
|
+
activeBackend: 'yamnet-onnx',
|
|
67
|
+
availableBackends: [{ id: 'yamnet-onnx', name: 'YAMNet ONNX', description: '', available: true }],
|
|
68
|
+
sampleRate: 16000,
|
|
69
|
+
chunkDurationMs: 500,
|
|
70
|
+
}),
|
|
71
|
+
getAddonResolver: () => ({ resolve: async () => { throw new Error('nop') }, shutdownAll: async () => {} }),
|
|
72
|
+
orchestratorStatus: () => null,
|
|
73
|
+
cameraDetectionStatus: () => null,
|
|
74
|
+
getDevicePipelineSteps: () => null,
|
|
75
|
+
setDevicePipelineSteps: async () => {},
|
|
76
|
+
clearDevicePipelineSteps: async () => {},
|
|
77
|
+
...overrides,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe('createCapRouter_pipelineExecutor — reference + audio endpoints', () => {
|
|
82
|
+
describe('listReferenceImages', () => {
|
|
83
|
+
it('delegates to provider.listReferenceImages', async () => {
|
|
84
|
+
const router = createCapRouter_pipelineExecutor(() =>
|
|
85
|
+
makeMockProvider({
|
|
86
|
+
listReferenceImages: () => [
|
|
87
|
+
{ id: 'persons-cars-animal.jpg', filename: 'persons-cars-animal.jpg', sizeKB: 120 },
|
|
88
|
+
] as never,
|
|
89
|
+
}),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const outcome = await invokeProcedure(router, 'listReferenceImages', makeCtx('admin'))
|
|
93
|
+
expect(outcome.ok).toBe(true)
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('getAudioCapabilities', () => {
|
|
98
|
+
it('returns backend list from provider', async () => {
|
|
99
|
+
const router = createCapRouter_pipelineExecutor(() => makeMockProvider())
|
|
100
|
+
const outcome = await invokeProcedure(router, 'getAudioCapabilities', makeCtx('admin'))
|
|
101
|
+
expect(outcome.ok).toBe(true)
|
|
102
|
+
if (outcome.ok) {
|
|
103
|
+
const caps = outcome.value as { activeBackend: string; sampleRate: number }
|
|
104
|
+
expect(caps.activeBackend).toBe('yamnet-onnx')
|
|
105
|
+
expect(caps.sampleRate).toBe(16000)
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe('missing provider', () => {
|
|
111
|
+
it('throws PRECONDITION_FAILED when getProvider returns null', async () => {
|
|
112
|
+
const router = createCapRouter_pipelineExecutor(() => null)
|
|
113
|
+
const outcome = await invokeProcedure(router, 'listReferenceImages', makeCtx('admin'))
|
|
114
|
+
expect(outcome.ok).toBe(false)
|
|
115
|
+
if (!outcome.ok) {
|
|
116
|
+
expect(outcome.code).toBe('PRECONDITION_FAILED')
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe('auth', () => {
|
|
122
|
+
it('rejects anonymous on listReferenceImages (protected query)', async () => {
|
|
123
|
+
const router = createCapRouter_pipelineExecutor(() => makeMockProvider())
|
|
124
|
+
const outcome = await invokeProcedure(router, 'listReferenceImages', makeCtx('anonymous'))
|
|
125
|
+
expect(outcome.ok).toBe(false)
|
|
126
|
+
if (!outcome.ok) expect(outcome.code).toBe('UNAUTHORIZED')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('allows viewer to read reference images (protected query)', async () => {
|
|
130
|
+
const router = createCapRouter_pipelineExecutor(() => makeMockProvider())
|
|
131
|
+
const outcome = await invokeProcedure(router, 'listReferenceImages', makeCtx('user'))
|
|
132
|
+
expect(outcome.ok).toBe(true)
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
})
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Spec for the codegen'd `settings-store` capability router.
|
|
4
|
+
*
|
|
5
|
+
* Exercises:
|
|
6
|
+
* - All 8 methods (get, set, query, insert, update, delete, count, isEmpty)
|
|
7
|
+
* - Auth enforcement (all methods are protected)
|
|
8
|
+
* - Missing provider → PRECONDITION_FAILED
|
|
9
|
+
* - Namespace passthrough (optional namespace field)
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
12
|
+
import { createCapRouter_settingsStore } from '../../api/trpc/generated-cap-routers.js'
|
|
13
|
+
import type { ISettingsStoreProvider } from '@camstack/types'
|
|
14
|
+
import { makeCtx, invokeProcedure } from './harness.js'
|
|
15
|
+
|
|
16
|
+
interface MockRecord {
|
|
17
|
+
readonly id: string
|
|
18
|
+
readonly data: Record<string, unknown>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface MockStore {
|
|
22
|
+
records: Map<string, MockRecord>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function makeMockProvider(): ISettingsStoreProvider & { _store: MockStore } {
|
|
26
|
+
const store: MockStore = { records: new Map() }
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
_store: store,
|
|
30
|
+
|
|
31
|
+
async get(input) {
|
|
32
|
+
const record = store.records.get(input.key)
|
|
33
|
+
return record?.data ?? null
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
async set(input) {
|
|
37
|
+
store.records.set(input.key, { id: input.key, data: input.value as Record<string, unknown> ?? {} })
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
async query(input) {
|
|
41
|
+
const results = [...store.records.values()]
|
|
42
|
+
if (input.filter?.limit) return results.slice(0, input.filter.limit)
|
|
43
|
+
return results
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
async insert(input) {
|
|
47
|
+
store.records.set(input.record.id, input.record)
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
async update(input) {
|
|
51
|
+
const existing = store.records.get(input.id)
|
|
52
|
+
if (existing) {
|
|
53
|
+
store.records.set(input.id, { id: input.id, data: { ...existing.data, ...input.data } })
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
async delete(input) {
|
|
58
|
+
store.records.delete(input.key)
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
async count() {
|
|
62
|
+
return store.records.size
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
async isEmpty() {
|
|
66
|
+
return store.records.size === 0
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe('createCapRouter_settingsStore', () => {
|
|
72
|
+
let provider: ReturnType<typeof makeMockProvider>
|
|
73
|
+
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
provider = makeMockProvider()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// ── Happy path: all 8 methods ──────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
describe('get', () => {
|
|
81
|
+
it('returns stored value by key', async () => {
|
|
82
|
+
provider._store.records.set('myKey', { id: 'myKey', data: { foo: 'bar' } })
|
|
83
|
+
const router = createCapRouter_settingsStore(() => provider)
|
|
84
|
+
const result = await invokeProcedure(router, 'get', makeCtx('admin'), { collection: 'test', key: 'myKey' })
|
|
85
|
+
expect(result.ok).toBe(true)
|
|
86
|
+
if (result.ok) expect(result.value).toMatchObject({ foo: 'bar' })
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('returns null for missing key', async () => {
|
|
90
|
+
const router = createCapRouter_settingsStore(() => provider)
|
|
91
|
+
const result = await invokeProcedure(router, 'get', makeCtx('admin'), { collection: 'test', key: 'nope' })
|
|
92
|
+
expect(result.ok).toBe(true)
|
|
93
|
+
if (result.ok) expect(result.value).toBeNull()
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('set', () => {
|
|
98
|
+
it('upserts a value', async () => {
|
|
99
|
+
const router = createCapRouter_settingsStore(() => provider)
|
|
100
|
+
const result = await invokeProcedure(router, 'set', makeCtx('admin'), { collection: 'test', key: 'k1', value: { x: 1 } })
|
|
101
|
+
expect(result.ok).toBe(true)
|
|
102
|
+
expect(provider._store.records.has('k1')).toBe(true)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe('query', () => {
|
|
107
|
+
it('returns all records', async () => {
|
|
108
|
+
provider._store.records.set('a', { id: 'a', data: { v: 1 } })
|
|
109
|
+
provider._store.records.set('b', { id: 'b', data: { v: 2 } })
|
|
110
|
+
const router = createCapRouter_settingsStore(() => provider)
|
|
111
|
+
const result = await invokeProcedure(router, 'query', makeCtx('admin'), { collection: 'test' })
|
|
112
|
+
expect(result.ok).toBe(true)
|
|
113
|
+
if (result.ok) expect(result.value).toHaveLength(2)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('respects filter.limit', async () => {
|
|
117
|
+
provider._store.records.set('a', { id: 'a', data: {} })
|
|
118
|
+
provider._store.records.set('b', { id: 'b', data: {} })
|
|
119
|
+
provider._store.records.set('c', { id: 'c', data: {} })
|
|
120
|
+
const router = createCapRouter_settingsStore(() => provider)
|
|
121
|
+
const result = await invokeProcedure(router, 'query', makeCtx('admin'), { collection: 'test', filter: { limit: 2 } })
|
|
122
|
+
expect(result.ok).toBe(true)
|
|
123
|
+
if (result.ok) expect(result.value).toHaveLength(2)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('insert', () => {
|
|
128
|
+
it('adds a record', async () => {
|
|
129
|
+
const router = createCapRouter_settingsStore(() => provider)
|
|
130
|
+
const result = await invokeProcedure(router, 'insert', makeCtx('admin'), {
|
|
131
|
+
collection: 'test',
|
|
132
|
+
record: { id: 'r1', data: { name: 'test' } },
|
|
133
|
+
})
|
|
134
|
+
expect(result.ok).toBe(true)
|
|
135
|
+
expect(provider._store.records.get('r1')?.data).toMatchObject({ name: 'test' })
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe('update', () => {
|
|
140
|
+
it('merges data into existing record', async () => {
|
|
141
|
+
provider._store.records.set('r1', { id: 'r1', data: { a: 1, b: 2 } })
|
|
142
|
+
const router = createCapRouter_settingsStore(() => provider)
|
|
143
|
+
const result = await invokeProcedure(router, 'update', makeCtx('admin'), {
|
|
144
|
+
collection: 'test',
|
|
145
|
+
id: 'r1',
|
|
146
|
+
data: { b: 99 },
|
|
147
|
+
})
|
|
148
|
+
expect(result.ok).toBe(true)
|
|
149
|
+
expect(provider._store.records.get('r1')?.data).toMatchObject({ a: 1, b: 99 })
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe('delete', () => {
|
|
154
|
+
it('removes a record', async () => {
|
|
155
|
+
provider._store.records.set('r1', { id: 'r1', data: {} })
|
|
156
|
+
const router = createCapRouter_settingsStore(() => provider)
|
|
157
|
+
const result = await invokeProcedure(router, 'delete', makeCtx('admin'), { collection: 'test', key: 'r1' })
|
|
158
|
+
expect(result.ok).toBe(true)
|
|
159
|
+
expect(provider._store.records.has('r1')).toBe(false)
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
describe('count', () => {
|
|
164
|
+
it('returns record count', async () => {
|
|
165
|
+
provider._store.records.set('a', { id: 'a', data: {} })
|
|
166
|
+
provider._store.records.set('b', { id: 'b', data: {} })
|
|
167
|
+
const router = createCapRouter_settingsStore(() => provider)
|
|
168
|
+
const result = await invokeProcedure(router, 'count', makeCtx('admin'), { collection: 'test' })
|
|
169
|
+
expect(result.ok).toBe(true)
|
|
170
|
+
if (result.ok) expect(result.value).toBe(2)
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
describe('isEmpty', () => {
|
|
175
|
+
it('returns true when empty', async () => {
|
|
176
|
+
const router = createCapRouter_settingsStore(() => provider)
|
|
177
|
+
const result = await invokeProcedure(router, 'isEmpty', makeCtx('admin'), { collection: 'test' })
|
|
178
|
+
expect(result.ok).toBe(true)
|
|
179
|
+
if (result.ok) expect(result.value).toBe(true)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('returns false when records exist', async () => {
|
|
183
|
+
provider._store.records.set('x', { id: 'x', data: {} })
|
|
184
|
+
const router = createCapRouter_settingsStore(() => provider)
|
|
185
|
+
const result = await invokeProcedure(router, 'isEmpty', makeCtx('admin'), { collection: 'test' })
|
|
186
|
+
expect(result.ok).toBe(true)
|
|
187
|
+
if (result.ok) expect(result.value).toBe(false)
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
// ── Namespace passthrough ──────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
describe('namespace', () => {
|
|
194
|
+
it('passes namespace to provider (optional field)', async () => {
|
|
195
|
+
let capturedNamespace: string | undefined
|
|
196
|
+
const nsProvider = makeMockProvider()
|
|
197
|
+
const originalGet = nsProvider.get.bind(nsProvider)
|
|
198
|
+
nsProvider.get = async (input: { namespace?: string; collection: string; key: string }) => {
|
|
199
|
+
capturedNamespace = input.namespace
|
|
200
|
+
return originalGet(input)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const router = createCapRouter_settingsStore(() => nsProvider)
|
|
204
|
+
await invokeProcedure(router, 'get', makeCtx('admin'), {
|
|
205
|
+
namespace: 'events',
|
|
206
|
+
collection: 'detections',
|
|
207
|
+
key: 'k1',
|
|
208
|
+
})
|
|
209
|
+
expect(capturedNamespace).toBe('events')
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
// ── Auth enforcement ───────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
describe('auth', () => {
|
|
216
|
+
it('rejects anonymous on all methods', async () => {
|
|
217
|
+
const router = createCapRouter_settingsStore(() => provider)
|
|
218
|
+
for (const method of ['get', 'set', 'query', 'insert', 'update', 'delete', 'count', 'isEmpty']) {
|
|
219
|
+
const result = await invokeProcedure(router, method, makeCtx('anonymous'), { collection: 'c', key: 'k' })
|
|
220
|
+
expect(result.ok).toBe(false)
|
|
221
|
+
if (!result.ok) expect(result.code).toBe('UNAUTHORIZED')
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('allows viewer on query methods (protected)', async () => {
|
|
226
|
+
const router = createCapRouter_settingsStore(() => provider)
|
|
227
|
+
for (const method of ['get', 'query', 'count', 'isEmpty']) {
|
|
228
|
+
const result = await invokeProcedure(router, method, makeCtx('user'), { collection: 'c', key: 'k' })
|
|
229
|
+
expect(result.ok).toBe(true)
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
// ── Missing provider ──────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
describe('missing provider', () => {
|
|
237
|
+
it('throws PRECONDITION_FAILED when provider is null', async () => {
|
|
238
|
+
const router = createCapRouter_settingsStore(() => null)
|
|
239
|
+
const result = await invokeProcedure(router, 'get', makeCtx('admin'), { collection: 'c', key: 'k' })
|
|
240
|
+
expect(result.ok).toBe(false)
|
|
241
|
+
if (!result.ok) {
|
|
242
|
+
expect(result.code).toBe('PRECONDITION_FAILED')
|
|
243
|
+
expect(result.message).toContain('settings-store')
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
})
|