@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
package/.env.example
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# CamStack Server Environment
|
|
2
|
+
# Copy to .env and customize. Loaded via Node.js --env-file flag.
|
|
3
|
+
|
|
4
|
+
# Fixed JWT secret (prevents token invalidation on server restart)
|
|
5
|
+
# Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
|
6
|
+
CAMSTACK_JWT_SECRET=
|
|
7
|
+
|
|
8
|
+
# Admin credentials
|
|
9
|
+
CAMSTACK_ADMIN_USER=admin
|
|
10
|
+
CAMSTACK_ADMIN_PASS=changeme
|
|
11
|
+
|
|
12
|
+
# Data directory (default: camstack-data)
|
|
13
|
+
# CAMSTACK_DATA=camstack-data
|
|
14
|
+
|
|
15
|
+
# Server port and host
|
|
16
|
+
# CAMSTACK_PORT=4443
|
|
17
|
+
# CAMSTACK_HOST=0.0.0.0
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@camstack/server",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"private": false,
|
|
5
|
+
"exports": {
|
|
6
|
+
"./package.json": "./package.json",
|
|
7
|
+
"./main.js": "./dist/main.js",
|
|
8
|
+
"./launcher.js": "./dist/launcher.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc -p tsconfig.build.json",
|
|
12
|
+
"dev": "npx concurrently -n server,ui -c blue,magenta \"tsx watch --env-file=.env --ignore=./camstack-data src/launcher.ts\" \"cd ../../packages/addon-admin-ui && npx vite --port 3001\"",
|
|
13
|
+
"serve": "tsx watch --env-file=.env --ignore=./camstack-data src/launcher.ts",
|
|
14
|
+
"serve:once": "tsx --env-file=.env src/launcher.ts",
|
|
15
|
+
"start": "node --env-file=.env dist/launcher.js",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"test": "vitest run --reporter verbose",
|
|
18
|
+
"test:watch": "vitest"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@camstack/addon-admin-ui": "*",
|
|
22
|
+
"@camstack/addon-advanced-notifier": "*",
|
|
23
|
+
"@camstack/addon-benchmark": "*",
|
|
24
|
+
"@camstack/addon-pipeline": "*",
|
|
25
|
+
"@camstack/addon-pipeline-orchestrator": "*",
|
|
26
|
+
"@camstack/addon-post-analysis": "*",
|
|
27
|
+
"@camstack/core": "*",
|
|
28
|
+
"@camstack/kernel": "*",
|
|
29
|
+
"@camstack/sdk": "*",
|
|
30
|
+
"@camstack/shm-ring": "*",
|
|
31
|
+
"@camstack/types": "*",
|
|
32
|
+
"@camstack/ui-library": "*",
|
|
33
|
+
"@fastify/cookie": "^11.0.2",
|
|
34
|
+
"@fastify/multipart": "^9.0.0",
|
|
35
|
+
"@fastify/static": "^8.0.0",
|
|
36
|
+
"@trpc/server": "^11.16.0",
|
|
37
|
+
"fastify": "^5",
|
|
38
|
+
"js-yaml": "^4",
|
|
39
|
+
"moleculer": "^0.15.0",
|
|
40
|
+
"tar": "^6.2.1",
|
|
41
|
+
"ws": "^8.20.0",
|
|
42
|
+
"zod": "^4.3.6"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@swc/core": "^1.15.18",
|
|
46
|
+
"@types/js-yaml": "^4",
|
|
47
|
+
"@types/node": "^22",
|
|
48
|
+
"@types/tar": "^6.1.13",
|
|
49
|
+
"@types/ws": "^8.18.1",
|
|
50
|
+
"tsx": "^4",
|
|
51
|
+
"typescript": "^5.7",
|
|
52
|
+
"unplugin-swc": "^1.5.9",
|
|
53
|
+
"vitest": "*"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// server/backend/src/__tests__/addon-install-e2e.test.ts
|
|
2
|
+
//
|
|
3
|
+
// E2E test: simulates addon directory scanning and verifies
|
|
4
|
+
// addon packages with package.json + dist/ can be loaded.
|
|
5
|
+
//
|
|
6
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
7
|
+
import * as fs from 'node:fs'
|
|
8
|
+
import * as path from 'node:path'
|
|
9
|
+
|
|
10
|
+
const TEST_ADDONS_DIR = path.resolve('test-output/fresh-addons')
|
|
11
|
+
|
|
12
|
+
describe('Addon Directory Loading', () => {
|
|
13
|
+
|
|
14
|
+
beforeAll(() => {
|
|
15
|
+
// Clean slate — delete the test addons directory
|
|
16
|
+
fs.rmSync(TEST_ADDONS_DIR, { recursive: true, force: true })
|
|
17
|
+
fs.mkdirSync(TEST_ADDONS_DIR, { recursive: true })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
afterAll(() => {
|
|
21
|
+
// Cleanup
|
|
22
|
+
fs.rmSync(TEST_ADDONS_DIR, { recursive: true, force: true })
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('AddonInstaller initializes the addons directory', async () => {
|
|
26
|
+
const { AddonInstaller } = await import('@camstack/kernel')
|
|
27
|
+
|
|
28
|
+
// Constructor creates the addons directory if it doesn't exist
|
|
29
|
+
const installer = new AddonInstaller({
|
|
30
|
+
addonsDir: TEST_ADDONS_DIR,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// Verify directory exists and installer was created
|
|
34
|
+
expect(installer.addonsDir).toBe(TEST_ADDONS_DIR)
|
|
35
|
+
expect(fs.existsSync(TEST_ADDONS_DIR)).toBe(true)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('AddonInstaller lists installed addon packages', async () => {
|
|
39
|
+
const { AddonInstaller } = await import('@camstack/kernel')
|
|
40
|
+
|
|
41
|
+
// Create a mock addon directory
|
|
42
|
+
const addonDir = path.join(TEST_ADDONS_DIR, 'addon-test')
|
|
43
|
+
fs.mkdirSync(addonDir, { recursive: true })
|
|
44
|
+
fs.writeFileSync(
|
|
45
|
+
path.join(addonDir, 'package.json'),
|
|
46
|
+
JSON.stringify({
|
|
47
|
+
name: '@camstack/addon-test',
|
|
48
|
+
version: '1.0.0',
|
|
49
|
+
camstack: { addons: [{ id: 'test', entry: './dist/index.js', slot: 'detector' }] },
|
|
50
|
+
}),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
const installer = new AddonInstaller({ addonsDir: TEST_ADDONS_DIR })
|
|
54
|
+
const installed = installer.listInstalled()
|
|
55
|
+
|
|
56
|
+
expect(installed).toHaveLength(1)
|
|
57
|
+
expect(installed[0]).toMatchObject({
|
|
58
|
+
name: '@camstack/addon-test',
|
|
59
|
+
version: '1.0.0',
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// Cleanup
|
|
63
|
+
fs.rmSync(addonDir, { recursive: true, force: true })
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('AddonLoader scans addons directory and loads packages', async () => {
|
|
67
|
+
const { AddonLoader } = await import('@camstack/kernel')
|
|
68
|
+
|
|
69
|
+
const loader = new AddonLoader()
|
|
70
|
+
|
|
71
|
+
// With an empty directory, no addons should be loaded
|
|
72
|
+
await loader.loadFromDirectory(TEST_ADDONS_DIR)
|
|
73
|
+
expect(loader.listAddons()).toHaveLength(0)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access -- test file: mock AddonContext for `addon.initialize` is deliberately loose */
|
|
2
|
+
// server/backend/src/__tests__/addon-pages-e2e.test.ts
|
|
3
|
+
//
|
|
4
|
+
// Integration tests: verify addon-pages capability wiring end-to-end,
|
|
5
|
+
// BenchmarkAddon page registration, and AdminUIAddon capability provider.
|
|
6
|
+
//
|
|
7
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
8
|
+
import { CapabilityRegistry } from '@camstack/kernel'
|
|
9
|
+
import type { AddonContext, IScopedLogger, ProviderRegistration } from '@camstack/types'
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Optional-module loader — returns the AdminUIAddon class if the admin-ui
|
|
13
|
+
// addon is built and resolvable, otherwise null. Typed against a narrow
|
|
14
|
+
// structural signature so the test never leaks `any` through the dynamic
|
|
15
|
+
// import (the module exports its own dist path which may not exist when
|
|
16
|
+
// the backend workspace is built in isolation).
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
interface AdminUIAddonInstance {
|
|
20
|
+
readonly id: string
|
|
21
|
+
initialize(ctx: AddonContext): Promise<ProviderRegistration[] | void | undefined | {
|
|
22
|
+
readonly providers?: readonly ProviderRegistration[]
|
|
23
|
+
}>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type AdminUIAddonCtor = new () => AdminUIAddonInstance
|
|
27
|
+
|
|
28
|
+
async function loadAdminUIAddon(): Promise<AdminUIAddonCtor | null> {
|
|
29
|
+
try {
|
|
30
|
+
const mod = await import('@camstack/addon-admin-ui/server/addon') as { AdminUIAddon?: AdminUIAddonCtor }
|
|
31
|
+
return mod.AdminUIAddon ?? null
|
|
32
|
+
} catch {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createMockLogger(): IScopedLogger {
|
|
38
|
+
const logger: IScopedLogger = {
|
|
39
|
+
error: vi.fn(),
|
|
40
|
+
warn: vi.fn(),
|
|
41
|
+
info: vi.fn(),
|
|
42
|
+
debug: vi.fn(),
|
|
43
|
+
child: vi.fn(() => logger),
|
|
44
|
+
}
|
|
45
|
+
return logger
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('Addon Pages Integration', () => {
|
|
49
|
+
it('BenchmarkAddon registers addon-pages capability and provides page list', async () => {
|
|
50
|
+
const { BenchmarkAddon } = await import('@camstack/addon-benchmark')
|
|
51
|
+
const addon = new BenchmarkAddon()
|
|
52
|
+
|
|
53
|
+
// Addon has id directly — manifest is attached by AddonLoader at boot time
|
|
54
|
+
expect(addon.id).toBe('benchmark')
|
|
55
|
+
|
|
56
|
+
// Initialize with a mock context to capture registered providers.
|
|
57
|
+
// Addons may register providers either via context.registerProvider() or
|
|
58
|
+
// by returning AddonInitResult / ProviderRegistration[] from initialize().
|
|
59
|
+
const providers = new Map<string, unknown>()
|
|
60
|
+
const mockContext = {
|
|
61
|
+
registerProvider: (capName: string, provider: unknown) => {
|
|
62
|
+
providers.set(capName, provider)
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
const result = await addon.initialize(mockContext as any)
|
|
66
|
+
// Process return-value registrations (AddonInitResult.providers or ProviderRegistration[])
|
|
67
|
+
if (result) {
|
|
68
|
+
const regs = Array.isArray(result) ? result : ((result as any).providers ?? [])
|
|
69
|
+
for (const reg of regs as ProviderRegistration[]) {
|
|
70
|
+
const capName = typeof reg.capability === 'string'
|
|
71
|
+
? reg.capability
|
|
72
|
+
: (reg.capability as any)?.name ?? String(reg.capability)
|
|
73
|
+
providers.set(capName, reg.provider)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const provider = providers.get('addon-pages') as { getPages(): unknown[] } | undefined
|
|
78
|
+
expect(provider).toBeDefined()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
const pages = provider!.getPages()
|
|
82
|
+
expect(pages).toHaveLength(1)
|
|
83
|
+
|
|
84
|
+
expect(pages[0]).toMatchObject({
|
|
85
|
+
id: 'benchmark',
|
|
86
|
+
label: 'Benchmark',
|
|
87
|
+
icon: 'gauge',
|
|
88
|
+
path: '/addon/benchmark',
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('AdminUIAddon provides admin-ui capability with static dir and version', async () => {
|
|
93
|
+
// Load `@camstack/addon-admin-ui/server/addon` lazily — the package exports
|
|
94
|
+
// a dist/server path that may not exist when the backend workspace is
|
|
95
|
+
// built in isolation (admin-ui has its own Vite-based build). Wrapping the
|
|
96
|
+
// dynamic import in a helper means the test can be typed cleanly without
|
|
97
|
+
// `@ts-expect-error`: when the module is absent we skip instead of
|
|
98
|
+
// leaking `any` into the rest of the spec.
|
|
99
|
+
const AdminUIAddonCtor = await loadAdminUIAddon()
|
|
100
|
+
if (!AdminUIAddonCtor) {
|
|
101
|
+
console.warn('[addon-pages-e2e] Skipping AdminUIAddon test — module not built')
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const addon = new AdminUIAddonCtor()
|
|
106
|
+
expect(addon.id).toBe('admin-ui')
|
|
107
|
+
|
|
108
|
+
const providers = new Map<string, unknown>()
|
|
109
|
+
const mockContext = {
|
|
110
|
+
registerProvider: (capName: string, provider: unknown) => {
|
|
111
|
+
providers.set(capName, provider)
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
const result = await addon.initialize(mockContext as any)
|
|
115
|
+
// Process return-value registrations (AddonInitResult.providers or ProviderRegistration[])
|
|
116
|
+
if (result) {
|
|
117
|
+
const regs = Array.isArray(result) ? result : ((result as any).providers ?? [])
|
|
118
|
+
for (const reg of regs as ProviderRegistration[]) {
|
|
119
|
+
const capName = typeof reg.capability === 'string'
|
|
120
|
+
? reg.capability
|
|
121
|
+
: (reg.capability as any)?.name ?? String(reg.capability)
|
|
122
|
+
providers.set(capName, reg.provider)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const ui = providers.get('admin-ui') as {
|
|
127
|
+
getStaticDir(): Promise<{ readonly staticDir: string }>
|
|
128
|
+
getVersion(): Promise<{ readonly version: string }>
|
|
129
|
+
} | undefined
|
|
130
|
+
expect(ui).toBeDefined()
|
|
131
|
+
expect(typeof ui!.getStaticDir).toBe('function')
|
|
132
|
+
expect(typeof ui!.getVersion).toBe('function')
|
|
133
|
+
const version = await ui!.getVersion()
|
|
134
|
+
expect(version.version).toBe('0.1.0')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('CapabilityRegistry wires addon-pages collection providers correctly', () => {
|
|
138
|
+
const registry = new CapabilityRegistry(createMockLogger())
|
|
139
|
+
registry.ready()
|
|
140
|
+
|
|
141
|
+
registry.declareCapability({ name: 'addon-pages', scope: 'system', mode: 'collection', methods: {} })
|
|
142
|
+
|
|
143
|
+
registry.registerProvider('addon-pages', 'benchmark', {
|
|
144
|
+
id: 'benchmark',
|
|
145
|
+
getPages: () => [
|
|
146
|
+
{
|
|
147
|
+
id: 'benchmark',
|
|
148
|
+
label: 'Benchmark',
|
|
149
|
+
icon: 'gauge',
|
|
150
|
+
path: '/addon/benchmark',
|
|
151
|
+
bundle: 'dist/pages/benchmark.js',
|
|
152
|
+
element: 'camstack-benchmark',
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const providers = registry.getCollection<{ getPages(): unknown[] }>('addon-pages')
|
|
158
|
+
expect(providers).toHaveLength(1)
|
|
159
|
+
expect(providers[0].getPages()).toHaveLength(1)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('CapabilityRegistry wires admin-ui singleton correctly', async () => {
|
|
163
|
+
const registry = new CapabilityRegistry(createMockLogger())
|
|
164
|
+
registry.ready()
|
|
165
|
+
|
|
166
|
+
registry.declareCapability({ name: 'admin-ui', scope: 'system', mode: 'singleton', methods: {} })
|
|
167
|
+
|
|
168
|
+
registry.registerProvider('admin-ui', 'admin-ui', {
|
|
169
|
+
getStaticDir: async () => ({ staticDir: '/some/path/dist' }),
|
|
170
|
+
getVersion: async () => ({ version: '0.1.0' }),
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const ui = registry.getSingleton<{ getVersion(): Promise<{ readonly version: string }> }>('admin-ui')
|
|
174
|
+
expect(ui).toBeDefined()
|
|
175
|
+
const version = await ui!.getVersion()
|
|
176
|
+
expect(version.version).toBe('0.1.0')
|
|
177
|
+
})
|
|
178
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { shouldRedirectToLogin } from '../auth/session-cookie.js'
|
|
3
|
+
|
|
4
|
+
describe('shouldRedirectToLogin', () => {
|
|
5
|
+
it('true for an HTML GET with no credentials', () => {
|
|
6
|
+
expect(shouldRedirectToLogin('GET', 'text/html,application/xhtml+xml')).toBe(true)
|
|
7
|
+
})
|
|
8
|
+
it('false for a non-GET', () => {
|
|
9
|
+
expect(shouldRedirectToLogin('POST', 'text/html')).toBe(false)
|
|
10
|
+
})
|
|
11
|
+
it('false for an API GET (JSON accept)', () => {
|
|
12
|
+
expect(shouldRedirectToLogin('GET', 'application/json')).toBe(false)
|
|
13
|
+
})
|
|
14
|
+
it('false when Accept is absent', () => {
|
|
15
|
+
expect(shouldRedirectToLogin('GET', undefined)).toBe(false)
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/consistent-type-assertions -- test mock typing */
|
|
2
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
3
|
+
import { createAddonSettingsRouter } from '../api/core/addon-settings.router.js'
|
|
4
|
+
import { makeCtx } from './cap-routers/harness.js'
|
|
5
|
+
|
|
6
|
+
function createMockConfigService() {
|
|
7
|
+
const addonStore = new Map<string, Record<string, unknown>>()
|
|
8
|
+
const deviceStore = new Map<string, Record<string, unknown>>()
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
getAddonConfig: (id: string) => addonStore.get(id) ?? {},
|
|
12
|
+
setAddonConfig: (id: string, v: Record<string, unknown>) => {
|
|
13
|
+
addonStore.set(id, v)
|
|
14
|
+
},
|
|
15
|
+
getAddonDevice: (id: string, devId: string) => deviceStore.get(`${id}:${devId}`) ?? {},
|
|
16
|
+
setAddonDevice: (id: string, devId: string, v: Record<string, unknown>) => {
|
|
17
|
+
deviceStore.set(`${id}:${devId}`, v)
|
|
18
|
+
},
|
|
19
|
+
// Cast to satisfy the ConfigService type expected by the router factory.
|
|
20
|
+
// Only the four methods above are exercised by the router.
|
|
21
|
+
} as Parameters<typeof createAddonSettingsRouter>[0]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('addon-settings router', () => {
|
|
25
|
+
let caller: ReturnType<ReturnType<typeof createAddonSettingsRouter>['createCaller']>
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
const cfg = createMockConfigService()
|
|
29
|
+
const router = createAddonSettingsRouter(cfg)
|
|
30
|
+
const ctx = makeCtx('admin')
|
|
31
|
+
caller = router.createCaller(ctx)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('getGlobal returns empty object when no stored config', async () => {
|
|
35
|
+
const result = await caller.getGlobal({ addonId: 'my-addon' })
|
|
36
|
+
expect(result).toEqual({})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('updateGlobal stores a field and getGlobal reflects it', async () => {
|
|
40
|
+
await caller.updateGlobal({ addonId: 'my-addon', field: 'threshold', value: 42 })
|
|
41
|
+
const result = await caller.getGlobal({ addonId: 'my-addon' })
|
|
42
|
+
expect(result).toEqual({ threshold: 42 })
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('getDeviceOverrides returns empty object when no stored overrides', async () => {
|
|
46
|
+
const result = await caller.getDeviceOverrides({ addonId: 'my-addon', deviceId: 'cam-1' })
|
|
47
|
+
expect(result).toEqual({})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('updateDevice stores a field and getDeviceOverrides reflects it', async () => {
|
|
51
|
+
await caller.updateDevice({ addonId: 'my-addon', deviceId: 'cam-1', field: 'resolution', value: '1080p' })
|
|
52
|
+
const result = await caller.getDeviceOverrides({ addonId: 'my-addon', deviceId: 'cam-1' })
|
|
53
|
+
expect(result).toEqual({ resolution: '1080p' })
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('updateGlobal merges with existing config without overwriting other fields', async () => {
|
|
57
|
+
await caller.updateGlobal({ addonId: 'my-addon', field: 'alpha', value: 1 })
|
|
58
|
+
await caller.updateGlobal({ addonId: 'my-addon', field: 'beta', value: 2 })
|
|
59
|
+
const result = await caller.getGlobal({ addonId: 'my-addon' })
|
|
60
|
+
expect(result).toEqual({ alpha: 1, beta: 2 })
|
|
61
|
+
})
|
|
62
|
+
})
|