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

354 lines
14 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 для Humanized Stepan v2.8 — Multi-user Farm Model.
Покриває:
1. FarmProfile keyed by chat_id (новий ключ farm_profile:agromatrix:chat:{chat_id})
2. Lazy migration: legacy key → chat-key (write-through)
3. Два users одного chat_id ділять один FarmProfile
4. Conflict detection: наступний user з відмінним legacy → лише лог, не перезаписує
5. Acceptance сценарій: userA оновлює farm, userB бачить ті ж дані
6. backward-compat: load_farm_profile без user_id → повертає default (не крашить)
"""
import sys
from pathlib import Path
from unittest.mock import patch, MagicMock
from copy import deepcopy
root = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(root))
sys.path.insert(0, str(root / 'packages' / 'agromatrix-tools'))
from crews.agromatrix_crew.memory_manager import (
_default_farm_profile,
_chat_fact_key,
_legacy_farm_fact_key,
_farm_profiles_differ,
migrate_farm_profile_legacy_to_chat,
load_farm_profile,
save_farm_profile,
_cache_set,
_cache_get,
)
from crews.agromatrix_crew.telemetry import TELEMETRY_TAG
# ─── Helpers ──────────────────────────────────────────────────────────────────
def _make_farm(chat_id: str, crops: list | None = None) -> dict:
p = _default_farm_profile(chat_id)
if crops:
p["crops"] = crops
return p
# ─── Unit: key helpers ────────────────────────────────────────────────────────
def test_chat_fact_key_format():
key = _chat_fact_key("123456")
assert key == "farm_profile:agromatrix:chat:123456"
def test_legacy_fact_key_format():
key = _legacy_farm_fact_key("user_42")
assert key == "farm_profile:agromatrix:user_42"
def test_chat_key_differs_from_legacy():
assert _chat_fact_key("999") != _legacy_farm_fact_key("999")
# ─── Unit: _farm_profiles_differ ─────────────────────────────────────────────
def test_farm_profiles_identical_not_differ():
a = _make_farm("chat1", crops=["пшениця"])
b = _make_farm("chat1", crops=["пшениця"])
assert _farm_profiles_differ(a, b) is False
def test_farm_profiles_different_crops():
a = _make_farm("chat1", crops=["пшениця"])
b = _make_farm("chat1", crops=["кукурудза"])
assert _farm_profiles_differ(a, b) is True
def test_farm_profiles_different_region():
a = _make_farm("chat1")
b = _make_farm("chat1")
a["region"] = "Полтавська"
b["region"] = "Харківська"
assert _farm_profiles_differ(a, b) is True
def test_farm_profiles_metadata_ignored():
"""Відмінності у updated_at та _version не впливають на результат."""
a = _make_farm("chat1", crops=["ріпак"])
b = deepcopy(a)
b["updated_at"] = "2026-01-01T00:00:00"
b["_version"] = 99
assert _farm_profiles_differ(a, b) is False
# ─── Unit: default farm profile ───────────────────────────────────────────────
def test_default_farm_version_5():
p = _default_farm_profile("chat_x")
assert p["_version"] == 5
def test_default_farm_has_new_fields():
p = _default_farm_profile("chat_x")
for field in ("farm_name", "field_ids", "crop_ids", "active_integrations",
"iot_sensors", "alert_thresholds", "seasonal_context"):
assert field in p, f"Missing field: {field}"
def test_default_farm_chat_id():
p = _default_farm_profile("chat_99")
assert p["chat_id"] == "chat_99"
# ─── Unit: load_farm_profile — no memory-service (cache only) ─────────────────
def test_load_farm_profile_from_cache():
"""Якщо в cache є profile під chat-key — повертаємо його."""
chat_id = "cache_test_chat"
stored = _make_farm(chat_id, crops=["соняшник"])
_cache_set(_chat_fact_key(chat_id), stored)
# patch _http_get_fact to ensure it's not called
with patch("crews.agromatrix_crew.memory_manager._http_get_fact") as mock_get:
result = load_farm_profile(chat_id, user_id="any_user")
mock_get.assert_not_called()
assert result["crops"] == ["соняшник"]
def test_load_farm_profile_falls_back_to_default():
"""При відсутності і в cache і в memory-service — default."""
chat_id = "nodata_chat_999"
with patch("crews.agromatrix_crew.memory_manager._http_get_fact", return_value=None):
result = load_farm_profile(chat_id, user_id=None)
assert result["_version"] == 5
assert result["chat_id"] == chat_id
def test_load_farm_profile_no_user_id_no_crash():
"""load_farm_profile без user_id не падає."""
chat_id = "chat_noid_888"
with patch("crews.agromatrix_crew.memory_manager._http_get_fact", return_value=None):
result = load_farm_profile(chat_id) # user_id=None за замовчуванням
assert result is not None
assert result["chat_id"] == chat_id
# ─── Unit: lazy migration (legacy → chat key) ────────────────────────────────
def test_load_farm_profile_migrates_legacy():
"""
load_farm_profile без chat-profile але з legacy → мігрує в chat-key.
"""
chat_id = "migration_chat_1"
user_id = "legacy_user_1"
legacy_profile = _make_farm(user_id, crops=["ячмінь"])
def mock_http_get(uid, key):
if key == _chat_fact_key(chat_id):
return None # нового ключа нема
if key == _legacy_farm_fact_key(user_id):
return legacy_profile
return None
with patch("crews.agromatrix_crew.memory_manager._http_get_fact", side_effect=mock_http_get), \
patch("crews.agromatrix_crew.memory_manager._http_upsert_fact", return_value=True):
result = load_farm_profile(chat_id, user_id=user_id)
assert result["crops"] == ["ячмінь"]
assert result.get("chat_id") == chat_id
assert "_migrated_from" in result
assert user_id in result["_migrated_from"]
def test_load_farm_profile_migration_sets_chat_id():
"""Після міграції chat_id у профілі відповідає chat_id, а не user_id."""
chat_id = "migration_chat_2"
user_id = "legacy_user_2"
legacy_profile = _make_farm(user_id)
def mock_http_get(uid, key):
return legacy_profile if key == _legacy_farm_fact_key(user_id) else None
with patch("crews.agromatrix_crew.memory_manager._http_get_fact", side_effect=mock_http_get), \
patch("crews.agromatrix_crew.memory_manager._http_upsert_fact", return_value=True):
result = load_farm_profile(chat_id, user_id=user_id)
assert result["chat_id"] == chat_id
# ─── Unit: migrate_farm_profile_legacy_to_chat ───────────────────────────────
def test_migrate_no_conflict_copies_profile():
"""Якщо chat-profile не існує → migration записує legacy."""
chat_id = "mig_chat_3"
user_id = "mig_user_3"
legacy = _make_farm(user_id, crops=["соя"])
with patch("crews.agromatrix_crew.memory_manager._http_get_fact", return_value=None), \
patch("crews.agromatrix_crew.memory_manager._http_upsert_fact", return_value=True):
result = migrate_farm_profile_legacy_to_chat(chat_id, user_id, legacy)
assert result["crops"] == ["соя"]
assert result["chat_id"] == chat_id
def test_migrate_conflict_keeps_existing():
"""
Якщо chat-profile вже існує і відрізняється від legacy → не перезаписуємо,
повертаємо існуючий.
"""
chat_id = "mig_chat_4"
user_id = "mig_user_4"
existing = _make_farm(chat_id, crops=["пшениця"])
legacy = _make_farm(user_id, crops=["кукурудза"]) # відрізняється
with patch("crews.agromatrix_crew.memory_manager._http_get_fact", return_value=existing), \
patch("crews.agromatrix_crew.memory_manager._http_upsert_fact") as mock_upsert:
result = migrate_farm_profile_legacy_to_chat(chat_id, user_id, legacy)
# Existing profile returned unchanged
assert result["crops"] == ["пшениця"]
# upsert NOT called (conflict — no overwrite)
mock_upsert.assert_not_called()
def test_migrate_conflict_logs_telemetry(caplog):
"""Conflict → tlog виводить farm_profile_conflict."""
import logging
chat_id = "mig_chat_5"
user_id = "mig_user_5"
existing = _make_farm(chat_id, crops=["пшениця"])
legacy = _make_farm(user_id, crops=["кукурудза"])
with patch("crews.agromatrix_crew.memory_manager._http_get_fact", return_value=existing), \
patch("crews.agromatrix_crew.memory_manager._http_upsert_fact", return_value=False), \
caplog.at_level(logging.INFO, logger="crews.agromatrix_crew.memory_manager"):
migrate_farm_profile_legacy_to_chat(chat_id, user_id, legacy)
tagged = [r.getMessage() for r in caplog.records if TELEMETRY_TAG in r.getMessage()]
assert any("farm_profile_conflict" in m for m in tagged), \
f"Expected farm_profile_conflict telemetry, got: {tagged}"
def test_migrate_no_conflict_logs_migrated(caplog):
"""Successful migration → tlog виводить farm_profile_migrated."""
import logging
chat_id = "mig_chat_6"
user_id = "mig_user_6"
legacy = _make_farm(user_id, crops=["ріпак"])
with patch("crews.agromatrix_crew.memory_manager._http_get_fact", return_value=None), \
patch("crews.agromatrix_crew.memory_manager._http_upsert_fact", return_value=True), \
caplog.at_level(logging.INFO, logger="crews.agromatrix_crew.memory_manager"):
migrate_farm_profile_legacy_to_chat(chat_id, user_id, legacy)
tagged = [r.getMessage() for r in caplog.records if TELEMETRY_TAG in r.getMessage()]
assert any("farm_profile_migrated" in m for m in tagged), \
f"Expected farm_profile_migrated telemetry, got: {tagged}"
# ─── Acceptance: two users share farm profile ────────────────────────────────
def test_two_users_same_chat_share_farm_profile():
"""
userA і userB в одному chatX → ділять один FarmProfile.
Перевірка: обидва load_farm_profile повертають одні й ті ж дані.
"""
chat_id = "shared_chat_XY"
user_a = "user_a_1"
user_b = "user_b_2"
shared_farm = _make_farm(chat_id, crops=["пшениця", "соняшник"])
# chat-key вже заповнений (userA зберіг)
_cache_set(_chat_fact_key(chat_id), shared_farm)
# userA завантажує
with patch("crews.agromatrix_crew.memory_manager._http_get_fact") as mock_get:
farm_a = load_farm_profile(chat_id, user_id=user_a)
mock_get.assert_not_called() # cache hit
# userB завантажує
with patch("crews.agromatrix_crew.memory_manager._http_get_fact") as mock_get:
farm_b = load_farm_profile(chat_id, user_id=user_b)
mock_get.assert_not_called() # cache hit
assert farm_a["crops"] == farm_b["crops"]
assert farm_a["chat_id"] == farm_b["chat_id"] == chat_id
def test_user_profile_per_user_independent():
"""UserProfile залишається per-user, не shared."""
from crews.agromatrix_crew.memory_manager import _default_user_profile
p_a = _default_user_profile("user_a_ind")
p_b = _default_user_profile("user_b_ind")
assert p_a["user_id"] != p_b["user_id"]
def test_farm_profile_update_visible_to_second_user():
"""
Якщо userA записав farm_profile (crops оновились) →
userB при наступному завантаженні бачить оновлені crops.
"""
chat_id = "update_shared_chat"
user_a = "user_upd_a"
user_b = "user_upd_b"
# Початковий стан
initial_farm = _make_farm(chat_id, crops=["пшениця"])
_cache_set(_chat_fact_key(chat_id), initial_farm)
# userA додає crop
updated_farm = deepcopy(initial_farm)
updated_farm["crops"].append("кукурудза")
_cache_set(_chat_fact_key(chat_id), updated_farm)
# userB завантажує
with patch("crews.agromatrix_crew.memory_manager._http_get_fact") as mock_get:
farm_b = load_farm_profile(chat_id, user_id=user_b)
mock_get.assert_not_called()
assert "кукурудза" in farm_b["crops"]
assert "пшениця" in farm_b["crops"]
# ─── backward-compat ──────────────────────────────────────────────────────────
def test_load_farm_backward_compat_no_user_id():
"""Старий виклик load_farm_profile(chat_id) без user_id — не падає."""
with patch("crews.agromatrix_crew.memory_manager._http_get_fact", return_value=None):
result = load_farm_profile("old_call_chat")
assert result is not None
assert result["_version"] == 5
def test_save_farm_uses_chat_key():
"""save_farm_profile зберігає під chat-key, не legacy."""
chat_id = "save_test_chat"
farm = _make_farm(chat_id)
calls: list[tuple] = []
def mock_upsert(uid, key, data):
calls.append((uid, key))
return True
with patch("crews.agromatrix_crew.memory_manager._http_upsert_fact", side_effect=mock_upsert):
save_farm_profile(chat_id, farm)
assert calls, "upsert not called"
used_key = calls[0][1]
assert used_key == _chat_fact_key(chat_id), \
f"Expected chat-key, got: {used_key!r}"
assert "chat:" in used_key
# Legacy key NOT used
assert _legacy_farm_fact_key(chat_id) != used_key