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,400 +0,0 @@
1
- /**
2
- * Private fleet fixture for the installed-artifact recovery and authority
3
- * live tests. One fixture owns one private tmux server, one private state
4
- * home and every CLI child it spawns, and retires all of them on `cleanup`.
5
- *
6
- * Socket unification: the CLI's plain `tmux` paths (adopt, restore, polling
7
- * wait) resolve `$TMUX_TMPDIR/tmux-<uid>/default`, while the daemon pins its
8
- * server by `TMUX_IDE_TMUX_SOCKET_PATH` and strips `TMUX_TMPDIR` from its
9
- * pinned client. Pointing the pinned path at exactly the resolved default
10
- * path makes both paths reach the same private server, which the fixture
11
- * proves with `assertUnifiedSocket` before any scenario runs.
12
- *
13
- * CLI under test: `TMUX_IDE_QUALIFY_CLI` (absolute path to an installed
14
- * `tmux-ide` executable or its `cli.js`) qualifies a packed installation;
15
- * otherwise the fixture bundles `bin/cli.ts` from this checkout.
16
- */
17
- import { build } from "esbuild";
18
- import { execFileSync, spawn, spawnSync } from "node:child_process";
19
- import { randomUUID } from "node:crypto";
20
- import { appendFileSync, chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync, } from "node:fs";
21
- import { userInfo } from "node:os";
22
- import { dirname, join } from "node:path";
23
- import { fileURLToPath } from "node:url";
24
- import WsWebSocket from "ws";
25
- export const repoRoot = fileURLToPath(new URL("../../../../../", import.meta.url));
26
- export const tmuxAvailable = spawnSync("tmux", ["-V"], { stdio: "ignore" }).status === 0;
27
- function evidencePath() {
28
- const dir = process.env.TMUX_IDE_STAGE6_EVIDENCE_DIR;
29
- if (!dir)
30
- return null;
31
- mkdirSync(dir, { recursive: true });
32
- return join(dir, "evidence.jsonl");
33
- }
34
- async function resolveCli(root) {
35
- const configured = process.env.TMUX_IDE_QUALIFY_CLI;
36
- if (configured) {
37
- const path = realpathSync(configured);
38
- return { path, label: `installed:${path}` };
39
- }
40
- const bundle = join(root, "cli.mjs");
41
- await build({
42
- entryPoints: [join(repoRoot, "bin/cli.ts")],
43
- outfile: bundle,
44
- bundle: true,
45
- platform: "node",
46
- format: "esm",
47
- target: "node20",
48
- logLevel: "silent",
49
- plugins: [
50
- {
51
- name: "external-dependencies",
52
- setup(builder) {
53
- builder.onResolve({ filter: /.*/ }, (args) => {
54
- if (args.kind === "entry-point" ||
55
- args.path.startsWith(".") ||
56
- args.path.startsWith("/") ||
57
- args.path.startsWith("@tmux-ide/") ||
58
- args.path === "@xterm/addon-unicode11")
59
- return;
60
- return { external: true };
61
- });
62
- },
63
- },
64
- ],
65
- });
66
- return { path: bundle, label: "checkout-bundle" };
67
- }
68
- export async function createPrivateFleet(prefix) {
69
- // A short root keeps the unix socket path well under the platform limit.
70
- const root = mkdtempSync(`/tmp/ti-${prefix}-`);
71
- const stateDir = join(root, "state");
72
- const home = join(root, "home");
73
- const tmuxTmpDir = join(root, "tmux");
74
- const socketDir = join(tmuxTmpDir, `tmux-${userInfo().uid}`);
75
- const socketPath = join(socketDir, "default");
76
- const binDir = join(root, "bin");
77
- for (const dir of [stateDir, home, tmuxTmpDir, socketDir, binDir])
78
- mkdirSync(dir, { mode: 0o700 });
79
- chmodSync(tmuxTmpDir, 0o700);
80
- chmodSync(socketDir, 0o700);
81
- if (!process.env.TMUX_IDE_QUALIFY_CLI) {
82
- // The checkout bundle resolves its externals from this repo's node_modules.
83
- execFileSync("ln", ["-s", join(repoRoot, "node_modules"), join(root, "node_modules")]);
84
- }
85
- const cliResolved = await resolveCli(root);
86
- const tmuxPath = realpathSync(execFileSync("/bin/sh", ["-c", "command -v tmux"], { encoding: "utf8" }).trim());
87
- // The chrome updater session runs `exec tmux-ide chrome-updater` from the
88
- // server's PATH; this shim routes it to the CLI under test.
89
- writeFileSync(join(binDir, "tmux-ide"), `#!/bin/sh\nexec ${JSON.stringify(process.execPath)} ${JSON.stringify(cliResolved.path)} "$@"\n`, { mode: 0o755 });
90
- const env = {
91
- ...Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith("TMUX_IDE_") &&
92
- key !== "TMUX" &&
93
- key !== "TMUX_PANE" &&
94
- key !== "TMUX_TMPDIR")),
95
- PATH: `${binDir}:${dirname(process.execPath)}:${process.env.PATH ?? ""}`,
96
- HOME: home,
97
- TMUX: "",
98
- TMUX_TMPDIR: tmuxTmpDir,
99
- TMUX_IDE_HOME: stateDir,
100
- TMUX_IDE_DAEMON_INFO_DIR: stateDir,
101
- TMUX_IDE_REGISTRY_DIR: stateDir,
102
- TMUX_IDE_SETTINGS_DIR: stateDir,
103
- TMUX_IDE_CONFIG: join(stateDir, "config.json"),
104
- TMUX_IDE_TMUX_BIN: tmuxPath,
105
- TMUX_IDE_TMUX_SOCKET_PATH: socketPath,
106
- NO_COLOR: "1",
107
- };
108
- const infoPath = join(stateDir, "daemon.json");
109
- const children = new Set();
110
- const exits = new Map();
111
- const sockets = new Set();
112
- const tmux = (...args) => execFileSync(tmuxPath, ["-S", socketPath, ...args], {
113
- env,
114
- encoding: "utf8",
115
- stdio: ["ignore", "pipe", "pipe"],
116
- }).trimEnd();
117
- const tmuxStatus = (...args) => spawnSync(tmuxPath, ["-S", socketPath, ...args], { env, stdio: "ignore" }).status ?? -1;
118
- const cli = (args) => {
119
- const child = spawn(process.execPath, [cliResolved.path, ...args], {
120
- cwd: root,
121
- env,
122
- stdio: ["ignore", "pipe", "pipe"],
123
- });
124
- children.add(child);
125
- let stdout = "";
126
- let stderr = "";
127
- child.stdout.on("data", (chunk) => {
128
- stdout = (stdout + chunk).slice(-64000);
129
- });
130
- child.stderr.on("data", (chunk) => {
131
- stderr = (stderr + chunk).slice(-64000);
132
- });
133
- exits.set(child, new Promise((resolve, reject) => {
134
- child.once("error", reject);
135
- child.once("close", (code, signal) => resolve({ code, signal, stdout, stderr }));
136
- }));
137
- return child;
138
- };
139
- const exit = (child) => exits.get(child);
140
- const bounded = async (promise, label, ms = 20000) => {
141
- let timer;
142
- try {
143
- return await Promise.race([
144
- promise,
145
- new Promise((_resolve, reject) => {
146
- timer = setTimeout(() => reject(new Error(`${label} exceeded ${ms}ms`)), ms);
147
- }),
148
- ]);
149
- }
150
- finally {
151
- clearTimeout(timer);
152
- }
153
- };
154
- const until = async (read, label, ms = 15000) => {
155
- const deadline = Date.now() + ms;
156
- while (Date.now() < deadline) {
157
- const value = read();
158
- if (value !== null)
159
- return value;
160
- await new Promise((resolve) => setTimeout(resolve, 25));
161
- }
162
- throw new Error(`Timed out after ${ms}ms: ${label}`);
163
- };
164
- const info = () => {
165
- try {
166
- return JSON.parse(readFileSync(infoPath, "utf8"));
167
- }
168
- catch {
169
- return null;
170
- }
171
- };
172
- const startDaemon = async () => {
173
- const child = cli(["--headless", "--json"]);
174
- const published = await Promise.race([
175
- until(() => {
176
- const value = info();
177
- return value?.pid === child.pid ? value : null;
178
- }, "daemon record publication"),
179
- exit(child).then((result) => {
180
- throw new Error(`Daemon exited before publication: ${result.stdout} ${result.stderr}`);
181
- }),
182
- ]);
183
- // The record publishes before the HTTP accept loop is guaranteed up.
184
- await until(() => {
185
- const probe = spawnSync("curl", [
186
- "-s",
187
- "-o",
188
- "/dev/null",
189
- "-w",
190
- "%{http_code}",
191
- `http://127.0.0.1:${published.port}/health`,
192
- ], { encoding: "utf8" });
193
- return probe.stdout === "200" ? true : null;
194
- }, "daemon health");
195
- return { child, info: published };
196
- };
197
- const headers = (info) => info.authToken ? { authorization: `Bearer ${info.authToken}` } : {};
198
- const fetchJson = async (info, path, init = {}) => {
199
- const response = await fetch(`http://127.0.0.1:${info.port}${path}`, {
200
- ...init,
201
- headers: { ...headers(info), "content-type": "application/json", ...(init.headers ?? {}) },
202
- signal: AbortSignal.timeout(5000),
203
- });
204
- return response.json();
205
- };
206
- const eventsClient = (info) => {
207
- const socket = new WsWebSocket(`ws://127.0.0.1:${info.port}/ws/events`, {
208
- headers: headers(info),
209
- });
210
- sockets.add(socket);
211
- const frames = [];
212
- let resolveReady;
213
- let rejectReady;
214
- const ready = new Promise((resolve, reject) => {
215
- resolveReady = resolve;
216
- rejectReady = reject;
217
- });
218
- ready.catch(() => undefined);
219
- const closed = new Promise((resolve) => {
220
- socket.once("close", (code) => {
221
- sockets.delete(socket);
222
- rejectReady(new Error(`events socket closed (${code}) before ready`));
223
- resolve(code);
224
- });
225
- });
226
- socket.on("error", (error) => rejectReady(error));
227
- socket.on("message", (data) => {
228
- let frame;
229
- try {
230
- frame = JSON.parse(String(data));
231
- }
232
- catch {
233
- return;
234
- }
235
- frames.push(frame);
236
- if (frame.type === "hello") {
237
- socket.send(JSON.stringify({
238
- type: "subscribe",
239
- sessions: [],
240
- legacyEvents: true,
241
- interests: [{ resource: "fleet-catalog", workspaceName: null }],
242
- interestRevision: 1,
243
- }));
244
- }
245
- else if (frame.type === "resource.interests-ack") {
246
- resolveReady();
247
- }
248
- });
249
- return {
250
- frames,
251
- ready,
252
- closed,
253
- close: () => {
254
- try {
255
- socket.close();
256
- }
257
- catch {
258
- // already gone
259
- }
260
- },
261
- };
262
- };
263
- const logBackfill = async (info) => {
264
- const controller = new AbortController();
265
- const timer = setTimeout(() => controller.abort(), 4000);
266
- const entries = [];
267
- try {
268
- const response = await fetch(`http://127.0.0.1:${info.port}/api/logs/daemon`, {
269
- headers: headers(info),
270
- signal: controller.signal,
271
- });
272
- const reader = response.body.getReader();
273
- let buffer = "";
274
- let sawBookmark = false;
275
- while (!sawBookmark) {
276
- const { value, done } = await reader.read();
277
- if (done)
278
- break;
279
- buffer += new TextDecoder().decode(value);
280
- let index = buffer.indexOf("\n\n");
281
- while (index !== -1) {
282
- const block = buffer.slice(0, index);
283
- buffer = buffer.slice(index + 2);
284
- const event = /^event: (.*)$/mu.exec(block)?.[1];
285
- const data = block
286
- .split("\n")
287
- .filter((line) => line.startsWith("data:"))
288
- .map((line) => line.slice(5).trimStart())
289
- .join("\n");
290
- if (event === "bookmark")
291
- sawBookmark = true;
292
- else if (data) {
293
- try {
294
- const parsed = JSON.parse(data);
295
- if (Array.isArray(parsed))
296
- entries.push(...parsed);
297
- else
298
- entries.push(parsed);
299
- }
300
- catch {
301
- entries.push({ raw: data, event });
302
- }
303
- }
304
- index = buffer.indexOf("\n\n");
305
- }
306
- }
307
- controller.abort();
308
- }
309
- catch {
310
- // aborted after the bookmark or on the deadline; return what arrived
311
- }
312
- finally {
313
- clearTimeout(timer);
314
- }
315
- return entries;
316
- };
317
- const stampAgent = (paneId, state) => {
318
- tmux("set-option", "-p", "-t", paneId, "@agent_state", `${state}:${Math.floor(Date.now() / 1000)}`);
319
- };
320
- const evidenceFile = evidencePath();
321
- const evidence = (record) => {
322
- if (!evidenceFile)
323
- return;
324
- appendFileSync(evidenceFile, JSON.stringify({ at: new Date().toISOString(), cli: cliResolved.label, ...record }) + "\n");
325
- };
326
- const cleanup = async () => {
327
- for (const socket of sockets) {
328
- try {
329
- socket.terminate();
330
- }
331
- catch {
332
- // already closed
333
- }
334
- }
335
- const current = info();
336
- if (current) {
337
- await fetch(`http://127.0.0.1:${current.port}/api/v2/action/daemon.shutdown`, {
338
- method: "POST",
339
- headers: { ...headers(current), "content-type": "application/json" },
340
- body: JSON.stringify({
341
- reason: "live fixture cleanup",
342
- expectedInstanceId: current.instanceId,
343
- }),
344
- signal: AbortSignal.timeout(5000),
345
- }).catch(() => undefined);
346
- }
347
- for (const child of children)
348
- if (child.exitCode === null && child.signalCode === null)
349
- child.kill("SIGTERM");
350
- await Promise.all([...children].map(async (child) => {
351
- try {
352
- await bounded(exit(child), "fixture child exit", 10000);
353
- }
354
- catch {
355
- child.kill("SIGKILL");
356
- await exit(child).catch(() => undefined);
357
- }
358
- }));
359
- spawnSync(tmuxPath, ["-S", socketPath, "kill-server"], { env, stdio: "ignore" });
360
- rmSync(root, { recursive: true, force: true });
361
- if (existsSync(root))
362
- throw new Error(`fixture root survived cleanup: ${root}`);
363
- };
364
- return {
365
- root,
366
- stateDir,
367
- socketPath,
368
- cliPath: cliResolved.path,
369
- cliLabel: cliResolved.label,
370
- env,
371
- tmux,
372
- tmuxStatus,
373
- cli,
374
- exit,
375
- bounded,
376
- until,
377
- info,
378
- startDaemon,
379
- eventsClient,
380
- fetchJson,
381
- logBackfill,
382
- stampAgent,
383
- evidence,
384
- cleanup,
385
- };
386
- }
387
- /**
388
- * Prove the plain-`tmux` CLI path and the daemon's pinned socket path name
389
- * the same server. Requires a running server on the fixture socket.
390
- */
391
- export function assertUnifiedSocket(fleet) {
392
- const plain = execFileSync("tmux", ["display-message", "-p", "#{socket_path}"], {
393
- env: fleet.env,
394
- encoding: "utf8",
395
- }).trim();
396
- const pinned = fleet.tmux("display-message", "-p", "#{socket_path}");
397
- if (plain !== fleet.socketPath || pinned !== fleet.socketPath)
398
- throw new Error(`socket split: plain=${plain} pinned=${pinned} expected=${fleet.socketPath}`);
399
- }
400
- export const uniqueName = (prefix) => `${prefix}-${randomUUID().slice(0, 8)}`;
@@ -1,182 +0,0 @@
1
- /**
2
- * MockPtyAdapter — scripted PTY backend for tests (T087).
3
- *
4
- * Mirrors the `PtyAdapter` contract but spawns no real processes. Every
5
- * call to {@link MockPtyAdapter.spawn} or {@link MockPtyAdapter.spawnSync}
6
- * returns a fresh {@link MockPtyProcess} the test then drives manually via
7
- * {@link MockPtyProcess.pushOutput} / {@link MockPtyProcess.emitExit}.
8
- *
9
- * Drop-in for any layer that takes a `PtyAdapter`. The adapter also keeps
10
- * a `spawnLog` of every `PtySpawnInput` so tests can assert wiring without
11
- * inspecting node-pty internals.
12
- *
13
- * Why this lives under `__tests__/`: it is purely test infrastructure and
14
- * MUST NOT be bundled into the production daemon. Vitest's `include`
15
- * pulls it in implicitly when sibling tests import it.
16
- */
17
- import { PtySpawnError, } from "../PtyAdapter.js";
18
- import { DEFAULT_PTY_INPUT_LIMITS, MonotonicPtyInput } from "../MonotonicPtyInput.js";
19
- export class MockPtyProcess {
20
- pid;
21
- /** Append-only log of every `write(...)` call so tests can assert input. */
22
- writeLog = [];
23
- /** Frames accepted through the fail-closed monotonic capability. */
24
- boundedWriteLog = [];
25
- /** Append-only log of every `resize(...)` call. */
26
- resizeLog = [];
27
- /** Records the last signal observed by `kill(...)`. */
28
- killed = null;
29
- exited = false;
30
- dataListeners = new Set();
31
- exitListeners = new Set();
32
- _input;
33
- boundedInput;
34
- constructor(pid, input, listeners = {}, inputLimits = DEFAULT_PTY_INPUT_LIMITS) {
35
- this.pid = pid;
36
- this._input = input;
37
- this.boundedInput = new MonotonicPtyInput(inputLimits, (frame) => {
38
- this.boundedWriteLog.push(Buffer.from(frame));
39
- });
40
- if (listeners.onData)
41
- this.dataListeners.add(listeners.onData);
42
- if (listeners.onExit)
43
- this.exitListeners.add(listeners.onExit);
44
- }
45
- /** Original spawn input — handy for assertions. */
46
- get spawnInput() {
47
- return this._input;
48
- }
49
- write(data) {
50
- if (this.exited)
51
- return;
52
- this.writeLog.push(data);
53
- }
54
- resize(cols, rows) {
55
- if (this.exited)
56
- return;
57
- this.resizeLog.push({ cols, rows });
58
- }
59
- paused = false;
60
- pause() {
61
- if (!this.exited)
62
- this.paused = true;
63
- }
64
- resume() {
65
- if (!this.exited)
66
- this.paused = false;
67
- }
68
- kill(signal) {
69
- if (this.exited)
70
- return;
71
- this.killed = signal ?? "SIGTERM";
72
- this.boundedInput.close();
73
- // Synthesize an exit so subscribers detach cleanly — matches the
74
- // node-pty fallback inside NodePtyAdapter.
75
- this.emitExit({ exitCode: 0, signal: typeof signal === "number" ? signal : null });
76
- }
77
- onData(callback) {
78
- this.dataListeners.add(callback);
79
- return () => {
80
- this.dataListeners.delete(callback);
81
- };
82
- }
83
- onExit(callback) {
84
- if (this.exited)
85
- return () => undefined;
86
- this.exitListeners.add(callback);
87
- return () => {
88
- this.exitListeners.delete(callback);
89
- };
90
- }
91
- // ------------------------------------------------------------------
92
- // Test-only drivers — never called by production code.
93
- // ------------------------------------------------------------------
94
- /** Push synthetic PTY output bytes to every `onData` subscriber. */
95
- pushOutput(data) {
96
- if (this.exited)
97
- return;
98
- const buf = data instanceof Buffer
99
- ? data
100
- : typeof data === "string"
101
- ? Buffer.from(data, "utf8")
102
- : Buffer.from(data);
103
- for (const listener of this.dataListeners)
104
- listener(buf);
105
- }
106
- /** Fire the terminal exit event (idempotent — subsequent calls are no-ops). */
107
- emitExit(event) {
108
- if (this.exited)
109
- return;
110
- this.exited = true;
111
- this.boundedInput.close();
112
- const listeners = [...this.exitListeners];
113
- this.dataListeners.clear();
114
- this.exitListeners.clear();
115
- for (const listener of listeners)
116
- listener(event);
117
- }
118
- /** Number of currently attached data listeners — handy for leak assertions. */
119
- get dataListenerCount() {
120
- return this.dataListeners.size;
121
- }
122
- /** Number of currently attached exit listeners. */
123
- get exitListenerCount() {
124
- return this.exitListeners.size;
125
- }
126
- }
127
- export class MockPtyAdapter {
128
- id = "mock";
129
- spawnLog = [];
130
- spawned = [];
131
- nextPid;
132
- _failNext;
133
- syncUnsupported;
134
- boundedInputLimits;
135
- constructor(options = {}) {
136
- this.nextPid = options.startingPid ?? 100_000;
137
- this._failNext = options.failNext;
138
- this.syncUnsupported = options.syncUnsupported ?? false;
139
- this.boundedInputLimits = options.boundedInputLimits ?? DEFAULT_PTY_INPUT_LIMITS;
140
- }
141
- /** Arm the next spawn to throw a typed error. */
142
- failNext(code, message) {
143
- this._failNext = { code, message };
144
- }
145
- async spawn(input, listeners) {
146
- if (this._failNext) {
147
- const err = this._failNext;
148
- this._failNext = undefined;
149
- throw new PtySpawnError({ adapter: this.id, code: err.code, message: err.message });
150
- }
151
- this.spawnLog.push(input);
152
- const proc = new MockPtyProcess(this.nextPid++, input, listeners, this.boundedInputLimits);
153
- this.spawned.push(proc);
154
- return proc;
155
- }
156
- spawnSync(input, listeners) {
157
- if (this.syncUnsupported) {
158
- throw new PtySpawnError({
159
- adapter: this.id,
160
- code: "sync_unsupported",
161
- message: "MockPtyAdapter is configured with syncUnsupported=true",
162
- });
163
- }
164
- if (this._failNext) {
165
- const err = this._failNext;
166
- this._failNext = undefined;
167
- throw new PtySpawnError({ adapter: this.id, code: err.code, message: err.message });
168
- }
169
- this.spawnLog.push(input);
170
- const proc = new MockPtyProcess(this.nextPid++, input, listeners, this.boundedInputLimits);
171
- this.spawned.push(proc);
172
- return proc;
173
- }
174
- /** Read-only view of all spawns so tests can assert wiring. */
175
- get spawnCount() {
176
- return this.spawnLog.length;
177
- }
178
- /** The most recently spawned mock process, or `null` if there were none. */
179
- lastSpawned() {
180
- return this.spawned.at(-1) ?? null;
181
- }
182
- }
@@ -1,98 +0,0 @@
1
- import { FIXTURE, SimulatedChannel, fixtureAutoReply, fixtureState, } from "./simulated-channel.js";
2
- /**
3
- * Scripted wrapper around the real control protocol simulator. It owns the
4
- * asynchronous capture/cursor choreography and exposes only bounded settling;
5
- * a broken bootstrap fails immediately instead of leaking a Vitest worker.
6
- */
7
- export class ScriptedChannelDriver {
8
- channel;
9
- #seedLines;
10
- #cursorLine;
11
- #cursorLinesByPane;
12
- #maxTurns;
13
- #handledWrites = 0;
14
- deferredCommands = [];
15
- constructor(handlers, options = {}) {
16
- const state = options.state ?? fixtureState();
17
- this.#seedLines = options.seedLines ?? ["ready"];
18
- this.#cursorLine = options.cursorLine ?? "0 0 100 50";
19
- this.#cursorLinesByPane = options.cursorLinesByPane ?? {};
20
- this.#maxTurns = options.maxTurns ?? 100;
21
- const basic = fixtureAutoReply(state);
22
- this.channel = new SimulatedChannel(handlers, (command) => {
23
- if (command.startsWith('display-message -p "#{qa:session_name}')) {
24
- return basic(command) ?? [];
25
- }
26
- // This scalar observation is not a deferred cursor probe. Answer it
27
- // before an automatic inventory reply can consume its pending FIFO slot.
28
- if (isHistorySizeProbe(command))
29
- return ["0"];
30
- if (command.includes("capture-pane") || command.startsWith("display-message"))
31
- return null;
32
- return basic(command) ?? [];
33
- });
34
- }
35
- /** Complete every scripted probe written since the prior turn. */
36
- pump() {
37
- for (let index = this.#handledWrites; index < this.channel.written.length; index += 1) {
38
- const command = this.channel.written[index];
39
- if (command.startsWith('display-message -p "#{qa:session_name}') ||
40
- isHistorySizeProbe(command))
41
- continue;
42
- if (command.includes("capture-pane") || command.startsWith("display-message")) {
43
- this.deferredCommands.push(command);
44
- }
45
- }
46
- this.#handledWrites = this.channel.written.length;
47
- while (this.deferredCommands.length > 0) {
48
- const command = this.deferredCommands.shift();
49
- if (command.includes("capture-pane -p -R"))
50
- // The default scripted driver models stock tmux, not malformed native
51
- // data. Only explicit unsupported evidence may select portable capture.
52
- this.channel.reply(["command capture-pane: unknown flag -R"], false);
53
- else if (command.includes("capture-pane"))
54
- this.channel.reply([...this.#seedLines]);
55
- else {
56
- const pane = /-t (%\d+)/u.exec(command)?.[1];
57
- this.channel.reply([
58
- pane ? (this.#cursorLinesByPane[pane] ?? this.#cursorLine) : this.#cursorLine,
59
- ]);
60
- }
61
- }
62
- }
63
- output(runtimePaneId, escapedControlBytes) {
64
- this.channel.output(runtimePaneId, escapedControlBytes);
65
- }
66
- exit(reason = "detached") {
67
- this.channel.feedLines(`%exit ${reason}`);
68
- }
69
- async settleUntil(predicate, label) {
70
- for (let turn = 0; turn < this.#maxTurns; turn += 1) {
71
- this.pump();
72
- // The real replica intentionally defers its first xterm write with a
73
- // zero-delay timer. Yielding only through the check phase (`setImmediate`)
74
- // can starve that timer for every bounded turn on Linux CI, making a
75
- // settled simulator look stuck with no pending control replies. Advance
76
- // both timer and check phases so this driver models a complete event-loop
77
- // turn rather than relying on platform-specific queue ordering.
78
- await new Promise((resolve) => setTimeout(resolve, 0));
79
- await new Promise((resolve) => setImmediate(resolve));
80
- if (predicate())
81
- return;
82
- }
83
- throw new Error(`Scripted channel did not settle ${label} within ${this.#maxTurns} turns; pending=${this.channel.core.pendingCount}; writes=${this.channel.written.join(" | ")}`);
84
- }
85
- }
86
- export function standardScriptedChannel(handlers, options) {
87
- return new ScriptedChannelDriver(handlers, {
88
- state: options?.state ?? {
89
- truthRows: [...FIXTURE.truthRows],
90
- windowRows: FIXTURE.windowRows(FIXTURE.layoutW1, FIXTURE.layoutW2),
91
- descriptorRows: [...FIXTURE.descriptorRows],
92
- },
93
- ...options,
94
- });
95
- }
96
- function isHistorySizeProbe(command) {
97
- return /^display-message -p -t %\d+ "#\{history_size\}"$/u.test(command);
98
- }