0ef5fcb1c5
Security / Dependency audit (pip-audit) (push) Has been cancelled
Security / CodeQL (javascript-typescript) (push) Has been cancelled
Security / CodeQL (python) (push) Has been cancelled
Security / Secret scan (gitleaks) (push) Has been cancelled
rust / test (ubuntu) (push) Has been cancelled
rust / simulator e2e (macos-latest) (push) Has been cancelled
rust / simulator e2e (ubuntu-latest) (push) Has been cancelled
rust / simulator e2e (windows-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
rust / wheels (x86_64-unknown-linux-gnu) (push) Has been cancelled
rust / wheels (x86_64-apple-darwin) (push) Has been cancelled
rust / audit (push) Has been cancelled
rust / parity (nightly, allowed to fail during Phase 0) (push) Has been cancelled
CI / commitlint (push) Has been skipped
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 0s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 0s
Dev Containers / validate-worktree (push) Failing after 0s
CI / changes (push) Failing after 4s
Deploy Documentation / validate (push) Has been skipped
Deploy Documentation / deploy (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 1s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 1s
OpenCode Plugin / typecheck + build + test (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
Wrap E2E / docker-wrap-e2e (push) Failing after 1s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 1s
Init E2E / docker-init-e2e (push) Failing after 4s
Merge Conflicts / merge-conflicts (push) Failing after 4s
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-wheel-windows (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / promote-latest (push) Has been cancelled
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled
175 lines
6.3 KiB
Python
175 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
|