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.

This commit is contained in:
AIOSAI
2026-07-11 18:46:36 -07:00
parent 766d697e08
commit 90048069d0
3 changed files with 38 additions and 28 deletions
+2
View File
@@ -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:
+1 -1
View File
@@ -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
@@ -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)