From 16f489c64d527d01cda894685a40af64816d5cb7 Mon Sep 17 00:00:00 2001 From: dev-belly Date: Sat, 24 Oct 2026 19:28:41 +0800 Subject: [PATCH] Treat whitespace-only emergency approver IDs as missing evidence --- docs/CONTROL_CATALOG.md | 2 ++ src/controltrace/rules.py | 4 ++-- tests/test_exports.py | 25 +++++++++++++++++++++++++ tests/test_rules.py | 10 ++++++---- 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/docs/CONTROL_CATALOG.md b/docs/CONTROL_CATALOG.md index 2f044f4..fce7f89 100644 --- a/docs/CONTROL_CATALOG.md +++ b/docs/CONTROL_CATALOG.md @@ -68,6 +68,8 @@ **需核实的例外。** 授权政策可能规定其他起算点或值班批准路径;如补批存在于外部系统,需补充原始证据。 +补批人 ID 为空字符串或仅含空白字符时同样视为缺失;及时的补批时间不能代替可识别的批准人。原始 ID 内容仍保留在关联证据和底稿中。 + **局限。** 补批及时不表示应急理由正当,也不表示事后审查足够独立或完整。 ## 复核与证据的使用边界 diff --git a/src/controltrace/rules.py b/src/controltrace/rules.py index fecc550..e42674f 100644 --- a/src/controltrace/rules.py +++ b/src/controltrace/rules.py @@ -708,13 +708,13 @@ def evaluate(data: dict[str, list[dict]]) -> list[dict[str, Any]]: completed_approvals = [ (approved, row) for row in matching_emergencies if (approved := parse_utc(row["retrospective_approved_at"])) is not None - and approved <= cutoff and row["retrospective_approver_id"] + and approved <= cutoff and (row["retrospective_approver_id"] or "").strip() ] incomplete_on_time = [ row for row in matching_emergencies if (approved := parse_utc(row["retrospective_approved_at"])) is not None and deployed <= approved <= min(due, cutoff) - and not row["retrospective_approver_id"] + and not (row["retrospective_approver_id"] or "").strip() ] before_deployment = [ row for approved, row in completed_approvals if approved < deployed diff --git a/tests/test_exports.py b/tests/test_exports.py index 7a1e1e6..cb83649 100644 --- a/tests/test_exports.py +++ b/tests/test_exports.py @@ -7,6 +7,7 @@ import json import zipfile +import duckdb import pytest from controltrace.cli import main @@ -16,6 +17,30 @@ from controltrace.verify import BundleVerificationError, verify_bundle +def test_blank_emergency_approver_stays_visible_through_database_and_bundle(tmp_path): + database = tmp_path / "blank-approver.duckdb" + initialize_db(database) + with duckdb.connect(str(database)) as connection: + connection.execute( + "UPDATE emergency_changes SET retrospective_approved_at = ?, " + "retrospective_approver_id = ? WHERE emergency_id = ?", + ["2025-06-21T09:00:00Z", " \t ", "EM005"], + ) + finding = next( + item for item in run_tests(database) + if item["entity_id"] == "D005" and item["issue_code"] == "emergency_approver_unknown" + ) + assert finding["classification"] == "manual_review" + source = next( + item["record"] for item in finding["related_records"] + if item["table"] == "emergency_changes" + ) + assert source["retrospective_approver_id"] == " \t " + destination = tmp_path / "workpapers.zip" + destination.write_bytes(workpaper_zip(database, {finding["finding_id"]})) + assert verify_bundle(destination)["exported_findings"] == 1 + + @pytest.mark.parametrize("payload", ["[]", "null", "42", '"not a manifest"']) def test_invalid_manifest_shape_returns_a_cli_error(tmp_path, capsys, payload): path = tmp_path / "invalid-manifest.zip" diff --git a/tests/test_rules.py b/tests/test_rules.py index ba825a1..f64b9c3 100644 --- a/tests/test_rules.py +++ b/tests/test_rules.py @@ -493,11 +493,13 @@ def test_multiple_emergency_records_use_any_complete_approval_within_deadline(): assert not _has(findings, "emergency_approval_overdue", "D006") -def test_emergency_approval_timestamp_without_approver_needs_review(): +@pytest.mark.parametrize("approver", [None, "", " ", "\t\r\n", "\u00a0"]) +def test_emergency_approval_timestamp_without_approver_needs_review(approver): data = deepcopy(generate_demo_data()) - _record(data, "emergency_changes", "emergency_id", "EM005")[ - "retrospective_approved_at" - ] = "2025-06-21T09:00:00Z" + _record(data, "emergency_changes", "emergency_id", "EM005").update( + retrospective_approved_at="2025-06-21T09:00:00Z", + retrospective_approver_id=approver, + ) findings = evaluate(data) assert _has(findings, "emergency_approver_unknown", "D005") assert not _has(findings, "emergency_approval_overdue", "D005")