Files
ss-tools/backend/tests/api/test_assistant_command_parser.py

368 lines
15 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.

# #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