@danceiny/gotry 0.0.1-rc.9 → 0.2.0-rc.28

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 (533) hide show
  1. package/README.md +172 -219
  2. package/README.zh-CN.md +222 -0
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-booking-copilot.js +53 -0
  5. package/bin/gotry-bootstrap.js +1788 -0
  6. package/bin/gotry-inner.js +733 -83
  7. package/bin/gotry-process-liveness.d.ts +27 -0
  8. package/bin/gotry-process-liveness.js +97 -0
  9. package/bin/gotry-runtime-resolution.d.ts +27 -0
  10. package/bin/gotry-runtime-resolution.js +49 -0
  11. package/bin/gotry.js +1 -1
  12. package/client/client.js +203 -0
  13. package/cordis.gotry-patch.yml +80 -56
  14. package/data/airline-airports.json +39 -0
  15. package/data/stations-12306-verify.json +524 -0
  16. package/dist/capabilities/agent-reach-deep.js +45 -122
  17. package/dist/capabilities/agent-reach.js +101 -146
  18. package/dist/capabilities/anything.js +161 -94
  19. package/dist/capabilities/artifacts.js +370 -0
  20. package/dist/capabilities/channel-health.js +131 -0
  21. package/dist/capabilities/channel-registry.js +256 -0
  22. package/dist/capabilities/doctor.js +553 -0
  23. package/dist/capabilities/effect.js +394 -0
  24. package/dist/capabilities/external-event.js +149 -0
  25. package/dist/capabilities/fact-log.js +55 -0
  26. package/dist/capabilities/flyai-config.js +337 -0
  27. package/dist/capabilities/flyai.js +947 -110
  28. package/dist/capabilities/fx-contract.js +293 -0
  29. package/dist/capabilities/geo-atlas.js +246 -0
  30. package/dist/capabilities/ground-transfer.js +676 -0
  31. package/dist/capabilities/hbcli.js +260 -123
  32. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  33. package/dist/capabilities/incident-log.js +131 -61
  34. package/dist/capabilities/itinerary-artifact.js +190 -0
  35. package/dist/capabilities/itinerary-deck-artifact.js +194 -0
  36. package/dist/capabilities/itinerary-deck-export.js +339 -0
  37. package/dist/capabilities/lavish-local.js +924 -0
  38. package/dist/capabilities/model-override.js +38 -0
  39. package/dist/capabilities/opensky.js +54 -43
  40. package/dist/capabilities/provider-detail-sanitize.js +64 -0
  41. package/dist/capabilities/resilience.js +191 -0
  42. package/dist/capabilities/route-provider-conformance.js +341 -0
  43. package/dist/capabilities/session/action-cache.js +50 -44
  44. package/dist/capabilities/session/adapters/ctrip-flight.js +120 -62
  45. package/dist/capabilities/session/adapters/ctrip-hotel.js +222 -0
  46. package/dist/capabilities/session/adapters/dida-portal.js +274 -0
  47. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  48. package/dist/capabilities/session/adapters/rail-12306.js +342 -0
  49. package/dist/capabilities/session/benchmark.js +189 -0
  50. package/dist/capabilities/session/bridge-ledger.js +224 -0
  51. package/dist/capabilities/session/extension-bridge.js +636 -0
  52. package/dist/capabilities/session/extension-channel.js +109 -0
  53. package/dist/capabilities/session/extension-distribution.js +230 -0
  54. package/dist/capabilities/session/extract.js +31 -21
  55. package/dist/capabilities/session/golden-score.js +114 -0
  56. package/dist/capabilities/session/health-watch.js +215 -0
  57. package/dist/capabilities/session/read-guard.js +75 -24
  58. package/dist/capabilities/session/static-flight-golden.js +150 -0
  59. package/dist/capabilities/session/transport.js +97 -83
  60. package/dist/capabilities/session/wizard.js +117 -0
  61. package/dist/capabilities/session-consent.js +178 -0
  62. package/dist/capabilities/session-login.js +170 -0
  63. package/dist/capabilities/session-search.js +635 -44
  64. package/dist/capabilities/spawn-bounded.js +203 -0
  65. package/dist/capabilities/sponsor-plugin.js +341 -0
  66. package/dist/capabilities/visa-policy.js +140 -0
  67. package/dist/capabilities/weather.js +243 -138
  68. package/dist/data/session-golden-20.json +25 -0
  69. package/dist/data/sf-golden-manifest.json +103 -0
  70. package/dist/data/sf-static-routes.json +91 -0
  71. package/dist/scripts/action-cache-tests.js +24 -59
  72. package/dist/scripts/agent-planning-turn-deadline-e2e.js +242 -0
  73. package/dist/scripts/agent-planning-turn-deadline-tests.js +185 -0
  74. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  75. package/dist/scripts/agent-reach-error-tests.js +354 -0
  76. package/dist/scripts/agent-reach-tests.js +26 -18
  77. package/dist/scripts/agent-reach-wrapper-tests.js +50 -50
  78. package/dist/scripts/anything-path-measure-tests.js +133 -0
  79. package/dist/scripts/anything-path-measure.js +150 -0
  80. package/dist/scripts/anything-tests.js +250 -60
  81. package/dist/scripts/artifact-client-contract-tests.js +220 -0
  82. package/dist/scripts/artifacts-capability-tests.js +727 -0
  83. package/dist/scripts/async-collect.js +50 -11
  84. package/dist/scripts/benchmark-environment-bridge-e2e.js +741 -0
  85. package/dist/scripts/benchmark-environment-bridge-tests.js +1452 -0
  86. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +126 -0
  87. package/dist/scripts/booking-copilot-availability-policy-tests.js +265 -0
  88. package/dist/scripts/booking-copilot-bin-proof-tests.js +66 -0
  89. package/dist/scripts/booking-copilot-cleanup-proof-helper.js +76 -0
  90. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +144 -0
  91. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +424 -0
  92. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +2839 -0
  93. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +309 -0
  94. package/dist/scripts/booking-copilot-dsh-readiness-proof.js +238 -0
  95. package/dist/scripts/booking-copilot-dsh-warmer-proof.js +126 -0
  96. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +156 -0
  97. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +70 -0
  98. package/dist/scripts/booking-copilot-intent-proof-tests.js +422 -0
  99. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +184 -0
  100. package/dist/scripts/booking-copilot-planner-live-probe.js +95 -0
  101. package/dist/scripts/booking-copilot-proof-runtime.js +42 -0
  102. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +210 -0
  103. package/dist/scripts/booking-copilot-runtime-proof-tests.js +2349 -0
  104. package/dist/scripts/booking-copilot-server-proof-tests.js +784 -0
  105. package/dist/scripts/booking-copilot-startup-proof-tests.js +94 -0
  106. package/dist/scripts/booking-executor-tests.js +80 -0
  107. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  108. package/dist/scripts/booking-saga-tests.js +125 -0
  109. package/dist/scripts/booking-surface-contract-proof-tests.js +212 -0
  110. package/dist/scripts/bootstrap-tests.js +1549 -0
  111. package/dist/scripts/bridge-events-tests.js +239 -0
  112. package/dist/scripts/bridge-ledger-tests.js +428 -0
  113. package/dist/scripts/build-changelog.js +238 -0
  114. package/dist/scripts/build-metrics-report.js +331 -0
  115. package/dist/scripts/changelog-tests.js +118 -0
  116. package/dist/scripts/channel-probe-tests.js +103 -0
  117. package/dist/scripts/channel-probe.js +169 -0
  118. package/dist/scripts/channel-registry-tests.js +208 -0
  119. package/dist/scripts/city-scenario-tier-tests.js +234 -0
  120. package/dist/scripts/companion-tests.js +25 -70
  121. package/dist/scripts/dida-cookie-probe.js +87 -0
  122. package/dist/scripts/dida-runner-stop-tests.js +311 -0
  123. package/dist/scripts/dida-sw-probe.js +114 -0
  124. package/dist/scripts/diff-test.js +17 -12
  125. package/dist/scripts/doctor-tests.js +600 -0
  126. package/dist/scripts/drill-multiuser-ledger-tests.js +1076 -0
  127. package/dist/scripts/drill-sdk-descendant-cleanup-tests.js +606 -0
  128. package/dist/scripts/drill-w2a-sensor-tests.js +788 -0
  129. package/dist/scripts/dsh-artifact-e2e.js +279 -0
  130. package/dist/scripts/dsh-artifact-web-e2e.js +675 -0
  131. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  132. package/dist/scripts/dsh-llm-tests.js +437 -0
  133. package/dist/scripts/dsh-runtime-closure-tests.js +182 -0
  134. package/dist/scripts/dsh-runtime-closure.js +162 -0
  135. package/dist/scripts/dsh-session-v3-migration-proof.js +109 -0
  136. package/dist/scripts/dsh-subprocess-local-proof.js +150 -0
  137. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  138. package/dist/scripts/effect-tests.js +539 -0
  139. package/dist/scripts/engine-run.js +1 -3
  140. package/dist/scripts/engine-tests.js +16 -10
  141. package/dist/scripts/evaluation-cadence-tests.js +174 -0
  142. package/dist/scripts/evaluation-contract-tests.js +268 -0
  143. package/dist/scripts/extension-distribution-cli.js +39 -0
  144. package/dist/scripts/extension-distribution-tests.js +443 -0
  145. package/dist/scripts/extension-tests.js +988 -0
  146. package/dist/scripts/external-event-tests.js +350 -0
  147. package/dist/scripts/fact-gate-tests.js +869 -0
  148. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  149. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  150. package/dist/scripts/flight-malformed-tests.js +159 -0
  151. package/dist/scripts/flyai-setup-tests.js +228 -0
  152. package/dist/scripts/flyai-setup-tool-tests.js +54 -0
  153. package/dist/scripts/flyai-tests.js +692 -0
  154. package/dist/scripts/flyai-tool-registration-contract-tests.js +91 -0
  155. package/dist/scripts/fx-contract-tests.js +201 -0
  156. package/dist/scripts/g5-guard-tests.js +150 -0
  157. package/dist/scripts/g5-guard.js +193 -0
  158. package/dist/scripts/gen-lunar-anchors.js +106 -0
  159. package/dist/scripts/geo-atlas-tests.js +283 -0
  160. package/dist/scripts/gotry-backend-kernel-tests.js +206 -0
  161. package/dist/scripts/gotry-try-demo-tests.js +79 -0
  162. package/dist/scripts/gotry-try-demo.js +49 -0
  163. package/dist/scripts/ground-transfer-tests.js +956 -0
  164. package/dist/scripts/hbcli-e2e-tests.js +159 -0
  165. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  166. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  167. package/dist/scripts/hbcli-tests.js +121 -69
  168. package/dist/scripts/health-watch-cli.js +73 -0
  169. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  170. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  171. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  172. package/dist/scripts/i18n-tests.js +41 -0
  173. package/dist/scripts/incident-tests.js +281 -87
  174. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  175. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  176. package/dist/scripts/issue-271-liveness-tests.js +569 -0
  177. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  178. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  179. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  180. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  181. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  182. package/dist/scripts/issue436-persisted-routing-e2e.js +247 -0
  183. package/dist/scripts/itinerary-artifact-tests.js +368 -0
  184. package/dist/scripts/itinerary-deck-artifact-tests.js +416 -0
  185. package/dist/scripts/itinerary-deck-export-tests.js +552 -0
  186. package/dist/scripts/itinerary-deck-tests.js +435 -0
  187. package/dist/scripts/itinerary-html-tests.js +426 -0
  188. package/dist/scripts/journey-tests.js +22 -27
  189. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  190. package/dist/scripts/kernel-manifest-gate.js +126 -0
  191. package/dist/scripts/kernel-manifest-tests.js +212 -0
  192. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  193. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  194. package/dist/scripts/kernel-manifest.js +369 -0
  195. package/dist/scripts/lavish-local-tests.js +1226 -0
  196. package/dist/scripts/lavish-product-tools-tests.js +537 -0
  197. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  198. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  199. package/dist/scripts/ledger-tests.js +382 -246
  200. package/dist/scripts/ledger-workflow-crash.js +10 -20
  201. package/dist/scripts/m3-cohort-tests.js +714 -0
  202. package/dist/scripts/m3-cohort.js +310 -0
  203. package/dist/scripts/managed-dsh-cleanup-diagnostic-proof.js +184 -0
  204. package/dist/scripts/managed-dsh-close-failure-proof.js +172 -0
  205. package/dist/scripts/managed-dsh-run-port-proof.js +153 -0
  206. package/dist/scripts/managed-dsh-terminal-proof.js +252 -0
  207. package/dist/scripts/map-tools-vendor-package-proof.js +365 -0
  208. package/dist/scripts/memory-capture-tests.js +242 -61
  209. package/dist/scripts/memory-decay-tests.js +18 -52
  210. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  211. package/dist/scripts/memory-lifecycle.js +196 -0
  212. package/dist/scripts/memory-metrics.js +24 -12
  213. package/dist/scripts/memory-value-report.js +631 -0
  214. package/dist/scripts/memory-value-tests.js +328 -0
  215. package/dist/scripts/metrics-report-tests.js +151 -0
  216. package/dist/scripts/model-override-e2e.js +137 -0
  217. package/dist/scripts/nightly-evidence-tests.js +203 -0
  218. package/dist/scripts/nightly-evidence.js +303 -0
  219. package/dist/scripts/nudge-digest.js +39 -28
  220. package/dist/scripts/observability-tests.js +381 -0
  221. package/dist/scripts/onboarding-tests.js +161 -0
  222. package/dist/scripts/opensky-check.js +18 -13
  223. package/dist/scripts/opensky-tests.js +28 -28
  224. package/dist/scripts/outcome-projection-tests.js +427 -0
  225. package/dist/scripts/persona-sim-tests.js +603 -0
  226. package/dist/scripts/persona-sim.js +1081 -0
  227. package/dist/scripts/persona-surface-guard-tests.js +49 -0
  228. package/dist/scripts/pnpm-dsh-closure-proof.js +17 -0
  229. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  230. package/dist/scripts/price-drift-tests.js +340 -0
  231. package/dist/scripts/price-drift-watch.js +447 -0
  232. package/dist/scripts/probe-poi-tests.js +53 -45
  233. package/dist/scripts/product-metrics.js +483 -0
  234. package/dist/scripts/publish-preverify.js +110 -0
  235. package/dist/scripts/realtime-pricing-tests.js +76 -0
  236. package/dist/scripts/recall-tests.js +513 -0
  237. package/dist/scripts/replay-async.js +22 -18
  238. package/dist/scripts/replay-real.js +18 -18
  239. package/dist/scripts/replay.js +54 -82
  240. package/dist/scripts/route-provider-conformance-tests.js +391 -0
  241. package/dist/scripts/session-attach-diagnose.js +20 -20
  242. package/dist/scripts/session-attach-poc.js +34 -37
  243. package/dist/scripts/session-benchmark.js +180 -0
  244. package/dist/scripts/session-dida-live-e2e.js +199 -0
  245. package/dist/scripts/session-extract-tests.js +14 -35
  246. package/dist/scripts/session-link-tests.js +478 -0
  247. package/dist/scripts/session-login.js +27 -46
  248. package/dist/scripts/session-tests.js +796 -96
  249. package/dist/scripts/session-zone-crash.js +49 -0
  250. package/dist/scripts/session-zone-ledger-tests.js +432 -0
  251. package/dist/scripts/session-zone-observation-tests.js +492 -0
  252. package/dist/scripts/session-zone-observe.js +135 -0
  253. package/dist/scripts/session-zone-wiring-tests.js +566 -0
  254. package/dist/scripts/session-zones-tests.js +374 -0
  255. package/dist/scripts/sf-live-benchmark.js +367 -0
  256. package/dist/scripts/sf-live-challenge-stop-tests.js +453 -0
  257. package/dist/scripts/sf-live-cli-tests.js +92 -0
  258. package/dist/scripts/sf-live-options.js +46 -0
  259. package/dist/scripts/sf-manifest-tests.js +30 -0
  260. package/dist/scripts/sf-manifest.js +33 -0
  261. package/dist/scripts/sf-soft-score-tests.js +67 -0
  262. package/dist/scripts/sf-summary-tests.js +324 -0
  263. package/dist/scripts/sf-summary.js +525 -0
  264. package/dist/scripts/share-tests.js +432 -0
  265. package/dist/scripts/skeleton-check.js +16 -14
  266. package/dist/scripts/skeleton-integration-test.js +28 -11
  267. package/dist/scripts/skills-contract-tests.js +51 -46
  268. package/dist/scripts/smoke-session-gate-tests.js +21 -0
  269. package/dist/scripts/smoke.js +690 -246
  270. package/dist/scripts/spawn-bounded-abort-tests.js +244 -0
  271. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  272. package/dist/scripts/state-cli-tests.js +226 -101
  273. package/dist/scripts/state-cli.js +565 -198
  274. package/dist/scripts/state-repair-cli.js +66 -0
  275. package/dist/scripts/state-repair-tests.js +140 -0
  276. package/dist/scripts/static-golden-tests.js +165 -0
  277. package/dist/scripts/time-eval-tests.js +212 -192
  278. package/dist/scripts/travel-timeline-tests.js +29 -93
  279. package/dist/scripts/turn-handoff-collect-tests.js +169 -0
  280. package/dist/scripts/turn-handoff-collect.js +161 -0
  281. package/dist/scripts/turn-policy-tests.js +55 -0
  282. package/dist/scripts/typed-contract-canary.js +176 -0
  283. package/dist/scripts/tz-resolver-sanity.js +50 -0
  284. package/dist/scripts/tz-resolver-tests.js +584 -0
  285. package/dist/scripts/unified-tests.js +60 -20
  286. package/dist/scripts/visa-policy-tests.js +62 -0
  287. package/dist/scripts/weather-tests.js +276 -41
  288. package/dist/scripts/wish-channel-gate-tests.js +54 -0
  289. package/dist/scripts/write-gate-tests.js +522 -0
  290. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  291. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  292. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  293. package/dist/scripts/z3-race-tests.js +46 -0
  294. package/dist/src/artifact-gate.js +829 -0
  295. package/dist/src/backend/kernel.js +91 -0
  296. package/dist/src/backend/modules/booking-copilot.js +92 -0
  297. package/dist/src/backend/modules/booking-executor.js +124 -0
  298. package/dist/src/backend/modules/session-search.js +551 -0
  299. package/dist/src/benchmark-agent-conformance.js +578 -0
  300. package/dist/src/benchmark-environment-bridge.js +515 -0
  301. package/dist/src/benchmark-headless-child-diagnostics.js +166 -0
  302. package/dist/src/benchmark-tool-isolation.js +151 -0
  303. package/dist/src/bookable-facts.js +533 -0
  304. package/dist/src/booking-saga.js +97 -0
  305. package/dist/src/booking-surface/availability-policy.js +697 -0
  306. package/dist/src/booking-surface/booking-intent.js +43 -0
  307. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  308. package/dist/src/booking-surface/canonical-schema.js +116 -0
  309. package/dist/src/booking-surface/contracts.js +75 -0
  310. package/dist/src/booking-surface/dsh-planner.js +1854 -0
  311. package/dist/src/booking-surface/dsh-plugin.js +378 -0
  312. package/dist/src/booking-surface/dsh-worker.js +98 -0
  313. package/dist/src/booking-surface/error-codes.js +231 -0
  314. package/dist/src/booking-surface/index.js +9 -0
  315. package/dist/src/booking-surface/managed-dsh-cleanup-diagnostic.js +97 -0
  316. package/dist/src/booking-surface/managed-dsh-run-port.js +244 -0
  317. package/dist/src/booking-surface/profile.js +24 -0
  318. package/dist/src/booking-surface/recovery-chain.js +367 -0
  319. package/dist/src/booking-surface/runtime.js +1968 -0
  320. package/dist/src/booking-surface/server.js +674 -0
  321. package/dist/src/booking-surface/startup.js +168 -0
  322. package/dist/src/booking-surface/validation.js +261 -0
  323. package/dist/src/bridge.js +20 -13
  324. package/dist/src/city-scenario-tier.js +378 -0
  325. package/dist/src/companions.js +48 -74
  326. package/dist/src/contracts.js +21 -43
  327. package/dist/src/dsh-llm.js +171 -115
  328. package/dist/src/engine.js +276 -193
  329. package/dist/src/evaluation-cadence.js +97 -0
  330. package/dist/src/evaluation-contracts.js +283 -0
  331. package/dist/src/flight-pack-adapter.js +54 -0
  332. package/dist/src/flight-pack-contract.js +2 -0
  333. package/dist/src/flyai-setup-tool.js +70 -0
  334. package/dist/src/gotry-backend.js +125 -0
  335. package/dist/src/hotel-date-gate.js +118 -0
  336. package/dist/src/i18n.js +88 -0
  337. package/dist/src/index.js +2385 -881
  338. package/dist/src/itinerary-deck.js +139 -0
  339. package/dist/src/itinerary-doc-shared.js +661 -0
  340. package/dist/src/itinerary-html.js +131 -0
  341. package/dist/src/journey.js +155 -108
  342. package/dist/src/lavish-tools.js +400 -0
  343. package/dist/src/ledger-repair-apply.js +538 -0
  344. package/dist/src/ledger-repair-plan.js +463 -0
  345. package/dist/src/loop.js +331 -161
  346. package/dist/src/m3-cohort.js +1577 -0
  347. package/dist/src/memory-capture.js +180 -32
  348. package/dist/src/memory-decay.js +30 -14
  349. package/dist/src/memory-lifecycle.js +1310 -0
  350. package/dist/src/memory-utility.js +27 -40
  351. package/dist/src/mock-llm.js +36 -61
  352. package/dist/src/model.js +81 -36
  353. package/dist/src/outcome-projection.js +746 -0
  354. package/dist/src/realtime-pricing.js +103 -0
  355. package/dist/src/recall/card.js +74 -0
  356. package/dist/src/recall/evaluator.js +140 -0
  357. package/dist/src/recall/integration.js +17 -0
  358. package/dist/src/recall/tick.js +166 -0
  359. package/dist/src/session-link/plan-it-action.js +48 -0
  360. package/dist/src/session-link/session-link.js +248 -0
  361. package/dist/src/session-zone-ledger.js +312 -0
  362. package/dist/src/session-zone-observation.js +354 -0
  363. package/dist/src/session-zone-wiring.js +658 -0
  364. package/dist/src/session-zones.js +510 -0
  365. package/dist/src/share/adapters.js +51 -0
  366. package/dist/src/share/share-consent.js +76 -0
  367. package/dist/src/share/share-deck.js +58 -0
  368. package/dist/src/share/share-token.js +146 -0
  369. package/dist/src/slot-spec.js +84 -106
  370. package/dist/src/state-ledger.js +696 -524
  371. package/dist/src/state-repair.js +299 -0
  372. package/dist/src/subagent-job-id-guard.js +58 -0
  373. package/dist/src/time-anchor.js +231 -33
  374. package/dist/src/tool-packet.js +20 -8
  375. package/dist/src/travel-slots.js +58 -35
  376. package/dist/src/travel-timeline.js +44 -63
  377. package/dist/src/turn-deadline.js +287 -0
  378. package/dist/src/turn-policy.js +118 -0
  379. package/dist/src/tz-resolver.js +143 -0
  380. package/dist/src/unified.js +600 -360
  381. package/dist/src/wish-pool.js +42 -12
  382. package/dist/src/write-gate.js +495 -0
  383. package/dist/src/z3-shared.js +284 -0
  384. package/extension/README.md +21 -0
  385. package/extension/README.zh-CN.md +21 -0
  386. package/extension/background.js +463 -0
  387. package/extension/content-bridge.js +270 -0
  388. package/extension/content-main.js +111 -0
  389. package/extension/manifest.json +60 -0
  390. package/package.json +407 -14
  391. package/schemas/booking.intent.schema.json +193 -0
  392. package/schemas/booking.surface.schema.json +2622 -0
  393. package/ts/capabilities/agent-reach-deep.ts +114 -0
  394. package/ts/capabilities/agent-reach.ts +198 -0
  395. package/ts/capabilities/anything.ts +282 -0
  396. package/ts/capabilities/artifacts.ts +428 -0
  397. package/ts/capabilities/fact-log.ts +54 -0
  398. package/ts/capabilities/flyai-config.ts +388 -0
  399. package/ts/capabilities/flyai.ts +1274 -0
  400. package/ts/capabilities/hbcli.ts +253 -71
  401. package/ts/capabilities/incident-log.ts +88 -59
  402. package/ts/capabilities/itinerary-artifact.ts +214 -0
  403. package/ts/capabilities/itinerary-deck-artifact.ts +218 -0
  404. package/ts/capabilities/itinerary-deck-export.ts +378 -0
  405. package/ts/capabilities/opensky.ts +121 -0
  406. package/ts/capabilities/session/action-cache.ts +148 -0
  407. package/ts/capabilities/session/adapters/ctrip-flight.ts +181 -0
  408. package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
  409. package/ts/capabilities/session/adapters/dida-portal.ts +338 -0
  410. package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
  411. package/ts/capabilities/session/adapters/rail-12306.ts +392 -0
  412. package/ts/capabilities/session/benchmark.ts +276 -0
  413. package/ts/capabilities/session/bridge-ledger.ts +333 -0
  414. package/ts/capabilities/session/extension-bridge.ts +794 -0
  415. package/ts/capabilities/session/extension-channel.ts +144 -0
  416. package/ts/capabilities/session/extension-distribution.ts +266 -0
  417. package/ts/capabilities/session/extract.ts +54 -0
  418. package/ts/capabilities/session/golden-score.ts +139 -0
  419. package/ts/capabilities/session/health-watch.ts +264 -0
  420. package/ts/capabilities/session/read-guard.ts +141 -0
  421. package/ts/capabilities/session/static-flight-golden.ts +209 -0
  422. package/ts/capabilities/session/transport.ts +146 -0
  423. package/ts/capabilities/session/wizard.ts +140 -0
  424. package/ts/capabilities/session-consent.ts +211 -0
  425. package/ts/capabilities/session-login.ts +208 -0
  426. package/ts/capabilities/session-search.ts +893 -0
  427. package/ts/capabilities/weather.ts +330 -0
  428. package/ts/dsh-runtime/vendor/README.md +68 -0
  429. package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
  430. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +405 -0
  431. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +404 -0
  432. package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +1584 -0
  433. package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
  434. package/ts/dsh-runtime/vendor/dsh-map-tools/icon.svg +5 -0
  435. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +595 -0
  436. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
  437. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +58 -0
  438. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
  439. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
  440. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +131 -0
  441. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
  442. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/geo.js +232 -0
  443. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +120 -0
  444. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +40 -0
  445. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/staticmap-route.js +115 -0
  446. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
  447. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
  448. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +267 -0
  449. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +90 -0
  450. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
  451. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
  452. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
  453. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
  454. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +23 -0
  455. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
  456. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/geo.d.ts +61 -0
  457. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
  458. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
  459. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/staticmap-route.d.ts +31 -0
  460. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
  461. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
  462. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +75 -0
  463. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +68 -0
  464. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
  465. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +97 -0
  466. package/ts/package.json +293 -3
  467. package/ts/scripts/async-collect.ts +91 -0
  468. package/ts/scripts/map-tools-vendor-package-proof.ts +404 -0
  469. package/ts/scripts/skeleton-check.ts +1 -1
  470. package/ts/scripts/skeleton-integration-test.ts +23 -1
  471. package/ts/scripts/state-cli.ts +586 -0
  472. package/ts/scripts/turn-handoff-collect.ts +178 -0
  473. package/ts/src/artifact-gate.ts +991 -0
  474. package/ts/src/benchmark-agent-conformance.ts +619 -0
  475. package/ts/src/benchmark-environment-bridge.ts +538 -0
  476. package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
  477. package/ts/src/benchmark-tool-isolation.ts +166 -0
  478. package/ts/src/bookable-facts.ts +837 -0
  479. package/ts/src/booking-saga.ts +133 -0
  480. package/ts/src/booking-surface/availability-policy.ts +522 -0
  481. package/ts/src/booking-surface/booking-intent.ts +86 -0
  482. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  483. package/ts/src/booking-surface/canonical-schema.js +116 -0
  484. package/ts/src/booking-surface/contracts.ts +475 -0
  485. package/ts/src/booking-surface/dsh-planner.ts +1949 -0
  486. package/ts/src/booking-surface/dsh-plugin.js +378 -0
  487. package/ts/src/booking-surface/dsh-worker.ts +110 -0
  488. package/ts/src/booking-surface/error-codes.ts +241 -0
  489. package/ts/src/booking-surface/index.ts +9 -0
  490. package/ts/src/booking-surface/managed-dsh-cleanup-diagnostic.ts +151 -0
  491. package/ts/src/booking-surface/managed-dsh-run-port.ts +273 -0
  492. package/ts/src/booking-surface/profile.ts +42 -0
  493. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  494. package/ts/src/booking-surface/runtime.ts +1793 -0
  495. package/ts/src/booking-surface/server.ts +733 -0
  496. package/ts/src/booking-surface/startup.ts +201 -0
  497. package/ts/src/booking-surface/validation.ts +217 -0
  498. package/ts/src/companions.ts +113 -0
  499. package/ts/src/contracts.ts +17 -3
  500. package/ts/src/dsh-llm.ts +152 -17
  501. package/ts/src/flyai-setup-tool.ts +77 -0
  502. package/ts/src/index.ts +2123 -200
  503. package/ts/src/itinerary-deck.ts +189 -0
  504. package/ts/src/itinerary-doc-shared.ts +741 -0
  505. package/ts/src/itinerary-html.ts +171 -0
  506. package/ts/src/loop.ts +234 -26
  507. package/ts/src/memory-capture.ts +283 -0
  508. package/ts/src/memory-decay.ts +55 -0
  509. package/ts/src/memory-utility.ts +70 -0
  510. package/ts/src/mock-llm.ts +4 -8
  511. package/ts/src/model.ts +88 -2
  512. package/ts/src/recall/card.ts +106 -0
  513. package/ts/src/recall/evaluator.ts +170 -0
  514. package/ts/src/recall/integration.ts +35 -0
  515. package/ts/src/recall/tick.ts +213 -0
  516. package/ts/src/session-link/plan-it-action.ts +78 -0
  517. package/ts/src/session-link/session-link.ts +275 -0
  518. package/ts/src/share/adapters.ts +93 -0
  519. package/ts/src/share/share-consent.ts +105 -0
  520. package/ts/src/share/share-deck.ts +85 -0
  521. package/ts/src/share/share-token.ts +166 -0
  522. package/ts/src/slot-spec.ts +172 -0
  523. package/ts/src/state-ledger.ts +1166 -0
  524. package/ts/src/subagent-job-id-guard.ts +72 -0
  525. package/ts/src/time-anchor.ts +338 -0
  526. package/ts/src/tool-packet.ts +32 -0
  527. package/ts/src/travel-slots.ts +219 -0
  528. package/ts/src/travel-timeline.ts +83 -0
  529. package/ts/src/turn-deadline.ts +366 -0
  530. package/ts/src/turn-policy.ts +154 -0
  531. package/ts/src/unified.ts +444 -105
  532. package/ts/src/wish-pool.ts +79 -0
  533. package/dist/scripts/session-attach-wait.js +0 -41
@@ -0,0 +1,1226 @@
1
+ /**
2
+ * Local Lavish Editor adapter tests (issue #443, parent #438).
3
+ *
4
+ * Offline section: a synthetic `lavish-axi` package tree exercises the adapter's
5
+ * trust checks, path allowlist, argv shape, byte/timeout bounds, state machine,
6
+ * process-group reaping, and lifecycle rules. Nothing here touches the network,
7
+ * a real browser, or any real Lavish installation.
8
+ *
9
+ * Live section (opt-in, GOTRY_LAVISH_LIVE=1): installs the pinned `lavish-axi@0.1.67`
10
+ * into a unique mkdtemp-scoped npm prefix from the public registry and runs the real
11
+ * CLI open / poll(waiting) / end / stop protocol against a synthetic HTML fixture.
12
+ *
13
+ * Run: cd ts && npx tsx scripts/lavish-local-tests.ts
14
+ * cd ts && GOTRY_LAVISH_LIVE=1 npx tsx scripts/lavish-local-tests.ts
15
+ */
16
+ import assert from 'node:assert/strict';
17
+ import { createRequire } from 'node:module';
18
+ import { spawn, spawnSync } from 'node:child_process';
19
+ import { createServer } from 'node:http';
20
+ import { mkdir, mkdtemp, readFile, realpath, rm, stat, symlink, writeFile } from 'node:fs/promises';
21
+ import { connect } from 'node:net';
22
+ import { tmpdir } from 'node:os';
23
+ import { join } from 'node:path';
24
+ import { LAVISH_AXI_VERSION, LavishLocalError, LavishLocalSession, allocateUnusedLoopbackPort, buildChildEnvironment, readTrustedCli, resolveArtifactPath, } from '../capabilities/lavish-local.js';
25
+ const MAX_PROMPT_FIELD_CHARS_TEST = 16 * 1024;
26
+ const MAX_PROMPT_FIELD_BYTES_TEST = 16 * 1024;
27
+ const TOON_ENTRY = createRequire(import.meta.url).resolve('@toon-format/toon');
28
+ const workRoot = await mkdtemp(join(tmpdir(), 'gotry-lavish-local-tests-'));
29
+ let assertions = 0;
30
+ function ok(condition, detail) {
31
+ assertions += 1;
32
+ assert.ok(condition, detail);
33
+ }
34
+ function expectFailure(result, code) {
35
+ assertions += 1;
36
+ assert.equal(result.ok, false, `expected failure(${code}), got ${JSON.stringify(result)}`);
37
+ const failure = result;
38
+ assert.equal(failure.code, code, `expected code ${code}, got ${failure.code} (${failure.detail})`);
39
+ assertNoLeak(failure.detail);
40
+ return failure;
41
+ }
42
+ const PROMPT_TEXT_SENTINEL = 'prompt-text-sentinel-must-not-leak';
43
+ const REPLY_SENTINEL = 'reply-sentinel-must-not-leak';
44
+ function assertNoLeak(text) {
45
+ assertions += 1;
46
+ assert.ok(!text.includes(PROMPT_TEXT_SENTINEL), `leaked prompt text: ${text}`);
47
+ assert.ok(!text.includes(REPLY_SENTINEL), `leaked reply text: ${text}`);
48
+ }
49
+ // ---------------------------------------------------------------------------
50
+ // synthetic lavish-axi package
51
+ // ---------------------------------------------------------------------------
52
+ const FAKE_CLI_BODY = `
53
+ import { appendFileSync, readFileSync, writeFileSync } from 'node:fs'
54
+ import { createServer } from 'node:http'
55
+ import { join } from 'node:path'
56
+ import { encode } from ${JSON.stringify(TOON_ENTRY)}
57
+
58
+ const stateDir = process.env.LAVISH_AXI_STATE_DIR
59
+ const port = Number(process.env.LAVISH_AXI_PORT)
60
+ const argv = process.argv.slice(2)
61
+
62
+ appendFileSync(join(stateDir, 'argv.jsonl'), JSON.stringify(argv) + '\\n')
63
+ appendFileSync(join(stateDir, 'env.jsonl'), JSON.stringify(process.env) + '\\n')
64
+
65
+ function control() {
66
+ try {
67
+ return JSON.parse(readFileSync(join(stateDir, 'control.json'), 'utf8'))
68
+ } catch {
69
+ return {}
70
+ }
71
+ }
72
+ function emit(value) {
73
+ process.stdout.write(encode(value) + '\\n')
74
+ }
75
+ function sessionUrl() {
76
+ return 'http://127.0.0.1:' + port + '/session/' + 'a'.repeat(16)
77
+ }
78
+ function writeOversize(stream, bytes) {
79
+ const chunk = 'x'.repeat(64 * 1024)
80
+ let written = 0
81
+ while (written < bytes) {
82
+ stream.write(chunk)
83
+ written += chunk.length
84
+ }
85
+ }
86
+
87
+ const command = argv[0]
88
+ const cfg = control()
89
+
90
+ if (command === 'server') {
91
+ writeFileSync(join(stateDir, 'server.pid'), String(process.pid))
92
+ const server = createServer((req, res) => {
93
+ if (req.url && req.url.indexOf('/health') === 0) {
94
+ res.setHeader('content-type', 'application/json')
95
+ res.end(JSON.stringify({ ok: true, app: 'lavish-axi', version: '${LAVISH_AXI_VERSION}' }))
96
+ return
97
+ }
98
+ res.statusCode = 404
99
+ res.end('{}')
100
+ })
101
+ server.listen(port, '127.0.0.1')
102
+ process.on('SIGTERM', () => { server.close(() => process.exit(0)); setTimeout(() => process.exit(0), 200) })
103
+ } else if (command === 'open') {
104
+ const file = argv[1]
105
+ const open = cfg.open || {}
106
+ if (open.oversizeStdout) writeOversize(process.stdout, open.oversizeStdout)
107
+ else if (open.oversizeStderr) writeOversize(process.stderr, open.oversizeStderr)
108
+ else if (open.stdoutRaw !== undefined) process.stdout.write(open.stdoutRaw)
109
+ else if (open.error) emit({ error: open.error, code: open.code || 'SERVER_ERROR' })
110
+ else emit({
111
+ session: { file: open.file || file, url: open.url || sessionUrl(), status: open.status || 'opened' },
112
+ next_step: 'synthetic next_step must never be surfaced as authority',
113
+ ...(open.self_paint_warning ? { self_paint_warning: open.self_paint_warning } : {}),
114
+ })
115
+ if (open.exitCode) process.exitCode = open.exitCode
116
+ } else if (command === 'poll') {
117
+ writeFileSync(join(stateDir, 'poll.pid'), String(process.pid))
118
+ const poll = cfg.poll || {}
119
+ const finish = () => {
120
+ if (poll.oversizeStdout) writeOversize(process.stdout, poll.oversizeStdout)
121
+ else if (poll.oversizeStderr) writeOversize(process.stderr, poll.oversizeStderr)
122
+ else if (poll.stdoutRaw !== undefined) process.stdout.write(poll.stdoutRaw)
123
+ else if (poll.error) emit({ error: poll.error, code: poll.code || 'SERVER_ERROR' })
124
+ else {
125
+ const status = poll.status || 'waiting'
126
+ emit({
127
+ session: {
128
+ file: poll.file || argv[1],
129
+ status,
130
+ ...(poll.session_ended ? { session_ended: true, ended_by: poll.ended_by || 'user' } : {}),
131
+ },
132
+ ...(status === 'feedback' ? { prompts: poll.prompts || [], artifact_failures: poll.artifact_failures || [] } : {}),
133
+ dom_snapshot: poll.dom_snapshot || '',
134
+ })
135
+ }
136
+ if (poll.exitCode) process.exitCode = poll.exitCode
137
+ }
138
+ const sleepMs = Number(poll.sleepMs || 0)
139
+ if (sleepMs > 0) setTimeout(finish, sleepMs)
140
+ else finish()
141
+ } else if (command === 'end') {
142
+ const end = cfg.end || {}
143
+ if (end.error) emit({ error: end.error, code: end.code || 'SERVER_ERROR' })
144
+ else emit({ session: { file: end.file || argv[1], status: end.status || 'ended' } })
145
+ if (end.exitCode) process.exitCode = end.exitCode
146
+ } else {
147
+ emit({ error: 'Unknown command: ' + command, code: 'VALIDATION_ERROR' })
148
+ process.exitCode = 2
149
+ }
150
+ `;
151
+ const fakePackageRoot = join(workRoot, 'fake-package', LAVISH_AXI_VERSION);
152
+ await mkdir(join(fakePackageRoot, 'dist'), { recursive: true });
153
+ await writeFile(join(fakePackageRoot, 'dist', 'cli.mjs'), FAKE_CLI_BODY);
154
+ function writeManifest(root, manifest) {
155
+ return writeFile(join(root, 'package.json'), `${JSON.stringify(manifest, null, 2)}\n`);
156
+ }
157
+ await writeManifest(fakePackageRoot, {
158
+ name: 'lavish-axi',
159
+ version: LAVISH_AXI_VERSION,
160
+ bin: { 'lavish-axi': 'dist/cli.mjs' },
161
+ });
162
+ await mkdir(join(workRoot, 'artifacts'), { recursive: true });
163
+ const artifactRoot = await realpath(join(workRoot, 'artifacts'));
164
+ const ARTIFACT = join(artifactRoot, 'card.html');
165
+ const ARTIFACT_BODY = '<!doctype html><html><body><h1>synthetic card</h1></body></html>\n';
166
+ await writeFile(ARTIFACT, ARTIFACT_BODY);
167
+ async function freshStateDir(label) {
168
+ return mkdtemp(join(workRoot, `state-${label}-`));
169
+ }
170
+ async function sessionFor(stateDir, extra = {}) {
171
+ return LavishLocalSession.create({
172
+ cliPackageRoot: fakePackageRoot,
173
+ cwd: artifactRoot,
174
+ allowedRoot: artifactRoot,
175
+ stateDir,
176
+ defaultPollTimeoutMs: 300,
177
+ maxPollTimeoutMs: 5_000,
178
+ commandTimeoutMs: 3_000,
179
+ ...extra,
180
+ });
181
+ }
182
+ /** Creates a session whose control file is already in place. */
183
+ async function controlSession(label, control, extra = {}) {
184
+ const stateDir = await freshStateDir(label);
185
+ await writeControl(stateDir, control);
186
+ return sessionFor(stateDir, extra);
187
+ }
188
+ async function readJsonl(path) {
189
+ try {
190
+ const raw = await readFile(path, 'utf8');
191
+ return raw.split('\n').filter(Boolean).map((line) => JSON.parse(line));
192
+ }
193
+ catch {
194
+ return [];
195
+ }
196
+ }
197
+ async function writeControl(stateDir, control) {
198
+ await writeFile(join(stateDir, 'control.json'), JSON.stringify(control));
199
+ }
200
+ function isProcessAlive(pid) {
201
+ try {
202
+ process.kill(pid, 0);
203
+ return true;
204
+ }
205
+ catch {
206
+ return false;
207
+ }
208
+ }
209
+ async function readPid(stateDir, name) {
210
+ try {
211
+ return Number(await readFile(join(stateDir, name), 'utf8'));
212
+ }
213
+ catch {
214
+ return null;
215
+ }
216
+ }
217
+ async function waitForHealth(port) {
218
+ const deadline = Date.now() + 10_000;
219
+ while (Date.now() < deadline) {
220
+ try {
221
+ const response = await fetch(`http://127.0.0.1:${port}/health`);
222
+ if (response.ok)
223
+ return true;
224
+ }
225
+ catch {
226
+ // keep waiting
227
+ }
228
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, 100));
229
+ }
230
+ return false;
231
+ }
232
+ const ownedFixtures = new Set();
233
+ const deferredCleanupErrors = [];
234
+ function trackFixture(pid, port, label) {
235
+ const entry = { pgid: pid, port, label };
236
+ ownedFixtures.add(entry);
237
+ return entry;
238
+ }
239
+ function probeGroup(pgid) {
240
+ try {
241
+ process.kill(-pgid, 0);
242
+ return { state: 'present' };
243
+ }
244
+ catch (error) {
245
+ const code = error.code;
246
+ return code === 'ESRCH'
247
+ ? { state: 'absent' }
248
+ : { state: 'error', detail: `group probe failed: ${code ?? String(error)}` };
249
+ }
250
+ }
251
+ function signalGroup(pgid, signal) {
252
+ try {
253
+ process.kill(-pgid, signal);
254
+ return { state: 'delivered' };
255
+ }
256
+ catch (error) {
257
+ const code = error.code;
258
+ return code === 'ESRCH'
259
+ ? { state: 'absent' }
260
+ : { state: 'error', detail: `${signal} failed: ${code ?? String(error)}` };
261
+ }
262
+ }
263
+ async function waitForGroupGone(pgid, timeoutMs) {
264
+ const deadline = Date.now() + timeoutMs;
265
+ while (true) {
266
+ const probe = probeGroup(pgid);
267
+ if (probe.state === 'absent')
268
+ return { gone: true };
269
+ if (probe.state === 'error')
270
+ return { gone: false, issue: probe.detail };
271
+ if (Date.now() >= deadline)
272
+ return { gone: false };
273
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, 50));
274
+ }
275
+ }
276
+ async function waitForPortClosed(port, timeoutMs) {
277
+ const deadline = Date.now() + timeoutMs;
278
+ while (true) {
279
+ const outcome = await new Promise((resolve) => {
280
+ const socket = connect(port, '127.0.0.1');
281
+ let settled = false;
282
+ const finish = (value) => {
283
+ if (settled)
284
+ return;
285
+ settled = true;
286
+ socket.destroy();
287
+ resolve(value);
288
+ };
289
+ socket.once('connect', () => finish('open'));
290
+ socket.once('error', (error) => {
291
+ const code = error.code;
292
+ finish(code === 'ECONNREFUSED' ? 'closed' : `port probe failed: ${code ?? String(error)}`);
293
+ });
294
+ socket.setTimeout(500, () => finish('port probe timed out'));
295
+ });
296
+ if (outcome === 'closed')
297
+ return { closed: true };
298
+ if (outcome !== 'open')
299
+ return { closed: false, issue: outcome };
300
+ if (Date.now() >= deadline)
301
+ return { closed: false, issue: 'port remained open' };
302
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, 50));
303
+ }
304
+ }
305
+ async function reapOwnedFixture(entry, options = {}) {
306
+ const graceMs = options.graceMs ?? 2_000;
307
+ const killGraceMs = options.killGraceMs ?? 2_000;
308
+ const portWaitMs = options.portWaitMs ?? 2_000;
309
+ const issues = [];
310
+ let groupGone = false;
311
+ let escalatedKill = false;
312
+ const term = signalGroup(entry.pgid, 'SIGTERM');
313
+ if (term.state === 'absent') {
314
+ groupGone = true;
315
+ }
316
+ else if (term.state === 'error') {
317
+ issues.push(term.detail);
318
+ }
319
+ else {
320
+ const waited = await waitForGroupGone(entry.pgid, graceMs);
321
+ groupGone = waited.gone;
322
+ if (waited.issue)
323
+ issues.push(waited.issue);
324
+ }
325
+ if (!groupGone && issues.length === 0) {
326
+ const kill = signalGroup(entry.pgid, 'SIGKILL');
327
+ escalatedKill = kill.state === 'delivered';
328
+ if (kill.state === 'absent') {
329
+ groupGone = true;
330
+ }
331
+ else if (kill.state === 'error') {
332
+ issues.push(kill.detail);
333
+ }
334
+ else {
335
+ const waited = await waitForGroupGone(entry.pgid, killGraceMs);
336
+ groupGone = waited.gone;
337
+ if (waited.issue)
338
+ issues.push(waited.issue);
339
+ else if (!waited.gone)
340
+ issues.push('process group survived SIGKILL');
341
+ }
342
+ }
343
+ const port = await waitForPortClosed(entry.port, portWaitMs);
344
+ if (port.issue)
345
+ issues.push(port.issue);
346
+ return { groupGone, portClosed: port.closed, escalatedKill, issues };
347
+ }
348
+ function reapSucceeded(result) {
349
+ return result.groupGone && result.portClosed && result.issues.length === 0;
350
+ }
351
+ function describeReap(entry, result) {
352
+ return `${entry.label} pgid=${entry.pgid} port=${entry.port} groupGone=${result.groupGone} portClosed=${result.portClosed} escalated=${result.escalatedKill} issues=${result.issues.join(',') || 'none'}`;
353
+ }
354
+ async function reapAllOwnedFixtures() {
355
+ const errors = [];
356
+ for (const entry of [...ownedFixtures]) {
357
+ try {
358
+ const result = await reapOwnedFixture(entry);
359
+ if (reapSucceeded(result))
360
+ ownedFixtures.delete(entry);
361
+ else
362
+ errors.push(new Error(`outer fixture cleanup failed: ${describeReap(entry, result)}`));
363
+ }
364
+ catch (error) {
365
+ errors.push(new Error(`outer fixture cleanup threw for ${entry.label} pgid=${entry.pgid}: ${asError(error).message}`));
366
+ }
367
+ }
368
+ return errors;
369
+ }
370
+ function asError(error) {
371
+ return error instanceof Error ? error : new Error(String(error));
372
+ }
373
+ function throwCollected(errors) {
374
+ if (errors.length === 0)
375
+ return;
376
+ if (errors.length === 1)
377
+ throw errors[0];
378
+ throw new AggregateError(errors, errors.map((error) => error.message).join(' | '));
379
+ }
380
+ let suitePrimary = undefined;
381
+ try {
382
+ // -------------------------------------------------------------------------
383
+ // 1. trusted CLI resolution
384
+ // -------------------------------------------------------------------------
385
+ {
386
+ const good = await readTrustedCli(fakePackageRoot);
387
+ ok(good.version === LAVISH_AXI_VERSION, 'the pinned version resolves');
388
+ ok(good.entry.endsWith('dist/cli.mjs'), `the entry resolves to the declared bin: ${good.entry}`);
389
+ await assert.rejects(() => readTrustedCli(join(workRoot, 'missing-package')), (error) => {
390
+ ok(error instanceof LavishLocalError && error.code === 'invalid-options', 'a missing package root is refused');
391
+ return true;
392
+ });
393
+ await assert.rejects(() => readTrustedCli('relative/path'), (error) => {
394
+ ok(error instanceof LavishLocalError && error.code === 'invalid-options', 'a relative cli root is refused');
395
+ return true;
396
+ });
397
+ const cases = [
398
+ ['wrong-name', { name: 'not-lavish', version: LAVISH_AXI_VERSION, bin: { 'lavish-axi': 'dist/cli.mjs' } }, 'wrong package name'],
399
+ ['wrong-version', { name: 'lavish-axi', version: '0.1.66', bin: { 'lavish-axi': 'dist/cli.mjs' } }, 'unpinned version'],
400
+ ['wrong-bin', { name: 'lavish-axi', version: LAVISH_AXI_VERSION, bin: { 'lavish-axi': 'dist/other.mjs' } }, 'non-pinned entry'],
401
+ ['no-bin', { name: 'lavish-axi', version: LAVISH_AXI_VERSION }, 'missing bin declaration'],
402
+ ];
403
+ for (const [label, manifest, description] of cases) {
404
+ const root = join(workRoot, label);
405
+ await mkdir(join(root, 'dist'), { recursive: true });
406
+ await writeManifest(root, manifest);
407
+ await writeFile(join(root, 'dist', 'cli.mjs'), '');
408
+ await writeFile(join(root, 'dist', 'other.mjs'), '');
409
+ await assert.rejects(() => readTrustedCli(root), (error) => {
410
+ ok(error instanceof LavishLocalError && error.code === 'cli-not-trusted', `${description} is refused`);
411
+ return true;
412
+ });
413
+ }
414
+ const missingEntry = join(workRoot, 'missing-entry');
415
+ await mkdir(missingEntry, { recursive: true });
416
+ await writeManifest(missingEntry, { name: 'lavish-axi', version: LAVISH_AXI_VERSION, bin: { 'lavish-axi': 'dist/cli.mjs' } });
417
+ await assert.rejects(() => readTrustedCli(missingEntry), (error) => {
418
+ ok(error instanceof LavishLocalError && error.code === 'cli-not-trusted', 'a missing declared entry is refused');
419
+ return true;
420
+ });
421
+ const escapingEntry = join(workRoot, 'escaping-entry');
422
+ await mkdir(join(escapingEntry, 'dist'), { recursive: true });
423
+ await writeManifest(escapingEntry, { name: 'lavish-axi', version: LAVISH_AXI_VERSION, bin: { 'lavish-axi': 'dist/cli.mjs' } });
424
+ await writeFile(join(workRoot, 'escape-target.mjs'), '');
425
+ await symlink(join(workRoot, 'escape-target.mjs'), join(escapingEntry, 'dist', 'cli.mjs')).catch(() => undefined);
426
+ await assert.rejects(() => readTrustedCli(escapingEntry), (error) => {
427
+ ok(error instanceof LavishLocalError && error.code === 'cli-not-trusted', 'an entry escaping the package root is refused');
428
+ return true;
429
+ });
430
+ }
431
+ // -------------------------------------------------------------------------
432
+ // 2. artifact path allowlist
433
+ // -------------------------------------------------------------------------
434
+ {
435
+ const accepted = await resolveArtifactPath(ARTIFACT, artifactRoot, artifactRoot);
436
+ ok(accepted.ok && accepted.file === ARTIFACT, 'an absolute html file inside the root is accepted');
437
+ const relative = await resolveArtifactPath('card.html', artifactRoot, artifactRoot);
438
+ ok(relative.ok && relative.file === ARTIFACT, 'a relative html file resolves against cwd');
439
+ expectFailure(await resolveArtifactPath('', artifactRoot, artifactRoot), 'path-rejected');
440
+ expectFailure(await resolveArtifactPath(42, artifactRoot, artifactRoot), 'path-rejected');
441
+ expectFailure(await resolveArtifactPath(`${artifactRoot}/car\0d.html`, artifactRoot, artifactRoot), 'path-rejected');
442
+ expectFailure(await resolveArtifactPath(join(artifactRoot, 'notes.txt'), artifactRoot, artifactRoot), 'path-rejected');
443
+ expectFailure(await resolveArtifactPath('../outside.html', artifactRoot, artifactRoot), 'path-rejected');
444
+ expectFailure(await resolveArtifactPath('/etc/hosts.html', artifactRoot, artifactRoot), 'path-rejected');
445
+ expectFailure(await resolveArtifactPath(artifactRoot, artifactRoot, artifactRoot), 'path-rejected');
446
+ const nested = join(artifactRoot, 'nested');
447
+ for (const denied of ['.git', 'node_modules']) {
448
+ await mkdir(join(nested, denied), { recursive: true });
449
+ const path = join(nested, denied, 'card.html');
450
+ await writeFile(path, ARTIFACT_BODY);
451
+ expectFailure(await resolveArtifactPath(path, artifactRoot, artifactRoot), 'path-rejected');
452
+ }
453
+ const outside = join(workRoot, 'outside.html');
454
+ await writeFile(outside, ARTIFACT_BODY);
455
+ expectFailure(await resolveArtifactPath(outside, artifactRoot, artifactRoot), 'path-rejected');
456
+ const escapeLink = join(artifactRoot, 'escape.html');
457
+ await symlink(outside, escapeLink).catch(() => undefined);
458
+ expectFailure(await resolveArtifactPath(escapeLink, artifactRoot, artifactRoot), 'path-rejected');
459
+ const directoryArtifact = join(artifactRoot, 'carddir.html');
460
+ await mkdir(directoryArtifact, { recursive: true });
461
+ expectFailure(await resolveArtifactPath(directoryArtifact, artifactRoot, artifactRoot), 'path-rejected');
462
+ }
463
+ // -------------------------------------------------------------------------
464
+ // 3. minimal child environment (no inherited LAVISH / Tailscale variables)
465
+ // -------------------------------------------------------------------------
466
+ {
467
+ const env = buildChildEnvironment('/tmp/owned-state', 4444);
468
+ assert.deepEqual(Object.keys(env).sort(), [
469
+ 'HOME',
470
+ 'LAVISH_AXI_HOST',
471
+ 'LAVISH_AXI_LINK_HOST',
472
+ 'LAVISH_AXI_NO_OPEN',
473
+ 'LAVISH_AXI_PORT',
474
+ 'LAVISH_AXI_STATE_DIR',
475
+ 'LAVISH_AXI_TELEMETRY',
476
+ 'PATH',
477
+ ].sort());
478
+ assertions += 1;
479
+ ok(env.LAVISH_AXI_HOST === '127.0.0.1' && env.LAVISH_AXI_LINK_HOST === '127.0.0.1', 'both hosts are forced to loopback');
480
+ ok(env.LAVISH_AXI_TELEMETRY === '0' && env.LAVISH_AXI_NO_OPEN === '1', 'telemetry is off and browser opening is off');
481
+ ok(env.LAVISH_AXI_STATE_DIR === '/tmp/owned-state' && env.LAVISH_AXI_PORT === '4444', 'the owned state dir and port are used');
482
+ ok(env.HOME === '/tmp/owned-state/home', 'HOME is redirected into the owned state dir');
483
+ const stateDir = await freshStateDir('env');
484
+ const hostile = {
485
+ LAVISH_AXI_HOST: '0.0.0.0',
486
+ LAVISH_AXI_ALLOWED_HOSTS: '*',
487
+ LAVISH_AXI_TELEMETRY: '1',
488
+ LAVISH_AXI_STATE_DIR: '/tmp/hostile-state',
489
+ LAVISH_AXI_PORT: '1',
490
+ LAVISH_AXI_IDLE_TIMEOUT_MS: '1',
491
+ TS_SOCKET: '/tmp/hostile-tailscale.sock',
492
+ };
493
+ const previous = new Map(Object.keys(hostile).map((key) => [key, process.env[key]]));
494
+ Object.assign(process.env, hostile);
495
+ try {
496
+ const session = await sessionFor(stateDir);
497
+ const opened = await session.open(ARTIFACT);
498
+ ok(opened.ok === true, `open works despite a hostile ambient environment: ${JSON.stringify(opened)}`);
499
+ const observed = JSON.parse((await readFile(join(stateDir, 'env.jsonl'), 'utf8')).trim().split('\n')[0]);
500
+ const allowedLavishKeys = new Set(Object.keys(buildChildEnvironment(stateDir, 0)));
501
+ const inherited = Object.keys(observed).filter((key) => key.startsWith('LAVISH_AXI_') && !allowedLavishKeys.has(key));
502
+ ok(inherited.length === 0, `no ambient LAVISH_AXI_* reached the child: ${inherited.join(',')}`);
503
+ ok(observed.LAVISH_AXI_HOST === '127.0.0.1', 'an ambient 0.0.0.0 host is overridden');
504
+ ok(observed.LAVISH_AXI_ALLOWED_HOSTS === undefined, 'an ambient DNS-rebinding allowlist is dropped');
505
+ ok(Object.keys(observed).filter((key) => key.startsWith('TS_')).length === 0, 'no Tailscale variables reach the child');
506
+ await session.stop();
507
+ }
508
+ finally {
509
+ for (const [key, value] of previous) {
510
+ if (value === undefined)
511
+ delete process.env[key];
512
+ else
513
+ process.env[key] = value;
514
+ }
515
+ }
516
+ }
517
+ // -------------------------------------------------------------------------
518
+ // 4. argv shape, lifecycle, and the no-shell guarantee
519
+ // -------------------------------------------------------------------------
520
+ {
521
+ const session = await controlSession('lifecycle', {});
522
+ const port = session.state().port;
523
+ const opened = await session.open(ARTIFACT);
524
+ ok(opened.ok === true, `open succeeds: ${JSON.stringify(opened)}`);
525
+ if (opened.ok) {
526
+ ok(opened.status === 'opened', 'open reports opened');
527
+ ok(opened.file === ARTIFACT, 'open echoes the resolved realpath');
528
+ ok(opened.url === `http://127.0.0.1:${port}/session/${'a'.repeat(16)}`, 'open returns the loopback session url');
529
+ ok(!('nextStep' in opened), 'the CLI next_step text is never surfaced as authority');
530
+ }
531
+ const state = session.state();
532
+ ok(state.sessionOpen === true && state.userEnded === false, 'instance state tracks the open session');
533
+ ok(state.stateDir.length > 0, 'the instance owns a state directory');
534
+ ok(state.port > 0 && state.port !== 4387, `the instance allocated an unused port: ${state.port}`);
535
+ const polled = await session.poll(ARTIFACT);
536
+ ok(polled.ok === true && polled.status === 'waiting', `poll reports waiting: ${JSON.stringify(polled)}`);
537
+ if (polled.ok) {
538
+ ok(polled.feedback.trust === 'untrusted', 'feedback carries an explicit untrusted marker');
539
+ ok(polled.feedback.prompts.length === 0 && polled.feedback.domSnapshot === '', 'a waiting poll carries no feedback');
540
+ }
541
+ const replyText = `${REPLY_SENTINEL}; $(touch ${workRoot}/pwned); rm -rf /`;
542
+ const replied = await session.reply(ARTIFACT, replyText);
543
+ ok(replied.ok === true && replied.kind === 'reply', `reply round-trips: ${JSON.stringify(replied)}`);
544
+ ok(!(await stat(join(workRoot, 'pwned')).then(() => true).catch(() => false)), 'reply text is never shell-expanded');
545
+ const ended = await session.end(ARTIFACT);
546
+ ok(ended.ok === true && ended.status === 'ended', `end reports ended: ${JSON.stringify(ended)}`);
547
+ const stopped = await session.stop();
548
+ ok(stopped.ok === true && stopped.status === 'stopped', `stop reaps the owned server: ${JSON.stringify(stopped)}`);
549
+ const serverPid = await readPid(session.state().stateDir, 'server.pid');
550
+ ok(serverPid !== null, 'the owned server recorded its pid');
551
+ if (serverPid !== null)
552
+ ok(!isProcessAlive(serverPid), 'the owned server process is gone after stop');
553
+ const stopAgain = await session.stop();
554
+ ok(stopAgain.ok === true && stopAgain.status === 'not-running', 'stop is idempotent');
555
+ expectFailure(await session.poll(ARTIFACT), 'instance-closed');
556
+ expectFailure(await session.open(ARTIFACT), 'instance-closed');
557
+ const argv = await readJsonl(join(session.state().stateDir, 'argv.jsonl'));
558
+ assert.deepEqual(argv, [
559
+ ['server', '--port', String(port)],
560
+ ['open', ARTIFACT, '--no-open'],
561
+ ['poll', ARTIFACT, '--timeout-ms', '300'],
562
+ ['poll', ARTIFACT, '--agent-reply', replyText, '--timeout-ms', '300'],
563
+ ['end', ARTIFACT],
564
+ ], `unexpected argv transcript: ${JSON.stringify(argv)}`);
565
+ assertions += 1;
566
+ const forbidden = argv.filter((line) => ['update', 'setup', 'share', 'export', 'playbook', 'design'].includes(line[0] ?? ''));
567
+ ok(forbidden.length === 0, `no self-upgrade, hook, or share command was ever invoked: ${JSON.stringify(forbidden)}`);
568
+ const transcript = await readFile(join(session.state().stateDir, 'argv.jsonl'), 'utf8');
569
+ ok(!transcript.includes('--reopen'), 'the adapter never passes --reopen');
570
+ }
571
+ // -------------------------------------------------------------------------
572
+ // 5. reply validation, malformed output, and unexpected states
573
+ // -------------------------------------------------------------------------
574
+ {
575
+ const session = await controlSession('validation', {});
576
+ ok((await session.open(ARTIFACT)).ok === true, 'the validation fixture opens');
577
+ expectFailure(await session.reply(ARTIFACT, ''), 'invalid-reply');
578
+ expectFailure(await session.reply(ARTIFACT, ' '), 'invalid-reply');
579
+ expectFailure(await session.reply(ARTIFACT, '--'), 'invalid-reply');
580
+ expectFailure(await session.reply(ARTIFACT, 7), 'invalid-reply');
581
+ expectFailure(await session.poll(ARTIFACT, { timeoutMs: 0 }), 'invalid-options');
582
+ expectFailure(await session.poll(ARTIFACT, { timeoutMs: -1 }), 'invalid-options');
583
+ expectFailure(await session.poll(ARTIFACT, { timeoutMs: 1.5 }), 'invalid-options');
584
+ expectFailure(await session.poll(ARTIFACT, { timeoutMs: 99_999 }), 'invalid-options');
585
+ await session.stop();
586
+ const scenarios = [
587
+ ['not-toon', { open: { stdoutRaw: 'this is not toon {{{\n' } }, 'malformed-output'],
588
+ ['toon-array', { open: { stdoutRaw: '- a\n- b\n' } }, 'malformed-output'],
589
+ ['unknown-status', { open: { status: 'mystery' } }, 'unexpected-status'],
590
+ ['foreign-url', { open: { url: `http://evil.example/session/${'b'.repeat(16)}` } }, 'malformed-output'],
591
+ ['wrong-port-url', { open: { url: `http://127.0.0.1:1/session/${'c'.repeat(16)}` } }, 'malformed-output'],
592
+ ['wrong-file', { open: { file: join(artifactRoot, 'other.html') } }, 'malformed-output'],
593
+ ['poll-unknown-status', { poll: { status: 'nonsense' } }, 'unexpected-status'],
594
+ ];
595
+ for (const [label, control, code] of scenarios) {
596
+ const scenarioSession = await controlSession(label, control);
597
+ const result = label.startsWith('poll-') ? await scenarioSession.poll(ARTIFACT) : await scenarioSession.open(ARTIFACT);
598
+ expectFailure(result, code);
599
+ await scenarioSession.stop();
600
+ }
601
+ const errorSession = await controlSession('cli-error', {
602
+ open: { error: 'Lavish Editor request failed: 500', code: 'SERVER_ERROR' },
603
+ });
604
+ const cliError = expectFailure(await errorSession.open(ARTIFACT), 'command-failed');
605
+ ok(cliError.detail.includes('SERVER_ERROR'), `the CLI error code is preserved: ${cliError.detail}`);
606
+ await errorSession.stop();
607
+ }
608
+ // -------------------------------------------------------------------------
609
+ // 5b. an unreadable poll response locks consumption, because it may already
610
+ // have consumed a once-only feedback delivery
611
+ // -------------------------------------------------------------------------
612
+ {
613
+ const unreadable = await controlSession('poll-missing-session', { poll: { stdoutRaw: 'status: whatever\n' } });
614
+ expectFailure(await unreadable.poll(ARTIFACT), 'malformed-output');
615
+ ok(unreadable.state().pollOutcomeUnknown === true, 'a poll response with no session object latches the unknown outcome');
616
+ const argvAfterFirst = (await readJsonl(join(unreadable.state().stateDir, 'argv.jsonl'))).length;
617
+ expectFailure(await unreadable.poll(ARTIFACT), 'poll-outcome-unknown');
618
+ expectFailure(await unreadable.reply(ARTIFACT, 'a reply that must not be sent'), 'poll-outcome-unknown');
619
+ const argvAfterRefusals = (await readJsonl(join(unreadable.state().stateDir, 'argv.jsonl'))).length;
620
+ ok(argvAfterRefusals === argvAfterFirst, `no further CLI invocation was made: ${argvAfterFirst} -> ${argvAfterRefusals}`);
621
+ await unreadable.stop();
622
+ const mystery = await controlSession('poll-mystery-latch', { poll: { status: 'mystery' } });
623
+ expectFailure(await mystery.poll(ARTIFACT), 'unexpected-status');
624
+ ok(mystery.state().pollOutcomeUnknown === true, 'an unreadable poll status latches the unknown outcome');
625
+ const argvAfterMystery = (await readJsonl(join(mystery.state().stateDir, 'argv.jsonl'))).length;
626
+ expectFailure(await mystery.poll(ARTIFACT), 'poll-outcome-unknown');
627
+ expectFailure(await mystery.reply(ARTIFACT, 'another reply that must not be sent'), 'poll-outcome-unknown');
628
+ ok((await readJsonl(join(mystery.state().stateDir, 'argv.jsonl'))).length === argvAfterMystery, 'a latched instance spawns nothing further');
629
+ await mystery.stop();
630
+ }
631
+ // -------------------------------------------------------------------------
632
+ // 5c. a non-zero exit code is never accepted as success, even with
633
+ // well-formed TOON on stdout
634
+ // -------------------------------------------------------------------------
635
+ {
636
+ const openExit = await controlSession('open-exit2', { open: { status: 'opened', exitCode: 2 } });
637
+ expectFailure(await openExit.open(ARTIFACT), 'command-failed');
638
+ ok(openExit.state().sessionOpen === false, 'a failed open never marks the session open');
639
+ await openExit.stop();
640
+ const endExit = await controlSession('end-exit2', { end: { status: 'ended', exitCode: 2 } });
641
+ expectFailure(await endExit.end(ARTIFACT), 'command-failed');
642
+ await endExit.stop();
643
+ const pollExit = await controlSession('poll-exit2', { poll: { status: 'waiting', exitCode: 2 } });
644
+ expectFailure(await pollExit.poll(ARTIFACT), 'command-failed');
645
+ ok(pollExit.state().pollOutcomeUnknown === true, 'a non-zero poll exit latches the unknown outcome');
646
+ const argvAfterPoll = (await readJsonl(join(pollExit.state().stateDir, 'argv.jsonl'))).length;
647
+ expectFailure(await pollExit.poll(ARTIFACT), 'poll-outcome-unknown');
648
+ ok((await readJsonl(join(pollExit.state().stateDir, 'argv.jsonl'))).length === argvAfterPoll, 'a poll that exited non-zero is not retried');
649
+ await pollExit.stop();
650
+ const feedbackExit = await controlSession('feedback-exit2', {
651
+ poll: { status: 'feedback', prompts: [{ uid: 'u', tag: 'note', text: PROMPT_TEXT_SENTINEL, selector: '#a' }], exitCode: 2 },
652
+ });
653
+ const feedbackFailure = expectFailure(await feedbackExit.poll(ARTIFACT), 'command-failed');
654
+ ok(!feedbackFailure.detail.includes(PROMPT_TEXT_SENTINEL), 'a rejected feedback payload is not echoed into the failure detail');
655
+ ok(feedbackExit.state().pollOutcomeUnknown === true, 'a rejected feedback delivery also latches');
656
+ await feedbackExit.stop();
657
+ const structured = await controlSession('exit-with-error', {
658
+ open: { error: 'Lavish Editor request failed: 500', code: 'SERVER_ERROR', exitCode: 2 },
659
+ });
660
+ const structuredFailure = expectFailure(await structured.open(ARTIFACT), 'command-failed');
661
+ ok(structuredFailure.detail.includes('SERVER_ERROR'), `the structured CLI error wins over the exit code: ${structuredFailure.detail}`);
662
+ await structured.stop();
663
+ }
664
+ // -------------------------------------------------------------------------
665
+ // 5d. reply text is bounded at runtime, before any argv or spawn
666
+ // -------------------------------------------------------------------------
667
+ {
668
+ const session = await controlSession('oversized-reply', {});
669
+ expectFailure(await session.reply(ARTIFACT, 'x'.repeat(2 * 1024 * 1024)), 'invalid-reply');
670
+ expectFailure(await session.reply(ARTIFACT, 'x'.repeat(20_000)), 'invalid-reply');
671
+ expectFailure(await session.reply(ARTIFACT, '\u{1D11E}'.repeat(8_000)), 'invalid-reply');
672
+ const argv = await readJsonl(join(session.state().stateDir, 'argv.jsonl'));
673
+ ok(argv.length === 0, `an oversized or over-bytes reply spawns nothing: ${JSON.stringify(argv)}`);
674
+ const stopped = await session.stop();
675
+ ok(stopped.ok === true, 'an instance that only rejected replies still stops cleanly');
676
+ }
677
+ // -------------------------------------------------------------------------
678
+ // 6. byte and time bounds, timeouts, and reaping
679
+ // -------------------------------------------------------------------------
680
+ {
681
+ const overflow = await controlSession('overflow', { open: { oversizeStdout: 512 * 1024 } }, { maxStdoutBytes: 4_096 });
682
+ expectFailure(await overflow.open(ARTIFACT), 'output-too-large');
683
+ await overflow.stop();
684
+ const stderrOverflow = await controlSession('stderr-overflow', { open: { oversizeStderr: 512 * 1024 } }, { maxStderrBytes: 4_096 });
685
+ expectFailure(await stderrOverflow.open(ARTIFACT), 'output-too-large');
686
+ await stderrOverflow.stop();
687
+ const hang = await controlSession('hang', {}, { defaultPollTimeoutMs: 200, maxPollTimeoutMs: 1_000, commandTimeoutMs: 400 });
688
+ ok((await hang.open(ARTIFACT)).ok === true, 'the fixture opens before the hang probe');
689
+ const hangStateDir = hang.state().stateDir;
690
+ await writeControl(hangStateDir, { poll: { sleepMs: 60_000 } });
691
+ const startedAt = Date.now();
692
+ expectFailure(await hang.poll(ARTIFACT), 'command-timeout');
693
+ ok(Date.now() - startedAt < 5_000, 'the adapter deadline fires long before the fixture would wake');
694
+ const pollPid = await readPid(hangStateDir, 'poll.pid');
695
+ ok(pollPid !== null, 'the hanging poll recorded its pid');
696
+ if (pollPid !== null)
697
+ ok(!isProcessAlive(pollPid), 'the timed-out poll process group was reaped');
698
+ const unknownOutcome = expectFailure(await hang.poll(ARTIFACT), 'poll-outcome-unknown');
699
+ ok(unknownOutcome.detail.length > 0, 'the unknown-outcome refusal explains itself');
700
+ const stopAfterTimeout = await hang.stop();
701
+ ok(stopAfterTimeout.ok === true, `stop still works after a timeout: ${JSON.stringify(stopAfterTimeout)}`);
702
+ const hangServerPid = await readPid(hangStateDir, 'server.pid');
703
+ if (hangServerPid !== null)
704
+ ok(!isProcessAlive(hangServerPid), 'the owned server is reaped after a timeout stop');
705
+ }
706
+ // -------------------------------------------------------------------------
707
+ // 7. feedback projection: bounded untrusted data, no filesystem access
708
+ // -------------------------------------------------------------------------
709
+ {
710
+ const attachmentTarget = join(artifactRoot, 'attachment.png');
711
+ const prompts = Array.from({ length: 80 }, (_value, index) => ({
712
+ uid: `uid-${index}`,
713
+ tag: index === 0 ? 'layout-warnings' : 'note',
714
+ selector: '#card',
715
+ text: index === 0 ? PROMPT_TEXT_SENTINEL : `feedback ${index}`,
716
+ target: { type: 'layout-warnings', warnings: [{ id: 'w1' }] },
717
+ attachments: Array.from({ length: 20 }, (_v, refIndex) => ({
718
+ id: `att-${index}-${refIndex}`,
719
+ name: 'screenshot.png',
720
+ path: attachmentTarget,
721
+ mime: 'image/png',
722
+ width: 10,
723
+ height: 10,
724
+ })),
725
+ }));
726
+ const session = await controlSession('feedback', {
727
+ poll: {
728
+ status: 'feedback',
729
+ prompts,
730
+ artifact_failures: Array.from({ length: 40 }, (_v, index) => ({ kind: 'missing-asset', detail: `failure ${index}` })),
731
+ dom_snapshot: 'd'.repeat(400 * 1024),
732
+ session_ended: true,
733
+ ended_by: 'user',
734
+ },
735
+ });
736
+ const result = await session.poll(ARTIFACT);
737
+ ok(result.ok === true && result.status === 'feedback', `feedback is delivered: ${JSON.stringify(result).slice(0, 200)}`);
738
+ if (result.ok) {
739
+ ok(result.sessionEnded === true && result.endedBy === 'user', 'the ended-by marker is preserved as data');
740
+ const feedback = result.feedback;
741
+ ok(feedback.prompts.length === 64 && feedback.promptsTruncated, `the prompt count is bounded: ${feedback.prompts.length}`);
742
+ ok(feedback.artifactFailures.length === 16 && feedback.artifactFailuresTruncated, 'artifact failures are bounded');
743
+ ok(feedback.domSnapshotTruncated && Buffer.byteLength(feedback.domSnapshot) <= 256 * 1024, 'the dom snapshot is byte-bounded');
744
+ ok(feedback.prompts[0].attachments.length === 16, 'attachment refs per prompt are bounded');
745
+ ok(feedback.prompts[0].target?.type === 'layout-warnings', 'the target type is kept as data');
746
+ const projection = JSON.stringify(feedback);
747
+ ok(!projection.includes(attachmentTarget), 'no attachment filesystem path is exposed or read');
748
+ ok(!projection.includes('"path"') && !projection.includes('"mime"'), 'only id/name attachment refs are projected');
749
+ assertions += 1;
750
+ assert.ok(projection.includes(PROMPT_TEXT_SENTINEL), 'prompt text is preserved as data');
751
+ }
752
+ ok((await session.end(ARTIFACT)).ok === true, 'the session can still be ended after feedback');
753
+ await session.stop();
754
+ }
755
+ // -------------------------------------------------------------------------
756
+ // 7b. prompt vs text separation: freeform + annotation shapes from real
757
+ // upstream lavish-axi@0.1.67 (see normalizePrompt at dist/cli.mjs ~8112
758
+ // and acceptedPrompts filter at ~7720: `tag === "message" && prompt.prompt`).
759
+ // Tag is the lower-cased element tagName (context() at ~5359: real
760
+ // annotations are h1 / div / p, not the literal "text").
761
+ // -------------------------------------------------------------------------
762
+ {
763
+ const REAL_USER_REQUEST = 'Synthetic acceptance feedback: add a visible section titled Agent update 1 with the text Browser feedback accepted. Keep all dates and facts unchanged.';
764
+ const realBrowserFreeform = await controlSession('real-browser-freeform', {
765
+ poll: {
766
+ status: 'feedback',
767
+ prompts: [
768
+ { uid: '', tag: 'message', selector: '', text: 'Freeform message', prompt: REAL_USER_REQUEST, attachments: [] },
769
+ ],
770
+ },
771
+ });
772
+ const freeformResult = await realBrowserFreeform.poll(ARTIFACT);
773
+ ok(freeformResult.ok === true, 'the real-browser freeform fixture delivers feedback');
774
+ if (freeformResult.ok) {
775
+ const prompts = freeformResult.feedback.prompts;
776
+ ok(prompts.length === 1, 'one freeform prompt is projected');
777
+ const only = prompts[0];
778
+ ok(only !== undefined, 'the freeform prompt is present');
779
+ if (only !== undefined) {
780
+ ok(only.tag === 'message', 'the freeform tag is preserved');
781
+ ok(only.text === 'Freeform message', `text is the upstream placeholder, not the request: ${only.text}`);
782
+ ok(only.textTruncated === false, 'the upstream placeholder text is not flagged as truncated');
783
+ ok(only.prompt === REAL_USER_REQUEST, `prompt is the actual user instruction, not silently substituted with text: ${only.prompt.slice(0, 80)}...`);
784
+ ok(only.promptTruncated === false, 'a request that fits within the byte bound is not flagged as truncated');
785
+ ok(only.selector === '' && only.target === undefined, 'freeform has no selector or target');
786
+ }
787
+ ok(freeformResult.feedback.promptsMalformed === 0, 'a well-formed freeform prompt is not counted as malformed');
788
+ }
789
+ await realBrowserFreeform.stop();
790
+ const realAnnotation = await controlSession('real-annotation', {
791
+ poll: {
792
+ status: 'feedback',
793
+ prompts: [
794
+ {
795
+ uid: 'a-1',
796
+ tag: 'h1',
797
+ selector: 'h1.day-3',
798
+ text: 'Day 3 — Kyoto half-day walking tour (08:00 – 14:30)',
799
+ prompt: 'Re-time this to start no earlier than 10:00.',
800
+ attachments: [],
801
+ },
802
+ ],
803
+ },
804
+ });
805
+ const annotationResult = await realAnnotation.poll(ARTIFACT);
806
+ ok(annotationResult.ok === true, 'the annotation fixture delivers feedback');
807
+ if (annotationResult.ok) {
808
+ const prompts = annotationResult.feedback.prompts;
809
+ ok(prompts.length === 1, 'one annotation prompt is projected');
810
+ const only = prompts[0];
811
+ ok(only !== undefined, 'the annotation prompt is present');
812
+ if (only !== undefined) {
813
+ ok(only.tag === 'h1', 'the annotation tag is the lower-cased element tagName');
814
+ ok(only.selector === 'h1.day-3', 'the originating selector is preserved');
815
+ ok(only.text === 'Day 3 — Kyoto half-day walking tour (08:00 – 14:30)', 'the selected snippet stays in text');
816
+ ok(only.prompt === 'Re-time this to start no earlier than 10:00.', 'the user comment stays in prompt');
817
+ ok(only.target === undefined, 'a plain HTML annotation has no target (table/mermaid only)');
818
+ ok(only.text !== only.prompt, 'text and prompt are not the same string for an annotation');
819
+ }
820
+ ok(annotationResult.feedback.promptsMalformed === 0, 'a well-formed annotation prompt is not counted as malformed');
821
+ }
822
+ await realAnnotation.stop();
823
+ const counterexampleRepro = await controlSession('root-counterexample', {
824
+ poll: {
825
+ status: 'feedback',
826
+ prompts: [
827
+ { uid: '', tag: 'message', selector: '', text: 'Freeform message', attachments: [] },
828
+ ],
829
+ },
830
+ });
831
+ const reproResult = await counterexampleRepro.poll(ARTIFACT);
832
+ ok(reproResult.ok === true, 'the root counterexample fixture still delivers feedback (no global failure)');
833
+ if (reproResult.ok) {
834
+ const prompts = reproResult.feedback.prompts;
835
+ ok(prompts.length === 1, 'the missing-prompt freeform prompt is not silently dropped');
836
+ const only = prompts[0];
837
+ ok(only !== undefined && only.prompt === '', `a missing prompt is exposed as the empty string, not as text: ${only?.prompt}`);
838
+ ok(only !== undefined && only.promptTruncated === false, 'a missing prompt is not falsely flagged as truncated');
839
+ ok(only !== undefined && only.text === 'Freeform message', 'the placeholder text is preserved');
840
+ ok(reproResult.feedback.promptsMalformed === 1, `the missing-prompt freeform is counted as malformed: ${reproResult.feedback.promptsMalformed}`);
841
+ }
842
+ await counterexampleRepro.stop();
843
+ const nonStringPrompt = await controlSession('non-string-prompt', {
844
+ poll: {
845
+ status: 'feedback',
846
+ prompts: [
847
+ { uid: 'u', tag: 'message', selector: '', text: 'Freeform message', prompt: 7 },
848
+ { uid: 'v', tag: 'h1', selector: 'h1.day-3', text: 'snippet', prompt: { bogus: true } },
849
+ { uid: 'w', tag: 'message', selector: '', text: 'Freeform message', prompt: null },
850
+ ],
851
+ },
852
+ });
853
+ const nonStringResult = await nonStringPrompt.poll(ARTIFACT);
854
+ ok(nonStringResult.ok === true, 'non-string prompt fields do not poison the whole poll');
855
+ if (nonStringResult.ok) {
856
+ ok(nonStringResult.feedback.promptsMalformed === 3, `every non-string prompt is counted as malformed regardless of tag: ${nonStringResult.feedback.promptsMalformed}`);
857
+ for (const projected of nonStringResult.feedback.prompts) {
858
+ ok(projected.prompt === '' && projected.promptTruncated === false, `non-string prompt projected as empty and not falsely truncated: ${JSON.stringify(projected)}`);
859
+ }
860
+ }
861
+ await nonStringPrompt.stop();
862
+ const emptyWithAttachment = await controlSession('empty-with-attachment', {
863
+ poll: {
864
+ status: 'feedback',
865
+ prompts: [
866
+ { uid: 'ea', tag: 'message', selector: '', text: 'Freeform message', prompt: '', attachments: [{ id: 'att-x', name: 'screenshot.png' }] },
867
+ ],
868
+ },
869
+ });
870
+ const emptyAttachResult = await emptyWithAttachment.poll(ARTIFACT);
871
+ ok(emptyAttachResult.ok === true, 'an empty-prompt request with attachments still delivers feedback');
872
+ if (emptyAttachResult.ok) {
873
+ ok(emptyAttachResult.feedback.promptsMalformed === 0, 'an empty-string prompt is NOT counted as malformed');
874
+ const only = emptyAttachResult.feedback.prompts[0];
875
+ ok(only !== undefined && only.prompt === '', 'the empty prompt is preserved as data');
876
+ ok(only !== undefined && only.promptTruncated === false, 'an empty prompt is not falsely flagged as truncated');
877
+ ok(only !== undefined && only.attachments.length === 1 && only.attachments[0]?.id === 'att-x', 'the attachment survives alongside an empty prompt');
878
+ }
879
+ await emptyWithAttachment.stop();
880
+ const emptyNoAttachment = await controlSession('empty-no-attachment', {
881
+ poll: {
882
+ status: 'feedback',
883
+ prompts: [
884
+ { uid: 'en', tag: 'h1', selector: 'h1.day-3', text: 'Day 3', prompt: '', attachments: [] },
885
+ ],
886
+ },
887
+ });
888
+ const emptyNoAttachResult = await emptyNoAttachment.poll(ARTIFACT);
889
+ ok(emptyNoAttachResult.ok === true, 'an empty-prompt annotation still delivers feedback');
890
+ if (emptyNoAttachResult.ok) {
891
+ ok(emptyNoAttachResult.feedback.promptsMalformed === 0, 'an empty-string prompt with no attachments is still NOT counted as malformed');
892
+ const only = emptyNoAttachResult.feedback.prompts[0];
893
+ ok(only !== undefined && only.prompt === '' && only.text === 'Day 3', 'the empty prompt does NOT fall back to the context text');
894
+ ok(only !== undefined && only.promptTruncated === false, 'an empty prompt is not falsely flagged as truncated');
895
+ }
896
+ await emptyNoAttachment.stop();
897
+ const malformedAnyTag = await controlSession('malformed-any-tag', {
898
+ poll: {
899
+ status: 'feedback',
900
+ prompts: [
901
+ { uid: 'l', tag: 'layout-warnings', selector: '', text: 'warning batch' },
902
+ { uid: 'n', tag: 'note', selector: '#x', text: 'note body' },
903
+ { uid: 'p', tag: 'div', selector: 'p', text: 'paragraph' },
904
+ ],
905
+ },
906
+ });
907
+ const malformedAnyResult = await malformedAnyTag.poll(ARTIFACT);
908
+ ok(malformedAnyResult.ok === true, 'missing-prompt fixtures on any tag deliver feedback');
909
+ if (malformedAnyResult.ok) {
910
+ ok(malformedAnyResult.feedback.promptsMalformed === 3, `missing prompt is counted as malformed on every tag, not just freeform/annotation: ${malformedAnyResult.feedback.promptsMalformed}`);
911
+ for (const projected of malformedAnyResult.feedback.prompts) {
912
+ ok(projected.prompt === '' && projected.promptTruncated === false, `missing-prompt is projected as empty without false truncation flags: ${JSON.stringify(projected)}`);
913
+ ok(projected.text.length > 0, `context text survives so consumers can audit the drop: ${projected.text}`);
914
+ }
915
+ }
916
+ await malformedAnyTag.stop();
917
+ const oversizedChar = 'A'.repeat(MAX_PROMPT_FIELD_CHARS_TEST + 200);
918
+ const charOverflow = await controlSession('prompt-char-overflow', {
919
+ poll: { status: 'feedback', prompts: [{ uid: 'c', tag: 'message', selector: '', text: 'Freeform message', prompt: oversizedChar }] },
920
+ });
921
+ const charResult = await charOverflow.poll(ARTIFACT);
922
+ ok(charResult.ok === true, 'an over-char-bound prompt still delivers feedback');
923
+ if (charResult.ok) {
924
+ const only = charResult.feedback.prompts[0];
925
+ ok(only !== undefined, 'the prompt is projected');
926
+ if (only !== undefined) {
927
+ ok(only.prompt.length === MAX_PROMPT_FIELD_CHARS_TEST, `the prompt is char-clipped: ${only.prompt.length}`);
928
+ ok(only.promptTruncated === true, 'the truncation flag is set explicitly');
929
+ }
930
+ ok(charResult.feedback.promptsMalformed === 0, 'over-char-bound prompts are truncated, not counted as malformed');
931
+ }
932
+ await charOverflow.stop();
933
+ const oversizedByte = '\u{1F600}'.repeat(MAX_PROMPT_FIELD_BYTES_TEST + 100);
934
+ const byteOverflow = await controlSession('prompt-byte-overflow', {
935
+ poll: { status: 'feedback', prompts: [{ uid: 'b', tag: 'message', selector: '', text: 'Freeform message', prompt: oversizedByte }] },
936
+ });
937
+ const byteResult = await byteOverflow.poll(ARTIFACT);
938
+ ok(byteResult.ok === true, 'an over-byte-bound prompt still delivers feedback');
939
+ if (byteResult.ok) {
940
+ const only = byteResult.feedback.prompts[0];
941
+ ok(only !== undefined, 'the prompt is projected');
942
+ if (only !== undefined) {
943
+ const projectedBytes = Buffer.byteLength(only.prompt, 'utf8');
944
+ ok(projectedBytes <= MAX_PROMPT_FIELD_BYTES_TEST, `the prompt is byte-clipped to the bound: ${projectedBytes}`);
945
+ ok(only.promptTruncated === true, 'the byte truncation flag is set explicitly');
946
+ ok(!only.prompt.endsWith('�'), 'no split multi-byte sequence survives in the truncated prompt');
947
+ }
948
+ ok(byteResult.feedback.promptsMalformed === 0, 'over-byte-bound prompts are truncated, not counted as malformed');
949
+ }
950
+ await byteOverflow.stop();
951
+ }
952
+ // -------------------------------------------------------------------------
953
+ // 8. user-ended stays closed; no automatic reopen
954
+ // -------------------------------------------------------------------------
955
+ {
956
+ const session = await controlSession('user-ended', { open: { status: 'user-ended' } });
957
+ const first = await session.open(ARTIFACT);
958
+ ok(first.ok === true && first.status === 'user-ended', `open reports user-ended: ${JSON.stringify(first)}`);
959
+ ok(session.state().userEnded === true && session.state().sessionOpen === false, 'the instance latches the user-ended state');
960
+ expectFailure(await session.open(ARTIFACT), 'session-user-ended');
961
+ const argv = await readJsonl(join(session.state().stateDir, 'argv.jsonl'));
962
+ ok(argv.filter((line) => line[0] === 'open').length === 1, `no reopen was attempted: ${JSON.stringify(argv)}`);
963
+ ok((await session.stop()).ok === true, 'stop still works after a user end');
964
+ const userEndedByPoll = await controlSession('user-ended-poll', {
965
+ poll: { status: 'feedback', prompts: [{ uid: 'u', tag: 'note', text: 'bye', selector: '#a' }], session_ended: true, ended_by: 'user' },
966
+ });
967
+ ok((await userEndedByPoll.poll(ARTIFACT)).ok === true, 'the session-ended feedback is delivered');
968
+ ok(userEndedByPoll.state().userEnded === true, 'a user end observed through poll also latches closed');
969
+ expectFailure(await userEndedByPoll.open(ARTIFACT), 'session-user-ended');
970
+ await userEndedByPoll.stop();
971
+ }
972
+ // -------------------------------------------------------------------------
973
+ // 9. a foreign server on the port is never adopted, preempted, or killed
974
+ // -------------------------------------------------------------------------
975
+ {
976
+ const foreign = createServer((_req, res) => {
977
+ res.setHeader('content-type', 'application/json');
978
+ res.end(JSON.stringify({ ok: true, app: 'something-else', version: '9.9.9' }));
979
+ });
980
+ await new Promise((resolveListen) => foreign.listen(0, '127.0.0.1', resolveListen));
981
+ const address = foreign.address();
982
+ assert.ok(address !== null && typeof address === 'object');
983
+ const foreignPort = address.port;
984
+ const session = await controlSession('foreign', {}, { port: foreignPort });
985
+ expectFailure(await session.open(ARTIFACT), 'port-occupied');
986
+ ok(foreign.listening, 'the foreign listener is untouched');
987
+ const argv = await readJsonl(join(session.state().stateDir, 'argv.jsonl'));
988
+ ok(argv.length === 0, `no command was issued against a foreign port: ${JSON.stringify(argv)}`);
989
+ const stopped = await session.stop();
990
+ ok(stopped.ok === true && stopped.status === 'not-running', 'stop never touches a server it does not own');
991
+ ok(foreign.listening, 'stop left the foreign listener running');
992
+ await new Promise((resolveClose) => foreign.close(() => resolveClose()));
993
+ }
994
+ {
995
+ // A pre-existing lavish-axi server of the very same pinned version is still
996
+ // not ours: the adapter must neither adopt nor shut it down. The test
997
+ // harness owns that exact fixture in a try/finally so every code path —
998
+ // including a failing assertion — is reaped with bounded exit/PID/port
999
+ // evidence. Only SIGKILL escalation is permitted, and only against the
1000
+ // harness-owned fixture group — never a broad pkill.
1001
+ const port = await allocateUnusedLoopbackPort();
1002
+ const preState = await freshStateDir('preexisting');
1003
+ await writeControl(preState, {});
1004
+ const preexisting = spawn(process.execPath, [join(fakePackageRoot, 'dist', 'cli.mjs'), 'server', '--port', String(port)], {
1005
+ cwd: artifactRoot,
1006
+ env: buildChildEnvironment(preState, port),
1007
+ detached: true,
1008
+ stdio: 'ignore',
1009
+ });
1010
+ const fixturePid = preexisting.pid;
1011
+ if (fixturePid === undefined) {
1012
+ throw new Error('failed to spawn pre-existing server: pid is undefined');
1013
+ }
1014
+ const fixture = trackFixture(fixturePid, port, 'preexisting');
1015
+ try {
1016
+ ok(await waitForHealth(port), 'the pre-existing same-version server is healthy before the probe');
1017
+ const session = await controlSession('preexisting-session', {}, { port });
1018
+ expectFailure(await session.open(ARTIFACT), 'port-occupied');
1019
+ ok(isProcessAlive(fixturePid), 'the pre-existing server was not adopted or killed');
1020
+ const argv = await readJsonl(join(session.state().stateDir, 'argv.jsonl'));
1021
+ ok(argv.length === 0, `no command was issued to the pre-existing server: ${JSON.stringify(argv)}`);
1022
+ const stopped = await session.stop();
1023
+ ok(stopped.ok === true && stopped.status === 'not-running', 'stop reports nothing owned rather than shutting a foreign server down');
1024
+ ok(isProcessAlive(fixturePid), 'the pre-existing server survived stop');
1025
+ }
1026
+ finally {
1027
+ try {
1028
+ const result = await reapOwnedFixture(fixture);
1029
+ if (reapSucceeded(result)) {
1030
+ ownedFixtures.delete(fixture);
1031
+ }
1032
+ else {
1033
+ // Retain the entry for one outer retry and retain this failed attempt
1034
+ // as evidence even if that retry succeeds.
1035
+ deferredCleanupErrors.push(new Error(`fixture cleanup failed: ${describeReap(fixture, result)}`));
1036
+ }
1037
+ }
1038
+ catch (error) {
1039
+ deferredCleanupErrors.push(new Error(`fixture cleanup threw for ${fixture.label} pgid=${fixture.pgid}: ${asError(error).message}`));
1040
+ }
1041
+ }
1042
+ }
1043
+ // -------------------------------------------------------------------------
1044
+ // 10. per-instance isolation: two instances, two state dirs, two ports
1045
+ // -------------------------------------------------------------------------
1046
+ {
1047
+ const firstState = await freshStateDir('pair-a');
1048
+ const secondState = await freshStateDir('pair-b');
1049
+ const [first, second] = await Promise.all([sessionFor(firstState), sessionFor(secondState)]);
1050
+ ok(first.state().port !== second.state().port, 'each instance allocates its own port');
1051
+ ok(first.state().stateDir !== second.state().stateDir, 'each instance owns its own state dir');
1052
+ const [firstOpen, secondOpen] = await Promise.all([first.open(ARTIFACT), second.open(ARTIFACT)]);
1053
+ ok(firstOpen.ok === true && secondOpen.ok === true, 'two instances serve concurrently');
1054
+ await Promise.all([first.stop(), second.stop()]);
1055
+ const [firstPid, secondPid] = await Promise.all([readPid(firstState, 'server.pid'), readPid(secondState, 'server.pid')]);
1056
+ ok(firstPid !== null && secondPid !== null && firstPid !== secondPid, 'the two instances owned different server processes');
1057
+ for (const pid of [firstPid, secondPid]) {
1058
+ if (pid !== null)
1059
+ ok(!isProcessAlive(pid), 'every owned server was reaped');
1060
+ }
1061
+ }
1062
+ // -------------------------------------------------------------------------
1063
+ // 11. the adapter never writes to stdout/stderr and never mutates the artifact
1064
+ // -------------------------------------------------------------------------
1065
+ {
1066
+ const session = await controlSession('quiet', {
1067
+ poll: { status: 'feedback', prompts: [{ uid: 'u', tag: 'note', text: PROMPT_TEXT_SENTINEL, selector: '#a' }] },
1068
+ });
1069
+ const before = await stat(ARTIFACT);
1070
+ const bodyBefore = await readFile(ARTIFACT, 'utf8');
1071
+ let written = 0;
1072
+ const stdoutWrite = process.stdout.write.bind(process.stdout);
1073
+ const stderrWrite = process.stderr.write.bind(process.stderr);
1074
+ const counter = () => {
1075
+ written += 1;
1076
+ return true;
1077
+ };
1078
+ process.stdout.write = counter;
1079
+ process.stderr.write = counter;
1080
+ try {
1081
+ await session.open(ARTIFACT);
1082
+ const feedback = await session.poll(ARTIFACT);
1083
+ ok(feedback.ok === true, 'the quiet probe still receives feedback');
1084
+ expectFailure(await session.poll(ARTIFACT, { timeoutMs: 0 }), 'invalid-options');
1085
+ await session.end(ARTIFACT);
1086
+ await session.stop();
1087
+ }
1088
+ finally {
1089
+ ;
1090
+ process.stdout.write = stdoutWrite;
1091
+ process.stderr.write = stderrWrite;
1092
+ }
1093
+ ok(written === 0, `the adapter wrote ${written} chunks to stdout/stderr; it must log nothing`);
1094
+ const after = await stat(ARTIFACT);
1095
+ const bodyAfter = await readFile(ARTIFACT, 'utf8');
1096
+ ok(bodyAfter === bodyBefore, 'the artifact contents are untouched');
1097
+ ok(after.mtimeMs === before.mtimeMs, 'the artifact mtime is untouched');
1098
+ ok(after.size === before.size, 'the artifact size is untouched');
1099
+ }
1100
+ // -------------------------------------------------------------------------
1101
+ // 12. opt-in: real installed CLI protocol probe
1102
+ // -------------------------------------------------------------------------
1103
+ if (process.env.GOTRY_LAVISH_LIVE !== '1') {
1104
+ console.log('SKIP: the real lavish-axi probe is opt-in; set GOTRY_LAVISH_LIVE=1 (public registry install into an mkdtemp prefix, no global install)');
1105
+ }
1106
+ else {
1107
+ await realCliProbe();
1108
+ }
1109
+ }
1110
+ catch (err) {
1111
+ suitePrimary = err;
1112
+ }
1113
+ finally {
1114
+ const errors = suitePrimary === undefined ? [...deferredCleanupErrors] : [asError(suitePrimary), ...deferredCleanupErrors];
1115
+ try {
1116
+ errors.push(...await reapAllOwnedFixtures());
1117
+ }
1118
+ catch (error) {
1119
+ errors.push(asError(error));
1120
+ }
1121
+ try {
1122
+ await rm(workRoot, { recursive: true, force: true });
1123
+ }
1124
+ catch (error) {
1125
+ errors.push(asError(error));
1126
+ }
1127
+ if (errors.length === 0)
1128
+ console.log(`LAVISH LOCAL ADAPTER TESTS OK (${assertions} assertions)`);
1129
+ throwCollected(errors);
1130
+ }
1131
+ async function realCliProbe() {
1132
+ const installRoot = await mkdtemp(join(tmpdir(), 'gotry-lavish-live-'));
1133
+ let ownedStateDir = null;
1134
+ try {
1135
+ await writeFile(join(installRoot, 'package.json'), `${JSON.stringify({ name: 'gotry-lavish-probe', private: true, version: '0.0.0' })}\n`);
1136
+ console.log(`=== live probe: npm install lavish-axi@${LAVISH_AXI_VERSION} into ${installRoot} ===`);
1137
+ const install = spawnSync('npm', ['install', '--no-save', '--no-audit', '--no-fund', '--ignore-scripts', `lavish-axi@${LAVISH_AXI_VERSION}`], {
1138
+ cwd: installRoot,
1139
+ encoding: 'utf8',
1140
+ env: { PATH: process.env.PATH ?? '', HOME: installRoot },
1141
+ });
1142
+ console.log('argv: npm install --no-save --no-audit --no-fund --ignore-scripts lavish-axi@' + LAVISH_AXI_VERSION);
1143
+ console.log(`exit: ${install.status}`);
1144
+ assert.equal(install.status, 0, `npm install failed: ${install.stderr}`);
1145
+ const packageRoot = join(installRoot, 'node_modules', 'lavish-axi');
1146
+ const cli = await readTrustedCli(packageRoot);
1147
+ console.log(`resolved entry: ${cli.entry}`);
1148
+ console.log(`resolved version: ${cli.version}`);
1149
+ assertions += 2;
1150
+ const liveArtifacts = join(installRoot, 'artifacts');
1151
+ await mkdir(liveArtifacts, { recursive: true });
1152
+ const artifact = join(liveArtifacts, 'synthetic-trip-card.html');
1153
+ await writeFile(artifact, '<!doctype html><html><body><div style="background:#fff;color:#111;height:120px">synthetic card</div></body></html>\n');
1154
+ const session = await LavishLocalSession.create({
1155
+ cliPackageRoot: packageRoot,
1156
+ cwd: liveArtifacts,
1157
+ allowedRoot: liveArtifacts,
1158
+ });
1159
+ const { port, stateDir } = session.state();
1160
+ ownedStateDir = stateDir;
1161
+ console.log(`owned loopback port: ${port}`);
1162
+ console.log(`owned state dir: ${stateDir}`);
1163
+ const beforeStop = spawnSync(process.execPath, [cli.entry, 'stop', '--port', String(port)], {
1164
+ encoding: 'utf8',
1165
+ cwd: liveArtifacts,
1166
+ env: buildChildEnvironment(stateDir, port),
1167
+ });
1168
+ console.log(`argv: lavish-axi stop --port ${port} -> exit ${beforeStop.status} stdout: ${beforeStop.stdout.trim()}`);
1169
+ ok(beforeStop.status === 0, 'the real stop command is well-defined with nothing running');
1170
+ const opened = await session.open(artifact);
1171
+ ok(opened.ok === true, `live open succeeds: ${JSON.stringify(opened)}`);
1172
+ if (opened.ok) {
1173
+ ok(opened.status === 'opened', `live open status is opened: ${opened.status}`);
1174
+ ok(opened.url.startsWith(`http://127.0.0.1:${port}/session/`), 'the live session url is loopback on the owned port');
1175
+ console.log(`argv: open <artifact> --no-open -> exit 0, status=${opened.status}, url=${opened.url.replace(/\/session\/[0-9a-f]{16}/, '/session/[redacted]')}`);
1176
+ }
1177
+ const polled = await session.poll(artifact, { timeoutMs: 2_500 });
1178
+ ok(polled.ok === true, `live poll succeeds: ${JSON.stringify(polled)}`);
1179
+ if (polled.ok) {
1180
+ ok(polled.status === 'waiting', `live poll status is waiting: ${polled.status}`);
1181
+ console.log(`argv: poll <artifact> --timeout-ms 2500 -> exit 0, status=${polled.status}`);
1182
+ }
1183
+ const ended = await session.end(artifact);
1184
+ ok(ended.ok === true && ended.status === 'ended', `live end succeeds: ${JSON.stringify(ended)}`);
1185
+ console.log('argv: end <artifact> -> exit 0, status=ended');
1186
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, 1_000));
1187
+ const afterEnd = spawnSync(process.execPath, [cli.entry, 'stop', '--port', String(port)], {
1188
+ encoding: 'utf8',
1189
+ cwd: liveArtifacts,
1190
+ env: buildChildEnvironment(stateDir, port),
1191
+ });
1192
+ console.log(`argv: lavish-axi stop --port ${port} (after end) -> exit ${afterEnd.status} stdout: ${afterEnd.stdout.trim()}`);
1193
+ ok(afterEnd.status === 0, 'the real stop command stays well-defined after a session ends');
1194
+ const stopped = await session.stop();
1195
+ ok(stopped.ok === true, `live stop succeeds: ${JSON.stringify(stopped)}`);
1196
+ console.log(`stop -> status=${stopped.ok ? stopped.status : 'error'}`);
1197
+ ok(stopped.ok && (stopped.status === 'stopped' || stopped.status === 'not-running'), 'live stop is idempotent');
1198
+ const health = spawnSync(process.execPath, ['-e', `fetch('http://127.0.0.1:${port}/health').then(() => process.exit(0), () => process.exit(3))`], { encoding: 'utf8' });
1199
+ ok(health.status === 3, 'the owned port is free after stop');
1200
+ console.log('post-stop health probe: no listener (expected)');
1201
+ // The server self-exits once the last session ends with nothing connected, so
1202
+ // the stop above never had to signal it. A second instance on the same owned
1203
+ // state dir and port gets a live server, then reaps it while it is running.
1204
+ const liveSession = await LavishLocalSession.create({
1205
+ cliPackageRoot: packageRoot,
1206
+ cwd: liveArtifacts,
1207
+ allowedRoot: liveArtifacts,
1208
+ stateDir,
1209
+ port,
1210
+ });
1211
+ const reopened = await liveSession.open(artifact);
1212
+ ok(reopened.ok === true && reopened.status === 'opened', `a fresh server opens a new session: ${JSON.stringify(reopened)}`);
1213
+ const liveReap = await liveSession.stop();
1214
+ ok(liveReap.ok === true && liveReap.status === 'stopped', `a live owned server is signalled and reaped: ${JSON.stringify(liveReap)}`);
1215
+ console.log(`open (fresh server) -> stop while live -> status=${liveReap.ok ? liveReap.status : 'error'}`);
1216
+ const healthAfterReap = spawnSync(process.execPath, ['-e', `fetch('http://127.0.0.1:${port}/health').then(() => process.exit(0), () => process.exit(3))`], { encoding: 'utf8' });
1217
+ ok(healthAfterReap.status === 3, 'the owned port is free after reaping a live server');
1218
+ console.log('post-reap health probe: no listener (expected)');
1219
+ console.log('NOTE: real browser feedback (feedback payloads, --agent-reply echo, browser_disconnected grace) is NOT exercised here; it stays a #438 TODO for a real browser session.');
1220
+ }
1221
+ finally {
1222
+ if (ownedStateDir)
1223
+ await rm(ownedStateDir, { recursive: true, force: true });
1224
+ await rm(installRoot, { recursive: true, force: true });
1225
+ }
1226
+ }