test: add 7 more test modules — assistant cmd parser, history, dispatch, admin routes, dataset review, maintenance + semantic resolver
This commit is contained in:
367
backend/tests/api/test_assistant_command_parser.py
Normal file
367
backend/tests/api/test_assistant_command_parser.py
Normal file
@@ -0,0 +1,367 @@
|
||||
# #region Test.Assistant.CommandParser [C:2] [TYPE Module] [SEMANTICS test,assistant,command,parser,nlu]
|
||||
# @BRIEF Tests for _command_parser.py — deterministic RU/EN command parser.
|
||||
# @RELATION BINDS_TO -> [AssistantCommandParser]
|
||||
# @TEST_EDGE: help_variants -> All help phrases map to show_capabilities
|
||||
# @TEST_EDGE: status_variants -> Status commands get_task_status with/without task_id
|
||||
# @TEST_EDGE: branch_create -> Branch create with dashboard_id
|
||||
# @TEST_EDGE: commit_variants -> Commit with quoted message
|
||||
# @TEST_EDGE: deploy_variants -> Deploy with env, production detection
|
||||
# @TEST_EDGE: migrate_variants -> Migration with src/tgt/dry_run/replace_db
|
||||
# @TEST_EDGE: backup_variants -> Backup with env
|
||||
# @TEST_EDGE: health_variants -> Health check with env
|
||||
# @TEST_EDGE: validation_variants -> LLM validation with provider
|
||||
# @TEST_EDGE: documentation_variants -> LLM documentation with dataset_id
|
||||
# @TEST_EDGE: fallback -> Unknown commands return clarify intent
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
import pytest
|
||||
|
||||
|
||||
def _make_config_manager(envs=None):
|
||||
cm = MagicMock()
|
||||
envs = envs or []
|
||||
cm.get_environments = MagicMock(return_value=envs)
|
||||
return cm
|
||||
|
||||
|
||||
def _make_env(id_, name_, stage="DEV", is_production=False, is_default=False):
|
||||
env = MagicMock()
|
||||
env.id = id_
|
||||
env.name = name_
|
||||
env.stage = stage
|
||||
env.is_production = is_production
|
||||
env.is_default = is_default
|
||||
return env
|
||||
|
||||
|
||||
class TestParseCommandHelp:
|
||||
"""_parse_command — help/capabilities variants."""
|
||||
|
||||
@pytest.mark.parametrize("msg", [
|
||||
"что ты умеешь",
|
||||
"что умеешь",
|
||||
"что ты можешь",
|
||||
"help",
|
||||
"помощь",
|
||||
"доступные команды",
|
||||
"какие команды",
|
||||
])
|
||||
def test_help_variants(self, msg):
|
||||
from src.api.routes.assistant._command_parser import _parse_command
|
||||
cm = _make_config_manager()
|
||||
result = _parse_command(msg, cm)
|
||||
assert result["domain"] == "assistant"
|
||||
assert result["operation"] == "show_capabilities"
|
||||
assert result["entities"] == {}
|
||||
assert result["confidence"] == 0.98
|
||||
assert result["risk_level"] == "safe"
|
||||
assert result["requires_confirmation"] is False
|
||||
|
||||
|
||||
class TestParseCommandStatus:
|
||||
"""_parse_command — status/task status variants."""
|
||||
|
||||
@pytest.mark.parametrize("msg,expected_conf", [
|
||||
("статус", 0.66),
|
||||
("status", 0.66),
|
||||
("state", 0.66),
|
||||
("проверь задачу", 0.66),
|
||||
("статус task-abc123", 0.92),
|
||||
("status task-abc-def", 0.92),
|
||||
])
|
||||
def test_status_variants(self, msg, expected_conf):
|
||||
from src.api.routes.assistant._command_parser import _parse_command
|
||||
cm = _make_config_manager()
|
||||
result = _parse_command(msg, cm)
|
||||
assert result["domain"] == "status"
|
||||
assert result["operation"] == "get_task_status"
|
||||
assert result["confidence"] == expected_conf
|
||||
assert result["risk_level"] == "safe"
|
||||
|
||||
|
||||
class TestParseCommandBranch:
|
||||
"""_parse_command — branch create variants."""
|
||||
|
||||
@pytest.mark.parametrize("msg,expected_branch,expected_conf", [
|
||||
("создай ветку feature/test", "feature/test", 0.7),
|
||||
("create branch feature/abc", "feature/abc", 0.7),
|
||||
("сделай ветку fix/123 для дашборд 42", "fix/123", 0.95),
|
||||
("create branch main", "main", 0.7),
|
||||
])
|
||||
def test_branch_create(self, msg, expected_branch, expected_conf):
|
||||
from src.api.routes.assistant._command_parser import _parse_command
|
||||
cm = _make_config_manager()
|
||||
result = _parse_command(msg, cm)
|
||||
assert result["domain"] == "git"
|
||||
assert result["operation"] == "create_branch"
|
||||
assert result["risk_level"] == "guarded"
|
||||
assert result["confidence"] == expected_conf
|
||||
|
||||
|
||||
class TestParseCommandCommit:
|
||||
"""_parse_command — commit variants."""
|
||||
|
||||
def test_commit_with_message(self):
|
||||
from src.api.routes.assistant._command_parser import _parse_command
|
||||
cm = _make_config_manager()
|
||||
result = _parse_command('commit "update dashboard filters"', cm)
|
||||
assert result["operation"] == "commit_changes"
|
||||
assert result["entities"]["message"] == "update dashboard filters"
|
||||
|
||||
def test_commit_without_quotes(self):
|
||||
from src.api.routes.assistant._command_parser import _parse_command
|
||||
cm = _make_config_manager()
|
||||
result = _parse_command("commit", cm)
|
||||
assert result["operation"] == "commit_changes"
|
||||
assert result["entities"]["message"] == "assistant: update dashboard changes"
|
||||
|
||||
|
||||
class TestParseCommandDeploy:
|
||||
"""_parse_command — deploy variants with production detection."""
|
||||
|
||||
def test_deploy_to_dev(self):
|
||||
from src.api.routes.assistant._command_parser import _parse_command
|
||||
envs = [_make_env("dev", "DEV", stage="DEV")]
|
||||
cm = _make_config_manager(envs)
|
||||
result = _parse_command("деплой дашборд 42 в dev", cm)
|
||||
assert result["operation"] == "deploy_dashboard"
|
||||
assert result["risk_level"] == "guarded"
|
||||
assert result["requires_confirmation"] is False
|
||||
|
||||
def test_deploy_to_production(self):
|
||||
from src.api.routes.assistant._command_parser import _parse_command
|
||||
prod = _make_env("prod", "PROD", stage="PROD", is_production=True)
|
||||
cm = _make_config_manager([prod])
|
||||
result = _parse_command("deploy dashboard 42 в prod", cm)
|
||||
assert result["operation"] == "deploy_dashboard"
|
||||
assert result["risk_level"] == "dangerous"
|
||||
assert result["requires_confirmation"] is True
|
||||
|
||||
def test_deploy_without_env(self):
|
||||
from src.api.routes.assistant._command_parser import _parse_command
|
||||
cm = _make_config_manager()
|
||||
result = _parse_command("разверн дашборд 42", cm)
|
||||
assert result["operation"] == "deploy_dashboard"
|
||||
assert result["confidence"] == 0.7
|
||||
|
||||
|
||||
class TestParseCommandMigration:
|
||||
"""_parse_command — migration variants."""
|
||||
|
||||
def test_migration_with_src_tgt(self):
|
||||
from src.api.routes.assistant._command_parser import _parse_command
|
||||
envs = [_make_env("prod", "PROD", stage="PROD", is_production=True)]
|
||||
cm = _make_config_manager(envs)
|
||||
result = _parse_command("миграц дашборд 42 с dev на prod", cm)
|
||||
assert result["operation"] == "execute_migration"
|
||||
assert result["entities"]["source_env"] == "dev"
|
||||
assert result["entities"]["target_env"] == "prod"
|
||||
assert result["risk_level"] == "dangerous" # because tgt=prod
|
||||
|
||||
def test_migration_dry_run(self):
|
||||
from src.api.routes.assistant._command_parser import _parse_command
|
||||
cm = _make_config_manager()
|
||||
result = _parse_command("migrate dashboard 42 from dev to staging --dry-run", cm)
|
||||
assert result["entities"]["dry_run"] is True
|
||||
assert result["entities"]["replace_db_config"] is False
|
||||
assert result["entities"]["fix_cross_filters"] is True
|
||||
|
||||
def test_migration_replace_db(self):
|
||||
from src.api.routes.assistant._command_parser import _parse_command
|
||||
cm = _make_config_manager()
|
||||
result = _parse_command("миграция дашборд 42 с dev на qa --replace-db-config --no-fix-cross-filters", cm)
|
||||
assert result["entities"]["replace_db_config"] is True
|
||||
assert result["entities"]["fix_cross_filters"] is False
|
||||
|
||||
def test_migration_without_dashboard(self):
|
||||
from src.api.routes.assistant._command_parser import _parse_command
|
||||
cm = _make_config_manager()
|
||||
result = _parse_command("migrate from dev to staging", cm)
|
||||
assert result["confidence"] == 0.72
|
||||
|
||||
|
||||
class TestParseCommandBackup:
|
||||
"""_parse_command — backup variants."""
|
||||
|
||||
@pytest.mark.parametrize("msg,expected_env", [
|
||||
("бэкап в dev", "dev"),
|
||||
("backup for prod", "prod"),
|
||||
("резерв из staging", "staging"),
|
||||
("бэкап", None),
|
||||
])
|
||||
def test_backup(self, msg, expected_env):
|
||||
from src.api.routes.assistant._command_parser import _parse_command
|
||||
cm = _make_config_manager()
|
||||
result = _parse_command(msg, cm)
|
||||
assert result["operation"] == "run_backup"
|
||||
assert result["entities"].get("environment") == expected_env
|
||||
assert result["risk_level"] == "guarded"
|
||||
|
||||
|
||||
class TestParseCommandHealth:
|
||||
"""_parse_command — health check variants."""
|
||||
|
||||
@pytest.mark.parametrize("msg,expected_env", [
|
||||
("здоровье в dev", "dev"),
|
||||
("health for prod", "prod"),
|
||||
("ошибки", None),
|
||||
("failing env staging", "staging"),
|
||||
("проблемы", None),
|
||||
])
|
||||
def test_health(self, msg, expected_env):
|
||||
from src.api.routes.assistant._command_parser import _parse_command
|
||||
cm = _make_config_manager()
|
||||
result = _parse_command(msg, cm)
|
||||
assert result["domain"] == "health"
|
||||
assert result["operation"] == "get_health_summary"
|
||||
assert result["entities"].get("environment") == expected_env
|
||||
assert result["risk_level"] == "safe"
|
||||
|
||||
|
||||
class TestParseCommandValidation:
|
||||
"""_parse_command — LLM validation variants."""
|
||||
|
||||
def test_validation_with_provider(self):
|
||||
from src.api.routes.assistant._command_parser import _parse_command
|
||||
cm = _make_config_manager()
|
||||
result = _parse_command("валидац дашборд 42 для env dev провайдер openai", cm)
|
||||
assert result["operation"] == "run_llm_validation"
|
||||
assert result["entities"]["environment"] == "dev"
|
||||
assert result["entities"]["provider"] == "openai"
|
||||
|
||||
def test_validation_without_env(self):
|
||||
from src.api.routes.assistant._command_parser import _parse_command
|
||||
cm = _make_config_manager()
|
||||
# "проверь" alone without a dashboard id -> low confidence
|
||||
result = _parse_command("проверь", cm)
|
||||
assert result["operation"] == "run_llm_validation"
|
||||
assert result["confidence"] == 0.64
|
||||
|
||||
|
||||
class TestParseCommandDocumentation:
|
||||
"""_parse_command — LLM documentation variants."""
|
||||
|
||||
def test_documentation_with_dataset(self):
|
||||
from src.api.routes.assistant._command_parser import _parse_command
|
||||
cm = _make_config_manager()
|
||||
result = _parse_command("документац датасет 123 env prod провайдер anthropic", cm)
|
||||
assert result["operation"] == "run_llm_documentation"
|
||||
assert result["entities"]["dataset_id"] == 123
|
||||
assert result["entities"]["environment"] == "prod"
|
||||
assert result["entities"]["provider"] == "anthropic"
|
||||
|
||||
def test_documentation_without_dataset(self):
|
||||
from src.api.routes.assistant._command_parser import _parse_command
|
||||
cm = _make_config_manager()
|
||||
result = _parse_command("generate docs", cm)
|
||||
assert result["operation"] == "run_llm_documentation"
|
||||
assert result["confidence"] == 0.64
|
||||
|
||||
|
||||
class TestParseCommandSearchDashboards:
|
||||
"""_parse_command — search/list dashboards."""
|
||||
|
||||
@pytest.mark.parametrize("msg,expected_env", [
|
||||
("список дашборд в dev", "dev"),
|
||||
("покажи дашборд в prod", "prod"),
|
||||
("найди дашборд test в staging", "staging"),
|
||||
("list dashboard в dev", "dev"),
|
||||
("search dashboard covid в dev", "dev"),
|
||||
("all dashboards", None),
|
||||
("все дашборд", None),
|
||||
])
|
||||
def test_search_dashboards(self, msg, expected_env):
|
||||
from src.api.routes.assistant._command_parser import _parse_command
|
||||
cm = _make_config_manager()
|
||||
result = _parse_command(msg, cm)
|
||||
assert result["domain"] == "dashboards"
|
||||
assert result["operation"] == "search_dashboards"
|
||||
assert result["entities"].get("environment") == expected_env
|
||||
|
||||
|
||||
class TestParseCommandEnvironments:
|
||||
"""_parse_command — list environments."""
|
||||
|
||||
@pytest.mark.parametrize("msg", [
|
||||
"окружени",
|
||||
"environment",
|
||||
"среды",
|
||||
"сред",
|
||||
])
|
||||
def test_list_environments(self, msg):
|
||||
from src.api.routes.assistant._command_parser import _parse_command
|
||||
cm = _make_config_manager()
|
||||
result = _parse_command(msg, cm)
|
||||
assert result["domain"] == "environments"
|
||||
assert result["operation"] == "list_environments"
|
||||
assert result["risk_level"] == "safe"
|
||||
|
||||
|
||||
class TestParseCommandMaintenance:
|
||||
"""_parse_command — maintenance/events."""
|
||||
|
||||
@pytest.mark.parametrize("msg", [
|
||||
"обслуживан",
|
||||
"maintenance",
|
||||
"события",
|
||||
])
|
||||
def test_list_maintenance(self, msg):
|
||||
from src.api.routes.assistant._command_parser import _parse_command
|
||||
cm = _make_config_manager()
|
||||
result = _parse_command(msg, cm)
|
||||
assert result["domain"] == "maintenance"
|
||||
assert result["operation"] == "list_maintenance_events"
|
||||
|
||||
|
||||
class TestParseCommandLlmProviders:
|
||||
"""_parse_command — LLM provider listing/status."""
|
||||
|
||||
def test_list_providers(self):
|
||||
from src.api.routes.assistant._command_parser import _parse_command
|
||||
cm = _make_config_manager()
|
||||
result = _parse_command("llm провайдер", cm)
|
||||
assert result["domain"] == "llm"
|
||||
assert result["operation"] == "list_llm_providers"
|
||||
|
||||
def test_llm_status_unreachable_via_parser(self):
|
||||
"""The deterministic parser cannot deliver get_llm_status because all
|
||||
keywords matching line 99 are intercepted by earlier checks (status,
|
||||
validation). This is a known source-code quirk; LLM status routing
|
||||
happens through the LLM planner path instead."""
|
||||
from src.api.routes.assistant._command_parser import _parse_command
|
||||
cm = _make_config_manager()
|
||||
result = _parse_command("llm провер", cm)
|
||||
# Line 74 (провер in validation) catches it first
|
||||
assert result["operation"] == "run_llm_validation"
|
||||
|
||||
|
||||
class TestParseCommandFallback:
|
||||
"""_parse_command — unknown commands return clarify intent."""
|
||||
|
||||
@pytest.mark.parametrize("msg", [
|
||||
"",
|
||||
"hello world",
|
||||
"какой сегодня день",
|
||||
"random gibberish xyz",
|
||||
"12345",
|
||||
])
|
||||
def test_fallback_to_clarify(self, msg):
|
||||
from src.api.routes.assistant._command_parser import _parse_command
|
||||
cm = _make_config_manager()
|
||||
result = _parse_command(msg, cm)
|
||||
assert result["domain"] == "unknown"
|
||||
assert result["operation"] == "clarify"
|
||||
assert result["confidence"] == 0.3
|
||||
assert result["risk_level"] == "safe"
|
||||
assert "domain" in result
|
||||
assert "operation" in result
|
||||
assert "entities" in result
|
||||
assert "confidence" in result
|
||||
assert "risk_level" in result
|
||||
assert "requires_confirmation" in result
|
||||
# #endregion Test.Assistant.CommandParser
|
||||
Reference in New Issue
Block a user