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,1086 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
-
3
- import { DAEMON_WIRE_PROTOCOL_VERSION } from "@tmux-ide/contracts";
4
- import {
5
- createWorkspaceEventSupervisor,
6
- type WorkspaceEventSocket,
7
- type WorkspaceEventSocketEvent,
8
- type WorkspaceEventSocketEventType,
9
- type WorkspaceEventSocketListener,
10
- } from "./workspace-event-supervisor.ts";
11
-
12
- function deferred<Value>() {
13
- let resolve!: (value: Value) => void;
14
- let reject!: (error: unknown) => void;
15
- const promise = new Promise<Value>((accept, decline) => {
16
- resolve = accept;
17
- reject = decline;
18
- });
19
- return { promise, resolve, reject };
20
- }
21
-
22
- class FakeSocket implements WorkspaceEventSocket {
23
- readyState = 1;
24
- readonly sent: unknown[] = [];
25
- readonly listeners = new Map<WorkspaceEventSocketEventType, Set<WorkspaceEventSocketListener>>();
26
- readonly close = vi.fn();
27
-
28
- addEventListener(type: WorkspaceEventSocketEventType, listener: WorkspaceEventSocketListener) {
29
- const bucket = this.listeners.get(type) ?? new Set();
30
- bucket.add(listener);
31
- this.listeners.set(type, bucket);
32
- }
33
- removeEventListener(type: WorkspaceEventSocketEventType, listener: WorkspaceEventSocketListener) {
34
- this.listeners.get(type)?.delete(listener);
35
- }
36
- send(data: string) {
37
- this.sent.push(JSON.parse(data));
38
- }
39
- emit(type: WorkspaceEventSocketEventType, event: WorkspaceEventSocketEvent = {}) {
40
- for (const listener of this.listeners.get(type) ?? []) listener(event);
41
- }
42
- frame(frame: unknown) {
43
- this.emit("message", { data: JSON.stringify(frame) });
44
- }
45
- }
46
-
47
- const daemon = {
48
- protocolVersion: DAEMON_WIRE_PROTOCOL_VERSION,
49
- productVersion: "1.0.0",
50
- instanceId: "123e4567-e89b-42d3-a456-426614174000",
51
- startedAt: "2026-08-17T10:00:00.000Z",
52
- };
53
-
54
- const resource = (revision: number) => ({
55
- workspaceName: "alpha",
56
- workspaceId: "workspace.0123456789abcdefabcd",
57
- sessionId: "session.0123456789abcdefabcd",
58
- resourceRevision: revision,
59
- semanticPaneIds: ["pane.alpha"],
60
- });
61
-
62
- async function tick(): Promise<void> {
63
- await new Promise<void>((resolve) => setTimeout(resolve, 0));
64
- }
65
-
66
- function fakeRefreshClock() {
67
- let nextId = 1;
68
- const callbacks = new Map<number, () => void>();
69
- return {
70
- clock: {
71
- setTimeout(callback: () => void) {
72
- const id = nextId++;
73
- callbacks.set(id, callback);
74
- return id;
75
- },
76
- clearTimeout(handle: unknown) {
77
- callbacks.delete(handle as number);
78
- },
79
- },
80
- get pending() {
81
- return callbacks.size;
82
- },
83
- runNext() {
84
- const entry = callbacks.entries().next().value as [number, () => void] | undefined;
85
- if (!entry) return false;
86
- callbacks.delete(entry[0]);
87
- entry[1]();
88
- return true;
89
- },
90
- };
91
- }
92
-
93
- describe("WorkspaceEventSupervisor", () => {
94
- it("bounds a sink-triggered refresh and cancels its delayed successor on dispose", async () => {
95
- const socket = new FakeSocket();
96
- const fetch = vi.fn(async () => resource(0));
97
- const refreshDiagnostics: Readonly<Record<string, unknown>>[] = [];
98
- const supervisor = createWorkspaceEventSupervisor({
99
- socket,
100
- daemon,
101
- workspaceName: "alpha",
102
- sessionName: "tmux-alpha",
103
- fetchTerminalRuntimeInventory: fetch,
104
- onDiagnostic: (phase, details) => {
105
- if (phase === "terminal-refresh") refreshDiagnostics.push(details);
106
- },
107
- });
108
- const preparing = supervisor.prepareTerminalRuntimeInventory(new AbortController().signal);
109
- socket.emit("open");
110
- socket.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
111
- await tick();
112
- socket.frame({
113
- type: "resource.interests-ack",
114
- interestRevision: 1,
115
- sequence: 0,
116
- unavailableInterests: [],
117
- });
118
- const prepared = await preparing;
119
- expect(
120
- supervisor.adoptTerminalRuntimeInventory(prepared, () => {
121
- supervisor.refreshTerminalRuntimeInventory();
122
- }),
123
- ).toEqual(resource(0));
124
-
125
- supervisor.refreshTerminalRuntimeInventory();
126
- await tick();
127
- expect(fetch).toHaveBeenCalledTimes(2);
128
- expect(refreshDiagnostics).toEqual([
129
- {
130
- reason: "consumer",
131
- coalescedRequests: 0,
132
- delayed: false,
133
- attempt: 1,
134
- outcome: "success",
135
- failure: null,
136
- },
137
- ]);
138
- await new Promise((resolve) => setTimeout(resolve, 5));
139
- expect(fetch).toHaveBeenCalledTimes(2);
140
- supervisor.dispose();
141
- await new Promise((resolve) => setTimeout(resolve, 30));
142
- expect(fetch).toHaveBeenCalledTimes(2);
143
- });
144
-
145
- it("delays a sink-requeued refresh and still publishes a later authority revision", async () => {
146
- const socket = new FakeSocket();
147
- const reads = [resource(0), resource(1), resource(2)];
148
- const diagnostics: Readonly<Record<string, unknown>>[] = [];
149
- const fetch = vi.fn(async () => reads.shift() ?? resource(2));
150
- const supervisor = createWorkspaceEventSupervisor({
151
- socket,
152
- daemon,
153
- workspaceName: "alpha",
154
- sessionName: "tmux-alpha",
155
- fetchTerminalRuntimeInventory: fetch,
156
- onDiagnostic: (phase, details) => {
157
- if (phase === "terminal-refresh") diagnostics.push(details);
158
- },
159
- });
160
- const preparing = supervisor.prepareTerminalRuntimeInventory(new AbortController().signal);
161
- socket.emit("open");
162
- socket.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
163
- await tick();
164
- socket.frame({
165
- type: "resource.interests-ack",
166
- interestRevision: 1,
167
- sequence: 0,
168
- unavailableInterests: [],
169
- });
170
- const prepared = await preparing;
171
- const published: number[] = [];
172
- supervisor.adoptTerminalRuntimeInventory(prepared, (next) => {
173
- published.push(next.resourceRevision);
174
- if (next.resourceRevision === 1) supervisor.refreshTerminalRuntimeInventory();
175
- });
176
-
177
- supervisor.refreshTerminalRuntimeInventory();
178
- await new Promise((resolve) => setTimeout(resolve, 40));
179
- expect(fetch).toHaveBeenCalledTimes(3);
180
- expect(published).toEqual([1, 2]);
181
- expect(diagnostics).toEqual([
182
- {
183
- reason: "consumer",
184
- coalescedRequests: 0,
185
- delayed: false,
186
- attempt: 1,
187
- outcome: "success",
188
- failure: null,
189
- },
190
- {
191
- reason: "consumer",
192
- coalescedRequests: 0,
193
- delayed: true,
194
- attempt: 1,
195
- outcome: "success",
196
- failure: null,
197
- },
198
- ]);
199
- supervisor.dispose();
200
- });
201
-
202
- it("retries one transient refresh failure on the bounded clock", async () => {
203
- const socket = new FakeSocket();
204
- const timer = fakeRefreshClock();
205
- let reads = 0;
206
- const supervisor = createWorkspaceEventSupervisor({
207
- socket,
208
- daemon,
209
- workspaceName: "alpha",
210
- sessionName: "tmux-alpha",
211
- terminalRefreshClock: timer.clock,
212
- fetchTerminalRuntimeInventory: async () => {
213
- reads += 1;
214
- if (reads === 2) throw new Error("transient");
215
- return resource(reads === 1 ? 0 : 1);
216
- },
217
- });
218
- const preparing = supervisor.prepareTerminalRuntimeInventory(new AbortController().signal);
219
- socket.emit("open");
220
- socket.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
221
- await tick();
222
- socket.frame({
223
- type: "resource.interests-ack",
224
- interestRevision: 1,
225
- sequence: 0,
226
- unavailableInterests: [],
227
- });
228
- const prepared = await preparing;
229
- const published: number[] = [];
230
- supervisor.adoptTerminalRuntimeInventory(prepared, (next) =>
231
- published.push(next.resourceRevision),
232
- );
233
-
234
- supervisor.refreshTerminalRuntimeInventory();
235
- await tick();
236
- expect(timer.pending).toBe(1);
237
- timer.runNext();
238
- await tick();
239
- expect(reads).toBe(3);
240
- expect(published).toEqual([1]);
241
- expect(timer.pending).toBe(0);
242
- supervisor.dispose();
243
- });
244
-
245
- it("bounds permanent refresh failure and lets an event replace a pending retry", async () => {
246
- const socket = new FakeSocket();
247
- const timer = fakeRefreshClock();
248
- let reads = 0;
249
- let fail = true;
250
- const diagnostics: Readonly<Record<string, unknown>>[] = [];
251
- const supervisor = createWorkspaceEventSupervisor({
252
- socket,
253
- daemon,
254
- workspaceName: "alpha",
255
- sessionName: "tmux-alpha",
256
- terminalRefreshClock: timer.clock,
257
- fetchTerminalRuntimeInventory: async () => {
258
- reads += 1;
259
- if (reads > 1 && fail) throw new Error("persistent");
260
- return resource(reads > 1 ? 1 : 0);
261
- },
262
- onDiagnostic: (phase, details) => {
263
- if (phase === "terminal-refresh") diagnostics.push(details);
264
- },
265
- });
266
- const preparing = supervisor.prepareTerminalRuntimeInventory(new AbortController().signal);
267
- socket.emit("open");
268
- socket.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
269
- await tick();
270
- socket.frame({
271
- type: "resource.interests-ack",
272
- interestRevision: 1,
273
- sequence: 0,
274
- unavailableInterests: [],
275
- });
276
- const prepared = await preparing;
277
- const published: number[] = [];
278
- supervisor.adoptTerminalRuntimeInventory(prepared, (next) =>
279
- published.push(next.resourceRevision),
280
- );
281
-
282
- supervisor.refreshTerminalRuntimeInventory();
283
- await tick();
284
- expect(timer.pending).toBe(1);
285
- socket.frame({
286
- type: "resource.changed",
287
- sequence: 1,
288
- workspaceName: "alpha",
289
- resource: "terminal-runtime-inventory",
290
- revision: 1,
291
- causeOperationId: null,
292
- });
293
- fail = false;
294
- timer.runNext();
295
- await tick();
296
- expect(published).toEqual([1]);
297
- expect(timer.pending).toBe(0);
298
- expect(diagnostics.at(-1)).toMatchObject({ reason: "event", coalescedRequests: 1 });
299
-
300
- fail = true;
301
- supervisor.refreshTerminalRuntimeInventory();
302
- await tick();
303
- expect(timer.pending).toBe(1);
304
- timer.runNext();
305
- await tick();
306
- expect(timer.pending).toBe(1);
307
- timer.runNext();
308
- await tick();
309
- expect(timer.pending).toBe(0);
310
- expect(diagnostics.slice(-3).map(({ attempt }) => attempt)).toEqual([1, 2, 3]);
311
- expect(diagnostics.at(-1)).toMatchObject({ outcome: "exhausted" });
312
- supervisor.dispose();
313
- });
314
-
315
- it("aborts an in-flight refresh without scheduling a retry on dispose", async () => {
316
- const socket = new FakeSocket();
317
- const timer = fakeRefreshClock();
318
- let reads = 0;
319
- let refreshAborted = false;
320
- const supervisor = createWorkspaceEventSupervisor({
321
- socket,
322
- daemon,
323
- workspaceName: "alpha",
324
- sessionName: "tmux-alpha",
325
- terminalRefreshClock: timer.clock,
326
- fetchTerminalRuntimeInventory: async (signal) => {
327
- reads += 1;
328
- if (reads === 1) return resource(0);
329
- return await new Promise<ReturnType<typeof resource>>((_resolve, reject) => {
330
- signal.addEventListener(
331
- "abort",
332
- () => {
333
- refreshAborted = true;
334
- reject(new Error("aborted"));
335
- },
336
- { once: true },
337
- );
338
- });
339
- },
340
- });
341
- const preparing = supervisor.prepareTerminalRuntimeInventory(new AbortController().signal);
342
- socket.emit("open");
343
- socket.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
344
- await tick();
345
- socket.frame({
346
- type: "resource.interests-ack",
347
- interestRevision: 1,
348
- sequence: 0,
349
- unavailableInterests: [],
350
- });
351
- const prepared = await preparing;
352
- supervisor.adoptTerminalRuntimeInventory(prepared, vi.fn());
353
- supervisor.refreshTerminalRuntimeInventory();
354
- await tick();
355
- supervisor.dispose();
356
- await tick();
357
- expect(refreshAborted).toBe(true);
358
- expect(timer.pending).toBe(0);
359
- });
360
-
361
- it("retries a stale response until the event revision becomes readable", async () => {
362
- const socket = new FakeSocket();
363
- const timer = fakeRefreshClock();
364
- const reads = [resource(0), resource(1), resource(2)];
365
- const diagnostics: Readonly<Record<string, unknown>>[] = [];
366
- const supervisor = createWorkspaceEventSupervisor({
367
- socket,
368
- daemon,
369
- workspaceName: "alpha",
370
- sessionName: "tmux-alpha",
371
- terminalRefreshClock: timer.clock,
372
- fetchTerminalRuntimeInventory: async () => reads.shift() ?? resource(2),
373
- onDiagnostic: (phase, details) => {
374
- if (phase === "terminal-refresh") diagnostics.push(details);
375
- },
376
- });
377
- const preparing = supervisor.prepareTerminalRuntimeInventory(new AbortController().signal);
378
- socket.emit("open");
379
- socket.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
380
- await tick();
381
- socket.frame({
382
- type: "resource.interests-ack",
383
- interestRevision: 1,
384
- sequence: 0,
385
- unavailableInterests: [],
386
- });
387
- const prepared = await preparing;
388
- const published: number[] = [];
389
- supervisor.adoptTerminalRuntimeInventory(prepared, (next) =>
390
- published.push(next.resourceRevision),
391
- );
392
- socket.frame({
393
- type: "resource.changed",
394
- sequence: 1,
395
- workspaceName: "alpha",
396
- resource: "terminal-runtime-inventory",
397
- revision: 2,
398
- causeOperationId: null,
399
- });
400
- await tick();
401
- expect(published).toEqual([]);
402
- expect(timer.pending).toBe(1);
403
- expect(diagnostics.at(-1)).toMatchObject({
404
- attempt: 1,
405
- outcome: "retry",
406
- failure: "stale-revision",
407
- });
408
- timer.runNext();
409
- await tick();
410
- expect(published).toEqual([2]);
411
- expect(timer.pending).toBe(0);
412
- supervisor.dispose();
413
- });
414
-
415
- it("exhausts three stale responses without publishing older authority", async () => {
416
- const socket = new FakeSocket();
417
- const timer = fakeRefreshClock();
418
- let initial = true;
419
- const diagnostics: Readonly<Record<string, unknown>>[] = [];
420
- const supervisor = createWorkspaceEventSupervisor({
421
- socket,
422
- daemon,
423
- workspaceName: "alpha",
424
- sessionName: "tmux-alpha",
425
- terminalRefreshClock: timer.clock,
426
- fetchTerminalRuntimeInventory: async () => {
427
- if (initial) {
428
- initial = false;
429
- return resource(0);
430
- }
431
- return resource(1);
432
- },
433
- onDiagnostic: (phase, details) => {
434
- if (phase === "terminal-refresh") diagnostics.push(details);
435
- },
436
- });
437
- const preparing = supervisor.prepareTerminalRuntimeInventory(new AbortController().signal);
438
- socket.emit("open");
439
- socket.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
440
- await tick();
441
- socket.frame({
442
- type: "resource.interests-ack",
443
- interestRevision: 1,
444
- sequence: 0,
445
- unavailableInterests: [],
446
- });
447
- const prepared = await preparing;
448
- const sink = vi.fn();
449
- supervisor.adoptTerminalRuntimeInventory(prepared, sink);
450
- socket.frame({
451
- type: "resource.changed",
452
- sequence: 1,
453
- workspaceName: "alpha",
454
- resource: "terminal-runtime-inventory",
455
- revision: 3,
456
- causeOperationId: null,
457
- });
458
- await tick();
459
- timer.runNext();
460
- await tick();
461
- timer.runNext();
462
- await tick();
463
- expect(timer.pending).toBe(0);
464
- expect(sink).not.toHaveBeenCalled();
465
- expect(diagnostics.map(({ attempt }) => attempt)).toEqual([1, 2, 3]);
466
- expect(diagnostics.at(-1)).toMatchObject({
467
- outcome: "exhausted",
468
- failure: "stale-revision",
469
- });
470
- supervisor.dispose();
471
- });
472
-
473
- it("charges an in-flight failed attempt despite repeated consumer coalescing", async () => {
474
- const socket = new FakeSocket();
475
- const timer = fakeRefreshClock();
476
- const firstRefresh = deferred<ReturnType<typeof resource>>();
477
- const finalRefresh = deferred<ReturnType<typeof resource>>();
478
- let reads = 0;
479
- const diagnostics: Readonly<Record<string, unknown>>[] = [];
480
- const supervisor = createWorkspaceEventSupervisor({
481
- socket,
482
- daemon,
483
- workspaceName: "alpha",
484
- sessionName: "tmux-alpha",
485
- terminalRefreshClock: timer.clock,
486
- fetchTerminalRuntimeInventory: async () => {
487
- reads += 1;
488
- if (reads === 1) return resource(0);
489
- if (reads === 2) return firstRefresh.promise;
490
- if (reads === 4) return finalRefresh.promise;
491
- throw new Error("persistent");
492
- },
493
- onDiagnostic: (phase, details) => {
494
- if (phase === "terminal-refresh") diagnostics.push(details);
495
- },
496
- });
497
- const preparing = supervisor.prepareTerminalRuntimeInventory(new AbortController().signal);
498
- socket.emit("open");
499
- socket.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
500
- await tick();
501
- socket.frame({
502
- type: "resource.interests-ack",
503
- interestRevision: 1,
504
- sequence: 0,
505
- unavailableInterests: [],
506
- });
507
- const prepared = await preparing;
508
- supervisor.adoptTerminalRuntimeInventory(prepared, vi.fn());
509
- supervisor.refreshTerminalRuntimeInventory();
510
- await tick();
511
- for (let index = 0; index < 1_000; index += 1) supervisor.refreshTerminalRuntimeInventory();
512
- for (let index = 1; index <= 1_000; index += 1)
513
- socket.frame({
514
- type: "resource.changed",
515
- sequence: index,
516
- workspaceName: "alpha",
517
- resource: "terminal-runtime-inventory",
518
- revision: 0,
519
- causeOperationId: null,
520
- });
521
- firstRefresh.reject(new Error("first failed"));
522
- await tick();
523
- expect(timer.pending).toBe(1);
524
- timer.runNext();
525
- await tick();
526
- expect(timer.pending).toBe(1);
527
- timer.runNext();
528
- await tick();
529
- for (let index = 0; index < 1_000; index += 1) supervisor.refreshTerminalRuntimeInventory();
530
- finalRefresh.reject(new Error("final failed"));
531
- await tick();
532
- expect(timer.pending).toBe(0);
533
- expect(reads).toBe(4);
534
- expect(diagnostics.map(({ attempt }) => attempt)).toEqual([1, 2, 3]);
535
- expect(diagnostics.at(-1)).toMatchObject({
536
- outcome: "exhausted",
537
- failure: "read-failed",
538
- });
539
- expect(diagnostics.at(-1)?.coalescedRequests).toBe(999);
540
- supervisor.refreshTerminalRuntimeInventory();
541
- await tick();
542
- expect(diagnostics.at(-1)).toMatchObject({ attempt: 1, coalescedRequests: 0 });
543
- supervisor.dispose();
544
- });
545
-
546
- it("installs terminal authority first and never publishes a dirty initial read", async () => {
547
- const socket = new FakeSocket();
548
- const first = deferred<ReturnType<typeof resource>>();
549
- const second = deferred<ReturnType<typeof resource>>();
550
- const reads = [first, second];
551
- const fetch = vi.fn(() => reads.shift()!.promise);
552
- const diagnostics: string[] = [];
553
- const supervisor = createWorkspaceEventSupervisor({
554
- socket,
555
- daemon,
556
- workspaceName: "alpha",
557
- sessionName: "tmux-alpha",
558
- fetchTerminalRuntimeInventory: fetch,
559
- onDiagnostic: (phase) => {
560
- diagnostics.push(`${phase}:${socket.sent.length}`);
561
- throw new Error("diagnostic sink failed");
562
- },
563
- });
564
- const preparing = supervisor.prepareTerminalRuntimeInventory(new AbortController().signal);
565
- socket.emit("open");
566
- socket.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
567
- await tick();
568
- expect(socket.sent[0]).toMatchObject({
569
- type: "subscribe",
570
- interests: [{ resource: "terminal-runtime-inventory", workspaceName: "alpha" }],
571
- interestRevision: 1,
572
- });
573
- socket.frame({
574
- type: "resource.interests-ack",
575
- interestRevision: 1,
576
- sequence: 0,
577
- unavailableInterests: [],
578
- });
579
- await tick();
580
- expect(diagnostics).toEqual([
581
- "terminal-event-socket-open:0",
582
- "terminal-event-hello:0",
583
- "terminal-interest-send:1",
584
- "terminal-interest-ack:1",
585
- ]);
586
- expect(fetch).toHaveBeenCalledTimes(1);
587
- socket.frame({
588
- type: "resource.changed",
589
- sequence: 1,
590
- workspaceName: "alpha",
591
- resource: "terminal-runtime-inventory",
592
- revision: 1,
593
- causeOperationId: null,
594
- });
595
- first.resolve(resource(1));
596
- await tick();
597
- expect(fetch).toHaveBeenCalledTimes(2);
598
- second.resolve(resource(1));
599
- const prepared = await preparing;
600
- const observed = vi.fn();
601
- expect(supervisor.adoptTerminalRuntimeInventory(prepared, observed)).toEqual(resource(1));
602
- expect(observed).not.toHaveBeenCalled();
603
- supervisor.dispose();
604
- });
605
-
606
- it("uses an independent application-shell ACK on the same parser/socket", async () => {
607
- const socket = new FakeSocket();
608
- const supervisor = createWorkspaceEventSupervisor({
609
- socket,
610
- daemon,
611
- workspaceName: "alpha",
612
- sessionName: "tmux-alpha",
613
- fetchTerminalRuntimeInventory: async () => resource(0),
614
- });
615
- const preparing = supervisor.prepareTerminalRuntimeInventory(new AbortController().signal);
616
- socket.emit("open");
617
- socket.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
618
- await tick();
619
- socket.frame({
620
- type: "resource.interests-ack",
621
- interestRevision: 1,
622
- sequence: 0,
623
- unavailableInterests: [],
624
- });
625
- const prepared = await preparing;
626
- expect(supervisor.adoptTerminalRuntimeInventory(prepared, vi.fn())).not.toBeNull();
627
- const verified = vi.fn();
628
- supervisor.connectApplicationShell({
629
- onVerifiedOpen: verified,
630
- onInvalidate: vi.fn(),
631
- onMalformedFrame: vi.fn(),
632
- onPeerMismatch: vi.fn(),
633
- onProtocolError: vi.fn(),
634
- onClose: vi.fn(),
635
- onError: vi.fn(),
636
- });
637
- await tick();
638
- expect(verified).not.toHaveBeenCalled();
639
- expect(socket.sent[1]).toMatchObject({
640
- type: "subscribe",
641
- interests: [{ resource: "terminal-runtime-inventory" }, { resource: "application-shell" }],
642
- interestRevision: 2,
643
- });
644
- socket.frame({
645
- type: "resource.interests-ack",
646
- interestRevision: 2,
647
- sequence: 0,
648
- unavailableInterests: [],
649
- });
650
- await tick();
651
- expect(verified).toHaveBeenCalledTimes(1);
652
- supervisor.dispose();
653
- expect(socket.close).toHaveBeenCalledTimes(1);
654
- });
655
-
656
- it("acknowledges one exact application-shell mutation before invalidation", async () => {
657
- const socket = new FakeSocket();
658
- const supervisor = createWorkspaceEventSupervisor({
659
- socket,
660
- daemon,
661
- workspaceName: "alpha",
662
- sessionName: "tmux-alpha",
663
- fetchTerminalRuntimeInventory: async () => resource(0),
664
- });
665
- const preparing = supervisor.prepareTerminalRuntimeInventory(new AbortController().signal);
666
- socket.emit("open");
667
- socket.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
668
- await tick();
669
- socket.frame({
670
- type: "resource.interests-ack",
671
- interestRevision: 1,
672
- sequence: 0,
673
- unavailableInterests: [],
674
- });
675
- const prepared = await preparing;
676
- supervisor.adoptTerminalRuntimeInventory(prepared, vi.fn());
677
- const ordered: string[] = [];
678
- const acknowledgements: unknown[] = [];
679
- const retiredAcknowledgement = vi.fn((acknowledgement: unknown) => {
680
- acknowledgements.push(acknowledgement);
681
- ordered.push("ack");
682
- });
683
- const retiredInvalidation = vi.fn(() => ordered.push("invalidate"));
684
- const connection = supervisor.connectApplicationShell({
685
- onVerifiedOpen: vi.fn(),
686
- onInvalidate: retiredInvalidation,
687
- onOperationAcknowledged: retiredAcknowledgement,
688
- onMalformedFrame: vi.fn(),
689
- onPeerMismatch: vi.fn(),
690
- onProtocolError: vi.fn(),
691
- onClose: vi.fn(),
692
- onError: vi.fn(),
693
- });
694
- await tick();
695
- socket.frame({
696
- type: "resource.interests-ack",
697
- interestRevision: 2,
698
- sequence: 0,
699
- unavailableInterests: [],
700
- });
701
- await tick();
702
- const operationId = "17000000-0000-4000-8000-000000000017";
703
- const changed = {
704
- type: "resource.changed" as const,
705
- sequence: 1,
706
- workspaceName: "alpha",
707
- resource: "application-shell" as const,
708
- revision: 8,
709
- causeOperationId: operationId,
710
- };
711
- socket.frame(changed);
712
- expect(ordered).toEqual(["ack", "invalidate"]);
713
- expect(acknowledgements).toEqual([
714
- {
715
- daemonInstanceId: daemon.instanceId,
716
- operationId,
717
- sequence: 1,
718
- revision: 8,
719
- },
720
- ]);
721
-
722
- socket.frame(changed);
723
- socket.frame({ ...changed, sequence: 0 });
724
- expect(ordered).toEqual(["ack", "invalidate"]);
725
- socket.frame({ ...changed, sequence: 3, revision: 9 });
726
- expect(ordered).toEqual(["ack", "invalidate", "invalidate"]);
727
- expect(retiredAcknowledgement).toHaveBeenCalledTimes(1);
728
- socket.frame({ ...changed, sequence: 4, workspaceName: "beta", revision: 10 });
729
- expect(retiredAcknowledgement).toHaveBeenCalledTimes(1);
730
-
731
- connection.close();
732
- const replacementAcknowledgement = vi.fn();
733
- const replacementInvalidation = vi.fn();
734
- const replacementConnection = supervisor.connectApplicationShell({
735
- onVerifiedOpen: vi.fn(),
736
- onInvalidate: replacementInvalidation,
737
- onOperationAcknowledged: replacementAcknowledgement,
738
- onMalformedFrame: vi.fn(),
739
- onPeerMismatch: vi.fn(),
740
- onProtocolError: vi.fn(),
741
- onClose: vi.fn(),
742
- onError: vi.fn(),
743
- });
744
- await tick();
745
- socket.frame({ ...changed, sequence: 5, revision: 11 });
746
- expect(retiredAcknowledgement).toHaveBeenCalledTimes(1);
747
- expect(retiredInvalidation).toHaveBeenCalledTimes(2);
748
- expect(replacementAcknowledgement).toHaveBeenCalledTimes(1);
749
- expect(replacementInvalidation).toHaveBeenCalledTimes(1);
750
-
751
- replacementConnection.close();
752
- const reentrantAcknowledgement = vi.fn();
753
- const reentrantInvalidation = vi.fn();
754
- const successorAcknowledgement = vi.fn();
755
- const successorInvalidation = vi.fn();
756
- let reentrantConnection!: ReturnType<typeof supervisor.connectApplicationShell>;
757
- reentrantConnection = supervisor.connectApplicationShell({
758
- onVerifiedOpen: vi.fn(),
759
- onInvalidate: reentrantInvalidation,
760
- onOperationAcknowledged: (acknowledgement) => {
761
- reentrantAcknowledgement(acknowledgement);
762
- reentrantConnection.close();
763
- supervisor.connectApplicationShell({
764
- onVerifiedOpen: vi.fn(),
765
- onInvalidate: successorInvalidation,
766
- onOperationAcknowledged: successorAcknowledgement,
767
- onMalformedFrame: vi.fn(),
768
- onPeerMismatch: vi.fn(),
769
- onProtocolError: vi.fn(),
770
- onClose: vi.fn(),
771
- onError: vi.fn(),
772
- });
773
- },
774
- onMalformedFrame: vi.fn(),
775
- onPeerMismatch: vi.fn(),
776
- onProtocolError: vi.fn(),
777
- onClose: vi.fn(),
778
- onError: vi.fn(),
779
- });
780
- await tick();
781
- socket.frame({ ...changed, sequence: 6, revision: 12 });
782
- expect(reentrantAcknowledgement).toHaveBeenCalledTimes(1);
783
- expect(reentrantInvalidation).not.toHaveBeenCalled();
784
- expect(successorInvalidation).not.toHaveBeenCalled();
785
- socket.frame({ ...changed, sequence: 7, revision: 13 });
786
- expect(successorAcknowledgement).toHaveBeenCalledTimes(1);
787
- expect(successorInvalidation).toHaveBeenCalledTimes(1);
788
- supervisor.dispose();
789
- });
790
-
791
- it("serializes the full terminal, shell, and catalog interest union on one socket", async () => {
792
- const socket = new FakeSocket();
793
- const supervisor = createWorkspaceEventSupervisor({
794
- socket,
795
- daemon,
796
- workspaceName: "alpha",
797
- sessionName: "tmux-alpha",
798
- fetchTerminalRuntimeInventory: async () => resource(0),
799
- });
800
- const catalogInvalidated = vi.fn();
801
- const catalog = supervisor.connectWorkspaceCatalog(catalogInvalidated);
802
- const shellVerified = vi.fn();
803
- supervisor.connectApplicationShell({
804
- onVerifiedOpen: shellVerified,
805
- onInvalidate: vi.fn(),
806
- onMalformedFrame: vi.fn(),
807
- onPeerMismatch: vi.fn(),
808
- onProtocolError: vi.fn(),
809
- onClose: vi.fn(),
810
- onError: vi.fn(),
811
- });
812
- socket.emit("open");
813
- socket.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
814
- await tick();
815
- expect(socket.sent).toHaveLength(1);
816
- expect(socket.sent[0]).toMatchObject({
817
- interests: [{ resource: "terminal-runtime-inventory", workspaceName: "alpha" }],
818
- interestRevision: 1,
819
- });
820
- socket.frame({
821
- type: "resource.interests-ack",
822
- interestRevision: 1,
823
- sequence: 0,
824
- unavailableInterests: [],
825
- });
826
- await tick();
827
- expect(socket.sent).toHaveLength(2);
828
- expect(socket.sent[1]).toMatchObject({
829
- interests: [
830
- { resource: "terminal-runtime-inventory", workspaceName: "alpha" },
831
- { resource: "application-shell", workspaceName: "alpha" },
832
- { resource: "workspace-catalog", workspaceName: null },
833
- ],
834
- interestRevision: 2,
835
- });
836
- socket.frame({
837
- type: "resource.interests-ack",
838
- interestRevision: 2,
839
- sequence: 0,
840
- unavailableInterests: [],
841
- });
842
- await catalog.ready;
843
- await tick();
844
- expect(shellVerified).toHaveBeenCalledTimes(1);
845
-
846
- socket.frame({
847
- type: "resource.changed",
848
- sequence: 1,
849
- workspaceName: null,
850
- resource: "workspace-catalog",
851
- revision: 1,
852
- causeOperationId: null,
853
- });
854
- expect(catalogInvalidated).toHaveBeenCalledTimes(1);
855
- socket.frame({
856
- type: "snapshot-required",
857
- afterSequence: 1,
858
- oldestAvailableSequence: 2,
859
- currentSequence: 2,
860
- reason: "journal-gap",
861
- });
862
- expect(catalogInvalidated).toHaveBeenCalledTimes(2);
863
- socket.frame({
864
- type: "resource.changed",
865
- sequence: 4,
866
- workspaceName: "alpha",
867
- resource: "terminal-runtime-inventory",
868
- revision: 4,
869
- causeOperationId: null,
870
- });
871
- expect(catalogInvalidated).toHaveBeenCalledTimes(3);
872
- catalog.close();
873
- socket.frame({
874
- type: "resource.changed",
875
- sequence: 5,
876
- workspaceName: null,
877
- resource: "workspace-catalog",
878
- revision: 5,
879
- causeOperationId: null,
880
- });
881
- expect(catalogInvalidated).toHaveBeenCalledTimes(3);
882
- supervisor.dispose();
883
- });
884
-
885
- it("reduces an active semantic supervisor to catalog-only authority", async () => {
886
- const socket = new FakeSocket();
887
- const supervisor = createWorkspaceEventSupervisor({
888
- socket,
889
- daemon,
890
- workspaceName: "alpha",
891
- sessionName: "tmux-alpha",
892
- fetchTerminalRuntimeInventory: async () => resource(0),
893
- });
894
- const invalidated = vi.fn();
895
- const catalog = supervisor.connectWorkspaceCatalog(invalidated);
896
- supervisor.connectApplicationShell({
897
- onVerifiedOpen: vi.fn(),
898
- onInvalidate: vi.fn(),
899
- onMalformedFrame: vi.fn(),
900
- onPeerMismatch: vi.fn(),
901
- onProtocolError: vi.fn(),
902
- onClose: vi.fn(),
903
- onError: vi.fn(),
904
- });
905
- socket.emit("open");
906
- socket.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
907
- await tick();
908
- socket.frame({
909
- type: "resource.interests-ack",
910
- interestRevision: 1,
911
- sequence: 0,
912
- unavailableInterests: [],
913
- });
914
- await tick();
915
- socket.frame({
916
- type: "resource.interests-ack",
917
- interestRevision: 2,
918
- sequence: 0,
919
- unavailableInterests: [],
920
- });
921
- await catalog.ready;
922
-
923
- const selected = supervisor.selectWorkspaceCatalogOnly();
924
- await tick();
925
- expect(socket.sent[2]).toMatchObject({
926
- type: "subscribe",
927
- interestRevision: 3,
928
- interests: [{ resource: "workspace-catalog", workspaceName: null }],
929
- });
930
- socket.frame({
931
- type: "resource.interests-ack",
932
- interestRevision: 3,
933
- sequence: 0,
934
- unavailableInterests: [],
935
- });
936
- await selected;
937
- socket.frame({
938
- type: "resource.changed",
939
- sequence: 1,
940
- workspaceName: null,
941
- resource: "workspace-catalog",
942
- revision: 1,
943
- causeOperationId: null,
944
- });
945
- expect(invalidated).toHaveBeenCalledOnce();
946
- supervisor.dispose();
947
- });
948
-
949
- it("fences a pending terminal install before settling catalog-only fallback", async () => {
950
- const socket = new FakeSocket();
951
- const supervisor = createWorkspaceEventSupervisor({
952
- socket,
953
- daemon,
954
- workspaceName: "alpha",
955
- sessionName: "tmux-alpha",
956
- fetchTerminalRuntimeInventory: async () => resource(0),
957
- });
958
- const catalog = supervisor.connectWorkspaceCatalog(vi.fn());
959
- socket.emit("open");
960
- socket.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
961
- await tick();
962
- expect(socket.sent).toHaveLength(1);
963
- expect(socket.sent[0]).toMatchObject({
964
- interestRevision: 1,
965
- interests: [{ resource: "terminal-runtime-inventory" }],
966
- });
967
- const selected = supervisor.selectWorkspaceCatalogOnly();
968
- await tick();
969
- expect(socket.sent).toHaveLength(1);
970
- socket.frame({
971
- type: "resource.interests-ack",
972
- interestRevision: 1,
973
- sequence: 0,
974
- unavailableInterests: [],
975
- });
976
- await tick();
977
- expect(socket.sent).toHaveLength(2);
978
- expect(socket.sent[1]).toMatchObject({
979
- interestRevision: 2,
980
- interests: [{ resource: "workspace-catalog", workspaceName: null }],
981
- });
982
- socket.frame({
983
- type: "resource.interests-ack",
984
- interestRevision: 2,
985
- sequence: 0,
986
- unavailableInterests: [],
987
- });
988
- await Promise.all([catalog.ready, selected]);
989
- expect(socket.sent).toHaveLength(2);
990
- supervisor.dispose();
991
- });
992
-
993
- it("rejects adoption when topology changes after clean completion", async () => {
994
- const socket = new FakeSocket();
995
- const supervisor = createWorkspaceEventSupervisor({
996
- socket,
997
- daemon,
998
- workspaceName: "alpha",
999
- sessionName: "tmux-alpha",
1000
- fetchTerminalRuntimeInventory: async () => resource(0),
1001
- });
1002
- const preparing = supervisor.prepareTerminalRuntimeInventory(new AbortController().signal);
1003
- socket.emit("open");
1004
- socket.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
1005
- await tick();
1006
- socket.frame({
1007
- type: "resource.interests-ack",
1008
- interestRevision: 1,
1009
- sequence: 0,
1010
- unavailableInterests: [],
1011
- });
1012
- const prepared = await preparing;
1013
- socket.frame({
1014
- type: "resource.changed",
1015
- sequence: 1,
1016
- workspaceName: "alpha",
1017
- resource: "terminal-runtime-inventory",
1018
- revision: 1,
1019
- causeOperationId: null,
1020
- });
1021
- expect(supervisor.adoptTerminalRuntimeInventory(prepared, vi.fn())).toBeNull();
1022
- supervisor.dispose();
1023
- });
1024
-
1025
- it("publishes nothing when both bounded initial attempts become dirty", async () => {
1026
- const socket = new FakeSocket();
1027
- const first = deferred<ReturnType<typeof resource>>();
1028
- const second = deferred<ReturnType<typeof resource>>();
1029
- const reads = [first, second];
1030
- const supervisor = createWorkspaceEventSupervisor({
1031
- socket,
1032
- daemon,
1033
- workspaceName: "alpha",
1034
- sessionName: "tmux-alpha",
1035
- fetchTerminalRuntimeInventory: () => reads.shift()!.promise,
1036
- });
1037
- const preparing = supervisor.prepareTerminalRuntimeInventory(new AbortController().signal);
1038
- socket.emit("open");
1039
- socket.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
1040
- await tick();
1041
- socket.frame({
1042
- type: "resource.interests-ack",
1043
- interestRevision: 1,
1044
- sequence: 0,
1045
- unavailableInterests: [],
1046
- });
1047
- await tick();
1048
- socket.frame({
1049
- type: "resource.changed",
1050
- sequence: 1,
1051
- workspaceName: "alpha",
1052
- resource: "terminal-runtime-inventory",
1053
- revision: 1,
1054
- causeOperationId: null,
1055
- });
1056
- first.resolve(resource(1));
1057
- await tick();
1058
- socket.frame({
1059
- type: "resource.changed",
1060
- sequence: 2,
1061
- workspaceName: "alpha",
1062
- resource: "terminal-runtime-inventory",
1063
- revision: 2,
1064
- causeOperationId: null,
1065
- });
1066
- second.resolve(resource(2));
1067
- await expect(preparing).rejects.toThrow("changed during synchronization");
1068
- supervisor.dispose();
1069
- });
1070
-
1071
- it("closes fail-closed on a malformed frame instead of retaining stale authority", async () => {
1072
- const socket = new FakeSocket();
1073
- const supervisor = createWorkspaceEventSupervisor({
1074
- socket,
1075
- daemon,
1076
- workspaceName: "alpha",
1077
- sessionName: "tmux-alpha",
1078
- fetchTerminalRuntimeInventory: async () => resource(0),
1079
- });
1080
- socket.emit("open");
1081
- socket.frame({ type: "hello", daemon, sessions: [], eventSequence: 0 });
1082
- socket.emit("message", { data: "not-json" });
1083
- expect(socket.close).toHaveBeenCalledWith(1008, "Invalid daemon event protocol");
1084
- supervisor.dispose();
1085
- });
1086
- });