import tempfile import unittest from pathlib import Path from app.services.dataset_export import ( APPROVED_EXPORT_STATUS, DRY_RUN_EXPORT_STATUS, ExportKeyMaps, build_dataset_record, build_manifest, cohen_kappa, intraclass_correlation, scan_for_pii, sha256_file, write_jsonl, ) class DatasetExportTests(unittest.TestCase): def test_build_dataset_record_uses_masked_text_and_pseudonymous_keys(self) -> None: keys = ExportKeyMaps() record = build_dataset_record( { "session_id": "11111111-1111-1111-1111-111111111111", "learner_id": "22222222-2222-2222-2222-222222222222", "turn_id": "33333333-3333-3333-3333-333333333333", "persona_code": "P1", "stage": "rapport", "speaker": "counselor", "text": "raw text should never be exported", "text_masked": "안녕하세요, [NAME]님.", "feedback_scores": [{"dimension": "공감", "score": 4}], }, item_index=1, export_manifest_id="phase3-rl-seed-test", keys=keys, ) self.assertEqual(record["participant_key"], "PX-0001") self.assertEqual(record["session_key"], "SX-0001") self.assertEqual(record["turn_key"], "TX-000001") self.assertEqual(record["text_masked"], "안녕하세요, [NAME]님.") blob = str(record) self.assertNotIn("11111111-1111-1111-1111-111111111111", blob) self.assertNotIn("22222222-2222-2222-2222-222222222222", blob) self.assertNotIn("raw text should never be exported", blob) def test_pii_scan_detects_identifiers_without_raw_samples(self) -> None: findings = scan_for_pii( { "text_masked": "메일 learner@hs.ac.kr, 전화 010-1234-5678, 주민 990101-1234567", "api_key": "sk-should-not-appear", } ) kinds = {finding["kind"] for finding in findings} self.assertIn("email", kinds) self.assertIn("phone", kinds) self.assertIn("national_id", kinds) self.assertIn("blocked_field", kinds) self.assertFalse(any("learner@hs.ac.kr" in finding["sample"] for finding in findings)) self.assertFalse(any("010-1234-5678" in finding["sample"] for finding in findings)) def test_agreement_metrics(self) -> None: annotations = [ {"item_id": "1", "labels": {"appropriateness": "good", "rapport_signal": 4.0}}, {"item_id": "1", "labels": {"appropriateness": "good", "rapport_signal": 4.1}}, {"item_id": "2", "labels": {"appropriateness": "bad", "rapport_signal": 2.0}}, {"item_id": "2", "labels": {"appropriateness": "bad", "rapport_signal": 2.1}}, {"item_id": "3", "labels": {"appropriateness": "good", "rapport_signal": 5.0}}, {"item_id": "3", "labels": {"appropriateness": "good", "rapport_signal": 5.0}}, ] self.assertEqual(cohen_kappa(annotations, "appropriateness"), 1.0) self.assertGreater(intraclass_correlation(annotations, "rapport_signal") or 0, 0.9) def test_manifest_gate_blocks_unapproved_approved_status(self) -> None: with self.assertRaisesRegex(ValueError, "PII scan must pass"): build_manifest( export_id="phase3-rl-seed-test", dataset_name="vignette_phase3_recursive_learning_seed", export_status=APPROVED_EXPORT_STATUS, purpose="test", records=[], jsonl_path="03-export/anonymized_dataset.jsonl", jsonl_sha256="", pii_findings=[{"kind": "email", "path": "$.text_masked", "sample": ""}], participants_included=0, agreement={"kappa": 0.59, "icc": 0.74, "gold_status": "not_gold"}, ) def test_jsonl_hash_manifest_dry_run(self) -> None: record = { "schema": "phase3_dataset_item_v1", "item_id": "DI-000001", "source_refs": {"session_started_at": "2026-06-27T00:00:00Z"}, } with tempfile.TemporaryDirectory() as tmp: path = Path(tmp) / "03-export" / "anonymized_dataset.jsonl" write_jsonl([record], path) digest = sha256_file(path) manifest = build_manifest( export_id="phase3-rl-seed-test", dataset_name="vignette_phase3_recursive_learning_seed", export_status=DRY_RUN_EXPORT_STATUS, purpose="test", records=[record], jsonl_path="03-export/anonymized_dataset.jsonl", jsonl_sha256=digest, pii_findings=[], participants_included=1, ) self.assertEqual(manifest["files"][0]["rows"], 1) self.assertEqual(manifest["files"][0]["sha256"], digest) self.assertEqual(manifest["pii_scan"]["status"], "pass") if __name__ == "__main__": unittest.main()