fix(mqtt): resolve B-9 by implementing lazy get_logs_dir() evaluation

- Replace import-time LOGS_DIR cwd binding with dynamic get_logs_dir() function
- Implement PEP 562 __getattr__ and __dir__ for transparent LOGS_DIR backward compatibility
- Update audit-log callers in mqtt_common.py and registry.py to use dynamic resolution
- Add 5 regression guards in tests/test_tier1_unit.py (276/276 PASS)
- Update IMPROVEMENTS.md, VERSIONS.md, registry.md, and include plan and peer review reports
This commit is contained in:
2026-08-17 13:37:51 +09:00
parent 8cee9374b1
commit ac82f9b993
8 changed files with 671 additions and 22 deletions
@@ -128,20 +128,30 @@ EVENTS_FILENAME = "events.ndjson"
STATUS_FILENAME = "status.json"
def _default_logs_dir() -> str:
"""Audit-log root. Overridable with ``DELEGATE_JOB_LOGS_DIR``; otherwise
``<cwd>/.mam/delegate_job_logs`` — we keep audit logs next to the
live registry (``.mam/jobs/``) so the two runtime artifacts sit
under the same parent dir and follow the same ``.gitignore`` rule.
The cwd of whichever process emits events (the bash wrapper and
scripts) is used as the anchor."""
def get_logs_dir() -> str:
"""Audit-log root, resolved at call time (B-9).
Overridable with ``DELEGATE_JOB_LOGS_DIR``; otherwise
``<cwd>/.mam/delegate_job_logs``. Resolved per call rather than at import
so a chdir after import cannot strand the audit trail in the old tree —
the same reason ``DEFAULT_REGISTRY_DIR`` stays a relative string.
"""
env = os.environ.get("DELEGATE_JOB_LOGS_DIR")
if env and env.strip():
return env
return os.path.join(os.getcwd(), ".mam", "delegate_job_logs")
LOGS_DIR = _default_logs_dir()
def __getattr__(name: str): # PEP 562 (3.7+)
"""Keep ``mqtt_common.LOGS_DIR`` working for external consumers
(documented in registry.md) while resolving it dynamically."""
if name == "LOGS_DIR":
return get_logs_dir()
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
def __dir__(): # PEP 562 recommendation — keep dir()/tab-completion discoverable
return sorted(set(globals()) | {"LOGS_DIR"})
# --------------------------------------------------------------------------
@@ -428,7 +438,7 @@ def _utcnow_precise() -> str:
# touched (it is reserved for data output).
# --------------------------------------------------------------------------
def job_log_dir(job_id: str, logs_dir: Optional[str] = None) -> Path:
return Path(logs_dir or LOGS_DIR) / job_id
return Path(logs_dir or get_logs_dir()) / job_id
def job_log_path(job_id: str, kind: str, logs_dir: Optional[str] = None) -> Path:
@@ -576,7 +586,7 @@ def iter_logged_events(job_id: str, logs_dir: Optional[str] = None):
def list_logged_jobs(logs_dir: Optional[str] = None) -> List[Dict[str, Any]]:
"""Return one meta record per job directory under the logs root, oldest
first. Falls back to ``{"job_id": <dir>}`` when meta.json is missing."""
base = Path(logs_dir or LOGS_DIR)
base = Path(logs_dir or get_logs_dir())
out: List[Dict[str, Any]] = []
if not base.exists():
return out
@@ -195,7 +195,7 @@ def get_feedback(job_id: str, registry_dir: str = DEFAULT_REGISTRY_DIR) -> str:
# 1) Try the unified audit log first (ndjson) since it's written synchronously by the subscriber
try:
import mqtt_common
logs_dir = mqtt_common.LOGS_DIR
logs_dir = mqtt_common.get_logs_dir()
events = list(mqtt_common.iter_logged_events(job_id, logs_dir))
for e in reversed(events):
if e.get("source_event") in ("completed", "error"):
@@ -386,7 +386,7 @@ def main(argv: Optional[List[str]] = None) -> int:
def _cmd_logs(args) -> int:
"""Pretty-print one job's events.ndjson, or summarise all logged jobs."""
logs_dir = args.logs_dir or mqtt_common.LOGS_DIR
logs_dir = args.logs_dir or mqtt_common.get_logs_dir()
if args.list_all:
jobs = mqtt_common.list_logged_jobs(logs_dir)