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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (533) hide show
  1. package/README.md +172 -219
  2. package/README.zh-CN.md +222 -0
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-booking-copilot.js +53 -0
  5. package/bin/gotry-bootstrap.js +1788 -0
  6. package/bin/gotry-inner.js +733 -83
  7. package/bin/gotry-process-liveness.d.ts +27 -0
  8. package/bin/gotry-process-liveness.js +97 -0
  9. package/bin/gotry-runtime-resolution.d.ts +27 -0
  10. package/bin/gotry-runtime-resolution.js +49 -0
  11. package/bin/gotry.js +1 -1
  12. package/client/client.js +203 -0
  13. package/cordis.gotry-patch.yml +80 -56
  14. package/data/airline-airports.json +39 -0
  15. package/data/stations-12306-verify.json +524 -0
  16. package/dist/capabilities/agent-reach-deep.js +45 -122
  17. package/dist/capabilities/agent-reach.js +101 -146
  18. package/dist/capabilities/anything.js +161 -94
  19. package/dist/capabilities/artifacts.js +370 -0
  20. package/dist/capabilities/channel-health.js +131 -0
  21. package/dist/capabilities/channel-registry.js +256 -0
  22. package/dist/capabilities/doctor.js +553 -0
  23. package/dist/capabilities/effect.js +394 -0
  24. package/dist/capabilities/external-event.js +149 -0
  25. package/dist/capabilities/fact-log.js +55 -0
  26. package/dist/capabilities/flyai-config.js +337 -0
  27. package/dist/capabilities/flyai.js +947 -110
  28. package/dist/capabilities/fx-contract.js +293 -0
  29. package/dist/capabilities/geo-atlas.js +246 -0
  30. package/dist/capabilities/ground-transfer.js +676 -0
  31. package/dist/capabilities/hbcli.js +260 -123
  32. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  33. package/dist/capabilities/incident-log.js +131 -61
  34. package/dist/capabilities/itinerary-artifact.js +190 -0
  35. package/dist/capabilities/itinerary-deck-artifact.js +194 -0
  36. package/dist/capabilities/itinerary-deck-export.js +339 -0
  37. package/dist/capabilities/lavish-local.js +924 -0
  38. package/dist/capabilities/model-override.js +38 -0
  39. package/dist/capabilities/opensky.js +54 -43
  40. package/dist/capabilities/provider-detail-sanitize.js +64 -0
  41. package/dist/capabilities/resilience.js +191 -0
  42. package/dist/capabilities/route-provider-conformance.js +341 -0
  43. package/dist/capabilities/session/action-cache.js +50 -44
  44. package/dist/capabilities/session/adapters/ctrip-flight.js +120 -62
  45. package/dist/capabilities/session/adapters/ctrip-hotel.js +222 -0
  46. package/dist/capabilities/session/adapters/dida-portal.js +274 -0
  47. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  48. package/dist/capabilities/session/adapters/rail-12306.js +342 -0
  49. package/dist/capabilities/session/benchmark.js +189 -0
  50. package/dist/capabilities/session/bridge-ledger.js +224 -0
  51. package/dist/capabilities/session/extension-bridge.js +636 -0
  52. package/dist/capabilities/session/extension-channel.js +109 -0
  53. package/dist/capabilities/session/extension-distribution.js +230 -0
  54. package/dist/capabilities/session/extract.js +31 -21
  55. package/dist/capabilities/session/golden-score.js +114 -0
  56. package/dist/capabilities/session/health-watch.js +215 -0
  57. package/dist/capabilities/session/read-guard.js +75 -24
  58. package/dist/capabilities/session/static-flight-golden.js +150 -0
  59. package/dist/capabilities/session/transport.js +97 -83
  60. package/dist/capabilities/session/wizard.js +117 -0
  61. package/dist/capabilities/session-consent.js +178 -0
  62. package/dist/capabilities/session-login.js +170 -0
  63. package/dist/capabilities/session-search.js +635 -44
  64. package/dist/capabilities/spawn-bounded.js +203 -0
  65. package/dist/capabilities/sponsor-plugin.js +341 -0
  66. package/dist/capabilities/visa-policy.js +140 -0
  67. package/dist/capabilities/weather.js +243 -138
  68. package/dist/data/session-golden-20.json +25 -0
  69. package/dist/data/sf-golden-manifest.json +103 -0
  70. package/dist/data/sf-static-routes.json +91 -0
  71. package/dist/scripts/action-cache-tests.js +24 -59
  72. package/dist/scripts/agent-planning-turn-deadline-e2e.js +242 -0
  73. package/dist/scripts/agent-planning-turn-deadline-tests.js +185 -0
  74. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  75. package/dist/scripts/agent-reach-error-tests.js +354 -0
  76. package/dist/scripts/agent-reach-tests.js +26 -18
  77. package/dist/scripts/agent-reach-wrapper-tests.js +50 -50
  78. package/dist/scripts/anything-path-measure-tests.js +133 -0
  79. package/dist/scripts/anything-path-measure.js +150 -0
  80. package/dist/scripts/anything-tests.js +250 -60
  81. package/dist/scripts/artifact-client-contract-tests.js +220 -0
  82. package/dist/scripts/artifacts-capability-tests.js +727 -0
  83. package/dist/scripts/async-collect.js +50 -11
  84. package/dist/scripts/benchmark-environment-bridge-e2e.js +741 -0
  85. package/dist/scripts/benchmark-environment-bridge-tests.js +1452 -0
  86. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +126 -0
  87. package/dist/scripts/booking-copilot-availability-policy-tests.js +265 -0
  88. package/dist/scripts/booking-copilot-bin-proof-tests.js +66 -0
  89. package/dist/scripts/booking-copilot-cleanup-proof-helper.js +76 -0
  90. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +144 -0
  91. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +424 -0
  92. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +2839 -0
  93. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +309 -0
  94. package/dist/scripts/booking-copilot-dsh-readiness-proof.js +238 -0
  95. package/dist/scripts/booking-copilot-dsh-warmer-proof.js +126 -0
  96. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +156 -0
  97. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +70 -0
  98. package/dist/scripts/booking-copilot-intent-proof-tests.js +422 -0
  99. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +184 -0
  100. package/dist/scripts/booking-copilot-planner-live-probe.js +95 -0
  101. package/dist/scripts/booking-copilot-proof-runtime.js +42 -0
  102. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +210 -0
  103. package/dist/scripts/booking-copilot-runtime-proof-tests.js +2349 -0
  104. package/dist/scripts/booking-copilot-server-proof-tests.js +784 -0
  105. package/dist/scripts/booking-copilot-startup-proof-tests.js +94 -0
  106. package/dist/scripts/booking-executor-tests.js +80 -0
  107. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  108. package/dist/scripts/booking-saga-tests.js +125 -0
  109. package/dist/scripts/booking-surface-contract-proof-tests.js +212 -0
  110. package/dist/scripts/bootstrap-tests.js +1549 -0
  111. package/dist/scripts/bridge-events-tests.js +239 -0
  112. package/dist/scripts/bridge-ledger-tests.js +428 -0
  113. package/dist/scripts/build-changelog.js +238 -0
  114. package/dist/scripts/build-metrics-report.js +331 -0
  115. package/dist/scripts/changelog-tests.js +118 -0
  116. package/dist/scripts/channel-probe-tests.js +103 -0
  117. package/dist/scripts/channel-probe.js +169 -0
  118. package/dist/scripts/channel-registry-tests.js +208 -0
  119. package/dist/scripts/city-scenario-tier-tests.js +234 -0
  120. package/dist/scripts/companion-tests.js +25 -70
  121. package/dist/scripts/dida-cookie-probe.js +87 -0
  122. package/dist/scripts/dida-runner-stop-tests.js +311 -0
  123. package/dist/scripts/dida-sw-probe.js +114 -0
  124. package/dist/scripts/diff-test.js +17 -12
  125. package/dist/scripts/doctor-tests.js +600 -0
  126. package/dist/scripts/drill-multiuser-ledger-tests.js +1076 -0
  127. package/dist/scripts/drill-sdk-descendant-cleanup-tests.js +606 -0
  128. package/dist/scripts/drill-w2a-sensor-tests.js +788 -0
  129. package/dist/scripts/dsh-artifact-e2e.js +279 -0
  130. package/dist/scripts/dsh-artifact-web-e2e.js +675 -0
  131. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  132. package/dist/scripts/dsh-llm-tests.js +437 -0
  133. package/dist/scripts/dsh-runtime-closure-tests.js +182 -0
  134. package/dist/scripts/dsh-runtime-closure.js +162 -0
  135. package/dist/scripts/dsh-session-v3-migration-proof.js +109 -0
  136. package/dist/scripts/dsh-subprocess-local-proof.js +150 -0
  137. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  138. package/dist/scripts/effect-tests.js +539 -0
  139. package/dist/scripts/engine-run.js +1 -3
  140. package/dist/scripts/engine-tests.js +16 -10
  141. package/dist/scripts/evaluation-cadence-tests.js +174 -0
  142. package/dist/scripts/evaluation-contract-tests.js +268 -0
  143. package/dist/scripts/extension-distribution-cli.js +39 -0
  144. package/dist/scripts/extension-distribution-tests.js +443 -0
  145. package/dist/scripts/extension-tests.js +988 -0
  146. package/dist/scripts/external-event-tests.js +350 -0
  147. package/dist/scripts/fact-gate-tests.js +869 -0
  148. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  149. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  150. package/dist/scripts/flight-malformed-tests.js +159 -0
  151. package/dist/scripts/flyai-setup-tests.js +228 -0
  152. package/dist/scripts/flyai-setup-tool-tests.js +54 -0
  153. package/dist/scripts/flyai-tests.js +692 -0
  154. package/dist/scripts/flyai-tool-registration-contract-tests.js +91 -0
  155. package/dist/scripts/fx-contract-tests.js +201 -0
  156. package/dist/scripts/g5-guard-tests.js +150 -0
  157. package/dist/scripts/g5-guard.js +193 -0
  158. package/dist/scripts/gen-lunar-anchors.js +106 -0
  159. package/dist/scripts/geo-atlas-tests.js +283 -0
  160. package/dist/scripts/gotry-backend-kernel-tests.js +206 -0
  161. package/dist/scripts/gotry-try-demo-tests.js +79 -0
  162. package/dist/scripts/gotry-try-demo.js +49 -0
  163. package/dist/scripts/ground-transfer-tests.js +956 -0
  164. package/dist/scripts/hbcli-e2e-tests.js +159 -0
  165. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  166. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  167. package/dist/scripts/hbcli-tests.js +121 -69
  168. package/dist/scripts/health-watch-cli.js +73 -0
  169. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  170. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  171. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  172. package/dist/scripts/i18n-tests.js +41 -0
  173. package/dist/scripts/incident-tests.js +281 -87
  174. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  175. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  176. package/dist/scripts/issue-271-liveness-tests.js +569 -0
  177. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  178. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  179. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  180. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  181. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  182. package/dist/scripts/issue436-persisted-routing-e2e.js +247 -0
  183. package/dist/scripts/itinerary-artifact-tests.js +368 -0
  184. package/dist/scripts/itinerary-deck-artifact-tests.js +416 -0
  185. package/dist/scripts/itinerary-deck-export-tests.js +552 -0
  186. package/dist/scripts/itinerary-deck-tests.js +435 -0
  187. package/dist/scripts/itinerary-html-tests.js +426 -0
  188. package/dist/scripts/journey-tests.js +22 -27
  189. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  190. package/dist/scripts/kernel-manifest-gate.js +126 -0
  191. package/dist/scripts/kernel-manifest-tests.js +212 -0
  192. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  193. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  194. package/dist/scripts/kernel-manifest.js +369 -0
  195. package/dist/scripts/lavish-local-tests.js +1226 -0
  196. package/dist/scripts/lavish-product-tools-tests.js +537 -0
  197. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  198. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  199. package/dist/scripts/ledger-tests.js +382 -246
  200. package/dist/scripts/ledger-workflow-crash.js +10 -20
  201. package/dist/scripts/m3-cohort-tests.js +714 -0
  202. package/dist/scripts/m3-cohort.js +310 -0
  203. package/dist/scripts/managed-dsh-cleanup-diagnostic-proof.js +184 -0
  204. package/dist/scripts/managed-dsh-close-failure-proof.js +172 -0
  205. package/dist/scripts/managed-dsh-run-port-proof.js +153 -0
  206. package/dist/scripts/managed-dsh-terminal-proof.js +252 -0
  207. package/dist/scripts/map-tools-vendor-package-proof.js +365 -0
  208. package/dist/scripts/memory-capture-tests.js +242 -61
  209. package/dist/scripts/memory-decay-tests.js +18 -52
  210. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  211. package/dist/scripts/memory-lifecycle.js +196 -0
  212. package/dist/scripts/memory-metrics.js +24 -12
  213. package/dist/scripts/memory-value-report.js +631 -0
  214. package/dist/scripts/memory-value-tests.js +328 -0
  215. package/dist/scripts/metrics-report-tests.js +151 -0
  216. package/dist/scripts/model-override-e2e.js +137 -0
  217. package/dist/scripts/nightly-evidence-tests.js +203 -0
  218. package/dist/scripts/nightly-evidence.js +303 -0
  219. package/dist/scripts/nudge-digest.js +39 -28
  220. package/dist/scripts/observability-tests.js +381 -0
  221. package/dist/scripts/onboarding-tests.js +161 -0
  222. package/dist/scripts/opensky-check.js +18 -13
  223. package/dist/scripts/opensky-tests.js +28 -28
  224. package/dist/scripts/outcome-projection-tests.js +427 -0
  225. package/dist/scripts/persona-sim-tests.js +603 -0
  226. package/dist/scripts/persona-sim.js +1081 -0
  227. package/dist/scripts/persona-surface-guard-tests.js +49 -0
  228. package/dist/scripts/pnpm-dsh-closure-proof.js +17 -0
  229. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  230. package/dist/scripts/price-drift-tests.js +340 -0
  231. package/dist/scripts/price-drift-watch.js +447 -0
  232. package/dist/scripts/probe-poi-tests.js +53 -45
  233. package/dist/scripts/product-metrics.js +483 -0
  234. package/dist/scripts/publish-preverify.js +110 -0
  235. package/dist/scripts/realtime-pricing-tests.js +76 -0
  236. package/dist/scripts/recall-tests.js +513 -0
  237. package/dist/scripts/replay-async.js +22 -18
  238. package/dist/scripts/replay-real.js +18 -18
  239. package/dist/scripts/replay.js +54 -82
  240. package/dist/scripts/route-provider-conformance-tests.js +391 -0
  241. package/dist/scripts/session-attach-diagnose.js +20 -20
  242. package/dist/scripts/session-attach-poc.js +34 -37
  243. package/dist/scripts/session-benchmark.js +180 -0
  244. package/dist/scripts/session-dida-live-e2e.js +199 -0
  245. package/dist/scripts/session-extract-tests.js +14 -35
  246. package/dist/scripts/session-link-tests.js +478 -0
  247. package/dist/scripts/session-login.js +27 -46
  248. package/dist/scripts/session-tests.js +796 -96
  249. package/dist/scripts/session-zone-crash.js +49 -0
  250. package/dist/scripts/session-zone-ledger-tests.js +432 -0
  251. package/dist/scripts/session-zone-observation-tests.js +492 -0
  252. package/dist/scripts/session-zone-observe.js +135 -0
  253. package/dist/scripts/session-zone-wiring-tests.js +566 -0
  254. package/dist/scripts/session-zones-tests.js +374 -0
  255. package/dist/scripts/sf-live-benchmark.js +367 -0
  256. package/dist/scripts/sf-live-challenge-stop-tests.js +453 -0
  257. package/dist/scripts/sf-live-cli-tests.js +92 -0
  258. package/dist/scripts/sf-live-options.js +46 -0
  259. package/dist/scripts/sf-manifest-tests.js +30 -0
  260. package/dist/scripts/sf-manifest.js +33 -0
  261. package/dist/scripts/sf-soft-score-tests.js +67 -0
  262. package/dist/scripts/sf-summary-tests.js +324 -0
  263. package/dist/scripts/sf-summary.js +525 -0
  264. package/dist/scripts/share-tests.js +432 -0
  265. package/dist/scripts/skeleton-check.js +16 -14
  266. package/dist/scripts/skeleton-integration-test.js +28 -11
  267. package/dist/scripts/skills-contract-tests.js +51 -46
  268. package/dist/scripts/smoke-session-gate-tests.js +21 -0
  269. package/dist/scripts/smoke.js +690 -246
  270. package/dist/scripts/spawn-bounded-abort-tests.js +244 -0
  271. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  272. package/dist/scripts/state-cli-tests.js +226 -101
  273. package/dist/scripts/state-cli.js +565 -198
  274. package/dist/scripts/state-repair-cli.js +66 -0
  275. package/dist/scripts/state-repair-tests.js +140 -0
  276. package/dist/scripts/static-golden-tests.js +165 -0
  277. package/dist/scripts/time-eval-tests.js +212 -192
  278. package/dist/scripts/travel-timeline-tests.js +29 -93
  279. package/dist/scripts/turn-handoff-collect-tests.js +169 -0
  280. package/dist/scripts/turn-handoff-collect.js +161 -0
  281. package/dist/scripts/turn-policy-tests.js +55 -0
  282. package/dist/scripts/typed-contract-canary.js +176 -0
  283. package/dist/scripts/tz-resolver-sanity.js +50 -0
  284. package/dist/scripts/tz-resolver-tests.js +584 -0
  285. package/dist/scripts/unified-tests.js +60 -20
  286. package/dist/scripts/visa-policy-tests.js +62 -0
  287. package/dist/scripts/weather-tests.js +276 -41
  288. package/dist/scripts/wish-channel-gate-tests.js +54 -0
  289. package/dist/scripts/write-gate-tests.js +522 -0
  290. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  291. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  292. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  293. package/dist/scripts/z3-race-tests.js +46 -0
  294. package/dist/src/artifact-gate.js +829 -0
  295. package/dist/src/backend/kernel.js +91 -0
  296. package/dist/src/backend/modules/booking-copilot.js +92 -0
  297. package/dist/src/backend/modules/booking-executor.js +124 -0
  298. package/dist/src/backend/modules/session-search.js +551 -0
  299. package/dist/src/benchmark-agent-conformance.js +578 -0
  300. package/dist/src/benchmark-environment-bridge.js +515 -0
  301. package/dist/src/benchmark-headless-child-diagnostics.js +166 -0
  302. package/dist/src/benchmark-tool-isolation.js +151 -0
  303. package/dist/src/bookable-facts.js +533 -0
  304. package/dist/src/booking-saga.js +97 -0
  305. package/dist/src/booking-surface/availability-policy.js +697 -0
  306. package/dist/src/booking-surface/booking-intent.js +43 -0
  307. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  308. package/dist/src/booking-surface/canonical-schema.js +116 -0
  309. package/dist/src/booking-surface/contracts.js +75 -0
  310. package/dist/src/booking-surface/dsh-planner.js +1854 -0
  311. package/dist/src/booking-surface/dsh-plugin.js +378 -0
  312. package/dist/src/booking-surface/dsh-worker.js +98 -0
  313. package/dist/src/booking-surface/error-codes.js +231 -0
  314. package/dist/src/booking-surface/index.js +9 -0
  315. package/dist/src/booking-surface/managed-dsh-cleanup-diagnostic.js +97 -0
  316. package/dist/src/booking-surface/managed-dsh-run-port.js +244 -0
  317. package/dist/src/booking-surface/profile.js +24 -0
  318. package/dist/src/booking-surface/recovery-chain.js +367 -0
  319. package/dist/src/booking-surface/runtime.js +1968 -0
  320. package/dist/src/booking-surface/server.js +674 -0
  321. package/dist/src/booking-surface/startup.js +168 -0
  322. package/dist/src/booking-surface/validation.js +261 -0
  323. package/dist/src/bridge.js +20 -13
  324. package/dist/src/city-scenario-tier.js +378 -0
  325. package/dist/src/companions.js +48 -74
  326. package/dist/src/contracts.js +21 -43
  327. package/dist/src/dsh-llm.js +171 -115
  328. package/dist/src/engine.js +276 -193
  329. package/dist/src/evaluation-cadence.js +97 -0
  330. package/dist/src/evaluation-contracts.js +283 -0
  331. package/dist/src/flight-pack-adapter.js +54 -0
  332. package/dist/src/flight-pack-contract.js +2 -0
  333. package/dist/src/flyai-setup-tool.js +70 -0
  334. package/dist/src/gotry-backend.js +125 -0
  335. package/dist/src/hotel-date-gate.js +118 -0
  336. package/dist/src/i18n.js +88 -0
  337. package/dist/src/index.js +2385 -881
  338. package/dist/src/itinerary-deck.js +139 -0
  339. package/dist/src/itinerary-doc-shared.js +661 -0
  340. package/dist/src/itinerary-html.js +131 -0
  341. package/dist/src/journey.js +155 -108
  342. package/dist/src/lavish-tools.js +400 -0
  343. package/dist/src/ledger-repair-apply.js +538 -0
  344. package/dist/src/ledger-repair-plan.js +463 -0
  345. package/dist/src/loop.js +331 -161
  346. package/dist/src/m3-cohort.js +1577 -0
  347. package/dist/src/memory-capture.js +180 -32
  348. package/dist/src/memory-decay.js +30 -14
  349. package/dist/src/memory-lifecycle.js +1310 -0
  350. package/dist/src/memory-utility.js +27 -40
  351. package/dist/src/mock-llm.js +36 -61
  352. package/dist/src/model.js +81 -36
  353. package/dist/src/outcome-projection.js +746 -0
  354. package/dist/src/realtime-pricing.js +103 -0
  355. package/dist/src/recall/card.js +74 -0
  356. package/dist/src/recall/evaluator.js +140 -0
  357. package/dist/src/recall/integration.js +17 -0
  358. package/dist/src/recall/tick.js +166 -0
  359. package/dist/src/session-link/plan-it-action.js +48 -0
  360. package/dist/src/session-link/session-link.js +248 -0
  361. package/dist/src/session-zone-ledger.js +312 -0
  362. package/dist/src/session-zone-observation.js +354 -0
  363. package/dist/src/session-zone-wiring.js +658 -0
  364. package/dist/src/session-zones.js +510 -0
  365. package/dist/src/share/adapters.js +51 -0
  366. package/dist/src/share/share-consent.js +76 -0
  367. package/dist/src/share/share-deck.js +58 -0
  368. package/dist/src/share/share-token.js +146 -0
  369. package/dist/src/slot-spec.js +84 -106
  370. package/dist/src/state-ledger.js +696 -524
  371. package/dist/src/state-repair.js +299 -0
  372. package/dist/src/subagent-job-id-guard.js +58 -0
  373. package/dist/src/time-anchor.js +231 -33
  374. package/dist/src/tool-packet.js +20 -8
  375. package/dist/src/travel-slots.js +58 -35
  376. package/dist/src/travel-timeline.js +44 -63
  377. package/dist/src/turn-deadline.js +287 -0
  378. package/dist/src/turn-policy.js +118 -0
  379. package/dist/src/tz-resolver.js +143 -0
  380. package/dist/src/unified.js +600 -360
  381. package/dist/src/wish-pool.js +42 -12
  382. package/dist/src/write-gate.js +495 -0
  383. package/dist/src/z3-shared.js +284 -0
  384. package/extension/README.md +21 -0
  385. package/extension/README.zh-CN.md +21 -0
  386. package/extension/background.js +463 -0
  387. package/extension/content-bridge.js +270 -0
  388. package/extension/content-main.js +111 -0
  389. package/extension/manifest.json +60 -0
  390. package/package.json +407 -14
  391. package/schemas/booking.intent.schema.json +193 -0
  392. package/schemas/booking.surface.schema.json +2622 -0
  393. package/ts/capabilities/agent-reach-deep.ts +114 -0
  394. package/ts/capabilities/agent-reach.ts +198 -0
  395. package/ts/capabilities/anything.ts +282 -0
  396. package/ts/capabilities/artifacts.ts +428 -0
  397. package/ts/capabilities/fact-log.ts +54 -0
  398. package/ts/capabilities/flyai-config.ts +388 -0
  399. package/ts/capabilities/flyai.ts +1274 -0
  400. package/ts/capabilities/hbcli.ts +253 -71
  401. package/ts/capabilities/incident-log.ts +88 -59
  402. package/ts/capabilities/itinerary-artifact.ts +214 -0
  403. package/ts/capabilities/itinerary-deck-artifact.ts +218 -0
  404. package/ts/capabilities/itinerary-deck-export.ts +378 -0
  405. package/ts/capabilities/opensky.ts +121 -0
  406. package/ts/capabilities/session/action-cache.ts +148 -0
  407. package/ts/capabilities/session/adapters/ctrip-flight.ts +181 -0
  408. package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
  409. package/ts/capabilities/session/adapters/dida-portal.ts +338 -0
  410. package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
  411. package/ts/capabilities/session/adapters/rail-12306.ts +392 -0
  412. package/ts/capabilities/session/benchmark.ts +276 -0
  413. package/ts/capabilities/session/bridge-ledger.ts +333 -0
  414. package/ts/capabilities/session/extension-bridge.ts +794 -0
  415. package/ts/capabilities/session/extension-channel.ts +144 -0
  416. package/ts/capabilities/session/extension-distribution.ts +266 -0
  417. package/ts/capabilities/session/extract.ts +54 -0
  418. package/ts/capabilities/session/golden-score.ts +139 -0
  419. package/ts/capabilities/session/health-watch.ts +264 -0
  420. package/ts/capabilities/session/read-guard.ts +141 -0
  421. package/ts/capabilities/session/static-flight-golden.ts +209 -0
  422. package/ts/capabilities/session/transport.ts +146 -0
  423. package/ts/capabilities/session/wizard.ts +140 -0
  424. package/ts/capabilities/session-consent.ts +211 -0
  425. package/ts/capabilities/session-login.ts +208 -0
  426. package/ts/capabilities/session-search.ts +893 -0
  427. package/ts/capabilities/weather.ts +330 -0
  428. package/ts/dsh-runtime/vendor/README.md +68 -0
  429. package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
  430. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +405 -0
  431. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +404 -0
  432. package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +1584 -0
  433. package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
  434. package/ts/dsh-runtime/vendor/dsh-map-tools/icon.svg +5 -0
  435. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +595 -0
  436. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
  437. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +58 -0
  438. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
  439. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
  440. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +131 -0
  441. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
  442. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/geo.js +232 -0
  443. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +120 -0
  444. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +40 -0
  445. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/staticmap-route.js +115 -0
  446. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
  447. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
  448. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +267 -0
  449. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +90 -0
  450. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
  451. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
  452. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
  453. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
  454. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +23 -0
  455. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
  456. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/geo.d.ts +61 -0
  457. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
  458. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
  459. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/staticmap-route.d.ts +31 -0
  460. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
  461. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
  462. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +75 -0
  463. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +68 -0
  464. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
  465. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +97 -0
  466. package/ts/package.json +293 -3
  467. package/ts/scripts/async-collect.ts +91 -0
  468. package/ts/scripts/map-tools-vendor-package-proof.ts +404 -0
  469. package/ts/scripts/skeleton-check.ts +1 -1
  470. package/ts/scripts/skeleton-integration-test.ts +23 -1
  471. package/ts/scripts/state-cli.ts +586 -0
  472. package/ts/scripts/turn-handoff-collect.ts +178 -0
  473. package/ts/src/artifact-gate.ts +991 -0
  474. package/ts/src/benchmark-agent-conformance.ts +619 -0
  475. package/ts/src/benchmark-environment-bridge.ts +538 -0
  476. package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
  477. package/ts/src/benchmark-tool-isolation.ts +166 -0
  478. package/ts/src/bookable-facts.ts +837 -0
  479. package/ts/src/booking-saga.ts +133 -0
  480. package/ts/src/booking-surface/availability-policy.ts +522 -0
  481. package/ts/src/booking-surface/booking-intent.ts +86 -0
  482. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  483. package/ts/src/booking-surface/canonical-schema.js +116 -0
  484. package/ts/src/booking-surface/contracts.ts +475 -0
  485. package/ts/src/booking-surface/dsh-planner.ts +1949 -0
  486. package/ts/src/booking-surface/dsh-plugin.js +378 -0
  487. package/ts/src/booking-surface/dsh-worker.ts +110 -0
  488. package/ts/src/booking-surface/error-codes.ts +241 -0
  489. package/ts/src/booking-surface/index.ts +9 -0
  490. package/ts/src/booking-surface/managed-dsh-cleanup-diagnostic.ts +151 -0
  491. package/ts/src/booking-surface/managed-dsh-run-port.ts +273 -0
  492. package/ts/src/booking-surface/profile.ts +42 -0
  493. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  494. package/ts/src/booking-surface/runtime.ts +1793 -0
  495. package/ts/src/booking-surface/server.ts +733 -0
  496. package/ts/src/booking-surface/startup.ts +201 -0
  497. package/ts/src/booking-surface/validation.ts +217 -0
  498. package/ts/src/companions.ts +113 -0
  499. package/ts/src/contracts.ts +17 -3
  500. package/ts/src/dsh-llm.ts +152 -17
  501. package/ts/src/flyai-setup-tool.ts +77 -0
  502. package/ts/src/index.ts +2123 -200
  503. package/ts/src/itinerary-deck.ts +189 -0
  504. package/ts/src/itinerary-doc-shared.ts +741 -0
  505. package/ts/src/itinerary-html.ts +171 -0
  506. package/ts/src/loop.ts +234 -26
  507. package/ts/src/memory-capture.ts +283 -0
  508. package/ts/src/memory-decay.ts +55 -0
  509. package/ts/src/memory-utility.ts +70 -0
  510. package/ts/src/mock-llm.ts +4 -8
  511. package/ts/src/model.ts +88 -2
  512. package/ts/src/recall/card.ts +106 -0
  513. package/ts/src/recall/evaluator.ts +170 -0
  514. package/ts/src/recall/integration.ts +35 -0
  515. package/ts/src/recall/tick.ts +213 -0
  516. package/ts/src/session-link/plan-it-action.ts +78 -0
  517. package/ts/src/session-link/session-link.ts +275 -0
  518. package/ts/src/share/adapters.ts +93 -0
  519. package/ts/src/share/share-consent.ts +105 -0
  520. package/ts/src/share/share-deck.ts +85 -0
  521. package/ts/src/share/share-token.ts +166 -0
  522. package/ts/src/slot-spec.ts +172 -0
  523. package/ts/src/state-ledger.ts +1166 -0
  524. package/ts/src/subagent-job-id-guard.ts +72 -0
  525. package/ts/src/time-anchor.ts +338 -0
  526. package/ts/src/tool-packet.ts +32 -0
  527. package/ts/src/travel-slots.ts +219 -0
  528. package/ts/src/travel-timeline.ts +83 -0
  529. package/ts/src/turn-deadline.ts +366 -0
  530. package/ts/src/turn-policy.ts +154 -0
  531. package/ts/src/unified.ts +444 -105
  532. package/ts/src/wish-pool.ts +79 -0
  533. package/dist/scripts/session-attach-wait.js +0 -41
@@ -1,49 +1,89 @@
1
+ /**
2
+ * TS 统一求解器断言(镜像 py test_unified 的航班链部分 + D-2 回归)。
3
+ * 运行(在 ts/ 下):npx tsx scripts/unified-tests.ts
4
+ */
1
5
  import assert from 'node:assert/strict';
2
6
  import { readFile } from 'node:fs/promises';
3
7
  import { join } from 'node:path';
4
8
  import { parseFlightPackToSpec, solveUnified } from '../src/unified.js';
5
9
  import { hhmmToMin } from '../src/model.js';
6
10
  const pack = JSON.parse(await readFile(join('..', 'data', 'flights_2026.json'), 'utf-8'));
11
+ // 1. 航班包经统一模型求解:可行、区间正确、负例排除、红眼精力
7
12
  const spec = parseFlightPackToSpec(pack);
8
13
  spec.budgetCny = 9000;
9
14
  const r1 = await solveUnified(spec);
10
15
  assert.equal(r1.feasible, true);
11
- assert.equal(r1.money_cny, r1.legs.reduce((a, l)=>a + l.price_cny, 0));
16
+ // 工作窗口生效后 f2 被迫选 VZ303(¥350),有效组合区间下移(M-1 后重算)
17
+ assert.equal(r1.money_cny, r1.legs.reduce((a, l) => a + l.price_cny, 0));
12
18
  assert.ok(7280 <= r1.money_cny && r1.money_cny <= 8350, `money=${r1.money_cny}`);
13
- const byLeg = Object.fromEntries(r1.legs.map((l)=>[
14
- l.leg,
15
- l
16
- ]));
17
- assert.notEqual(byLeg['f4'].service, 'DZ6252');
19
+ const byLeg = Object.fromEntries(r1.legs.map(l => [l.leg, l]));
20
+ assert.notEqual(byLeg['f4'].service, 'DZ6252'); // 负例被锚点排除
18
21
  assert.equal(byLeg['f5'].service, 'EK329');
19
- assert.equal(byLeg['f5'].energy_pct, 79);
20
- assert.ok(byLeg['f5'].wake.includes('前一日'));
21
- assert.equal(byLeg['f5'].door_to_door, '11h20m');
22
+ assert.equal(byLeg['f5'].energy_pct, 79); // 红眼睡眠模型(D-6 校准:机上 75 + 落地接驳 45min×5%/h≈+4 → 79;上限 80)
23
+ assert.ok(byLeg['f5'].wake.includes('前一日')); // 跨日显示
24
+ // D-5 时区感知核算(与 Python oracle 同款断言)
25
+ assert.equal(byLeg['f5'].door_to_door, '11h20m'); // 3h 前置 + 7h35m 真实飞行 + 45m 接驳
22
26
  if (byLeg['f3'].service === 'MU6088') {
23
- assert.equal(byLeg['f3'].door_to_door, '6h15m');
27
+ assert.equal(byLeg['f3'].door_to_door, '6h15m'); // +1h 时差已扣
24
28
  }
29
+ // 2. D-2 回归:锚点冲突时 core 字符串不带竖线,精确点名
25
30
  const tight = parseFlightPackToSpec(pack);
26
31
  tight.segments[0].anchors.arriveByMin = hhmmToMin('15:00');
27
32
  const r2 = await solveUnified(tight);
28
33
  assert.equal(r2.feasible, false);
29
34
  assert.ok(r2.unsat_core.includes('f1:arrive_by'), `core=${JSON.stringify(r2.unsat_core)}`);
30
- assert.ok(r2.unsat_core.every((c)=>!c.includes('|')), 'core 无竖线残留');
31
- assert.ok(r2.suggestions.some((sg)=>sg.relax === 'f1:arrive_by'));
35
+ assert.ok(r2.unsat_core.every(c => !c.includes('|')), 'core 无竖线残留');
36
+ assert.ok(r2.suggestions.some(sg => sg.relax === 'f1:arrive_by'));
37
+ // 3. 预算冲突 core 命名
32
38
  const poor = parseFlightPackToSpec(pack);
33
39
  poor.budgetCny = 1000;
34
40
  const r3 = await solveUnified(poor);
35
41
  assert.equal(r3.feasible, false);
36
42
  assert.ok(r3.unsat_core.includes('total:budget'));
43
+ // 4. M-1:工作窗口排除周五晚班,gate q3 被确定性回答(与 Python oracle 同款)
37
44
  const excluded = r1.work_window_exclusions ?? [];
38
- assert.ok(excluded.some((e)=>e.segment === 'f2' && e.option === 'TG216'), JSON.stringify(excluded));
39
- assert.ok(excluded.some((e)=>e.segment === 'f2' && e.option === 'TG218'));
40
- assert.equal(byLeg['f2'].service, 'VZ303');
45
+ assert.ok(excluded.some(e => e.segment === 'f2' && e.option === 'TG216'), JSON.stringify(excluded));
46
+ assert.ok(excluded.some(e => e.segment === 'f2' && e.option === 'TG218'));
47
+ assert.equal(byLeg['f2'].service, 'VZ303'); // 只剩周六早班——与真实选择一致
41
48
  const off = parseFlightPackToSpec(pack);
42
49
  off.budgetCny = 9000;
43
50
  off.workWindow = undefined;
44
51
  const r4 = await solveUnified(off);
45
- assert.deepEqual(r4.work_window_exclusions, []);
46
- console.log(`TS UNIFIED TESTS: 4/4 OK(工作窗口生效,f2=VZ303,¥${r1.money_cny})`);
47
-
48
-
49
- //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/unified-tests.ts
52
+ assert.deepEqual(r4.work_window_exclusions, []); // 关掉窗口,周五班恢复可选
53
+ // 5. Issue #620:yunnan-pack 的 yn0 段缺 buffer_min/origin_transfer_min/dest_transfer_min
54
+ // → 旧实现 Number(undefined)=NaN → Int.val(NaN) 在 z3 WASM 里 `Assertion failed`
55
+ // → 被兜底翻译成「不可行 + unsat_core:['wasm_runtime_error']」。三条红线:
56
+ // ① parse 边界必须点名拒收(不默认补 0);② 绕过 parse 的 NaN spec 必须以
57
+ // solver_error 返回而**绝不**是 infeasible;③ 补齐那三个字段后整包必须真能判定。
58
+ const yunnanRaw = JSON.parse(await readFile(join('..', 'data', 'yunnan-pack.json'), 'utf-8'));
59
+ // ① 真实数据包:yn0 的三个缺字段 → NaN → 必须是显式 solver_error,绝不是「不可行」
60
+ const yunnanSpecRaw = parseFlightPackToSpec(structuredClone(yunnanRaw));
61
+ yunnanSpecRaw.budgetCny = 9000;
62
+ assert.ok(Number.isNaN(yunnanSpecRaw.segments[0].options[0].move.bufferMin), 'yn0 缺 buffer_min 的事实前提');
63
+ const rYunnanRaw = await solveUnified(yunnanSpecRaw);
64
+ assert.equal(rYunnanRaw.solver_error?.code, 'solver_input_not_integer', `yunnan 包必须以 solver_error 返回(got ${JSON.stringify(rYunnanRaw)})`);
65
+ assert.ok(rYunnanRaw.solver_error.message.includes('yn0.bufferMin'), `solver_error 必须点名出问题的段与字段(got ${rYunnanRaw.solver_error.message})`);
66
+ assert.equal(rYunnanRaw.feasible, false, 'solver_error 轮次没有产出可行解');
67
+ assert.deepEqual(rYunnanRaw.unsat_core, undefined, '求解器失败不得伪造 unsat_core(机器 token 不进冲突位)');
68
+ assert.ok(!JSON.stringify(rYunnanRaw).includes('wasm_runtime_error'), '不得再出现 wasm_runtime_error 这个伪冲突 token');
69
+ // ② 最小输入:健康包里塞一个 NaN 接驳分钟 → 同样是 solver_error,点名到字段
70
+ const nanSpec = parseFlightPackToSpec(pack);
71
+ nanSpec.budgetCny = 9000;
72
+ nanSpec.segments[0].options[0].move.bufferMin = Number.NaN;
73
+ const rNan = await solveUnified(nanSpec);
74
+ assert.equal(rNan.solver_error?.code, 'solver_input_not_integer', `NaN 接驳必须以 solver_error 返回(got ${JSON.stringify(rNan)})`);
75
+ assert.ok(rNan.solver_error.message.includes('f1.bufferMin'), `solver_error 必须点名字段(got ${rNan.solver_error.message})`);
76
+ assert.equal(rNan.feasible, false, 'solver_error 轮次没有产出可行解');
77
+ // ③ 补齐 yn0 的三个字段(由测试提供,不改数据包:真值待 founder 校准)→ 整包真能判定,
78
+ // 证明断言失败的最小输入正是这三个字段,其余四段编码本身没问题
79
+ const yunnanFixed = structuredClone(yunnanRaw);
80
+ Object.assign(yunnanFixed['legs'][0], {
81
+ buffer_min: 60, origin_transfer_min: 30, dest_transfer_min: 30,
82
+ });
83
+ const yunnanSpec = parseFlightPackToSpec(yunnanFixed);
84
+ yunnanSpec.budgetCny = 9000;
85
+ const rYunnan = await solveUnified(yunnanSpec);
86
+ assert.equal(rYunnan.solver_error, undefined, `补齐三字段后不得再有求解器失败(got ${JSON.stringify(rYunnan.solver_error)})`);
87
+ assert.equal(rYunnan.feasible, true, `yunnan 整包应可行(unsat_core=${JSON.stringify(rYunnan.unsat_core)})`);
88
+ assert.equal(rYunnan.legs.length, 5, '五段全判定');
89
+ console.log(`TS UNIFIED TESTS: 5/5 OK(工作窗口生效,f2=VZ303,¥${r1.money_cny};#620 yunnan NaN→solver_error 点名 yn0,补齐三字段后可行 ¥${rYunnan.money_cny})`);
@@ -0,0 +1,62 @@
1
+ /**
2
+ * 政策事实生产端 v1 测试(issue #141,D-26 收口切片;冻结泰国页 fixture,零网络):
3
+ *
4
+ * 1. extractVisaSection:「签证入境」章节抽取(免签/落地签条款在案;缺章节=null)
5
+ * 2. splitPolicyEntries:中文序号段落切分(一、二、…),短段过滤
6
+ * 3. policyFactsFromMfa:PolicyFact 形态完整(schema/fact_id/as_of/review_by=D+30/来源证据链);幂等
7
+ * 4. VISA_POLICY_FETCH 效应行:策略表行在案(永不重试+断路器护站);注入 handler 走抓取编排
8
+ * 5. listCountryPaths:洲列表页国家路径提取(仅本站相对链接)
9
+ *
10
+ * 运行(在 ts/ 下):npx tsx scripts/visa-policy-tests.ts
11
+ */
12
+ import assert from 'node:assert/strict';
13
+ import { readFileSync } from 'node:fs';
14
+ import { dirname, join } from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+ import { splitPolicyEntries, continentListUrl, countryPageUrl, extractVisaSection, policyFactsFromMfa, } from '../capabilities/visa-policy.js';
17
+ import { makeProductionInterpreter, __resetEffectBreakersForTest } from '../capabilities/effect.js';
18
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
19
+ const FIXTURE = join(ROOT, 'data', 'fixtures', 'visa-policy-tg-sample.html');
20
+ let passed = 0;
21
+ function check(cond, msg) {
22
+ if (cond) {
23
+ passed++;
24
+ console.log(` ok - ${msg}`);
25
+ }
26
+ else {
27
+ console.error(` FAIL - ${msg}`);
28
+ process.exitCode = 1;
29
+ }
30
+ }
31
+ const html = readFileSync(FIXTURE, 'utf-8');
32
+ // 1. 章节抽取
33
+ const section = extractVisaSection(html);
34
+ check(!!section && section.includes('免签'), '签证入境章节应被抽取(免签条款在案)');
35
+ check(section.includes('2024'), '生效日期应保留(逐段溯源)');
36
+ // 2. 条目切分
37
+ const entries = splitPolicyEntries(section);
38
+ check(entries.length >= 2, `中文序号段落切分(实际 ${entries.length} 条)`);
39
+ check(entries.every(e => e.length > 30), '条目长度下限(短段过滤)');
40
+ // 3. PolicyFact 形态
41
+ const fetchedAt = '2026-09-05T00:00:00.000Z';
42
+ const facts = policyFactsFromMfa({ countryLabel: '泰国', countryPath: 'tg_647570', section: section, fetchedAt });
43
+ check(facts.length === entries.length, '事实数=条目数');
44
+ check(facts.every(f => f.kind === 'policy' && f.schema === 'gotry_bookable_fact.v1'), 'PolicyFact 形态');
45
+ check(facts.every(f => f.as_of === '2026-09-05' && f.review_by === '2026-10-05'), 'as_of 抓取日 + review_by=D+30');
46
+ check(facts.every(f => f.source.startsWith('cs-mfa@') && f.query_id.startsWith('visa-policy:cs-mfa:')), '来源证据链 cs.mfa.gov.cn');
47
+ // 幂等:同输入重跑 fact_id 稳定
48
+ const facts2 = policyFactsFromMfa({ countryLabel: '泰国', countryPath: 'tg_647570', section: section, fetchedAt });
49
+ check(facts.every((f, i) => f.fact_id === facts2[i].fact_id), 'fact_id 幂等');
50
+ // 4. URL 形态受控
51
+ assert.equal(countryPageUrl('yz_645708', 'tg_647570'), 'https://cs.mfa.gov.cn/zggmcg/ljmdd/yz_645708/tg_647570/', 'URL 形态');
52
+ assert.equal(countryPageUrl('..', 'x'), null, '路径穿越拒绝');
53
+ assert.equal(continentListUrl('..'), null, '洲路径穿越拒绝');
54
+ // 5. 效应行:VISA_POLICY_FETCH 在注册表(永不重试+断路器护站)
55
+ __resetEffectBreakersForTest();
56
+ const interp = makeProductionInterpreter({ sleep: async () => { }, breakers: new Map(), handlers: {
57
+ VISA_POLICY_FETCH: async () => ({ ok: true, via: 'cs-mfa', evidence: 'e', latencyMs: 1, facts: [], countries: ['tg_647570'] }),
58
+ } });
59
+ const out = await interp({ effect: 'VISA_POLICY_FETCH', params: { country: 'tg_647570' } });
60
+ assert.equal(out.trace.channel, 'api', 'trace.channel=api(策略表行在案)');
61
+ assert.equal(out.trace.attempts, 1, '礼貌抓取:单次执行(策略表 retry=null 生效)');
62
+ console.log(`\nVISA POLICY TESTS: ${passed} pass${process.exitCode ? ', FAIL' : ' (全绿)'}`);
@@ -1,48 +1,283 @@
1
+ /**
2
+ * 天气能力层确定性回归:受控 fetch fixture 覆盖行为和时间预算。
3
+ *
4
+ * 真实 Open-Meteo/Nominatim 观测不参与合并闸;本套件必须离线、快速、可重复。
5
+ */
1
6
  import assert from 'node:assert/strict';
2
7
  import { geocodePlace, getForecast, getClimate, wmoLabel } from '../capabilities/weather.js';
3
- const geo = await geocodePlace('大理市', {
4
- count: 10
8
+ const jsonResponse = (body, status = 200) => new Response(JSON.stringify(body), {
9
+ status, headers: { 'content-type': 'application/json' },
5
10
  });
6
- assert.equal(geo.ok, true, `geocode ok: ${geo.error ?? ''}`);
7
- assert.ok(geo.results.length > 0, '应有结果');
8
- const dali = geo.results.find((r)=>(r.admin1 ?? '').includes('云南')) ?? geo.results[0];
9
- assert.ok(Math.abs(dali.latitude - 25.6) < 0.5, `纬度应≈25.6,实际 ${dali.latitude}(${dali.name},${dali.admin1})`);
10
- assert.match(geo.evidence, /open-meteo-geo@2/, '证据链带时间戳');
11
- console.log(`1. geocode 大理市 → ${dali.latitude},${dali.longitude} (${dali.name},${dali.admin1}) OK`);
12
- const fc = await getForecast({
13
- latitude: dali.latitude,
14
- longitude: dali.longitude
15
- }, {
16
- days: 7
11
+ const geoDali = { results: [{ name: '大理市', latitude: 25.6, longitude: 100.2, country: '中国', admin1: '云南省', population: 600000, feature_code: 'PPLA' }] };
12
+ const geoPhuket = [{ name: 'Phuket', lat: '7.88', lon: '98.39', address: { country: '泰国', province: '普吉府' } }];
13
+ const forecastBody = (days) => ({
14
+ timezone: 'Asia/Shanghai',
15
+ daily: {
16
+ time: Array.from({ length: days }, (_, i) => `2026-09-${String(i + 1).padStart(2, '0')}`),
17
+ temperature_2m_max: Array.from({ length: days }, () => 30),
18
+ temperature_2m_min: Array.from({ length: days }, () => 21),
19
+ precipitation_probability_max: Array.from({ length: days }, () => 60),
20
+ weathercode: Array.from({ length: days }, () => 61),
21
+ },
17
22
  });
18
- assert.equal(fc.ok, true, `forecast ok: ${fc.error ?? ''}`);
19
- assert.equal(fc.daily.length, 7, '7 天');
20
- assert.ok(fc.daily[0].tempMaxC > -50 && fc.daily[0].tempMaxC < 60, '温度合理范围');
21
- assert.match(fc.evidence, /\[实时API:open-meteo@2/, '证据链');
22
- assert.ok(fc.latencyMs < 5000, `延迟应 <5s,实际 ${fc.latencyMs}ms`);
23
- console.log(`2. forecast 大理 7 天:${fc.daily[0].tempMinC}–${fc.daily[0].tempMaxC}°C ${wmoLabel(fc.daily[0].weatherCode)} OK`);
24
- const cl = await getClimate({
25
- latitude: dali.latitude,
26
- longitude: dali.longitude
27
- }, 8);
28
- assert.equal(cl.ok, true, `climate ok: ${cl.error ?? ''}`);
29
- assert.ok(cl.daily.length >= 28, '8 月应有 ≥28 天');
30
- assert.match(cl.evidence, /open-meteo-climate@/, '气候证据链');
31
- console.log(`3. climate 大理去年 8 月:${cl.daily.length} 天 OK`);
32
- const bad = await getForecast({
33
- latitude: 999,
34
- longitude: 0
35
- }, {
36
- timeoutMs: 10_000
23
+ const climateBodyFor = (year, month) => {
24
+ const days = new Date(Date.UTC(year, month, 0)).getUTCDate();
25
+ const monthText = String(month).padStart(2, '0');
26
+ return { daily: { time: Array.from({ length: days }, (_, i) => `${year}-${monthText}-${String(i + 1).padStart(2, '0')}`), temperature_2m_max: Array(days).fill(29), temperature_2m_min: Array(days).fill(21), weathercode: Array(days).fill(2) } };
27
+ };
28
+ const climateBody = climateBodyFor(2025, 8);
29
+ const mockFetch = (handler) => async (input, init) => jsonResponse(await handler(String(input), init?.signal ?? new AbortController().signal));
30
+ // 1. 地理编码主源命中 + schema/别名正常路径
31
+ const geo = await geocodePlace('大理市', { count: 10, fetchImpl: mockFetch(() => geoDali) });
32
+ assert.equal(geo.ok, true);
33
+ assert.equal(geo.via, 'open-meteo');
34
+ assert.ok(Math.abs(geo.results[0].latitude - 25.6) < 0.5);
35
+ assert.match(geo.evidence, /open-meteo-geo@2/);
36
+ console.log('1. geocode 主源命中 + alias/schema OK');
37
+ // 2. 零结果进入 Nominatim 兜底,并保持正确国家/坐标
38
+ const seenRequests = [];
39
+ const phuket = await geocodePlace('普吉岛', { count: 3, fetchImpl: async (input, init) => {
40
+ seenRequests.push({ url: String(input), headers: new Headers(init?.headers) });
41
+ return jsonResponse(String(input).includes('nominatim') ? geoPhuket : { results: [] });
42
+ } });
43
+ assert.equal(phuket.ok, true);
44
+ assert.equal(phuket.via, 'nominatim');
45
+ assert.match(phuket.results[0].country ?? '', /泰国/);
46
+ assert.ok(Math.abs(phuket.results[0].latitude - 8) < 0.5);
47
+ assert.match(seenRequests[0].url, /name=%E6%99%AE%E5%90%89%E5%B2%9B.*count=3.*language=zh/);
48
+ assert.match(seenRequests[1].url, /q=%E6%99%AE%E5%90%89%E5%B2%9B.*limit=3.*accept-language=zh/);
49
+ assert.match(seenRequests[1].headers.get('user-agent') ?? '', /gotry-travel-agent/);
50
+ const badAddress = await geocodePlace('坏地址', { fetchImpl: mockFetch((url) => url.includes('nominatim') ? [{ name: 'X', lat: '1', lon: '2', address: { country: {}, province: 7 } }] : { results: [] }) });
51
+ assert.equal(badAddress.ok, true);
52
+ assert.equal('country' in badAddress.results[0], false);
53
+ assert.equal('admin1' in badAddress.results[0], false);
54
+ console.log('2. geocode 兜底链 + 地名 alias OK');
55
+ // 3. 预报正常 schema
56
+ const fc = await getForecast({ latitude: 25.6, longitude: 100.2 }, { days: 7, fetchImpl: mockFetch(() => forecastBody(7)) });
57
+ assert.equal(fc.ok, true);
58
+ assert.equal(fc.daily?.length, 7);
59
+ assert.equal(fc.daily?.[0]?.weatherCode, 61);
60
+ assert.ok(fc.latencyMs < 1000);
61
+ console.log('3. forecast schema/行为 OK');
62
+ // 4. 慢 forecast 必须在受控 timeout 内 graceful fallback(不抛错)
63
+ const slow = mockFetch((_url, signal) => new Promise((_, reject) => {
64
+ const abort = () => reject(new Error('This operation was aborted'));
65
+ if (signal.aborted)
66
+ abort();
67
+ else
68
+ signal.addEventListener('abort', abort, { once: true });
69
+ setTimeout(() => reject(new Error('late response')), 250);
70
+ }));
71
+ const slowStarted = Date.now();
72
+ const slowFc = await getForecast({ latitude: 25.6, longitude: 100.2 }, { timeoutMs: 25, fetchImpl: slow });
73
+ assert.equal(slowFc.ok, false);
74
+ assert.ok(Date.now() - slowStarted < 150, 'slow forecast must respect timeout');
75
+ assert.match(slowFc.evidence, /error/);
76
+ const never = mockFetch(() => new Promise(() => undefined));
77
+ const neverStarted = Date.now();
78
+ const neverFc = await getForecast({ latitude: 25.6, longitude: 100.2 }, { timeoutMs: 20, fetchImpl: never });
79
+ assert.equal(neverFc.ok, false);
80
+ assert.ok(Date.now() - neverStarted < 150, 'hard deadline must cover a fetch that ignores abort');
81
+ const neverBody = await getForecast({ latitude: 25.6, longitude: 100.2 }, { timeoutMs: 20, fetchImpl: async () => ({ ok: true, json: () => new Promise(() => undefined) }) });
82
+ assert.equal(neverBody.ok, false, 'hard deadline must cover a body decoder that never settles');
83
+ let lateResolve;
84
+ const lateResolveResult = await getForecast({ latitude: 25.6, longitude: 100.2 }, {
85
+ timeoutMs: 10,
86
+ fetchImpl: async () => ({ ok: true, json: () => new Promise(resolve => { lateResolve = resolve; }) }),
37
87
  });
38
- assert.equal(bad.ok, false, '非法输入应返回 ok=false');
39
- assert.ok(bad.evidence.includes('error'), '降级证据标注');
40
- console.log(`4. 降级 ok=false 不抛错 OK`);
88
+ assert.equal(lateResolveResult.ok, false);
89
+ lateResolve?.(forecastBody(7));
90
+ let lateReject;
91
+ const lateRejectResult = await getForecast({ latitude: 25.6, longitude: 100.2 }, {
92
+ timeoutMs: 10,
93
+ fetchImpl: async () => ({ ok: true, json: () => new Promise((_, reject) => { lateReject = reject; }) }),
94
+ });
95
+ assert.equal(lateRejectResult.ok, false);
96
+ lateReject?.(Object.create(null));
97
+ await new Promise(resolve => setImmediate(resolve));
98
+ console.log('4. slow forecast timeout + graceful fallback OK');
99
+ // 5. 双源 fallback 的总预算:主源耗尽预算时不再发第二个请求。
100
+ let calls = 0;
101
+ const slowBoth = mockFetch((_url, signal) => {
102
+ calls += 1;
103
+ return new Promise((_, reject) => {
104
+ const abort = () => reject(new Error('network timeout'));
105
+ if (signal.aborted)
106
+ abort();
107
+ else
108
+ signal.addEventListener('abort', abort, { once: true });
109
+ });
110
+ });
111
+ const chainStarted = Date.now();
112
+ const chain = await geocodePlace('不存在的地方', { timeoutMs: 35, fetchImpl: slowBoth });
113
+ assert.equal(chain.ok, false);
114
+ assert.equal(calls, 1, 'an exhausted primary budget must not start fallback');
115
+ assert.ok(Date.now() - chainStarted < 150, 'fallback chain must share one total budget');
116
+ assert.match(chain.evidence, /open-meteo-geo@error/);
117
+ let fastPrimaryCalls = 0;
118
+ let fastFallbackCalls = 0;
119
+ const fallbackAfterFastFailure = await geocodePlace('不存在的地方', { timeoutMs: 100, fetchImpl: async (input, init) => {
120
+ if (String(input).includes('nominatim')) {
121
+ fastFallbackCalls += 1;
122
+ return jsonResponse([]);
123
+ }
124
+ fastPrimaryCalls += 1;
125
+ throw new Error('ECONNRESET');
126
+ } });
127
+ assert.equal(fallbackAfterFastFailure.ok, false);
128
+ assert.equal(fastPrimaryCalls, 1, 'primary should be attempted once');
129
+ assert.equal(fastFallbackCalls, 1, 'a fast primary failure should still attempt fallback once');
130
+ const network = await getForecast({ latitude: 25.6, longitude: 100.2 }, { fetchImpl: mockFetch(() => { throw new Error('ECONNRESET'); }) });
131
+ assert.equal(network.ok, false);
132
+ assert.match(network.error ?? '', /ECONNRESET/);
133
+ const httpError = await getForecast({ latitude: 25.6, longitude: 100.2 }, { fetchImpl: async () => jsonResponse({}, 503) });
134
+ assert.equal(httpError.ok, false);
135
+ assert.match(httpError.error ?? '', /HTTP 503/);
136
+ const malformedGeo = await geocodePlace('坏响应', { timeoutMs: 100, fetchImpl: mockFetch((url) => url.includes('nominatim') ? {} : { results: {} }) });
137
+ assert.equal(malformedGeo.ok, false);
138
+ console.log('5. slow geocode + strict fallback budget + network/HTTP errors OK');
139
+ // 6. climate schema、malformed schema、WMO mapping
140
+ const cl = await getClimate({ latitude: 25.6, longitude: 100.2 }, 8, { year: 2025, fetchImpl: mockFetch(() => climateBody) });
141
+ assert.equal(cl.ok, true);
142
+ assert.equal(cl.daily?.length, 31);
143
+ const malformed = await getForecast({ latitude: 25.6, longitude: 100.2 }, { days: 7, fetchImpl: mockFetch(() => ({ daily: { time: [] } })) });
144
+ assert.equal(malformed.ok, false);
145
+ const malformedElement = await getForecast({ latitude: 25.6, longitude: 100.2 }, { days: 7, fetchImpl: mockFetch(() => ({ daily: { ...forecastBody(7).daily, temperature_2m_max: [30, 'bad', 30, 30, 30, 30, 30] } })) });
146
+ assert.equal(malformedElement.ok, false);
147
+ const wrongPrecip = await getForecast({ latitude: 25.6, longitude: 100.2 }, { days: 7, fetchImpl: mockFetch(() => ({ daily: { ...forecastBody(7).daily, precipitation_probability_max: 'bad' } })) });
148
+ assert.equal(wrongPrecip.ok, false);
149
+ const wrongPrecipObject = await getForecast({ latitude: 25.6, longitude: 100.2 }, { days: 7, fetchImpl: mockFetch(() => ({ daily: { ...forecastBody(7).daily, precipitation_probability_max: {} } })) });
150
+ assert.equal(wrongPrecipObject.ok, false);
151
+ const absentPrecip = { daily: { ...forecastBody(7).daily } };
152
+ delete absentPrecip.daily.precipitation_probability_max;
153
+ const forecastAbsentPrecip = await getForecast({ latitude: 25.6, longitude: 100.2 }, { fetchImpl: mockFetch(() => absentPrecip) });
154
+ assert.equal(forecastAbsentPrecip.ok, true);
155
+ assert.equal(forecastAbsentPrecip.daily?.[0]?.precipProbMaxPct, null);
156
+ const forecastNullPrecip = await getForecast({ latitude: 25.6, longitude: 100.2 }, { fetchImpl: mockFetch(() => ({ daily: { ...forecastBody(7).daily, precipitation_probability_max: null } })) });
157
+ assert.equal(forecastNullPrecip.ok, true);
158
+ assert.equal(forecastNullPrecip.daily?.[0]?.precipProbMaxPct, null);
159
+ const forecastValidPrecip = await getForecast({ latitude: 25.6, longitude: 100.2 }, { fetchImpl: mockFetch(() => forecastBody(7)) });
160
+ assert.equal(forecastValidPrecip.ok, true);
161
+ assert.equal(forecastValidPrecip.daily?.[0]?.precipProbMaxPct, 60);
162
+ const nullClimate = await getClimate({ latitude: 25.6, longitude: 100.2 }, 8, { fetchImpl: mockFetch(() => null) });
163
+ assert.equal(nullClimate.ok, false);
164
+ const slowClimate = await getClimate({ latitude: 25.6, longitude: 100.2 }, 8, { timeoutMs: 20, fetchImpl: slow });
165
+ assert.equal(slowClimate.ok, false);
166
+ const shortClimate = await getClimate({ latitude: 25.6, longitude: 100.2 }, 8, { fetchImpl: mockFetch(() => ({ daily: { ...climateBody.daily, time: ['2025-08-01'], temperature_2m_max: [29], temperature_2m_min: [21], weathercode: [2] } })) });
167
+ assert.equal(shortClimate.ok, false);
168
+ for (const badDays of [0, Number.NaN, 2.5]) {
169
+ let fetches = 0;
170
+ const invalid = await getForecast({ latitude: 25.6, longitude: 100.2 }, { days: badDays, fetchImpl: mockFetch(() => { fetches += 1; return forecastBody(7); }) });
171
+ assert.equal(invalid.ok, false);
172
+ assert.equal(fetches, 0);
173
+ }
174
+ for (const badTimeout of [0, -1, Number.NaN, Number.POSITIVE_INFINITY, 2.5]) {
175
+ let forecastFetches = 0;
176
+ const invalidForecast = await getForecast({ latitude: 25.6, longitude: 100.2 }, { timeoutMs: badTimeout, fetchImpl: mockFetch(() => { forecastFetches += 1; return forecastBody(7); }) });
177
+ assert.equal(invalidForecast.ok, false);
178
+ assert.equal(forecastFetches, 0, `invalid forecast timeout ${String(badTimeout)} must not fetch`);
179
+ let climateFetches = 0;
180
+ const invalidClimateTimeout = await getClimate({ latitude: 25.6, longitude: 100.2 }, 8, { timeoutMs: badTimeout, fetchImpl: mockFetch(() => { climateFetches += 1; return climateBody; }) });
181
+ assert.equal(invalidClimateTimeout.ok, false);
182
+ assert.equal(climateFetches, 0, `invalid climate timeout ${String(badTimeout)} must not fetch`);
183
+ let geoFetches = 0;
184
+ const invalidGeoTimeout = await geocodePlace('大理市', { timeoutMs: badTimeout, fetchImpl: mockFetch(() => { geoFetches += 1; return geoDali; }) });
185
+ assert.equal(invalidGeoTimeout.ok, false);
186
+ assert.equal(geoFetches, 0, `invalid geocode timeout ${String(badTimeout)} must not fetch`);
187
+ }
188
+ let cappedDays = '';
189
+ const capped = await getForecast({ latitude: 25.6, longitude: 100.2 }, { days: 99, fetchImpl: async (input) => { cappedDays = String(input); return jsonResponse(forecastBody(16)); } });
190
+ assert.equal(capped.ok, true);
191
+ assert.match(cappedDays, /forecast_days=16/);
192
+ const wrongMonth = await getClimate({ latitude: 25.6, longitude: 100.2 }, 9, { year: 2025, fetchImpl: mockFetch(() => climateBody) });
193
+ assert.equal(wrongMonth.ok, false);
194
+ const leapBody = { daily: { time: Array.from({ length: 29 }, (_, i) => `2024-02-${String(i + 1).padStart(2, '0')}`), temperature_2m_max: Array(29).fill(10), temperature_2m_min: Array(29).fill(1), weathercode: Array(29).fill(0) } };
195
+ const leap = await getClimate({ latitude: 25.6, longitude: 100.2 }, 2, { year: 2024, fetchImpl: mockFetch(() => leapBody) });
196
+ assert.equal(leap.ok, true);
197
+ const historicalMinimum = await getClimate({ latitude: 25.6, longitude: 100.2 }, 8, { year: 1940, fetchImpl: mockFetch(() => climateBodyFor(1940, 8)) });
198
+ assert.equal(historicalMinimum.ok, true);
199
+ const currentYear = new Date().getUTCFullYear();
200
+ const currentClimate = await getClimate({ latitude: 25.6, longitude: 100.2 }, 8, { year: currentYear, fetchImpl: mockFetch(() => climateBodyFor(currentYear, 8)) });
201
+ assert.equal(currentClimate.ok, true);
202
+ for (const badYear of [1939, 2025.5, currentYear + 1]) {
203
+ let yearFetches = 0;
204
+ const invalidYear = await getClimate({ latitude: 25.6, longitude: 100.2 }, 8, { year: badYear, fetchImpl: mockFetch(() => { yearFetches += 1; return climateBody; }) });
205
+ assert.equal(invalidYear.ok, false);
206
+ assert.equal(yearFetches, 0, `invalid climate year ${String(badYear)} must not fetch`);
207
+ }
208
+ const duplicateDayBody = climateBodyFor(2025, 8);
209
+ duplicateDayBody.daily.time[1] = duplicateDayBody.daily.time[0];
210
+ const duplicateDay = await getClimate({ latitude: 25.6, longitude: 100.2 }, 8, { year: 2025, fetchImpl: mockFetch(() => duplicateDayBody) });
211
+ assert.equal(duplicateDay.ok, false);
212
+ const day99Body = climateBodyFor(2025, 8);
213
+ day99Body.daily.time[0] = '2025-08-99';
214
+ const day99 = await getClimate({ latitude: 25.6, longitude: 100.2 }, 8, { year: 2025, fetchImpl: mockFetch(() => day99Body) });
215
+ assert.equal(day99.ok, false);
216
+ let invalidClimateFetches = 0;
217
+ const invalidClimate = await getClimate({ latitude: 25.6, longitude: 100.2 }, 13, { year: 2025, fetchImpl: mockFetch(() => { invalidClimateFetches += 1; return climateBody; }) });
218
+ assert.equal(invalidClimate.ok, false);
219
+ assert.equal(invalidClimateFetches, 0);
220
+ let geoZeroFetches = 0;
221
+ const geoZero = await geocodePlace('大理市', { timeoutMs: 0, fetchImpl: mockFetch(() => { geoZeroFetches += 1; return geoDali; }) });
222
+ assert.equal(geoZero.ok, false);
223
+ assert.equal(geoZeroFetches, 0);
224
+ let climateZeroFetches = 0;
225
+ const climateZero = await getClimate({ latitude: 25.6, longitude: 100.2 }, 8, { timeoutMs: 0, fetchImpl: mockFetch(() => { climateZeroFetches += 1; return climateBody; }) });
226
+ assert.equal(climateZero.ok, false);
227
+ assert.equal(climateZeroFetches, 0);
228
+ let primaryResolutionFetches = 0;
229
+ const originalPerformanceNow = performance.now;
230
+ Object.defineProperty(performance, 'now', { configurable: true, value: (() => {
231
+ const values = [100, 104.5, 104.5];
232
+ return () => values.shift() ?? 104.5;
233
+ })() });
234
+ try {
235
+ const primaryResolution = await geocodePlace('剩余不足一毫秒', { timeoutMs: 5, fetchImpl: mockFetch(() => { primaryResolutionFetches += 1; return geoDali; }) });
236
+ assert.equal(primaryResolution.ok, false);
237
+ assert.equal(primaryResolutionFetches, 0, 'sub-millisecond primary budget must not fetch');
238
+ }
239
+ finally {
240
+ Object.defineProperty(performance, 'now', { configurable: true, value: originalPerformanceNow });
241
+ }
242
+ let primaryBudgetFetches = 0;
243
+ let fallbackResolutionFetches = 0;
244
+ Object.defineProperty(performance, 'now', { configurable: true, value: (() => {
245
+ const values = [200, 200, 205];
246
+ return () => values.shift() ?? 205;
247
+ })() });
248
+ try {
249
+ const fallbackResolution = await geocodePlace('兜底剩余不足一毫秒', { timeoutMs: 5, fetchImpl: async (input) => {
250
+ if (String(input).includes('nominatim')) {
251
+ fallbackResolutionFetches += 1;
252
+ return jsonResponse([]);
253
+ }
254
+ primaryBudgetFetches += 1;
255
+ return jsonResponse({ results: [] });
256
+ } });
257
+ assert.equal(fallbackResolution.ok, false);
258
+ assert.equal(primaryBudgetFetches, 1);
259
+ assert.equal(fallbackResolutionFetches, 0, 'sub-millisecond fallback budget must not fetch');
260
+ }
261
+ finally {
262
+ Object.defineProperty(performance, 'now', { configurable: true, value: originalPerformanceNow });
263
+ }
264
+ const nonErrorRejection = await getForecast({ latitude: 25.6, longitude: 100.2 }, { fetchImpl: async () => { throw Object.create(null); } });
265
+ assert.equal(nonErrorRejection.ok, false);
266
+ assert.equal(nonErrorRejection.error, 'request failed');
267
+ const symbolRejection = await getForecast({ latitude: 25.6, longitude: 100.2 }, { fetchImpl: async () => { throw Symbol('untrusted'); } });
268
+ assert.equal(symbolRejection.ok, false);
269
+ assert.equal(symbolRejection.error, 'request failed');
270
+ const originalDateNow = Date.now;
271
+ Date.now = () => 0;
272
+ try {
273
+ const rollback = await getForecast({ latitude: 25.6, longitude: 100.2 }, { timeoutMs: 20, fetchImpl: mockFetch(() => forecastBody(7)) });
274
+ assert.equal(rollback.ok, true, 'wall-clock rollback must not expand or break monotonic budget');
275
+ }
276
+ finally {
277
+ Date.now = originalDateNow;
278
+ }
41
279
  assert.equal(wmoLabel(0), '晴');
42
280
  assert.equal(wmoLabel(95), '雷暴');
43
- assert.match(wmoLabel(999), /天气码999/, '未知码回退');
44
- console.log('5. WMO 码映射 OK');
45
- console.log('\nWEATHER TESTS: 5/5 OK(Open-Meteo 真实 API,免费无 key)');
46
-
47
-
48
- //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/weather-tests.ts
281
+ assert.match(wmoLabel(999), /天气码999/);
282
+ console.log('6. climate/schema guard/WMO OK');
283
+ console.log('\nWEATHER TESTS: deterministic 6/6 OK (live APIs are peripheral smoke only)');
@@ -0,0 +1,54 @@
1
+ /**
2
+ * 愿望池通道否证测试(外部事件接缝第 2 段,issue #82 兼容方向;全离线纯函数):
3
+ * 1. conditions.channels 命名通道处于 down → 否证成行条件,即使 days/budget/month 全命中
4
+ * 2. 通道全健康(channelDown 空集)→ 不否证不加分,score/hits 与旧口径一致
5
+ * 3. ctx.channelDown 缺席(旧调用方)→ 行为与旧版完全一致(零破坏)
6
+ * 4. 畸形 channels(非数组/非字符串元素)→ 忽略,不误杀
7
+ * 5. pickNudgeWish 集成:最高分愿望被否证 → 次高分顶上(0..1 纪律不变)
8
+ *
9
+ * 运行: cd ts && npx tsx scripts/wish-channel-gate-tests.ts
10
+ */
11
+ import assert from 'node:assert/strict';
12
+ import { scoreWishMatch, pickNudgeWish } from '../src/wish-pool.js';
13
+ const base = {
14
+ wish_id: 'w-dubai',
15
+ name: '迪拜',
16
+ added_at: '2026-09-01T00:00:00Z',
17
+ conditions: { days: 5, budget_cny: 8000, best_months: [10, 11], channels: ['hbcli-hotel'] },
18
+ };
19
+ // ── 1. 命名通道 down → 否证 ─────────────────────────────────────────────────
20
+ const vetoed = scoreWishMatch(base, {
21
+ days: 10, budgetCny: 20000, month: 10,
22
+ channelDown: new Set(['hbcli-hotel']),
23
+ });
24
+ assert.equal(vetoed, null, 'days/budget/month 全命中但依赖通道 down → 否证返回 null');
25
+ // ── 2. 通道健康 → 不否证不加分(评分口径不变,/3)────────────────────────────
26
+ const healthy = scoreWishMatch(base, {
27
+ days: 10, budgetCny: 20000, month: 10,
28
+ channelDown: new Set(),
29
+ });
30
+ assert.ok(healthy && healthy.score === 3 && healthy.hits.join('+') === 'days≥5+budget≥8000+month=10', '健康时评分与旧口径一致(score=3)');
31
+ // ── 3. channelDown 缺席(旧调用方)→ 零破坏 ──────────────────────────────────
32
+ const legacy = scoreWishMatch(base, { days: 10, budgetCny: 20000, month: 10 });
33
+ assert.ok(legacy && legacy.score === 3, '不传 channelDown 的旧调用行为不变');
34
+ // ── 4. 畸形 channels 忽略 ────────────────────────────────────────────────────
35
+ const malformed = scoreWishMatch({
36
+ ...base,
37
+ conditions: { days: 5, channels: 'hbcli-hotel' },
38
+ }, { days: 10, month: 10, channelDown: new Set(['hbcli-hotel']) });
39
+ assert.ok(malformed && malformed.score === 1, '非数组 channels 被忽略,不误杀');
40
+ const mixed = scoreWishMatch({
41
+ ...base,
42
+ conditions: { days: 5, channels: [42, null, 'hbcli-hotel'] },
43
+ }, { days: 10, month: 10, channelDown: new Set(['hbcli-hotel']) });
44
+ assert.equal(mixed, null, '数组内字符串元素仍参与否证(非字符串忽略)');
45
+ // ── 5. pickNudgeWish 集成:最高分被否证 → 次高顶上 ───────────────────────────
46
+ const pool = [
47
+ { ...base, added_at: '2026-09-01T00:00:00Z' },
48
+ { wish_id: 'w-osaka', name: '大阪', added_at: '2026-09-02T00:00:00Z', conditions: { days: 5, best_months: [10] } },
49
+ ];
50
+ const picked = pickNudgeWish(pool, { days: 10, month: 10, channelDown: new Set(['hbcli-hotel']) });
51
+ assert.ok(picked && picked.wishId === 'w-osaka', `被否证的 w-dubai 退出,次高 w-osaka 顶上,实际 ${picked?.wishId}`);
52
+ const none = pickNudgeWish([base], { days: 10, month: 10, channelDown: new Set(['hbcli-hotel']) });
53
+ assert.equal(none, null, '全部被否证 → null(不硬推纪律保持)');
54
+ console.log('WISH CHANNEL GATE TESTS: 5/5 OK(否证 / 健康不加分 / 旧调用零破坏 / 畸形忽略 / 0..1 集成)');