From 90048069d07db6f5e3ed224f32259389fb6cc1e6 Mon Sep 17 00:00:00 2001 From: AIOSAI Date: Sat, 11 Jul 2026 18:46:36 -0700 Subject: [PATCH] fix seedgo-audit red on PR659 (2 branches 99%, from S300 commits) + kill the flake that blocked this commit's first gate run. AUDIT: aipass doctor.py _fix_owner_seating had 2 silent catches (FileNotFoundError/TimeoutExpired returned WARN without logging) -> added logger.info/warning matching sibling _check_owner_seating; devpulse README claimed 407 tests (pytest pass count incl parametrized) but readme checker counts test FUNCTIONS -> corrected to 309 (grep-verified). Both re-audit 100% locally; aipass doctor tests 133 pass. FLAKE: test_watchdog_agent bounce/lock-removal/replied tests patched GLOBAL time.sleep with unguarded fakes (agent_handler.time IS the time module) -> prax daemon threads ran the side effect concurrently, re-truncating last_bounce.json mid-read in _classify_exit -> JSONDecodeError path -> exit_code None != 1 (failed the gate suite run, passes isolated). Fix: _agent_only_sleep caller-frame guard (same as yesterdays _fake_clock_sleep, 766d697e) on all 3 tests; 17 pass 3x deterministic. --- src/aipass/aipass/apps/modules/doctor.py | 2 + src/aipass/devpulse/README.md | 2 +- .../devpulse/tests/test_watchdog_agent.py | 62 +++++++++++-------- 3 files changed, 38 insertions(+), 28 deletions(-) diff --git a/src/aipass/aipass/apps/modules/doctor.py b/src/aipass/aipass/apps/modules/doctor.py index 5dc4bac7..6969d277 100644 --- a/src/aipass/aipass/apps/modules/doctor.py +++ b/src/aipass/aipass/apps/modules/doctor.py @@ -252,8 +252,10 @@ def _fix_owner_seating() -> List[CheckResult]: timeout=60, ) except FileNotFoundError: + logger.info("[doctor] drone not on PATH — skipping owner fix") return [CheckResult("owner fix", GLYPH_WARN, "drone not found", "")] except subprocess.TimeoutExpired: + logger.warning("[doctor] sync-registry --fix timed out") return [CheckResult("owner fix", GLYPH_WARN, "fix timed out", "")] if proc.returncode == 0: diff --git a/src/aipass/devpulse/README.md b/src/aipass/devpulse/README.md index 2c4d2db3..44723600 100644 --- a/src/aipass/devpulse/README.md +++ b/src/aipass/devpulse/README.md @@ -44,7 +44,7 @@ src/aipass/devpulse/ │ │ └── watchdog/ # Agent, timer, schedule, registry │ └── plugins/ # Plugin extension point ├── devpulse_json/ # JSON handler storage (config, data, logs per module) -├── tests/ # 407 tests +├── tests/ # 309 tests ├── artifacts/ # Birth certificate, reports ├── dropbox/ # Received files, archived plans, install audit ├── docs/ # Transition notes diff --git a/src/aipass/devpulse/tests/test_watchdog_agent.py b/src/aipass/devpulse/tests/test_watchdog_agent.py index cb6b7294..8a51b48a 100644 --- a/src/aipass/devpulse/tests/test_watchdog_agent.py +++ b/src/aipass/devpulse/tests/test_watchdog_agent.py @@ -56,6 +56,29 @@ def _write_lock(branch_path: Path, pid: int) -> Path: return lock_file +def _agent_only_sleep(side_effect): + """Wrap a sleep side-effect so ONLY calls from the agent module fire it. + + Patching agent_handler.time.sleep mutates the GLOBAL time module — any + daemon thread sleeping during the patch window (prax logger spawns three on + first log) would run the side effect concurrently with the main thread, + e.g. re-truncating the bounce file mid-read in _classify_exit. Foreign + callers get a real 1ms sleep instead. Same guard as _fake_clock_sleep. + """ + agent_file = Path(agent_handler.__file__).resolve() + real_sleep = time.sleep + + def fake_sleep(_seconds): + caller = Path(sys._getframe(1).f_code.co_filename).resolve() + if caller != agent_file: + real_sleep(0.001) + return + side_effect() + real_sleep(0.01) + + return fake_sleep + + # ───────────────────────────────────────────────────────────────────────────── # Unit tests — return shape per branch # ───────────────────────────────────────────────────────────────────────────── @@ -91,17 +114,11 @@ def test_watch_agent_completed_via_lock_removal(monkeypatch, tmp_path): lock_file = _write_lock(branch_path, pid=os.getpid()) monkeypatch.setattr(agent_handler, "_find_repo_root", lambda *a, **kw: tmp_path) - call_count = {"n": 0} - real_sleep = time.sleep - - def fake_sleep(seconds): - """Remove the lock on the second poll cycle to simulate clean exit.""" - call_count["n"] += 1 - if call_count["n"] >= 1: - lock_file.unlink(missing_ok=True) - real_sleep(0.01) - - monkeypatch.setattr(agent_handler.time, "sleep", fake_sleep) + monkeypatch.setattr( + agent_handler.time, + "sleep", + _agent_only_sleep(lambda: lock_file.unlink(missing_ok=True)), + ) result = agent_handler.watch_agent("@fakebranch", timeout_seconds=5, poll_interval=0.01) @@ -121,17 +138,11 @@ def test_watch_agent_completed_replied_via_sent_folder(monkeypatch, tmp_path): sent_msg = {"to": "@devpulse", "from": "@fakebranch", "subject": "Done", "timestamp": "2026-04-14 00:01:00"} (sent_dir / "reply.json").write_text(json.dumps(sent_msg), encoding="utf-8") - call_count = {"n": 0} - real_sleep = time.sleep - - def fake_sleep(seconds): - """Remove lock on first poll to simulate clean exit.""" - call_count["n"] += 1 - if call_count["n"] >= 1: - lock_file.unlink(missing_ok=True) - real_sleep(0.01) - - monkeypatch.setattr(agent_handler.time, "sleep", fake_sleep) + monkeypatch.setattr( + agent_handler.time, + "sleep", + _agent_only_sleep(lambda: lock_file.unlink(missing_ok=True)), + ) result = agent_handler.watch_agent("@fakebranch", timeout_seconds=5, poll_interval=0.01) @@ -147,15 +158,12 @@ def test_watch_agent_crashed_via_bounce_file(monkeypatch, tmp_path): bounce_file = branch_path / ".ai_mail.local" / "last_bounce.json" monkeypatch.setattr(agent_handler, "_find_repo_root", lambda *a, **kw: tmp_path) - real_sleep = time.sleep - - def fake_sleep(seconds): + def crash_exit(): """Drop a bounce file then remove the lock to simulate crash exit.""" bounce_file.write_text(json.dumps({"exit_code": 1, "reason": "test"}), encoding="utf-8") lock_file.unlink(missing_ok=True) - real_sleep(0.01) - monkeypatch.setattr(agent_handler.time, "sleep", fake_sleep) + monkeypatch.setattr(agent_handler.time, "sleep", _agent_only_sleep(crash_exit)) result = agent_handler.watch_agent("@fakebranch", timeout_seconds=5, poll_interval=0.01)