tmux-ide 2.9.2 → 2.9.4

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 (267) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +121 -140
  3. package/bin/cli.js +2457 -727
  4. package/package.json +23 -8
  5. package/packages/contracts/src/desktop-host.ts +3 -0
  6. package/packages/contracts/src/saved-machines.ts +19 -0
  7. package/packages/contracts/src/terminal-replica.ts +1 -1
  8. package/packages/core/package.json +3 -2
  9. package/packages/core/src/terminal-delivery.ts +51 -3
  10. package/packages/core/src/terminal-fnv64-wasm.ts +4 -3
  11. package/packages/core/src/terminal-replica-hash-cache.ts +129 -31
  12. package/packages/core/src/terminal-replica-owned-row.ts +27 -0
  13. package/packages/core/src/terminal-replica.ts +13 -28
  14. package/packages/daemon/dist/command-center/daemon-fleet-facts-observer.js +96 -10
  15. package/packages/daemon/dist/command-center/resources/saved-machine-route.js +40 -1
  16. package/packages/daemon/dist/command-center/ws-events.js +5 -24
  17. package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +8 -0
  18. package/packages/daemon/dist/lib/canonical-daemon.js +28 -2
  19. package/packages/daemon/dist/lib/daemon-embed.js +1 -1
  20. package/packages/daemon/dist/lib/daemon-service-manager.js +122 -0
  21. package/packages/daemon/dist/lib/daemon-service-plan.js +127 -0
  22. package/packages/daemon/dist/lib/daemon-service.js +259 -0
  23. package/packages/daemon/dist/lib/install-origin.js +2 -0
  24. package/packages/daemon/dist/lib/local-fleet-request.js +9 -3
  25. package/packages/daemon/dist/lib/log.js +6 -10
  26. package/packages/daemon/dist/lib/managed-installer-update.js +67 -0
  27. package/packages/daemon/dist/lib/{tui-download-lock.js → private-operation-lock.js} +11 -11
  28. package/packages/daemon/dist/lib/remote-daemon-info.js +3 -2
  29. package/packages/daemon/dist/lib/remote-daemon-lifecycle.js +4 -4
  30. package/packages/daemon/dist/lib/remote-tmux-command.js +10 -0
  31. package/packages/daemon/dist/lib/runtime-namespace.js +28 -16
  32. package/packages/daemon/dist/lib/saved-machines.js +40 -2
  33. package/packages/daemon/dist/lib/ssh-daemon-transport.js +24 -9
  34. package/packages/daemon/dist/lib/tmux-server-owner.js +1 -1
  35. package/packages/daemon/dist/lib/tmux-server-registration.js +21 -3
  36. package/packages/daemon/dist/lib/tui-binary.js +2 -2
  37. package/packages/daemon/dist/lib/update.js +18 -0
  38. package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +24 -1
  39. package/packages/daemon/dist/machines.js +40 -4
  40. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
  41. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  42. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  43. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
  44. package/packages/daemon/dist/terminal/attachments/direct-websocket.js +4 -1
  45. package/packages/daemon/dist/terminal/attachments/native-runtime.js +22 -13
  46. package/packages/daemon/dist/terminal/mirror/mirror-service.js +101 -3
  47. package/packages/daemon/dist/terminal/mirror/registered-window-guard.js +62 -0
  48. package/packages/daemon/dist/terminal/mirror/session-channel.js +75 -4
  49. package/packages/daemon/dist/terminal/mirror/shared-window-index.js +45 -0
  50. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +11 -1
  51. package/packages/daemon/dist/terminal/session-runtime/multiplexer-backend.js +1 -0
  52. package/packages/daemon/dist/terminal/session-runtime/registry.js +56 -7
  53. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +3 -1
  54. package/packages/daemon/dist/tui/integrations/claude-agent-teams.js +264 -0
  55. package/packages/daemon/dist/tui/mirror/dialog-model.js +2 -35
  56. package/packages/daemon/dist/tui/mirror/runtime/application-daemon-authority-owner.js +8 -0
  57. package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +20 -2
  58. package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +7 -1
  59. package/packages/daemon/dist/tui/mirror/runtime/application-machine-authority.js +59 -9
  60. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +27 -19
  61. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +23 -18
  62. package/packages/daemon/dist/tui/mirror/runtime/application-machine-startup.js +31 -2
  63. package/packages/daemon/dist/tui/mirror/runtime/application-palette-search-owner.js +7 -3
  64. package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +3 -0
  65. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +146 -2
  66. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +201 -50
  67. package/packages/daemon/dist/tui/mirror/runtime/bounded-performance-record-writer.js +50 -10
  68. package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -5
  69. package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +3 -0
  70. package/packages/daemon/dist/tui/mirror/runtime/workspace-terminal-fast-lane.js +8 -2
  71. package/packages/daemon/dist/tui/mirror/terminal-text.js +35 -0
  72. package/packages/daemon/dist/tui/mirror/workspace/connection-feedback.js +5 -3
  73. package/packages/daemon/src/command-center/daemon-fleet-facts-observer.ts +100 -10
  74. package/packages/daemon/src/command-center/resources/saved-machine-route.ts +48 -1
  75. package/packages/daemon/src/command-center/ws-events.ts +5 -25
  76. package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +12 -0
  77. package/packages/daemon/src/lib/canonical-daemon.ts +44 -2
  78. package/packages/daemon/src/lib/daemon-embed.ts +1 -0
  79. package/packages/daemon/src/lib/daemon-service-manager.ts +152 -0
  80. package/packages/daemon/src/lib/daemon-service-plan.ts +153 -0
  81. package/packages/daemon/src/lib/daemon-service.ts +301 -0
  82. package/packages/daemon/src/lib/install-origin.ts +5 -0
  83. package/packages/daemon/src/lib/local-fleet-request.ts +19 -3
  84. package/packages/daemon/src/lib/log.ts +13 -9
  85. package/packages/daemon/src/lib/managed-installer-update.ts +79 -0
  86. package/packages/daemon/src/lib/{tui-download-lock.ts → private-operation-lock.ts} +15 -11
  87. package/packages/daemon/src/lib/remote-daemon-info.ts +3 -3
  88. package/packages/daemon/src/lib/remote-daemon-lifecycle.ts +4 -4
  89. package/packages/daemon/src/lib/remote-tmux-command.ts +12 -0
  90. package/packages/daemon/src/lib/runtime-namespace.ts +27 -18
  91. package/packages/daemon/src/lib/saved-machines.ts +40 -1
  92. package/packages/daemon/src/lib/ssh-daemon-transport.ts +25 -7
  93. package/packages/daemon/src/lib/tmux-server-owner.ts +1 -0
  94. package/packages/daemon/src/lib/tmux-server-registration.ts +21 -3
  95. package/packages/daemon/src/lib/tui-binary.ts +2 -2
  96. package/packages/daemon/src/lib/update.ts +24 -0
  97. package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +22 -0
  98. package/packages/daemon/src/machines.ts +53 -5
  99. package/packages/daemon/src/terminal/attachments/direct-websocket.ts +8 -2
  100. package/packages/daemon/src/terminal/attachments/native-runtime.ts +28 -13
  101. package/packages/daemon/src/terminal/mirror/mirror-service.ts +105 -3
  102. package/packages/daemon/src/terminal/mirror/registered-window-guard.ts +66 -0
  103. package/packages/daemon/src/terminal/mirror/session-channel.ts +92 -4
  104. package/packages/daemon/src/terminal/mirror/shared-window-index.ts +42 -0
  105. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +12 -1
  106. package/packages/daemon/src/terminal/session-runtime/multiplexer-backend.ts +2 -0
  107. package/packages/daemon/src/terminal/session-runtime/registry.ts +70 -8
  108. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +3 -1
  109. package/packages/daemon/src/tui/integrations/claude-agent-teams.ts +334 -0
  110. package/packages/daemon/src/tui/mirror/dialog-model.ts +2 -32
  111. package/packages/daemon/src/tui/mirror/runtime/application-daemon-authority-owner.ts +8 -0
  112. package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +19 -2
  113. package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +6 -1
  114. package/packages/daemon/src/tui/mirror/runtime/application-machine-authority.ts +60 -11
  115. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +39 -33
  116. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +31 -23
  117. package/packages/daemon/src/tui/mirror/runtime/application-machine-startup.ts +38 -2
  118. package/packages/daemon/src/tui/mirror/runtime/application-palette-search-owner.ts +7 -4
  119. package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +3 -0
  120. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +1 -1
  121. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +47 -46
  122. package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +165 -2
  123. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +207 -53
  124. package/packages/daemon/src/tui/mirror/runtime/bounded-performance-record-writer.ts +49 -9
  125. package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +6 -5
  126. package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +4 -0
  127. package/packages/daemon/src/tui/mirror/runtime/workspace-terminal-fast-lane.ts +8 -3
  128. package/packages/daemon/src/tui/mirror/terminal-text.ts +32 -0
  129. package/packages/daemon/src/tui/mirror/workspace/connection-feedback.ts +6 -3
  130. package/packages/daemon-client/src/fleet-connection-status.ts +10 -1
  131. package/packages/daemon-client/src/workspace-client.ts +4 -1
  132. package/skill/SKILL.md +30 -30
  133. package/packages/contracts/src/__tests__/actions-contract.test.ts +0 -119
  134. package/packages/contracts/src/__tests__/agent-graph-overlay.test.ts +0 -428
  135. package/packages/contracts/src/__tests__/app-window-mutation.test.ts +0 -51
  136. package/packages/contracts/src/__tests__/app-window-state.test.ts +0 -34
  137. package/packages/contracts/src/__tests__/application-shell.test.ts +0 -645
  138. package/packages/contracts/src/__tests__/automation-operations.test.ts +0 -78
  139. package/packages/contracts/src/__tests__/causal-cell.test.ts +0 -47
  140. package/packages/contracts/src/__tests__/client-view-state.test.ts +0 -115
  141. package/packages/contracts/src/__tests__/cohesion-fixture.test.ts +0 -106
  142. package/packages/contracts/src/__tests__/commands.test.ts +0 -86
  143. package/packages/contracts/src/__tests__/control.test.ts +0 -154
  144. package/packages/contracts/src/__tests__/daemon-events.test.ts +0 -405
  145. package/packages/contracts/src/__tests__/daemon-resources.test.ts +0 -143
  146. package/packages/contracts/src/__tests__/daemon-wire.test.ts +0 -149
  147. package/packages/contracts/src/__tests__/desktop-host.test.ts +0 -269
  148. package/packages/contracts/src/__tests__/desktop-missions.test.ts +0 -215
  149. package/packages/contracts/src/__tests__/experience-conformance.test.ts +0 -153
  150. package/packages/contracts/src/__tests__/experience-kernel-boundary.test.ts +0 -113
  151. package/packages/contracts/src/__tests__/experience-shell.test.ts +0 -65
  152. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v2.json +0 -35
  153. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +0 -35
  154. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v4.json +0 -35
  155. package/packages/contracts/src/__tests__/fixtures/scoped-receipt.ts +0 -62
  156. package/packages/contracts/src/__tests__/fleet-agent-graph.test.ts +0 -231
  157. package/packages/contracts/src/__tests__/fleet-catalog.test.ts +0 -270
  158. package/packages/contracts/src/__tests__/fleet-lifecycle.test.ts +0 -62
  159. package/packages/contracts/src/__tests__/focus-overlay.test.ts +0 -110
  160. package/packages/contracts/src/__tests__/ide-config.test.ts +0 -64
  161. package/packages/contracts/src/__tests__/interaction-evidence.test.ts +0 -288
  162. package/packages/contracts/src/__tests__/interaction-receipt-v2.test.ts +0 -72
  163. package/packages/contracts/src/__tests__/issue-error.test.ts +0 -78
  164. package/packages/contracts/src/__tests__/mission-projections.test.ts +0 -186
  165. package/packages/contracts/src/__tests__/missions.test.ts +0 -272
  166. package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +0 -242
  167. package/packages/contracts/src/__tests__/pane-appearance.test.ts +0 -146
  168. package/packages/contracts/src/__tests__/pane-read-result.test.ts +0 -55
  169. package/packages/contracts/src/__tests__/pane-stream.test.ts +0 -536
  170. package/packages/contracts/src/__tests__/pane-widget-marker.test.ts +0 -327
  171. package/packages/contracts/src/__tests__/performance-metrics.test.ts +0 -73
  172. package/packages/contracts/src/__tests__/performance-qualification.test.ts +0 -137
  173. package/packages/contracts/src/__tests__/rich-card-widget.test.ts +0 -29
  174. package/packages/contracts/src/__tests__/session-runtime.test.ts +0 -512
  175. package/packages/contracts/src/__tests__/startup-readiness.test.ts +0 -409
  176. package/packages/contracts/src/__tests__/terminal-attachment-stream.test.ts +0 -72
  177. package/packages/contracts/src/__tests__/terminal-attachments.test.ts +0 -329
  178. package/packages/contracts/src/__tests__/terminal-delivery.test.ts +0 -88
  179. package/packages/contracts/src/__tests__/terminal-runtime-inventory.test.ts +0 -68
  180. package/packages/contracts/src/__tests__/terminals.test.ts +0 -87
  181. package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +0 -166
  182. package/packages/contracts/src/__tests__/transport-contract-loading.test.ts +0 -91
  183. package/packages/contracts/src/__tests__/visual-tokens.test.ts +0 -224
  184. package/packages/contracts/src/__tests__/window-links.test.ts +0 -243
  185. package/packages/contracts/src/__tests__/workspace-changes-resource.test.ts +0 -317
  186. package/packages/contracts/src/__tests__/workspace-changes-view.test.ts +0 -122
  187. package/packages/contracts/src/__tests__/workspace-config.test.ts +0 -294
  188. package/packages/contracts/src/__tests__/workspace-files-resource.test.ts +0 -261
  189. package/packages/contracts/src/__tests__/workspace-files-tree.test.ts +0 -147
  190. package/packages/contracts/src/__tests__/workspace-pane-creation.test.ts +0 -249
  191. package/packages/contracts/src/__tests__/workspace-promotion.test.ts +0 -94
  192. package/packages/contracts/src/__tests__/workspace-resource-identity.test.ts +0 -90
  193. package/packages/contracts/src/daemon-resource-request.test.ts +0 -192
  194. package/packages/contracts/src/native-operation-identity.test.ts +0 -23
  195. package/packages/contracts/src/workspace-admission.test.ts +0 -21
  196. package/packages/contracts/src/workspace-catalog-resource.test.ts +0 -146
  197. package/packages/core/src/agent-provisioning.test.ts +0 -42
  198. package/packages/core/src/application-shell-session.test.ts +0 -105
  199. package/packages/core/src/interaction-evidence.test.ts +0 -134
  200. package/packages/core/src/interaction-flow.test.ts +0 -66
  201. package/packages/core/src/interaction-receipts.test.ts +0 -671
  202. package/packages/core/src/navigator.test.ts +0 -34
  203. package/packages/core/src/optimistic-projection-conformance.test.ts +0 -75
  204. package/packages/core/src/optimistic-projection.test.ts +0 -118
  205. package/packages/core/src/performance-metrics.test.ts +0 -121
  206. package/packages/core/src/performance-qualification.test.ts +0 -283
  207. package/packages/core/src/saved-machines.test.ts +0 -80
  208. package/packages/core/src/terminal-compact-cold.test.ts +0 -87
  209. package/packages/core/src/terminal-conformance.test.ts +0 -48
  210. package/packages/core/src/terminal-delivery-cache-lifetime.test.ts +0 -167
  211. package/packages/core/src/terminal-delivery-encode-cooperative.test.ts +0 -140
  212. package/packages/core/src/terminal-delivery-representation-hash.test.ts +0 -77
  213. package/packages/core/src/terminal-delivery.test.ts +0 -1471
  214. package/packages/core/src/terminal-fnv64-wasm.test.ts +0 -61
  215. package/packages/core/src/terminal-replica-cold.test.ts +0 -31
  216. package/packages/core/src/terminal-replica-cooperative.test.ts +0 -170
  217. package/packages/core/src/terminal-replica-hash-cache.test.ts +0 -283
  218. package/packages/core/src/terminal-replica.test.ts +0 -347
  219. package/packages/core/src/workspace-selection.test.ts +0 -63
  220. package/packages/daemon/dist/__tests__/support.js +0 -33
  221. package/packages/daemon/dist/lib/__tests__/fixtures/workspace-state-writer.js +0 -58
  222. package/packages/daemon/dist/lib/__tests__/installed-recovery-fixture.js +0 -400
  223. package/packages/daemon/dist/terminal/__tests__/MockPtyAdapter.js +0 -182
  224. package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +0 -98
  225. package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +0 -156
  226. package/packages/daemon/src/tui/mirror/__snapshots__/activity-surface-renderer.test.tsx.snap +0 -42
  227. package/packages/daemon/src/tui/mirror/__snapshots__/changes-terminal-surface-renderer.test.tsx.snap +0 -267
  228. package/packages/daemon/src/tui/mirror/__snapshots__/home-files-surface-renderer.test.tsx.snap +0 -267
  229. package/packages/daemon/src/tui/mirror/__snapshots__/missions-surface-renderer.test.tsx.snap +0 -134
  230. package/packages/daemon/src/tui/mirror/__snapshots__/recipes-gallery-renderer.test.tsx.snap +0 -141
  231. package/packages/daemon/src/tui/mirror/__snapshots__/shell-chrome-renderer.test.tsx.snap +0 -134
  232. package/packages/daemon/src/tui/mirror/runtime/__snapshots__/application-shell-view-renderer.test.tsx.snap +0 -87
  233. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/agent-terminal-canvas-renderer.test.tsx.snap +0 -348
  234. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/application-shell-renderer.test.tsx.snap +0 -134
  235. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/command-palette-surface-renderer.test.tsx.snap +0 -255
  236. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/pane-frame-renderer.test.tsx.snap +0 -525
  237. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/terminal-pane-header-polish-renderer.test.tsx.snap +0 -31
  238. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/workbench-shell-renderer.test.tsx.snap +0 -134
  239. package/packages/daemon-client/src/application-shell-session.test.ts +0 -196
  240. package/packages/daemon-client/src/automation-client.test.ts +0 -214
  241. package/packages/daemon-client/src/bootstrap-coordinator.test.ts +0 -145
  242. package/packages/daemon-client/src/connection-supervisor.test.ts +0 -109
  243. package/packages/daemon-client/src/direct-application-shell-transport.test.ts +0 -941
  244. package/packages/daemon-client/src/first-latest-coordinator.test.ts +0 -175
  245. package/packages/daemon-client/src/fleet-dial-scheduler.test.ts +0 -114
  246. package/packages/daemon-client/src/fleet-environments.test.ts +0 -40
  247. package/packages/daemon-client/src/generation-bound-store-timers.test.ts +0 -74
  248. package/packages/daemon-client/src/owner-action-client.test.ts +0 -245
  249. package/packages/daemon-client/src/pane-stream-client.test.ts +0 -2841
  250. package/packages/daemon-client/src/pane-stream-clock-calibration.test.ts +0 -207
  251. package/packages/daemon-client/src/pane-stream-loopback-benchmark.test.ts +0 -51
  252. package/packages/daemon-client/src/push-resource-session.test.ts +0 -509
  253. package/packages/daemon-client/src/resource-replica.test.ts +0 -186
  254. package/packages/daemon-client/src/runtime-resource-ledger.test.ts +0 -18
  255. package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +0 -114
  256. package/packages/daemon-client/src/terminal-fast-lane.test.ts +0 -835
  257. package/packages/daemon-client/src/tmux-server-client.test.ts +0 -279
  258. package/packages/daemon-client/src/tmux-server-interaction-events.test.ts +0 -230
  259. package/packages/daemon-client/src/workspace-catalog-v2.test.ts +0 -109
  260. package/packages/daemon-client/src/workspace-client-boundaries.test.ts +0 -59
  261. package/packages/daemon-client/src/workspace-client-operations.test.ts +0 -96
  262. package/packages/daemon-client/src/workspace-client.test.ts +0 -2912
  263. package/packages/daemon-client/src/workspace-event-supervisor.test.ts +0 -1086
  264. package/packages/daemon-client/src/workspace-pane-client.test.ts +0 -80
  265. package/packages/presentation/src/presentation-contract.test.ts +0 -63
  266. package/packages/tmux-bridge/src/runner.test.ts +0 -614
  267. package/packages/tmux-bridge/src/targeting.test.ts +0 -107
@@ -1,941 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import {
3
- APPLICATION_SHELL_RESOURCE_V2_VERSION,
4
- DAEMON_WIRE_PROTOCOL_VERSION,
5
- ApplicationShellProjectionInputV2SchemaZ,
6
- COHESION_FIXTURE_V1,
7
- type DesktopApplicationShellTarget,
8
- type DesktopDaemonHostDescriptor,
9
- } from "@tmux-ide/contracts";
10
-
11
- import {
12
- createDirectLoopbackDaemonTransport,
13
- type TerminalFirstDaemonTransport,
14
- } from "./direct-application-shell-transport.ts";
15
- import type {
16
- WorkspaceEventSocket,
17
- WorkspaceEventSocketEvent,
18
- WorkspaceEventSocketEventType,
19
- WorkspaceEventSocketListener,
20
- WorkspaceEventSocketOptions,
21
- } from "./workspace-event-supervisor.ts";
22
-
23
- const daemon = {
24
- protocolVersion: DAEMON_WIRE_PROTOCOL_VERSION,
25
- productVersion: "2.8.0",
26
- instanceId: "9bcf33b0-c837-4a94-b5e8-c0977f54464f",
27
- startedAt: "2026-08-09T12:00:00.000Z",
28
- };
29
-
30
- const descriptor: DesktopDaemonHostDescriptor = {
31
- ...daemon,
32
- apiBaseUrl: "http://127.0.0.1:6060/",
33
- };
34
-
35
- const target: DesktopApplicationShellTarget = {
36
- daemon,
37
- workspaceName: "workspace.alpha",
38
- };
39
-
40
- const terminalFirstResource = ApplicationShellProjectionInputV2SchemaZ.parse({
41
- project: COHESION_FIXTURE_V1.project,
42
- workspace: {
43
- ...COHESION_FIXTURE_V1.workspace,
44
- sidebar: {
45
- ...COHESION_FIXTURE_V1.workspace.sidebar,
46
- agents: COHESION_FIXTURE_V1.workspace.sidebar.agents.map((agent) => ({
47
- ...agent,
48
- paneId: null,
49
- })),
50
- },
51
- },
52
- dock: COHESION_FIXTURE_V1.dock,
53
- focus: { ...COHESION_FIXTURE_V1.focus, overlays: [] },
54
- connection: COHESION_FIXTURE_V1.connection,
55
- terminalInventory: {
56
- activeResourceId: "pane.worker",
57
- resources: [
58
- {
59
- id: "pane.worker",
60
- title: "Worker",
61
- kind: "terminal",
62
- active: true,
63
- attachability: { status: "available", semanticPaneId: "pane.worker" },
64
- interactionEndpoint: null,
65
- nativeIdentity: null,
66
- },
67
- ],
68
- },
69
- });
70
-
71
- function json(body: unknown, status = 200): Response {
72
- return new Response(JSON.stringify(body), {
73
- status,
74
- headers: { "content-type": "application/json" },
75
- });
76
- }
77
-
78
- class FakeSocket implements WorkspaceEventSocket {
79
- readyState = 1;
80
- readonly sent: unknown[] = [];
81
- readonly listeners = new Map<WorkspaceEventSocketEventType, Set<WorkspaceEventSocketListener>>();
82
- addEventListener(type: WorkspaceEventSocketEventType, listener: WorkspaceEventSocketListener) {
83
- const bucket = this.listeners.get(type) ?? new Set();
84
- bucket.add(listener);
85
- this.listeners.set(type, bucket);
86
- }
87
- removeEventListener(type: WorkspaceEventSocketEventType, listener: WorkspaceEventSocketListener) {
88
- this.listeners.get(type)?.delete(listener);
89
- }
90
- send(value: string) {
91
- this.sent.push(JSON.parse(value));
92
- }
93
- close() {}
94
- emit(type: WorkspaceEventSocketEventType, event: WorkspaceEventSocketEvent = {}) {
95
- for (const listener of this.listeners.get(type) ?? []) listener(event);
96
- }
97
- frame(value: unknown) {
98
- this.emit("message", { data: JSON.stringify(value) });
99
- }
100
- }
101
-
102
- class FakeReconnectClock {
103
- #next = 1;
104
- readonly callbacks = new Map<number, () => void>();
105
- setTimeout = (callback: () => void): number => {
106
- const handle = this.#next++;
107
- this.callbacks.set(handle, callback);
108
- return handle;
109
- };
110
- clearTimeout = (handle: unknown): void => {
111
- this.callbacks.delete(handle as number);
112
- };
113
- runNext(): void {
114
- const entry = this.callbacks.entries().next().value as [number, () => void] | undefined;
115
- if (!entry) throw new Error("No reconnect timer is pending");
116
- this.callbacks.delete(entry[0]);
117
- entry[1]();
118
- }
119
- }
120
-
121
- async function tick(): Promise<void> {
122
- await new Promise<void>((resolve) => setTimeout(resolve, 0));
123
- }
124
-
125
- describe("direct application-shell transport version selection", () => {
126
- it("uses owner-authenticated terminal and application-shell barriers on one socket", async () => {
127
- const socket = new FakeSocket();
128
- const socketUrls: string[] = [];
129
- const socketOptions: Array<WorkspaceEventSocketOptions | undefined> = [];
130
- const requests: URL[] = [];
131
- const diagnostics: string[] = [];
132
- const diagnosticActionStates: string[] = [];
133
- let fallbackListenerCount: number | null = null;
134
- const transport = createDirectLoopbackDaemonTransport({
135
- descriptor,
136
- ownerToken: "owner-secret",
137
- resolveSessionName: () => "alpha",
138
- applicationShellResourceVersion: APPLICATION_SHELL_RESOURCE_V2_VERSION,
139
- terminalRuntimeAuthority: true,
140
- terminalRuntimeDiagnostic: (phase) => {
141
- diagnostics.push(phase);
142
- diagnosticActionStates.push(
143
- `${phase}:${socketOptions.length}:${socket.sent.length}:${requests.length}`,
144
- );
145
- if (phase === "terminal-fallback-selected") {
146
- fallbackListenerCount = socket.listeners.get("message")?.size ?? 0;
147
- }
148
- throw new Error("diagnostic sink failed");
149
- },
150
- createWebSocket: (url, options) => {
151
- socketUrls.push(url);
152
- socketOptions.push(options);
153
- return socket;
154
- },
155
- fetch: async (input) => {
156
- const url = new URL(String(input));
157
- requests.push(url);
158
- if (url.pathname.endsWith("/terminal-runtime-inventory")) {
159
- return json({
160
- version: 1,
161
- daemon,
162
- resource: {
163
- workspaceName: "workspace.alpha",
164
- workspaceId: "workspace.0123456789abcdefabcd",
165
- sessionId: "session.0123456789abcdefabcd",
166
- resourceRevision: 0,
167
- semanticPaneIds: ["pane.worker"],
168
- },
169
- });
170
- }
171
- return json({
172
- version: APPLICATION_SHELL_RESOURCE_V2_VERSION,
173
- daemon,
174
- resource: terminalFirstResource,
175
- });
176
- },
177
- }) as TerminalFirstDaemonTransport;
178
- const preparation = transport.prepareTerminalRuntimeInventory(
179
- target,
180
- new AbortController().signal,
181
- );
182
- socket.emit("open");
183
- socket.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
184
- await tick();
185
- socket.frame({
186
- type: "resource.interests-ack",
187
- interestRevision: 1,
188
- sequence: 0,
189
- unavailableInterests: [],
190
- });
191
- const prepared = await preparation;
192
- transport.adoptTerminalRuntimeInventory(prepared, () => undefined);
193
- expect(diagnostics).toEqual([
194
- "terminal-event-socket-create",
195
- "terminal-event-socket-open",
196
- "terminal-event-hello",
197
- "terminal-interest-send",
198
- "terminal-interest-ack",
199
- "terminal-http-start",
200
- "terminal-http-response",
201
- "terminal-capability-adopted",
202
- ]);
203
- expect(socketOptions).toEqual([{ headers: { Authorization: "Bearer owner-secret" } }]);
204
- expect(socketUrls).toEqual(["ws://127.0.0.1:6060/ws/events?mode=semantic"]);
205
- expect(requests.map(({ pathname }) => pathname)).toEqual([
206
- "/api/project/alpha/terminal-runtime-inventory",
207
- ]);
208
- expect(diagnosticActionStates).toContain("terminal-event-socket-create:1:0:0");
209
- expect(diagnosticActionStates).toContain("terminal-interest-send:1:1:0");
210
- expect(diagnosticActionStates).toContain("terminal-http-start:1:1:1");
211
-
212
- let verified = 0;
213
- transport.connectEvents(target, {
214
- onVerifiedOpen: () => (verified += 1),
215
- onInvalidate: () => undefined,
216
- onMalformedFrame: () => undefined,
217
- onPeerMismatch: () => undefined,
218
- onProtocolError: () => undefined,
219
- onClose: () => undefined,
220
- onError: () => undefined,
221
- });
222
- const shellRead = transport.fetchApplicationShell(target, new AbortController().signal);
223
- await tick();
224
- expect(verified).toBe(0);
225
- expect(requests).toHaveLength(1);
226
- socket.frame({
227
- type: "resource.interests-ack",
228
- interestRevision: 2,
229
- sequence: 0,
230
- unavailableInterests: [],
231
- });
232
- await shellRead;
233
- expect(verified).toBe(1);
234
- expect(requests.at(-1)!.pathname).toBe("/api/project/alpha/application-shell");
235
- expect(socketOptions).toHaveLength(1);
236
- transport.selectApplicationShellFallback("unknown");
237
- expect(diagnostics.at(-1)).toBe("terminal-fallback-selected");
238
- expect(fallbackListenerCount).toBe(0);
239
- });
240
-
241
- it("keeps the fallback event connection on the legacy endpoint", () => {
242
- const socketUrls: string[] = [];
243
- const socketOptions: Array<WorkspaceEventSocketOptions | undefined> = [];
244
- const transport = createDirectLoopbackDaemonTransport({
245
- descriptor,
246
- ownerToken: "owner-secret",
247
- resolveSessionName: () => "alpha",
248
- terminalRuntimeAuthority: true,
249
- createWebSocket: (url, options) => {
250
- socketUrls.push(url);
251
- socketOptions.push(options);
252
- return new FakeSocket();
253
- },
254
- fetch: async () => json({}),
255
- }) as TerminalFirstDaemonTransport;
256
-
257
- void transport
258
- .prepareTerminalRuntimeInventory(target, new AbortController().signal)
259
- .catch(() => undefined);
260
- transport.selectApplicationShellFallback("deadline");
261
- transport.connectEvents(target, {
262
- onVerifiedOpen: () => undefined,
263
- onInvalidate: () => undefined,
264
- onMalformedFrame: () => undefined,
265
- onPeerMismatch: () => undefined,
266
- onProtocolError: () => undefined,
267
- onClose: () => undefined,
268
- onError: () => undefined,
269
- });
270
-
271
- expect(socketUrls).toEqual([
272
- "ws://127.0.0.1:6060/ws/events?mode=semantic",
273
- "ws://127.0.0.1:6060/ws/events",
274
- ]);
275
- expect(socketOptions).toEqual([
276
- { headers: { Authorization: "Bearer owner-secret" } },
277
- undefined,
278
- ]);
279
- });
280
-
281
- it("fences a pending terminal install before production fallback settles catalog-only", async () => {
282
- const socket = new FakeSocket();
283
- const clock = new FakeReconnectClock();
284
- const transport = createDirectLoopbackDaemonTransport({
285
- descriptor,
286
- ownerToken: "owner-secret",
287
- resolveSessionName: () => "alpha",
288
- terminalRuntimeAuthority: true,
289
- terminalReconnectClock: clock,
290
- createWebSocket: () => socket,
291
- fetch: async () =>
292
- json({
293
- version: 1,
294
- daemon,
295
- resource: {
296
- workspaceName: "workspace.alpha",
297
- workspaceId: "workspace.0123456789abcdefabcd",
298
- sessionId: "session.0123456789abcdefabcd",
299
- resourceRevision: 1,
300
- semanticPaneIds: ["pane.alpha"],
301
- },
302
- }),
303
- }) as TerminalFirstDaemonTransport;
304
- const preparing = transport.prepareTerminalRuntimeInventory(
305
- target,
306
- new AbortController().signal,
307
- );
308
- const catalog = transport.connectWorkspaceCatalog(target, () => undefined);
309
- socket.emit("open");
310
- socket.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
311
- await tick();
312
- expect(socket.sent[0]).toMatchObject({
313
- interestRevision: 1,
314
- interests: [{ resource: "terminal-runtime-inventory" }],
315
- });
316
- transport.selectApplicationShellFallback("deadline");
317
- socket.frame({
318
- type: "resource.interests-ack",
319
- interestRevision: 1,
320
- sequence: 0,
321
- unavailableInterests: [],
322
- });
323
- await tick();
324
- expect(socket.sent[1]).toMatchObject({
325
- interestRevision: 2,
326
- interests: [{ resource: "workspace-catalog", workspaceName: null }],
327
- });
328
- socket.frame({
329
- type: "resource.interests-ack",
330
- interestRevision: 2,
331
- sequence: 0,
332
- unavailableInterests: [],
333
- });
334
- await catalog.ready;
335
- const replacementInvalidated: string[] = [];
336
- const replacementCatalog = transport.connectWorkspaceCatalog(target, () =>
337
- replacementInvalidated.push("changed"),
338
- );
339
- await replacementCatalog.ready;
340
- const catalogOnlySubscriptions = socket.sent.length;
341
- catalog.close();
342
- socket.frame({
343
- type: "resource.changed",
344
- sequence: 1,
345
- workspaceName: null,
346
- resource: "workspace-catalog",
347
- revision: 1,
348
- causeOperationId: null,
349
- });
350
- expect(replacementInvalidated).toEqual(["changed"]);
351
- expect(socket.sent).toHaveLength(catalogOnlySubscriptions);
352
- const prepared = await preparing;
353
- expect(transport.adoptTerminalRuntimeInventory(prepared, () => undefined)).toBeNull();
354
- socket.emit("close");
355
- expect(clock.callbacks.size).toBe(1);
356
- transport.disposeEventSupervisor();
357
- expect(clock.callbacks.size).toBe(0);
358
- });
359
-
360
- it("recovers catalog-only when the semantic socket retires before terminal preparation settles", async () => {
361
- const sockets: FakeSocket[] = [];
362
- const clock = new FakeReconnectClock();
363
- const transport = createDirectLoopbackDaemonTransport({
364
- descriptor,
365
- ownerToken: "owner-secret",
366
- resolveSessionName: () => "alpha",
367
- terminalRuntimeAuthority: true,
368
- terminalReconnectClock: clock,
369
- createWebSocket: () => {
370
- const socket = new FakeSocket();
371
- sockets.push(socket);
372
- return socket;
373
- },
374
- fetch: async () => json({}),
375
- }) as TerminalFirstDaemonTransport;
376
- const preparing = transport.prepareTerminalRuntimeInventory(
377
- target,
378
- new AbortController().signal,
379
- );
380
- const invalidated: string[] = [];
381
- const catalog = transport.connectWorkspaceCatalog(target, () => invalidated.push("fresh"));
382
- sockets[0]!.emit("open");
383
- sockets[0]!.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
384
- await tick();
385
- expect(sockets[0]!.sent[0]).toMatchObject({
386
- interests: [{ resource: "terminal-runtime-inventory" }],
387
- });
388
- sockets[0]!.emit("close");
389
- await expect(preparing).rejects.toThrow("disconnected");
390
- expect(clock.callbacks.size).toBe(0);
391
-
392
- transport.selectApplicationShellFallback("preparation-rejected");
393
- expect(clock.callbacks.size).toBe(1);
394
- clock.runNext();
395
- await tick();
396
- sockets[1]!.emit("open");
397
- sockets[1]!.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
398
- await tick();
399
- expect(sockets[1]!.sent[0]).toMatchObject({
400
- interestRevision: 1,
401
- interests: [{ resource: "workspace-catalog", workspaceName: null }],
402
- });
403
- sockets[1]!.frame({
404
- type: "resource.interests-ack",
405
- interestRevision: 1,
406
- sequence: 0,
407
- unavailableInterests: [],
408
- });
409
- await catalog.ready;
410
- expect(invalidated).toEqual(["fresh"]);
411
- transport.disposeEventSupervisor();
412
- });
413
-
414
- it("does not resolve post-fallback readiness from an ACK on a retired supervisor", async () => {
415
- const sockets: FakeSocket[] = [];
416
- const clock = new FakeReconnectClock();
417
- const transport = createDirectLoopbackDaemonTransport({
418
- descriptor,
419
- ownerToken: "owner-secret",
420
- resolveSessionName: () => "alpha",
421
- terminalRuntimeAuthority: true,
422
- terminalReconnectClock: clock,
423
- createWebSocket: () => {
424
- const socket = new FakeSocket();
425
- sockets.push(socket);
426
- return socket;
427
- },
428
- fetch: async () => json({}),
429
- }) as TerminalFirstDaemonTransport;
430
- transport.selectApplicationShellFallback("deadline");
431
- const catalog = transport.connectWorkspaceCatalog(target, () => undefined);
432
- let ready = false;
433
- void catalog.ready.then(() => (ready = true));
434
- sockets[0]!.emit("open");
435
- sockets[0]!.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
436
- await tick();
437
- expect(sockets[0]!.sent[0]).toMatchObject({
438
- interests: [{ resource: "workspace-catalog", workspaceName: null }],
439
- });
440
- sockets[0]!.frame({
441
- type: "resource.interests-ack",
442
- interestRevision: 1,
443
- sequence: 0,
444
- unavailableInterests: [],
445
- });
446
- sockets[0]!.emit("close");
447
- await tick();
448
- expect(ready).toBe(false);
449
- expect(clock.callbacks.size).toBe(1);
450
-
451
- clock.runNext();
452
- await tick();
453
- sockets[1]!.emit("open");
454
- sockets[1]!.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
455
- await tick();
456
- sockets[1]!.frame({
457
- type: "resource.interests-ack",
458
- interestRevision: 1,
459
- sequence: 0,
460
- unavailableInterests: [],
461
- });
462
- await catalog.ready;
463
- expect(ready).toBe(true);
464
- transport.disposeEventSupervisor();
465
- });
466
-
467
- it("replaces a retired supervisor and publishes its clean terminal authority", async () => {
468
- const sockets: FakeSocket[] = [];
469
- const clock = new FakeReconnectClock();
470
- let revision = 1;
471
- const transport = createDirectLoopbackDaemonTransport({
472
- descriptor,
473
- ownerToken: "owner-secret",
474
- resolveSessionName: () => "alpha",
475
- terminalRuntimeAuthority: true,
476
- terminalReconnectClock: clock,
477
- createWebSocket: () => {
478
- const socket = new FakeSocket();
479
- sockets.push(socket);
480
- return socket;
481
- },
482
- fetch: async () =>
483
- json({
484
- version: 1,
485
- daemon,
486
- resource: {
487
- workspaceName: "workspace.alpha",
488
- workspaceId: "workspace.0123456789abcdefabcd",
489
- sessionId: "session.0123456789abcdefabcd",
490
- resourceRevision: revision,
491
- semanticPaneIds: [`pane.${revision}`],
492
- },
493
- }),
494
- }) as TerminalFirstDaemonTransport;
495
- const preparation = transport.prepareTerminalRuntimeInventory(
496
- target,
497
- new AbortController().signal,
498
- );
499
- sockets[0]!.emit("open");
500
- sockets[0]!.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
501
- await tick();
502
- sockets[0]!.frame({
503
- type: "resource.interests-ack",
504
- interestRevision: 1,
505
- sequence: 0,
506
- unavailableInterests: [],
507
- });
508
- const prepared = await preparation;
509
- const published: string[][] = [];
510
- let verified = 0;
511
- expect(
512
- transport.adoptTerminalRuntimeInventory(prepared, (resource) =>
513
- published.push([...resource.semanticPaneIds]),
514
- ),
515
- ).not.toBeNull();
516
- transport.connectEvents(target, {
517
- onVerifiedOpen: () => (verified += 1),
518
- onInvalidate: () => undefined,
519
- onMalformedFrame: () => undefined,
520
- onPeerMismatch: () => undefined,
521
- onProtocolError: () => undefined,
522
- onClose: () => undefined,
523
- onError: () => undefined,
524
- });
525
- const catalogInvalidated: string[] = [];
526
- const catalogConnection = transport.connectWorkspaceCatalog(target, () =>
527
- catalogInvalidated.push("invalidated"),
528
- );
529
- await tick();
530
- sockets[0]!.frame({
531
- type: "resource.interests-ack",
532
- interestRevision: 2,
533
- sequence: 0,
534
- unavailableInterests: [],
535
- });
536
- await catalogConnection.ready;
537
- await tick();
538
- expect(verified).toBe(1);
539
- expect(catalogInvalidated).toEqual([]);
540
-
541
- revision = 2;
542
- sockets[0]!.emit("close");
543
- await tick();
544
- expect(catalogInvalidated).toEqual([]);
545
- expect(sockets).toHaveLength(2);
546
- expect(sockets[0]!.listeners.get("message")?.size ?? 0).toBe(0);
547
- sockets[1]!.emit("open");
548
- sockets[1]!.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
549
- await tick();
550
- sockets[1]!.frame({
551
- type: "resource.interests-ack",
552
- interestRevision: 1,
553
- sequence: 0,
554
- unavailableInterests: [],
555
- });
556
- await tick();
557
- expect(published).toEqual([]);
558
- expect(sockets).toHaveLength(2);
559
- expect(sockets[1]!.sent).toHaveLength(2);
560
- expect(sockets[1]!.sent[1]).toMatchObject({
561
- type: "subscribe",
562
- interestRevision: 2,
563
- interests: [
564
- { resource: "terminal-runtime-inventory" },
565
- { resource: "application-shell" },
566
- { resource: "workspace-catalog", workspaceName: null },
567
- ],
568
- });
569
- expect(verified).toBe(1);
570
- sockets[1]!.frame({
571
- type: "resource.interests-ack",
572
- interestRevision: 2,
573
- sequence: 0,
574
- unavailableInterests: [{ resource: "workspace-catalog", workspaceName: null }],
575
- });
576
- for (let attempt = 0; attempt < 3 && sockets[1]!.sent.length < 3; attempt += 1) await tick();
577
- expect(sockets[1]!.sent[2]).toMatchObject({
578
- interestRevision: 3,
579
- interests: [
580
- { resource: "terminal-runtime-inventory" },
581
- { resource: "application-shell" },
582
- { resource: "workspace-catalog", workspaceName: null },
583
- ],
584
- });
585
- sockets[1]!.frame({
586
- type: "resource.interests-ack",
587
- interestRevision: 3,
588
- sequence: 0,
589
- unavailableInterests: [{ resource: "workspace-catalog", workspaceName: null }],
590
- });
591
- for (let attempt = 0; attempt < 3 && clock.callbacks.size === 0; attempt += 1) await tick();
592
- expect(published).toEqual([]);
593
- expect(clock.callbacks.size).toBe(1);
594
-
595
- revision = 3;
596
- clock.runNext();
597
- await tick();
598
- expect(sockets).toHaveLength(3);
599
- sockets[2]!.emit("open");
600
- sockets[2]!.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
601
- await tick();
602
- sockets[2]!.frame({
603
- type: "resource.interests-ack",
604
- interestRevision: 1,
605
- sequence: 0,
606
- unavailableInterests: [],
607
- });
608
- await tick();
609
- expect(sockets[2]!.sent[1]).toMatchObject({
610
- interests: [
611
- { resource: "terminal-runtime-inventory" },
612
- { resource: "application-shell" },
613
- { resource: "workspace-catalog", workspaceName: null },
614
- ],
615
- });
616
- sockets[2]!.frame({
617
- type: "resource.interests-ack",
618
- interestRevision: 2,
619
- sequence: 0,
620
- unavailableInterests: [],
621
- });
622
- await tick();
623
- expect(published).toEqual([["pane.3"]]);
624
- expect(verified).toBe(2);
625
- expect(catalogInvalidated).toEqual(["invalidated"]);
626
- sockets[2]!.frame({
627
- type: "resource.changed",
628
- sequence: 1,
629
- workspaceName: null,
630
- resource: "workspace-catalog",
631
- revision: 1,
632
- causeOperationId: null,
633
- });
634
- expect(catalogInvalidated).toEqual(["invalidated", "invalidated"]);
635
- transport.disposeEventSupervisor();
636
- sockets[2]!.frame({
637
- type: "resource.changed",
638
- sequence: 2,
639
- workspaceName: null,
640
- resource: "workspace-catalog",
641
- revision: 2,
642
- causeOperationId: null,
643
- });
644
- expect(catalogInvalidated).toEqual(["invalidated", "invalidated"]);
645
- });
646
-
647
- it("retires each failed replacement and reconnects terminal then application-shell on a fresh socket", async () => {
648
- const sockets: FakeSocket[] = [];
649
- const clock = new FakeReconnectClock();
650
- let terminalReads = 0;
651
- const transport = createDirectLoopbackDaemonTransport({
652
- descriptor,
653
- ownerToken: "owner-secret",
654
- resolveSessionName: () => "alpha",
655
- terminalRuntimeAuthority: true,
656
- terminalReconnectClock: clock,
657
- createWebSocket: () => {
658
- const socket = new FakeSocket();
659
- sockets.push(socket);
660
- return socket;
661
- },
662
- fetch: async () => {
663
- terminalReads += 1;
664
- if (terminalReads >= 2 && terminalReads <= 5) throw new Error("read failed");
665
- return json({
666
- version: 1,
667
- daemon,
668
- resource: {
669
- workspaceName: "workspace.alpha",
670
- workspaceId: "workspace.0123456789abcdefabcd",
671
- sessionId: "session.0123456789abcdefabcd",
672
- resourceRevision: terminalReads,
673
- semanticPaneIds: [`pane.${terminalReads}`],
674
- },
675
- });
676
- },
677
- }) as TerminalFirstDaemonTransport;
678
- const preparation = transport.prepareTerminalRuntimeInventory(
679
- target,
680
- new AbortController().signal,
681
- );
682
- sockets[0]!.emit("open");
683
- sockets[0]!.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
684
- await tick();
685
- sockets[0]!.frame({
686
- type: "resource.interests-ack",
687
- interestRevision: 1,
688
- sequence: 0,
689
- unavailableInterests: [],
690
- });
691
- const prepared = await preparation;
692
- const published: string[][] = [];
693
- transport.adoptTerminalRuntimeInventory(prepared, (resource) =>
694
- published.push([...resource.semanticPaneIds]),
695
- );
696
- let verified = 0;
697
- transport.connectEvents(target, {
698
- onVerifiedOpen: () => (verified += 1),
699
- onInvalidate: () => undefined,
700
- onMalformedFrame: () => undefined,
701
- onPeerMismatch: () => undefined,
702
- onProtocolError: () => undefined,
703
- onClose: () => undefined,
704
- onError: () => undefined,
705
- });
706
-
707
- sockets[0]!.emit("close");
708
- await tick();
709
- for (const expectedSocketCount of [2, 3]) {
710
- expect(sockets).toHaveLength(expectedSocketCount);
711
- const failed = sockets.at(-1)!;
712
- failed.emit("open");
713
- failed.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
714
- await tick();
715
- failed.frame({
716
- type: "resource.interests-ack",
717
- interestRevision: 1,
718
- sequence: 0,
719
- unavailableInterests: [],
720
- });
721
- await tick();
722
- await tick();
723
- expect(failed.listeners.get("message")?.size ?? 0).toBe(0);
724
- expect(clock.callbacks.size).toBe(1);
725
- clock.runNext();
726
- }
727
- expect(sockets).toHaveLength(4);
728
- const recovered = sockets[3]!;
729
- recovered.emit("open");
730
- recovered.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
731
- await tick();
732
- recovered.frame({
733
- type: "resource.interests-ack",
734
- interestRevision: 1,
735
- sequence: 0,
736
- unavailableInterests: [],
737
- });
738
- await tick();
739
- expect(published).toEqual([["pane.6"]]);
740
- expect(recovered.sent[1]).toMatchObject({
741
- type: "subscribe",
742
- interestRevision: 2,
743
- interests: [{ resource: "terminal-runtime-inventory" }, { resource: "application-shell" }],
744
- });
745
- recovered.frame({
746
- type: "resource.interests-ack",
747
- interestRevision: 2,
748
- sequence: 0,
749
- unavailableInterests: [],
750
- });
751
- await tick();
752
- expect(verified).toBe(1);
753
- expect(clock.callbacks.size).toBe(0);
754
- transport.disposeEventSupervisor();
755
- });
756
-
757
- it("backs off when a replacement socket retires before terminal preparation settles", async () => {
758
- const sockets: FakeSocket[] = [];
759
- const clock = new FakeReconnectClock();
760
- const transport = createDirectLoopbackDaemonTransport({
761
- descriptor,
762
- resolveSessionName: () => "alpha",
763
- terminalRuntimeAuthority: true,
764
- terminalReconnectClock: clock,
765
- createWebSocket: () => {
766
- const socket = new FakeSocket();
767
- sockets.push(socket);
768
- return socket;
769
- },
770
- fetch: async () =>
771
- json({
772
- version: 1,
773
- daemon,
774
- resource: {
775
- workspaceName: "workspace.alpha",
776
- workspaceId: "workspace.0123456789abcdefabcd",
777
- sessionId: "session.0123456789abcdefabcd",
778
- resourceRevision: 1,
779
- semanticPaneIds: ["pane.1"],
780
- },
781
- }),
782
- }) as TerminalFirstDaemonTransport;
783
- const preparation = transport.prepareTerminalRuntimeInventory(
784
- target,
785
- new AbortController().signal,
786
- );
787
- sockets[0]!.emit("open");
788
- sockets[0]!.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
789
- await tick();
790
- sockets[0]!.frame({
791
- type: "resource.interests-ack",
792
- interestRevision: 1,
793
- sequence: 0,
794
- unavailableInterests: [],
795
- });
796
- const prepared = await preparation;
797
- transport.adoptTerminalRuntimeInventory(prepared, () => undefined);
798
-
799
- sockets[0]!.emit("close");
800
- await tick();
801
- expect(sockets).toHaveLength(2);
802
- sockets[1]!.emit("close");
803
- await tick();
804
-
805
- expect(sockets).toHaveLength(2);
806
- expect(clock.callbacks.size).toBe(1);
807
- clock.runNext();
808
- expect(sockets).toHaveLength(3);
809
- transport.disposeEventSupervisor();
810
- });
811
-
812
- it("cancels a failed replacement retry without creating a late socket after disposal", async () => {
813
- const sockets: FakeSocket[] = [];
814
- const clock = new FakeReconnectClock();
815
- let reads = 0;
816
- const transport = createDirectLoopbackDaemonTransport({
817
- descriptor,
818
- resolveSessionName: () => "alpha",
819
- terminalRuntimeAuthority: true,
820
- terminalReconnectClock: clock,
821
- createWebSocket: () => {
822
- const socket = new FakeSocket();
823
- sockets.push(socket);
824
- return socket;
825
- },
826
- fetch: async () => {
827
- reads += 1;
828
- if (reads > 1) throw new Error("read failed");
829
- return json({
830
- version: 1,
831
- daemon,
832
- resource: {
833
- workspaceName: "workspace.alpha",
834
- workspaceId: "workspace.0123456789abcdefabcd",
835
- sessionId: "session.0123456789abcdefabcd",
836
- resourceRevision: 1,
837
- semanticPaneIds: ["pane.1"],
838
- },
839
- });
840
- },
841
- }) as TerminalFirstDaemonTransport;
842
- const preparation = transport.prepareTerminalRuntimeInventory(
843
- target,
844
- new AbortController().signal,
845
- );
846
- sockets[0]!.emit("open");
847
- sockets[0]!.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
848
- await tick();
849
- sockets[0]!.frame({
850
- type: "resource.interests-ack",
851
- interestRevision: 1,
852
- sequence: 0,
853
- unavailableInterests: [],
854
- });
855
- const prepared = await preparation;
856
- transport.adoptTerminalRuntimeInventory(prepared, () => undefined);
857
- sockets[0]!.emit("close");
858
- await tick();
859
- sockets[1]!.emit("open");
860
- sockets[1]!.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
861
- await tick();
862
- sockets[1]!.frame({
863
- type: "resource.interests-ack",
864
- interestRevision: 1,
865
- sequence: 0,
866
- unavailableInterests: [],
867
- });
868
- await tick();
869
- const lateCallback = clock.callbacks.values().next().value as () => void;
870
- expect(clock.callbacks.size).toBe(1);
871
- transport.disposeEventSupervisor();
872
- expect(clock.callbacks.size).toBe(0);
873
- lateCallback();
874
- await tick();
875
- expect(sockets).toHaveLength(2);
876
- });
877
-
878
- it("requests and validates terminal-first V2 without app-window enrichment", async () => {
879
- const requests: URL[] = [];
880
- const transport = createDirectLoopbackDaemonTransport({
881
- descriptor,
882
- resolveSessionName: () => "alpha",
883
- applicationShellResourceVersion: APPLICATION_SHELL_RESOURCE_V2_VERSION,
884
- fetch: async (input) => {
885
- requests.push(new URL(String(input)));
886
- return json({
887
- version: APPLICATION_SHELL_RESOURCE_V2_VERSION,
888
- daemon,
889
- resource: terminalFirstResource,
890
- });
891
- },
892
- });
893
-
894
- const resource = await transport.fetchApplicationShell(target, new AbortController().signal);
895
-
896
- expect(requests).toHaveLength(1);
897
- expect(requests[0]!.pathname).toBe("/api/project/alpha/application-shell");
898
- expect(requests[0]!.searchParams.get("version")).toBe("2");
899
- expect(resource.terminalInventory).toEqual(terminalFirstResource.terminalInventory);
900
- expect("appWindows" in resource).toBe(false);
901
- });
902
-
903
- it("rejects a V2 response that omits the required terminal inventory", async () => {
904
- const incomplete = Object.fromEntries(
905
- Object.entries(terminalFirstResource).filter(([key]) => key !== "terminalInventory"),
906
- );
907
- const transport = createDirectLoopbackDaemonTransport({
908
- descriptor,
909
- resolveSessionName: () => "alpha",
910
- applicationShellResourceVersion: APPLICATION_SHELL_RESOURCE_V2_VERSION,
911
- fetch: async () =>
912
- json({
913
- version: APPLICATION_SHELL_RESOURCE_V2_VERSION,
914
- daemon,
915
- resource: incomplete,
916
- }),
917
- });
918
-
919
- await expect(
920
- transport.fetchApplicationShell(target, new AbortController().signal),
921
- ).rejects.toMatchObject({ kind: "schema-invalid" });
922
- });
923
-
924
- it("retains V3 as the default for canvas hosts", async () => {
925
- const requests: URL[] = [];
926
- const transport = createDirectLoopbackDaemonTransport({
927
- descriptor,
928
- resolveSessionName: () => "alpha",
929
- fetch: async (input) => {
930
- requests.push(new URL(String(input)));
931
- return json({ error: "fixture stops after request capture" }, 500);
932
- },
933
- });
934
-
935
- await expect(
936
- transport.fetchApplicationShell(target, new AbortController().signal),
937
- ).rejects.toMatchObject({ kind: "http-error", statusCode: 500 });
938
- expect(requests).toHaveLength(1);
939
- expect(requests[0]!.searchParams.get("version")).toBe("3");
940
- });
941
- });