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
This commit is contained in:
Apple
2026-03-03 07:14:14 -08:00
parent e9dedffa48
commit 129e4ea1fc
241 changed files with 69349 additions and 0 deletions

View File

@@ -0,0 +1,326 @@
"""
tests/test_stepan_v47_farm_state_bridge.py
Unit tests for v4.7 FarmOS Farm State Bridge in run.py:
- _load_farm_state_snapshot: reads fact from memory-service
- TTL enforcement (24h)
- Fail-closed: HTTP errors, JSON errors, missing fields → None
- PII: no raw chat_id in URL (synthetic uid used)
- Integration guard: injected into task_parts only in non-doc / non-web mode
"""
from __future__ import annotations
import json
import os
import sys
import types
import pytest
from datetime import datetime, timezone, timedelta
from unittest.mock import patch, MagicMock
# ── Paths ─────────────────────────────────────────────────────────────────────
_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, _ROOT):
if _p not in sys.path:
sys.path.insert(0, _p)
# ── Minimal stubs so run.py importable without full stack ──────────────────────
def _stub(name: str) -> types.ModuleType:
m = types.ModuleType(name)
sys.modules.setdefault(name, m)
return sys.modules[name]
# crewai
_crewai = _stub("crewai")
_crewai.Crew = type("Crew", (), {"__init__": lambda *a, **k: None, "kickoff": lambda *a, **k: ""})
_crewai.Task = type("Task", (), {"__init__": lambda *a, **k: None})
# langchain_core
_lcc = _stub("langchain_core")
_lcc_tools = _stub("langchain_core.tools")
_lcc_tools.Tool = type("Tool", (), {"__init__": lambda *a, **k: None})
# agromatrix_tools
_agt = _stub("agromatrix_tools")
_agt.tool_dictionary = _stub("agromatrix_tools.tool_dictionary")
_agt.tool_operation_plan = _stub("agromatrix_tools.tool_operation_plan")
_stub("agromatrix_tools.tool_farmos_read")
# crews modules stubs
for _mod in [
"crews.agromatrix_crew.agents.stepan_orchestrator",
"crews.agromatrix_crew.agents.operations_agent",
"crews.agromatrix_crew.agents.iot_agent",
"crews.agromatrix_crew.agents.platform_agent",
"crews.agromatrix_crew.agents.spreadsheet_agent",
"crews.agromatrix_crew.agents.sustainability_agent",
"crews.agromatrix_crew.audit",
"crews.agromatrix_crew.operator_commands",
"crews.agromatrix_crew.memory_manager",
"crews.agromatrix_crew.style_adapter",
"crews.agromatrix_crew.reflection_engine",
"crews.agromatrix_crew.light_reply",
"crews.agromatrix_crew.depth_classifier",
"crews.agromatrix_crew.telemetry",
"crews.agromatrix_crew.session_context",
"crews.agromatrix_crew.proactivity",
"crews.agromatrix_crew.doc_facts",
"crews.agromatrix_crew.doc_focus",
"crews.agromatrix_crew.farm_state",
]:
m = _stub(_mod)
# Set needed attributes
m.route_operator_command = lambda *a, **k: None
m.route_operator_text = lambda *a, **k: None
m.load_user_profile = lambda *a, **k: {}
m.save_user_profile = lambda *a, **k: None
m.load_farm_profile = lambda *a, **k: {}
m.save_farm_profile = lambda *a, **k: None
m.update_profile_if_needed = lambda *a, **k: {}
m.adapt_response_style = lambda *a, **k: ""
m.build_style_prefix = lambda *a, **k: ""
m.reflect_on_response = lambda *a, **k: ""
m.build_light_reply = lambda *a, **k: None
m.classify_light_event = lambda *a, **k: None
m.classify_depth = lambda *a, **k: "deep"
m.tlog = lambda *a, **k: None
m.load_session = lambda *a, **k: {}
m.update_session = lambda *a, **k: None
m.is_doc_focus_active = lambda *a, **k: False
m.is_doc_focus_cooldown_active = lambda *a, **k: False
m.DOC_FOCUS_TTL = 600
m.DOC_FOCUS_COOLDOWN_S = 60
m.maybe_add_proactivity = lambda *a, **k: ""
m.extract_doc_facts = lambda *a, **k: {}
m.merge_doc_facts = lambda *a, **k: {}
m.can_answer_from_facts = lambda *a, **k: (False, None)
m.compute_scenario = lambda *a, **k: None
m.format_facts_as_text = lambda *a, **k: ""
m.extract_fact_claims = lambda *a, **k: []
m.build_self_correction = lambda *a, **k: ""
m._is_doc_question = lambda *a, **k: False
m._detect_domain = lambda *a, **k: "general"
m.detect_context_signals = lambda *a, **k: {}
m.build_mode_clarifier = lambda *a, **k: ""
m.detect_farm_state_updates = lambda *a, **k: {}
m.update_farm_state = lambda *a, **k: None
m.build_farm_state_prefix = lambda *a, **k: ""
m.audit_event = lambda *a, **k: None
m.new_trace = lambda *a, **k: {"trace_id": "t1"}
m.build_stepan = lambda *a, **k: MagicMock()
m.build_operations = lambda *a, **k: MagicMock()
m.build_iot = lambda *a, **k: MagicMock()
m.build_platform = lambda *a, **k: MagicMock()
m.build_spreadsheet = lambda *a, **k: MagicMock()
m.build_sustainability = lambda *a, **k: MagicMock()
# stub subprocess, vision_guard
_stub("subprocess")
_vg = _stub("vision_guard")
_vg.get_vision_lock = lambda *a, **k: {}
# Now import the function under test
import importlib
import crews.agromatrix_crew.run as _run_mod
importlib.reload(_run_mod)
from crews.agromatrix_crew.run import _load_farm_state_snapshot, _FARM_STATE_SNAPSHOT_TTL_S
# ── Helpers ───────────────────────────────────────────────────────────────────
def _fresh_ts(age_seconds: float = 0.0) -> str:
ts = datetime.now(timezone.utc) - timedelta(seconds=age_seconds)
return ts.isoformat()
def _make_resp(text: str, age_s: float = 0.0, status: int = 200) -> MagicMock:
# memory-service повертає fact_value_json як рядок (str) або dict залежно від версії
# наш код підтримує обидва варіанти через isinstance(val, str) → json.loads
payload = {
"id": "test-uuid",
"user_id": "farm:test",
"fact_key": "farm_state:agromatrix:chat:test",
"fact_value_json": json.dumps({
"_version": 1,
"source": "farmos",
"generated_at": _fresh_ts(age_s),
"counts": {"asset_land": 2, "asset_plant": 1, "asset_equipment": 0},
"top": {"asset_land": ["Поле A", "Поле B"], "asset_plant": [], "asset_equipment": []},
"text": text,
})
}
r = MagicMock()
r.status_code = status
r.json.return_value = payload
return r
# ── Tests: _load_farm_state_snapshot ─────────────────────────────────────────
class TestLoadFarmStateSnapshot:
def test_returns_text_when_fresh(self):
with patch("httpx.get", return_value=_make_resp("Farm state (FarmOS):\n- Поля: 2")) as mock_get:
result = _load_farm_state_snapshot("chat_123")
assert result == "Farm state (FarmOS):\n- Поля: 2"
def test_uses_synthetic_uid_not_raw_chat_id(self):
"""PII: URL params must use synthetic uid farm:{chat_id}, not raw chat_id directly as user."""
captured = {}
def fake_get(url, params=None, timeout=None):
captured["params"] = params or {}
return _make_resp("text")
with patch("httpx.get", side_effect=fake_get):
_load_farm_state_snapshot("sensitive_42")
uid = captured["params"].get("user_id", "")
assert uid == "farm:sensitive_42", f"Expected synthetic uid, got: {uid!r}"
assert "sensitive_42" not in captured["params"].get("fact_key", "").split(":")[0], \
"chat_id should not appear as user_id directly"
def test_correct_fact_key(self):
captured = {}
def fake_get(url, params=None, timeout=None):
captured["params"] = params or {}
return _make_resp("text")
with patch("httpx.get", side_effect=fake_get):
_load_farm_state_snapshot("chat_999")
assert captured["params"].get("fact_key") == "farm_state:agromatrix:chat:chat_999"
def test_uses_correct_endpoint_path(self):
"""URL повинен бути /facts/{fact_key} а не /facts/get."""
captured = {}
def fake_get(url, params=None, timeout=None):
captured["url"] = url
captured["params"] = params or {}
return _make_resp("text")
with patch("httpx.get", side_effect=fake_get):
_load_farm_state_snapshot("chat_ep_test")
assert "/facts/farm_state:agromatrix:chat:chat_ep_test" in captured["url"]
assert "/facts/get" not in captured["url"]
def test_returns_none_when_non_200(self):
with patch("httpx.get", return_value=_make_resp("text", status=404)):
result = _load_farm_state_snapshot("chat_x")
assert result is None
def test_returns_none_when_snapshot_older_than_24h(self):
with patch("httpx.get", return_value=_make_resp("text", age_s=86401.0)):
result = _load_farm_state_snapshot("chat_x")
assert result is None
def test_returns_text_when_snapshot_just_within_24h(self):
with patch("httpx.get", return_value=_make_resp("ok_text", age_s=86399.0)):
result = _load_farm_state_snapshot("chat_x")
assert result == "ok_text"
def test_returns_none_on_connection_error(self):
import httpx as _httpx
with patch("httpx.get", side_effect=_httpx.ConnectError("fail")):
result = _load_farm_state_snapshot("chat_x")
assert result is None
def test_returns_none_on_timeout(self):
import httpx as _httpx
with patch("httpx.get", side_effect=_httpx.ReadTimeout("t/o")):
result = _load_farm_state_snapshot("chat_x")
assert result is None
def test_returns_none_when_fact_value_missing(self):
r = MagicMock()
r.status_code = 200
r.json.return_value = {}
with patch("httpx.get", return_value=r):
result = _load_farm_state_snapshot("chat_x")
assert result is None
def test_returns_none_when_text_empty(self):
payload = {
"fact_value_json": json.dumps({
"source": "farmos",
"generated_at": _fresh_ts(),
"text": "",
})
}
r = MagicMock()
r.status_code = 200
r.json.return_value = payload
with patch("httpx.get", return_value=r):
result = _load_farm_state_snapshot("chat_x")
assert result is None
def test_handles_fact_value_as_plain_json_string(self):
"""fact_value (not fact_value_json) as a JSON string also works."""
payload = {
"fact_value": json.dumps({
"source": "farmos",
"generated_at": _fresh_ts(),
"text": "snapshot from fact_value",
})
}
r = MagicMock()
r.status_code = 200
r.json.return_value = payload
with patch("httpx.get", return_value=r):
result = _load_farm_state_snapshot("chat_x")
assert result == "snapshot from fact_value"
def test_returns_none_when_data_not_dict(self):
"""If fact_value_json parses to a list — return None."""
payload = {"fact_value_json": json.dumps(["bad", "data"])}
r = MagicMock()
r.status_code = 200
r.json.return_value = payload
with patch("httpx.get", return_value=r):
result = _load_farm_state_snapshot("chat_x")
assert result is None
def test_uses_memory_service_url_env(self):
captured = {}
def fake_get(url, params=None, timeout=None):
captured["url"] = url
return _make_resp("text")
with patch.dict(os.environ, {"MEMORY_SERVICE_URL": "http://custom-mem:9999"}):
with patch("httpx.get", side_effect=fake_get):
_load_farm_state_snapshot("chat_x")
assert "custom-mem:9999" in captured.get("url", "")
def test_uses_agx_memory_service_url_env_priority(self):
"""AGX_MEMORY_SERVICE_URL takes priority over MEMORY_SERVICE_URL."""
captured = {}
def fake_get(url, params=None, timeout=None):
captured["url"] = url
return _make_resp("text")
with patch.dict(os.environ, {
"MEMORY_SERVICE_URL": "http://old-mem:8000",
"AGX_MEMORY_SERVICE_URL": "http://new-mem:7777",
}):
with patch("httpx.get", side_effect=fake_get):
_load_farm_state_snapshot("chat_x")
assert "new-mem:7777" in captured.get("url", "")
def test_timeout_is_2s(self):
captured = {}
def fake_get(url, params=None, timeout=None):
captured["timeout"] = timeout
return _make_resp("text")
with patch("httpx.get", side_effect=fake_get):
_load_farm_state_snapshot("chat_x")
assert captured.get("timeout") == 2.0
def test_missing_generated_at_still_returns_text(self):
"""If generated_at is absent, we can't enforce TTL — still return text."""
payload = {
"fact_value_json": json.dumps({
"source": "farmos",
"text": "snapshot without date",
})
}
r = MagicMock()
r.status_code = 200
r.json.return_value = payload
with patch("httpx.get", return_value=r):
result = _load_farm_state_snapshot("chat_x")
assert result == "snapshot without date"
def test_farm_state_snapshot_ttl_constant_is_24h(self):
assert _FARM_STATE_SNAPSHOT_TTL_S == 86400.0