@danceiny/gotry 0.0.1-rc.15 → 0.0.1-rc.17

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 (238) hide show
  1. package/README.md +51 -13
  2. package/README.zh-CN.md +26 -12
  3. package/bin/gotry-booking-copilot.js +53 -0
  4. package/bin/gotry-bootstrap.js +357 -8
  5. package/bin/gotry-inner.js +442 -60
  6. package/bin/gotry-runtime-resolution.d.ts +27 -0
  7. package/bin/gotry-runtime-resolution.js +50 -0
  8. package/bin/gotry.js +1 -1
  9. package/cordis.gotry-patch.yml +15 -1
  10. package/data/airline-airports.json +39 -0
  11. package/dist/capabilities/agent-reach-deep.js +1 -1
  12. package/dist/capabilities/agent-reach.js +1 -1
  13. package/dist/capabilities/anything.js +1 -1
  14. package/dist/capabilities/artifacts.js +1 -1
  15. package/dist/capabilities/effect.js +293 -0
  16. package/dist/capabilities/fact-log.js +41 -0
  17. package/dist/capabilities/flyai.js +20 -6
  18. package/dist/capabilities/hbcli.js +43 -1
  19. package/dist/capabilities/incident-log.js +1 -1
  20. package/dist/capabilities/model-override.js +18 -0
  21. package/dist/capabilities/opensky.js +1 -1
  22. package/dist/capabilities/resilience.js +90 -0
  23. package/dist/capabilities/session/action-cache.js +1 -1
  24. package/dist/capabilities/session/adapters/ctrip-flight.js +1 -1
  25. package/dist/capabilities/session/adapters/meituan-local.js +1 -1
  26. package/dist/capabilities/session/benchmark.js +2 -1
  27. package/dist/capabilities/session/extension-bridge.js +358 -0
  28. package/dist/capabilities/session/extension-channel.js +93 -0
  29. package/dist/capabilities/session/extension-distribution.js +234 -0
  30. package/dist/capabilities/session/extract.js +1 -1
  31. package/dist/capabilities/session/golden-score.js +92 -0
  32. package/dist/capabilities/session/health-watch.js +216 -0
  33. package/dist/capabilities/session/read-guard.js +1 -1
  34. package/dist/capabilities/session/static-flight-golden.js +137 -0
  35. package/dist/capabilities/session/transport.js +1 -1
  36. package/dist/capabilities/session/wizard.js +147 -0
  37. package/dist/capabilities/session-consent.js +1 -1
  38. package/dist/capabilities/session-login.js +83 -1
  39. package/dist/capabilities/session-search.js +111 -2
  40. package/dist/capabilities/weather.js +168 -46
  41. package/dist/data/session-golden-20.json +25 -0
  42. package/dist/data/sf-golden-manifest.json +102 -0
  43. package/dist/data/sf-static-routes.json +91 -0
  44. package/dist/scripts/action-cache-tests.js +1 -1
  45. package/dist/scripts/agent-planning-budget-e2e.js +227 -0
  46. package/dist/scripts/agent-planning-budget-tests.js +173 -0
  47. package/dist/scripts/agent-reach-deep-tests.js +1 -1
  48. package/dist/scripts/agent-reach-tests.js +1 -1
  49. package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
  50. package/dist/scripts/anything-tests.js +1 -1
  51. package/dist/scripts/async-collect.js +1 -1
  52. package/dist/scripts/benchmark-environment-bridge-e2e.js +981 -0
  53. package/dist/scripts/benchmark-environment-bridge-tests.js +2544 -0
  54. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +335 -0
  55. package/dist/scripts/booking-copilot-availability-policy-v2-tests.js +1355 -0
  56. package/dist/scripts/booking-copilot-bin-proof-tests.js +89 -0
  57. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +113 -0
  58. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +356 -0
  59. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +557 -0
  60. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +140 -0
  61. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +202 -0
  62. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +102 -0
  63. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +374 -0
  64. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +447 -0
  65. package/dist/scripts/booking-copilot-runtime-proof-tests.js +205 -0
  66. package/dist/scripts/booking-copilot-server-proof-tests.js +317 -0
  67. package/dist/scripts/booking-copilot-startup-proof-tests.js +283 -0
  68. package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +3935 -0
  69. package/dist/scripts/booking-saga-tests.js +1 -1
  70. package/dist/scripts/booking-surface-contract-proof-tests.js +393 -0
  71. package/dist/scripts/booking-surface-v2-contract-proof-tests.js +1174 -0
  72. package/dist/scripts/bootstrap-tests.js +59 -3
  73. package/dist/scripts/build-changelog.js +279 -0
  74. package/dist/scripts/changelog-tests.js +165 -0
  75. package/dist/scripts/companion-tests.js +1 -1
  76. package/dist/scripts/diff-test.js +1 -1
  77. package/dist/scripts/dsh-runtime-closure-tests.js +225 -0
  78. package/dist/scripts/dsh-runtime-closure.js +150 -0
  79. package/dist/scripts/effect-tests.js +474 -0
  80. package/dist/scripts/engine-run.js +1 -1
  81. package/dist/scripts/engine-tests.js +1 -1
  82. package/dist/scripts/evaluation-cadence-tests.js +347 -0
  83. package/dist/scripts/evaluation-contract-tests.js +574 -0
  84. package/dist/scripts/extension-distribution-cli.js +35 -0
  85. package/dist/scripts/extension-distribution-tests.js +341 -0
  86. package/dist/scripts/extension-tests.js +573 -0
  87. package/dist/scripts/fact-gate-tests.js +318 -0
  88. package/dist/scripts/flyai-tests.js +1 -1
  89. package/dist/scripts/hbcli-e2e-tests.js +180 -0
  90. package/dist/scripts/hbcli-tests.js +1 -1
  91. package/dist/scripts/health-watch-cli.js +51 -0
  92. package/dist/scripts/i18n-tests.js +1 -1
  93. package/dist/scripts/incident-tests.js +1 -1
  94. package/dist/scripts/journey-tests.js +1 -1
  95. package/dist/scripts/ledger-tests.js +1 -1
  96. package/dist/scripts/ledger-workflow-crash.js +1 -1
  97. package/dist/scripts/memory-capture-tests.js +1 -1
  98. package/dist/scripts/memory-decay-tests.js +1 -1
  99. package/dist/scripts/memory-metrics.js +1 -1
  100. package/dist/scripts/memory-value-report.js +1 -1
  101. package/dist/scripts/model-override-e2e.js +176 -0
  102. package/dist/scripts/nightly-evidence-tests.js +67 -4
  103. package/dist/scripts/nightly-evidence.js +59 -3
  104. package/dist/scripts/nudge-digest.js +1 -1
  105. package/dist/scripts/onboarding-tests.js +186 -0
  106. package/dist/scripts/opensky-check.js +1 -1
  107. package/dist/scripts/opensky-tests.js +1 -1
  108. package/dist/scripts/pnpm-dsh-closure-proof.js +20 -0
  109. package/dist/scripts/price-drift-tests.js +441 -0
  110. package/dist/scripts/price-drift-watch.js +491 -0
  111. package/dist/scripts/probe-poi-tests.js +1 -1
  112. package/dist/scripts/product-metrics.js +1 -1
  113. package/dist/scripts/publish-preverify.js +40 -4
  114. package/dist/scripts/realtime-pricing-tests.js +1 -1
  115. package/dist/scripts/replay-async.js +1 -1
  116. package/dist/scripts/replay-real.js +1 -1
  117. package/dist/scripts/replay.js +1 -1
  118. package/dist/scripts/session-attach-diagnose.js +1 -1
  119. package/dist/scripts/session-attach-poc.js +1 -1
  120. package/dist/scripts/session-benchmark.js +1 -1
  121. package/dist/scripts/session-extract-tests.js +1 -1
  122. package/dist/scripts/session-login.js +1 -1
  123. package/dist/scripts/session-tests.js +70 -20
  124. package/dist/scripts/sf-live-benchmark.js +338 -0
  125. package/dist/scripts/sf-live-cli-tests.js +21 -0
  126. package/dist/scripts/sf-soft-score-tests.js +108 -0
  127. package/dist/scripts/sf-summary.js +93 -0
  128. package/dist/scripts/skeleton-check.js +1 -1
  129. package/dist/scripts/skeleton-integration-test.js +1 -1
  130. package/dist/scripts/skills-contract-tests.js +1 -1
  131. package/dist/scripts/smoke-session-gate-tests.js +29 -0
  132. package/dist/scripts/smoke.js +154 -33
  133. package/dist/scripts/state-cli-tests.js +1 -1
  134. package/dist/scripts/state-cli.js +1 -1
  135. package/dist/scripts/static-golden-tests.js +299 -0
  136. package/dist/scripts/time-eval-tests.js +1 -1
  137. package/dist/scripts/travel-timeline-tests.js +1 -1
  138. package/dist/scripts/unified-tests.js +1 -1
  139. package/dist/scripts/weather-tests.js +694 -44
  140. package/dist/scripts/z3-race-tests.js +1 -1
  141. package/dist/src/artifact-gate.js +310 -0
  142. package/dist/src/benchmark-agent-conformance.js +370 -0
  143. package/dist/src/benchmark-environment-bridge.js +384 -0
  144. package/dist/src/benchmark-headless-child-diagnostics.js +173 -0
  145. package/dist/src/benchmark-tool-isolation.js +124 -0
  146. package/dist/src/bookable-facts.js +349 -0
  147. package/dist/src/booking-saga.js +1 -1
  148. package/dist/src/booking-surface/availability-policy-v2.js +830 -0
  149. package/dist/src/booking-surface/canonical-schema.js +113 -0
  150. package/dist/src/booking-surface/contracts-v2.js +89 -0
  151. package/dist/src/booking-surface/contracts.js +46 -0
  152. package/dist/src/booking-surface/dsh-planner.js +453 -0
  153. package/dist/src/booking-surface/dsh-plugin.js +93 -0
  154. package/dist/src/booking-surface/error-codes.js +94 -0
  155. package/dist/src/booking-surface/index.js +15 -0
  156. package/dist/src/booking-surface/profile.js +68 -0
  157. package/dist/src/booking-surface/runtime-v2.js +1771 -0
  158. package/dist/src/booking-surface/runtime.js +351 -0
  159. package/dist/src/booking-surface/server-v2.js +334 -0
  160. package/dist/src/booking-surface/server.js +302 -0
  161. package/dist/src/booking-surface/startup.js +159 -0
  162. package/dist/src/booking-surface/validation-v2.js +319 -0
  163. package/dist/src/booking-surface/validation.js +809 -0
  164. package/dist/src/bridge.js +1 -1
  165. package/dist/src/companions.js +1 -1
  166. package/dist/src/contracts.js +1 -1
  167. package/dist/src/dsh-llm.js +1 -1
  168. package/dist/src/engine.js +1 -1
  169. package/dist/src/evaluation-cadence.js +234 -0
  170. package/dist/src/evaluation-contracts.js +906 -0
  171. package/dist/src/i18n.js +1 -1
  172. package/dist/src/index.js +234 -60
  173. package/dist/src/journey.js +1 -1
  174. package/dist/src/loop.js +1 -1
  175. package/dist/src/memory-capture.js +1 -1
  176. package/dist/src/memory-decay.js +1 -1
  177. package/dist/src/memory-utility.js +1 -1
  178. package/dist/src/mock-llm.js +1 -1
  179. package/dist/src/model.js +1 -1
  180. package/dist/src/realtime-pricing.js +14 -7
  181. package/dist/src/slot-spec.js +1 -1
  182. package/dist/src/state-ledger.js +2 -1
  183. package/dist/src/time-anchor.js +1 -1
  184. package/dist/src/tool-budget.js +136 -0
  185. package/dist/src/tool-packet.js +1 -1
  186. package/dist/src/travel-slots.js +1 -1
  187. package/dist/src/travel-timeline.js +1 -1
  188. package/dist/src/unified.js +1 -1
  189. package/dist/src/wish-pool.js +1 -1
  190. package/dist/src/z3-shared.js +1 -1
  191. package/extension/README.md +50 -0
  192. package/extension/background.js +160 -0
  193. package/extension/content-bridge.js +41 -0
  194. package/extension/content-main.js +62 -0
  195. package/extension/manifest.json +32 -0
  196. package/package.json +291 -12
  197. package/schemas/booking.surface.v1.schema.json +927 -0
  198. package/schemas/booking.surface.v2.schema.json +61 -0
  199. package/ts/capabilities/fact-log.ts +54 -0
  200. package/ts/capabilities/flyai.ts +16 -3
  201. package/ts/capabilities/hbcli.ts +46 -0
  202. package/ts/capabilities/session/benchmark.ts +4 -1
  203. package/ts/capabilities/session/extension-bridge.ts +377 -0
  204. package/ts/capabilities/session/extension-channel.ts +95 -0
  205. package/ts/capabilities/session/extension-distribution.ts +264 -0
  206. package/ts/capabilities/session/golden-score.ts +139 -0
  207. package/ts/capabilities/session/health-watch.ts +257 -0
  208. package/ts/capabilities/session/static-flight-golden.ts +209 -0
  209. package/ts/capabilities/session/wizard.ts +140 -0
  210. package/ts/capabilities/session-login.ts +63 -2
  211. package/ts/capabilities/session-search.ts +105 -3
  212. package/ts/capabilities/weather.ts +141 -52
  213. package/ts/package.json +3 -3
  214. package/ts/src/artifact-gate.ts +365 -0
  215. package/ts/src/benchmark-agent-conformance.ts +448 -0
  216. package/ts/src/benchmark-environment-bridge.ts +348 -0
  217. package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
  218. package/ts/src/benchmark-tool-isolation.ts +166 -0
  219. package/ts/src/bookable-facts.ts +557 -0
  220. package/ts/src/booking-surface/availability-policy-v2.ts +523 -0
  221. package/ts/src/booking-surface/canonical-schema.js +113 -0
  222. package/ts/src/booking-surface/contracts-v2.ts +118 -0
  223. package/ts/src/booking-surface/contracts.ts +380 -0
  224. package/ts/src/booking-surface/dsh-planner.ts +452 -0
  225. package/ts/src/booking-surface/dsh-plugin.js +93 -0
  226. package/ts/src/booking-surface/error-codes.ts +101 -0
  227. package/ts/src/booking-surface/index.ts +12 -0
  228. package/ts/src/booking-surface/profile.ts +42 -0
  229. package/ts/src/booking-surface/runtime-v2.ts +1466 -0
  230. package/ts/src/booking-surface/runtime.ts +483 -0
  231. package/ts/src/booking-surface/server-v2.ts +247 -0
  232. package/ts/src/booking-surface/server.ts +324 -0
  233. package/ts/src/booking-surface/startup.ts +196 -0
  234. package/ts/src/booking-surface/validation-v2.ts +205 -0
  235. package/ts/src/booking-surface/validation.ts +453 -0
  236. package/ts/src/index.ts +161 -14
  237. package/ts/src/state-ledger.ts +1 -0
  238. package/ts/src/tool-budget.ts +165 -0
@@ -1,55 +1,705 @@
1
1
  import assert from 'node:assert/strict';
2
2
  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)=>{
54
+ const days = new Date(Date.UTC(year, month, 0)).getUTCDate();
55
+ 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
+ };
66
+ };
67
+ const climateBody = climateBodyFor(2025, 8);
68
+ const mockFetch = (handler)=>async (input, init)=>jsonResponse(await handler(String(input), init?.signal ?? new AbortController().signal));
3
69
  const geo = await geocodePlace('大理市', {
4
- count: 10
5
- });
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 phuket = await geocodePlace('普吉岛');
13
- assert.equal(phuket.ok, true, `普吉岛 geocode ok: ${phuket.error ?? ''}`);
14
- assert.equal(phuket.via, 'nominatim', 'open-meteo 0 结果应走 nominatim 兜底层');
15
- assert.ok(Math.abs(phuket.results[0].latitude - 8.0) < 0.5, `普吉岛纬度应≈8,实际 ${phuket.results[0].latitude}(${phuket.results[0].name})`);
16
- assert.match(phuket.results[0].country ?? '', /泰国/, '国家标签应为泰国');
17
- assert.match(phuket.evidence, /\[实时API:nominatim@2/, '兜底层证据链带时间戳');
18
- console.log(`1b. geocode 普吉岛(兜底层)→ ${phuket.results[0].name}(${phuket.results[0].admin1},${phuket.results[0].country}) OK`);
70
+ count: 10,
71
+ fetchImpl: mockFetch(()=>geoDali)
72
+ });
73
+ assert.equal(geo.ok, true);
74
+ assert.equal(geo.via, 'open-meteo');
75
+ assert.ok(Math.abs(geo.results[0].latitude - 25.6) < 0.5);
76
+ assert.match(geo.evidence, /open-meteo-geo@2/);
77
+ console.log('1. geocode 主源命中 + alias/schema OK');
78
+ 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
+ });
91
+ assert.equal(phuket.ok, true);
92
+ assert.equal(phuket.via, 'nominatim');
93
+ assert.match(phuket.results[0].country ?? '', /泰国/);
94
+ assert.ok(Math.abs(phuket.results[0].latitude - 8) < 0.5);
95
+ assert.match(seenRequests[0].url, /name=%E6%99%AE%E5%90%89%E5%B2%9B.*count=3.*language=zh/);
96
+ assert.match(seenRequests[1].url, /q=%E6%99%AE%E5%90%89%E5%B2%9B.*limit=3.*accept-language=zh/);
97
+ 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
+ });
113
+ assert.equal(badAddress.ok, true);
114
+ assert.equal('country' in badAddress.results[0], false);
115
+ assert.equal('admin1' in badAddress.results[0], false);
116
+ console.log('2. geocode 兜底链 + 地名 alias OK');
19
117
  const fc = await getForecast({
20
- latitude: dali.latitude,
21
- longitude: dali.longitude
118
+ latitude: 25.6,
119
+ longitude: 100.2
120
+ }, {
121
+ days: 7,
122
+ fetchImpl: mockFetch(()=>forecastBody(7))
123
+ });
124
+ assert.equal(fc.ok, true);
125
+ assert.equal(fc.daily?.length, 7);
126
+ assert.equal(fc.daily?.[0]?.weatherCode, 61);
127
+ assert.ok(fc.latencyMs < 1000);
128
+ 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
+ }));
137
+ 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
+ });
145
+ assert.equal(slowFc.ok, false);
146
+ assert.ok(Date.now() - slowStarted < 150, 'slow forecast must respect timeout');
147
+ assert.match(slowFc.evidence, /error/);
148
+ const never = mockFetch(()=>new Promise(()=>undefined));
149
+ 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
+ });
157
+ assert.equal(neverFc.ok, false);
158
+ 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
+ });
169
+ assert.equal(neverBody.ok, false, 'hard deadline must cover a body decoder that never settles');
170
+ let lateResolve;
171
+ const lateResolveResult = await getForecast({
172
+ latitude: 25.6,
173
+ longitude: 100.2
174
+ }, {
175
+ timeoutMs: 10,
176
+ fetchImpl: async ()=>({
177
+ ok: true,
178
+ json: ()=>new Promise((resolve)=>{
179
+ lateResolve = resolve;
180
+ })
181
+ })
182
+ });
183
+ assert.equal(lateResolveResult.ok, false);
184
+ lateResolve?.(forecastBody(7));
185
+ let lateReject;
186
+ const lateRejectResult = await getForecast({
187
+ latitude: 25.6,
188
+ longitude: 100.2
189
+ }, {
190
+ timeoutMs: 10,
191
+ fetchImpl: async ()=>({
192
+ ok: true,
193
+ json: ()=>new Promise((_, reject)=>{
194
+ lateReject = reject;
195
+ })
196
+ })
197
+ });
198
+ assert.equal(lateRejectResult.ok, false);
199
+ lateReject?.(Object.create(null));
200
+ await new Promise((resolve)=>setImmediate(resolve));
201
+ console.log('4. slow forecast timeout + graceful fallback OK');
202
+ let calls = 0;
203
+ const slowBoth = mockFetch((_url, signal)=>{
204
+ 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
+ });
211
+ });
212
+ });
213
+ const chainStarted = Date.now();
214
+ const chain = await geocodePlace('不存在的地方', {
215
+ timeoutMs: 35,
216
+ fetchImpl: slowBoth
217
+ });
218
+ assert.equal(chain.ok, false);
219
+ assert.equal(calls, 1, 'an exhausted primary budget must not start fallback');
220
+ assert.ok(Date.now() - chainStarted < 150, 'fallback chain must share one total budget');
221
+ assert.match(chain.evidence, /open-meteo-geo@error/);
222
+ let fastPrimaryCalls = 0;
223
+ let fastFallbackCalls = 0;
224
+ const fallbackAfterFastFailure = await geocodePlace('不存在的地方', {
225
+ timeoutMs: 100,
226
+ fetchImpl: async (input, init)=>{
227
+ if (String(input).includes('nominatim')) {
228
+ fastFallbackCalls += 1;
229
+ return jsonResponse([]);
230
+ }
231
+ fastPrimaryCalls += 1;
232
+ throw new Error('ECONNRESET');
233
+ }
234
+ });
235
+ assert.equal(fallbackAfterFastFailure.ok, false);
236
+ assert.equal(fastPrimaryCalls, 1, 'primary should be attempted once');
237
+ 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
22
241
  }, {
23
- days: 7
242
+ fetchImpl: mockFetch(()=>{
243
+ throw new Error('ECONNRESET');
244
+ })
245
+ });
246
+ assert.equal(network.ok, false);
247
+ 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
+ });
254
+ assert.equal(httpError.ok, false);
255
+ assert.match(httpError.error ?? '', /HTTP 503/);
256
+ const malformedGeo = await geocodePlace('坏响应', {
257
+ timeoutMs: 100,
258
+ fetchImpl: mockFetch((url)=>url.includes('nominatim') ? {} : {
259
+ results: {}
260
+ })
24
261
  });
25
- assert.equal(fc.ok, true, `forecast ok: ${fc.error ?? ''}`);
26
- assert.equal(fc.daily.length, 7, '7 天');
27
- assert.ok(fc.daily[0].tempMaxC > -50 && fc.daily[0].tempMaxC < 60, '温度合理范围');
28
- assert.match(fc.evidence, /\[实时API:open-meteo@2/, '证据链');
29
- assert.ok(fc.latencyMs < 5000, `延迟应 <5s,实际 ${fc.latencyMs}ms`);
30
- console.log(`2. forecast 大理 7 天:${fc.daily[0].tempMinC}–${fc.daily[0].tempMaxC}°C ${wmoLabel(fc.daily[0].weatherCode)} OK`);
262
+ assert.equal(malformedGeo.ok, false);
263
+ console.log('5. slow geocode + strict fallback budget + network/HTTP errors OK');
31
264
  const cl = await getClimate({
32
- latitude: dali.latitude,
33
- longitude: dali.longitude
34
- }, 8);
35
- assert.equal(cl.ok, true, `climate ok: ${cl.error ?? ''}`);
36
- assert.ok(cl.daily.length >= 28, '8 月应有 ≥28 天');
37
- assert.match(cl.evidence, /open-meteo-climate@/, '气候证据链');
38
- console.log(`3. climate 大理去年 8 月:${cl.daily.length} 天 OK`);
39
- const bad = await getForecast({
40
- latitude: 999,
41
- longitude: 0
42
- }, {
43
- timeoutMs: 10_000
44
- });
45
- assert.equal(bad.ok, false, '非法输入应返回 ok=false');
46
- assert.ok(bad.evidence.includes('error'), '降级证据标注');
47
- console.log(`4. 降级 ok=false 不抛错 OK`);
265
+ latitude: 25.6,
266
+ longitude: 100.2
267
+ }, 8, {
268
+ year: 2025,
269
+ fetchImpl: mockFetch(()=>climateBody)
270
+ });
271
+ assert.equal(cl.ok, true);
272
+ 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
+ });
284
+ 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
+ });
305
+ 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
+ });
318
+ 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
+ });
331
+ assert.equal(wrongPrecipObject.ok, false);
332
+ const absentPrecip = {
333
+ daily: {
334
+ ...forecastBody(7).daily
335
+ }
336
+ };
337
+ 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
+ });
344
+ assert.equal(forecastAbsentPrecip.ok, true);
345
+ 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
+ });
357
+ assert.equal(forecastNullPrecip.ok, true);
358
+ 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
+ });
365
+ assert.equal(forecastValidPrecip.ok, true);
366
+ 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
+ });
373
+ 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
+ });
381
+ 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
+ });
404
+ assert.equal(shortClimate.ok, false);
405
+ for (const badDays of [
406
+ 0,
407
+ Number.NaN,
408
+ 2.5
409
+ ]){
410
+ 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
+ });
421
+ assert.equal(invalid.ok, false);
422
+ assert.equal(fetches, 0);
423
+ }
424
+ for (const badTimeout of [
425
+ 0,
426
+ -1,
427
+ Number.NaN,
428
+ Number.POSITIVE_INFINITY,
429
+ 2.5
430
+ ]){
431
+ 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
+ });
442
+ assert.equal(invalidForecast.ok, false);
443
+ assert.equal(forecastFetches, 0, `invalid forecast timeout ${String(badTimeout)} must not fetch`);
444
+ 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
+ });
455
+ assert.equal(invalidClimateTimeout.ok, false);
456
+ assert.equal(climateFetches, 0, `invalid climate timeout ${String(badTimeout)} must not fetch`);
457
+ let geoFetches = 0;
458
+ const invalidGeoTimeout = await geocodePlace('大理市', {
459
+ timeoutMs: badTimeout,
460
+ fetchImpl: mockFetch(()=>{
461
+ geoFetches += 1;
462
+ return geoDali;
463
+ })
464
+ });
465
+ assert.equal(invalidGeoTimeout.ok, false);
466
+ assert.equal(geoFetches, 0, `invalid geocode timeout ${String(badTimeout)} must not fetch`);
467
+ }
468
+ 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
+ });
479
+ assert.equal(capped.ok, true);
480
+ 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
+ });
488
+ 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
+ });
506
+ 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
+ });
514
+ assert.equal(historicalMinimum.ok, true);
515
+ 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
+ });
523
+ assert.equal(currentClimate.ok, true);
524
+ for (const badYear of [
525
+ 1939,
526
+ 2025.5,
527
+ currentYear + 1
528
+ ]){
529
+ 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
+ });
540
+ assert.equal(invalidYear.ok, false);
541
+ assert.equal(yearFetches, 0, `invalid climate year ${String(badYear)} must not fetch`);
542
+ }
543
+ const duplicateDayBody = climateBodyFor(2025, 8);
544
+ 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
+ });
552
+ assert.equal(duplicateDay.ok, false);
553
+ const day99Body = climateBodyFor(2025, 8);
554
+ 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
+ });
562
+ assert.equal(day99.ok, false);
563
+ 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
+ });
574
+ assert.equal(invalidClimate.ok, false);
575
+ assert.equal(invalidClimateFetches, 0);
576
+ let geoZeroFetches = 0;
577
+ const geoZero = await geocodePlace('大理市', {
578
+ timeoutMs: 0,
579
+ fetchImpl: mockFetch(()=>{
580
+ geoZeroFetches += 1;
581
+ return geoDali;
582
+ })
583
+ });
584
+ assert.equal(geoZero.ok, false);
585
+ assert.equal(geoZeroFetches, 0);
586
+ 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
+ });
597
+ assert.equal(climateZero.ok, false);
598
+ assert.equal(climateZeroFetches, 0);
599
+ let primaryResolutionFetches = 0;
600
+ 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
+ });
612
+ try {
613
+ const primaryResolution = await geocodePlace('剩余不足一毫秒', {
614
+ timeoutMs: 5,
615
+ fetchImpl: mockFetch(()=>{
616
+ primaryResolutionFetches += 1;
617
+ return geoDali;
618
+ })
619
+ });
620
+ assert.equal(primaryResolution.ok, false);
621
+ 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
+ });
627
+ }
628
+ let primaryBudgetFetches = 0;
629
+ 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
+ });
641
+ try {
642
+ const fallbackResolution = await geocodePlace('兜底剩余不足一毫秒', {
643
+ timeoutMs: 5,
644
+ fetchImpl: async (input)=>{
645
+ if (String(input).includes('nominatim')) {
646
+ fallbackResolutionFetches += 1;
647
+ return jsonResponse([]);
648
+ }
649
+ primaryBudgetFetches += 1;
650
+ return jsonResponse({
651
+ results: []
652
+ });
653
+ }
654
+ });
655
+ assert.equal(fallbackResolution.ok, false);
656
+ assert.equal(primaryBudgetFetches, 1);
657
+ 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
+ }
664
+ const nonErrorRejection = await getForecast({
665
+ latitude: 25.6,
666
+ longitude: 100.2
667
+ }, {
668
+ fetchImpl: async ()=>{
669
+ throw Object.create(null);
670
+ }
671
+ });
672
+ assert.equal(nonErrorRejection.ok, false);
673
+ 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
+ });
682
+ assert.equal(symbolRejection.ok, false);
683
+ assert.equal(symbolRejection.error, 'request failed');
684
+ const originalDateNow = Date.now;
685
+ Date.now = ()=>0;
686
+ try {
687
+ const rollback = await getForecast({
688
+ latitude: 25.6,
689
+ longitude: 100.2
690
+ }, {
691
+ timeoutMs: 20,
692
+ fetchImpl: mockFetch(()=>forecastBody(7))
693
+ });
694
+ assert.equal(rollback.ok, true, 'wall-clock rollback must not expand or break monotonic budget');
695
+ } finally{
696
+ Date.now = originalDateNow;
697
+ }
48
698
  assert.equal(wmoLabel(0), '晴');
49
699
  assert.equal(wmoLabel(95), '雷暴');
50
- assert.match(wmoLabel(999), /天气码999/, '未知码回退');
51
- console.log('5. WMO 码映射 OK');
52
- console.log('\nWEATHER TESTS: 6/6 OK(Open-Meteo 真实 API,免费无 key;普吉岛走 Nominatim 兜底层)');
700
+ assert.match(wmoLabel(999), /天气码999/);
701
+ console.log('6. climate/schema guard/WMO OK');
702
+ console.log('\nWEATHER TESTS: deterministic 6/6 OK (live APIs are peripheral smoke only)');
53
703
 
54
704
 
55
- //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/weather-tests.ts
705
+ //# sourceURL=ts/scripts/weather-tests.ts