""" Тести для 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