Covers routes/activities that enter the journal without BRouter waytags (imports, uploads, pre-existing rows): - overpass-ways.server.ts: server-side Overpass client (way[highway] + geom in a bbox, configurable OVERPASS_URLS, timeout + oversized-bbox guard). - surface-match.server.ts: pure nearest-way map-matcher → per-segment surface/highway (unmatched → unknown). Unit-tested. - surface-backfill pg-boss job: load geom → skip if breakdown exists → Overpass → match → computeSurfaceBreakdown → store → emit `surface_breakdown` SSE to the owner. Idempotent, retry-safe, best-effort; registered in server.ts. - Enqueued from createActivity (imports/uploads) + owner-on-open in the route & activity detail loaders (non-Planner routes, old rows), deduped via singletonKey. - useSurfaceBackfillUpdates: detail pages subscribe to /api/events and revalidate() when their row's backfill lands (live bars, no reload). - Privacy manifest updated (DE + EN) for the Overpass bbox lookup. Tests: matchSurfaces unit; surface-backfill job (mocked Overpass/db/events: store + emit, skip-if-present, skip-if-no-ways). Verified the real Overpass→match→breakdown pipeline against a live Berlin bbox (509 ways → plausible asphalt/paving_stones/footway mix). typecheck + lint + unit (journal 333) green. Note: the worker runs via server.ts (prod/staging), not `react-router dev`, so the live job + SSE exercise on a deployed instance. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
56 lines
2.3 KiB
TypeScript
56 lines
2.3 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
const execute = vi.fn();
|
|
vi.mock("../lib/db.ts", () => ({ getDb: () => ({ execute }) }));
|
|
|
|
const fetchWaysInBbox = vi.fn();
|
|
vi.mock("../lib/overpass-ways.server.ts", () => ({ fetchWaysInBbox: (...a: unknown[]) => fetchWaysInBbox(...a) }));
|
|
|
|
const emitTo = vi.fn();
|
|
vi.mock("../lib/events.server.ts", () => ({ emitTo: (...a: unknown[]) => emitTo(...a) }));
|
|
|
|
vi.mock("../lib/logger.server.ts", () => ({ logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() } }));
|
|
|
|
import { runSurfaceBackfill } from "./surface-backfill.ts";
|
|
|
|
const lineString = (coords: number[][]) => JSON.stringify({ type: "LineString", coordinates: coords });
|
|
|
|
beforeEach(() => {
|
|
execute.mockReset();
|
|
fetchWaysInBbox.mockReset();
|
|
emitTo.mockReset();
|
|
});
|
|
|
|
describe("runSurfaceBackfill", () => {
|
|
it("matches ways, stores the breakdown, and emits to the owner", async () => {
|
|
execute.mockResolvedValueOnce([
|
|
{ geojson: lineString([[0, 0], [0.001, 0], [0.002, 0]]), ownerId: "user-1", hasBreakdown: false },
|
|
]); // SELECT
|
|
execute.mockResolvedValueOnce(undefined); // UPDATE
|
|
fetchWaysInBbox.mockResolvedValue([
|
|
{ highway: "residential", surface: "asphalt", geometry: [{ lat: 0, lon: 0 }, { lat: 0, lon: 0.01 }] },
|
|
]);
|
|
|
|
await runSurfaceBackfill("activity", "act-1");
|
|
|
|
expect(fetchWaysInBbox).toHaveBeenCalledOnce();
|
|
expect(execute).toHaveBeenCalledTimes(2); // SELECT + UPDATE
|
|
expect(emitTo).toHaveBeenCalledWith("user-1", "surface_breakdown", { kind: "activity", id: "act-1" });
|
|
});
|
|
|
|
it("skips a row that already has a breakdown", async () => {
|
|
execute.mockResolvedValueOnce([{ geojson: lineString([[0, 0], [1, 0]]), ownerId: "u", hasBreakdown: true }]);
|
|
await runSurfaceBackfill("route", "r-1");
|
|
expect(fetchWaysInBbox).not.toHaveBeenCalled();
|
|
expect(execute).toHaveBeenCalledTimes(1); // only the SELECT
|
|
expect(emitTo).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not store or emit when Overpass returns no ways", async () => {
|
|
execute.mockResolvedValueOnce([{ geojson: lineString([[0, 0], [0.001, 0]]), ownerId: "u", hasBreakdown: false }]);
|
|
fetchWaysInBbox.mockResolvedValue([]);
|
|
await runSurfaceBackfill("activity", "a-2");
|
|
expect(execute).toHaveBeenCalledTimes(1); // SELECT only
|
|
expect(emitTo).not.toHaveBeenCalled();
|
|
});
|
|
});
|