diff --git a/src/audit.py b/src/audit.py index 67daeed..10d48f0 100644 --- a/src/audit.py +++ b/src/audit.py @@ -121,13 +121,18 @@ def audit_log_path() -> Path: """ global _LOG_PATH if _LOG_PATH is None: + # Resolve the session id BEFORE acquiring _LOCK. ``_session_id`` + # also takes ``_LOCK`` to write its first value, and the lock + # is non-reentrant — nesting them deadlocks the first caller + # in a clean module state. (Hit in ``log_session_start`` since + # it's the first GUI call and both globals are unset.) + sid = _session_id()[:8] with _LOCK: if _LOG_PATH is None: try: ts = datetime.now(tz=timezone.utc).strftime("%Y%m%dT%H%M%SZ") except Exception: ts = "unknown" - sid = _session_id()[:8] _LOG_PATH = audit_log_dir() / f"datatools-{ts}-{sid}.jsonl" return _LOG_PATH diff --git a/tests/test_audit.py b/tests/test_audit.py index 7e06559..2ee2f30 100644 --- a/tests/test_audit.py +++ b/tests/test_audit.py @@ -26,9 +26,14 @@ import pytest @pytest.fixture def isolated_audit(monkeypatch, tmp_path): """Redirect audit writes into ``tmp_path`` and reset module state - so each test starts fresh.""" + so each test starts fresh. + + The kill switch is bypassed for the duration of the test by + patching the module-level constant directly — these tests need + the real producer path to run.""" monkeypatch.setenv("DATATOOLS_AUDIT_DIR", str(tmp_path)) from src import audit + monkeypatch.setattr(audit, "_DISABLED", False) audit.reset_for_tests() yield audit # Best-effort cleanup so a runaway writer thread doesn't keep @@ -115,6 +120,7 @@ class TestUnwritableTargetDoesntCrash: not_a_dir.write_text("hi") monkeypatch.setenv("DATATOOLS_AUDIT_DIR", str(not_a_dir)) from src import audit + monkeypatch.setattr(audit, "_DISABLED", False) audit.reset_for_tests() try: start = time.perf_counter()