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,287 @@
"""
tests/test_stepan_v42_vision_bridge.py
Unit tests for v4.2 Vision → Agronomy Bridge.
Тестуємо ізольовано: без crewai, без httpx, без memory-service.
Перевіряємо логіку безпосередньо з vision_guard + session_context.
"""
from __future__ import annotations
import time
import sys
import os
import pytest
# ── Налаштовуємо шляхи для локального запуску ────────────────────────────────
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_GATEWAY = os.path.join(_ROOT, "gateway-bot")
_CREWS = os.path.join(_ROOT, "crews")
if _GATEWAY not in sys.path:
sys.path.insert(0, _GATEWAY)
if _CREWS not in sys.path:
sys.path.insert(0, _CREWS)
import vision_guard as vg
# ── Fixtures ──────────────────────────────────────────────────────────────────
@pytest.fixture(autouse=True)
def clear_vg_lock():
vg._VISION_LOCK.clear()
yield
vg._VISION_LOCK.clear()
def _make_session() -> dict:
"""Мінімальна сесія для тестів (без load_session)."""
return {
"doc_focus": False,
"doc_focus_ts": 0.0,
"doc_focus_cooldown_until": 0.0,
"active_doc_id": None,
"doc_facts": {},
"fact_claims": [],
"last_photo_ts": 0.0,
"farm_state": {},
"vision_last_label": "",
"updated_at": time.time(),
}
# ── Helper: симулює логіку v4.2 з run.py ─────────────────────────────────────
def _run_vision_bridge(
session: dict,
agent_id: str,
chat_id: str,
text: str = "",
) -> dict:
"""
Відтворює блок v4.2 з run.py (без crewai/LLM).
Повертає оновлену session.
"""
# Читаємо vision lock (аналог _vb_get_vision_lock)
try:
lock = vg.get_vision_lock(agent_id, chat_id)
if lock:
label = (lock.get("user_label") or lock.get("label") or "").strip()
if label:
session["vision_last_label"] = label
except Exception:
pass
# User text override
if text:
try:
text_label = vg.detect_user_override(text)
if text_label:
session["vision_last_label"] = text_label
except Exception:
pass
return session
def _should_inject_prefix(session: dict, context_mode: str, domain: str) -> bool:
"""Відтворює умову для vision bridge prefix injection."""
if context_mode == "doc":
return False
if domain == "web":
return False
label = (session.get("vision_last_label") or "").strip()
farm_crop = str((session.get("farm_state") or {}).get("current_crop", "")).strip()
return bool(label and label != farm_crop)
# ─── Тест 1: vision label з'являється у session після lock ───────────────────
class TestVisionLabelInSession:
def test_label_loaded_from_lock(self):
"""Якщо vision lock є — vision_last_label заповнюється."""
vg.set_vision_lock("agromatrix", "chat1", "fid", "соняшник", "high",
file_unique_id="uid1")
session = _make_session()
session = _run_vision_bridge(session, "agromatrix", "chat1")
assert session["vision_last_label"] == "соняшник"
def test_user_label_preferred_over_model_label(self):
"""user_label має пріоритет над model label."""
vg.set_vision_lock("agromatrix", "chat1", "fid", "пшениця", "high",
file_unique_id="uid1")
vg.set_user_label("agromatrix", "chat1", "Соняшник")
session = _make_session()
session = _run_vision_bridge(session, "agromatrix", "chat1")
assert session["vision_last_label"] == "соняшник"
def test_no_label_if_no_lock(self):
"""Без lock — vision_last_label порожній."""
session = _make_session()
session = _run_vision_bridge(session, "agromatrix", "chat_new")
assert session["vision_last_label"] == ""
def test_expired_lock_no_label(self):
"""Протухлий lock — не вставляємо label."""
vg.set_vision_lock("agromatrix", "chatX", "fid", "кукурудза", "high")
vg._VISION_LOCK["agromatrix:chatX"]["ts"] = time.time() - vg.VISION_LOCK_TTL - 1
session = _make_session()
session = _run_vision_bridge(session, "agromatrix", "chatX")
assert session["vision_last_label"] == ""
# ─── Тест 2: prefix injection тільки не в doc/web режимі ─────────────────────
class TestPrefixInjection:
def test_injected_in_general_mode(self):
vg.set_vision_lock("agromatrix", "chat1", "fid", "соняшник", "high")
session = _make_session()
session = _run_vision_bridge(session, "agromatrix", "chat1")
assert _should_inject_prefix(session, "general", "general") is True
def test_not_injected_in_doc_mode(self):
"""Rule: doc mode → NЕ вставляємо vision prefix."""
vg.set_vision_lock("agromatrix", "chat1", "fid", "кукурудза", "high")
session = _make_session()
session = _run_vision_bridge(session, "agromatrix", "chat1")
assert _should_inject_prefix(session, "doc", "general") is False
def test_not_injected_in_web_mode(self):
"""Rule: web mode → НЕ вставляємо vision prefix."""
vg.set_vision_lock("agromatrix", "chat1", "fid", "пшениця", "high")
session = _make_session()
session = _run_vision_bridge(session, "agromatrix", "chat1")
assert _should_inject_prefix(session, "general", "web") is False
def test_not_injected_if_empty_label(self):
"""Без label — не вставляємо."""
session = _make_session()
assert _should_inject_prefix(session, "general", "general") is False
def test_not_injected_if_same_as_farm_crop(self):
"""Якщо farm_state.current_crop === vision_last_label — не дублюємо."""
vg.set_vision_lock("agromatrix", "chat1", "fid", "кукурудза", "high")
session = _make_session()
session = _run_vision_bridge(session, "agromatrix", "chat1")
session["farm_state"] = {"current_crop": "кукурудза"}
assert _should_inject_prefix(session, "general", "general") is False
# ─── Тест 3: user text override ──────────────────────────────────────────────
class TestTextOverride:
def test_text_override_changes_label(self):
"""Юзер явно пише "це соняшник" → перезаписує label."""
vg.set_vision_lock("agromatrix", "chat1", "fid", "пшениця", "high")
session = _make_session()
session["vision_last_label"] = "пшениця"
# Симулюємо нову відправку з override text
session = _run_vision_bridge(session, "agromatrix", "chat1", text="це соняшник")
assert session["vision_last_label"] == "соняшник"
def test_negation_does_not_override(self):
"""Негація "це не соняшник" → не перезаписує."""
vg.set_vision_lock("agromatrix", "chat1", "fid", "пшениця", "high")
session = _make_session()
session["vision_last_label"] = "пшениця"
session = _run_vision_bridge(session, "agromatrix", "chat1", text="це не соняшник")
assert session["vision_last_label"] == "пшениця"
def test_irrelevant_text_no_change(self):
"""Звичайний текст → label не змінюється."""
vg.set_vision_lock("agromatrix", "chat1", "fid", "ріпак", "high")
session = _make_session()
session["vision_last_label"] = "ріпак"
session = _run_vision_bridge(session, "agromatrix", "chat1",
text="чи варто внести азот зараз?")
assert session["vision_last_label"] == "ріпак"
def test_text_override_sets_label_without_lock(self):
"""User пише "соняшник" навіть без попереднього lock."""
session = _make_session()
session = _run_vision_bridge(session, "agromatrix", "chat_no_lock",
text="це кукурудза")
assert session["vision_last_label"] == "кукурудза"
# ─── Тест 4: TTL — label зникає після 30 хвилин ──────────────────────────────
class TestTTL:
def test_label_absent_after_ttl(self):
"""Після TTL lock→ порожній → label не завантажується."""
vg.set_vision_lock("agromatrix", "chatTTL", "fid", "ячмінь", "high")
# Симулюємо протухання
vg._VISION_LOCK["agromatrix:chatTTL"]["ts"] = time.time() - vg.VISION_LOCK_TTL - 1
session = _make_session()
session = _run_vision_bridge(session, "agromatrix", "chatTTL")
assert session["vision_last_label"] == ""
def test_label_present_within_ttl(self):
"""Якщо lock свіжий — label є."""
vg.set_vision_lock("agromatrix", "chatTTL2", "fid", "горох", "high")
session = _make_session()
session = _run_vision_bridge(session, "agromatrix", "chatTTL2")
assert session["vision_last_label"] == "горох"
# ─── Тест 5: ізоляція між чатами ─────────────────────────────────────────────
class TestChatIsolation:
def test_different_chats_isolated(self):
"""Два різні чати — окремі label."""
vg.set_vision_lock("agromatrix", "chatA", "f1", "соняшник", "high")
vg.set_vision_lock("agromatrix", "chatB", "f2", "пшениця", "high")
sA = _make_session()
sA = _run_vision_bridge(sA, "agromatrix", "chatA")
sB = _make_session()
sB = _run_vision_bridge(sB, "agromatrix", "chatB")
assert sA["vision_last_label"] == "соняшник"
assert sB["vision_last_label"] == "пшениця"
def test_forward_different_chat_no_bleed(self):
"""Forward фото в інший чат — чужий lock не потрапляє."""
vg.set_vision_lock("agromatrix", "chatSource", "f1", "ріпак", "high",
file_unique_id="uid_shared")
# В іншому чаті немає lock навіть з тим самим photo
sB = _make_session()
sB = _run_vision_bridge(sB, "agromatrix", "chatDest")
assert sB["vision_last_label"] == ""
# ─── Тест 6: Smoke cycle ─────────────────────────────────────────────────────
class TestSmokeCycle:
def test_full_vision_agronomy_flow(self):
"""
1. Фото → lock_set (соняшник)
2. Степан отримує label у session
3. Питання без фото → prefix інжектується
4. Юзер каже "тепер це кукурудза" → label змінюється
5. TTL минає → label пропадає
"""
# 1. Vision response saved
vg.set_vision_lock("agromatrix", "chat_cycle", "fid",
"соняшник", "high", file_unique_id="uid_cycle")
# 2. run.py bridge
session = _make_session()
session = _run_vision_bridge(session, "agromatrix", "chat_cycle")
assert session["vision_last_label"] == "соняшник"
# 3. Inject в general mode
assert _should_inject_prefix(session, "general", "general") is True
assert _should_inject_prefix(session, "doc", "general") is False
# 4. Text override
session = _run_vision_bridge(session, "agromatrix", "chat_cycle",
text="тепер це кукурудза")
assert session["vision_last_label"] == "кукурудза"
# 5. TTL
vg._VISION_LOCK["agromatrix:chat_cycle"]["ts"] = (
time.time() - vg.VISION_LOCK_TTL - 1
)
session2 = _make_session()
session2 = _run_vision_bridge(session2, "agromatrix", "chat_cycle")
assert session2["vision_last_label"] == ""