atlas-iac/testing/tests/test_hermes_cli_quota_gating.py
jenkins 6df688adcc hermes: test quota gating, cooldown recovery, and failover policy
Deterministic coverage for the quota-aware lane: threshold boundaries
(14.9/15/15.1), both-below preference, fetch-failure fail-open, cooldown
elapsed-vs-not hysteresis, quota-reset recovery (never for auth),
explicit fail-closed in both directions, bounded double-failure block,
failure-reason classification, metrics emission, and worker env key
stripping. Based on PR #15 (fix/hermes-result-decomposition-reliability).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-17 20:31:12 -03:00

300 lines
11 KiB
Python

"""Quota-aware soft routing thresholds, parsing, and manifest wiring."""
from __future__ import annotations
from testing.tests.test_hermes_cli_support import (
HERMES,
Path,
lanes,
yaml,
)
EXPORTER_TEXT = """\
# HELP atlas_ai_quota_remaining_percent Remaining percentage.
# TYPE atlas_ai_quota_remaining_percent gauge
atlas_ai_quota_remaining_percent{limit="codex",provider="openai",window="five_hour"} 37.5
atlas_ai_quota_remaining_percent{limit="codex",provider="openai",window="seven_day"} 80
atlas_ai_quota_remaining_percent{limit="bonus-credits",provider="openai",window="five_hour"} 1
atlas_ai_quota_remaining_percent{limit="claude",provider="anthropic",window="five_hour"} 62
atlas_ai_quota_remaining_percent{limit="claude",provider="anthropic",window="seven_day"} 40
atlas_ai_quota_remaining_percent{limit="claude",provider="anthropic",window="seven_day_opus"} 1
atlas_ai_quota_remaining_percent{limit="other",provider="somebody",window="five_hour"} 2
atlas_ai_quota_reset_timestamp_seconds{limit="codex",provider="openai",window="five_hour"} 111
atlas_ai_quota_reset_timestamp_seconds{limit="codex",provider="openai",window="seven_day"} 222
atlas_ai_quota_reset_timestamp_seconds{limit="claude",provider="anthropic",window="seven_day"} 444
atlas_ai_quota_fetch_success{provider="openai"} 1
atlas_ai_quota_fetch_success{provider="anthropic"} 1
not a metric line at all
"""
class _MetricsResponse:
def __init__(self, text: str):
self._payload = text.encode("utf-8")
def __enter__(self):
return self
def __exit__(self, *_args):
return False
def read(self, _limit: int) -> bytes:
return self._payload
def test_parse_reduces_each_provider_to_its_binding_account_window():
snapshot = lanes.parse_quota_metrics(EXPORTER_TEXT)
assert snapshot["codex"].remaining_percent == 37.5
assert snapshot["codex"].reset_timestamp == 111
# The model-specific seven_day_opus window must not veto claude; the
# binding account window is seven_day at 40% with its own reset.
assert snapshot["claude"].remaining_percent == 40
assert snapshot["claude"].reset_timestamp == 444
def test_parse_drops_provider_after_failed_exporter_fetch():
text = EXPORTER_TEXT.replace(
'atlas_ai_quota_fetch_success{provider="openai"} 1',
'atlas_ai_quota_fetch_success{provider="openai"} 0',
)
snapshot = lanes.parse_quota_metrics(text)
assert "codex" not in snapshot
assert snapshot["claude"].remaining_percent == 40
def test_parse_reports_no_reset_when_binding_window_lacks_one():
text = (
'atlas_ai_quota_remaining_percent{limit="claude",provider="anthropic",window="five_hour"} 9\n'
)
snapshot = lanes.parse_quota_metrics(text)
assert snapshot == {"claude": lanes.ProviderQuota(9.0, None)}
def test_fetch_snapshot_fails_open_on_transport_error(monkeypatch, capsys):
failures = []
monkeypatch.setattr(lanes, "record_quota_fetch_failure", lambda: failures.append(1))
def refused(_url, timeout):
raise OSError("connection refused")
assert lanes.fetch_quota_snapshot(open_url=refused) == {}
assert failures == [1]
assert "fails open" in capsys.readouterr().err
def test_fetch_snapshot_counts_empty_signal_and_parses_live_payload(
monkeypatch, capsys
):
failures = []
monkeypatch.setattr(lanes, "record_quota_fetch_failure", lambda: failures.append(1))
empty = lanes.fetch_quota_snapshot(
open_url=lambda _url, timeout: _MetricsResponse("")
)
assert empty == {}
assert failures == [1]
assert "no gated windows" in capsys.readouterr().err
live = lanes.fetch_quota_snapshot(
open_url=lambda _url, timeout: _MetricsResponse(EXPORTER_TEXT)
)
assert live["codex"].remaining_percent == 37.5
assert failures == [1]
def test_threshold_boundary_is_strictly_below():
for remaining, expected in ((14.9, "codex"), (15.0, None), (15.1, None)):
snapshot = {
"codex": lanes.ProviderQuota(remaining, None),
"claude": lanes.ProviderQuota(80.0, None),
}
excluded, note = lanes.quota_soft_exclusion(snapshot, 15.0)
assert excluded == expected
if expected is None:
assert note is None
else:
assert "14.9% remaining" in note
assert "threshold 15%" in note
def test_both_providers_below_threshold_prefers_more_remaining():
snapshot = {
"codex": lanes.ProviderQuota(3.0, None),
"claude": lanes.ProviderQuota(9.0, None),
}
excluded, note = lanes.quota_soft_exclusion(snapshot, 15.0)
assert excluded == "codex"
assert "every provider is below" in note
assert "preferring claude (9% left) over codex (3% left)" in note
def test_both_below_tie_excludes_deterministically():
snapshot = {
"codex": lanes.ProviderQuota(5.0, None),
"claude": lanes.ProviderQuota(5.0, None),
}
excluded, _note = lanes.quota_soft_exclusion(snapshot, 15.0)
assert excluded == "claude"
def test_single_known_provider_below_threshold_is_excluded():
snapshot = {"claude": lanes.ProviderQuota(2.0, None)}
excluded, note = lanes.quota_soft_exclusion(snapshot, 15.0)
assert excluded == "claude"
assert "routing new auto work" in note
def test_kanban_setting_reads_deployed_config_and_fails_to_default(
tmp_path: Path, monkeypatch
):
monkeypatch.setattr(lanes, "DATA_ROOT", tmp_path)
assert lanes.kanban_setting("provider_quota_min_remaining_percent", 15.0) == 15.0
config = tmp_path / "config.yaml"
config.write_text("kanban:\n provider_quota_min_remaining_percent: 22\n")
assert lanes.kanban_setting("provider_quota_min_remaining_percent", 15.0) == 22.0
config.write_text("kanban: 5\n")
assert lanes.kanban_setting("provider_quota_min_remaining_percent", 15.0) == 15.0
config.write_text("just a string\n")
assert lanes.kanban_setting("provider_quota_min_remaining_percent", 15.0) == 15.0
config.write_text("kanban:\n provider_quota_min_remaining_percent: true\n")
assert lanes.kanban_setting("provider_quota_min_remaining_percent", 15.0) == 15.0
config.write_text("kanban:\n provider_quota_min_remaining_percent: '20'\n")
assert lanes.kanban_setting("provider_quota_min_remaining_percent", 15.0) == 15.0
config.write_text("kanban: [\n")
assert lanes.kanban_setting("provider_quota_min_remaining_percent", 15.0) == 15.0
def test_selection_constraint_never_gates_explicit_lanes():
def forbidden():
raise AssertionError("explicit lanes must not read quota state")
constraint = lanes.selection_constraint("cli-codex-high", snapshot_from=forbidden)
assert constraint == lanes.SelectionConstraint(None, None, None, ())
def test_selection_constraint_health_exclusion_wins_and_carries_resets(monkeypatch):
observed = {}
def health(now=None, quota_resets=None):
observed["resets"] = quota_resets
return "codex"
monkeypatch.setattr(lanes, "fresh_unavailable_provider", health)
snapshot = {"claude": lanes.ProviderQuota(50.0, 1234.0)}
constraint = lanes.selection_constraint(
"cli-auto", snapshot_from=lambda: snapshot
)
assert constraint.exclude_provider == "codex"
assert constraint.source == "health"
assert constraint.notes == ()
assert observed["resets"] == {"claude": 1234.0}
def test_selection_constraint_quota_gate_publishes_gauges(
tmp_path: Path, monkeypatch
):
monkeypatch.setattr(lanes, "DATA_ROOT", tmp_path)
monkeypatch.setattr(
lanes, "fresh_unavailable_provider", lambda *_args, **_kwargs: None
)
snapshot = {
"codex": lanes.ProviderQuota(10.0, 555.0),
"claude": lanes.ProviderQuota(60.0, None),
}
constraint = lanes.selection_constraint("cli-auto", snapshot_from=lambda: snapshot)
assert constraint.exclude_provider == "codex"
assert constraint.source == "quota"
assert constraint.exclude_reason == "is below its remaining-quota routing threshold"
assert len(constraint.notes) == 1 and "Quota guard: codex" in constraint.notes[0]
rendered = lanes.METRICS.render().decode("utf-8")
assert 'hermes_cli_quota_remaining_percent{provider="codex"} 10' in rendered
assert 'hermes_cli_quota_reset_timestamp_seconds{provider="codex"} 555' in rendered
assert 'hermes_cli_provider_soft_excluded{provider="codex"} 1' in rendered
assert 'hermes_cli_provider_soft_excluded{provider="claude"} 0' in rendered
def test_selection_constraint_fails_open_without_quota_signal(
tmp_path: Path, monkeypatch
):
monkeypatch.setattr(lanes, "DATA_ROOT", tmp_path)
monkeypatch.setattr(
lanes, "fresh_unavailable_provider", lambda *_args, **_kwargs: None
)
constraint = lanes.selection_constraint("cli-auto", snapshot_from=dict)
assert constraint == lanes.SelectionConstraint(None, None, None, ())
def _agent_config_kanban() -> dict:
document = yaml.safe_load((HERMES / "agent-configmap.yaml").read_text())
return yaml.safe_load(document["data"]["config.yaml"])["kanban"]
def test_configmap_declares_quota_routing_defaults():
kanban = _agent_config_kanban()
assert kanban["provider_quota_min_remaining_percent"] == 15
assert kanban["provider_capacity_cooldown_seconds"] == 300
assert kanban["provider_auth_cooldown_seconds"] == 3600
def test_lane_metrics_are_wired_for_scraping():
deployment = yaml.safe_load((HERMES / "agent-deployment.yaml").read_text())
containers = deployment["spec"]["template"]["spec"]["containers"]
lane = next(item for item in containers if item["name"] == "cli-lane-runner")
env = {entry["name"]: entry.get("value") for entry in lane["env"]}
assert {"name": "lane-metrics", "containerPort": 9011, "protocol": "TCP"} in lane[
"ports"
]
assert env["HERMES_CLI_LANE_METRICS_PORT"] == "9011"
assert env["HERMES_CLI_QUOTA_METRICS_URL"] == "http://127.0.0.1:9010/metrics"
services = {
item["metadata"]["name"]: item
for item in yaml.safe_load_all((HERMES / "service.yaml").read_text())
if item
}
metrics_service = services["hermes-cli-lane-metrics"]
annotations = metrics_service["metadata"]["annotations"]
assert annotations["prometheus.io/scrape"] == "true"
assert annotations["prometheus.io/port"] == "9011"
assert metrics_service["spec"]["selector"] == {"app": "hermes-agent"}
assert metrics_service["spec"]["ports"][0]["targetPort"] == "lane-metrics"
policies = {
item["metadata"]["name"]: item
for item in yaml.safe_load_all((HERMES / "networkpolicy.yaml").read_text())
if item
}
agent_ingress = policies["hermes-agent-isolation"]["spec"]["ingress"]
monitoring_ports = [
port["port"]
for rule in agent_ingress
for port in rule.get("ports", [])
if any(
source.get("namespaceSelector", {})
.get("matchLabels", {})
.get("kubernetes.io/metadata.name")
== "monitoring"
for source in rule.get("from", [])
)
]
assert 9010 in monitoring_ports and 9011 in monitoring_ports