"""Testy context.py — odhad tokenů, TokenStats, kompakce kontextu.""" from types import SimpleNamespace from context import (TRUNCATION_MARK, TokenStats, compact_messages, estimate_tokens) def _msgs(*parts): return list(parts) def sys_msg(text="system prompt"): return {"role": "system", "content": text} def user_msg(text): return {"role": "user", "content": text} def asst_tool_call(name, call_id): return {"role": "assistant", "content": "", "tool_calls": [{"id": call_id, "type": "function", "function": {"name": name, "arguments": "{}"}}]} def tool_msg(call_id, content): return {"role": "tool", "tool_call_id": call_id, "content": content} # ---------------------------------------------------------------- odhad def test_estimate_tokens_scales_with_length(): short = estimate_tokens([user_msg("abcd" * 10)]) long = estimate_tokens([user_msg("abcd" * 1000)]) assert long > short * 10 # ~4 znaky/token + režie assert abs(estimate_tokens([user_msg("x" * 4000)]) - 1004) < 50 def test_estimate_counts_tool_calls(): plain = estimate_tokens([{"role": "assistant", "content": "hi"}]) with_calls = estimate_tokens([asst_tool_call("run_command", "c1")]) assert with_calls > plain # ---------------------------------------------------------------- TokenStats def test_token_stats_object_and_dict_usage(): stats = TokenStats() stats.add("local", SimpleNamespace(prompt_tokens=100, completion_tokens=20)) stats.add("api", {"prompt_tokens": 50, "completion_tokens": 5}) stats.add("local", None) # cache hit / bez usage — nepočítá se snap = stats.snapshot() assert snap["calls"] == 2 assert snap["prompt_tokens"] == 150 assert snap["completion_tokens"] == 25 assert snap["total_tokens"] == 175 assert snap["last_context_tokens"] == 50 assert snap["by_source"]["local"]["calls"] == 1 assert snap["by_source"]["api"]["prompt_tokens"] == 50 def test_token_stats_saved(): stats = TokenStats() stats.add_saved(500) stats.add_saved(-10) # záporné se ignoruje assert stats.snapshot()["saved_by_compaction_tokens"] == 500 # ---------------------------------------------------------------- kompakce def _history(steps, result_chars=4000): msgs = [sys_msg(), user_msg("Oprav bug v projektu")] for i in range(steps): msgs.append(asst_tool_call(f"tool_{i}", f"c{i}")) msgs.append(tool_msg(f"c{i}", "x" * result_chars)) return msgs def test_under_budget_untouched(): msgs = _history(2) out, saved = compact_messages(msgs, budget_tokens=10**6) assert out == msgs and saved == 0 def test_phase1_ages_old_tool_results(): msgs = _history(10) out, saved = compact_messages(msgs, budget_tokens=8000, keep_last_steps=2, aged_chars=500) assert saved > 0 # staré tool výsledky zkráceny, poslední 2 bloky netknuté aged = [m for m in out if m.get("role") == "tool" and TRUNCATION_MARK in m["content"]] full = [m for m in out if m.get("role") == "tool" and TRUNCATION_MARK not in m["content"]] assert len(aged) >= 1 assert len(full) >= 2 assert out[-1]["content"].endswith("x" * 100) # poslední výsledek celý # vstup nemodifikován assert TRUNCATION_MARK not in msgs[3]["content"] def test_phase2_drops_oldest_blocks_with_summary(): msgs = _history(20) out, saved = compact_messages(msgs, budget_tokens=4000, keep_last_steps=3, aged_chars=300) assert saved > 0 assert estimate_tokens(out) <= 4000 # kotva zachována assert out[0]["role"] == "system" assert out[1]["role"] == "user" and "Oprav bug" in out[1]["content"] # souhrnná poznámka o vypuštění summary = [m for m in out if "Kompakce kontextu" in str(m.get("content"))] assert len(summary) == 1 assert "tool_0" in summary[0]["content"] # struktura validní: každá tool zpráva má před sebou assistant s tool_calls for i, m in enumerate(out): if m.get("role") == "tool": prev = next(p for p in reversed(out[:i]) if p.get("role") in ("assistant", "user", "system")) assert prev.get("tool_calls"), "tool zpráva bez tool_calls předchůdce" # poslední 3 bloky přežily tool_names = [tc["function"]["name"] for m in out for tc in (m.get("tool_calls") or [])] assert tool_names[-3:] == ["tool_17", "tool_18", "tool_19"] def test_best_effort_when_budget_impossible(): msgs = _history(20) out, saved = compact_messages(msgs, budget_tokens=10, keep_last_steps=3, aged_chars=300) # nevejde se ani po maximální kompakci — vrací best effort, nikoli prázdno assert saved > 0 assert any(m.get("role") == "user" for m in out) assert estimate_tokens(out) < estimate_tokens(msgs)