Files

140 lines
5.4 KiB
Python

"""Unit coverage for agent/turn_liveness.py config resolution (#95548/#95663).
AGENTS.md rejects new non-secret ``HERMES_*`` env knobs: the watchdog's
behavioral settings live in ``agent.turn_liveness`` in config.yaml, and the
resolver must validate them — a typo must never crash durable-turn startup,
and NaN/Inf must never silently disable the timeout or freeze the watcher
thread.
"""
from __future__ import annotations
import logging
from agent.turn_liveness import (
DEFAULT_TURN_LIVENESS_POLL_S,
DEFAULT_TURN_LIVENESS_TIMEOUT_S,
resolve_turn_liveness_settings,
)
def test_defaults_when_config_missing_or_unset():
# No config at all.
assert resolve_turn_liveness_settings(None) == (
DEFAULT_TURN_LIVENESS_TIMEOUT_S,
DEFAULT_TURN_LIVENESS_POLL_S,
)
assert resolve_turn_liveness_settings({}) == (
DEFAULT_TURN_LIVENESS_TIMEOUT_S,
DEFAULT_TURN_LIVENESS_POLL_S,
)
# Section present but keys absent.
assert resolve_turn_liveness_settings(
{"agent": {"turn_liveness": {}}}
) == (DEFAULT_TURN_LIVENESS_TIMEOUT_S, DEFAULT_TURN_LIVENESS_POLL_S)
def test_precedence_explicit_values_win_over_defaults():
timeout, poll = resolve_turn_liveness_settings(
{"agent": {"turn_liveness": {"timeout_s": 30, "poll_s": 5}}}
)
assert timeout == 30.0
assert poll == 5.0
def test_precedence_numeric_strings_accepted():
# YAML may hand back strings; numeric coercion is part of the contract.
timeout, poll = resolve_turn_liveness_settings(
{"agent": {"turn_liveness": {"timeout_s": "45", "poll_s": "2.5"}}}
)
assert timeout == 45.0
assert poll == 2.5
def test_timeout_zero_is_documented_opt_out():
timeout, poll = resolve_turn_liveness_settings(
{"agent": {"turn_liveness": {"timeout_s": 0, "poll_s": 15}}}
)
assert timeout is None
assert poll == 15.0
# Negative is the same documented "disabled" path.
assert resolve_turn_liveness_settings(
{"agent": {"turn_liveness": {"timeout_s": -1}}}
)[0] is None
def test_typo_does_not_crash_and_falls_back_to_default(caplog):
# The old raw float() env parsing raised ValueError into durable-turn
# startup on a typo. The resolver must warn + default instead.
with caplog.at_level(logging.WARNING, logger="agent.turn_liveness"):
timeout, poll = resolve_turn_liveness_settings(
{"agent": {"turn_liveness": {"timeout_s": "oops", "poll_s": "typo"}}}
)
assert timeout == DEFAULT_TURN_LIVENESS_TIMEOUT_S
assert poll == DEFAULT_TURN_LIVENESS_POLL_S
assert len(caplog.records) == 2
assert all("agent.turn_liveness" in r.getMessage() for r in caplog.records)
def test_nan_timeout_falls_back_to_default_not_disabled(caplog):
# float("nan") > 0 is False, so the old code silently disabled the
# watchdog on NaN. The resolver must reject it and keep the default.
with caplog.at_level(logging.WARNING, logger="agent.turn_liveness"):
timeout, poll = resolve_turn_liveness_settings(
{"agent": {"turn_liveness": {"timeout_s": float("nan")}}}
)
assert timeout == DEFAULT_TURN_LIVENESS_TIMEOUT_S # NOT None
assert poll == DEFAULT_TURN_LIVENESS_POLL_S
assert len(caplog.records) == 1
def test_inf_poll_falls_back_to_default_not_frozen_watcher(caplog):
# float("inf") poll made Event.wait(inf) hang the watcher thread
# forever. The resolver must reject it.
with caplog.at_level(logging.WARNING, logger="agent.turn_liveness"):
timeout, poll = resolve_turn_liveness_settings(
{"agent": {"turn_liveness": {"poll_s": float("inf")}}}
)
assert timeout == DEFAULT_TURN_LIVENESS_TIMEOUT_S
assert poll == DEFAULT_TURN_LIVENESS_POLL_S
assert len(caplog.records) == 1
def test_inf_timeout_falls_back_to_default(caplog):
# inf timeout would never fire (idle < inf always true) — silent
# disablement. Rejected.
with caplog.at_level(logging.WARNING, logger="agent.turn_liveness"):
timeout, _ = resolve_turn_liveness_settings(
{"agent": {"turn_liveness": {"timeout_s": float("inf")}}}
)
assert timeout == DEFAULT_TURN_LIVENESS_TIMEOUT_S
assert len(caplog.records) == 1
def test_non_positive_poll_falls_back_to_default(caplog):
with caplog.at_level(logging.WARNING, logger="agent.turn_liveness"):
_, poll = resolve_turn_liveness_settings(
{"agent": {"turn_liveness": {"poll_s": 0}}}
)
assert poll == DEFAULT_TURN_LIVENESS_POLL_S
with caplog.at_level(logging.WARNING, logger="agent.turn_liveness"):
_, poll = resolve_turn_liveness_settings(
{"agent": {"turn_liveness": {"poll_s": -3}}}
)
assert poll == DEFAULT_TURN_LIVENESS_POLL_S
def test_malformed_sections_fall_back_to_defaults(caplog):
# Non-dict `agent` or non-dict `turn_liveness` must not raise.
with caplog.at_level(logging.WARNING, logger="agent.turn_liveness"):
result = resolve_turn_liveness_settings({"agent": "nonsense"})
assert result == (DEFAULT_TURN_LIVENESS_TIMEOUT_S, DEFAULT_TURN_LIVENESS_POLL_S)
with caplog.at_level(logging.WARNING, logger="agent.turn_liveness"):
result = resolve_turn_liveness_settings(
{"agent": {"turn_liveness": "nonsense"}}
)
assert result == (DEFAULT_TURN_LIVENESS_TIMEOUT_S, DEFAULT_TURN_LIVENESS_POLL_S)
# The malformed section itself is surfaced as a warning.
assert len(caplog.records) >= 1