"""Follow-ups must inherit context, and must never inherit it silently. The cases here are the ones the owner named on 2026-08-05: "còn trẻ em thì sao?", "giải thích kỹ hơn", and not making the user repeat themselves. The adversarial cases are the ones that make inheritance dangerous in a formulary — a stale drug, and an explicit mention being overridden by context. """ from __future__ import annotations from rag.conversation import ( FOCUS_TTL_TURNS, ConversationState, Focus, Turn, detect_population, detect_verbosity, looks_like_followup, resolve_against, update_focus, ) def _state(turn_count: int = 1, **focus_fields) -> ConversationState: focus = Focus() for name, value in focus_fields.items(): focus = focus.with_field(name, value, turn_count - 1) return ConversationState("c1", focus=focus, turn_count=turn_count) # --- the follow-ups the owner asked for -------------------------------------- def test_con_tre_em_thi_sao_inherits_drug_and_section(): state = _state(drug_id="metformin", section_key="lieu_luong_va_cach_dung") resolved = resolve_against(state, "còn trẻ em thì sao?", None, None) assert resolved.drug_id == "metformin" assert resolved.section_key == "lieu_luong_va_cach_dung" assert resolved.population == "tre_em" assert resolved.inherited_drug is True def test_giai_thich_ky_hon_sets_verbosity_and_keeps_the_topic(): state = _state(drug_id="warfarin", section_key="tuong_tac_thuoc") resolved = resolve_against(state, "giải thích kỹ hơn", None, None) assert resolved.drug_id == "warfarin" assert resolved.verbosity == "detailed" def test_the_user_is_not_made_to_repeat_the_drug(): state = _state(drug_id="metformin") resolved = resolve_against(state, "chống chỉ định", None, "chong_chi_dinh") assert resolved.drug_id == "metformin" assert resolved.section_key == "chong_chi_dinh" # --- what makes inheritance safe --------------------------------------------- def test_an_explicit_drug_always_beats_context(): """Naming a drug must override whatever the conversation was about, or a deliberate topic change silently answers about the previous medicine.""" state = _state(drug_id="metformin", section_key="lieu_luong_va_cach_dung") resolved = resolve_against(state, "liều dùng warfarin", "warfarin", None) assert resolved.drug_id == "warfarin" assert resolved.inherited_drug is False def test_a_stale_drug_is_dropped_rather_than_inherited(): """Beyond the TTL the drug is not context, it is a hazard.""" state = _state(turn_count=FOCUS_TTL_TURNS + 3, drug_id="metformin") # `_state` stamps at turn_count - 1, so age is 1; age it past the TTL. aged = ConversationState( "c1", focus=Focus(drug_id="metformin", set_at_turn={"drug_id": 0}), turn_count=FOCUS_TTL_TURNS + 2, ) assert state.inherited("drug_id") == "metformin" assert aged.inherited("drug_id") is None resolved = resolve_against(aged, "còn trẻ em thì sao?", None, None) assert resolved.drug_id is None def test_an_inherited_drug_must_be_named_in_the_answer(): state = _state(drug_id="metformin") inherited = resolve_against(state, "còn trẻ em thì sao?", None, None) explicit = resolve_against(state, "liều warfarin", "warfarin", None) assert inherited.needs_carry_over_notice is True assert explicit.needs_carry_over_notice is False # --- phrase detection --------------------------------------------------------- def test_longest_population_phrase_wins(): """`phụ nữ cho con bú` must not be read as `phụ nữ`, and `trẻ sơ sinh` must not be read as `trẻ em` — the same rule `sections.py` relies on.""" assert detect_population("phụ nữ cho con bú") == "phu_nu_cho_con_bu" assert detect_population("trẻ sơ sinh dùng sao") == "tre_so_sinh" assert detect_population("bà bầu uống được không") == "phu_nu_co_thai" assert detect_population("liều cho người lớn") == "nguoi_lon" def test_no_population_named_is_none_not_a_guess(): assert detect_population("liều dùng paracetamol") is None assert detect_verbosity("liều dùng paracetamol") is None def test_followup_markers(): assert looks_like_followup("còn trẻ em thì sao?") is True assert looks_like_followup("so với metformin thì sao") is True assert looks_like_followup("liều dùng paracetamol") is False # --- window and focus update -------------------------------------------------- def test_recent_window_evicts_oldest(): state = ConversationState("c1") for index in range(8): state = state.append(Turn("user", f"q{index}", "2026-08-05"), window=6) assert len(state.recent) == 6 assert state.recent[0].text == "q2" assert state.turn_count == 8 def test_evicted_turns_reach_overflow_not_silently_dropped(): """Bug fixed 2026-08-06 (Codex review, F-06): `overflow()` used to check `len(self.recent) > window`, but `append()` already truncates `recent` to `window`, so that comparison could never be true — evicted turns never reached the summariser no matter how long a conversation ran. Exact repro from the review: 8 turns into a window of 6.""" state = ConversationState("c1") for index in range(8): state = state.append(Turn("user", f"q{index}", "2026-08-05"), window=6) overflow = state.overflow() assert [turn.text for turn in overflow] == ["q0", "q1"] def test_overflow_accumulates_across_the_two_appends_one_turn_makes(): """A live turn typically calls `append()` twice in a row (user, then assistant). Each can evict at most one turn; the second call's overflow must not overwrite, and so lose, the first's.""" state = ConversationState("c1") for index in range(6): state = state.append(Turn("user", f"q{index}", "2026-08-05"), window=6) assert state.overflow() == () # window exactly full, nothing evicted yet state = state.append(Turn("user", "q6", "2026-08-05"), window=6) state = state.append(Turn("assistant", "a6", "2026-08-05"), window=6) assert [turn.text for turn in state.overflow()] == ["q0", "q1"] def test_overflow_is_empty_again_after_the_caller_clears_it(): from dataclasses import replace state = ConversationState("c1") for index in range(8): state = state.append(Turn("user", f"q{index}", "2026-08-05"), window=6) assert state.overflow() != () state = replace(state, pending_overflow=()) assert state.overflow() == () def test_focus_update_stamps_the_current_turn(): state = _state(turn_count=3) resolved = resolve_against(state, "liều dùng metformin", "metformin", "lieu_luong_va_cach_dung") focus = update_focus(state, resolved) assert focus.drug_id == "metformin" assert focus.set_at_turn["drug_id"] == 3