mirror of
https://github.com/zvx-echo6/navi.git
synced 2026-08-26 17:31:37 +00:00
Auto: foot-as-last-resort fallback when picked mode fails to route
If the capability-picked mode cannot route, retry foot ONCE (foot always routes modulo bbox limits) instead of surfacing a wall to the user. On success, ship the foot route tagged with auto_fallback_from=<picked mode> for the UI; on foot failure, return the original error. No fallback when the picked mode is already foot. selected_mode_set still reflects the original capability intersection. Tests: no_fallthrough -> falls_back_to_foot_on_error; + both-fail returns error; + no-fallback-when-picked-is-foot. Full offroute suite: 86 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
63aea8aea5
commit
91711f5f3f
2 changed files with 58 additions and 4 deletions
|
|
@ -882,6 +882,29 @@ class OffrouteRouter:
|
|||
logger.info("auto: classified mode=%s, routed once in %.2fs",
|
||||
mode, time.perf_counter() - _probe_t0)
|
||||
|
||||
# Foot-as-last-resort: foot always routes (modulo bbox limits), so if the
|
||||
# capability-picked mode failed, fall back to foot ONCE rather than surface a
|
||||
# wall to the user. selected_mode_set still reflects the original eligibility.
|
||||
if result.get("status") != "ok" and mode != "foot":
|
||||
_foot_t0 = time.perf_counter()
|
||||
foot_result = self.route(
|
||||
start_lat, start_lon, end_lat, end_lon,
|
||||
mode="foot", boundary_mode=boundary_mode, annotate_mvum=False
|
||||
)
|
||||
if foot_result.get("status") == "ok":
|
||||
best_result = foot_result
|
||||
best_result["selected_mode"] = "foot"
|
||||
best_result["auto_fallback_from"] = mode # surface to client/UI
|
||||
best_minutes = (foot_result.get("summary") or {}).get(
|
||||
"total_effort_minutes", float("inf"))
|
||||
last_error = None
|
||||
logger.info("auto: %s failed, foot fallback succeeded in %.2fs",
|
||||
mode, time.perf_counter() - _foot_t0)
|
||||
else:
|
||||
# foot also failed -- keep the original last_error (return original error)
|
||||
logger.info("auto: %s failed, foot fallback also failed in %.2fs",
|
||||
mode, time.perf_counter() - _foot_t0)
|
||||
|
||||
if best_result is not None:
|
||||
# MVUM Layer 3a: a "drive to a trailhead, switch, continue offroad" plan may
|
||||
# beat the single-mode winner on long trips. If so, return it instead.
|
||||
|
|
|
|||
|
|
@ -326,9 +326,26 @@ def test_route_auto_picks_capability_mode(monkeypatch):
|
|||
assert calls == ["vehicle"] # ONE route call, not four
|
||||
|
||||
|
||||
def test_route_auto_no_fallthrough_on_route_error(monkeypatch):
|
||||
# Route-once: if the capability-picked mode (vehicle) cannot route, the error is
|
||||
# returned -- classify-once does NOT fall through to other modes (PR1 trade-off).
|
||||
def test_route_auto_falls_back_to_foot_on_error(monkeypatch):
|
||||
# Foot-as-last-resort: the capability-picked mode (vehicle) fails, so Auto retries
|
||||
# foot ONCE and ships it, tagging auto_fallback_from for the UI.
|
||||
_typed_all(monkeypatch)
|
||||
calls = []
|
||||
per_mode = {m: {"status": "error", "message": f"{m} failed"} for m in AUTO_MODE_PRIORITY}
|
||||
per_mode["foot"] = {"status": "ok"}
|
||||
monkeypatch.setattr(OffrouteRouter, "route", _stub_route(per_mode, calls))
|
||||
r = object.__new__(OffrouteRouter)
|
||||
out = r._route_auto(42.0, -114.0, 42.5, -114.5, "pragmatic")
|
||||
assert out["status"] == "ok"
|
||||
assert out["selected_mode"] == "foot"
|
||||
assert out["auto_fallback_from"] == "vehicle"
|
||||
assert out["selected_mode_set"] == sorted(ALL_MODES) # original eligibility, not foot-only
|
||||
assert calls == ["vehicle", "foot"]
|
||||
|
||||
|
||||
def test_route_auto_returns_error_when_picked_and_foot_both_fail(monkeypatch):
|
||||
# Picked mode AND the foot fallback both fail -> original error surfaces, exactly
|
||||
# two attempts (picked, then foot).
|
||||
_typed_all(monkeypatch)
|
||||
calls = []
|
||||
per_mode = {m: {"status": "error", "message": f"{m} failed"} for m in AUTO_MODE_PRIORITY}
|
||||
|
|
@ -338,7 +355,21 @@ def test_route_auto_no_fallthrough_on_route_error(monkeypatch):
|
|||
assert out["status"] == "error"
|
||||
assert "selected_mode" not in out
|
||||
assert out["selected_mode_set"] == sorted(ALL_MODES)
|
||||
assert calls == ["vehicle"] # only the picked mode is attempted
|
||||
assert calls == ["vehicle", "foot"]
|
||||
|
||||
|
||||
def test_route_auto_no_fallback_when_picked_is_foot(monkeypatch):
|
||||
# When the picked mode is already foot (foot-only intersection), there is no second
|
||||
# attempt -- foot cannot fall back to itself.
|
||||
calls = []
|
||||
per_mode = {"foot": {"status": "error", "message": "foot failed"}}
|
||||
monkeypatch.setattr(OffrouteRouter, "route", _stub_route(per_mode, calls))
|
||||
r = object.__new__(OffrouteRouter)
|
||||
out = r._route_auto(42.0, -114.0, 42.5, -114.5, "pragmatic",
|
||||
start_category="building:house", end_category="natural:peak") # -> {foot}
|
||||
assert out["status"] == "error"
|
||||
assert out["selected_mode_set"] == ["foot"]
|
||||
assert calls == ["foot"] # no fallback attempt
|
||||
|
||||
|
||||
def test_route_auto_tagged_road_to_road_no_spatial_probe(monkeypatch):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue