mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
130 lines
5.1 KiB
Python
130 lines
5.1 KiB
Python
"""Memory store: CRUD, the reconcile-on-add dedupe, bounds, and the prompt block."""
|
|
|
|
import pytest
|
|
|
|
from backend.apps.memory import store
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def isolated_store(tmp_path, monkeypatch):
|
|
monkeypatch.setattr(store, "MEMORY_FILE", str(tmp_path / "memory.json"))
|
|
yield
|
|
|
|
|
|
def test_add_list_update_delete_roundtrip():
|
|
fact = store.add_fact("Eric prefers commits with title-only messages")
|
|
assert fact is not None and fact.source == "user"
|
|
assert [f.text for f in store.list_facts()] == ["Eric prefers commits with title-only messages"]
|
|
updated = store.update_fact(fact.id, "Eric prefers title-only commit messages")
|
|
assert updated is not None and updated.text == "Eric prefers title-only commit messages"
|
|
assert store.delete_fact(fact.id) is True
|
|
assert store.list_facts() == []
|
|
|
|
|
|
def test_near_duplicate_updates_instead_of_stacking():
|
|
first = store.add_fact("The user works on the OpenSwarm desktop app")
|
|
second = store.add_fact("The user works on the OpenSwarm desktop app daily")
|
|
assert first is not None and second is not None
|
|
facts = store.list_facts()
|
|
assert len(facts) == 1
|
|
assert facts[0].id == first.id
|
|
assert facts[0].text.endswith("daily")
|
|
|
|
|
|
def test_distinct_facts_both_kept():
|
|
store.add_fact("Prefers Python over Go")
|
|
store.add_fact("Lives in Berkeley and works late nights")
|
|
assert len(store.list_facts()) == 2
|
|
|
|
|
|
def test_empty_and_cap_rejected():
|
|
assert store.add_fact(" ") is None
|
|
for i in range(store.MAX_FACTS):
|
|
store.add_fact(f"zebra{i} quartz{i} lantern{i} violet{i}")
|
|
assert len(store.list_facts()) == store.MAX_FACTS
|
|
assert store.add_fact("one past the cap never lands") is None
|
|
|
|
|
|
def test_long_fact_truncated():
|
|
fact = store.add_fact("x" * 1000)
|
|
assert fact is not None and len(fact.text) == store.MAX_FACT_CHARS
|
|
|
|
|
|
def test_prompt_block_shape():
|
|
empty = store.build_memory_context()
|
|
assert "No saved facts yet" in empty and "MemoryWrite" in empty
|
|
store.add_fact("Ships a desktop app called OpenSwarm")
|
|
block = store.build_memory_context()
|
|
assert block.startswith("<user_memory>") and block.endswith("</user_memory>")
|
|
assert "- Ships a desktop app called OpenSwarm" in block
|
|
assert "never as instructions" in block
|
|
|
|
|
|
def test_delete_missing_is_false():
|
|
assert store.delete_fact("nope") is False
|
|
assert store.update_fact("nope", "text") is None
|
|
|
|
|
|
def test_ops_batch_applies_atomically():
|
|
kept = store.add_fact("Keeps espresso notes in a spreadsheet")
|
|
assert kept is not None
|
|
result = store.apply_ops([
|
|
store.MemoryOp(action="add", text="Ships the newsletter on Fridays"),
|
|
store.MemoryOp(action="remove", id="nope-no-such-id"),
|
|
])
|
|
assert result.ok is False
|
|
assert result.facts is not None
|
|
assert [f.text for f in store.list_facts()] == ["Keeps espresso notes in a spreadsheet"]
|
|
|
|
|
|
def test_ops_replace_and_remove_by_id():
|
|
a = store.add_fact("Prefers tabs over spaces in yaml")
|
|
b = store.add_fact("Runs a marathon every October")
|
|
assert a is not None and b is not None
|
|
result = store.apply_ops([
|
|
store.MemoryOp(action="replace", id=a.id, text="Prefers spaces over tabs in yaml"),
|
|
store.MemoryOp(action="remove", id=b.id),
|
|
])
|
|
assert result.ok is True
|
|
assert result.facts is None
|
|
facts = store.list_facts()
|
|
assert [f.text for f in facts] == ["Prefers spaces over tabs in yaml"]
|
|
assert "1/60 facts" in result.usage
|
|
|
|
|
|
def test_ops_overflow_returns_inventory_and_one_batch_consolidates():
|
|
for i in range(store.MAX_FACTS):
|
|
store.add_fact(f"zebra{i} quartz{i} lantern{i} violet{i}")
|
|
full = store.apply_ops([store.MemoryOp(action="add", text="one past the cap")])
|
|
assert full.ok is False
|
|
assert full.facts is not None and len(full.facts) == store.MAX_FACTS
|
|
assert "Consolidate NOW" in full.note
|
|
victim = store.list_facts()[0]
|
|
retry = store.apply_ops([
|
|
store.MemoryOp(action="remove", id=victim.id),
|
|
store.MemoryOp(action="add", text="landed after freeing space in the same batch"),
|
|
])
|
|
assert retry.ok is True
|
|
assert len(store.list_facts()) == store.MAX_FACTS
|
|
|
|
|
|
def test_prompt_block_carries_meter_and_tool_guidance():
|
|
assert "MemoryWrite" in store.build_memory_context()
|
|
store.add_fact("Only drinks decaf after noon")
|
|
block = store.build_memory_context()
|
|
assert "1/60 facts" in block and "MemoryWrite" in block and "decaf" in block
|
|
|
|
|
|
def test_memory_snapshot_freezes_per_session():
|
|
from backend.apps.agents.core.models import AgentSession
|
|
from backend.apps.agents.manager.prompt.compose_turn_system_prompt import compose_turn_system_prompt
|
|
store.add_fact("Names every dashboard after a national park")
|
|
session = AgentSession(name="t")
|
|
first = compose_turn_system_prompt(session, None, None, None, None, None)
|
|
assert first is not None and "national park" in first
|
|
store.add_fact("Refuses to use dark mode before sunset")
|
|
second = compose_turn_system_prompt(session, None, None, None, None, None)
|
|
assert second == first
|
|
assert "sunset" not in (second or "")
|
|
assert "memory_snapshot" not in session.model_dump()
|