179 lines
9.8 KiB
Python
179 lines
9.8 KiB
Python
from yuqei_sdk.contract_drafting import (
|
|
ContractDraftOutput, ContractDraftRequest, DraftFact, DraftFactBinding, DraftInputConflictAssessment, DraftMissingField,
|
|
DraftQualityFinding, assess_draft_input_conflicts, draft_digest, draft_output_schema, validate_draft_output,
|
|
)
|
|
|
|
|
|
def request(policy=True):
|
|
return ContractDraftRequest(contract_title="测试", contract_type="服务", base_document="正文",
|
|
facts=[DraftFact(name="控制", value="不接触", role="control"), DraftFact(name="范围", value="接触合成文件")],
|
|
template_snapshot={"input_conflict_policy": "draft-input-conflict.v1"} if policy else {})
|
|
|
|
|
|
def output(*, evidence=True, status="incomplete", gap=False):
|
|
findings = [{"code": "FACT_CONTROL_CONFLICT", "severity": "error", "message": "输入冲突",
|
|
"fields": ["控制", "范围"], "fact_evidence": ([{"field": "控制", "value": "不接触"},
|
|
{"field": "范围", "value": "接触合成文件"}] if evidence else [])}]
|
|
if gap:
|
|
findings.append({"code": "TEMPLATE_FIELD_GAP", "severity": "warning", "message": "gap", "fields": ["控制"]})
|
|
return ContractDraftOutput(schema_version="contract-draft.v1", status=status, draft_document="正文 接触合成文件",
|
|
missing_fields=[{"field": "控制", "question": "请确认", "stage": "before_signature"}], assumptions=[], risk_notes=[],
|
|
fact_bindings=[{"field": "范围", "value": "接触合成文件", "document_excerpt": "接触合成文件"}],
|
|
source_bindings=[], change_explanations=[], quality_findings=findings)
|
|
|
|
|
|
def codes(req, out): return {item.code for item in validate_draft_output(req, out)}
|
|
|
|
|
|
def test_valid_structured_conflict_covers_only_its_missing_target():
|
|
assessment = assess_draft_input_conflicts(request(), output())
|
|
assert assessment == DraftInputConflictAssessment(policy_enabled=True, valid_finding_indexes=[0], covered_missing_fields=["控制"])
|
|
assert "draft_unmodeled_blocking_fact" not in codes(request(), output())
|
|
|
|
|
|
def test_legacy_policy_does_not_reclassify_same_output():
|
|
assert not assess_draft_input_conflicts(request(False), output()).policy_enabled
|
|
assert "draft_unmodeled_blocking_fact" in codes(request(False), output())
|
|
|
|
|
|
def test_missing_or_changed_evidence_and_candidate_ready_are_invalid():
|
|
assert "draft_input_conflict_evidence_invalid" in codes(request(), output(evidence=False))
|
|
changed = output(); changed.quality_findings[0].fact_evidence[0].value = "别的值"
|
|
assert "draft_input_conflict_evidence_invalid" in codes(request(), changed)
|
|
assert "draft_input_conflict_evidence_invalid" in codes(request(), output(status="candidate_ready"))
|
|
|
|
|
|
def test_template_gap_cannot_mask_a_provided_fact_under_new_policy():
|
|
assert "draft_template_gap_masks_provided_fact" in codes(request(), output(gap=True))
|
|
|
|
|
|
def test_evidence_is_forbidden_on_other_finding_codes_and_empty_legacy_serialization_is_stable():
|
|
out = output(); out.quality_findings[0].code = "OTHER"
|
|
assert assess_draft_input_conflicts(request(), out).invalid_finding_indexes == [0]
|
|
finding = out.quality_findings[0].model_copy(update={"fact_evidence": []})
|
|
assert "fact_evidence" not in finding.model_dump(mode="json")
|
|
|
|
|
|
def test_legacy_finding_serialization_and_digest_remain_unchanged_when_evidence_is_empty():
|
|
legacy = {"code": "OLD", "severity": "warning", "message": "旧输出", "fields": ["范围"]}
|
|
finding = DraftQualityFinding.model_validate(legacy)
|
|
assert finding.model_dump(mode="json") == legacy
|
|
assert draft_digest(finding.model_dump(mode="json")) == draft_digest(legacy)
|
|
out = output()
|
|
out.quality_findings = [finding]
|
|
assert "fact_evidence" not in out.model_dump(mode="json")["quality_findings"][0]
|
|
|
|
|
|
def test_provider_schema_requires_evidence_only_for_opted_in_conflict_policy():
|
|
runtime_finding = ContractDraftOutput.model_json_schema()["$defs"]["DraftQualityFinding"]
|
|
legacy = draft_output_schema()
|
|
provider_finding = draft_output_schema(request())["$defs"]["DraftQualityFinding"]
|
|
assert "fact_evidence" in runtime_finding["properties"]
|
|
assert "fact_evidence" not in runtime_finding["required"]
|
|
assert "fact_evidence" in provider_finding["required"]
|
|
assert "fact_evidence" not in legacy["$defs"]["DraftQualityFinding"]["properties"]
|
|
assert "DraftConflictFactEvidence" not in legacy["$defs"]
|
|
assert draft_digest(legacy) == "0d59e4969533b5dfe961df6005f80cea56d419c837f77022e7b2c965fd953e2d"
|
|
assert draft_digest(draft_output_schema(request())) == "2bdf2d71cd5d73c1c513902d1f85a59cce168cb8a835ebfdf2d1986998d18b5e"
|
|
|
|
|
|
def test_conflict_evidence_remains_strict_when_change_tracking_is_also_enabled():
|
|
req = request()
|
|
req.template_snapshot["change_evidence_policy"] = "draft-change-evidence.v1"
|
|
finding = draft_output_schema(req)["$defs"]["DraftQualityFinding"]
|
|
assert "fact_evidence" in finding["properties"]
|
|
assert "fact_evidence" in finding["required"]
|
|
|
|
|
|
def test_instruction_and_evidence_roles_cannot_certify_input_conflicts():
|
|
req = request()
|
|
req.facts[1].role = "instruction"
|
|
assert assess_draft_input_conflicts(req, output()).invalid_finding_indexes == [0]
|
|
req.facts[1].role = "evidence"
|
|
assert assess_draft_input_conflicts(req, output()).invalid_finding_indexes == [0]
|
|
|
|
|
|
def test_duplicate_fields_or_evidence_cannot_certify_input_conflicts():
|
|
duplicate_fields = output()
|
|
duplicate_fields.quality_findings[0].fields = ["控制", "范围", "范围"]
|
|
assert assess_draft_input_conflicts(request(), duplicate_fields).invalid_finding_indexes == [0]
|
|
duplicate_evidence = output()
|
|
duplicate_evidence.quality_findings[0].fact_evidence.append(
|
|
duplicate_evidence.quality_findings[0].fact_evidence[-1].model_copy())
|
|
assert assess_draft_input_conflicts(request(), duplicate_evidence).invalid_finding_indexes == [0]
|
|
|
|
|
|
def test_valid_conflict_does_not_explain_an_unrelated_missing_field():
|
|
out = output()
|
|
out.missing_fields.append(DraftMissingField(field="任意逃逸字段", question="请补充", stage="before_signature"))
|
|
findings = validate_draft_output(request(), out)
|
|
unexplained = next(item for item in findings if item.code == "draft_unmodeled_blocking_fact")
|
|
assert unexplained.fields == ["任意逃逸字段"]
|
|
assert unexplained.message == "新增阻断缺项既没有 TEMPLATE_FIELD_GAP 采集缺口说明,也没有有效冲突补问关联。"
|
|
|
|
|
|
def test_legacy_unmodeled_blocking_fact_message_is_unchanged():
|
|
out = output()
|
|
out.missing_fields.append(DraftMissingField(field="任意逃逸字段", question="请补充", stage="before_signature"))
|
|
findings = validate_draft_output(request(False), out)
|
|
unexplained = next(item for item in findings if item.code == "draft_unmodeled_blocking_fact")
|
|
assert unexplained.message == "模型新增了模板未声明的阻断缺项,但没有以 TEMPLATE_FIELD_GAP 说明模板采集缺口。"
|
|
|
|
|
|
def test_template_gap_can_still_cover_an_unprovided_optional_field():
|
|
out = output()
|
|
out.missing_fields = [DraftMissingField(field="可选但未填字段", question="请确认是否需要", stage="before_signature")]
|
|
out.quality_findings = [DraftQualityFinding(code="TEMPLATE_FIELD_GAP", severity="warning", message="模板未采集该可选字段。",
|
|
fields=["可选但未填字段"])]
|
|
assert "draft_unmodeled_blocking_fact" not in codes(request(), out)
|
|
|
|
|
|
def test_legacy_runtime_shape_parses_and_serializes_without_new_member():
|
|
legacy = {"code": "OLD", "severity": "warning", "message": "旧结构", "fields": []}
|
|
assert DraftQualityFinding.model_validate(legacy).model_dump(mode="json") == legacy
|
|
runtime = ContractDraftOutput.model_json_schema()["$defs"]["DraftQualityFinding"]
|
|
assert set(runtime["required"]) == set(legacy)
|
|
|
|
|
|
def test_composite_question_target_is_distinct_from_valid_fact_evidence():
|
|
out = output()
|
|
out.missing_fields[0] = DraftMissingField(
|
|
field="控制、范围和一致口径", question="请统一确认", stage="before_signature")
|
|
assessment = assess_draft_input_conflicts(request(), out)
|
|
assert assessment.invalid_finding_indexes == [0]
|
|
assert assessment.invalid_evidence_finding_indexes == []
|
|
assert assessment.invalid_question_target_finding_indexes == [0]
|
|
result = codes(request(), out)
|
|
assert "draft_input_conflict_question_target_invalid" in result
|
|
assert "draft_input_conflict_evidence_invalid" not in result
|
|
|
|
|
|
def test_unrelated_real_field_is_still_an_invalid_question_target():
|
|
req = request()
|
|
req.facts.append(DraftFact(name="无关事实", value="无关值", role="body_fact"))
|
|
out = output()
|
|
out.draft_document += " 无关值"
|
|
out.fact_bindings.append(DraftFactBinding(field="无关事实", value="无关值", document_excerpt="无关值"))
|
|
out.missing_fields[0] = DraftMissingField(field="无关事实", question="请确认", stage="before_signature")
|
|
assessment = assess_draft_input_conflicts(req, out)
|
|
assert assessment.invalid_question_target_finding_indexes == [0]
|
|
assert assessment.covered_missing_fields == []
|
|
|
|
|
|
def test_exact_conflict_field_remains_a_valid_question_target():
|
|
assessment = assess_draft_input_conflicts(request(), output())
|
|
assert assessment.valid_finding_indexes == [0]
|
|
assert assessment.invalid_finding_indexes == []
|
|
assert assessment.invalid_evidence_finding_indexes == []
|
|
assert assessment.invalid_question_target_finding_indexes == []
|
|
|
|
|
|
def test_legacy_policy_has_no_new_diagnostics_for_composite_target():
|
|
out = output()
|
|
out.missing_fields[0] = DraftMissingField(field="控制和范围", question="请确认", stage="before_signature")
|
|
assessment = assess_draft_input_conflicts(request(False), out)
|
|
assert not assessment.policy_enabled
|
|
assert assessment.invalid_finding_indexes == []
|
|
assert assessment.invalid_evidence_finding_indexes == []
|
|
assert assessment.invalid_question_target_finding_indexes == []
|