Files
Anton 052591281d Initial clean version - Agent Maxim
- Complete agent system with planning and execution
- Bridge API with FastAPI server
- WhatsApp-style mobile dashboard
- Enhanced web search with query improvement
- Security guards and policy checks
- Comprehensive documentation
2026-04-09 18:07:43 +03:00

135 lines
6.4 KiB
Python

"""Regression tests for Hardening Sprint 2 — bridge layer."""
from __future__ import annotations
import json
import sys
import time
from pathlib import Path
from unittest.mock import patch
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
# ---------------------------------------------------------------------------
# 1. Dynamic Ollama availability — fallback / recovery
# ---------------------------------------------------------------------------
class TestOllamaAvailability:
def setup_method(self):
import controller.decision_model as dm
dm._OLLAMA_CACHE = None # always start clean
def test_unavailable_returns_ollama_unavailable_mode(self):
import controller.decision_model as dm
# Force unavailable via cache
dm._OLLAMA_CACHE = (time.monotonic(), False)
d = dm.decide("inspect project", 0, None, 3)
assert d.mode == "ollama-unavailable", f"Expected ollama-unavailable, got {d.mode}"
assert d.action in ("run_agent", "stop")
def test_available_uses_ollama_path(self):
import controller.decision_model as dm
# Force available via cache — real Ollama call will follow
dm._OLLAMA_CACHE = (time.monotonic(), True)
# Patch _decide_ollama to avoid real HTTP, just confirm it was called
with patch.object(dm, "_decide_ollama", wraps=dm._decide_ollama) as mock_ollama:
# Patch ollama_adapter.execute to return valid JSON without real call
from adapters import ollama_adapter
from adapters.base import AdapterResult
with patch.object(ollama_adapter, "execute", return_value=AdapterResult(
response='{"action":"run_agent","goal":"inspect project","reason":"test"}',
error=None, exit_code=0, mode="real", metadata={},
)):
d = dm.decide("inspect project", 0, None, 3)
mock_ollama.assert_called_once()
assert d.mode == "ollama"
assert d.parsed_success is True
def test_cache_expiry_triggers_recheck(self):
"""After cache TTL, _ollama_available() re-checks; no restart needed."""
import controller.decision_model as dm
# Set cache as unavailable but expired (ts far in the past)
dm._OLLAMA_CACHE = (time.monotonic() - dm._OLLAMA_CACHE_TTL - 1, False)
# Patch the HTTP call to return True (Ollama "came back up")
with patch("urllib.request.urlopen", return_value=None):
result = dm._ollama_available()
assert result is True, "Cache expired → should re-check and see Ollama available"
assert dm._OLLAMA_CACHE[1] is True, "Cache must be updated to True after re-check"
def test_cache_is_reused_within_ttl(self):
"""Within TTL, no HTTP call is made."""
import controller.decision_model as dm
dm._OLLAMA_CACHE = (time.monotonic(), False) # unavailable, fresh cache
with patch("urllib.request.urlopen") as mock_urlopen:
result = dm._ollama_available()
mock_urlopen.assert_not_called()
assert result is False
# ---------------------------------------------------------------------------
# 2. bridge_run_error audit event — emission and order
# ---------------------------------------------------------------------------
class TestBridgeRunErrorAudit:
def test_exception_emits_bridge_run_error(self, tmp_path):
"""Force exception in execute_run → audit must contain bridge_run_error."""
import controller.decision_model as dm
from controller.controller_loop import RUNS_DIR, run_controller
from core.run_manager import RunManager
# Override RUNS_DIR to tmp_path so we don't pollute real runs
with patch("controller.controller_loop.RUNS_DIR", tmp_path), \
patch("controller.decision_model._OLLAMA_CACHE", (time.monotonic(), False)), \
patch.object(RunManager, "execute_run", side_effect=RuntimeError("injected")):
state = run_controller("inspect project", run_id="test-audit-gap")
audit_path = tmp_path / "test-audit-gap" / "controller_audit.jsonl"
assert audit_path.exists()
events = [json.loads(l) for l in audit_path.read_text().splitlines()]
event_types = [e["event_type"] for e in events]
assert "bridge_run_error" in event_types
def test_audit_event_order_on_exception(self, tmp_path):
"""Order guarantee: bridge_run_started → bridge_run_error → bridge_run_finished."""
import controller.decision_model as dm
from controller.controller_loop import run_controller
from core.run_manager import RunManager
with patch("controller.controller_loop.RUNS_DIR", tmp_path), \
patch("controller.decision_model._OLLAMA_CACHE", (time.monotonic(), False)), \
patch.object(RunManager, "execute_run", side_effect=RuntimeError("injected")):
run_controller("inspect project", run_id="test-audit-order")
events = [
json.loads(l)
for l in (tmp_path / "test-audit-order" / "controller_audit.jsonl").read_text().splitlines()
]
types = [e["event_type"] for e in events]
# Find positions of the three events for iteration 0
idx_started = next(i for i, t in enumerate(types) if t == "bridge_run_started")
idx_error = next(i for i, t in enumerate(types) if t == "bridge_run_error")
idx_finished = next(i for i, t in enumerate(types) if t == "bridge_run_finished")
assert idx_started < idx_error < idx_finished, (
f"Expected started < error < finished, got positions {idx_started} {idx_error} {idx_finished}"
)
def test_exception_result_has_failed_status(self, tmp_path):
"""Synthetic result on exception must have status=failed, agent_final_status=failed."""
import controller.decision_model as dm
from controller.controller_loop import run_controller
from core.run_manager import RunManager
with patch("controller.controller_loop.RUNS_DIR", tmp_path), \
patch("controller.decision_model._OLLAMA_CACHE", (time.monotonic(), False)), \
patch.object(RunManager, "execute_run", side_effect=RuntimeError("injected")):
state = run_controller("inspect project", run_id="test-audit-result")
assert len(state.bridge_run_results) > 0
br = state.bridge_run_results[0]
assert br["status"] == "failed"
assert br["agent_final_status"] == "failed"