feat(cron): add deploy sync guard to catch stale code before cascading failures
Some checks failed
Forge CI / smoke-and-build (pull_request) Failing after 26s
Some checks failed
Forge CI / smoke-and-build (pull_request) Failing after 26s
When the installed run_agent.py diverges from what scheduler.py expects, every cron job fails with TypeError on AIAgent.__init__() — a silent total outage that cascades into gateway restarts, asyncio shutdown errors, and auth token expiry. This commit adds a _validate_agent_interface() guard that: - Inspects AIAgent.__init__ at runtime via inspect.signature - Verifies every kwarg the scheduler passes exists in the constructor - Fails fast with a clear remediation message on mismatch - Runs once per gateway process (cached, zero per-job overhead) The guard is called at the top of run_job() before any work begins. It would have caught the tool_choice TypeError that caused 1,199 failures across 55 jobs (meta-issue #343). Includes 3 tests: pass, fail, and cache verification.
This commit is contained in:
@@ -863,3 +863,54 @@ class TestTickAdvanceBeforeRun:
|
||||
adv_mock.assert_called_once_with("test-advance")
|
||||
# advance must happen before run
|
||||
assert call_order == [("advance", "test-advance"), ("run", "test-advance")]
|
||||
|
||||
|
||||
class TestDeploySyncGuard:
|
||||
"""Tests for _validate_agent_interface() — the deploy sync guard."""
|
||||
|
||||
def test_passes_when_all_params_present(self):
|
||||
"""Validation passes when AIAgent accepts every scheduler kwarg."""
|
||||
from cron.scheduler import _validate_agent_interface, _agent_interface_validated
|
||||
import cron.scheduler as sched_mod
|
||||
|
||||
# Reset the cached flag so the check actually runs.
|
||||
sched_mod._agent_interface_validated = False
|
||||
# Should not raise.
|
||||
_validate_agent_interface()
|
||||
assert sched_mod._agent_interface_validated is True
|
||||
|
||||
def test_fails_when_param_missing(self):
|
||||
"""Validation raises RuntimeError when AIAgent is missing a required param."""
|
||||
import cron.scheduler as sched_mod
|
||||
from unittest.mock import MagicMock
|
||||
import inspect
|
||||
|
||||
# Save and restore.
|
||||
orig_flag = sched_mod._agent_interface_validated
|
||||
try:
|
||||
sched_mod._agent_interface_validated = False
|
||||
|
||||
# Build a fake AIAgent class whose __init__ lacks 'tool_choice'.
|
||||
class FakeAIAgent:
|
||||
def __init__(self, model="", max_iterations=90, quiet_mode=False,
|
||||
disabled_toolsets=None, skip_memory=False, platform=None,
|
||||
session_id=None, session_db=None):
|
||||
pass
|
||||
|
||||
fake_module = MagicMock()
|
||||
fake_module.AIAgent = FakeAIAgent
|
||||
|
||||
with pytest.raises(RuntimeError, match="Missing parameters: tool_choice"):
|
||||
with patch.dict("sys.modules", {"run_agent": fake_module}):
|
||||
sched_mod._validate_agent_interface()
|
||||
finally:
|
||||
sched_mod._agent_interface_validated = orig_flag
|
||||
|
||||
def test_cached_after_first_run(self):
|
||||
"""Second call is a no-op (uses cached flag)."""
|
||||
import cron.scheduler as sched_mod
|
||||
|
||||
sched_mod._agent_interface_validated = True
|
||||
# Should not raise even if we somehow break AIAgent — the flag is set.
|
||||
sched_mod._validate_agent_interface()
|
||||
# No exception = pass.
|
||||
|
||||
Reference in New Issue
Block a user