Files
microdao-daarion/tests/test_stepan_v43_farmos.py
Apple 129e4ea1fc feat(platform): add new services, tools, tests and crews modules
New router intelligence modules (26 files): alert_ingest/store, audit_store,
architecture_pressure, backlog_generator/store, cost_analyzer, data_governance,
dependency_scanner, drift_analyzer, incident_* (5 files), llm_enrichment,
platform_priority_digest, provider_budget, release_check_runner, risk_* (6 files),
signature_state_store, sofiia_auto_router, tool_governance

New services:
- sofiia-console: Dockerfile, adapters/, monitor/nodes/ops/voice modules, launchd, react static
- memory-service: integration_endpoints, integrations, voice_endpoints, static UI
- aurora-service: full app suite (analysis, job_store, orchestrator, reporting, schemas, subagents)
- sofiia-supervisor: new supervisor service
- aistalk-bridge-lite: Telegram bridge lite
- calendar-service: CalDAV calendar service with reminders
- mlx-stt-service / mlx-tts-service: Apple Silicon speech services
- binance-bot-monitor: market monitor service
- node-worker: STT/TTS memory providers

New tools (9): agent_email, browser_tool, contract_tool, observability_tool,
oncall_tool, pr_reviewer_tool, repo_tool, safe_code_executor, secure_vault

New crews: agromatrix_crew (10 modules: depth_classifier, doc_facts, doc_focus,
farm_state, light_reply, llm_factory, memory_manager, proactivity, reflection_engine,
session_context, style_adapter, telemetry)

Tests: 85+ test files for all new modules
Made-with: Cursor
2026-03-03 07:14:14 -08:00

245 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
tests/test_stepan_v43_farmos.py
Unit tests for v4.3 FarmOS minimal tool pipeline.
Scope:
- agromatrix_tools.tool_farmos_read._farmos_ping_impl (fail-closed logic)
- crews.agromatrix_crew.operator_commands.handle_farmos_status
- crews.agromatrix_crew.operator_commands.parse_operator_command (farmos registerd)
Без залежностей від crewai, httpx, memory-service.
Тільки monkeypatch env + mock requests.
"""
from __future__ import annotations
import os
import sys
import types
import pytest
from unittest.mock import patch, MagicMock
# ── Шляхи ────────────────────────────────────────────────────────────────────
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_PKG = os.path.join(_ROOT, "packages", "agromatrix-tools")
_CREWS = os.path.join(_ROOT, "crews")
for _p in (_PKG, _CREWS):
if _p not in sys.path:
sys.path.insert(0, _p)
# ── Мок недоступних модулів до будь-якого імпорту agromatrix_tools ────────────
# nats, crewai — потрібні тільки на сервері, тут мокуємо.
def _stub_module(name: str) -> types.ModuleType:
m = types.ModuleType(name)
sys.modules[name] = m
return m
for _mod in (
"nats", "nats.aio", "nats.aio.client",
"crewai", "crewai.tools",
):
if _mod not in sys.modules:
_stub_module(_mod)
# crewai.tools.tool — повертає identity decorator
sys.modules["crewai.tools"].tool = lambda name: (lambda f: f) # type: ignore[attr-defined]
# nats.aio.client.Client stub
sys.modules["nats.aio.client"].Client = MagicMock # type: ignore[attr-defined]
# audit_tool_call stub (використовує nats)
import importlib, agromatrix_tools # noqa: E402
# Форсуємо stub для audit всередині пакету
_audit_stub = types.ModuleType("agromatrix_tools.audit")
_audit_stub.audit_tool_call = lambda *a, **kw: None # type: ignore[attr-defined]
sys.modules["agromatrix_tools.audit"] = _audit_stub
# Тепер безпечно імпортуємо
import importlib as _il # noqa: F811
if "agromatrix_tools.tool_farmos_read" in sys.modules:
_il.reload(sys.modules["agromatrix_tools.tool_farmos_read"])
from agromatrix_tools.tool_farmos_read import _farmos_ping_impl # noqa: E402
from crews.agromatrix_crew.operator_commands import ( # noqa: E402
handle_farmos_status,
parse_operator_command,
OPERATOR_COMMANDS,
)
# ── Fixtures ──────────────────────────────────────────────────────────────────
@pytest.fixture(autouse=True)
def clear_farmos_env(monkeypatch):
"""Прибираємо farmos env перед кожним тестом."""
for k in ("FARMOS_BASE_URL", "FARMOS_TOKEN", "FARMOS_USER", "FARMOS_PASS", "FARMOS_PASSWORD"):
monkeypatch.delenv(k, raising=False)
yield
# ─── _farmos_ping_impl ────────────────────────────────────────────────────────
class TestFarmosPingImpl:
def test_no_base_url(self):
result = _farmos_ping_impl()
assert "FARMOS_BASE_URL" in result
assert "відсутній" in result.lower() or "не налаштований" in result.lower()
def test_base_url_no_auth(self, monkeypatch):
monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
result = _farmos_ping_impl()
assert "токен" in result.lower() or "логін" in result.lower() or "auth" in result.lower()
def test_base_url_with_token_ok(self, monkeypatch):
monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
monkeypatch.setenv("FARMOS_TOKEN", "testtoken123")
mock_resp = MagicMock()
mock_resp.status_code = 200
with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
result = _farmos_ping_impl()
assert "доступний" in result.lower()
assert "FarmOS доступний." == result
def test_base_url_with_token_401(self, monkeypatch):
monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
monkeypatch.setenv("FARMOS_TOKEN", "badtoken")
mock_resp = MagicMock()
mock_resp.status_code = 401
with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
result = _farmos_ping_impl()
assert "401" in result
assert "авторизац" in result.lower()
def test_base_url_with_token_403(self, monkeypatch):
monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
monkeypatch.setenv("FARMOS_TOKEN", "limitedtoken")
mock_resp = MagicMock()
mock_resp.status_code = 403
with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
result = _farmos_ping_impl()
assert "403" in result
assert "авторизац" in result.lower()
def test_base_url_with_token_503(self, monkeypatch):
monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
monkeypatch.setenv("FARMOS_TOKEN", "tok")
mock_resp = MagicMock()
mock_resp.status_code = 503
with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
result = _farmos_ping_impl()
assert "503" in result
def test_timeout(self, monkeypatch):
monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
monkeypatch.setenv("FARMOS_TOKEN", "tok")
import requests as _r
with patch("agromatrix_tools.tool_farmos_read.requests.get",
side_effect=_r.exceptions.Timeout()):
result = _farmos_ping_impl()
assert "timeout" in result.lower()
def test_connection_error(self, monkeypatch):
monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
monkeypatch.setenv("FARMOS_TOKEN", "tok")
import requests as _r
with patch("agromatrix_tools.tool_farmos_read.requests.get",
side_effect=_r.exceptions.ConnectionError("refused")):
result = _farmos_ping_impl()
assert "недоступний" in result.lower()
def test_user_pass_auth_ok(self, monkeypatch):
monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
monkeypatch.setenv("FARMOS_USER", "admin")
monkeypatch.setenv("FARMOS_PASS", "secret")
mock_resp = MagicMock()
mock_resp.status_code = 200
with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
result = _farmos_ping_impl()
assert "доступний" in result.lower()
def test_never_reveals_url(self, monkeypatch):
"""Відповідь не містить BASE_URL або токен."""
monkeypatch.setenv("FARMOS_BASE_URL", "http://secret-farmos.internal")
monkeypatch.setenv("FARMOS_TOKEN", "supersecrettoken")
mock_resp = MagicMock()
mock_resp.status_code = 200
with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
result = _farmos_ping_impl()
assert "secret-farmos" not in result
assert "supersecrettoken" not in result
def test_never_raises(self, monkeypatch):
"""При будь-якій помилці — повертає рядок, не кидає."""
monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
monkeypatch.setenv("FARMOS_TOKEN", "tok")
with patch("agromatrix_tools.tool_farmos_read.requests.get",
side_effect=RuntimeError("unexpected")):
result = _farmos_ping_impl()
assert isinstance(result, str)
assert len(result) > 0
# ─── handle_farmos_status ─────────────────────────────────────────────────────
class TestHandleFarmosStatus:
def test_no_args_returns_status(self):
result = handle_farmos_status([])
assert isinstance(result, dict)
assert "summary" in result
assert "FARMOS_BASE_URL" in result["summary"] or "FarmOS" in result["summary"]
def test_status_arg_same_as_no_args(self):
r1 = handle_farmos_status([])
r2 = handle_farmos_status(["status"])
assert r1["summary"] == r2["summary"]
def test_unknown_subcommand(self):
result = handle_farmos_status(["foo"])
assert "підтримується" in result["summary"].lower() or "farmos" in result["summary"].lower()
def test_returns_dict_with_required_keys(self):
result = handle_farmos_status([])
for key in ("status", "summary", "artifacts", "tool_calls", "next_actions"):
assert key in result
def test_with_ok_farmos(self, monkeypatch):
monkeypatch.setenv("FARMOS_BASE_URL", "http://farmos.example.com")
monkeypatch.setenv("FARMOS_TOKEN", "tok")
mock_resp = MagicMock()
mock_resp.status_code = 200
with patch("agromatrix_tools.tool_farmos_read.requests.get", return_value=mock_resp):
result = handle_farmos_status([])
assert "доступний" in result["summary"].lower()
def test_internal_error_fallback(self, monkeypatch):
"""handle_farmos_status не падає при внутрішній помилці importside."""
# Патчимо саме там де lazy import шукає
with patch("agromatrix_tools.tool_farmos_read._farmos_ping_impl",
side_effect=RuntimeError("crash")):
result = handle_farmos_status([])
assert "помилка" in result["summary"].lower() or "недоступний" in result["summary"].lower()
# ─── OPERATOR_COMMANDS registry ───────────────────────────────────────────────
class TestFarmosCommandRegistry:
def test_farmos_in_operator_commands(self):
assert "farmos" in OPERATOR_COMMANDS
def test_parse_farmos_no_args(self):
parsed = parse_operator_command("/farmos")
assert parsed is not None
assert parsed["cmd"] == "farmos"
assert parsed["args"] == []
def test_parse_farmos_status(self):
parsed = parse_operator_command("/farmos status")
assert parsed is not None
assert parsed["cmd"] == "farmos"
assert "status" in parsed["args"]
def test_parse_farmos_foo(self):
parsed = parse_operator_command("/farmos foo")
assert parsed is not None
assert parsed["cmd"] == "farmos"
assert "foo" in parsed["args"]