@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
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
-
import { NetworkQualityService } from './network-quality.service'
|
|
3
|
-
|
|
4
|
-
describe('NetworkQualityService', () => {
|
|
5
|
-
let service: NetworkQualityService
|
|
6
|
-
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
service = new NetworkQualityService()
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
it('should return null for unknown device', () => {
|
|
12
|
-
expect(service.getDeviceStats(999)).toBeNull()
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
it('should track stream bitrate with rolling average', () => {
|
|
16
|
-
service.reportStreamStats(1, 'main', 8000)
|
|
17
|
-
service.reportStreamStats(1, 'main', 10000)
|
|
18
|
-
service.reportStreamStats(1, 'main', 9000)
|
|
19
|
-
|
|
20
|
-
const stats = service.getDeviceStats(1)
|
|
21
|
-
expect(stats).not.toBeNull()
|
|
22
|
-
expect(stats!.streams['main']!.observedBitrateKbps).toBe(9000)
|
|
23
|
-
expect(stats!.streams['main']!.peakBitrateKbps).toBe(10000)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('should track packet loss', () => {
|
|
27
|
-
service.reportStreamStats(1, 'main', 8000, 0.5)
|
|
28
|
-
service.reportStreamStats(1, 'main', 8000, 1.5)
|
|
29
|
-
|
|
30
|
-
const stats = service.getDeviceStats(1)
|
|
31
|
-
expect(stats!.streams['main']!.packetLossPercent).toBe(1.0)
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('should track client stats', () => {
|
|
35
|
-
service.reportClientStats(1, {
|
|
36
|
-
rttMs: 50,
|
|
37
|
-
jitterMs: 5,
|
|
38
|
-
estimatedBandwidthKbps: 20000,
|
|
39
|
-
packetLossPercent: 3,
|
|
40
|
-
})
|
|
41
|
-
const stats = service.getDeviceStats(1)
|
|
42
|
-
expect(stats!.client?.rttMs).toBe(50)
|
|
43
|
-
expect(stats!.client?.estimatedBandwidthKbps).toBe(20000)
|
|
44
|
-
expect(stats!.client?.packetLossPercent).toBe(3)
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('should list all device stats', () => {
|
|
48
|
-
service.reportStreamStats(1, 'main', 8000)
|
|
49
|
-
service.reportStreamStats(2, 'sub', 2000)
|
|
50
|
-
const all = service.getAllStats()
|
|
51
|
-
expect(all).toHaveLength(2)
|
|
52
|
-
})
|
|
53
|
-
})
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { NotificationService } from '@camstack/core'
|
|
2
|
-
import type { Notification } from '@camstack/types'
|
|
3
|
-
import { CapabilityService } from '../capability/capability.service'
|
|
4
|
-
import { LoggingService } from '../logging/logging.service'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* NestJS-injectable wrapper around the core NotificationService.
|
|
8
|
-
*
|
|
9
|
-
* Lazily creates the NotificationService on first use, wiring in the
|
|
10
|
-
* CapabilityRegistry for proxy-based output resolution.
|
|
11
|
-
*/
|
|
12
|
-
export class NotificationServiceWrapper {
|
|
13
|
-
private _service: NotificationService | null = null
|
|
14
|
-
|
|
15
|
-
constructor(
|
|
16
|
-
private readonly caps: CapabilityService,
|
|
17
|
-
private readonly logging: LoggingService,
|
|
18
|
-
) {}
|
|
19
|
-
|
|
20
|
-
get service(): NotificationService {
|
|
21
|
-
if (!this._service) {
|
|
22
|
-
this._service = new NotificationService(this.logging.createLogger('notifications'))
|
|
23
|
-
const registry = this.caps.getRegistry()
|
|
24
|
-
if (registry) {
|
|
25
|
-
this._service.setRegistry(registry)
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return this._service
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async notify(notification: Notification): Promise<void> {
|
|
32
|
-
return this.service.notify(notification)
|
|
33
|
-
}
|
|
34
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { ToastService } from '@camstack/core'
|
|
2
|
-
import type { Toast } from '@camstack/types'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* NestJS-injectable wrapper around the core ToastService.
|
|
6
|
-
*
|
|
7
|
-
* Provides toast broadcasting to connected UI clients.
|
|
8
|
-
*/
|
|
9
|
-
export class ToastServiceWrapper {
|
|
10
|
-
private readonly _service = new ToastService()
|
|
11
|
-
|
|
12
|
-
get service(): ToastService {
|
|
13
|
-
return this._service
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
broadcast(toast: Toast): void {
|
|
17
|
-
this._service.broadcast(toast)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
sendToUser(userId: string, toast: Toast): void {
|
|
21
|
-
this._service.sendToUser(userId, toast)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
subscribe(connectionId: string, userId: string, callback: (toast: Toast) => void): () => void {
|
|
25
|
-
return this._service.subscribe(connectionId, userId, callback)
|
|
26
|
-
}
|
|
27
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const PROVIDER_MANAGER = Symbol('PROVIDER_MANAGER')
|
|
@@ -1,444 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return -- test file: mock typing crosses generic boundaries */
|
|
2
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
-
/**
|
|
4
|
-
* ReplEngineService specs — exercises the actual current shape of the
|
|
5
|
-
* REPL service, including the SystemManager warm-boot path that
|
|
6
|
-
* recently hung the entire REPL when the broker tried to route
|
|
7
|
-
* `live.onEvent` through the Moleculer service registry.
|
|
8
|
-
*
|
|
9
|
-
* Mocks at the AddonRegistry boundary (the dependency of the service)
|
|
10
|
-
* — the rest is real: ReplEngine, SystemManager, EventBus adapter.
|
|
11
|
-
*/
|
|
12
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
13
|
-
import { ReplEngineService } from './repl-engine.service'
|
|
14
|
-
import { SystemEventBus } from '@camstack/core'
|
|
15
|
-
import type { ReplSessionContext } from '@camstack/core'
|
|
16
|
-
import type { DeviceBinding } from '@camstack/types'
|
|
17
|
-
import { DeviceType, EventCategory } from '@camstack/types'
|
|
18
|
-
|
|
19
|
-
// ── In-process broker api fixture ─────────────────────────────────────
|
|
20
|
-
//
|
|
21
|
-
// Mimics what `addonRegistry.getBrokerApi()` returns: a plain object
|
|
22
|
-
// with `<cap>.<method>.{query|mutate}` shape that resolves locally.
|
|
23
|
-
// The REPL service merges `live.onEvent` via the EventBus adapter on
|
|
24
|
-
// top of this base.
|
|
25
|
-
|
|
26
|
-
interface BrokerApiState {
|
|
27
|
-
bindings: Map<number, DeviceBinding>
|
|
28
|
-
snapshots: Record<string, Record<string, Record<string, unknown>>>
|
|
29
|
-
devices: Map<
|
|
30
|
-
number,
|
|
31
|
-
{
|
|
32
|
-
id: number
|
|
33
|
-
stableId: string
|
|
34
|
-
addonId: string
|
|
35
|
-
type: DeviceType
|
|
36
|
-
name: string
|
|
37
|
-
parentDeviceId: number | null
|
|
38
|
-
role: string | null
|
|
39
|
-
online: boolean
|
|
40
|
-
features: string[]
|
|
41
|
-
isCamera: boolean
|
|
42
|
-
config: Record<string, unknown>
|
|
43
|
-
}
|
|
44
|
-
>
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function makeBrokerApi(state: BrokerApiState): unknown {
|
|
48
|
-
return {
|
|
49
|
-
deviceManager: {
|
|
50
|
-
getAllBindings: {
|
|
51
|
-
query: vi.fn(async () => Array.from(state.bindings.values())),
|
|
52
|
-
},
|
|
53
|
-
listAll: {
|
|
54
|
-
query: vi.fn(async () => Array.from(state.devices.values())),
|
|
55
|
-
},
|
|
56
|
-
getBindings: {
|
|
57
|
-
query: vi.fn(
|
|
58
|
-
async ({ deviceId }: { deviceId: number }) =>
|
|
59
|
-
state.bindings.get(deviceId) ?? { deviceId, entries: [] },
|
|
60
|
-
),
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
deviceState: {
|
|
64
|
-
getCapSlice: {
|
|
65
|
-
query: vi.fn(
|
|
66
|
-
async ({ deviceId, capName }: { deviceId: number; capName: string }) =>
|
|
67
|
-
state.snapshots[String(deviceId)]?.[capName] ?? null,
|
|
68
|
-
),
|
|
69
|
-
},
|
|
70
|
-
getAllSnapshots: {
|
|
71
|
-
query: vi.fn(async () => state.snapshots),
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ── Harness ───────────────────────────────────────────────────────────
|
|
78
|
-
|
|
79
|
-
interface Harness {
|
|
80
|
-
service: ReplEngineService
|
|
81
|
-
state: BrokerApiState
|
|
82
|
-
eventBus: SystemEventBus
|
|
83
|
-
/** Re-fetch the broker api spies for assertion. */
|
|
84
|
-
brokerApi: any
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function makeHarness(seed?: Partial<BrokerApiState>): Harness {
|
|
88
|
-
// Reset the static SystemMirror singleton so each test gets a fresh
|
|
89
|
-
// warm-boot. The service caches it in static class fields — accept
|
|
90
|
-
// the test-only reach-in to keep tests isolated.
|
|
91
|
-
;(ReplEngineService as any).systemMirror = null
|
|
92
|
-
;(ReplEngineService as any).systemMirrorInit = null
|
|
93
|
-
|
|
94
|
-
const state: BrokerApiState = {
|
|
95
|
-
bindings: seed?.bindings ?? new Map(),
|
|
96
|
-
snapshots: seed?.snapshots ?? {},
|
|
97
|
-
devices: seed?.devices ?? new Map(),
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const brokerApi = makeBrokerApi(state)
|
|
101
|
-
const eventBus = new SystemEventBus(1000)
|
|
102
|
-
|
|
103
|
-
// Real EventBusService stub — the service only calls .subscribe.
|
|
104
|
-
const eventBusService = {
|
|
105
|
-
subscribe: (filter: any, handler: any) => eventBus.subscribe(filter, handler),
|
|
106
|
-
emit: (evt: any) => eventBus.emit(evt),
|
|
107
|
-
getRecent: () => [],
|
|
108
|
-
} as any
|
|
109
|
-
|
|
110
|
-
const deviceRegistry = {
|
|
111
|
-
getAll: () => Array.from(state.devices.values()),
|
|
112
|
-
getById: (id: number) => state.devices.get(id) ?? null,
|
|
113
|
-
getAllForAddon: (addonId: string) =>
|
|
114
|
-
Array.from(state.devices.values()).filter((d) => d.addonId === addonId),
|
|
115
|
-
getAllWithAddonId: () =>
|
|
116
|
-
Array.from(state.devices.values()).map((d) => ({ addonId: d.addonId, device: d })),
|
|
117
|
-
getAddonId: (id: number) => state.devices.get(id)?.addonId ?? null,
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const integrationRegistry = {
|
|
121
|
-
listIntegrations: vi.fn(async () => []),
|
|
122
|
-
getIntegration: vi.fn(async () => null),
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const addonRegistry = {
|
|
126
|
-
getBrokerApi: () => brokerApi,
|
|
127
|
-
getDeviceRegistry: () => deviceRegistry,
|
|
128
|
-
getIntegrationRegistry: () => integrationRegistry,
|
|
129
|
-
listAddons: () => [],
|
|
130
|
-
} as any
|
|
131
|
-
|
|
132
|
-
const loggingService = {
|
|
133
|
-
createLogger: () => ({
|
|
134
|
-
info: vi.fn(),
|
|
135
|
-
child: vi.fn(),
|
|
136
|
-
debug: vi.fn(),
|
|
137
|
-
warn: vi.fn(),
|
|
138
|
-
error: vi.fn(),
|
|
139
|
-
}),
|
|
140
|
-
} as any
|
|
141
|
-
|
|
142
|
-
const service = new ReplEngineService(addonRegistry, eventBusService, loggingService)
|
|
143
|
-
return { service, state, eventBus, brokerApi }
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const mkBinding = (deviceId: number, capNames: string[]): DeviceBinding => ({
|
|
147
|
-
deviceId,
|
|
148
|
-
entries: capNames.map((capName) => ({
|
|
149
|
-
capName,
|
|
150
|
-
kind: 'native' as const,
|
|
151
|
-
providerAddonId: 'test',
|
|
152
|
-
providerNodeId: 'hub',
|
|
153
|
-
nativeAddonId: 'test',
|
|
154
|
-
})),
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
const mkDevice = (
|
|
158
|
-
id: number,
|
|
159
|
-
overrides: Partial<BrokerApiState['devices'] extends Map<number, infer V> ? V : never> = {},
|
|
160
|
-
): BrokerApiState['devices'] extends Map<number, infer V> ? V : never =>
|
|
161
|
-
({
|
|
162
|
-
id,
|
|
163
|
-
stableId: `stable-${id}`,
|
|
164
|
-
addonId: 'addon-test',
|
|
165
|
-
type: DeviceType.Camera,
|
|
166
|
-
name: `Device ${id}`,
|
|
167
|
-
parentDeviceId: null,
|
|
168
|
-
role: null,
|
|
169
|
-
online: true,
|
|
170
|
-
features: [],
|
|
171
|
-
isCamera: true,
|
|
172
|
-
config: {},
|
|
173
|
-
...overrides,
|
|
174
|
-
}) as any
|
|
175
|
-
|
|
176
|
-
const SYSTEM_CTX: ReplSessionContext = { scope: { type: 'system' }, variables: {} }
|
|
177
|
-
|
|
178
|
-
// ── Tests ─────────────────────────────────────────────────────────────
|
|
179
|
-
|
|
180
|
-
describe('ReplEngineService — basic eval', () => {
|
|
181
|
-
let h: Harness
|
|
182
|
-
beforeEach(() => {
|
|
183
|
-
h = makeHarness()
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
it('evaluates simple arithmetic expressions', async () => {
|
|
187
|
-
const r = await h.service.execute('1 + 1', SYSTEM_CTX)
|
|
188
|
-
expect(r.type).toBe('value')
|
|
189
|
-
expect(r.output).toBe('2')
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
it('returns void for variable declarations', async () => {
|
|
193
|
-
const r = await h.service.execute('const x = 42', SYSTEM_CTX)
|
|
194
|
-
expect(r.type).toBe('void')
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
it('returns error type when user code throws', async () => {
|
|
198
|
-
const r = await h.service.execute("throw new Error('boom')", SYSTEM_CTX)
|
|
199
|
-
expect(r.type).toBe('error')
|
|
200
|
-
expect(r.output).toContain('boom')
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
it('reports duration for every eval', async () => {
|
|
204
|
-
const r = await h.service.execute('1 + 1', SYSTEM_CTX)
|
|
205
|
-
expect(r.duration).toBeGreaterThanOrEqual(0)
|
|
206
|
-
expect(r.duration).toBeLessThan(5_000)
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
it('isolates blocked globals — fetch / setTimeout / require unavailable', async () => {
|
|
210
|
-
const fetchR = await h.service.execute('typeof fetch', SYSTEM_CTX)
|
|
211
|
-
expect(fetchR.output).toBe("'undefined'")
|
|
212
|
-
const reqR = await h.service.execute('typeof require', SYSTEM_CTX)
|
|
213
|
-
expect(reqR.output).toBe("'undefined'")
|
|
214
|
-
const setTimeoutR = await h.service.execute('typeof setTimeout', SYSTEM_CTX)
|
|
215
|
-
expect(setTimeoutR.output).toBe("'undefined'")
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
it('exposes JSON / Math / Date as standard globals', async () => {
|
|
219
|
-
const json = await h.service.execute('JSON.stringify({a:1})', SYSTEM_CTX)
|
|
220
|
-
expect(json.output).toContain('"a":1')
|
|
221
|
-
const math = await h.service.execute('Math.max(1, 5, 3)', SYSTEM_CTX)
|
|
222
|
-
expect(math.output).toBe('5')
|
|
223
|
-
})
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
describe('ReplEngineService — system scope sandbox', () => {
|
|
227
|
-
let h: Harness
|
|
228
|
-
beforeEach(() => {
|
|
229
|
-
h = makeHarness({
|
|
230
|
-
bindings: new Map([[1, mkBinding(1, ['battery'])]]),
|
|
231
|
-
devices: new Map([[1, mkDevice(1, { name: 'Test Cam' })]]),
|
|
232
|
-
snapshots: { '1': { battery: { sleeping: true, percentage: 50 } } },
|
|
233
|
-
})
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
it('binds `sm` to the warm-booted SystemManager', async () => {
|
|
237
|
-
const r = await h.service.execute('typeof sm', SYSTEM_CTX)
|
|
238
|
-
expect(r.output).toBe("'object'")
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
it('sm.getDeviceById returns a typed proxy with sync state', async () => {
|
|
242
|
-
const r = await h.service.execute(
|
|
243
|
-
'sm.getDeviceById(1).state.battery.value.sleeping',
|
|
244
|
-
SYSTEM_CTX,
|
|
245
|
-
)
|
|
246
|
-
expect(r.type).toBe('value')
|
|
247
|
-
expect(r.output).toBe('true')
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
it('sm.getDeviceByName resolves the metadata mirror', async () => {
|
|
251
|
-
const r = await h.service.execute("sm.getDeviceByName('Test Cam').deviceId", SYSTEM_CTX)
|
|
252
|
-
expect(r.output).toBe('1')
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
it('sm.summary returns counts', async () => {
|
|
256
|
-
const r = await h.service.execute('sm.summary().totalDevices', SYSTEM_CTX)
|
|
257
|
-
expect(r.output).toBe('1')
|
|
258
|
-
})
|
|
259
|
-
|
|
260
|
-
it('sm.query filters by addonId + caps + online', async () => {
|
|
261
|
-
h.state.bindings.set(2, mkBinding(2, ['snapshot']))
|
|
262
|
-
h.state.devices.set(2, mkDevice(2, { name: 'Cam 2', addonId: 'reolink', online: true }))
|
|
263
|
-
h.state.devices.set(1, mkDevice(1, { name: 'Cam 1', addonId: 'reolink', online: false }))
|
|
264
|
-
// Re-warm-boot the SM so the new devices show up.
|
|
265
|
-
;(ReplEngineService as any).systemMirror = null
|
|
266
|
-
;(ReplEngineService as any).systemMirrorInit = null
|
|
267
|
-
|
|
268
|
-
const r = await h.service.execute(
|
|
269
|
-
"sm.query({ addonId: 'reolink', online: true }).map(d => d.deviceId)",
|
|
270
|
-
SYSTEM_CTX,
|
|
271
|
-
)
|
|
272
|
-
expect(r.output).toContain('2')
|
|
273
|
-
expect(r.output).not.toContain('1,')
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
it('legacy variables remain accessible (backward-compat)', async () => {
|
|
277
|
-
const r1 = await h.service.execute('typeof addonRegistry', SYSTEM_CTX)
|
|
278
|
-
expect(r1.output).toBe("'object'")
|
|
279
|
-
const r2 = await h.service.execute('typeof eventBus', SYSTEM_CTX)
|
|
280
|
-
expect(r2.output).toBe("'object'")
|
|
281
|
-
const r3 = await h.service.execute('typeof getDevice', SYSTEM_CTX)
|
|
282
|
-
expect(r3.output).toBe("'function'")
|
|
283
|
-
})
|
|
284
|
-
})
|
|
285
|
-
|
|
286
|
-
describe('ReplEngineService — device scope sandbox', () => {
|
|
287
|
-
const DEVICE_CTX: ReplSessionContext = {
|
|
288
|
-
scope: { type: 'device', deviceId: 8 },
|
|
289
|
-
variables: {},
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
let h: Harness
|
|
293
|
-
beforeEach(() => {
|
|
294
|
-
h = makeHarness({
|
|
295
|
-
bindings: new Map([[8, mkBinding(8, ['battery', 'snapshot'])]]),
|
|
296
|
-
devices: new Map([[8, mkDevice(8, { name: 'Sala', addonId: 'reolink' })]]),
|
|
297
|
-
snapshots: { '8': { battery: { sleeping: false, percentage: 88 } } },
|
|
298
|
-
})
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
it('pre-binds `device` as a DeviceProxy with sync state', async () => {
|
|
302
|
-
const r = await h.service.execute('device.state.battery.value.percentage', DEVICE_CTX)
|
|
303
|
-
expect(r.type).toBe('value')
|
|
304
|
-
expect(r.output).toBe('88')
|
|
305
|
-
})
|
|
306
|
-
|
|
307
|
-
it('exposes deviceId, info, sm, rawDevice', async () => {
|
|
308
|
-
const a = await h.service.execute('deviceId', DEVICE_CTX)
|
|
309
|
-
expect(a.output).toBe('8')
|
|
310
|
-
const b = await h.service.execute('info.name', DEVICE_CTX)
|
|
311
|
-
expect(b.output).toBe("'Sala'")
|
|
312
|
-
const c = await h.service.execute('typeof sm', DEVICE_CTX)
|
|
313
|
-
expect(c.output).toBe("'object'")
|
|
314
|
-
const d = await h.service.execute('typeof rawDevice', DEVICE_CTX)
|
|
315
|
-
// rawDevice is the raw IDevice from registry — null in our harness for device 8 (we use plain objects)
|
|
316
|
-
// but the type check ensures the binding is in scope.
|
|
317
|
-
expect(['undefined', "'object'"]).toContain(d.output)
|
|
318
|
-
})
|
|
319
|
-
|
|
320
|
-
it('device proxy exposes .state for every cap with runtimeState', async () => {
|
|
321
|
-
const r = await h.service.execute('typeof device.state.battery.subscribe', DEVICE_CTX)
|
|
322
|
-
expect(r.output).toBe("'function'")
|
|
323
|
-
const r2 = await h.service.execute('typeof device.state.motion.value', DEVICE_CTX)
|
|
324
|
-
expect(r2.output).toBe("'undefined'")
|
|
325
|
-
})
|
|
326
|
-
|
|
327
|
-
it('device proxy exposes cap method dispatchers', async () => {
|
|
328
|
-
const r = await h.service.execute('typeof device.snapshot.getSnapshot', DEVICE_CTX)
|
|
329
|
-
expect(r.output).toBe("'function'")
|
|
330
|
-
})
|
|
331
|
-
})
|
|
332
|
-
|
|
333
|
-
describe('ReplEngineService — SystemManager warm-boot resilience', () => {
|
|
334
|
-
it("warm-boot does NOT hang on `live.onEvent` (regression — broker can't route)", async () => {
|
|
335
|
-
// The bug: `getBrokerApi().live.onEvent.subscribe(...)` polls
|
|
336
|
-
// forever for a Moleculer service that doesn't exist. The fix
|
|
337
|
-
// injects a direct EventBus adapter for `live` so SM init never
|
|
338
|
-
// touches the broker for subscriptions.
|
|
339
|
-
const h = makeHarness({
|
|
340
|
-
bindings: new Map([[1, mkBinding(1, ['battery'])]]),
|
|
341
|
-
devices: new Map([[1, mkDevice(1)]]),
|
|
342
|
-
snapshots: {},
|
|
343
|
-
})
|
|
344
|
-
|
|
345
|
-
// 5 second hard ceiling — way above the 15s SM timeout but below
|
|
346
|
-
// the broker's infinite poll. If the bug regresses this test
|
|
347
|
-
// hangs the runner, not silently passes.
|
|
348
|
-
const result = await Promise.race([
|
|
349
|
-
h.service.execute('typeof sm', SYSTEM_CTX),
|
|
350
|
-
new Promise<never>((_, reject) =>
|
|
351
|
-
setTimeout(() => reject(new Error('test ceiling — REPL eval hung')), 5_000),
|
|
352
|
-
),
|
|
353
|
-
])
|
|
354
|
-
expect(result.type).toBe('value')
|
|
355
|
-
expect(result.output).toBe("'object'")
|
|
356
|
-
})
|
|
357
|
-
|
|
358
|
-
it('first eval triggers warm-boot; subsequent evals reuse the cached SystemManager', async () => {
|
|
359
|
-
const h = makeHarness({
|
|
360
|
-
bindings: new Map([[1, mkBinding(1, ['battery'])]]),
|
|
361
|
-
devices: new Map([[1, mkDevice(1)]]),
|
|
362
|
-
snapshots: {},
|
|
363
|
-
})
|
|
364
|
-
|
|
365
|
-
await h.service.execute('typeof sm', SYSTEM_CTX)
|
|
366
|
-
await h.service.execute('typeof sm', SYSTEM_CTX)
|
|
367
|
-
await h.service.execute('typeof sm', SYSTEM_CTX)
|
|
368
|
-
|
|
369
|
-
expect(h.brokerApi.deviceManager.getAllBindings.query).toHaveBeenCalledTimes(1)
|
|
370
|
-
expect(h.brokerApi.deviceState.getAllSnapshots.query).toHaveBeenCalledTimes(1)
|
|
371
|
-
expect(h.brokerApi.deviceManager.listAll.query).toHaveBeenCalledTimes(1)
|
|
372
|
-
})
|
|
373
|
-
|
|
374
|
-
it('live.onEvent uses the in-process EventBus, not the broker', async () => {
|
|
375
|
-
const h = makeHarness({
|
|
376
|
-
bindings: new Map([[1, mkBinding(1, ['battery'])]]),
|
|
377
|
-
devices: new Map([[1, mkDevice(1)]]),
|
|
378
|
-
snapshots: { '1': { battery: { sleeping: true, percentage: 50 } } },
|
|
379
|
-
})
|
|
380
|
-
|
|
381
|
-
// Boot SM via first eval.
|
|
382
|
-
await h.service.execute('typeof sm', SYSTEM_CTX)
|
|
383
|
-
|
|
384
|
-
// Fire an in-process state-change event — SM mirror should pick it up.
|
|
385
|
-
h.eventBus.emit({
|
|
386
|
-
id: 'test-1',
|
|
387
|
-
timestamp: Date.now(),
|
|
388
|
-
category: EventCategory.DeviceStateChanged,
|
|
389
|
-
source: { type: 'device', id: 1, deviceId: 1 },
|
|
390
|
-
data: { deviceId: 1, capName: 'battery', slice: { sleeping: false, percentage: 90 } },
|
|
391
|
-
})
|
|
392
|
-
// Microtask flush.
|
|
393
|
-
await new Promise((r) => setTimeout(r, 10))
|
|
394
|
-
|
|
395
|
-
const r = await h.service.execute(
|
|
396
|
-
'sm.getDeviceById(1).state.battery.value.sleeping',
|
|
397
|
-
SYSTEM_CTX,
|
|
398
|
-
)
|
|
399
|
-
expect(r.output).toBe('false')
|
|
400
|
-
})
|
|
401
|
-
})
|
|
402
|
-
|
|
403
|
-
describe('ReplEngineService — error paths', () => {
|
|
404
|
-
let h: Harness
|
|
405
|
-
beforeEach(() => {
|
|
406
|
-
h = makeHarness()
|
|
407
|
-
})
|
|
408
|
-
|
|
409
|
-
it('returns error when accessing undefined variables', async () => {
|
|
410
|
-
const r = await h.service.execute('undefinedVariable.foo', SYSTEM_CTX)
|
|
411
|
-
expect(r.type).toBe('error')
|
|
412
|
-
})
|
|
413
|
-
|
|
414
|
-
it('error output does not leak the engine internals', async () => {
|
|
415
|
-
const r = await h.service.execute('throw new TypeError("user error")', SYSTEM_CTX)
|
|
416
|
-
expect(r.type).toBe('error')
|
|
417
|
-
expect(r.output).toContain('user error')
|
|
418
|
-
// Should NOT contain stack frames from repl-engine.ts itself.
|
|
419
|
-
expect(r.output).not.toContain('repl-engine.ts')
|
|
420
|
-
})
|
|
421
|
-
})
|
|
422
|
-
|
|
423
|
-
describe('ReplEngineService — completions', () => {
|
|
424
|
-
let h: Harness
|
|
425
|
-
beforeEach(() => {
|
|
426
|
-
h = makeHarness({
|
|
427
|
-
bindings: new Map([[1, mkBinding(1, ['battery'])]]),
|
|
428
|
-
devices: new Map([[1, mkDevice(1)]]),
|
|
429
|
-
snapshots: {},
|
|
430
|
-
})
|
|
431
|
-
})
|
|
432
|
-
|
|
433
|
-
it('returns sandbox keys when partial is empty', async () => {
|
|
434
|
-
const completions = await h.service.getCompletions('', SYSTEM_CTX)
|
|
435
|
-
expect(completions).toContain('sm')
|
|
436
|
-
expect(completions).toContain('JSON')
|
|
437
|
-
expect(completions).toContain('Math')
|
|
438
|
-
})
|
|
439
|
-
|
|
440
|
-
it('filters by partial prefix', async () => {
|
|
441
|
-
const completions = await h.service.getCompletions('sm', SYSTEM_CTX)
|
|
442
|
-
expect(completions).toContain('sm')
|
|
443
|
-
})
|
|
444
|
-
})
|