@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
@@ -1,93 +1,45 @@
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 jsonResponse = (body, status = 200)=>new Response(JSON.stringify(body), {
4
- status,
5
- headers: {
6
- 'content-type': 'application/json'
7
- }
8
- });
9
- const geoDali = {
10
- results: [
11
- {
12
- name: '大理市',
13
- latitude: 25.6,
14
- longitude: 100.2,
15
- country: '中国',
16
- admin1: '云南省',
17
- population: 600000,
18
- feature_code: 'PPLA'
19
- }
20
- ]
21
- };
22
- const geoPhuket = [
23
- {
24
- name: 'Phuket',
25
- lat: '7.88',
26
- lon: '98.39',
27
- address: {
28
- country: '泰国',
29
- province: '普吉府'
30
- }
31
- }
32
- ];
33
- const forecastBody = (days)=>({
34
- timezone: 'Asia/Shanghai',
35
- daily: {
36
- time: Array.from({
37
- length: days
38
- }, (_, i)=>`2026-09-${String(i + 1).padStart(2, '0')}`),
39
- temperature_2m_max: Array.from({
40
- length: days
41
- }, ()=>30),
42
- temperature_2m_min: Array.from({
43
- length: days
44
- }, ()=>21),
45
- precipitation_probability_max: Array.from({
46
- length: days
47
- }, ()=>60),
48
- weathercode: Array.from({
49
- length: days
50
- }, ()=>61)
51
- }
52
- });
53
- const climateBodyFor = (year, month)=>{
8
+ const jsonResponse = (body, status = 200) => new Response(JSON.stringify(body), {
9
+ status, headers: { 'content-type': 'application/json' },
10
+ });
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
+ },
22
+ });
23
+ const climateBodyFor = (year, month) => {
54
24
  const days = new Date(Date.UTC(year, month, 0)).getUTCDate();
55
25
  const monthText = String(month).padStart(2, '0');
56
- return {
57
- daily: {
58
- time: Array.from({
59
- length: days
60
- }, (_, i)=>`${year}-${monthText}-${String(i + 1).padStart(2, '0')}`),
61
- temperature_2m_max: Array(days).fill(29),
62
- temperature_2m_min: Array(days).fill(21),
63
- weathercode: Array(days).fill(2)
64
- }
65
- };
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) } };
66
27
  };
67
28
  const climateBody = climateBodyFor(2025, 8);
68
- const mockFetch = (handler)=>async (input, init)=>jsonResponse(await handler(String(input), init?.signal ?? new AbortController().signal));
69
- const geo = await geocodePlace('大理市', {
70
- count: 10,
71
- fetchImpl: mockFetch(()=>geoDali)
72
- });
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) });
73
32
  assert.equal(geo.ok, true);
74
33
  assert.equal(geo.via, 'open-meteo');
75
34
  assert.ok(Math.abs(geo.results[0].latitude - 25.6) < 0.5);
76
35
  assert.match(geo.evidence, /open-meteo-geo@2/);
77
36
  console.log('1. geocode 主源命中 + alias/schema OK');
37
+ // 2. 零结果进入 Nominatim 兜底,并保持正确国家/坐标
78
38
  const seenRequests = [];
79
- const phuket = await geocodePlace('普吉岛', {
80
- count: 3,
81
- fetchImpl: async (input, init)=>{
82
- seenRequests.push({
83
- url: String(input),
84
- headers: new Headers(init?.headers)
85
- });
86
- return jsonResponse(String(input).includes('nominatim') ? geoPhuket : {
87
- results: []
88
- });
89
- }
90
- });
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
+ } });
91
43
  assert.equal(phuket.ok, true);
92
44
  assert.equal(phuket.via, 'nominatim');
93
45
  assert.match(phuket.results[0].country ?? '', /泰国/);
@@ -95,604 +47,233 @@ assert.ok(Math.abs(phuket.results[0].latitude - 8) < 0.5);
95
47
  assert.match(seenRequests[0].url, /name=%E6%99%AE%E5%90%89%E5%B2%9B.*count=3.*language=zh/);
96
48
  assert.match(seenRequests[1].url, /q=%E6%99%AE%E5%90%89%E5%B2%9B.*limit=3.*accept-language=zh/);
97
49
  assert.match(seenRequests[1].headers.get('user-agent') ?? '', /gotry-travel-agent/);
98
- const badAddress = await geocodePlace('坏地址', {
99
- fetchImpl: mockFetch((url)=>url.includes('nominatim') ? [
100
- {
101
- name: 'X',
102
- lat: '1',
103
- lon: '2',
104
- address: {
105
- country: {},
106
- province: 7
107
- }
108
- }
109
- ] : {
110
- results: []
111
- })
112
- });
50
+ const badAddress = await geocodePlace('坏地址', { fetchImpl: mockFetch((url) => url.includes('nominatim') ? [{ name: 'X', lat: '1', lon: '2', address: { country: {}, province: 7 } }] : { results: [] }) });
113
51
  assert.equal(badAddress.ok, true);
114
52
  assert.equal('country' in badAddress.results[0], false);
115
53
  assert.equal('admin1' in badAddress.results[0], false);
116
54
  console.log('2. geocode 兜底链 + 地名 alias OK');
117
- const fc = await getForecast({
118
- latitude: 25.6,
119
- longitude: 100.2
120
- }, {
121
- days: 7,
122
- fetchImpl: mockFetch(()=>forecastBody(7))
123
- });
55
+ // 3. 预报正常 schema
56
+ const fc = await getForecast({ latitude: 25.6, longitude: 100.2 }, { days: 7, fetchImpl: mockFetch(() => forecastBody(7)) });
124
57
  assert.equal(fc.ok, true);
125
58
  assert.equal(fc.daily?.length, 7);
126
59
  assert.equal(fc.daily?.[0]?.weatherCode, 61);
127
60
  assert.ok(fc.latencyMs < 1000);
128
61
  console.log('3. forecast schema/行为 OK');
129
- const slow = mockFetch((_url, signal)=>new Promise((_, reject)=>{
130
- const abort = ()=>reject(new Error('This operation was aborted'));
131
- if (signal.aborted) abort();
132
- else signal.addEventListener('abort', abort, {
133
- once: true
134
- });
135
- setTimeout(()=>reject(new Error('late response')), 250);
136
- }));
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
+ }));
137
71
  const slowStarted = Date.now();
138
- const slowFc = await getForecast({
139
- latitude: 25.6,
140
- longitude: 100.2
141
- }, {
142
- timeoutMs: 25,
143
- fetchImpl: slow
144
- });
72
+ const slowFc = await getForecast({ latitude: 25.6, longitude: 100.2 }, { timeoutMs: 25, fetchImpl: slow });
145
73
  assert.equal(slowFc.ok, false);
146
74
  assert.ok(Date.now() - slowStarted < 150, 'slow forecast must respect timeout');
147
75
  assert.match(slowFc.evidence, /error/);
148
- const never = mockFetch(()=>new Promise(()=>undefined));
76
+ const never = mockFetch(() => new Promise(() => undefined));
149
77
  const neverStarted = Date.now();
150
- const neverFc = await getForecast({
151
- latitude: 25.6,
152
- longitude: 100.2
153
- }, {
154
- timeoutMs: 20,
155
- fetchImpl: never
156
- });
78
+ const neverFc = await getForecast({ latitude: 25.6, longitude: 100.2 }, { timeoutMs: 20, fetchImpl: never });
157
79
  assert.equal(neverFc.ok, false);
158
80
  assert.ok(Date.now() - neverStarted < 150, 'hard deadline must cover a fetch that ignores abort');
159
- const neverBody = await getForecast({
160
- latitude: 25.6,
161
- longitude: 100.2
162
- }, {
163
- timeoutMs: 20,
164
- fetchImpl: async ()=>({
165
- ok: true,
166
- json: ()=>new Promise(()=>undefined)
167
- })
168
- });
81
+ const neverBody = await getForecast({ latitude: 25.6, longitude: 100.2 }, { timeoutMs: 20, fetchImpl: async () => ({ ok: true, json: () => new Promise(() => undefined) }) });
169
82
  assert.equal(neverBody.ok, false, 'hard deadline must cover a body decoder that never settles');
170
83
  let lateResolve;
171
- const lateResolveResult = await getForecast({
172
- latitude: 25.6,
173
- longitude: 100.2
174
- }, {
84
+ const lateResolveResult = await getForecast({ latitude: 25.6, longitude: 100.2 }, {
175
85
  timeoutMs: 10,
176
- fetchImpl: async ()=>({
177
- ok: true,
178
- json: ()=>new Promise((resolve)=>{
179
- lateResolve = resolve;
180
- })
181
- })
86
+ fetchImpl: async () => ({ ok: true, json: () => new Promise(resolve => { lateResolve = resolve; }) }),
182
87
  });
183
88
  assert.equal(lateResolveResult.ok, false);
184
89
  lateResolve?.(forecastBody(7));
185
90
  let lateReject;
186
- const lateRejectResult = await getForecast({
187
- latitude: 25.6,
188
- longitude: 100.2
189
- }, {
91
+ const lateRejectResult = await getForecast({ latitude: 25.6, longitude: 100.2 }, {
190
92
  timeoutMs: 10,
191
- fetchImpl: async ()=>({
192
- ok: true,
193
- json: ()=>new Promise((_, reject)=>{
194
- lateReject = reject;
195
- })
196
- })
93
+ fetchImpl: async () => ({ ok: true, json: () => new Promise((_, reject) => { lateReject = reject; }) }),
197
94
  });
198
95
  assert.equal(lateRejectResult.ok, false);
199
96
  lateReject?.(Object.create(null));
200
- await new Promise((resolve)=>setImmediate(resolve));
97
+ await new Promise(resolve => setImmediate(resolve));
201
98
  console.log('4. slow forecast timeout + graceful fallback OK');
99
+ // 5. 双源 fallback 的总预算:主源耗尽预算时不再发第二个请求。
202
100
  let calls = 0;
203
- const slowBoth = mockFetch((_url, signal)=>{
101
+ const slowBoth = mockFetch((_url, signal) => {
204
102
  calls += 1;
205
- return new Promise((_, reject)=>{
206
- const abort = ()=>reject(new Error('network timeout'));
207
- if (signal.aborted) abort();
208
- else signal.addEventListener('abort', abort, {
209
- once: true
210
- });
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 });
211
109
  });
212
110
  });
213
111
  const chainStarted = Date.now();
214
- const chain = await geocodePlace('不存在的地方', {
215
- timeoutMs: 35,
216
- fetchImpl: slowBoth
217
- });
112
+ const chain = await geocodePlace('不存在的地方', { timeoutMs: 35, fetchImpl: slowBoth });
218
113
  assert.equal(chain.ok, false);
219
114
  assert.equal(calls, 1, 'an exhausted primary budget must not start fallback');
220
115
  assert.ok(Date.now() - chainStarted < 150, 'fallback chain must share one total budget');
221
116
  assert.match(chain.evidence, /open-meteo-geo@error/);
222
117
  let fastPrimaryCalls = 0;
223
118
  let fastFallbackCalls = 0;
224
- const fallbackAfterFastFailure = await geocodePlace('不存在的地方', {
225
- timeoutMs: 100,
226
- fetchImpl: async (input, init)=>{
119
+ const fallbackAfterFastFailure = await geocodePlace('不存在的地方', { timeoutMs: 100, fetchImpl: async (input, init) => {
227
120
  if (String(input).includes('nominatim')) {
228
121
  fastFallbackCalls += 1;
229
122
  return jsonResponse([]);
230
123
  }
231
124
  fastPrimaryCalls += 1;
232
125
  throw new Error('ECONNRESET');
233
- }
234
- });
126
+ } });
235
127
  assert.equal(fallbackAfterFastFailure.ok, false);
236
128
  assert.equal(fastPrimaryCalls, 1, 'primary should be attempted once');
237
129
  assert.equal(fastFallbackCalls, 1, 'a fast primary failure should still attempt fallback once');
238
- const network = await getForecast({
239
- latitude: 25.6,
240
- longitude: 100.2
241
- }, {
242
- fetchImpl: mockFetch(()=>{
243
- throw new Error('ECONNRESET');
244
- })
245
- });
130
+ const network = await getForecast({ latitude: 25.6, longitude: 100.2 }, { fetchImpl: mockFetch(() => { throw new Error('ECONNRESET'); }) });
246
131
  assert.equal(network.ok, false);
247
132
  assert.match(network.error ?? '', /ECONNRESET/);
248
- const httpError = await getForecast({
249
- latitude: 25.6,
250
- longitude: 100.2
251
- }, {
252
- fetchImpl: async ()=>jsonResponse({}, 503)
253
- });
133
+ const httpError = await getForecast({ latitude: 25.6, longitude: 100.2 }, { fetchImpl: async () => jsonResponse({}, 503) });
254
134
  assert.equal(httpError.ok, false);
255
135
  assert.match(httpError.error ?? '', /HTTP 503/);
256
- const malformedGeo = await geocodePlace('坏响应', {
257
- timeoutMs: 100,
258
- fetchImpl: mockFetch((url)=>url.includes('nominatim') ? {} : {
259
- results: {}
260
- })
261
- });
136
+ const malformedGeo = await geocodePlace('坏响应', { timeoutMs: 100, fetchImpl: mockFetch((url) => url.includes('nominatim') ? {} : { results: {} }) });
262
137
  assert.equal(malformedGeo.ok, false);
263
138
  console.log('5. slow geocode + strict fallback budget + network/HTTP errors OK');
264
- const cl = await getClimate({
265
- latitude: 25.6,
266
- longitude: 100.2
267
- }, 8, {
268
- year: 2025,
269
- fetchImpl: mockFetch(()=>climateBody)
270
- });
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) });
271
141
  assert.equal(cl.ok, true);
272
142
  assert.equal(cl.daily?.length, 31);
273
- const malformed = await getForecast({
274
- latitude: 25.6,
275
- longitude: 100.2
276
- }, {
277
- days: 7,
278
- fetchImpl: mockFetch(()=>({
279
- daily: {
280
- time: []
281
- }
282
- }))
283
- });
143
+ const malformed = await getForecast({ latitude: 25.6, longitude: 100.2 }, { days: 7, fetchImpl: mockFetch(() => ({ daily: { time: [] } })) });
284
144
  assert.equal(malformed.ok, false);
285
- const malformedElement = await getForecast({
286
- latitude: 25.6,
287
- longitude: 100.2
288
- }, {
289
- days: 7,
290
- fetchImpl: mockFetch(()=>({
291
- daily: {
292
- ...forecastBody(7).daily,
293
- temperature_2m_max: [
294
- 30,
295
- 'bad',
296
- 30,
297
- 30,
298
- 30,
299
- 30,
300
- 30
301
- ]
302
- }
303
- }))
304
- });
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] } })) });
305
146
  assert.equal(malformedElement.ok, false);
306
- const wrongPrecip = await getForecast({
307
- latitude: 25.6,
308
- longitude: 100.2
309
- }, {
310
- days: 7,
311
- fetchImpl: mockFetch(()=>({
312
- daily: {
313
- ...forecastBody(7).daily,
314
- precipitation_probability_max: 'bad'
315
- }
316
- }))
317
- });
147
+ const wrongPrecip = await getForecast({ latitude: 25.6, longitude: 100.2 }, { days: 7, fetchImpl: mockFetch(() => ({ daily: { ...forecastBody(7).daily, precipitation_probability_max: 'bad' } })) });
318
148
  assert.equal(wrongPrecip.ok, false);
319
- const wrongPrecipObject = await getForecast({
320
- latitude: 25.6,
321
- longitude: 100.2
322
- }, {
323
- days: 7,
324
- fetchImpl: mockFetch(()=>({
325
- daily: {
326
- ...forecastBody(7).daily,
327
- precipitation_probability_max: {}
328
- }
329
- }))
330
- });
149
+ const wrongPrecipObject = await getForecast({ latitude: 25.6, longitude: 100.2 }, { days: 7, fetchImpl: mockFetch(() => ({ daily: { ...forecastBody(7).daily, precipitation_probability_max: {} } })) });
331
150
  assert.equal(wrongPrecipObject.ok, false);
332
- const absentPrecip = {
333
- daily: {
334
- ...forecastBody(7).daily
335
- }
336
- };
151
+ const absentPrecip = { daily: { ...forecastBody(7).daily } };
337
152
  delete absentPrecip.daily.precipitation_probability_max;
338
- const forecastAbsentPrecip = await getForecast({
339
- latitude: 25.6,
340
- longitude: 100.2
341
- }, {
342
- fetchImpl: mockFetch(()=>absentPrecip)
343
- });
153
+ const forecastAbsentPrecip = await getForecast({ latitude: 25.6, longitude: 100.2 }, { fetchImpl: mockFetch(() => absentPrecip) });
344
154
  assert.equal(forecastAbsentPrecip.ok, true);
345
155
  assert.equal(forecastAbsentPrecip.daily?.[0]?.precipProbMaxPct, null);
346
- const forecastNullPrecip = await getForecast({
347
- latitude: 25.6,
348
- longitude: 100.2
349
- }, {
350
- fetchImpl: mockFetch(()=>({
351
- daily: {
352
- ...forecastBody(7).daily,
353
- precipitation_probability_max: null
354
- }
355
- }))
356
- });
156
+ const forecastNullPrecip = await getForecast({ latitude: 25.6, longitude: 100.2 }, { fetchImpl: mockFetch(() => ({ daily: { ...forecastBody(7).daily, precipitation_probability_max: null } })) });
357
157
  assert.equal(forecastNullPrecip.ok, true);
358
158
  assert.equal(forecastNullPrecip.daily?.[0]?.precipProbMaxPct, null);
359
- const forecastValidPrecip = await getForecast({
360
- latitude: 25.6,
361
- longitude: 100.2
362
- }, {
363
- fetchImpl: mockFetch(()=>forecastBody(7))
364
- });
159
+ const forecastValidPrecip = await getForecast({ latitude: 25.6, longitude: 100.2 }, { fetchImpl: mockFetch(() => forecastBody(7)) });
365
160
  assert.equal(forecastValidPrecip.ok, true);
366
161
  assert.equal(forecastValidPrecip.daily?.[0]?.precipProbMaxPct, 60);
367
- const nullClimate = await getClimate({
368
- latitude: 25.6,
369
- longitude: 100.2
370
- }, 8, {
371
- fetchImpl: mockFetch(()=>null)
372
- });
162
+ const nullClimate = await getClimate({ latitude: 25.6, longitude: 100.2 }, 8, { fetchImpl: mockFetch(() => null) });
373
163
  assert.equal(nullClimate.ok, false);
374
- const slowClimate = await getClimate({
375
- latitude: 25.6,
376
- longitude: 100.2
377
- }, 8, {
378
- timeoutMs: 20,
379
- fetchImpl: slow
380
- });
164
+ const slowClimate = await getClimate({ latitude: 25.6, longitude: 100.2 }, 8, { timeoutMs: 20, fetchImpl: slow });
381
165
  assert.equal(slowClimate.ok, false);
382
- const shortClimate = await getClimate({
383
- latitude: 25.6,
384
- longitude: 100.2
385
- }, 8, {
386
- fetchImpl: mockFetch(()=>({
387
- daily: {
388
- ...climateBody.daily,
389
- time: [
390
- '2025-08-01'
391
- ],
392
- temperature_2m_max: [
393
- 29
394
- ],
395
- temperature_2m_min: [
396
- 21
397
- ],
398
- weathercode: [
399
- 2
400
- ]
401
- }
402
- }))
403
- });
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] } })) });
404
167
  assert.equal(shortClimate.ok, false);
405
- for (const badDays of [
406
- 0,
407
- Number.NaN,
408
- 2.5
409
- ]){
168
+ for (const badDays of [0, Number.NaN, 2.5]) {
410
169
  let fetches = 0;
411
- const invalid = await getForecast({
412
- latitude: 25.6,
413
- longitude: 100.2
414
- }, {
415
- days: badDays,
416
- fetchImpl: mockFetch(()=>{
417
- fetches += 1;
418
- return forecastBody(7);
419
- })
420
- });
170
+ const invalid = await getForecast({ latitude: 25.6, longitude: 100.2 }, { days: badDays, fetchImpl: mockFetch(() => { fetches += 1; return forecastBody(7); }) });
421
171
  assert.equal(invalid.ok, false);
422
172
  assert.equal(fetches, 0);
423
173
  }
424
- for (const badTimeout of [
425
- 0,
426
- -1,
427
- Number.NaN,
428
- Number.POSITIVE_INFINITY,
429
- 2.5
430
- ]){
174
+ for (const badTimeout of [0, -1, Number.NaN, Number.POSITIVE_INFINITY, 2.5]) {
431
175
  let forecastFetches = 0;
432
- const invalidForecast = await getForecast({
433
- latitude: 25.6,
434
- longitude: 100.2
435
- }, {
436
- timeoutMs: badTimeout,
437
- fetchImpl: mockFetch(()=>{
438
- forecastFetches += 1;
439
- return forecastBody(7);
440
- })
441
- });
176
+ const invalidForecast = await getForecast({ latitude: 25.6, longitude: 100.2 }, { timeoutMs: badTimeout, fetchImpl: mockFetch(() => { forecastFetches += 1; return forecastBody(7); }) });
442
177
  assert.equal(invalidForecast.ok, false);
443
178
  assert.equal(forecastFetches, 0, `invalid forecast timeout ${String(badTimeout)} must not fetch`);
444
179
  let climateFetches = 0;
445
- const invalidClimateTimeout = await getClimate({
446
- latitude: 25.6,
447
- longitude: 100.2
448
- }, 8, {
449
- timeoutMs: badTimeout,
450
- fetchImpl: mockFetch(()=>{
451
- climateFetches += 1;
452
- return climateBody;
453
- })
454
- });
180
+ const invalidClimateTimeout = await getClimate({ latitude: 25.6, longitude: 100.2 }, 8, { timeoutMs: badTimeout, fetchImpl: mockFetch(() => { climateFetches += 1; return climateBody; }) });
455
181
  assert.equal(invalidClimateTimeout.ok, false);
456
182
  assert.equal(climateFetches, 0, `invalid climate timeout ${String(badTimeout)} must not fetch`);
457
183
  let geoFetches = 0;
458
- const invalidGeoTimeout = await geocodePlace('大理市', {
459
- timeoutMs: badTimeout,
460
- fetchImpl: mockFetch(()=>{
461
- geoFetches += 1;
462
- return geoDali;
463
- })
464
- });
184
+ const invalidGeoTimeout = await geocodePlace('大理市', { timeoutMs: badTimeout, fetchImpl: mockFetch(() => { geoFetches += 1; return geoDali; }) });
465
185
  assert.equal(invalidGeoTimeout.ok, false);
466
186
  assert.equal(geoFetches, 0, `invalid geocode timeout ${String(badTimeout)} must not fetch`);
467
187
  }
468
188
  let cappedDays = '';
469
- const capped = await getForecast({
470
- latitude: 25.6,
471
- longitude: 100.2
472
- }, {
473
- days: 99,
474
- fetchImpl: async (input)=>{
475
- cappedDays = String(input);
476
- return jsonResponse(forecastBody(16));
477
- }
478
- });
189
+ const capped = await getForecast({ latitude: 25.6, longitude: 100.2 }, { days: 99, fetchImpl: async (input) => { cappedDays = String(input); return jsonResponse(forecastBody(16)); } });
479
190
  assert.equal(capped.ok, true);
480
191
  assert.match(cappedDays, /forecast_days=16/);
481
- const wrongMonth = await getClimate({
482
- latitude: 25.6,
483
- longitude: 100.2
484
- }, 9, {
485
- year: 2025,
486
- fetchImpl: mockFetch(()=>climateBody)
487
- });
192
+ const wrongMonth = await getClimate({ latitude: 25.6, longitude: 100.2 }, 9, { year: 2025, fetchImpl: mockFetch(() => climateBody) });
488
193
  assert.equal(wrongMonth.ok, false);
489
- const leapBody = {
490
- daily: {
491
- time: Array.from({
492
- length: 29
493
- }, (_, i)=>`2024-02-${String(i + 1).padStart(2, '0')}`),
494
- temperature_2m_max: Array(29).fill(10),
495
- temperature_2m_min: Array(29).fill(1),
496
- weathercode: Array(29).fill(0)
497
- }
498
- };
499
- const leap = await getClimate({
500
- latitude: 25.6,
501
- longitude: 100.2
502
- }, 2, {
503
- year: 2024,
504
- fetchImpl: mockFetch(()=>leapBody)
505
- });
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) });
506
196
  assert.equal(leap.ok, true);
507
- const historicalMinimum = await getClimate({
508
- latitude: 25.6,
509
- longitude: 100.2
510
- }, 8, {
511
- year: 1940,
512
- fetchImpl: mockFetch(()=>climateBodyFor(1940, 8))
513
- });
197
+ const historicalMinimum = await getClimate({ latitude: 25.6, longitude: 100.2 }, 8, { year: 1940, fetchImpl: mockFetch(() => climateBodyFor(1940, 8)) });
514
198
  assert.equal(historicalMinimum.ok, true);
515
199
  const currentYear = new Date().getUTCFullYear();
516
- const currentClimate = await getClimate({
517
- latitude: 25.6,
518
- longitude: 100.2
519
- }, 8, {
520
- year: currentYear,
521
- fetchImpl: mockFetch(()=>climateBodyFor(currentYear, 8))
522
- });
200
+ const currentClimate = await getClimate({ latitude: 25.6, longitude: 100.2 }, 8, { year: currentYear, fetchImpl: mockFetch(() => climateBodyFor(currentYear, 8)) });
523
201
  assert.equal(currentClimate.ok, true);
524
- for (const badYear of [
525
- 1939,
526
- 2025.5,
527
- currentYear + 1
528
- ]){
202
+ for (const badYear of [1939, 2025.5, currentYear + 1]) {
529
203
  let yearFetches = 0;
530
- const invalidYear = await getClimate({
531
- latitude: 25.6,
532
- longitude: 100.2
533
- }, 8, {
534
- year: badYear,
535
- fetchImpl: mockFetch(()=>{
536
- yearFetches += 1;
537
- return climateBody;
538
- })
539
- });
204
+ const invalidYear = await getClimate({ latitude: 25.6, longitude: 100.2 }, 8, { year: badYear, fetchImpl: mockFetch(() => { yearFetches += 1; return climateBody; }) });
540
205
  assert.equal(invalidYear.ok, false);
541
206
  assert.equal(yearFetches, 0, `invalid climate year ${String(badYear)} must not fetch`);
542
207
  }
543
208
  const duplicateDayBody = climateBodyFor(2025, 8);
544
209
  duplicateDayBody.daily.time[1] = duplicateDayBody.daily.time[0];
545
- const duplicateDay = await getClimate({
546
- latitude: 25.6,
547
- longitude: 100.2
548
- }, 8, {
549
- year: 2025,
550
- fetchImpl: mockFetch(()=>duplicateDayBody)
551
- });
210
+ const duplicateDay = await getClimate({ latitude: 25.6, longitude: 100.2 }, 8, { year: 2025, fetchImpl: mockFetch(() => duplicateDayBody) });
552
211
  assert.equal(duplicateDay.ok, false);
553
212
  const day99Body = climateBodyFor(2025, 8);
554
213
  day99Body.daily.time[0] = '2025-08-99';
555
- const day99 = await getClimate({
556
- latitude: 25.6,
557
- longitude: 100.2
558
- }, 8, {
559
- year: 2025,
560
- fetchImpl: mockFetch(()=>day99Body)
561
- });
214
+ const day99 = await getClimate({ latitude: 25.6, longitude: 100.2 }, 8, { year: 2025, fetchImpl: mockFetch(() => day99Body) });
562
215
  assert.equal(day99.ok, false);
563
216
  let invalidClimateFetches = 0;
564
- const invalidClimate = await getClimate({
565
- latitude: 25.6,
566
- longitude: 100.2
567
- }, 13, {
568
- year: 2025,
569
- fetchImpl: mockFetch(()=>{
570
- invalidClimateFetches += 1;
571
- return climateBody;
572
- })
573
- });
217
+ const invalidClimate = await getClimate({ latitude: 25.6, longitude: 100.2 }, 13, { year: 2025, fetchImpl: mockFetch(() => { invalidClimateFetches += 1; return climateBody; }) });
574
218
  assert.equal(invalidClimate.ok, false);
575
219
  assert.equal(invalidClimateFetches, 0);
576
220
  let geoZeroFetches = 0;
577
- const geoZero = await geocodePlace('大理市', {
578
- timeoutMs: 0,
579
- fetchImpl: mockFetch(()=>{
580
- geoZeroFetches += 1;
581
- return geoDali;
582
- })
583
- });
221
+ const geoZero = await geocodePlace('大理市', { timeoutMs: 0, fetchImpl: mockFetch(() => { geoZeroFetches += 1; return geoDali; }) });
584
222
  assert.equal(geoZero.ok, false);
585
223
  assert.equal(geoZeroFetches, 0);
586
224
  let climateZeroFetches = 0;
587
- const climateZero = await getClimate({
588
- latitude: 25.6,
589
- longitude: 100.2
590
- }, 8, {
591
- timeoutMs: 0,
592
- fetchImpl: mockFetch(()=>{
593
- climateZeroFetches += 1;
594
- return climateBody;
595
- })
596
- });
225
+ const climateZero = await getClimate({ latitude: 25.6, longitude: 100.2 }, 8, { timeoutMs: 0, fetchImpl: mockFetch(() => { climateZeroFetches += 1; return climateBody; }) });
597
226
  assert.equal(climateZero.ok, false);
598
227
  assert.equal(climateZeroFetches, 0);
599
228
  let primaryResolutionFetches = 0;
600
229
  const originalPerformanceNow = performance.now;
601
- Object.defineProperty(performance, 'now', {
602
- configurable: true,
603
- value: (()=>{
604
- const values = [
605
- 100,
606
- 104.5,
607
- 104.5
608
- ];
609
- return ()=>values.shift() ?? 104.5;
610
- })()
611
- });
230
+ Object.defineProperty(performance, 'now', { configurable: true, value: (() => {
231
+ const values = [100, 104.5, 104.5];
232
+ return () => values.shift() ?? 104.5;
233
+ })() });
612
234
  try {
613
- const primaryResolution = await geocodePlace('剩余不足一毫秒', {
614
- timeoutMs: 5,
615
- fetchImpl: mockFetch(()=>{
616
- primaryResolutionFetches += 1;
617
- return geoDali;
618
- })
619
- });
235
+ const primaryResolution = await geocodePlace('剩余不足一毫秒', { timeoutMs: 5, fetchImpl: mockFetch(() => { primaryResolutionFetches += 1; return geoDali; }) });
620
236
  assert.equal(primaryResolution.ok, false);
621
237
  assert.equal(primaryResolutionFetches, 0, 'sub-millisecond primary budget must not fetch');
622
- } finally{
623
- Object.defineProperty(performance, 'now', {
624
- configurable: true,
625
- value: originalPerformanceNow
626
- });
238
+ }
239
+ finally {
240
+ Object.defineProperty(performance, 'now', { configurable: true, value: originalPerformanceNow });
627
241
  }
628
242
  let primaryBudgetFetches = 0;
629
243
  let fallbackResolutionFetches = 0;
630
- Object.defineProperty(performance, 'now', {
631
- configurable: true,
632
- value: (()=>{
633
- const values = [
634
- 200,
635
- 200,
636
- 205
637
- ];
638
- return ()=>values.shift() ?? 205;
639
- })()
640
- });
244
+ Object.defineProperty(performance, 'now', { configurable: true, value: (() => {
245
+ const values = [200, 200, 205];
246
+ return () => values.shift() ?? 205;
247
+ })() });
641
248
  try {
642
- const fallbackResolution = await geocodePlace('兜底剩余不足一毫秒', {
643
- timeoutMs: 5,
644
- fetchImpl: async (input)=>{
249
+ const fallbackResolution = await geocodePlace('兜底剩余不足一毫秒', { timeoutMs: 5, fetchImpl: async (input) => {
645
250
  if (String(input).includes('nominatim')) {
646
251
  fallbackResolutionFetches += 1;
647
252
  return jsonResponse([]);
648
253
  }
649
254
  primaryBudgetFetches += 1;
650
- return jsonResponse({
651
- results: []
652
- });
653
- }
654
- });
255
+ return jsonResponse({ results: [] });
256
+ } });
655
257
  assert.equal(fallbackResolution.ok, false);
656
258
  assert.equal(primaryBudgetFetches, 1);
657
259
  assert.equal(fallbackResolutionFetches, 0, 'sub-millisecond fallback budget must not fetch');
658
- } finally{
659
- Object.defineProperty(performance, 'now', {
660
- configurable: true,
661
- value: originalPerformanceNow
662
- });
663
260
  }
664
- const nonErrorRejection = await getForecast({
665
- latitude: 25.6,
666
- longitude: 100.2
667
- }, {
668
- fetchImpl: async ()=>{
669
- throw Object.create(null);
670
- }
671
- });
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); } });
672
265
  assert.equal(nonErrorRejection.ok, false);
673
266
  assert.equal(nonErrorRejection.error, 'request failed');
674
- const symbolRejection = await getForecast({
675
- latitude: 25.6,
676
- longitude: 100.2
677
- }, {
678
- fetchImpl: async ()=>{
679
- throw Symbol('untrusted');
680
- }
681
- });
267
+ const symbolRejection = await getForecast({ latitude: 25.6, longitude: 100.2 }, { fetchImpl: async () => { throw Symbol('untrusted'); } });
682
268
  assert.equal(symbolRejection.ok, false);
683
269
  assert.equal(symbolRejection.error, 'request failed');
684
270
  const originalDateNow = Date.now;
685
- Date.now = ()=>0;
271
+ Date.now = () => 0;
686
272
  try {
687
- const rollback = await getForecast({
688
- latitude: 25.6,
689
- longitude: 100.2
690
- }, {
691
- timeoutMs: 20,
692
- fetchImpl: mockFetch(()=>forecastBody(7))
693
- });
273
+ const rollback = await getForecast({ latitude: 25.6, longitude: 100.2 }, { timeoutMs: 20, fetchImpl: mockFetch(() => forecastBody(7)) });
694
274
  assert.equal(rollback.ok, true, 'wall-clock rollback must not expand or break monotonic budget');
695
- } finally{
275
+ }
276
+ finally {
696
277
  Date.now = originalDateNow;
697
278
  }
698
279
  assert.equal(wmoLabel(0), '晴');
@@ -700,6 +281,3 @@ assert.equal(wmoLabel(95), '雷暴');
700
281
  assert.match(wmoLabel(999), /天气码999/);
701
282
  console.log('6. climate/schema guard/WMO OK');
702
283
  console.log('\nWEATHER TESTS: deterministic 6/6 OK (live APIs are peripheral smoke only)');
703
-
704
-
705
- //# sourceURL=ts/scripts/weather-tests.ts