c5df00c6d3
- Fix ruff format violations in app.py, conversation.py, and tui_conversation_pruning_steps.py (3 files reformatted) - Fix ruff E501 line-too-long in helper_tui_conversation_pruning.py - Expand ConversationSettings docstring to explain hysteresis design - Add _note_active invariant comment in ConversationStream.__init__ - Add full docstring to load_conversation_settings (config path, keys, fallback behaviour) - Narrow bare except Exception to (OSError, json.JSONDecodeError) and (ValueError, TypeError) in load_conversation_settings - Add Robot Framework integration tests (tui_conversation_pruning.robot + helper) covering prune-trigger, note-inserted, protected-blocks, clear-resets-state, settings-defaults - Add ASV benchmarks (conversation_stream_bench.py) covering add_block baseline, heavy-load pruning, render after prune, clear+repopulate - Add CHANGELOG.md entry for feat(tui) conversation content pruning ISSUES CLOSED: #6350
153 lines
5.1 KiB
Python
153 lines
5.1 KiB
Python
"""Helper script for tui_conversation_pruning.robot integration tests.
|
|
|
|
Each subcommand is a self-contained check that prints a sentinel on success.
|
|
Tests ConversationStream pruning behaviour directly — no Textual event loop required.
|
|
"""
|
|
|
|
# ruff: noqa: E402
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
_SRC = str(Path(__file__).resolve().parents[1] / "src")
|
|
if _SRC not in sys.path:
|
|
sys.path.insert(0, _SRC)
|
|
|
|
from cleveragents.tui.conversation import (
|
|
DEFAULT_PRUNE_EXCESS,
|
|
DEFAULT_PRUNE_LOW_MARK,
|
|
ConversationSettings,
|
|
ConversationStream,
|
|
load_conversation_settings,
|
|
)
|
|
|
|
|
|
def _make_stream(
|
|
prune_low_mark: int = 100,
|
|
prune_excess: int = 50,
|
|
preserve_recent_lines: int = 10,
|
|
) -> ConversationStream:
|
|
settings = ConversationSettings(
|
|
prune_low_mark=prune_low_mark,
|
|
prune_excess=prune_excess,
|
|
preserve_recent_lines=preserve_recent_lines,
|
|
)
|
|
return ConversationStream(settings=settings)
|
|
|
|
|
|
def _multiline_block(n_lines: int, label: str = "line") -> str:
|
|
return "\n".join(f"{label} {i}" for i in range(n_lines))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Subcommands
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def cmd_prune_trigger() -> None:
|
|
"""Verify pruning fires and total_lines returns to <= prune_low_mark."""
|
|
stream = _make_stream(prune_low_mark=100, prune_excess=50)
|
|
# trigger_line_count = 150; push 200 lines total
|
|
for i in range(20):
|
|
stream.add_block(_multiline_block(10, f"msg{i}"), block_type="message")
|
|
|
|
assert stream.total_lines <= 100, (
|
|
f"Expected total_lines <= prune_low_mark (100), got {stream.total_lines}"
|
|
)
|
|
print("tui-prune-trigger-ok")
|
|
|
|
|
|
def cmd_prune_note_inserted() -> None:
|
|
"""Verify a note block is at index 0 with the expected text after pruning."""
|
|
stream = _make_stream(prune_low_mark=100, prune_excess=50)
|
|
for i in range(20):
|
|
stream.add_block(_multiline_block(10, f"msg{i}"), block_type="message")
|
|
|
|
blocks = stream.blocks
|
|
assert len(blocks) > 0, "Expected at least one block after pruning"
|
|
first = blocks[0]
|
|
assert first.block_type == "note", (
|
|
f"Expected first block to be 'note', got {first.block_type!r}"
|
|
)
|
|
assert "(Earlier messages pruned" in first.text, (
|
|
f"Expected pruned-note text in first block, got: {first.text!r}"
|
|
)
|
|
print("tui-prune-note-inserted-ok")
|
|
|
|
|
|
def cmd_prune_protected() -> None:
|
|
"""Verify protected blocks survive pruning."""
|
|
stream = _make_stream(prune_low_mark=100, prune_excess=50)
|
|
protected_text = "protected welcome block"
|
|
stream.add_block(protected_text, block_type="welcome", protected=True)
|
|
|
|
for i in range(20):
|
|
stream.add_block(_multiline_block(10, f"msg{i}"), block_type="message")
|
|
|
|
texts = [b.text for b in stream.blocks]
|
|
assert protected_text in texts, (
|
|
f"Protected block was removed during pruning. Block types: "
|
|
f"{[b.block_type for b in stream.blocks]}"
|
|
)
|
|
print("tui-prune-protected-ok")
|
|
|
|
|
|
def cmd_clear_resets_state() -> None:
|
|
"""Verify clear() resets blocks, total_lines, and the note-active state."""
|
|
stream = _make_stream(prune_low_mark=100, prune_excess=50)
|
|
for i in range(20):
|
|
stream.add_block(_multiline_block(10, f"msg{i}"), block_type="message")
|
|
|
|
assert len(stream.blocks) > 0, "Expected blocks before clear"
|
|
|
|
stream.clear()
|
|
|
|
assert len(stream.blocks) == 0, (
|
|
f"Expected 0 blocks after clear, got {len(stream.blocks)}"
|
|
)
|
|
assert stream.total_lines == 0, (
|
|
f"Expected total_lines == 0 after clear, got {stream.total_lines}"
|
|
)
|
|
# After clear, a single small block should not trigger pruning
|
|
stream.add_block("hello", block_type="message")
|
|
assert len(stream.blocks) == 1, (
|
|
f"Expected 1 block after clear + add, got {len(stream.blocks)}"
|
|
)
|
|
print("tui-clear-resets-state-ok")
|
|
|
|
|
|
def cmd_settings_defaults() -> None:
|
|
"""Verify load_conversation_settings returns defaults when config file is absent."""
|
|
import tempfile
|
|
|
|
absent_path = Path(tempfile.mkdtemp()) / "no-such-file.json"
|
|
settings = load_conversation_settings(config_path=absent_path)
|
|
|
|
assert settings.prune_low_mark == DEFAULT_PRUNE_LOW_MARK, (
|
|
f"prune_low_mark: {settings.prune_low_mark} != {DEFAULT_PRUNE_LOW_MARK}"
|
|
)
|
|
assert settings.prune_excess == DEFAULT_PRUNE_EXCESS, (
|
|
f"Expected prune_excess={DEFAULT_PRUNE_EXCESS}, got {settings.prune_excess}"
|
|
)
|
|
print("tui-settings-defaults-ok")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Dispatch
|
|
# ---------------------------------------------------------------------------
|
|
|
|
COMMANDS = {
|
|
"prune-trigger": cmd_prune_trigger,
|
|
"prune-note-inserted": cmd_prune_note_inserted,
|
|
"prune-protected": cmd_prune_protected,
|
|
"clear-resets-state": cmd_clear_resets_state,
|
|
"settings-defaults": cmd_settings_defaults,
|
|
}
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS:
|
|
print(f"Usage: {sys.argv[0]} [{' | '.join(COMMANDS)}]", file=sys.stderr)
|
|
sys.exit(1)
|
|
COMMANDS[sys.argv[1]]()
|