외부 임상 검토 원장을 보강
This commit is contained in:
parent
dd71cb9710
commit
3b540a881f
8 changed files with 310 additions and 50 deletions
|
|
@ -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"])
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue