拆分图表实时观测兜底
This commit is contained in:
@@ -36,6 +36,7 @@ async function flushMicrotasks() {
|
||||
|
||||
export async function runTests() {
|
||||
assert(DASHBOARD_REFRESH_POLICY_MS.observation === 60_000, "observation layer should refresh every 60 seconds");
|
||||
assert(DASHBOARD_REFRESH_POLICY_MS.liveObservationFallback === 3 * 60_000, "chart observation fallback should poll every 180 seconds when SSE is unavailable");
|
||||
assert(DASHBOARD_REFRESH_POLICY_MS.scanRows === 2 * 60_000, "region/city rows should refresh every 2 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");
|
||||
@@ -86,19 +87,21 @@ export async function runTests() {
|
||||
"selected city chart should consume SSE patches and keep METAR cadence for heavy probability refreshes instead of a 2-minute forced refresh",
|
||||
);
|
||||
assert(
|
||||
chartSource.includes("NO_PATCH_CACHED_DETAIL_REFRESH_MS = DASHBOARD_REFRESH_POLICY_MS.observation") &&
|
||||
chartSource.includes("refreshCachedDetail") &&
|
||||
chartSource.includes("runHourlyDetailFetch") &&
|
||||
chartSource.includes("fetchOptions: { bypassLocalCache: true }") &&
|
||||
chartSource.includes("LIVE_OBSERVATION_FALLBACK_MS = DASHBOARD_REFRESH_POLICY_MS.liveObservationFallback") &&
|
||||
chartSource.includes("refreshLiveObservation") &&
|
||||
chartSource.includes("fetchLiveObservationForCity") &&
|
||||
chartSource.includes("mergeObservationPayloadIntoHourly") &&
|
||||
chartLogicSource.includes("options.bypassLocalCache") &&
|
||||
chartLogicSource.includes("const forceRefresh = Boolean(options.ignoreCache)"),
|
||||
"visible charts should bypass the five-minute browser detail cache every observation cadence without force-refreshing backend sources",
|
||||
"visible charts should use the no-store observation endpoint for 180-second SSE fallback without forcing detail-batch refreshes",
|
||||
);
|
||||
const componentHourlyFetchCalls = chartSource.match(/fetchHourlyForecastForCity\(city,/g) || [];
|
||||
const hourlyFetcherBlock =
|
||||
/function useHourlyDetailFetcher\([\s\S]*?\n}\r?\n\r?\n\/\/ 岸岸 Main component/.exec(chartSource)?.[0] || "";
|
||||
assert(
|
||||
chartSource.includes("function useHourlyDetailFetcher") &&
|
||||
componentHourlyFetchCalls.length === 1 &&
|
||||
!/fetchHourlyForecastForCity\(city,[\s\S]*?\)\s*\.then\(/.test(chartSource),
|
||||
!/fetchHourlyForecastForCity\(city,[\s\S]*?\)\s*\.then\(/.test(hourlyFetcherBlock),
|
||||
"temperature chart should centralize full-detail fetch lifecycle in useHourlyDetailFetcher instead of duplicating then/catch branches across effects",
|
||||
);
|
||||
assert(
|
||||
@@ -159,7 +162,13 @@ export async function runTests() {
|
||||
assert(
|
||||
chartLogicSource.includes("forceRefresh: boolean") &&
|
||||
chartLogicSource.includes('force_refresh: forceRefresh ? "true" : "false"'),
|
||||
"ignoreCache chart detail refreshes must send force_refresh=true so fresh METAR observations bypass proxy and backend chart caches",
|
||||
"ignoreCache chart detail refreshes must send force_refresh=true only for heavy model/detail resyncs, not the 180-second observation fallback",
|
||||
);
|
||||
assert(
|
||||
chartLogicSource.includes("/api/city/${encodeURIComponent(city)}/observation") &&
|
||||
chartLogicSource.includes('cache: "no-store"') &&
|
||||
chartLogicSource.includes("mergeObservationPayloadIntoHourly"),
|
||||
"live observation fetches must call the no-store per-city observation endpoint and merge without touching cached model detail",
|
||||
);
|
||||
assert(
|
||||
chartLogicSource.includes("cityDetailBatchQueueKey") &&
|
||||
|
||||
@@ -197,8 +197,8 @@ export function runTests() {
|
||||
"temperature chart must keep METAR cadence for heavy patch-triggered probability refreshes",
|
||||
);
|
||||
assert(
|
||||
chart.includes("NO_PATCH_CACHED_DETAIL_REFRESH_MS = DASHBOARD_REFRESH_POLICY_MS.observation"),
|
||||
"temperature chart must use observation cadence for lightweight cached no-patch refreshes",
|
||||
chart.includes("LIVE_OBSERVATION_FALLBACK_MS = DASHBOARD_REFRESH_POLICY_MS.liveObservationFallback"),
|
||||
"temperature chart must use the 180-second observation fallback cadence when SSE patches stop",
|
||||
);
|
||||
assert(chart.includes("TemperatureChartCanvas"), "temperature chart shell must compose the extracted chart canvas");
|
||||
assert(chart.includes("TemperatureStatsBars"), "temperature chart shell must compose the extracted stat bars");
|
||||
@@ -229,13 +229,15 @@ export function runTests() {
|
||||
chart.includes("ignoreCache: true") && chart.includes("currentCityLocalDate !== loadedLocalDate"),
|
||||
"temperature chart must background-refresh full city detail when the city-local day rolls over",
|
||||
);
|
||||
const fallbackRefreshBlock = chart.match(/const refreshCachedDetail = \(\) => \{[\s\S]*?\n \};/)?.[0] || "";
|
||||
const fallbackRefreshBlock = chart.match(/const refreshLiveObservation = \(\) => \{[\s\S]*?\n \};/)?.[0] || "";
|
||||
assert(
|
||||
fallbackRefreshBlock.includes("runHourlyDetailFetch") &&
|
||||
fallbackRefreshBlock.includes("fetchOptions: { bypassLocalCache: true }") &&
|
||||
fallbackRefreshBlock.includes("fetchLiveObservationForCity") &&
|
||||
fallbackRefreshBlock.includes("applyLiveObservationPayload") &&
|
||||
chart.includes("mergeObservationPayloadIntoHourly") &&
|
||||
!fallbackRefreshBlock.includes("runHourlyDetailFetch") &&
|
||||
!fallbackRefreshBlock.includes("ignoreCache: true") &&
|
||||
!fallbackRefreshBlock.includes("setIsHourlyLoading(true)"),
|
||||
"no-patch fallback refresh should revalidate through cached backend detail without force-refreshing sources or showing the loading overlay",
|
||||
"no-patch fallback refresh should merge no-store observation data without refreshing cached detail-batch or showing the loading overlay",
|
||||
);
|
||||
const resyncBlock = chart.match(/useEffect\(\(\) => \{\s*if \(!resyncVersion \|\| !city\) return;[\s\S]*?\}, \[resyncVersion, city, runHourlyDetailFetch\]\);/)?.[0] || "";
|
||||
assert(
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { CityDetail } from "@/lib/dashboard-types";
|
||||
import { buildChartTimeAxis, buildDebBaselinePath } from "@/lib/temperature-chart-paths";
|
||||
import {
|
||||
buildFullDayChartData,
|
||||
mergeObservationPayloadIntoHourly,
|
||||
mergeHourlyWithLiveObservations,
|
||||
mergePatchIntoHourly,
|
||||
mergeRowObservationIntoHourly,
|
||||
@@ -835,6 +836,53 @@ export function runTests() {
|
||||
"instant-restore cache must include live-merged runway history so returning to terminal shows it immediately",
|
||||
);
|
||||
|
||||
const observationOnlyPayload = {
|
||||
city: "chengdu",
|
||||
local_date: "2026-06-15",
|
||||
local_time: "17:45",
|
||||
airport_current: {
|
||||
temp: 27.1,
|
||||
obs_time: "2026-06-15T17:45:00+08:00",
|
||||
source_code: "amsc_awos",
|
||||
source_label: "AMSC AWOS",
|
||||
},
|
||||
airport_primary: {
|
||||
temp: 27.1,
|
||||
obs_time: "2026-06-15T17:45:00+08:00",
|
||||
source_code: "amsc_awos",
|
||||
source_label: "AMSC AWOS",
|
||||
},
|
||||
metar_today_obs: [
|
||||
{
|
||||
time: "17:45",
|
||||
temp: 27.1,
|
||||
obs_time: "2026-06-15T17:45:00+08:00",
|
||||
source_code: "amsc_awos",
|
||||
},
|
||||
],
|
||||
runway_plate_history: {
|
||||
"02L/20R": [{ time: "2026-06-15T17:45:00+08:00", tdz_temp: 27.1, end_temp: 26.8 }],
|
||||
},
|
||||
};
|
||||
const observationMergedChengdu = mergeObservationPayloadIntoHourly(
|
||||
cachedChengduDetail,
|
||||
observationOnlyPayload as any,
|
||||
);
|
||||
assert(
|
||||
observationMergedChengdu?.airportCurrent?.temp === 27.1 &&
|
||||
observationMergedChengdu?.airportPrimary?.source_code === "amsc_awos",
|
||||
"observation endpoint payload should update the current observation block",
|
||||
);
|
||||
assert(
|
||||
observationMergedChengdu?.modelCurves?.ECMWF?.length === 2 &&
|
||||
observationMergedChengdu?.debHourlyPath?.temps?.includes(30.9),
|
||||
"observation endpoint payload must not clear DEB or multi-model chart detail",
|
||||
);
|
||||
assert(
|
||||
(observationMergedChengdu?.runwayPlateHistory?.["02L/20R"] || []).length === 2,
|
||||
"observation endpoint payload should append fresh runway history onto cached detail history",
|
||||
);
|
||||
|
||||
_hourlyCache.clear();
|
||||
for (let i = 0; i < 180; i += 1) {
|
||||
const row = {
|
||||
|
||||
Reference in New Issue
Block a user