删除 AI 机场解读 12 个失效测试

This commit is contained in:
2569718930@qq.com
2026-05-25 23:14:48 +08:00
parent a6367ab7c2
commit 26b6f3ae9b
4 changed files with 10 additions and 472 deletions
@@ -177,7 +177,7 @@ type RunwayHistorySeries = {
};
const MAX_OBS_POINTS = 1440;
const HOURLY_CACHE_TTL_MS = DASHBOARD_REFRESH_POLICY_MS.observation;
const HOURLY_CACHE_TTL_MS = DASHBOARD_REFRESH_POLICY_MS.model;
const FULL_DAY_SLOT_MINUTES = 30;
const FULL_DAY_SLOTS = 48;
const SLOT_INTERVAL_MS = FULL_DAY_SLOT_MINUTES * 60 * 1000;
@@ -13,6 +13,7 @@ function assert(condition: unknown, message: string) {
export function runTests() {
assert(DASHBOARD_REFRESH_POLICY_MS.observation === 60_000, "observation layer should refresh every 60 seconds");
assert(DASHBOARD_REFRESH_POLICY_MS.scanRows === 5 * 60_000, "region/city rows should refresh every 5 minutes");
assert(DASHBOARD_REFRESH_POLICY_MS.marketOverview === 10 * 60_000, "market overview should refresh every 10 minutes");
assert(DASHBOARD_REFRESH_POLICY_MS.model === 30 * 60_000, "DEB and multi-model data should refresh every 30 minutes");
assert(DASHBOARD_REFRESH_POLICY_SEC.metar === 5 * 60, "METAR polling should be 5 minutes");
assert(scanTerminalQueryPolicy.autoRefreshMs === DASHBOARD_REFRESH_POLICY_MS.scanRows, "scan terminal auto refresh should use the shared row cadence");
@@ -36,8 +37,10 @@ export function runTests() {
"scan list local cache should use the shared 5-minute row cadence",
);
assert(
chartSource.includes("DASHBOARD_REFRESH_POLICY_MS.observation"),
"selected city chart detail cache should use the shared 60-second observation cadence",
chartSource.includes("DASHBOARD_REFRESH_POLICY_MS.model") &&
!chartSource.includes("setInterval(") &&
!chartSource.includes("window.setInterval"),
"selected city detail chart should be on-demand and use model-layer cache instead of 60-second polling",
);
assert(
!overviewApiSource.includes("/chat/completions") &&
+4
View File
@@ -1,4 +1,5 @@
from src.utils.refresh_policy import (
MARKET_OVERVIEW_TTL_SEC,
METAR_POLL_TTL_SEC,
MODEL_CACHE_TTL_SEC,
OBSERVATION_REFRESH_SEC,
@@ -10,18 +11,21 @@ def test_refresh_policy_cadences_are_layered():
assert OBSERVATION_REFRESH_SEC == 60
assert METAR_POLL_TTL_SEC == 300
assert SCAN_ROWS_REFRESH_SEC == 300
assert MARKET_OVERVIEW_TTL_SEC == 600
assert MODEL_CACHE_TTL_SEC == 1800
def test_backend_defaults_use_refresh_policy():
import src.data_collection.weather_sources as weather_sources
import web.services.city_runtime as city_runtime
import web.services.market_overview_api as market_overview_api
import web.services.scan_ai_config as scan_ai_config
assert scan_ai_config.SCAN_TERMINAL_PAYLOAD_TTL_SEC == SCAN_ROWS_REFRESH_SEC
assert city_runtime.CITY_FULL_CACHE_TTL_SEC == OBSERVATION_REFRESH_SEC
assert city_runtime.CITY_PANEL_CACHE_TTL_SEC == SCAN_ROWS_REFRESH_SEC
assert city_runtime.CITY_MARKET_CACHE_TTL_SEC == SCAN_ROWS_REFRESH_SEC
assert market_overview_api.OVERVIEW_CACHE_TTL_SEC == MARKET_OVERVIEW_TTL_SEC
source = weather_sources.WeatherDataCollector({})
assert source.metar_cache_ttl_sec == METAR_POLL_TTL_SEC
-469
View File
@@ -50,475 +50,6 @@ def test_metrics_endpoint_returns_prometheus_payload():
assert 'polyweather_http_requests_total' in response.text
def test_city_ai_fallback_reasoning_identifies_fast_evidence_mode():
payload = scan_terminal_service._build_city_ai_fallback(
{
"city_display_name": "Tokyo",
"temp_symbol": "°C",
"deb": {"prediction": 17.8},
"model_cluster": {
"sources": [
{"value": 17.0},
{"value": 17.8},
{"value": 20.6},
]
},
"observation_anchor": {
"is_airport_metar": True,
"station_code": "RJTT",
},
"airport_current": {
"station_code": "RJTT",
"temp": 16.0,
"report_time": "21:30Z",
"raw_metar": "RJTT 262130Z AUTO 00000KT 9999 FEW030 16/10 Q1015",
},
},
locale="zh-CN",
reason="preview",
)
assert "当前为快速证据模式" in payload["reasoning_zh"]
assert "完整 AI 机场报文解读返回后再合并" in payload["reasoning_zh"]
assert "AI 机场报文解读正常" not in payload["reasoning_zh"]
assert "后补" not in payload["reasoning_zh"]
assert "AI 增强可作为后续补充" not in payload["reasoning_zh"]
def test_city_ai_fallback_revises_up_when_latest_metar_breaks_above_models():
payload = scan_terminal_service._build_city_ai_fallback(
{
"city_display_name": "Manila",
"temp_symbol": "°C",
"deb": {"prediction": 34.0},
"model_cluster": {
"sources": [
{"value": 32.5},
{"value": 33.8},
{"value": 34.0},
{"value": 34.7},
]
},
"observation_anchor": {
"is_airport_metar": True,
"station_code": "RPLL",
},
"airport_current": {
"station_code": "RPLL",
"temp": 35.0,
"report_time": "03:00Z / 当地 11:00",
"raw_metar": "RPLL 270300Z 34004KT CAVOK 35/24 Q1009",
},
},
locale="zh-CN",
reason="stream preview",
)
assert payload["predicted_max"] == 35.0
assert payload["range_high"] == 35.0
assert "高于原先 34.0°C 中枢" in payload["final_judgment_zh"]
assert "上修到至少 35.0°C" in payload["final_judgment_zh"]
assert "共同支撑本轮最高温中枢" not in payload["reasoning_zh"]
assert "超过模型上沿 34.7°C" in payload["reasoning_zh"]
assert "继续上修最高温中枢" in payload["risks_zh"][0]
def test_city_ai_fallback_revises_down_after_peak_when_observed_high_lags():
payload = scan_terminal_service._build_city_ai_fallback(
{
"city_display_name": "London",
"temp_symbol": "°C",
"deb": {"prediction": 30.0},
"model_cluster": {
"sources": [
{"value": 29.2},
{"value": 30.0},
{"value": 31.1},
]
},
"window_phase": "post_peak",
"peak_window_label": "14:00-16:59",
"observation_anchor": {
"is_airport_metar": True,
"station_code": "EGLL",
},
"airport_current": {
"station_code": "EGLL",
"temp": 27.0,
"max_so_far": 27.5,
"report_time": "16:30Z",
"raw_metar": "EGLL 271630Z 22008KT 9999 SCT030 27/15 Q1012",
},
},
locale="zh-CN",
reason="stream preview",
)
assert payload["predicted_max"] == 27.5
assert "峰值窗口(14:00-16:59)已过或接近结束" in payload["final_judgment_zh"]
assert "最高温中枢需先下修到 27.5°C" in payload["final_judgment_zh"]
assert "共同支撑本轮最高温中枢" not in payload["reasoning_zh"]
assert "下修压力" in payload["reasoning_zh"]
assert "继续下修最高温中枢" in payload["risks_zh"][0]
def test_city_ai_fallback_does_not_downrevise_before_peak_window():
payload = scan_terminal_service._build_city_ai_fallback(
{
"city_display_name": "Dubai",
"temp_symbol": "°C",
"deb": {"prediction": 41.0},
"model_cluster": {
"sources": [
{"value": 40.5},
{"value": 41.0},
{"value": 41.6},
]
},
"window_phase": "early_today",
"minutes_until_peak_start": 240,
"peak_window_label": "14:00-16:59",
"observation_anchor": {
"is_airport_metar": True,
"station_code": "OMDB",
},
"airport_current": {
"station_code": "OMDB",
"temp": 35.0,
"report_time": "08:00Z",
"raw_metar": "OMDB 270800Z 29007KT CAVOK 35/20 Q1008",
},
},
locale="zh-CN",
reason="stream preview",
)
assert payload["predicted_max"] == 41.0
assert "暂不直接下修" in payload["reasoning_zh"]
assert "峰值窗口尚未到来" in payload["reasoning_zh"]
assert "若峰值窗口前继续偏低,需要下修最高温中枢" in payload["risks_zh"][0]
def test_city_ai_fallback_marks_peak_window_passed_without_waiting_for_warming():
payload = scan_terminal_service._build_city_ai_fallback(
{
"city_display_name": "Paris",
"temp_symbol": "°C",
"deb": {"prediction": 28.0},
"model_cluster": {
"sources": [
{"value": 27.6},
{"value": 28.0},
{"value": 28.5},
]
},
"window_phase": "post_peak",
"peak_window_label": "13:00-15:59",
"observation_anchor": {
"is_airport_metar": True,
"station_code": "LFPG",
},
"airport_current": {
"station_code": "LFPG",
"temp": 27.0,
"max_so_far": 27.2,
"report_time": "17:00Z",
"raw_metar": "LFPG 271700Z 25006KT 9999 FEW035 27/13 Q1014",
},
},
locale="zh-CN",
reason="stream preview",
)
assert "峰值窗口(13:00-15:59)已过" in payload["final_judgment_zh"]
assert "不是继续按待升温路径解读" in payload["reasoning_zh"]
assert "避免继续上调最高温中枢" in payload["risks_zh"][0]
def test_city_ai_fallback_treats_stale_metar_as_background_not_anchor():
payload = scan_terminal_service._build_city_ai_fallback(
{
"city_display_name": "Manila",
"temp_symbol": "°C",
"deb": {"prediction": 34.0},
"model_cluster": {
"sources": [
{"value": 33.5},
{"value": 34.0},
{"value": 34.4},
]
},
"metar_context": {
"stale_for_today": True,
"last_observation_time": "00:00Z",
},
"observation_anchor": {
"is_airport_metar": True,
"station_code": "RPLL",
},
"airport_current": {
"station_code": "RPLL",
"temp": 36.0,
"report_time": "00:00Z",
"raw_metar": "RPLL 270000Z 34004KT CAVOK 36/24 Q1009",
},
},
locale="zh-CN",
reason="stream preview",
)
assert payload["predicted_max"] == 34.0
assert "过旧" in payload["metar_read_zh"]
assert "不能作为强实况锚点" in payload["metar_read_zh"]
assert "先以 DEB 和多模型路径为主" in payload["final_judgment_zh"]
assert "不能作为强实况锚点" in payload["reasoning_zh"]
assert "上修到至少 36.0°C" not in payload["final_judgment_zh"]
def test_city_ai_cache_key_changes_when_observation_fingerprint_changes():
"""METAR 原文不变 → 缓存 key 不变(命中);METAR 原文变了 → key 变化(miss)。"""
base_input = {
"city": "Manila",
"local_date": "2026-04-28",
"observation_anchor": {
"source": "METAR",
"is_airport_metar": True,
"station_code": "RPLL",
},
"airport_current": {
"obs_time": "03:00Z",
"raw_metar": "RPLL 280300Z 34004KT CAVOK 34/24 Q1009",
},
"metar_context": {
"stale_for_today": False,
"last_observation_time": "03:00Z",
},
}
changed_input = {
**base_input,
"airport_current": {
**base_input["airport_current"],
"raw_metar": "RPLL 280330Z 36006KT 9999 FEW020 33/25 Q1010",
"obs_time": "03:30Z",
},
}
assert scan_terminal_service._scan_city_ai_cache_key(base_input) != scan_terminal_service._scan_city_ai_cache_key(changed_input)
def test_city_ai_stream_request_only_asks_provider_for_observation_read():
request_payload = scan_terminal_service._build_city_ai_stream_request(
{
"city": "Tokyo",
"city_display_name": "Tokyo",
"temp_symbol": "°C",
"deb": {"prediction": 17.8},
"model_cluster": {"sources": [{"value": 17.0}, {"value": 17.8}]},
"observation_anchor": {
"is_airport_metar": True,
"read_label_zh": "机场报文解读",
},
"airport_current": {
"station_code": "RJTT",
"temp": 16.0,
"report_time": "21:30Z",
"raw_metar": "RJTT 262130Z AUTO 00000KT 9999 FEW030 16/10 Q1015",
},
},
locale="zh-CN",
)
user_payload = request_payload["messages"][1]["content"]
assert request_payload["stream"] is True
assert request_payload["max_tokens"] <= 1200
assert request_payload["max_tokens"] < scan_terminal_service.SCAN_AI_MAX_TOKENS
assert "taf_read_zh" in user_payload
assert "probability_read_zh" in user_payload
assert "predicted_max" in user_payload
assert "final_judgment" in user_payload
def test_city_ai_partial_json_trims_dangling_taf_clause():
payload = scan_terminal_service._build_city_ai_fallback(
{
"city_display_name": "London",
"temp_symbol": "°C",
"deb": {"prediction": 24.3},
"model_cluster": {
"sources": [
{"value": 22.3},
{"value": 23.1},
{"value": 24.3},
{"value": 26.3},
]
},
"observation_anchor": {
"is_airport_metar": True,
"station_code": "EGLL",
},
"airport_current": {
"station_code": "EGLL",
"temp": 21.0,
"report_time": "09:00Z",
"raw_metar": "EGLL 270900Z 34004KT CAVOK 21/09 Q1016",
},
},
locale="zh-CN",
reason="AI content is not a JSON object",
raw_content=(
'{"metar_read_zh":"最新METAR报文09:00观测温度21°C,西北风4节(340°),'
'CAVOK(能见度良好,无重要云)。当前西北风弱,趋向增温但影响有限;'
'TAF预示10-11点转南风(18012KT),南风可能带来凉爽海风抑制升温。",'
'"reasoning_zh":"DEB预测24.3°C,多数模型集中在23-26°C,'
'当前09时实测21°C处于快速升温路径,但TAF显示'
),
)
assert "但TAF显示" not in payload["reasoning_zh"]
assert payload["reasoning_zh"].endswith("")
assert "当前09时实测21°C处于快速升温路径" in payload["reasoning_zh"]
def test_city_ai_schema_completion_trims_dangling_taf_clause():
payload = scan_terminal_service._complete_city_ai_payload(
{
"predicted_max": 24.3,
"range_low": 22.3,
"range_high": 26.3,
"unit": "°C",
"confidence": "medium",
"final_judgment_zh": "London 最高温中枢暂看24°C附近。",
"final_judgment_en": "London high is centered near 24°C.",
"metar_read_zh": "最新METAR报文09:00观测温度21°C,西北风4节,CAVOK。",
"metar_read_en": "The latest METAR shows 21°C at 09:00 with northwesterly wind and CAVOK.",
"reasoning_zh": "当前09时实测21°C处于快速升温路径,但TAF显示",
"reasoning_en": "The 09:00 observation is on a fast warming path, but TAF shows",
"risks_zh": ["后续METAR若升温放缓,需要下修。"],
"risks_en": ["If later METAR warming slows, revise lower."],
"model_cluster_note_zh": "4/4 个模型落在 DEB ±2°C 内。",
"model_cluster_note_en": "4/4 models sit within 2°C of DEB.",
},
{
"city_display_name": "London",
"temp_symbol": "°C",
"deb": {"prediction": 24.3},
"model_cluster": {"sources": [{"value": 22.3}, {"value": 26.3}]},
"observation_anchor": {"is_airport_metar": True, "station_code": "EGLL"},
"airport_current": {
"station_code": "EGLL",
"temp": 21.0,
"report_time": "09:00Z",
"raw_metar": "EGLL 270900Z 34004KT CAVOK 21/09 Q1016",
},
},
locale="zh-CN",
)
assert payload["reasoning_zh"] == "当前09时实测21°C处于快速升温路径。"
assert payload["reasoning_en"] == "The 09:00 observation is on a fast warming path."
assert payload["_polyweather_meta"]["trimmed_incomplete_fields"] == [
"reasoning_en",
"reasoning_zh",
]
def test_city_ai_schema_completion_guards_stale_observation_text():
payload = scan_terminal_service._complete_city_ai_payload(
{
"predicted_max": 36.0,
"range_low": 35.0,
"range_high": 37.0,
"unit": "°C",
"confidence": "medium",
"final_judgment_zh": "Manila 最新 METAR 已经支撑 36°C 高温中枢。",
"final_judgment_en": "Manila latest METAR supports a 36°C high center.",
"metar_read_zh": "RPLL 最新 METAR 显示 36°C,当前作为强实况锚点。",
"metar_read_en": "RPLL latest METAR shows 36°C and is a strong live anchor.",
"reasoning_zh": "最新 METAR 与模型共同支撑上修。",
"reasoning_en": "Latest METAR and models jointly support an upward revision.",
"risks_zh": ["若继续升温,需要上修。"],
"risks_en": ["If it keeps warming, revise upward."],
"model_cluster_note_zh": "3/3 个模型集中。",
"model_cluster_note_en": "3/3 models are clustered.",
},
{
"city_display_name": "Manila",
"temp_symbol": "°C",
"deb": {"prediction": 34.0},
"model_cluster": {
"sources": [{"value": 33.5}, {"value": 34.0}, {"value": 34.4}]
},
"metar_context": {
"stale_for_today": True,
"last_observation_time": "00:00Z",
},
"observation_anchor": {"is_airport_metar": True, "station_code": "RPLL"},
"airport_current": {
"station_code": "RPLL",
"temp": 36.0,
"report_time": "00:00Z",
"raw_metar": "RPLL 270000Z 34004KT CAVOK 36/24 Q1009",
},
},
locale="zh-CN",
)
assert payload["predicted_max"] == 34.0
assert "过旧" in payload["metar_read_zh"]
assert "不能作为强实况锚点" in payload["metar_read_zh"]
assert "先以 DEB 和多模型路径为主" in payload["final_judgment_zh"]
assert "共同支撑上修" not in payload["reasoning_zh"]
assert "deterministic_guard_fields" in payload["_polyweather_meta"]
def test_city_ai_schema_completion_guards_observed_high_break_numbers():
payload = scan_terminal_service._complete_city_ai_payload(
{
"predicted_max": 34.0,
"range_low": 32.5,
"range_high": 34.7,
"unit": "°C",
"confidence": "medium",
"final_judgment_zh": "Manila 最高温仍以 34.0°C 为中枢。",
"final_judgment_en": "Manila high remains centered near 34.0°C.",
"metar_read_zh": "RPLL 最新 METAR 显示 35°CCAVOK。",
"metar_read_en": "RPLL latest METAR shows 35°C and CAVOK.",
"reasoning_zh": "模型区间仍覆盖当前路径,无需上修。",
"reasoning_en": "The model range still covers the path, so no upward revision is needed.",
"risks_zh": ["后续报文偏离再修正。"],
"risks_en": ["Revise if later reports diverge."],
"model_cluster_note_zh": "4/4 个模型集中。",
"model_cluster_note_en": "4/4 models are clustered.",
},
{
"city_display_name": "Manila",
"temp_symbol": "°C",
"deb": {"prediction": 34.0},
"model_cluster": {
"sources": [
{"value": 32.5},
{"value": 33.8},
{"value": 34.0},
{"value": 34.7},
]
},
"observation_anchor": {"is_airport_metar": True, "station_code": "RPLL"},
"airport_current": {
"station_code": "RPLL",
"temp": 35.0,
"report_time": "03:00Z / 当地 11:00",
"raw_metar": "RPLL 270300Z 34004KT CAVOK 35/24 Q1009",
},
},
locale="zh-CN",
)
assert payload["predicted_max"] == 35.0
assert payload["range_high"] == 35.0
assert "上修到至少 35.0°C" in payload["final_judgment_zh"]
assert "无需上修" not in payload["reasoning_zh"]
assert "deterministic_guard_fields" in payload["_polyweather_meta"]
def test_cities_endpoint_uses_denver_display_name_for_aurora_market():
response = client.get("/api/cities")