Files
ai-contract-platform/yuqei-ai-sdk-python/tests/test_draft_repair.py
T

128 lines
6.5 KiB
Python

import copy
import importlib.util
from pathlib import Path
import pytest
from pydantic import ValidationError
_SPEC = importlib.util.spec_from_file_location("semantic_review_test_helpers", Path(__file__).with_name("test_draft_semantic_review.py"))
assert _SPEC and _SPEC.loader
_HELPERS = importlib.util.module_from_spec(_SPEC)
_SPEC.loader.exec_module(_HELPERS)
output, request = _HELPERS.output, _HELPERS.request
from yuqei_sdk.contract_drafting import ContractDraftRequest
from yuqei_sdk.draft_repair import (
DraftRepairBudget,
DraftRepairReviewBinding,
_sha,
build_draft_repair_context,
)
from yuqei_sdk.draft_semantic_review import (
DraftSemanticReviewRequest,
assess_draft_semantic_review,
draft_semantic_review_output_schema,
)
def repair_context():
req = request()
metadata = {"id": "meta-risk", "field": "risk_notes", "index": 0,
"exact_excerpt": req.candidate_advisories.risk_notes[0], "candidate_excerpt": "实际损失",
"candidate_occurrence": None, "disposition": "inaccurate", "explanation": "概括不完整。"}
instruction = {"finding_ids": [], "metadata_finding_ids": ["meta-risk"],
"action": "修正风险说明", "constraints": ["不得改变正文"]}
out = output(req, status="repairable", metadata_findings=[metadata], repair_instructions=[instruction])
out.coverage.advisory_checks[0].metadata_finding_ids = ["meta-risk"]
assessment = assess_draft_semantic_review(req, out)
return build_draft_repair_context(parent_candidate_id=req.candidate_id,
parent_input_revision=req.input_revision, parent_draft_request_sha256=req.draft_request_sha256,
semantic_review_id="review-1", semantic_job_id="job-1", semantic_audit_run_id="audit-1",
semantic_receipt_sha256="9" * 64, semantic_request=req, semantic_output=out,
semantic_assessment=assessment)
def test_legacy_request_omits_repair_context_and_hash_shape():
req = ContractDraftRequest(contract_title="测试", contract_type="服务", base_document="正文")
assert "repair_context" not in req.model_dump(mode="json")
assert "repair_context" not in req.model_dump_json()
def test_repair_context_recomputes_assessment_and_binds_parent_body():
context = repair_context()
assert context.parent.candidate_body_sha256 == _sha(context.semantic_request["candidate_document"])
tampered = context.model_dump(mode="json")
tampered["semantic_assessment"]["errors"] = ["provider_claim"]
with pytest.raises(ValidationError):
type(context).model_validate(tampered)
for field, value in (("candidate_id", "other"), ("input_revision", 99), ("draft_request_sha256", "0" * 64)):
tampered = context.model_dump(mode="json")
tampered["parent"][field] = value
with pytest.raises(ValidationError):
type(context).model_validate(tampered)
@pytest.mark.parametrize("value", [True, 1.0, "1"])
def test_repair_budget_rejects_non_integer_literal_one(value):
with pytest.raises(ValidationError):
DraftRepairBudget(draft_attempts=value, followup_semantic_review_attempts=1)
with pytest.raises(ValidationError):
DraftRepairReviewBinding(repair_context_sha256="1" * 64,
parent_candidate_id="candidate", max_attempts=value)
def test_post_repair_review_requires_full_context_and_matching_binding():
context = repair_context()
parent = DraftSemanticReviewRequest.model_validate(context.semantic_request)
payload = parent.model_dump(mode="json")
payload["base_document"] = parent.candidate_document
payload["candidate_document"] = parent.candidate_document
payload["candidate_body_sha256"] = _sha(parent.candidate_document)
payload["repair_context"] = context.model_dump(mode="json")
binding = DraftRepairReviewBinding(repair_context_sha256=_sha(context.model_dump(mode="json")),
parent_candidate_id=context.parent.candidate_id)
payload["repair_binding"] = binding.model_dump(mode="json")
assert DraftSemanticReviewRequest.model_validate(payload).repair_context is not None
post_request = DraftSemanticReviewRequest.model_validate(payload)
schema = draft_semantic_review_output_schema(post_request)
assert schema["properties"]["repair_resolution_checks"]["type"] == "array"
assert "repair_resolution_checks" not in draft_semantic_review_output_schema()["properties"]
for field in ("repair_context", "repair_binding"):
broken = copy.deepcopy(payload)
broken.pop(field)
with pytest.raises(ValidationError):
DraftSemanticReviewRequest.model_validate(broken)
def test_post_repair_resolution_checks_are_exact_and_gate_passed():
context = repair_context()
parent = DraftSemanticReviewRequest.model_validate(context.semantic_request)
payload = parent.model_dump(mode="json")
payload.update(base_document=parent.candidate_document,
repair_context=context.model_dump(mode="json"),
repair_binding=DraftRepairReviewBinding(
repair_context_sha256=_sha(context.model_dump(mode="json")),
parent_candidate_id=context.parent.candidate_id).model_dump(mode="json"))
post = DraftSemanticReviewRequest.model_validate(payload)
check = {"finding_id": "meta-risk", "status": "resolved", "candidate_excerpt": "实际损失",
"candidate_occurrence": None, "metadata_field": "risk_notes", "metadata_index": 0,
"metadata_excerpt": post.candidate_advisories.risk_notes[0], "explanation": "已重新核对说明。"}
result = output(post, repair_resolution_checks=[check])
assert assess_draft_semantic_review(post, result).status == "valid"
missing = output(post, repair_resolution_checks=[])
assert "repair_resolution_coverage_invalid" in assess_draft_semantic_review(post, missing).errors
unresolved = output(post, repair_resolution_checks=[{**check, "status": "unresolved"}])
assert "passed_with_unresolved_repair" in assess_draft_semantic_review(post, unresolved).errors
broken = copy.deepcopy(payload)
broken["repair_binding"]["repair_context_sha256"] = "0" * 64
with pytest.raises(ValidationError):
DraftSemanticReviewRequest.model_validate(broken)
for mutate in (
lambda value: value["facts"].__setitem__(0, {**value["facts"][0], "value": "十一日"}),
lambda value: value["template"].__setitem__("id", "other-template"),
lambda value: value.__setitem__("playbook_sha256", "0" * 64),
):
broken = copy.deepcopy(payload)
mutate(broken)
with pytest.raises(ValidationError):
DraftSemanticReviewRequest.model_validate(broken)