from pathlib import Path import pytest from agr_assistent.commands.catalog import default_commands_text from agr_assistent.commands.model import CommandError, load_commands, parse_command, parse_key_combo def _command(**overrides: object) -> dict: item = {"name": "test", "description": "Тест", "action": {"type": "open", "target": "https://x.io"}} item.update(overrides) return item def test_default_commands_file_is_valid(tmp_path: Path) -> None: path = tmp_path / "commands.yaml" path.write_text(default_commands_text(), encoding="utf-8") commands, errors = load_commands(path) assert errors == [] assert {command.name for command in commands} >= {"media_play_pause", "volume_up", "web_search"} def test_commented_examples_are_valid_too(tmp_path: Path) -> None: """Примеры в конце файла закомментированы — раскомментированные, они тоже должны загружаться.""" lines = default_commands_text().splitlines() start = next(i for i, line in enumerate(lines) if "--- Примеры" in line) uncommented = lines[: start + 1] + [ line.replace(" # ", " ", 1) if line.startswith(" # ") else line for line in lines[start + 1 :] ] path = tmp_path / "commands.yaml" path.write_text("\n".join(uncommented), encoding="utf-8") commands, errors = load_commands(path) assert errors == [] assert {"shutdown_computer", "disk_space", "room_light"} <= {c.name for c in commands} def test_parameters_schema_defaults_and_summary() -> None: command = parse_command( _command( parameters={ "level": {"type": "integer", "minimum": 0, "maximum": 100}, "steps": {"type": "integer", "default": 5}, "note": "Просто описание", }, action={"type": "keys", "keys": "volume_up", "repeat": "{steps}"}, ) ) assert command.schema() == { "type": "object", "properties": { "level": {"type": "integer", "minimum": 0, "maximum": 100}, "steps": {"type": "integer"}, "note": {"type": "string", "description": "Просто описание"}, }, "required": ["level", "note"], } assert command.with_defaults({"level": 30}) == {"steps": 5, "level": 30} assert command.summary({"level": 30}) == "Тест (steps: 5, level: 30)" @pytest.mark.parametrize( ("overrides", "message"), [ ({"name": "плохое имя"}, "латиница"), ({"name": "remember"}, "занято"), ({"description": ""}, "описание"), ({"action": {"type": "shell", "command": "rm"}}, "run, open, http или keys"), ({"action": {"type": "run"}}, "program"), ({"action": {"type": "http", "url": "ftp://x"}}, "http://"), ({"action": {"type": "keys", "keys": "ctrl+bogus"}}, "неизвестная клавиша"), ({"action": {"type": "open", "target": "https://x.io/{query}"}}, "неизвестные параметры"), ( {"parameters": {"level": {"type": "integer"}}, "phrases": ["громкость"]}, "не хватает обязательных", ), ], ) def test_invalid_commands_are_rejected(overrides: dict, message: str) -> None: with pytest.raises(CommandError, match=message): parse_command(_command(**overrides)) def test_one_broken_command_does_not_break_others(tmp_path: Path) -> None: path = tmp_path / "commands.yaml" path.write_text( """ commands: - name: good description: Хорошая action: {type: open, target: "https://x.io"} - name: bad description: Плохая action: {type: nope} - name: good description: Дубль action: {type: open, target: "https://y.io"} """, encoding="utf-8", ) commands, errors = load_commands(path) assert [command.name for command in commands] == ["good"] assert len(errors) == 2 assert "bad" in errors[0] and "уже используется" in errors[1] def test_key_combos() -> None: assert parse_key_combo("ctrl+shift+esc") == [0x11, 0x10, 0x1B] assert parse_key_combo("volume_mute") == [0xAD] with pytest.raises(CommandError): parse_key_combo("ctrl+")