tmux-ide 2.9.2 → 2.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/README.md +1 -1
  2. package/bin/cli.js +2071 -625
  3. package/package.json +14 -5
  4. package/packages/contracts/src/desktop-host.ts +3 -0
  5. package/packages/contracts/src/saved-machines.ts +19 -0
  6. package/packages/contracts/src/terminal-replica.ts +1 -1
  7. package/packages/core/package.json +3 -2
  8. package/packages/core/src/terminal-delivery.ts +51 -3
  9. package/packages/core/src/terminal-fnv64-wasm.ts +4 -3
  10. package/packages/core/src/terminal-replica-hash-cache.ts +129 -31
  11. package/packages/core/src/terminal-replica-owned-row.ts +27 -0
  12. package/packages/core/src/terminal-replica.ts +13 -28
  13. package/packages/daemon/dist/command-center/daemon-fleet-facts-observer.js +96 -10
  14. package/packages/daemon/dist/command-center/resources/saved-machine-route.js +40 -1
  15. package/packages/daemon/dist/command-center/ws-events.js +5 -24
  16. package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +8 -0
  17. package/packages/daemon/dist/lib/canonical-daemon.js +28 -2
  18. package/packages/daemon/dist/lib/daemon-embed.js +1 -1
  19. package/packages/daemon/dist/lib/daemon-service-manager.js +122 -0
  20. package/packages/daemon/dist/lib/daemon-service-plan.js +127 -0
  21. package/packages/daemon/dist/lib/daemon-service.js +259 -0
  22. package/packages/daemon/dist/lib/install-origin.js +2 -0
  23. package/packages/daemon/dist/lib/local-fleet-request.js +9 -3
  24. package/packages/daemon/dist/lib/log.js +6 -10
  25. package/packages/daemon/dist/lib/managed-installer-update.js +67 -0
  26. package/packages/daemon/dist/lib/{tui-download-lock.js → private-operation-lock.js} +11 -11
  27. package/packages/daemon/dist/lib/remote-daemon-info.js +3 -2
  28. package/packages/daemon/dist/lib/remote-daemon-lifecycle.js +4 -4
  29. package/packages/daemon/dist/lib/remote-tmux-command.js +10 -0
  30. package/packages/daemon/dist/lib/runtime-namespace.js +28 -16
  31. package/packages/daemon/dist/lib/saved-machines.js +40 -2
  32. package/packages/daemon/dist/lib/ssh-daemon-transport.js +24 -9
  33. package/packages/daemon/dist/lib/tmux-server-owner.js +1 -1
  34. package/packages/daemon/dist/lib/tmux-server-registration.js +21 -3
  35. package/packages/daemon/dist/lib/tui-binary.js +2 -2
  36. package/packages/daemon/dist/lib/update.js +18 -0
  37. package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +24 -1
  38. package/packages/daemon/dist/machines.js +40 -4
  39. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
  40. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  41. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  42. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
  43. package/packages/daemon/dist/terminal/attachments/direct-websocket.js +4 -1
  44. package/packages/daemon/dist/terminal/attachments/native-runtime.js +22 -13
  45. package/packages/daemon/dist/terminal/mirror/mirror-service.js +101 -3
  46. package/packages/daemon/dist/terminal/mirror/registered-window-guard.js +62 -0
  47. package/packages/daemon/dist/terminal/mirror/session-channel.js +75 -4
  48. package/packages/daemon/dist/terminal/mirror/shared-window-index.js +45 -0
  49. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +11 -1
  50. package/packages/daemon/dist/terminal/session-runtime/multiplexer-backend.js +1 -0
  51. package/packages/daemon/dist/terminal/session-runtime/registry.js +56 -7
  52. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +3 -1
  53. package/packages/daemon/dist/tui/mirror/dialog-model.js +2 -35
  54. package/packages/daemon/dist/tui/mirror/runtime/application-daemon-authority-owner.js +8 -0
  55. package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +20 -2
  56. package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +7 -1
  57. package/packages/daemon/dist/tui/mirror/runtime/application-machine-authority.js +59 -9
  58. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +27 -19
  59. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +23 -18
  60. package/packages/daemon/dist/tui/mirror/runtime/application-machine-startup.js +31 -2
  61. package/packages/daemon/dist/tui/mirror/runtime/application-palette-search-owner.js +7 -3
  62. package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +3 -0
  63. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +146 -2
  64. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +201 -50
  65. package/packages/daemon/dist/tui/mirror/runtime/bounded-performance-record-writer.js +50 -10
  66. package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -5
  67. package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +3 -0
  68. package/packages/daemon/dist/tui/mirror/runtime/workspace-terminal-fast-lane.js +8 -2
  69. package/packages/daemon/dist/tui/mirror/terminal-text.js +35 -0
  70. package/packages/daemon/dist/tui/mirror/workspace/connection-feedback.js +5 -3
  71. package/packages/daemon/src/command-center/daemon-fleet-facts-observer.ts +100 -10
  72. package/packages/daemon/src/command-center/resources/saved-machine-route.ts +48 -1
  73. package/packages/daemon/src/command-center/ws-events.ts +5 -25
  74. package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +12 -0
  75. package/packages/daemon/src/lib/canonical-daemon.ts +44 -2
  76. package/packages/daemon/src/lib/daemon-embed.ts +1 -0
  77. package/packages/daemon/src/lib/daemon-service-manager.ts +152 -0
  78. package/packages/daemon/src/lib/daemon-service-plan.ts +153 -0
  79. package/packages/daemon/src/lib/daemon-service.ts +301 -0
  80. package/packages/daemon/src/lib/install-origin.ts +5 -0
  81. package/packages/daemon/src/lib/local-fleet-request.ts +19 -3
  82. package/packages/daemon/src/lib/log.ts +13 -9
  83. package/packages/daemon/src/lib/managed-installer-update.ts +79 -0
  84. package/packages/daemon/src/lib/{tui-download-lock.ts → private-operation-lock.ts} +15 -11
  85. package/packages/daemon/src/lib/remote-daemon-info.ts +3 -3
  86. package/packages/daemon/src/lib/remote-daemon-lifecycle.ts +4 -4
  87. package/packages/daemon/src/lib/remote-tmux-command.ts +12 -0
  88. package/packages/daemon/src/lib/runtime-namespace.ts +27 -18
  89. package/packages/daemon/src/lib/saved-machines.ts +40 -1
  90. package/packages/daemon/src/lib/ssh-daemon-transport.ts +25 -7
  91. package/packages/daemon/src/lib/tmux-server-owner.ts +1 -0
  92. package/packages/daemon/src/lib/tmux-server-registration.ts +21 -3
  93. package/packages/daemon/src/lib/tui-binary.ts +2 -2
  94. package/packages/daemon/src/lib/update.ts +24 -0
  95. package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +22 -0
  96. package/packages/daemon/src/machines.ts +53 -5
  97. package/packages/daemon/src/terminal/attachments/direct-websocket.ts +8 -2
  98. package/packages/daemon/src/terminal/attachments/native-runtime.ts +28 -13
  99. package/packages/daemon/src/terminal/mirror/mirror-service.ts +105 -3
  100. package/packages/daemon/src/terminal/mirror/registered-window-guard.ts +66 -0
  101. package/packages/daemon/src/terminal/mirror/session-channel.ts +92 -4
  102. package/packages/daemon/src/terminal/mirror/shared-window-index.ts +42 -0
  103. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +12 -1
  104. package/packages/daemon/src/terminal/session-runtime/multiplexer-backend.ts +2 -0
  105. package/packages/daemon/src/terminal/session-runtime/registry.ts +70 -8
  106. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +3 -1
  107. package/packages/daemon/src/tui/mirror/dialog-model.ts +2 -32
  108. package/packages/daemon/src/tui/mirror/runtime/application-daemon-authority-owner.ts +8 -0
  109. package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +19 -2
  110. package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +6 -1
  111. package/packages/daemon/src/tui/mirror/runtime/application-machine-authority.ts +60 -11
  112. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +39 -33
  113. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +31 -23
  114. package/packages/daemon/src/tui/mirror/runtime/application-machine-startup.ts +38 -2
  115. package/packages/daemon/src/tui/mirror/runtime/application-palette-search-owner.ts +7 -4
  116. package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +3 -0
  117. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +1 -1
  118. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +47 -46
  119. package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +165 -2
  120. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +207 -53
  121. package/packages/daemon/src/tui/mirror/runtime/bounded-performance-record-writer.ts +49 -9
  122. package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +6 -5
  123. package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +4 -0
  124. package/packages/daemon/src/tui/mirror/runtime/workspace-terminal-fast-lane.ts +8 -3
  125. package/packages/daemon/src/tui/mirror/terminal-text.ts +32 -0
  126. package/packages/daemon/src/tui/mirror/workspace/connection-feedback.ts +6 -3
  127. package/packages/daemon-client/src/fleet-connection-status.ts +10 -1
  128. package/packages/daemon-client/src/workspace-client.ts +4 -1
  129. package/packages/contracts/src/__tests__/actions-contract.test.ts +0 -119
  130. package/packages/contracts/src/__tests__/agent-graph-overlay.test.ts +0 -428
  131. package/packages/contracts/src/__tests__/app-window-mutation.test.ts +0 -51
  132. package/packages/contracts/src/__tests__/app-window-state.test.ts +0 -34
  133. package/packages/contracts/src/__tests__/application-shell.test.ts +0 -645
  134. package/packages/contracts/src/__tests__/automation-operations.test.ts +0 -78
  135. package/packages/contracts/src/__tests__/causal-cell.test.ts +0 -47
  136. package/packages/contracts/src/__tests__/client-view-state.test.ts +0 -115
  137. package/packages/contracts/src/__tests__/cohesion-fixture.test.ts +0 -106
  138. package/packages/contracts/src/__tests__/commands.test.ts +0 -86
  139. package/packages/contracts/src/__tests__/control.test.ts +0 -154
  140. package/packages/contracts/src/__tests__/daemon-events.test.ts +0 -405
  141. package/packages/contracts/src/__tests__/daemon-resources.test.ts +0 -143
  142. package/packages/contracts/src/__tests__/daemon-wire.test.ts +0 -149
  143. package/packages/contracts/src/__tests__/desktop-host.test.ts +0 -269
  144. package/packages/contracts/src/__tests__/desktop-missions.test.ts +0 -215
  145. package/packages/contracts/src/__tests__/experience-conformance.test.ts +0 -153
  146. package/packages/contracts/src/__tests__/experience-kernel-boundary.test.ts +0 -113
  147. package/packages/contracts/src/__tests__/experience-shell.test.ts +0 -65
  148. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v2.json +0 -35
  149. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +0 -35
  150. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v4.json +0 -35
  151. package/packages/contracts/src/__tests__/fixtures/scoped-receipt.ts +0 -62
  152. package/packages/contracts/src/__tests__/fleet-agent-graph.test.ts +0 -231
  153. package/packages/contracts/src/__tests__/fleet-catalog.test.ts +0 -270
  154. package/packages/contracts/src/__tests__/fleet-lifecycle.test.ts +0 -62
  155. package/packages/contracts/src/__tests__/focus-overlay.test.ts +0 -110
  156. package/packages/contracts/src/__tests__/ide-config.test.ts +0 -64
  157. package/packages/contracts/src/__tests__/interaction-evidence.test.ts +0 -288
  158. package/packages/contracts/src/__tests__/interaction-receipt-v2.test.ts +0 -72
  159. package/packages/contracts/src/__tests__/issue-error.test.ts +0 -78
  160. package/packages/contracts/src/__tests__/mission-projections.test.ts +0 -186
  161. package/packages/contracts/src/__tests__/missions.test.ts +0 -272
  162. package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +0 -242
  163. package/packages/contracts/src/__tests__/pane-appearance.test.ts +0 -146
  164. package/packages/contracts/src/__tests__/pane-read-result.test.ts +0 -55
  165. package/packages/contracts/src/__tests__/pane-stream.test.ts +0 -536
  166. package/packages/contracts/src/__tests__/pane-widget-marker.test.ts +0 -327
  167. package/packages/contracts/src/__tests__/performance-metrics.test.ts +0 -73
  168. package/packages/contracts/src/__tests__/performance-qualification.test.ts +0 -137
  169. package/packages/contracts/src/__tests__/rich-card-widget.test.ts +0 -29
  170. package/packages/contracts/src/__tests__/session-runtime.test.ts +0 -512
  171. package/packages/contracts/src/__tests__/startup-readiness.test.ts +0 -409
  172. package/packages/contracts/src/__tests__/terminal-attachment-stream.test.ts +0 -72
  173. package/packages/contracts/src/__tests__/terminal-attachments.test.ts +0 -329
  174. package/packages/contracts/src/__tests__/terminal-delivery.test.ts +0 -88
  175. package/packages/contracts/src/__tests__/terminal-runtime-inventory.test.ts +0 -68
  176. package/packages/contracts/src/__tests__/terminals.test.ts +0 -87
  177. package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +0 -166
  178. package/packages/contracts/src/__tests__/transport-contract-loading.test.ts +0 -91
  179. package/packages/contracts/src/__tests__/visual-tokens.test.ts +0 -224
  180. package/packages/contracts/src/__tests__/window-links.test.ts +0 -243
  181. package/packages/contracts/src/__tests__/workspace-changes-resource.test.ts +0 -317
  182. package/packages/contracts/src/__tests__/workspace-changes-view.test.ts +0 -122
  183. package/packages/contracts/src/__tests__/workspace-config.test.ts +0 -294
  184. package/packages/contracts/src/__tests__/workspace-files-resource.test.ts +0 -261
  185. package/packages/contracts/src/__tests__/workspace-files-tree.test.ts +0 -147
  186. package/packages/contracts/src/__tests__/workspace-pane-creation.test.ts +0 -249
  187. package/packages/contracts/src/__tests__/workspace-promotion.test.ts +0 -94
  188. package/packages/contracts/src/__tests__/workspace-resource-identity.test.ts +0 -90
  189. package/packages/contracts/src/daemon-resource-request.test.ts +0 -192
  190. package/packages/contracts/src/native-operation-identity.test.ts +0 -23
  191. package/packages/contracts/src/workspace-admission.test.ts +0 -21
  192. package/packages/contracts/src/workspace-catalog-resource.test.ts +0 -146
  193. package/packages/core/src/agent-provisioning.test.ts +0 -42
  194. package/packages/core/src/application-shell-session.test.ts +0 -105
  195. package/packages/core/src/interaction-evidence.test.ts +0 -134
  196. package/packages/core/src/interaction-flow.test.ts +0 -66
  197. package/packages/core/src/interaction-receipts.test.ts +0 -671
  198. package/packages/core/src/navigator.test.ts +0 -34
  199. package/packages/core/src/optimistic-projection-conformance.test.ts +0 -75
  200. package/packages/core/src/optimistic-projection.test.ts +0 -118
  201. package/packages/core/src/performance-metrics.test.ts +0 -121
  202. package/packages/core/src/performance-qualification.test.ts +0 -283
  203. package/packages/core/src/saved-machines.test.ts +0 -80
  204. package/packages/core/src/terminal-compact-cold.test.ts +0 -87
  205. package/packages/core/src/terminal-conformance.test.ts +0 -48
  206. package/packages/core/src/terminal-delivery-cache-lifetime.test.ts +0 -167
  207. package/packages/core/src/terminal-delivery-encode-cooperative.test.ts +0 -140
  208. package/packages/core/src/terminal-delivery-representation-hash.test.ts +0 -77
  209. package/packages/core/src/terminal-delivery.test.ts +0 -1471
  210. package/packages/core/src/terminal-fnv64-wasm.test.ts +0 -61
  211. package/packages/core/src/terminal-replica-cold.test.ts +0 -31
  212. package/packages/core/src/terminal-replica-cooperative.test.ts +0 -170
  213. package/packages/core/src/terminal-replica-hash-cache.test.ts +0 -283
  214. package/packages/core/src/terminal-replica.test.ts +0 -347
  215. package/packages/core/src/workspace-selection.test.ts +0 -63
  216. package/packages/daemon/dist/__tests__/support.js +0 -33
  217. package/packages/daemon/dist/lib/__tests__/fixtures/workspace-state-writer.js +0 -58
  218. package/packages/daemon/dist/lib/__tests__/installed-recovery-fixture.js +0 -400
  219. package/packages/daemon/dist/terminal/__tests__/MockPtyAdapter.js +0 -182
  220. package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +0 -98
  221. package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +0 -156
  222. package/packages/daemon/src/tui/mirror/__snapshots__/activity-surface-renderer.test.tsx.snap +0 -42
  223. package/packages/daemon/src/tui/mirror/__snapshots__/changes-terminal-surface-renderer.test.tsx.snap +0 -267
  224. package/packages/daemon/src/tui/mirror/__snapshots__/home-files-surface-renderer.test.tsx.snap +0 -267
  225. package/packages/daemon/src/tui/mirror/__snapshots__/missions-surface-renderer.test.tsx.snap +0 -134
  226. package/packages/daemon/src/tui/mirror/__snapshots__/recipes-gallery-renderer.test.tsx.snap +0 -141
  227. package/packages/daemon/src/tui/mirror/__snapshots__/shell-chrome-renderer.test.tsx.snap +0 -134
  228. package/packages/daemon/src/tui/mirror/runtime/__snapshots__/application-shell-view-renderer.test.tsx.snap +0 -87
  229. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/agent-terminal-canvas-renderer.test.tsx.snap +0 -348
  230. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/application-shell-renderer.test.tsx.snap +0 -134
  231. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/command-palette-surface-renderer.test.tsx.snap +0 -255
  232. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/pane-frame-renderer.test.tsx.snap +0 -525
  233. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/terminal-pane-header-polish-renderer.test.tsx.snap +0 -31
  234. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/workbench-shell-renderer.test.tsx.snap +0 -134
  235. package/packages/daemon-client/src/application-shell-session.test.ts +0 -196
  236. package/packages/daemon-client/src/automation-client.test.ts +0 -214
  237. package/packages/daemon-client/src/bootstrap-coordinator.test.ts +0 -145
  238. package/packages/daemon-client/src/connection-supervisor.test.ts +0 -109
  239. package/packages/daemon-client/src/direct-application-shell-transport.test.ts +0 -941
  240. package/packages/daemon-client/src/first-latest-coordinator.test.ts +0 -175
  241. package/packages/daemon-client/src/fleet-dial-scheduler.test.ts +0 -114
  242. package/packages/daemon-client/src/fleet-environments.test.ts +0 -40
  243. package/packages/daemon-client/src/generation-bound-store-timers.test.ts +0 -74
  244. package/packages/daemon-client/src/owner-action-client.test.ts +0 -245
  245. package/packages/daemon-client/src/pane-stream-client.test.ts +0 -2841
  246. package/packages/daemon-client/src/pane-stream-clock-calibration.test.ts +0 -207
  247. package/packages/daemon-client/src/pane-stream-loopback-benchmark.test.ts +0 -51
  248. package/packages/daemon-client/src/push-resource-session.test.ts +0 -509
  249. package/packages/daemon-client/src/resource-replica.test.ts +0 -186
  250. package/packages/daemon-client/src/runtime-resource-ledger.test.ts +0 -18
  251. package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +0 -114
  252. package/packages/daemon-client/src/terminal-fast-lane.test.ts +0 -835
  253. package/packages/daemon-client/src/tmux-server-client.test.ts +0 -279
  254. package/packages/daemon-client/src/tmux-server-interaction-events.test.ts +0 -230
  255. package/packages/daemon-client/src/workspace-catalog-v2.test.ts +0 -109
  256. package/packages/daemon-client/src/workspace-client-boundaries.test.ts +0 -59
  257. package/packages/daemon-client/src/workspace-client-operations.test.ts +0 -96
  258. package/packages/daemon-client/src/workspace-client.test.ts +0 -2912
  259. package/packages/daemon-client/src/workspace-event-supervisor.test.ts +0 -1086
  260. package/packages/daemon-client/src/workspace-pane-client.test.ts +0 -80
  261. package/packages/presentation/src/presentation-contract.test.ts +0 -63
  262. package/packages/tmux-bridge/src/runner.test.ts +0 -614
  263. 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
- });