Files
microdao-daarion/tests/test_stepan_v42_vision_bridge.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

288 lines
13 KiB
Python
Raw 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_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"] == ""