Files
AIPass/tests/test_seedgo_cli.py
T
AIOSAIandClaude Opus 4.6 fc6f83b3e1 ✨ feat(seedgo): add Rich formatting to CLI output
Replace raw ANSI escape codes with Rich library for all human-facing
seedgo CLI output. Adds display.py module with styled components:
header panels, plugin result lines, check items with severity markers,
summary sections, and plugin tables.

- seedgo check: branded header, styled verdicts, hint formatting
- seedgo list: Rich table with bordered columns
- seedgo init: styled success message
- drone_adapter: use direct import instead of parsing CLI output
- Add rich>=13.0 as dependency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 23:29:54 -08:00

1208 lines
44 KiB
Python

"""
Seed Go Phase 2 Tests — CLI, Runner, and Reporter
Covers:
- runner.py: calculate_score, calculate_overall, run_checks, file matching
- reporter.py: human / json / github output formats
- cli.py: init / check / list commands via argparse + subprocess
All tests use tmp_path for isolation. Mock plugins are written as .py files
into .seedgo/plugins/ — no monkeypatching of discovery needed.
sys.path fix ensures src/ is importable when running from the repo root:
python -m pytest tests/test_seedgo_cli.py -v
"""
import json
import subprocess
import sys
from pathlib import Path
import pytest
from seedgo.models import CheckItem, CheckResult, Severity
from seedgo.runner import (
_file_matches_types,
_find_project_files,
calculate_overall,
calculate_score,
run_checks,
)
from seedgo.reporter import report_results
# ---------------------------------------------------------------------------
# Shared fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def tmp_project(tmp_path):
"""Minimal project root with .seedgo/ directory tree."""
seedgo_dir = tmp_path / ".seedgo"
seedgo_dir.mkdir()
(seedgo_dir / "plugins").mkdir()
return tmp_path
@pytest.fixture
def passing_plugin(tmp_project):
"""A plugin that always passes for *.py files."""
code = '''\
PLUGIN_NAME = "always-pass"
PLUGIN_DESCRIPTION = "Always returns a passing result"
FILE_TYPES = ["*.py"]
import sys
_src = str(__import__("pathlib").Path(__file__).parent.parent.parent.parent.parent / "src")
if _src not in sys.path:
sys.path.insert(0, _src)
from seedgo.models import CheckResult, CheckItem, Severity
def check(file_path, config=None):
return CheckResult(
plugin=PLUGIN_NAME,
passed=True,
checks=[CheckItem(name="always-passes", passed=True, message="All good")],
score=100,
file_path=file_path,
)
'''
(tmp_project / ".seedgo" / "plugins" / "always_pass.py").write_text(code)
return tmp_project
@pytest.fixture
def failing_plugin(tmp_project):
"""A plugin that always fails with one ERROR for *.py files."""
code = '''\
PLUGIN_NAME = "always-fail"
PLUGIN_DESCRIPTION = "Always returns a failing result"
FILE_TYPES = ["*.py"]
import sys
_src = str(__import__("pathlib").Path(__file__).parent.parent.parent.parent.parent / "src")
if _src not in sys.path:
sys.path.insert(0, _src)
from seedgo.models import CheckResult, CheckItem, Severity
def check(file_path, config=None):
return CheckResult(
plugin=PLUGIN_NAME,
passed=False,
checks=[
CheckItem(
name="always-fails",
passed=False,
message="This check always fails",
severity=Severity.ERROR,
line=1,
fix_hint="Cannot fix",
)
],
score=0,
file_path=file_path,
)
'''
(tmp_project / ".seedgo" / "plugins" / "always_fail.py").write_text(code)
return tmp_project
@pytest.fixture
def warning_plugin(tmp_project):
"""A plugin that returns a WARNING-level failure for *.py files."""
code = '''\
PLUGIN_NAME = "warn-plugin"
PLUGIN_DESCRIPTION = "Returns a warning"
FILE_TYPES = ["*.py"]
import sys
_src = str(__import__("pathlib").Path(__file__).parent.parent.parent.parent.parent / "src")
if _src not in sys.path:
sys.path.insert(0, _src)
from seedgo.models import CheckResult, CheckItem, Severity
def check(file_path, config=None):
return CheckResult(
plugin=PLUGIN_NAME,
passed=False,
checks=[
CheckItem(
name="warns",
passed=False,
message="A warning issue",
severity=Severity.WARNING,
line=5,
)
],
score=50,
file_path=file_path,
)
'''
(tmp_project / ".seedgo" / "plugins" / "warn_plugin.py").write_text(code)
return tmp_project
@pytest.fixture
def sample_py_file(tmp_project):
"""A Python source file to check."""
src = tmp_project / "src"
src.mkdir()
f = src / "main.py"
f.write_text("def hello(): pass\n")
return f
# ---------------------------------------------------------------------------
# calculate_score tests
# ---------------------------------------------------------------------------
class TestCalculateScore:
def test_no_checks_returns_100(self):
result = CheckResult(plugin="p", passed=True, checks=[])
assert calculate_score(result, {}) == 100
def test_all_passing_checks_returns_100(self):
result = CheckResult(
plugin="p",
passed=True,
checks=[
CheckItem(name="c1", passed=True, message="ok", severity=Severity.ERROR),
CheckItem(name="c2", passed=True, message="ok", severity=Severity.WARNING),
],
)
assert calculate_score(result, {}) == 100
def test_all_failing_errors_returns_0(self):
result = CheckResult(
plugin="p",
passed=False,
checks=[
CheckItem(name="c1", passed=False, message="fail", severity=Severity.ERROR),
CheckItem(name="c2", passed=False, message="fail", severity=Severity.ERROR),
],
)
assert calculate_score(result, {}) == 0
def test_half_errors_failing_returns_50(self):
result = CheckResult(
plugin="p",
passed=False,
checks=[
CheckItem(name="c1", passed=True, message="ok", severity=Severity.ERROR),
CheckItem(name="c2", passed=False, message="fail", severity=Severity.ERROR),
],
)
assert calculate_score(result, {}) == 50
def test_warning_has_half_weight_by_default(self):
"""One passing ERROR (weight 1.0) + one failing WARNING (weight 0.5) = 1/1.5 * 100 = 66."""
result = CheckResult(
plugin="p",
passed=False,
checks=[
CheckItem(name="c1", passed=True, message="ok", severity=Severity.ERROR),
CheckItem(name="c2", passed=False, message="warn", severity=Severity.WARNING),
],
)
score = calculate_score(result, {})
assert score == int((1.0 / 1.5) * 100) # 66
def test_info_has_zero_weight_by_default(self):
"""INFO items don't affect score — one failing INFO should still give 100."""
result = CheckResult(
plugin="p",
passed=True,
checks=[
CheckItem(name="c1", passed=False, message="info", severity=Severity.INFO),
],
)
assert calculate_score(result, {}) == 100
def test_custom_weights_from_config(self):
"""Custom weights override defaults."""
config = {"error_weight": 2.0, "warning_weight": 1.0, "info_weight": 0.0}
result = CheckResult(
plugin="p",
passed=False,
checks=[
CheckItem(name="c1", passed=True, message="ok", severity=Severity.ERROR),
CheckItem(name="c2", passed=False, message="fail", severity=Severity.WARNING),
],
)
# passed_weight = 2.0 (ERROR passed), total_weight = 2.0 + 1.0 = 3.0
# score = int(2/3 * 100) = 66
score = calculate_score(result, config)
assert score == int((2.0 / 3.0) * 100)
def test_all_info_checks_returns_100(self):
"""All INFO (zero weight) → total_weight == 0 → score = 100."""
result = CheckResult(
plugin="p",
passed=True,
checks=[
CheckItem(name="i1", passed=False, message="info", severity=Severity.INFO),
CheckItem(name="i2", passed=False, message="info", severity=Severity.INFO),
],
)
assert calculate_score(result, {}) == 100
# ---------------------------------------------------------------------------
# calculate_overall tests
# ---------------------------------------------------------------------------
class TestCalculateOverall:
def test_empty_results_returns_100_pass(self):
overall = calculate_overall([], {})
assert overall["overall_score"] == 100
assert overall["passed"] is True
assert overall["plugins_passed"] == 0
assert overall["plugins_failed"] == 0
assert overall["error_count"] == 0
def test_single_passing_result(self):
results = [
CheckResult(
plugin="p",
passed=True,
checks=[CheckItem(name="c", passed=True, message="ok")],
)
]
overall = calculate_overall(results, {})
assert overall["overall_score"] == 100
assert overall["passed"] is True
assert overall["plugins_passed"] == 1
assert overall["plugins_failed"] == 0
def test_single_failing_result_with_error(self):
results = [
CheckResult(
plugin="p",
passed=False,
checks=[
CheckItem(name="c", passed=False, message="fail", severity=Severity.ERROR)
],
)
]
overall = calculate_overall(results, {})
assert overall["overall_score"] == 0
assert overall["passed"] is False
assert overall["error_count"] == 1
def test_error_blocks_pass_even_at_high_score(self):
"""An ERROR-severity failure must block pass even if score is above threshold."""
results = [
CheckResult(
plugin="p",
passed=False,
checks=[
CheckItem(name="ok1", passed=True, message="ok", severity=Severity.ERROR),
CheckItem(name="ok2", passed=True, message="ok", severity=Severity.ERROR),
CheckItem(name="ok3", passed=True, message="ok", severity=Severity.ERROR),
CheckItem(name="fail", passed=False, message="fail", severity=Severity.ERROR),
],
)
]
overall = calculate_overall(results, {"threshold": 70})
# Score = 75 (3/4 * 100), above threshold=70 — but error blocks pass
assert overall["overall_score"] == 75
assert overall["error_count"] == 1
assert overall["passed"] is False
def test_warning_does_not_block_pass(self):
"""A WARNING-only failure should allow passing if score >= threshold."""
results = [
CheckResult(
plugin="p",
passed=False,
checks=[
CheckItem(name="ok1", passed=True, message="ok", severity=Severity.ERROR),
CheckItem(name="warn", passed=False, message="warn", severity=Severity.WARNING),
],
)
]
# passed_weight = 1.0, total_weight = 1.5, score = 66
overall = calculate_overall(results, {"threshold": 60})
assert overall["error_count"] == 0
assert overall["warning_count"] == 1
assert overall["passed"] is True # score 66 >= threshold 60, no errors
def test_overall_score_is_mean_of_results(self):
"""overall_score should be the mean of individual result scores."""
r1 = CheckResult(
plugin="p1",
passed=True,
checks=[CheckItem(name="c", passed=True, message="ok", severity=Severity.ERROR)],
)
r2 = CheckResult(
plugin="p2",
passed=False,
checks=[CheckItem(name="c", passed=False, message="fail", severity=Severity.ERROR)],
)
overall = calculate_overall([r1, r2], {})
# r1 score = 100, r2 score = 0, mean = 50
assert overall["overall_score"] == 50
def test_threshold_respected(self):
results = [
CheckResult(
plugin="p",
passed=True,
checks=[CheckItem(name="c", passed=True, message="ok")],
)
]
overall = calculate_overall(results, {"threshold": 90})
assert overall["threshold"] == 90
def test_info_count_tracked(self):
results = [
CheckResult(
plugin="p",
passed=False,
checks=[
CheckItem(name="info", passed=False, message="info", severity=Severity.INFO),
],
)
]
overall = calculate_overall(results, {})
assert overall["info_count"] == 1
assert overall["error_count"] == 0
assert overall["warning_count"] == 0
def test_plugins_passed_and_failed_counts(self):
results = [
CheckResult(
plugin="p1",
passed=True,
checks=[CheckItem(name="c", passed=True, message="ok", severity=Severity.ERROR)],
),
CheckResult(
plugin="p2",
passed=False,
checks=[CheckItem(name="c", passed=False, message="fail", severity=Severity.ERROR)],
),
CheckResult(
plugin="p3",
passed=False,
checks=[CheckItem(name="c", passed=False, message="fail", severity=Severity.ERROR)],
),
]
overall = calculate_overall(results, {"threshold": 75})
assert overall["plugins_passed"] == 1
assert overall["plugins_failed"] == 2
# ---------------------------------------------------------------------------
# File matching tests
# ---------------------------------------------------------------------------
class TestFileMatchesTypes:
def test_py_pattern_matches_py_file(self):
assert _file_matches_types("/path/to/file.py", ["*.py"]) is True
def test_py_pattern_does_not_match_js_file(self):
assert _file_matches_types("/path/to/file.js", ["*.py"]) is False
def test_wildcard_matches_any_file(self):
assert _file_matches_types("/path/to/file.anything", ["*"]) is True
def test_multiple_patterns_any_match(self):
assert _file_matches_types("/path/to/file.ts", ["*.js", "*.ts"]) is True
def test_no_patterns_returns_false(self):
assert _file_matches_types("/path/to/file.py", []) is False
def test_specific_filename_match(self):
assert _file_matches_types("/path/to/Makefile", ["Makefile"]) is True
class TestFindProjectFiles:
def test_finds_py_files_in_project(self, tmp_project):
src = tmp_project / "src"
src.mkdir()
(src / "main.py").write_text("pass")
(src / "utils.py").write_text("pass")
config = {"paths": {"include": ["."], "exclude": []}}
files = _find_project_files(str(tmp_project), config)
file_names = [Path(f).name for f in files]
assert "main.py" in file_names
assert "utils.py" in file_names
def test_excludes_patterns(self, tmp_project):
src = tmp_project / "src"
tests = tmp_project / "tests"
src.mkdir()
tests.mkdir()
(src / "main.py").write_text("pass")
(tests / "test_main.py").write_text("pass")
config = {"paths": {"include": ["."], "exclude": ["tests/"]}}
files = _find_project_files(str(tmp_project), config)
file_names = [Path(f).name for f in files]
assert "main.py" in file_names
assert "test_main.py" not in file_names
def test_specific_include_path(self, tmp_project):
src = tmp_project / "src"
other = tmp_project / "other"
src.mkdir()
other.mkdir()
(src / "main.py").write_text("pass")
(other / "other.py").write_text("pass")
config = {"paths": {"include": ["src"], "exclude": []}}
files = _find_project_files(str(tmp_project), config)
file_names = [Path(f).name for f in files]
assert "main.py" in file_names
assert "other.py" not in file_names
# ---------------------------------------------------------------------------
# run_checks integration tests
# ---------------------------------------------------------------------------
class TestRunChecks:
def test_no_plugins_returns_empty_results_and_pass(self, tmp_project, sample_py_file):
# Filter to a nonexistent plugin name so builtins are also excluded
results, overall = run_checks(str(tmp_project), files=[str(sample_py_file)], plugins=["nonexistent-plugin-xyz"])
assert results == []
assert overall["passed"] is True
assert overall["overall_score"] == 100
def test_passing_plugin_gives_pass(self, passing_plugin, sample_py_file):
# Filter to only the local always-pass plugin to isolate from builtins
results, overall = run_checks(str(passing_plugin), files=[str(sample_py_file)], plugins=["always-pass"])
assert len(results) == 1
assert results[0].passed is True
assert overall["passed"] is True
def test_failing_plugin_gives_fail(self, failing_plugin, sample_py_file):
# Filter to only the local always-fail plugin to isolate from builtins
results, overall = run_checks(str(failing_plugin), files=[str(sample_py_file)], plugins=["always-fail"])
assert len(results) == 1
assert results[0].passed is False
assert overall["passed"] is False
assert overall["error_count"] == 1
def test_plugin_filter_restricts_plugins(self, passing_plugin, failing_plugin, sample_py_file):
"""When plugins filter is given, only those plugins should run."""
# Both plugins are in the same project dir (failing_plugin fixture modifies passing_plugin's dir)
# We need a project with both; use the failing_plugin fixture which wraps passing_plugin
# Actually since fixtures use the same tmp_project, we can't easily combine them.
# Instead test with a fresh project that has one plugin, filtered out.
results, overall = run_checks(
str(passing_plugin),
files=[str(sample_py_file)],
plugins=["nonexistent-plugin"],
)
assert results == []
assert overall["passed"] is True
def test_file_type_filtering(self, passing_plugin):
"""Plugin with FILE_TYPES=['*.py'] should not run on .txt files."""
txt_file = passing_plugin / "src" / "readme.txt"
(passing_plugin / "src").mkdir(exist_ok=True)
txt_file.write_text("hello")
results, overall = run_checks(str(passing_plugin), files=[str(txt_file)])
assert results == []
def test_explicit_files_override_discovery(self, passing_plugin, sample_py_file):
# Filter to only the local always-pass plugin to isolate from builtins
results, overall = run_checks(str(passing_plugin), files=[str(sample_py_file)], plugins=["always-pass"])
# Only the one file we specified should be checked
assert len(results) == 1
assert results[0].file_path == str(sample_py_file)
def test_score_set_on_results(self, passing_plugin, sample_py_file):
results, _ = run_checks(str(passing_plugin), files=[str(sample_py_file)])
assert results[0].score == 100
def test_crashed_plugin_returns_failed_result(self, tmp_project, sample_py_file):
"""A plugin that raises an exception should return a failed CheckResult, not crash."""
code = '''\
PLUGIN_NAME = "crash-plugin"
PLUGIN_DESCRIPTION = "Always crashes"
FILE_TYPES = ["*.py"]
def check(file_path, config=None):
raise RuntimeError("kaboom")
'''
(tmp_project / ".seedgo" / "plugins" / "crash_plugin.py").write_text(code)
# Filter to only the crash plugin to isolate from builtins
results, overall = run_checks(str(tmp_project), files=[str(sample_py_file)], plugins=["crash-plugin"])
assert len(results) == 1
assert results[0].passed is False
assert results[0].score == 0
assert "kaboom" in results[0].metadata.get("error", "")
# ---------------------------------------------------------------------------
# Reporter tests
# ---------------------------------------------------------------------------
class TestReportHuman:
"""Human format prints directly via Rich console. Tests capture stdout."""
def _make_result(self, passed=True, score=100, checks=None):
return CheckResult(
plugin="test-plugin",
passed=passed,
checks=checks or [],
score=score,
file_path="/path/to/file.py",
)
def _overall(self, score=100, passed=True, threshold=75):
return {
"overall_score": score,
"passed": passed,
"threshold": threshold,
"plugins_passed": 1 if passed else 0,
"plugins_failed": 0 if passed else 1,
"error_count": 0,
"warning_count": 0,
"info_count": 0,
}
def _report_captured(self, results, overall, capsys):
"""Call report_results for human format and return captured output."""
report_results(results, overall, format="human")
return capsys.readouterr().out
def test_output_contains_plugin_name(self, capsys):
result = self._make_result()
output = self._report_captured([result], self._overall(), capsys)
assert "test-plugin" in output
def test_passing_result_contains_pass(self, capsys):
result = self._make_result(passed=True, score=100)
output = self._report_captured([result], self._overall(score=100, passed=True), capsys)
assert "PASS" in output
def test_failing_result_contains_fail(self, capsys):
checks = [CheckItem(name="c", passed=False, message="fail", severity=Severity.ERROR)]
result = self._make_result(passed=False, score=0, checks=checks)
output = self._report_captured([result], self._overall(score=0, passed=False), capsys)
assert "FAIL" in output
def test_score_shown_in_output(self, capsys):
result = self._make_result(score=75)
output = self._report_captured([result], self._overall(score=75), capsys)
assert "75" in output
def test_check_item_message_shown(self, capsys):
checks = [CheckItem(name="bare-except", passed=False, message="Found bare except", severity=Severity.ERROR)]
result = self._make_result(passed=False, checks=checks)
output = self._report_captured([result], self._overall(passed=False), capsys)
assert "Found bare except" in output
def test_fix_hint_shown_for_failing_check(self, capsys):
checks = [
CheckItem(
name="c",
passed=False,
message="fail",
severity=Severity.ERROR,
fix_hint="Use except Exception:",
)
]
result = self._make_result(passed=False, checks=checks)
output = self._report_captured([result], self._overall(passed=False), capsys)
assert "Use except Exception:" in output
def test_no_results_shows_no_checks_ran(self, capsys):
output = self._report_captured([], self._overall(), capsys)
assert "No checks ran" in output
def test_threshold_shown_in_summary(self, capsys):
result = self._make_result()
output = self._report_captured([result], self._overall(threshold=80), capsys)
assert "80" in output
def test_line_number_shown_when_present(self, capsys):
checks = [CheckItem(name="c", passed=False, message="fail", severity=Severity.ERROR, line=42)]
result = self._make_result(passed=False, checks=checks)
output = self._report_captured([result], self._overall(passed=False), capsys)
assert "42" in output
def test_invalid_format_raises_value_error(self):
with pytest.raises(ValueError, match="Unknown format"):
report_results([], {}, format="xml")
class TestReportJSON:
def test_output_is_valid_json(self):
results = [CheckResult(plugin="p", passed=True, checks=[], score=100, file_path="/f.py")]
overall = {
"overall_score": 100, "passed": True, "threshold": 75,
"plugins_passed": 1, "plugins_failed": 0,
"error_count": 0, "warning_count": 0, "info_count": 0,
}
output = report_results(results, overall, format="json")
data = json.loads(output)
assert isinstance(data, dict)
def test_json_contains_overall_score(self):
results = [CheckResult(plugin="p", passed=True, checks=[], score=100, file_path="/f.py")]
overall = {
"overall_score": 88, "passed": True, "threshold": 75,
"plugins_passed": 1, "plugins_failed": 0,
"error_count": 0, "warning_count": 0, "info_count": 0,
}
data = json.loads(report_results(results, overall, format="json"))
assert data["overall_score"] == 88
def test_json_contains_results_list(self):
results = [CheckResult(plugin="p", passed=True, checks=[], score=100, file_path="/f.py")]
overall = {
"overall_score": 100, "passed": True, "threshold": 75,
"plugins_passed": 1, "plugins_failed": 0,
"error_count": 0, "warning_count": 0, "info_count": 0,
}
data = json.loads(report_results(results, overall, format="json"))
assert "results" in data
assert len(data["results"]) == 1
assert data["results"][0]["plugin"] == "p"
def test_json_severity_is_string(self):
"""Severity enum must be serialized as string in JSON output."""
checks = [CheckItem(name="c", passed=False, message="fail", severity=Severity.ERROR)]
results = [CheckResult(plugin="p", passed=False, checks=checks, score=0, file_path="/f.py")]
overall = {
"overall_score": 0, "passed": False, "threshold": 75,
"plugins_passed": 0, "plugins_failed": 1,
"error_count": 1, "warning_count": 0, "info_count": 0,
}
data = json.loads(report_results(results, overall, format="json"))
sev = data["results"][0]["checks"][0]["severity"]
assert isinstance(sev, str)
assert sev == "error"
def test_json_passed_field_present(self):
results = []
overall = {
"overall_score": 100, "passed": True, "threshold": 75,
"plugins_passed": 0, "plugins_failed": 0,
"error_count": 0, "warning_count": 0, "info_count": 0,
}
data = json.loads(report_results(results, overall, format="json"))
assert "passed" in data
class TestReportGitHub:
def test_error_produces_error_annotation(self):
checks = [
CheckItem(
name="c",
passed=False,
message="Missing type hint",
severity=Severity.ERROR,
line=42,
)
]
results = [CheckResult(plugin="type-hints", passed=False, checks=checks, score=0, file_path="src/main.py")]
overall = {
"overall_score": 0, "passed": False, "threshold": 75,
"plugins_passed": 0, "plugins_failed": 1,
"error_count": 1, "warning_count": 0, "info_count": 0,
}
output = report_results(results, overall, format="github")
assert "::error" in output
assert "Missing type hint" in output
def test_warning_produces_warning_annotation(self):
checks = [
CheckItem(
name="c",
passed=False,
message="Class too large",
severity=Severity.WARNING,
line=15,
)
]
results = [CheckResult(plugin="no-god-objects", passed=False, checks=checks, score=50, file_path="src/models.py")]
overall = {
"overall_score": 50, "passed": False, "threshold": 75,
"plugins_passed": 0, "plugins_failed": 1,
"error_count": 0, "warning_count": 1, "info_count": 0,
}
output = report_results(results, overall, format="github")
assert "::warning" in output
assert "Class too large" in output
def test_file_and_line_in_annotation(self):
checks = [
CheckItem(name="c", passed=False, message="msg", severity=Severity.ERROR, line=7)
]
results = [CheckResult(plugin="p", passed=False, checks=checks, score=0, file_path="/some/file.py")]
overall = {
"overall_score": 0, "passed": False, "threshold": 75,
"plugins_passed": 0, "plugins_failed": 1,
"error_count": 1, "warning_count": 0, "info_count": 0,
}
output = report_results(results, overall, format="github")
assert "file=/some/file.py" in output
assert "line=7" in output
def test_passing_checks_not_in_github_output(self):
checks = [
CheckItem(name="ok", passed=True, message="all good", severity=Severity.ERROR),
]
results = [CheckResult(plugin="p", passed=True, checks=checks, score=100, file_path="/f.py")]
overall = {
"overall_score": 100, "passed": True, "threshold": 75,
"plugins_passed": 1, "plugins_failed": 0,
"error_count": 0, "warning_count": 0, "info_count": 0,
}
output = report_results(results, overall, format="github")
assert "::error" not in output
assert "::warning" not in output
def test_no_results_produces_empty_output(self):
overall = {
"overall_score": 100, "passed": True, "threshold": 75,
"plugins_passed": 0, "plugins_failed": 0,
"error_count": 0, "warning_count": 0, "info_count": 0,
}
output = report_results([], overall, format="github")
assert output == ""
def test_plugin_name_included_in_annotation(self):
checks = [CheckItem(name="c", passed=False, message="fail", severity=Severity.ERROR)]
results = [CheckResult(plugin="my-plugin", passed=False, checks=checks, score=0, file_path="/f.py")]
overall = {
"overall_score": 0, "passed": False, "threshold": 75,
"plugins_passed": 0, "plugins_failed": 1,
"error_count": 1, "warning_count": 0, "info_count": 0,
}
output = report_results(results, overall, format="github")
assert "my-plugin" in output
# ---------------------------------------------------------------------------
# CLI tests — via subprocess to test the real entry point
# ---------------------------------------------------------------------------
class TestCLIInit:
def test_init_creates_config_file(self, tmp_path):
subprocess.run(
[sys.executable, "-m", "seedgo.cli"],
input="",
capture_output=True,
text=True,
cwd=str(tmp_path),
env={**__import__("os").environ, "PYTHONPATH": str(Path(__file__).parent.parent / "src")},
)
# The above runs cli as module — we need the main() entry
# Actually test by calling main() directly via runner script
pass # Covered by direct function tests below
def test_init_via_main_creates_config(self, tmp_path):
"""Call _cmd_init directly to test config creation."""
from seedgo.cli import _cmd_init
import argparse
args = argparse.Namespace(profile=None)
old_cwd = __import__("os").getcwd()
try:
__import__("os").chdir(str(tmp_path))
_cmd_init(args)
finally:
__import__("os").chdir(old_cwd)
assert (tmp_path / ".seedgo" / "config.json").exists()
def test_init_with_profile_embeds_profile(self, tmp_path):
from seedgo.cli import _cmd_init
import argparse
args = argparse.Namespace(profile="python-basic")
old_cwd = __import__("os").getcwd()
try:
__import__("os").chdir(str(tmp_path))
_cmd_init(args)
finally:
__import__("os").chdir(old_cwd)
config_path = tmp_path / ".seedgo" / "config.json"
data = json.loads(config_path.read_text())
assert data["profile"] == "python-basic"
def test_init_fails_if_config_exists(self, tmp_project):
"""If config already exists, init should exit with code 1."""
from seedgo.cli import _cmd_init
import argparse
(tmp_project / ".seedgo" / "config.json").write_text("{}")
args = argparse.Namespace(profile=None)
old_cwd = __import__("os").getcwd()
try:
__import__("os").chdir(str(tmp_project))
with pytest.raises(SystemExit) as exc_info:
_cmd_init(args)
finally:
__import__("os").chdir(old_cwd)
assert exc_info.value.code == 1
def test_init_creates_plugins_dir(self, tmp_path):
from seedgo.cli import _cmd_init
import argparse
args = argparse.Namespace(profile=None)
old_cwd = __import__("os").getcwd()
try:
__import__("os").chdir(str(tmp_path))
_cmd_init(args)
finally:
__import__("os").chdir(old_cwd)
assert (tmp_path / ".seedgo" / "plugins").is_dir()
class TestCLICheck:
def test_check_with_no_plugins_exits_0(self, tmp_project, sample_py_file):
"""No matching plugins = no failures = exit code 0."""
from seedgo.cli import _cmd_check
import argparse
# Filter to a nonexistent plugin so builtins are also excluded
args = argparse.Namespace(
files=[str(sample_py_file)],
format="human",
threshold=None,
plugins=["nonexistent-plugin-xyz"],
)
old_cwd = __import__("os").getcwd()
try:
__import__("os").chdir(str(tmp_project))
with pytest.raises(SystemExit) as exc_info:
_cmd_check(args)
finally:
__import__("os").chdir(old_cwd)
assert exc_info.value.code == 0
def test_check_with_passing_plugin_exits_0(self, passing_plugin, sample_py_file):
from seedgo.cli import _cmd_check
import argparse
# Filter to only the local always-pass plugin to isolate from builtins
args = argparse.Namespace(
files=[str(sample_py_file)],
format="human",
threshold=None,
plugins=["always-pass"],
)
old_cwd = __import__("os").getcwd()
try:
__import__("os").chdir(str(passing_plugin))
with pytest.raises(SystemExit) as exc_info:
_cmd_check(args)
finally:
__import__("os").chdir(old_cwd)
assert exc_info.value.code == 0
def test_check_with_failing_plugin_exits_1(self, failing_plugin, sample_py_file):
from seedgo.cli import _cmd_check
import argparse
args = argparse.Namespace(
files=[str(sample_py_file)],
format="human",
threshold=None,
plugins=None,
)
old_cwd = __import__("os").getcwd()
try:
__import__("os").chdir(str(failing_plugin))
with pytest.raises(SystemExit) as exc_info:
_cmd_check(args)
finally:
__import__("os").chdir(old_cwd)
assert exc_info.value.code == 1
def test_check_json_format_produces_valid_json(self, passing_plugin, sample_py_file, capsys):
from seedgo.cli import _cmd_check
import argparse
args = argparse.Namespace(
files=[str(sample_py_file)],
format="json",
threshold=None,
plugins=None,
)
old_cwd = __import__("os").getcwd()
try:
__import__("os").chdir(str(passing_plugin))
with pytest.raises(SystemExit):
_cmd_check(args)
finally:
__import__("os").chdir(old_cwd)
captured = capsys.readouterr()
data = json.loads(captured.out)
assert "overall_score" in data
def test_check_threshold_override(self, passing_plugin, sample_py_file):
"""--threshold 101 should force a fail even with a passing plugin."""
from seedgo.cli import _cmd_check
import argparse
args = argparse.Namespace(
files=[str(sample_py_file)],
format="human",
threshold=101, # impossible to reach
plugins=None,
)
old_cwd = __import__("os").getcwd()
try:
__import__("os").chdir(str(passing_plugin))
with pytest.raises(SystemExit) as exc_info:
_cmd_check(args)
finally:
__import__("os").chdir(old_cwd)
assert exc_info.value.code == 1
def test_check_without_seedgo_dir_exits_1(self, tmp_path, capsys):
"""Running check outside a project (no .seedgo/) should exit 1 with error."""
from seedgo.cli import _cmd_check
import argparse
args = argparse.Namespace(
files=[],
format="human",
threshold=None,
plugins=None,
)
old_cwd = __import__("os").getcwd()
try:
__import__("os").chdir(str(tmp_path))
with pytest.raises(SystemExit) as exc_info:
_cmd_check(args)
finally:
__import__("os").chdir(old_cwd)
assert exc_info.value.code == 1
class TestCLIList:
def test_list_with_no_plugins_prints_message(self, tmp_project, capsys):
# With builtin plugins now shipping, list always shows at least the builtins.
# The test verifies list exits 0 and produces output — specific message
# check updated to reflect that builtins are always discovered.
from seedgo.cli import _cmd_list
import argparse
args = argparse.Namespace()
old_cwd = __import__("os").getcwd()
try:
__import__("os").chdir(str(tmp_project))
with pytest.raises(SystemExit) as exc_info:
_cmd_list(args)
finally:
__import__("os").chdir(old_cwd)
captured = capsys.readouterr()
assert exc_info.value.code == 0
# Builtins are always present now — output should list them
assert "plugin" in captured.out.lower()
def test_list_shows_plugin_names(self, passing_plugin, capsys):
from seedgo.cli import _cmd_list
import argparse
args = argparse.Namespace()
old_cwd = __import__("os").getcwd()
try:
__import__("os").chdir(str(passing_plugin))
with pytest.raises(SystemExit):
_cmd_list(args)
finally:
__import__("os").chdir(old_cwd)
captured = capsys.readouterr()
assert "always-pass" in captured.out
def test_list_shows_source(self, passing_plugin, capsys):
from seedgo.cli import _cmd_list
import argparse
args = argparse.Namespace()
old_cwd = __import__("os").getcwd()
try:
__import__("os").chdir(str(passing_plugin))
with pytest.raises(SystemExit):
_cmd_list(args)
finally:
__import__("os").chdir(old_cwd)
captured = capsys.readouterr()
assert "local" in captured.out
def test_list_shows_file_types(self, passing_plugin, capsys):
from seedgo.cli import _cmd_list
import argparse
args = argparse.Namespace()
old_cwd = __import__("os").getcwd()
try:
__import__("os").chdir(str(passing_plugin))
with pytest.raises(SystemExit):
_cmd_list(args)
finally:
__import__("os").chdir(old_cwd)
captured = capsys.readouterr()
assert "*.py" in captured.out
def test_list_exits_0(self, tmp_project):
from seedgo.cli import _cmd_list
import argparse
args = argparse.Namespace()
old_cwd = __import__("os").getcwd()
try:
__import__("os").chdir(str(tmp_project))
with pytest.raises(SystemExit) as exc_info:
_cmd_list(args)
finally:
__import__("os").chdir(old_cwd)
assert exc_info.value.code == 0
# ---------------------------------------------------------------------------
# CLI main() argparse dispatch tests
# ---------------------------------------------------------------------------
class TestCLIMain:
"""Test the main() entry point's argparse dispatch via sys.argv patching."""
def test_main_no_args_exits_0(self):
"""main() with no command prints help and exits 0."""
from seedgo.cli import main
with pytest.MonkeyPatch().context() as mp:
mp.setattr(sys, "argv", ["seedgo"])
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 0
def test_main_init_command_dispatches(self, tmp_path):
"""main() with 'init' dispatches to _cmd_init."""
from seedgo.cli import main
with pytest.MonkeyPatch().context() as mp:
mp.setattr(sys, "argv", ["seedgo", "init"])
mp.chdir(tmp_path)
main()
assert (tmp_path / ".seedgo" / "config.json").exists()
def test_main_init_with_profile(self, tmp_path):
"""main() with 'init --profile python-basic' passes profile through."""
from seedgo.cli import main
with pytest.MonkeyPatch().context() as mp:
mp.setattr(sys, "argv", ["seedgo", "init", "--profile", "python-basic"])
mp.chdir(tmp_path)
main()
data = json.loads((tmp_path / ".seedgo" / "config.json").read_text())
assert data["profile"] == "python-basic"
def test_main_list_command_dispatches(self, tmp_project, capsys):
"""main() with 'list' dispatches to _cmd_list."""
from seedgo.cli import main
with pytest.MonkeyPatch().context() as mp:
mp.setattr(sys, "argv", ["seedgo", "list"])
mp.chdir(tmp_project)
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 0
def test_main_check_no_seedgo_exits_1(self, tmp_path, capsys):
"""main() check outside a project exits 1."""
from seedgo.cli import main
with pytest.MonkeyPatch().context() as mp:
mp.setattr(sys, "argv", ["seedgo", "check"])
mp.chdir(tmp_path)
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 1
def test_main_check_with_passing_plugin(self, passing_plugin, sample_py_file):
"""main() check with passing plugin exits 0."""
from seedgo.cli import main
with pytest.MonkeyPatch().context() as mp:
# Filter to only the local always-pass plugin to isolate from builtins
mp.setattr(sys, "argv", ["seedgo", "check", str(sample_py_file), "--plugin", "always-pass"])
mp.chdir(passing_plugin)
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 0
def test_main_check_github_format(self, tmp_project, sample_py_file, capsys):
"""main() check --format github produces annotation-style output (empty if passing)."""
from seedgo.cli import main
with pytest.MonkeyPatch().context() as mp:
# Filter to a nonexistent plugin so builtins are also excluded — pure format test
mp.setattr(sys, "argv", [
"seedgo", "check", str(sample_py_file),
"--format", "github",
"--plugin", "nonexistent-plugin-xyz",
])
mp.chdir(tmp_project)
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 0
def test_main_check_nonexistent_file_exits_1(self, tmp_project, capsys):
"""main() check with a file that doesn't exist exits 1."""
from seedgo.cli import main
with pytest.MonkeyPatch().context() as mp:
mp.setattr(sys, "argv", ["seedgo", "check", "/nonexistent/file.py"])
mp.chdir(tmp_project)
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 1
# ---------------------------------------------------------------------------
# Public API export test
# ---------------------------------------------------------------------------
class TestPublicAPIPhase2:
def test_run_checks_importable_from_seedgo(self):
from seedgo import run_checks as rc
assert callable(rc)
def test_run_checks_in_dunder_all(self):
import seedgo
assert "run_checks" in seedgo.__all__