File size: 14,979 Bytes
14184e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
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