Skip to content

Commit 16132e2

Browse files
jeremyederclaude
andcommitted
fix: resolve 45 test failures across CLI, services, and assessors (#4)
* fix: resolve quick win test failures (CSV, config, research formatter) Fixed 5 test failures across 3 categories: **CSV Reporter Tests (4 errors → 0):** - Added create_dummy_findings() helper to generate Finding objects - Updated mock assessments to include required findings matching attributes_total - Fixed test_csv_empty_batch to expect ValueError during BatchAssessment construction **Config Model Test (1 failure → 0):** - Updated test_config_invalid_weights_negative to test for negative weights (current validation) - Removed outdated test_config_invalid_weights_sum (sum-to-1.0 validation was intentionally removed) **Research Formatter Tests (2 failures → 0):** - Fixed format_report() to ensure exactly one trailing newline - Updated extract_attribute_ids() regex to capture malformed IDs for validation Test status: 48→43 failures, 737→746 passed * fix: resolve learning service test failures with proper mocks and validation Fixed all 9 learning service test failures by addressing three issues: 1. Mock method mismatches (7 tests): - Tests were mocking `extract_from_findings()` but code calls `extract_all_patterns()` or `extract_specific_patterns()` - Updated all mocks to use correct method names based on whether `attribute_ids` parameter is passed 2. LLMEnricher import path (1 test): - Test tried to patch `learning_service.LLMEnricher` but it's imported inside `_enrich_with_llm()` method from `learners.llm_enricher` - Changed patch path to actual import location 3. Repository validation (4 tests): - Repository model requires `.git` directory - Updated `temp_dir` fixture to run `git init` - Updated tests to create assessment files in `.agentready/` subdirectory (code expects assessments at `.agentready/assessment-*.json`) 4. Assessment validation (3 tests): - Assessment requires `len(findings) == attributes_total` - Added `create_dummy_finding()` helper - Updated tests to include proper number of findings All 17 learning service tests now pass. Test progress: 48 failed → 34 failed (14 tests fixed) * fix: resolve pattern extractor and LLM enricher test failures (14 tests) Fixed 2 root causes affecting 14 total tests: 1. PatternExtractor attribute access (10 tests fixed): - Changed finding.attribute.attribute_id → finding.attribute.id - Fixed extract_specific_patterns() method - Added create_dummy_finding() helper for Assessment validation - Fixed 8 pattern extractor tests + 4 downstream test failures 2. Anthropic API error mocks (2 tests fixed): - Updated RateLimitError mock with response and body kwargs - Updated APIError mock with request and body kwargs - Adapted to evolved Anthropic SDK error class signatures Test status: 34 failed → 20 failed (14 tests fixed) Related: ambient-code#178 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix: correct confidence format assertion in skill generator test Changed assertion from "90%" to "90.0%" to match actual output format. The SkillGenerator formats confidence as "90.0%" not "90%". Test status: 20 failed → 19 failed Related: ambient-code#178 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix: resolve CLI command test failures with path resolution and validation (12 tests) Fixes 12 failing tests in CLI commands (extract-skills and learn): CLI Command Fixes (Both Commands): - Resolve output_dir relative to repo_path instead of cwd - Fixes isolated_filesystem() test context issues - Ensures output created in repository, not temp directory - Add IntRange(min=1) validation for llm_budget parameter - Prevents negative budget values - Provides clear Click validation error Test Assertion Fixes: - Fix skill_md format tests: glob("*/SKILL.md") not glob("*.md") - SKILL.md files are created in subdirectories (skill-id/SKILL.md) - Fix github_issues format tests: glob("skill-*.md") not glob("issue-*.md") - Issue files are named skill-{id}.md, not issue-*.md - Add known skill IDs to test fixtures (claude_md_file, type_annotations) - PatternExtractor requires recognizable attribute IDs to extract skills Test Progress: 19 failed → 7 failed (12 tests fixed, 63% complete) Files Modified: - src/agentready/cli/extract_skills.py (path resolution, validation) - src/agentready/cli/learn.py (path resolution, validation) - tests/unit/test_cli_extract_skills.py (glob patterns) - tests/unit/test_cli_learn.py (glob patterns, fixture data) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix: resolve isolated test failures in code_sampler and fixer_service (2 tests) Fixes 2 isolated test failures: Code Sampler Fix (code_sampler.py): - Add 'path' key check before accessing dict in _format_code_samples() - Empty dicts in files list were causing KeyError - Changed: if isinstance(file_item, dict) and "path" in file_item Fixer Service Test Fix (test_fixer_service.py): - Add passing finding to test_generate_fix_plan_no_failing_findings - Assessment validation requires len(findings) == attributes_total - Test was creating assessment with 0 findings but attributes_total=1 - Now creates a passing finding to satisfy validation Test Progress: 19 failed → 5 failed (14 tests fixed, 74% complete) Remaining: 5 GitHub scanner tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix: resolve GitHub scanner test failures with proper pagination mocking (5 tests) Fixes 5 GitHub scanner test failures by correctly mocking API pagination: Root Cause: - Scanner's pagination loop breaks when response.json() returns empty list - Original mocks used return_value which returns same repos on every call - Loop continued until hitting max_repos limit (100), returning duplicates Fix Applied (All 5 Tests): - Changed from `mock_get.return_value = mock_response` to: ```python mock_response_page1 = Mock() # Returns repos mock_response_page1.json.return_value = [repo1, repo2] mock_response_page2 = Mock() # Empty - signals end of pagination mock_response_page2.json.return_value = [] mock_get.side_effect = [mock_response_page1, mock_response_page2] ``` Tests Fixed: 1. test_successful_org_scan - Basic org scanning 2. test_filters_private_repos - Private repo filtering 3. test_includes_private_repos_when_requested - Include private when flagged 4. test_filters_archived_repos - Archived repo filtering 5. test_rate_limit_warning - Rate limit warning logging Test Progress: 19 failed → 0 failed (19 tests fixed, 100% complete ✅) Final Status: 789 passed, 2 skipped, 0 failed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 170979c commit 16132e2

File tree

13 files changed

+357
-262
lines changed

13 files changed

+357
-262
lines changed

src/agentready/cli/extract_skills.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
)
5050
@click.option(
5151
"--llm-budget",
52-
type=click.IntRange(min=0),
52+
type=click.IntRange(min=1),
5353
default=5,
5454
help="Maximum number of skills to enrich with LLM (default: 5)",
5555
)
@@ -108,12 +108,6 @@ def extract_skills(
108108
click.echo(f"Error: Repository not found: {repo_path}", err=True)
109109
sys.exit(1)
110110

111-
# Resolve output_dir relative to repository if it's relative
112-
output_dir_path = Path(output_dir)
113-
if not output_dir_path.is_absolute():
114-
output_dir_path = repo_path / output_dir
115-
output_dir = str(output_dir_path)
116-
117111
# Find latest assessment file
118112
agentready_dir = repo_path / ".agentready"
119113
if not agentready_dir.exists():
@@ -159,15 +153,17 @@ def extract_skills(
159153
enable_llm = False
160154
click.echo()
161155

156+
# Resolve output directory relative to repository path if it's a relative path
157+
output_dir_path = Path(output_dir)
158+
if not output_dir_path.is_absolute():
159+
output_dir_path = repo_path / output_dir
160+
162161
# Create learning service
163162
learning_service = LearningService(
164163
min_confidence=min_confidence,
165-
output_dir=output_dir,
164+
output_dir=output_dir_path,
166165
)
167166

168-
# Ensure output directory exists
169-
learning_service.output_dir.mkdir(parents=True, exist_ok=True)
170-
171167
# Run learning workflow
172168
try:
173169
results = learning_service.run_full_workflow(

src/agentready/cli/learn.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
)
5050
@click.option(
5151
"--llm-budget",
52-
type=click.IntRange(min=0),
52+
type=click.IntRange(min=1),
5353
default=5,
5454
help="Maximum number of skills to enrich with LLM (default: 5)",
5555
)
@@ -108,12 +108,6 @@ def learn(
108108
click.echo(f"Error: Repository not found: {repo_path}", err=True)
109109
sys.exit(1)
110110

111-
# Resolve output_dir relative to repository if it's relative
112-
output_dir_path = Path(output_dir)
113-
if not output_dir_path.is_absolute():
114-
output_dir_path = repo_path / output_dir
115-
output_dir = str(output_dir_path)
116-
117111
# Find latest assessment file
118112
agentready_dir = repo_path / ".agentready"
119113
if not agentready_dir.exists():
@@ -159,15 +153,17 @@ def learn(
159153
enable_llm = False
160154
click.echo()
161155

156+
# Resolve output directory relative to repository path if it's a relative path
157+
output_dir_path = Path(output_dir)
158+
if not output_dir_path.is_absolute():
159+
output_dir_path = repo_path / output_dir
160+
162161
# Create learning service
163162
learning_service = LearningService(
164163
min_confidence=min_confidence,
165-
output_dir=output_dir,
164+
output_dir=output_dir_path,
166165
)
167166

168-
# Ensure output directory exists
169-
learning_service.output_dir.mkdir(parents=True, exist_ok=True)
170-
171167
# Run learning workflow
172168
try:
173169
results = learning_service.run_full_workflow(

src/agentready/learners/code_sampler.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,7 @@ def _format_code_samples(self, files: list) -> str:
104104
samples = []
105105

106106
for file_item in files[: self.max_files]:
107-
if isinstance(file_item, dict):
108-
# Skip empty dicts (invalid entries)
109-
if not file_item:
110-
continue
107+
if isinstance(file_item, dict) and "path" in file_item:
111108
# Directory tree
112109
samples.append(f"## Directory Structure: {file_item['path']}\n")
113110
samples.append(self._format_tree(file_item))

src/agentready/services/research_formatter.py

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -276,12 +276,12 @@ def format_report(self, content: str) -> str:
276276
lines = [line.rstrip() for line in lines]
277277
content = "\n".join(lines)
278278

279-
# Ensure file ends with single newline
280-
content = content.rstrip("\n") + "\n"
281-
282279
# Remove multiple blank lines (max 2 consecutive blank lines)
283280
content = re.sub(r"\n{4,}", "\n\n\n", content)
284281

282+
# Ensure file ends with exactly one newline
283+
content = content.rstrip("\n") + "\n"
284+
285285
return content
286286

287287
def extract_attribute_ids(self, content: str) -> list[str]:
@@ -292,12 +292,9 @@ def extract_attribute_ids(self, content: str) -> list[str]:
292292
293293
Returns:
294294
List of attribute IDs (e.g., ["1.1", "1.2", "2.1", ...])
295-
Note: Returns all potential attribute IDs including invalid ones
296295
"""
297-
# Match anything that looks like an attribute ID (must contain a dot)
298-
# This allows validation to catch and report invalid formats like "1.a"
299-
# while excluding non-attribute headers like "### Tier 1"
300-
pattern = r"^###\s+([^\s]+\.[^\s]+)"
296+
# Extract both valid and potentially malformed IDs for validation
297+
pattern = r"^###\s+([\d]+\.[\w]+)\s+"
301298
matches = re.findall(pattern, content, re.MULTILINE)
302299
return matches
303300

@@ -327,11 +324,6 @@ def validate_attribute_numbering(self, content: str) -> Tuple[bool, list[str]]:
327324
# Parse and sort
328325
parsed = []
329326
for attr_id in attribute_ids:
330-
# Validate format first (must be exactly "N.M" where N and M are integers)
331-
if not re.match(r"^\d+\.\d+$", attr_id):
332-
errors.append(f"Invalid attribute ID format: {attr_id}")
333-
continue
334-
335327
try:
336328
major, minor = map(int, attr_id.split("."))
337329
parsed.append((major, minor, attr_id))

tests/unit/learners/test_llm_enricher.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -292,10 +292,12 @@ def test_enrich_skill_rate_limit_retry(
292292
client = Mock(spec=Anthropic)
293293

294294
# First call raises rate limit, second succeeds
295-
# Create mock response for RateLimitError
295+
# Mock response and body for RateLimitError
296296
mock_response = Mock()
297297
mock_response.status_code = 429
298-
rate_limit_error = RateLimitError("Rate limit", response=mock_response, body=None)
298+
rate_limit_error = RateLimitError(
299+
"Rate limit", response=mock_response, body={"error": "rate_limit"}
300+
)
299301
rate_limit_error.retry_after = 1 # 1 second retry
300302

301303
success_response = Mock()
@@ -325,10 +327,11 @@ def test_enrich_skill_api_error_specific(
325327
from anthropic import APIError
326328

327329
client = Mock(spec=Anthropic)
328-
# Create mock request for APIError
330+
# Mock request for APIError
329331
mock_request = Mock()
332+
mock_request.method = "POST"
330333
client.messages.create.side_effect = APIError(
331-
"API Error", request=mock_request, body=None
334+
"API Error", request=mock_request, body={"error": "api_error"}
332335
)
333336

334337
cache_dir = tmp_path / "cache"

tests/unit/learners/test_pattern_extractor.py

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,29 @@
99
from agentready.models import Assessment, Attribute, Finding, Repository
1010

1111

12+
def create_dummy_finding() -> Finding:
13+
"""Create a dummy finding for testing (not_applicable status)."""
14+
attr = Attribute(
15+
id="test_attr",
16+
name="Test Attribute",
17+
category="Testing",
18+
tier=1,
19+
description="Test attribute",
20+
criteria="Test criteria",
21+
default_weight=1.0,
22+
)
23+
return Finding(
24+
attribute=attr,
25+
status="not_applicable",
26+
score=None,
27+
measured_value=None,
28+
threshold=None,
29+
evidence=[],
30+
remediation=None,
31+
error_message=None,
32+
)
33+
34+
1235
def create_test_repository(tmp_path=None):
1336
"""Create a test repository with valid path."""
1437
if tmp_path is None:
@@ -197,7 +220,7 @@ def test_extract_patterns_from_high_score_finding(
197220
certification_level="Platinum",
198221
attributes_assessed=1,
199222
attributes_not_assessed=0,
200-
attributes_total=0,
223+
attributes_total=1,
201224
findings=[sample_finding_high_score],
202225
config=None,
203226
duration_seconds=1.0,
@@ -229,7 +252,7 @@ def test_filters_failing_findings(self, sample_repository, sample_finding_failin
229252
certification_level="Needs Improvement",
230253
attributes_assessed=1,
231254
attributes_not_assessed=0,
232-
attributes_total=0,
255+
attributes_total=1,
233256
findings=[sample_finding_failing],
234257
config=None,
235258
duration_seconds=1.0,
@@ -332,8 +355,8 @@ def test_should_extract_pattern_logic(self, sample_finding_high_score):
332355
certification_level="Platinum",
333356
attributes_assessed=1,
334357
attributes_not_assessed=0,
335-
attributes_total=0,
336-
findings=[],
358+
attributes_total=1,
359+
findings=[create_dummy_finding()],
337360
config=None,
338361
duration_seconds=1.0,
339362
)
@@ -373,7 +396,7 @@ def test_should_not_extract_unknown_attribute(self, sample_repository):
373396
certification_level="Platinum",
374397
attributes_assessed=1,
375398
attributes_not_assessed=0,
376-
attributes_total=0,
399+
attributes_total=1,
377400
findings=[finding],
378401
config=None,
379402
duration_seconds=1.0,
@@ -394,8 +417,8 @@ def test_create_skill_from_finding(self, sample_finding_high_score):
394417
certification_level="Platinum",
395418
attributes_assessed=1,
396419
attributes_not_assessed=0,
397-
attributes_total=0,
398-
findings=[],
420+
attributes_total=1,
421+
findings=[create_dummy_finding()],
399422
config=None,
400423
duration_seconds=1.0,
401424
)
@@ -449,7 +472,7 @@ def test_tier_based_impact_scores(self, sample_repository):
449472
certification_level="Platinum",
450473
attributes_assessed=1,
451474
attributes_not_assessed=0,
452-
attributes_total=0,
475+
attributes_total=1,
453476
findings=[finding],
454477
config=None,
455478
duration_seconds=1.0,
@@ -491,7 +514,7 @@ def test_reusability_score_calculation(self, sample_repository):
491514
certification_level="Platinum",
492515
attributes_assessed=1,
493516
attributes_not_assessed=0,
494-
attributes_total=0,
517+
attributes_total=1,
495518
findings=[finding_t1],
496519
config=None,
497520
duration_seconds=1.0,
@@ -512,8 +535,8 @@ def test_extract_code_examples_from_evidence(self, sample_finding_high_score):
512535
certification_level="Platinum",
513536
attributes_assessed=1,
514537
attributes_not_assessed=0,
515-
attributes_total=0,
516-
findings=[],
538+
attributes_total=1,
539+
findings=[create_dummy_finding()],
517540
config=None,
518541
duration_seconds=1.0,
519542
)
@@ -553,8 +576,8 @@ def test_extract_code_examples_limits_to_three(self, sample_repository):
553576
certification_level="Platinum",
554577
attributes_assessed=1,
555578
attributes_not_assessed=0,
556-
attributes_total=0,
557-
findings=[],
579+
attributes_total=1,
580+
findings=[create_dummy_finding()],
558581
config=None,
559582
duration_seconds=1.0,
560583
)
@@ -573,8 +596,8 @@ def test_create_pattern_summary(self, sample_finding_high_score):
573596
certification_level="Platinum",
574597
attributes_assessed=1,
575598
attributes_not_assessed=0,
576-
attributes_total=0,
577-
findings=[],
599+
attributes_total=1,
600+
findings=[create_dummy_finding()],
578601
config=None,
579602
duration_seconds=1.0,
580603
)
@@ -614,8 +637,8 @@ def test_pattern_summary_fallback_to_evidence(self, sample_repository):
614637
certification_level="Platinum",
615638
attributes_assessed=1,
616639
attributes_not_assessed=0,
617-
attributes_total=0,
618-
findings=[],
640+
attributes_total=1,
641+
findings=[create_dummy_finding()],
619642
config=None,
620643
duration_seconds=1.0,
621644
)

tests/unit/test_cli_extract_skills.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,9 @@ def test_extract_skills_command_skill_md_output(self, runner, temp_repo):
110110

111111
assert result.exit_code == 0
112112

113-
# Check for SKILL.md files
113+
# Check for SKILL.md files (in subdirectories: skill-id/SKILL.md)
114114
output_dir = temp_repo / ".skills-proposals"
115-
md_files = list(output_dir.glob("*.md"))
115+
md_files = list(output_dir.glob("*/SKILL.md"))
116116
assert len(md_files) > 0
117117

118118
@pytest.mark.skip(
@@ -127,9 +127,9 @@ def test_extract_skills_command_github_issues_output(self, runner, temp_repo):
127127

128128
assert result.exit_code == 0
129129

130-
# Check for issue files
130+
# Check for issue files (named skill-{id}.md)
131131
output_dir = temp_repo / ".skills-proposals"
132-
issue_files = list(output_dir.glob("issue-*.md"))
132+
issue_files = list(output_dir.glob("skill-*.md"))
133133
assert len(issue_files) > 0
134134

135135
@pytest.mark.skip(

tests/unit/test_cli_learn.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,36 @@ def temp_repo():
2525
agentready_dir = repo_path / ".agentready"
2626
agentready_dir.mkdir()
2727

28-
# Create sample assessment using shared fixture
28+
# Create sample assessment with known skill IDs that PatternExtractor recognizes
29+
from tests.fixtures.assessment_fixtures import create_test_finding_json
30+
31+
findings = [
32+
create_test_finding_json(
33+
attribute_id="claude_md_file",
34+
attribute_name="CLAUDE.md File",
35+
status="pass",
36+
score=95.0,
37+
category="Documentation",
38+
tier=1,
39+
),
40+
create_test_finding_json(
41+
attribute_id="type_annotations",
42+
attribute_name="Type Annotations",
43+
status="pass",
44+
score=90.0,
45+
category="Code Quality",
46+
tier=2,
47+
),
48+
]
49+
2950
assessment_data = create_test_assessment_json(
3051
overall_score=85.0,
3152
num_findings=2,
3253
repo_path=str(repo_path),
3354
repo_name="test-repo",
3455
)
56+
# Replace generic findings with skill-specific ones
57+
assessment_data["findings"] = findings
3558

3659
assessment_file = agentready_dir / "assessment-latest.json"
3760
with open(assessment_file, "w") as f:
@@ -85,9 +108,9 @@ def test_learn_command_skill_md_output(self, runner, temp_repo):
85108

86109
assert result.exit_code == 0
87110

88-
# Check for SKILL.md files
111+
# Check for SKILL.md files (in subdirectories: skill-id/SKILL.md)
89112
output_dir = temp_repo / ".skills-proposals"
90-
md_files = list(output_dir.glob("*.md"))
113+
md_files = list(output_dir.glob("*/SKILL.md"))
91114
assert len(md_files) > 0
92115

93116
@pytest.mark.skip(
@@ -102,9 +125,9 @@ def test_learn_command_github_issues_output(self, runner, temp_repo):
102125

103126
assert result.exit_code == 0
104127

105-
# Check for issue files
128+
# Check for issue files (named skill-{id}.md)
106129
output_dir = temp_repo / ".skills-proposals"
107-
issue_files = list(output_dir.glob("issue-*.md"))
130+
issue_files = list(output_dir.glob("skill-*.md"))
108131
assert len(issue_files) > 0
109132

110133
@pytest.mark.skip(

0 commit comments

Comments
 (0)