Guard charts from stale scan rows

This commit is contained in:
2569718930@qq.com
2026-06-10 14:00:43 +08:00
parent c43d916680
commit df40e6c2ca
4 changed files with 137 additions and 1 deletions
@@ -334,4 +334,72 @@ export function runTests() {
preservedPatchRunway?.values.some((value) => value === 28.8),
"stale full-detail responses must not overwrite a newer live SSE observation point",
);
const chengduDetail = {
forecastTodayHigh: null,
debPrediction: 31,
debQuality: null,
debHourlyPath: null,
localDate: "2026-06-10",
localTime: "13:46",
times: [],
temps: [],
modelCurves: undefined,
runwayPlateHistory: {
"02L/20R": [
{ timestamp: "13:35", temp_c: 29.6, value: 29.6 },
{ timestamp: "13:39", temp_c: 29.8, value: 29.8 },
{ timestamp: "13:43", temp_c: 30.4, value: 30.4 },
],
},
runwayBandHistory: undefined,
amos: null,
current: null,
airportCurrent: { temp: 28, obs_time: "13:00", max_so_far: 28 },
airportPrimary: { temp: 28, obs_time: "13:00", max_so_far: 28 },
forecastDaily: [],
multiModelDaily: {},
probabilities: null,
airportPrimaryTodayObs: [["13:00", 28]],
} as any;
const staleChengduRow = {
city: "chengdu",
local_date: "2026-06-07",
local_time: "21:37",
current_temp: 21.0,
current_max_so_far: 25.0,
temp_symbol: "°C",
tz_offset_seconds: 8 * 3600,
runway_plate_history: {
"02L/20R": [
{ time: "2026-06-07T13:20:00+00:00", temp: 21.2 },
{ time: "2026-06-07T13:30:00+00:00", temp: 21.4 },
],
},
} as any;
const chengduMerged = mergeRowObservationIntoHourly(chengduDetail, staleChengduRow);
const chengduChart = buildFullDayChartData(
{
city: "chengdu",
local_date: "2026-06-10",
local_time: "13:46",
temp_symbol: "°C",
tz_offset_seconds: 8 * 3600,
} as any,
chengduMerged,
false,
);
const chengduSettlementRunway = chengduChart.series.find((item) => item.key === "runway_02L_20R");
assert(
chengduSettlementRunway?.values.some((value) => value === 30.4),
"current-date Chengdu detail runway history should remain visible after receiving a stale scan row",
);
assert(
!chengduSettlementRunway?.values.some((value) => value !== null && value <= 22),
"stale previous-day Chengdu scan rows must not append a fake latest runway point to current-date detail",
);
assert(
chengduMerged?.airportCurrent?.temp === 28,
"stale previous-day scan rows must not replace current-date detail airport conditions",
);
}
@@ -1088,6 +1088,16 @@ function mergeRunwayPlateHistory(
return Object.keys(result).length ? result : undefined;
}
function hourlyLocalDatesConflict(
base: HourlyForecast,
live: HourlyForecast,
row: ScanOpportunityRow | null,
) {
const baseDate = String(base?.localDate || "").trim();
const liveDate = String(live?.localDate || row?.local_date || "").trim();
return Boolean(baseDate && liveDate && baseDate !== liveDate);
}
function rowLooksLikeRunwaySensor(row: ScanOpportunityRow | null) {
const cityKey = normalizeCityKey(row?.city);
const sourceText = [
@@ -1235,6 +1245,7 @@ function mergeHourlyWithLiveObservations(
): HourlyForecast {
if (!base) return live;
if (!live) return base;
if (hourlyLocalDatesConflict(base, live, row)) return base;
const localDate = base.localDate || live.localDate || row?.local_date || null;
const runwayPlateHistory = mergeRunwayPlateHistory(base.runwayPlateHistory, live.runwayPlateHistory);
const amos = runwayPlateHistory
+40
View File
@@ -3127,6 +3127,46 @@ def test_scan_terminal_cold_requests_start_background_build_without_blocking(mon
assert all("初始化" in result["stale_reason"] or "刷新中" in result["stale_reason"] for result in results)
def test_scan_terminal_nonforce_ignores_ancient_success_snapshot(monkeypatch):
filters = {"scan_mode": "tradable", "limit": 17, "min_edge_pct": 6.75}
old_success_t = 1780839484.0
monkeypatch.setattr(
scan_terminal_service,
"get_cached_scan_terminal_payload",
lambda *_args, **_kwargs: None,
)
monkeypatch.setattr(
scan_terminal_service,
"get_scan_terminal_cache_entry",
lambda *_args, **_kwargs: {
"t": old_success_t,
"success_t": old_success_t,
"success_payload": {
"generated_at": "2026-06-07T13:38:04.694350Z",
"rows": [{"id": "chengdu:2026-06-07", "city": "chengdu", "current_temp": 21.0}],
"summary": {"candidate_total": 1},
},
},
)
monkeypatch.setattr(
scan_terminal_service.time,
"time",
lambda: old_success_t + scan_terminal_service.SCAN_TERMINAL_PAYLOAD_TTL_SEC * 3,
)
monkeypatch.setattr(
scan_terminal_service,
"_start_scan_terminal_background_refresh",
lambda *_args, **_kwargs: True,
)
payload = scan_terminal_service.build_scan_terminal_payload(filters)
assert payload["status"] == "failed"
assert payload["rows"] == []
assert payload["summary"]["candidate_total"] == 0
def test_scan_terminal_prewarm_builds_default_terminal_payload(monkeypatch):
calls = []
+18 -1
View File
@@ -43,6 +43,10 @@ from web.scan_terminal_ranker import build_ranked_scan_terminal_result
_SCAN_TERMINAL_INFLIGHT_BUILD_LOCK = threading.Lock()
_SCAN_TERMINAL_INFLIGHT_BUILDS: Dict[str, Future] = {}
SCAN_TERMINAL_STALE_SUCCESS_MAX_AGE_SEC = max(
SCAN_TERMINAL_PAYLOAD_TTL_SEC * 2,
600,
)
def _normalize_locale(value: Any) -> str:
@@ -61,6 +65,15 @@ def _rows_count(payload: Dict[str, Any]) -> int:
return len(rows) if isinstance(rows, list) else 0
def _success_payload_within_stale_window(cached_entry: Dict[str, Any]) -> bool:
timestamp = cached_entry.get("success_t") or cached_entry.get("t")
try:
success_ts = float(timestamp)
except Exception:
return True
return (time.time() - success_ts) <= float(SCAN_TERMINAL_STALE_SUCCESS_MAX_AGE_SEC)
def _build_stale_payload_for_timeout_if_better_cached(
*,
filters: Dict[str, Any],
@@ -361,7 +374,11 @@ def build_scan_terminal_payload(
else get_scan_terminal_cache_entry(filters) or {}
)
success_payload = cached_entry.get("success_payload")
if isinstance(success_payload, dict) and success_payload:
if (
isinstance(success_payload, dict)
and success_payload
and _success_payload_within_stale_window(cached_entry)
):
started = _start_scan_terminal_background_refresh(filters)
return build_stale_scan_terminal_payload(
filters=filters,