Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/CONTROL_CATALOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@

**需核实的例外。** 授权政策可能规定其他起算点或值班批准路径;如补批存在于外部系统,需补充原始证据。

补批人 ID 为空字符串或仅含空白字符时同样视为缺失;及时的补批时间不能代替可识别的批准人。原始 ID 内容仍保留在关联证据和底稿中。

**局限。** 补批及时不表示应急理由正当,也不表示事后审查足够独立或完整。

## 复核与证据的使用边界
Expand Down
4 changes: 2 additions & 2 deletions src/controltrace/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions tests/test_exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import json
import zipfile

import duckdb
import pytest

from controltrace.cli import main
Expand All @@ -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"
Expand Down
10 changes: 6 additions & 4 deletions tests/test_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading