Spaces:
Sleeping
Sleeping
| 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") | |
| 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"] | |
| 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") == [] | |
| 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"] | |
| 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 == [] | |
| 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] | |
| 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" | |
| 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"] | |
| 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"] | |
| 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 == [] | |
| 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"] | |
| 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"), | |
| ] | |
| 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")] | |
| 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 | |
| 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] | |
| 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") | |
| 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"] | |
| 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 | |
| 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 | |