NODEDC_PLATFORM/services/map-gateway/test/reference-station-source.te...

205 lines
7.8 KiB
JavaScript

import assert from "node:assert/strict";
import { mkdtemp, readFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
import {
MAP_REFERENCE_SEARCH_SCHEMA,
MAP_REFERENCE_SNAPSHOT_SCHEMA,
TRANSPORT_STATION_PROFILE_ID,
createReferenceStationSource,
} from "../src/reference-station-source.mjs";
const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
const seedFile = join(root, "reference-sources", "transport-stations", "moscow-v1.json");
test("packaged transport station profile is normalized and provider-neutral", async () => {
const cacheDir = await mkdtemp(join(tmpdir(), "nodedc-reference-stations-"));
const source = await createReferenceStationSource({ seedFile, cacheDir, fetchEnabled: false });
const snapshot = await source.snapshot();
assert.equal(snapshot.schemaVersion, MAP_REFERENCE_SNAPSHOT_SCHEMA);
assert.equal(snapshot.profileId, TRANSPORT_STATION_PROFILE_ID);
assert.equal(snapshot.complete, true);
assert.equal(snapshot.metadata.factCount, 638);
assert.deepEqual(snapshot.metadata.categoryCounts, {
metro: 273,
railway_station: 357,
railway_terminal: 8,
});
assert.ok(snapshot.facts.every((fact) => !JSON.stringify(fact).match(/https?:|token|credential|endpoint/i)));
assert.ok(snapshot.facts.every((fact) => ["map.station", "map.terminal"].includes(fact.semanticType)));
});
test("bbox inside the seed spatial-cell coverage never waits for upstream", async () => {
const cacheDir = await mkdtemp(join(tmpdir(), "nodedc-reference-stations-"));
let calls = 0;
const source = await createReferenceStationSource({
seedFile,
cacheDir,
cellDegrees: 0.5,
fetchImpl: async () => {
calls += 1;
throw new Error("seed coverage must not call upstream");
},
});
const snapshot = await source.snapshot({ bbox: [36.9, 55.4, 38.2, 56.1] });
assert.equal(calls, 0);
assert.equal(snapshot.complete, true);
assert.equal(snapshot.metadata.requestedCellCount, 12);
assert.ok(snapshot.facts.length > 0);
});
test("bbox fetch uses one cached spatial cell and never exposes upstream payload", async () => {
const cacheDir = await mkdtemp(join(tmpdir(), "nodedc-reference-stations-"));
let calls = 0;
const source = await createReferenceStationSource({
seedFile,
cacheDir,
cellDegrees: 1,
fetchImpl: async () => {
calls += 1;
return new Response(JSON.stringify({
elements: [{
type: "node",
id: 987654321,
lat: 59.93,
lon: 30.31,
tags: { railway: "station", station: "subway", name: "Тестовая", website: "https://not-projected.invalid" },
}],
}), { status: 200, headers: { "content-type": "application/json" } });
},
});
const first = await source.snapshot({ bbox: [30, 59, 31, 60] });
const second = await source.snapshot({ bbox: [30, 59, 31, 60] });
assert.equal(calls, 1);
assert.ok(first.facts.some((fact) => fact.sourceId === "osm.node.987654321"));
assert.deepEqual(first.facts, second.facts);
assert.ok(!JSON.stringify(first).includes("not-projected"));
const cached = JSON.parse(await readFile(join(cacheDir, "reference-features", TRANSPORT_STATION_PROFILE_ID, "30_59.json"), "utf8"));
assert.equal(cached.schemaVersion, "nodedc.map-reference-cell/v1");
});
test("viewport cells use bounded concurrency and deduplicate inflight fetches", async () => {
const cacheDir = await mkdtemp(join(tmpdir(), "nodedc-reference-stations-"));
let active = 0;
let maximumActive = 0;
let calls = 0;
const starts = [];
const source = await createReferenceStationSource({
seedFile,
cacheDir,
cellDegrees: 0.5,
fetchImpl: async () => {
calls += 1;
starts.push(Date.now());
active += 1;
maximumActive = Math.max(maximumActive, active);
await new Promise((resolveDelay) => setTimeout(resolveDelay, 15));
active -= 1;
return new Response(JSON.stringify({ elements: [] }), {
status: 200,
headers: { "content-type": "application/json" },
});
},
});
const bbox = [29.9, 59.7, 30.9, 60.2];
await Promise.all([source.snapshot({ bbox }), source.snapshot({ bbox })]);
assert.equal(calls, 6);
assert.ok(maximumActive <= 2);
assert.ok(starts.slice(1).every((startedAt, index) => startedAt - starts[index] >= 650));
const status = await source.status();
assert.equal(status.activeFetches, 0);
assert.equal(status.queuedFetches, 0);
assert.equal(status.upstreamState, "ready");
});
test("Russian name wins and upstream failures are safe diagnostics", async () => {
const cacheDir = await mkdtemp(join(tmpdir(), "nodedc-reference-stations-"));
let fail = false;
const source = await createReferenceStationSource({
seedFile,
cacheDir,
cellDegrees: 1,
fetchImpl: async () => {
if (fail) throw new TypeError("getaddrinfo EAI_AGAIN overpass.internal.example");
return new Response(JSON.stringify({
elements: [{
type: "node",
id: 987654322,
lat: 59.93,
lon: 30.31,
tags: {
railway: "station",
station: "subway",
name: "Petrogradskaya",
"name:ru": "Петроградская",
},
}],
}), { status: 200, headers: { "content-type": "application/json" } });
},
});
const snapshot = await source.snapshot({ bbox: [30, 59, 31, 60] });
assert.equal(snapshot.facts[0].attributes.name, "Петроградская");
fail = true;
await source.snapshot({ bbox: [31, 59, 32, 60] });
const status = await source.status();
assert.equal(status.upstreamState, "degraded");
assert.equal(status.lastFailure, "reference_station_upstream_unavailable");
assert.ok(status.lastFailureAt);
assert.ok(!JSON.stringify(status).includes("overpass.internal.example"));
});
test("global station search is normalized, deduplicated and persisted", async () => {
const cacheDir = await mkdtemp(join(tmpdir(), "nodedc-reference-stations-"));
let calls = 0;
const source = await createReferenceStationSource({
seedFile,
cacheDir,
fetchImpl: async (_url, options) => {
calls += 1;
const body = new URLSearchParams(options.body);
assert.match(body.get("data"), /Петроградская/u);
return new Response(JSON.stringify({
elements: [{
type: "node",
id: 987654323,
lat: 59.96639,
lon: 30.3113,
tags: {
railway: "station",
station: "subway",
name: "Petrogradskaya",
"name:ru": "Петроградская",
website: "https://not-projected.invalid",
},
}],
}), { status: 200, headers: { "content-type": "application/json" } });
},
});
const first = await source.search({ query: "Петроградская", limit: 8 });
const second = await source.search({ query: "Петроградская", limit: 8 });
assert.equal(first.schemaVersion, MAP_REFERENCE_SEARCH_SCHEMA);
assert.equal(first.complete, true);
assert.equal(calls, 1);
assert.deepEqual(first.facts, second.facts);
assert.equal(first.facts[0].attributes.name, "Петроградская");
assert.ok(!JSON.stringify(first).includes("not-projected"));
const persisted = JSON.parse(await readFile(
join(cacheDir, "reference-features", `${TRANSPORT_STATION_PROFILE_ID}.search.json`),
"utf8",
));
assert.equal(persisted.schemaVersion, "nodedc.map-reference-search-index/v1");
const restarted = await createReferenceStationSource({
seedFile,
cacheDir,
fetchEnabled: false,
});
const cached = await restarted.search({ query: "Петроградская", limit: 8 });
assert.equal(cached.complete, true);
assert.equal(cached.facts[0].sourceId, "osm.node.987654323");
});