kin/tests/test_kin_101_regression.py

234 lines
10 KiB
Python
Raw Normal View History

2026-03-17 19:30:15 +02:00
"""
Regression tests for KIN-101:
Auto-test fallback: _detect_test_command and pipeline skip logic.
Coverage:
(1) _detect_test_command file-based detection
(2) Pipeline: auto-detect used when test_command is NULL
(3) Pipeline: skips tests with log when no framework detected (no blocking)
"""
import json
import os
import pytest
from pathlib import Path
from unittest.mock import patch, MagicMock
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def conn():
from core.db import init_db
from core import models
c = init_db(":memory:")
models.create_project(c, "vdol", "ВДОЛЬ", "~/projects/vdolipoperek",
tech_stack=["vue3"])
models.create_task(c, "VDOL-001", "vdol", "Fix bug",
brief={"route_type": "debug"})
yield c
c.close()
def _mock_success():
m = MagicMock()
m.returncode = 0
m.stdout = json.dumps({
"status": "done",
"changes": [],
"notes": "",
})
m.stderr = ""
return m
# ---------------------------------------------------------------------------
# (1) _detect_test_command — file detection logic
# ---------------------------------------------------------------------------
class TestDetectTestCommand:
def test_makefile_with_test_target(self, tmp_path):
"""Makefile с target 'test:' → возвращает 'make test'."""
from agents.runner import _detect_test_command
(tmp_path / "Makefile").write_text("build:\n\tgo build\n\ntest:\n\tgo test ./...\n")
assert _detect_test_command(str(tmp_path)) == "make test"
def test_makefile_without_test_target(self, tmp_path):
"""Makefile без target 'test' → не выбирается."""
from agents.runner import _detect_test_command
(tmp_path / "Makefile").write_text("build:\n\tgo build\n")
# Should fall through to other candidates (no package.json/pyproject.toml/tsconfig)
assert _detect_test_command(str(tmp_path)) is None
def test_package_json_with_scripts_test(self, tmp_path):
"""package.json со scripts.test → возвращает 'npm test'."""
from agents.runner import _detect_test_command
pkg = {"name": "myapp", "scripts": {"test": "vitest"}}
(tmp_path / "package.json").write_text(json.dumps(pkg))
assert _detect_test_command(str(tmp_path)) == "npm test"
def test_package_json_without_scripts_test(self, tmp_path):
"""package.json без scripts.test → не выбирается."""
from agents.runner import _detect_test_command
pkg = {"name": "myapp", "scripts": {"build": "vite build"}}
(tmp_path / "package.json").write_text(json.dumps(pkg))
assert _detect_test_command(str(tmp_path)) is None
def test_package_json_invalid_json_skipped(self, tmp_path):
"""Невалидный package.json → не крашится, пропускается."""
from agents.runner import _detect_test_command
(tmp_path / "package.json").write_text("not json {{")
assert _detect_test_command(str(tmp_path)) is None
def test_pyproject_toml_returns_pytest(self, tmp_path):
"""pyproject.toml → возвращает 'pytest'."""
from agents.runner import _detect_test_command
(tmp_path / "pyproject.toml").write_text("[tool.pytest.ini_options]\n")
assert _detect_test_command(str(tmp_path)) == "pytest"
def test_setup_py_returns_pytest(self, tmp_path):
"""setup.py → возвращает 'pytest'."""
from agents.runner import _detect_test_command
(tmp_path / "setup.py").write_text("from setuptools import setup\nsetup(name='x')\n")
assert _detect_test_command(str(tmp_path)) == "pytest"
def test_tsconfig_returns_npx_tsc(self, tmp_path):
"""tsconfig.json → возвращает 'npx tsc --noEmit'."""
from agents.runner import _detect_test_command
(tmp_path / "tsconfig.json").write_text('{"compilerOptions": {}}\n')
assert _detect_test_command(str(tmp_path)) == "npx tsc --noEmit"
def test_no_files_returns_none(self, tmp_path):
"""Пустая директория → возвращает None."""
from agents.runner import _detect_test_command
assert _detect_test_command(str(tmp_path)) is None
def test_priority_makefile_over_package_json(self, tmp_path):
"""Makefile с test target приоритетнее package.json."""
from agents.runner import _detect_test_command
(tmp_path / "Makefile").write_text("test:\n\tpytest\n")
(tmp_path / "package.json").write_text(json.dumps({"scripts": {"test": "jest"}}))
assert _detect_test_command(str(tmp_path)) == "make test"
def test_priority_package_json_over_pyproject(self, tmp_path):
"""package.json со scripts.test приоритетнее pyproject.toml."""
from agents.runner import _detect_test_command
(tmp_path / "package.json").write_text(json.dumps({"scripts": {"test": "jest"}}))
(tmp_path / "pyproject.toml").write_text("[build-system]\n")
assert _detect_test_command(str(tmp_path)) == "npm test"
def test_nonexistent_path_returns_none(self):
"""Несуществующая директория → возвращает None без исключений."""
from agents.runner import _detect_test_command
assert _detect_test_command("/nonexistent/path/xyz") is None
# ---------------------------------------------------------------------------
# (2) Pipeline: auto-detect used when test_command is NULL
# ---------------------------------------------------------------------------
class TestPipelineAutoDetect:
@patch("agents.runner._run_autocommit")
@patch("agents.runner._detect_test_command")
@patch("agents.runner._run_project_tests")
@patch("agents.runner.subprocess.run")
def test_null_test_command_triggers_autodetect(
self, mock_run, mock_tests, mock_detect, mock_autocommit, conn
):
"""Если test_command=NULL — вызывается _detect_test_command."""
from agents.runner import run_pipeline
from core import models
mock_run.return_value = _mock_success()
mock_tests.return_value = {"success": True, "output": "OK", "returncode": 0}
mock_detect.return_value = "pytest"
# Leave test_command as NULL (default)
models.update_project(conn, "vdol", auto_test_enabled=True)
steps = [{"role": "backend_dev", "brief": "implement"}]
result = run_pipeline(conn, "VDOL-001", steps)
assert result["success"] is True
mock_detect.assert_called_once()
# _run_project_tests вызывается с обнаруженной командой
mock_tests.assert_called_once()
called_cmd = mock_tests.call_args[0][1]
assert called_cmd == "pytest"
@patch("agents.runner._run_autocommit")
@patch("agents.runner._detect_test_command")
@patch("agents.runner._run_project_tests")
@patch("agents.runner.subprocess.run")
def test_explicit_test_command_skips_autodetect(
self, mock_run, mock_tests, mock_detect, mock_autocommit, conn
):
"""Если test_command задан явно — _detect_test_command не вызывается."""
from agents.runner import run_pipeline
from core import models
mock_run.return_value = _mock_success()
mock_tests.return_value = {"success": True, "output": "OK", "returncode": 0}
models.update_project(conn, "vdol", auto_test_enabled=True, test_command="npm test")
steps = [{"role": "backend_dev", "brief": "implement"}]
run_pipeline(conn, "VDOL-001", steps)
mock_detect.assert_not_called()
called_cmd = mock_tests.call_args[0][1]
assert called_cmd == "npm test"
# ---------------------------------------------------------------------------
# (3) Pipeline: no framework detected → skip without blocking
# ---------------------------------------------------------------------------
class TestPipelineNoFramework:
@patch("agents.runner._run_autocommit")
@patch("agents.runner._detect_test_command")
@patch("agents.runner._run_project_tests")
@patch("agents.runner.subprocess.run")
def test_no_framework_skips_tests(
self, mock_run, mock_tests, mock_detect, mock_autocommit, conn
):
"""Если _detect_test_command=None — тесты пропускаются, pipeline не блокируется."""
from agents.runner import run_pipeline
from core import models
mock_run.return_value = _mock_success()
mock_detect.return_value = None
models.update_project(conn, "vdol", auto_test_enabled=True)
steps = [{"role": "backend_dev", "brief": "implement"}]
result = run_pipeline(conn, "VDOL-001", steps)
# Pipeline succeeds
assert result["success"] is True
# _run_project_tests NOT called
mock_tests.assert_not_called()
2026-03-17 20:58:06 +02:00
# Task should be in review (auto_eligible=False → runner sets status='review')
2026-03-17 19:30:15 +02:00
task = models.get_task(conn, "VDOL-001")
2026-03-17 20:58:06 +02:00
assert task["status"] == "review"
2026-03-17 19:30:15 +02:00
@patch("agents.runner._run_autocommit")
@patch("agents.runner._detect_test_command")
@patch("agents.runner._run_project_tests")
@patch("agents.runner.subprocess.run")
def test_no_framework_result_has_skipped_flag(
self, mock_run, mock_tests, mock_detect, mock_autocommit, conn
):
"""При пропуске тестов в results есть запись с _skipped=True и output='no test framework detected'."""
from agents.runner import run_pipeline
from core import models
mock_run.return_value = _mock_success()
mock_detect.return_value = None
models.update_project(conn, "vdol", auto_test_enabled=True)
steps = [{"role": "backend_dev", "brief": "implement"}]
result = run_pipeline(conn, "VDOL-001", steps)
auto_test_entries = [r for r in result.get("results", []) if r.get("role") == "_auto_test"]
assert len(auto_test_entries) == 1
entry = auto_test_entries[0]
assert entry["_skipped"] is True
assert entry["success"] is True
assert "no test framework detected" in entry["output"]