@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.
- package/README.md +102 -194
- package/README.zh-CN.md +115 -210
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-bootstrap.js +466 -35
- package/bin/gotry-inner.js +255 -36
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +2 -2
- package/client/client.js +174 -0
- package/cordis.gotry-patch.yml +70 -93
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +63 -148
- package/dist/capabilities/anything.js +89 -75
- package/dist/capabilities/artifacts.js +123 -104
- package/dist/capabilities/channel-health.js +65 -45
- package/dist/capabilities/channel-registry.js +108 -87
- package/dist/capabilities/doctor.js +357 -235
- package/dist/capabilities/effect.js +223 -309
- package/dist/capabilities/fact-log.js +28 -14
- package/dist/capabilities/flyai.js +245 -156
- package/dist/capabilities/ground-transfer.js +509 -0
- package/dist/capabilities/hbcli.js +144 -148
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/model-override.js +30 -10
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/resilience.js +50 -32
- package/dist/capabilities/session/action-cache.js +50 -44
- package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
- package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
- package/dist/capabilities/session/adapters/dida-portal.js +206 -0
- package/dist/capabilities/session/adapters/meituan-local.js +35 -54
- package/dist/capabilities/session/adapters/rail-12306.js +169 -112
- package/dist/capabilities/session/benchmark.js +85 -149
- package/dist/capabilities/session/extension-bridge.js +320 -259
- package/dist/capabilities/session/extension-channel.js +57 -76
- package/dist/capabilities/session/extension-distribution.js +115 -119
- package/dist/capabilities/session/extract.js +31 -21
- package/dist/capabilities/session/golden-score.js +55 -33
- package/dist/capabilities/session/health-watch.js +146 -154
- package/dist/capabilities/session/read-guard.js +55 -47
- package/dist/capabilities/session/static-flight-golden.js +49 -36
- package/dist/capabilities/session/transport.js +54 -61
- package/dist/capabilities/session/wizard.js +63 -93
- package/dist/capabilities/session-consent.js +143 -47
- package/dist/capabilities/session-login.js +95 -126
- package/dist/capabilities/session-search.js +391 -253
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +87 -73
- package/dist/capabilities/weather.js +163 -235
- package/dist/scripts/action-cache-tests.js +24 -59
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
- package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
- package/dist/scripts/anything-tests.js +58 -62
- package/dist/scripts/artifact-client-contract-tests.js +69 -0
- package/dist/scripts/artifacts-capability-tests.js +235 -0
- package/dist/scripts/async-collect.js +26 -15
- package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
- package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
- package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
- package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
- package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
- package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
- package/dist/scripts/booking-executor-tests.js +80 -0
- package/dist/scripts/booking-recovery-chain-tests.js +182 -0
- package/dist/scripts/booking-saga-tests.js +61 -123
- package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
- package/dist/scripts/bootstrap-tests.js +1383 -128
- package/dist/scripts/build-changelog.js +89 -130
- package/dist/scripts/build-metrics-report.js +135 -143
- package/dist/scripts/changelog-tests.js +35 -82
- package/dist/scripts/channel-probe-tests.js +44 -116
- package/dist/scripts/channel-probe.js +84 -167
- package/dist/scripts/channel-registry-tests.js +97 -185
- package/dist/scripts/companion-tests.js +25 -70
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +428 -120
- package/dist/scripts/dsh-artifact-e2e.js +150 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
- package/dist/scripts/dsh-runtime-closure.js +75 -63
- package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +165 -552
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +69 -242
- package/dist/scripts/evaluation-contract-tests.js +193 -499
- package/dist/scripts/extension-distribution-cli.js +19 -15
- package/dist/scripts/extension-distribution-tests.js +98 -211
- package/dist/scripts/extension-tests.js +366 -389
- package/dist/scripts/fact-gate-tests.js +725 -263
- package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
- package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
- package/dist/scripts/flight-malformed-tests.js +159 -0
- package/dist/scripts/flyai-tests.js +325 -68
- package/dist/scripts/g5-guard-tests.js +131 -0
- package/dist/scripts/g5-guard.js +189 -0
- package/dist/scripts/gen-lunar-anchors.js +106 -0
- package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +71 -90
- package/dist/scripts/hbcli-live-optin-tests.js +96 -0
- package/dist/scripts/hbcli-release-contract-tests.js +122 -0
- package/dist/scripts/hbcli-tests.js +41 -115
- package/dist/scripts/health-watch-cli.js +42 -28
- package/dist/scripts/hotel-date-gate-tests.js +496 -0
- package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
- package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
- package/dist/scripts/i18n-tests.js +16 -9
- package/dist/scripts/incident-tests.js +281 -87
- package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
- package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
- package/dist/scripts/issue-271-liveness-tests.js +547 -0
- package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
- package/dist/scripts/issue-289-model-retry-tests.js +441 -0
- package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
- package/dist/scripts/issue-338-home-city-e2e.js +577 -0
- package/dist/scripts/issue343-real-entry-e2e.js +160 -0
- package/dist/scripts/journey-tests.js +22 -27
- package/dist/scripts/kernel-manifest-evidence.js +109 -0
- package/dist/scripts/kernel-manifest-gate.js +126 -0
- package/dist/scripts/kernel-manifest-tests.js +212 -0
- package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
- package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
- package/dist/scripts/kernel-manifest.js +369 -0
- package/dist/scripts/ledger-repair-apply-tests.js +316 -0
- package/dist/scripts/ledger-repair-plan-tests.js +375 -0
- package/dist/scripts/ledger-tests.js +325 -340
- package/dist/scripts/ledger-workflow-crash.js +10 -20
- package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
- package/dist/scripts/memory-capture-tests.js +242 -61
- package/dist/scripts/memory-decay-tests.js +18 -52
- package/dist/scripts/memory-lifecycle-tests.js +657 -0
- package/dist/scripts/memory-lifecycle.js +196 -0
- package/dist/scripts/memory-metrics.js +24 -12
- package/dist/scripts/memory-value-report.js +474 -222
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +62 -199
- package/dist/scripts/model-override-e2e.js +66 -105
- package/dist/scripts/nightly-evidence-tests.js +64 -89
- package/dist/scripts/nightly-evidence.js +108 -111
- package/dist/scripts/nudge-digest.js +39 -28
- package/dist/scripts/onboarding-tests.js +63 -88
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/persona-surface-guard-tests.js +37 -12
- package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +126 -227
- package/dist/scripts/price-drift-watch.js +209 -253
- package/dist/scripts/probe-poi-tests.js +52 -45
- package/dist/scripts/product-metrics.js +187 -273
- package/dist/scripts/publish-preverify.js +47 -41
- package/dist/scripts/realtime-pricing-tests.js +37 -76
- package/dist/scripts/replay-async.js +20 -18
- package/dist/scripts/replay-real.js +16 -18
- package/dist/scripts/replay.js +52 -82
- package/dist/scripts/session-attach-diagnose.js +16 -12
- package/dist/scripts/session-attach-poc.js +34 -37
- package/dist/scripts/session-benchmark.js +66 -147
- package/dist/scripts/session-dida-live-e2e.js +151 -0
- package/dist/scripts/session-extract-tests.js +14 -35
- package/dist/scripts/session-login.js +14 -13
- package/dist/scripts/session-tests.js +545 -583
- package/dist/scripts/sf-live-benchmark.js +92 -110
- package/dist/scripts/sf-live-cli-tests.js +2 -9
- package/dist/scripts/sf-soft-score-tests.js +19 -60
- package/dist/scripts/sf-summary-tests.js +149 -0
- package/dist/scripts/sf-summary.js +466 -84
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +11 -10
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +12 -20
- package/dist/scripts/smoke.js +541 -422
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +226 -101
- package/dist/scripts/state-cli.js +549 -212
- package/dist/scripts/static-golden-tests.js +46 -180
- package/dist/scripts/time-eval-tests.js +210 -192
- package/dist/scripts/travel-timeline-tests.js +29 -93
- package/dist/scripts/turn-handoff-collect-tests.js +46 -93
- package/dist/scripts/turn-handoff-collect.js +71 -99
- package/dist/scripts/turn-policy-tests.js +28 -29
- package/dist/scripts/typed-contract-canary.js +77 -162
- package/dist/scripts/tz-resolver-sanity.js +50 -0
- package/dist/scripts/tz-resolver-tests.js +584 -0
- package/dist/scripts/unified-tests.js +23 -19
- package/dist/scripts/visa-policy-tests.js +31 -44
- package/dist/scripts/weather-tests.js +121 -543
- package/dist/scripts/wish-channel-gate-tests.js +29 -86
- package/dist/scripts/write-gate-tests.js +522 -0
- package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
- package/dist/scripts/z3-lifecycle-tests.js +170 -0
- package/dist/scripts/z3-race-repeat-tests.js +39 -0
- package/dist/scripts/z3-race-tests.js +18 -12
- package/dist/src/artifact-gate.js +652 -221
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +55 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +194 -0
- package/dist/src/benchmark-agent-conformance.js +296 -262
- package/dist/src/benchmark-environment-bridge.js +298 -413
- package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
- package/dist/src/benchmark-tool-isolation.js +94 -67
- package/dist/src/bookable-facts.js +299 -174
- package/dist/src/booking-saga.js +64 -120
- package/dist/src/booking-surface/availability-policy.js +443 -576
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/contracts.js +24 -80
- package/dist/src/booking-surface/dsh-planner.js +510 -281
- package/dist/src/booking-surface/dsh-plugin.js +17 -1
- package/dist/src/booking-surface/error-codes.js +82 -10
- package/dist/src/booking-surface/index.js +0 -3
- package/dist/src/booking-surface/profile.js +10 -54
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +765 -891
- package/dist/src/booking-surface/server.js +200 -251
- package/dist/src/booking-surface/startup.js +58 -54
- package/dist/src/booking-surface/validation.js +150 -218
- package/dist/src/bridge.js +20 -13
- package/dist/src/companions.js +48 -74
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +82 -120
- package/dist/src/engine.js +253 -239
- package/dist/src/evaluation-cadence.js +72 -209
- package/dist/src/evaluation-contracts.js +220 -843
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/gotry-backend.js +70 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +18 -7
- package/dist/src/index.js +1425 -1629
- package/dist/src/journey.js +132 -81
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +257 -166
- package/dist/src/memory-capture.js +180 -32
- package/dist/src/memory-decay.js +30 -14
- package/dist/src/memory-lifecycle.js +1310 -0
- package/dist/src/memory-utility.js +27 -40
- package/dist/src/mock-llm.js +36 -61
- package/dist/src/model.js +81 -36
- package/dist/src/realtime-pricing.js +63 -72
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +310 -471
- package/dist/src/subagent-job-id-guard.js +58 -0
- package/dist/src/time-anchor.js +231 -33
- package/dist/src/tool-packet.js +20 -8
- package/dist/src/travel-slots.js +58 -35
- package/dist/src/travel-timeline.js +44 -63
- package/dist/src/turn-deadline.js +121 -127
- package/dist/src/turn-policy.js +61 -89
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +504 -381
- package/dist/src/wish-pool.js +39 -14
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +273 -7
- package/extension/README.md +4 -3
- package/extension/background.js +125 -14
- package/extension/content-bridge.js +23 -1
- package/extension/content-main.js +15 -4
- package/extension/manifest.json +16 -8
- package/package.json +251 -220
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/anything.ts +1 -1
- package/ts/capabilities/artifacts.ts +46 -9
- package/ts/capabilities/flyai.ts +156 -37
- package/ts/capabilities/hbcli.ts +1 -1
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
- package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
- package/ts/capabilities/session/extension-bridge.ts +226 -101
- package/ts/capabilities/session/extension-channel.ts +22 -9
- package/ts/capabilities/session/wizard.ts +2 -2
- package/ts/capabilities/session-consent.ts +98 -14
- package/ts/capabilities/session-login.ts +1 -0
- package/ts/capabilities/session-search.ts +315 -31
- package/ts/dsh-runtime/vendor/README.md +8 -4
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
- package/ts/package.json +236 -16
- package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
- package/ts/scripts/state-cli.ts +418 -51
- package/ts/src/artifact-gate.ts +561 -15
- package/ts/src/benchmark-environment-bridge.ts +37 -20
- package/ts/src/bookable-facts.ts +183 -7
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/dsh-planner.ts +240 -32
- package/ts/src/booking-surface/dsh-plugin.js +17 -1
- package/ts/src/booking-surface/error-codes.ts +75 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/server.ts +84 -51
- package/ts/src/booking-surface/validation.ts +2 -0
- package/ts/src/contracts.ts +12 -2
- package/ts/src/dsh-llm.ts +4 -8
- package/ts/src/index.ts +609 -90
- package/ts/src/loop.ts +116 -7
- package/ts/src/memory-capture.ts +227 -3
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/state-ledger.ts +47 -29
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +217 -13
- 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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
{
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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(
|
|
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)
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
}
|
|
623
|
-
|
|
624
|
-
|
|
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
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
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
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
-
}
|
|
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
|