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,2841 +0,0 @@
1
- import { describe, expect, it, mock } from "bun:test";
2
- import type { TerminalDeliveryServerMessage } from "@tmux-ide/contracts";
3
-
4
- import {
5
- classifyPaneStreamInputTransportDelay,
6
- openPaneStreamRuntimeClient,
7
- type PaneStreamClientSocket,
8
- type PaneStreamInputTransportStageEvent,
9
- } from "./pane-stream-client.ts";
10
- import { runtimeResourceSnapshot } from "./runtime-resource-ledger.ts";
11
-
12
- const INSTANCE = "11111111-1111-4111-8111-111111111111";
13
- const REQUEST = "22222222-2222-4222-8222-222222222222";
14
- const TRANSACTION = "33333333-3333-4333-8333-333333333333";
15
- const OPERATION = "44444444-4444-4444-8444-444444444444";
16
- const TICKET = `ps2_${"a".repeat(43)}`;
17
-
18
- class FakeSocket implements PaneStreamClientSocket {
19
- readyState = 1;
20
- bufferedAmount: number | undefined;
21
- readonly sent: unknown[] = [];
22
- readonly listeners = new Map<string, Set<(event: { data?: unknown }) => void>>();
23
- closeCalls = 0;
24
- closed: { code?: number; reason?: string } | null = null;
25
- onSend: ((frame: Record<string, unknown>) => void) | null = null;
26
-
27
- addEventListener(type: string, listener: (event: { data?: unknown }) => void): void {
28
- const listeners = this.listeners.get(type) ?? new Set();
29
- listeners.add(listener);
30
- this.listeners.set(type, listeners);
31
- }
32
- removeEventListener(type: string, listener: (event: { data?: unknown }) => void): void {
33
- this.listeners.get(type)?.delete(listener);
34
- }
35
- send(data: string): void {
36
- const frame = JSON.parse(data) as Record<string, unknown>;
37
- this.sent.push(frame);
38
- this.onSend?.(frame);
39
- }
40
- close(code?: number, reason?: string): void {
41
- this.closeCalls += 1;
42
- this.closed = { code, reason };
43
- }
44
- emit(type: string, data?: unknown): void {
45
- for (const listener of this.listeners.get(type) ?? []) listener({ data });
46
- }
47
- message(frame: unknown): void {
48
- this.emit("message", JSON.stringify(frame));
49
- }
50
- }
51
-
52
- function descriptor(viewerMode: "interactive" | "read-only" = "interactive") {
53
- return {
54
- protocolVersion: 2,
55
- webSocketUrl: "ws://127.0.0.1:6060/v2/terminal/pane-streams/redeem",
56
- subprotocol: "tmux-ide-pane-stream.v2",
57
- redemptionTicket: TICKET,
58
- daemonInstanceId: INSTANCE,
59
- requestId: REQUEST,
60
- expiresAt: Date.now() + 5_000,
61
- panes: ["pane.editor"],
62
- effectiveViewerMode: viewerMode,
63
- } as const;
64
- }
65
-
66
- function options(socket: FakeSocket, overrides: Record<string, unknown> = {}) {
67
- return {
68
- baseUrl: "http://127.0.0.1:6060",
69
- ownerToken: "owner-secret",
70
- daemonInstanceId: INSTANCE,
71
- origin: "http://127.0.0.1:5173",
72
- hostClientId: "tui:one",
73
- requestId: REQUEST,
74
- stream: {
75
- protocolVersion: 2 as const,
76
- workspaceName: "alpha",
77
- panes: ["pane.editor"],
78
- viewerMode: "interactive" as const,
79
- terminalDelivery: {
80
- protocolVersions: [1],
81
- encodings: ["semantic-v1" as const],
82
- richPlacements: false,
83
- },
84
- },
85
- createSocket: mock((_issued, headers) => {
86
- expect(headers).toEqual({
87
- Origin: "http://127.0.0.1:5173",
88
- "X-Tmux-Ide-Host-Client-Id": "tui:one",
89
- "X-Tmux-Ide-Request-Id": REQUEST,
90
- });
91
- queueMicrotask(() => socket.emit("open"));
92
- return socket;
93
- }),
94
- fetch: mock(async (_url, init) => {
95
- expect(init?.headers).toMatchObject({
96
- Authorization: "Bearer owner-secret",
97
- Origin: "http://127.0.0.1:5173",
98
- "X-Tmux-Ide-Request-Id": REQUEST,
99
- "X-Tmux-Ide-Expected-Daemon-Instance-Id": INSTANCE,
100
- "X-Tmux-Ide-Host-Client-Id": "tui:one",
101
- });
102
- return Response.json({ status: "issued", descriptor: descriptor() });
103
- }) as typeof fetch,
104
- onNegotiated: mock(),
105
- onTerminalDelivery: mock(),
106
- onFault: mock(),
107
- ...overrides,
108
- };
109
- }
110
-
111
- function acceptInteractiveHandshake(socket: FakeSocket): void {
112
- socket.onSend = (frame) => {
113
- if (frame.type === "redeem") {
114
- queueMicrotask(() =>
115
- socket.message({
116
- type: "ready",
117
- protocolVersion: 2,
118
- daemonInstanceId: INSTANCE,
119
- requestId: REQUEST,
120
- panes: ["pane.editor"],
121
- effectiveViewerMode: "interactive",
122
- }),
123
- );
124
- return;
125
- }
126
- if (frame.type === "authority-request") {
127
- queueMicrotask(() =>
128
- socket.message({
129
- type: "authority-receipt",
130
- requestId: frame.requestId,
131
- authority: frame.authority,
132
- status: "granted",
133
- lease: {
134
- generation: INSTANCE,
135
- session: "alpha",
136
- clientId: "tui:one",
137
- authority: frame.authority,
138
- token: "55555555-5555-4555-8555-555555555555",
139
- revision: 1,
140
- },
141
- snapshot: {
142
- generation: INSTANCE,
143
- session: "alpha",
144
- revision: 1,
145
- nativeGeometryYieldUntilMs: 0,
146
- owners: { input: "tui:one", focus: null, geometry: null },
147
- clients: [],
148
- },
149
- }),
150
- );
151
- return;
152
- }
153
- if (frame.type === "input") {
154
- queueMicrotask(() => socket.message({ type: "input-ack", pane: frame.pane, seq: frame.seq }));
155
- }
156
- };
157
- }
158
-
159
- describe("semantic pane-stream runtime client", () => {
160
- it("rejects a regressing full-layout topology epoch before publication", async () => {
161
- const socket = new FakeSocket();
162
- acceptInteractiveHandshake(socket);
163
- const layouts: unknown[] = [];
164
- const faults: Error[] = [];
165
- await openPaneStreamRuntimeClient(
166
- options(socket, {
167
- requestInitialInputAuthority: false,
168
- onLayoutSnapshot: (frame: unknown) => layouts.push(frame),
169
- onFault: (error: Error) => faults.push(error),
170
- }),
171
- );
172
- const frame = {
173
- type: "layout-snapshot",
174
- topologyEpoch: 4,
175
- windowLinks: {
176
- liveSessionId: "live-session." + "a".repeat(20),
177
- linkRevision: 1,
178
- activeLinkId: "window-link." + "a".repeat(32),
179
- links: [
180
- {
181
- linkId: "window-link." + "a".repeat(32),
182
- semanticWindowId: "window.one",
183
- displayIndex: 0,
184
- },
185
- ],
186
- },
187
- layouts: [
188
- {
189
- type: "layout",
190
- semanticWindowId: "window.one",
191
- windowName: "work",
192
- currentWindow: true,
193
- cols: 80,
194
- rows: 24,
195
- zoomed: false,
196
- paneBorderStatus: "off",
197
- panes: [
198
- {
199
- pane: "pane.editor",
200
- left: 0,
201
- top: 0,
202
- width: 80,
203
- height: 24,
204
- active: true,
205
- },
206
- ],
207
- },
208
- ],
209
- };
210
- socket.message(frame);
211
- expect(layouts).toHaveLength(1);
212
- socket.message(frame);
213
- expect(layouts).toHaveLength(1);
214
- expect(faults).toHaveLength(1);
215
- expect(socket.closed).toMatchObject({ code: 1008, reason: "protocol-error" });
216
- });
217
-
218
- it.each(["session", "revision", "membership", "index"])(
219
- "rejects %s link authority changes before publication",
220
- async (change) => {
221
- const socket = new FakeSocket();
222
- acceptInteractiveHandshake(socket);
223
- const published = mock();
224
- const faults = mock();
225
- const client = await openPaneStreamRuntimeClient(
226
- options(socket, { onLayoutSnapshot: published, onFault: faults }),
227
- );
228
- const firstId = `window-link.${"a".repeat(32)}`;
229
- const secondId = `window-link.${"b".repeat(32)}`;
230
- const windowLinks = {
231
- liveSessionId: `live-session.${"a".repeat(20)}`,
232
- linkRevision: 3,
233
- activeLinkId: firstId,
234
- links: [
235
- { linkId: firstId, semanticWindowId: "window.one", displayIndex: 0 },
236
- { linkId: secondId, semanticWindowId: "window.one", displayIndex: 1 },
237
- ],
238
- };
239
- const frame = {
240
- type: "layout-snapshot",
241
- topologyEpoch: 1,
242
- windowLinks,
243
- layouts: [
244
- {
245
- type: "layout",
246
- semanticWindowId: "window.one",
247
- windowName: "work",
248
- currentWindow: true,
249
- cols: 80,
250
- rows: 24,
251
- zoomed: false,
252
- paneBorderStatus: "off",
253
- panes: [{ pane: "pane.editor", left: 0, top: 0, width: 80, height: 24, active: true }],
254
- },
255
- ],
256
- };
257
- socket.message(frame);
258
- socket.message({
259
- ...frame,
260
- topologyEpoch: 2,
261
- windowLinks: { ...windowLinks, activeLinkId: secondId },
262
- });
263
- expect(published).toHaveBeenCalledTimes(2);
264
- const changed = structuredClone(windowLinks);
265
- if (change === "session") changed.liveSessionId = `live-session.${"b".repeat(20)}`;
266
- if (change === "revision") changed.linkRevision = 2;
267
- if (change === "membership") changed.links.pop();
268
- if (change === "index") changed.links[1]!.displayIndex = 9;
269
- socket.message({ ...frame, topologyEpoch: 3, windowLinks: changed });
270
- expect(published).toHaveBeenCalledTimes(2);
271
- expect(faults).toHaveBeenCalledTimes(1);
272
- client.close();
273
- },
274
- );
275
-
276
- it("calibrates five bounded clock probes before readiness and never exposes raw origins", async () => {
277
- const socket = new FakeSocket();
278
- const calibrations: unknown[] = [];
279
- const outcomes: unknown[] = [];
280
- let raw = 8_000_000_000_000;
281
- socket.onSend = (frame) => {
282
- if (frame.type === "redeem") {
283
- queueMicrotask(() =>
284
- socket.message({
285
- type: "ready",
286
- protocolVersion: 2,
287
- daemonInstanceId: INSTANCE,
288
- requestId: REQUEST,
289
- connectionClientId: "tui:one",
290
- panes: ["pane.editor"],
291
- effectiveViewerMode: "interactive",
292
- diagnosticCapabilities: ["clock-bounds-v1"],
293
- }),
294
- );
295
- } else if (frame.type === "clock-probe") {
296
- const clientSendMicros = frame.clientSendMicros as number;
297
- queueMicrotask(() =>
298
- socket.message({
299
- type: "clock-probe-ack",
300
- requestId: REQUEST,
301
- daemonInstanceId: INSTANCE,
302
- probe: frame.probe,
303
- clientSendMicros,
304
- daemonReceiveMicros: clientSendMicros + 100,
305
- daemonSendMicros: clientSendMicros + 101,
306
- }),
307
- );
308
- }
309
- };
310
- await openPaneStreamRuntimeClient(
311
- options(socket, {
312
- requestInitialInputAuthority: false,
313
- diagnosticCapabilities: ["clock-bounds-v1"],
314
- diagnosticSharedNowMicros: () => (raw += 10),
315
- onClockCalibration: (value: unknown) => calibrations.push(value),
316
- onClockCalibrationOutcome: (value: unknown) => outcomes.push(value),
317
- }),
318
- );
319
- const probes = socket.sent.filter(
320
- (frame) => (frame as { type?: string }).type === "clock-probe",
321
- ) as Array<{ clientSendMicros: number }>;
322
- expect(probes).toHaveLength(5);
323
- expect(probes.every(({ clientSendMicros }) => clientSendMicros < 1_000)).toBe(true);
324
- expect(JSON.stringify(socket.sent)).not.toContain("8000000000000");
325
- expect(calibrations).toHaveLength(1);
326
- expect(calibrations[0]).toMatchObject({
327
- requestId: REQUEST,
328
- daemonInstanceId: INSTANCE,
329
- uncertaintyMicros: 9,
330
- });
331
- expect(outcomes).toEqual([
332
- expect.objectContaining({
333
- reason: "calibrated",
334
- attemptedProbes: 5,
335
- receivedProbes: 5,
336
- validProbes: 5,
337
- selectedProbes: 1,
338
- }),
339
- ]);
340
- const finalProbe = socket.sent.findLast(
341
- (frame) => (frame as { type?: string }).type === "clock-probe",
342
- ) as { probe: number; clientSendMicros: number };
343
- socket.message({
344
- type: "clock-probe-ack",
345
- requestId: REQUEST,
346
- daemonInstanceId: INSTANCE,
347
- probe: finalProbe.probe,
348
- clientSendMicros: finalProbe.clientSendMicros,
349
- daemonReceiveMicros: finalProbe.clientSendMicros + 100,
350
- daemonSendMicros: finalProbe.clientSendMicros + 101,
351
- });
352
- expect(calibrations).toHaveLength(1);
353
- });
354
-
355
- it("does no clock work when the capability is absent", async () => {
356
- const socket = new FakeSocket();
357
- acceptInteractiveHandshake(socket);
358
- const shared = mock(() => {
359
- throw new Error("must not run");
360
- });
361
- await openPaneStreamRuntimeClient(
362
- options(socket, {
363
- requestInitialInputAuthority: false,
364
- diagnosticClockProbeCount: 99,
365
- diagnosticSharedNowMicros: shared,
366
- }),
367
- );
368
- expect(shared).not.toHaveBeenCalled();
369
- expect(socket.sent.some((frame) => (frame as { type?: string }).type === "clock-probe")).toBe(
370
- false,
371
- );
372
- });
373
-
374
- it("settles a timed-out calibration once and ignores a late reply", async () => {
375
- const socket = new FakeSocket();
376
- const calibrations: unknown[] = [];
377
- const outcomes: unknown[] = [];
378
- let probe: { probe: number; clientSendMicros: number } | undefined;
379
- socket.onSend = (frame) => {
380
- if (frame.type === "redeem") {
381
- queueMicrotask(() =>
382
- socket.message({
383
- type: "ready",
384
- protocolVersion: 2,
385
- daemonInstanceId: INSTANCE,
386
- requestId: REQUEST,
387
- panes: ["pane.editor"],
388
- effectiveViewerMode: "interactive",
389
- diagnosticCapabilities: ["clock-bounds-v1"],
390
- }),
391
- );
392
- } else if (frame.type === "clock-probe") probe = frame as typeof probe;
393
- };
394
- await openPaneStreamRuntimeClient(
395
- options(socket, {
396
- requestInitialInputAuthority: false,
397
- diagnosticCapabilities: ["clock-bounds-v1"],
398
- diagnosticSharedNowMicros: (() => {
399
- let now = 1_000;
400
- return () => (now += 10);
401
- })(),
402
- onClockCalibration: (value: unknown) => calibrations.push(value),
403
- onClockCalibrationOutcome: (value: unknown) => outcomes.push(value),
404
- }),
405
- );
406
- expect(calibrations).toEqual([null]);
407
- expect(outcomes).toEqual([
408
- expect.objectContaining({
409
- reason: "timeout-no-sample",
410
- attemptedProbes: 1,
411
- receivedProbes: 0,
412
- validProbes: 0,
413
- selectedProbes: 0,
414
- }),
415
- ]);
416
- socket.message({
417
- type: "clock-probe-ack",
418
- requestId: REQUEST,
419
- daemonInstanceId: INSTANCE,
420
- probe: probe!.probe,
421
- clientSendMicros: probe!.clientSendMicros,
422
- daemonReceiveMicros: 100,
423
- daemonSendMicros: 101,
424
- });
425
- expect(calibrations).toEqual([null]);
426
- expect(outcomes).toHaveLength(1);
427
- });
428
-
429
- it("settles a retired pending calibration immediately and releases its timer", async () => {
430
- for (const retirement of ["socket-close", "protocol-failure"] as const) {
431
- const socket = new FakeSocket();
432
- const outcomes: unknown[] = [];
433
- let probe: Record<string, unknown> | null = null;
434
- const before = runtimeResourceSnapshot()["runtime-timer"].active;
435
- socket.onSend = (frame) => {
436
- if (frame.type === "redeem") {
437
- queueMicrotask(() =>
438
- socket.message({
439
- type: "ready",
440
- protocolVersion: 2,
441
- daemonInstanceId: INSTANCE,
442
- requestId: REQUEST,
443
- panes: ["pane.editor"],
444
- effectiveViewerMode: "interactive",
445
- diagnosticCapabilities: ["clock-bounds-v1"],
446
- }),
447
- );
448
- } else if (frame.type === "clock-probe") {
449
- probe = frame;
450
- queueMicrotask(() => {
451
- if (retirement === "socket-close") socket.emit("close");
452
- else socket.message({ type: "not-a-pane-stream-frame" });
453
- });
454
- }
455
- };
456
- await expect(
457
- openPaneStreamRuntimeClient(
458
- options(socket, {
459
- requestInitialInputAuthority: false,
460
- diagnosticCapabilities: ["clock-bounds-v1"],
461
- diagnosticSharedNowMicros: (() => {
462
- let now = 1_000;
463
- return () => (now += 10);
464
- })(),
465
- onClockCalibration: () => undefined,
466
- onClockCalibrationOutcome: (value: unknown) => outcomes.push(value),
467
- }),
468
- ),
469
- ).rejects.toThrow();
470
- expect(outcomes).toEqual([
471
- expect.objectContaining({
472
- reason: "connection-closed",
473
- attemptedProbes: 1,
474
- receivedProbes: 0,
475
- validProbes: 0,
476
- selectedProbes: 0,
477
- }),
478
- ]);
479
- expect(runtimeResourceSnapshot()["runtime-timer"].active).toBe(before);
480
- socket.message({
481
- type: "clock-probe-ack",
482
- requestId: REQUEST,
483
- daemonInstanceId: INSTANCE,
484
- probe: probe!.probe,
485
- clientSendMicros: probe!.clientSendMicros,
486
- daemonReceiveMicros: 5,
487
- daemonSendMicros: 6,
488
- });
489
- expect(outcomes).toHaveLength(1);
490
- }
491
- });
492
-
493
- it("aborts a pending calibration immediately without adopting the provisional client", async () => {
494
- const socket = new FakeSocket();
495
- const controller = new AbortController();
496
- const outcomes: unknown[] = [];
497
- const onNegotiated = mock();
498
- const before = runtimeResourceSnapshot()["runtime-timer"].active;
499
- socket.onSend = (frame) => {
500
- if (frame.type === "redeem") {
501
- queueMicrotask(() =>
502
- socket.message({
503
- type: "ready",
504
- protocolVersion: 2,
505
- daemonInstanceId: INSTANCE,
506
- requestId: REQUEST,
507
- panes: ["pane.editor"],
508
- effectiveViewerMode: "interactive",
509
- diagnosticCapabilities: ["clock-bounds-v1"],
510
- }),
511
- );
512
- } else if (frame.type === "clock-probe") {
513
- queueMicrotask(() => controller.abort(new Error("retired generation")));
514
- }
515
- };
516
- await expect(
517
- openPaneStreamRuntimeClient(
518
- options(socket, {
519
- signal: controller.signal,
520
- onNegotiated,
521
- requestInitialInputAuthority: false,
522
- diagnosticCapabilities: ["clock-bounds-v1"],
523
- diagnosticSharedNowMicros: () => 1_000,
524
- onClockCalibration: () => undefined,
525
- onClockCalibrationOutcome: (value: unknown) => outcomes.push(value),
526
- }),
527
- ),
528
- ).rejects.toThrow("retired generation");
529
- expect(outcomes).toEqual([
530
- expect.objectContaining({
531
- reason: "connection-closed",
532
- attemptedProbes: 1,
533
- receivedProbes: 0,
534
- validProbes: 0,
535
- selectedProbes: 0,
536
- }),
537
- ]);
538
- expect(onNegotiated).not.toHaveBeenCalled();
539
- expect(runtimeResourceSnapshot()["runtime-timer"].active).toBe(before);
540
- });
541
-
542
- it("admits a 300ms cold first probe and calibrates before readiness", async () => {
543
- const socket = new FakeSocket();
544
- const outcomes: unknown[] = [];
545
- let now = 1_000;
546
- socket.onSend = (frame) => {
547
- if (frame.type === "redeem") {
548
- queueMicrotask(() =>
549
- socket.message({
550
- type: "ready",
551
- protocolVersion: 2,
552
- daemonInstanceId: INSTANCE,
553
- requestId: REQUEST,
554
- panes: ["pane.editor"],
555
- effectiveViewerMode: "interactive",
556
- diagnosticCapabilities: ["clock-bounds-v1"],
557
- }),
558
- );
559
- } else if (frame.type === "clock-probe") {
560
- const reply = () => {
561
- now += frame.probe === 1 ? 300_000 : 10;
562
- socket.message({
563
- type: "clock-probe-ack",
564
- requestId: REQUEST,
565
- daemonInstanceId: INSTANCE,
566
- probe: frame.probe,
567
- clientSendMicros: frame.clientSendMicros,
568
- daemonReceiveMicros: (frame.clientSendMicros as number) + 20,
569
- daemonSendMicros: (frame.clientSendMicros as number) + 21,
570
- });
571
- };
572
- if (frame.probe === 1) setTimeout(reply, 300);
573
- else queueMicrotask(reply);
574
- }
575
- };
576
- await openPaneStreamRuntimeClient(
577
- options(socket, {
578
- requestInitialInputAuthority: false,
579
- diagnosticCapabilities: ["clock-bounds-v1"],
580
- diagnosticSharedNowMicros: () => now,
581
- onClockCalibration: () => undefined,
582
- onClockCalibrationOutcome: (value: unknown) => outcomes.push(value),
583
- }),
584
- );
585
- expect(outcomes).toEqual([
586
- expect.objectContaining({ reason: "calibrated", attemptedProbes: 5, receivedProbes: 5 }),
587
- ]);
588
- });
589
-
590
- it("retains a valid sample when a later probe times out", async () => {
591
- const socket = new FakeSocket();
592
- const calibrations: unknown[] = [];
593
- const outcomes: unknown[] = [];
594
- let now = 1_000;
595
- socket.onSend = (frame) => {
596
- if (frame.type === "redeem") {
597
- queueMicrotask(() =>
598
- socket.message({
599
- type: "ready",
600
- protocolVersion: 2,
601
- daemonInstanceId: INSTANCE,
602
- requestId: REQUEST,
603
- panes: ["pane.editor"],
604
- effectiveViewerMode: "interactive",
605
- diagnosticCapabilities: ["clock-bounds-v1"],
606
- }),
607
- );
608
- } else if (frame.type === "clock-probe" && frame.probe === 1) {
609
- now += 20;
610
- queueMicrotask(() =>
611
- socket.message({
612
- type: "clock-probe-ack",
613
- requestId: REQUEST,
614
- daemonInstanceId: INSTANCE,
615
- probe: 1,
616
- clientSendMicros: frame.clientSendMicros,
617
- daemonReceiveMicros: (frame.clientSendMicros as number) + 5,
618
- daemonSendMicros: (frame.clientSendMicros as number) + 6,
619
- }),
620
- );
621
- }
622
- };
623
- await openPaneStreamRuntimeClient(
624
- options(socket, {
625
- requestInitialInputAuthority: false,
626
- diagnosticCapabilities: ["clock-bounds-v1"],
627
- diagnosticSharedNowMicros: () => now,
628
- onClockCalibration: (value: unknown) => calibrations.push(value),
629
- onClockCalibrationOutcome: (value: unknown) => outcomes.push(value),
630
- }),
631
- );
632
- expect(calibrations[0]).toMatchObject({ probe: 1 });
633
- expect(outcomes).toEqual([
634
- expect.objectContaining({
635
- reason: "timeout-retained-sample",
636
- attemptedProbes: 2,
637
- receivedProbes: 1,
638
- validProbes: 1,
639
- selectedProbes: 1,
640
- selectedProbe: 1,
641
- }),
642
- ]);
643
-
644
- const failOpenSocket = new FakeSocket();
645
- let failOpenNow = 1_000;
646
- failOpenSocket.onSend = (frame) => {
647
- if (frame.type === "redeem") {
648
- queueMicrotask(() =>
649
- failOpenSocket.message({
650
- type: "ready",
651
- protocolVersion: 2,
652
- daemonInstanceId: INSTANCE,
653
- requestId: REQUEST,
654
- panes: ["pane.editor"],
655
- effectiveViewerMode: "interactive",
656
- diagnosticCapabilities: ["clock-bounds-v1"],
657
- }),
658
- );
659
- } else if (frame.type === "clock-probe") {
660
- failOpenNow += 10;
661
- queueMicrotask(() =>
662
- failOpenSocket.message({
663
- type: "clock-probe-ack",
664
- requestId: REQUEST,
665
- daemonInstanceId: INSTANCE,
666
- probe: frame.probe,
667
- clientSendMicros: frame.clientSendMicros,
668
- daemonReceiveMicros: (frame.clientSendMicros as number) + 2,
669
- daemonSendMicros: (frame.clientSendMicros as number) + 3,
670
- }),
671
- );
672
- }
673
- };
674
- await expect(
675
- openPaneStreamRuntimeClient(
676
- options(failOpenSocket, {
677
- requestInitialInputAuthority: false,
678
- diagnosticCapabilities: ["clock-bounds-v1"],
679
- diagnosticSharedNowMicros: () => failOpenNow,
680
- onClockCalibration: () => {
681
- throw new Error("calibration sink failed");
682
- },
683
- onClockCalibrationOutcome: () => {
684
- throw new Error("outcome sink failed");
685
- },
686
- onConnectionDiagnostic: (phase: string) => {
687
- if (phase === "clock-calibration") throw new Error("connection sink failed");
688
- },
689
- }),
690
- ),
691
- ).resolves.toBeDefined();
692
- });
693
-
694
- it("names malformed, duplicate, and unavailable calibration replies exactly once", async () => {
695
- const cases = [
696
- {
697
- reason: "ack-request-mismatch",
698
- mutate: (frame: Record<string, unknown>) => ({
699
- ...frame,
700
- requestId: "99999999-9999-4999-8999-999999999999",
701
- }),
702
- },
703
- {
704
- reason: "ack-generation-mismatch",
705
- mutate: (frame: Record<string, unknown>) => ({
706
- ...frame,
707
- daemonInstanceId: "99999999-9999-4999-8999-999999999999",
708
- }),
709
- },
710
- {
711
- reason: "ack-probe-mismatch",
712
- mutate: (frame: Record<string, unknown>) => ({ ...frame, probe: 2 }),
713
- },
714
- {
715
- reason: "ack-client-send-mismatch",
716
- mutate: (frame: Record<string, unknown>) => ({
717
- ...frame,
718
- clientSendMicros: (frame.clientSendMicros as number) + 1,
719
- }),
720
- },
721
- ] as const;
722
- for (const fixture of cases) {
723
- const socket = new FakeSocket();
724
- const outcomes: unknown[] = [];
725
- socket.onSend = (frame) => {
726
- if (frame.type === "redeem") {
727
- queueMicrotask(() =>
728
- socket.message({
729
- type: "ready",
730
- protocolVersion: 2,
731
- daemonInstanceId: INSTANCE,
732
- requestId: REQUEST,
733
- panes: ["pane.editor"],
734
- effectiveViewerMode: "interactive",
735
- diagnosticCapabilities: ["clock-bounds-v1"],
736
- }),
737
- );
738
- } else if (frame.type === "clock-probe") {
739
- const ack = {
740
- type: "clock-probe-ack",
741
- requestId: REQUEST,
742
- daemonInstanceId: INSTANCE,
743
- probe: frame.probe,
744
- clientSendMicros: frame.clientSendMicros,
745
- daemonReceiveMicros: (frame.clientSendMicros as number) + 5,
746
- daemonSendMicros: (frame.clientSendMicros as number) + 6,
747
- };
748
- queueMicrotask(() => socket.message(fixture.mutate(ack)));
749
- }
750
- };
751
- await openPaneStreamRuntimeClient(
752
- options(socket, {
753
- requestInitialInputAuthority: false,
754
- diagnosticCapabilities: ["clock-bounds-v1"],
755
- diagnosticSharedNowMicros: () => 1_020,
756
- onClockCalibration: () => undefined,
757
- onClockCalibrationOutcome: (value: unknown) => outcomes.push(value),
758
- }),
759
- );
760
- expect(outcomes).toEqual([
761
- expect.objectContaining({
762
- reason: fixture.reason,
763
- attemptedProbes: 1,
764
- receivedProbes: 1,
765
- validProbes: 0,
766
- selectedProbes: 0,
767
- }),
768
- ]);
769
- }
770
-
771
- const duplicateSocket = new FakeSocket();
772
- const duplicateOutcomes: unknown[] = [];
773
- let firstAck: Record<string, unknown> | null = null;
774
- duplicateSocket.onSend = (frame) => {
775
- if (frame.type === "redeem") {
776
- queueMicrotask(() =>
777
- duplicateSocket.message({
778
- type: "ready",
779
- protocolVersion: 2,
780
- daemonInstanceId: INSTANCE,
781
- requestId: REQUEST,
782
- panes: ["pane.editor"],
783
- effectiveViewerMode: "interactive",
784
- diagnosticCapabilities: ["clock-bounds-v1"],
785
- }),
786
- );
787
- } else if (frame.type === "clock-probe" && frame.probe === 1) {
788
- firstAck = {
789
- type: "clock-probe-ack",
790
- requestId: REQUEST,
791
- daemonInstanceId: INSTANCE,
792
- probe: 1,
793
- clientSendMicros: frame.clientSendMicros,
794
- daemonReceiveMicros: (frame.clientSendMicros as number) + 5,
795
- daemonSendMicros: (frame.clientSendMicros as number) + 6,
796
- };
797
- queueMicrotask(() => duplicateSocket.message(firstAck));
798
- } else if (frame.type === "clock-probe" && frame.probe === 2) {
799
- queueMicrotask(() => duplicateSocket.message(firstAck));
800
- }
801
- };
802
- await openPaneStreamRuntimeClient(
803
- options(duplicateSocket, {
804
- requestInitialInputAuthority: false,
805
- diagnosticCapabilities: ["clock-bounds-v1"],
806
- diagnosticSharedNowMicros: (() => {
807
- let now = 1_000;
808
- return () => (now += 10);
809
- })(),
810
- onClockCalibration: () => undefined,
811
- onClockCalibrationOutcome: (value: unknown) => duplicateOutcomes.push(value),
812
- }),
813
- );
814
- expect(duplicateOutcomes).toEqual([
815
- expect.objectContaining({
816
- reason: "ack-probe-mismatch",
817
- attemptedProbes: 2,
818
- receivedProbes: 2,
819
- validProbes: 1,
820
- selectedProbes: 0,
821
- }),
822
- ]);
823
-
824
- const clockSocket = new FakeSocket();
825
- const clockOutcomes: unknown[] = [];
826
- clockSocket.onSend = (frame) => {
827
- if (frame.type === "redeem")
828
- queueMicrotask(() =>
829
- clockSocket.message({
830
- type: "ready",
831
- protocolVersion: 2,
832
- daemonInstanceId: INSTANCE,
833
- requestId: REQUEST,
834
- panes: ["pane.editor"],
835
- effectiveViewerMode: "interactive",
836
- diagnosticCapabilities: ["clock-bounds-v1"],
837
- }),
838
- );
839
- };
840
- await openPaneStreamRuntimeClient(
841
- options(clockSocket, {
842
- requestInitialInputAuthority: false,
843
- diagnosticCapabilities: ["clock-bounds-v1"],
844
- diagnosticSharedNowMicros: () => {
845
- throw new Error("clock unavailable");
846
- },
847
- onClockCalibration: () => undefined,
848
- onClockCalibrationOutcome: (value: unknown) => clockOutcomes.push(value),
849
- }),
850
- );
851
- expect(clockOutcomes).toEqual([
852
- expect.objectContaining({
853
- reason: "clock-unavailable",
854
- attemptedProbes: 1,
855
- receivedProbes: 0,
856
- validProbes: 0,
857
- selectedProbes: 0,
858
- }),
859
- ]);
860
- });
861
-
862
- it("preserves key/paste FIFO while diagnostic edges isolate a delayed next turn", async () => {
863
- const socket = new FakeSocket();
864
- acceptInteractiveHandshake(socket);
865
- let nowMicros = 1_000;
866
- const nextTurns: Array<{ run: () => void; cancelled: boolean }> = [];
867
- const stages: PaneStreamInputTransportStageEvent[] = [];
868
- const client = await openPaneStreamRuntimeClient(
869
- options(socket, {
870
- onInputTransportStage: (event: PaneStreamInputTransportStageEvent) => {
871
- expect(
872
- socket.sent.some(
873
- (frame) =>
874
- (frame as { type?: string; seq?: number }).type === "input" &&
875
- (frame as { seq?: number }).seq === event.sequence,
876
- ),
877
- ).toBe(true);
878
- stages.push(event);
879
- },
880
- diagnosticNowMicros: () => (nowMicros += 10),
881
- diagnosticNextTurn: (callback: () => void) => {
882
- const turn = { run: callback, cancelled: false };
883
- nextTurns.push(turn);
884
- return () => {
885
- turn.cancelled = true;
886
- };
887
- },
888
- }),
889
- );
890
- const keyTrace = "00000000-0000-4000-8000-000000000081";
891
- const pasteTrace = "00000000-0000-4000-8000-000000000082";
892
- const bracketedPaste = "\u001b[200~alpha\nbeta\u001b[201~";
893
- const key = client.sendTerminalInput(
894
- { workspaceName: "alpha", semanticPaneId: "pane.editor" },
895
- { kind: "key", data: "Enter" },
896
- keyTrace,
897
- );
898
- const paste = client.sendTerminalInput(
899
- { workspaceName: "alpha", semanticPaneId: "pane.editor" },
900
- { kind: "text", data: bracketedPaste },
901
- pasteTrace,
902
- );
903
-
904
- expect(socket.sent.filter((frame) => (frame as { type?: string }).type === "input")).toEqual([
905
- {
906
- type: "input",
907
- kind: "key",
908
- pane: "pane.editor",
909
- seq: 1,
910
- data: "Enter",
911
- performanceTraceId: keyTrace,
912
- },
913
- {
914
- type: "input",
915
- kind: "text",
916
- pane: "pane.editor",
917
- seq: 2,
918
- data: bracketedPaste,
919
- performanceTraceId: pasteTrace,
920
- },
921
- ]);
922
- expect(stages).toEqual([]);
923
-
924
- nowMicros += 50_000;
925
- for (const turn of nextTurns) if (!turn.cancelled) turn.run();
926
- expect(stages.map(({ traceId, operation }) => `${traceId}:${operation}`)).toEqual([
927
- `${keyTrace}:pane-stream-frame-enqueued`,
928
- `${keyTrace}:pane-stream-socket-send-return`,
929
- `${keyTrace}:pane-stream-next-event-loop-turn`,
930
- `${keyTrace}:pane-stream-observer-returned`,
931
- `${pasteTrace}:pane-stream-frame-enqueued`,
932
- `${pasteTrace}:pane-stream-socket-send-return`,
933
- `${pasteTrace}:pane-stream-next-event-loop-turn`,
934
- `${pasteTrace}:pane-stream-observer-returned`,
935
- ]);
936
- expect(
937
- stages.filter(({ operation }) => operation === "pane-stream-next-event-loop-turn"),
938
- ).toEqual([
939
- expect.objectContaining({ traceId: keyTrace, sequence: 1 }),
940
- expect.objectContaining({ traceId: pasteTrace, sequence: 2 }),
941
- ]);
942
- for (const traceId of [keyTrace, pasteTrace]) {
943
- const traceStages = stages.filter((stage) => stage.traceId === traceId);
944
- const sent = stages.find(
945
- (stage) =>
946
- stage.traceId === traceId && stage.operation === "pane-stream-socket-send-return",
947
- )!;
948
- const next = stages.find(
949
- (stage) =>
950
- stage.traceId === traceId && stage.operation === "pane-stream-next-event-loop-turn",
951
- )!;
952
- expect(next.atMicros - sent.atMicros).toBeGreaterThan(50_000);
953
- expect(classifyPaneStreamInputTransportDelay(traceStages, 10_000)).toBe(
954
- "client-event-loop-stall",
955
- );
956
- expect(traceStages.map(({ operation }) => operation)).toEqual([
957
- "pane-stream-frame-enqueued",
958
- "pane-stream-socket-send-return",
959
- "pane-stream-next-event-loop-turn",
960
- "pane-stream-observer-returned",
961
- ]);
962
- }
963
- const keyStages = stages.filter((stage) => stage.traceId === keyTrace);
964
- expect(classifyPaneStreamInputTransportDelay([...keyStages, keyStages[2]!], 10_000)).toBe(
965
- "incomplete",
966
- );
967
- expect(
968
- classifyPaneStreamInputTransportDelay([keyStages[1]!, keyStages[0]!, keyStages[2]!], 10_000),
969
- ).toBe("incomplete");
970
- expect(
971
- classifyPaneStreamInputTransportDelay(
972
- [keyStages[0]!, keyStages[1]!, { ...keyStages[2]!, traceId: pasteTrace }],
973
- 10_000,
974
- ),
975
- ).toBe("incomplete");
976
- await expect(Promise.all([key, paste])).resolves.toEqual(["ok", "ok"]);
977
- client.close();
978
- });
979
-
980
- it("records bounded socket buffer admission and a next-turn drain watermark", async () => {
981
- const socket = new FakeSocket();
982
- socket.bufferedAmount = 0;
983
- acceptInteractiveHandshake(socket);
984
- const priorOnSend = socket.onSend;
985
- socket.onSend = (frame) => {
986
- priorOnSend?.(frame);
987
- if (frame.type === "input") socket.bufferedAmount = 384;
988
- };
989
- const turns: Array<() => void> = [];
990
- const stages: PaneStreamInputTransportStageEvent[] = [];
991
- const client = await openPaneStreamRuntimeClient(
992
- options(socket, {
993
- onInputTransportStage: (event: PaneStreamInputTransportStageEvent) => stages.push(event),
994
- diagnosticNowMicros: () => 1_000,
995
- diagnosticNextTurn: (callback: () => void) => {
996
- turns.push(callback);
997
- return () => undefined;
998
- },
999
- }),
1000
- );
1001
- const pending = client.sendTerminalInput(
1002
- { workspaceName: "alpha", semanticPaneId: "pane.editor" },
1003
- { kind: "key", data: "x" },
1004
- "00000000-0000-4000-8000-000000000099",
1005
- );
1006
- socket.bufferedAmount = 0;
1007
- turns.forEach((turn) => turn());
1008
- await pending;
1009
- expect(
1010
- stages
1011
- .filter(({ operation }) => operation.includes("buffer"))
1012
- .map(({ operation, bufferedAmount, drained }) => ({
1013
- operation,
1014
- bufferedAmount,
1015
- drained,
1016
- })),
1017
- ).toEqual([
1018
- {
1019
- operation: "pane-stream-buffer-before-send",
1020
- bufferedAmount: 0,
1021
- drained: undefined,
1022
- },
1023
- {
1024
- operation: "pane-stream-buffer-after-send",
1025
- bufferedAmount: 384,
1026
- drained: undefined,
1027
- },
1028
- {
1029
- operation: "pane-stream-buffer-next-turn",
1030
- bufferedAmount: 0,
1031
- drained: undefined,
1032
- },
1033
- {
1034
- operation: "pane-stream-buffer-drain-watermark",
1035
- bufferedAmount: 0,
1036
- drained: true,
1037
- },
1038
- ]);
1039
- expect(classifyPaneStreamInputTransportDelay(stages, 10_000)).toBe("no-client-transport-stall");
1040
- });
1041
-
1042
- it("commits FIFO identity before a reentrant transport observer can send", async () => {
1043
- const socket = new FakeSocket();
1044
- acceptInteractiveHandshake(socket);
1045
- const turns: Array<() => void> = [];
1046
- let reentrant: Promise<unknown> | null = null;
1047
- let client!: Awaited<ReturnType<typeof openPaneStreamRuntimeClient>>;
1048
- client = await openPaneStreamRuntimeClient(
1049
- options(socket, {
1050
- onInputTransportStage: (event: PaneStreamInputTransportStageEvent) => {
1051
- if (event.traceId.endsWith("088") && event.operation === "pane-stream-frame-enqueued") {
1052
- reentrant = client.sendTerminalInput(
1053
- { workspaceName: "alpha", semanticPaneId: "pane.editor" },
1054
- { kind: "key", data: "Tab" },
1055
- "00000000-0000-4000-8000-000000000089",
1056
- );
1057
- }
1058
- },
1059
- diagnosticNowMicros: (() => {
1060
- let now = 0;
1061
- return () => (now += 1);
1062
- })(),
1063
- diagnosticNextTurn: (callback: () => void) => {
1064
- turns.push(callback);
1065
- return () => undefined;
1066
- },
1067
- }),
1068
- );
1069
- const first = client.sendTerminalInput(
1070
- { workspaceName: "alpha", semanticPaneId: "pane.editor" },
1071
- { kind: "key", data: "Enter" },
1072
- "00000000-0000-4000-8000-000000000088",
1073
- );
1074
- for (let index = 0; index < turns.length; index += 1) turns[index]!();
1075
- expect(
1076
- socket.sent
1077
- .filter((frame) => (frame as { type?: string }).type === "input")
1078
- .map((frame) => (frame as { seq: number; data: string }).seq),
1079
- ).toEqual([1, 2]);
1080
- await expect(first).resolves.toBe("ok");
1081
- await expect(reentrant).resolves.toBe("ok");
1082
- client.close();
1083
- });
1084
-
1085
- it("captures next-turn time before slow observer publication", async () => {
1086
- const socket = new FakeSocket();
1087
- acceptInteractiveHandshake(socket);
1088
- let nowMicros = 0;
1089
- let runTurn: (() => void) | null = null;
1090
- const stages: PaneStreamInputTransportStageEvent[] = [];
1091
- const client = await openPaneStreamRuntimeClient(
1092
- options(socket, {
1093
- diagnosticNowMicros: () => (nowMicros += 10),
1094
- onInputTransportStage: (event: PaneStreamInputTransportStageEvent) => {
1095
- stages.push(event);
1096
- nowMicros += 100_000;
1097
- },
1098
- diagnosticNextTurn: (callback: () => void) => {
1099
- runTurn = callback;
1100
- return () => undefined;
1101
- },
1102
- }),
1103
- );
1104
- const input = client.sendTerminalInput(
1105
- { workspaceName: "alpha", semanticPaneId: "pane.editor" },
1106
- { kind: "key", data: "Enter" },
1107
- "00000000-0000-4000-8000-000000000090",
1108
- );
1109
- expect(stages).toEqual([]);
1110
- runTurn!();
1111
- expect(stages.map(({ atMicros }) => atMicros)).toEqual([10, 20, 30, 300_040]);
1112
- expect(stages.at(-1)?.operation).toBe("pane-stream-observer-returned");
1113
- expect(classifyPaneStreamInputTransportDelay(stages, 1_000)).toBe("observer-callback-stall");
1114
- await expect(input).resolves.toBe("ok");
1115
- client.close();
1116
- });
1117
-
1118
- it("does no diagnostic clock or next-turn work without a transport observer", async () => {
1119
- const socket = new FakeSocket();
1120
- acceptInteractiveHandshake(socket);
1121
- const diagnosticNowMicros = mock(() => 1);
1122
- const diagnosticNextTurn = mock((_callback: () => void) => () => undefined);
1123
- const client = await openPaneStreamRuntimeClient(
1124
- options(socket, { diagnosticNowMicros, diagnosticNextTurn }),
1125
- );
1126
- await client.sendTerminalInput(
1127
- { workspaceName: "alpha", semanticPaneId: "pane.editor" },
1128
- { kind: "key", data: "Enter" },
1129
- "00000000-0000-4000-8000-000000000083",
1130
- );
1131
- expect(diagnosticNowMicros).not.toHaveBeenCalled();
1132
- expect(diagnosticNextTurn).not.toHaveBeenCalled();
1133
- client.close();
1134
- });
1135
-
1136
- it("keeps input live when diagnostic clocks, observers, or schedulers throw", async () => {
1137
- for (const failure of ["clock", "observer", "scheduler"] as const) {
1138
- const socket = new FakeSocket();
1139
- acceptInteractiveHandshake(socket);
1140
- const client = await openPaneStreamRuntimeClient(
1141
- options(socket, {
1142
- onInputTransportStage: () => {
1143
- if (failure === "observer") throw new Error("observer failed");
1144
- },
1145
- diagnosticNowMicros: () => {
1146
- if (failure === "clock") throw new Error("clock failed");
1147
- return 1;
1148
- },
1149
- diagnosticNextTurn: (callback: () => void) => {
1150
- if (failure === "scheduler") throw new Error("scheduler failed");
1151
- callback();
1152
- return () => undefined;
1153
- },
1154
- }),
1155
- );
1156
- await expect(
1157
- client.sendTerminalInput(
1158
- { workspaceName: "alpha", semanticPaneId: "pane.editor" },
1159
- { kind: "key", data: "Enter" },
1160
- `00000000-0000-4000-8000-00000000008${failure === "clock" ? 4 : failure === "observer" ? 5 : 6}`,
1161
- ),
1162
- ).resolves.toBe("ok");
1163
- expect(
1164
- socket.sent.filter((frame) => (frame as { type?: string }).type === "input"),
1165
- ).toHaveLength(1);
1166
- expect(socket.closed).toBeNull();
1167
- client.close();
1168
- }
1169
- });
1170
-
1171
- it("cancels and releases a pending diagnostic next turn on close", async () => {
1172
- const socket = new FakeSocket();
1173
- acceptInteractiveHandshake(socket);
1174
- const before = runtimeResourceSnapshot()["runtime-timer"].active;
1175
- const stages: PaneStreamInputTransportStageEvent[] = [];
1176
- const client = await openPaneStreamRuntimeClient(
1177
- options(socket, { onInputTransportStage: (event) => stages.push(event) }),
1178
- );
1179
- const input = client.sendTerminalInput(
1180
- { workspaceName: "alpha", semanticPaneId: "pane.editor" },
1181
- { kind: "key", data: "Enter" },
1182
- "00000000-0000-4000-8000-000000000087",
1183
- );
1184
- client.close();
1185
- await expect(input).rejects.toThrow("client closed");
1186
- await Bun.sleep(0);
1187
- expect(stages).toEqual([]);
1188
- expect(runtimeResourceSnapshot()["runtime-timer"].active).toBe(before);
1189
- });
1190
-
1191
- it("propagates AbortSignal through capability issuance", async () => {
1192
- const socket = new FakeSocket();
1193
- const controller = new AbortController();
1194
- let observedSignal: AbortSignal | null = null;
1195
- const fetch = mock(
1196
- async (_url: string | URL | Request, init?: RequestInit): Promise<Response> => {
1197
- observedSignal = init?.signal as AbortSignal;
1198
- return await new Promise<Response>((_resolve, reject) => {
1199
- observedSignal?.addEventListener(
1200
- "abort",
1201
- () => reject(observedSignal?.reason ?? new DOMException("aborted", "AbortError")),
1202
- { once: true },
1203
- );
1204
- });
1205
- },
1206
- ) as typeof globalThis.fetch;
1207
- const opening = openPaneStreamRuntimeClient(
1208
- options(socket, { signal: controller.signal, fetch }),
1209
- );
1210
- await Bun.sleep(0);
1211
- expect(observedSignal).toBe(controller.signal);
1212
- controller.abort(new Error("retired issue"));
1213
- await expect(opening).rejects.toThrow("retired issue");
1214
- expect(socket.closeCalls).toBe(0);
1215
- });
1216
-
1217
- it("rejects direct readiness when initial input authority is denied", async () => {
1218
- const baseline = runtimeResourceSnapshot();
1219
- const socket = new FakeSocket();
1220
- socket.onSend = (frame) => {
1221
- if (frame.type === "redeem") {
1222
- queueMicrotask(() =>
1223
- socket.message({
1224
- type: "ready",
1225
- protocolVersion: 2,
1226
- daemonInstanceId: INSTANCE,
1227
- requestId: REQUEST,
1228
- panes: ["pane.editor"],
1229
- effectiveViewerMode: "interactive",
1230
- }),
1231
- );
1232
- } else if (frame.type === "authority-request") {
1233
- queueMicrotask(() =>
1234
- socket.message({
1235
- type: "authority-receipt",
1236
- requestId: frame.requestId,
1237
- authority: "input",
1238
- status: "rejected",
1239
- lease: null,
1240
- snapshot: {
1241
- generation: INSTANCE,
1242
- session: "alpha",
1243
- revision: 1,
1244
- nativeGeometryYieldUntilMs: 0,
1245
- owners: { input: "another-client", focus: null, geometry: null },
1246
- clients: [],
1247
- },
1248
- }),
1249
- );
1250
- }
1251
- };
1252
-
1253
- await expect(openPaneStreamRuntimeClient(options(socket))).rejects.toThrow(
1254
- "input authority was denied",
1255
- );
1256
- expect(socket.closeCalls).toBe(1);
1257
- expect([...socket.listeners.values()].every((listeners) => listeners.size === 0)).toBe(true);
1258
- const settled = runtimeResourceSnapshot();
1259
- expect(settled["pane-stream-socket"].active).toBe(baseline["pane-stream-socket"].active);
1260
- expect(settled["socket-listener"].active).toBe(baseline["socket-listener"].active);
1261
- expect(settled["runtime-timer"].active).toBe(baseline["runtime-timer"].active);
1262
- });
1263
-
1264
- it("exposes verified display readiness before lazily acquiring input authority", async () => {
1265
- const baseline = runtimeResourceSnapshot();
1266
- const socket = new FakeSocket();
1267
- let authorityRequest: Record<string, unknown> | null = null;
1268
- socket.onSend = (frame) => {
1269
- if (frame.type === "redeem") {
1270
- queueMicrotask(() =>
1271
- socket.message({
1272
- type: "ready",
1273
- protocolVersion: 2,
1274
- daemonInstanceId: INSTANCE,
1275
- requestId: REQUEST,
1276
- connectionClientId: "tui:one",
1277
- panes: ["pane.editor"],
1278
- effectiveViewerMode: "interactive",
1279
- }),
1280
- );
1281
- }
1282
- if (frame.type === "authority-request") authorityRequest = frame;
1283
- };
1284
- const opening = openPaneStreamRuntimeClient(
1285
- options(socket, { requestInitialInputAuthority: false }),
1286
- );
1287
- await Bun.sleep(0);
1288
- const client = await opening;
1289
- expect(client.connectionClientId).toBe("tui:one");
1290
- expect(authorityRequest).toBeNull();
1291
- expect(
1292
- await client.sendTerminalInput(
1293
- { workspaceName: "alpha", semanticPaneId: "pane.editor" },
1294
- { kind: "text", data: "blocked" },
1295
- ),
1296
- ).toBe("authority-lost");
1297
- const authority = client.requestAuthority("input");
1298
- await Bun.sleep(0);
1299
- expect(authorityRequest).toMatchObject({ type: "authority-request", authority: "input" });
1300
- socket.message({
1301
- type: "authority-receipt",
1302
- requestId: authorityRequest!.requestId,
1303
- authority: "input",
1304
- status: "granted",
1305
- lease: {
1306
- generation: INSTANCE,
1307
- session: "alpha",
1308
- clientId: "tui:one",
1309
- authority: "input",
1310
- token: "55555555-5555-4555-8555-555555555555",
1311
- revision: 2,
1312
- },
1313
- snapshot: {
1314
- generation: INSTANCE,
1315
- session: "alpha",
1316
- revision: 2,
1317
- nativeGeometryYieldUntilMs: 0,
1318
- owners: { input: "tui:one", focus: null, geometry: null },
1319
- clients: [
1320
- {
1321
- clientId: "tui:one",
1322
- surface: "opentui",
1323
- state: "foreground",
1324
- connectedRevision: 1,
1325
- activityRevision: 2,
1326
- },
1327
- ],
1328
- },
1329
- });
1330
- await authority;
1331
- expect(client.authoritySnapshot?.owners.input).toBe("tui:one");
1332
- client.sendText("pane.editor", "immediate");
1333
- expect(socket.sent.at(-1)).toMatchObject({ type: "input", data: "immediate" });
1334
- client.close();
1335
- client.close();
1336
- expect(socket.closeCalls).toBe(1);
1337
- expect([...socket.listeners.values()].every((listeners) => listeners.size === 0)).toBe(true);
1338
- const settled = runtimeResourceSnapshot();
1339
- expect(settled["pane-stream-socket"].active).toBe(baseline["pane-stream-socket"].active);
1340
- expect(settled["socket-listener"].active).toBe(baseline["socket-listener"].active);
1341
- expect(settled["runtime-timer"].active).toBe(baseline["runtime-timer"].active);
1342
- });
1343
-
1344
- it("returns the exact authority snapshot carried by a release receipt", async () => {
1345
- const socket = new FakeSocket();
1346
- socket.onSend = (frame) => {
1347
- if (frame.type === "redeem") {
1348
- queueMicrotask(() =>
1349
- socket.message({
1350
- type: "ready",
1351
- protocolVersion: 2,
1352
- daemonInstanceId: INSTANCE,
1353
- requestId: REQUEST,
1354
- panes: ["pane.editor"],
1355
- effectiveViewerMode: "interactive",
1356
- }),
1357
- );
1358
- } else if (frame.type === "authority-release") {
1359
- queueMicrotask(() =>
1360
- socket.message({
1361
- type: "authority-receipt",
1362
- requestId: frame.requestId,
1363
- authority: frame.authority,
1364
- status: "released",
1365
- lease: null,
1366
- snapshot: {
1367
- generation: INSTANCE,
1368
- session: "alpha",
1369
- revision: 17,
1370
- nativeGeometryYieldUntilMs: 0,
1371
- owners: { input: null, focus: "web:one", geometry: null },
1372
- clients: [],
1373
- },
1374
- }),
1375
- );
1376
- }
1377
- };
1378
- const client = await openPaneStreamRuntimeClient(
1379
- options(socket, { requestInitialInputAuthority: false }),
1380
- );
1381
-
1382
- await expect(client.releaseAuthority("input")).resolves.toMatchObject({
1383
- generation: INSTANCE,
1384
- revision: 17,
1385
- owners: { input: null, focus: "web:one", geometry: null },
1386
- });
1387
- client.close();
1388
- });
1389
-
1390
- it("serializes an immediate reclaim behind a delayed release acknowledgement", async () => {
1391
- const socket = new FakeSocket();
1392
- socket.onSend = (frame) => {
1393
- if (frame.type !== "redeem") return;
1394
- queueMicrotask(() =>
1395
- socket.message({
1396
- type: "ready",
1397
- protocolVersion: 2,
1398
- daemonInstanceId: INSTANCE,
1399
- requestId: REQUEST,
1400
- panes: ["pane.editor"],
1401
- effectiveViewerMode: "interactive",
1402
- }),
1403
- );
1404
- };
1405
- const client = await openPaneStreamRuntimeClient(
1406
- options(socket, { requestInitialInputAuthority: false }),
1407
- );
1408
- const initial = client.requestAuthority("focus");
1409
- await Bun.sleep(0);
1410
- const initialRequest = socket.sent.find((frame) => frame.type === "authority-request")!;
1411
- socket.message({
1412
- type: "authority-receipt",
1413
- requestId: initialRequest.requestId,
1414
- authority: "focus",
1415
- status: "granted",
1416
- lease: {
1417
- generation: INSTANCE,
1418
- session: "alpha",
1419
- clientId: "tui:one",
1420
- authority: "focus",
1421
- token: "55555555-5555-4555-8555-555555555555",
1422
- revision: 2,
1423
- },
1424
- snapshot: {
1425
- generation: INSTANCE,
1426
- session: "alpha",
1427
- revision: 2,
1428
- nativeGeometryYieldUntilMs: 0,
1429
- owners: { input: null, focus: "tui:one", geometry: null },
1430
- clients: [],
1431
- },
1432
- });
1433
- await initial;
1434
-
1435
- const release = client.releaseAuthority("focus");
1436
- expect(client.ownsConnectionAuthority("focus")).toBe(false);
1437
- const reclaimOne = client.requestAuthority("focus");
1438
- const reclaimTwo = client.requestAuthority("focus");
1439
- await Bun.sleep(0);
1440
- const releaseFrame = socket.sent.find((frame) => frame.type === "authority-release")!;
1441
- expect(socket.sent.filter((frame) => frame.type === "authority-request")).toHaveLength(1);
1442
-
1443
- socket.message({
1444
- type: "authority-receipt",
1445
- requestId: releaseFrame.requestId,
1446
- authority: "focus",
1447
- status: "released",
1448
- lease: null,
1449
- snapshot: {
1450
- generation: INSTANCE,
1451
- session: "alpha",
1452
- revision: 3,
1453
- nativeGeometryYieldUntilMs: 0,
1454
- owners: { input: null, focus: null, geometry: null },
1455
- clients: [],
1456
- },
1457
- });
1458
- await release;
1459
- await Bun.sleep(0);
1460
- const requestFrames = socket.sent.filter((frame) => frame.type === "authority-request");
1461
- expect(requestFrames).toHaveLength(2);
1462
- expect(socket.sent.indexOf(releaseFrame)).toBeLessThan(socket.sent.indexOf(requestFrames[1]!));
1463
- const reclaimRequest = requestFrames[1]!;
1464
- socket.message({
1465
- type: "authority-receipt",
1466
- requestId: reclaimRequest.requestId,
1467
- authority: "focus",
1468
- status: "granted",
1469
- lease: {
1470
- generation: INSTANCE,
1471
- session: "alpha",
1472
- clientId: "tui:one",
1473
- authority: "focus",
1474
- token: "66666666-6666-4666-8666-666666666666",
1475
- revision: 4,
1476
- },
1477
- snapshot: {
1478
- generation: INSTANCE,
1479
- session: "alpha",
1480
- revision: 4,
1481
- nativeGeometryYieldUntilMs: 0,
1482
- owners: { input: null, focus: "tui:one", geometry: null },
1483
- clients: [],
1484
- },
1485
- });
1486
- const [one, two] = await Promise.all([reclaimOne, reclaimTwo]);
1487
- expect(one).toEqual(two);
1488
- expect(client.ownsConnectionAuthority("focus")).toBe(true);
1489
- expect(client.connectionAuthorityClientId("focus")).toBe("tui:one");
1490
- expect(socket.sent.filter((frame) => frame.type === "authority-request")).toHaveLength(2);
1491
- client.close();
1492
- });
1493
-
1494
- it("emits a queued release after the preceding authority request times out", async () => {
1495
- const socket = new FakeSocket();
1496
- socket.onSend = (frame) => {
1497
- if (frame.type !== "redeem") return;
1498
- queueMicrotask(() =>
1499
- socket.message({
1500
- type: "ready",
1501
- protocolVersion: 2,
1502
- daemonInstanceId: INSTANCE,
1503
- requestId: REQUEST,
1504
- panes: ["pane.editor"],
1505
- effectiveViewerMode: "interactive",
1506
- }),
1507
- );
1508
- };
1509
- const client = await openPaneStreamRuntimeClient(
1510
- options(socket, { requestInitialInputAuthority: false }),
1511
- );
1512
- const request = client.requestAuthority("input");
1513
- void request.catch(() => undefined);
1514
- const release = client.releaseAuthority("input");
1515
- await Bun.sleep(2_050);
1516
- await expect(request).rejects.toThrow("authority timed out");
1517
- const requestFrame = socket.sent.find((frame) => frame.type === "authority-request")!;
1518
- const releaseFrame = socket.sent.find((frame) => frame.type === "authority-release")!;
1519
- expect(socket.sent.indexOf(requestFrame)).toBeLessThan(socket.sent.indexOf(releaseFrame));
1520
-
1521
- socket.message({
1522
- type: "authority-receipt",
1523
- requestId: requestFrame.requestId,
1524
- authority: "input",
1525
- status: "granted",
1526
- lease: {
1527
- generation: INSTANCE,
1528
- session: "alpha",
1529
- clientId: "tui:one",
1530
- authority: "input",
1531
- token: "55555555-5555-4555-8555-555555555555",
1532
- revision: 2,
1533
- },
1534
- snapshot: {
1535
- generation: INSTANCE,
1536
- session: "alpha",
1537
- revision: 2,
1538
- nativeGeometryYieldUntilMs: 0,
1539
- owners: { input: "tui:one", focus: null, geometry: null },
1540
- clients: [],
1541
- },
1542
- });
1543
- expect(client.ownsConnectionAuthority("input")).toBe(false);
1544
- expect(client.connectionAuthorityClientId("input")).toBeNull();
1545
- socket.message({
1546
- type: "authority-receipt",
1547
- requestId: releaseFrame.requestId,
1548
- authority: "input",
1549
- status: "released",
1550
- lease: null,
1551
- snapshot: {
1552
- generation: INSTANCE,
1553
- session: "alpha",
1554
- revision: 3,
1555
- nativeGeometryYieldUntilMs: 0,
1556
- owners: { input: null, focus: null, geometry: null },
1557
- clients: [],
1558
- },
1559
- });
1560
- await expect(release).resolves.toMatchObject({ owners: { input: null } });
1561
- client.close();
1562
- });
1563
-
1564
- it("coalesces a fresh reclaim after release timeout and ignores one exact late ACK", async () => {
1565
- const socket = new FakeSocket();
1566
- socket.onSend = (frame) => {
1567
- if (frame.type !== "redeem") return;
1568
- queueMicrotask(() =>
1569
- socket.message({
1570
- type: "ready",
1571
- protocolVersion: 2,
1572
- daemonInstanceId: INSTANCE,
1573
- requestId: REQUEST,
1574
- panes: ["pane.editor"],
1575
- effectiveViewerMode: "interactive",
1576
- }),
1577
- );
1578
- };
1579
- const client = await openPaneStreamRuntimeClient(
1580
- options(socket, { requestInitialInputAuthority: false }),
1581
- );
1582
- const release = client.releaseAuthority("geometry");
1583
- void release.catch(() => undefined);
1584
- const reclaimOne = client.requestAuthority("geometry");
1585
- const reclaimTwo = client.requestAuthority("geometry");
1586
- await Bun.sleep(2_050);
1587
- await expect(release).rejects.toThrow("release timed out");
1588
- const releaseFrame = socket.sent.find((frame) => frame.type === "authority-release")!;
1589
- const requestFrames = socket.sent.filter((frame) => frame.type === "authority-request");
1590
- expect(requestFrames).toHaveLength(1);
1591
- const reclaimFrame = requestFrames[0]!;
1592
-
1593
- socket.message({
1594
- type: "authority-receipt",
1595
- requestId: releaseFrame.requestId,
1596
- authority: "geometry",
1597
- status: "released",
1598
- lease: null,
1599
- snapshot: {
1600
- generation: INSTANCE,
1601
- session: "alpha",
1602
- revision: 3,
1603
- nativeGeometryYieldUntilMs: 0,
1604
- owners: { input: null, focus: null, geometry: null },
1605
- clients: [],
1606
- },
1607
- });
1608
- expect(client.ownsConnectionAuthority("geometry")).toBe(false);
1609
- socket.message({
1610
- type: "authority-receipt",
1611
- requestId: reclaimFrame.requestId,
1612
- authority: "geometry",
1613
- status: "granted",
1614
- lease: {
1615
- generation: INSTANCE,
1616
- session: "alpha",
1617
- clientId: "tui:one",
1618
- authority: "geometry",
1619
- token: "66666666-6666-4666-8666-666666666666",
1620
- revision: 4,
1621
- },
1622
- snapshot: {
1623
- generation: INSTANCE,
1624
- session: "alpha",
1625
- revision: 4,
1626
- nativeGeometryYieldUntilMs: 0,
1627
- owners: { input: null, focus: null, geometry: "tui:one" },
1628
- clients: [],
1629
- },
1630
- });
1631
- await Promise.all([reclaimOne, reclaimTwo]);
1632
- expect(client.ownsConnectionAuthority("geometry")).toBe(true);
1633
- expect(socket.sent.filter((frame) => frame.type === "authority-request")).toHaveLength(1);
1634
-
1635
- socket.message({
1636
- type: "authority-receipt",
1637
- requestId: releaseFrame.requestId,
1638
- authority: "geometry",
1639
- status: "released",
1640
- lease: null,
1641
- snapshot: {
1642
- generation: INSTANCE,
1643
- session: "alpha",
1644
- revision: 3,
1645
- nativeGeometryYieldUntilMs: 0,
1646
- owners: { input: null, focus: null, geometry: null },
1647
- clients: [],
1648
- },
1649
- });
1650
- expect(socket.closed).toEqual({ code: 1008, reason: "protocol-error" });
1651
- });
1652
-
1653
- it("rejects a serialized authority chain on close without emitting its queued operations", async () => {
1654
- const socket = new FakeSocket();
1655
- socket.onSend = (frame) => {
1656
- if (frame.type !== "redeem") return;
1657
- queueMicrotask(() =>
1658
- socket.message({
1659
- type: "ready",
1660
- protocolVersion: 2,
1661
- daemonInstanceId: INSTANCE,
1662
- requestId: REQUEST,
1663
- panes: ["pane.editor"],
1664
- effectiveViewerMode: "interactive",
1665
- }),
1666
- );
1667
- };
1668
- const client = await openPaneStreamRuntimeClient(
1669
- options(socket, { requestInitialInputAuthority: false }),
1670
- );
1671
- const initial = client.requestAuthority("input");
1672
- const release = client.releaseAuthority("input");
1673
- const reclaim = client.requestAuthority("input");
1674
- client.close();
1675
-
1676
- expect(
1677
- (await Promise.allSettled([initial, release, reclaim])).every(
1678
- (result) => result.status === "rejected",
1679
- ),
1680
- ).toBe(true);
1681
- expect(socket.sent.filter((frame) => frame.type === "authority-request")).toHaveLength(1);
1682
- expect(socket.sent.filter((frame) => frame.type === "authority-release")).toHaveLength(0);
1683
- });
1684
-
1685
- it("rejects every pending authority action status or lease inconsistency", async () => {
1686
- const cases = [
1687
- { action: "request", status: "released", lease: null },
1688
- { action: "request", status: "granted", lease: null },
1689
- { action: "request", status: "rejected", lease: "present" },
1690
- { action: "release", status: "granted", lease: "present" },
1691
- { action: "release", status: "rejected", lease: null },
1692
- { action: "release", status: "released", lease: "present" },
1693
- ] as const;
1694
- for (const testCase of cases) {
1695
- const socket = new FakeSocket();
1696
- socket.onSend = (frame) => {
1697
- if (frame.type !== "redeem") return;
1698
- queueMicrotask(() =>
1699
- socket.message({
1700
- type: "ready",
1701
- protocolVersion: 2,
1702
- daemonInstanceId: INSTANCE,
1703
- requestId: REQUEST,
1704
- panes: ["pane.editor"],
1705
- effectiveViewerMode: "interactive",
1706
- }),
1707
- );
1708
- };
1709
- const client = await openPaneStreamRuntimeClient(
1710
- options(socket, { requestInitialInputAuthority: false }),
1711
- );
1712
- const operation =
1713
- testCase.action === "request"
1714
- ? client.requestAuthority("input")
1715
- : client.releaseAuthority("input");
1716
- await Bun.sleep(0);
1717
- const sent = socket.sent.find((frame) =>
1718
- testCase.action === "request"
1719
- ? frame.type === "authority-request"
1720
- : frame.type === "authority-release",
1721
- )!;
1722
- const lease =
1723
- testCase.lease === "present"
1724
- ? {
1725
- generation: INSTANCE,
1726
- session: "alpha",
1727
- clientId: "tui:one",
1728
- authority: "input" as const,
1729
- token: "55555555-5555-4555-8555-555555555555",
1730
- revision: 2,
1731
- }
1732
- : null;
1733
- socket.message({
1734
- type: "authority-receipt",
1735
- requestId: sent.requestId,
1736
- authority: "input",
1737
- status: testCase.status,
1738
- lease,
1739
- snapshot: {
1740
- generation: INSTANCE,
1741
- session: "alpha",
1742
- revision: 2,
1743
- nativeGeometryYieldUntilMs: 0,
1744
- owners: {
1745
- input: testCase.status === "granted" ? "tui:one" : null,
1746
- focus: null,
1747
- geometry: null,
1748
- },
1749
- clients: [],
1750
- },
1751
- });
1752
- await expect(operation).rejects.toThrow("did not match a request");
1753
- expect(socket.closed).toEqual({ code: 1008, reason: "protocol-error" });
1754
- }
1755
- });
1756
-
1757
- it("rejects an action-mismatched receipt for a retired authority request", async () => {
1758
- const socket = new FakeSocket();
1759
- socket.onSend = (frame) => {
1760
- if (frame.type !== "redeem") return;
1761
- queueMicrotask(() =>
1762
- socket.message({
1763
- type: "ready",
1764
- protocolVersion: 2,
1765
- daemonInstanceId: INSTANCE,
1766
- requestId: REQUEST,
1767
- panes: ["pane.editor"],
1768
- effectiveViewerMode: "interactive",
1769
- }),
1770
- );
1771
- };
1772
- const client = await openPaneStreamRuntimeClient(
1773
- options(socket, { requestInitialInputAuthority: false }),
1774
- );
1775
- const request = client.requestAuthority("focus");
1776
- void request.catch(() => undefined);
1777
- await Bun.sleep(2_050);
1778
- await expect(request).rejects.toThrow("authority timed out");
1779
- const requestFrame = socket.sent.find((frame) => frame.type === "authority-request")!;
1780
- socket.message({
1781
- type: "authority-receipt",
1782
- requestId: requestFrame.requestId,
1783
- authority: "focus",
1784
- status: "released",
1785
- lease: null,
1786
- snapshot: {
1787
- generation: INSTANCE,
1788
- session: "alpha",
1789
- revision: 3,
1790
- nativeGeometryYieldUntilMs: 0,
1791
- owners: { input: null, focus: null, geometry: null },
1792
- clients: [],
1793
- },
1794
- });
1795
- expect(socket.closed).toEqual({ code: 1008, reason: "protocol-error" });
1796
- });
1797
-
1798
- it("resolves only after verified ready and decodes delivery chunks", async () => {
1799
- const socket = new FakeSocket();
1800
- socket.onSend = (frame) => {
1801
- if (frame.type === "redeem") {
1802
- expect(JSON.stringify(frame)).not.toContain("owner-secret");
1803
- queueMicrotask(() =>
1804
- socket.message({
1805
- type: "ready",
1806
- protocolVersion: 2,
1807
- daemonInstanceId: INSTANCE,
1808
- requestId: REQUEST,
1809
- panes: ["pane.editor"],
1810
- effectiveViewerMode: "interactive",
1811
- }),
1812
- );
1813
- } else if (frame.type === "viewport") {
1814
- queueMicrotask(() =>
1815
- socket.message({
1816
- type: "viewport-ack",
1817
- seq: frame.seq,
1818
- cols: frame.cols,
1819
- rows: frame.rows,
1820
- outcome: "ok",
1821
- authorityLease: frame.authorityLease,
1822
- }),
1823
- );
1824
- } else if (frame.type === "authority-request") {
1825
- const authorityRevision = frame.authority === "geometry" ? 2 : 1;
1826
- queueMicrotask(() =>
1827
- socket.message({
1828
- type: "authority-receipt",
1829
- requestId: frame.requestId,
1830
- authority: frame.authority,
1831
- status: "granted",
1832
- lease: {
1833
- generation: INSTANCE,
1834
- session: "alpha",
1835
- clientId: "tui:one",
1836
- authority: frame.authority,
1837
- token: "55555555-5555-4555-8555-555555555555",
1838
- revision: authorityRevision,
1839
- },
1840
- snapshot: {
1841
- generation: INSTANCE,
1842
- session: "alpha",
1843
- revision: authorityRevision,
1844
- nativeGeometryYieldUntilMs: 0,
1845
- owners: {
1846
- input: "tui:one",
1847
- focus: null,
1848
- geometry: frame.authority === "geometry" ? "tui:one" : null,
1849
- },
1850
- clients: [
1851
- {
1852
- clientId: "tui:one",
1853
- surface: "opentui",
1854
- state: "foreground",
1855
- connectedRevision: 1,
1856
- activityRevision: 1,
1857
- },
1858
- ],
1859
- },
1860
- }),
1861
- );
1862
- } else if (frame.type === "semantic-intent") {
1863
- queueMicrotask(() =>
1864
- socket.message({
1865
- type: "semantic-intent-ack",
1866
- operationId: frame.operationId,
1867
- outcome: { status: "applied", result: null },
1868
- }),
1869
- );
1870
- } else if (frame.type === "input") {
1871
- queueMicrotask(() =>
1872
- socket.message({ type: "input-ack", pane: frame.pane, seq: frame.seq }),
1873
- );
1874
- }
1875
- };
1876
- let consumedOwnedChunk: Uint8Array | null = null;
1877
- let consumedOwnedChunkContents: number[] | null = null;
1878
- const onTerminalDelivery = mock((_: string, message: TerminalDeliveryServerMessage) => {
1879
- if (message.type !== "terminal.delivery.chunk") return;
1880
- consumedOwnedChunk = message.bytes;
1881
- consumedOwnedChunkContents = [...message.bytes];
1882
- return { consumedOwnedChunk: true as const };
1883
- });
1884
- const onTerminalFrameArrival = mock();
1885
- const client = await openPaneStreamRuntimeClient(
1886
- options(socket, { onTerminalDelivery, onTerminalFrameArrival }),
1887
- );
1888
- socket.message({
1889
- type: "terminal-delivery-envelope",
1890
- pane: "pane.editor",
1891
- envelope: {
1892
- type: "terminal.delivery",
1893
- workspaceName: "alpha",
1894
- semanticPaneId: "pane.editor",
1895
- generation: INSTANCE,
1896
- incarnation: `${INSTANCE}:0`,
1897
- deliveryNonce: "00000000-0000-4000-8000-000000000097",
1898
- transactionId: TRANSACTION,
1899
- performanceTraceId: "00000000-0000-4000-8000-000000000099",
1900
- protocolVersion: 1,
1901
- encoding: "semantic-v1",
1902
- frame: "seed",
1903
- baseRevision: null,
1904
- canonicalRevision: 0,
1905
- canonicalStateHash: "0000000000000000",
1906
- representationHash: "0000000000000000",
1907
- representationBytes: 0,
1908
- chunkCount: 1,
1909
- canonicalEquivalent: true,
1910
- history: "complete",
1911
- richPlacements: false,
1912
- },
1913
- });
1914
- expect(onTerminalFrameArrival).toHaveBeenCalledTimes(1);
1915
- expect(onTerminalFrameArrival.mock.calls[0]?.[0]).toMatchObject({
1916
- pane: "pane.editor",
1917
- traceId: "00000000-0000-4000-8000-000000000099",
1918
- });
1919
- expect(onTerminalFrameArrival.mock.calls[0]?.[0].atMicros).toBeGreaterThan(0);
1920
- expect(await client.requestAuthority("input")).not.toBeNull();
1921
- const textSent = client.sendTerminalInput(
1922
- { workspaceName: "alpha", semanticPaneId: "pane.editor" },
1923
- { kind: "text", data: "echo hi" },
1924
- "00000000-0000-4000-8000-000000000099",
1925
- );
1926
- const keySent = client.sendTerminalInput(
1927
- { workspaceName: "alpha", semanticPaneId: "pane.editor" },
1928
- { kind: "key", data: "Enter" },
1929
- );
1930
- const fitted = client.fitViewport(132, 44);
1931
- const submitted = client.submitIntent(OPERATION, {
1932
- verb: "workspace.pane.select",
1933
- workspaceName: "alpha",
1934
- semanticPaneId: "pane.editor",
1935
- });
1936
- await fitted;
1937
- expect(await Promise.all([textSent, keySent])).toEqual(["ok", "ok"]);
1938
- socket.message({ type: "input-ack", pane: "pane.editor", seq: 1 });
1939
- expect(socket.closed).toBeNull();
1940
- await expect(
1941
- client.sendTerminalInput(
1942
- { workspaceName: "another", semanticPaneId: "pane.editor" },
1943
- { kind: "key", data: "Up" },
1944
- ),
1945
- ).rejects.toThrow("another workspace");
1946
- expect(
1947
- socket.sent
1948
- .filter((frame) =>
1949
- ["input", "viewport", "semantic-intent"].includes((frame as { type: string }).type),
1950
- )
1951
- .slice(-4),
1952
- ).toEqual([
1953
- {
1954
- type: "input",
1955
- kind: "text",
1956
- pane: "pane.editor",
1957
- seq: 1,
1958
- data: "echo hi",
1959
- performanceTraceId: "00000000-0000-4000-8000-000000000099",
1960
- },
1961
- { type: "input", kind: "key", pane: "pane.editor", seq: 2, data: "Enter" },
1962
- {
1963
- type: "semantic-intent",
1964
- operationId: OPERATION,
1965
- intent: {
1966
- verb: "workspace.pane.select",
1967
- workspaceName: "alpha",
1968
- semanticPaneId: "pane.editor",
1969
- },
1970
- },
1971
- {
1972
- type: "viewport",
1973
- seq: 1,
1974
- cols: 132,
1975
- rows: 44,
1976
- authorityLease: {
1977
- generation: INSTANCE,
1978
- session: "alpha",
1979
- clientId: "tui:one",
1980
- authority: "geometry",
1981
- token: "55555555-5555-4555-8555-555555555555",
1982
- revision: 2,
1983
- },
1984
- },
1985
- ]);
1986
- socket.message({
1987
- type: "terminal-delivery-chunk",
1988
- pane: "pane.editor",
1989
- transactionId: TRANSACTION,
1990
- index: 0,
1991
- data: "aGk=",
1992
- });
1993
- expect(onTerminalDelivery).toHaveBeenCalledWith(
1994
- "pane.editor",
1995
- expect.objectContaining({ transactionId: TRANSACTION, index: 0 }),
1996
- );
1997
- expect(consumedOwnedChunkContents).toEqual([104, 105]);
1998
- expect(consumedOwnedChunk?.byteLength).toBe(0);
1999
- socket.message({
2000
- type: "authority-snapshot",
2001
- snapshot: {
2002
- generation: INSTANCE,
2003
- session: "alpha",
2004
- revision: 3,
2005
- nativeGeometryYieldUntilMs: 0,
2006
- owners: { input: "another-client", focus: null, geometry: null },
2007
- clients: [],
2008
- },
2009
- });
2010
- const inputFrameCount = socket.sent.filter(
2011
- (frame) => (frame as { type?: string }).type === "input",
2012
- ).length;
2013
- expect(
2014
- await client.sendTerminalInput(
2015
- { workspaceName: "alpha", semanticPaneId: "pane.editor" },
2016
- { kind: "key", data: "C-c" },
2017
- ),
2018
- ).toBe("authority-lost");
2019
- expect(
2020
- socket.sent.filter((frame) => (frame as { type?: string }).type === "input"),
2021
- ).toHaveLength(inputFrameCount);
2022
- expect(await submitted).toBeNull();
2023
- client.close();
2024
- });
2025
-
2026
- it("requests a connection-local geometry grant when a fresh socket inherits global ownership", async () => {
2027
- const socket = new FakeSocket();
2028
- let grantRevision = 3;
2029
- socket.onSend = (frame) => {
2030
- if (frame.type === "redeem") {
2031
- queueMicrotask(() =>
2032
- socket.message({
2033
- type: "ready",
2034
- protocolVersion: 2,
2035
- daemonInstanceId: INSTANCE,
2036
- requestId: REQUEST,
2037
- panes: ["pane.editor"],
2038
- effectiveViewerMode: "interactive",
2039
- authority: {
2040
- generation: INSTANCE,
2041
- session: "alpha",
2042
- revision: 2,
2043
- nativeGeometryYieldUntilMs: 0,
2044
- owners: { input: null, focus: null, geometry: "tui:one" },
2045
- clients: [],
2046
- },
2047
- }),
2048
- );
2049
- } else if (frame.type === "authority-request") {
2050
- const revision = grantRevision;
2051
- grantRevision += 2;
2052
- queueMicrotask(() =>
2053
- socket.message({
2054
- type: "authority-receipt",
2055
- requestId: frame.requestId,
2056
- authority: "geometry",
2057
- status: "granted",
2058
- lease: {
2059
- generation: INSTANCE,
2060
- session: "alpha",
2061
- clientId: "tui:one",
2062
- authority: "geometry",
2063
- token: "55555555-5555-4555-8555-555555555555",
2064
- revision,
2065
- },
2066
- snapshot: {
2067
- generation: INSTANCE,
2068
- session: "alpha",
2069
- revision,
2070
- nativeGeometryYieldUntilMs: 0,
2071
- owners: { input: null, focus: null, geometry: "tui:one" },
2072
- clients: [],
2073
- },
2074
- }),
2075
- );
2076
- } else if (frame.type === "viewport") {
2077
- queueMicrotask(() =>
2078
- socket.message({
2079
- type: "viewport-ack",
2080
- seq: frame.seq,
2081
- cols: frame.cols,
2082
- rows: frame.rows,
2083
- outcome: "ok",
2084
- authorityLease: frame.authorityLease,
2085
- }),
2086
- );
2087
- }
2088
- };
2089
- const client = await openPaneStreamRuntimeClient(
2090
- options(socket, { requestInitialInputAuthority: false }),
2091
- );
2092
- expect(client.ownsConnectionAuthority("geometry")).toBe(false);
2093
- await client.fitViewport(120, 40);
2094
- expect(client.ownsConnectionAuthority("geometry")).toBe(true);
2095
- await client.fitViewport(121, 41);
2096
- expect(await client.requestAuthority("geometry")).not.toBeNull();
2097
- expect(socket.sent.filter((frame) => frame.type === "authority-request")).toHaveLength(1);
2098
- expect(socket.sent.findIndex((frame) => frame.type === "authority-request")).toBeLessThan(
2099
- socket.sent.findIndex((frame) => frame.type === "viewport"),
2100
- );
2101
- expect(socket.sent.filter((frame) => frame.type === "viewport")).toHaveLength(2);
2102
- socket.message({
2103
- type: "authority-snapshot",
2104
- snapshot: {
2105
- generation: INSTANCE,
2106
- session: "alpha",
2107
- revision: 4,
2108
- nativeGeometryYieldUntilMs: 0,
2109
- owners: { input: null, focus: null, geometry: null },
2110
- clients: [],
2111
- },
2112
- });
2113
- expect(client.ownsConnectionAuthority("geometry")).toBe(false);
2114
- await client.fitViewport(122, 42);
2115
- expect(socket.sent.filter((frame) => frame.type === "authority-request")).toHaveLength(2);
2116
- client.close();
2117
- });
2118
-
2119
- it("keeps semantic workspace and authenticated authority runtime session distinct", async () => {
2120
- const socket = new FakeSocket();
2121
- const runtimeSession = "tmux-runtime-a";
2122
- let revision = 1;
2123
- socket.onSend = (frame) => {
2124
- if (frame.type === "redeem") {
2125
- queueMicrotask(() =>
2126
- socket.message({
2127
- type: "ready",
2128
- protocolVersion: 2,
2129
- daemonInstanceId: INSTANCE,
2130
- requestId: REQUEST,
2131
- panes: ["pane.editor"],
2132
- effectiveViewerMode: "interactive",
2133
- authority: {
2134
- generation: INSTANCE,
2135
- session: runtimeSession,
2136
- revision,
2137
- nativeGeometryYieldUntilMs: 0,
2138
- owners: { input: null, focus: null, geometry: null },
2139
- clients: [],
2140
- },
2141
- }),
2142
- );
2143
- } else if (frame.type === "authority-request") {
2144
- revision += 1;
2145
- const granted = frame.authority === "geometry";
2146
- const leaseRevision = revision;
2147
- if (granted) revision += 1;
2148
- queueMicrotask(() =>
2149
- socket.message({
2150
- type: "authority-receipt",
2151
- requestId: frame.requestId,
2152
- authority: frame.authority,
2153
- status: granted ? "granted" : "rejected",
2154
- lease: granted
2155
- ? {
2156
- generation: INSTANCE,
2157
- session: runtimeSession,
2158
- clientId: "tui:one",
2159
- authority: frame.authority,
2160
- token: "55555555-5555-4555-8555-555555555555",
2161
- revision: leaseRevision,
2162
- }
2163
- : null,
2164
- snapshot: {
2165
- generation: INSTANCE,
2166
- session: runtimeSession,
2167
- revision,
2168
- nativeGeometryYieldUntilMs: 0,
2169
- owners: {
2170
- input: null,
2171
- focus: null,
2172
- geometry: granted ? "tui:one" : null,
2173
- },
2174
- clients: [],
2175
- },
2176
- }),
2177
- );
2178
- } else if (frame.type === "viewport") {
2179
- queueMicrotask(() =>
2180
- socket.message({
2181
- type: "viewport-ack",
2182
- seq: frame.seq,
2183
- cols: frame.cols,
2184
- rows: frame.rows,
2185
- outcome: "ok",
2186
- authorityLease: frame.authorityLease,
2187
- }),
2188
- );
2189
- } else if (frame.type === "authority-release") {
2190
- revision += 1;
2191
- queueMicrotask(() =>
2192
- socket.message({
2193
- type: "authority-receipt",
2194
- requestId: frame.requestId,
2195
- authority: frame.authority,
2196
- status: "released",
2197
- lease: null,
2198
- snapshot: {
2199
- generation: INSTANCE,
2200
- session: runtimeSession,
2201
- revision,
2202
- nativeGeometryYieldUntilMs: 0,
2203
- owners: { input: null, focus: null, geometry: null },
2204
- clients: [],
2205
- },
2206
- }),
2207
- );
2208
- }
2209
- };
2210
- const client = await openPaneStreamRuntimeClient(
2211
- options(socket, { requestInitialInputAuthority: false }),
2212
- );
2213
- await expect(client.requestAuthority("focus")).resolves.toBeNull();
2214
- await expect(client.fitViewport(140, 46)).resolves.toBe("ok");
2215
- const viewport = socket.sent.find((frame) => frame.type === "viewport");
2216
- expect(viewport).toMatchObject({
2217
- cols: 140,
2218
- rows: 46,
2219
- authorityLease: { session: runtimeSession, authority: "geometry", revision: 3 },
2220
- });
2221
- await expect(client.releaseAuthority("geometry")).resolves.toMatchObject({
2222
- session: runtimeSession,
2223
- owners: { geometry: null },
2224
- });
2225
- socket.message({
2226
- type: "authority-snapshot",
2227
- snapshot: {
2228
- generation: INSTANCE,
2229
- session: runtimeSession,
2230
- revision: revision + 1,
2231
- nativeGeometryYieldUntilMs: 0,
2232
- owners: { input: null, focus: null, geometry: null },
2233
- clients: [],
2234
- },
2235
- });
2236
- expect(socket.closed).toBeNull();
2237
- socket.message({
2238
- type: "authority-snapshot",
2239
- snapshot: {
2240
- generation: INSTANCE,
2241
- session: "tmux-runtime-b",
2242
- revision: revision + 2,
2243
- nativeGeometryYieldUntilMs: 0,
2244
- owners: { input: null, focus: null, geometry: null },
2245
- clients: [],
2246
- },
2247
- });
2248
- expect(socket.closed).toEqual({ code: 1008, reason: "protocol-error" });
2249
- });
2250
-
2251
- it("accepts only monotonic or exactly idempotent authority snapshot replay", async () => {
2252
- const initial = {
2253
- generation: INSTANCE,
2254
- session: "tmux-runtime-a",
2255
- revision: 3,
2256
- nativeGeometryYieldUntilMs: 0,
2257
- owners: { input: "tui:one", focus: null, geometry: null },
2258
- clients: [
2259
- {
2260
- clientId: "tui:one",
2261
- surface: "opentui" as const,
2262
- state: "foreground" as const,
2263
- connectedRevision: 1,
2264
- activityRevision: 2,
2265
- },
2266
- ],
2267
- };
2268
- const open = async () => {
2269
- const socket = new FakeSocket();
2270
- socket.onSend = (frame) => {
2271
- if (frame.type === "redeem") {
2272
- queueMicrotask(() =>
2273
- socket.message({
2274
- type: "ready",
2275
- protocolVersion: 2,
2276
- daemonInstanceId: INSTANCE,
2277
- requestId: REQUEST,
2278
- panes: ["pane.editor"],
2279
- effectiveViewerMode: "interactive",
2280
- authority: initial,
2281
- }),
2282
- );
2283
- }
2284
- };
2285
- const client = await openPaneStreamRuntimeClient(
2286
- options(socket, { requestInitialInputAuthority: false }),
2287
- );
2288
- return { client, socket };
2289
- };
2290
-
2291
- const duplicate = await open();
2292
- duplicate.socket.message({ type: "authority-snapshot", snapshot: initial });
2293
- expect(duplicate.socket.closed).toBeNull();
2294
- duplicate.client.close();
2295
-
2296
- const regressing = await open();
2297
- regressing.socket.message({
2298
- type: "authority-snapshot",
2299
- snapshot: { ...initial, revision: 2 },
2300
- });
2301
- expect(regressing.socket.closed).toEqual({ code: 1008, reason: "protocol-error" });
2302
-
2303
- const conflicting = await open();
2304
- conflicting.socket.message({
2305
- type: "authority-snapshot",
2306
- snapshot: { ...initial, owners: { ...initial.owners, focus: "tui:one" } },
2307
- });
2308
- expect(conflicting.socket.closed).toEqual({ code: 1008, reason: "protocol-error" });
2309
-
2310
- const staleDeadline = await open();
2311
- staleDeadline.socket.message({
2312
- type: "authority-snapshot",
2313
- snapshot: { ...initial, nativeGeometryYieldUntilMs: 180 },
2314
- });
2315
- expect(staleDeadline.socket.closed).toEqual({ code: 1008, reason: "protocol-error" });
2316
-
2317
- const advancingDeadline = await open();
2318
- for (const revision of [4, 5]) {
2319
- advancingDeadline.socket.message({
2320
- type: "authority-snapshot",
2321
- snapshot: { ...initial, revision, nativeGeometryYieldUntilMs: 180 + revision },
2322
- });
2323
- expect(advancingDeadline.socket.closed).toBeNull();
2324
- }
2325
- advancingDeadline.client.close();
2326
- });
2327
-
2328
- it.each([
2329
- ["granted", "regressing"],
2330
- ["granted", "same-revision-conflict"],
2331
- ["rejected", "regressing"],
2332
- ["rejected", "same-revision-conflict"],
2333
- ["released", "regressing"],
2334
- ["released", "same-revision-conflict"],
2335
- ] as const)(
2336
- "does not continue a %s authority receipt after a %s snapshot",
2337
- async (status, staleKind) => {
2338
- const socket = new FakeSocket();
2339
- const initial = {
2340
- generation: INSTANCE,
2341
- session: "tmux-runtime-a",
2342
- revision: 3,
2343
- nativeGeometryYieldUntilMs: 0,
2344
- owners: { input: null, focus: null, geometry: null },
2345
- clients: [],
2346
- };
2347
- socket.onSend = (frame) => {
2348
- if (frame.type !== "redeem") return;
2349
- queueMicrotask(() =>
2350
- socket.message({
2351
- type: "ready",
2352
- protocolVersion: 2,
2353
- daemonInstanceId: INSTANCE,
2354
- requestId: REQUEST,
2355
- panes: ["pane.editor"],
2356
- effectiveViewerMode: "interactive",
2357
- authority: initial,
2358
- }),
2359
- );
2360
- };
2361
- const client = await openPaneStreamRuntimeClient(
2362
- options(socket, { requestInitialInputAuthority: false }),
2363
- );
2364
- const operation =
2365
- status === "released" ? client.releaseAuthority("focus") : client.requestAuthority("focus");
2366
- void operation.catch(() => undefined);
2367
- await Promise.resolve();
2368
- const request = socket.sent.find((frame) =>
2369
- status === "released"
2370
- ? frame.type === "authority-release"
2371
- : frame.type === "authority-request",
2372
- )!;
2373
- const granted = status === "granted";
2374
- const snapshot = {
2375
- ...initial,
2376
- revision: staleKind === "regressing" ? 2 : 3,
2377
- nativeGeometryYieldUntilMs: staleKind === "same-revision-conflict" ? 1 : 0,
2378
- owners: { ...initial.owners, focus: granted ? "tui:one" : null },
2379
- };
2380
- socket.message({
2381
- type: "authority-receipt",
2382
- requestId: request.requestId,
2383
- authority: "focus",
2384
- status,
2385
- lease: granted
2386
- ? {
2387
- generation: INSTANCE,
2388
- session: "tmux-runtime-a",
2389
- clientId: "tui:one",
2390
- authority: "focus",
2391
- token: "55555555-5555-4555-8555-555555555555",
2392
- revision: snapshot.revision,
2393
- }
2394
- : null,
2395
- snapshot,
2396
- });
2397
- await expect(operation).rejects.toThrow();
2398
- expect(socket.closed).toEqual({ code: 1008, reason: "protocol-error" });
2399
- expect(client.ownsConnectionAuthority("focus")).toBe(false);
2400
- },
2401
- );
2402
-
2403
- it.each([
2404
- ["geometry-authority-conflict", "55555555-5555-4555-8555-555555555555"],
2405
- ["ok", "66666666-6666-4666-8666-666666666666"],
2406
- ] as const)("binds viewport outcome %s to the exact granted lease", async (outcome, token) => {
2407
- const socket = new FakeSocket();
2408
- socket.onSend = (frame) => {
2409
- if (frame.type === "redeem") {
2410
- queueMicrotask(() =>
2411
- socket.message({
2412
- type: "ready",
2413
- protocolVersion: 2,
2414
- daemonInstanceId: INSTANCE,
2415
- requestId: REQUEST,
2416
- panes: ["pane.editor"],
2417
- effectiveViewerMode: "interactive",
2418
- }),
2419
- );
2420
- } else if (frame.type === "authority-request") {
2421
- queueMicrotask(() =>
2422
- socket.message({
2423
- type: "authority-receipt",
2424
- requestId: frame.requestId,
2425
- authority: "geometry",
2426
- status: "granted",
2427
- lease: {
2428
- generation: INSTANCE,
2429
- session: "tmux-runtime-a",
2430
- clientId: "tui:one",
2431
- authority: "geometry",
2432
- token: "55555555-5555-4555-8555-555555555555",
2433
- revision: 1,
2434
- },
2435
- snapshot: {
2436
- generation: INSTANCE,
2437
- session: "tmux-runtime-a",
2438
- revision: 1,
2439
- nativeGeometryYieldUntilMs: 0,
2440
- owners: { input: null, focus: null, geometry: "tui:one" },
2441
- clients: [],
2442
- },
2443
- }),
2444
- );
2445
- } else if (frame.type === "viewport") {
2446
- queueMicrotask(() => {
2447
- if (outcome === "geometry-authority-conflict") {
2448
- socket.message({
2449
- type: "authority-snapshot",
2450
- snapshot: {
2451
- generation: INSTANCE,
2452
- session: "tmux-runtime-a",
2453
- revision: 2,
2454
- nativeGeometryYieldUntilMs: 0,
2455
- owners: { input: null, focus: null, geometry: null },
2456
- clients: [],
2457
- },
2458
- });
2459
- }
2460
- socket.message({
2461
- type: "viewport-ack",
2462
- seq: frame.seq,
2463
- cols: frame.cols,
2464
- rows: frame.rows,
2465
- outcome,
2466
- authorityLease: { ...frame.authorityLease, token },
2467
- });
2468
- });
2469
- }
2470
- };
2471
- const client = await openPaneStreamRuntimeClient(
2472
- options(socket, { requestInitialInputAuthority: false }),
2473
- );
2474
- const fitted = client.fitViewport(140, 46);
2475
- if (outcome === "geometry-authority-conflict") {
2476
- await expect(fitted).resolves.toBe("geometry-authority-conflict");
2477
- expect(client.ownsConnectionAuthority("geometry")).toBe(false);
2478
- expect(socket.closed).toBeNull();
2479
- } else {
2480
- await expect(fitted).rejects.toThrow();
2481
- expect(socket.closed).toEqual({ code: 1008, reason: "protocol-error" });
2482
- }
2483
- });
2484
-
2485
- it("bounds unacknowledged input and retires every pending write exactly once", async () => {
2486
- const baseline = runtimeResourceSnapshot();
2487
- const socket = new FakeSocket();
2488
- socket.onSend = (frame) => {
2489
- if (frame.type === "redeem") {
2490
- queueMicrotask(() =>
2491
- socket.message({
2492
- type: "ready",
2493
- protocolVersion: 2,
2494
- daemonInstanceId: INSTANCE,
2495
- requestId: REQUEST,
2496
- panes: ["pane.editor"],
2497
- effectiveViewerMode: "interactive",
2498
- }),
2499
- );
2500
- } else if (frame.type === "authority-request") {
2501
- queueMicrotask(() =>
2502
- socket.message({
2503
- type: "authority-receipt",
2504
- requestId: frame.requestId,
2505
- authority: "input",
2506
- status: "granted",
2507
- lease: {
2508
- generation: INSTANCE,
2509
- session: "alpha",
2510
- clientId: "tui:one",
2511
- authority: "input",
2512
- token: "55555555-5555-4555-8555-555555555555",
2513
- revision: 1,
2514
- },
2515
- snapshot: {
2516
- generation: INSTANCE,
2517
- session: "alpha",
2518
- revision: 1,
2519
- nativeGeometryYieldUntilMs: 0,
2520
- owners: { input: "tui:one", focus: null, geometry: null },
2521
- clients: [],
2522
- },
2523
- }),
2524
- );
2525
- }
2526
- };
2527
- const client = await openPaneStreamRuntimeClient(options(socket));
2528
- expect(await client.requestAuthority("input")).not.toBeNull();
2529
- const pending = Array.from({ length: 256 }, (_, index) =>
2530
- client.sendTerminalInput(
2531
- { workspaceName: "alpha", semanticPaneId: "pane.editor" },
2532
- { kind: "text", data: `input-${index}` },
2533
- ),
2534
- );
2535
- await expect(
2536
- client.sendTerminalInput(
2537
- { workspaceName: "alpha", semanticPaneId: "pane.editor" },
2538
- { kind: "key", data: "Enter" },
2539
- ),
2540
- ).rejects.toThrow("queue is full");
2541
- expect(
2542
- socket.sent.filter((frame) => (frame as { type?: string }).type === "input"),
2543
- ).toHaveLength(256);
2544
- expect(runtimeResourceSnapshot()["runtime-timer"].active).toBe(
2545
- baseline["runtime-timer"].active + 256,
2546
- );
2547
-
2548
- client.close();
2549
- const settled = await Promise.allSettled(pending);
2550
- expect(settled.every((result) => result.status === "rejected")).toBe(true);
2551
- socket.message({ type: "input-ack", pane: "pane.editor", seq: 256 });
2552
- expect(socket.closed).toEqual({ code: 1000, reason: "client-closed" });
2553
- expect(runtimeResourceSnapshot()["runtime-timer"].active).toBe(
2554
- baseline["runtime-timer"].active,
2555
- );
2556
- });
2557
-
2558
- it("ledgers and retires every pending operation timer", async () => {
2559
- const baseline = runtimeResourceSnapshot();
2560
- const socket = new FakeSocket();
2561
- socket.onSend = (frame) => {
2562
- if (frame.type !== "redeem") return;
2563
- queueMicrotask(() =>
2564
- socket.message({
2565
- type: "ready",
2566
- protocolVersion: 2,
2567
- daemonInstanceId: INSTANCE,
2568
- requestId: REQUEST,
2569
- panes: ["pane.editor"],
2570
- effectiveViewerMode: "interactive",
2571
- authority: {
2572
- generation: INSTANCE,
2573
- session: "alpha",
2574
- revision: 1,
2575
- nativeGeometryYieldUntilMs: 0,
2576
- owners: { input: "tui:one", focus: null, geometry: "tui:one" },
2577
- clients: [],
2578
- },
2579
- }),
2580
- );
2581
- };
2582
- const client = await openPaneStreamRuntimeClient(
2583
- options(socket, { requestInitialInputAuthority: false }),
2584
- );
2585
- const pending = [
2586
- client.sendTerminalInput(
2587
- { workspaceName: "alpha", semanticPaneId: "pane.editor" },
2588
- { kind: "text", data: "pending" },
2589
- ),
2590
- client.fitViewport(120, 40),
2591
- client.submitIntent(OPERATION, {
2592
- verb: "workspace.pane.select",
2593
- workspaceName: "alpha",
2594
- semanticPaneId: "pane.editor",
2595
- }),
2596
- client.requestAuthority("focus"),
2597
- client.releaseAuthority("input"),
2598
- ];
2599
- await Bun.sleep(0);
2600
- expect(runtimeResourceSnapshot()["runtime-timer"].active).toBe(
2601
- baseline["runtime-timer"].active + 5,
2602
- );
2603
- client.close();
2604
- expect(
2605
- (await Promise.allSettled(pending)).every((result) => result.status === "rejected"),
2606
- ).toBe(true);
2607
- expect(runtimeResourceSnapshot()["runtime-timer"].active).toBe(
2608
- baseline["runtime-timer"].active,
2609
- );
2610
- });
2611
-
2612
- it("rejects a ready frame from another generation or mode", async () => {
2613
- const socket = new FakeSocket();
2614
- socket.onSend = (frame) => {
2615
- if (frame.type !== "redeem") return;
2616
- queueMicrotask(() =>
2617
- socket.message({
2618
- type: "ready",
2619
- protocolVersion: 2,
2620
- daemonInstanceId: INSTANCE,
2621
- requestId: REQUEST,
2622
- panes: ["pane.editor"],
2623
- effectiveViewerMode: "read-only",
2624
- }),
2625
- );
2626
- };
2627
- await expect(openPaneStreamRuntimeClient(options(socket))).rejects.toThrow(
2628
- "peer identity did not match",
2629
- );
2630
- expect(socket.closed).toEqual({ code: 1008, reason: "protocol-error" });
2631
- });
2632
-
2633
- it("rejects a daemon ready identity for another redeemed client", async () => {
2634
- const socket = new FakeSocket();
2635
- socket.onSend = (frame) => {
2636
- if (frame.type !== "redeem") return;
2637
- queueMicrotask(() =>
2638
- socket.message({
2639
- type: "ready",
2640
- protocolVersion: 2,
2641
- daemonInstanceId: INSTANCE,
2642
- requestId: REQUEST,
2643
- connectionClientId: "web:foreign",
2644
- panes: ["pane.editor"],
2645
- effectiveViewerMode: "interactive",
2646
- }),
2647
- );
2648
- };
2649
- await expect(openPaneStreamRuntimeClient(options(socket))).rejects.toThrow(
2650
- "peer client identity did not match",
2651
- );
2652
- expect(socket.closed).toEqual({ code: 1008, reason: "protocol-error" });
2653
- });
2654
-
2655
- it("fails closed when legacy raw-v1 output appears on a semantic lane", async () => {
2656
- const socket = new FakeSocket();
2657
- socket.onSend = (frame) => {
2658
- if (frame.type === "redeem") {
2659
- queueMicrotask(() =>
2660
- socket.message({
2661
- type: "ready",
2662
- protocolVersion: 2,
2663
- daemonInstanceId: INSTANCE,
2664
- requestId: REQUEST,
2665
- panes: ["pane.editor"],
2666
- effectiveViewerMode: "interactive",
2667
- }),
2668
- );
2669
- } else if (frame.type === "authority-request") {
2670
- queueMicrotask(() =>
2671
- socket.message({
2672
- type: "authority-receipt",
2673
- requestId: frame.requestId,
2674
- authority: "input",
2675
- status: "granted",
2676
- lease: {
2677
- generation: INSTANCE,
2678
- session: "alpha",
2679
- clientId: "tui:one",
2680
- authority: "input",
2681
- token: "55555555-5555-4555-8555-555555555555",
2682
- revision: 2,
2683
- },
2684
- snapshot: {
2685
- generation: INSTANCE,
2686
- session: "alpha",
2687
- revision: 2,
2688
- nativeGeometryYieldUntilMs: 0,
2689
- owners: { input: "tui:one", focus: null, geometry: null },
2690
- clients: [
2691
- {
2692
- clientId: "tui:one",
2693
- surface: "opentui",
2694
- state: "foreground",
2695
- connectedRevision: 1,
2696
- activityRevision: 2,
2697
- },
2698
- ],
2699
- },
2700
- }),
2701
- );
2702
- }
2703
- };
2704
- const onFault = mock();
2705
- await openPaneStreamRuntimeClient(options(socket, { onFault }));
2706
- socket.message({ type: "output", pane: "pane.editor", seq: 1, data: "eA==" });
2707
- expect(onFault).toHaveBeenCalledWith(
2708
- expect.objectContaining({ message: expect.stringContaining("legacy output") }),
2709
- );
2710
- expect(socket.closed).toEqual({ code: 1008, reason: "protocol-error" });
2711
- });
2712
-
2713
- it("preserves a retryable topology rejection as an exact typed runtime fault", async () => {
2714
- const socket = new FakeSocket();
2715
- acceptInteractiveHandshake(socket);
2716
- const onFault = mock();
2717
- await openPaneStreamRuntimeClient(options(socket, { onFault }));
2718
-
2719
- socket.message({
2720
- type: "error",
2721
- protocolVersion: 2,
2722
- code: "topology-changed",
2723
- retryable: true,
2724
- });
2725
-
2726
- expect(onFault).toHaveBeenCalledWith(
2727
- expect.objectContaining({
2728
- name: "PaneStreamOperationError",
2729
- code: "topology-changed",
2730
- }),
2731
- );
2732
- expect(socket.closed).toEqual({ code: 1008, reason: "protocol-error" });
2733
- });
2734
-
2735
- it("enriches a causal probe with exact authenticated transport facts", async () => {
2736
- const socket = new FakeSocket();
2737
- socket.onSend = (frame) => {
2738
- if (frame.type === "redeem") {
2739
- queueMicrotask(() =>
2740
- socket.message({
2741
- type: "ready",
2742
- protocolVersion: 2,
2743
- daemonInstanceId: INSTANCE,
2744
- requestId: REQUEST,
2745
- panes: ["pane.editor"],
2746
- effectiveViewerMode: "interactive",
2747
- diagnosticCapabilities: ["causal-cell-v1"],
2748
- }),
2749
- );
2750
- } else if (frame.type === "authority-request") {
2751
- queueMicrotask(() =>
2752
- socket.message({
2753
- type: "authority-receipt",
2754
- requestId: frame.requestId,
2755
- authority: "input",
2756
- status: "granted",
2757
- lease: {
2758
- generation: INSTANCE,
2759
- session: "alpha",
2760
- clientId: "tui:one",
2761
- authority: "input",
2762
- token: "55555555-5555-4555-8555-555555555555",
2763
- revision: 2,
2764
- },
2765
- snapshot: {
2766
- generation: INSTANCE,
2767
- session: "alpha",
2768
- revision: 2,
2769
- nativeGeometryYieldUntilMs: 0,
2770
- owners: { input: "tui:one", focus: null, geometry: null },
2771
- clients: [],
2772
- },
2773
- }),
2774
- );
2775
- } else if (frame.type === "input") {
2776
- queueMicrotask(() =>
2777
- socket.message({ type: "input-ack", pane: "pane.editor", seq: frame.seq }),
2778
- );
2779
- }
2780
- };
2781
- const client = await openPaneStreamRuntimeClient(
2782
- options(socket, {
2783
- requestInitialInputAuthority: false,
2784
- diagnosticCapabilities: ["causal-cell-v1"],
2785
- }),
2786
- );
2787
- socket.message({
2788
- type: "terminal-delivery-ready",
2789
- pane: "pane.editor",
2790
- negotiation: {
2791
- accepted: true,
2792
- negotiated: {
2793
- protocolVersion: 1,
2794
- encoding: "semantic-v1",
2795
- richPlacements: false,
2796
- generation: INSTANCE,
2797
- deliveryNonce: "00000000-0000-4000-8000-000000000097",
2798
- },
2799
- },
2800
- });
2801
- await client.requestAuthority("input");
2802
- const cell = {
2803
- grapheme: " ",
2804
- width: 1,
2805
- foreground: { kind: "default" },
2806
- background: { kind: "default" },
2807
- attributes: 0,
2808
- } as const;
2809
- await client.sendTerminalInput(
2810
- { workspaceName: "alpha", semanticPaneId: "pane.editor" },
2811
- { kind: "text", data: "probe" },
2812
- "00000000-0000-4000-8000-000000000099",
2813
- {
2814
- version: 1,
2815
- capability: "causal-cell-v1",
2816
- traceId: "00000000-0000-4000-8000-000000000099",
2817
- semanticPaneId: "pane.editor",
2818
- generation: INSTANCE,
2819
- incarnation: `${INSTANCE}:0`,
2820
- baselineRevision: 7,
2821
- baselineStateHash: "0000000000000000",
2822
- geometry: { cols: 2, rows: 1, row: 0, column: 1 },
2823
- before: cell,
2824
- after: { ...cell, grapheme: "X" },
2825
- },
2826
- );
2827
- expect(
2828
- socket.sent.findLast((frame) => (frame as { type: string }).type === "input"),
2829
- ).toMatchObject({
2830
- seq: 1,
2831
- pane: "pane.editor",
2832
- causalProbe: {
2833
- clientId: "tui:one",
2834
- transportNonce: REQUEST,
2835
- deliveryNonce: "00000000-0000-4000-8000-000000000097",
2836
- inputSequence: 1,
2837
- semanticPaneId: "pane.editor",
2838
- },
2839
- });
2840
- });
2841
- });