#673 offenders adopt prax append_jsonl: @backup (1 .jsonl site), @hooks (2 .jsonl sites), @trigger (11 raw .log appenders across 8 files -> .jsonl + downstream medic_state/medic/log_watcher readers + 5 tests). Zero raw open('a') log appenders remain fleet-wide. Verified: backup 18 / hooks 114 / trigger 189 tests green, no recursion regression, append_jsonl wired at every site.

This commit is contained in:
AIOSAI
2026-07-10 18:24:06 -07:00
parent dfd6732f5b
commit 4404b60305
18 changed files with 153 additions and 130 deletions
@@ -14,7 +14,7 @@ import tempfile
from datetime import datetime, timezone
from pathlib import Path
from aipass.prax import logger
from aipass.prax import append_jsonl, logger
def log_operation(operation: str, data: dict) -> None:
@@ -24,12 +24,9 @@ def log_operation(operation: str, data: dict) -> None:
"operation": operation,
**data,
}
log_dir = Path(__file__).resolve().parents[3] / "logs"
log_dir.mkdir(exist_ok=True)
log_file = log_dir / "operations.jsonl"
log_file = Path(__file__).resolve().parents[3] / "logs" / "operations.jsonl"
try:
with open(log_file, "a", encoding="utf-8") as f:
f.write(json.dumps(entry, default=str) + "\n")
append_jsonl(log_file, entry)
except OSError as e:
logger.warning(f"Failed to write operation log: {e}")
@@ -10,9 +10,9 @@
"""JSONL diagnostic logging — appends structured entries for hook activity."""
import json
from pathlib import Path
from aipass.prax import append_jsonl
from aipass.prax.apps.modules.logger import system_logger as logger
BRANCH_ROOT = Path(__file__).resolve().parent.parent.parent.parent
@@ -22,9 +22,7 @@ LOG_FILE = BRANCH_ROOT / "logs" / "engine.jsonl"
def log_entry(entry: dict) -> None:
"""Append a JSONL log entry for detailed diagnostics."""
try:
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
append_jsonl(LOG_FILE, entry)
except OSError as exc:
logger.error("[HOOKS] log write failed: %s", exc)
@@ -28,6 +28,7 @@ from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
from aipass.prax import append_jsonl
from aipass.prax.apps.modules.logger import system_logger as logger
PENDING_DIR = Path.home() / ".aipass" / "telegram_pending"
@@ -760,8 +761,6 @@ def _write_delivery_log(intended_text: str, chunks: list[str], chunk_results: li
record["culprit"] = culprit
try:
_DELIVERY_LOG.parent.mkdir(parents=True, exist_ok=True)
with open(_DELIVERY_LOG, "a", encoding="utf-8") as f:
f.write(json.dumps(record) + "\n")
append_jsonl(_DELIVERY_LOG, record)
except OSError as e:
logger.warning("[HOOKS] telegram: delivery log write failed: %s", e)
+11 -8
View File
@@ -19,23 +19,26 @@ import os
import tempfile
from contextlib import contextmanager
from pathlib import Path
from datetime import datetime, timezone
try:
from aipass.prax import append_jsonl as _append_jsonl
except Exception:
_append_jsonl = None
# Trigger package root: .../aipass/trigger/
TRIGGER_ROOT = Path(__file__).resolve().parents[1]
_CONFIG_LOG = TRIGGER_ROOT / "logs" / "config.log"
_CONFIG_LOG = TRIGGER_ROOT / "logs" / "config.jsonl"
def _log_warning(message: str) -> None:
"""Log warning to file (config cannot import prax logger — circular)."""
"""Log warning to file (recursion-safe prax path)."""
if _append_jsonl is None:
return
try:
_CONFIG_LOG.parent.mkdir(parents=True, exist_ok=True)
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
with open(_CONFIG_LOG, "a", encoding="utf-8") as f:
f.write(f"{ts} | WARNING | {message}\n")
_append_jsonl(_CONFIG_LOG, {"level": "WARNING", "msg": message})
except Exception:
pass # Meta-logging: cannot log a failure to log
pass
# AIPass package root: .../aipass/
@@ -35,24 +35,28 @@ Architecture (Medic v2):
import json
import time
from datetime import datetime, timezone
from datetime import datetime
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional
from aipass.trigger.apps.config import TRIGGER_ROOT
from aipass.trigger.apps.handlers.json import json_handler
_HANDLER_LOG = TRIGGER_ROOT / "logs" / "error_detected_handler.log"
try:
from aipass.prax import append_jsonl as _append_jsonl
except Exception:
_append_jsonl = None
_HANDLER_LOG = TRIGGER_ROOT / "logs" / "error_detected_handler.jsonl"
def _log_warning(message: str) -> None:
"""Log warning to file (event handlers cannot import Prax logger - causes recursion)."""
"""Log warning to file (recursion-safe prax path)."""
if _append_jsonl is None:
return
try:
_HANDLER_LOG.parent.mkdir(parents=True, exist_ok=True)
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
with open(_HANDLER_LOG, "a", encoding="utf-8") as f:
f.write(f"{ts} | WARNING | {message}\n")
_append_jsonl(_HANDLER_LOG, {"level": "WARNING", "msg": message})
except Exception:
pass
pass # seedgo:bypass meta-logging
def _find_repo_root() -> Path:
@@ -348,22 +352,29 @@ REPORT TO @devpulse:
def _write_suppression_log(reason: str, branch: str, module: str, message: str) -> None:
"""Write a line to the medic suppression log."""
if _append_jsonl is None:
return
try:
suppressed_log = TRIGGER_ROOT / "logs" / "medic_suppressed.log"
suppressed_log.parent.mkdir(parents=True, exist_ok=True)
with open(suppressed_log, "a", encoding="utf-8") as f:
f.write(f"{datetime.now().isoformat()} | {reason} - {branch}: {module} - {message[:100]}\n")
suppressed_log = TRIGGER_ROOT / "logs" / "medic_suppressed.jsonl"
entry = {
"ts": datetime.now().isoformat(),
"reason": reason,
"branch": branch,
"module": module,
"msg": message[:100],
}
_append_jsonl(suppressed_log, entry)
except Exception as exc:
_log_warning(f"suppression log write failed ({reason}): {exc}")
def _write_rate_log(reason: str, detail: str) -> None:
"""Write a line to the rate-limited log."""
if _append_jsonl is None:
return
try:
rate_log = TRIGGER_ROOT / "logs" / "rate_limited.log"
rate_log.parent.mkdir(parents=True, exist_ok=True)
with open(rate_log, "a", encoding="utf-8") as f:
f.write(f"{datetime.now().isoformat()} | {reason}: {detail}\n")
rate_log = TRIGGER_ROOT / "logs" / "rate_limited.jsonl"
_append_jsonl(rate_log, {"ts": datetime.now().isoformat(), "reason": reason, "detail": detail})
except Exception as exc:
_log_warning(f"rate log write failed ({reason}): {exc}")
@@ -24,24 +24,27 @@ Event data expected:
- error: str | None — error message if success=False
"""
from datetime import datetime, timezone
from typing import Any
from aipass.trigger.apps.config import TRIGGER_ROOT
from aipass.trigger.apps.handlers.json import json_handler
_HANDLER_LOG = TRIGGER_ROOT / "logs" / "memory_pool_handler.log"
try:
from aipass.prax import append_jsonl as _append_jsonl
except Exception:
_append_jsonl = None
_HANDLER_LOG = TRIGGER_ROOT / "logs" / "memory_pool_handler.jsonl"
def _log_warning(message: str) -> None:
"""Log warning to file (event handlers cannot import prax logger — causes recursion)."""
"""Log warning to file (recursion-safe prax path)."""
if _append_jsonl is None:
return
try:
_HANDLER_LOG.parent.mkdir(parents=True, exist_ok=True)
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
with open(_HANDLER_LOG, "a", encoding="utf-8") as f:
f.write(f"{ts} | WARNING | {message}\n")
_append_jsonl(_HANDLER_LOG, {"level": "WARNING", "msg": message})
except Exception:
pass # Meta-logging: cannot log a failure to log
pass # seedgo:bypass meta-logging
def handle_memory_pool_auto_processed(
@@ -29,6 +29,11 @@ from typing import Optional
from aipass.trigger.apps.config import TRIGGER_ROOT, AIPASS_PKG_ROOT, atomic_write_json
from aipass.trigger.apps.handlers.json import json_handler
try:
from aipass.prax import append_jsonl as _append_jsonl
except Exception:
_append_jsonl = None
def _find_repo_root() -> Path:
"""Walk up from this file to find the repo root (contains AIPASS_REGISTRY.json)."""
@@ -45,21 +50,19 @@ REPO_ROOT = _find_repo_root()
FLOW_JSON_DIR = AIPASS_PKG_ROOT / "flow" / "flow_json"
REGISTRY_FILE = FLOW_JSON_DIR / "PLAN_REGISTRY.json"
# Log file for handler errors (no Prax imports in handlers - causes recursion)
HANDLER_LOG = TRIGGER_ROOT / "logs" / "plan_file_handler.log"
HANDLER_LOG = TRIGGER_ROOT / "logs" / "plan_file_handler.jsonl"
MODULE_NAME = "trigger.plan_file"
def _log_error(message: str) -> None:
"""Log error to file (handlers cannot import Prax logger - causes recursion)"""
"""Log error to file (recursion-safe prax path)."""
if _append_jsonl is None:
return
try:
HANDLER_LOG.parent.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now(timezone.utc).isoformat()
with open(HANDLER_LOG, "a", encoding="utf-8") as f:
f.write(f"[{timestamp}] [{MODULE_NAME}] {message}\n")
_append_jsonl(HANDLER_LOG, {"level": "ERROR", "module": MODULE_NAME, "msg": message})
except Exception:
pass # Last resort - cannot fail on logging failure
pass # seedgo:bypass meta-logging
def _load_registry() -> dict:
@@ -20,24 +20,27 @@ Events:
"""
import subprocess
from datetime import datetime, timezone
from typing import Any
from aipass.trigger.apps.config import TRIGGER_ROOT
from aipass.trigger.apps.handlers.json import json_handler
_HANDLER_LOG = TRIGGER_ROOT / "logs" / "pr_status_sync_handler.log"
try:
from aipass.prax import append_jsonl as _append_jsonl
except Exception:
_append_jsonl = None
_HANDLER_LOG = TRIGGER_ROOT / "logs" / "pr_status_sync_handler.jsonl"
def _log_info(message: str) -> None:
"""Log to file (event handlers cannot import prax logger — causes recursion)."""
"""Log to file (recursion-safe prax path)."""
if _append_jsonl is None:
return
try:
_HANDLER_LOG.parent.mkdir(parents=True, exist_ok=True)
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
with open(_HANDLER_LOG, "a", encoding="utf-8") as f:
f.write(f"{ts} | INFO | {message}\n")
_append_jsonl(_HANDLER_LOG, {"level": "INFO", "msg": message})
except Exception:
pass # Meta-logging: cannot log a failure to log
pass # seedgo:bypass meta-logging
def _run_status_sync(reason: str) -> None:
@@ -8,23 +8,25 @@
"""Event Handler Registry - Setup all event handlers on startup"""
from datetime import datetime, timezone
from aipass.trigger.apps.handlers.json import json_handler
from aipass.trigger.apps.config import TRIGGER_ROOT
_HANDLER_LOG = TRIGGER_ROOT / "logs" / "registry_handler.log"
try:
from aipass.prax import append_jsonl as _append_jsonl
except Exception:
_append_jsonl = None
_HANDLER_LOG = TRIGGER_ROOT / "logs" / "registry_handler.jsonl"
def _log_warning(message: str) -> None:
"""Log warning to file (event handlers cannot import Prax logger — causes recursion)."""
"""Log warning to file (recursion-safe prax path)."""
if _append_jsonl is None:
return
try:
_HANDLER_LOG.parent.mkdir(parents=True, exist_ok=True)
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
with open(_HANDLER_LOG, "a", encoding="utf-8") as f:
f.write(f"{ts} | WARNING | {message}\n")
_append_jsonl(_HANDLER_LOG, {"level": "WARNING", "msg": message})
except Exception:
pass # Meta-logging: cannot log a failure to log
pass # seedgo:bypass meta-logging
def setup_handlers():
@@ -21,14 +21,19 @@ import json
import hashlib
import time
from pathlib import Path
from datetime import datetime, timedelta, timezone
from datetime import datetime, timedelta
from typing import Any, Callable, Dict, List, Optional, Set
from aipass.trigger.apps.config import TRIGGER_ROOT, atomic_write_json
from aipass.trigger.apps.handlers.json import json_handler
try:
from aipass.prax import append_jsonl as _append_jsonl
except Exception:
_append_jsonl = None
SYSTEM_LOGS_DIR = TRIGGER_ROOT.parent.parent.parent / "system_logs"
TRIGGER_DATA_FILE = TRIGGER_ROOT / "trigger_json" / "trigger_data.json"
SUPPRESSED_LOG = TRIGGER_ROOT / "logs" / "medic_suppressed.log"
SUPPRESSED_LOG = TRIGGER_ROOT / "logs" / "medic_suppressed.jsonl"
MAX_HASHES = 500
MAX_LOOKBACK_HOURS = 24
@@ -38,18 +43,17 @@ MAX_ERRORS_PER_SCAN = 50 # Stop after this many new errors found
MAX_FILE_SIZE_BYTES = 512_000 # Skip files larger than 500KB
SCAN_TIME_BUDGET_SECONDS = 5.0 # Abort entire scan after this many seconds
_HANDLER_LOG = TRIGGER_ROOT / "logs" / "startup_handler.log"
_HANDLER_LOG = TRIGGER_ROOT / "logs" / "startup_handler.jsonl"
def _log_warning(message: str) -> None:
"""Log warning to file (event handlers cannot import Prax logger - causes recursion)."""
"""Log warning to file (recursion-safe prax path)."""
if _append_jsonl is None:
return
try:
_HANDLER_LOG.parent.mkdir(parents=True, exist_ok=True)
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
with open(_HANDLER_LOG, "a", encoding="utf-8") as f:
f.write(f"{ts} | WARNING | {message}\n")
_append_jsonl(_HANDLER_LOG, {"level": "WARNING", "msg": message})
except Exception:
pass
pass # seedgo:bypass meta-logging
def _load_trigger_data() -> Dict[str, Any]:
@@ -96,18 +100,13 @@ def _save_trigger_data(data: Dict[str, Any]) -> None:
def _log_suppression(reason: str) -> None:
"""Log a catchup suppression event to medic_suppressed.log.
Args:
reason: Description of why scanning was capped or skipped
"""
"""Log a catchup suppression event to medic_suppressed.jsonl."""
if _append_jsonl is None:
return
try:
SUPPRESSED_LOG.parent.mkdir(parents=True, exist_ok=True)
with open(SUPPRESSED_LOG, "a", encoding="utf-8") as f:
f.write(f"{datetime.now().isoformat()} | error_catchup: {reason}\n")
_append_jsonl(SUPPRESSED_LOG, {"ts": datetime.now().isoformat(), "source": "error_catchup", "reason": reason})
except Exception as exc:
_log_warning(f"log suppression write failed: {exc}")
return
def _generate_error_hash(source_module: str, message: str) -> str:
@@ -12,12 +12,17 @@ import json
import os
import sys
from pathlib import Path
from datetime import datetime, timezone
from datetime import datetime
from typing import Dict, Any, Optional
import inspect
from aipass.trigger.apps.config import atomic_write_json
try:
from aipass.prax import append_jsonl as _append_jsonl
except Exception:
_append_jsonl = None
if sys.platform == "win32":
os.environ.setdefault("PYTHONUTF8", "1")
for _stream in (sys.stdout, sys.stderr):
@@ -28,19 +33,19 @@ if sys.platform == "win32":
# Infrastructure — redirect to temp dir during tests
_test_log_dir = os.environ.get("AIPASS_TEST_LOG_DIR")
if _test_log_dir:
_LOG_FILE = Path(_test_log_dir) / "trigger" / "json_handler.log"
_LOG_FILE = Path(_test_log_dir) / "trigger" / "json_handler.jsonl"
else:
_LOG_FILE = Path(__file__).parent.parent.parent.parent / "logs" / "json_handler.log"
_LOG_FILE = Path(__file__).parent.parent.parent.parent / "logs" / "json_handler.jsonl"
def _log_warning(msg: str) -> None:
"""File-based warning logger to avoid circular imports with prax."""
"""Recursion-safe warning logger via prax append_jsonl."""
if _append_jsonl is None:
return
try:
_LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
with open(_LOG_FILE, "a", encoding="utf-8") as f:
f.write(f"{datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')} | WARNING | {msg}\n")
_append_jsonl(_LOG_FILE, {"level": "WARNING", "msg": msg})
except Exception:
pass # Meta-logging: cannot log a failure to log
pass # seedgo:bypass meta-logging
# Constants
@@ -48,8 +48,8 @@ STALE_ENTRY_THRESHOLD_SECONDS = 300 # 5 minutes
# Compared case-insensitively against Path.name (see on_modified)
EXCLUDED_LOG_FILES: Set[str] = {
"dispatch.log",
"medic_suppressed.log",
"rate_limited.log",
"medic_suppressed.jsonl",
"rate_limited.jsonl",
"error_monitor.log",
"log_watcher.log",
"log_watcher.log.1",
@@ -28,8 +28,8 @@ from aipass.trigger.apps.handlers.json import json_handler
logger = get_direct_logger()
TRIGGER_CONFIG_FILE = TRIGGER_ROOT / "trigger_json" / "trigger_config.json"
MEDIC_SUPPRESSED_LOG = TRIGGER_ROOT / "logs" / "medic_suppressed.log"
RATE_LIMITED_LOG = TRIGGER_ROOT / "logs" / "rate_limited.log"
MEDIC_SUPPRESSED_LOG = TRIGGER_ROOT / "logs" / "medic_suppressed.jsonl"
RATE_LIMITED_LOG = TRIGGER_ROOT / "logs" / "rate_limited.jsonl"
def read_config() -> dict:
@@ -190,8 +190,8 @@ def get_suppression_stats() -> Dict[str, Any]:
lines = MEDIC_SUPPRESSED_LOG.read_text(encoding="utf-8").strip().splitlines()
suppressed_count = len(lines)
if lines:
last_line = lines[-1]
last_suppressed = last_line.split(" | ")[0] if " | " in last_line else "unknown"
entry = json.loads(lines[-1])
last_suppressed = entry.get("ts", "unknown")
except Exception as exc:
logger.warning("get_suppression_stats failed: %s", exc)
return {"suppressed_count": 0, "last_suppressed": "error reading log"}
@@ -216,8 +216,8 @@ def get_rate_limit_stats() -> Dict[str, Any]:
lines = RATE_LIMITED_LOG.read_text(encoding="utf-8").strip().splitlines()
dispatch_count = len(lines)
if lines:
last_line = lines[-1]
last_dispatch = last_line.split(" | ")[0] if " | " in last_line else "unknown"
entry = json.loads(lines[-1])
last_dispatch = entry.get("ts", "unknown")
except Exception as exc:
logger.warning("get_rate_limit_stats failed: %s", exc)
return {"rate_limited_count": 0, "last_rate_limited": "error reading log"}
+5 -5
View File
@@ -198,11 +198,11 @@ def print_help() -> None:
console.print()
console.print(" [yellow]off[/yellow] Global kill switch. ALL error dispatch stops. No branch")
console.print(" receives auto-healing emails. Errors still logged to")
console.print(" medic_suppressed.log for review.")
console.print(" medic_suppressed.jsonl for review.")
console.print()
console.print(" [yellow]mute[/yellow] Per-branch suppress. Only the muted branch stops receiving")
console.print(" dispatch. All other branches continue normally. Muted errors")
console.print(" logged to medic_suppressed.log.")
console.print(" logged to medic_suppressed.jsonl.")
console.print()
console.rule("EXAMPLES")
console.print()
@@ -227,7 +227,7 @@ def print_help() -> None:
console.print(" -> handler checks medic_enabled -> checks branch mute list")
console.print(" -> dispatches fix-it email to affected branch (or suppresses)")
console.print()
console.print(" Suppressed errors: trigger/logs/medic_suppressed.log")
console.print(" Suppressed errors: trigger/logs/medic_suppressed.jsonl")
console.print()
@@ -292,7 +292,7 @@ def _handle_status(console) -> None:
console.print(f" Last rate limit: {rate_limits['last_rate_limited']}")
console.print()
if not enabled:
console.print(" [dim]All error dispatch suppressed. Errors logged to medic_suppressed.log[/dim]")
console.print(" [dim]All error dispatch suppressed. Errors logged to medic_suppressed.jsonl[/dim]")
def _handle_on(console) -> None:
@@ -342,7 +342,7 @@ def _handle_off(console) -> None:
Panel(
"[bold yellow]Medic DISABLED[/bold yellow]\n\n"
"Error dispatch is [yellow]suppressed[/yellow]. Errors are still detected\n"
"and logged to [dim]medic_suppressed.log[/dim] for review.\n"
"and logged to [dim]medic_suppressed.jsonl[/dim] for review.\n"
"Log watcher: [yellow]stopped[/yellow]",
title="Medic",
border_style="yellow",
+2 -2
View File
@@ -1073,10 +1073,10 @@ class TestShouldProcessEdgeCases:
assert watcher._should_process(path) is False
def test_excluded_medic_suppressed(self):
"""medic_suppressed.log is excluded."""
"""medic_suppressed.jsonl is excluded."""
lw = _import_log_watcher()
watcher = lw.BranchLogWatcher()
path = str(Path("/src") / "aipass" / "flow" / "logs" / "medic_suppressed.log")
path = str(Path("/src") / "aipass" / "flow" / "logs" / "medic_suppressed.jsonl")
assert watcher._should_process(path) is False
+1 -1
View File
@@ -307,7 +307,7 @@ def test_handle_command_status_suppression_hint_when_disabled():
console = _get_console()
printed = _get_print_str_args(console)
hint = " [dim]All error dispatch suppressed. Errors logged to medic_suppressed.log[/dim]"
hint = " [dim]All error dispatch suppressed. Errors logged to medic_suppressed.jsonl[/dim]"
assert hint in printed, f"Expected suppression hint '{hint}' in printed args: {printed}"
+17 -17
View File
@@ -63,8 +63,8 @@ def state_mod(tmp_path, monkeypatch):
import aipass.trigger.apps.handlers.medic_state as mod
config_file = tmp_path / "trigger_json" / "trigger_config.json"
suppressed_log = tmp_path / "logs" / "medic_suppressed.log"
rate_limited_log = tmp_path / "logs" / "rate_limited.log"
suppressed_log = tmp_path / "logs" / "medic_suppressed.jsonl"
rate_limited_log = tmp_path / "logs" / "rate_limited.jsonl"
monkeypatch.setattr(mod, "TRIGGER_CONFIG_FILE", config_file)
monkeypatch.setattr(mod, "MEDIC_SUPPRESSED_LOG", suppressed_log)
@@ -424,26 +424,26 @@ class TestGetSuppressionStats:
assert result["last_suppressed"] == "never"
def test_populated_log(self, state_mod):
"""get_suppression_stats parses log lines and returns correct stats."""
"""get_suppression_stats parses JSONL lines and returns correct stats."""
log_file = state_mod.MEDIC_SUPPRESSED_LOG
log_file.parent.mkdir(parents=True, exist_ok=True)
lines = [
"2026-04-01 10:00:00 | ImportError | FLOW",
"2026-04-02 11:00:00 | TimeoutError | API",
"2026-04-03 12:00:00 | ValueError | DRONE",
'{"ts": "2026-04-01T10:00:00", "reason": "count<2", "branch": "FLOW"}',
'{"ts": "2026-04-02T11:00:00", "reason": "count<2", "branch": "API"}',
'{"ts": "2026-04-03T12:00:00", "reason": "count<2", "branch": "DRONE"}',
]
log_file.write_text("\n".join(lines), encoding="utf-8")
result = state_mod.get_suppression_stats()
assert result["suppressed_count"] == 3
assert result["last_suppressed"] == "2026-04-03 12:00:00"
assert result["last_suppressed"] == "2026-04-03T12:00:00"
def test_log_line_without_pipe_separator(self, state_mod):
"""get_suppression_stats returns 'unknown' when last line has no pipe."""
def test_log_line_without_ts_field(self, state_mod):
"""get_suppression_stats returns 'unknown' when last entry has no ts."""
log_file = state_mod.MEDIC_SUPPRESSED_LOG
log_file.parent.mkdir(parents=True, exist_ok=True)
log_file.write_text("malformed line without pipe", encoding="utf-8")
log_file.write_text('{"reason": "no timestamp"}', encoding="utf-8")
result = state_mod.get_suppression_stats()
@@ -478,25 +478,25 @@ class TestGetRateLimitStats:
assert result["last_rate_limited"] == "never"
def test_populated_log(self, state_mod):
"""get_rate_limit_stats parses log lines and returns correct stats."""
"""get_rate_limit_stats parses JSONL lines and returns correct stats."""
log_file = state_mod.RATE_LIMITED_LOG
log_file.parent.mkdir(parents=True, exist_ok=True)
lines = [
"2026-04-01 08:00:00 | ImportError | fp123",
"2026-04-02 09:00:00 | TimeoutError | fp456",
'{"ts": "2026-04-01T08:00:00", "reason": "backoff", "detail": "fp123"}',
'{"ts": "2026-04-02T09:00:00", "reason": "backoff", "detail": "fp456"}',
]
log_file.write_text("\n".join(lines), encoding="utf-8")
result = state_mod.get_rate_limit_stats()
assert result["rate_limited_count"] == 2
assert result["last_rate_limited"] == "2026-04-02 09:00:00"
assert result["last_rate_limited"] == "2026-04-02T09:00:00"
def test_log_line_without_pipe_separator(self, state_mod):
"""get_rate_limit_stats returns 'unknown' when last line has no pipe."""
def test_log_line_without_ts_field(self, state_mod):
"""get_rate_limit_stats returns 'unknown' when last entry has no ts."""
log_file = state_mod.RATE_LIMITED_LOG
log_file.parent.mkdir(parents=True, exist_ok=True)
log_file.write_text("malformed line", encoding="utf-8")
log_file.write_text('{"reason": "no timestamp"}', encoding="utf-8")
result = state_mod.get_rate_limit_stats()
@@ -192,7 +192,7 @@ class TestHandleMemoryPoolAutoProcessedFailure:
error="pool write failed",
)
log_file = tmp_path / "logs" / "memory_pool_handler.log"
log_file = tmp_path / "logs" / "memory_pool_handler.jsonl"
assert log_file.exists()
content = log_file.read_text()
assert "pool write failed" in content