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

212 lines
8.9 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.
"""
Тести для Human Light Reply v2:
- greeting без crew
- "дякую" → коротка відповідь (< 40 символів)
- greeting + last_topic → відповідь містить topic
- ack/thanks → без питань
- seeded randomness → стабільна для одного user_id
- short_followup → без action verbs
"""
import sys
from pathlib import Path
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.light_reply import (
build_light_reply,
classify_light_event,
_is_short_followup,
_seeded_rng,
_topic_label,
)
# ─── classify_light_event ────────────────────────────────────────────────────
def test_greeting_classified():
assert classify_light_event("привіт", None) == "greeting"
assert classify_light_event("Привіт!", None) == "greeting"
assert classify_light_event("добрий ранок", None) == "greeting"
assert classify_light_event("hello", None) == "greeting"
assert classify_light_event("вітаю", None) == "greeting"
def test_thanks_classified():
assert classify_light_event("дякую", None) == "thanks"
assert classify_light_event("Дякую!", None) == "thanks"
assert classify_light_event("спасибі", None) == "thanks"
assert classify_light_event("велике дякую", None) == "thanks"
def test_ack_classified():
assert classify_light_event("ок", None) == "ack"
assert classify_light_event("Ок.", None) == "ack"
assert classify_light_event("зрозумів", None) == "ack"
assert classify_light_event("добре", None) == "ack"
assert classify_light_event("чудово", None) == "ack"
assert classify_light_event("так", None) == "ack"
def test_short_followup_classified():
assert classify_light_event("а на завтра?", "plan_day") == "short_followup"
assert classify_light_event("а по полю 12?", "plan_day") == "short_followup"
def test_action_verb_not_followup():
# "зроби план" has action verb → not short_followup
assert classify_light_event("зроби план на завтра", "plan_day") != "short_followup"
def test_short_followup_no_topic():
# No last_topic → not short_followup
result = classify_light_event("а на завтра?", None)
assert result != "short_followup"
def test_deep_input_not_light_event():
# Long operational query → None
result = classify_light_event("сплануй тиждень по полях 1, 2, 3 для пшениці", "plan_day")
assert result is None
# ─── build_light_reply ───────────────────────────────────────────────────────
def test_greeting_no_crew_path():
"""build_light_reply повертає рядок без запуску будь-яких crew."""
profile = {"user_id": "u1", "name": None, "last_topic": None}
reply = build_light_reply("привіт", profile)
assert reply is not None
assert isinstance(reply, str)
assert len(reply) > 0
def test_greeting_with_name():
profile = {"user_id": "u1", "name": "Іван", "last_topic": None}
reply = build_light_reply("привіт", profile)
assert reply is not None
assert "Іван" in reply
def test_greeting_with_last_topic_contains_topic():
"""Якщо є last_topic — відповідь на привітання містить назву теми (case-insensitive)."""
profile = {"user_id": "u2", "name": None, "last_topic": "plan_day"}
reply = build_light_reply("привіт", profile)
assert reply is not None
label = _topic_label("plan_day")
assert label.lower() in reply.lower(), f"Expected '{label}' (case-insensitive) in reply: {reply!r}"
def test_thanks_short():
"""Відповідь на 'дякую' коротша за 40 символів."""
profile = {"user_id": "u3", "name": None, "last_topic": None}
reply = build_light_reply("дякую", profile)
assert reply is not None
assert len(reply) < 40, f"Reply too long: {reply!r}"
def test_thanks_no_question_mark():
"""Відповідь на 'дякую' не містить питання."""
profile = {"user_id": "u3", "name": None, "last_topic": None}
reply = build_light_reply("дякую", profile)
assert reply is not None
assert "?" not in reply, f"Should not ask a question: {reply!r}"
def test_ack_short():
profile = {"user_id": "u4", "name": None, "last_topic": None}
reply = build_light_reply("зрозумів", profile)
assert reply is not None
assert len(reply) < 40, f"Ack reply too long: {reply!r}"
def test_ack_no_question_mark():
profile = {"user_id": "u4", "name": None, "last_topic": None}
reply = build_light_reply("ок", profile)
assert reply is not None
assert "?" not in reply, f"Ack should not ask question: {reply!r}"
def test_short_followup_contains_topic():
profile = {"user_id": "u5", "name": None, "last_topic": "plan_vs_fact"}
reply = build_light_reply("а на завтра?", profile)
assert reply is not None
label = _topic_label("plan_vs_fact")
assert label in reply, f"Expected topic in followup reply: {reply!r}"
def test_offtopic_returns_none_or_str():
"""Для нечіткого запиту або не-light-event build_light_reply повертає None."""
profile = {"user_id": "u6", "name": None, "last_topic": None}
# "що робити з трактором" — не greeting/thanks/ack/followup
reply = build_light_reply("що робити з трактором взагалі", profile)
# Should be None (falls back to LLM) since no clear light category
assert reply is None
def test_seeded_stable_same_user():
"""Одному user_id — завжди та ж відповідь (стабільний seed)."""
profile = {"user_id": "stable_user_123", "name": None, "last_topic": None}
r1 = build_light_reply("привіт", profile)
r2 = build_light_reply("привіт", profile)
assert r1 == r2, f"Should be deterministic: {r1!r} != {r2!r}"
def test_seeded_different_users():
"""Різні user_id можуть давати різні відповіді (не обов'язково, але перевіряємо що обидва рядки)."""
p1 = {"user_id": "user_aaa", "name": None, "last_topic": None}
p2 = {"user_id": "user_zzz", "name": None, "last_topic": None}
r1 = build_light_reply("привіт", p1)
r2 = build_light_reply("привіт", p2)
# Both must be non-None strings
assert r1 is not None and r2 is not None
assert isinstance(r1, str) and isinstance(r2, str)
def test_no_chekim_dopomohty():
"""Жоден варіант не містить 'чим можу допомогти'."""
profile = {"user_id": "u7", "name": None, "last_topic": None}
for greeting in ["привіт", "добрий ранок", "hello"]:
reply = build_light_reply(greeting, profile)
if reply:
assert "чим можу допомогти" not in reply.lower(), f"Forbidden phrase in: {reply!r}"
assert "чим допомогти" not in reply.lower()
def test_no_farewell_script():
"""Відповідь на 'дякую' або 'ок' не містить шаблонних вступів."""
profile = {"user_id": "u8", "name": None, "last_topic": None}
for text in ["дякую", "спасибі", "ок", "зрозумів"]:
reply = build_light_reply(text, profile)
if reply:
for forbidden in ["звісно", "чудово!", "дозвольте", "я радий"]:
assert forbidden not in reply.lower(), f"Forbidden phrase '{forbidden}' in: {reply!r}"
# ─── _is_short_followup ──────────────────────────────────────────────────────
def test_short_followup_tomorrow():
assert _is_short_followup("а на завтра?", "plan_day") is True
def test_short_followup_with_action_verb_is_false():
assert _is_short_followup("зроби план на завтра", "plan_day") is False
def test_short_followup_no_topic_is_false():
assert _is_short_followup("а на завтра?", None) is False
def test_short_followup_long_text_is_false():
long_text = "а що буде на завтра по всіх полях господарства?"
assert _is_short_followup(long_text, "plan_day") is False
def test_seeded_rng_stable():
rng1 = _seeded_rng("user_abc")
rng2 = _seeded_rng("user_abc")
choices1 = [rng1.randint(0, 100) for _ in range(5)]
choices2 = [rng2.randint(0, 100) for _ in range(5)]
assert choices1 == choices2