diff --git a/tests/test_quality_gates.py b/tests/test_quality_gates.py new file mode 100644 index 00000000..fef0e721 --- /dev/null +++ b/tests/test_quality_gates.py @@ -0,0 +1,165 @@ +"""Tests for CI Automation Gate and Task Gate. + +Tests the quality gate infrastructure: +- ci_automation_gate.py: function length, linting, trailing whitespace +- task_gate.py: pre/post task validation +""" + +import json +import os +import tempfile +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +import sys +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts")) + +from ci_automation_gate import QualityGate + + +# ========================================================================= +# QualityGate — ci_automation_gate.py +# ========================================================================= + +class TestQualityGateLinting: + """Test trailing whitespace and final newline checks.""" + + def test_clean_file_passes(self, tmp_path): + f = tmp_path / "clean.py" + f.write_text("def foo():\n return 1\n") + gate = QualityGate(fix=False) + gate.check_file(f) + assert gate.failures == 0 + assert gate.warnings == 0 + + def test_trailing_whitespace_warns(self, tmp_path): + f = tmp_path / "messy.py" + f.write_text("def foo(): \n return 1\n") + gate = QualityGate(fix=False) + gate.check_file(f) + assert gate.warnings >= 1 + assert gate.failures == 0 + + def test_missing_final_newline_warns(self, tmp_path): + f = tmp_path / "no_newline.py" + f.write_text("def foo():\n return 1") + gate = QualityGate(fix=False) + gate.check_file(f) + assert gate.warnings >= 1 + + def test_fix_mode_cleans_whitespace(self, tmp_path): + f = tmp_path / "messy.py" + f.write_text("def foo(): \n return 1\n") + gate = QualityGate(fix=True) + gate.check_file(f) + fixed = f.read_text() + assert " \n" not in fixed # trailing spaces removed + assert fixed.endswith("\n") + + def test_fix_mode_adds_final_newline(self, tmp_path): + f = tmp_path / "no_newline.py" + f.write_text("def foo():\n return 1") + gate = QualityGate(fix=True) + gate.check_file(f) + fixed = f.read_text() + assert fixed.endswith("\n") + + +class TestQualityGateFunctionLength: + """Test function length detection for JS/TS files.""" + + def test_short_function_passes(self, tmp_path): + f = tmp_path / "short.js" + f.write_text("function foo() {\n return 1;\n}\n") + gate = QualityGate(fix=False) + gate.check_file(f) + assert gate.failures == 0 + + def test_long_function_warns(self, tmp_path): + body = "\n".join(f" console.log({i});" for i in range(25)) + f = tmp_path / "long.js" + f.write_text(f"function foo() {{\n{body}\n}}\n") + gate = QualityGate(fix=False) + gate.check_file(f) + assert gate.warnings >= 1 + + def test_very_long_function_fails(self, tmp_path): + body = "\n".join(f" console.log({i});" for i in range(55)) + f = tmp_path / "huge.js" + f.write_text(f"function foo() {{\n{body}\n}}\n") + gate = QualityGate(fix=False) + gate.check_file(f) + assert gate.failures >= 1 + + def test_python_files_skip_length_check(self, tmp_path): + """Python files should not trigger JS function length regex.""" + body = "\n".join(f" x = {i}" for i in range(60)) + f = tmp_path / "long.py" + f.write_text(f"def foo():\n{body}\n return x\n") + gate = QualityGate(fix=False) + gate.check_file(f) + assert gate.failures == 0 # JS regex won't match Python + + def test_non_code_files_skipped(self, tmp_path): + f = tmp_path / "readme.md" + f.write_text("# Hello \n\nSome text") + gate = QualityGate(fix=False) + gate.check_file(f) + # .md files should be skipped entirely + assert gate.failures == 0 + assert gate.warnings == 0 + + +class TestQualityGateRun: + """Test the full directory scan.""" + + def test_run_exits_1_on_failure(self, tmp_path): + body = "\n".join(f" console.log({i});" for i in range(55)) + f = tmp_path / "huge.js" + f.write_text(f"function foo() {{\n{body}\n}}\n") + gate = QualityGate(fix=False) + with pytest.raises(SystemExit) as exc: + gate.run(str(tmp_path)) + assert exc.value.code == 1 + + def test_run_exits_0_on_clean(self, tmp_path): + f = tmp_path / "clean.py" + f.write_text("x = 1\n") + gate = QualityGate(fix=False) + gate.run(str(tmp_path)) # should not raise + assert gate.failures == 0 + + def test_run_skips_node_modules(self, tmp_path): + nm = tmp_path / "node_modules" + nm.mkdir() + bad = nm / "huge.js" + body = "\n".join(f" console.log({i});" for i in range(55)) + bad.write_text(f"function foo() {{\n{body}\n}}\n") + gate = QualityGate(fix=False) + gate.run(str(tmp_path)) + assert gate.failures == 0 # node_modules skipped + + +# ========================================================================= +# Task Gate — task_gate.py (integration-level tests) +# ========================================================================= + +class TestTaskGateImports: + """Verify task_gate module is importable.""" + + def test_import_task_gate(self): + from task_gate import FILTER_TAGS, AGENT_USERNAMES + assert isinstance(FILTER_TAGS, list) + assert len(FILTER_TAGS) > 0 + assert isinstance(AGENT_USERNAMES, set) + assert "timmy" in AGENT_USERNAMES + + def test_filter_tags_contain_epic(self): + from task_gate import FILTER_TAGS + assert any("EPIC" in tag for tag in FILTER_TAGS) + + def test_filter_tags_contain_permanent(self): + from task_gate import FILTER_TAGS + assert any("PERMANENT" in tag for tag in FILTER_TAGS)