Skip to content

Commit 223b272

Browse files
Tony363claude
andauthored
test: comprehensive test coverage from 31% to 87% (#90)
* test: comprehensive test coverage from 31% to 87% (#90) Add 1885 tests across 22 new test files covering all scripts/ and .github/scripts/ modules. Key areas: validate_agents, create_prs, apply_autofix, bedrock_helper, notify_slack, security_consensus, generate-docstrings, generate-type-hints, run_stochastic_local, compute_pass_rates, readme_checker, and validate_schema. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: mock requests via sys.modules for CI without requests installed The .github/scripts/notify_slack.py imports requests inside its function body. CI doesn't have requests installed, so tests must use patch.dict("sys.modules", ...) instead of patch("requests.post"). Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 22acba2 commit 223b272

22 files changed

Lines changed: 8964 additions & 1 deletion

coverage.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

tests/github_scripts/__init__.py

Whitespace-only changes.

tests/github_scripts/conftest.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""Conftest for .github/scripts tests.
2+
3+
Mocks the `anthropic` module since it's not installed in the test environment.
4+
The .github/scripts files do `import anthropic` at module level and sys.exit(1)
5+
if it's missing. We need to provide a fake module before importing them.
6+
"""
7+
8+
import sys
9+
import types
10+
from unittest.mock import MagicMock
11+
12+
13+
def _ensure_anthropic_mock():
14+
"""Install a fake anthropic module if the real one isn't available."""
15+
if "anthropic" not in sys.modules:
16+
mock_mod = types.ModuleType("anthropic")
17+
mock_mod.Anthropic = MagicMock
18+
mock_mod.APIError = type("APIError", (Exception,), {})
19+
sys.modules["anthropic"] = mock_mod
20+
21+
22+
_ensure_anthropic_mock()
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
"""Tests for .github/scripts/apply-docstrings.py pure functions."""
2+
3+
import importlib
4+
import json
5+
import sys
6+
from pathlib import Path
7+
8+
scripts_dir = str(Path(__file__).parent.parent.parent / ".github" / "scripts")
9+
if scripts_dir not in sys.path:
10+
sys.path.insert(0, scripts_dir)
11+
12+
mod = importlib.import_module("apply-docstrings")
13+
14+
is_protected = mod.is_protected
15+
find_function_info = mod.find_function_info
16+
find_def_end_line = mod.find_def_end_line
17+
get_body_indent = mod.get_body_indent
18+
format_docstring = mod.format_docstring
19+
apply_docstrings = mod.apply_docstrings
20+
21+
22+
class TestIsProtected:
23+
"""Tests for is_protected."""
24+
25+
def test_workflow_protected(self):
26+
assert is_protected(".github/workflows/ci.yml") is True
27+
28+
def test_agents_protected(self):
29+
assert is_protected("agents/core/dev.md") is True
30+
31+
def test_regular_ok(self):
32+
assert is_protected("src/main.py") is False
33+
34+
35+
class TestFindFunctionInfo:
36+
"""Tests for find_function_info."""
37+
38+
def test_finds_simple_function(self):
39+
source = "def foo():\n pass\n"
40+
info = find_function_info(source, "foo")
41+
assert info is not None
42+
assert info["lineno"] == 1
43+
assert info["has_docstring"] is False
44+
45+
def test_finds_function_with_docstring(self):
46+
source = 'def bar():\n """Hello."""\n pass\n'
47+
info = find_function_info(source, "bar")
48+
assert info is not None
49+
assert info["has_docstring"] is True
50+
51+
def test_finds_async_function(self):
52+
source = "async def baz():\n pass\n"
53+
info = find_function_info(source, "baz")
54+
assert info is not None
55+
assert info["lineno"] == 1
56+
57+
def test_returns_none_for_missing(self):
58+
source = "def foo():\n pass\n"
59+
info = find_function_info(source, "nonexistent")
60+
assert info is None
61+
62+
def test_returns_none_for_syntax_error(self):
63+
source = "def foo(:\n broken syntax"
64+
info = find_function_info(source, "foo")
65+
assert info is None
66+
67+
68+
class TestFindDefEndLine:
69+
"""Tests for find_def_end_line."""
70+
71+
def test_single_line_def(self):
72+
lines = ["def foo():\n", " pass\n"]
73+
assert find_def_end_line(lines, 0) == 0
74+
75+
def test_multiline_def(self):
76+
lines = [
77+
"def foo(\n",
78+
" x,\n",
79+
" y,\n",
80+
"):\n",
81+
" pass\n",
82+
]
83+
assert find_def_end_line(lines, 0) == 3
84+
85+
86+
class TestGetBodyIndent:
87+
"""Tests for get_body_indent."""
88+
89+
def test_standard_indent(self):
90+
lines = ["def foo():\n", " pass\n"]
91+
indent = get_body_indent(lines, 0)
92+
assert indent == " "
93+
94+
def test_nested_indent(self):
95+
lines = ["class X:\n", " def foo():\n", " pass\n"]
96+
indent = get_body_indent(lines, 1)
97+
assert indent == " "
98+
99+
100+
class TestFormatDocstring:
101+
"""Tests for format_docstring."""
102+
103+
def test_single_line(self):
104+
result = format_docstring("Short summary.", " ")
105+
assert len(result) == 1
106+
assert result[0] == ' """Short summary."""\n'
107+
108+
def test_multi_line(self):
109+
result = format_docstring("Summary.\n\nArgs:\n x: value", " ")
110+
assert result[0] == ' """Summary.\n'
111+
assert result[-1] == ' """\n'
112+
113+
114+
class TestApplyDocstrings:
115+
"""Tests for apply_docstrings integration."""
116+
117+
def test_empty_suggestions(self, tmp_path: Path):
118+
p = tmp_path / "suggestions.json"
119+
p.write_text(json.dumps({"functions_documented": []}))
120+
121+
result = apply_docstrings(str(p))
122+
assert result["files_modified"] == 0
123+
assert result["docstrings_inserted"] == 0
124+
125+
def test_applies_docstring_to_file(self, tmp_path: Path):
126+
# Create target file
127+
target = tmp_path / "target.py"
128+
target.write_text("def greet():\n return 'hello'\n")
129+
130+
suggestions = {
131+
"functions_documented": [
132+
{
133+
"file": str(target),
134+
"function": "greet",
135+
"docstring": "Return a greeting.",
136+
}
137+
]
138+
}
139+
p = tmp_path / "suggestions.json"
140+
p.write_text(json.dumps(suggestions))
141+
142+
result = apply_docstrings(str(p))
143+
assert result["docstrings_inserted"] == 1
144+
assert result["files_modified"] == 1
145+
146+
modified = target.read_text()
147+
assert '"""Return a greeting."""' in modified
148+
149+
def test_skips_existing_docstring(self, tmp_path: Path):
150+
target = tmp_path / "target.py"
151+
target.write_text('def greet():\n """Existing."""\n return "hello"\n')
152+
153+
suggestions = {
154+
"functions_documented": [
155+
{
156+
"file": str(target),
157+
"function": "greet",
158+
"docstring": "New docstring.",
159+
}
160+
]
161+
}
162+
p = tmp_path / "suggestions.json"
163+
p.write_text(json.dumps(suggestions))
164+
165+
result = apply_docstrings(str(p))
166+
assert result["skipped_existing"] == 1
167+
assert result["docstrings_inserted"] == 0
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
"""Tests for .github/scripts/apply-type-hints.py pure functions."""
2+
3+
import importlib
4+
import json
5+
import sys
6+
from pathlib import Path
7+
8+
scripts_dir = str(Path(__file__).parent.parent.parent / ".github" / "scripts")
9+
if scripts_dir not in sys.path:
10+
sys.path.insert(0, scripts_dir)
11+
12+
mod = importlib.import_module("apply-type-hints")
13+
14+
is_protected = mod.is_protected
15+
find_function_line = mod.find_function_line
16+
find_def_end_line = mod.find_def_end_line
17+
get_existing_imports = mod.get_existing_imports
18+
find_last_import_line = mod.find_last_import_line
19+
normalize_import = mod.normalize_import
20+
apply_type_hints = mod.apply_type_hints
21+
22+
23+
class TestIsProtected:
24+
"""Tests for is_protected."""
25+
26+
def test_github_scripts_protected(self):
27+
assert is_protected(".github/scripts/helper.py") is True
28+
29+
def test_claude_dir_protected(self):
30+
assert is_protected(".claude/rules/test.md") is True
31+
32+
def test_validate_agents_protected(self):
33+
assert is_protected("scripts/validate_agents.py") is True
34+
35+
def test_regular_ok(self):
36+
assert is_protected("src/service.py") is False
37+
38+
39+
class TestFindFunctionLine:
40+
"""Tests for find_function_line."""
41+
42+
def test_simple_function(self):
43+
source = "x = 1\ndef foo():\n pass\n"
44+
assert find_function_line(source, "foo") == 2
45+
46+
def test_async_function(self):
47+
source = "async def bar():\n pass\n"
48+
assert find_function_line(source, "bar") == 1
49+
50+
def test_not_found(self):
51+
source = "def foo(): pass\n"
52+
assert find_function_line(source, "bar") is None
53+
54+
def test_syntax_error(self):
55+
source = "def incomplete("
56+
assert find_function_line(source, "incomplete") is None
57+
58+
59+
class TestFindDefEndLine:
60+
"""Tests for find_def_end_line."""
61+
62+
def test_single_line(self):
63+
lines = ["def foo():\n", " pass\n"]
64+
assert find_def_end_line(lines, 0) == 0
65+
66+
def test_multiline_signature(self):
67+
lines = ["def foo(\n", " a: int,\n", " b: str,\n", ") -> None:\n", " pass\n"]
68+
assert find_def_end_line(lines, 0) == 3
69+
70+
71+
class TestGetExistingImports:
72+
"""Tests for get_existing_imports."""
73+
74+
def test_finds_imports(self):
75+
source = "import os\nfrom pathlib import Path\n\nx = 1"
76+
imports = get_existing_imports(source)
77+
assert "import os" in imports
78+
assert "from pathlib import Path" in imports
79+
assert len(imports) == 2
80+
81+
def test_no_imports(self):
82+
source = "x = 1\ny = 2"
83+
imports = get_existing_imports(source)
84+
assert imports == set()
85+
86+
87+
class TestFindLastImportLine:
88+
"""Tests for find_last_import_line."""
89+
90+
def test_finds_last(self):
91+
lines = ["import os\n", "from sys import path\n", "\n", "x = 1\n"]
92+
assert find_last_import_line(lines) == 1
93+
94+
def test_no_imports(self):
95+
lines = ["x = 1\n", "y = 2\n"]
96+
assert find_last_import_line(lines) == -1
97+
98+
99+
class TestNormalizeImport:
100+
"""Tests for normalize_import."""
101+
102+
def test_strips_whitespace(self):
103+
result = normalize_import(" from typing import Any ")
104+
assert result == ["from typing import Any"]
105+
106+
107+
class TestApplyTypeHints:
108+
"""Tests for apply_type_hints integration."""
109+
110+
def test_empty_suggestions(self, tmp_path: Path):
111+
p = tmp_path / "suggestions.json"
112+
p.write_text(json.dumps({"functions_annotated": []}))
113+
114+
result = apply_type_hints(str(p))
115+
assert result["files_modified"] == 0
116+
assert result["functions_applied"] == 0
117+
118+
def test_applies_hint_to_file(self, tmp_path: Path):
119+
target = tmp_path / "target.py"
120+
target.write_text("def greet(name):\n return f'hello {name}'\n")
121+
122+
suggestions = {
123+
"functions_annotated": [
124+
{
125+
"file": str(target),
126+
"function": "greet",
127+
"typed_signature": "def greet(name: str) -> str:",
128+
"imports_needed": [],
129+
}
130+
]
131+
}
132+
p = tmp_path / "suggestions.json"
133+
p.write_text(json.dumps(suggestions))
134+
135+
result = apply_type_hints(str(p))
136+
assert result["functions_applied"] == 1
137+
138+
modified = target.read_text()
139+
assert "name: str" in modified
140+
assert "-> str" in modified

0 commit comments

Comments
 (0)