"""Objection intelligence — detection, typed counters, stage-aware persuasion.""" from app.services.objection import count_prior_objections, detect_objection from app.services.prompter import ( OBJECTION_HANDLING_INSTRUCTIONS, get_conversation_stage_directive, get_objection_instruction, get_reading_anchor, ) # ── Detection ───────────────────────────────────────────────────────────────── def test_price_objection_detected(): assert detect_objection("this seems too expensive for me") == "price" assert detect_objection("I can't afford another book right now") == "price" def test_time_objection_detected(): assert detect_objection("i don't have time to read these days") == "time" assert detect_objection("looks too long, I'd never finish it") == "time" def test_relevance_objection_detected(): assert detect_objection("i have no interest in politics") == "relevance" assert detect_objection("this is not my thing honestly") == "relevance" def test_skepticism_objection_detected(): assert detect_objection("is it actually good or just hype?") == "skepticism" assert detect_objection("what makes it different from other books") == "skepticism" def test_genre_objection_detected(): assert detect_objection("i only read romance novels") == "genre" def test_alternatives_objection_detected(): assert detect_objection("i already read something similar last year") == "alternatives" def test_generic_value_challenge_maps_to_relevance(): assert detect_objection("so why should i buy this") == "relevance" assert detect_objection("convince me") == "relevance" def test_normal_question_is_not_objection(): assert detect_objection("who is the main character?") is None assert detect_objection("what is this book about") is None def test_prior_objections_counted_from_history(): history = [ {"role": "user", "content": "what is this about"}, {"role": "assistant", "content": "It explores..."}, {"role": "user", "content": "why should i buy this, i have no interest in politics"}, {"role": "assistant", "content": "Fair question..."}, ] assert count_prior_objections(history) == 1 # ── Instructions ────────────────────────────────────────────────────────────── def test_each_objection_type_has_instruction(): for obj_type in ("price", "time", "relevance", "skepticism", "genre", "alternatives"): assert obj_type in OBJECTION_HANDLING_INSTRUCTIONS assert len(OBJECTION_HANDLING_INSTRUCTIONS[obj_type]) > 40 def test_objection_instruction_empty_when_no_objection(): assert get_objection_instruction(None) == "" def test_repeat_objection_softens_the_counter(): first = get_objection_instruction("price", prior_objections=0) repeat = get_objection_instruction("price", prior_objections=1) assert len(repeat) > len(first) assert "do not repeat" in repeat.lower() # ── Personal anchor ─────────────────────────────────────────────────────────── def test_reading_anchor_uses_most_recent_tag(): anchor = get_reading_anchor(["genre", "characters"]) assert "characters" in anchor.lower() or "who they will meet" in anchor.lower() def test_reading_anchor_empty_without_tags(): assert get_reading_anchor([]) == "" # ── Conversation stage ──────────────────────────────────────────────────────── def test_stage_discovery_on_early_turns(): directive = get_conversation_stage_directive(0, 0.0) assert "DISCOVERY" in directive def test_stage_decision_when_hot(): directive = get_conversation_stage_directive(6, 0.9) assert "DECISION" in directive def test_stage_consideration_mid_journey(): directive = get_conversation_stage_directive(4, 0.6) assert "CONSIDERATION" in directive