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
212 lines
8.9 KiB
Python
212 lines
8.9 KiB
Python
"""
|
||
Тести для 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
|