mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
174 lines
6.3 KiB
Python
174 lines
6.3 KiB
Python
"""Tests for the Memory Budget Manager."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from headroom.memory.budget import (
|
|
BudgetConfig,
|
|
MemoryBudgetManager,
|
|
)
|
|
from headroom.memory.writers.base import MemoryEntry
|
|
|
|
|
|
def _make_entry(
|
|
content: str = "test memory",
|
|
importance: float = 0.5,
|
|
age_days: float = 0,
|
|
access_count: int = 0,
|
|
entity_refs: list[str] | None = None,
|
|
category: str = "",
|
|
) -> MemoryEntry:
|
|
return MemoryEntry(
|
|
content=content,
|
|
importance=importance,
|
|
created_at=time.time() - age_days * 86400,
|
|
access_count=access_count,
|
|
entity_refs=entity_refs or [],
|
|
category=category,
|
|
)
|
|
|
|
|
|
class TestBudgetManager:
|
|
@pytest.fixture
|
|
def manager(self, tmp_path: Path) -> MemoryBudgetManager:
|
|
config = BudgetConfig(staleness_check_git=False) # No git for unit tests
|
|
return MemoryBudgetManager(project_path=tmp_path, config=config)
|
|
|
|
def test_basic_optimize(self, manager: MemoryBudgetManager):
|
|
entries = [_make_entry(f"Memory {i}", importance=0.5 + i * 0.1) for i in range(5)]
|
|
optimized, report = manager.optimize(entries, "generic")
|
|
|
|
assert report.total_memories == 5
|
|
assert report.kept > 0
|
|
assert report.tokens_after <= 3000 # Default budget
|
|
|
|
def test_budget_limits(self, manager: MemoryBudgetManager):
|
|
# Create many entries that exceed budget
|
|
entries = [
|
|
_make_entry(f"A long memory content string number {i} " * 10, importance=0.8)
|
|
for i in range(50)
|
|
]
|
|
optimized, report = manager.optimize(entries, "claude") # 2000 token budget
|
|
|
|
assert report.pruned_budget > 0
|
|
assert report.kept < 50
|
|
|
|
def test_temporal_decay(self, manager: MemoryBudgetManager):
|
|
old = _make_entry("Old memory", importance=0.5, age_days=30)
|
|
new = _make_entry("New memory", importance=0.5, age_days=0)
|
|
|
|
optimized, report = manager.optimize([old, new], "generic")
|
|
|
|
# New should rank higher
|
|
if len(optimized) >= 2:
|
|
assert optimized[0].content == "New memory"
|
|
|
|
def test_access_count_boost(self, manager: MemoryBudgetManager):
|
|
unused = _make_entry("Unused", importance=0.5, age_days=5, access_count=0)
|
|
used = _make_entry("Heavily used", importance=0.5, age_days=5, access_count=10)
|
|
|
|
optimized, report = manager.optimize([unused, used], "generic")
|
|
|
|
if len(optimized) >= 2:
|
|
# Used memory should have higher importance after decay+boost
|
|
assert optimized[0].content == "Heavily used"
|
|
|
|
def test_merge_similar(self, manager: MemoryBudgetManager):
|
|
entries = [
|
|
_make_entry(
|
|
"Use source .venv/bin/activate && pytest for running tests", importance=0.5
|
|
),
|
|
_make_entry(
|
|
"Use source .venv/bin/activate && pytest for running tests", importance=0.8
|
|
),
|
|
_make_entry("Something completely different about architecture", importance=0.6),
|
|
]
|
|
optimized, report = manager.optimize(entries, "generic")
|
|
|
|
# The two identical ones should be merged
|
|
assert report.merged >= 1
|
|
assert report.kept <= 2
|
|
|
|
def test_very_old_pruned(self, manager: MemoryBudgetManager):
|
|
"""Test that very old, low-importance memories get pruned by decay."""
|
|
entries = [
|
|
_make_entry("Ancient memory", importance=0.2, age_days=100),
|
|
_make_entry("Recent memory", importance=0.5, age_days=0),
|
|
]
|
|
optimized, report = manager.optimize(entries, "generic")
|
|
|
|
contents = [m.content for m in optimized]
|
|
assert "Recent memory" in contents
|
|
# Ancient memory with 0.2 importance after 100 days of decay should be below threshold
|
|
|
|
def test_report_tokens(self, manager: MemoryBudgetManager):
|
|
# Use entries large enough to trigger budget pruning
|
|
entries = [
|
|
_make_entry(f"A very long memory content entry {i} " * 30, importance=0.8)
|
|
for i in range(20)
|
|
]
|
|
_, report = manager.optimize(entries, "claude") # 2000 token budget
|
|
|
|
assert report.tokens_before > 0
|
|
assert report.pruned_budget > 0 # Should drop some due to budget
|
|
assert report.kept < 20
|
|
|
|
|
|
class TestStalenessDetection:
|
|
def test_stale_file_ref(self, tmp_path: Path):
|
|
"""Test that memories referencing non-existent files are flagged stale."""
|
|
# Create a manager with git check disabled but file-exists check active
|
|
config = BudgetConfig(staleness_check_git=False)
|
|
manager = MemoryBudgetManager(project_path=tmp_path, config=config)
|
|
|
|
# The entity_refs check uses Path.exists(), so non-existent paths flag stale
|
|
entries = [
|
|
_make_entry(
|
|
"File `/nonexistent/path/foo.py` has the auth logic",
|
|
importance=0.5,
|
|
entity_refs=["/nonexistent/path/foo.py"],
|
|
),
|
|
_make_entry(
|
|
"Use pytest for tests",
|
|
importance=0.5,
|
|
),
|
|
]
|
|
# With git disabled, staleness only checks entity_refs via Path.exists()
|
|
optimized, report = manager.optimize(entries, "generic")
|
|
|
|
assert report.pruned_staleness >= 1
|
|
|
|
def test_fresh_file_kept(self, tmp_path: Path):
|
|
"""Test that memories referencing existing files are kept."""
|
|
# Create the referenced file
|
|
(tmp_path / "real_file.py").write_text("# exists")
|
|
|
|
config = BudgetConfig(staleness_check_git=False)
|
|
manager = MemoryBudgetManager(project_path=tmp_path, config=config)
|
|
|
|
entries = [
|
|
_make_entry(
|
|
"Real file has important code",
|
|
importance=0.5,
|
|
entity_refs=[str(tmp_path / "real_file.py")],
|
|
),
|
|
]
|
|
optimized, report = manager.optimize(entries, "generic")
|
|
|
|
assert report.pruned_staleness == 0
|
|
assert report.kept == 1
|
|
|
|
|
|
class TestBudgetConfig:
|
|
def test_default_budgets(self):
|
|
config = BudgetConfig()
|
|
assert config.agent_budgets["claude"] == 2000
|
|
assert config.agent_budgets["cursor"] == 3000
|
|
|
|
def test_custom_budgets(self):
|
|
config = BudgetConfig(agent_budgets={"claude": 5000})
|
|
assert config.agent_budgets["claude"] == 5000
|