kin: auto-commit after pipeline
This commit is contained in:
parent
e7c65c22e5
commit
0e522e54a9
7 changed files with 363 additions and 65 deletions
|
|
@ -2197,8 +2197,11 @@ def test_create_project_with_test_command(client):
|
|||
assert row[0] == "npm test"
|
||||
|
||||
|
||||
def test_patch_project_test_command_empty_string_stores_empty(client):
|
||||
"""KIN-ARCH-008: PATCH с пустой строкой сохраняет пустую строку (не NULL, в отличие от deploy_command)."""
|
||||
def test_patch_project_test_command_empty_string_stores_null(client):
|
||||
"""KIN-101: PATCH с пустой строкой сохраняет NULL — включает режим авто-определения фреймворка.
|
||||
|
||||
Пустая строка — сентинел для сброса test_command (аналогично deploy_command и другим полям).
|
||||
"""
|
||||
client.patch("/api/projects/p1", json={"test_command": "pytest -v"})
|
||||
client.patch("/api/projects/p1", json={"test_command": ""})
|
||||
|
||||
|
|
@ -2206,7 +2209,7 @@ def test_patch_project_test_command_empty_string_stores_empty(client):
|
|||
conn = init_db(api_module.DB_PATH)
|
||||
row = conn.execute("SELECT test_command FROM projects WHERE id = 'p1'").fetchone()
|
||||
conn.close()
|
||||
assert row[0] == ""
|
||||
assert row[0] is None
|
||||
|
||||
|
||||
def test_get_projects_includes_test_command(client):
|
||||
|
|
|
|||
|
|
@ -260,7 +260,7 @@ class TestAutoTestInPipeline:
|
|||
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)
|
||||
models.update_project(conn, "vdol", auto_test_enabled=True, test_command="make test")
|
||||
|
||||
steps = [{"role": "backend_dev", "brief": "implement"}]
|
||||
result = run_pipeline(conn, "VDOL-001", steps)
|
||||
|
|
@ -301,7 +301,7 @@ class TestAutoTestInPipeline:
|
|||
{"success": False, "output": "FAILED: test_foo", "returncode": 1},
|
||||
{"success": True, "output": "OK", "returncode": 0},
|
||||
]
|
||||
models.update_project(conn, "vdol", auto_test_enabled=True)
|
||||
models.update_project(conn, "vdol", auto_test_enabled=True, test_command="make test")
|
||||
|
||||
with patch.dict(os.environ, {"KIN_AUTO_TEST_MAX_ATTEMPTS": "3"}):
|
||||
steps = [{"role": "backend_dev", "brief": "implement"}]
|
||||
|
|
@ -327,7 +327,7 @@ class TestAutoTestInPipeline:
|
|||
mock_run.return_value = _mock_success()
|
||||
# Тест всегда падает
|
||||
mock_tests.return_value = {"success": False, "output": "FAILED", "returncode": 1}
|
||||
models.update_project(conn, "vdol", auto_test_enabled=True)
|
||||
models.update_project(conn, "vdol", auto_test_enabled=True, test_command="make test")
|
||||
|
||||
with patch.dict(os.environ, {"KIN_AUTO_TEST_MAX_ATTEMPTS": "2"}):
|
||||
steps = [{"role": "backend_dev", "brief": "implement"}]
|
||||
|
|
@ -348,7 +348,7 @@ class TestAutoTestInPipeline:
|
|||
from agents.runner import run_pipeline
|
||||
from core import models
|
||||
mock_run.return_value = _mock_success()
|
||||
models.update_project(conn, "vdol", auto_test_enabled=True)
|
||||
models.update_project(conn, "vdol", auto_test_enabled=True, test_command="make test")
|
||||
|
||||
steps = [{"role": "debugger", "brief": "find"}]
|
||||
run_pipeline(conn, "VDOL-001", steps)
|
||||
|
|
|
|||
233
tests/test_kin_101_regression.py
Normal file
233
tests/test_kin_101_regression.py
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
"""
|
||||
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()
|
||||
# Task NOT blocked
|
||||
task = models.get_task(conn, "VDOL-001")
|
||||
assert task["status"] != "blocked"
|
||||
|
||||
@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"]
|
||||
|
|
@ -789,10 +789,10 @@ def test_schema_project_has_test_command_column(conn):
|
|||
assert "test_command" in cols
|
||||
|
||||
|
||||
def test_test_command_default_is_make_test(conn):
|
||||
"""KIN-ARCH-008: новый проект без test_command получает дефолт 'make test'."""
|
||||
def test_test_command_default_is_null(conn):
|
||||
"""KIN-101: новый проект без test_command получает NULL (авто-определение фреймворка)."""
|
||||
p = models.create_project(conn, "prj_tc", "TC Project", "/tmp/tc")
|
||||
assert p["test_command"] == "make test"
|
||||
assert p["test_command"] is None
|
||||
|
||||
|
||||
def test_test_command_can_be_set(conn):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue