외부 임상 검토 원장을 보강

This commit is contained in:
Yun Chan 2026-08-28 21:01:57 +09:00
parent dd71cb9710
commit 3b540a881f
8 changed files with 310 additions and 50 deletions

View file

@ -66,11 +66,24 @@ class ClinicalCrisisReviewCheckerTest(unittest.TestCase):
reviewed_at = "2026-08-28"
for case, decision in zip(case_set["cases"], decisions, strict=True):
requires_change = decision in {"conditional", "fail"}
case["reviewer_assessment"] = {
"decision": decision,
"rationale": f"{case['case_id']} 외부 검토 판정",
"findings_and_required_changes": (
"안전 전환 문구를 보강해야 함" if requires_change else "수정 요구 없음"
),
"re_review": (
"2026-09-04 수정본 제출 후 재검토"
if requires_change
else "재검토 불필요"
),
"reviewed_at": reviewed_at,
}
case_set["youth_review_responses"] = {
field: f"{field} 외부 검토 답변"
for field in checker.YOUTH_REVIEW_RESPONSE_FIELDS
}
_write_json(case_set_path, case_set)
evidence_path = root / "data/clinical/evidence/c-001-review.txt"
@ -81,9 +94,10 @@ class ClinicalCrisisReviewCheckerTest(unittest.TestCase):
manifest["approval"] = {
"reviewer": "검토자",
"organization": "외부 임상기관",
"reviewer_role_or_qualification": "임상 검토 자격 fixture",
"reviewed_at": reviewed_at,
"decision": status,
"notes": None if status == "approved" else f"{status} 판정 사유",
"notes": f"{status} 판정 사유",
"evidence_ref": "data/clinical/evidence/c-001-review.txt",
"evidence_sha256": _sha256(evidence_path),
"reviewed_protocol_version": manifest["version"],
@ -105,6 +119,8 @@ class ClinicalCrisisReviewCheckerTest(unittest.TestCase):
case_set_path = root / checker.CASE_SET_REL
case_set = _read_json(case_set_path)
case_set["cases"][0]["reviewer_assessment"]["decision"] = "pass"
case_set["cases"][0]["reviewer_assessment"]["findings_and_required_changes"] = "없음"
case_set["youth_review_responses"]["guardian_notification"] = "답변"
_write_json(case_set_path, case_set)
errors = checker.validate_review_contract(repo_root=root)
@ -112,6 +128,17 @@ class ClinicalCrisisReviewCheckerTest(unittest.TestCase):
self.assertTrue(
any("P1-CRISIS-001" in error and ".decision" in error and "null" in error for error in errors),
)
self.assertTrue(
any(
"P1-CRISIS-001" in error
and ".findings_and_required_changes" in error
and "null" in error
for error in errors
),
)
self.assertTrue(
any("youth_review_responses.guardian_notification" in error for error in errors),
)
def test_each_completed_status_passes_its_case_decision_contract(self) -> None:
fixtures = (
@ -167,6 +194,9 @@ class ClinicalCrisisReviewCheckerTest(unittest.TestCase):
case_set = _read_json(case_set_path)
case_set["cases"][0]["reviewer_assessment"]["rationale"] = " "
case_set["cases"][1]["reviewer_assessment"]["reviewed_at"] = "2026-08-27"
case_set["cases"][2]["reviewer_assessment"]["findings_and_required_changes"] = None
case_set["cases"][3]["reviewer_assessment"]["re_review"] = " "
case_set["youth_review_responses"]["safeguarding"] = None
_write_json(case_set_path, case_set)
manifest_path = root / checker.MANIFEST_REL
manifest = _read_json(manifest_path)
@ -176,6 +206,68 @@ class ClinicalCrisisReviewCheckerTest(unittest.TestCase):
errors = checker.validate_review_contract(repo_root=root)
self.assertTrue(any("P1-CRISIS-001" in error and "rationale" in error for error in errors))
self.assertTrue(any("P1-CRISIS-002" in error and "전체 검토일" in error for error in errors))
self.assertTrue(
any("P1-CRISIS-003" in error and "findings_and_required_changes" in error for error in errors),
)
self.assertTrue(any("P1-CRISIS-004" in error and "re_review" in error for error in errors))
self.assertTrue(any("youth_review_responses.safeguarding" in error for error in errors))
def test_completed_state_requires_reviewer_qualification_and_notes(self) -> None:
root = self._fixture()
self._complete(root, "approved", ["pass"] * 6)
manifest_path = root / checker.MANIFEST_REL
manifest = _read_json(manifest_path)
manifest["approval"]["reviewer_role_or_qualification"] = " "
manifest["approval"]["notes"] = None
_write_json(manifest_path, manifest)
errors = checker.validate_review_contract(repo_root=root)
self.assertTrue(any("reviewer_role_or_qualification" in error for error in errors))
self.assertTrue(any("approval.notes" in error for error in errors))
def test_conditional_or_fail_case_rejects_no_change_and_no_re_review_claims(self) -> None:
fixtures = (
("conditional", ["conditional", "pass", "pass", "pass", "pass", "pass"]),
("rejected", ["fail", "pass", "pass", "pass", "pass", "pass"]),
)
for status, decisions in fixtures:
with self.subTest(status=status):
root = self._fixture()
self._complete(root, status, decisions)
case_set_path = root / checker.CASE_SET_REL
case_set = _read_json(case_set_path)
case_set["cases"][0]["reviewer_assessment"][
"findings_and_required_changes"
] = "수정 요구 없음"
case_set["cases"][0]["reviewer_assessment"]["re_review"] = "재검토 불필요"
_write_json(case_set_path, case_set)
manifest_path = root / checker.MANIFEST_REL
manifest = _read_json(manifest_path)
manifest["approval"]["reviewed_case_set_sha256"] = _sha256(case_set_path)
_write_json(manifest_path, manifest)
errors = checker.validate_review_contract(repo_root=root)
self.assertTrue(any("실제 수정 요구" in error for error in errors))
self.assertTrue(any("재검토 제출 조건" in error for error in errors))
def test_youth_review_response_keys_must_match_exactly_without_jsonschema(self) -> None:
root = self._fixture()
case_set_path = root / checker.CASE_SET_REL
case_set = _read_json(case_set_path)
del case_set["youth_review_responses"]["documentation"]
case_set["youth_review_responses"]["unexpected"] = None
_write_json(case_set_path, case_set)
original_validator = checker.Draft202012Validator
original_format_checker = checker.FormatChecker
checker.Draft202012Validator = None
checker.FormatChecker = None
try:
errors = checker.validate_review_contract(repo_root=root)
finally:
checker.Draft202012Validator = original_validator
checker.FormatChecker = original_format_checker
self.assertTrue(any("youth_review_responses: 필드 불일치" in error for error in errors))
def test_evidence_must_exist_under_clinical_evidence_and_match_hash(self) -> None:
root = self._fixture()
@ -251,6 +343,7 @@ class ClinicalCrisisReviewCheckerTest(unittest.TestCase):
summary = json.loads(stdout.getvalue())
self.assertTrue(summary["ok"])
self.assertEqual(summary["case_count"], 6)
self.assertFalse(summary["review_complete"])
self.assertFalse(summary["external_review_replaced"])