@camstack/server 0.2.2 → 1.0.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/{src/agent-status-page.ts → dist/agent-status-page.js} +30 -45
- package/dist/api/addon-upload.js +441 -0
- package/dist/api/addons-custom.router.js +91 -0
- package/dist/api/auth-whoami.js +55 -0
- package/dist/api/bridge-addons.router.js +109 -0
- package/dist/api/capabilities.router.js +229 -0
- package/dist/api/core/addon-settings.router.js +117 -0
- package/dist/api/core/agents.router.js +73 -0
- package/dist/api/core/auth.router.js +286 -0
- package/dist/api/core/bulk-update-coordinator.js +229 -0
- package/dist/api/core/cap-providers.js +1124 -0
- package/dist/api/core/capabilities.router.js +138 -0
- package/dist/api/core/collection-preference.js +17 -0
- package/dist/api/core/event-bus-proxy.router.js +45 -0
- package/dist/api/core/hwaccel.router.js +91 -0
- package/dist/api/core/live-events.router.js +61 -0
- package/dist/api/core/logs.router.js +172 -0
- package/dist/api/core/notifications.router.js +67 -0
- package/dist/api/core/repl.router.js +35 -0
- package/dist/api/core/settings-backend.router.js +121 -0
- package/dist/api/core/stream-probe.router.js +58 -0
- package/dist/api/core/system-events.router.js +100 -0
- package/dist/api/health/health.routes.js +68 -0
- package/{src/api/oauth2/consent-page.ts → dist/api/oauth2/consent-page.js} +11 -20
- package/dist/api/oauth2/oauth2-routes.js +219 -0
- package/dist/api/trpc/cap-mount-helpers.js +194 -0
- package/dist/api/trpc/cap-route-error-formatter.js +133 -0
- package/dist/api/trpc/client-ip.js +147 -0
- package/dist/api/trpc/core-cap-bridge.js +115 -0
- package/dist/api/trpc/generated-cap-mounts.js +388 -0
- package/dist/api/trpc/generated-cap-routers.js +7635 -0
- package/dist/api/trpc/scope-access.js +93 -0
- package/dist/api/trpc/trpc.context.js +184 -0
- package/dist/api/trpc/trpc.middleware.js +139 -0
- package/dist/api/trpc/trpc.router.js +188 -0
- package/dist/auth/session-cookie.js +47 -0
- package/dist/boot/boot-config.js +241 -0
- package/dist/boot/integration-id-backfill.js +76 -0
- package/dist/boot/post-boot.service.js +85 -0
- package/dist/core/addon/addon-call-gateway.js +99 -0
- package/dist/core/addon/addon-package.service.js +1560 -0
- package/dist/core/addon/addon-registry.service.js +2739 -0
- package/{src/core/addon/addon-row-manifest.ts → dist/core/addon/addon-row-manifest.js} +5 -5
- package/dist/core/addon/addon-search.service.js +62 -0
- package/dist/core/addon/addon-settings-provider.js +102 -0
- package/dist/core/addon/addon.tokens.js +5 -0
- package/dist/core/addon-bridge/addon-bridge.service.js +145 -0
- package/dist/core/addon-pages/addon-pages.service.js +107 -0
- package/dist/core/addon-widgets/addon-widgets.service.js +120 -0
- package/dist/core/agent/agent-registry.service.js +477 -0
- package/dist/core/auth/auth.service.js +10 -0
- package/dist/core/capability/capability.service.js +58 -0
- package/dist/core/config/config.schema.js +7 -0
- package/dist/core/config/config.service.js +10 -0
- package/dist/core/events/event-bus.service.js +83 -0
- package/dist/core/feature/feature.service.js +10 -0
- package/dist/core/lifecycle/lifecycle-state-machine.js +6 -0
- package/dist/core/logging/log-ring-buffer.js +6 -0
- package/dist/core/logging/logging.service.js +130 -0
- package/dist/core/logging/scoped-logger.js +6 -0
- package/dist/core/moleculer/cap-call-fn.js +50 -0
- package/dist/core/moleculer/cap-route-authority.js +122 -0
- package/dist/core/moleculer/moleculer.service.js +898 -0
- package/dist/core/network/network-quality.service.js +7 -0
- package/dist/core/notification/notification-wrapper.service.js +33 -0
- package/dist/core/notification/toast-wrapper.service.js +25 -0
- package/dist/core/provider/provider.tokens.js +4 -0
- package/dist/core/repl/repl-engine.service.js +140 -0
- package/dist/core/storage/fs-storage-backend.js +6 -0
- package/dist/core/storage/storage-location-manager.js +6 -0
- package/dist/core/storage/storage.service.js +7 -0
- package/dist/core/streaming/stream-probe.service.js +209 -0
- package/dist/core/topology/topology-emitter.service.js +106 -0
- package/dist/launcher.js +325 -0
- package/dist/main.js +1098 -0
- package/dist/manual-boot.js +227 -0
- package/package.json +5 -1
- package/src/__tests__/addon-install-e2e.test.ts +0 -74
- package/src/__tests__/addon-pages-e2e.test.ts +0 -200
- package/src/__tests__/addon-route-session.test.ts +0 -17
- package/src/__tests__/addon-settings-router.spec.ts +0 -67
- package/src/__tests__/addon-upload.spec.ts +0 -475
- package/src/__tests__/agent-registry.spec.ts +0 -179
- package/src/__tests__/agent-status-page.spec.ts +0 -82
- package/src/__tests__/auth-session-cookie.test.ts +0 -48
- package/src/__tests__/bulk-update-coordinator.spec.ts +0 -303
- package/src/__tests__/cap-ownership-authority.spec.ts +0 -431
- package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +0 -206
- package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +0 -37
- package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +0 -110
- package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +0 -292
- package/src/__tests__/cap-providers-bulk-update.spec.ts +0 -408
- package/src/__tests__/cap-route-adapter.spec.ts +0 -302
- package/src/__tests__/cap-routers/_meta.spec.ts +0 -199
- package/src/__tests__/cap-routers/addon-settings.router.spec.ts +0 -115
- package/src/__tests__/cap-routers/broker-routing.router.spec.ts +0 -177
- package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +0 -125
- package/src/__tests__/cap-routers/capabilities-node.spec.ts +0 -68
- package/src/__tests__/cap-routers/device-link-overlay.spec.ts +0 -137
- package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +0 -194
- package/src/__tests__/cap-routers/harness.ts +0 -163
- package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +0 -133
- package/src/__tests__/cap-routers/null-provider-guard.spec.ts +0 -64
- package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +0 -159
- package/src/__tests__/cap-routers/settings-store.router.spec.ts +0 -291
- package/src/__tests__/capability-e2e.test.ts +0 -384
- package/src/__tests__/cli-e2e.test.ts +0 -150
- package/src/__tests__/core-cap-bridge.spec.ts +0 -91
- package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +0 -40
- package/src/__tests__/device-settings-contribution-dispatch.spec.ts +0 -280
- package/src/__tests__/embedded-deps-e2e.test.ts +0 -125
- package/src/__tests__/event-bus-proxy-router.spec.ts +0 -75
- package/src/__tests__/fixtures/mock-analysis-addon-a.ts +0 -37
- package/src/__tests__/fixtures/mock-analysis-addon-b.ts +0 -37
- package/src/__tests__/fixtures/mock-log-addon.ts +0 -37
- package/src/__tests__/fixtures/mock-storage-addon.ts +0 -40
- package/src/__tests__/framework-allowlist.spec.ts +0 -96
- package/src/__tests__/framework-installer-defer-restart.spec.ts +0 -165
- package/src/__tests__/https-e2e.test.ts +0 -124
- package/src/__tests__/lifecycle-e2e.test.ts +0 -189
- package/src/__tests__/live-events-subscription.spec.ts +0 -149
- package/src/__tests__/moleculer/uds-readiness.spec.ts +0 -150
- package/src/__tests__/moleculer/uds-topology.spec.ts +0 -418
- package/src/__tests__/moleculer/uds-unowned-call.spec.ts +0 -383
- package/src/__tests__/moleculer-register-node-idempotency.spec.ts +0 -273
- package/src/__tests__/native-cap-route.spec.ts +0 -427
- package/src/__tests__/oauth2-account-linking.spec.ts +0 -867
- package/src/__tests__/post-boot-restart.spec.ts +0 -161
- package/src/__tests__/singleton-contention.test.ts +0 -499
- package/src/__tests__/streaming-diagnostic.test.ts +0 -615
- package/src/__tests__/streaming-scale.test.ts +0 -314
- package/src/__tests__/uds-addon-call-wiring.spec.ts +0 -242
- package/src/__tests__/uds-log-ingest.spec.ts +0 -183
- package/src/api/__tests__/addons-custom.spec.ts +0 -148
- package/src/api/__tests__/capabilities.router.test.ts +0 -56
- package/src/api/addon-upload.ts +0 -529
- package/src/api/addons-custom.router.ts +0 -101
- package/src/api/auth-whoami.ts +0 -101
- package/src/api/bridge-addons.router.ts +0 -122
- package/src/api/capabilities.router.ts +0 -265
- package/src/api/core/__tests__/auth-router-totp.spec.ts +0 -297
- package/src/api/core/__tests__/integration-markers.spec.ts +0 -10
- package/src/api/core/addon-settings.router.ts +0 -127
- package/src/api/core/agents.router.ts +0 -86
- package/src/api/core/auth.router.ts +0 -322
- package/src/api/core/bulk-update-coordinator.ts +0 -305
- package/src/api/core/cap-providers.ts +0 -1339
- package/src/api/core/capabilities.router.ts +0 -149
- package/src/api/core/collection-preference.ts +0 -40
- package/src/api/core/event-bus-proxy.router.ts +0 -45
- package/src/api/core/hwaccel.router.ts +0 -108
- package/src/api/core/live-events.router.ts +0 -67
- package/src/api/core/logs.router.ts +0 -195
- package/src/api/core/notifications.router.ts +0 -66
- package/src/api/core/repl.router.ts +0 -39
- package/src/api/core/settings-backend.router.ts +0 -140
- package/src/api/core/stream-probe.router.ts +0 -57
- package/src/api/core/system-events.router.ts +0 -125
- package/src/api/health/health.routes.ts +0 -117
- package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +0 -62
- package/src/api/oauth2/oauth2-routes.ts +0 -281
- package/src/api/trpc/__tests__/client-ip.spec.ts +0 -146
- package/src/api/trpc/__tests__/scope-access-device.spec.ts +0 -268
- package/src/api/trpc/__tests__/scope-access.spec.ts +0 -102
- package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +0 -136
- package/src/api/trpc/cap-mount-helpers.ts +0 -245
- package/src/api/trpc/cap-route-error-formatter.ts +0 -171
- package/src/api/trpc/client-ip.ts +0 -147
- package/src/api/trpc/core-cap-bridge.ts +0 -154
- package/src/api/trpc/generated-cap-mounts.ts +0 -1240
- package/src/api/trpc/generated-cap-routers.ts +0 -11523
- package/src/api/trpc/scope-access.ts +0 -110
- package/src/api/trpc/trpc.context.ts +0 -258
- package/src/api/trpc/trpc.middleware.ts +0 -146
- package/src/api/trpc/trpc.router.ts +0 -389
- package/src/auth/session-cookie.ts +0 -54
- package/src/boot/__tests__/integration-id-backfill.spec.ts +0 -131
- package/src/boot/boot-config.ts +0 -259
- package/src/boot/integration-id-backfill.ts +0 -109
- package/src/boot/post-boot.service.ts +0 -105
- package/src/core/addon/__tests__/addon-registry-capability.test.ts +0 -62
- package/src/core/addon/__tests__/addon-row-manifest.spec.ts +0 -62
- package/src/core/addon/addon-call-gateway.ts +0 -171
- package/src/core/addon/addon-package.service.ts +0 -1787
- package/src/core/addon/addon-registry.service.ts +0 -3130
- package/src/core/addon/addon-search.service.ts +0 -91
- package/src/core/addon/addon-settings-provider.ts +0 -220
- package/src/core/addon/addon.tokens.ts +0 -2
- package/src/core/addon-bridge/addon-bridge.service.ts +0 -130
- package/src/core/addon-pages/addon-pages.service.spec.ts +0 -117
- package/src/core/addon-pages/addon-pages.service.ts +0 -82
- package/src/core/addon-widgets/addon-widgets.service.ts +0 -95
- package/src/core/agent/agent-registry.service.ts +0 -529
- package/src/core/auth/auth.service.spec.ts +0 -86
- package/src/core/auth/auth.service.ts +0 -8
- package/src/core/capability/capability.service.ts +0 -66
- package/src/core/config/config.schema.ts +0 -3
- package/src/core/config/config.service.spec.ts +0 -175
- package/src/core/config/config.service.ts +0 -7
- package/src/core/events/event-bus.service.spec.ts +0 -235
- package/src/core/events/event-bus.service.ts +0 -89
- package/src/core/feature/feature.service.spec.ts +0 -99
- package/src/core/feature/feature.service.ts +0 -8
- package/src/core/lifecycle/lifecycle-state-machine.spec.ts +0 -166
- package/src/core/lifecycle/lifecycle-state-machine.ts +0 -3
- package/src/core/logging/log-ring-buffer.ts +0 -3
- package/src/core/logging/logging.service.spec.ts +0 -287
- package/src/core/logging/logging.service.ts +0 -143
- package/src/core/logging/scoped-logger.ts +0 -3
- package/src/core/moleculer/cap-call-fn.spec.ts +0 -173
- package/src/core/moleculer/cap-call-fn.ts +0 -107
- package/src/core/moleculer/cap-route-authority.ts +0 -194
- package/src/core/moleculer/moleculer.service.ts +0 -1072
- package/src/core/network/network-quality.service.spec.ts +0 -53
- package/src/core/network/network-quality.service.ts +0 -5
- package/src/core/notification/notification-wrapper.service.ts +0 -34
- package/src/core/notification/toast-wrapper.service.ts +0 -27
- package/src/core/provider/provider.tokens.ts +0 -1
- package/src/core/repl/repl-engine.service.spec.ts +0 -444
- package/src/core/repl/repl-engine.service.ts +0 -155
- package/src/core/storage/fs-storage-backend.spec.ts +0 -70
- package/src/core/storage/fs-storage-backend.ts +0 -3
- package/src/core/storage/storage-location-manager.spec.ts +0 -130
- package/src/core/storage/storage-location-manager.ts +0 -3
- package/src/core/storage/storage.service.spec.ts +0 -73
- package/src/core/storage/storage.service.ts +0 -3
- package/src/core/streaming/stream-probe.service.ts +0 -221
- package/src/core/topology/topology-emitter.service.ts +0 -105
- package/src/launcher.ts +0 -314
- package/src/main.ts +0 -1245
- package/src/manual-boot.ts +0 -301
- package/tsconfig.build.json +0 -8
- package/tsconfig.json +0 -33
- package/vitest.config.ts +0 -26
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Boot — constructs every service by hand in dependency order, runs
|
|
4
|
+
* `onModuleInit` hooks in sequence, and exposes a `BootedApp` facade
|
|
5
|
+
* around the Fastify instance. This is the only boot path; NestJS was
|
|
6
|
+
* removed to drop the accidental DI layer on top of an already-explicit
|
|
7
|
+
* dependency graph.
|
|
8
|
+
*/
|
|
9
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
10
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
11
|
+
};
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.bootManual = bootManual;
|
|
14
|
+
const fastify_1 = __importDefault(require("fastify"));
|
|
15
|
+
const config_service_1 = require("./core/config/config.service");
|
|
16
|
+
const logging_service_1 = require("./core/logging/logging.service");
|
|
17
|
+
const event_bus_service_1 = require("./core/events/event-bus.service");
|
|
18
|
+
const storage_service_1 = require("./core/storage/storage.service");
|
|
19
|
+
const capability_service_1 = require("./core/capability/capability.service");
|
|
20
|
+
const feature_service_1 = require("./core/feature/feature.service");
|
|
21
|
+
const auth_service_1 = require("./core/auth/auth.service");
|
|
22
|
+
const stream_probe_service_1 = require("./core/streaming/stream-probe.service");
|
|
23
|
+
const network_quality_service_1 = require("./core/network/network-quality.service");
|
|
24
|
+
const toast_wrapper_service_1 = require("./core/notification/toast-wrapper.service");
|
|
25
|
+
const notification_wrapper_service_1 = require("./core/notification/notification-wrapper.service");
|
|
26
|
+
const addon_pages_service_1 = require("./core/addon-pages/addon-pages.service");
|
|
27
|
+
const addon_widgets_service_1 = require("./core/addon-widgets/addon-widgets.service");
|
|
28
|
+
const addon_bridge_service_1 = require("./core/addon-bridge/addon-bridge.service");
|
|
29
|
+
const moleculer_service_1 = require("./core/moleculer/moleculer.service");
|
|
30
|
+
const agent_registry_service_1 = require("./core/agent/agent-registry.service");
|
|
31
|
+
const addon_registry_service_1 = require("./core/addon/addon-registry.service");
|
|
32
|
+
const addon_search_service_1 = require("./core/addon/addon-search.service");
|
|
33
|
+
const addon_package_service_1 = require("./core/addon/addon-package.service");
|
|
34
|
+
const repl_engine_service_1 = require("./core/repl/repl-engine.service");
|
|
35
|
+
const topology_emitter_service_1 = require("./core/topology/topology-emitter.service");
|
|
36
|
+
const post_boot_service_1 = require("./boot/post-boot.service");
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Service container — narrowing via `instanceof`, no casts.
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
class ServiceContainer {
|
|
41
|
+
services = new Map();
|
|
42
|
+
register(ctor, instance) {
|
|
43
|
+
this.services.set(ctor, instance);
|
|
44
|
+
}
|
|
45
|
+
get(ctor) {
|
|
46
|
+
const inst = this.services.get(ctor);
|
|
47
|
+
if (inst === undefined) {
|
|
48
|
+
throw new Error(`ServiceContainer: ${ctor.name} is not registered`);
|
|
49
|
+
}
|
|
50
|
+
if (!(inst instanceof ctor)) {
|
|
51
|
+
throw new Error(`ServiceContainer: ${ctor.name} instance type mismatch at resolve time`);
|
|
52
|
+
}
|
|
53
|
+
return inst;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Manual boot
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
async function bootManual(opts) {
|
|
60
|
+
const { infra, fastifyOpts } = opts;
|
|
61
|
+
void infra; // infraContext is consumed by main.ts directly (locationManager, etc.)
|
|
62
|
+
const configPath = process.env.CONFIG_PATH ?? 'camstack-data/config.yaml';
|
|
63
|
+
// ---- Layer 0: services with no service deps -----------------------------
|
|
64
|
+
const configService = new config_service_1.ConfigService(configPath);
|
|
65
|
+
const storageService = new storage_service_1.StorageService();
|
|
66
|
+
const capabilityService = new capability_service_1.CapabilityService();
|
|
67
|
+
const networkQualityService = new network_quality_service_1.NetworkQualityService();
|
|
68
|
+
const toastWrapper = new toast_wrapper_service_1.ToastServiceWrapper();
|
|
69
|
+
// ---- Layer 1: need ConfigService / LoggingService -----------------------
|
|
70
|
+
const loggingService = new logging_service_1.LoggingService(configService);
|
|
71
|
+
const eventBusService = new event_bus_service_1.EventBusService(configService);
|
|
72
|
+
// Wire device-name cache for log formatter — subscribes here so the
|
|
73
|
+
// very first DeviceRegistered event populates the cache before any
|
|
74
|
+
// log line needing `deviceName` is emitted.
|
|
75
|
+
loggingService.attachDeviceNameStream(eventBusService);
|
|
76
|
+
const featureService = new feature_service_1.FeatureService(configService);
|
|
77
|
+
const authService = new auth_service_1.AuthService(configService);
|
|
78
|
+
const streamProbeService = new stream_probe_service_1.StreamProbeService(loggingService);
|
|
79
|
+
const addonBridgeService = new addon_bridge_service_1.AddonBridgeService(loggingService);
|
|
80
|
+
const addonSearchService = new addon_search_service_1.AddonSearchService(loggingService);
|
|
81
|
+
// ---- Layer 2: need Layer 0/1 -------------------------------------------
|
|
82
|
+
const notificationWrapper = new notification_wrapper_service_1.NotificationServiceWrapper(capabilityService, loggingService);
|
|
83
|
+
const addonPagesService = new addon_pages_service_1.AddonPagesService(loggingService, configService, capabilityService);
|
|
84
|
+
const moleculerService = new moleculer_service_1.MoleculerService(eventBusService, configService, loggingService, capabilityService, streamProbeService);
|
|
85
|
+
// ---- Layer 3: need Moleculer / AddonRegistry ---------------------------
|
|
86
|
+
const agentRegistryService = new agent_registry_service_1.AgentRegistryService(eventBusService, moleculerService, capabilityService);
|
|
87
|
+
const addonRegistryService = new addon_registry_service_1.AddonRegistryService(loggingService, eventBusService, configService, storageService, capabilityService, moleculerService, streamProbeService);
|
|
88
|
+
// AgentRegistryService needs the hub addon registry for placement-based
|
|
89
|
+
// reconciliation, but is constructed first (it has no other dependency
|
|
90
|
+
// on AddonRegistryService) — wire it post-construction.
|
|
91
|
+
agentRegistryService.setAddonRegistry(addonRegistryService);
|
|
92
|
+
// ---- Layer 4: need AddonRegistry ---------------------------------------
|
|
93
|
+
// AddonWidgetsService — needs AddonRegistryService for the bundled-addon
|
|
94
|
+
// dist sub-folder lookup (see service docstring).
|
|
95
|
+
const addonWidgetsService = new addon_widgets_service_1.AddonWidgetsService(loggingService, capabilityService, addonRegistryService);
|
|
96
|
+
const addonPackageService = new addon_package_service_1.AddonPackageService(loggingService, eventBusService, configService, addonRegistryService, notificationWrapper, toastWrapper);
|
|
97
|
+
const replEngineService = new repl_engine_service_1.ReplEngineService(addonRegistryService, eventBusService, loggingService);
|
|
98
|
+
const topologyEmitterService = new topology_emitter_service_1.TopologyEmitterService(eventBusService, agentRegistryService, addonRegistryService);
|
|
99
|
+
const postBootService = new post_boot_service_1.PostBootService(addonRegistryService, eventBusService, loggingService);
|
|
100
|
+
// ---- Container ---------------------------------------------------------
|
|
101
|
+
const container = new ServiceContainer();
|
|
102
|
+
container.register(config_service_1.ConfigService, configService);
|
|
103
|
+
container.register(logging_service_1.LoggingService, loggingService);
|
|
104
|
+
container.register(event_bus_service_1.EventBusService, eventBusService);
|
|
105
|
+
container.register(storage_service_1.StorageService, storageService);
|
|
106
|
+
container.register(capability_service_1.CapabilityService, capabilityService);
|
|
107
|
+
container.register(feature_service_1.FeatureService, featureService);
|
|
108
|
+
container.register(auth_service_1.AuthService, authService);
|
|
109
|
+
container.register(stream_probe_service_1.StreamProbeService, streamProbeService);
|
|
110
|
+
container.register(network_quality_service_1.NetworkQualityService, networkQualityService);
|
|
111
|
+
container.register(toast_wrapper_service_1.ToastServiceWrapper, toastWrapper);
|
|
112
|
+
container.register(notification_wrapper_service_1.NotificationServiceWrapper, notificationWrapper);
|
|
113
|
+
container.register(addon_pages_service_1.AddonPagesService, addonPagesService);
|
|
114
|
+
container.register(addon_widgets_service_1.AddonWidgetsService, addonWidgetsService);
|
|
115
|
+
container.register(addon_bridge_service_1.AddonBridgeService, addonBridgeService);
|
|
116
|
+
container.register(moleculer_service_1.MoleculerService, moleculerService);
|
|
117
|
+
container.register(agent_registry_service_1.AgentRegistryService, agentRegistryService);
|
|
118
|
+
container.register(addon_registry_service_1.AddonRegistryService, addonRegistryService);
|
|
119
|
+
container.register(addon_search_service_1.AddonSearchService, addonSearchService);
|
|
120
|
+
container.register(addon_package_service_1.AddonPackageService, addonPackageService);
|
|
121
|
+
container.register(repl_engine_service_1.ReplEngineService, replEngineService);
|
|
122
|
+
container.register(topology_emitter_service_1.TopologyEmitterService, topologyEmitterService);
|
|
123
|
+
container.register(post_boot_service_1.PostBootService, postBootService);
|
|
124
|
+
// ---- Fastify instance --------------------------------------------------
|
|
125
|
+
const fastify = (0, fastify_1.default)(fastifyOpts);
|
|
126
|
+
// /health and /health/* — registered in `main.ts` via
|
|
127
|
+
// `registerHealthRoutes` once MoleculerService + AgentRegistryService are
|
|
128
|
+
// available. The detailed shape is shared with each agent's `/health`
|
|
129
|
+
// (see `packages/agent/src/agent-http.ts`).
|
|
130
|
+
// ---- Shutdown coordination --------------------------------------------
|
|
131
|
+
let closed = false;
|
|
132
|
+
let shutdownHooksRegistered = false;
|
|
133
|
+
const close = async () => {
|
|
134
|
+
if (closed)
|
|
135
|
+
return;
|
|
136
|
+
closed = true;
|
|
137
|
+
const logErr = (label, err) => {
|
|
138
|
+
console.error(`[manual-boot] ${label} destroy failed:`, err);
|
|
139
|
+
};
|
|
140
|
+
try {
|
|
141
|
+
topologyEmitterService.onModuleDestroy();
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
logErr('TopologyEmitter', err);
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
await addonRegistryService.onModuleDestroy();
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
logErr('AddonRegistry', err);
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
await addonBridgeService.onModuleDestroy();
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
logErr('AddonBridge', err);
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
await moleculerService.onModuleDestroy();
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
logErr('Moleculer', err);
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
await fastify.close();
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
logErr('Fastify', err);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
return {
|
|
172
|
+
get(ctor) {
|
|
173
|
+
return container.get(ctor);
|
|
174
|
+
},
|
|
175
|
+
getHttpAdapter() {
|
|
176
|
+
return { getInstance: () => fastify };
|
|
177
|
+
},
|
|
178
|
+
enableCors() {
|
|
179
|
+
fastify.addHook('onRequest', async (request, reply) => {
|
|
180
|
+
reply.header('access-control-allow-origin', '*');
|
|
181
|
+
reply.header('access-control-allow-methods', 'GET,POST,PUT,DELETE,OPTIONS,PATCH');
|
|
182
|
+
reply.header('access-control-allow-headers', 'content-type,authorization');
|
|
183
|
+
if (request.method === 'OPTIONS') {
|
|
184
|
+
reply.status(204).send();
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
enableShutdownHooks() {
|
|
189
|
+
if (shutdownHooksRegistered)
|
|
190
|
+
return;
|
|
191
|
+
shutdownHooksRegistered = true;
|
|
192
|
+
const handler = () => {
|
|
193
|
+
close().catch((err) => {
|
|
194
|
+
console.error('[manual-boot] shutdown close failed:', err);
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
process.once('SIGTERM', handler);
|
|
198
|
+
process.once('SIGINT', handler);
|
|
199
|
+
},
|
|
200
|
+
async init() {
|
|
201
|
+
// Init order (dependency-graph-derived):
|
|
202
|
+
// 1. MoleculerService — starts broker and calls EventBus.attachBroker()
|
|
203
|
+
// 2. AddonBridgeService — no cross-service deps
|
|
204
|
+
// 3. AgentRegistryService — subscribes to broker.localBus.on('$node.connected')
|
|
205
|
+
// 4. AddonRegistryService — boots every addon (needs broker live + config)
|
|
206
|
+
await moleculerService.onModuleInit();
|
|
207
|
+
await addonBridgeService.onModuleInit();
|
|
208
|
+
agentRegistryService.onModuleInit();
|
|
209
|
+
await addonRegistryService.onModuleInit();
|
|
210
|
+
// Reconcile already-connected agents now that the hub's installed
|
|
211
|
+
// addon set is populated. Agents online before the hub started never
|
|
212
|
+
// fire a fresh `$node.connected` event, so the per-connect trigger
|
|
213
|
+
// alone would miss them — this catches the boot-time case (e.g. a
|
|
214
|
+
// long-running dev-agent with a now-stale addon). Fire-and-forget so
|
|
215
|
+
// an unreachable agent never delays boot.
|
|
216
|
+
void agentRegistryService.reconcileConnectedAgents();
|
|
217
|
+
// Topology emitter must boot AFTER AgentRegistry + AddonRegistry —
|
|
218
|
+
// its initial snapshot reads both. Subscribes to lifecycle events
|
|
219
|
+
// for incremental snapshots and emits a periodic safety net.
|
|
220
|
+
topologyEmitterService.onModuleInit();
|
|
221
|
+
},
|
|
222
|
+
async listen(port, host) {
|
|
223
|
+
await fastify.listen({ port, host });
|
|
224
|
+
},
|
|
225
|
+
close,
|
|
226
|
+
};
|
|
227
|
+
}
|
package/package.json
CHANGED
|
@@ -1,74 +0,0 @@
|
|
|
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
|
-
beforeAll(() => {
|
|
14
|
-
// Clean slate — delete the test addons directory
|
|
15
|
-
fs.rmSync(TEST_ADDONS_DIR, { recursive: true, force: true })
|
|
16
|
-
fs.mkdirSync(TEST_ADDONS_DIR, { recursive: true })
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
afterAll(() => {
|
|
20
|
-
// Cleanup
|
|
21
|
-
fs.rmSync(TEST_ADDONS_DIR, { recursive: true, force: true })
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
it('AddonInstaller initializes the addons directory', async () => {
|
|
25
|
-
const { AddonInstaller } = await import('@camstack/kernel')
|
|
26
|
-
|
|
27
|
-
// Constructor creates the addons directory if it doesn't exist
|
|
28
|
-
const installer = new AddonInstaller({
|
|
29
|
-
addonsDir: TEST_ADDONS_DIR,
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
// Verify directory exists and installer was created
|
|
33
|
-
expect(installer.addonsDir).toBe(TEST_ADDONS_DIR)
|
|
34
|
-
expect(fs.existsSync(TEST_ADDONS_DIR)).toBe(true)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('AddonInstaller lists installed addon packages', async () => {
|
|
38
|
-
const { AddonInstaller } = await import('@camstack/kernel')
|
|
39
|
-
|
|
40
|
-
// Create a mock addon directory
|
|
41
|
-
const addonDir = path.join(TEST_ADDONS_DIR, 'addon-test')
|
|
42
|
-
fs.mkdirSync(addonDir, { recursive: true })
|
|
43
|
-
fs.writeFileSync(
|
|
44
|
-
path.join(addonDir, 'package.json'),
|
|
45
|
-
JSON.stringify({
|
|
46
|
-
name: '@camstack/addon-test',
|
|
47
|
-
version: '1.0.0',
|
|
48
|
-
camstack: { addons: [{ id: 'test', entry: './dist/index.js', slot: 'detector' }] },
|
|
49
|
-
}),
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
const installer = new AddonInstaller({ addonsDir: TEST_ADDONS_DIR })
|
|
53
|
-
const installed = installer.listInstalled()
|
|
54
|
-
|
|
55
|
-
expect(installed).toHaveLength(1)
|
|
56
|
-
expect(installed[0]).toMatchObject({
|
|
57
|
-
name: '@camstack/addon-test',
|
|
58
|
-
version: '1.0.0',
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
// Cleanup
|
|
62
|
-
fs.rmSync(addonDir, { recursive: true, force: true })
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('AddonLoader scans addons directory and loads packages', async () => {
|
|
66
|
-
const { AddonLoader } = await import('@camstack/kernel')
|
|
67
|
-
|
|
68
|
-
const loader = new AddonLoader()
|
|
69
|
-
|
|
70
|
-
// With an empty directory, no addons should be loaded
|
|
71
|
-
await loader.loadFromDirectory(TEST_ADDONS_DIR)
|
|
72
|
-
expect(loader.listAddons()).toHaveLength(0)
|
|
73
|
-
})
|
|
74
|
-
})
|
|
@@ -1,200 +0,0 @@
|
|
|
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<
|
|
22
|
-
| ProviderRegistration[]
|
|
23
|
-
| void
|
|
24
|
-
| undefined
|
|
25
|
-
| {
|
|
26
|
-
readonly providers?: readonly ProviderRegistration[]
|
|
27
|
-
}
|
|
28
|
-
>
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
type AdminUIAddonCtor = new () => AdminUIAddonInstance
|
|
32
|
-
|
|
33
|
-
async function loadAdminUIAddon(): Promise<AdminUIAddonCtor | null> {
|
|
34
|
-
try {
|
|
35
|
-
const mod = (await import('@camstack/addon-admin-ui/server/addon')) as {
|
|
36
|
-
AdminUIAddon?: AdminUIAddonCtor
|
|
37
|
-
}
|
|
38
|
-
return mod.AdminUIAddon ?? null
|
|
39
|
-
} catch {
|
|
40
|
-
return null
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function createMockLogger(): IScopedLogger {
|
|
45
|
-
const logger: IScopedLogger = {
|
|
46
|
-
error: vi.fn(),
|
|
47
|
-
warn: vi.fn(),
|
|
48
|
-
info: vi.fn(),
|
|
49
|
-
debug: vi.fn(),
|
|
50
|
-
child: vi.fn(() => logger),
|
|
51
|
-
}
|
|
52
|
-
return logger
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
describe('Addon Pages Integration', () => {
|
|
56
|
-
it('BenchmarkAddon registers addon-pages capability and provides page list', async () => {
|
|
57
|
-
const { BenchmarkAddon } = await import('@camstack/addon-benchmark')
|
|
58
|
-
const addon = new BenchmarkAddon()
|
|
59
|
-
|
|
60
|
-
// Addon has id directly — manifest is attached by AddonLoader at boot time
|
|
61
|
-
expect(addon.id).toBe('benchmark')
|
|
62
|
-
|
|
63
|
-
// Initialize with a mock context to capture registered providers.
|
|
64
|
-
// Addons may register providers either via context.registerProvider() or
|
|
65
|
-
// by returning AddonInitResult / ProviderRegistration[] from initialize().
|
|
66
|
-
const providers = new Map<string, unknown>()
|
|
67
|
-
const mockContext = {
|
|
68
|
-
registerProvider: (capName: string, provider: unknown) => {
|
|
69
|
-
providers.set(capName, provider)
|
|
70
|
-
},
|
|
71
|
-
}
|
|
72
|
-
const result = await addon.initialize(mockContext as any)
|
|
73
|
-
// Process return-value registrations (AddonInitResult.providers or ProviderRegistration[])
|
|
74
|
-
if (result) {
|
|
75
|
-
const regs = Array.isArray(result) ? result : ((result as any).providers ?? [])
|
|
76
|
-
for (const reg of regs as ProviderRegistration[]) {
|
|
77
|
-
const capName =
|
|
78
|
-
typeof reg.capability === 'string'
|
|
79
|
-
? reg.capability
|
|
80
|
-
: ((reg.capability as any)?.name ?? String(reg.capability))
|
|
81
|
-
providers.set(capName, reg.provider)
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const provider = providers.get('addon-pages') as { getPages(): unknown[] } | undefined
|
|
86
|
-
expect(provider).toBeDefined()
|
|
87
|
-
|
|
88
|
-
const pages = provider!.getPages()
|
|
89
|
-
expect(pages).toHaveLength(1)
|
|
90
|
-
|
|
91
|
-
expect(pages[0]).toMatchObject({
|
|
92
|
-
id: 'benchmark',
|
|
93
|
-
label: 'Benchmark',
|
|
94
|
-
icon: 'gauge',
|
|
95
|
-
path: '/addon/benchmark',
|
|
96
|
-
})
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it('AdminUIAddon provides admin-ui capability with static dir and version', async () => {
|
|
100
|
-
// Load `@camstack/addon-admin-ui/server/addon` lazily — the package exports
|
|
101
|
-
// a dist/server path that may not exist when the backend workspace is
|
|
102
|
-
// built in isolation (admin-ui has its own Vite-based build). Wrapping the
|
|
103
|
-
// dynamic import in a helper means the test can be typed cleanly without
|
|
104
|
-
// `@ts-expect-error`: when the module is absent we skip instead of
|
|
105
|
-
// leaking `any` into the rest of the spec.
|
|
106
|
-
const AdminUIAddonCtor = await loadAdminUIAddon()
|
|
107
|
-
if (!AdminUIAddonCtor) {
|
|
108
|
-
console.warn('[addon-pages-e2e] Skipping AdminUIAddon test — module not built')
|
|
109
|
-
return
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const addon = new AdminUIAddonCtor()
|
|
113
|
-
expect(addon.id).toBe('admin-ui')
|
|
114
|
-
|
|
115
|
-
const providers = new Map<string, unknown>()
|
|
116
|
-
const mockContext = {
|
|
117
|
-
registerProvider: (capName: string, provider: unknown) => {
|
|
118
|
-
providers.set(capName, provider)
|
|
119
|
-
},
|
|
120
|
-
}
|
|
121
|
-
const result = await addon.initialize(mockContext as any)
|
|
122
|
-
// Process return-value registrations (AddonInitResult.providers or ProviderRegistration[])
|
|
123
|
-
if (result) {
|
|
124
|
-
const regs = Array.isArray(result) ? result : ((result as any).providers ?? [])
|
|
125
|
-
for (const reg of regs as ProviderRegistration[]) {
|
|
126
|
-
const capName =
|
|
127
|
-
typeof reg.capability === 'string'
|
|
128
|
-
? reg.capability
|
|
129
|
-
: ((reg.capability as any)?.name ?? String(reg.capability))
|
|
130
|
-
providers.set(capName, reg.provider)
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const ui = providers.get('admin-ui') as
|
|
135
|
-
| {
|
|
136
|
-
getStaticDir(): Promise<{ readonly staticDir: string }>
|
|
137
|
-
getVersion(): Promise<{ readonly version: string }>
|
|
138
|
-
}
|
|
139
|
-
| undefined
|
|
140
|
-
expect(ui).toBeDefined()
|
|
141
|
-
expect(typeof ui!.getStaticDir).toBe('function')
|
|
142
|
-
expect(typeof ui!.getVersion).toBe('function')
|
|
143
|
-
const version = await ui!.getVersion()
|
|
144
|
-
expect(version.version).toBe('0.1.0')
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('CapabilityRegistry wires addon-pages collection providers correctly', () => {
|
|
148
|
-
const registry = new CapabilityRegistry(createMockLogger())
|
|
149
|
-
registry.ready()
|
|
150
|
-
|
|
151
|
-
registry.declareCapability({
|
|
152
|
-
name: 'addon-pages',
|
|
153
|
-
scope: 'system',
|
|
154
|
-
mode: 'collection',
|
|
155
|
-
methods: {},
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
registry.registerProvider('addon-pages', 'benchmark', {
|
|
159
|
-
id: 'benchmark',
|
|
160
|
-
getPages: () => [
|
|
161
|
-
{
|
|
162
|
-
id: 'benchmark',
|
|
163
|
-
label: 'Benchmark',
|
|
164
|
-
icon: 'gauge',
|
|
165
|
-
path: '/addon/benchmark',
|
|
166
|
-
bundle: 'dist/pages/benchmark.js',
|
|
167
|
-
element: 'camstack-benchmark',
|
|
168
|
-
},
|
|
169
|
-
],
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
const providers = registry.getCollection<{ getPages(): unknown[] }>('addon-pages')
|
|
173
|
-
expect(providers).toHaveLength(1)
|
|
174
|
-
expect(providers[0].getPages()).toHaveLength(1)
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
it('CapabilityRegistry wires admin-ui singleton correctly', async () => {
|
|
178
|
-
const registry = new CapabilityRegistry(createMockLogger())
|
|
179
|
-
registry.ready()
|
|
180
|
-
|
|
181
|
-
registry.declareCapability({
|
|
182
|
-
name: 'admin-ui',
|
|
183
|
-
scope: 'system',
|
|
184
|
-
mode: 'singleton',
|
|
185
|
-
methods: {},
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
registry.registerProvider('admin-ui', 'admin-ui', {
|
|
189
|
-
getStaticDir: async () => ({ staticDir: '/some/path/dist' }),
|
|
190
|
-
getVersion: async () => ({ version: '0.1.0' }),
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
const ui = registry.getSingleton<{ getVersion(): Promise<{ readonly version: string }> }>(
|
|
194
|
-
'admin-ui',
|
|
195
|
-
)
|
|
196
|
-
expect(ui).toBeDefined()
|
|
197
|
-
const version = await ui!.getVersion()
|
|
198
|
-
expect(version.version).toBe('0.1.0')
|
|
199
|
-
})
|
|
200
|
-
})
|
|
@@ -1,17 +0,0 @@
|
|
|
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
|
-
})
|
|
@@ -1,67 +0,0 @@
|
|
|
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({
|
|
52
|
-
addonId: 'my-addon',
|
|
53
|
-
deviceId: 'cam-1',
|
|
54
|
-
field: 'resolution',
|
|
55
|
-
value: '1080p',
|
|
56
|
-
})
|
|
57
|
-
const result = await caller.getDeviceOverrides({ addonId: 'my-addon', deviceId: 'cam-1' })
|
|
58
|
-
expect(result).toEqual({ resolution: '1080p' })
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('updateGlobal merges with existing config without overwriting other fields', async () => {
|
|
62
|
-
await caller.updateGlobal({ addonId: 'my-addon', field: 'alpha', value: 1 })
|
|
63
|
-
await caller.updateGlobal({ addonId: 'my-addon', field: 'beta', value: 2 })
|
|
64
|
-
const result = await caller.getGlobal({ addonId: 'my-addon' })
|
|
65
|
-
expect(result).toEqual({ alpha: 1, beta: 2 })
|
|
66
|
-
})
|
|
67
|
-
})
|