trips/router/internal/traffic/traffic_test.go
Greg Pomerantz fc9cf44052 traffic: live-traffic pipeline (511NY → Overpass match → OSRM segment speeds)
Verified end-to-end on the local OSRM stack:
- internal/traffic: 511NY OData client (lenient decode, raw archive),
  Overpass way matcher (class-window selection: highest-class road within
  150m beats a closer service road), incident policy map, CSV builder
- cmd/traffic: fetch | match | apply; apply copies the dataset (CoW) and
  re-customizes the traffic layer in ~15s (v26 customize has no
  --output-prefix; it writes contract files in place)
- Proof: synthetic I-90 closure → 38s (static :5000) vs 142s (traffic
  :5002) on the same 0.87 km stretch
- Policy speeds are 'assumed' provenance; R4: traffic routes must carry
  computed(traffic, as_of, policy=assumed)
2026-09-06 21:22:36 -04:00

113 lines
3.2 KiB
Go

package traffic
import (
"strings"
"testing"
)
func TestPolicyFor(t *testing.T) {
cases := []struct {
in string
ok bool
kph int
}{
{"Road Closed", true, 5},
{"ACCIDENT", true, 25},
{"Accident - Multi Vehicle", true, 25},
{"road work", true, 50},
{"Bridge Outage (Unknown)", false, 0},
{"", false, 0},
}
for _, c := range cases {
p, ok := PolicyFor(c.in)
if ok != c.ok || (ok && p.SpeedKmh != c.kph) {
t.Errorf("PolicyFor(%q) = %+v, %v; want ok=%v kph=%d", c.in, p, ok, c.ok, c.kph)
}
}
}
func TestBuildCSV(t *testing.T) {
// same pair, two incidents → most severe (highest weight) wins
csv := BuildCSV([]Assignment{
{Pair: [2]int64{100, 200}, SpeedKmh: 50, Weight: 40},
{Pair: [2]int64{200, 100}, SpeedKmh: 5, Weight: 100}, // reversed order, must dedupe
{Pair: [2]int64{300, 400}, SpeedKmh: 25, Weight: 90},
})
lines := strings.Split(strings.TrimSpace(csv), "\n")
if len(lines) != 2 {
t.Fatalf("want 2 rows, got %d: %v", len(lines), lines)
}
if lines[0] != "100,200,5" {
t.Errorf("row0 = %q; want 100,200,5", lines[0])
}
if lines[1] != "300,400,25" {
t.Errorf("row1 = %q; want 300,400,25", lines[1])
}
}
func TestClassRank(t *testing.T) {
if !(classRank("motorway") > classRank("trunk") && classRank("trunk") > classRank("primary") &&
classRank("primary") > classRank("residential")) {
t.Errorf("classRank ordering wrong")
}
}
func TestClosestNodePairPrefersClass(t *testing.T) {
// motorway at 120m, service at 20m → motorway wins (within class window)
ways := []Way{
{
ID: 1, Highway: "service",
Nodes: []WayNode{{ID: 11, Lat: 45.0000, Lon: -73.0000}, {ID: 12, Lat: 45.0000, Lon: -72.9990}},
},
{
ID: 2, Highway: "motorway",
Nodes: []WayNode{{ID: 21, Lat: 45.0011, Lon: -73.0000}, {ID: 22, Lat: 45.0011, Lon: -72.9990}},
},
}
// point 20m from service line (lat 45.0000) and ~120m from motorway line (lat 45.0011)
way, a, b, _, ok := ClosestNodePair(ways, 45.0002, -73.0)
if !ok {
t.Fatal("no match")
}
if way.Highway != "motorway" || a != 21 || b != 22 {
t.Errorf("got %s (%d→%d); want motorway (21→22)", way.Highway, a, b)
}
}
func TestClosestNodePairNearestFallback(t *testing.T) {
// only a service road exists, 500m away → nearest still wins
ways := []Way{
{
ID: 1, Highway: "service",
Nodes: []WayNode{{ID: 11, Lat: 45.0050, Lon: -73.0000}, {ID: 12, Lat: 45.0050, Lon: -72.9900}},
},
}
way, _, _, d, ok := ClosestNodePair(ways, 45.0, -73.0)
if !ok || way.ID != 1 {
t.Fatalf("got ok=%v way=%+v", ok, way)
}
if d < 500 || d > 560 {
t.Errorf("dist = %.0f; want ~500", d)
}
}
func TestDecodeEvents(t *testing.T) {
// OData v3 envelope
body := []byte(`{"d":[{"EventType":"Road Closed","Description":"I-90 closed","City":"Albany","Location":{"Latitude":42.75,"Longitude":-73.93}},{"EventType":"Accident","lat":42.1,"lon":-73.5}]}`)
events, err := decodeEvents(body)
if err != nil {
t.Fatal(err)
}
if len(events) != 2 {
t.Fatalf("want 2 events, got %d", len(events))
}
e0 := events[0]
if e0.Type != "Road Closed" || !e0.Valid() || e0.Lat != 42.75 || e0.Lon != -73.93 {
t.Errorf("event0 = %+v", e0)
}
e1 := events[1]
if e1.Type != "Accident" || !e1.Valid() {
t.Errorf("event1 = %+v", e1)
}
}