205 lines
7.8 KiB
JavaScript
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");
|
|
});
|