from app.schemas.commit_adaptation import InteractionQuote, MemoryProposal from app.services.commit_candidate_adapters import ( StudentInteraction, candidate_to_project_memory_record, proposal_to_memory_candidate, ) from app.services.memory_candidates import MemoryCandidate, make_candidate_id from app.rag.project_memory import ProjectMemoryRecord def _interaction(text: str, actor: str = "student") -> StudentInteraction: return StudentInteraction( interaction_id="interaction-1", timestamp=1_700_000_000.0, actor=actor, source="chat_turn", agent_id="tutor", text=text, ) def test_adapter_builds_real_strict_memory_candidate(): interactions = {"interaction-1": _interaction("Do not give me the complete code.")} proposal = MemoryProposal( proposal_id="p1", destination="student", kind="workflow_preference", statement="Prefers to implement code incrementally rather than receiving complete solutions.", interaction_ids=["interaction-1"], evidence_quotes=[InteractionQuote(interaction_id="interaction-1", quote="Do not give me the complete code.")], confidence=0.9, ) candidate = proposal_to_memory_candidate(proposal, project_id="project-a", interactions_by_id=interactions) assert isinstance(candidate, MemoryCandidate) assert candidate.candidate_id assert candidate.interaction_ids == ["interaction-1"] assert candidate.evidence_ids == [] def test_explicit_but_not_global_wording_downgrades_to_project_scope(): interactions = {"interaction-1": _interaction("Do not give me the complete code.")} proposal = MemoryProposal( proposal_id="p1", destination="student", # LLM proposed global — not corroborated by wording kind="workflow_preference", statement="Prefers incremental code guidance.", interaction_ids=["interaction-1"], evidence_quotes=[InteractionQuote(interaction_id="interaction-1", quote="Do not give me the complete code.")], confidence=0.9, ) candidate = proposal_to_memory_candidate(proposal, project_id="project-a", interactions_by_id=interactions) assert candidate.destination == "project" # attribution reflects whether the STATEMENT was explicit, independent of # final destination — an explicit-but-project-scoped note is still more # reliably attributed than a merely-inferred one. assert candidate.attribution == "explicit_student" def test_explicit_global_wording_keeps_student_destination(): interactions = {"interaction-1": _interaction("I generally prefer diagrams over equations.")} proposal = MemoryProposal( proposal_id="p1", destination="student", kind="learning_style", statement="Generally prefers diagrams over equations.", interaction_ids=["interaction-1"], evidence_quotes=[InteractionQuote(interaction_id="interaction-1", quote="I generally prefer diagrams over equations.")], confidence=0.9, ) candidate = proposal_to_memory_candidate(proposal, project_id="project-a", interactions_by_id=interactions) assert candidate.destination == "student" assert candidate.attribution == "explicit_student" def test_quote_from_assistant_actor_is_not_verified(): interactions = {"interaction-1": _interaction("I generally prefer diagrams over equations.", actor="assistant")} proposal = MemoryProposal( proposal_id="p1", destination="student", kind="learning_style", statement="Generally prefers diagrams over equations.", interaction_ids=["interaction-1"], evidence_quotes=[InteractionQuote(interaction_id="interaction-1", quote="I generally prefer diagrams over equations.")], confidence=0.9, ) candidate = proposal_to_memory_candidate(proposal, project_id="project-a", interactions_by_id=interactions) # No verified quote -> not explicit -> not global -> falls back to inferred profile proposal assert candidate.attribution == "idea_observer_profile_proposal" assert candidate.interaction_ids == [] def test_quote_not_found_in_interaction_text_is_not_verified(): interactions = {"interaction-1": _interaction("I like short answers.")} proposal = MemoryProposal( proposal_id="p1", destination="student", kind="learning_style", statement="Fabricated claim.", interaction_ids=["interaction-1"], evidence_quotes=[InteractionQuote(interaction_id="interaction-1", quote="I generally prefer diagrams")], confidence=0.9, ) candidate = proposal_to_memory_candidate(proposal, project_id="project-a", interactions_by_id=interactions) assert candidate.attribution == "idea_observer_profile_proposal" assert candidate.interaction_ids == [] def test_quote_normalization_tolerates_smart_quotes_case_and_whitespace(): interactions = {"interaction-1": _interaction("I GENERALLY prefer’s diagrams.")} proposal = MemoryProposal( proposal_id="p1", destination="student", kind="learning_style", statement="Generally prefers diagrams.", interaction_ids=["interaction-1"], evidence_quotes=[InteractionQuote(interaction_id="interaction-1", quote="i generally prefer's diagrams.")], confidence=0.9, ) candidate = proposal_to_memory_candidate(proposal, project_id="project-a", interactions_by_id=interactions) assert candidate.interaction_ids == ["interaction-1"] def test_candidate_to_project_memory_record_builds_real_record(): candidate = MemoryCandidate( candidate_id=make_candidate_id("project", "project-a", "project_observation", "Uses PyTorch for all experiments."), destination="project", project_id="project-a", kind="project_observation", statement="Uses PyTorch for all experiments.", attribution="idea_observer_interaction", confidence=0.8, interaction_ids=["interaction-1"], evidence_ids=[], ) record = candidate_to_project_memory_record(candidate) assert isinstance(record, ProjectMemoryRecord) assert record.novelty_key == candidate.recurrence_key assert record.memory_id.startswith("pm_") assert record.attribution == "idea_observer_interaction" def test_candidate_to_project_memory_record_rejects_student_destination(): import pytest candidate = MemoryCandidate( candidate_id=make_candidate_id("student", "project-a", "preferred_name", "The student prefers to be called Anshuman."), destination="student", project_id="project-a", kind="preferred_name", statement="The student prefers to be called Anshuman.", attribution="explicit_student", confidence=0.99, interaction_ids=["interaction-1"], evidence_ids=[], ) with pytest.raises(ValueError, match="destination='project'"): candidate_to_project_memory_record(candidate)