@danceiny/gotry 0.0.1-rc.21 → 0.0.1-rc.24

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 (327) hide show
  1. package/README.md +102 -194
  2. package/README.zh-CN.md +115 -210
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-bootstrap.js +466 -35
  5. package/bin/gotry-inner.js +255 -36
  6. package/bin/gotry-process-liveness.d.ts +27 -0
  7. package/bin/gotry-process-liveness.js +97 -0
  8. package/bin/gotry-runtime-resolution.d.ts +1 -1
  9. package/bin/gotry-runtime-resolution.js +2 -2
  10. package/client/client.js +174 -0
  11. package/cordis.gotry-patch.yml +70 -93
  12. package/dist/capabilities/agent-reach-deep.js +45 -122
  13. package/dist/capabilities/agent-reach.js +63 -148
  14. package/dist/capabilities/anything.js +89 -75
  15. package/dist/capabilities/artifacts.js +123 -104
  16. package/dist/capabilities/channel-health.js +65 -45
  17. package/dist/capabilities/channel-registry.js +108 -87
  18. package/dist/capabilities/doctor.js +357 -235
  19. package/dist/capabilities/effect.js +223 -309
  20. package/dist/capabilities/fact-log.js +28 -14
  21. package/dist/capabilities/flyai.js +245 -156
  22. package/dist/capabilities/ground-transfer.js +509 -0
  23. package/dist/capabilities/hbcli.js +144 -148
  24. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  25. package/dist/capabilities/incident-log.js +131 -61
  26. package/dist/capabilities/model-override.js +30 -10
  27. package/dist/capabilities/opensky.js +54 -43
  28. package/dist/capabilities/resilience.js +50 -32
  29. package/dist/capabilities/session/action-cache.js +50 -44
  30. package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
  31. package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
  32. package/dist/capabilities/session/adapters/dida-portal.js +206 -0
  33. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  34. package/dist/capabilities/session/adapters/rail-12306.js +169 -112
  35. package/dist/capabilities/session/benchmark.js +85 -149
  36. package/dist/capabilities/session/extension-bridge.js +320 -259
  37. package/dist/capabilities/session/extension-channel.js +57 -76
  38. package/dist/capabilities/session/extension-distribution.js +115 -119
  39. package/dist/capabilities/session/extract.js +31 -21
  40. package/dist/capabilities/session/golden-score.js +55 -33
  41. package/dist/capabilities/session/health-watch.js +146 -154
  42. package/dist/capabilities/session/read-guard.js +55 -47
  43. package/dist/capabilities/session/static-flight-golden.js +49 -36
  44. package/dist/capabilities/session/transport.js +54 -61
  45. package/dist/capabilities/session/wizard.js +63 -93
  46. package/dist/capabilities/session-consent.js +143 -47
  47. package/dist/capabilities/session-login.js +95 -126
  48. package/dist/capabilities/session-search.js +391 -253
  49. package/dist/capabilities/sponsor-plugin.js +341 -0
  50. package/dist/capabilities/visa-policy.js +87 -73
  51. package/dist/capabilities/weather.js +163 -235
  52. package/dist/scripts/action-cache-tests.js +24 -59
  53. package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
  54. package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
  55. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  56. package/dist/scripts/agent-reach-tests.js +26 -18
  57. package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
  58. package/dist/scripts/anything-tests.js +58 -62
  59. package/dist/scripts/artifact-client-contract-tests.js +69 -0
  60. package/dist/scripts/artifacts-capability-tests.js +235 -0
  61. package/dist/scripts/async-collect.js +26 -15
  62. package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
  63. package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
  64. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
  65. package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
  66. package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
  67. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
  68. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
  69. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
  70. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
  71. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
  72. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
  73. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
  74. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
  75. package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
  76. package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
  77. package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
  78. package/dist/scripts/booking-executor-tests.js +80 -0
  79. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  80. package/dist/scripts/booking-saga-tests.js +61 -123
  81. package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
  82. package/dist/scripts/bootstrap-tests.js +1383 -128
  83. package/dist/scripts/build-changelog.js +89 -130
  84. package/dist/scripts/build-metrics-report.js +135 -143
  85. package/dist/scripts/changelog-tests.js +35 -82
  86. package/dist/scripts/channel-probe-tests.js +44 -116
  87. package/dist/scripts/channel-probe.js +84 -167
  88. package/dist/scripts/channel-registry-tests.js +97 -185
  89. package/dist/scripts/companion-tests.js +25 -70
  90. package/dist/scripts/dida-cookie-probe.js +87 -0
  91. package/dist/scripts/dida-sw-probe.js +114 -0
  92. package/dist/scripts/diff-test.js +17 -12
  93. package/dist/scripts/doctor-tests.js +428 -120
  94. package/dist/scripts/dsh-artifact-e2e.js +150 -0
  95. package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
  96. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  97. package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
  98. package/dist/scripts/dsh-runtime-closure.js +75 -63
  99. package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
  100. package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
  101. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  102. package/dist/scripts/effect-tests.js +165 -552
  103. package/dist/scripts/engine-run.js +1 -3
  104. package/dist/scripts/engine-tests.js +16 -10
  105. package/dist/scripts/evaluation-cadence-tests.js +69 -242
  106. package/dist/scripts/evaluation-contract-tests.js +193 -499
  107. package/dist/scripts/extension-distribution-cli.js +19 -15
  108. package/dist/scripts/extension-distribution-tests.js +98 -211
  109. package/dist/scripts/extension-tests.js +366 -389
  110. package/dist/scripts/fact-gate-tests.js +725 -263
  111. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  112. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  113. package/dist/scripts/flight-malformed-tests.js +159 -0
  114. package/dist/scripts/flyai-tests.js +325 -68
  115. package/dist/scripts/g5-guard-tests.js +131 -0
  116. package/dist/scripts/g5-guard.js +189 -0
  117. package/dist/scripts/gen-lunar-anchors.js +106 -0
  118. package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
  119. package/dist/scripts/ground-transfer-tests.js +956 -0
  120. package/dist/scripts/hbcli-e2e-tests.js +71 -90
  121. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  122. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  123. package/dist/scripts/hbcli-tests.js +41 -115
  124. package/dist/scripts/health-watch-cli.js +42 -28
  125. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  126. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  127. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  128. package/dist/scripts/i18n-tests.js +16 -9
  129. package/dist/scripts/incident-tests.js +281 -87
  130. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  131. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  132. package/dist/scripts/issue-271-liveness-tests.js +547 -0
  133. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  134. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  135. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  136. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  137. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  138. package/dist/scripts/journey-tests.js +22 -27
  139. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  140. package/dist/scripts/kernel-manifest-gate.js +126 -0
  141. package/dist/scripts/kernel-manifest-tests.js +212 -0
  142. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  143. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  144. package/dist/scripts/kernel-manifest.js +369 -0
  145. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  146. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  147. package/dist/scripts/ledger-tests.js +325 -340
  148. package/dist/scripts/ledger-workflow-crash.js +10 -20
  149. package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
  150. package/dist/scripts/memory-capture-tests.js +242 -61
  151. package/dist/scripts/memory-decay-tests.js +18 -52
  152. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  153. package/dist/scripts/memory-lifecycle.js +196 -0
  154. package/dist/scripts/memory-metrics.js +24 -12
  155. package/dist/scripts/memory-value-report.js +474 -222
  156. package/dist/scripts/memory-value-tests.js +328 -0
  157. package/dist/scripts/metrics-report-tests.js +62 -199
  158. package/dist/scripts/model-override-e2e.js +66 -105
  159. package/dist/scripts/nightly-evidence-tests.js +64 -89
  160. package/dist/scripts/nightly-evidence.js +108 -111
  161. package/dist/scripts/nudge-digest.js +39 -28
  162. package/dist/scripts/onboarding-tests.js +63 -88
  163. package/dist/scripts/opensky-check.js +18 -13
  164. package/dist/scripts/opensky-tests.js +28 -28
  165. package/dist/scripts/persona-surface-guard-tests.js +37 -12
  166. package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
  167. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  168. package/dist/scripts/price-drift-tests.js +126 -227
  169. package/dist/scripts/price-drift-watch.js +209 -253
  170. package/dist/scripts/probe-poi-tests.js +52 -45
  171. package/dist/scripts/product-metrics.js +187 -273
  172. package/dist/scripts/publish-preverify.js +47 -41
  173. package/dist/scripts/realtime-pricing-tests.js +37 -76
  174. package/dist/scripts/replay-async.js +20 -18
  175. package/dist/scripts/replay-real.js +16 -18
  176. package/dist/scripts/replay.js +52 -82
  177. package/dist/scripts/session-attach-diagnose.js +16 -12
  178. package/dist/scripts/session-attach-poc.js +34 -37
  179. package/dist/scripts/session-benchmark.js +66 -147
  180. package/dist/scripts/session-dida-live-e2e.js +151 -0
  181. package/dist/scripts/session-extract-tests.js +14 -35
  182. package/dist/scripts/session-login.js +14 -13
  183. package/dist/scripts/session-tests.js +545 -583
  184. package/dist/scripts/sf-live-benchmark.js +92 -110
  185. package/dist/scripts/sf-live-cli-tests.js +2 -9
  186. package/dist/scripts/sf-soft-score-tests.js +19 -60
  187. package/dist/scripts/sf-summary-tests.js +149 -0
  188. package/dist/scripts/sf-summary.js +466 -84
  189. package/dist/scripts/skeleton-check.js +16 -14
  190. package/dist/scripts/skeleton-integration-test.js +11 -10
  191. package/dist/scripts/skills-contract-tests.js +51 -46
  192. package/dist/scripts/smoke-session-gate-tests.js +12 -20
  193. package/dist/scripts/smoke.js +541 -422
  194. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  195. package/dist/scripts/state-cli-tests.js +226 -101
  196. package/dist/scripts/state-cli.js +549 -212
  197. package/dist/scripts/static-golden-tests.js +46 -180
  198. package/dist/scripts/time-eval-tests.js +210 -192
  199. package/dist/scripts/travel-timeline-tests.js +29 -93
  200. package/dist/scripts/turn-handoff-collect-tests.js +46 -93
  201. package/dist/scripts/turn-handoff-collect.js +71 -99
  202. package/dist/scripts/turn-policy-tests.js +28 -29
  203. package/dist/scripts/typed-contract-canary.js +77 -162
  204. package/dist/scripts/tz-resolver-sanity.js +50 -0
  205. package/dist/scripts/tz-resolver-tests.js +584 -0
  206. package/dist/scripts/unified-tests.js +23 -19
  207. package/dist/scripts/visa-policy-tests.js +31 -44
  208. package/dist/scripts/weather-tests.js +121 -543
  209. package/dist/scripts/wish-channel-gate-tests.js +29 -86
  210. package/dist/scripts/write-gate-tests.js +522 -0
  211. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  212. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  213. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  214. package/dist/scripts/z3-race-tests.js +18 -12
  215. package/dist/src/artifact-gate.js +652 -221
  216. package/dist/src/backend/kernel.js +91 -0
  217. package/dist/src/backend/modules/booking-copilot.js +55 -0
  218. package/dist/src/backend/modules/booking-executor.js +124 -0
  219. package/dist/src/backend/modules/session-search.js +194 -0
  220. package/dist/src/benchmark-agent-conformance.js +296 -262
  221. package/dist/src/benchmark-environment-bridge.js +298 -413
  222. package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
  223. package/dist/src/benchmark-tool-isolation.js +94 -67
  224. package/dist/src/bookable-facts.js +299 -174
  225. package/dist/src/booking-saga.js +64 -120
  226. package/dist/src/booking-surface/availability-policy.js +443 -576
  227. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  228. package/dist/src/booking-surface/contracts.js +24 -80
  229. package/dist/src/booking-surface/dsh-planner.js +510 -281
  230. package/dist/src/booking-surface/dsh-plugin.js +17 -1
  231. package/dist/src/booking-surface/error-codes.js +82 -10
  232. package/dist/src/booking-surface/index.js +0 -3
  233. package/dist/src/booking-surface/profile.js +10 -54
  234. package/dist/src/booking-surface/recovery-chain.js +367 -0
  235. package/dist/src/booking-surface/runtime.js +765 -891
  236. package/dist/src/booking-surface/server.js +200 -251
  237. package/dist/src/booking-surface/startup.js +58 -54
  238. package/dist/src/booking-surface/validation.js +150 -218
  239. package/dist/src/bridge.js +20 -13
  240. package/dist/src/companions.js +48 -74
  241. package/dist/src/contracts.js +21 -43
  242. package/dist/src/dsh-llm.js +82 -120
  243. package/dist/src/engine.js +253 -239
  244. package/dist/src/evaluation-cadence.js +72 -209
  245. package/dist/src/evaluation-contracts.js +220 -843
  246. package/dist/src/flight-pack-adapter.js +54 -0
  247. package/dist/src/flight-pack-contract.js +2 -0
  248. package/dist/src/gotry-backend.js +70 -0
  249. package/dist/src/hotel-date-gate.js +118 -0
  250. package/dist/src/i18n.js +18 -7
  251. package/dist/src/index.js +1425 -1629
  252. package/dist/src/journey.js +132 -81
  253. package/dist/src/ledger-repair-apply.js +538 -0
  254. package/dist/src/ledger-repair-plan.js +463 -0
  255. package/dist/src/loop.js +257 -166
  256. package/dist/src/memory-capture.js +180 -32
  257. package/dist/src/memory-decay.js +30 -14
  258. package/dist/src/memory-lifecycle.js +1310 -0
  259. package/dist/src/memory-utility.js +27 -40
  260. package/dist/src/mock-llm.js +36 -61
  261. package/dist/src/model.js +81 -36
  262. package/dist/src/realtime-pricing.js +63 -72
  263. package/dist/src/slot-spec.js +84 -106
  264. package/dist/src/state-ledger.js +310 -471
  265. package/dist/src/subagent-job-id-guard.js +58 -0
  266. package/dist/src/time-anchor.js +231 -33
  267. package/dist/src/tool-packet.js +20 -8
  268. package/dist/src/travel-slots.js +58 -35
  269. package/dist/src/travel-timeline.js +44 -63
  270. package/dist/src/turn-deadline.js +121 -127
  271. package/dist/src/turn-policy.js +61 -89
  272. package/dist/src/tz-resolver.js +143 -0
  273. package/dist/src/unified.js +504 -381
  274. package/dist/src/wish-pool.js +39 -14
  275. package/dist/src/write-gate.js +495 -0
  276. package/dist/src/z3-shared.js +273 -7
  277. package/extension/README.md +4 -3
  278. package/extension/background.js +125 -14
  279. package/extension/content-bridge.js +23 -1
  280. package/extension/content-main.js +15 -4
  281. package/extension/manifest.json +16 -8
  282. package/package.json +251 -220
  283. package/ts/capabilities/agent-reach.ts +4 -4
  284. package/ts/capabilities/anything.ts +1 -1
  285. package/ts/capabilities/artifacts.ts +46 -9
  286. package/ts/capabilities/flyai.ts +156 -37
  287. package/ts/capabilities/hbcli.ts +1 -1
  288. package/ts/capabilities/incident-log.ts +88 -59
  289. package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
  290. package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
  291. package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
  292. package/ts/capabilities/session/extension-bridge.ts +226 -101
  293. package/ts/capabilities/session/extension-channel.ts +22 -9
  294. package/ts/capabilities/session/wizard.ts +2 -2
  295. package/ts/capabilities/session-consent.ts +98 -14
  296. package/ts/capabilities/session-login.ts +1 -0
  297. package/ts/capabilities/session-search.ts +315 -31
  298. package/ts/dsh-runtime/vendor/README.md +8 -4
  299. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
  300. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
  301. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
  302. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
  303. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
  304. package/ts/package.json +236 -16
  305. package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
  306. package/ts/scripts/state-cli.ts +418 -51
  307. package/ts/src/artifact-gate.ts +561 -15
  308. package/ts/src/benchmark-environment-bridge.ts +37 -20
  309. package/ts/src/bookable-facts.ts +183 -7
  310. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  311. package/ts/src/booking-surface/dsh-planner.ts +240 -32
  312. package/ts/src/booking-surface/dsh-plugin.js +17 -1
  313. package/ts/src/booking-surface/error-codes.ts +75 -0
  314. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  315. package/ts/src/booking-surface/server.ts +84 -51
  316. package/ts/src/booking-surface/validation.ts +2 -0
  317. package/ts/src/contracts.ts +12 -2
  318. package/ts/src/dsh-llm.ts +4 -8
  319. package/ts/src/index.ts +609 -90
  320. package/ts/src/loop.ts +116 -7
  321. package/ts/src/memory-capture.ts +227 -3
  322. package/ts/src/mock-llm.ts +4 -8
  323. package/ts/src/model.ts +88 -2
  324. package/ts/src/state-ledger.ts +47 -29
  325. package/ts/src/subagent-job-id-guard.ts +72 -0
  326. package/ts/src/time-anchor.ts +217 -13
  327. package/ts/src/unified.ts +316 -54
@@ -0,0 +1,496 @@
1
+ /**
2
+ * issue #283 维护测试:`gotry_hotel_search` 在缺/空/单侧/无法解析/倒序/同日/
3
+ * 不存在的日历日日期时,必须返回可行动 input_required,**不得发起 hbcli**;
4
+ * 完整、有效且退房晚于入住的日期经真实 apply 注册入口进入 fixture hbcli,
5
+ * argv 必须含解析后的绝对日期。隔离 stateRoot + 临时 fixture hbcli 在 finally
6
+ * 中清理;没有调用真实 HotelByte、访问凭证或写入共享用户数据。
7
+ *
8
+ * 运行:cd ts && npx tsx scripts/hotel-date-gate-tests.ts
9
+ *
10
+ * 测试纪律:
11
+ * - fixture hbcli argv 日志初始写 `__NOT_SPAWNED__` 哨兵,任何 spawn 必改写;
12
+ * `assertSpawned` 显式拒哨兵形态,`assertNoSpawn` 显式要求哨兵未被改写;
13
+ * - bridge-latency 走真实 `<stateRoot>/gotry-state/bridge-latency.jsonl`(与
14
+ * ensureStateDir 一致),用 before/after 快照钉死「闸失败 → 零写入」;
15
+ * - 自然日期回归使用固定 now(2026-09-09 周三),覆盖本周/跨周末/跨月/跨年,
16
+ * 用 slot-spec 同一口径算术(`weekdayOffsetDays`),避开真实时钟周日退化;
17
+ * - exact argv 断言使用完整 flag + value 字符串,允许不同顺序但每对 flag/value
18
+ * 必出现,不放过「hbcli 收到错误日期」;
19
+ * - presentResult 渲染层不暴露内部工程词(check_in_blank / 闸拒绝 / ISO 串),
20
+ * UI 只见「入住日/退房日」+ 具体日期示例。
21
+ */
22
+ import { mkdtempSync, writeFileSync, chmodSync, rmSync, readFileSync, existsSync } from 'node:fs';
23
+ import { tmpdir } from 'node:os';
24
+ import { join } from 'node:path';
25
+ import { apply } from '../src/index.js';
26
+ /** 闸未发起 hbcli 时 fixture argv 日志的固定哨兵(非空白字符串!空白也可被 spawn 写入) */
27
+ const NO_SPAWN_SENTINEL = '__NOT_SPAWNED__';
28
+ /** 临时 fixture hbcli:把 $@ 写入日志,然后 exit 0 + 空 JSON */
29
+ function installRecordingFixture(tmp, name) {
30
+ const logPath = join(tmp, `${name}.argv.log`);
31
+ const fixturePath = join(tmp, name);
32
+ writeFileSync(logPath, `${NO_SPAWN_SENTINEL}\n`);
33
+ writeFileSync(fixturePath, `#!/bin/sh\necho "$@" > '${logPath}'\necho '{}'\nexit 0\n`);
34
+ chmodSync(fixturePath, 0o755);
35
+ return { path: fixturePath, logPath };
36
+ }
37
+ /** 真实 apply 注册入口 + 临时 fixture hbcli + 隔离 stateRoot 装配 */
38
+ async function bootstrap() {
39
+ const tmp = mkdtempSync(join(tmpdir(), 'gotry-issue283-'));
40
+ const stateRoot = mkdtempSync(join(tmpdir(), 'gotry-issue283-state-'));
41
+ const stateDir = join(stateRoot, 'gotry-state');
42
+ const latencyPath = join(stateDir, 'bridge-latency.jsonl');
43
+ const fixture = installRecordingFixture(tmp, 'hbcli-record');
44
+ const registered = [];
45
+ const ctx = {
46
+ tools: { register: (t) => registered.push(t) },
47
+ systemPrompt: { variable: () => () => '' },
48
+ on: () => () => { },
49
+ };
50
+ const cfg = {
51
+ stateRoot,
52
+ timeoutMs: 5_000,
53
+ hbcliBin: fixture.path,
54
+ sessionAccess: 'off',
55
+ benchmarkEnvironmentConfigPath: '',
56
+ };
57
+ apply(ctx, cfg);
58
+ const byName = (name) => {
59
+ const t = registered.find(t => t.name === name);
60
+ if (!t)
61
+ throw new Error(`tool ${name} not registered`);
62
+ return t;
63
+ };
64
+ const cleanup = () => {
65
+ try {
66
+ rmSync(tmp, { recursive: true, force: true });
67
+ }
68
+ catch { }
69
+ try {
70
+ rmSync(stateRoot, { recursive: true, force: true });
71
+ }
72
+ catch { }
73
+ };
74
+ return { tmp, stateRoot, stateDir, latencyPath, fixturePath: fixture.path, fixtureLogPath: fixture.logPath, byName, cleanup };
75
+ }
76
+ /** 重置哨兵:每次 no-spawn 断言前先把日志改回哨兵,确保断言的是本次执行而非历史残留 */
77
+ function resetNoSpawnSentinel(fixtureLogPath) {
78
+ writeFileSync(fixtureLogPath, `${NO_SPAWN_SENTINEL}\n`);
79
+ }
80
+ function assertNoSpawn(label, fixtureLogPath) {
81
+ let argv = '';
82
+ try {
83
+ argv = readFileSync(fixtureLogPath, 'utf-8').trim();
84
+ }
85
+ catch {
86
+ argv = '';
87
+ }
88
+ if (argv !== NO_SPAWN_SENTINEL) {
89
+ throw new Error(`FAIL[${label}]: 闸失败时 fixture hbcli 不应被调用,但日志被改写: ${argv.slice(0, 200)}`);
90
+ }
91
+ }
92
+ function assertSpawned(label, fixtureLogPath) {
93
+ let argv = '';
94
+ try {
95
+ argv = readFileSync(fixtureLogPath, 'utf-8').trim();
96
+ }
97
+ catch {
98
+ argv = '';
99
+ }
100
+ if (argv === '' || argv === NO_SPAWN_SENTINEL) {
101
+ throw new Error(`FAIL[${label}]: fixture hbcli 应被调用但日志未被改写(${fixtureLogPath})`);
102
+ }
103
+ return argv;
104
+ }
105
+ /** 读 bridge-latency.jsonl 行数(不存在 → 0);ENOENT 不算错 */
106
+ function readLatencyLineCount(path) {
107
+ if (!existsSync(path))
108
+ return 0;
109
+ const text = readFileSync(path, 'utf-8');
110
+ if (!text.trim())
111
+ return 0;
112
+ return text.trim().split('\n').length;
113
+ }
114
+ /** 精确 argv 断言:flag 后紧跟 value(忽略中间 flag 名);flag/value 配对出现即通过 */
115
+ function assertHasPair(argv, flag, value, label) {
116
+ const tokens = argv.split(/\s+/).filter(Boolean);
117
+ for (let i = 0; i < tokens.length - 1; i++) {
118
+ if (tokens[i] === flag && tokens[i + 1] === value)
119
+ return;
120
+ }
121
+ throw new Error(`FAIL[${label} argv]: 期望 ${flag} ${value} 配对出现,实际 ${argv}`);
122
+ }
123
+ const suite = await bootstrap();
124
+ const hotel = suite.byName('gotry_hotel_search');
125
+ try {
126
+ // ============================================================
127
+ // §A 闸失败路径(共 N 个反例)—— 必须 no-spawn + 一致的 reason/missing/raw/message/evidence
128
+ // ============================================================
129
+ // 1) 基线反例:空字符串两侧 → no spawn
130
+ {
131
+ resetNoSpawnSentinel(suite.fixtureLogPath);
132
+ const empty = await hotel.execute({ destination: '迪拜', checkIn: '', checkOut: '', adults: 2 }, null);
133
+ if (empty.ok !== false)
134
+ throw new Error(`FAIL[空日期]: 应 ok=false,实际 ${JSON.stringify(empty).slice(0, 200)}`);
135
+ if (empty.verdict !== 'input_required')
136
+ throw new Error(`FAIL[空日期]: 应 verdict=input_required,实际 ${empty.verdict}`);
137
+ if (!empty.reason?.startsWith('check_') || !empty.reason.endsWith('_blank'))
138
+ throw new Error(`FAIL[空日期]: reason 应为 check_*_blank 形态,实际 ${empty.reason}`);
139
+ if (!empty.missing?.includes('checkIn') || !empty.missing?.includes('checkOut'))
140
+ throw new Error(`FAIL[空日期]: missing 应含 checkIn/checkOut,实际 ${JSON.stringify(empty.missing)}`);
141
+ if (!empty.evidence?.includes('hotel-date-gate'))
142
+ throw new Error(`FAIL[空日期]: evidence 应带 hotel-date-gate 标注,实际 ${empty.evidence}`);
143
+ if (empty.raw?.checkIn !== '' || empty.raw?.checkOut !== '')
144
+ throw new Error(`FAIL[空日期]: raw 应回显空串,实际 ${JSON.stringify(empty.raw)}`);
145
+ assertNoSpawn('空日期', suite.fixtureLogPath);
146
+ console.log('1) 空日期 → input_required(check_*_blank),both missing,no spawn OK');
147
+ }
148
+ // 2) 完全缺失(字段未传) → no spawn
149
+ {
150
+ resetNoSpawnSentinel(suite.fixtureLogPath);
151
+ const missing = await hotel.execute({ destination: '迪拜', adults: 2 }, null);
152
+ if (missing.ok !== false || missing.verdict !== 'input_required')
153
+ throw new Error(`FAIL[缺失]: ${JSON.stringify(missing).slice(0, 200)}`);
154
+ if (!missing.missing?.includes('checkIn') || !missing.missing?.includes('checkOut'))
155
+ throw new Error(`FAIL[缺失]: missing 应含两侧,实际 ${JSON.stringify(missing.missing)}`);
156
+ if (missing.raw?.checkIn !== undefined || missing.raw?.checkOut !== undefined)
157
+ throw new Error(`FAIL[缺失]: raw 应全 undefined,实际 ${JSON.stringify(missing.raw)}`);
158
+ assertNoSpawn('完全缺失', suite.fixtureLogPath);
159
+ console.log('2) 完全缺失 → input_required, 两侧 missing, raw=undefined, no spawn OK');
160
+ }
161
+ // 3) 单侧:仅入住,无退房 → no spawn
162
+ {
163
+ resetNoSpawnSentinel(suite.fixtureLogPath);
164
+ const oneSide = await hotel.execute({ destination: '普吉', checkIn: '2026-09-18', adults: 2 }, null);
165
+ if (oneSide.ok !== false || oneSide.verdict !== 'input_required' || oneSide.reason !== 'check_out_missing')
166
+ throw new Error(`FAIL[单侧]: ${JSON.stringify(oneSide).slice(0, 200)}`);
167
+ if (!oneSide.missing?.includes('checkOut'))
168
+ throw new Error(`FAIL[单侧]: missing 应含 checkOut,实际 ${JSON.stringify(oneSide.missing)}`);
169
+ if (oneSide.raw?.checkIn !== '2026-09-18' || oneSide.raw?.checkOut !== undefined)
170
+ throw new Error(`FAIL[单侧]: raw 应回显 checkIn 原话、checkOut 缺失,实际 ${JSON.stringify(oneSide.raw)}`);
171
+ assertNoSpawn('单侧', suite.fixtureLogPath);
172
+ console.log('3) 单侧(只 checkIn) → input_required(check_out_missing),no spawn OK');
173
+ }
174
+ // 3b) 单侧反向:仅退房,无入住 → no spawn
175
+ {
176
+ resetNoSpawnSentinel(suite.fixtureLogPath);
177
+ const oneSideRev = await hotel.execute({ destination: '普吉', checkOut: '2026-09-20', adults: 2 }, null);
178
+ if (oneSideRev.ok !== false || oneSideRev.reason !== 'check_in_missing')
179
+ throw new Error(`FAIL[单侧反向]: ${JSON.stringify(oneSideRev).slice(0, 200)}`);
180
+ if (!oneSideRev.missing?.includes('checkIn'))
181
+ throw new Error(`FAIL[单侧反向]: missing 应含 checkIn,实际 ${JSON.stringify(oneSideRev.missing)}`);
182
+ assertNoSpawn('单侧反向', suite.fixtureLogPath);
183
+ console.log('3b) 单侧(只 checkOut) → input_required(check_in_missing),no spawn OK');
184
+ }
185
+ // 3c) 混合(缺失 + 空串)→ 一次报齐两侧(避免两轮往返);原 3c 期望 reason=check_in_missing,
186
+ // 现两侧均报缺(missing 含 checkIn+checkOut),evidence 带 :both
187
+ {
188
+ resetNoSpawnSentinel(suite.fixtureLogPath);
189
+ const mixed = await hotel.execute({ destination: '普吉', checkOut: '', adults: 2 }, null);
190
+ if (mixed.ok !== false || mixed.verdict !== 'input_required')
191
+ throw new Error(`FAIL[混合缺失空白]: ${JSON.stringify(mixed).slice(0, 200)}`);
192
+ if (!mixed.missing?.includes('checkIn') || !mixed.missing?.includes('checkOut'))
193
+ throw new Error(`FAIL[混合缺失空白]: missing 应含 checkIn+checkOut(一次报齐),实际 ${JSON.stringify(mixed.missing)}`);
194
+ if (!mixed.evidence?.includes(':both'))
195
+ throw new Error(`FAIL[混合缺失空白]: evidence 应带 :both 标注,实际 ${mixed.evidence}`);
196
+ assertNoSpawn('混合缺失空白', suite.fixtureLogPath);
197
+ console.log('3c) 混合(checkIn 缺 + checkOut 空) → 一次报齐两侧,no spawn OK');
198
+ }
199
+ // 3d) 混合(空串 + 缺失)→ 一次报齐两侧
200
+ {
201
+ resetNoSpawnSentinel(suite.fixtureLogPath);
202
+ const mixed2 = await hotel.execute({ destination: '普吉', checkIn: '', adults: 2 }, null);
203
+ if (mixed2.ok !== false || mixed2.verdict !== 'input_required')
204
+ throw new Error(`FAIL[混合空白缺失]: ${JSON.stringify(mixed2).slice(0, 200)}`);
205
+ if (!mixed2.missing?.includes('checkIn') || !mixed2.missing?.includes('checkOut'))
206
+ throw new Error(`FAIL[混合空白缺失]: missing 应含 checkIn+checkOut(一次报齐),实际 ${JSON.stringify(mixed2.missing)}`);
207
+ if (mixed2.raw?.checkIn !== '' || mixed2.raw?.checkOut !== undefined)
208
+ throw new Error(`FAIL[混合空白缺失]: raw 应回显 checkIn 空串+checkOut 缺失,实际 ${JSON.stringify(mixed2.raw)}`);
209
+ if (!mixed2.evidence?.includes(':both'))
210
+ throw new Error(`FAIL[混合空白缺失]: evidence 应带 :both 标注,实际 ${mixed2.evidence}`);
211
+ assertNoSpawn('混合空白缺失', suite.fixtureLogPath);
212
+ console.log('3d) 混合(checkIn 空 + checkOut 缺) → 一次报齐两侧,no spawn OK');
213
+ }
214
+ // 4) 倒序:退房早于入住 → no spawn
215
+ {
216
+ resetNoSpawnSentinel(suite.fixtureLogPath);
217
+ const reversed = await hotel.execute({ destination: '曼谷', checkIn: '2026-09-20', checkOut: '2026-09-18', adults: 2 }, null);
218
+ if (reversed.ok !== false || reversed.verdict !== 'input_required' || reversed.reason !== 'check_out_not_after_check_in')
219
+ throw new Error(`FAIL[倒序]: ${JSON.stringify(reversed).slice(0, 200)}`);
220
+ if (!reversed.missing?.includes('checkOut'))
221
+ throw new Error(`FAIL[倒序]: missing 应含 checkOut(指明需更正的一侧),实际 ${JSON.stringify(reversed.missing)}`);
222
+ if (reversed.raw?.checkIn !== '2026-09-20' || reversed.raw?.checkOut !== '2026-09-18')
223
+ throw new Error(`FAIL[倒序]: raw 应回显原话,实际 ${JSON.stringify(reversed.raw)}`);
224
+ if (!reversed.evidence?.includes(':reversed'))
225
+ throw new Error(`FAIL[倒序]: evidence 应带 :reversed 子标签,实际 ${reversed.evidence}`);
226
+ assertNoSpawn('倒序', suite.fixtureLogPath);
227
+ console.log('4) 倒序 → input_required(check_out_not_after_check_in:reversed),no spawn OK');
228
+ }
229
+ // 5) 同日:0 晚不查
230
+ {
231
+ resetNoSpawnSentinel(suite.fixtureLogPath);
232
+ const sameDay = await hotel.execute({ destination: '大理', checkIn: '2026-09-18', checkOut: '2026-09-18', adults: 2 }, null);
233
+ if (sameDay.ok !== false || sameDay.reason !== 'check_out_not_after_check_in')
234
+ throw new Error(`FAIL[同日]: ${JSON.stringify(sameDay).slice(0, 200)}`);
235
+ if (!sameDay.evidence?.includes(':same_day'))
236
+ throw new Error(`FAIL[同日]: evidence 应带 :same_day 子标签,实际 ${sameDay.evidence}`);
237
+ assertNoSpawn('同日', suite.fixtureLogPath);
238
+ console.log('5) 同日 → input_required(check_out_not_after_check_in:same_day),no spawn OK');
239
+ }
240
+ // 6) 无法解析:词表外表达 → no spawn
241
+ {
242
+ resetNoSpawnSentinel(suite.fixtureLogPath);
243
+ const unresolved = await hotel.execute({ destination: '大理', checkIn: '近期', checkOut: '下周', adults: 2 }, null);
244
+ if (unresolved.ok !== false || unresolved.verdict !== 'input_required')
245
+ throw new Error(`FAIL[无法解析]: ${JSON.stringify(unresolved).slice(0, 200)}`);
246
+ if (unresolved.reason !== 'check_in_unresolved')
247
+ throw new Error(`FAIL[无法解析]: reason 应为 check_in_unresolved,实际 ${unresolved.reason}`);
248
+ if (!unresolved.missing?.includes('checkIn'))
249
+ throw new Error(`FAIL[无法解析]: missing 应含 checkIn,实际 ${JSON.stringify(unresolved.missing)}`);
250
+ if (!unresolved.message?.includes('合法的具体日期'))
251
+ throw new Error(`FAIL[无法解析]: message 应明示「合法的具体日期」,实际 ${unresolved.message}`);
252
+ assertNoSpawn('无法解析', suite.fixtureLogPath);
253
+ console.log('6) 无法解析 → input_required(check_in_unresolved),no spawn OK');
254
+ }
255
+ // 6b) 不存在的日历日(2026-02-30 / 2026-13-01)→ 闸拒绝(原闸 1 维护回归反例)
256
+ // issue #283 红线:无效日历日期不能伪装成合法,不发供应商命令
257
+ {
258
+ resetNoSpawnSentinel(suite.fixtureLogPath);
259
+ const invalidDate = await hotel.execute({ destination: '大理', checkIn: '2026-02-30', checkOut: '2026-09-20', adults: 2 }, null);
260
+ if (invalidDate.ok !== false || invalidDate.verdict !== 'input_required')
261
+ throw new Error(`FAIL[不存在的日历日]: ${JSON.stringify(invalidDate).slice(0, 200)}`);
262
+ if (invalidDate.reason !== 'check_in_unresolved')
263
+ throw new Error(`FAIL[不存在的日历日]: reason 应为 check_in_unresolved,实际 ${invalidDate.reason}`);
264
+ if (!invalidDate.missing?.includes('checkIn'))
265
+ throw new Error(`FAIL[不存在的日历日]: missing 应含 checkIn,实际 ${JSON.stringify(invalidDate.missing)}`);
266
+ if (!invalidDate.message?.includes('合法'))
267
+ throw new Error(`FAIL[不存在的日历日]: message 应明示「合法」,实际 ${invalidDate.message}`);
268
+ if (invalidDate.raw?.checkIn !== '2026-02-30')
269
+ throw new Error(`FAIL[不存在的日历日]: raw 应回显原话 2026-02-30,实际 ${JSON.stringify(invalidDate.raw)}`);
270
+ assertNoSpawn('不存在的日历日', suite.fixtureLogPath);
271
+ console.log('6b) 不存在的日历日(2026-02-30) → input_required(check_in_unresolved),no spawn OK');
272
+ }
273
+ // 6c) 月份越界(2026-13-01)
274
+ {
275
+ resetNoSpawnSentinel(suite.fixtureLogPath);
276
+ const monthOut = await hotel.execute({ destination: '大理', checkIn: '2026-09-18', checkOut: '2026-13-01', adults: 2 }, null);
277
+ if (monthOut.ok !== false || monthOut.verdict !== 'input_required' || monthOut.reason !== 'check_out_unresolved') {
278
+ throw new Error(`FAIL[月份越界]: ${JSON.stringify(monthOut).slice(0, 200)}`);
279
+ }
280
+ assertNoSpawn('月份越界', suite.fixtureLogPath);
281
+ console.log('6c) 月份越界(2026-13-01) → input_required(check_out_unresolved),no spawn OK');
282
+ }
283
+ // 6d) 闰年错日(2025-02-29)
284
+ {
285
+ resetNoSpawnSentinel(suite.fixtureLogPath);
286
+ const leap = await hotel.execute({ destination: '大理', checkIn: '2025-02-29', checkOut: '2025-03-02', adults: 2 }, null);
287
+ if (leap.ok !== false || leap.verdict !== 'input_required' || leap.reason !== 'check_in_unresolved') {
288
+ throw new Error(`FAIL[闰年错日]: ${JSON.stringify(leap).slice(0, 200)}`);
289
+ }
290
+ assertNoSpawn('闰年错日', suite.fixtureLogPath);
291
+ console.log('6d) 闰年错日(2025-02-29) → input_required(check_in_unresolved),no spawn OK');
292
+ }
293
+ // 6e) +N 算术溢出(2026-09-18+100000000)→ 闸拒绝(issue #283 第二层红线)
294
+ // resolveSlotDate 的 addDaysYmd 溢出产生 Invalid Date("NaN-NaN-NaN"),
295
+ // 闸终消费层 isStrictIsoYmd 拒掉非 YYYY-MM-DD 格式,归 unresolved 拒绝发供应商命令
296
+ {
297
+ resetNoSpawnSentinel(suite.fixtureLogPath);
298
+ const bigPlus = await hotel.execute({ destination: '大理', checkIn: '2026-09-18', checkOut: '2026-09-18+100000000', adults: 2 }, null);
299
+ if (bigPlus.ok !== false || bigPlus.verdict !== 'input_required')
300
+ throw new Error(`FAIL[+N 溢出]: ${JSON.stringify(bigPlus).slice(0, 200)}`);
301
+ if (bigPlus.reason !== 'check_out_unresolved')
302
+ throw new Error(`FAIL[+N 溢出]: reason 应为 check_out_unresolved,实际 ${bigPlus.reason}`);
303
+ if (!bigPlus.missing?.includes('checkOut'))
304
+ throw new Error(`FAIL[+N 溢出]: missing 应含 checkOut,实际 ${JSON.stringify(bigPlus.missing)}`);
305
+ if (bigPlus.raw?.checkOut !== '2026-09-18+100000000')
306
+ throw new Error(`FAIL[+N 溢出]: raw 应回显原话,实际 ${JSON.stringify(bigPlus.raw)}`);
307
+ if (!bigPlus.message?.includes('合法'))
308
+ throw new Error(`FAIL[+N 溢出]: message 应明示「合法」,实际 ${bigPlus.message}`);
309
+ assertNoSpawn('+N 溢出', suite.fixtureLogPath);
310
+ console.log('6e) +N 算术溢出(2026-09-18+100000000 → NaN-NaN-NaN)→ input_required(check_out_unresolved),no spawn OK');
311
+ }
312
+ // 6f) JS 自动进位跨千年(9999-12-31+1 → "10000-01-01")→ 闸拒绝
313
+ // 闸终消费层 isStrictIsoYmd 拒掉 5 位数年份(> 9999),归 unresolved
314
+ {
315
+ resetNoSpawnSentinel(suite.fixtureLogPath);
316
+ const year10k = await hotel.execute({ destination: '大理', checkIn: '2026-09-18', checkOut: '9999-12-31+1', adults: 2 }, null);
317
+ if (year10k.ok !== false || year10k.verdict !== 'input_required')
318
+ throw new Error(`FAIL[跨千年]: ${JSON.stringify(year10k).slice(0, 200)}`);
319
+ if (year10k.reason !== 'check_out_unresolved')
320
+ throw new Error(`FAIL[跨千年]: reason 应为 check_out_unresolved,实际 ${year10k.reason}`);
321
+ if (year10k.raw?.checkOut !== '9999-12-31+1')
322
+ throw new Error(`FAIL[跨千年]: raw 应回显原话,实际 ${JSON.stringify(year10k.raw)}`);
323
+ assertNoSpawn('跨千年', suite.fixtureLogPath);
324
+ console.log('6f) JS 自动进位跨千年(9999-12-31+1 → 10000-01-01)→ input_required(check_out_unresolved),no spawn OK');
325
+ }
326
+ // 7) 类型错误:非字符串入参(直接测闸函数)→ 闸降级为 *_blank:type
327
+ {
328
+ const { evaluateHotelStayDates } = await import('../src/hotel-date-gate.js');
329
+ const typeErr = evaluateHotelStayDates(42, '2026-09-20');
330
+ if (typeErr.ok !== false || typeErr.reason !== 'check_in_blank')
331
+ throw new Error(`FAIL[类型错误]: ${JSON.stringify(typeErr).slice(0, 200)}`);
332
+ if (!typeErr.missing?.includes('checkIn'))
333
+ throw new Error(`FAIL[类型错误]: missing 应含 checkIn,实际 ${JSON.stringify(typeErr.missing)}`);
334
+ if (!typeErr.evidence?.includes(':type'))
335
+ throw new Error(`FAIL[类型错误]: evidence 应带 :type 标注,实际 ${typeErr.evidence}`);
336
+ if (typeErr.raw?.checkIn !== undefined)
337
+ throw new Error(`FAIL[类型错误]: raw.checkIn 应为 undefined(非字符串不入 raw),实际 ${JSON.stringify(typeErr.raw)}`);
338
+ console.log('7) 类型错误(数字 checkIn) → 闸函数直接 input_required(check_in_blank:type),raw undefined OK');
339
+ }
340
+ // ============================================================
341
+ // §B 闸通过路径 —— 必须真实 spawn + fixture argv 含精确日期
342
+ // ============================================================
343
+ // 8) 有效日期(绝对 ISO) → 真实工具入口 → fixture hbcli 收到精确 argv
344
+ {
345
+ const ok = await hotel.execute({ destination: '迪拜', checkIn: '2026-09-18', checkOut: '2026-09-20', adults: 2 }, null);
346
+ if (ok.ok !== true)
347
+ throw new Error(`FAIL[有效日期]: 应 ok=true,实际 ${JSON.stringify(ok).slice(0, 200)}`);
348
+ if (ok.date_notes && ok.date_notes.length)
349
+ throw new Error(`FAIL[有效日期]: 绝对 ISO 不应产生 slot-resolved note,实际 ${JSON.stringify(ok.date_notes)}`);
350
+ const argv = assertSpawned('有效日期', suite.fixtureLogPath);
351
+ if (!argv.includes('search'))
352
+ throw new Error(`FAIL[有效日期 argv]: 缺 search 子命令,实际 ${argv}`);
353
+ if (!argv.includes('hotel-list'))
354
+ throw new Error(`FAIL[有效日期 argv]: 缺 hotel-list,实际 ${argv}`);
355
+ assertHasPair(argv, '--destination-name', '迪拜', '有效日期');
356
+ assertHasPair(argv, '--check-in', '2026-09-18', '有效日期');
357
+ assertHasPair(argv, '--check-out', '2026-09-20', '有效日期');
358
+ if (!argv.includes('--room-occupancies'))
359
+ throw new Error(`FAIL[有效日期 argv]: 缺 --room-occupancies,实际 ${argv}`);
360
+ console.log(`8) 有效日期 → 真实工具入口 + fixture 完整 argv OK:${argv.slice(0, 200)}`);
361
+ }
362
+ // 9) 自然语言日期 slot-spec 口径:固定时钟 2026-09-09(周三)+ 锚点卡词表「明天/下周五+3天」
363
+ // 闸函数直接调,注入 fixed now;真实 execute 路径见 10(避免真实时钟周日退化撞 same_day)
364
+ // 固定 now 保证 todayDow=3,「下周五」= today+9(不会退化到 today+0 撞 same_day)
365
+ {
366
+ const fixedNow = new Date(2026, 8, 9); // 2026-09-09(本地时区,周三)
367
+ const { evaluateHotelStayDates } = await import('../src/hotel-date-gate.js');
368
+ const probe = evaluateHotelStayDates('明天', '下周五+3天', { now: fixedNow });
369
+ if (probe.ok !== true)
370
+ throw new Error(`FAIL[自然语言预演]: 闸应通过,实际 ${JSON.stringify(probe).slice(0, 200)}`);
371
+ // 明天 = today + 1 = 2026-09-10;下周五 = today + 9 = 2026-09-18;+3 天 = 2026-09-21
372
+ if (probe.checkIn !== '2026-09-10' || probe.checkOut !== '2026-09-21') {
373
+ throw new Error(`FAIL[自然语言预演]: 期望 checkIn=2026-09-10 checkOut=2026-09-21,实际 ${probe.checkIn}/${probe.checkOut}`);
374
+ }
375
+ console.log('9) 自然语言(明天/下周五+3天 → 2026-09-10/2026-09-21,固定时钟周三 2026-09-09,slot-spec 口径)闸预演 OK');
376
+ }
377
+ // 10) 真实 execute + 自然语言(真实时钟):下个月初/下个月中旬
378
+ // 不管今天是几号,「下个月初」必然是下月 1 号、「下个月中旬」必然是下月 11 号,
379
+ // 退房永远晚于入住,不撞 same_day;跨月/跨年用 Date 自动进位(同 slot-spec 口径)
380
+ // 计算期望:以真实 today 出发,「下个月初」= (today 年/月+1)/01;「下个月中旬」= (today 年/月+1)/11
381
+ {
382
+ const realNow = new Date();
383
+ const nextMonthFirst = new Date(realNow.getFullYear(), realNow.getMonth() + 1, 1);
384
+ const nextMonthMid = new Date(realNow.getFullYear(), realNow.getMonth() + 1, 11);
385
+ const fmt = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
386
+ const expectIn = fmt(nextMonthFirst);
387
+ const expectOut = fmt(nextMonthMid);
388
+ if (expectOut <= expectIn)
389
+ throw new Error(`FAIL[锚点卡跨月]: 算术口径出错 expectIn=${expectIn} expectOut=${expectOut}`);
390
+ const ok = await hotel.execute({ destination: '大理', checkIn: '下个月初', checkOut: '下个月中旬', adults: 2 }, null);
391
+ if (ok.ok !== true)
392
+ throw new Error(`FAIL[锚点卡跨月]: 应 ok=true,实际 ${JSON.stringify(ok).slice(0, 200)}`);
393
+ const argv = assertSpawned('锚点卡跨月', suite.fixtureLogPath);
394
+ assertHasPair(argv, '--check-in', expectIn, '锚点卡跨月');
395
+ assertHasPair(argv, '--check-out', expectOut, '锚点卡跨月');
396
+ console.log(`10) 真实 execute + 锚点卡跨月(下个月初/下个月中旬 → ${expectIn}/${expectOut},用 Date 自动进位)进 fixture argv OK`);
397
+ }
398
+ // ============================================================
399
+ // §C 闸失败不写副作用面(bridge-latency + 共享用户状态)
400
+ // ============================================================
401
+ // 11) 闸失败不写延迟日志 + 成功路径写一行(before/after diff,真实 gotry-state 子路径)
402
+ {
403
+ const before = readLatencyLineCount(suite.latencyPath);
404
+ if (before < 1)
405
+ throw new Error(`FAIL[11 before]: §B 应至少 1 行 baseline,实际=${before}`);
406
+ const failCall = await hotel.execute({ destination: '大理', checkIn: '', checkOut: '' }, null);
407
+ if (failCall.ok !== false)
408
+ throw new Error(`FAIL[11]: 闸应拒绝,实际 ${JSON.stringify(failCall).slice(0, 200)}`);
409
+ const afterFail = readLatencyLineCount(suite.latencyPath);
410
+ if (afterFail !== before)
411
+ throw new Error(`FAIL[11 after-fail]: 闸失败应零增量,before=${before} after=${afterFail}`);
412
+ console.log(`11a) 闸失败:bridge-latency.jsonl 零增量(before=${before} after-fail=${afterFail})OK`);
413
+ const okCall = await hotel.execute({ destination: '大理', checkIn: '2026-09-18', checkOut: '2026-09-20', adults: 2 }, null);
414
+ if (okCall.ok !== true)
415
+ throw new Error(`FAIL[11 ok]: 闸应通过,实际 ${JSON.stringify(okCall).slice(0, 200)}`);
416
+ const afterOk = readLatencyLineCount(suite.latencyPath);
417
+ if (afterOk !== before + 1)
418
+ throw new Error(`FAIL[11 after-ok]: 闸通过应恰好 +1,before=${before} after=${afterOk}`);
419
+ const lines = readFileSync(suite.latencyPath, 'utf-8').trim().split('\n');
420
+ const newLine = lines[lines.length - 1] ?? '';
421
+ if (!/hotel_search/.test(newLine))
422
+ throw new Error(`FAIL[11 after-ok]: 新行 kind 应含 hotel_search,实际 ${newLine}`);
423
+ console.log(`11b) 闸通过:bridge-latency.jsonl +1 行(=${afterOk}),新行=${newLine.slice(0, 120)}`);
424
+ }
425
+ // 12) 闸失败不写共享用户状态(隔离纪律:动机/愿望池/时间线 零落盘)
426
+ {
427
+ let userStateTouched = false;
428
+ if (existsSync(suite.stateDir)) {
429
+ const { readdirSync } = await import('node:fs');
430
+ const files = readdirSync(suite.stateDir).filter(n => !n.startsWith('.') && n !== 'bridge-latency.jsonl');
431
+ if (files.length > 0) {
432
+ userStateTouched = true;
433
+ throw new Error(`FAIL[12]: gotry-state 应只含 bridge-latency.jsonl,实际含 ${files.join(', ')}`);
434
+ }
435
+ }
436
+ const failCall2 = await hotel.execute({ destination: '大理', checkIn: '近期', checkOut: '' }, null);
437
+ if (failCall2.ok !== false)
438
+ throw new Error(`FAIL[12]: 闸应拒绝,实际 ${JSON.stringify(failCall2).slice(0, 200)}`);
439
+ if (existsSync(suite.stateDir)) {
440
+ const { readdirSync } = await import('node:fs');
441
+ const files = readdirSync(suite.stateDir).filter(n => !n.startsWith('.') && n !== 'bridge-latency.jsonl');
442
+ if (files.length > 0)
443
+ throw new Error(`FAIL[12]: 闸失败后 gotry-state 不应新增用户面文件,实际含 ${files.join(', ')}`);
444
+ }
445
+ console.log(`12) 闸失败不写共享用户状态(隔离纪律):${userStateTouched ? '失败' : 'OK'}`);
446
+ }
447
+ // 13) 空白字符串(用户传了空白而非空串)→ blank 拒绝
448
+ {
449
+ resetNoSpawnSentinel(suite.fixtureLogPath);
450
+ const blanks = await hotel.execute({ destination: '大理', checkIn: '2026-09-18', checkOut: ' ', adults: 2 }, null);
451
+ if (blanks.ok !== false || blanks.reason !== 'check_out_blank')
452
+ throw new Error(`FAIL[空白串]: ${JSON.stringify(blanks).slice(0, 200)}`);
453
+ if (blanks.raw?.checkOut !== ' ')
454
+ throw new Error(`FAIL[空白串]: raw.checkOut 应回显用户原话(空白),实际 ${JSON.stringify(blanks.raw)}`);
455
+ assertNoSpawn('空白串', suite.fixtureLogPath);
456
+ console.log('13) 空白字符串 → input_required(check_out_blank),raw 回显空白,no spawn OK');
457
+ }
458
+ // 14) presentResult 闸失败卡:用户可行动中文 + 不暴露内部工程词
459
+ {
460
+ resetNoSpawnSentinel(suite.fixtureLogPath);
461
+ const gateFail = await hotel.execute({ destination: '曼谷', checkIn: '', checkOut: '', adults: 2 }, null);
462
+ if (gateFail.ok !== false)
463
+ throw new Error(`FAIL[14]: 闸应拒绝`);
464
+ const card = hotel.presentResult?.({ destination: '曼谷' }, gateFail);
465
+ if (!card)
466
+ throw new Error('FAIL[14]: presentResult 应返回卡片对象');
467
+ if (!card.title?.includes('酒店') || !card.title?.includes('曼谷'))
468
+ throw new Error(`FAIL[14]: 标题应含「酒店:曼谷」,实际 ${card.title}`);
469
+ if (card.title?.includes('check_') || card.title?.includes('闸拒'))
470
+ throw new Error(`FAIL[14]: 标题不应暴露工程词,实际 ${card.title}`);
471
+ const body = card.content?.[0]?.text ?? '';
472
+ if (!body.includes('入住日') || !body.includes('退房日'))
473
+ throw new Error(`FAIL[14]: 内容应指明需补哪些字段,实际 ${body}`);
474
+ if (!body.includes('2026-09-18') && !body.includes('2026-09-20'))
475
+ throw new Error(`FAIL[14]: 内容应给具体日期示例,实际 ${body}`);
476
+ if (body.includes('闸拒') || body.includes('check_') || body.includes('ISO'))
477
+ throw new Error(`FAIL[14]: 内容不应暴露工程词,实际 ${body}`);
478
+ // P2 措辞修正:presentResult 卡文案直接对用户说话,不再绕一道
479
+ // 「请告诉用户/请告诉 gotry/请向用户确认」转给模型;date-gate.message
480
+ // 里残留的字段指引也归一为直接对用户措辞,但保留日期示例等可行动细节。
481
+ if (!body.includes('请补充') && !body.includes('请提供'))
482
+ throw new Error(`FAIL[14]: 内容应使用直接对用户措辞(请补充/请提供),实际 ${body}`);
483
+ if (body.includes('请告诉用户'))
484
+ throw new Error(`FAIL[14]: 内容不应再含模型转手措辞「请告诉用户」,实际 ${body}`);
485
+ if (body.includes('请告诉 gotry'))
486
+ throw new Error(`FAIL[14]: 内容不应再含模型转手措辞「请告诉 gotry」,实际 ${body}`);
487
+ if (body.includes('请向用户确认'))
488
+ throw new Error(`FAIL[14]: 内容不应再含模型转手措辞「请向用户确认」,实际 ${body}`);
489
+ assertNoSpawn('presentResult sanity', suite.fixtureLogPath);
490
+ console.log('14) presentResult 闸失败卡:title「酒店:曼谷 需要入住日和退房日」+ body 给具体日期示例,无工程词 OK');
491
+ }
492
+ console.log('\nHOTEL DATE GATE TESTS: 16/16 OK');
493
+ }
494
+ finally {
495
+ suite.cleanup();
496
+ }