study-buddy / tests /test_commit_adaptation.py
GitHub Actions
deploy d092bea3608b7a29952f16357fda39b7a29e399b
2e818da
Raw
History Blame Contribute Delete
10.6 kB
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."]