128 lines
6.5 KiB
Python
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)
|