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"); });