import pytest from app.schemas.commit_adaptation import CommitAdaptationReview, InteractionQuote, MemoryProposal from app.services import ( approved_profile_staging, commit_adaptation, pending_review_store, student_interaction_log, ) class _FakeReviewer: def __init__(self, review: CommitAdaptationReview): self._review = review self.calls = 0 async def review_commit_with_llm(self, **kwargs): self.calls += 1 return self._review class _FakeProjectMemory: def __init__(self): self.written_records = [] def upsert(self, records): self.written_records.extend(records) return len(records) class _FakeStudentMemory: def __init__(self, flush_ok: bool = True, stage_ok: bool = True): self.flush_ok = flush_ok self.stage_ok = stage_ok self.staged_candidates = [] async def query_prior_knowledge(self, *args, **kwargs): return "No prior profile." async def stage_promoted_candidate(self, candidate): self.staged_candidates.append(candidate) return self.stage_ok async def flush_project(self, project_id): return {"research_profile": self.flush_ok} class _FakeAgentControlPersona: def __init__(self): self.persona_calls = [] def get_inferred_text(self, project_id): return "" def get_manual_text(self, project_id): return "" def set_inferred_text(self, project_id, text): self.persona_calls.append((project_id, text)) @pytest.fixture(autouse=True) def _isolate_paths(tmp_path, monkeypatch): monkeypatch.setattr(student_interaction_log, "_ROOT", tmp_path / "student_interactions") monkeypatch.setattr(approved_profile_staging, "_ROOT", tmp_path / "approved_profile_staging") monkeypatch.setattr(pending_review_store, "_ROOT", tmp_path / "pending_reviews") monkeypatch.setattr(pending_review_store, "_UNCERTAIN_ROOT", tmp_path / "uncertain_flushes") @pytest.mark.asyncio async def test_run_commit_adaptation_is_noop_with_no_pending_interactions(monkeypatch): reviewer = _FakeReviewer(CommitAdaptationReview(memory_proposals=[], inferred_persona_text="")) monkeypatch.setattr(commit_adaptation, "agent_control", reviewer) await commit_adaptation.run_commit_adaptation(project_id="project-a") assert reviewer.calls == 0 @pytest.mark.asyncio async def test_run_commit_adaptation_persists_project_candidate_to_chroma(monkeypatch): interaction = student_interaction_log.append_student_interaction( project_id="project-a", source="chat_turn", agent_id="tutor", text="I use PyTorch for everything.", ) review = CommitAdaptationReview( memory_proposals=[ MemoryProposal( proposal_id="p1", destination="project", kind="project_observation", statement="Uses PyTorch for all experiments.", interaction_ids=[interaction.interaction_id], evidence_quotes=[ InteractionQuote(interaction_id=interaction.interaction_id, quote="I use PyTorch for everything.") ], confidence=0.8, ) ], inferred_persona_text="Assume PyTorch familiarity.", ) reviewer = _FakeReviewer(review) monkeypatch.setattr(commit_adaptation, "agent_control", reviewer) fake_project_memory = _FakeProjectMemory() monkeypatch.setattr(commit_adaptation, "project_memory", fake_project_memory) fake_student_memory = _FakeStudentMemory() monkeypatch.setattr(commit_adaptation, "student_memory", fake_student_memory) fake_persona = _FakeAgentControlPersona() monkeypatch.setattr(commit_adaptation, "agent_control_persona", fake_persona) await commit_adaptation.run_commit_adaptation(project_id="project-a") assert len(fake_project_memory.written_records) == 1 assert fake_project_memory.written_records[0].statement == "Uses PyTorch for all experiments." assert fake_persona.persona_calls == [("project-a", "Assume PyTorch familiarity.")] assert pending_review_store.load_pending_review("project-a") is None @pytest.mark.asyncio async def test_cognee_failure_marks_uncertain_and_does_not_update_persona(monkeypatch): interaction = student_interaction_log.append_student_interaction( project_id="project-a", source="chat_turn", agent_id="tutor", text="I generally prefer diagrams.", ) review = CommitAdaptationReview( memory_proposals=[ MemoryProposal( proposal_id="p1", destination="student", kind="learning_style", statement="Generally prefers diagrams.", interaction_ids=[interaction.interaction_id], evidence_quotes=[ InteractionQuote(interaction_id=interaction.interaction_id, quote="I generally prefer diagrams.") ], confidence=0.9, ) ], inferred_persona_text="Lean on diagrams.", ) reviewer = _FakeReviewer(review) monkeypatch.setattr(commit_adaptation, "agent_control", reviewer) monkeypatch.setattr(commit_adaptation, "project_memory", _FakeProjectMemory()) fake_student_memory = _FakeStudentMemory(flush_ok=False) monkeypatch.setattr(commit_adaptation, "student_memory", fake_student_memory) fake_persona = _FakeAgentControlPersona() monkeypatch.setattr(commit_adaptation, "agent_control_persona", fake_persona) await commit_adaptation.run_commit_adaptation(project_id="project-a") # "I generally prefer diagrams." matches the explicit-global signal # patterns, so the gate promotes it immediately (bypassing recurrence) -- # this is what actually reaches the flush-attempt code path. assert fake_student_memory.staged_candidates, "candidate was never promoted -- test doesn't exercise a real flush failure" assert fake_persona.persona_calls == [] pending = pending_review_store.load_pending_review("project-a") assert pending is not None assert pending.status == "cognee_flush_uncertain" @pytest.mark.asyncio async def test_stale_flush_started_batch_is_archived_and_never_replayed(monkeypatch): student_interaction_log.append_student_interaction( project_id="project-a", source="chat_turn", agent_id="tutor", text="Some message.", ) interactions, i_from, i_through = student_interaction_log.read_pending("project-a") pending_review_store.save_pending_review( "project-a", status="cognee_flush_started", candidate_ids=["c1"], interaction_from_offset=i_from, interaction_through_offset=i_through, profile_staging_from_offset=0, profile_staging_through_offset=0, review=CommitAdaptationReview(memory_proposals=[], inferred_persona_text="stale"), flush_attempt_id="crashed-run", ) reviewer = _FakeReviewer(CommitAdaptationReview(memory_proposals=[], inferred_persona_text="")) monkeypatch.setattr(commit_adaptation, "agent_control", reviewer) await commit_adaptation.run_commit_adaptation(project_id="project-a") assert reviewer.calls == 0 # archived-and-recovered, must not fall through into a fresh review this same call assert pending_review_store.load_pending_review("project-a") is None interactions_after, _, _ = student_interaction_log.read_pending("project-a") assert interactions_after == [] # cursor advanced past the archived batch @pytest.mark.asyncio async def test_ready_to_flush_retry_reuses_exact_stored_ranges_not_expanded_ones(monkeypatch): interaction = student_interaction_log.append_student_interaction( project_id="project-a", source="chat_turn", agent_id="tutor", text="I use PyTorch for everything.", ) _, i_from, i_through = student_interaction_log.read_pending("project-a") stored_review = CommitAdaptationReview( memory_proposals=[ MemoryProposal( proposal_id="p1", destination="project", kind="project_observation", statement="Uses PyTorch for all experiments.", interaction_ids=[interaction.interaction_id], evidence_quotes=[ InteractionQuote(interaction_id=interaction.interaction_id, quote="I use PyTorch for everything.") ], confidence=0.8, ) ], inferred_persona_text="Assume PyTorch familiarity.", ) from app.services.commit_candidate_adapters import proposal_to_memory_candidate candidate = proposal_to_memory_candidate( stored_review.memory_proposals[0], project_id="project-a", interactions_by_id={interaction.interaction_id: interaction}, ) pending_review_store.save_pending_review( "project-a", status="ready_to_flush", candidate_ids=[candidate.candidate_id], interaction_from_offset=i_from, interaction_through_offset=i_through, profile_staging_from_offset=0, profile_staging_through_offset=0, review=stored_review, ) # A new interaction arrives after the pending review was captured -- # it must NOT be swept into this retry's batch. student_interaction_log.append_student_interaction( project_id="project-a", source="chat_turn", agent_id="tutor", text="Unrelated later message.", ) reviewer = _FakeReviewer(CommitAdaptationReview(memory_proposals=[], inferred_persona_text="should not be called")) monkeypatch.setattr(commit_adaptation, "agent_control", reviewer) fake_project_memory = _FakeProjectMemory() monkeypatch.setattr(commit_adaptation, "project_memory", fake_project_memory) fake_student_memory = _FakeStudentMemory() monkeypatch.setattr(commit_adaptation, "student_memory", fake_student_memory) fake_persona = _FakeAgentControlPersona() monkeypatch.setattr(commit_adaptation, "agent_control_persona", fake_persona) await commit_adaptation.run_commit_adaptation(project_id="project-a") assert reviewer.calls == 0, "retry must not call the LLM again" assert len(fake_project_memory.written_records) == 1 assert fake_project_memory.written_records[0].statement == "Uses PyTorch for all experiments." assert fake_persona.persona_calls == [("project-a", "Assume PyTorch familiarity.")] # The cursor only advanced to the ORIGINAL through_offset -- the later, # unrelated message remains pending for the next Commit. remaining, _, _ = student_interaction_log.read_pending("project-a") assert [i.text for i in remaining] == ["Unrelated later message."]