/
ump-team
/
ump-infra
Обзор
Документация
Войти
/
ump-team
/
ump-infra
Код
Запросы
1
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
dev/test
tests/test_integration_cli.py
213 строк
9 KB
Dmitry Kochenov
v0.0.5: рефакторинг валидации фаз и улучшение PR-логики
19 июл 2026, 23:40
19 июл 2026, 23:40
a686387
Код
Авторство
О чём код?
"""Интеграционные тесты для CLI-скриптов UMP. Запускает скрипты как subprocess в изолированном окружении (tmp_path), проверяет exit codes и сообщения об ошибках. Покрытие: - agent_log.py CLI: add → tail → show → clear - orchestrate_step.py --init-progress / --show-progress / --record-action - extract_section.py: извлечение секций из реального MD-файла - get_step_title.py: поиск шага в YAML - Отсутствующий plan файл → nonzero exit с понятным сообщением (не Traceback) """ from __future__ import annotations import json import os import subprocess import sys from pathlib import Path import pytest PROJECT_ROOT = Path(__file__).resolve().parent.parent SCRIPTS_DIR = PROJECT_ROOT / 'scripts' def run_script(script_name: str, args: list[str], cwd: Path, env: dict | None = None) -> subprocess.CompletedProcess: """Запустить скрипт scripts/<script_name> в cwd и вернуть результат.""" script_path = SCRIPTS_DIR / script_name full_env = os.environ.copy() if env: full_env.update(env) return subprocess.run( [sys.executable, str(script_path), *args], cwd=cwd, capture_output=True, text=True, env=full_env, timeout=30, ) @pytest.fixture def fake_project(tmp_path: Path) -> Path: """Создать минимальный fake-проект с ump-ui-config.yaml и plans/.""" (tmp_path / 'ump-ui-config.yaml').write_text( 'project:\n name: test\n type: web-app\n', encoding='utf-8', ) (tmp_path / 'plans').mkdir() (tmp_path / '_meta').mkdir() return tmp_path class TestAgentLogCLI: def test_add_creates_log_entry(self, fake_project: Path) -> None: result = run_script('agent_log.py', ['add', 'test message', '--type', 'action'], fake_project) assert result.returncode == 0 log_file = fake_project / '_meta' / 'agent.log' assert log_file.exists() lines = log_file.read_text(encoding='utf-8').splitlines() assert len(lines) == 1 entry = json.loads(lines[0]) assert entry['message'] == 'test message' def test_tail_prints_entries(self, fake_project: Path) -> None: # Сначала добавим несколько записей for i in range(3): run_script('agent_log.py', ['add', f'message-{i}'], fake_project) result = run_script('agent_log.py', ['tail', '--lines', '5'], fake_project) assert result.returncode == 0 assert 'message-0' in result.stdout assert 'message-2' in result.stdout def test_show_with_step_filter(self, fake_project: Path) -> None: run_script('agent_log.py', ['add', 'first', '--step', '1.0'], fake_project) run_script('agent_log.py', ['add', 'second', '--step', '1.6'], fake_project) result = run_script('agent_log.py', ['show', '--step', '1.6'], fake_project) assert result.returncode == 0 assert 'second' in result.stdout assert 'first' not in result.stdout def test_clear_removes_all_entries(self, fake_project: Path) -> None: run_script('agent_log.py', ['add', 'msg'], fake_project) result = run_script('agent_log.py', ['clear'], fake_project) assert result.returncode == 0 assert not (fake_project / '_meta' / 'agent.log').exists() def test_invalid_type_rejected_by_argparse(self, fake_project: Path) -> None: """CLI argparse отклоняет невалидный --type (choices=VALID_TYPES). Фоллбэк на 'action' происходит только при программном вызове add_entry с неизвестным типом — см. test_agent_log.py::TestAddEntryBasics. """ result = run_script( 'agent_log.py', ['add', 'msg', '--type', 'invalid_type'], fake_project, ) assert result.returncode == 2 # argparse error assert 'invalid choice' in result.stderr class TestExtractSectionCLI: def test_extract_known_section(self, fake_project: Path) -> None: md = fake_project / 'doc.md' md.write_text( '# T\n\n## 1. First\n\ntext\n\n## 2. Second\n\nmore\n', encoding='utf-8', ) result = run_script('extract_section.py', [str(md), '1'], fake_project) assert result.returncode == 0 assert 'First' in result.stdout def test_missing_section_exits_1(self, fake_project: Path) -> None: md = fake_project / 'doc.md' md.write_text('# T\n\n## 1. First\n\ntext\n', encoding='utf-8') result = run_script('extract_section.py', [str(md), '99'], fake_project) assert result.returncode == 1 def test_missing_file_exits_2(self, fake_project: Path) -> None: result = run_script('extract_section.py', ['/nonexistent.md', '1'], fake_project) assert result.returncode == 2 assert 'не существует' in result.stderr class TestOrchestrateStepCLI: """Без реального plan-файла скрипт должен корректно сообщать об ошибке.""" def test_show_progress_missing_step_exits_nonzero(self, fake_project: Path) -> None: """--show-progress без существующего шага в планах → exit != 0.""" result = run_script( 'orchestrate_step.py', ['1.0', '--show-progress'], fake_project, ) assert result.returncode != 0 # Сообщение должно объяснять причину, а не быть Traceback Python assert 'Traceback' not in result.stderr # Шаг не найден в планах — это валидная ошибка assert 'not found' in result.stderr.lower() def test_init_progress_creates_file(self, fake_project: Path) -> None: # Нужен YAML-план с шагом 1.0 plan = fake_project / 'plans' / '01-setup.yaml' plan.write_text( 'stage: 01\nname: Setup\nsteps:\n' ' - id: "1.0"\n' ' title: Init\n' ' branch: main\n' ' docs_target: docs/01-setup/step-1-0.md\n', encoding='utf-8', ) # --no-ack требуется в v36 (ack-файл не используется, но legacy-параметр остался) result = run_script( 'orchestrate_step.py', ['1.0', '--init-progress', '--no-ack'], fake_project, ) assert result.returncode == 0 progress_file = fake_project / '_meta' / 'step-progress-1-0.yaml' assert progress_file.exists() def test_record_action_without_progress_exits_nonzero(self, fake_project: Path) -> None: """--record-action без существующего шага → exit != 0.""" result = run_script( 'orchestrate_step.py', ['1.0', '--record-action', 'readFile:test.md'], fake_project, ) assert result.returncode != 0 assert 'Traceback' not in result.stderr class TestGetStepTitleCLI: def test_existing_step_returns_title(self, fake_project: Path) -> None: plan = fake_project / 'plans' / '01-setup.yaml' plan.write_text( 'stage: 01\nname: Setup\nsteps:\n' ' - id: "1.6"\n' ' title: Директории проекта\n', encoding='utf-8', ) result = run_script('get_step_title.py', ['1.6'], fake_project) assert result.returncode == 0 assert 'Директории' in result.stdout def test_missing_step_returns_empty_stdout(self, fake_project: Path) -> None: """Шаг не найден → пустой stdout, exit 0 (не падает).""" plan = fake_project / 'plans' / '01-setup.yaml' plan.write_text( 'stage: 01\nname: Setup\nsteps:\n - id: "1.0"\n title: Init\n', encoding='utf-8', ) result = run_script('get_step_title.py', ['9.9'], fake_project) assert result.returncode == 0 assert result.stdout.strip() == '' class TestValidateMdCLI: def test_valid_md_exits_0(self, fake_project: Path) -> None: md = fake_project / 'good.md' md.write_text('# Title\n\nSome text.\n', encoding='utf-8') result = run_script('validate_md.py', [str(md)], fake_project) assert result.returncode == 0 def test_invalid_md_exits_1(self, fake_project: Path) -> None: md = fake_project / 'bad.md' # Нет H1 md.write_text('Just text without heading\n', encoding='utf-8') result = run_script('validate_md.py', [str(md)], fake_project) assert result.returncode == 1 assert 'M01' in result.stdout or 'M01' in result.stderr