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

360 lines
15 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 for Doc Handoff (PROMPT 28, v3.3) — chat-scoped doc_context.
Перевіряємо:
1. save_chat_doc_context / get_chat_doc_context logic (unit, з mock memory_client)
2. doc_context fallback у run.py (симуляція через session_context)
3. Інтеграційний сценарій: upload doc → питання (інший session) → Stepan бачить doc_id
"""
import sys
import os
import json
import time
import asyncio
from unittest.mock import AsyncMock, MagicMock, patch
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "gateway-bot"))
# ── helpers ──────────────────────────────────────────────────────────────────
DOC_A = {
"doc_id": "parsed_doc_abc123",
"file_unique_id": "tg_unique_xyz",
"file_name": "Звіт кукурудза.xlsx",
"extracted_summary": "Площа: 497 га. Прибуток: 5 972 016 грн. Добрива: 1 521 084 грн.",
"source": "telegram",
}
def run_async(coro):
"""Запустити корутину синхронно для тестів (Python 3.10+ safe)."""
return asyncio.run(coro)
# ── Unit tests: save_chat_doc_context ────────────────────────────────────────
def test_save_chat_doc_context_calls_upsert():
"""save_chat_doc_context має викликати memory_client.upsert_fact з правильним ключем."""
mock_client = AsyncMock()
mock_client.upsert_fact = AsyncMock(return_value=True)
with patch("services.doc_service.memory_client", mock_client):
from services.doc_service import save_chat_doc_context
result = run_async(save_chat_doc_context("chat_001", "agromatrix", DOC_A))
assert result is True
mock_client.upsert_fact.assert_called_once()
call_kwargs = mock_client.upsert_fact.call_args.kwargs
assert call_kwargs["user_id"] == "chat:agromatrix:chat_001"
assert call_kwargs["fact_key"] == "doc_context_chat:agromatrix:chat_001"
saved = call_kwargs["fact_value_json"]
assert saved["doc_id"] == "parsed_doc_abc123"
assert saved["file_name"] == "Звіт кукурудза.xlsx"
def test_save_chat_doc_context_truncates_summary():
"""extracted_summary обрізається до 4000 символів."""
mock_client = AsyncMock()
mock_client.upsert_fact = AsyncMock(return_value=True)
big_doc = dict(DOC_A)
big_doc["extracted_summary"] = "x" * 9000
with patch("services.doc_service.memory_client", mock_client):
from services.doc_service import save_chat_doc_context
run_async(save_chat_doc_context("chat_002", "agromatrix", big_doc))
saved = mock_client.upsert_fact.call_args.kwargs["fact_value_json"]
assert len(saved["extracted_summary"]) == 4000
def test_save_chat_doc_context_fail_safe():
"""При помилці memory_client — повертаємо False без exception."""
mock_client = AsyncMock()
mock_client.upsert_fact = AsyncMock(side_effect=RuntimeError("memory down"))
with patch("services.doc_service.memory_client", mock_client):
from services.doc_service import save_chat_doc_context
result = run_async(save_chat_doc_context("chat_003", "agromatrix", DOC_A))
assert result is False
# ── Unit tests: get_chat_doc_context ─────────────────────────────────────────
def test_get_chat_doc_context_returns_dict():
"""get_chat_doc_context повертає dict якщо fact є."""
mock_client = AsyncMock()
mock_client.get_fact = AsyncMock(return_value={
"fact_value_json": DOC_A,
})
with patch("services.doc_service.memory_client", mock_client):
from services.doc_service import get_chat_doc_context
result = run_async(get_chat_doc_context("chat_010", "agromatrix"))
assert result is not None
assert result["doc_id"] == "parsed_doc_abc123"
assert result["file_name"] == "Звіт кукурудза.xlsx"
call_kwargs = mock_client.get_fact.call_args.kwargs
assert call_kwargs["user_id"] == "chat:agromatrix:chat_010"
assert call_kwargs["fact_key"] == "doc_context_chat:agromatrix:chat_010"
def test_get_chat_doc_context_json_string_deserialized():
"""fact_value_json може прийти як JSON-рядок — deserialize правильно."""
mock_client = AsyncMock()
mock_client.get_fact = AsyncMock(return_value={
"fact_value_json": json.dumps(DOC_A),
})
with patch("services.doc_service.memory_client", mock_client):
from services.doc_service import get_chat_doc_context
result = run_async(get_chat_doc_context("chat_011", "agromatrix"))
assert result is not None
assert result["doc_id"] == "parsed_doc_abc123"
def test_get_chat_doc_context_returns_none_when_missing():
"""Якщо fact немає — повертаємо None."""
mock_client = AsyncMock()
mock_client.get_fact = AsyncMock(return_value=None)
with patch("services.doc_service.memory_client", mock_client):
from services.doc_service import get_chat_doc_context
result = run_async(get_chat_doc_context("chat_012", "agromatrix"))
assert result is None
def test_get_chat_doc_context_fail_safe():
"""При помилці memory_client — повертаємо None без exception."""
mock_client = AsyncMock()
mock_client.get_fact = AsyncMock(side_effect=RuntimeError("timeout"))
with patch("services.doc_service.memory_client", mock_client):
from services.doc_service import get_chat_doc_context
result = run_async(get_chat_doc_context("chat_013", "agromatrix"))
assert result is None
def test_get_chat_doc_context_returns_none_without_doc_id():
"""Якщо fact є але без doc_id і file_unique_id — повертаємо None."""
mock_client = AsyncMock()
mock_client.get_fact = AsyncMock(return_value={
"fact_value_json": {"file_name": "test.xlsx"}, # немає doc_id
})
with patch("services.doc_service.memory_client", mock_client):
from services.doc_service import get_chat_doc_context
result = run_async(get_chat_doc_context("chat_014", "agromatrix"))
assert result is None
# ── Інтеграційний сценарій ───────────────────────────────────────────────────
async def _scenario_upload_then_question():
stored = {}
async def mock_upsert(user_id, fact_key, fact_value_json, team_id=None, **kwargs):
stored[fact_key] = fact_value_json
return True
async def mock_get(user_id, fact_key, **kwargs):
val = stored.get(fact_key)
return {"fact_value_json": val} if val else None
mock_client = AsyncMock()
mock_client.upsert_fact = AsyncMock(side_effect=mock_upsert)
mock_client.get_fact = AsyncMock(side_effect=mock_get)
with patch("services.doc_service.memory_client", mock_client):
from services.doc_service import save_chat_doc_context, get_chat_doc_context
await save_chat_doc_context("chat_999", "agromatrix", DOC_A)
result = await get_chat_doc_context("chat_999", "agromatrix")
return result
def test_upload_then_question_different_session():
"""
Сценарій: upload doc (session_1) → питання (session_2) → Stepan бачить doc_id.
Симулюємо через save_chat_doc_context / get_chat_doc_context:
- chat_id незмінний, але session_id різний (як у Telegram)
"""
result = asyncio.run(_scenario_upload_then_question())
assert result is not None, "Stepan має бачити doc_id після upload"
assert result["doc_id"] == "parsed_doc_abc123"
assert "Звіт кукурудза" in result["file_name"]
assert "прибуток" in result.get("extracted_summary", "").lower()
async def _scenario_agent_isolation():
stored = {}
async def mock_upsert(user_id, fact_key, fact_value_json, team_id=None, **kwargs):
stored[fact_key] = fact_value_json
return True
async def mock_get(user_id, fact_key, **kwargs):
val = stored.get(fact_key)
return {"fact_value_json": val} if val else None
mock_client = AsyncMock()
mock_client.upsert_fact = AsyncMock(side_effect=mock_upsert)
mock_client.get_fact = AsyncMock(side_effect=mock_get)
with patch("services.doc_service.memory_client", mock_client):
from services.doc_service import save_chat_doc_context, get_chat_doc_context
await save_chat_doc_context("shared_chat", "agromatrix", DOC_A)
result_helion = await get_chat_doc_context("shared_chat", "helion")
result_agro = await get_chat_doc_context("shared_chat", "agromatrix")
return result_helion, result_agro
def test_chat_scoped_key_isolated_between_agents():
"""doc_context_chat:{agent_id}:{chat_id} — різні агенти мають різні ключі."""
result_helion, result_agro = asyncio.run(_scenario_agent_isolation())
assert result_helion is None, "Helion не має бачити doc agromatrix"
assert result_agro is not None, "Agromatrix має бачити свій doc"
assert result_agro["doc_id"] == "parsed_doc_abc123"
# ── Fix A: dedup tests ───────────────────────────────────────────────────────
async def _scenario_dedup_same_file():
"""Двічі надсилаємо той самий file_unique_id → upsert_fact викликається один раз."""
stored = {}
upsert_calls = []
async def mock_upsert(user_id, fact_key, fact_value_json, team_id=None, **kwargs):
upsert_calls.append(fact_key)
stored[fact_key] = fact_value_json
return True
async def mock_get(user_id, fact_key, **kwargs):
val = stored.get(fact_key)
return {"fact_value_json": val} if val else None
mock_client = AsyncMock()
mock_client.upsert_fact = AsyncMock(side_effect=mock_upsert)
mock_client.get_fact = AsyncMock(side_effect=mock_get)
with patch("services.doc_service.memory_client", mock_client):
from services.doc_service import save_chat_doc_context
# Перший раз — зберегти
await save_chat_doc_context("chat_dup", "agromatrix", DOC_A)
# Другий раз той самий file_unique_id → no-op
await save_chat_doc_context("chat_dup", "agromatrix", DOC_A)
return upsert_calls
def test_dedup_same_file_unique_id_no_extra_write():
"""Якщо file_unique_id не змінився — upsert_fact викликається лише один раз."""
calls = asyncio.run(_scenario_dedup_same_file())
assert len(calls) == 1, f"Expected 1 upsert call, got {len(calls)}: {calls}"
async def _scenario_dedup_different_file():
"""Різний file_unique_id → upsert_fact викликається двічі."""
stored = {}
upsert_calls = []
async def mock_upsert(user_id, fact_key, fact_value_json, team_id=None, **kwargs):
upsert_calls.append(fact_key)
stored[fact_key] = fact_value_json
return True
async def mock_get(user_id, fact_key, **kwargs):
val = stored.get(fact_key)
return {"fact_value_json": val} if val else None
mock_client = AsyncMock()
mock_client.upsert_fact = AsyncMock(side_effect=mock_upsert)
mock_client.get_fact = AsyncMock(side_effect=mock_get)
doc_b = dict(DOC_A)
doc_b["file_unique_id"] = "tg_unique_NEW"
doc_b["doc_id"] = "parsed_doc_def456"
with patch("services.doc_service.memory_client", mock_client):
from services.doc_service import save_chat_doc_context
await save_chat_doc_context("chat_dup2", "agromatrix", DOC_A)
await save_chat_doc_context("chat_dup2", "agromatrix", doc_b)
return upsert_calls
def test_dedup_different_file_unique_id_writes_both():
"""Якщо file_unique_id змінився — upsert_fact викликається двічі."""
calls = asyncio.run(_scenario_dedup_different_file())
assert len(calls) == 2, f"Expected 2 upsert calls, got {len(calls)}: {calls}"
# ── Fix B: sanitize tests ────────────────────────────────────────────────────
def test_sanitize_summary_removes_rag_prefix():
"""_sanitize_summary видаляє [RAG відповідь по документу]: префікс."""
from services.doc_service import _sanitize_summary
raw = "[RAG відповідь по документу]: Прибуток 5 972 016 грн."
result = _sanitize_summary(raw)
assert "[RAG" not in result
assert "Прибуток 5 972 016 грн." in result
def test_sanitize_summary_removes_rag_prefix_variants():
"""_sanitize_summary видаляє різні варіанти [RAG...]: префіксів."""
from services.doc_service import _sanitize_summary
variants = [
"[RAG answer]: some text",
"[RAG Query Result]: some text",
"[rag відповідь]: some text",
]
for v in variants:
result = _sanitize_summary(v)
assert not result.startswith("[RAG"), f"Not sanitized: {result!r}"
assert "some text" in result
def test_sanitize_summary_removes_trace_id():
"""_sanitize_summary видаляє trace_id=... артефакти."""
from services.doc_service import _sanitize_summary
raw = "Прибуток 5 млн. trace_id=97182015-0c71-4b93-a829-ef5421dea914 Добрива 1.5 млн."
result = _sanitize_summary(raw)
assert "trace_id=" not in result
assert "Прибуток 5 млн." in result
assert "Добрива 1.5 млн." in result
def test_sanitize_summary_empty_string():
"""_sanitize_summary не ламається на порожньому рядку."""
from services.doc_service import _sanitize_summary
assert _sanitize_summary("") == ""
assert _sanitize_summary(None) is None
def test_save_chat_doc_context_sanitizes_summary():
"""save_chat_doc_context зберігає вже sanitized extracted_summary."""
mock_client = AsyncMock()
mock_client.upsert_fact = AsyncMock(return_value=True)
mock_client.get_fact = AsyncMock(return_value=None) # no existing → no dedup skip
dirty_doc = dict(DOC_A)
dirty_doc["extracted_summary"] = "[RAG відповідь]: Прибуток 5 млн. trace_id=abc-123 Добрива 1 млн."
with patch("services.doc_service.memory_client", mock_client):
from services.doc_service import save_chat_doc_context
run_async(save_chat_doc_context("chat_sanit", "agromatrix", dirty_doc))
saved = mock_client.upsert_fact.call_args.kwargs["fact_value_json"]
summary = saved.get("extracted_summary", "")
assert "[RAG" not in summary, f"RAG prefix leaked: {summary!r}"
assert "trace_id=" not in summary, f"trace_id leaked: {summary!r}"
assert "Прибуток 5 млн." in summary