Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: XNLLLLH <XNLLLLH@users.noreply.github.com>
207 lines
7.6 KiB
Python
207 lines
7.6 KiB
Python
"""Phase 07.6 W1 / tests for the startup grace before the first
|
|
`_s4_offline_loop` iteration.
|
|
|
|
Defends against the regression where a freshly-spawned daemon immediately
|
|
runs the heavy S4 viability scan (sigma.compute_and_emit ->
|
|
retrieve.build_runtime_graph -> runtime_graph_cache.save -> json.dumps),
|
|
materialising a multi-GB intermediate Python string (CONTEXT.md D-01:
|
|
py-spy 2026-04-29 PID 7959 RSS 7.6GB).
|
|
|
|
Project async-test idiom (mandatory): sync `def test_X(...)` body wraps
|
|
`asyncio.run(_async_body(...))`. The project does NOT depend on
|
|
`pytest-asyncio`; `@pytest.mark.asyncio` markers silently pass without
|
|
running. See tests/test_cpu_watchdog.py:12, tests/test_cascade_no_block.py:11
|
|
for the canonical pattern. The plan template prescribed pytest-asyncio
|
|
markers; this file deviates (Rule 1 — fake-GREEN avoidance) per project
|
|
precedent.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import time
|
|
from types import SimpleNamespace
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _fake_store():
|
|
"""_s4_offline_loop only forwards `store` to s4.run_offline_pass and
|
|
write_event; both are stubbed in these tests, so a SimpleNamespace
|
|
placeholder is enough — never touches LanceDB.
|
|
"""
|
|
return SimpleNamespace()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test 1: grace=0 fast-path — first iter runs within ≤100ms
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_grace_zero_runs_first_iter_within_100ms(monkeypatch):
|
|
"""D-06 (a): grace=0 => stubbed run_offline_pass invoked within ≤100ms."""
|
|
asyncio.run(_grace_zero_fast_path_body(monkeypatch))
|
|
|
|
|
|
async def _grace_zero_fast_path_body(monkeypatch):
|
|
import iai_mcp.daemon as daemon_mod
|
|
|
|
monkeypatch.setattr(daemon_mod, "S4_FIRST_ITER_GRACE_SEC", 0.0)
|
|
called = asyncio.Event()
|
|
call_count = {"n": 0}
|
|
|
|
def _stub_run_offline_pass(_store):
|
|
call_count["n"] += 1
|
|
called.set()
|
|
|
|
monkeypatch.setattr(daemon_mod.s4, "run_offline_pass", _stub_run_offline_pass)
|
|
shutdown = asyncio.Event()
|
|
store = _fake_store()
|
|
t0 = time.monotonic()
|
|
task = asyncio.create_task(daemon_mod._s4_offline_loop(store, shutdown))
|
|
try:
|
|
await asyncio.wait_for(called.wait(), timeout=0.1)
|
|
elapsed = time.monotonic() - t0
|
|
assert elapsed <= 0.15, (
|
|
f"first run_offline_pass took {elapsed*1000:.1f}ms; expected <=100ms "
|
|
f"(plus ~50ms slack for to_thread schedule)"
|
|
)
|
|
finally:
|
|
shutdown.set()
|
|
try:
|
|
await asyncio.wait_for(task, timeout=1.0)
|
|
except asyncio.TimeoutError:
|
|
task.cancel()
|
|
try:
|
|
await task
|
|
except (asyncio.CancelledError, Exception):
|
|
pass
|
|
assert call_count["n"] >= 1
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test 2: grace>0 deferred-path — no call before grace, ≥1 call after
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_grace_positive_defers_first_iter(monkeypatch):
|
|
"""D-06 (b): grace=0.5 => no call before 0.4s; ≥1 call after 0.7s."""
|
|
asyncio.run(_grace_positive_deferred_body(monkeypatch))
|
|
|
|
|
|
async def _grace_positive_deferred_body(monkeypatch):
|
|
import iai_mcp.daemon as daemon_mod
|
|
|
|
monkeypatch.setattr(daemon_mod, "S4_FIRST_ITER_GRACE_SEC", 0.5)
|
|
call_count = {"n": 0}
|
|
|
|
def _stub_run_offline_pass(_store):
|
|
call_count["n"] += 1
|
|
|
|
monkeypatch.setattr(daemon_mod.s4, "run_offline_pass", _stub_run_offline_pass)
|
|
shutdown = asyncio.Event()
|
|
store = _fake_store()
|
|
task = asyncio.create_task(daemon_mod._s4_offline_loop(store, shutdown))
|
|
try:
|
|
await asyncio.sleep(0.4)
|
|
assert call_count["n"] == 0, (
|
|
f"S4 ran before 0.5s grace elapsed: call_count={call_count['n']}"
|
|
)
|
|
# Total ~0.7s — past 0.5s grace + to_thread schedule slack.
|
|
await asyncio.sleep(0.3)
|
|
assert call_count["n"] >= 1, (
|
|
f"S4 did not run after grace elapsed: call_count={call_count['n']}"
|
|
)
|
|
finally:
|
|
shutdown.set()
|
|
try:
|
|
await asyncio.wait_for(task, timeout=1.0)
|
|
except asyncio.TimeoutError:
|
|
task.cancel()
|
|
try:
|
|
await task
|
|
except (asyncio.CancelledError, Exception):
|
|
pass
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test 3: shutdown during grace — clean return, no run, no exception
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_shutdown_during_grace_returns_cleanly(monkeypatch):
|
|
"""shutdown set during grace => loop returns cleanly, 0 calls."""
|
|
asyncio.run(_shutdown_during_grace_body(monkeypatch))
|
|
|
|
|
|
async def _shutdown_during_grace_body(monkeypatch):
|
|
import iai_mcp.daemon as daemon_mod
|
|
|
|
monkeypatch.setattr(daemon_mod, "S4_FIRST_ITER_GRACE_SEC", 5.0)
|
|
call_count = {"n": 0}
|
|
|
|
def _stub_run_offline_pass(_store):
|
|
call_count["n"] += 1
|
|
|
|
monkeypatch.setattr(daemon_mod.s4, "run_offline_pass", _stub_run_offline_pass)
|
|
shutdown = asyncio.Event()
|
|
store = _fake_store()
|
|
task = asyncio.create_task(daemon_mod._s4_offline_loop(store, shutdown))
|
|
await asyncio.sleep(0.05)
|
|
shutdown.set()
|
|
# raises if loop did not return cleanly within 1s.
|
|
await asyncio.wait_for(task, timeout=1.0)
|
|
assert call_count["n"] == 0, (
|
|
f"S4 ran despite shutdown during grace: call_count={call_count['n']}"
|
|
)
|
|
assert task.done(), "loop task did not finish"
|
|
assert task.exception() is None, (
|
|
f"loop raised during shutdown-in-grace: {task.exception()!r}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test 4: existing s4_offline_pass_error event-emit preserved
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_run_offline_pass_error_still_emits_event(monkeypatch):
|
|
"""Existing layered-defense preserved: run_offline_pass raises => write_event
|
|
called with kind='s4_offline_pass_error' + severity='warning'.
|
|
"""
|
|
asyncio.run(_error_event_preserved_body(monkeypatch))
|
|
|
|
|
|
async def _error_event_preserved_body(monkeypatch):
|
|
import iai_mcp.daemon as daemon_mod
|
|
|
|
monkeypatch.setattr(daemon_mod, "S4_FIRST_ITER_GRACE_SEC", 0.0)
|
|
events: list[tuple[str, dict, str]] = []
|
|
|
|
def _stub_run_offline_pass(_store):
|
|
raise RuntimeError("boom")
|
|
|
|
def _stub_write_event(_store, kind, payload, severity="info", **_kwargs):
|
|
events.append((kind, dict(payload) if isinstance(payload, dict) else payload, severity))
|
|
|
|
monkeypatch.setattr(daemon_mod.s4, "run_offline_pass", _stub_run_offline_pass)
|
|
monkeypatch.setattr(daemon_mod, "write_event", _stub_write_event)
|
|
shutdown = asyncio.Event()
|
|
store = _fake_store()
|
|
task = asyncio.create_task(daemon_mod._s4_offline_loop(store, shutdown))
|
|
# Give the loop time to: enter while-body, hit run_offline_pass raise,
|
|
# emit s4_offline_pass_error, then await the inter-iteration wait_for.
|
|
await asyncio.sleep(0.1)
|
|
shutdown.set()
|
|
try:
|
|
await asyncio.wait_for(task, timeout=1.0)
|
|
except asyncio.TimeoutError:
|
|
task.cancel()
|
|
try:
|
|
await task
|
|
except (asyncio.CancelledError, Exception):
|
|
pass
|
|
matching = [
|
|
e for e in events
|
|
if e[0] == "s4_offline_pass_error"
|
|
and e[2] == "warning"
|
|
and "boom" in str(e[1])
|
|
]
|
|
assert matching, f"expected s4_offline_pass_error event with severity=warning + 'boom' payload, got: {events}"
|