from copy import deepcopy from unittest.mock import AsyncMock import pytest from backend.prospecting import execution from backend.prospecting.contracts import ProspectingBrief @pytest.fixture def task(monkeypatch): monkeypatch.setattr(execution, "get_llm_config", lambda: {"provider": "fixture", "model": "fixture-model"}) return {"id": "incremental-fixture", "inputs": {"_goal_version": 1}, "artifacts": {"sources": [ {"source_id": "note", "source_type": "note", "content": "正在准备装修", "source_url": "https://example.test/n"}, {"source_id": "reply", "source_type": "reply", "content": "我也想找设计师", "source_url": "https://example.test/n", "parent_source_id": "note"}, ]}} def analyzer(): async def analyze(brief, sources, *, context_sources=None): return {"decisions": [{"source_id": row["source_id"], "source_type": row["source_type"], "decision": "keep", "quote": row["content"], "reason": "本人表达需求"} for row in sources], "analysis_model": "fixture-model"} return AsyncMock(side_effect=analyze) async def test_identical_input_reuses_decisions_and_force_is_explicit(task): brief = ProspectingBrief(target_candidates=2, target_customer="准备装修的人") analyze = analyzer() first = await execution.analyze_evidence(task, brief, analyze) task["artifacts"].update(first) second = await execution.analyze_evidence(task, brief, analyze) assert second["analysis_cached"] and second["analysis_new_count"] == 0 assert len(second["leads"]) == 2 analyze.assert_awaited_once() await execution.analyze_evidence(task, brief, analyze, force=True) assert analyze.await_count == 2 async def test_only_new_sources_are_analyzed_with_full_parent_context(task): brief = ProspectingBrief(target_candidates=3, target_customer="准备装修的人") analyze = analyzer() task["artifacts"].update(await execution.analyze_evidence(task, brief, analyze)) task["artifacts"]["sources"].append({"source_id": "new", "source_type": "reply", "source_url": "https://example.test/n", "parent_source_id": "reply", "content": "可以先沟通方案吗"}) result = await execution.analyze_evidence(task, brief, analyze) assert [row["source_id"] for row in analyze.await_args.args[1]] == ["new"] assert len(analyze.await_args.kwargs["context_sources"]) == 3 assert result["analysis_reused_count"] == 2 and len(result["leads"]) == 3 async def test_changed_parent_or_user_correction_invalidates_related_judgment(task): brief = ProspectingBrief(target_candidates=3, target_customer="准备装修的人") analyze = analyzer() task["artifacts"].update(await execution.analyze_evidence(task, brief, analyze)) task["artifacts"]["sources"][0]["content"] = "装修已完成,不需要服务" result = await execution.analyze_evidence(task, brief, analyze) assert result["analysis_new_count"] == 2 task["artifacts"].update(result) task["artifacts"]["corrections"] = [{"source_id": "reply", "text": "这是同行自荐"}] await execution.analyze_evidence(task, brief, analyze) assert analyze.await_args.args[1][0]["user_correction"] == "这是同行自荐" async def test_changing_only_people_target_keeps_evidence_judgments(task): brief = ProspectingBrief(target_candidates=3, target_customer="准备装修的人") analyze = analyzer() task["artifacts"].update(await execution.analyze_evidence(task, brief, analyze)) changed = brief.model_copy(update={"target_candidates": 8}) assert execution.analysis_is_current(task, changed) assert (await execution.analyze_evidence(task, changed, analyze))["analysis_cached"] assert not execution.analysis_is_current(task, changed.model_copy(update={"region": "杭州"})) @pytest.mark.parametrize("explicit_note_id", [True, False]) async def test_signed_note_url_changes_preserve_context_and_invalidate_discussion(task, explicit_note_id): from backend.prospecting.analysis import _related_context note, reply = task["artifacts"]["sources"] note["source_url"] = "https://www.xiaohongshu.com/explore/abc123?xsec_token=old-fixture" reply["source_url"] = "https://www.xiaohongshu.com/discovery/item/abc123?xsec_token=new-fixture" reply.pop("parent_source_id") if explicit_note_id: note["note_id"] = reply["note_id"] = "abc123" assert _related_context([reply], [note, reply]) == [note] brief = ProspectingBrief(target_candidates=2, target_customer="准备装修的人") analyze = analyzer() task["artifacts"].update(await execution.analyze_evidence(task, brief, analyze)) note["content"] = "已装修完了,先前是找客户的广告" result = await execution.analyze_evidence(task, brief, analyze) assert result["analysis_new_count"] == 2