Files
maskanx_cm_backend/tests/test_prompt_cache.py
T

245 lines
8.4 KiB
Python
Raw Normal View History

2026-08-01 10:28:22 +05:30
# -*- coding: utf-8 -*-
"""Tests for CachedPromptBuilder, DynamicContext, PersonaPromptPool."""
from dataclasses import dataclass
from pathlib import Path
import pytest
from adclaw.agents.prompt import (
DEFAULT_SYS_PROMPT,
CachedPromptBuilder,
CachedSection,
DynamicContext,
PersonaPromptPool,
select_memory_tier,
)
@dataclass
class FakePersona:
id: str = "writer"
name: str = "Writer"
soul_md: str = "I write content."
@pytest.fixture
def working_dir(tmp_path):
(tmp_path / "AGENTS.md").write_text("Agent instructions here.")
(tmp_path / "SOUL.md").write_text("Soul identity here.")
return tmp_path
# ---------------------------------------------------------------------------
# CachedSection Tests
# ---------------------------------------------------------------------------
class TestCachedSection:
def test_load_reads_file(self, tmp_path):
f = tmp_path / "test.md"
f.write_text("hello world")
section = CachedSection(path=f)
content = section.load()
assert content == "hello world"
assert section.content_hash != ""
def test_cache_hit_on_unchanged(self, tmp_path):
f = tmp_path / "test.md"
f.write_text("hello")
section = CachedSection(path=f)
section.load()
hash1 = section.content_hash
# Load again — should use cache
section.load(force=True)
assert section.content_hash == hash1
def test_cache_miss_on_change(self, tmp_path):
f = tmp_path / "test.md"
f.write_text("hello")
section = CachedSection(path=f)
section.load()
hash1 = section.content_hash
# Change file
f.write_text("world")
section.load(force=True)
assert section.content_hash != hash1
assert section.content == "world"
def test_missing_file_returns_empty(self, tmp_path):
section = CachedSection(path=tmp_path / "nonexistent.md")
assert section.load() == ""
def test_strips_yaml_frontmatter(self, tmp_path):
f = tmp_path / "test.md"
f.write_text("---\ntitle: test\n---\nActual content")
section = CachedSection(path=f)
assert section.load() == "Actual content"
# ---------------------------------------------------------------------------
# DynamicContext Tests
# ---------------------------------------------------------------------------
class TestDynamicContext:
def test_empty_renders_empty(self):
ctx = DynamicContext()
assert ctx.render() == ""
def test_env_only(self):
ctx = DynamicContext(env_context="Platform: linux")
assert ctx.render() == "Platform: linux"
def test_all_fields(self):
ctx = DynamicContext(
env_context="env",
aom_tier="memory data",
aom_tier_name="L2",
active_tools="tool1, tool2",
team_summary="team info",
)
rendered = ctx.render()
assert "env" in rendered
assert "# Memory Context (L2)" in rendered
assert "memory data" in rendered
assert "# Active Tools" in rendered
assert "# Team Summary" in rendered
def test_team_summary_gets_header(self):
ctx = DynamicContext(team_summary="Some team info")
assert "# Team Summary" in ctx.render()
# ---------------------------------------------------------------------------
# CachedPromptBuilder Tests
# ---------------------------------------------------------------------------
class TestCachedPromptBuilder:
def test_builds_static_from_files(self, working_dir):
builder = CachedPromptBuilder(working_dir=working_dir)
prompt = builder.static_prompt
assert "Agent instructions here." in prompt
assert "Soul identity here." in prompt
assert "# AGENTS.md" in prompt
def test_static_cache_hit(self, working_dir):
builder = CachedPromptBuilder(working_dir=working_dir)
p1 = builder.static_prompt
p2 = builder.static_prompt
assert p1 is p2 # same object = cache hit
def test_static_invalidation_on_file_change(self, working_dir):
builder = CachedPromptBuilder(working_dir=working_dir)
p1 = builder.static_prompt
# Change file
(working_dir / "AGENTS.md").write_text("New instructions.")
builder.invalidate()
p2 = builder.static_prompt
assert p1 != p2
assert "New instructions." in p2
def test_build_with_dynamic(self, working_dir):
builder = CachedPromptBuilder(working_dir=working_dir)
dynamic = DynamicContext(env_context="Platform: linux")
prompt = builder.build(dynamic=dynamic)
assert "Agent instructions" in prompt
assert "Platform: linux" in prompt
def test_build_without_dynamic(self, working_dir):
builder = CachedPromptBuilder(working_dir=working_dir)
assert builder.build() == builder.static_prompt
def test_persona_override(self, working_dir):
persona = FakePersona()
builder = CachedPromptBuilder(working_dir=working_dir, persona=persona)
prompt = builder.static_prompt
assert "I write content." in prompt
assert "# SOUL.md (Writer)" in prompt
def test_set_persona_invalidates(self, working_dir):
builder = CachedPromptBuilder(working_dir=working_dir)
p1 = builder.static_prompt
builder.set_persona(FakePersona())
p2 = builder.static_prompt
assert p1 != p2
def test_missing_required_returns_default(self, tmp_path):
# No AGENTS.md
(tmp_path / "SOUL.md").write_text("soul")
builder = CachedPromptBuilder(working_dir=tmp_path)
assert builder.static_prompt == DEFAULT_SYS_PROMPT
# ---------------------------------------------------------------------------
# PersonaPromptPool Tests
# ---------------------------------------------------------------------------
class TestPersonaPromptPool:
def test_get_default(self, working_dir):
pool = PersonaPromptPool(working_dir=working_dir)
builder = pool.get()
assert isinstance(builder, CachedPromptBuilder)
assert pool.size == 1
def test_persona_isolation(self, working_dir):
pool = PersonaPromptPool(working_dir=working_dir)
b1 = pool.get()
b2 = pool.get(FakePersona(id="writer"))
assert b1 is not b2
assert pool.size == 2
def test_same_persona_reuses(self, working_dir):
pool = PersonaPromptPool(working_dir=working_dir)
persona = FakePersona(id="writer")
b1 = pool.get(persona)
b2 = pool.get(persona)
assert b1 is b2
def test_invalidate_all(self, working_dir):
pool = PersonaPromptPool(working_dir=working_dir)
pool.get()
pool.get(FakePersona())
pool.invalidate_all()
assert pool.size == 0
# ---------------------------------------------------------------------------
# select_memory_tier Tests
# ---------------------------------------------------------------------------
class TestSelectMemoryTier:
def test_selects_richest_that_fits(self):
tiers = {"L0": "short", "L1": "medium length text", "L2": "a" * 1000}
name, content = select_memory_tier(tiers, available_tokens=500, static_tokens=100)
assert name == "L2"
assert content == "a" * 1000
def test_falls_back_to_smaller(self):
tiers = {"L0": "tiny", "L1": "small", "L2": "a" * 100000}
name, _content = select_memory_tier(tiers, available_tokens=100, static_tokens=90)
assert name in ("L0", "L1")
def test_empty_tiers_returns_l0(self):
tiers = {"L0": "", "L1": "", "L2": ""}
name, _content = select_memory_tier(tiers, available_tokens=1000, static_tokens=0)
assert name == "L0"
def test_negative_budget_returns_empty(self):
"""When static_tokens > available_tokens, no tier should be injected."""
tiers = {"L0": "some content", "L1": "more", "L2": "a" * 1000}
_name, content = select_memory_tier(tiers, available_tokens=100, static_tokens=200)
assert content == ""
class TestPersonaPromptPoolEviction:
def test_eviction_at_max_size(self, working_dir):
pool = PersonaPromptPool(working_dir=working_dir)
# Fill pool to max
for i in range(PersonaPromptPool._MAX_POOL_SIZE + 5):
pool.get(FakePersona(id=f"persona_{i}", name=f"P{i}", soul_md=f"Soul {i}"))
assert pool.size <= PersonaPromptPool._MAX_POOL_SIZE