tmux-ide 2.9.2 → 2.9.4

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