Files
hermes-agent/tests/agent/test_session_activity.py
Teknium 62f353eed5 refactor(gateway): iteration-progress formatter lives in agent/session_activity, tests trimmed to the invariant bar
The salvaged helper was appended to the gateway/run.py facade; new behaviour
belongs in a topical sibling. agent/session_activity.py already owns the
activity-snapshot contract the three render sites read from, so the formatter
moves there as format_iteration_progress and the three call sites import it at
module level instead of late-importing the facade.

Tests trimmed to the salvage bar (<= 2 invariant tests per fix): one
parameterized contract on the helper (unbounded/None hide the ceiling, a real
budget keeps N/M) plus the contributor's end-to-end busy-ack test through the
real render path. The per-site heartbeat/timeout tests exercised the same
helper through mocks and are dropped; evals/gateway_status_render/
iteration_ceiling_ab.py drives all three real render sites with a real AIAgent
for before/after evidence (3 sentinel leaks on main -> 0).

Related: #103109 (same fix, same target module; credited in the PR),
#102845 (third render site, closed by its author in favour of #102817).
2026-09-06 05:35:12 -07:00

115 lines
4.2 KiB
Python

"""Unit tests for the shared session activity observation contract."""
import sys
from types import SimpleNamespace
import pytest
from agent.session_activity import (
ACTIVITY_DESCRIPTION_MAX,
ActivityProvenance,
bound_activity_description,
build_activity_snapshot,
format_iteration_progress,
normalize_activity_provenance,
reset_session_activity_persist_window,
)
@pytest.mark.parametrize(
"max_iterations, expected",
[
(sys.maxsize, "iteration 3"), # AIAgent's default: unbounded, so no ceiling is shown
(None, "iteration 3"),
(250, "iteration 3/250"), # a real budget (e.g. delegation.max_iterations) keeps N/M
],
)
def test_format_iteration_progress_hides_unbounded_ceiling(max_iterations, expected):
out = format_iteration_progress(3, max_iterations)
assert out == expected
assert str(sys.maxsize) not in out
def test_bound_activity_description_truncates():
long = "x" * (ACTIVITY_DESCRIPTION_MAX + 80)
out = bound_activity_description(long)
assert len(out) == ACTIVITY_DESCRIPTION_MAX
assert out.endswith("…")
def test_reset_session_activity_persist_window_clears_rate_limit():
agent = SimpleNamespace(_session_activity_last_persist_mono=1234.5)
reset_session_activity_persist_window(agent)
assert agent._session_activity_last_persist_mono == 0.0
def test_reset_session_activity_persist_window_swallows_missing_attr():
reset_session_activity_persist_window(object())
def test_normalize_activity_provenance_defaults_to_unknown():
assert normalize_activity_provenance(None) is ActivityProvenance.UNKNOWN
assert normalize_activity_provenance("") is ActivityProvenance.UNKNOWN
assert normalize_activity_provenance("not-a-real-source") is ActivityProvenance.UNKNOWN
assert normalize_activity_provenance("agent.activity") is ActivityProvenance.UNKNOWN
assert (
normalize_activity_provenance(ActivityProvenance.AGENT_COMPRESSION)
is ActivityProvenance.AGENT_COMPRESSION
)
assert (
normalize_activity_provenance("agent.compression_timeout")
is ActivityProvenance.AGENT_COMPRESSION_TIMEOUT
)
def test_build_activity_snapshot_includes_compat_aliases():
snap = build_activity_snapshot(
last_activity_at=100.0,
last_activity_description="starting API call #1",
last_activity_provenance=ActivityProvenance.UNKNOWN,
now=110.0,
extra={"api_call_count": 1},
)
assert snap["last_activity_at"] == 100.0
assert snap["last_activity_description"] == "starting API call #1"
assert snap["last_activity_provenance"] == "unknown"
assert snap["seconds_since_activity"] == 10.0
assert snap["last_activity_ts"] == 100.0
assert snap["last_activity_desc"] == "starting API call #1"
assert snap["description"] == "starting API call #1"
assert snap["api_call_count"] == 1
assert "phase" not in snap
assert "last_progress_at" not in snap
def test_build_activity_snapshot_maps_missing_provenance_to_unknown():
snap = build_activity_snapshot(
last_activity_at=1.0,
last_activity_description="starting new turn (cached)",
last_activity_provenance=None,
now=2.0,
)
assert snap["last_activity_provenance"] == "unknown"
def test_build_activity_snapshot_preserves_compression_transition_provenances():
"""Compaction / timeout / cooldown share the observation source (#72424)."""
for provenance, desc in (
(ActivityProvenance.AGENT_COMPRESSION, "context compression in progress"),
(ActivityProvenance.AGENT_COMPRESSION_TIMEOUT, "context compression timed out"),
(
ActivityProvenance.AGENT_COMPRESSION_COOLDOWN,
"compression blocked (cooldown: 30s remaining)",
),
):
snap = build_activity_snapshot(
last_activity_at=50.0,
last_activity_description=desc,
last_activity_provenance=provenance,
now=55.0,
)
assert snap["last_activity_provenance"] == provenance.value
assert snap["provenance"] == provenance.value
assert snap["last_activity_description"] == desc
assert snap["seconds_since_activity"] == 5.0