"""Detail-path guards: skip intents never pollute drafts; LLM gate routes before fact-merge.""" from __future__ import annotations from types import SimpleNamespace from typing import Any from app.builder_conversation.candidate import _candidate_rewrite from app.builder_conversation.predicates import _is_no_information_reply from app.builder_conversation.rescue import llm_detail_route from app.chat_intents import ChatTurnClassification from builder_flow_helpers import create_builder_session, send_message, start_education from test_api import active_component class _StubClassifier: def __init__(self, result: ChatTurnClassification) -> None: self.result = result def classify(self, message: str, *, state_summary: dict[str, Any]) -> ChatTurnClassification: return self.result def _agent(classifier: Any) -> Any: return SimpleNamespace( expander=SimpleNamespace(expand=lambda entry, *, context: {}), _chat_intent_classifier=classifier, ) def _profile_with_draft() -> dict[str, Any]: return { "job_type": "campus", "builder": { "active_section": "education", "identity_draft": {"school": "X 大学", "description": "完成数据库课程项目。"}, "gap_state": {"asked": ["academic_result"], "skipped": [], "rounds": 1}, }, } def test_skip_words_count_as_no_information() -> None: for word in ("跳过", "先跳过", "跳过吧", "不用了", "不需要", "以后再说", "没有了"): assert _is_no_information_reply(word), word def test_skip_reply_does_not_pollute_description(client: Any) -> None: session_id, body = create_builder_session(client) start_education(client, session_id, body) send_message(client, session_id, "完成数据库课程项目。") # triggers the gap prompt reply = send_message(client, session_id, "跳过") card = active_component(reply)["data"] assert card["component"] == "experience_confirm_card" assert "跳过" not in str(card["value"].get("description") or "") def test_detail_gate_no_info_skips_gap_without_merging() -> None: agent = _agent(_StubClassifier(ChatTurnClassification(intent="no_info", confidence=0.9))) transition = llm_detail_route(agent, _profile_with_draft(), "先跳过这个") assert transition is not None state = transition.profile["builder"] assert "先跳过这个" not in state["identity_draft"]["description"] assert "academic_result" in state["gap_state"]["skipped"] def test_detail_gate_revise_regenerates_candidate() -> None: agent = _agent(_StubClassifier(ChatTurnClassification( intent="revise_proposal", confidence=0.9, revision_instruction="再简洁一点", ))) transition = llm_detail_route(agent, _profile_with_draft(), "帮我再精简下") assert transition is not None assert transition.profile["builder"]["pending_entry"]["_proposal"] is not None assert "帮我再精简下" not in transition.profile["builder"]["identity_draft"]["description"] def test_detail_gate_chitchat_does_not_merge() -> None: agent = _agent(_StubClassifier(ChatTurnClassification(intent="chitchat", confidence=0.9))) transition = llm_detail_route(agent, _profile_with_draft(), "好的谢谢") assert transition is not None assert transition.profile["builder"]["identity_draft"]["description"] == "完成数据库课程项目。" def test_detail_gate_passes_facts_and_low_confidence_through() -> None: facts = _agent(_StubClassifier(ChatTurnClassification(intent="provide_facts", confidence=0.9))) assert llm_detail_route(facts, _profile_with_draft(), "GPA 4.3") is None shaky = _agent(_StubClassifier(ChatTurnClassification(intent="no_info", confidence=0.4))) assert llm_detail_route(shaky, _profile_with_draft(), "跳过") is None def test_candidate_rewrite_ensure_facts_appends_missing() -> None: class _Expander: def expand(self, entry: dict[str, Any], *, context: dict[str, Any]) -> dict[str, Any]: return {"optimized_description": "主修课程:数据结构、计算机视觉。", "source": "test"} agent = SimpleNamespace(expander=_Expander()) proposal = _candidate_rewrite( agent, {"job_type": "campus"}, {"description": "学习数据结构、计算机视觉课程。GPA: 4.3/5.0,排名前百分之10。"}, "education", ensure_facts=True, ) assert "GPA: 4.3/5.0" in proposal["optimized_description"] assert "排名前百分之10" in proposal["optimized_description"] assert proposal["uncovered_facts"] == []