@crouton-kit/crouter 0.3.28 → 0.3.30

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 (324) hide show
  1. package/dist/build-root.d.ts +15 -7
  2. package/dist/build-root.js +48 -34
  3. package/dist/builtin-memory/crouter-development/marketplaces.md +7 -6
  4. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +1 -1
  5. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +1 -1
  6. package/dist/builtin-memory/crouter-development/personas.md +1 -1
  7. package/dist/builtin-memory/crouter-development/plugins.md +20 -18
  8. package/dist/builtin-memory/design.md +1 -1
  9. package/dist/builtin-memory/development.md +1 -1
  10. package/dist/builtin-memory/internal/INDEX.md +1 -1
  11. package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
  12. package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
  13. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  14. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  15. package/dist/builtin-memory/planning.md +1 -1
  16. package/dist/builtin-memory/spec.md +6 -4
  17. package/dist/builtin-personas/orchestration-kernel.md +4 -5
  18. package/dist/builtin-personas/runtime-base.md +5 -4
  19. package/dist/builtin-personas/spec/PERSONA.md +8 -4
  20. package/dist/builtin-personas/spec/orchestrator.md +5 -3
  21. package/dist/builtin-personas/spec/requirements/PERSONA.md +10 -0
  22. package/dist/builtin-views/canvas/core.mjs +586 -0
  23. package/dist/builtin-views/canvas/text.mjs +58 -0
  24. package/dist/builtin-views/canvas/tui.mjs +165 -0
  25. package/dist/builtin-views/canvas/web.jsx +120 -0
  26. package/dist/builtin-views/git-pr/core.mjs +673 -0
  27. package/dist/builtin-views/git-pr/text.mjs +84 -0
  28. package/dist/builtin-views/git-pr/tui.mjs +302 -0
  29. package/dist/builtin-views/git-pr/web.jsx +216 -0
  30. package/dist/builtin-views/inbox/_lib/render.mjs +1 -3
  31. package/dist/builtin-views/inbox/core.mjs +1273 -0
  32. package/dist/builtin-views/inbox/text.mjs +73 -0
  33. package/dist/builtin-views/inbox/tui.mjs +312 -0
  34. package/dist/builtin-views/inbox/web.jsx +188 -0
  35. package/dist/builtin-views/linkedin/core.mjs +906 -0
  36. package/dist/builtin-views/linkedin/text.mjs +69 -0
  37. package/dist/builtin-views/linkedin/tui.mjs +427 -0
  38. package/dist/builtin-views/linkedin/web.jsx +206 -0
  39. package/dist/builtin-views/workspace-sidebar/core.mjs +621 -0
  40. package/dist/builtin-views/workspace-sidebar/text.mjs +53 -0
  41. package/dist/builtin-views/workspace-sidebar/tui.mjs +142 -0
  42. package/dist/builtin-views/workspace-sidebar/web.jsx +109 -0
  43. package/dist/cli.js +22 -17
  44. package/dist/clients/attach/__tests__/action-parity.test.js +48 -0
  45. package/dist/clients/attach/__tests__/clipboard-image.test.js +76 -0
  46. package/dist/clients/attach/__tests__/editor-newline.test.js +57 -0
  47. package/dist/clients/attach/__tests__/git-info.test.js +31 -0
  48. package/dist/clients/attach/__tests__/onrequest-wired.test.js +20 -0
  49. package/dist/clients/attach/__tests__/slash-quit-copy.test.js +42 -0
  50. package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
  51. package/dist/clients/attach/attach-cmd.js +282 -46
  52. package/dist/clients/attach/auth-pickers.d.ts +9 -0
  53. package/dist/clients/attach/auth-pickers.js +194 -0
  54. package/dist/clients/attach/canvas-panels.d.ts +4 -2
  55. package/dist/clients/attach/canvas-panels.js +45 -17
  56. package/dist/clients/attach/chat-view.d.ts +27 -2
  57. package/dist/clients/attach/chat-view.js +98 -9
  58. package/dist/clients/attach/clipboard-image.d.ts +47 -8
  59. package/dist/clients/attach/clipboard-image.js +150 -26
  60. package/dist/clients/attach/clipboard-text.d.ts +3 -0
  61. package/dist/clients/attach/clipboard-text.js +39 -0
  62. package/dist/clients/attach/config-load.d.ts +54 -7
  63. package/dist/clients/attach/config-load.js +99 -3
  64. package/dist/clients/attach/context-message.d.ts +12 -0
  65. package/dist/clients/attach/context-message.js +72 -0
  66. package/dist/clients/attach/git-info.d.ts +20 -0
  67. package/dist/clients/attach/git-info.js +44 -0
  68. package/dist/clients/attach/graph-overlay.d.ts +13 -4
  69. package/dist/clients/attach/graph-overlay.js +59 -19
  70. package/dist/clients/attach/input-controller.d.ts +75 -13
  71. package/dist/clients/attach/input-controller.js +305 -67
  72. package/dist/clients/attach/pickers.d.ts +58 -0
  73. package/dist/clients/attach/pickers.js +171 -0
  74. package/dist/clients/attach/slash-commands.d.ts +22 -0
  75. package/dist/clients/attach/slash-commands.js +91 -25
  76. package/dist/clients/attach/titled-editor.d.ts +33 -0
  77. package/dist/clients/attach/titled-editor.js +84 -0
  78. package/dist/clients/attach/view-socket.d.ts +18 -1
  79. package/dist/clients/attach/view-socket.js +70 -1
  80. package/dist/clients/web/dev-server.d.ts +7 -0
  81. package/dist/clients/web/dev-server.js +59 -0
  82. package/dist/clients/web/events.d.ts +14 -0
  83. package/dist/clients/web/events.js +151 -0
  84. package/dist/clients/web/server.d.ts +18 -0
  85. package/dist/clients/web/server.js +450 -0
  86. package/dist/clients/web/web-cmd.d.ts +2 -0
  87. package/dist/clients/web/web-cmd.js +120 -0
  88. package/dist/commands/canvas.js +1 -2
  89. package/dist/commands/chord.js +4 -25
  90. package/dist/commands/dashboard.js +5 -1
  91. package/dist/commands/memory/__tests__/lint-schema.test.js +6 -6
  92. package/dist/commands/memory/lint.js +14 -2
  93. package/dist/commands/memory/read.js +2 -2
  94. package/dist/commands/memory/shared.d.ts +7 -1
  95. package/dist/commands/memory/shared.js +18 -3
  96. package/dist/commands/memory/write.js +8 -4
  97. package/dist/commands/memory.js +4 -4
  98. package/dist/commands/node.d.ts +1 -1
  99. package/dist/commands/node.js +64 -65
  100. package/dist/commands/pkg.js +1 -2
  101. package/dist/commands/revive.js +11 -3
  102. package/dist/commands/sys/sync.d.ts +1 -0
  103. package/dist/commands/sys/sync.js +187 -0
  104. package/dist/commands/sys.js +3 -2
  105. package/dist/commands/view-cycle.js +2 -2
  106. package/dist/commands/view-list.js +8 -8
  107. package/dist/commands/view-new.js +21 -17
  108. package/dist/commands/view-pick.js +1 -1
  109. package/dist/commands/view-run.js +35 -14
  110. package/dist/commands/view.js +8 -6
  111. package/dist/commands/workspace.d.ts +2 -0
  112. package/dist/commands/workspace.js +161 -0
  113. package/dist/core/__tests__/broker-double-spawn.test.d.ts +1 -0
  114. package/dist/core/__tests__/broker-double-spawn.test.js +143 -0
  115. package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
  116. package/dist/core/__tests__/broker-fork-seam.test.js +102 -0
  117. package/dist/core/__tests__/broker-sdk-wiring.test.js +2 -0
  118. package/dist/core/__tests__/child-death-wake.test.js +56 -111
  119. package/dist/core/__tests__/close.test.js +14 -20
  120. package/dist/core/__tests__/context-intro.test.js +19 -19
  121. package/dist/core/__tests__/daemon-boot.test.js +30 -23
  122. package/dist/core/__tests__/draw-style.test.js +1 -1
  123. package/dist/core/__tests__/error-stall-recycle.test.d.ts +1 -0
  124. package/dist/core/__tests__/error-stall-recycle.test.js +141 -0
  125. package/dist/core/__tests__/fixtures/fake-engine.d.ts +17 -1
  126. package/dist/core/__tests__/fixtures/fake-engine.js +22 -2
  127. package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
  128. package/dist/core/__tests__/focuses.test.js +25 -13
  129. package/dist/core/__tests__/fork.test.js +22 -5
  130. package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +1 -0
  131. package/dist/core/__tests__/full/broker-control-preempt.test.js +61 -0
  132. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
  133. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
  134. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
  135. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
  136. package/dist/core/__tests__/full/broker-pane-resolution.test.js +90 -0
  137. package/dist/core/__tests__/full/cascade-close.test.js +22 -8
  138. package/dist/core/__tests__/full/detach-focus.test.js +66 -169
  139. package/dist/core/__tests__/helpers/harness.d.ts +6 -9
  140. package/dist/core/__tests__/helpers/harness.js +19 -36
  141. package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
  142. package/dist/core/__tests__/human-node-not-supervised.test.js +83 -0
  143. package/dist/core/__tests__/lifecycle.test.js +1 -19
  144. package/dist/core/__tests__/listing-completeness.test.js +2 -2
  145. package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
  146. package/dist/core/__tests__/memory-resolver.test.js +1 -1
  147. package/dist/core/__tests__/on-read-dedup-resume.test.js +2 -2
  148. package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
  149. package/dist/core/__tests__/refresh-stall-recycle.test.js +88 -0
  150. package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
  151. package/dist/core/__tests__/relaunch-root.test.js +114 -0
  152. package/dist/core/__tests__/reset.test.js +19 -79
  153. package/dist/core/__tests__/steer-note.test.js +9 -7
  154. package/dist/core/__tests__/tmux-surface.test.js +13 -7
  155. package/dist/core/bootstrap.d.ts +0 -3
  156. package/dist/core/bootstrap.js +1 -143
  157. package/dist/core/canvas/browse/__tests__/model.test.js +86 -1
  158. package/dist/core/canvas/browse/__tests__/render.test.js +2 -1
  159. package/dist/core/canvas/browse/app.js +131 -8
  160. package/dist/core/canvas/browse/model.d.ts +23 -5
  161. package/dist/core/canvas/browse/model.js +68 -13
  162. package/dist/core/canvas/browse/render.d.ts +7 -1
  163. package/dist/core/canvas/browse/render.js +178 -47
  164. package/dist/core/canvas/canvas.js +1 -1
  165. package/dist/core/canvas/focuses.d.ts +4 -4
  166. package/dist/core/canvas/focuses.js +7 -7
  167. package/dist/core/canvas/nav-model.js +8 -7
  168. package/dist/core/canvas/render.d.ts +68 -9
  169. package/dist/core/canvas/render.js +195 -61
  170. package/dist/core/canvas/types.d.ts +16 -23
  171. package/dist/core/config.js +1 -4
  172. package/dist/core/memory-resolver.js +43 -5
  173. package/dist/core/runtime/bearings.d.ts +1 -1
  174. package/dist/core/runtime/bearings.js +8 -8
  175. package/dist/core/runtime/branded-host.d.ts +16 -0
  176. package/dist/core/runtime/branded-host.js +127 -0
  177. package/dist/core/runtime/broker-protocol.d.ts +212 -6
  178. package/dist/core/runtime/broker.d.ts +4 -1
  179. package/dist/core/runtime/broker.js +494 -58
  180. package/dist/core/runtime/close.d.ts +12 -2
  181. package/dist/core/runtime/close.js +37 -16
  182. package/dist/core/runtime/front-door.js +6 -14
  183. package/dist/core/runtime/host.d.ts +14 -34
  184. package/dist/core/runtime/host.js +14 -50
  185. package/dist/core/runtime/launch.d.ts +12 -9
  186. package/dist/core/runtime/launch.js +27 -15
  187. package/dist/core/runtime/lifecycle.d.ts +1 -1
  188. package/dist/core/runtime/lifecycle.js +15 -19
  189. package/dist/core/runtime/nodes.d.ts +0 -55
  190. package/dist/core/runtime/nodes.js +6 -74
  191. package/dist/core/runtime/placement.d.ts +91 -335
  192. package/dist/core/runtime/placement.js +291 -858
  193. package/dist/core/runtime/promote.d.ts +2 -0
  194. package/dist/core/runtime/promote.js +24 -6
  195. package/dist/core/runtime/recap.d.ts +8 -0
  196. package/dist/core/runtime/recap.js +107 -0
  197. package/dist/core/runtime/recycle.js +25 -61
  198. package/dist/core/runtime/reset.d.ts +43 -37
  199. package/dist/core/runtime/reset.js +131 -218
  200. package/dist/core/runtime/revive.d.ts +9 -29
  201. package/dist/core/runtime/revive.js +40 -139
  202. package/dist/core/runtime/spawn.d.ts +15 -16
  203. package/dist/core/runtime/spawn.js +165 -194
  204. package/dist/core/runtime/surface-bg.d.ts +11 -0
  205. package/dist/core/runtime/surface-bg.js +68 -0
  206. package/dist/core/runtime/tmux.d.ts +15 -52
  207. package/dist/core/runtime/tmux.js +35 -99
  208. package/dist/core/scope.d.ts +1 -2
  209. package/dist/core/scope.js +1 -5
  210. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts +1 -0
  211. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +57 -0
  212. package/dist/core/skill-sync/builtins.d.ts +42 -0
  213. package/dist/core/skill-sync/builtins.js +112 -0
  214. package/dist/core/skill-sync/claude-plugins.d.ts +23 -0
  215. package/dist/core/skill-sync/claude-plugins.js +71 -0
  216. package/dist/core/skill-sync/engine.d.ts +42 -0
  217. package/dist/core/skill-sync/engine.js +633 -0
  218. package/dist/core/skill-sync/export.d.ts +23 -0
  219. package/dist/core/skill-sync/export.js +86 -0
  220. package/dist/core/skill-sync/manifest.d.ts +64 -0
  221. package/dist/core/skill-sync/manifest.js +181 -0
  222. package/dist/core/skill-sync/profile.d.ts +76 -0
  223. package/dist/core/skill-sync/profile.js +173 -0
  224. package/dist/core/skill-sync/snapshot.d.ts +57 -0
  225. package/dist/core/skill-sync/snapshot.js +120 -0
  226. package/dist/core/substrate/index.d.ts +1 -1
  227. package/dist/core/substrate/index.js +1 -1
  228. package/dist/core/substrate/injected-store.js +3 -3
  229. package/dist/core/substrate/on-read.js +68 -6
  230. package/dist/core/substrate/render.d.ts +8 -11
  231. package/dist/core/substrate/render.js +29 -43
  232. package/dist/core/substrate/schema.d.ts +10 -3
  233. package/dist/core/substrate/schema.js +6 -3
  234. package/dist/core/tui/host.d.ts +12 -4
  235. package/dist/core/tui/host.js +280 -149
  236. package/dist/core/view/bridge.d.ts +10 -0
  237. package/dist/core/view/bridge.js +31 -0
  238. package/dist/core/view/chrome.d.ts +9 -0
  239. package/dist/core/view/chrome.js +22 -0
  240. package/dist/core/view/contract.d.ts +171 -0
  241. package/dist/core/view/contract.js +23 -0
  242. package/dist/core/view/loader.d.ts +31 -0
  243. package/dist/core/view/loader.js +188 -0
  244. package/dist/core/view/transport-local.d.ts +7 -0
  245. package/dist/core/view/transport-local.js +70 -0
  246. package/dist/core/view/transport.d.ts +4 -0
  247. package/dist/core/view/transport.js +15 -0
  248. package/dist/daemon/crtrd.d.ts +38 -27
  249. package/dist/daemon/crtrd.js +303 -376
  250. package/dist/daemon/manage.js +6 -1
  251. package/dist/index.js +1 -1
  252. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +28 -20
  253. package/dist/pi-extensions/canvas-context-intro.js +4 -4
  254. package/dist/pi-extensions/canvas-doc-substrate.js +16 -16
  255. package/dist/pi-extensions/canvas-nav.js +5 -0
  256. package/dist/pi-extensions/canvas-recap.d.ts +37 -0
  257. package/dist/pi-extensions/canvas-recap.js +236 -0
  258. package/dist/pi-extensions/canvas-resume.js +2 -1
  259. package/dist/pi-extensions/canvas-stophook.d.ts +14 -12
  260. package/dist/pi-extensions/canvas-stophook.js +87 -101
  261. package/dist/pi-extensions/canvas-view.js +2 -1
  262. package/dist/pi-extensions/widget-order-bus.d.ts +6 -0
  263. package/dist/pi-extensions/widget-order-bus.js +34 -0
  264. package/dist/prompts/view.d.ts +2 -2
  265. package/dist/prompts/view.js +145 -73
  266. package/dist/types.d.ts +1 -6
  267. package/dist/types.js +1 -3
  268. package/dist/web/ViewChrome.d.ts +7 -0
  269. package/dist/web/ViewChrome.js +28 -0
  270. package/dist/web/ViewPane.d.ts +39 -0
  271. package/dist/web/ViewPane.js +48 -0
  272. package/dist/web/index.d.ts +6 -0
  273. package/dist/web/index.js +16 -0
  274. package/dist/web/runtime.d.ts +39 -0
  275. package/dist/web/runtime.js +133 -0
  276. package/dist/web/states.d.ts +24 -0
  277. package/dist/web/states.js +24 -0
  278. package/dist/web/transport-http.d.ts +5 -0
  279. package/dist/web/transport-http.js +28 -0
  280. package/dist/web-client/assets/index-BUvQb4hR.css +2 -0
  281. package/dist/web-client/assets/index-ClLQXYAE.js +10 -0
  282. package/dist/web-client/index.html +13 -0
  283. package/package.json +20 -6
  284. package/dist/builtin-views/canvas/client.mjs +0 -303
  285. package/dist/builtin-views/canvas/view.mjs +0 -576
  286. package/dist/builtin-views/git-pr/client.mjs +0 -440
  287. package/dist/builtin-views/git-pr/view.mjs +0 -675
  288. package/dist/builtin-views/inbox/sources/gmail.mjs +0 -965
  289. package/dist/builtin-views/inbox/sources/linkedin.mjs +0 -427
  290. package/dist/builtin-views/inbox/view.mjs +0 -889
  291. package/dist/builtin-views/linkedin/client.mjs +0 -610
  292. package/dist/builtin-views/linkedin/view.mjs +0 -1171
  293. package/dist/commands/pkg/bridge.d.ts +0 -1
  294. package/dist/commands/pkg/bridge.js +0 -137
  295. package/dist/commands/skill/author.d.ts +0 -3
  296. package/dist/commands/skill/author.js +0 -140
  297. package/dist/commands/skill/shared.d.ts +0 -3
  298. package/dist/commands/skill/shared.js +0 -19
  299. package/dist/commands/skill.d.ts +0 -2
  300. package/dist/commands/skill.js +0 -21
  301. package/dist/commands/tmux-spread.d.ts +0 -2
  302. package/dist/commands/tmux-spread.js +0 -144
  303. package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +0 -292
  304. package/dist/core/__tests__/full/placement-focus.test.js +0 -309
  305. package/dist/core/__tests__/full/placement-reconcile.test.js +0 -212
  306. package/dist/core/__tests__/full/placement-revive.test.js +0 -238
  307. package/dist/core/__tests__/full/placement-teardown.test.js +0 -297
  308. package/dist/core/__tests__/home-session.test.js +0 -186
  309. package/dist/core/__tests__/relaunch.test.js +0 -335
  310. package/dist/core/bridge-map.d.ts +0 -19
  311. package/dist/core/bridge-map.js +0 -73
  312. package/dist/core/tui/contract.d.ts +0 -83
  313. package/dist/core/tui/contract.js +0 -8
  314. package/dist/core/tui/loader.d.ts +0 -16
  315. package/dist/core/tui/loader.js +0 -94
  316. package/dist/prompts/skill.d.ts +0 -2
  317. package/dist/prompts/skill.js +0 -650
  318. /package/dist/{core/__tests__/full/daemon-liveness-pane.full.test.d.ts → clients/attach/__tests__/action-parity.test.d.ts} +0 -0
  319. /package/dist/{core/__tests__/full/placement-focus.test.d.ts → clients/attach/__tests__/clipboard-image.test.d.ts} +0 -0
  320. /package/dist/{core/__tests__/full/placement-reconcile.test.d.ts → clients/attach/__tests__/editor-newline.test.d.ts} +0 -0
  321. /package/dist/{core/__tests__/full/placement-revive.test.d.ts → clients/attach/__tests__/git-info.test.d.ts} +0 -0
  322. /package/dist/{core/__tests__/full/placement-teardown.test.d.ts → clients/attach/__tests__/onrequest-wired.test.d.ts} +0 -0
  323. /package/dist/{core/__tests__/home-session.test.d.ts → clients/attach/__tests__/slash-quit-copy.test.d.ts} +0 -0
  324. /package/dist/{core/__tests__/relaunch.test.d.ts → clients/attach/__tests__/titled-editor.test.d.ts} +0 -0
@@ -1,297 +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
- // ---------------------------------------------------------------------------
149
- // BUG REGRESSION (lost focus pane on the push-final race): a focused child's
150
- // `crtr push final` seeds its manager's inbox MID-TURN; the daemon's second pass
151
- // can revive the dormant manager BACKSTAGE (the focus row still points at the
152
- // child) before the child's agent_end runs this handoff. At that point the
153
- // manager is status='active' with a real backstage pane whose pi is still
154
- // BOOTING — its recorded pi_pid is the OLD dead pid (the fresh pi records its
155
- // pid only at session_start). Pre-fix, MAJOR 1 gated on isPidAlive(pi_pid), so
156
- // it missed the booting manager; the dormant branch missed it too (no longer
157
- // idle-release) → handFocusToManager returned false → the stophook disarmed the
158
- // freeze and the USER'S VIEWPORT PANE REAPED while the manager ran on invisibly
159
- // backstage — the "pane gets lost" bug. The gate is now the PANE itself
160
- // (paneRunning: exists + command running), which a booting pi satisfies.
161
- // ---------------------------------------------------------------------------
162
- test('handFocusToManager (BUG REGRESSION): just-revived manager (RUNNING pane, STALE dead pi_pid) → swap, not reap', { skip: !hasTmux() }, () => {
163
- const user = `crtr-hfm-race-user-${process.pid}`;
164
- const back = `crtr-hfm-race-back-${process.pid}`;
165
- spawnSync('tmux', ['new-session', '-d', '-s', user, '-c', '/tmp', 'sleep 600']);
166
- spawnSync('tmux', ['new-session', '-d', '-s', back, '-c', '/tmp', 'sleep 600']);
167
- try {
168
- const userWindow = tmuxOut(['list-windows', '-t', user, '-F', '#{window_id}']).split('\n')[0];
169
- const backWindow = tmuxOut(['list-windows', '-t', back, '-F', '#{window_id}']).split('\n')[0];
170
- const focusPane = tmuxOut(['display-message', '-p', '-t', `${user}:${userWindow}`, '#{pane_id}']);
171
- // The manager's backstage pane: a RUNNING command (the booting pi stand-in).
172
- const mgrPane = tmuxOut(['split-window', '-d', '-P', '-F', '#{pane_id}', '-t', `${back}:${backWindow}`, 'sleep 600']);
173
- createNode(node('M', { pane: focusPane, tmux_session: user, window: userWindow, status: 'done' }));
174
- // The just-revived state: active (transition 'revive'), intent cleared, but
175
- // pi_pid NOT yet re-recorded — dead/stale (revive deliberately leaves it).
176
- createNode(node('mgr', { pane: mgrPane, tmux_session: back, window: backWindow, status: 'active', pi_pid: null }));
177
- openFocusRow('f', focusPane, user, 'M');
178
- assert.equal(handFocusToManager('f', 'mgr'), true, 'a booting just-revived manager takes the focus — the pane is what matters, not the stale pid');
179
- assert.equal(getFocusByNode('mgr')?.focus_id, 'f', 'the manager occupies the focus row');
180
- assert.equal(getFocusById('f')?.pane, mgrPane, 'focus row re-anchored to the manager pane (swap happened)');
181
- assert.equal(paneSession(mgrPane), user, 'the booting manager pane physically moved into the user viewport');
182
- // Non-vacuous: the pre-fix isPidAlive(mgr.pi_pid) gate fails on pi_pid=null →
183
- // returns false without repointing → every assert above fails, and the caller
184
- // would have reaped the user's pane.
185
- }
186
- finally {
187
- spawnSync('tmux', ['kill-session', '-t', user], { stdio: 'ignore' });
188
- spawnSync('tmux', ['kill-session', '-t', back], { stdio: 'ignore' });
189
- }
190
- });
191
- test('handFocusToManager: manager pane is a FROZEN remain-on-exit corpse (pane_dead=1) → false, no swap (dead-focus-pane guarantee holds under the pane gate)', { skip: !hasTmux() }, async () => {
192
- const user = `crtr-hfm-corpse-user-${process.pid}`;
193
- const back = `crtr-hfm-corpse-back-${process.pid}`;
194
- spawnSync('tmux', ['new-session', '-d', '-s', user, '-c', '/tmp', 'sleep 600']);
195
- spawnSync('tmux', ['new-session', '-d', '-s', back, '-c', '/tmp', 'sleep 600']);
196
- try {
197
- const userWindow = tmuxOut(['list-windows', '-t', user, '-F', '#{window_id}']).split('\n')[0];
198
- const backWindow = tmuxOut(['list-windows', '-t', back, '-F', '#{window_id}']).split('\n')[0];
199
- const focusPane = tmuxOut(['display-message', '-p', '-t', `${user}:${userWindow}`, '#{pane_id}']);
200
- // A short-lived command frozen by remain-on-exit → pane EXISTS but pane_dead=1.
201
- const mgrPane = tmuxOut(['split-window', '-d', '-P', '-F', '#{pane_id}', '-t', `${back}:${backWindow}`, 'sleep 0.2']);
202
- spawnSync('tmux', ['set-option', '-p', '-t', mgrPane, 'remain-on-exit', 'on'], { stdio: 'ignore' });
203
- // Wait until tmux reports the pane dead (frozen corpse), bounded.
204
- for (let i = 0; i < 50; i++) {
205
- if (tmuxOut(['display-message', '-p', '-t', mgrPane, '#{pane_dead}']) === '1')
206
- break;
207
- await new Promise((r) => setTimeout(r, 100));
208
- }
209
- assert.equal(tmuxOut(['display-message', '-p', '-t', mgrPane, '#{pane_dead}']), '1', 'precondition: the manager pane is a frozen corpse');
210
- createNode(node('M', { pane: focusPane, tmux_session: user, window: userWindow, status: 'done' }));
211
- createNode(node('mgr', { pane: mgrPane, tmux_session: back, window: backWindow, status: 'active', pi_pid: null }));
212
- openFocusRow('f', focusPane, user, 'M');
213
- assert.equal(handFocusToManager('f', 'mgr'), false, 'a corpse pane never claims the viewport — caller disarms + reaps');
214
- assert.equal(getFocusByNode('mgr'), null, 'occupant NOT repointed (no stranded frozen focus)');
215
- assert.equal(getFocusById('f')?.node_id, 'M', 'M still occupies the focus → caller runs closeFocusToShell');
216
- // Non-vacuous: a gate of bare paneExists (existence without pane_dead) would
217
- // swap the corpse into the viewport and return true — both asserts fail.
218
- }
219
- finally {
220
- spawnSync('tmux', ['kill-session', '-t', user], { stdio: 'ignore' });
221
- spawnSync('tmux', ['kill-session', '-t', back], { stdio: 'ignore' });
222
- }
223
- });
224
- test('handFocusToManager: DORMANT idle-release manager (dead pane) → occupant repointed, NO swap, focus pane UNCHANGED', () => {
225
- openFocusRow('f', '%focus', 'Suser', 'M');
226
- // The manager is dormant + idle-release (the ONLY dormant manager the daemon
227
- // revives): its pane is recorded but NOT a live tmux pane (its dead-pi window
228
- // collapsed) and its pi is dead. isNodePaneAlive(mgr) is therefore false, so the
229
- // live-swap is skipped — the external daemon later respawns it into the frozen
230
- // %focus, exactly because status='idle' && intent='idle-release'.
231
- createNode(node('mgr', { pane: '%mgr-dead', tmux_session: 'back', window: '@wb', pi_pid: null, status: 'idle', intent: 'idle-release' }));
232
- assert.equal(handFocusToManager('f', 'mgr'), true, 'still a takeover (occupant repointed)');
233
- assert.equal(getFocusByNode('mgr')?.focus_id, 'f', 'the manager occupies the focus row (DB repoint)');
234
- assert.equal(getFocusByNode('M'), null, 'the finished node no longer occupies it');
235
- // The split: with no physical swap, the focus row stays anchored on the ORIGINAL
236
- // frozen focus pane (the pane the daemon will respawn the manager INTO) and the
237
- // manager's backstage presence is left untouched.
238
- assert.equal(getFocusById('f')?.pane, '%focus', 'focus row pane NOT re-anchored to the manager pane (no swap)');
239
- assert.equal(getNode('mgr').pane, '%mgr-dead', 'manager presence pane unchanged');
240
- assert.equal(getNode('mgr').window, '@wb', 'manager presence window unchanged (never moved into a viewport)');
241
- // Non-vacuous: an impl that re-anchored the focus row to the manager pane in
242
- // the DB (or wrote the manager's presence to the viewport) regardless of a real
243
- // physical swap would set f.pane='%mgr-dead' and mgr.window=the focus window —
244
- // both asserts fail. Proves the swap gates on PANE LIVENESS, not merely on the
245
- // manager having a pane.
246
- });
247
- // ---------------------------------------------------------------------------
248
- // BUG REGRESSION (dead-focus-pane): handFocusToManager must return FALSE — so
249
- // the stophook caller runs closeFocusToShell to DISARM remain-on-exit and the
250
- // pane REAPS on exit — whenever NO successor will actually claim the frozen pane.
251
- // Pre-fix it repointed the occupant and returned true for ANY non-null,
252
- // not-already-focused manager (even done/dead/canceled, idle-but-not-idle-
253
- // release, or a live-but-paneless inline root). The daemon revives NONE of those
254
- // into the frozen pane, so closeFocusToShell was skipped, remain-on-exit stayed
255
- // ON, and the focus pane FROZE forever as a dead pane with no reaper.
256
- // Diagnosis: nodes/mq32wjve-68de0c31/context/dead-focus-pane-fix.md
257
- // ---------------------------------------------------------------------------
258
- test('handFocusToManager (BUG REGRESSION): a manager the daemon will NEVER revive → false, occupant UNCHANGED (caller disarms + reaps)', () => {
259
- openFocusRow('f', '%focus', 'Suser', 'M'); // M occupies the frozen focus pane
260
- // (a) DONE manager — the daemon ignores done nodes entirely (the most common
261
- // trigger: demoting a child to terminal whose manager already finished).
262
- createNode(node('mgrDone', { status: 'done', pane: '%d', tmux_session: 'back', window: '@wd' }));
263
- assert.equal(handFocusToManager('f', 'mgrDone'), false, 'a DONE manager will never be revived → caller must disarm + reap');
264
- assert.equal(getFocusByNode('mgrDone'), null, 'a DONE manager is NOT repointed into the focus row');
265
- // (b) DORMANT but NOT idle-release — idle with a different intent; crtrd's
266
- // second pass (idle && idle-release) skips it, so it never enters the pane.
267
- createNode(node('mgrIdleDone', { status: 'idle', intent: 'done', pane: '%i', tmux_session: 'back', window: '@wi' }));
268
- assert.equal(handFocusToManager('f', 'mgrIdleDone'), false, 'idle but NOT idle-release → daemon never revives it → false');
269
- assert.equal(getFocusByNode('mgrIdleDone'), null, 'a non-idle-release manager is NOT repointed');
270
- // (c) LIVE-but-PANELESS manager (an inline root, pane == null) — the live-swap
271
- // branch is skipped (no pane to swap) and, being active not idle-release,
272
- // the daemon never revives it either.
273
- createNode(node('mgrPaneless', { status: 'active', pi_pid: process.pid, pane: null }));
274
- assert.equal(handFocusToManager('f', 'mgrPaneless'), false, 'a live-but-paneless inline root cannot claim the pane → false');
275
- assert.equal(getFocusByNode('mgrPaneless'), null, 'a paneless manager is NOT repointed');
276
- // The occupant is untouched across all three → the caller (canvas-stophook
277
- // agent_end done-branch) runs closeFocusToShell, disarming the freeze.
278
- assert.equal(getFocusById('f')?.node_id, 'M', 'M still occupies its focus → caller disarms remain-on-exit + closes the row');
279
- // Non-vacuous: the pre-fix impl repointed the occupant + returned true for each
280
- // of (a)/(b)/(c), so every false return AND every "NOT repointed" assert fails
281
- // against it — and that stray true would have stranded the pane frozen forever.
282
- });
283
- // ---------------------------------------------------------------------------
284
- // tearDownNode (pure DB; no tmux — pane is null so closePane never runs).
285
- // ---------------------------------------------------------------------------
286
- test('tearDownNode: closes the focus row M occupied and nulls its LOCATION', () => {
287
- createNode(node('M', { pane: null, window: null }));
288
- openFocusRow('fM', null, 'Sa', 'M'); // M occupies a focus row
289
- tearDownNode('M');
290
- assert.equal(getFocusByNode('M'), null, 'the focus row M occupied is closed');
291
- const m = getNode('M');
292
- assert.equal(m.pane ?? null, null, 'pane nulled');
293
- assert.equal(m.window ?? null, null, 'window nulled');
294
- assert.equal(m.tmux_session ?? null, null, 'session nulled');
295
- // Non-vacuous: an impl that skips closeFocusRow leaves fM → getFocusByNode('M')
296
- // is non-null.
297
- });
@@ -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
- });