diff --git a/agents/dispatch_agent.py b/agents/dispatch_agent.py index b8bfa93..3d2a035 100644 --- a/agents/dispatch_agent.py +++ b/agents/dispatch_agent.py @@ -34,10 +34,18 @@ DISPATCH_STREAM = os.getenv("DISPATCH_STREAM", "") # The LLM is only consulted before the first alert, so cost is bounded per zone. DISPATCH_REALERT_EVERY = int(os.getenv("DISPATCH_REALERT_EVERY", "100")) -# A miler counts as available only if the Go backend's presence key says so. -# The milers:locations geo index keeps last-known positions indefinitely, so +# Presence, as reported by the Go backend's miler_status: key. The +# milers:locations geo index keeps last-known positions indefinitely, so # membership alone says nothing about whether anyone is actually on duty. +# +# Three-state on purpose. Only 2 of 34 milers had a status key in production +# (2026-09-22), so treating "no key" as off-duty would report "nobody +# available" even where the backend can assign — a confident wrong answer. +# Absent presence data is reported as unknown and the decision is told so. MILER_AVAILABLE_STATUSES = {"available"} +MILER_UNAVAILABLE_STATUSES = {"break", "offline", "busy", "onbreak", "off_duty", "unavailable"} + +AVAILABLE, UNAVAILABLE, UNKNOWN = "available", "unavailable", "unknown" class DispatchAgent(SpecializedAgent): @@ -138,25 +146,34 @@ class DispatchAgent(SpecializedAgent): # Redis GEO lookup # # ------------------------------------------------------------------ # - async def _miler_is_available(self, miler_id) -> bool: - """Liveness check against the backend-owned miler_status: key - ({"userid": .., "status": "Available" | "Break" | ...}). Missing or - unparseable → not available: geo-index presence alone is not evidence.""" + async def _miler_presence(self, miler_id) -> str: + """AVAILABLE / UNAVAILABLE / UNKNOWN from the backend-owned + miler_status: key ({"userid": .., "status": "Available"|"Break"|..}). + + No key, an unparseable value, or an unrecognised status → UNKNOWN, never + UNAVAILABLE: most milers have no key at all, and absence of evidence is + not evidence of absence.""" try: raw = await self._redis.get(f"miler_status:{miler_id}") if not raw: - return False - status = json.loads(raw).get("status", "") - return str(status).lower() in MILER_AVAILABLE_STATUSES + return UNKNOWN + status = str(json.loads(raw).get("status", "")).lower() except Exception: - return False + return UNKNOWN + if status in MILER_AVAILABLE_STATUSES: + return AVAILABLE + if status in MILER_UNAVAILABLE_STATUSES: + return UNAVAILABLE + return UNKNOWN async def _find_zone( self, lat: float, lon: float, radius_km: int = 10 ) -> Optional[Dict[str, Any]]: - """GEORADIUS sweep on milers:locations filtered to milers whose status - key says they are available. Returns the nearest available miler (with - how many index entries were skipped as stale/off-duty), or None.""" + """GEORADIUS sweep on milers:locations, preferring a miler the backend + reports Available. If none is confirmed Available but some have no + presence data, the nearest of those is returned with presence=UNKNOWN — + it may well be assignable. Returns None only when every candidate is + confirmed unavailable (or there are none).""" try: results = await self._redis.georadius( "milers:locations", @@ -168,21 +185,32 @@ class DispatchAgent(SpecializedAgent): if not results: return None + first_unknown = None for candidate in results: - if await self._miler_is_available(candidate): - miler_info = await self._redis.hgetall(f"miler:{candidate}") - return { - "miler_id": candidate, - "hub_id": miler_info.get("hub_id", "UNKNOWN"), - "zone_id": miler_info.get("zone_id", "unknown"), - "avg_delivery_time": int(miler_info.get("avg_delivery_time", 60)), - "stale_candidates_skipped": len(results) - 1, - } + presence = await self._miler_presence(candidate) + if presence == AVAILABLE: + return await self._miler_record(candidate, AVAILABLE, results) + if presence == UNKNOWN and first_unknown is None: + first_unknown = candidate + + if first_unknown is not None: + return await self._miler_record(first_unknown, UNKNOWN, results) return None except Exception as e: logger.warning(f"Redis GEORADIUS error: {e}") return None + async def _miler_record(self, miler_id, presence, candidates) -> Dict[str, Any]: + miler_info = await self._redis.hgetall(f"miler:{miler_id}") + return { + "miler_id": miler_id, + "presence": presence, + "hub_id": miler_info.get("hub_id", "UNKNOWN"), + "zone_id": miler_info.get("zone_id", "unknown"), + "avg_delivery_time": int(miler_info.get("avg_delivery_time", 60)), + "candidates_considered": len(candidates), + } + async def _count_in_geo_index(self, lat: float, lon: float, radius_km: int = 30) -> Optional[int]: """Raw geo-index membership (any status) — reported separately so the decision can tell 'nobody registered here' from 'riders exist but none on duty'.""" @@ -327,22 +355,30 @@ class DispatchAgent(SpecializedAgent): facts["has_coordinates"] = has_coords nearest_km = None + nearest_presence = None if has_coords: for radius in (10, 20, 30): try: - if await self._find_zone(lat, lon, radius_km=radius): - nearest_km = radius - break + found = await self._find_zone(lat, lon, radius_km=radius) except Exception as e: logger.warning(f"assignment facts: GEORADIUS {radius}km failed: {e}") break + if found: + nearest_km = radius + nearest_presence = found.get("presence", UNKNOWN) + break if not has_coords: - facts["nearest_available_miler_within_km"] = "unknown (no coordinates)" + facts["nearest_miler_within_km"] = "unknown (no coordinates)" + facts["nearest_miler_presence"] = "unknown (no coordinates)" elif nearest_km is None: - facts["nearest_available_miler_within_km"] = "none within 30km" + facts["nearest_miler_within_km"] = "none within 30km" + facts["nearest_miler_presence"] = "none found" else: - facts["nearest_available_miler_within_km"] = nearest_km + facts["nearest_miler_within_km"] = nearest_km + # "available" = backend confirms on duty; "unknown" = no presence + # data for any nearby miler, so this is not evidence of a gap. + facts["nearest_miler_presence"] = nearest_presence # Distinguish "no riders registered here" from "riders exist, none on duty". if has_coords: diff --git a/core/llm.py b/core/llm.py index 60fe250..93e9557 100644 --- a/core/llm.py +++ b/core/llm.py @@ -181,13 +181,20 @@ fallback failed, so no rider was assigned. Decide how to react. Choose exactly o - "ops_alert": a genuine coverage gap in this zone (repeated failures, no nearby riders). Alert operations to onboard or redirect riders here. - "escalate": ambiguous or contradictory — e.g. riders ARE nearby yet assignment keeps failing, which suggests a systemic issue rather than a coverage gap. A human dispatcher should look. -Weigh how many times assignment has failed in this zone today, how far the nearest *available* rider is, -the time of day, and whether coordinates were even available. "nearest_available_miler_within_km" counts -only riders whose live status is Available; "milers_in_geo_index_within_30km" is the raw count of riders -ever seen nearby (it includes off-duty and stale entries), so a large index count with no available rider -means riders exist here but nobody is on duty — a staffing gap, not a systemic fault. A single failure with -an available rider nearby is usually transient; repeated failures with no available rider is a coverage -gap; repeated failures *despite* available riders nearby is not a coverage gap and warrants a human. +Weigh how many times assignment has failed in this zone today, how far the nearest rider is and whether +that rider is actually on duty, the time of day, and whether coordinates were even available. + +Read the rider facts carefully: +- "nearest_miler_within_km": distance to the nearest rider in the location index. +- "nearest_miler_presence": "available" = the backend confirms that rider is on duty; + "unavailable" = confirmed off duty / on break; "unknown" = there is NO presence data for nearby + riders. Unknown is not evidence of a coverage gap — do not treat it as "nobody is working". +- "milers_in_geo_index_within_30km": raw count of riders ever seen nearby, including stale entries. + +A single failure with a rider nearby is usually transient. Repeated failures with no rider at all within +30 km is a genuine coverage gap. Repeated failures *despite* a confirmed-available rider nearby is not a +coverage gap but a systemic issue — escalate to a human. When presence is unknown, lean toward monitoring +or escalating for a human to check rather than declaring a coverage gap. Report an honest confidence in [0, 1].""" _ASSIGNMENT_SCHEMA = { diff --git a/evals/assignment_cases.jsonl b/evals/assignment_cases.jsonl index e37043f..433d351 100644 --- a/evals/assignment_cases.jsonl +++ b/evals/assignment_cases.jsonl @@ -1,11 +1,12 @@ -{"id": "first-failure-close-rider", "hour_of_day_utc": 14, "facts": {"zone_id": "hyderabad", "failures_today": 1, "has_coordinates": true, "nearest_available_miler_within_km": 10, "milers_in_geo_index_within_30km": 3, "alert_already_sent_today": false}, "acceptable": ["monitor", "notify_customer"], "ideal": "monitor", "rationale": "Single failure with a rider only 10km away \u2014 almost certainly transient; let the backend retry."} -{"id": "repeated-zone-gap", "hour_of_day_utc": 12, "facts": {"zone_id": "pune", "failures_today": 4, "has_coordinates": true, "nearest_available_miler_within_km": "none within 30km", "milers_in_geo_index_within_30km": 0, "alert_already_sent_today": false}, "acceptable": ["ops_alert", "escalate"], "ideal": "ops_alert", "rationale": "Repeated failures and no rider within 30km \u2014 a genuine coverage gap; alert ops."} -{"id": "second-failure-far-rider", "hour_of_day_utc": 18, "facts": {"zone_id": "bangalore", "failures_today": 2, "has_coordinates": true, "nearest_available_miler_within_km": 30, "milers_in_geo_index_within_30km": 3, "alert_already_sent_today": false}, "acceptable": ["notify_customer", "monitor"], "ideal": "notify_customer", "rationale": "A couple of failures with the nearest rider 30km out during rush \u2014 real delay; keep the customer informed."} -{"id": "no-coordinates-first", "hour_of_day_utc": 10, "facts": {"zone_id": "unknown", "failures_today": 1, "has_coordinates": false, "nearest_available_miler_within_km": "unknown (no coordinates)", "alert_already_sent_today": false}, "acceptable": ["monitor", "escalate"], "ideal": "monitor", "rationale": "Coverage can't be assessed without coordinates, but a single failure is most likely transient."} -{"id": "many-failures-riders-near", "hour_of_day_utc": 15, "facts": {"zone_id": "mumbai_west", "failures_today": 5, "has_coordinates": true, "nearest_available_miler_within_km": 10, "milers_in_geo_index_within_30km": 3, "alert_already_sent_today": false}, "acceptable": ["escalate", "ops_alert"], "ideal": "escalate", "rationale": "Riders ARE nearby yet assignment keeps failing \u2014 not a coverage gap; a systemic issue for a human."} -{"id": "night-coverage-gap", "hour_of_day_utc": 2, "facts": {"zone_id": "kolkata", "failures_today": 3, "has_coordinates": true, "nearest_available_miler_within_km": "none within 30km", "milers_in_geo_index_within_30km": 0, "alert_already_sent_today": false}, "acceptable": ["ops_alert", "escalate"], "ideal": "ops_alert", "rationale": "Repeated overnight failures with no nearby rider \u2014 a coverage gap worth flagging to ops."} -{"id": "single-far-rider", "hour_of_day_utc": 9, "facts": {"zone_id": "hyderabad", "failures_today": 1, "has_coordinates": true, "nearest_available_miler_within_km": 30, "milers_in_geo_index_within_30km": 3, "alert_already_sent_today": false}, "acceptable": ["monitor", "notify_customer"], "ideal": "monitor", "rationale": "One failure, distant rider \u2014 likely resolves on retry."} -{"id": "persistent-far-rush", "hour_of_day_utc": 18, "facts": {"zone_id": "mumbai_east", "failures_today": 3, "has_coordinates": true, "nearest_available_miler_within_km": 30, "milers_in_geo_index_within_30km": 3, "alert_already_sent_today": false}, "acceptable": ["notify_customer", "ops_alert"], "ideal": "notify_customer", "rationale": "Rush-hour, repeated, distant rider \u2014 the customer should hear about the delay."} -{"id": "borderline-count", "hour_of_day_utc": 13, "facts": {"zone_id": "north_delhi", "failures_today": 3, "has_coordinates": true, "nearest_available_miler_within_km": 20, "milers_in_geo_index_within_30km": 3, "alert_already_sent_today": false}, "acceptable": ["notify_customer", "ops_alert", "monitor"], "ideal": "notify_customer", "rationale": "Genuinely borderline \u2014 a few failures with a moderately-close rider; any of these is defensible."} -{"id": "severe-gap", "hour_of_day_utc": 11, "facts": {"zone_id": "pune", "failures_today": 8, "has_coordinates": true, "nearest_available_miler_within_km": "none within 30km", "milers_in_geo_index_within_30km": 0, "alert_already_sent_today": false}, "acceptable": ["ops_alert", "escalate"], "ideal": "ops_alert", "rationale": "Many failures and no rider anywhere near \u2014 a clear, severe coverage gap."} -{"id": "index-full-nobody-on-duty", "hour_of_day_utc": 7, "facts": {"zone_id": "coimbatore", "failures_today": 6, "nearest_available_miler_within_km": "none within 30km", "milers_in_geo_index_within_30km": 26, "has_coordinates": true, "alert_already_sent_today": false}, "acceptable": ["ops_alert", "escalate"], "ideal": "ops_alert", "rationale": "26 riders have been seen here but none is Available \u2014 a staffing/on-duty gap; ops needs to get riders online, not a systemic fault."} +{"id": "first-failure-close-rider", "hour_of_day_utc": 14, "facts": {"zone_id": "hyderabad", "failures_today": 1, "nearest_miler_within_km": 10, "nearest_miler_presence": "available", "milers_in_geo_index_within_30km": 3, "has_coordinates": true, "alert_already_sent_today": false}, "acceptable": ["monitor", "notify_customer"], "ideal": "monitor", "rationale": "Single failure with a rider only 10km away \u2014 almost certainly transient; let the backend retry."} +{"id": "repeated-zone-gap", "hour_of_day_utc": 12, "facts": {"zone_id": "pune", "failures_today": 4, "nearest_miler_within_km": "none within 30km", "nearest_miler_presence": "none found", "milers_in_geo_index_within_30km": 0, "has_coordinates": true, "alert_already_sent_today": false}, "acceptable": ["ops_alert", "escalate"], "ideal": "ops_alert", "rationale": "Repeated failures and no rider within 30km \u2014 a genuine coverage gap; alert ops."} +{"id": "second-failure-far-rider", "hour_of_day_utc": 18, "facts": {"zone_id": "bangalore", "failures_today": 2, "nearest_miler_within_km": 30, "nearest_miler_presence": "available", "milers_in_geo_index_within_30km": 3, "has_coordinates": true, "alert_already_sent_today": false}, "acceptable": ["notify_customer", "monitor"], "ideal": "notify_customer", "rationale": "A couple of failures with the nearest rider 30km out during rush \u2014 real delay; keep the customer informed."} +{"id": "no-coordinates-first", "hour_of_day_utc": 10, "facts": {"zone_id": "unknown", "failures_today": 1, "nearest_miler_within_km": "unknown (no coordinates)", "nearest_miler_presence": "unknown (no coordinates)", "has_coordinates": false, "alert_already_sent_today": false}, "acceptable": ["monitor", "escalate"], "ideal": "monitor", "rationale": "Coverage can't be assessed without coordinates, but a single failure is most likely transient."} +{"id": "many-failures-riders-near", "hour_of_day_utc": 15, "facts": {"zone_id": "mumbai_west", "failures_today": 5, "nearest_miler_within_km": 10, "nearest_miler_presence": "available", "milers_in_geo_index_within_30km": 3, "has_coordinates": true, "alert_already_sent_today": false}, "acceptable": ["escalate", "ops_alert"], "ideal": "escalate", "rationale": "Riders ARE nearby yet assignment keeps failing \u2014 not a coverage gap; a systemic issue for a human."} +{"id": "night-coverage-gap", "hour_of_day_utc": 2, "facts": {"zone_id": "kolkata", "failures_today": 3, "nearest_miler_within_km": "none within 30km", "nearest_miler_presence": "none found", "milers_in_geo_index_within_30km": 0, "has_coordinates": true, "alert_already_sent_today": false}, "acceptable": ["ops_alert", "escalate"], "ideal": "ops_alert", "rationale": "Repeated overnight failures with no nearby rider \u2014 a coverage gap worth flagging to ops."} +{"id": "single-far-rider", "hour_of_day_utc": 9, "facts": {"zone_id": "hyderabad", "failures_today": 1, "nearest_miler_within_km": 30, "nearest_miler_presence": "available", "milers_in_geo_index_within_30km": 3, "has_coordinates": true, "alert_already_sent_today": false}, "acceptable": ["monitor", "notify_customer"], "ideal": "monitor", "rationale": "One failure, distant rider \u2014 likely resolves on retry."} +{"id": "persistent-far-rush", "hour_of_day_utc": 18, "facts": {"zone_id": "mumbai_east", "failures_today": 3, "nearest_miler_within_km": 30, "nearest_miler_presence": "available", "milers_in_geo_index_within_30km": 3, "has_coordinates": true, "alert_already_sent_today": false}, "acceptable": ["notify_customer", "ops_alert"], "ideal": "notify_customer", "rationale": "Rush-hour, repeated, distant rider \u2014 the customer should hear about the delay."} +{"id": "borderline-count", "hour_of_day_utc": 13, "facts": {"zone_id": "north_delhi", "failures_today": 3, "nearest_miler_within_km": 20, "nearest_miler_presence": "available", "milers_in_geo_index_within_30km": 3, "has_coordinates": true, "alert_already_sent_today": false}, "acceptable": ["notify_customer", "ops_alert", "monitor"], "ideal": "notify_customer", "rationale": "Genuinely borderline \u2014 a few failures with a moderately-close rider; any of these is defensible."} +{"id": "severe-gap", "hour_of_day_utc": 11, "facts": {"zone_id": "pune", "failures_today": 8, "nearest_miler_within_km": "none within 30km", "nearest_miler_presence": "none found", "milers_in_geo_index_within_30km": 0, "has_coordinates": true, "alert_already_sent_today": false}, "acceptable": ["ops_alert", "escalate"], "ideal": "ops_alert", "rationale": "Many failures and no rider anywhere near \u2014 a clear, severe coverage gap."} +{"id": "index-full-nobody-on-duty", "hour_of_day_utc": 7, "facts": {"zone_id": "coimbatore", "failures_today": 6, "nearest_miler_within_km": "none within 30km", "nearest_miler_presence": "none found", "milers_in_geo_index_within_30km": 26, "has_coordinates": true, "alert_already_sent_today": false}, "acceptable": ["ops_alert", "escalate"], "ideal": "ops_alert", "rationale": "26 riders have been seen here but none is Available \u2014 a staffing/on-duty gap; ops needs to get riders online, not a systemic fault."} +{"id": "rider-near-presence-unknown", "hour_of_day_utc": 11, "facts": {"zone_id": "coimbatore", "failures_today": 4, "nearest_miler_within_km": 10, "nearest_miler_presence": "unknown", "milers_in_geo_index_within_30km": 26, "has_coordinates": true, "alert_already_sent_today": false}, "acceptable": ["escalate", "monitor", "notify_customer"], "ideal": "escalate", "rationale": "Riders are nearby but the backend reports no presence data \u2014 we cannot tell on-duty from off-duty, so this is NOT a coverage gap; a human should check why assignment fails."} diff --git a/evals/assignment_eval.py b/evals/assignment_eval.py index e0f16f6..f814e9a 100644 --- a/evals/assignment_eval.py +++ b/evals/assignment_eval.py @@ -4,7 +4,7 @@ Offline eval for the DispatchAgent assignment-failure decision Same scoring as the stall eval (see evals/_harness.py): acceptable-rate is the headline, exact-rate secondary. The cases use the real gatherer keys -(zone_id, failures_today, nearest_available_miler_within_km, +(zone_id, failures_today, nearest_miler_within_km, nearest_miler_presence, milers_in_geo_index_within_30km, has_coordinates) so the eval reflects what DispatchAgent actually sends. diff --git a/tests/test_dispatch_agent.py b/tests/test_dispatch_agent.py index ba784ae..47ff5f0 100644 --- a/tests/test_dispatch_agent.py +++ b/tests/test_dispatch_agent.py @@ -18,13 +18,16 @@ from agents.dispatch_agent import DispatchAgent class FakeRedis: """decode_responses=True style. `geo` is the GEORADIUS result for every call. - `available` is the set of miler ids whose miler_status: says Available; - every other geo member is treated as present-but-off-duty (the stale-index case).""" - def __init__(self, geo=None, counter_start=0, raise_on=(), available=None, alerted=False): + `available` / `on_break` are miler ids with a miler_status: key saying so. + Any geo member in neither set has NO status key at all — the common case in + production, which must read as unknown presence, not off-duty.""" + def __init__(self, geo=None, counter_start=0, raise_on=(), available=None, + on_break=(), alerted=False): self._geo = list(geo) if geo is not None else [] self._counter = counter_start self._raise_on = set(raise_on) self._available = set(available) if available is not None else set(self._geo) + self._on_break = set(on_break) self._kv = {} if alerted: self._kv["__alerted__"] = "1" @@ -37,9 +40,9 @@ class FakeRedis: mid = key.split(":", 1)[1] if mid in self._available: return '{"userid": %s, "status": "Available"}' % (mid if mid.isdigit() else 0) - if mid in self._geo: + if mid in self._on_break: return '{"userid": 0, "status": "Break"}' - return None + return None # no status key — unknown, not off-duty return self._kv.get(key) async def exists(self, key): @@ -117,7 +120,8 @@ class TestGatherAssignmentFacts(unittest.IsolatedAsyncioTestCase): facts, count = await make_agent(redis)._gather_assignment_facts("hyderabad", 17.4, 78.4) self.assertEqual(facts["zone_id"], "hyderabad") self.assertTrue(facts["has_coordinates"]) - self.assertEqual(facts["nearest_available_miler_within_km"], 10) + self.assertEqual(facts["nearest_miler_within_km"], 10) + self.assertEqual(facts["nearest_miler_presence"], "available") self.assertEqual(facts["milers_in_geo_index_within_30km"], 1) self.assertFalse(facts["alert_already_sent_today"]) self.assertEqual(facts["failures_today"], 1) @@ -127,7 +131,7 @@ class TestGatherAssignmentFacts(unittest.IsolatedAsyncioTestCase): async def test_no_rider_within_30km(self): redis = FakeRedis(geo=[], counter_start=2) # every sweep empty facts, count = await make_agent(redis)._gather_assignment_facts("pune", 18.5, 73.8) - self.assertEqual(facts["nearest_available_miler_within_km"], "none within 30km") + self.assertEqual(facts["nearest_miler_within_km"], "none within 30km") self.assertEqual(facts["milers_in_geo_index_within_30km"], 0) self.assertEqual(count, 3) @@ -135,7 +139,7 @@ class TestGatherAssignmentFacts(unittest.IsolatedAsyncioTestCase): redis = FakeRedis(geo=["m1"]) facts, count = await make_agent(redis)._gather_assignment_facts("unknown", None, None) self.assertFalse(facts["has_coordinates"]) - self.assertEqual(facts["nearest_available_miler_within_km"], "unknown (no coordinates)") + self.assertEqual(facts["nearest_miler_within_km"], "unknown (no coordinates)") self.assertNotIn("milers_in_geo_index_within_30km", facts) self.assertEqual(count, 1) # counter still incremented self.assertEqual(redis.incr_calls and 1, 1) @@ -143,37 +147,53 @@ class TestGatherAssignmentFacts(unittest.IsolatedAsyncioTestCase): async def test_georadius_error_is_swallowed(self): redis = FakeRedis(raise_on={"georadius"}) facts, count = await make_agent(redis)._gather_assignment_facts("z", 1.0, 2.0) # must not raise - self.assertEqual(facts["nearest_available_miler_within_km"], "none within 30km") + self.assertEqual(facts["nearest_miler_within_km"], "none within 30km") self.assertEqual(facts["failures_today"], 1) async def test_counter_error_is_swallowed(self): redis = FakeRedis(geo=["m9"], raise_on={"incr"}) facts, count = await make_agent(redis)._gather_assignment_facts("z", 1.0, 2.0) # must not raise - self.assertEqual(facts["nearest_available_miler_within_km"], 10) # coverage still gathered + self.assertEqual(facts["nearest_miler_within_km"], 10) # coverage still gathered self.assertEqual(facts["failures_today"], 0) # counter degraded to 0 self.assertEqual(count, 0) -class TestLiveness(unittest.IsolatedAsyncioTestCase): - """The prod incident: 26 riders in the geo index, none on duty. The old - check said 'rider within 20 km' and called it a coverage gap.""" +class TestPresence(unittest.IsolatedAsyncioTestCase): + """Presence is three-state. The prod incident had 26 riders in the geo index; + only 2 of 34 milers had a status key at all, so "no key" must read as unknown + — reporting it as off-duty would be a confident wrong answer.""" - async def test_stale_index_entries_are_not_available_riders(self): - redis = FakeRedis(geo=["4", "7", "12"], available=[]) # all present, all off-duty - facts, _ = await make_agent(redis)._gather_assignment_facts("coimbatore", 11.0, 76.9) - self.assertEqual(facts["nearest_available_miler_within_km"], "none within 30km") - self.assertEqual(facts["milers_in_geo_index_within_30km"], 3) # but the index knows them - - async def test_first_available_candidate_wins(self): - redis = FakeRedis(geo=["4", "7", "12"], available=["12"]) + async def test_confirmed_available_rider_is_preferred(self): + redis = FakeRedis(geo=["4", "7", "12"], available=["12"], on_break=["4", "7"]) found = await make_agent(redis)._find_zone(11.0, 76.9, radius_km=10) self.assertEqual(found["miler_id"], "12") - self.assertEqual(found["stale_candidates_skipped"], 2) + self.assertEqual(found["presence"], "available") - async def test_missing_status_key_means_unavailable(self): - redis = FakeRedis(geo=["99"], available=[]) - redis._geo = [] # nothing in geo → get() returns None for status - self.assertFalse(await make_agent(redis)._miler_is_available("99")) + async def test_no_status_key_reads_as_unknown_not_unavailable(self): + redis = FakeRedis(geo=["4", "7"], available=[], on_break=[]) # no status keys at all + agent = make_agent(redis) + self.assertEqual(await agent._miler_presence("4"), "unknown") + found = await agent._find_zone(11.0, 76.9, radius_km=10) + self.assertIsNotNone(found) # still reported — may be assignable + self.assertEqual(found["presence"], "unknown") + + async def test_all_confirmed_off_duty_returns_none(self): + redis = FakeRedis(geo=["4", "7"], available=[], on_break=["4", "7"]) + self.assertIsNone(await make_agent(redis)._find_zone(11.0, 76.9, radius_km=10)) + + async def test_facts_report_presence_unknown_with_rider_nearby(self): + redis = FakeRedis(geo=["4", "7", "12"], available=[], on_break=[]) + facts, _ = await make_agent(redis)._gather_assignment_facts("coimbatore", 11.0, 76.9) + self.assertEqual(facts["nearest_miler_within_km"], 10) + self.assertEqual(facts["nearest_miler_presence"], "unknown") + self.assertEqual(facts["milers_in_geo_index_within_30km"], 3) + + async def test_facts_report_none_found_when_all_off_duty(self): + redis = FakeRedis(geo=["4"], available=[], on_break=["4"]) + facts, _ = await make_agent(redis)._gather_assignment_facts("coimbatore", 11.0, 76.9) + self.assertEqual(facts["nearest_miler_within_km"], "none within 30km") + self.assertEqual(facts["nearest_miler_presence"], "none found") + self.assertEqual(facts["milers_in_geo_index_within_30km"], 1) # index still knows them class FakeMessages: