fix(audit): raise on write-after-close + weakref.finalize + parametrized event coverage

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
transcrilive
2026-05-10 02:48:38 +02:00
parent b688c4ef77
commit 3d595e021f
2 changed files with 96 additions and 6 deletions

View File

@@ -7,6 +7,7 @@ from __future__ import annotations
import dataclasses
import io
import json
import weakref
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Literal
@@ -128,22 +129,36 @@ class ErrorEvent(_BaseEvent):
class AuditWriter:
"""Streaming JSONL writer. No-op when path is None."""
def __init__(self, path: Path | str | None, mode: str = "w") -> None:
"""Streaming JSONL writer. No-op when path is None.
Once `close()` is called, subsequent `write()` calls raise ValueError.
Use as a context manager (`with AuditWriter(p) as w:`) for guaranteed close.
"""
def __init__(self, path: Path | str | None, mode: Literal["w", "a"] = "w") -> None:
self._path = Path(path) if path is not None else None
self._fp: io.TextIOBase | None = None
self._enabled = self._path is not None
self._closed = False
self._fp: io.TextIOWrapper | None = None
if self._path is not None:
self._path.parent.mkdir(parents=True, exist_ok=True)
self._fp = self._path.open(mode, encoding="utf-8")
if self._fp is not None:
weakref.finalize(self, self._fp.close)
def write(self, event: _BaseEvent) -> None:
if self._fp is None:
if not self._enabled:
return
if self._closed:
raise ValueError("AuditWriter is closed")
assert self._fp is not None
self._fp.write(json.dumps(event.to_dict(), ensure_ascii=False))
self._fp.write("\n")
self._fp.flush()
self._fp.flush() # flush per event so a crash mid-run preserves the audit trail
def close(self) -> None:
if self._closed:
return
self._closed = True
if self._fp is not None:
self._fp.close()
self._fp = None
@@ -151,5 +166,5 @@ class AuditWriter:
def __enter__(self) -> "AuditWriter":
return self
def __exit__(self, *args) -> None:
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
self.close()