diff --git a/backend/apps/agents/core/fault_injection.py b/backend/apps/agents/core/fault_injection.py index c6a53176..a905f962 100644 --- a/backend/apps/agents/core/fault_injection.py +++ b/backend/apps/agents/core/fault_injection.py @@ -25,6 +25,7 @@ KNOWN_FAULTS: Set[str] = { "sidecar_wedge", # a builtin tool never returns (ENG-368 heartbeat ceiling) "transport_death", # the CLI's pipe dies, not the provider (ENG-382 respawn-not-rebuild) "empty_finish", # a turn ends with no answer after tool work (ENG-354, ENG-390) + "dead_lane", # the router has already given up on the credential (ENG-414 preflight) } diff --git a/backend/apps/agents/manager/run/lane_preflight.py b/backend/apps/agents/manager/run/lane_preflight.py index ddeb6a6e..e54c6761 100644 --- a/backend/apps/agents/manager/run/lane_preflight.py +++ b/backend/apps/agents/manager/run/lane_preflight.py @@ -90,8 +90,19 @@ def connection_is_dead(conn: Dict) -> bool: return conn.get("errorCode") in (401, 403) +# The shape the router publishes for a credential it has given up on, and the shape a drill injects. +# It is built to satisfy `connection_is_dead` above, and a test pins that, so a drill can never fire +# a fault the guard ignores and still report a pass. +def injected_dead_conn(provider: str) -> Dict: + return {"provider": provider, "testStatus": "unavailable", "errorCode": 401} + + async def dead_connection(provider: str) -> Optional[Dict]: """The provider's connection if the router considers it dead, else None. Never raises: a preflight that cannot read health must let the turn proceed, because guessing "dead" would ground a working lane.""" + from backend.apps.agents.core.fault_injection import armed + if armed("dead_lane"): + logger.warning("[fault] dead_lane armed: reporting %s as a dead credential", provider) + return injected_dead_conn(provider) try: from backend.apps.nine_router import get_providers for conn in await get_providers(): diff --git a/backend/tests/test_lane_preflight_never_declares_death.py b/backend/tests/test_lane_preflight_never_declares_death.py index cd9c20fe..ed0d551e 100644 --- a/backend/tests/test_lane_preflight_never_declares_death.py +++ b/backend/tests/test_lane_preflight_never_declares_death.py @@ -116,3 +116,32 @@ async def test_a_router_that_does_not_come_back_still_stops_the_turn(monkeypatch async def p_async(value): return value + + +# ---------------------------------------------------- the drill seam itself (OSW_FAULT=dead_lane) + +def test_the_injected_fault_is_the_shape_the_REAL_classifier_catches(): + """A drill that fires a fault the guard ignores would report a pass while proving nothing. + Same rule the fault_injection module already states for its other faults.""" + conn = p_pf.injected_dead_conn("claude") + assert p_pf.connection_is_dead(conn) is True + assert conn["errorCode"] in (401, 403), "connection_is_dead requires auth evidence, not just unavailable" + + +def test_the_fault_is_declared_so_a_typo_cannot_arm_nothing(): + from backend.apps.agents.core.fault_injection import KNOWN_FAULTS + assert "dead_lane" in KNOWN_FAULTS + + +@pytest.mark.asyncio +async def test_the_fault_is_inert_unless_armed(monkeypatch): + monkeypatch.delenv("OSW_FAULT", raising=False) + called = {"n": 0} + + async def p_real(provider): + called["n"] += 1 + return None + + monkeypatch.setattr(p_pf, "dead_connection", p_real) + await p_pf.preflight_lane("cc/claude-opus-5", p_session()) + assert called["n"] == 1, "unarmed, the real health read must still happen"