"""Regression tests for KIN-102: Legacy test_command='make test' blocks autocommit in projects without Makefile. Root cause: projects with legacy test_command='make test' (old schema default) fail the auto-test runner because the project has no Makefile. This triggers an auto-fix loop that eventually marks the pipeline as failed + task as blocked, causing an early return in run_pipeline BEFORE _run_autocommit is reached. Fix: migration in core/db.py resets test_command='make test' → NULL for all projects, allowing auto-detection to find the real test framework or skip tests gracefully. Coverage: (1) Migration: test_command='make test' → NULL (KIN-102 fix) (2) Migration: other test_command values are NOT reset (3) Bug scenario: legacy 'make test' → pipeline blocked, autocommit skipped (4) Post-fix: NULL test_command + no framework → pipeline succeeds + autocommit called """ import json import os import pytest 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, "sharedbox", "SharedBox", "~/projects/sharedbox", tech_stack=["typescript", "node"], ) models.create_task( c, "SHAREDBOX-003", "sharedbox", "Setup DB", brief={"route_type": "backend_dev"}, ) yield c c.close() def _mock_agent_success(): m = MagicMock() m.returncode = 0 m.stdout = json.dumps({"status": "done", "changes": [], "notes": ""}) m.stderr = "" return m def _mock_make_test_fail(): return { "success": False, "output": "make: No rule to make target `test'. Stop.", "returncode": 2, } # --------------------------------------------------------------------------- # (1) Migration: test_command='make test' → NULL # --------------------------------------------------------------------------- class TestKin102Migration: def test_legacy_make_test_reset_to_null(self): """Migration resets test_command='make test' → NULL (KIN-102 fix).""" from core.db import init_db, _migrate from core import models c = init_db(":memory:") models.create_project(c, "sbx", "SharedBox", "~/projects/sharedbox", tech_stack=["typescript"]) c.execute("UPDATE projects SET test_command='make test', auto_test_enabled=1 WHERE id='sbx'") c.commit() # Verify pre-condition: legacy value is set row = c.execute("SELECT test_command FROM projects WHERE id='sbx'").fetchone() assert row[0] == "make test", "Pre-condition: test_command must be 'make test' before migration" # Run migration _migrate(c) # After migration: test_command must be NULL row = c.execute("SELECT test_command FROM projects WHERE id='sbx'").fetchone() assert row[0] is None, f"Expected NULL after migration, got {row[0]!r}" c.close() def test_npm_test_not_reset_by_migration(self): """Migration must NOT reset test_command='npm test' (explicitly set by user).""" from core.db import init_db, _migrate from core import models c = init_db(":memory:") models.create_project(c, "sbx", "SharedBox", "~/projects/sharedbox", tech_stack=["typescript"]) c.execute("UPDATE projects SET test_command='npm test' WHERE id='sbx'") c.commit() _migrate(c) row = c.execute("SELECT test_command FROM projects WHERE id='sbx'").fetchone() assert row[0] == "npm test", "Migration must not change test_command='npm test'" c.close() def test_pytest_not_reset_by_migration(self): """Migration must NOT reset test_command='pytest' (explicitly set by user).""" from core.db import init_db, _migrate from core import models c = init_db(":memory:") models.create_project(c, "sbx", "SharedBox", "~/projects/sharedbox", tech_stack=["python"]) c.execute("UPDATE projects SET test_command='pytest' WHERE id='sbx'") c.commit() _migrate(c) row = c.execute("SELECT test_command FROM projects WHERE id='sbx'").fetchone() assert row[0] == "pytest", "Migration must not change test_command='pytest'" c.close() def test_null_test_command_stays_null_after_migration(self): """Migration must NOT affect projects that already have test_command=NULL.""" from core.db import init_db, _migrate from core import models c = init_db(":memory:") models.create_project(c, "sbx", "SharedBox", "~/projects/sharedbox", tech_stack=["typescript"]) # Default test_command is NULL — don't change it _migrate(c) row = c.execute("SELECT test_command FROM projects WHERE id='sbx'").fetchone() assert row[0] is None, "Migration must not touch NULL test_command" c.close() def test_migration_is_idempotent(self): """Running migration twice must not cause errors or data corruption.""" from core.db import init_db, _migrate from core import models c = init_db(":memory:") models.create_project(c, "sbx", "SharedBox", "~/projects/sharedbox", tech_stack=["typescript"]) c.execute("UPDATE projects SET test_command='make test' WHERE id='sbx'") c.commit() _migrate(c) _migrate(c) # second run must be safe row = c.execute("SELECT test_command FROM projects WHERE id='sbx'").fetchone() assert row[0] is None c.close() # --------------------------------------------------------------------------- # (2) Bug scenario: legacy 'make test' → pipeline blocked, autocommit not called # --------------------------------------------------------------------------- class TestKin102BugScenario: @patch("agents.runner._run_autocommit") @patch("agents.runner._run_project_tests") @patch("agents.runner.subprocess.run") def test_legacy_make_test_blocks_task( self, mock_run, mock_tests, mock_autocommit, conn ): """Bug: legacy test_command='make test' on project without Makefile → task blocked.""" from agents.runner import run_pipeline from core import models models.update_project(conn, "sharedbox", auto_test_enabled=True, test_command="make test") mock_tests.return_value = _mock_make_test_fail() mock_run.return_value = _mock_agent_success() with patch.dict(os.environ, {"KIN_AUTO_TEST_MAX_ATTEMPTS": "0"}): result = run_pipeline(conn, "SHAREDBOX-003", [{"role": "backend_dev", "brief": "setup db"}]) assert result["success"] is False task = models.get_task(conn, "SHAREDBOX-003") assert task["status"] == "blocked" @patch("agents.runner._run_autocommit") @patch("agents.runner._run_project_tests") @patch("agents.runner.subprocess.run") def test_legacy_make_test_skips_autocommit( self, mock_run, mock_tests, mock_autocommit, conn ): """Bug: when pipeline fails due to legacy 'make test', _run_autocommit is never called.""" from agents.runner import run_pipeline from core import models models.update_project(conn, "sharedbox", auto_test_enabled=True, test_command="make test") mock_tests.return_value = _mock_make_test_fail() mock_run.return_value = _mock_agent_success() with patch.dict(os.environ, {"KIN_AUTO_TEST_MAX_ATTEMPTS": "0"}): run_pipeline(conn, "SHAREDBOX-003", [{"role": "backend_dev", "brief": "setup db"}]) # Early return before _run_autocommit — never reached mock_autocommit.assert_not_called() @patch("agents.runner._run_autocommit") @patch("agents.runner._run_project_tests") @patch("agents.runner.subprocess.run") def test_blocked_reason_mentions_make_test( self, mock_run, mock_tests, mock_autocommit, conn ): """Blocked reason must mention 'make test' for diagnosability.""" from agents.runner import run_pipeline from core import models models.update_project(conn, "sharedbox", auto_test_enabled=True, test_command="make test") mock_tests.return_value = _mock_make_test_fail() mock_run.return_value = _mock_agent_success() with patch.dict(os.environ, {"KIN_AUTO_TEST_MAX_ATTEMPTS": "0"}): result = run_pipeline(conn, "SHAREDBOX-003", [{"role": "backend_dev", "brief": "setup db"}]) assert "make test" in result.get("error", ""), ( f"Blocked reason must contain 'make test', got: {result.get('error')!r}" ) # --------------------------------------------------------------------------- # (3) Post-fix: NULL test_command + no framework → pipeline succeeds + autocommit called # --------------------------------------------------------------------------- class TestKin102PostFix: @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_no_framework_pipeline_succeeds( self, mock_run, mock_tests, mock_detect, mock_autocommit, conn ): """After fix: test_command=NULL, no framework detected → pipeline succeeds.""" from agents.runner import run_pipeline from core import models # Simulate post-migration state: test_command reset to NULL models.update_project(conn, "sharedbox", auto_test_enabled=True, test_command=None) mock_run.return_value = _mock_agent_success() mock_detect.return_value = None # No test framework found (no Makefile/package.json) result = run_pipeline(conn, "SHAREDBOX-003", [{"role": "backend_dev", "brief": "setup db"}]) assert result["success"] is True mock_tests.assert_not_called() @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_no_framework_autocommit_called( self, mock_run, mock_tests, mock_detect, mock_autocommit, conn ): """After fix: test_command=NULL + no framework → _run_autocommit IS called.""" from agents.runner import run_pipeline from core import models models.update_project(conn, "sharedbox", auto_test_enabled=True, test_command=None) mock_run.return_value = _mock_agent_success() mock_detect.return_value = None run_pipeline(conn, "SHAREDBOX-003", [{"role": "backend_dev", "brief": "setup db"}]) # After fix: no early return → autocommit is reached mock_autocommit.assert_called_once() @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 ): """After fix: test_command=NULL → _detect_test_command is called for auto-detection.""" from agents.runner import run_pipeline from core import models models.update_project(conn, "sharedbox", auto_test_enabled=True, test_command=None) mock_run.return_value = _mock_agent_success() mock_detect.return_value = None run_pipeline(conn, "SHAREDBOX-003", [{"role": "backend_dev", "brief": "setup db"}]) mock_detect.assert_called_once()