@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,141 @@
1
+ /**
2
+ * HotelByte 交易 bridge unknown 查单对账契约测试(issue #232;全离线纯函数,零真实供应商调用):
3
+ * 1. book 执行分类:CLI exit0 ≠ 成功——timeout/kill/坏响应/exit0-non-JSON/无绑定/部分确认归 unknown,
4
+ * 显式业务失败归 supplier_failed,只有带 customerReferenceNo 绑定的显式成功才 binding_verified
5
+ * 2. 对账种子 fail-closed:attemptId/customerReferenceNo/idem_key/fingerprint 缺一即拒(可追溯红线)
6
+ * 3. 查单 miss 在恢复窗口内(180s Phase1 + ≤10min Phase2)保持 unknown,结构性禁止重订
7
+ * 4. 窗口届满本身不是无订单证明——仍 unknown,转人工对账,不允许新 intent
8
+ * 5. 查单命中:终态成功(含迟到成功)→ reconciled_success;供应商终态取消/失败 → reconciled_failed
9
+ * (权威负证据,唯一允许新 intent 的路径);非终态 → 保持 unknown
10
+ * 6. 查单冲突(同 ref 多单/参考号不匹配)→ 显式 conflict + manual_reconcile,不静默当成功也不丢弃
11
+ * 7. 查询自身失败/畸形 → unknown 不变(探针失败不证明订单不存在);permission_denied → 人工
12
+ * 8. 人工确认无订单(须绑定 attempt 的非空操作证据)→ reconciled_failed + 允许新 intent
13
+ * 9. 终态后出现反证(迟到命中)→ 只置 conflict 交人工,不静默翻转
14
+ * 10. 可追溯:每条事件带 probeDigest;记录携带 fact_id/审批 digest/outbox 占位/attempt;
15
+ * 纯函数——推进返回新状态,原状态不可变
16
+ *
17
+ * 运行: cd ts && npx tsx scripts/hotelbyte-reconcile-tests.ts
18
+ */
19
+ import assert from 'node:assert/strict';
20
+ import { classifyBookExecution, enterUnknown, advanceReconciliation, recoveryWindowEndsAt, newIntentAllowed, } from '../capabilities/hotelbyte-transaction.js';
21
+ const T0 = '2026-09-11T10:00:00.000Z';
22
+ const WINDOW_END = recoveryWindowEndsAt(T0);
23
+ /** 标准对账种子:绑定键齐全,可追溯字段占位(fact_id / 审批 digest / outbox intent) */
24
+ function seed(overrides = {}) {
25
+ return enterUnknown({
26
+ attemptId: 'att-0001',
27
+ customerReferenceNo: 'REF-GOTRY-0001',
28
+ intentIdemKey: 'idem-book-abc',
29
+ requestFingerprintSha256: 'f'.repeat(64),
30
+ enteredUnknownAt: T0,
31
+ recoveryWindowEndsAt: WINDOW_END,
32
+ factId: 'fact_hotel_123',
33
+ approvalReceiptDigest: 'receipt-digest-01',
34
+ outboxIntentRef: 'outbox-intent-01',
35
+ ...overrides,
36
+ });
37
+ }
38
+ function bookResult(overrides = {}) {
39
+ return { exitCode: 0, signal: null, timedOut: false, stdout: '', stderr: '', ...overrides };
40
+ }
41
+ // ── 1. book 执行分类:exit0 非成功证明 ────────────────────────────────────────
42
+ const timeoutCls = classifyBookExecution(bookResult({ timedOut: true }), 'REF-GOTRY-0001');
43
+ assert.equal(timeoutCls.kind, 'unknown', '超时 → unknown');
44
+ assert.match(timeoutCls.reason, /timeout/);
45
+ const killCls = classifyBookExecution(bookResult({ signal: 'SIGKILL' }), 'REF-GOTRY-0001');
46
+ assert.equal(killCls.kind, 'unknown', '进程被杀 → unknown');
47
+ const nonJsonCls = classifyBookExecution(bookResult({ stdout: 'OK booking done' }), 'REF-GOTRY-0001');
48
+ assert.equal(nonJsonCls.kind, 'unknown', 'exit0 + 非 JSON 输出 → unknown(坏响应)');
49
+ const unboundCls = classifyBookExecution(bookResult({ stdout: JSON.stringify({ status: 'success', orderRef: 'HB-1' }) }), 'REF-GOTRY-0001');
50
+ assert.equal(unboundCls.kind, 'unknown', 'exit0 + success 但缺 customerReferenceNo 绑定 → unknown(不可归因)');
51
+ const mismatchCls = classifyBookExecution(bookResult({ stdout: JSON.stringify({ status: 'success', customerReferenceNo: 'REF-OTHER' }) }), 'REF-GOTRY-0001');
52
+ assert.equal(mismatchCls.kind, 'unknown', 'exit0 + success 但参考号不属于本 attempt → unknown');
53
+ const pendingCls = classifyBookExecution(bookResult({ stdout: JSON.stringify({ status: 'pending', customerReferenceNo: 'REF-GOTRY-0001' }) }), 'REF-GOTRY-0001');
54
+ assert.equal(pendingCls.kind, 'unknown', '部分确认(pending)→ unknown,不静默当成功');
55
+ const failedCls = classifyBookExecution(bookResult({ stdout: JSON.stringify({ status: 'failed', customerReferenceNo: 'REF-GOTRY-0001' }) }), 'REF-GOTRY-0001');
56
+ assert.equal(failedCls.kind, 'supplier_failed', '显式业务失败 → supplier_failed');
57
+ const nonzeroCls = classifyBookExecution(bookResult({ exitCode: 2, stderr: 'boom' }), 'REF-GOTRY-0001');
58
+ assert.equal(nonzeroCls.kind, 'unknown', '非零退码且无可解析业务结论 → unknown(不假定无副作用)');
59
+ const verifiedCls = classifyBookExecution(bookResult({ stdout: JSON.stringify({ status: 'success', customerReferenceNo: 'REF-GOTRY-0001', orderRef: 'HB-9' }) }), 'REF-GOTRY-0001');
60
+ assert.equal(verifiedCls.kind, 'binding_verified', '带绑定参考号的显式成功 → binding_verified');
61
+ // ── 2. 对账种子 fail-closed:绑定键缺一即拒 ─────────────────────────────────
62
+ for (const [field, broken] of [
63
+ ['attemptId', { attemptId: '' }],
64
+ ['customerReferenceNo', { customerReferenceNo: ' ' }],
65
+ ['intentIdemKey', { intentIdemKey: '' }],
66
+ ['requestFingerprintSha256', { requestFingerprintSha256: '' }],
67
+ ['recoveryWindowEndsAt', { recoveryWindowEndsAt: '' }],
68
+ ]) {
69
+ assert.throws(() => seed(broken), Error, `缺 ${field} → 拒绝对账记录(fail-closed)`);
70
+ }
71
+ // ── 3. 恢复窗口内 miss → 保持 unknown,禁止重订 ──────────────────────────────
72
+ const withinWindow = advanceReconciliation(seed(), { kind: 'miss' }, '2026-09-11T10:05:00.000Z');
73
+ assert.equal(withinWindow.status, 'unknown', '窗口内 miss → 仍 unknown');
74
+ assert.equal(withinWindow.nextAction, 'keep_querying', '窗口内 miss → 继续查单');
75
+ assert.equal(newIntentAllowed(withinWindow), false, 'unknown 期间结构性禁止新 intent(不重订)');
76
+ // ── 4. 窗口届满本身不是无订单证明 ────────────────────────────────────────────
77
+ const pastWindow = advanceReconciliation(seed(), { kind: 'miss' }, '2026-09-11T10:30:00.000Z');
78
+ assert.equal(pastWindow.status, 'unknown', '窗口届满 miss → 仍 unknown(不是 reconciled_failed)');
79
+ assert.equal(pastWindow.nextAction, 'manual_reconcile', '窗口届满 → 转人工对账');
80
+ assert.equal(newIntentAllowed(pastWindow), false, '时间届满不允许自动新 intent');
81
+ // ── 5. 查单命中:终态成功 / 迟到成功 / 供应商终态取消 / 非终态 ────────────────
82
+ const hitConfirmed = advanceReconciliation(seed(), { kind: 'hit', orders: [{ customerReferenceNo: 'REF-GOTRY-0001', orderRef: 'HB-1', supplierStatus: 'confirmed' }] }, '2026-09-11T10:02:00.000Z');
83
+ assert.equal(hitConfirmed.status, 'reconciled_success', '命中终态成功 → reconciled_success');
84
+ assert.equal(newIntentAllowed(hitConfirmed), false, '已成立订单 → 不允许同请求新 intent');
85
+ // 迟到成功:窗口外才查到,unknown 升级 reconciled_success,不重订
86
+ const lateHit = advanceReconciliation(advanceReconciliation(seed(), { kind: 'miss' }, '2026-09-11T10:20:00.000Z'), { kind: 'hit', orders: [{ customerReferenceNo: 'REF-GOTRY-0001', orderRef: 'HB-1', supplierStatus: 'confirmed' }] }, '2026-09-11T10:25:00.000Z');
87
+ assert.equal(lateHit.status, 'reconciled_success', '窗口外迟到成功 → unknown 升级 reconciled_success');
88
+ assert.ok(lateHit.history.some((e) => e.to === 'reconciled_success'), '升级路径进审计链');
89
+ const hitCanceled = advanceReconciliation(seed(), { kind: 'hit', orders: [{ customerReferenceNo: 'REF-GOTRY-0001', orderRef: 'HB-2', supplierStatus: 'auto_canceled' }] }, '2026-09-11T10:02:00.000Z');
90
+ assert.equal(hitCanceled.status, 'reconciled_failed', '供应商终态自动取消 → reconciled_failed(按供应商回执)');
91
+ assert.equal(newIntentAllowed(hitCanceled), true, '绑定 attempt 的权威负证据 → 允许新 intent');
92
+ const hitProcessing = advanceReconciliation(seed(), { kind: 'hit', orders: [{ customerReferenceNo: 'REF-GOTRY-0001', orderRef: 'HB-3', supplierStatus: 'processing' }] }, '2026-09-11T10:02:00.000Z');
93
+ assert.equal(hitProcessing.status, 'unknown', '命中但非终态 → 仍 unknown');
94
+ assert.equal(hitProcessing.nextAction, 'keep_querying', '非终态命中 → 继续查单');
95
+ // ── 6. 查单冲突:多单 / 参考号不匹配 → 显式 conflict + 人工,不静默成功不丢弃 ──
96
+ const multi = advanceReconciliation(seed(), { kind: 'hit', orders: [
97
+ { customerReferenceNo: 'REF-GOTRY-0001', orderRef: 'HB-A', supplierStatus: 'confirmed' },
98
+ { customerReferenceNo: 'REF-GOTRY-0001', orderRef: 'HB-B', supplierStatus: 'confirmed' },
99
+ ] }, '2026-09-11T10:02:00.000Z');
100
+ assert.equal(multi.status, 'unknown', '同 ref 多单 → 不静默当成功');
101
+ assert.equal(multi.nextAction, 'manual_reconcile', '多单冲突 → 人工对账');
102
+ assert.equal(multi.conflict, true, '多单冲突显式标记');
103
+ const foreignRef = advanceReconciliation(seed(), { kind: 'hit', orders: [{ customerReferenceNo: 'REF-OTHER', orderRef: 'HB-C', supplierStatus: 'confirmed' }] }, '2026-09-11T10:02:00.000Z');
104
+ assert.equal(foreignRef.status, 'unknown', '返回的订单不属于本参考号 → 不静默当成功也不丢弃');
105
+ assert.equal(foreignRef.conflict, true, '参考号不匹配 → 显式 conflict');
106
+ assert.equal(foreignRef.nextAction, 'manual_reconcile', '参考号不匹配 → 人工对账');
107
+ // ── 7. 查询自身失败/畸形/权限拒绝:探针失败不证明订单不存在 ─────────────────
108
+ const qFailed = advanceReconciliation(seed(), { kind: 'query_failed', reason: 'transport down' }, '2026-09-11T10:02:00.000Z');
109
+ assert.equal(qFailed.status, 'unknown', '查询失败 → 仍 unknown');
110
+ assert.equal(qFailed.nextAction, 'keep_querying', '查询失败 → 继续查单');
111
+ const malformed = advanceReconciliation(seed(), { kind: 'malformed', reason: 'unparseable body' }, '2026-09-11T10:02:00.000Z');
112
+ assert.equal(malformed.status, 'unknown', '畸形响应 → 仍 unknown(不静默当 miss)');
113
+ const denied = advanceReconciliation(seed(), { kind: 'permission_denied' }, '2026-09-11T10:02:00.000Z');
114
+ assert.equal(denied.status, 'unknown', '权限拒绝 → 仍 unknown');
115
+ assert.equal(denied.nextAction, 'manual_reconcile', '权限拒绝 → 人工承接');
116
+ // ── 8. 人工确认无订单:绑定 attempt 的非空操作证据才生效 ─────────────────────
117
+ const manualOk = advanceReconciliation(pastWindow, { kind: 'manual_no_order', operatorEvidence: 'op-7: UAT 后台按 REF-GOTRY-0001 + attempt att-0001 查无订单,截图存档 #rev-41' }, '2026-09-11T11:00:00.000Z');
118
+ assert.equal(manualOk.status, 'reconciled_failed', '人工确认无订单 → reconciled_failed');
119
+ assert.equal(newIntentAllowed(manualOk), true, '人工权威负证据 → 允许新 intent');
120
+ assert.throws(() => advanceReconciliation(pastWindow, { kind: 'manual_no_order', operatorEvidence: ' ' }, '2026-09-11T11:00:00.000Z'), Error, '空操作证据的人工无订单确认 → 拒绝');
121
+ const manualTooEarly = advanceReconciliation(seed(), { kind: 'manual_no_order', operatorEvidence: 'op claim' }, T0);
122
+ assert.equal(manualTooEarly.status, 'reconciled_failed', '窗口内人工确认亦为权威负证据(人工可早于窗口收敛)');
123
+ // ── 9. 终态后出现反证:只置 conflict 交人工,不静默翻转 ──────────────────────
124
+ const lateAfterFailed = advanceReconciliation(manualOk, { kind: 'hit', orders: [{ customerReferenceNo: 'REF-GOTRY-0001', orderRef: 'HB-X', supplierStatus: 'confirmed' }] }, '2026-09-11T12:00:00.000Z');
125
+ assert.equal(lateAfterFailed.status, 'reconciled_failed', '终态不被静默翻转');
126
+ assert.equal(lateAfterFailed.conflict, true, '终态后命中 → 显式冲突标记');
127
+ assert.equal(lateAfterFailed.nextAction, 'manual_reconcile', '终态后命中 → 人工复核');
128
+ assert.equal(newIntentAllowed(lateAfterFailed), false, '冲突未解 → 新 intent 冻结');
129
+ // ── 10. 可追溯 + 纯函数不可变 ────────────────────────────────────────────────
130
+ const before = advanceReconciliation(seed(), { kind: 'miss' }, '2026-09-11T10:05:00.000Z');
131
+ assert.ok(before.history.length >= 2, 'enter_unknown + query_miss 都进审计链');
132
+ assert.ok(before.history.every((e) => typeof e.probeDigest === 'string' && /^[0-9a-f]{64}$/.test(e.probeDigest)), '每条事件带 probeDigest');
133
+ assert.equal(before.attemptId, 'att-0001', 'attemptId 不变(不可变 attempt)');
134
+ const afterSecond = advanceReconciliation(before, { kind: 'miss' }, '2026-09-11T10:06:00.000Z');
135
+ assert.ok(afterSecond.history.length > before.history.length, '推进追加事件');
136
+ assert.equal(before.history.length + 1, afterSecond.history.length, '原状态不被修改(纯函数)');
137
+ assert.equal(before.factId, 'fact_hotel_123', 'fact_id 关联保留');
138
+ assert.equal(before.approvalReceiptDigest, 'receipt-digest-01', '审批 receipt digest 关联保留');
139
+ assert.equal(before.outboxIntentRef, 'outbox-intent-01', 'outbox intent 占位(#231)关联保留');
140
+ assert.equal(recoveryWindowEndsAt(T0), '2026-09-11T10:13:00.000Z', '恢复窗口 = 180s Phase1 + 600s Phase2 上界(780s)');
141
+ console.log('HOTELBYTE RECONCILE TESTS: 10/10 OK(book 分类 exit0≠成功 / 种子 fail-closed / 窗口内 miss 保 unknown / 届满非无订单证明 / 命中终态与迟到收敛 / 冲突显式人工 / 探针失败不证明不存在 / 人工权威负证据 / 终态反证只置 conflict / 可追溯+不可变)');
@@ -0,0 +1,373 @@
1
+ /**
2
+ * HotelByte 假 CLI spawn 级完整链路 E2E(issue #232 验收 §5;全离线 fixture,零真实供应商调用):
3
+ * 本地假 hbcli 二进制(shell fixture,经真实 child_process spawn)跑通
4
+ * quote(check-avail)→ approval(L3 receipt digest)→ book(trade book)→ unknown(enterUnknown)
5
+ * → query-orders(advanceReconciliation)完整链,证明纯函数契约(hotelbyte-transaction.ts)
6
+ * 在真实子进程边界上成立。时间轴为模拟常量;进程边界(exit/signal/timeout/stdout)是真实 spawn 产物。
7
+ *
8
+ * 场景×结果矩阵(每行先红后绿:同一次 spawn 的真实输出,先喂「无对账层的裸调用」证明会处理错,
9
+ * 再走契约全链证明终态正确):
10
+ * 1. quote 面:check-avail 真实 spawn 返回 session/ratePkgId/金额/币种/条款/有效期——book 前绑定钉死(验收 §2)
11
+ * 2. 裸调用红基线:exit0 即成功/非零即失败/查无即无订单——timeout 被当失败(会盲目重订→双订风险)、
12
+ * exit0 垃圾/无绑定/部分确认被当成功、窗口内 miss 被当无订单——全部证伪(红)
13
+ * 3. golden 链:book exit0+success 且 customerReferenceNo/ratePkgId 经 argv 回传一致 → binding_verified,不进对账
14
+ * 4. timeout→miss→迟到成功(验收 §4 组合):30s abort→unknown→窗口内 miss 保 unknown 不重订→窗口外
15
+ * 查到 confirmed→reconciled_success(迟到成功收敛,不重订)
16
+ * 5. timeout→miss→迟到自动取消(验收 §4 组合):unknown→miss→auto_canceled→reconciled_failed
17
+ * (权威负证据,唯一允许新 intent 路径)
18
+ * 6. exit0+非 JSON 垃圾→unknown(坏响应不可当成功)→查单 confirmed→reconciled_success
19
+ * 7. exit0+success 无绑定参考号→unknown(不可归因)→查单 confirmed→reconciled_success
20
+ * 8. 部分确认(pending)→unknown→非终态 processing 保 unknown→窗口届满 miss→转人工
21
+ * (时间届满本身不是无订单证明,不重订不允许新 intent)
22
+ * 9. 进程被杀(kill -9)→unknown(副作用不可判定)
23
+ * 10. 同 ref 多单→显式 conflict+manual_reconcile,不静默当成功也不丢弃
24
+ * 11. 查单探针失败面:查询 crash→query_failed/垃圾→malformed(均保 unknown:探针失败不证明订单不存在);
25
+ * permission_denied→转人工
26
+ * 12. 显式业务失败(exit3+{"status":"failed"})→supplier_failed(唯一非 unknown 失败分类,对比裸调用此例碰巧同判)
27
+ *
28
+ * 红线(结构性保证):唯一被执行的可执行文件是 mkdtemp 下的本地 fixture 脚本;
29
+ * 套件零 PATH 上的 hbcli 解析、零网络、零凭据;GOTRY_HBCLI_LIVE 不参与、置 1 也不改变本套件行为;
30
+ * 所有 spawn 带有界超时(hang 场景 500ms 内 SIGKILL 收敛;常规场景 15s 只防真死锁,全栈冷态首跑亦稳定)。
31
+ *
32
+ * 运行: cd ts && npx tsx scripts/hotelbyte-spawn-e2e-tests.ts
33
+ */
34
+ import assert from 'node:assert/strict';
35
+ import { spawn } from 'node:child_process';
36
+ import { createHash } from 'node:crypto';
37
+ import { chmod, mkdtemp, rm, writeFile } from 'node:fs/promises';
38
+ import { tmpdir } from 'node:os';
39
+ import { join } from 'node:path';
40
+ import { classifyBookExecution, enterUnknown, advanceReconciliation, recoveryWindowEndsAt, newIntentAllowed, } from '../capabilities/hotelbyte-transaction.js';
41
+ // ── 时间轴(模拟常量;窗口 = 180s Phase1 + ≤600s Phase2) ─────────────────────
42
+ const T0 = '2026-09-11T10:00:00.000Z';
43
+ const WINDOW_END = recoveryWindowEndsAt(T0);
44
+ const IN_WINDOW = '2026-09-11T10:00:30.000Z'; // T0+30s:Phase1 内
45
+ const PAST_WINDOW = '2026-09-11T10:20:00.000Z'; // T0+20min:780s 窗口外
46
+ // ── 链路固定绑定键(不可变 attempt;全程同一 ref) ────────────────────────────
47
+ const REF = 'REF-GOTRY-E2E-0001';
48
+ const ATTEMPT_ID = 'att-e2e-0001';
49
+ const RATE_PKG = 'pkg-e2e-42';
50
+ function seedBase() {
51
+ return {
52
+ attemptId: ATTEMPT_ID,
53
+ customerReferenceNo: REF,
54
+ intentIdemKey: 'idem-e2e-0001',
55
+ requestFingerprintSha256: 'e'.repeat(64),
56
+ enteredUnknownAt: T0,
57
+ recoveryWindowEndsAt: WINDOW_END,
58
+ factId: 'fact_hotel_e2e',
59
+ approvalReceiptDigest: undefined, // 各场景 book 前由 approval 面填入
60
+ outboxIntentRef: 'outbox-intent-e2e-01',
61
+ };
62
+ }
63
+ // ── 假 CLI fixture:单脚本,行为由环境变量编程,argv 形态对齐 staicli 0.0.3 ────
64
+ const FAKE_CLI = `#!/bin/sh
65
+ # 本地假 hbcli(spawn 级 E2E fixture;零真实供应商接触):行为由 FAKE_* 环境变量编程。
66
+ cmd="$1 $2"
67
+ read_flag() {
68
+ flag="$1"; shift
69
+ prev=""
70
+ for a in "$@"; do
71
+ [ "$prev" = "$flag" ] && printf '%s' "$a"
72
+ prev="$a"
73
+ done
74
+ }
75
+ case "$cmd" in
76
+ "search check-avail")
77
+ printf '{"session":"sess-fixture-01","ratePkgId":"%s","totalAmount":1280,"currency":"CNY","terms":"non-refundable","validUntil":"2026-09-11T10:05:00Z"}' "$FAKE_RATE_PKG_ID"
78
+ ;;
79
+ "trade book")
80
+ ref=$(read_flag --customer-reference-no "$@")
81
+ pkg=$(read_flag --rate-pkg-id "$@")
82
+ case "$FAKE_BOOK_MODE" in
83
+ ok) printf '{"status":"success","customerReferenceNo":"%s","ratePkgId":"%s","orderRef":"HB-ORD-77"}' "$ref" "$pkg" ;;
84
+ hang) echo 'waiting on supplier...' >&2; while :; do sleep 1; done ;;
85
+ garbage) printf 'HTTP/1.1 200 OK\\n<html>your booking maybe succeeded?</html>' ;;
86
+ pending) printf '{"status":"pending","customerReferenceNo":"%s","orderRef":"HB-ORD-88"}' "$ref" ;;
87
+ unbound) printf '{"status":"success","orderRef":"HB-ORD-99"}' ;;
88
+ suicide) kill -9 $$ ;;
89
+ bizfail) printf '{"status":"failed","reason":"inventory_gone","customerReferenceNo":"%s"}' "$ref"; exit 3 ;;
90
+ crash) echo 'kaboom' >&2; exit 2 ;;
91
+ *) echo "unknown FAKE_BOOK_MODE=$FAKE_BOOK_MODE" >&2; exit 64 ;;
92
+ esac
93
+ ;;
94
+ "trade query-orders")
95
+ ref=$(read_flag --customer-reference-no "$@")
96
+ case "$FAKE_QUERY_MODE" in
97
+ miss) printf '{"orders":[]}' ;;
98
+ hit-confirmed) printf '{"orders":[{"customerReferenceNo":"%s","orderRef":"HB-ORD-77","supplierStatus":"confirmed"}]}' "$ref" ;;
99
+ hit-auto-canceled) printf '{"orders":[{"customerReferenceNo":"%s","orderRef":"HB-ORD-77","supplierStatus":"auto_canceled"}]}' "$ref" ;;
100
+ hit-processing) printf '{"orders":[{"customerReferenceNo":"%s","orderRef":"HB-ORD-77","supplierStatus":"processing"}]}' "$ref" ;;
101
+ multi) printf '{"orders":[{"customerReferenceNo":"%s","orderRef":"HB-ORD-A","supplierStatus":"confirmed"},{"customerReferenceNo":"%s","orderRef":"HB-ORD-B","supplierStatus":"confirmed"}]}' "$ref" "$ref" ;;
102
+ denied) printf '{"error":"permission_denied"}' ;;
103
+ crash) echo 'query transport down' >&2; exit 4 ;;
104
+ garbage) printf '<<<not json at all>>>' ;;
105
+ *) echo "unknown FAKE_QUERY_MODE=$FAKE_QUERY_MODE" >&2; exit 64 ;;
106
+ esac
107
+ ;;
108
+ *)
109
+ echo "error: unknown command '$1 $2'" >&2
110
+ exit 1
111
+ ;;
112
+ esac
113
+ `;
114
+ async function runFake(bin, args, envVars, timeoutMs) {
115
+ return new Promise((resolve, reject) => {
116
+ let stdout = '';
117
+ let stderr = '';
118
+ let settled = false;
119
+ let timedOut = false;
120
+ const started = Date.now();
121
+ const child = spawn(bin, args, { env: { ...process.env, ...envVars } });
122
+ const timer = setTimeout(() => {
123
+ if (!settled) {
124
+ timedOut = true;
125
+ child.kill('SIGKILL');
126
+ }
127
+ }, timeoutMs);
128
+ child.stdout.on('data', (d) => { stdout += d.toString(); });
129
+ child.stderr.on('data', (d) => { stderr += d.toString(); });
130
+ child.on('close', (code, signal) => {
131
+ if (settled)
132
+ return;
133
+ settled = true;
134
+ clearTimeout(timer);
135
+ resolve({
136
+ exitCode: code,
137
+ signal: signal ?? null,
138
+ timedOut,
139
+ stdout,
140
+ stderr,
141
+ latencyMs: Date.now() - started,
142
+ });
143
+ });
144
+ child.on('error', (e) => {
145
+ if (settled)
146
+ return;
147
+ settled = true;
148
+ clearTimeout(timer);
149
+ reject(new Error(`fixture spawn 失败(测试自身缺陷,非场景):${e.message}`));
150
+ });
151
+ });
152
+ }
153
+ function toBookExecutionResult(o) {
154
+ return { exitCode: o.exitCode, signal: o.signal, timedOut: o.timedOut, stdout: o.stdout, stderr: o.stderr };
155
+ }
156
+ /** 查单 spawn → 授权查询面探针映射(exit≠0/超时/被杀=探针自身失败;探针失败≠订单不存在)。 */
157
+ function probeFromQuerySpawn(o) {
158
+ if (o.timedOut)
159
+ return { kind: 'query_failed', reason: `query timeout(${o.latencyMs}ms abort;探针失败≠订单不存在)` };
160
+ if (o.signal)
161
+ return { kind: 'query_failed', reason: `query process killed:${o.signal}(探针失败≠订单不存在)` };
162
+ if (o.exitCode !== 0) {
163
+ return { kind: 'query_failed', reason: `exit ${o.exitCode}:${o.stderr.trim().slice(0, 120)}` };
164
+ }
165
+ const start = o.stdout.search(/[{[]/);
166
+ if (start < 0)
167
+ return { kind: 'malformed', reason: 'no-json-marker' };
168
+ let parsed;
169
+ try {
170
+ parsed = JSON.parse(o.stdout.slice(start));
171
+ }
172
+ catch (e) {
173
+ return { kind: 'malformed', reason: `unparseable:${e.message.slice(0, 80)}` };
174
+ }
175
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
176
+ return { kind: 'malformed', reason: 'non-object-json' };
177
+ }
178
+ const body = parsed;
179
+ if (body['error'] === 'permission_denied')
180
+ return { kind: 'permission_denied' };
181
+ if (!Array.isArray(body['orders']))
182
+ return { kind: 'malformed', reason: 'orders field missing' };
183
+ const orders = [];
184
+ for (const raw of body['orders']) {
185
+ const x = raw;
186
+ if (x === null || typeof x !== 'object' ||
187
+ typeof x['customerReferenceNo'] !== 'string' ||
188
+ typeof x['orderRef'] !== 'string' ||
189
+ typeof x['supplierStatus'] !== 'string') {
190
+ return { kind: 'malformed', reason: 'order entry shape' };
191
+ }
192
+ orders.push({ customerReferenceNo: x['customerReferenceNo'], orderRef: x['orderRef'], supplierStatus: x['supplierStatus'] });
193
+ }
194
+ return orders.length === 0 ? { kind: 'miss' } : { kind: 'hit', orders };
195
+ }
196
+ function naiveBareBook(o) {
197
+ return o.exitCode === 0 ? 'success' : 'failed';
198
+ }
199
+ function naiveBareQueryAllowsRebook(o) {
200
+ // 天真接线:查无订单 → 判「无订单」→ 允许立即重订(这正是要证伪的危险行为)
201
+ return probeLikeOrders(o).length === 0;
202
+ }
203
+ function probeLikeOrders(o) {
204
+ try {
205
+ const start = o.stdout.search(/[{[]/);
206
+ if (start < 0)
207
+ return [];
208
+ const parsed = JSON.parse(o.stdout.slice(start));
209
+ const orders = parsed?.orders;
210
+ return Array.isArray(orders) ? orders : [];
211
+ }
212
+ catch {
213
+ return [];
214
+ }
215
+ }
216
+ const tmp = await mkdtemp(join(tmpdir(), 'hb-spawn-e2e-'));
217
+ try {
218
+ const fakeBin = join(tmp, 'hbfake');
219
+ await writeFile(fakeBin, FAKE_CLI, { mode: 0o755 });
220
+ await chmod(fakeBin, 0o755);
221
+ assert.ok(fakeBin.startsWith(tmpdir()), '红线自检:唯一可执行文件在临时目录(零 PATH 解析,零真实 hbcli)');
222
+ // book 复用参数:绑定 ref 与 rate-pkg 经真实 argv 进子进程(回传断言即 spawn 边界证明)
223
+ const bookArgs = ['trade', 'book', '--json', '--rate-pkg-id', RATE_PKG, '--customer-reference-no', REF];
224
+ async function bookSpawn(mode, args = bookArgs, timeoutMs = 15_000) {
225
+ return runFake(fakeBin, args, { FAKE_BOOK_MODE: mode, FAKE_RATE_PKG_ID: RATE_PKG }, timeoutMs);
226
+ }
227
+ async function querySpawn(mode) {
228
+ return runFake(fakeBin, ['trade', 'query-orders', '--json', '--customer-reference-no', REF], { FAKE_QUERY_MODE: mode }, 15_000);
229
+ }
230
+ // ── 1. quote 面:check-avail 真实 spawn,book 前绑定钉死(验收 §2) ──────────
231
+ const quoteOutcome = await runFake(fakeBin, ['search', 'check-avail', '--json', '--rate-pkg-id', RATE_PKG], { FAKE_RATE_PKG_ID: RATE_PKG }, 15_000);
232
+ assert.equal(quoteOutcome.exitCode, 0, 'quote spawn exit0');
233
+ assert.equal(quoteOutcome.timedOut, false, 'quote spawn 不超时');
234
+ const quote = JSON.parse(quoteOutcome.stdout);
235
+ for (const field of ['session', 'ratePkgId', 'totalAmount', 'currency', 'terms', 'validUntil']) {
236
+ const v = quote[field];
237
+ assert.ok(v !== undefined && v !== null && String(v).length > 0, `quote 绑定字段 ${field} 必须在场(验收 §2)`);
238
+ }
239
+ assert.equal(quote['ratePkgId'], RATE_PKG, 'quote 绑定本 attempt 的 ratePkg');
240
+ assert.equal(quote['currency'], 'CNY', '币种在场');
241
+ // approval 面:L3 receipt 只携摘要(sha256),不落敏感原值
242
+ const quoteSnapshotDigest = createHash('sha256').update(quoteOutcome.stdout).digest('hex');
243
+ const approvalReceiptDigest = createHash('sha256')
244
+ .update(JSON.stringify({ seam: 'hotelbyte.book.v1', quoteSnapshotDigest, approver: 'L3-fixture-approval', approvedAt: T0 }))
245
+ .digest('hex');
246
+ assert.match(approvalReceiptDigest, /^[0-9a-f]{64}$/, '审批 receipt 为 sha256 摘要');
247
+ console.log('1. quote 面(真实 spawn:session/rate/金额币种/条款/有效期绑定)+ L3 审批摘要 OK');
248
+ // ── 2. 裸调用红基线:同批真实 spawn 输出,证明无对账层的天真接线会处理错 ────
249
+ const redTimeout = await bookSpawn('hang', bookArgs, 500);
250
+ assert.equal(redTimeout.timedOut, true, 'hang 场景被 500ms 有界超时 SIGKILL 收敛');
251
+ assert.equal(naiveBareBook(redTimeout), 'failed', '红:timeout 被裸调用判 failed(会盲目重订→双订风险)');
252
+ const redGarbage = await bookSpawn('garbage');
253
+ assert.equal(naiveBareBook(redGarbage), 'success', '红:exit0+非 JSON 垃圾被裸调用判 success');
254
+ const redUnbound = await bookSpawn('unbound');
255
+ assert.equal(naiveBareBook(redUnbound), 'success', '红:exit0+无绑定成功被裸调用判 success(不可归因也当成了成功)');
256
+ const redPending = await bookSpawn('pending');
257
+ assert.equal(naiveBareBook(redPending), 'success', '红:exit0+部分确认(pending)被裸调用判 success');
258
+ const redCrash = await bookSpawn('crash');
259
+ assert.equal(naiveBareBook(redCrash), 'failed', '红:非零退码且无业务结论被裸调用判 failed(实际副作用不可判定)');
260
+ const redMiss = await querySpawn('miss');
261
+ assert.equal(naiveBareQueryAllowsRebook(redMiss), true, '红:窗口内 miss 被裸调用判「无订单可重订」(供应商可能迟到建单)');
262
+ console.log('2. 裸调用红基线(6 个真实 spawn 输出全被天真接线处理错)OK');
263
+ // ── 3. golden 链:quote→approval→book(exit0+success,argv 回传绑定一致) ────
264
+ const goldenOutcome = await bookSpawn('ok');
265
+ assert.equal(goldenOutcome.exitCode, 0);
266
+ const goldenPayload = JSON.parse(goldenOutcome.stdout);
267
+ assert.equal(goldenPayload['customerReferenceNo'], REF, 'customerReferenceNo 经真实 argv 进子进程并回传(spawn 边界绑定证明)');
268
+ assert.equal(goldenPayload['ratePkgId'], RATE_PKG, 'ratePkgId 与 quote 绑定一致(quote→book 链未断)');
269
+ const goldenCls = classifyBookExecution(toBookExecutionResult(goldenOutcome), REF);
270
+ assert.deepEqual(goldenCls, { kind: 'binding_verified', customerReferenceNo: REF, payload: goldenPayload }, 'golden → binding_verified');
271
+ console.log('3. golden 链(ref/ratePkg argv 回传一致 → binding_verified,不进对账)OK');
272
+ // ── 4. timeout→miss→迟到成功(验收 §4 组合一) ──────────────────────────────
273
+ const s4book = await bookSpawn('hang', bookArgs, 500);
274
+ const s4cls = classifyBookExecution(toBookExecutionResult(s4book), REF);
275
+ assert.equal(s4cls.kind, 'unknown', 'timeout → unknown(CLI abort ≠ 后端停止)');
276
+ assert.match(s4cls.reason, /timeout/);
277
+ let s4 = enterUnknown({ ...seedBase(), approvalReceiptDigest });
278
+ assert.equal(s4.nextAction, 'keep_querying', '进 unknown 即禁止盲目重试/重订');
279
+ const s4miss = advanceReconciliation(s4, probeFromQuerySpawn(await querySpawn('miss')), IN_WINDOW);
280
+ assert.equal(s4miss.status, 'unknown', '窗口内 miss → 保持 unknown');
281
+ assert.equal(s4miss.nextAction, 'keep_querying', '窗口内 miss → 继续查,结构性禁止重订');
282
+ assert.equal(newIntentAllowed(s4miss), false, 'unknown 期间禁止新 intent');
283
+ s4 = s4miss;
284
+ const s4hit = advanceReconciliation(s4, probeFromQuerySpawn(await querySpawn('hit-confirmed')), PAST_WINDOW);
285
+ assert.equal(s4hit.status, 'reconciled_success', '迟到成功(窗口外查到 confirmed)→ reconciled_success,不重订');
286
+ assert.equal(newIntentAllowed(s4hit), false, '订单已成立 → 不允许同请求新 intent');
287
+ assert.ok(s4hit.history.some((e) => e.action === 'query_miss_within_window'), '窗口内 miss 进审计链');
288
+ assert.ok(s4hit.history.some((e) => e.action === 'supplier_terminal_success'), '迟到收敛进审计链');
289
+ const s4hitOrders = probeFromQuerySpawn(await querySpawn('hit-confirmed'));
290
+ assert.ok(s4hitOrders.kind === 'hit' && s4hitOrders.orders.every((o) => o.customerReferenceNo === REF), '查单确实按本 attempt 的 ref 查(argv 回传)');
291
+ console.log('4. timeout→miss→迟到成功(unknown 全程不重订,收敛 reconciled_success)OK');
292
+ // ── 5. timeout→miss→迟到自动取消(验收 §4 组合二) ──────────────────────────
293
+ const s5book = await bookSpawn('hang', bookArgs, 500);
294
+ assert.equal(classifyBookExecution(toBookExecutionResult(s5book), REF).kind, 'unknown');
295
+ const s5unknown = enterUnknown({ ...seedBase(), approvalReceiptDigest });
296
+ const s5miss = advanceReconciliation(s5unknown, probeFromQuerySpawn(await querySpawn('miss')), IN_WINDOW);
297
+ assert.equal(s5miss.status, 'unknown', '窗口内 miss 保 unknown');
298
+ const s5cancel = advanceReconciliation(s5miss, probeFromQuerySpawn(await querySpawn('hit-auto-canceled')), IN_WINDOW);
299
+ assert.equal(s5cancel.status, 'reconciled_failed', '迟到自动取消按供应商回执收敛 reconciled_failed');
300
+ assert.equal(newIntentAllowed(s5cancel), true, '绑定 attempt 的权威负证据 → 唯一允许新 intent 路径');
301
+ assert.ok(s5cancel.history.every((e) => e.probeDigest.length === 64), '审计链带 probeDigest');
302
+ console.log('5. timeout→miss→迟到自动取消(收敛 reconciled_failed,允许新 intent)OK');
303
+ // ── 6. exit0+非 JSON 垃圾→unknown→查单收敛 ─────────────────────────────────
304
+ const s6book = await bookSpawn('garbage');
305
+ const s6cls = classifyBookExecution(toBookExecutionResult(s6book), REF);
306
+ assert.equal(s6cls.kind, 'unknown', '坏响应不可当成功');
307
+ assert.match(s6cls.reason, /exit0_non_json/);
308
+ const s6hit = advanceReconciliation(enterUnknown({ ...seedBase(), approvalReceiptDigest }), probeFromQuerySpawn(await querySpawn('hit-confirmed')), IN_WINDOW);
309
+ assert.equal(s6hit.status, 'reconciled_success', '垃圾响应场景经查单收敛 reconciled_success');
310
+ console.log('6. exit0 垃圾→unknown(不假成功)→查单收敛 OK');
311
+ // ── 7. exit0+success 无绑定参考号→unknown→查单收敛 ─────────────────────────
312
+ const s7book = await bookSpawn('unbound');
313
+ const s7cls = classifyBookExecution(toBookExecutionResult(s7book), REF);
314
+ assert.equal(s7cls.kind, 'unknown', '无绑定成功不可归因');
315
+ assert.match(s7cls.reason, /missing_reference/);
316
+ const s7hit = advanceReconciliation(enterUnknown({ ...seedBase(), approvalReceiptDigest }), probeFromQuerySpawn(await querySpawn('hit-confirmed')), IN_WINDOW);
317
+ assert.equal(s7hit.status, 'reconciled_success', '无绑定场景经查单按 ref 归因收敛');
318
+ console.log('7. exit0 无绑定成功→unknown→查单归因收敛 OK');
319
+ // ── 8. 部分确认(pending)→非终态→窗口届满 miss→转人工(不重订) ─────────────
320
+ const s8book = await bookSpawn('pending');
321
+ const s8cls = classifyBookExecution(toBookExecutionResult(s8book), REF);
322
+ assert.equal(s8cls.kind, 'unknown', '部分确认 ≠ 成功');
323
+ assert.match(s8cls.reason, /partial_or_indeterminate/);
324
+ let s8 = enterUnknown({ ...seedBase(), approvalReceiptDigest });
325
+ const s8processing = advanceReconciliation(s8, probeFromQuerySpawn(await querySpawn('hit-processing')), IN_WINDOW);
326
+ assert.equal(s8processing.status, 'unknown', '订单存在但非终态 → 保持 unknown');
327
+ assert.equal(s8processing.nextAction, 'keep_querying', '非终态 → 继续查单');
328
+ s8 = s8processing;
329
+ const s8expired = advanceReconciliation(s8, probeFromQuerySpawn(await querySpawn('miss')), PAST_WINDOW);
330
+ assert.equal(s8expired.status, 'unknown', '窗口届满 miss → 仍 unknown(时间届满不是无订单证明)');
331
+ assert.equal(s8expired.nextAction, 'manual_reconcile', '窗口届满 → 转人工对账');
332
+ assert.equal(newIntentAllowed(s8expired), false, '届满不自动允许新 intent(不重订)');
333
+ console.log('8. pending→processing→窗口届满转人工(仍 unknown,不重订)OK');
334
+ // ── 9. 进程被杀(kill -9)→unknown(副作用不可判定) ─────────────────────────
335
+ const s9book = await bookSpawn('suicide');
336
+ assert.equal(s9book.exitCode, null, '被杀进程无退码');
337
+ assert.equal(s9book.signal, 'SIGKILL', '真实 spawn 捕获 SIGKILL');
338
+ const s9cls = classifyBookExecution(toBookExecutionResult(s9book), REF);
339
+ assert.equal(s9cls.kind, 'unknown', '被杀 → unknown');
340
+ assert.match(s9cls.reason, /process_killed/);
341
+ const s9unknown = enterUnknown({ ...seedBase(), approvalReceiptDigest });
342
+ assert.equal(s9unknown.status, 'unknown', '被杀场景进显式对账');
343
+ console.log('9. 进程被杀(SIGKILL 真实捕获)→unknown OK');
344
+ // ── 10. 同 ref 多单→显式 conflict+manual_reconcile ─────────────────────────
345
+ const s10multi = advanceReconciliation(enterUnknown({ ...seedBase(), approvalReceiptDigest }), probeFromQuerySpawn(await querySpawn('multi')), IN_WINDOW);
346
+ assert.equal(s10multi.status, 'unknown', '多单不静默当成功');
347
+ assert.equal(s10multi.nextAction, 'manual_reconcile', '多单冲突 → 人工对账');
348
+ assert.equal(s10multi.conflict, true, '冲突显式标记');
349
+ assert.equal(newIntentAllowed(s10multi), false, '冲突未解 → 冻结新 intent');
350
+ console.log('10. 同 ref 多单(真实查询返回 2 笔)→显式冲突交人工 OK');
351
+ // ── 11. 查单探针失败面:crash/garbage/denied(探针失败≠订单不存在) ──────────
352
+ const s11unknown = enterUnknown({ ...seedBase(), approvalReceiptDigest });
353
+ const s11crash = advanceReconciliation(s11unknown, probeFromQuerySpawn(await querySpawn('crash')), IN_WINDOW);
354
+ assert.equal(s11crash.status, 'unknown', '查询 crash → 探针失败,保持 unknown');
355
+ assert.equal(s11crash.nextAction, 'keep_querying', '查询失败 → 继续查');
356
+ const s11garbage = advanceReconciliation(s11crash, probeFromQuerySpawn(await querySpawn('garbage')), IN_WINDOW);
357
+ assert.equal(s11garbage.status, 'unknown', '查询垃圾输出 → malformed,不静默当 miss');
358
+ const s11denied = advanceReconciliation(s11garbage, probeFromQuerySpawn(await querySpawn('denied')), IN_WINDOW);
359
+ assert.equal(s11denied.nextAction, 'manual_reconcile', 'permission_denied → 自助查单不可用,转人工');
360
+ assert.equal(s11denied.status, 'unknown', 'permission_denied → 仍 unknown');
361
+ console.log('11. 查单探针失败面(crash→query_failed/garbage→malformed/denied→人工)OK');
362
+ // ── 12. 显式业务失败(exit3+{"status":"failed"})→supplier_failed ───────────
363
+ const s12book = await bookSpawn('bizfail');
364
+ assert.equal(s12book.exitCode, 3);
365
+ const s12cls = classifyBookExecution(toBookExecutionResult(s12book), REF);
366
+ assert.equal(s12cls.kind, 'supplier_failed', '带显式业务失败 payload 的非零退码 → supplier_failed');
367
+ assert.ok(s12cls.kind === 'supplier_failed' && s12cls.payload !== null && s12cls.payload['reason'] === 'inventory_gone', 'payload 保留业务结论');
368
+ console.log('12. 显式业务失败(exit3+failed payload)→supplier_failed OK');
369
+ console.log('\nHOTELBYTE SPAWN E2E TESTS: 12/12 OK(假 CLI 经真实 spawn 跑通 quote→approval→book→unknown→query-orders 全链;裸调用红基线 6 证伪;timeout→miss→迟到成功/自动取消;垃圾/无绑定/部分确认不假成功;届满转人工;冲突显式;零真实 hbcli·网络·凭据)');
370
+ }
371
+ finally {
372
+ await rm(tmp, { recursive: true, force: true });
373
+ }
@@ -1,18 +1,31 @@
1
+ /**
2
+ * i18n 目录回归(README Known limitation「zh-CN 体验」的工程清偿验证面):
3
+ * 1. en 目录零缺键(enCoverage 为空);
4
+ * 2. 默认 zh-CN:engine 金标准 answer_md 中文措辞逐项在位(与 engine-tests 金标准同源);
5
+ * 3. GOTRY_LOCALE=en / setLocale('en'):answer_md 切英文、判定位仍含班次/金额(数据不动,只换词面);
6
+ * 4. 插值与回退:缺变量保留 {var},en 缺键回退 zh。
7
+ *
8
+ * 运行(在 ts/ 下,零网):npx tsx scripts/i18n-tests.ts
9
+ */
1
10
  import assert from 'node:assert/strict';
2
11
  import { readFile } from 'node:fs/promises';
3
12
  import { join } from 'node:path';
4
13
  import { enCoverage, getLocale, setLocale, t } from '../src/i18n.js';
5
14
  import { solve } from '../src/engine.js';
6
15
  import { parseCandidate, parseRequest } from '../src/model.js';
16
+ // 0. en 目录完备
7
17
  assert.deepEqual(enCoverage(), [], 'en 目录缺键:' + enCoverage().join(','));
18
+ // zh-CN 金标准(默认 locale):与 engine-tests 同一 golden 输入,断言中文词面
8
19
  const erhai = JSON.parse(await readFile(join('..', 'data', 'golden_erhai.json'), 'utf-8'));
9
20
  const req = parseRequest(erhai['request']);
10
21
  const candidates = erhai['candidates'].map(parseCandidate);
22
+ // 1. 默认 zh
11
23
  assert.equal(getLocale(), 'zh-CN', '默认 locale 应为 zh-CN');
12
24
  const zhRes = await solve(req, candidates);
13
25
  const zhZh = String(zhRes['answer_md']);
14
26
  assert.ok(zhZh.includes('千岛湖') && zhZh.includes('下一次出发') && zhZh.includes('待你决定的两个问题'));
15
27
  void zhZh;
28
+ // 2. 英文面:同一输入,断言换语言只换词面、判定数据不动
16
29
  setLocale('en');
17
30
  const enRes = await solve(req, candidates);
18
31
  const enMd = String(enRes['answer_md']);
@@ -20,15 +33,9 @@ assert.ok(enMd.includes('feasible'), '英文判定应含 feasible');
20
33
  assert.ok(!enMd.includes('现在不行'), '英文面不得残留中文判定词');
21
34
  assert.ok(enMd.includes('千岛湖'), '候选名(数据)跨语言不动');
22
35
  assert.ok(/¥\d+/.test(enMd), '金额不翻译');
23
- assert.equal(t('md.wish_budget', {
24
- cny: '4950'
25
- }).includes('4950'), true, '插值生效(以当前 locale 渲染)');
36
+ // 3. 词条插值与回退
37
+ assert.equal(t('md.wish_budget', { cny: '4950' }).includes('4950'), true, '插值生效(以当前 locale 渲染)');
26
38
  setLocale('zh-CN');
27
39
  assert.equal(getLocale(), 'zh-CN');
28
- assert.equal(t('md.wish_budget', {
29
- cny: 4950
30
- }), '、约 ¥4950', 'zh 模板逐字节');
40
+ assert.equal(t('md.wish_budget', { cny: 4950 }), '、约 ¥4950', 'zh 模板逐字节');
31
41
  console.log('\nI18N TESTS: 4/4 OK(en 零缺键/默认 zh 金标准一致/en 切换数据不动/插值+回退)');
32
-
33
-
34
- //# sourceURL=ts/scripts/i18n-tests.ts