study-buddy / tests /test_student_memory.py
GitHub Actions
deploy b3d187d69e5df10bb2ec396e89f539f853465a06
14184e3
Raw
History Blame Contribute Delete
15 kB
import inspect
import sys
import types
from types import SimpleNamespace
import pytest
from app.services.student_memory import StudentMemoryService
def install_fake_cognee(monkeypatch, **attrs):
module = types.ModuleType("cognee")
for key, value in attrs.items():
setattr(module, key, value)
monkeypatch.setitem(sys.modules, "cognee", module)
return module
def fake_search_type():
return SimpleNamespace(GRAPH_COMPLETION="graph", TEMPORAL="temporal", AGENTIC_COMPLETION="agentic")
@pytest.mark.asyncio
async def test_project_observation_is_quarantined_before_cognee_write(monkeypatch, tmp_path):
import app.services.student_memory as student_memory
monkeypatch.setattr(student_memory, "MEMORY_ROOT", tmp_path)
calls = []
async def remember(text, **kwargs):
calls.append(("remember", kwargs["dataset_name"]))
install_fake_cognee(monkeypatch, remember=remember)
ok = await StudentMemoryService().stage_project_observation("p1", "Attention", ["student connected QK lookup"])
assert ok is True
assert calls == []
pending = StudentMemoryService().list_pending_memory("p1")
assert len(pending) == 1
assert pending[0]["dataset"] == "project_p1"
assert "student connected QK lookup" in pending[0]["text"]
@pytest.mark.asyncio
async def test_pending_memory_promotion_discards_one_off_entries(monkeypatch, tmp_path):
import app.services.student_memory as student_memory
monkeypatch.setattr(student_memory, "MEMORY_ROOT", tmp_path)
calls = []
class Datasets:
async def list_datasets(self):
return [SimpleNamespace(name="project_p1"), SimpleNamespace(name="research_profile")]
async def remember(text, **kwargs):
calls.append((kwargs["dataset_name"], text))
install_fake_cognee(monkeypatch, datasets=Datasets(), remember=remember)
service = StudentMemoryService()
service.stage_pending_memory("p1", dataset="project_p1", session_id="p1", text="Student briefly mentioned DeepSeek once.", kind="project")
service.stage_pending_memory("p1", dataset="research_profile", session_id="p1", text="Durable learner signal: Student prefers mechanism-first explanations.", kind="profile")
result = await service.promote_pending_memory("p1")
assert result["promoted"] == 1
assert result["discarded"] == 1
assert calls == [("research_profile", "Durable learner signal: Student prefers mechanism-first explanations.")]
assert service.list_pending_memory("p1") == []
@pytest.mark.asyncio
async def test_profile_write_failure_does_not_block_project_write(monkeypatch):
calls = []
class Datasets:
async def list_datasets(self):
return [SimpleNamespace(name="project_p1"), SimpleNamespace(name="research_profile")]
async def remember(text, **kwargs):
calls.append(kwargs["dataset_name"])
if kwargs["dataset_name"] == "research_profile":
raise RuntimeError("profile write failed")
install_fake_cognee(monkeypatch, datasets=Datasets(), remember=remember)
await StudentMemoryService().stage_profile_observation("p1", "Student prefers concise explanations")
assert calls == ["research_profile"]
@pytest.mark.asyncio
async def test_generic_session_summary_does_not_pollute_profile_memory(monkeypatch):
calls = []
class Datasets:
async def list_datasets(self):
return [SimpleNamespace(name="project_p1"), SimpleNamespace(name="research_profile")]
async def remember(text, **kwargs):
calls.append(kwargs["dataset_name"])
install_fake_cognee(monkeypatch, datasets=Datasets(), remember=remember)
await StudentMemoryService().push_session("p1", "Topic", [], [], "This session covered Adam and bias correction.")
assert calls == []
@pytest.mark.asyncio
async def test_recall_uses_only_context_and_falls_back_on_typeerror(monkeypatch):
calls = []
async def recall(**kwargs):
calls.append(kwargs)
if "only_context" in kwargs:
raise TypeError("unexpected keyword")
return ["memory context"]
install_fake_cognee(monkeypatch, SearchType=fake_search_type(), recall=recall)
result = await StudentMemoryService().query_prior_knowledge("attention", project_id="p1")
assert "memory context" in result
assert calls[0]["only_context"] is True
assert calls[0]["feedback_influence"] == 0.35
assert "only_context" not in calls[1]
@pytest.mark.asyncio
async def test_temporal_recall_uses_temporal_search_type(monkeypatch):
calls = []
async def recall(**kwargs):
calls.append(kwargs)
return ["changed over time"]
install_fake_cognee(monkeypatch, SearchType=fake_search_type(), recall=recall)
result = await StudentMemoryService().query_prior_knowledge("attention", project_id="p1", mode="temporal")
assert "changed over time" in result
assert calls[0]["query_type"] == "temporal"
@pytest.mark.asyncio
async def test_profile_recall_query_is_name_aware(monkeypatch):
calls = []
async def recall(**kwargs):
calls.append(kwargs)
return ["Preferred name: Anshuman"]
install_fake_cognee(monkeypatch, SearchType=fake_search_type(), recall=recall)
result = await StudentMemoryService().query_prior_knowledge("attention", project_id="p1", mode="profile")
assert "Anshuman" in result
assert "preferred name" in calls[0]["query_text"]
assert "call me" in calls[0]["query_text"]
@pytest.mark.asyncio
async def test_style_feedback_is_profile_memory_not_native_weighting(monkeypatch):
calls = []
class Session:
async def add_feedback(self, **kwargs):
calls.append(("feedback", kwargs))
return True
async def add_frequency_weights(self, **kwargs):
calls.append(("weights", kwargs))
return True
class Datasets:
async def list_datasets(self):
return [SimpleNamespace(name="research_profile")]
async def remember(text, **kwargs):
calls.append(("remember", kwargs))
install_fake_cognee(monkeypatch, session=Session(), datasets=Datasets(), remember=remember)
result = await StudentMemoryService().record_style_feedback("p1", "more concise")
assert result == {"profile_memory": True}
assert [call[0] for call in calls] == ["remember"]
@pytest.mark.asyncio
async def test_native_feedback_requires_cognee_recall_ids(monkeypatch):
calls = []
class Session:
async def add_feedback(self, **kwargs):
calls.append(("feedback", kwargs))
return True
async def add_frequency_weights(self, **kwargs):
calls.append(("weights", kwargs))
return True
install_fake_cognee(monkeypatch, session=Session())
result = await StudentMemoryService().record_feedback("p1", "style_feedback", 1, "more concise", ["n1"], ["e1"])
assert result == {"feedback": False, "frequency_weights": False, "skipped": True}
assert calls == []
@pytest.mark.asyncio
async def test_native_feedback_uses_cognee_recall_metadata(monkeypatch):
calls = []
class Session:
async def add_feedback(self, **kwargs):
calls.append(("feedback", kwargs))
return True
async def add_frequency_weights(self, **kwargs):
calls.append(("weights", kwargs))
return True
install_fake_cognee(monkeypatch, session=Session())
result = await StudentMemoryService().record_feedback(
"p1",
"qa1",
1,
"more concise",
["cg-node-1"],
["cg-edge-1"],
cognee_native=True,
)
assert result == {"feedback": True, "frequency_weights": True}
assert calls[0][1]["feedback_text"] == "more concise"
assert calls[1][1]["node_ids"] == ["cg-node-1"]
@pytest.mark.asyncio
async def test_flush_project_can_distill_then_improve(monkeypatch):
calls = []
class Datasets:
async def list_datasets(self):
return [SimpleNamespace(name="project_p1"), SimpleNamespace(name="research_profile")]
class Session:
async def distill_session(self, **kwargs):
calls.append(("distill", kwargs["dataset"]))
async def improve(**kwargs):
calls.append(("improve", kwargs["dataset"]))
install_fake_cognee(monkeypatch, datasets=Datasets(), session=Session(), improve=improve)
result = await StudentMemoryService().flush_project("p1", strategy="distill_then_improve")
assert result == {"project_p1": True, "research_profile": True}
assert calls == [
("distill", "project_p1"),
("improve", "project_p1"),
("distill", "research_profile"),
("improve", "research_profile"),
]
@pytest.mark.asyncio
async def test_flush_profile_only_improves_research_profile(monkeypatch):
calls = []
class Datasets:
async def list_datasets(self):
return [SimpleNamespace(name="project_p1"), SimpleNamespace(name="research_profile")]
async def improve(**kwargs):
calls.append(("improve", kwargs["dataset"]))
install_fake_cognee(monkeypatch, datasets=Datasets(), improve=improve)
result = await StudentMemoryService().flush_profile("p1")
assert result is True
assert calls == [("improve", "research_profile")]
@pytest.mark.asyncio
async def test_native_wrappers_tolerate_missing_cognee_apis(monkeypatch):
install_fake_cognee(monkeypatch)
service = StudentMemoryService()
assert (await service.run_project_memify("p1"))["ok"] is False
assert (await service.get_schema_inventory("p1"))["ok"] is False
assert (await service.get_provenance("p1"))["ok"] is False
assert (await service.export_memory("p1"))["ok"] is True
@pytest.mark.asyncio
async def test_research_memory_reviewer_skill_bootstrap_uses_cognee_skill_content(monkeypatch):
calls = []
class Datasets:
async def list_datasets(self):
return [SimpleNamespace(name="project_p1")]
async def remember(text, **kwargs):
calls.append((text, kwargs))
return {"remembered": True}
install_fake_cognee(monkeypatch, datasets=Datasets(), remember=remember)
result = await StudentMemoryService().bootstrap_project_skill("p1")
assert result["ok"] is True
assert calls[0][1]["dataset_name"] == "project_p1"
assert calls[0][1]["content_type"] == "skills"
assert calls[0][1]["skill_name"] == "research-memory-reviewer"
assert "research-memory-reviewer" in calls[0][0]
@pytest.mark.asyncio
async def test_forget_project_document_resets_project_memory_without_document_id(monkeypatch):
calls = []
async def forget(**kwargs):
calls.append(("forget", kwargs))
return {"ok": True}
class Datasets:
async def list_datasets(self):
return [SimpleNamespace(name="project_p1")]
async def add(text, dataset_name):
calls.append(("add", dataset_name, text))
install_fake_cognee(monkeypatch, datasets=Datasets(), forget=forget, add=add)
result = await StudentMemoryService().forget_project_document("p1", "a" * 64)
assert result["ok"] is True
assert calls[0] == ("forget", {"dataset": "project_p1", "memory_only": True})
assert all("document_id" not in call[1] for call in calls if call[0] == "forget")
@pytest.mark.asyncio
async def test_memory_liveness_reports_degraded_when_recall_fails(monkeypatch):
class Datasets:
async def list_datasets(self):
return [SimpleNamespace(name="project_p1"), SimpleNamespace(name="research_profile")]
async def add(text, dataset_name):
return None
async def remember(text, **kwargs):
return None
async def improve(**kwargs):
return None
async def recall(**kwargs):
raise RuntimeError("recall broken")
install_fake_cognee(
monkeypatch,
datasets=Datasets(),
add=add,
remember=remember,
improve=improve,
recall=recall,
SearchType=fake_search_type(),
)
status = await StudentMemoryService().memory_liveness("p1", force=True)
assert status["state"] == "degraded"
assert status["checks"]["recall"] is False
assert "recall broken" in status["last_error"]
@pytest.mark.asyncio
async def test_memory_status_skips_liveness_probe_by_default(monkeypatch):
calls = []
class Datasets:
async def list_datasets(self):
calls.append("list_datasets")
return [SimpleNamespace(name="project_p1"), SimpleNamespace(name="research_profile")]
async def get_schema_inventory(**kwargs):
calls.append(("inventory", kwargs["dataset"]))
return [{"name": "Claim"}]
async def get_memory_provenance_graph(**kwargs):
calls.append("provenance")
return [], []
async def export(**kwargs):
calls.append(("export", kwargs["dataset"]))
return []
async def remember(*args, **kwargs):
raise AssertionError("memory_status should not run liveness writes by default")
async def improve(**kwargs):
raise AssertionError("memory_status should not flush Cognee by default")
async def recall(**kwargs):
raise AssertionError("memory_status should not recall by default")
install_fake_cognee(
monkeypatch,
datasets=Datasets(),
get_schema_inventory=get_schema_inventory,
get_memory_provenance_graph=get_memory_provenance_graph,
export=export,
remember=remember,
improve=improve,
recall=recall,
SearchType=fake_search_type(),
)
status = await StudentMemoryService().memory_status("p1")
assert status["state"] == "ready"
assert status["liveness"] == {}
assert "list_datasets" in calls
assert ("inventory", "project_p1") in calls
@pytest.mark.asyncio
async def test_temporal_recall_falls_back_to_local_ledger(monkeypatch, tmp_path):
monkeypatch.setattr("app.services.student_memory.MEMORY_ROOT", tmp_path)
service = StudentMemoryService()
service.record_temporal_event("p1", "commit", "Student connected Adam to sparse gradients")
class Datasets:
async def list_datasets(self):
return [SimpleNamespace(name="project_p1")]
async def recall(**kwargs):
raise RuntimeError("No temporal graph")
install_fake_cognee(monkeypatch, datasets=Datasets(), recall=recall, SearchType=fake_search_type())
result = await service.query_prior_knowledge("Adam", project_id="p1", mode="temporal")
assert "Temporal project memory" in result
assert "sparse gradients" in result
def test_study_buddy_agent_no_longer_calls_missing_memory_remember():
from app.agents.study_buddy_agent import StudyBuddyAgent
source = inspect.getsource(StudyBuddyAgent.evaluate_and_ask_next)
assert ".remember(" not in source
assert "stage_project_observation" in source