""" 🧠 Diary → graph console-logging regression tests. After #282 added duplicate-skip on the cumulative-summary re-flush path, the `🧠 Knowledge graph: learned N new facts` line in ``update_diary_from_dialogue_memory`` went silent on every flush past the first: every re-extraction routed to a node that already contained the fact, ``stored`` came back empty, and the print was gated on a non-empty list. From the user's perspective the memory pipeline looked dead. These tests lock in all four CLI states (mixed, only-new, all-duplicate, silent-empty) plus singular pluralisation, so the regression can't slip back in unnoticed. """ from unittest.mock import patch import pytest from jarvis.memory.graph_ops import GraphUpdateResult @pytest.mark.unit class TestKnowledgeGraphConsoleLogging: """Behavioural tests for the 🧠 console line emitted after a diary flush.""" def _run_flush(self, db, dialogue_memory, graph_result): """Drive ``update_diary_from_dialogue_memory`` with a stubbed summariser and graph updater, returning whatever it printed. ``graph_result`` is the ``GraphUpdateResult`` the patched ``update_graph_from_dialogue`` should return. """ from jarvis.memory.conversation import update_diary_from_dialogue_memory dialogue_memory.add_message("user", "I learned that bats are not blind") dialogue_memory.add_message("assistant", "Correct, they use echolocation in addition to sight.") with patch( "jarvis.memory.conversation.generate_conversation_summary", return_value=("User asked about bats. Bats are not blind.", "bats, biology"), ), patch( "jarvis.memory.graph_ops.update_graph_from_dialogue", return_value=graph_result, ): return update_diary_from_dialogue_memory( db=db, dialogue_memory=dialogue_memory, ollama_base_url="http://localhost:11434", ollama_chat_model="test", ollama_embed_model="test", force=True, timeout_sec=5.0, ) def test_logs_count_when_new_facts_stored(self, db, dialogue_memory, capsys): """Mixed flush: 2 new + 1 duplicate prints the count and per-fact preview.""" result = GraphUpdateResult( stored=[ ("Bats use echolocation.", "world"), ("User is curious about bats.", "user"), ], skipped=1, ) summary_id = self._run_flush(db, dialogue_memory, result) assert summary_id is not None out = capsys.readouterr().out assert "🧠 Knowledge graph: learned 2 new facts" in out assert "(1 duplicate skipped)" in out assert "Bats use echolocation. → world" in out assert "User is curious about bats. → user" in out def test_logs_singular_when_one_new_fact(self, db, dialogue_memory, capsys): """Pluralisation: a single new fact uses singular wording.""" result = GraphUpdateResult( stored=[("Bats use echolocation.", "world")], skipped=0, ) self._run_flush(db, dialogue_memory, result) out = capsys.readouterr().out assert "🧠 Knowledge graph: learned 1 new fact" in out # No trailing 's' on 'fact' and no "duplicates skipped" tail. assert "1 new facts" not in out assert "duplicate" not in out def test_logs_duplicates_when_only_skipped(self, db, dialogue_memory, capsys): """All-duplicate flush still prints a status line. This is the regression #282 introduced: extraction ran, the LLM produced facts, but every one was a duplicate so ``stored`` was empty and the previous gate suppressed the print entirely. The user lost their only signal that the memory pipeline was alive. """ result = GraphUpdateResult(stored=[], skipped=3) self._run_flush(db, dialogue_memory, result) out = capsys.readouterr().out assert "🧠 Knowledge graph: nothing new" in out assert "(3 duplicates skipped)" in out def test_logs_singular_duplicate(self, db, dialogue_memory, capsys): """Pluralisation: a single duplicate uses singular wording.""" result = GraphUpdateResult(stored=[], skipped=1) self._run_flush(db, dialogue_memory, result) out = capsys.readouterr().out assert "(1 duplicate skipped)" in out assert "1 duplicates" not in out def test_silent_when_extractor_returned_nothing(self, db, dialogue_memory, capsys): """Empty extraction (no facts and no duplicates) stays quiet. Distinct from the all-duplicate case: there's genuinely nothing to report, so we don't add console noise on every diary flush that didn't yield knowledge. """ result = GraphUpdateResult(stored=[], skipped=0) self._run_flush(db, dialogue_memory, result) out = capsys.readouterr().out assert "🧠 Knowledge graph" not in out