@camstack/server 1.0.0 → 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.
Files changed (234) hide show
  1. package/{src/agent-status-page.ts → dist/agent-status-page.js} +30 -45
  2. package/dist/api/addon-upload.js +441 -0
  3. package/dist/api/addons-custom.router.js +91 -0
  4. package/dist/api/auth-whoami.js +55 -0
  5. package/dist/api/bridge-addons.router.js +109 -0
  6. package/dist/api/capabilities.router.js +229 -0
  7. package/dist/api/core/addon-settings.router.js +117 -0
  8. package/dist/api/core/agents.router.js +73 -0
  9. package/dist/api/core/auth.router.js +286 -0
  10. package/dist/api/core/bulk-update-coordinator.js +229 -0
  11. package/dist/api/core/cap-providers.js +1124 -0
  12. package/dist/api/core/capabilities.router.js +138 -0
  13. package/dist/api/core/collection-preference.js +17 -0
  14. package/dist/api/core/event-bus-proxy.router.js +45 -0
  15. package/dist/api/core/hwaccel.router.js +91 -0
  16. package/dist/api/core/live-events.router.js +61 -0
  17. package/dist/api/core/logs.router.js +172 -0
  18. package/dist/api/core/notifications.router.js +67 -0
  19. package/dist/api/core/repl.router.js +35 -0
  20. package/dist/api/core/settings-backend.router.js +121 -0
  21. package/dist/api/core/stream-probe.router.js +58 -0
  22. package/dist/api/core/system-events.router.js +100 -0
  23. package/dist/api/health/health.routes.js +68 -0
  24. package/{src/api/oauth2/consent-page.ts → dist/api/oauth2/consent-page.js} +11 -20
  25. package/dist/api/oauth2/oauth2-routes.js +219 -0
  26. package/dist/api/trpc/cap-mount-helpers.js +194 -0
  27. package/dist/api/trpc/cap-route-error-formatter.js +133 -0
  28. package/dist/api/trpc/client-ip.js +147 -0
  29. package/dist/api/trpc/core-cap-bridge.js +115 -0
  30. package/dist/api/trpc/generated-cap-mounts.js +388 -0
  31. package/dist/api/trpc/generated-cap-routers.js +7635 -0
  32. package/dist/api/trpc/scope-access.js +93 -0
  33. package/dist/api/trpc/trpc.context.js +184 -0
  34. package/dist/api/trpc/trpc.middleware.js +139 -0
  35. package/dist/api/trpc/trpc.router.js +188 -0
  36. package/dist/auth/session-cookie.js +47 -0
  37. package/dist/boot/boot-config.js +241 -0
  38. package/dist/boot/integration-id-backfill.js +76 -0
  39. package/dist/boot/post-boot.service.js +85 -0
  40. package/dist/core/addon/addon-call-gateway.js +99 -0
  41. package/dist/core/addon/addon-package.service.js +1560 -0
  42. package/dist/core/addon/addon-registry.service.js +2739 -0
  43. package/{src/core/addon/addon-row-manifest.ts → dist/core/addon/addon-row-manifest.js} +5 -5
  44. package/dist/core/addon/addon-search.service.js +62 -0
  45. package/dist/core/addon/addon-settings-provider.js +102 -0
  46. package/dist/core/addon/addon.tokens.js +5 -0
  47. package/dist/core/addon-bridge/addon-bridge.service.js +145 -0
  48. package/dist/core/addon-pages/addon-pages.service.js +107 -0
  49. package/dist/core/addon-widgets/addon-widgets.service.js +120 -0
  50. package/dist/core/agent/agent-registry.service.js +477 -0
  51. package/dist/core/auth/auth.service.js +10 -0
  52. package/dist/core/capability/capability.service.js +58 -0
  53. package/dist/core/config/config.schema.js +7 -0
  54. package/dist/core/config/config.service.js +10 -0
  55. package/dist/core/events/event-bus.service.js +83 -0
  56. package/dist/core/feature/feature.service.js +10 -0
  57. package/dist/core/lifecycle/lifecycle-state-machine.js +6 -0
  58. package/dist/core/logging/log-ring-buffer.js +6 -0
  59. package/dist/core/logging/logging.service.js +130 -0
  60. package/dist/core/logging/scoped-logger.js +6 -0
  61. package/dist/core/moleculer/cap-call-fn.js +50 -0
  62. package/dist/core/moleculer/cap-route-authority.js +122 -0
  63. package/dist/core/moleculer/moleculer.service.js +898 -0
  64. package/dist/core/network/network-quality.service.js +7 -0
  65. package/dist/core/notification/notification-wrapper.service.js +33 -0
  66. package/dist/core/notification/toast-wrapper.service.js +25 -0
  67. package/dist/core/provider/provider.tokens.js +4 -0
  68. package/dist/core/repl/repl-engine.service.js +140 -0
  69. package/dist/core/storage/fs-storage-backend.js +6 -0
  70. package/dist/core/storage/storage-location-manager.js +6 -0
  71. package/dist/core/storage/storage.service.js +7 -0
  72. package/dist/core/streaming/stream-probe.service.js +209 -0
  73. package/dist/core/topology/topology-emitter.service.js +106 -0
  74. package/dist/launcher.js +325 -0
  75. package/dist/main.js +1098 -0
  76. package/dist/manual-boot.js +227 -0
  77. package/package.json +5 -1
  78. package/src/__tests__/addon-install-e2e.test.ts +0 -74
  79. package/src/__tests__/addon-pages-e2e.test.ts +0 -200
  80. package/src/__tests__/addon-route-session.test.ts +0 -17
  81. package/src/__tests__/addon-settings-router.spec.ts +0 -67
  82. package/src/__tests__/addon-upload.spec.ts +0 -475
  83. package/src/__tests__/agent-registry.spec.ts +0 -179
  84. package/src/__tests__/agent-status-page.spec.ts +0 -82
  85. package/src/__tests__/auth-session-cookie.test.ts +0 -48
  86. package/src/__tests__/bulk-update-coordinator.spec.ts +0 -303
  87. package/src/__tests__/cap-ownership-authority.spec.ts +0 -431
  88. package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +0 -206
  89. package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +0 -37
  90. package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +0 -110
  91. package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +0 -292
  92. package/src/__tests__/cap-providers-bulk-update.spec.ts +0 -408
  93. package/src/__tests__/cap-route-adapter.spec.ts +0 -302
  94. package/src/__tests__/cap-routers/_meta.spec.ts +0 -199
  95. package/src/__tests__/cap-routers/addon-settings.router.spec.ts +0 -115
  96. package/src/__tests__/cap-routers/broker-routing.router.spec.ts +0 -177
  97. package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +0 -125
  98. package/src/__tests__/cap-routers/capabilities-node.spec.ts +0 -68
  99. package/src/__tests__/cap-routers/device-link-overlay.spec.ts +0 -137
  100. package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +0 -194
  101. package/src/__tests__/cap-routers/harness.ts +0 -163
  102. package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +0 -133
  103. package/src/__tests__/cap-routers/null-provider-guard.spec.ts +0 -64
  104. package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +0 -159
  105. package/src/__tests__/cap-routers/settings-store.router.spec.ts +0 -291
  106. package/src/__tests__/capability-e2e.test.ts +0 -384
  107. package/src/__tests__/cli-e2e.test.ts +0 -150
  108. package/src/__tests__/core-cap-bridge.spec.ts +0 -91
  109. package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +0 -40
  110. package/src/__tests__/device-settings-contribution-dispatch.spec.ts +0 -280
  111. package/src/__tests__/embedded-deps-e2e.test.ts +0 -125
  112. package/src/__tests__/event-bus-proxy-router.spec.ts +0 -75
  113. package/src/__tests__/fixtures/mock-analysis-addon-a.ts +0 -37
  114. package/src/__tests__/fixtures/mock-analysis-addon-b.ts +0 -37
  115. package/src/__tests__/fixtures/mock-log-addon.ts +0 -37
  116. package/src/__tests__/fixtures/mock-storage-addon.ts +0 -40
  117. package/src/__tests__/framework-allowlist.spec.ts +0 -96
  118. package/src/__tests__/framework-installer-defer-restart.spec.ts +0 -165
  119. package/src/__tests__/https-e2e.test.ts +0 -124
  120. package/src/__tests__/lifecycle-e2e.test.ts +0 -189
  121. package/src/__tests__/live-events-subscription.spec.ts +0 -149
  122. package/src/__tests__/moleculer/uds-readiness.spec.ts +0 -150
  123. package/src/__tests__/moleculer/uds-topology.spec.ts +0 -418
  124. package/src/__tests__/moleculer/uds-unowned-call.spec.ts +0 -383
  125. package/src/__tests__/moleculer-register-node-idempotency.spec.ts +0 -273
  126. package/src/__tests__/native-cap-route.spec.ts +0 -427
  127. package/src/__tests__/oauth2-account-linking.spec.ts +0 -867
  128. package/src/__tests__/post-boot-restart.spec.ts +0 -161
  129. package/src/__tests__/singleton-contention.test.ts +0 -499
  130. package/src/__tests__/streaming-diagnostic.test.ts +0 -615
  131. package/src/__tests__/streaming-scale.test.ts +0 -314
  132. package/src/__tests__/uds-addon-call-wiring.spec.ts +0 -242
  133. package/src/__tests__/uds-log-ingest.spec.ts +0 -183
  134. package/src/api/__tests__/addons-custom.spec.ts +0 -148
  135. package/src/api/__tests__/capabilities.router.test.ts +0 -56
  136. package/src/api/addon-upload.ts +0 -529
  137. package/src/api/addons-custom.router.ts +0 -101
  138. package/src/api/auth-whoami.ts +0 -101
  139. package/src/api/bridge-addons.router.ts +0 -122
  140. package/src/api/capabilities.router.ts +0 -265
  141. package/src/api/core/__tests__/auth-router-totp.spec.ts +0 -297
  142. package/src/api/core/__tests__/integration-markers.spec.ts +0 -10
  143. package/src/api/core/addon-settings.router.ts +0 -127
  144. package/src/api/core/agents.router.ts +0 -86
  145. package/src/api/core/auth.router.ts +0 -322
  146. package/src/api/core/bulk-update-coordinator.ts +0 -305
  147. package/src/api/core/cap-providers.ts +0 -1339
  148. package/src/api/core/capabilities.router.ts +0 -149
  149. package/src/api/core/collection-preference.ts +0 -40
  150. package/src/api/core/event-bus-proxy.router.ts +0 -45
  151. package/src/api/core/hwaccel.router.ts +0 -108
  152. package/src/api/core/live-events.router.ts +0 -67
  153. package/src/api/core/logs.router.ts +0 -195
  154. package/src/api/core/notifications.router.ts +0 -66
  155. package/src/api/core/repl.router.ts +0 -39
  156. package/src/api/core/settings-backend.router.ts +0 -140
  157. package/src/api/core/stream-probe.router.ts +0 -57
  158. package/src/api/core/system-events.router.ts +0 -125
  159. package/src/api/health/health.routes.ts +0 -117
  160. package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +0 -62
  161. package/src/api/oauth2/oauth2-routes.ts +0 -281
  162. package/src/api/trpc/__tests__/client-ip.spec.ts +0 -146
  163. package/src/api/trpc/__tests__/scope-access-device.spec.ts +0 -268
  164. package/src/api/trpc/__tests__/scope-access.spec.ts +0 -102
  165. package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +0 -136
  166. package/src/api/trpc/cap-mount-helpers.ts +0 -245
  167. package/src/api/trpc/cap-route-error-formatter.ts +0 -171
  168. package/src/api/trpc/client-ip.ts +0 -147
  169. package/src/api/trpc/core-cap-bridge.ts +0 -154
  170. package/src/api/trpc/generated-cap-mounts.ts +0 -1240
  171. package/src/api/trpc/generated-cap-routers.ts +0 -11523
  172. package/src/api/trpc/scope-access.ts +0 -110
  173. package/src/api/trpc/trpc.context.ts +0 -258
  174. package/src/api/trpc/trpc.middleware.ts +0 -146
  175. package/src/api/trpc/trpc.router.ts +0 -389
  176. package/src/auth/session-cookie.ts +0 -54
  177. package/src/boot/__tests__/integration-id-backfill.spec.ts +0 -131
  178. package/src/boot/boot-config.ts +0 -259
  179. package/src/boot/integration-id-backfill.ts +0 -109
  180. package/src/boot/post-boot.service.ts +0 -105
  181. package/src/core/addon/__tests__/addon-registry-capability.test.ts +0 -62
  182. package/src/core/addon/__tests__/addon-row-manifest.spec.ts +0 -62
  183. package/src/core/addon/addon-call-gateway.ts +0 -171
  184. package/src/core/addon/addon-package.service.ts +0 -1787
  185. package/src/core/addon/addon-registry.service.ts +0 -3130
  186. package/src/core/addon/addon-search.service.ts +0 -91
  187. package/src/core/addon/addon-settings-provider.ts +0 -220
  188. package/src/core/addon/addon.tokens.ts +0 -2
  189. package/src/core/addon-bridge/addon-bridge.service.ts +0 -130
  190. package/src/core/addon-pages/addon-pages.service.spec.ts +0 -117
  191. package/src/core/addon-pages/addon-pages.service.ts +0 -82
  192. package/src/core/addon-widgets/addon-widgets.service.ts +0 -95
  193. package/src/core/agent/agent-registry.service.ts +0 -529
  194. package/src/core/auth/auth.service.spec.ts +0 -86
  195. package/src/core/auth/auth.service.ts +0 -8
  196. package/src/core/capability/capability.service.ts +0 -66
  197. package/src/core/config/config.schema.ts +0 -3
  198. package/src/core/config/config.service.spec.ts +0 -175
  199. package/src/core/config/config.service.ts +0 -7
  200. package/src/core/events/event-bus.service.spec.ts +0 -235
  201. package/src/core/events/event-bus.service.ts +0 -89
  202. package/src/core/feature/feature.service.spec.ts +0 -99
  203. package/src/core/feature/feature.service.ts +0 -8
  204. package/src/core/lifecycle/lifecycle-state-machine.spec.ts +0 -166
  205. package/src/core/lifecycle/lifecycle-state-machine.ts +0 -3
  206. package/src/core/logging/log-ring-buffer.ts +0 -3
  207. package/src/core/logging/logging.service.spec.ts +0 -287
  208. package/src/core/logging/logging.service.ts +0 -143
  209. package/src/core/logging/scoped-logger.ts +0 -3
  210. package/src/core/moleculer/cap-call-fn.spec.ts +0 -173
  211. package/src/core/moleculer/cap-call-fn.ts +0 -107
  212. package/src/core/moleculer/cap-route-authority.ts +0 -194
  213. package/src/core/moleculer/moleculer.service.ts +0 -1072
  214. package/src/core/network/network-quality.service.spec.ts +0 -53
  215. package/src/core/network/network-quality.service.ts +0 -5
  216. package/src/core/notification/notification-wrapper.service.ts +0 -34
  217. package/src/core/notification/toast-wrapper.service.ts +0 -27
  218. package/src/core/provider/provider.tokens.ts +0 -1
  219. package/src/core/repl/repl-engine.service.spec.ts +0 -444
  220. package/src/core/repl/repl-engine.service.ts +0 -155
  221. package/src/core/storage/fs-storage-backend.spec.ts +0 -70
  222. package/src/core/storage/fs-storage-backend.ts +0 -3
  223. package/src/core/storage/storage-location-manager.spec.ts +0 -130
  224. package/src/core/storage/storage-location-manager.ts +0 -3
  225. package/src/core/storage/storage.service.spec.ts +0 -73
  226. package/src/core/storage/storage.service.ts +0 -3
  227. package/src/core/streaming/stream-probe.service.ts +0 -221
  228. package/src/core/topology/topology-emitter.service.ts +0 -105
  229. package/src/launcher.ts +0 -314
  230. package/src/main.ts +0 -1245
  231. package/src/manual-boot.ts +0 -301
  232. package/tsconfig.build.json +0 -8
  233. package/tsconfig.json +0 -33
  234. package/vitest.config.ts +0 -26
@@ -1,82 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { renderAgentStatusPage, type AgentStatusData } from '../agent-status-page'
3
-
4
- const makeStatusData = (overrides: Partial<AgentStatusData> = {}): AgentStatusData => ({
5
- agentId: 'agent-abc12345',
6
- agentName: 'test-agent',
7
- hubUrl: 'ws://localhost:4443/agent',
8
- connected: true,
9
- activeTaskCount: 3,
10
- taskTypes: ['pipeline.decode', 'pipeline.detect'],
11
- platform: 'darwin',
12
- arch: 'arm64',
13
- cpuCores: 8,
14
- memoryTotalMB: 16384,
15
- memoryFreeMB: 8192,
16
- uptime: 3661,
17
- ...overrides,
18
- })
19
-
20
- describe('renderAgentStatusPage', () => {
21
- it('renders valid HTML', () => {
22
- const html = renderAgentStatusPage(makeStatusData())
23
- expect(html).toContain('<!DOCTYPE html>')
24
- expect(html).toContain('</html>')
25
- })
26
-
27
- it('includes agent name and ID', () => {
28
- const html = renderAgentStatusPage(makeStatusData())
29
- expect(html).toContain('test-agent')
30
- expect(html).toContain('agent-abc12345')
31
- })
32
-
33
- it('shows connected status with green dot', () => {
34
- const html = renderAgentStatusPage(makeStatusData({ connected: true }))
35
- expect(html).toContain('Connected')
36
- expect(html).toContain('#22c55e')
37
- })
38
-
39
- it('shows disconnected status with red dot', () => {
40
- const html = renderAgentStatusPage(makeStatusData({ connected: false }))
41
- expect(html).toContain('Disconnected')
42
- expect(html).toContain('#ef4444')
43
- })
44
-
45
- it('displays task types', () => {
46
- const html = renderAgentStatusPage(makeStatusData())
47
- expect(html).toContain('pipeline.decode')
48
- expect(html).toContain('pipeline.detect')
49
- })
50
-
51
- it('shows "no task handlers" when list is empty', () => {
52
- const html = renderAgentStatusPage(makeStatusData({ taskTypes: [] }))
53
- expect(html).toContain('No task handlers registered')
54
- })
55
-
56
- it('displays hardware info', () => {
57
- const html = renderAgentStatusPage(makeStatusData())
58
- expect(html).toContain('darwin')
59
- expect(html).toContain('arm64')
60
- expect(html).toContain('8')
61
- })
62
-
63
- it('calculates memory usage', () => {
64
- const html = renderAgentStatusPage(makeStatusData({ memoryTotalMB: 16384, memoryFreeMB: 4096 }))
65
- // 12288 / 16384 = 75%
66
- expect(html).toContain('12288')
67
- expect(html).toContain('75%')
68
- })
69
-
70
- it('escapes HTML in user-provided strings', () => {
71
- const html = renderAgentStatusPage(
72
- makeStatusData({ agentName: '<script>alert("xss")</script>' }),
73
- )
74
- expect(html).not.toContain('<script>')
75
- expect(html).toContain('&lt;script&gt;')
76
- })
77
-
78
- it('auto-refreshes every 5 seconds', () => {
79
- const html = renderAgentStatusPage(makeStatusData())
80
- expect(html).toContain('http-equiv="refresh" content="5"')
81
- })
82
- })
@@ -1,48 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import {
3
- buildSessionCookie,
4
- clearSessionCookie,
5
- SESSION_COOKIE,
6
- isEmbedRedirectTarget,
7
- } from '../auth/session-cookie.js'
8
-
9
- describe('session cookie', () => {
10
- it('buildSessionCookie produces an httpOnly lax cookie carrying the token', () => {
11
- const c = buildSessionCookie('jwt-abc', 3600)
12
- expect(c.name).toBe(SESSION_COOKIE)
13
- expect(c.value).toBe('jwt-abc')
14
- expect(c.options.httpOnly).toBe(true)
15
- expect(c.options.sameSite).toBe('lax')
16
- expect(c.options.secure).toBe(true)
17
- expect(c.options.path).toBe('/')
18
- expect(c.options.maxAge).toBe(3600)
19
- })
20
-
21
- it('clearSessionCookie expires the cookie', () => {
22
- const c = clearSessionCookie()
23
- expect(c.name).toBe(SESSION_COOKIE)
24
- expect(c.options.maxAge).toBe(0)
25
- })
26
- })
27
-
28
- describe('isEmbedRedirectTarget', () => {
29
- it('accepts a stream-broker embed path (with query)', () => {
30
- expect(isEmbedRedirectTarget('/addon/stream-broker/embed/?mode=grid')).toBe(true)
31
- expect(isEmbedRedirectTarget('/addon/stream-broker/embed/?mode=player&devices=7,8')).toBe(true)
32
- expect(isEmbedRedirectTarget('/addon/stream-broker/embed/')).toBe(true)
33
- })
34
-
35
- it('rejects open redirects (absolute / protocol-relative / backslash)', () => {
36
- expect(isEmbedRedirectTarget('https://evil.com/addon/stream-broker/embed/')).toBe(false)
37
- expect(isEmbedRedirectTarget('//evil.com')).toBe(false)
38
- expect(isEmbedRedirectTarget('/\\evil.com')).toBe(false)
39
- expect(isEmbedRedirectTarget('http://x/addon/stream-broker/embed/')).toBe(false)
40
- })
41
-
42
- it('rejects non-embed paths and path traversal', () => {
43
- expect(isEmbedRedirectTarget('/addon/other/embed/')).toBe(false)
44
- expect(isEmbedRedirectTarget('/etc/passwd')).toBe(false)
45
- expect(isEmbedRedirectTarget('/addon/stream-broker/embed/../../secret')).toBe(false)
46
- expect(isEmbedRedirectTarget('')).toBe(false)
47
- })
48
- })
@@ -1,303 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
- import {
3
- BulkUpdateCoordinator,
4
- type BulkUpdateCoordinatorDeps,
5
- } from '../api/core/bulk-update-coordinator.js'
6
- import { EventCategory, type BulkUpdateState } from '@camstack/types'
7
-
8
- interface TestRig {
9
- readonly coordinator: BulkUpdateCoordinator
10
- readonly events: BulkUpdateState[]
11
- readonly updateAddon: ReturnType<typeof vi.fn>
12
- readonly updateFrameworkPackage: ReturnType<typeof vi.fn>
13
- readonly restartServer: ReturnType<typeof vi.fn>
14
- readonly now: () => number
15
- }
16
-
17
- function makeRig(opts?: { failItems?: ReadonlySet<string> }): TestRig {
18
- const events: BulkUpdateState[] = []
19
- const fail = opts?.failItems ?? new Set<string>()
20
- let clock = 1_000_000
21
-
22
- const updateAddon = vi.fn(async (input: { name: string; version: string }) => {
23
- if (fail.has(input.name)) throw new Error(`mock fail ${input.name}`)
24
- })
25
- const updateFrameworkPackage = vi.fn(
26
- async (input: { packageName: string; version: string; deferRestart: boolean }) => {
27
- if (fail.has(input.packageName)) throw new Error(`mock fail fw ${input.packageName}`)
28
- },
29
- )
30
- const restartServer = vi.fn(async () => {
31
- // simulates the real restartServer: in a real run the process dies
32
- })
33
-
34
- const deps: BulkUpdateCoordinatorDeps = {
35
- eventBus: {
36
- emit: (category, payload) => {
37
- if (category === EventCategory.AddonsBulkUpdateProgress) {
38
- events.push(structuredClone(payload as BulkUpdateState))
39
- }
40
- },
41
- } as unknown as BulkUpdateCoordinatorDeps['eventBus'],
42
- updateAddon,
43
- updateFrameworkPackage,
44
- restartServer,
45
- logger: {
46
- info: vi.fn(),
47
- warn: vi.fn(),
48
- error: vi.fn(),
49
- debug: vi.fn(),
50
- } as unknown as BulkUpdateCoordinatorDeps['logger'],
51
- clock: () => clock++,
52
- }
53
-
54
- return {
55
- coordinator: new BulkUpdateCoordinator(deps),
56
- events,
57
- updateAddon,
58
- updateFrameworkPackage,
59
- restartServer,
60
- now: () => clock,
61
- }
62
- }
63
-
64
- describe('BulkUpdateCoordinator', () => {
65
- beforeEach(() => {
66
- vi.useFakeTimers()
67
- })
68
- afterEach(() => {
69
- vi.useRealTimers()
70
- })
71
-
72
- it('regular-only happy path: processes 3 addons in order, no restart', async () => {
73
- const rig = makeRig()
74
- const { id } = rig.coordinator.start({
75
- nodeId: 'hub',
76
- items: [
77
- { name: '@camstack/addon-a', version: '1.0.1', isSystem: false },
78
- { name: '@camstack/addon-b', version: '2.0.1', isSystem: false },
79
- { name: '@camstack/addon-c', version: '3.0.1', isSystem: false },
80
- ],
81
- })
82
-
83
- await vi.runAllTimersAsync()
84
-
85
- const final = rig.coordinator.get(id)!
86
- expect(final.completed).toBe(3)
87
- expect(final.failed).toBe(0)
88
- expect(final.phase).toBe('finalizing')
89
- expect(final.items.every((i) => i.status === 'done')).toBe(true)
90
- expect(rig.restartServer).not.toHaveBeenCalled()
91
- expect(rig.updateAddon).toHaveBeenCalledTimes(3)
92
- })
93
-
94
- it('mixed happy path: regular first, system after, single restart at end', async () => {
95
- const rig = makeRig()
96
- const { id } = rig.coordinator.start({
97
- nodeId: 'hub',
98
- items: [
99
- { name: '@camstack/addon-a', version: '1.0.1', isSystem: false },
100
- { name: '@camstack/types', version: '0.1.40', isSystem: true },
101
- { name: '@camstack/addon-b', version: '2.0.1', isSystem: false },
102
- { name: '@camstack/kernel', version: '0.1.30', isSystem: true },
103
- ],
104
- })
105
-
106
- await vi.runAllTimersAsync()
107
-
108
- const final = rig.coordinator.get(id)!
109
- expect(final.completed).toBe(4)
110
- expect(rig.updateAddon).toHaveBeenCalledTimes(2)
111
- expect(rig.updateFrameworkPackage).toHaveBeenCalledTimes(2)
112
- expect(rig.updateFrameworkPackage).toHaveBeenCalledWith(
113
- expect.objectContaining({ deferRestart: true }),
114
- )
115
- expect(rig.restartServer).toHaveBeenCalledOnce()
116
-
117
- // Verify ordering via event sequence: regular items reach 'updating' before any system item
118
- const updatingNames = rig.events.map((s) => s.current).filter((n): n is string => n !== null)
119
- const firstSystemIdx = updatingNames.findIndex(
120
- (n) => n === '@camstack/types' || n === '@camstack/kernel',
121
- )
122
- const lastRegularIdx = updatingNames.findLastIndex(
123
- (n) => n === '@camstack/addon-a' || n === '@camstack/addon-b',
124
- )
125
- expect(firstSystemIdx).toBeGreaterThan(lastRegularIdx)
126
- })
127
-
128
- it('failure isolation: failed item does not block others', async () => {
129
- const rig = makeRig({ failItems: new Set(['@camstack/addon-b']) })
130
- const { id } = rig.coordinator.start({
131
- nodeId: 'hub',
132
- items: [
133
- { name: '@camstack/addon-a', version: '1.0.1', isSystem: false },
134
- { name: '@camstack/addon-b', version: '2.0.1', isSystem: false },
135
- { name: '@camstack/addon-c', version: '3.0.1', isSystem: false },
136
- ],
137
- })
138
-
139
- await vi.runAllTimersAsync()
140
-
141
- const final = rig.coordinator.get(id)!
142
- expect(final.failed).toBe(1)
143
- expect(final.items.find((i) => i.name === '@camstack/addon-b')?.status).toBe('failed')
144
- expect(final.items.find((i) => i.name === '@camstack/addon-b')?.error).toContain('mock fail')
145
- expect(final.items.find((i) => i.name === '@camstack/addon-a')?.status).toBe('done')
146
- expect(final.items.find((i) => i.name === '@camstack/addon-c')?.status).toBe('done')
147
- })
148
-
149
- it('cancel pre-restart: loop exits, no restart, queued items remain queued', async () => {
150
- const rig = makeRig()
151
- rig.updateAddon.mockImplementation(async () => {
152
- // Slow enough that cancel fires before item 2
153
- await new Promise<void>((resolve) => {
154
- setTimeout(resolve, 50)
155
- })
156
- })
157
-
158
- const { id } = rig.coordinator.start({
159
- nodeId: 'hub',
160
- items: [
161
- { name: '@camstack/addon-a', version: '1.0.1', isSystem: false },
162
- { name: '@camstack/addon-b', version: '2.0.1', isSystem: false },
163
- { name: '@camstack/addon-c', version: '3.0.1', isSystem: false },
164
- ],
165
- })
166
-
167
- // Advance time so item 1 starts updating
168
- await vi.advanceTimersByTimeAsync(10)
169
- const cancel = rig.coordinator.cancel(id)
170
- expect(cancel.cancelled).toBe(true)
171
- await vi.runAllTimersAsync()
172
-
173
- const final = rig.coordinator.get(id)!
174
- expect(final.cancelled).toBe(true)
175
- expect(rig.restartServer).not.toHaveBeenCalled()
176
- // At least one item should still be queued (we cancelled before item 2 ran)
177
- expect(final.items.filter((i) => i.status === 'queued').length).toBeGreaterThanOrEqual(1)
178
- })
179
-
180
- it('cancel ignored during restarting phase', async () => {
181
- const rig = makeRig()
182
- rig.restartServer.mockImplementation(async () => {
183
- // Hang briefly so we can attempt cancel during restart
184
- await new Promise<void>((resolve) => {
185
- setTimeout(resolve, 50)
186
- })
187
- })
188
-
189
- const { id } = rig.coordinator.start({
190
- nodeId: 'hub',
191
- items: [{ name: '@camstack/types', version: '0.1.40', isSystem: true }],
192
- })
193
-
194
- // Let it reach restarting
195
- await vi.advanceTimersByTimeAsync(10)
196
- const state = rig.coordinator.get(id)
197
- if (state?.phase === 'restarting') {
198
- const cancel = rig.coordinator.cancel(id)
199
- expect(cancel.cancelled).toBe(false)
200
- }
201
-
202
- await vi.runAllTimersAsync()
203
- })
204
-
205
- it('restart failure: pending-restart items get promoted to done with caveat error', async () => {
206
- const rig = makeRig()
207
- rig.restartServer.mockRejectedValueOnce(new Error('restart crashed'))
208
-
209
- const { id } = rig.coordinator.start({
210
- nodeId: 'hub',
211
- items: [{ name: '@camstack/types', version: '0.1.40', isSystem: true }],
212
- })
213
-
214
- await vi.runAllTimersAsync()
215
-
216
- const final = rig.coordinator.get(id)!
217
- const typesItem = final.items.find((i) => i.name === '@camstack/types')!
218
- expect(typesItem.status).toBe('done')
219
- expect(typesItem.error).toContain('Restart failed')
220
- })
221
-
222
- it('concurrent start for same nodeId is rejected', () => {
223
- const rig = makeRig()
224
- rig.coordinator.start({
225
- nodeId: 'hub',
226
- items: [{ name: '@camstack/addon-a', version: '1.0.1', isSystem: false }],
227
- })
228
-
229
- expect(() =>
230
- rig.coordinator.start({
231
- nodeId: 'hub',
232
- items: [{ name: '@camstack/addon-b', version: '2.0.1', isSystem: false }],
233
- }),
234
- ).toThrow(/already in progress/i)
235
- })
236
-
237
- it('auto-cleanup: state purged 5 min after completedAt', async () => {
238
- const rig = makeRig()
239
- const { id } = rig.coordinator.start({
240
- nodeId: 'hub',
241
- items: [{ name: '@camstack/addon-a', version: '1.0.1', isSystem: false }],
242
- })
243
-
244
- await vi.runAllTimersAsync()
245
- expect(rig.coordinator.get(id)).not.toBeNull()
246
-
247
- await vi.advanceTimersByTimeAsync(5 * 60 * 1000 + 100)
248
- expect(rig.coordinator.get(id)).toBeNull()
249
- })
250
-
251
- it('list excludes stale states after cleanup window (lazy cleanup symmetry)', async () => {
252
- const rig = makeRig()
253
- const { id } = rig.coordinator.start({
254
- nodeId: 'hub',
255
- items: [{ name: '@camstack/addon-a', version: '1.0.1', isSystem: false }],
256
- })
257
-
258
- await vi.runAllTimersAsync()
259
-
260
- // Right after completion: both get() and list() see the state
261
- expect(rig.coordinator.get(id)).not.toBeNull()
262
- expect(rig.coordinator.list()).toHaveLength(1)
263
-
264
- // After cleanup window: both should agree that the state is gone
265
- await vi.advanceTimersByTimeAsync(5 * 60 * 1000 + 100)
266
- expect(rig.coordinator.get(id)).toBeNull()
267
- expect(rig.coordinator.list()).toHaveLength(0)
268
- expect(rig.coordinator.list('hub')).toHaveLength(0)
269
- })
270
-
271
- it('list filters by nodeId', () => {
272
- const rig = makeRig()
273
- rig.coordinator.start({
274
- nodeId: 'hub',
275
- items: [{ name: '@camstack/addon-a', version: '1.0.1', isSystem: false }],
276
- })
277
- expect(rig.coordinator.list('hub')).toHaveLength(1)
278
- expect(rig.coordinator.list('agent-1')).toHaveLength(0)
279
- expect(rig.coordinator.list()).toHaveLength(1)
280
- })
281
-
282
- it('emits AddonsBulkUpdateProgress event on every status transition', async () => {
283
- const rig = makeRig()
284
- rig.coordinator.start({
285
- nodeId: 'hub',
286
- items: [
287
- { name: '@camstack/addon-a', version: '1.0.1', isSystem: false },
288
- { name: '@camstack/addon-b', version: '2.0.1', isSystem: false },
289
- ],
290
- })
291
-
292
- await vi.runAllTimersAsync()
293
-
294
- // At minimum: phase transition + 2 items × 2 transitions (updating → done)
295
- expect(rig.events.length).toBeGreaterThanOrEqual(5)
296
- // Every payload must be a complete BulkUpdateState (not a partial diff)
297
- for (const evt of rig.events) {
298
- expect(evt.id).toBeDefined()
299
- expect(evt.nodeId).toBe('hub')
300
- expect(Array.isArray(evt.items)).toBe(true)
301
- }
302
- })
303
- })