Skip to content

Commit 67b7033

Browse files
authored
Merge b15454c into ee8bb7e
2 parents ee8bb7e + b15454c commit 67b7033

7 files changed

Lines changed: 145 additions & 77 deletions

File tree

src/agentready/cli/align.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def align(repository, dry_run, attributes, interactive):
8383
scanner = Scanner(repo_path, config)
8484

8585
# Create assessors
86-
from agentready.assessors import create_all_assessors
86+
from agentready.cli.main import create_all_assessors
8787

8888
assessors = create_all_assessors()
8989

src/agentready/models/config.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,19 @@ def get_weight(self, attribute_id: str, default: float) -> float:
6161
def is_excluded(self, attribute_id: str) -> bool:
6262
"""Check if attribute is excluded from assessment."""
6363
return attribute_id in self.excluded_attributes
64+
65+
@classmethod
66+
def load_default(cls) -> "Config":
67+
"""Create a default configuration with no customizations.
68+
69+
Returns:
70+
Config with empty weights, no exclusions, no overrides
71+
"""
72+
return cls(
73+
weights={},
74+
excluded_attributes=[],
75+
language_overrides={},
76+
output_dir=None,
77+
report_theme="default",
78+
custom_theme=None,
79+
)

src/agentready/models/repository.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ class Repository:
3232

3333
def __post_init__(self):
3434
"""Validate repository data after initialization."""
35+
# Convert string paths to Path objects for runtime type safety
36+
if isinstance(self.path, str):
37+
object.__setattr__(self, "path", Path(self.path))
38+
3539
if not self.path.exists():
3640
raise ValueError(f"Repository path does not exist: {self.path}")
3741

src/agentready/utils/privacy.py

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def sanitize_path(path: Path | str, relative_to: Path | None = None) -> str:
3232
"secret/data.txt"
3333
"""
3434
path_obj = Path(path) if isinstance(path, str) else path
35+
requested_relative = relative_to is not None
3536

3637
# Try to make relative to specified directory
3738
if relative_to:
@@ -44,25 +45,48 @@ def sanitize_path(path: Path | str, relative_to: Path | None = None) -> str:
4445
# Convert to string for replacements
4546
path_str = str(path_obj)
4647

47-
# Redact home directory
48-
try:
49-
home = str(Path.home())
50-
if path_str.startswith(home):
51-
path_str = path_str.replace(home, "~")
52-
except (RuntimeError, OSError):
53-
pass
54-
55-
# Redact username
48+
# Redact home directory and username
49+
# Note: Do specific replacements first (home directories) before generic username replacement
5650
try:
5751
username = getpass.getuser()
58-
path_str = path_str.replace(f"/{username}/", "/<user>/")
59-
path_str = path_str.replace(f"\\{username}\\", "\\<user>\\")
52+
# Replace specific home directory patterns first
6053
path_str = path_str.replace(f"/Users/{username}/", "~/")
6154
path_str = path_str.replace(f"/home/{username}/", "~/")
6255
path_str = path_str.replace(f"C:\\Users\\{username}\\", "~\\")
56+
# Then do generic username replacement for other locations
57+
path_str = path_str.replace(f"/{username}/", "/<user>/")
58+
path_str = path_str.replace(f"\\{username}\\", "\\<user>\\")
6359
except Exception:
6460
pass
6561

62+
# Fallback: Redact home directory using Path.home() for current user
63+
try:
64+
home = str(Path.home())
65+
if path_str.startswith(home):
66+
path_str = path_str.replace(home, "~", 1)
67+
except (RuntimeError, OSError):
68+
pass
69+
70+
# Generic home directory pattern sanitization for any username
71+
# Replace common home directory patterns even if they don't match current user
72+
path_str = re.sub(r"/home/[^/]+/", "~/", path_str)
73+
path_str = re.sub(r"/Users/[^/]+/", "~/", path_str)
74+
path_str = re.sub(r"C:\\Users\\[^\\]+\\", r"~\\", path_str)
75+
76+
# Final fallback: Redact any remaining absolute paths to avoid leaking sensitive locations
77+
# This catches paths like /secret, /opt/app, /var/sensitive, etc.
78+
# Only do this if the path hasn't already been sanitized (contains ~ or <user>)
79+
# AND if relative_to wasn't requested (in that case, preserve original for debugging)
80+
if not requested_relative:
81+
if (
82+
"~" not in path_str
83+
and "<user>" not in path_str
84+
and "<path>" not in path_str
85+
):
86+
# If it's an absolute path, redact it
87+
if path_str.startswith("/") or (len(path_str) > 2 and path_str[1] == ":"):
88+
path_str = "<path>"
89+
6690
return path_str
6791

6892

@@ -91,7 +115,7 @@ def sanitize_command_args(args: List[str]) -> List[str]:
91115
continue
92116

93117
# Redact values after these flags
94-
if arg in ("--config", "-c", "--api-key", "--token", "--password"):
118+
if arg in ("--config", "-c", "--api-key", "--key", "--token", "--password"):
95119
sanitized.append(arg)
96120
skip_next = True
97121
continue

tests/unit/cli/test_main.py

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,23 @@ def test_repo(tmp_path):
4646

4747

4848
@pytest.fixture
49-
def mock_assessment():
49+
def mock_assessment(tmp_path):
5050
"""Create a mock assessment for testing."""
51+
from datetime import datetime
52+
5153
from agentready.models.assessment import Assessment
54+
from agentready.models.attribute import Attribute
55+
from agentready.models.finding import Finding
5256
from agentready.models.repository import Repository
5357

58+
# Create a temporary directory with .git for Repository validation
59+
test_repo_path = tmp_path / "test-repo"
60+
test_repo_path.mkdir()
61+
(test_repo_path / ".git").mkdir()
62+
5463
repo = Repository(
5564
name="test-repo",
56-
path=Path("/tmp/test"),
65+
path=test_repo_path,
5766
url=None,
5867
branch="main",
5968
commit_hash="abc123",
@@ -62,14 +71,40 @@ def mock_assessment():
6271
total_lines=100,
6372
)
6473

74+
# Create 25 dummy findings to match attributes_total requirement
75+
findings = []
76+
for i in range(25):
77+
attr = Attribute(
78+
id=f"attr_{i}",
79+
name=f"Attribute {i}",
80+
category="Testing",
81+
tier=1,
82+
description="Test attribute",
83+
criteria="Test criteria",
84+
default_weight=0.5,
85+
)
86+
finding = Finding(
87+
attribute=attr,
88+
status="pass" if i < 20 else "not_applicable",
89+
score=100.0 if i < 20 else 0.0,
90+
measured_value="present",
91+
threshold="present",
92+
evidence=[f"Test evidence {i}"],
93+
remediation=None,
94+
error_message=None,
95+
)
96+
findings.append(finding)
97+
6598
assessment = Assessment(
6699
repository=repo,
67-
findings=[],
100+
timestamp=datetime.now(),
101+
findings=findings,
68102
overall_score=85.0,
69103
certification_level="Gold",
70104
attributes_assessed=20,
71105
attributes_not_assessed=5,
72106
attributes_total=25,
107+
config=None,
73108
duration_seconds=1.5,
74109
)
75110

@@ -393,9 +428,7 @@ def test_load_config_sensitive_output_dir(self, tmp_path):
393428
config_file = tmp_path / "config.yaml"
394429
config_file.write_text("output_dir: /etc/passwords")
395430

396-
with pytest.raises(
397-
ValueError, match="cannot be in sensitive system directory"
398-
):
431+
with pytest.raises(ValueError, match="cannot be in sensitive system directory"):
399432
load_config(config_file)
400433

401434
def test_load_config_invalid_report_theme(self, tmp_path):
@@ -482,9 +515,7 @@ def test_generate_config_creates_file(self, runner):
482515
"""Test generate-config creates config file."""
483516
with runner.isolated_filesystem():
484517
# Create example config
485-
Path(".agentready-config.example.yaml").write_text(
486-
"weights:\n attr1: 1.0"
487-
)
518+
Path(".agentready-config.example.yaml").write_text("weights:\n attr1: 1.0")
488519

489520
result = runner.invoke(generate_config, [])
490521

@@ -504,9 +535,7 @@ def test_generate_config_overwrite_prompt(self, runner):
504535
"""Test generate-config prompts when file exists."""
505536
with runner.isolated_filesystem():
506537
# Create both example and target
507-
Path(".agentready-config.example.yaml").write_text(
508-
"weights:\n attr1: 1.0"
509-
)
538+
Path(".agentready-config.example.yaml").write_text("weights:\n attr1: 1.0")
510539
Path(".agentready-config.yaml").write_text("existing: content")
511540

512541
# Decline overwrite
@@ -520,9 +549,7 @@ def test_generate_config_overwrite_confirm(self, runner):
520549
"""Test generate-config overwrites when confirmed."""
521550
with runner.isolated_filesystem():
522551
# Create both example and target
523-
Path(".agentready-config.example.yaml").write_text(
524-
"weights:\n attr1: 2.0"
525-
)
552+
Path(".agentready-config.example.yaml").write_text("weights:\n attr1: 2.0")
526553
Path(".agentready-config.yaml").write_text("existing: content")
527554

528555
# Confirm overwrite
@@ -598,9 +625,7 @@ def test_assess_large_repo_warning(self, runner, test_repo, mock_assessment):
598625
mock_scanner_class.return_value = mock_scanner
599626

600627
# Mock file count to be large
601-
with patch(
602-
"agentready.cli.main.safe_subprocess_run"
603-
) as mock_subprocess:
628+
with patch("agentready.cli.main.safe_subprocess_run") as mock_subprocess:
604629
# Simulate large repo with 15000 files
605630
mock_subprocess.return_value = MagicMock(
606631
returncode=0, stdout="\n".join(["file.py"] * 15000)
@@ -624,7 +649,9 @@ def test_run_assessment_function(self, test_repo, mock_assessment):
624649
mock_scanner_class.return_value = mock_scanner
625650

626651
# Call run_assessment directly
627-
run_assessment(str(test_repo), verbose=False, output_dir=None, config_path=None)
652+
run_assessment(
653+
str(test_repo), verbose=False, output_dir=None, config_path=None
654+
)
628655

629656
# Should have created reports
630657
assert (test_repo / ".agentready").exists()

tests/unit/learners/test_pattern_extractor.py

Lines changed: 39 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,50 @@
11
"""Unit tests for pattern extraction."""
22

33
from datetime import datetime
4+
from pathlib import Path
45

56
import pytest
67

78
from agentready.learners.pattern_extractor import PatternExtractor
89
from agentready.models import Assessment, Attribute, Finding, Repository
910

1011

12+
def create_test_repository(tmp_path=None):
13+
"""Create a test repository with valid path."""
14+
if tmp_path is None:
15+
# For inline usage without fixture, create minimal valid repo
16+
import tempfile
17+
18+
temp_dir = Path(tempfile.mkdtemp())
19+
(temp_dir / ".git").mkdir(exist_ok=True)
20+
test_repo = temp_dir
21+
else:
22+
test_repo = tmp_path / "test-repo"
23+
test_repo.mkdir(exist_ok=True)
24+
(test_repo / ".git").mkdir(exist_ok=True)
25+
26+
return Repository(
27+
path=test_repo,
28+
name="test",
29+
url=None,
30+
branch="main",
31+
commit_hash="abc",
32+
languages={},
33+
total_files=0,
34+
total_lines=0,
35+
)
36+
37+
1138
@pytest.fixture
12-
def sample_repository():
39+
def sample_repository(tmp_path):
1340
"""Create test repository."""
41+
# Create temporary directory with .git for Repository validation
42+
test_repo = tmp_path / "test-repo"
43+
test_repo.mkdir()
44+
(test_repo / ".git").mkdir()
45+
1446
return Repository(
15-
path="/tmp/test",
47+
path=test_repo,
1648
name="test-repo",
1749
url=None,
1850
branch="main",
@@ -188,9 +220,7 @@ def test_filters_low_score_findings(self, sample_assessment_with_findings):
188220
assert len(skills) == 1
189221
assert skills[0].confidence == 95.0
190222

191-
def test_filters_failing_findings(
192-
self, sample_repository, sample_finding_failing
193-
):
223+
def test_filters_failing_findings(self, sample_repository, sample_finding_failing):
194224
"""Test that failing findings are filtered."""
195225
assessment = Assessment(
196226
repository=sample_repository,
@@ -296,16 +326,7 @@ def test_extract_specific_patterns_filters_correctly(
296326
def test_should_extract_pattern_logic(self, sample_finding_high_score):
297327
"""Test _should_extract_pattern() logic."""
298328
assessment = Assessment(
299-
repository=Repository(
300-
path="/tmp",
301-
name="test",
302-
url=None,
303-
branch="main",
304-
commit_hash="abc",
305-
languages={},
306-
total_files=0,
307-
total_lines=0,
308-
),
329+
repository=create_test_repository(),
309330
timestamp=datetime.now(),
310331
overall_score=95.0,
311332
certification_level="Platinum",
@@ -367,16 +388,7 @@ def test_should_not_extract_unknown_attribute(self, sample_repository):
367388
def test_create_skill_from_finding(self, sample_finding_high_score):
368389
"""Test _create_skill_from_finding() creates valid skill."""
369390
assessment = Assessment(
370-
repository=Repository(
371-
path="/tmp",
372-
name="test",
373-
url=None,
374-
branch="main",
375-
commit_hash="abc",
376-
languages={},
377-
total_files=0,
378-
total_lines=0,
379-
),
391+
repository=create_test_repository(),
380392
timestamp=datetime.now(),
381393
overall_score=95.0,
382394
certification_level="Platinum",
@@ -494,16 +506,7 @@ def test_reusability_score_calculation(self, sample_repository):
494506
def test_extract_code_examples_from_evidence(self, sample_finding_high_score):
495507
"""Test extracting code examples from evidence."""
496508
assessment = Assessment(
497-
repository=Repository(
498-
path="/tmp",
499-
name="test",
500-
url=None,
501-
branch="main",
502-
commit_hash="abc",
503-
languages={},
504-
total_files=0,
505-
total_lines=0,
506-
),
509+
repository=create_test_repository(),
507510
timestamp=datetime.now(),
508511
overall_score=95.0,
509512
certification_level="Platinum",
@@ -564,16 +567,7 @@ def test_extract_code_examples_limits_to_three(self, sample_repository):
564567
def test_create_pattern_summary(self, sample_finding_high_score):
565568
"""Test pattern summary generation."""
566569
assessment = Assessment(
567-
repository=Repository(
568-
path="/tmp",
569-
name="test",
570-
url=None,
571-
branch="main",
572-
commit_hash="abc",
573-
languages={},
574-
total_files=0,
575-
total_lines=0,
576-
),
570+
repository=create_test_repository(),
577571
timestamp=datetime.now(),
578572
overall_score=95.0,
579573
certification_level="Platinum",

tests/unit/test_fixer_service.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ def generate_fix(self, repository: Repository, finding: Finding):
4747
@pytest.fixture
4848
def sample_repository(tmp_path):
4949
"""Create test repository."""
50+
# Create .git directory for Repository validation
51+
(tmp_path / ".git").mkdir()
52+
5053
return Repository(
5154
path=tmp_path,
5255
name="test-repo",

0 commit comments

Comments
 (0)