156 lines
5.2 KiB
Python
156 lines
5.2 KiB
Python
|
|
"""Switchyard routing and provider-health behavior."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from testing.tests.test_hermes_cli_support import (
|
||
|
|
Path,
|
||
|
|
_SwitchyardResponse,
|
||
|
|
json,
|
||
|
|
lanes,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_auto_lane_uses_switchyard_worker_decision():
|
||
|
|
observed = {}
|
||
|
|
|
||
|
|
def route(request, timeout):
|
||
|
|
observed["payload"] = json.loads(request.data)
|
||
|
|
observed["timeout"] = timeout
|
||
|
|
return _SwitchyardResponse("worker/claude/claude-opus-5/xhigh")
|
||
|
|
|
||
|
|
route = lanes.select_route(
|
||
|
|
"Perform the security review.",
|
||
|
|
"cli-auto",
|
||
|
|
open_request=route,
|
||
|
|
)
|
||
|
|
|
||
|
|
assert route.provider == "claude"
|
||
|
|
assert route.model == "claude-opus-5"
|
||
|
|
assert route.effort == "xhigh"
|
||
|
|
assert route.classifier == "switchyard-classifier"
|
||
|
|
assert observed["payload"]["model"] == "atlas/worker/auto/maximum"
|
||
|
|
assert observed["timeout"] == 60
|
||
|
|
|
||
|
|
|
||
|
|
def test_manual_lane_is_still_enforced_by_switchyard():
|
||
|
|
observed = {}
|
||
|
|
|
||
|
|
def route(request, timeout):
|
||
|
|
observed["payload"] = json.loads(request.data)
|
||
|
|
return _SwitchyardResponse(
|
||
|
|
"worker/claude/claude-sonnet-5/high", "manual route"
|
||
|
|
)
|
||
|
|
|
||
|
|
route = lanes.select_route(
|
||
|
|
"Implement it.",
|
||
|
|
"cli-claude-high",
|
||
|
|
open_request=route,
|
||
|
|
)
|
||
|
|
|
||
|
|
assert route.provider == "claude"
|
||
|
|
assert route.effort == "high"
|
||
|
|
assert route.classifier == "switchyard-manual"
|
||
|
|
assert route.reason == "manual route"
|
||
|
|
assert observed["payload"]["model"] == "atlas/worker/manual/claude/high"
|
||
|
|
|
||
|
|
|
||
|
|
def test_cross_provider_retry_passes_failed_provider_to_switchyard():
|
||
|
|
observed = {}
|
||
|
|
|
||
|
|
def route(request, timeout):
|
||
|
|
observed["payload"] = json.loads(request.data)
|
||
|
|
return _SwitchyardResponse("worker/claude/claude-sonnet-5/high")
|
||
|
|
|
||
|
|
route = lanes.select_route(
|
||
|
|
"Retry after capacity exhaustion.",
|
||
|
|
"cli-auto",
|
||
|
|
exclude_provider="codex",
|
||
|
|
open_request=route,
|
||
|
|
)
|
||
|
|
|
||
|
|
assert route.provider == "claude"
|
||
|
|
assert route.classifier == "switchyard-classifier"
|
||
|
|
content = observed["payload"]["messages"][0]["content"]
|
||
|
|
assert "codex provider failed or exhausted capacity" in content
|
||
|
|
|
||
|
|
|
||
|
|
def test_classifier_cannot_select_a_freshly_excluded_provider():
|
||
|
|
payloads = []
|
||
|
|
|
||
|
|
def route(request, timeout):
|
||
|
|
payload = json.loads(request.data)
|
||
|
|
payloads.append(payload)
|
||
|
|
if len(payloads) == 1:
|
||
|
|
return _SwitchyardResponse("worker/claude/opus/xhigh")
|
||
|
|
return _SwitchyardResponse("worker/codex/sol/xhigh", "healthy route")
|
||
|
|
|
||
|
|
selected = lanes.select_route(
|
||
|
|
"Perform a consequential review.",
|
||
|
|
"cli-auto",
|
||
|
|
exclude_provider="claude",
|
||
|
|
exclude_reason="is unavailable according to fresh native health",
|
||
|
|
open_request=route,
|
||
|
|
)
|
||
|
|
|
||
|
|
assert selected.provider == "codex"
|
||
|
|
assert selected.effort == "xhigh"
|
||
|
|
assert selected.classifier == "switchyard-classifier-health-guard"
|
||
|
|
assert payloads[0]["model"] == "atlas/worker/auto/maximum"
|
||
|
|
assert payloads[1]["model"] == "atlas/worker/manual/codex/xhigh"
|
||
|
|
assert "claude provider is unavailable" in payloads[0]["messages"][0]["content"]
|
||
|
|
|
||
|
|
|
||
|
|
def test_fresh_native_health_excludes_only_one_proven_down_provider(
|
||
|
|
tmp_path: Path, monkeypatch
|
||
|
|
):
|
||
|
|
paths = {
|
||
|
|
"codex": tmp_path / "codex.json",
|
||
|
|
"claude": tmp_path / "claude.json",
|
||
|
|
}
|
||
|
|
paths["codex"].write_text('{"state":"available"}\n', encoding="utf-8")
|
||
|
|
paths["claude"].write_text('{"state":"unavailable"}\n', encoding="utf-8")
|
||
|
|
monkeypatch.setattr(lanes, "PROVIDER_HEALTH_PATHS", paths)
|
||
|
|
|
||
|
|
now = max(path.stat().st_mtime for path in paths.values())
|
||
|
|
assert lanes.fresh_unavailable_provider(now=now) == "claude"
|
||
|
|
|
||
|
|
paths["codex"].write_text('{"state":"unavailable"}\n', encoding="utf-8")
|
||
|
|
now = max(path.stat().st_mtime for path in paths.values())
|
||
|
|
assert lanes.fresh_unavailable_provider(now=now) is None
|
||
|
|
|
||
|
|
|
||
|
|
def test_explicit_auth_failure_survives_restart_health_gap(tmp_path: Path, monkeypatch):
|
||
|
|
paths = {
|
||
|
|
"codex": tmp_path / "codex.json",
|
||
|
|
"claude": tmp_path / "claude.json",
|
||
|
|
}
|
||
|
|
paths["codex"].write_text(
|
||
|
|
'{"state":"available","authenticated":true}\n', encoding="utf-8"
|
||
|
|
)
|
||
|
|
paths["claude"].write_text(
|
||
|
|
'{"state":"unavailable","authenticated":false}\n', encoding="utf-8"
|
||
|
|
)
|
||
|
|
monkeypatch.setattr(lanes, "PROVIDER_HEALTH_PATHS", paths)
|
||
|
|
|
||
|
|
stale_during_rollout = paths["claude"].stat().st_mtime + 10 * 60
|
||
|
|
assert lanes.fresh_unavailable_provider(now=stale_during_rollout) == "claude"
|
||
|
|
|
||
|
|
paths["claude"].write_text(
|
||
|
|
'{"state":"unavailable","authenticated":true}\n', encoding="utf-8"
|
||
|
|
)
|
||
|
|
transient_stale = paths["claude"].stat().st_mtime + 10 * 60
|
||
|
|
assert lanes.fresh_unavailable_provider(now=transient_stale) is None
|
||
|
|
|
||
|
|
|
||
|
|
def test_worker_environment_preserves_vault_backed_cli_homes(monkeypatch):
|
||
|
|
"""Kanban workers must not fall back to credential-free persistent homes."""
|
||
|
|
monkeypatch.setenv("CODEX_HOME", "/runtime-access/codex")
|
||
|
|
monkeypatch.setenv("CLAUDE_CONFIG_DIR", "/runtime-access/claude")
|
||
|
|
|
||
|
|
env = lanes._base_env()
|
||
|
|
|
||
|
|
assert env["HOME"] == str(lanes.DATA_ROOT / "home")
|
||
|
|
assert env["CODEX_HOME"] == "/runtime-access/codex"
|
||
|
|
assert env["CLAUDE_CONFIG_DIR"] == "/runtime-access/claude"
|
||
|
|
assert env["GIT_TERMINAL_PROMPT"] == "0"
|