Add chart freshness diagnostics
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
} from "@/lib/refresh-policy";
|
||||
import { scanTerminalQueryPolicy } from "@/components/dashboard/scan-terminal/scan-terminal-client";
|
||||
import {
|
||||
__buildChartFreshnessContextForTest,
|
||||
__getInitialDetailLoadDelayMsForTest,
|
||||
__shouldFetchCityDetailForChartForTest,
|
||||
__shouldPollLiveChartForTest,
|
||||
@@ -15,9 +16,11 @@ import {
|
||||
HOURLY_CACHE_TTL_MS,
|
||||
__resolveCityDetailFromBatchForTest,
|
||||
__readHourlyCacheEntryForTest,
|
||||
__rememberCityDetailBatchDiagnosticsForTest,
|
||||
__resetHourlyDetailRequestQueueForTest,
|
||||
__runQueuedHourlyDetailRequestForTest,
|
||||
clearCityDetailCache,
|
||||
readCityDetailBatchDiagnostics,
|
||||
} from "@/components/dashboard/scan-terminal/temperature-chart-logic";
|
||||
|
||||
function assert(condition: unknown, message: string) {
|
||||
@@ -119,6 +122,90 @@ export async function runTests() {
|
||||
__shouldPollLiveChartForTest({ city: "shanghai", compact: true, isActive: false, isMaximized: false }) === true,
|
||||
"compact grid slots are visible charts and should run the no-patch fallback guard",
|
||||
);
|
||||
assert(
|
||||
typeof __buildChartFreshnessContextForTest === "function",
|
||||
"temperature charts should expose a structured freshness context for feedback diagnostics",
|
||||
);
|
||||
const degradedFreshness = __buildChartFreshnessContextForTest(
|
||||
{
|
||||
rowAppliedAtMs: 1_000,
|
||||
rowObservationTime: "12:45",
|
||||
ssePatchAppliedAtMs: 5_000,
|
||||
sseObservedAt: "2026-06-10T04:48:00Z",
|
||||
sseRevision: 42,
|
||||
detailRequestedAtMs: 7_000,
|
||||
detailResolvedAtMs: null,
|
||||
detailErrorAtMs: 9_000,
|
||||
detailStatus: "degraded",
|
||||
detailSource: "partial_or_timeout",
|
||||
sseEmittedAtMs: 5_500,
|
||||
sseServerToClientLatencySec: 5,
|
||||
sseCollectorToClientLatencySec: 60,
|
||||
sseSourceToCollectorLatencySec: 64,
|
||||
},
|
||||
11_000,
|
||||
);
|
||||
assert(
|
||||
degradedFreshness.live_path === "sse" &&
|
||||
degradedFreshness.live_age_sec === 6 &&
|
||||
degradedFreshness.row_applied_age_sec === 10 &&
|
||||
degradedFreshness.sse_patch_age_sec === 6,
|
||||
"freshness context should make the newest live row/SSE path observable independently of detail loading",
|
||||
);
|
||||
assert(
|
||||
degradedFreshness.detail_status === "degraded" &&
|
||||
degradedFreshness.detail_source === "partial_or_timeout" &&
|
||||
degradedFreshness.detail_request_age_sec === 4 &&
|
||||
degradedFreshness.detail_error_age_sec === 2 &&
|
||||
degradedFreshness.detail_age_sec === null,
|
||||
"freshness context should report degraded full-detail state without implying the live point is unavailable",
|
||||
);
|
||||
assert(
|
||||
degradedFreshness.sse_emitted_at_ms === 5_500 &&
|
||||
degradedFreshness.sse_server_to_client_latency_sec === 5 &&
|
||||
degradedFreshness.sse_collector_to_client_latency_sec === 60 &&
|
||||
degradedFreshness.sse_source_to_collector_latency_sec === 64,
|
||||
"freshness context should expose SSE delivery and collector latency diagnostics",
|
||||
);
|
||||
__rememberCityDetailBatchDiagnosticsForTest("Paris", "10m", {
|
||||
response_source: "next_proxy_timeout",
|
||||
partial_reason: "proxy_timeout",
|
||||
city_status: { paris: { status: "proxy_timeout" } },
|
||||
});
|
||||
const rememberedBatchDiagnostics = readCityDetailBatchDiagnostics("paris", "10m");
|
||||
assert(
|
||||
rememberedBatchDiagnostics?.response_source === "next_proxy_timeout" &&
|
||||
rememberedBatchDiagnostics?.city_status?.paris?.status === "proxy_timeout",
|
||||
"chart logic should retain the latest detail-batch diagnostics for feedback context",
|
||||
);
|
||||
assert(
|
||||
chartSource.includes("readCityDetailBatchDiagnostics") &&
|
||||
chartSource.includes("detail_batch_diagnostics"),
|
||||
"temperature chart feedback context should include the latest detail-batch diagnostics",
|
||||
);
|
||||
const rowWinsFreshness = __buildChartFreshnessContextForTest(
|
||||
{
|
||||
rowAppliedAtMs: 10_000,
|
||||
rowObservationTime: "12:50",
|
||||
ssePatchAppliedAtMs: 5_000,
|
||||
sseObservedAt: "2026-06-10T04:48:00Z",
|
||||
sseRevision: 42,
|
||||
sseEmittedAtMs: null,
|
||||
sseServerToClientLatencySec: null,
|
||||
sseCollectorToClientLatencySec: null,
|
||||
sseSourceToCollectorLatencySec: null,
|
||||
detailRequestedAtMs: null,
|
||||
detailResolvedAtMs: null,
|
||||
detailErrorAtMs: null,
|
||||
detailStatus: "idle",
|
||||
detailSource: "none",
|
||||
},
|
||||
11_000,
|
||||
);
|
||||
assert(
|
||||
rowWinsFreshness.live_path === "row" && rowWinsFreshness.live_age_sec === 1,
|
||||
"freshness context should report the newest live path when a scan-row update is newer than the last SSE patch",
|
||||
);
|
||||
assert(
|
||||
__shouldPollLiveChartForTest({ city: "shanghai", compact: false, isActive: false, isMaximized: false }) === false,
|
||||
"inactive non-compact charts should not run live polling",
|
||||
@@ -192,7 +279,7 @@ export async function runTests() {
|
||||
);
|
||||
assert(
|
||||
chartSource.includes("allowStale: true") &&
|
||||
chartCanvasSourceIncludes(chartSource, "数据暂不可用") &&
|
||||
chartCanvasSourceIncludes(chartSource, "详情暂不可用") &&
|
||||
chartCanvasSourceIncludes(chartSource, "handleRetryDetail"),
|
||||
"city detail charts should show stale cache first and expose a retryable unavailable state",
|
||||
);
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import {
|
||||
__applySsePatchForTest,
|
||||
getLatestPatchesSnapshot,
|
||||
} from "@/hooks/use-sse-patches";
|
||||
|
||||
function assert(condition: unknown, message: string) {
|
||||
if (!condition) throw new Error(message);
|
||||
@@ -127,6 +131,36 @@ export function runTests() {
|
||||
!subscriptionBlock.includes("ensureSsePatchConnection();"),
|
||||
"city subscription mount/unmount should schedule one coalesced SSE reconnect instead of reconnecting per chart",
|
||||
);
|
||||
const originalDateNow = Date.now;
|
||||
try {
|
||||
Date.now = () => 1_000_000;
|
||||
__applySsePatchForTest({
|
||||
type: "city_observation_patch.v1",
|
||||
city: "Latency City",
|
||||
source: "amsc_awos",
|
||||
obs_time: "2026-06-10T04:50:00Z",
|
||||
observed_at_utc: "2026-06-10T04:50:00Z",
|
||||
revision: 987001,
|
||||
ts: 940_000,
|
||||
sse_emitted_at_ms: 995_000,
|
||||
payload: {
|
||||
temp: 30.5,
|
||||
latency_sec: 64,
|
||||
received_at_utc: "2026-06-10T04:51:04Z",
|
||||
},
|
||||
});
|
||||
const latencyPatch = getLatestPatchesSnapshot().get("latency city") as any;
|
||||
assert(
|
||||
latencyPatch?.delivery?.client_received_at_ms === 1_000_000 &&
|
||||
latencyPatch?.delivery?.sse_emitted_at_ms === 995_000 &&
|
||||
latencyPatch?.delivery?.server_to_client_latency_sec === 5 &&
|
||||
latencyPatch?.delivery?.collector_to_client_latency_sec === 60 &&
|
||||
latencyPatch?.delivery?.source_to_collector_latency_sec === 64,
|
||||
"frontend patch hook should retain SSE delivery latency diagnostics for feedback context",
|
||||
);
|
||||
} finally {
|
||||
Date.now = originalDateNow;
|
||||
}
|
||||
|
||||
const bffEventsRoute = readFrontendFile("app", "api", "events", "route.ts");
|
||||
assert(bffEventsRoute.includes("searchParams"), "Next.js SSE proxy must forward query parameters to FastAPI");
|
||||
@@ -249,7 +283,9 @@ export function runTests() {
|
||||
chart.includes("refreshProbabilityOverlayAfterPatch"),
|
||||
"temperature chart must trigger a throttled background probability refresh after live observation patches",
|
||||
);
|
||||
const patchEffectBlock = chart.match(/useEffect\(\(\) => \{\s*if \(!latestPatch[\s\S]*?\}, \[latestPatch, row, city, targetResolution, compact, isActive, isMaximized, applySuccessfulHourlyDetail\]\);/)?.[0] || "";
|
||||
const patchEffectBlock = chart.match(
|
||||
/useEffect\(\(\) => \{\s*if \(!latestPatch[\s\S]*?refreshProbabilityOverlayAfterPatch\(\);[\s\S]*?\}, \[[^\]]*latestPatch[^\]]*applySuccessfulHourlyDetail[^\]]*\]\);/,
|
||||
)?.[0] || "";
|
||||
assert(
|
||||
patchEffectBlock.includes("refreshProbabilityOverlayAfterPatch") &&
|
||||
patchEffectBlock.includes("ignoreCache: true") &&
|
||||
|
||||
Reference in New Issue
Block a user