Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
"""Tests for per-turn accounting: summary formatter, collector, spinner flow, gating.
|
||||
|
||||
The formatter and collector are pure (no terminal, no agent, no network), so
|
||||
they're exercised directly. The gating test drives the real CLI methods on a
|
||||
stub object with only the attributes the gate reads, so quiet-mode /
|
||||
config-false behaviour is verified against the shipped code path rather than
|
||||
against a re-implementation.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from agent.turn_summary import (
|
||||
TurnSummaryCollector,
|
||||
TurnTally,
|
||||
format_elapsed,
|
||||
format_token_flow,
|
||||
format_turn_summary,
|
||||
)
|
||||
|
||||
|
||||
# ── format_elapsed ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"seconds,expected",
|
||||
[
|
||||
(0.0, "0.0s"),
|
||||
(12.44, "12.4s"),
|
||||
(59.9, "59.9s"),
|
||||
(60.0, "1m00s"),
|
||||
(125.0, "2m05s"),
|
||||
(-3.0, "0.0s"),
|
||||
],
|
||||
)
|
||||
def test_format_elapsed(seconds, expected):
|
||||
assert format_elapsed(seconds) == expected
|
||||
|
||||
|
||||
# ── format_turn_summary: pure formatter ─────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def test_pluralization_singular_and_plural():
|
||||
one = TurnTally(verbs={"read": {"files": 1}})
|
||||
many = TurnTally(verbs={"read": {"files": 3}})
|
||||
assert format_turn_summary(1.0, one) == "⋯ 1.0s · read 1 file"
|
||||
assert format_turn_summary(1.0, many) == "⋯ 1.0s · read 3 files"
|
||||
|
||||
|
||||
def test_pluralization_irregular_nouns():
|
||||
"""The singulariser handles -ies / -ses without producing 'memorie'."""
|
||||
mem = TurnTally(verbs={"updated": {"memories": 1}})
|
||||
assert format_turn_summary(1.0, mem) == "⋯ 1.0s · updated 1 memory"
|
||||
times = TurnTally(verbs={"searched the web": {"times": 1}})
|
||||
assert format_turn_summary(1.0, times) == "⋯ 1.0s · searched the web 1 time"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def test_long_tallies_truncate_to_more_tail():
|
||||
tally = TurnTally(
|
||||
verbs={
|
||||
"edited": {"files": 1},
|
||||
"read": {"files": 2},
|
||||
"ran": {"commands": 3},
|
||||
"searched": {"paths": 4},
|
||||
"browsed": {"pages": 5},
|
||||
"delegated": {"tasks": 6},
|
||||
}
|
||||
)
|
||||
line = format_turn_summary(9.0, tally)
|
||||
assert line == "⋯ 9.0s · edited 1 file · read 2 files · ran 3 commands · searched 4 paths · +2 more"
|
||||
assert line.count("·") == 5
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# ── collector semantics ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def test_begin_resets_previous_turn():
|
||||
collector = TurnSummaryCollector()
|
||||
collector.begin()
|
||||
collector.record_tool("read_file", result="x")
|
||||
collector.begin()
|
||||
assert collector.tally.total_tools == 0
|
||||
assert collector.render(0.5) == ""
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# ── spinner token flow (PART B) ────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
|
||||
def test_format_token_flow_bad_input_is_empty():
|
||||
assert format_token_flow(None) == ""
|
||||
assert format_token_flow("lots") == ""
|
||||
|
||||
|
||||
# ── gating: quiet mode / config false / non-interactive ────────────────────
|
||||
|
||||
|
||||
class _StubAgent:
|
||||
def __init__(self, quiet_mode=False, session_output_tokens=0):
|
||||
self.quiet_mode = quiet_mode
|
||||
self.session_output_tokens = session_output_tokens
|
||||
|
||||
|
||||
def _make_cli(**overrides):
|
||||
"""Bind the real CLI accounting methods onto a minimal stub object.
|
||||
|
||||
Avoids constructing HermesCLI (which loads config, sessions, and a
|
||||
prompt_toolkit app) while still exercising the shipped gate + emit code.
|
||||
"""
|
||||
import cli as cli_module
|
||||
|
||||
class _Stub:
|
||||
_turn_summary_enabled = True
|
||||
_spinner_token_flow_enabled = True
|
||||
tool_progress_mode = "all"
|
||||
_interactive_turn = True
|
||||
_agent_running = True
|
||||
agent = None
|
||||
_turn_summary_collector = None
|
||||
_turn_summary_start = 0.0
|
||||
_turn_token_baseline = 0
|
||||
_spinner_text = "⚡ reading file"
|
||||
_tool_start_time = 0
|
||||
|
||||
_turn_summary_is_active = cli_module.HermesCLI._turn_summary_is_active
|
||||
_turn_summary_begin = cli_module.HermesCLI._turn_summary_begin
|
||||
_turn_summary_record = cli_module.HermesCLI._turn_summary_record
|
||||
_turn_summary_emit = cli_module.HermesCLI._turn_summary_emit
|
||||
_spinner_token_flow = cli_module.HermesCLI._spinner_token_flow
|
||||
_render_spinner_text = cli_module.HermesCLI._render_spinner_text
|
||||
|
||||
stub = _Stub()
|
||||
for key, value in overrides.items():
|
||||
setattr(stub, key, value)
|
||||
return stub
|
||||
|
||||
|
||||
def _emit_and_capture(stub, monkeypatch):
|
||||
printed = []
|
||||
import cli as cli_module
|
||||
|
||||
monkeypatch.setattr(cli_module, "_cprint", lambda text: printed.append(text))
|
||||
stub._turn_summary_begin()
|
||||
stub._turn_summary_record("read_file", "contents", False)
|
||||
stub._turn_summary_emit()
|
||||
return printed
|
||||
|
||||
|
||||
def test_gating_enabled_prints_summary(monkeypatch):
|
||||
stub = _make_cli()
|
||||
printed = _emit_and_capture(stub, monkeypatch)
|
||||
assert len(printed) == 1
|
||||
assert "read 1 file" in printed[0]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def test_spinner_token_flow_appears_when_enabled():
|
||||
stub = _make_cli(agent=_StubAgent(session_output_tokens=1200))
|
||||
assert stub._spinner_token_flow() == "↓ 1.2k tok"
|
||||
assert "↓ 1.2k tok" in stub._render_spinner_text()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def test_content_free_diff_reports_unknown_not_zero_zero():
|
||||
"""A diff with no +/- content lines (bare hunk header) must render as an
|
||||
edit with UNKNOWN deltas, never a misleading '+0 -0'.
|
||||
|
||||
Found by E2E-rendering the collector against realistic tool payloads: the
|
||||
unit suite only fed diffs that had real content lines.
|
||||
"""
|
||||
from agent.turn_summary import TurnSummaryCollector
|
||||
|
||||
c = TurnSummaryCollector()
|
||||
c.begin()
|
||||
c.record_tool("patch", result={"success": True, "diff": "@@ -1,3 +1,15 @@"})
|
||||
line = c.render(3.0)
|
||||
assert "edited 1 file" in line
|
||||
assert "+0 -0" not in line
|
||||
|
||||
real = TurnSummaryCollector()
|
||||
real.begin()
|
||||
real.record_tool(
|
||||
"patch",
|
||||
result={"success": True, "diff": "--- a/x\n+++ b/x\n@@ -1 +1,2 @@\n-old\n+new\n+extra\n"},
|
||||
)
|
||||
assert "+2 -1" in real.render(1.0)
|
||||
Reference in New Issue
Block a user