@crouton-kit/crouter 0.3.27 → 0.3.29

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 (343) hide show
  1. package/dist/build-root.js +4 -2
  2. package/dist/builtin-memory/crouter-development/marketplaces.md +7 -6
  3. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +1 -1
  4. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +1 -1
  5. package/dist/builtin-memory/crouter-development/personas.md +1 -1
  6. package/dist/builtin-memory/crouter-development/plugins.md +20 -18
  7. package/dist/builtin-memory/design.md +1 -1
  8. package/dist/builtin-memory/development.md +1 -1
  9. package/dist/builtin-memory/internal/INDEX.md +1 -1
  10. package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
  11. package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
  12. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  13. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  14. package/dist/builtin-memory/planning.md +1 -1
  15. package/dist/builtin-memory/spec.md +6 -4
  16. package/dist/builtin-personas/orchestration-kernel.md +4 -5
  17. package/dist/builtin-personas/runtime-base.md +8 -4
  18. package/dist/builtin-personas/spec/PERSONA.md +8 -4
  19. package/dist/builtin-personas/spec/orchestrator.md +5 -3
  20. package/dist/builtin-personas/spec/requirements/PERSONA.md +10 -0
  21. package/dist/builtin-views/canvas/core.mjs +586 -0
  22. package/dist/builtin-views/canvas/text.mjs +58 -0
  23. package/dist/builtin-views/canvas/tui.mjs +165 -0
  24. package/dist/builtin-views/canvas/web.jsx +120 -0
  25. package/dist/builtin-views/git-pr/core.mjs +673 -0
  26. package/dist/builtin-views/git-pr/text.mjs +84 -0
  27. package/dist/builtin-views/git-pr/tui.mjs +302 -0
  28. package/dist/builtin-views/git-pr/web.jsx +216 -0
  29. package/dist/builtin-views/inbox/_lib/render.mjs +1 -3
  30. package/dist/builtin-views/inbox/core.mjs +1273 -0
  31. package/dist/builtin-views/inbox/text.mjs +73 -0
  32. package/dist/builtin-views/inbox/tui.mjs +312 -0
  33. package/dist/builtin-views/inbox/web.jsx +188 -0
  34. package/dist/builtin-views/linkedin/core.mjs +906 -0
  35. package/dist/builtin-views/linkedin/text.mjs +69 -0
  36. package/dist/builtin-views/linkedin/tui.mjs +427 -0
  37. package/dist/builtin-views/linkedin/web.jsx +206 -0
  38. package/dist/builtin-views/workspace-sidebar/core.mjs +621 -0
  39. package/dist/builtin-views/workspace-sidebar/text.mjs +53 -0
  40. package/dist/builtin-views/workspace-sidebar/tui.mjs +142 -0
  41. package/dist/builtin-views/workspace-sidebar/web.jsx +109 -0
  42. package/dist/cli.js +3 -3
  43. package/dist/clients/attach/__tests__/action-parity.test.js +48 -0
  44. package/dist/clients/attach/__tests__/clipboard-image.test.js +76 -0
  45. package/dist/clients/attach/__tests__/editor-newline.test.js +57 -0
  46. package/dist/clients/attach/__tests__/git-info.test.js +31 -0
  47. package/dist/clients/attach/__tests__/onrequest-wired.test.js +20 -0
  48. package/dist/clients/attach/__tests__/reconnect-giveup.test.js +30 -0
  49. package/dist/clients/attach/__tests__/slash-quit-copy.test.js +42 -0
  50. package/dist/clients/attach/__tests__/titled-editor.test.d.ts +1 -0
  51. package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
  52. package/dist/clients/attach/attach-cmd.js +445 -41
  53. package/dist/clients/attach/auth-pickers.d.ts +9 -0
  54. package/dist/clients/attach/auth-pickers.js +194 -0
  55. package/dist/clients/attach/canvas-panels.d.ts +12 -0
  56. package/dist/clients/attach/canvas-panels.js +78 -0
  57. package/dist/clients/attach/chat-view.d.ts +27 -2
  58. package/dist/clients/attach/chat-view.js +98 -9
  59. package/dist/clients/attach/clipboard-image.d.ts +47 -8
  60. package/dist/clients/attach/clipboard-image.js +150 -26
  61. package/dist/clients/attach/clipboard-text.d.ts +3 -0
  62. package/dist/clients/attach/clipboard-text.js +39 -0
  63. package/dist/clients/attach/config-load.d.ts +54 -7
  64. package/dist/clients/attach/config-load.js +99 -3
  65. package/dist/clients/attach/context-message.d.ts +12 -0
  66. package/dist/clients/attach/context-message.js +72 -0
  67. package/dist/clients/attach/git-info.d.ts +20 -0
  68. package/dist/clients/attach/git-info.js +44 -0
  69. package/dist/clients/attach/graph-overlay.d.ts +43 -0
  70. package/dist/clients/attach/graph-overlay.js +306 -0
  71. package/dist/clients/attach/input-controller.d.ts +81 -13
  72. package/dist/clients/attach/input-controller.js +307 -67
  73. package/dist/clients/attach/pickers.d.ts +58 -0
  74. package/dist/clients/attach/pickers.js +171 -0
  75. package/dist/clients/attach/slash-commands.d.ts +44 -1
  76. package/dist/clients/attach/slash-commands.js +250 -27
  77. package/dist/clients/attach/titled-editor.d.ts +33 -0
  78. package/dist/clients/attach/titled-editor.js +84 -0
  79. package/dist/clients/attach/view-socket.d.ts +37 -2
  80. package/dist/clients/attach/view-socket.js +131 -7
  81. package/dist/clients/web/dev-server.d.ts +7 -0
  82. package/dist/clients/web/dev-server.js +59 -0
  83. package/dist/clients/web/events.d.ts +14 -0
  84. package/dist/clients/web/events.js +151 -0
  85. package/dist/clients/web/server.d.ts +18 -0
  86. package/dist/clients/web/server.js +450 -0
  87. package/dist/clients/web/web-cmd.d.ts +2 -0
  88. package/dist/clients/web/web-cmd.js +120 -0
  89. package/dist/commands/canvas.js +1 -2
  90. package/dist/commands/chord.js +1 -1
  91. package/dist/commands/dashboard.js +5 -1
  92. package/dist/commands/human/prompts.js +3 -3
  93. package/dist/commands/human/queue.d.ts +17 -0
  94. package/dist/commands/human/queue.js +111 -4
  95. package/dist/commands/memory/__tests__/lint-schema.test.js +27 -4
  96. package/dist/commands/memory/lint.d.ts +5 -4
  97. package/dist/commands/memory/lint.js +23 -7
  98. package/dist/commands/memory/read.js +2 -2
  99. package/dist/commands/memory/shared.d.ts +7 -1
  100. package/dist/commands/memory/shared.js +18 -3
  101. package/dist/commands/memory/write.js +20 -7
  102. package/dist/commands/memory.js +4 -4
  103. package/dist/commands/node.d.ts +1 -1
  104. package/dist/commands/node.js +64 -65
  105. package/dist/commands/pkg.js +1 -2
  106. package/dist/commands/revive.js +11 -3
  107. package/dist/commands/sys/feedback.d.ts +1 -0
  108. package/dist/commands/sys/feedback.js +163 -0
  109. package/dist/commands/sys/sync.d.ts +1 -0
  110. package/dist/commands/sys/sync.js +187 -0
  111. package/dist/commands/sys.js +4 -2
  112. package/dist/commands/view-cycle.js +2 -2
  113. package/dist/commands/view-list.js +8 -8
  114. package/dist/commands/view-new.js +21 -17
  115. package/dist/commands/view-pick.js +1 -1
  116. package/dist/commands/view-run.js +35 -14
  117. package/dist/commands/view.js +8 -6
  118. package/dist/commands/workspace.d.ts +2 -0
  119. package/dist/commands/workspace.js +161 -0
  120. package/dist/core/__tests__/broker-double-spawn.test.d.ts +1 -0
  121. package/dist/core/__tests__/broker-double-spawn.test.js +143 -0
  122. package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
  123. package/dist/core/__tests__/broker-fork-seam.test.js +102 -0
  124. package/dist/core/__tests__/broker-sdk-wiring.test.js +2 -0
  125. package/dist/core/__tests__/broker-snapshot-history.test.d.ts +1 -0
  126. package/dist/core/__tests__/broker-snapshot-history.test.js +105 -0
  127. package/dist/core/__tests__/child-death-wake.test.js +56 -111
  128. package/dist/core/__tests__/close.test.js +14 -20
  129. package/dist/core/__tests__/context-intro.test.js +19 -19
  130. package/dist/core/__tests__/daemon-boot.test.js +30 -23
  131. package/dist/core/__tests__/draw-style.test.js +1 -1
  132. package/dist/core/__tests__/error-stall-recycle.test.d.ts +1 -0
  133. package/dist/core/__tests__/error-stall-recycle.test.js +141 -0
  134. package/dist/core/__tests__/fixtures/fake-engine.d.ts +24 -1
  135. package/dist/core/__tests__/fixtures/fake-engine.js +32 -2
  136. package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
  137. package/dist/core/__tests__/focuses.test.js +25 -13
  138. package/dist/core/__tests__/fork.test.js +22 -5
  139. package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +1 -0
  140. package/dist/core/__tests__/full/broker-control-preempt.test.js +61 -0
  141. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
  142. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
  143. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
  144. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
  145. package/dist/core/__tests__/full/broker-pane-resolution.test.js +90 -0
  146. package/dist/core/__tests__/full/cascade-close.test.js +22 -8
  147. package/dist/core/__tests__/full/detach-focus.test.js +66 -169
  148. package/dist/core/__tests__/helpers/harness.d.ts +6 -9
  149. package/dist/core/__tests__/helpers/harness.js +19 -36
  150. package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
  151. package/dist/core/__tests__/human-node-not-supervised.test.js +83 -0
  152. package/dist/core/__tests__/human-stranded-deliver.test.d.ts +1 -0
  153. package/dist/core/__tests__/human-stranded-deliver.test.js +108 -0
  154. package/dist/core/__tests__/lifecycle.test.js +1 -19
  155. package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
  156. package/dist/core/__tests__/memory-resolver.test.js +1 -1
  157. package/dist/core/__tests__/on-read-dedup-resume.test.d.ts +1 -0
  158. package/dist/core/__tests__/on-read-dedup-resume.test.js +81 -0
  159. package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
  160. package/dist/core/__tests__/refresh-stall-recycle.test.js +88 -0
  161. package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
  162. package/dist/core/__tests__/relaunch-root.test.js +114 -0
  163. package/dist/core/__tests__/reset.test.js +19 -79
  164. package/dist/core/__tests__/steer-note.test.js +9 -7
  165. package/dist/core/__tests__/tmux-surface.test.js +13 -7
  166. package/dist/core/bootstrap.d.ts +0 -3
  167. package/dist/core/bootstrap.js +1 -143
  168. package/dist/core/canvas/browse/__tests__/model.test.js +86 -1
  169. package/dist/core/canvas/browse/__tests__/render.test.js +2 -1
  170. package/dist/core/canvas/browse/app.js +131 -8
  171. package/dist/core/canvas/browse/model.d.ts +23 -5
  172. package/dist/core/canvas/browse/model.js +68 -13
  173. package/dist/core/canvas/browse/render.d.ts +7 -1
  174. package/dist/core/canvas/browse/render.js +178 -47
  175. package/dist/core/canvas/canvas.js +1 -1
  176. package/dist/core/canvas/focuses.d.ts +4 -4
  177. package/dist/core/canvas/focuses.js +7 -7
  178. package/dist/core/canvas/nav-model.d.ts +162 -0
  179. package/dist/core/canvas/nav-model.js +487 -0
  180. package/dist/core/canvas/paths.d.ts +7 -0
  181. package/dist/core/canvas/paths.js +9 -0
  182. package/dist/core/canvas/render.d.ts +68 -9
  183. package/dist/core/canvas/render.js +195 -61
  184. package/dist/core/canvas/types.d.ts +16 -23
  185. package/dist/core/config.js +1 -4
  186. package/dist/core/memory-resolver.js +43 -5
  187. package/dist/core/runtime/bearings.d.ts +1 -1
  188. package/dist/core/runtime/bearings.js +8 -8
  189. package/dist/core/runtime/branded-host.d.ts +16 -0
  190. package/dist/core/runtime/branded-host.js +127 -0
  191. package/dist/core/runtime/broker-protocol.d.ts +212 -6
  192. package/dist/core/runtime/broker-sdk.d.ts +0 -12
  193. package/dist/core/runtime/broker-sdk.js +77 -6
  194. package/dist/core/runtime/broker.d.ts +5 -1
  195. package/dist/core/runtime/broker.js +520 -59
  196. package/dist/core/runtime/close.d.ts +12 -2
  197. package/dist/core/runtime/close.js +37 -16
  198. package/dist/core/runtime/front-door.js +14 -7
  199. package/dist/core/runtime/host.d.ts +14 -34
  200. package/dist/core/runtime/host.js +14 -50
  201. package/dist/core/runtime/launch.d.ts +12 -9
  202. package/dist/core/runtime/launch.js +27 -15
  203. package/dist/core/runtime/lifecycle.d.ts +1 -1
  204. package/dist/core/runtime/lifecycle.js +15 -19
  205. package/dist/core/runtime/nodes.d.ts +0 -55
  206. package/dist/core/runtime/nodes.js +6 -74
  207. package/dist/core/runtime/placement.d.ts +91 -334
  208. package/dist/core/runtime/placement.js +262 -828
  209. package/dist/core/runtime/promote.d.ts +2 -0
  210. package/dist/core/runtime/promote.js +24 -6
  211. package/dist/core/runtime/recap.d.ts +8 -0
  212. package/dist/core/runtime/recap.js +107 -0
  213. package/dist/core/runtime/recycle.js +25 -61
  214. package/dist/core/runtime/reset.d.ts +43 -37
  215. package/dist/core/runtime/reset.js +131 -218
  216. package/dist/core/runtime/revive.d.ts +9 -29
  217. package/dist/core/runtime/revive.js +45 -135
  218. package/dist/core/runtime/spawn.d.ts +15 -11
  219. package/dist/core/runtime/spawn.js +168 -136
  220. package/dist/core/runtime/surface-bg.d.ts +11 -0
  221. package/dist/core/runtime/surface-bg.js +68 -0
  222. package/dist/core/runtime/tmux.d.ts +15 -43
  223. package/dist/core/runtime/tmux.js +38 -80
  224. package/dist/core/scope.d.ts +1 -2
  225. package/dist/core/scope.js +1 -5
  226. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts +1 -0
  227. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +57 -0
  228. package/dist/core/skill-sync/builtins.d.ts +42 -0
  229. package/dist/core/skill-sync/builtins.js +112 -0
  230. package/dist/core/skill-sync/claude-plugins.d.ts +23 -0
  231. package/dist/core/skill-sync/claude-plugins.js +71 -0
  232. package/dist/core/skill-sync/engine.d.ts +42 -0
  233. package/dist/core/skill-sync/engine.js +633 -0
  234. package/dist/core/skill-sync/export.d.ts +23 -0
  235. package/dist/core/skill-sync/export.js +86 -0
  236. package/dist/core/skill-sync/manifest.d.ts +64 -0
  237. package/dist/core/skill-sync/manifest.js +181 -0
  238. package/dist/core/skill-sync/profile.d.ts +76 -0
  239. package/dist/core/skill-sync/profile.js +173 -0
  240. package/dist/core/skill-sync/snapshot.d.ts +57 -0
  241. package/dist/core/skill-sync/snapshot.js +120 -0
  242. package/dist/core/spawn.d.ts +14 -0
  243. package/dist/core/spawn.js +29 -9
  244. package/dist/core/substrate/index.d.ts +2 -2
  245. package/dist/core/substrate/index.js +7 -7
  246. package/dist/core/substrate/injected-store.d.ts +10 -0
  247. package/dist/core/substrate/injected-store.js +55 -0
  248. package/dist/core/substrate/on-read.js +68 -6
  249. package/dist/core/substrate/render.d.ts +8 -11
  250. package/dist/core/substrate/render.js +29 -43
  251. package/dist/core/substrate/schema.d.ts +16 -11
  252. package/dist/core/substrate/schema.js +32 -31
  253. package/dist/core/tui/host.d.ts +12 -4
  254. package/dist/core/tui/host.js +280 -149
  255. package/dist/core/view/bridge.d.ts +10 -0
  256. package/dist/core/view/bridge.js +31 -0
  257. package/dist/core/view/chrome.d.ts +9 -0
  258. package/dist/core/view/chrome.js +22 -0
  259. package/dist/core/view/contract.d.ts +171 -0
  260. package/dist/core/view/contract.js +23 -0
  261. package/dist/core/view/loader.d.ts +31 -0
  262. package/dist/core/view/loader.js +188 -0
  263. package/dist/core/view/transport-local.d.ts +7 -0
  264. package/dist/core/view/transport-local.js +70 -0
  265. package/dist/core/view/transport.d.ts +4 -0
  266. package/dist/core/view/transport.js +15 -0
  267. package/dist/daemon/crtrd.d.ts +38 -27
  268. package/dist/daemon/crtrd.js +303 -376
  269. package/dist/daemon/manage.js +6 -1
  270. package/dist/index.js +1 -1
  271. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +28 -20
  272. package/dist/pi-extensions/canvas-context-intro.js +4 -4
  273. package/dist/pi-extensions/canvas-doc-substrate.js +29 -20
  274. package/dist/pi-extensions/canvas-nav.js +35 -385
  275. package/dist/pi-extensions/canvas-recap.d.ts +37 -0
  276. package/dist/pi-extensions/canvas-recap.js +236 -0
  277. package/dist/pi-extensions/canvas-resume.js +2 -1
  278. package/dist/pi-extensions/canvas-stophook.d.ts +15 -13
  279. package/dist/pi-extensions/canvas-stophook.js +119 -103
  280. package/dist/pi-extensions/canvas-view.js +2 -1
  281. package/dist/pi-extensions/widget-order-bus.d.ts +6 -0
  282. package/dist/pi-extensions/widget-order-bus.js +34 -0
  283. package/dist/prompts/view.d.ts +2 -2
  284. package/dist/prompts/view.js +145 -73
  285. package/dist/types.d.ts +1 -6
  286. package/dist/types.js +1 -3
  287. package/dist/web/ViewChrome.d.ts +7 -0
  288. package/dist/web/ViewChrome.js +28 -0
  289. package/dist/web/ViewPane.d.ts +39 -0
  290. package/dist/web/ViewPane.js +48 -0
  291. package/dist/web/index.d.ts +6 -0
  292. package/dist/web/index.js +16 -0
  293. package/dist/web/runtime.d.ts +39 -0
  294. package/dist/web/runtime.js +133 -0
  295. package/dist/web/states.d.ts +24 -0
  296. package/dist/web/states.js +24 -0
  297. package/dist/web/transport-http.d.ts +5 -0
  298. package/dist/web/transport-http.js +28 -0
  299. package/dist/web-client/assets/index-BUvQb4hR.css +2 -0
  300. package/dist/web-client/assets/index-ClLQXYAE.js +10 -0
  301. package/dist/web-client/index.html +13 -0
  302. package/package.json +20 -6
  303. package/dist/builtin-views/canvas/client.mjs +0 -303
  304. package/dist/builtin-views/canvas/view.mjs +0 -576
  305. package/dist/builtin-views/git-pr/client.mjs +0 -440
  306. package/dist/builtin-views/git-pr/view.mjs +0 -675
  307. package/dist/builtin-views/inbox/sources/gmail.mjs +0 -965
  308. package/dist/builtin-views/inbox/sources/linkedin.mjs +0 -427
  309. package/dist/builtin-views/inbox/view.mjs +0 -889
  310. package/dist/builtin-views/linkedin/client.mjs +0 -610
  311. package/dist/builtin-views/linkedin/view.mjs +0 -1171
  312. package/dist/commands/pkg/bridge.d.ts +0 -1
  313. package/dist/commands/pkg/bridge.js +0 -137
  314. package/dist/commands/skill/author.d.ts +0 -3
  315. package/dist/commands/skill/author.js +0 -140
  316. package/dist/commands/skill/shared.d.ts +0 -3
  317. package/dist/commands/skill/shared.js +0 -19
  318. package/dist/commands/skill.d.ts +0 -2
  319. package/dist/commands/skill.js +0 -21
  320. package/dist/commands/tmux-spread.d.ts +0 -2
  321. package/dist/commands/tmux-spread.js +0 -144
  322. package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +0 -292
  323. package/dist/core/__tests__/full/placement-focus.test.js +0 -309
  324. package/dist/core/__tests__/full/placement-reconcile.test.js +0 -212
  325. package/dist/core/__tests__/full/placement-revive.test.js +0 -238
  326. package/dist/core/__tests__/full/placement-teardown.test.js +0 -221
  327. package/dist/core/__tests__/home-session.test.js +0 -186
  328. package/dist/core/__tests__/relaunch.test.js +0 -335
  329. package/dist/core/bridge-map.d.ts +0 -19
  330. package/dist/core/bridge-map.js +0 -73
  331. package/dist/core/tui/contract.d.ts +0 -83
  332. package/dist/core/tui/contract.js +0 -8
  333. package/dist/core/tui/loader.d.ts +0 -16
  334. package/dist/core/tui/loader.js +0 -94
  335. package/dist/prompts/skill.d.ts +0 -2
  336. package/dist/prompts/skill.js +0 -650
  337. /package/dist/{core/__tests__/full/daemon-liveness-pane.full.test.d.ts → clients/attach/__tests__/action-parity.test.d.ts} +0 -0
  338. /package/dist/{core/__tests__/full/placement-focus.test.d.ts → clients/attach/__tests__/clipboard-image.test.d.ts} +0 -0
  339. /package/dist/{core/__tests__/full/placement-reconcile.test.d.ts → clients/attach/__tests__/editor-newline.test.d.ts} +0 -0
  340. /package/dist/{core/__tests__/full/placement-revive.test.d.ts → clients/attach/__tests__/git-info.test.d.ts} +0 -0
  341. /package/dist/{core/__tests__/full/placement-teardown.test.d.ts → clients/attach/__tests__/onrequest-wired.test.d.ts} +0 -0
  342. /package/dist/{core/__tests__/home-session.test.d.ts → clients/attach/__tests__/reconnect-giveup.test.d.ts} +0 -0
  343. /package/dist/{core/__tests__/relaunch.test.d.ts → clients/attach/__tests__/slash-quit-copy.test.d.ts} +0 -0
@@ -1,221 +0,0 @@
1
- // Run with: node --import tsx/esm --test src/core/__tests__/placement-teardown.test.ts
2
- //
3
- // STEP 7 of the placement/focus migration: the lifecycle-successor + teardown
4
- // verbs (§1.6/§2.3, flow (e)). Two PURE surfaces (no tmux — pane ids below are
5
- // never realized, so closePane no-ops and only the DB effects are asserted):
6
- //
7
- // • handFocusToManager(focusId, managerId) — the §1.6 manager-takeover, a PURE
8
- // DB occupant swap. Returns true (TAKEOVER) only when a successor will
9
- // GENUINELY claim the frozen pane — a live manager (swapped in) or a dormant
10
- // manager the daemon will revive (status='idle' && intent='idle-release').
11
- // Returns false (caller disarms the freeze + closes the focus) for every
12
- // other case; each guard is asserted distinctly.
13
- // • tearDownNode(nodeId) — close/reset teardown: close the focus row it
14
- // occupies and null its LOCATION.
15
- import { test, before, after, beforeEach } from 'node:test';
16
- import assert from 'node:assert/strict';
17
- import { mkdtempSync, rmSync } from 'node:fs';
18
- import { tmpdir } from 'node:os';
19
- import { join } from 'node:path';
20
- import { spawnSync } from 'node:child_process';
21
- import { createNode, getNode } from '../../canvas/canvas.js';
22
- import { openFocusRow, getFocusByNode, getFocusById } from '../../canvas/focuses.js';
23
- import { closeDb } from '../../canvas/db.js';
24
- import { handFocusToManager, tearDownNode } from '../../runtime/placement.js';
25
- let home;
26
- function node(id, over = {}) {
27
- return {
28
- node_id: id,
29
- name: id,
30
- created: new Date().toISOString(),
31
- cwd: '/tmp/work',
32
- kind: 'developer',
33
- mode: 'base',
34
- lifecycle: 'terminal',
35
- status: 'active',
36
- ...over,
37
- };
38
- }
39
- function hasTmux() {
40
- return spawnSync('tmux', ['-V'], { stdio: 'ignore' }).status === 0;
41
- }
42
- function tmuxOut(args) {
43
- return (spawnSync('tmux', args, { encoding: 'utf8' }).stdout ?? '').trim();
44
- }
45
- function paneSession(pane) {
46
- return tmuxOut(['display-message', '-p', '-t', pane, '#{session_name}']);
47
- }
48
- function windowCount(session) {
49
- return tmuxOut(['list-windows', '-t', session, '-F', '#{window_id}']).split('\n').filter((s) => s !== '').length;
50
- }
51
- before(() => {
52
- home = mkdtempSync(join(tmpdir(), 'crtr-placement-teardown-'));
53
- process.env['CRTR_HOME'] = home;
54
- });
55
- beforeEach(() => {
56
- closeDb();
57
- rmSync(home, { recursive: true, force: true });
58
- });
59
- after(() => {
60
- closeDb();
61
- rmSync(home, { recursive: true, force: true });
62
- delete process.env['CRTR_HOME'];
63
- });
64
- // ---------------------------------------------------------------------------
65
- // handFocusToManager (pure DB) — §1.6 manager-takeover + its three false-guards.
66
- // ---------------------------------------------------------------------------
67
- test('handFocusToManager: dormant idle-release manager → TAKEOVER (repoints the row, returns true)', () => {
68
- openFocusRow('f', '%m', 'Sa', 'M');
69
- // The daemon's superviseTick second pass revives a node ONLY when it is idle +
70
- // idle-release (crtrd.ts ~309) — that is the ONLY dormant manager that will be
71
- // brought into the frozen %m, so it is the ONLY dormant takeover that returns true.
72
- createNode(node('mgr', { status: 'idle', intent: 'idle-release' }));
73
- assert.equal(handFocusToManager('f', 'mgr'), true, 'a manager the daemon WILL revive takes the focus');
74
- assert.equal(getFocusByNode('mgr')?.focus_id, 'f', 'the row now shows the manager');
75
- assert.equal(getFocusByNode('M'), null, 'the finished node no longer occupies it');
76
- // Non-vacuous: a no-op impl returns false / leaves M as occupant → both the
77
- // return value and the occupant assert fail.
78
- });
79
- test('handFocusToManager: managerId null → false, occupant UNCHANGED', () => {
80
- openFocusRow('f', '%m', 'Sa', 'M');
81
- assert.equal(handFocusToManager('f', null), false, 'no manager → caller must close the focus');
82
- assert.equal(getFocusById('f')?.node_id, 'M', 'occupant unchanged');
83
- // Non-vacuous: an impl that skipped the null guard would either return true or
84
- // setFocusOccupant(f, null) → NOT NULL violation; the clean false + unchanged
85
- // occupant fail against both.
86
- });
87
- test('handFocusToManager: manager IS the focus occupant → false (no self-handoff)', () => {
88
- openFocusRow('f', '%m', 'Sa', 'M');
89
- assert.equal(handFocusToManager('f', 'M'), false, 'handing a node its own focus is a no-op false');
90
- assert.equal(getFocusById('f')?.node_id, 'M', 'occupant unchanged');
91
- // Non-vacuous: an impl missing the `managerId === f.node_id` guard would return
92
- // true.
93
- });
94
- test('handFocusToManager: manager already focused ELSEWHERE → false, neither focus moved (UNIQUE node_id)', () => {
95
- openFocusRow('fM', '%m', 'Sa', 'M');
96
- openFocusRow('fMgr', '%g', 'Sb', 'mgr'); // mgr already occupies its own viewport
97
- assert.equal(handFocusToManager('fM', 'mgr'), false, 'a busy manager is not moved — caller closes');
98
- assert.equal(getFocusById('fM')?.node_id, 'M', "M's focus is not handed over");
99
- assert.equal(getFocusByNode('mgr')?.focus_id, 'fMgr', "mgr's other focus is untouched");
100
- // Non-vacuous: an impl missing the already-focused guard would setFocusOccupant
101
- // (fM, mgr) → UNIQUE(node_id) violation (mgr already in fMgr), so the expected
102
- // clean false + both rows intact fail.
103
- });
104
- // ---------------------------------------------------------------------------
105
- // MAJOR 1 — the LIVE-vs-DORMANT manager split inside handFocusToManager.
106
- // ---------------------------------------------------------------------------
107
- test('handFocusToManager: LIVE backstage manager → swaps its pane INTO the focus slot (MAJOR 1)', { skip: !hasTmux() }, () => {
108
- // Two real sessions: `user` (the viewport %m sits frozen in) + `back` (where
109
- // the manager's pi runs live, the normal multi-child state). The swap is a
110
- // real tmux op, so this needs a live server (gated like the other §5.2 tests).
111
- const user = `crtr-hfm-user-${process.pid}`;
112
- const back = `crtr-hfm-back-${process.pid}`;
113
- spawnSync('tmux', ['new-session', '-d', '-s', user, '-c', '/tmp', 'sleep 600']);
114
- spawnSync('tmux', ['new-session', '-d', '-s', back, '-c', '/tmp', 'sleep 600']);
115
- try {
116
- const userWindow = tmuxOut(['list-windows', '-t', user, '-F', '#{window_id}']).split('\n')[0];
117
- const backWindow = tmuxOut(['list-windows', '-t', back, '-F', '#{window_id}']).split('\n')[0];
118
- const focusPane = tmuxOut(['display-message', '-p', '-t', `${user}:${userWindow}`, '#{pane_id}']); // M's frozen focus pane (%m, the viewport)
119
- const mgrPane = tmuxOut(['split-window', '-d', '-P', '-F', '#{pane_id}', '-t', `${back}:${backWindow}`, 'sleep 600']); // mgr's LIVE backstage pane
120
- createNode(node('M', { pane: focusPane, tmux_session: user, window: userWindow, status: 'done' }));
121
- createNode(node('mgr', { pane: mgrPane, tmux_session: back, window: backWindow, status: 'active', pi_pid: process.pid }));
122
- openFocusRow('f', focusPane, user, 'M');
123
- const userWinsBefore = windowCount(user);
124
- assert.equal(handFocusToManager('f', 'mgr'), true, 'a LIVE manager takes the focus');
125
- // Pure-DB takeover (also covered ungated above): occupant repointed M → mgr.
126
- assert.equal(getFocusByNode('mgr')?.focus_id, 'f', 'the manager occupies the focus row');
127
- assert.equal(getFocusByNode('M'), null, 'the finished node no longer occupies it');
128
- // The synchronous swap (MAJOR 1): the focus row re-anchors to mgr's pane,
129
- // which has physically moved into the user viewport; the old focus pane (%m)
130
- // swapped down into the backstage slot; mgr's presence is the OLD focus slot.
131
- assert.equal(getFocusById('f')?.pane, mgrPane, 'focus row re-anchored to the manager pane (swap)');
132
- assert.equal(paneSession(mgrPane), user, 'the manager pane physically moved into the user viewport');
133
- assert.equal(getNode('mgr').window, userWindow, 'manager presence window == the OLD focus slot window');
134
- assert.equal(getNode('mgr').tmux_session, user, 'manager presence session == the viewport');
135
- assert.equal(paneSession(focusPane), back, '%m (the dead node\'s pane) swapped DOWN into the backstage slot');
136
- assert.equal(windowCount(user), userWinsBefore, 'no new user window — a swap, not an open (screen position invariant)');
137
- // Non-vacuous: the pre-MAJOR-1 impl only repointed the occupant and deferred
138
- // the physical revive to the daemon — but the daemon revives ONLY dormant
139
- // managers, so a LIVE one is never brought in. That impl leaves the focus row
140
- // pane == focusPane, mgrPane still in `back`, %m frozen in the viewport, and
141
- // mgr's presence at the backstage — every swap assert above fails against it.
142
- }
143
- finally {
144
- spawnSync('tmux', ['kill-session', '-t', user], { stdio: 'ignore' });
145
- spawnSync('tmux', ['kill-session', '-t', back], { stdio: 'ignore' });
146
- }
147
- });
148
- test('handFocusToManager: DORMANT idle-release manager (dead pane) → occupant repointed, NO swap, focus pane UNCHANGED', () => {
149
- openFocusRow('f', '%focus', 'Suser', 'M');
150
- // The manager is dormant + idle-release (the ONLY dormant manager the daemon
151
- // revives): its pane is recorded but NOT a live tmux pane (its dead-pi window
152
- // collapsed) and its pi is dead. isNodePaneAlive(mgr) is therefore false, so the
153
- // live-swap is skipped — the external daemon later respawns it into the frozen
154
- // %focus, exactly because status='idle' && intent='idle-release'.
155
- createNode(node('mgr', { pane: '%mgr-dead', tmux_session: 'back', window: '@wb', pi_pid: null, status: 'idle', intent: 'idle-release' }));
156
- assert.equal(handFocusToManager('f', 'mgr'), true, 'still a takeover (occupant repointed)');
157
- assert.equal(getFocusByNode('mgr')?.focus_id, 'f', 'the manager occupies the focus row (DB repoint)');
158
- assert.equal(getFocusByNode('M'), null, 'the finished node no longer occupies it');
159
- // The split: with no physical swap, the focus row stays anchored on the ORIGINAL
160
- // frozen focus pane (the pane the daemon will respawn the manager INTO) and the
161
- // manager's backstage presence is left untouched.
162
- assert.equal(getFocusById('f')?.pane, '%focus', 'focus row pane NOT re-anchored to the manager pane (no swap)');
163
- assert.equal(getNode('mgr').pane, '%mgr-dead', 'manager presence pane unchanged');
164
- assert.equal(getNode('mgr').window, '@wb', 'manager presence window unchanged (never moved into a viewport)');
165
- // Non-vacuous: an impl that re-anchored the focus row to the manager pane in
166
- // the DB (or wrote the manager's presence to the viewport) regardless of a real
167
- // physical swap would set f.pane='%mgr-dead' and mgr.window=the focus window —
168
- // both asserts fail. Proves the swap gates on PANE LIVENESS, not merely on the
169
- // manager having a pane.
170
- });
171
- // ---------------------------------------------------------------------------
172
- // BUG REGRESSION (dead-focus-pane): handFocusToManager must return FALSE — so
173
- // the stophook caller runs closeFocusToShell to DISARM remain-on-exit and the
174
- // pane REAPS on exit — whenever NO successor will actually claim the frozen pane.
175
- // Pre-fix it repointed the occupant and returned true for ANY non-null,
176
- // not-already-focused manager (even done/dead/canceled, idle-but-not-idle-
177
- // release, or a live-but-paneless inline root). The daemon revives NONE of those
178
- // into the frozen pane, so closeFocusToShell was skipped, remain-on-exit stayed
179
- // ON, and the focus pane FROZE forever as a dead pane with no reaper.
180
- // Diagnosis: nodes/mq32wjve-68de0c31/context/dead-focus-pane-fix.md
181
- // ---------------------------------------------------------------------------
182
- test('handFocusToManager (BUG REGRESSION): a manager the daemon will NEVER revive → false, occupant UNCHANGED (caller disarms + reaps)', () => {
183
- openFocusRow('f', '%focus', 'Suser', 'M'); // M occupies the frozen focus pane
184
- // (a) DONE manager — the daemon ignores done nodes entirely (the most common
185
- // trigger: demoting a child to terminal whose manager already finished).
186
- createNode(node('mgrDone', { status: 'done', pane: '%d', tmux_session: 'back', window: '@wd' }));
187
- assert.equal(handFocusToManager('f', 'mgrDone'), false, 'a DONE manager will never be revived → caller must disarm + reap');
188
- assert.equal(getFocusByNode('mgrDone'), null, 'a DONE manager is NOT repointed into the focus row');
189
- // (b) DORMANT but NOT idle-release — idle with a different intent; crtrd's
190
- // second pass (idle && idle-release) skips it, so it never enters the pane.
191
- createNode(node('mgrIdleDone', { status: 'idle', intent: 'done', pane: '%i', tmux_session: 'back', window: '@wi' }));
192
- assert.equal(handFocusToManager('f', 'mgrIdleDone'), false, 'idle but NOT idle-release → daemon never revives it → false');
193
- assert.equal(getFocusByNode('mgrIdleDone'), null, 'a non-idle-release manager is NOT repointed');
194
- // (c) LIVE-but-PANELESS manager (an inline root, pane == null) — the live-swap
195
- // branch is skipped (no pane to swap) and, being active not idle-release,
196
- // the daemon never revives it either.
197
- createNode(node('mgrPaneless', { status: 'active', pi_pid: process.pid, pane: null }));
198
- assert.equal(handFocusToManager('f', 'mgrPaneless'), false, 'a live-but-paneless inline root cannot claim the pane → false');
199
- assert.equal(getFocusByNode('mgrPaneless'), null, 'a paneless manager is NOT repointed');
200
- // The occupant is untouched across all three → the caller (canvas-stophook
201
- // agent_end done-branch) runs closeFocusToShell, disarming the freeze.
202
- assert.equal(getFocusById('f')?.node_id, 'M', 'M still occupies its focus → caller disarms remain-on-exit + closes the row');
203
- // Non-vacuous: the pre-fix impl repointed the occupant + returned true for each
204
- // of (a)/(b)/(c), so every false return AND every "NOT repointed" assert fails
205
- // against it — and that stray true would have stranded the pane frozen forever.
206
- });
207
- // ---------------------------------------------------------------------------
208
- // tearDownNode (pure DB; no tmux — pane is null so closePane never runs).
209
- // ---------------------------------------------------------------------------
210
- test('tearDownNode: closes the focus row M occupied and nulls its LOCATION', () => {
211
- createNode(node('M', { pane: null, window: null }));
212
- openFocusRow('fM', null, 'Sa', 'M'); // M occupies a focus row
213
- tearDownNode('M');
214
- assert.equal(getFocusByNode('M'), null, 'the focus row M occupied is closed');
215
- const m = getNode('M');
216
- assert.equal(m.pane ?? null, null, 'pane nulled');
217
- assert.equal(m.window ?? null, null, 'window nulled');
218
- assert.equal(m.tmux_session ?? null, null, 'session nulled');
219
- // Non-vacuous: an impl that skips closeFocusRow leaves fM → getFocusByNode('M')
220
- // is non-null.
221
- });
@@ -1,186 +0,0 @@
1
- // Run with: node --import tsx/esm --test src/core/__tests__/home-session.test.ts
2
- //
3
- // STEP 1 of the placement/focus migration: the durable REVIVE-HOME field.
4
- // `home_session` separates a node's revive target from its live LOCATION so a
5
- // later step can kill the focus taint. This step only ADDS + POPULATES +
6
- // DEFAULTS the field — no behavior change. Covers:
7
- // - home_session round-trips through meta.json (it IS durable identity)
8
- // - the birth-session decision (`resolveBirthSession`) for the child /
9
- // inline-root / --root cases each site sets home_session from
10
- // - recycle + relaunch (pane-recycle) births populate home_session
11
- // - a legacy meta with NO home_session defaults to tmux_session ?? nodeSession()
12
- import { test, before, after, beforeEach } from 'node:test';
13
- import assert from 'node:assert/strict';
14
- import { mkdtempSync, rmSync, readFileSync, writeFileSync } from 'node:fs';
15
- import { tmpdir } from 'node:os';
16
- import { join } from 'node:path';
17
- import { createNode, getNode, updateNode } from '../canvas/canvas.js';
18
- import { nodeMetaPath } from '../canvas/paths.js';
19
- import { closeDb } from '../canvas/db.js';
20
- import { resolveBirthSession, homeSessionOf, childBackstageOf } from '../runtime/nodes.js';
21
- import { nodeSession } from '../runtime/nodes.js';
22
- import { relaunchRoot } from '../runtime/reset.js';
23
- import { recycleNode } from '../runtime/recycle.js';
24
- let home;
25
- function node(id, over = {}) {
26
- return {
27
- node_id: id,
28
- name: id,
29
- created: new Date().toISOString(),
30
- cwd: '/tmp/work',
31
- kind: 'general',
32
- mode: 'base',
33
- lifecycle: 'resident',
34
- status: 'active',
35
- ...over,
36
- };
37
- }
38
- /** Make ensureDaemon (called by recycleNode) a no-op by faking a live daemon
39
- * pidfile pointing at THIS test process — so no real daemon is ever spawned. */
40
- function fakeLiveDaemon() {
41
- writeFileSync(join(home, 'crtrd.pid'), String(process.pid), 'utf8');
42
- }
43
- before(() => {
44
- home = mkdtempSync(join(tmpdir(), 'crtr-homesession-'));
45
- process.env['CRTR_HOME'] = home;
46
- });
47
- beforeEach(() => {
48
- closeDb();
49
- rmSync(home, { recursive: true, force: true });
50
- delete process.env['CRTR_ROOT_SESSION'];
51
- delete process.env['CRTR_NODE_SESSION'];
52
- });
53
- after(() => {
54
- closeDb();
55
- rmSync(home, { recursive: true, force: true });
56
- delete process.env['CRTR_HOME'];
57
- delete process.env['CRTR_ROOT_SESSION'];
58
- delete process.env['CRTR_NODE_SESSION'];
59
- });
60
- // ---------------------------------------------------------------------------
61
- // Persistence: home_session is DURABLE IDENTITY (meta.json), round-tripped by
62
- // createNode / getNode / updateNode — and NOT a runtime field.
63
- // ---------------------------------------------------------------------------
64
- test('home_session round-trips through meta.json and the hydrated view', () => {
65
- createNode(node('n', { home_session: 'crtr', tmux_session: 'user-sess', window: '@1' }));
66
- // On disk: meta.json carries home_session (identity), NOT the runtime LOCATION.
67
- const raw = JSON.parse(readFileSync(nodeMetaPath('n'), 'utf8'));
68
- assert.equal(raw['home_session'], 'crtr', 'home_session persisted to meta.json (durable identity)');
69
- assert.ok(!('tmux_session' in raw), 'tmux_session stays a runtime field, not in meta.json');
70
- // Hydrated view returns it; the live LOCATION is independent.
71
- const m = getNode('n');
72
- assert.equal(m?.home_session, 'crtr', 'getNode hydrates home_session');
73
- assert.equal(m?.tmux_session, 'user-sess', 'home_session is distinct from the live LOCATION');
74
- });
75
- test('updateNode patches home_session (the demote rewriter path) and preserves it', () => {
76
- createNode(node('n', { home_session: 'crtr' }));
77
- updateNode('n', { home_session: 'recycled-sess' });
78
- assert.equal(getNode('n')?.home_session, 'recycled-sess', 'home_session rewritten by updateNode');
79
- // An unrelated identity patch leaves home_session intact (RMW round-trip).
80
- updateNode('n', { description: 'a-handle' });
81
- assert.equal(getNode('n')?.home_session, 'recycled-sess', 'home_session survives an unrelated identity edit');
82
- });
83
- // ---------------------------------------------------------------------------
84
- // The legacy / back-compat DEFAULT: a meta with no home_session reads back
85
- // tmux_session ?? nodeSession().
86
- // ---------------------------------------------------------------------------
87
- test('homeSessionOf: a present home_session is returned verbatim', () => {
88
- createNode(node('n', { home_session: 'home-sess', tmux_session: 'live-sess' }));
89
- assert.equal(homeSessionOf('n'), 'home-sess', 'the stored revive-home wins over the live LOCATION');
90
- });
91
- test('homeSessionOf: legacy meta (no home_session) defaults to tmux_session', () => {
92
- createNode(node('legacy', { tmux_session: 'live-sess' }));
93
- assert.equal(getNode('legacy')?.home_session, undefined, 'no home_session on a legacy node');
94
- assert.equal(homeSessionOf('legacy'), 'live-sess', 'defaults to the last live LOCATION');
95
- });
96
- test('homeSessionOf: legacy meta with no LOCATION either defaults to nodeSession()', () => {
97
- createNode(node('legacy', { tmux_session: null }));
98
- assert.equal(homeSessionOf('legacy'), nodeSession(), 'falls through to the shared backstage');
99
- });
100
- test('homeSessionOf: the backstage default honors CRTR_NODE_SESSION', () => {
101
- process.env['CRTR_NODE_SESSION'] = 'my-backstage';
102
- createNode(node('legacy'));
103
- assert.equal(homeSessionOf('legacy'), 'my-backstage', 'nodeSession() default is env-overridable');
104
- });
105
- test('homeSessionOf: unknown node falls back to the backstage', () => {
106
- assert.equal(homeSessionOf('ghost'), nodeSession());
107
- });
108
- // ---------------------------------------------------------------------------
109
- // childBackstageOf — the session a node's CHILDREN spawn into (their
110
- // CRTR_ROOT_SESSION). REGRESSION for the front-door-root subtree-exile bug:
111
- // a refreshed inline root (home_session = a USER session it adopted) was
112
- // sourcing children's CRTR_ROOT_SESSION from home_session, so every yield
113
- // re-pointed its entire subtree into the user's working session. A root's
114
- // children must always flow to the shared backstage `nodeSession()`, never the
115
- // user session, while a managed child's children inherit its backstage
116
- // home_session. (Bug: node mq2u219p spawned all 13 children into `cli`.)
117
- // ---------------------------------------------------------------------------
118
- test('childBackstageOf: a managed child uses its backstage home_session', () => {
119
- // A child's home_session is ALWAYS the backstage it was born into; its live
120
- // tmux_session may be a user session (focus taint) but must NOT leak to kids.
121
- createNode(node('child', { parent: 'p', home_session: 'crtr', tmux_session: 'user-sess' }));
122
- assert.equal(childBackstageOf('child'), 'crtr', 'children inherit the backstage, not the tainted LOCATION');
123
- });
124
- test('childBackstageOf: a front-door ROOT routes children to the backstage, NOT its adopted user session', () => {
125
- // The bug: an inline root adopts the user's session as home_session ('cli').
126
- // Sourcing children's CRTR_ROOT_SESSION from home_session exiled the whole
127
- // subtree into 'cli' on every refresh-yield. A root (parent === null) must
128
- // route children to nodeSession() instead.
129
- createNode(node('root', { parent: null, home_session: 'cli', tmux_session: 'cli' }));
130
- assert.equal(childBackstageOf('root'), nodeSession(), "a root's children flow to the backstage");
131
- assert.notEqual(childBackstageOf('root'), 'cli', 'NEVER the user session the root pane adopted');
132
- });
133
- test('childBackstageOf: a root honors CRTR_NODE_SESSION for the backstage', () => {
134
- process.env['CRTR_NODE_SESSION'] = 'my-backstage';
135
- createNode(node('root', { parent: null, home_session: 'cli' }));
136
- assert.equal(childBackstageOf('root'), 'my-backstage', 'the backstage default is env-overridable');
137
- });
138
- test('childBackstageOf: unknown node falls back to the backstage', () => {
139
- assert.equal(childBackstageOf('ghost'), nodeSession());
140
- });
141
- // ---------------------------------------------------------------------------
142
- // The birth-session decision each site sets home_session from. Pure, so the
143
- // child / inline-root / --root births are testable without a live tmux (the
144
- // real spawnChild/bootRoot are tmux + pi + process.exit coupled).
145
- // ---------------------------------------------------------------------------
146
- test('birth: a managed child homes to the shared backstage (nodeSession), never a user session', () => {
147
- // No CRTR_ROOT_SESSION inherited, not adopting the caller.
148
- assert.equal(resolveBirthSession({ adoptCaller: false, here: { session: 'user-sess' }, rootSession: undefined }), nodeSession(), 'a child ignores the caller session and homes to crtr');
149
- });
150
- test('birth: a managed child inherits CRTR_ROOT_SESSION as its backstage', () => {
151
- assert.equal(resolveBirthSession({ adoptCaller: false, here: null, rootSession: 'crtr-subtree' }), 'crtr-subtree', 'the inherited root session is the child backstage');
152
- });
153
- test('birth: an independent --root inside tmux homes to the caller current session', () => {
154
- assert.equal(resolveBirthSession({ adoptCaller: true, here: { session: 'user-sess' }, rootSession: 'crtr' }), 'user-sess', 'a --root adopts the caller session where the spawner is working');
155
- });
156
- test('birth: a --root NOT inside tmux falls back to the backstage', () => {
157
- assert.equal(resolveBirthSession({ adoptCaller: true, here: null, rootSession: undefined }), nodeSession(), 'no caller session → the backstage');
158
- });
159
- test('birth: the inline front door (bootRoot) homes to its adopted session', () => {
160
- // bootRoot adopts the caller session when inside tmux, else nodeSession().
161
- assert.equal(resolveBirthSession({ adoptCaller: true, here: { session: 'term-sess' }, rootSession: undefined }), 'term-sess', 'inline root adopts the terminal it took over');
162
- assert.equal(resolveBirthSession({ adoptCaller: true, here: null, rootSession: undefined }), nodeSession(), 'inline root with no tmux homes to the backstage');
163
- });
164
- // ---------------------------------------------------------------------------
165
- // Pane-recycle births populate home_session. relaunchRoot (option C) is fully
166
- // unit-testable (injected respawn, no tmux); recycleNode runs to completion with
167
- // no tmux (respawn dispatch just fails) once the daemon spawn is neutralized.
168
- // ---------------------------------------------------------------------------
169
- test('relaunch birth: the fresh root homes to the recycled pane session', () => {
170
- createNode(node('root', { parent: null, lifecycle: 'resident', tmux_session: 'crtr', window: '@7' }));
171
- // No live tmux → paneLocation(pane) is null → loc falls back to the old root's
172
- // tmux_session ('crtr'); home_session must adopt it.
173
- const res = relaunchRoot('root', 'test-pane', { relaunchRootInPane: () => { } });
174
- assert.ok(res !== null, 'relaunchRoot minted a fresh root');
175
- assert.equal(getNode(res.newNodeId)?.home_session, 'crtr', 'fresh root homes to the recycled pane session');
176
- });
177
- test('recycle birth: the recycled root populates home_session (backstage when no pane location)', async () => {
178
- createNode(node('M', { parent: null, lifecycle: 'resident', tmux_session: 'crtr', window: '@3' }));
179
- fakeLiveDaemon(); // createNode ensured the home dir; now neutralize ensureDaemon
180
- // No live tmux → paneLocation('%0') is null → home_session defaults to the
181
- // backstage. The respawn dispatch fails (no tmux), but the fresh root is still
182
- // born — and must carry a populated home_session.
183
- const res = await recycleNode('M', '%0');
184
- assert.ok(res.newRoot !== null, 'recycle minted a fresh root');
185
- assert.equal(getNode(res.newRoot)?.home_session, nodeSession(), 'recycled root homes to the backstage');
186
- });