160 lines
5.8 KiB
TypeScript
160 lines
5.8 KiB
TypeScript
import {
|
|
CITY_LIST_CACHE_TTL_MS,
|
|
cityListItemsToScanRows,
|
|
mergeScanRowsWithCityFallbackRows,
|
|
readCachedCityList,
|
|
writeCachedCityList,
|
|
} from "@/components/dashboard/scan-terminal/city-fallback-rows";
|
|
import type { CityListItem } from "@/lib/dashboard-types";
|
|
|
|
function assert(condition: unknown, message: string) {
|
|
if (!condition) throw new Error(message);
|
|
}
|
|
|
|
export function runTests() {
|
|
const fs = require("node:fs") as typeof import("node:fs");
|
|
const path = require("node:path") as typeof import("node:path");
|
|
const dashboardSource = fs.readFileSync(
|
|
path.join(process.cwd(), "components", "dashboard", "ScanTerminalDashboard.tsx"),
|
|
"utf8",
|
|
);
|
|
const citiesRouteSource = fs.readFileSync(
|
|
path.join(process.cwd(), "app", "api", "cities", "route.ts"),
|
|
"utf8",
|
|
);
|
|
const staticCitiesPath = path.join(process.cwd(), "lib", "static-cities.ts");
|
|
|
|
const cities: CityListItem[] = [
|
|
{
|
|
airport: "Taipei Songshan",
|
|
display_name: "Taipei",
|
|
icao: "RCSS",
|
|
lat: 25.0697,
|
|
lon: 121.5525,
|
|
name: "taipei",
|
|
risk_level: "medium",
|
|
temp_unit: "celsius",
|
|
utc_offset_seconds: 28800,
|
|
},
|
|
{
|
|
airport: "LaGuardia",
|
|
display_name: "New York",
|
|
icao: "KLGA",
|
|
lat: 40.7769,
|
|
lon: -73.874,
|
|
name: "new york",
|
|
risk_level: "low",
|
|
temp_unit: "fahrenheit",
|
|
utc_offset_seconds: -14400,
|
|
},
|
|
];
|
|
|
|
const rows = cityListItemsToScanRows(cities);
|
|
const cacheStorage = new Map<string, string>();
|
|
const memoryStorage = {
|
|
getItem: (key: string) => cacheStorage.get(key) ?? null,
|
|
removeItem: (key: string) => { cacheStorage.delete(key); },
|
|
setItem: (key: string, value: string) => { cacheStorage.set(key, value); },
|
|
};
|
|
|
|
writeCachedCityList(cities, memoryStorage, 1_000);
|
|
const cachedCities = readCachedCityList(memoryStorage, 1_000 + 60_000);
|
|
|
|
assert(rows.length === 2, "fallback rows should preserve every city");
|
|
assert(rows[0].id === "city-fallback:taipei", "fallback row id should be stable");
|
|
assert(rows[0].city === "taipei", "fallback row should keep canonical city key");
|
|
assert(rows[0].city_display_name === "Taipei", "fallback row should keep display name");
|
|
assert(rows[0].airport === "Taipei Songshan", "fallback row should expose airport for selector display");
|
|
assert(rows[0].trading_region === "east_asia", "fallback row should derive region from timezone");
|
|
assert(rows[0].temp_symbol === "°C", "celsius cities should use °C");
|
|
assert(rows[1].trading_region === "north_america", "known cities should keep their configured product region");
|
|
assert(rows[1].temp_symbol === "°F", "fahrenheit cities should use °F");
|
|
assert(cachedCities?.length === 2, "fresh city registry cache should return the stored city list");
|
|
assert(cachedCities?.[0]?.name === "taipei", "city registry cache should preserve city payloads");
|
|
assert(
|
|
readCachedCityList(memoryStorage, 1_000 + CITY_LIST_CACHE_TTL_MS + 1) === null,
|
|
"expired city registry cache should be ignored so the dashboard can revalidate",
|
|
);
|
|
|
|
const scanRows = [
|
|
{
|
|
id: "scan:paris",
|
|
city: "paris",
|
|
city_display_name: "Paris",
|
|
current_temp: 31,
|
|
deb_prediction: 30.5,
|
|
temp_symbol: "°C",
|
|
},
|
|
] as any[];
|
|
const fallbackRows = cityListItemsToScanRows([
|
|
{
|
|
airport: "Paris Le Bourget",
|
|
display_name: "Paris",
|
|
icao: "LFPB",
|
|
lat: 48.97,
|
|
lon: 2.44,
|
|
name: "paris",
|
|
risk_level: "medium",
|
|
temp_unit: "celsius",
|
|
utc_offset_seconds: 7200,
|
|
},
|
|
{
|
|
airport: "Munich Airport",
|
|
display_name: "Munich",
|
|
icao: "EDDM",
|
|
lat: 48.35,
|
|
lon: 11.78,
|
|
name: "munich",
|
|
risk_level: "medium",
|
|
temp_unit: "celsius",
|
|
utc_offset_seconds: 7200,
|
|
},
|
|
{
|
|
airport: "Cape Town International",
|
|
display_name: "Cape Town",
|
|
icao: "FACT",
|
|
lat: -33.97,
|
|
lon: 18.6,
|
|
name: "cape town",
|
|
risk_level: "medium",
|
|
temp_unit: "celsius",
|
|
utc_offset_seconds: 7200,
|
|
},
|
|
]);
|
|
const merged = mergeScanRowsWithCityFallbackRows(scanRows, fallbackRows);
|
|
const mergedCities = merged.map((row) => row.city);
|
|
|
|
assert(merged.length === 3, "city selector data should include scan rows plus missing registry cities");
|
|
assert(mergedCities.includes("paris"), "merged rows should keep scan city");
|
|
assert(mergedCities.includes("munich"), "merged rows should include missing registry city");
|
|
assert(mergedCities.includes("cape town"), "merged rows should include another missing registry city");
|
|
assert(
|
|
merged.filter((row) => row.city === "paris").length === 1,
|
|
"merged rows should not duplicate cities already present in scan rows",
|
|
);
|
|
assert(
|
|
merged.find((row) => row.city === "paris")?.current_temp === 31,
|
|
"scan rows should win over fallback rows for cities already present",
|
|
);
|
|
|
|
assert(
|
|
dashboardSource.includes("cityListItemsToScanRows") &&
|
|
dashboardSource.includes("STATIC_CITY_LIST") &&
|
|
dashboardSource.includes("readCachedCityList") &&
|
|
dashboardSource.includes("writeCachedCityList") &&
|
|
dashboardSource.includes("useState<ScanOpportunityRow[]>(() =>") &&
|
|
dashboardSource.includes("/api/cities") &&
|
|
dashboardSource.includes('cache: "default"') &&
|
|
dashboardSource.includes("cityFallbackRows"),
|
|
"terminal dashboard should seed fallback rows from cached/static city snapshots and avoid no-store /api/cities fetches when possible",
|
|
);
|
|
assert(fs.existsSync(staticCitiesPath), "/api/cities route should have a static city snapshot fallback");
|
|
assert(
|
|
citiesRouteSource.includes("STATIC_CITY_LIST") &&
|
|
citiesRouteSource.includes("AbortController") &&
|
|
citiesRouteSource.includes("x-polyweather-cities-source") &&
|
|
citiesRouteSource.includes("static-fallback"),
|
|
"/api/cities should return a quick static fallback when the backend city registry is cold or unavailable",
|
|
);
|
|
}
|