"""Tests for live session context breakdown."""
from unittest.mock import MagicMock, patch
from agent.context_breakdown import compute_session_context_breakdown
def _make_agent(
*,
stable: str = "identity and guidance",
context: str = "",
volatile: str = "timestamp line",
tools: list | None = None,
context_length: int = 200_000,
last_prompt_tokens: int = 0,
):
agent = MagicMock()
agent.model = "openai/gpt-5.4"
agent.tools = tools or [
{"type": "function", "function": {"name": "terminal", "description": "run"}},
{"type": "function", "function": {"name": "mcp_demo_tool", "description": "mcp"}},
{"type": "function", "function": {"name": "delegate_task", "description": "spawn"}},
]
agent._memory_store = None
agent._memory_enabled = True
agent._user_profile_enabled = True
agent.context_compressor = MagicMock(
context_length=context_length,
last_prompt_tokens=last_prompt_tokens,
)
return agent, {"stable": stable, "context": context, "volatile": volatile}
def test_breakdown_includes_major_categories():
stable = (
"base guidance\n"
"\n demo:\n - hello: hi\n"
)
context = "# Project Context\nFollow AGENTS.md"
volatile = "Current time: now"
history = [{"role": "user", "content": "hello there"}]
agent, parts = _make_agent(stable=stable, context=context, volatile=volatile)
with patch("agent.system_prompt.build_system_prompt_parts", return_value=parts):
data = compute_session_context_breakdown(agent, history)
ids = {item["id"] for item in data["categories"]}
assert {"system_prompt", "tool_definitions", "rules", "skills", "mcp", "subagent_definitions", "conversation"} <= ids
assert data["context_max"] == 200_000
assert data["estimated_total"] > 0
def test_breakdown_uses_measured_context_when_available():
agent, parts = _make_agent(last_prompt_tokens=42_000)
with patch("agent.system_prompt.build_system_prompt_parts", return_value=parts):
data = compute_session_context_breakdown(agent, [])
assert data["context_used"] == 42_000
assert data["context_percent"] == 21
# ── /context renderers (pure functions over the payload) ────────────────────
from agent.context_breakdown import ( # noqa: E402
compute_context_details,
render_context_breakdown_lines,
render_context_category_lines,
render_context_details_lines,
render_context_grid,
)
def _payload(**overrides):
base = {
"categories": [
{"id": "system_prompt", "label": "System prompt", "tokens": 10_000},
{"id": "tool_definitions", "label": "Tool definitions", "tokens": 20_000},
{"id": "skills", "label": "Skills", "tokens": 5_000},
{"id": "conversation", "label": "Conversation", "tokens": 15_000},
],
"context_max": 200_000,
"context_percent": 25,
"context_used": 50_000,
"estimated_total": 50_000,
"model": "openai/gpt-test",
}
base.update(overrides)
return base
def test_grid_is_5x20_and_mostly_free():
rows = render_context_grid(_payload())
assert len(rows) == 5
cells = " ".join(rows).split(" ")
assert len(cells) == 100
# 50k / 200k → 25 used cells, 75 free
assert cells.count("·") == 75
# Category glyphs proportional: 10k→5, 20k→10, 5k→2-3, 15k→7-8 cells
assert cells.count("■") == 5
assert cells.count("▣") == 10
def test_grid_nonzero_category_never_invisible():
payload = _payload(
categories=[{"id": "memory", "label": "Memory", "tokens": 10}],
estimated_total=10,
context_used=10,
)
rows = render_context_grid(payload)
assert "▧" in " ".join(rows)
def test_grid_without_context_max_is_all_free():
rows = render_context_grid(_payload(context_max=0))
cells = " ".join(rows).split(" ")
assert set(cells) == {"·"}
def test_category_lines_include_tokens_percent_and_free_space():
lines = render_context_category_lines(_payload())
text = "\n".join(lines)
assert "Estimated usage by category" in text
assert "System prompt" in text and "10,000 tokens" in text
assert "5.0%" in text # 10k / 200k
assert "Free space" in text and "150,000 tokens" in text
def test_category_lines_no_categories():
lines = render_context_category_lines(_payload(categories=[]))
assert any("no data yet" in line for line in lines)
def test_breakdown_lines_grid_toggle():
with_grid = render_context_breakdown_lines(_payload(), grid=True)
without = render_context_breakdown_lines(_payload(), grid=False)
assert any("·" in line for line in with_grid[:5])
assert not any("·" in line for line in without[:2])
# Both include the window summary and the expand hint
for lines in (with_grid, without):
text = "\n".join(lines)
assert "Context window: 50,000 / 200,000 tokens (25%)" in text
assert "/context all" in text
def test_breakdown_lines_with_details_omits_hint():
details = {
"skills": [
{"name": "alpha", "index_tokens": 25, "skill_md_tokens": 800},
{"name": "beta", "index_tokens": 30, "skill_md_tokens": None},
],
"toolsets": [
{"toolset": "terminal", "tool_count": 3, "schema_tokens": 4_000},
],
}
lines = render_context_breakdown_lines(_payload(), details=details, grid=False)
text = "\n".join(lines)
assert "Toolsets by schema cost" in text
assert "terminal" in text and "4,000 tokens" in text
assert "Skills by cost" in text
assert "alpha" in text and "beta" in text
assert "n/a" in text # unmapped SKILL.md renders n/a, not a crash
assert "Use /context all" not in text
def test_details_lines_caps_listing():
details = {
"skills": [
{"name": f"skill-{i}", "index_tokens": 10, "skill_md_tokens": 100}
for i in range(20)
],
"toolsets": [],
}
lines = render_context_details_lines(details)
assert any("… and 5 more" in line for line in lines)
def test_compute_context_details_maps_bytes_to_tokens():
agent, parts = _make_agent(
stable=(
"base\n\n demo:\n"
" - hello: a demo skill\n"
),
)
fake_skills = [{
"name": "hello",
"index_line_bytes": 40,
"index_line_total_bytes": 40,
"index_line_shared_bytes": 0,
"index_line_skill_count": 1,
"skill_md_bytes": 401,
"path": "/tmp/hello/SKILL.md",
}]
fake_toolsets = [{"toolset": "terminal", "tool_count": 2, "json_bytes": 399}]
with patch("agent.system_prompt.build_system_prompt_parts", return_value=parts), \
patch("hermes_cli.prompt_size._compute_skills_breakdown", return_value=fake_skills), \
patch("hermes_cli.prompt_size._compute_toolsets_breakdown", return_value=fake_toolsets):
details = compute_context_details(agent)
assert details["skills"] == [
{"name": "hello", "index_tokens": 10, "skill_md_tokens": 101},
]
assert details["toolsets"] == [
{"toolset": "terminal", "tool_count": 2, "schema_tokens": 100},
]