190 lines
6.1 KiB
Python
190 lines
6.1 KiB
Python
"""P4 gate tests — memory store + sessions."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import aisuite as ai
|
|
from coworker.conversations import ConversationStore
|
|
from coworker.memory import Scope, SQLiteMemoryStore, format_memories, memory_tools
|
|
from coworker.sessions import SessionRecord
|
|
from coworker.tools import ToolRegistry
|
|
|
|
|
|
def _store(tmp_path):
|
|
return SQLiteMemoryStore(tmp_path / "mem.db")
|
|
|
|
|
|
# -- memory store ---------------------------------------------------------------
|
|
|
|
|
|
def test_memory_round_trip(tmp_path):
|
|
store = _store(tmp_path)
|
|
item = store.add(
|
|
"prefers tabs over spaces", scope=Scope.WORKSPACE, workspace="/proj"
|
|
)
|
|
assert store.get(item.id).content == "prefers tabs over spaces"
|
|
assert [m.content for m in store.list(workspace="/proj")] == [
|
|
"prefers tabs over spaces"
|
|
]
|
|
|
|
|
|
def test_workspace_scope_isolation(tmp_path):
|
|
store = _store(tmp_path)
|
|
store.add("A secret", scope=Scope.WORKSPACE, workspace="/proj/a")
|
|
assert store.list(workspace="/proj/b") == []
|
|
assert len(store.list(workspace="/proj/a")) == 1
|
|
|
|
|
|
def test_global_scope_visible_regardless_of_workspace(tmp_path):
|
|
store = _store(tmp_path)
|
|
store.add("use 2-space indent everywhere", scope=Scope.GLOBAL)
|
|
assert len(store.list(scope=Scope.GLOBAL)) == 1
|
|
|
|
|
|
def test_memory_listable_and_editable(tmp_path):
|
|
store = _store(tmp_path)
|
|
item = store.add("old note", scope=Scope.WORKSPACE, workspace="/proj")
|
|
updated = store.update(item.id, "new note")
|
|
assert updated.content == "new note"
|
|
assert store.delete(item.id) is True
|
|
assert store.get(item.id) is None
|
|
|
|
|
|
def test_format_memories_shows_ids(tmp_path):
|
|
store = _store(tmp_path)
|
|
item = store.add("fact one", workspace="/proj")
|
|
rendered = format_memories(store.list(workspace="/proj"))
|
|
assert "fact one" in rendered and "Known memories" in rendered
|
|
assert f"[#{item.id}]" in rendered # ids let the agent update/forget
|
|
|
|
|
|
# -- remember tool --------------------------------------------------------------
|
|
|
|
|
|
def test_remember_tool_persists(tmp_path):
|
|
store = _store(tmp_path)
|
|
reg = ToolRegistry()
|
|
reg.register_all(memory_tools(store, workspace="/proj"))
|
|
assert "remember" in reg.names()
|
|
|
|
result = reg.execute("remember", {"content": "deploys on Fridays are banned"})
|
|
assert result["saved"] is True
|
|
assert any(
|
|
m.content == "deploys on Fridays are banned"
|
|
for m in store.list(workspace="/proj")
|
|
)
|
|
|
|
|
|
def test_memory_update_and_forget_tools(tmp_path):
|
|
store = _store(tmp_path)
|
|
reg = ToolRegistry()
|
|
reg.register_all(memory_tools(store, workspace="/proj"))
|
|
assert {"remember", "memory_update", "memory_forget"} <= set(reg.names())
|
|
|
|
saved = reg.execute("remember", {"content": "uses npm"})
|
|
updated = reg.execute(
|
|
"memory_update", {"memory_id": saved["id"], "content": "uses pnpm, not npm"}
|
|
)
|
|
assert updated["updated"] is True
|
|
assert store.get(saved["id"]).content == "uses pnpm, not npm"
|
|
|
|
gone = reg.execute("memory_forget", {"memory_id": saved["id"]})
|
|
assert gone["deleted"] is True
|
|
assert store.get(saved["id"]) is None
|
|
|
|
|
|
def test_memory_update_and_forget_unknown_id(tmp_path):
|
|
store = _store(tmp_path)
|
|
reg = ToolRegistry()
|
|
reg.register_all(memory_tools(store, workspace="/proj"))
|
|
assert (
|
|
"no memory"
|
|
in reg.execute("memory_update", {"memory_id": 99, "content": "x"})["error"]
|
|
)
|
|
assert "no memory" in reg.execute("memory_forget", {"memory_id": 99})["error"]
|
|
|
|
|
|
# -- sessions -------------------------------------------------------------------
|
|
|
|
|
|
def test_session_save_and_resume(tmp_path):
|
|
store = ConversationStore(tmp_path)
|
|
messages = [
|
|
{"role": "system", "content": "be helpful"},
|
|
{"role": "user", "content": "hi"},
|
|
{"role": "assistant", "content": "hello"},
|
|
]
|
|
store.save(
|
|
SessionRecord(
|
|
session_id="s1",
|
|
workspace="/proj",
|
|
model="gpt-5.5",
|
|
mode="interactive",
|
|
messages=messages,
|
|
)
|
|
)
|
|
loaded = store.load("s1")
|
|
assert loaded is not None
|
|
assert loaded.messages == messages
|
|
assert loaded.model == "gpt-5.5"
|
|
# messages live in an append-only jsonl, not the index db
|
|
assert (tmp_path / "conversations" / "s1.jsonl").exists()
|
|
|
|
|
|
class _StubProvider:
|
|
def complete(self, **kwargs): # pragma: no cover - not invoked
|
|
raise NotImplementedError
|
|
|
|
def capabilities(self, model): # pragma: no cover
|
|
raise NotImplementedError
|
|
|
|
|
|
def test_build_code_engine_injects_memory(tmp_path):
|
|
from coworker.agent import build_code_engine
|
|
|
|
workspace = str(tmp_path.resolve())
|
|
store = SQLiteMemoryStore(tmp_path / "mem.db")
|
|
store.add(
|
|
"always run black before committing", scope=Scope.WORKSPACE, workspace=workspace
|
|
)
|
|
|
|
engine = build_code_engine(
|
|
workspace=tmp_path, provider=_StubProvider(), memory_store=store
|
|
)
|
|
try:
|
|
assert {"remember", "memory_update", "memory_forget"} <= set(
|
|
engine.registry.names()
|
|
)
|
|
assert engine.messages[0]["role"] == "system"
|
|
assert "always run black" in engine.messages[0]["content"]
|
|
# when-to-remember guidance rides along with the tools
|
|
assert "memory_update" in engine.messages[0]["content"]
|
|
assert (
|
|
"Don't save what the repo already records" in engine.messages[0]["content"]
|
|
)
|
|
finally:
|
|
engine.executor.close()
|
|
|
|
|
|
def test_session_append_only_and_list(tmp_path):
|
|
store = ConversationStore(tmp_path)
|
|
store.save(
|
|
SessionRecord(
|
|
"s1", "/proj", "gpt-5.5", "interactive", [{"role": "user", "content": "a"}]
|
|
)
|
|
)
|
|
store.save(
|
|
SessionRecord(
|
|
"s1",
|
|
"/proj",
|
|
"gpt-5.5",
|
|
"interactive",
|
|
[{"role": "user", "content": "a"}, {"role": "user", "content": "b"}],
|
|
)
|
|
)
|
|
loaded = store.load("s1")
|
|
assert len(loaded.messages) == 2 # appended, not duplicated
|
|
listed = store.list(workspace="/proj")
|
|
assert len(listed) == 1
|
|
assert listed[0].message_count == 2
|
|
assert listed[0].title == "a"
|