import pytest from test_support.external_qa import require_external_qa import json from pathlib import Path from yuqei_sdk.contract_intake import (ContractIntakeRequest, ContractIntakeOutput, QUESTION_COMPLETION_VERSION, complete_required_questions, validate_intake_output) def request(**changes): data={"text":"费用为100元,开始日期2026-09-10。", "template_snapshot":{"id":"service","content_sha256":"a"*64, "input_schema":{"version":"template-input.v1","fields":{"费用":{"data_type":"money"},"开始日期":{"data_type":"date"},"启用":{"data_type":"choice","options":["是","否"]}},"rules":[]}, "required_variables":["费用"],"optional_variables":["开始日期","启用"]},"existing_values":{}} data.update(changes); return ContractIntakeRequest.model_validate(data) def output(proposals): return ContractIntakeOutput.model_validate({"schema_version":"contract-intake.v1","proposals":proposals,"questions":[],"applicability":"compatible","warnings":[]}) def test_grounded_typed_proposal_passes(): result=output([{"field":"费用","value_json":"100","source_excerpt":"费用为100元","state":"extracted","explanation":"原文明示"}]) assert validate_intake_output(request(), result)==[] @pytest.mark.parametrize("value,error", [("NaN","合法 JSON"),("true","有限数值"),("101","来源摘录")]) def test_invalid_values_are_explicit(value,error): excerpt="不存在" if value=="101" else "费用为100元" result=output([{"field":"费用","value_json":value,"source_excerpt":excerpt,"state":"extracted","explanation":"测试"}]) assert error in " ".join(validate_intake_output(request(), result)) def test_existing_value_conflict_is_resolved_by_apply_flow_not_schema_rejected(): result=output([{"field":"费用","value_json":"100","source_excerpt":"费用为100元","state":"extracted","explanation":"测试"}]) assert validate_intake_output(request(existing_values={"费用":99}), result)==[] assert validate_intake_output(request(existing_values={"费用":""}), result)==[] def test_required_question_completion_treats_empty_existing_as_missing_without_inventing_fact(): intake=request(existing_values={"费用":""}) raw=ContractIntakeOutput.model_validate({**output([]).model_dump(),"questions":[]}) normalized, added, origins=complete_required_questions(intake,raw) assert added==["费用"] and normalized.proposals==raw.proposals assert normalized.questions[0].model_dump()=={"field":"费用","question":"请补充模板必填字段:费用。","reason":"missing"} assert origins==[{"field":"费用","reason":"missing","origin":"template_required"}] assert QUESTION_COMPLETION_VERSION=="contract-intake-question-completion.v1" assert validate_intake_output(intake,normalized)==[] def test_required_question_completion_skips_excluded_and_does_not_mask_bad_model_questions(): excluded=ContractIntakeOutput.model_validate({**output([]).model_dump(),"applicability":"excluded"}) assert complete_required_questions(request(),excluded)[1]==[] wrong=ContractIntakeOutput.model_validate({**output([{"field":"费用","value_json":"[100,200]","source_excerpt":"费用为100元", "state":"ambiguous","explanation":"有两个金额"}]).model_dump(), "questions":[{"field":"费用","question":"请确认","reason":"missing"}]}) normalized, added, _=complete_required_questions(request(),wrong) assert added==[] assert "歧义事实必须提供关联问题" in " ".join(validate_intake_output(request(),normalized)) def test_duplicate_model_questions_remain_invalid_after_completion(): raw=ContractIntakeOutput.model_validate({**output([]).model_dump(),"questions":[ {"field":"费用","question":"请补充费用","reason":"missing"}, {"field":"费用","question":"费用是多少","reason":"missing"}]}) normalized, _, _=complete_required_questions(request(),raw) assert "重复问题" in " ".join(validate_intake_output(request(),normalized)) def test_whitespace_text_rejected(): with pytest.raises(ValueError): request(text=" ") def test_ambiguous_money_candidates_are_each_typed_but_extracted_array_is_rejected(): proposal={"field":"费用","value_json":"[50000,60000]","source_excerpt":"费用为100元","state":"ambiguous","explanation":"两个候选"} ambiguous=ContractIntakeOutput.model_validate({**output([proposal]).model_dump(),"questions":[{"field":"费用","question":"请选择金额","reason":"ambiguous"}]}) assert validate_intake_output(request(), ambiguous)==[] extracted=output([{**proposal,"state":"extracted"}]) assert "有限数值" in " ".join(validate_intake_output(request(), extracted)) def test_ambiguous_candidates_still_reject_nonfinite_member(): proposal={"field":"费用","value_json":"[50000,1e999]","source_excerpt":"费用为100元","state":"ambiguous","explanation":"测试"} result=ContractIntakeOutput.model_validate({**output([proposal]).model_dump(),"questions":[{"field":"费用","question":"请选择金额","reason":"ambiguous"}]}) assert "必须有限" in " ".join(validate_intake_output(request(), result)) @pytest.mark.parametrize("value_json", ["[]", "[null]", "[\"\"]", "[true]", "[1e999]"]) def test_ambiguous_money_rejects_empty_or_invalid_candidates(value_json): proposal={"field":"费用","value_json":value_json,"source_excerpt":"费用为100元","state":"ambiguous","explanation":"测试"} result=ContractIntakeOutput.model_validate({**output([proposal]).model_dump(),"questions":[{"field":"费用","question":"请选择金额","reason":"ambiguous"}]}) assert validate_intake_output(request(), result) def test_postfix_regression_replays_run01_failure_without_mutating_artifact(): require_external_qa( "template-prompt-audit-20260828/tp06-11-intake/run-01/INTAKE-003-ambiguous-injection.json", "template-prompt-audit-20260828/tp06-11-intake/frozen-v1.json", ) root=Path(__file__).resolve().parents[2] artifact_path=root/"docs/qa/template-prompt-audit-20260828/tp06-11-intake/run-01/INTAKE-003-ambiguous-injection.json" frozen_path=root/"docs/qa/template-prompt-audit-20260828/tp06-11-intake/frozen-v1.json" before=artifact_path.read_bytes(); artifact=json.loads(before); frozen=json.loads(frozen_path.read_text(encoding="utf-8")) case=next(item for item in frozen["cases"] if item["id"]==artifact["case_id"]) replay_errors=validate_intake_output(ContractIntakeRequest.model_validate(case["request"]), ContractIntakeOutput.model_validate(artifact["job"]["result"]["output"])) assert replay_errors==[] # postfix_regression: original assessment remains immutable and failed. assert artifact_path.read_bytes()==before and artifact["assessment"]["passed"] is False def test_question_completion_replays_known_postfix_missing_failure_without_rewriting_evidence(): require_external_qa( "template-prompt-audit-20260828/tp06-12/intake-postfix-run-01/INTAKE-002-missing.json", "template-prompt-audit-20260828/tp06-11-intake/frozen-v1.json", ) root=Path(__file__).resolve().parents[2] artifact_path=root/"docs/qa/template-prompt-audit-20260828/tp06-12/intake-postfix-run-01/INTAKE-002-missing.json" frozen_path=root/"docs/qa/template-prompt-audit-20260828/tp06-11-intake/frozen-v1.json" before=artifact_path.read_bytes(); artifact=json.loads(before); frozen=json.loads(frozen_path.read_text(encoding="utf-8")) case=next(item for item in frozen["cases"] if item["id"]==artifact["case_id"]) intake=ContractIntakeRequest.model_validate(case["request"]) raw=ContractIntakeOutput.model_validate(artifact["job"]["result"]["output"]) normalized, added, origins=complete_required_questions(intake,raw) assert len(added)==15 assert all(item["origin"]=="template_required" for item in origins if item["field"] in added) assert validate_intake_output(intake,normalized)==[] assert raw.proposals==normalized.proposals assert artifact_path.read_bytes()==before and artifact["assessment"]["passed"] is False