diff --git a/web/src/config.test.ts b/web/src/config.test.ts new file mode 100644 index 0000000..1c4e4a0 --- /dev/null +++ b/web/src/config.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect } from "vitest"; +import { VEHICLE_PROFILES, CLASS_LABELS } from "./config"; + +// Canonical pipeline tokens, hardcoded from pipeline/normalize.py:50-72 +// (CLASS_DATEFIELD keys + EBIKE_FIELDS keys) as the source of truth. +const CANONICAL_TOKENS = new Set([ + "passenger", + "high_clearance", + "truck", + "bus", + "motorhome", + "4wd_gt50", + "2wd_gt50", + "tracked_ohv_gt50", + "other_ohv_gt50", + "atv", + "motorcycle", + "other_wheeled_ohv", + "tracked_ohv_lt50", + "other_ohv_lt50", + "e_bike1", + "e_bike2", + "e_bike3", +]); + +describe("VEHICLE_PROFILES", () => { + it("suv4x4 includes the 4WD/2WD >50″ classes so gt50-only trails show open", () => { + const suv4x4 = VEHICLE_PROFILES.find((p) => p.key === "suv4x4"); + expect(suv4x4).toBeDefined(); + expect(suv4x4!.tokens).toContain("4wd_gt50"); + expect(suv4x4!.tokens).toContain("2wd_gt50"); + }); + + it("every token used by every profile has a label in CLASS_LABELS", () => { + for (const profile of VEHICLE_PROFILES) { + for (const token of profile.tokens) { + expect( + typeof CLASS_LABELS[token] === "string" && CLASS_LABELS[token].length > 0, + `expected CLASS_LABELS["${token}"] (used by profile "${profile.key}") to be a non-empty string`, + ).toBe(true); + } + } + }); + + it("every token used by every profile is a canonical pipeline token", () => { + for (const profile of VEHICLE_PROFILES) { + for (const token of profile.tokens) { + expect( + CANONICAL_TOKENS.has(token), + `token "${token}" (used by profile "${profile.key}") is not in the canonical pipeline token list`, + ).toBe(true); + } + } + }); + + // Deliberate: see plans/003 "decided mapping" — whether a green-sticker + // UTV >50″ counts as a "4WD vehicle >50″" for a given forest's + // designation is a genuine domain ambiguity, so utv_wide is left + // unchanged. Changing this requires the domain check noted in plans/003's + // maintenance notes (checking against an official MVUM PDF). + it("utv_wide tokens are unchanged (deliberate, see plans/003)", () => { + const utvWide = VEHICLE_PROFILES.find((p) => p.key === "utv_wide"); + expect(utvWide).toBeDefined(); + expect(utvWide!.tokens).toEqual(["other_ohv_gt50", "tracked_ohv_gt50"]); + }); +}); diff --git a/web/src/config.ts b/web/src/config.ts index a9a58bc..d230cd3 100644 --- a/web/src/config.ts +++ b/web/src/config.ts @@ -8,6 +8,11 @@ // So each selector option is a *profile* that maps to a set of underlying MVUM // class tokens (emitted in `classes` by pipeline/normalize.py); a route is open // to the profile if it permits ANY of those tokens. +// +// suv4x4 includes the 4WD/2WD >50″ classes: MVUM designates by vehicle +// type/width, and a street-legal 4×4 IS a 4WD >50″ — without these tokens, +// gt50-only jeep trails (e.g. Bronco Peak, Cleveland NF) would never show +// open for anyone. export interface VehicleProfile { key: string; @@ -19,7 +24,7 @@ export interface VehicleProfile { export const VEHICLE_PROFILES: VehicleProfile[] = [ // Street-legal: highway-legal roads are the big set; plated bikes add moto trails. { key: "moto_plated", label: "Dual-sport motorcycle (plated)", group: "Street-legal (plated)", tokens: ["passenger", "high_clearance", "motorcycle"] }, - { key: "suv4x4", label: "SUV / 4×4 / truck (street-legal)", group: "Street-legal (plated)", tokens: ["passenger", "high_clearance"] }, + { key: "suv4x4", label: "SUV / 4×4 / truck (street-legal)", group: "Street-legal (plated)", tokens: ["passenger", "high_clearance", "4wd_gt50", "2wd_gt50"] }, { key: "car", label: "Car / passenger (street-legal)", group: "Street-legal (plated)", tokens: ["passenger"] }, // Off-road only: OHV-designated routes for that class. { key: "dirtbike", label: "Dirt bike (OHV / green sticker)", group: "Off-road only (green / red sticker)", tokens: ["motorcycle"] },