import os import pytest from app.modules.rag.intent_router_v2 import GigaChatIntentRouterFactory from app.modules.shared.env_loader import load_workspace_env from tests.rag.asserts_intent_router import ( assert_domains, assert_file_only_scope, assert_intent, assert_test_policy, ) from tests.rag.intent_router_testkit import run_sequence pytestmark = pytest.mark.intent_router def _live_gigachat_enabled() -> bool: load_workspace_env() return os.getenv("RUN_INTENT_ROUTER_V2_LIVE", "").strip() == "1" and bool(os.getenv("GIGACHAT_TOKEN", "").strip()) def test_e2e_path_carryover_flow() -> None: first, second, third = run_sequence( [ "Посмотри файл app/core/config.py", "Теперь объясни функцию load_config", "Почему так?", ] ) assert_file_only_scope(first, "app/core/config.py") assert "app/core/config.py" in second.retrieval_spec.filters.path_scope assert "app/core/config.py" in third.retrieval_spec.filters.path_scope second_file_anchors = [anchor.value for anchor in second.query_plan.anchors if anchor.type == "FILE_PATH" and anchor.source == "conversation_state"] assert second_file_anchors == ["app/core/config.py"] assert "app/core/config.py" in second.query_plan.keyword_hints assert "app/core" not in second.query_plan.keyword_hints assert any(anchor.type == "FILE_PATH" and anchor.source == "conversation_state" and anchor.span is None for anchor in third.query_plan.anchors) carried_symbols = [anchor.value for anchor in third.query_plan.anchors if anchor.type == "SYMBOL" and anchor.source == "conversation_state"] assert carried_symbols in ([], ["load_config"]) assert third.query_plan.sub_intent == "EXPLAIN_LOCAL" layer_ids = [item.layer_id for item in third.retrieval_spec.layer_queries] assert "C3_ENTRYPOINTS" not in layer_ids def test_e2e_docs_switch_from_code_topic() -> None: first, second = run_sequence( [ "Объясни как работает ConfigManager", "А что про это сказано в документации?", ] ) assert_intent(first, "CODE_QA") assert_intent(second, "DOCS_QA") assert second.conversation_mode == "SWITCH" assert_domains(second, ["DOCS"]) carried = [ anchor for anchor in second.query_plan.anchors if anchor.type == "SYMBOL" and anchor.value == "ConfigManager" and anchor.source == "conversation_state" ] assert carried assert carried[0].span is None assert "ConfigManager" in second.query_plan.expansions assert "ConfigManager" in second.query_plan.keyword_hints def test_e2e_tests_toggle_flow() -> None: first, second = run_sequence( [ "Покажи тесты для ConfigManager", "А теперь не про тесты, а про прод код", ] ) assert_intent(first, "CODE_QA") assert_intent(second, "CODE_QA") assert_test_policy(first, "INCLUDE") assert_test_policy(second, "EXCLUDE") assert first.query_plan.sub_intent == "FIND_TESTS" assert second.query_plan.sub_intent == "EXPLAIN" assert "tests" in second.query_plan.negations assert not second.query_plan.expansions assert second.evidence_policy.require_flow is False def test_e2e_open_file_then_generic_next_steps_is_lightweight() -> None: first, second = run_sequence( [ "Открой файл app/core/config.py", "Что дальше?", ] ) assert_file_only_scope(first, "app/core/config.py") assert_file_only_scope(second, "app/core/config.py") assert second.query_plan.sub_intent in {"EXPLAIN_LOCAL", "NEXT_STEPS"} layer_ids = [item.layer_id for item in second.retrieval_spec.layer_queries] assert "C3_ENTRYPOINTS" not in layer_ids assert second.evidence_policy.require_flow is False assert "app/core/config.py" in second.query_plan.keyword_hints @pytest.mark.skipif( not _live_gigachat_enabled(), reason="requires RUN_INTENT_ROUTER_V2_LIVE=1 and GIGACHAT_TOKEN in environment or .env", ) def test_intent_router_live_smoke_path_carryover() -> None: router = GigaChatIntentRouterFactory().build() first, second = run_sequence( [ "Открой файл app/core/config.py", "Что дальше?", ], router=router, trace_label="intent-router-live", ) assert_file_only_scope(first, "app/core/config.py") assert "app/core/config.py" in second.retrieval_spec.filters.path_scope assert second.query_plan.sub_intent in {"EXPLAIN_LOCAL", "NEXT_STEPS"} layer_ids = [item.layer_id for item in second.retrieval_spec.layer_queries] assert "C3_ENTRYPOINTS" not in layer_ids assert second.evidence_policy.require_flow is False