Files
Aperant/tests/test_output_validator.py
bfc232825b feat(pr-review): evidence-based validation and trigger-driven exploration (#1593)
* gsd update

* docs: define v1 requirements with holistic PR understanding

29 requirements across 6 categories:
- Holistic PR Understanding (5) - context synthesis and passing
- Validation Pipeline (3) - finding-validator for all reviews
- Schema Enforcement (5) - VerificationEvidence required
- Prompt Improvements (6) - understand intent, evidence requirements
- Code Simplification (6) - remove programmatic filters
- Measurement (4) - 5 PRs to validate

Key addition: Pass gathered context (related files, import graph) to specialists.
Currently gathered but unused.

* feat(01-01): add Phase 0 synthesis instruction to orchestrator prompt

- Add 'Phase 0: Understand the PR Holistically' section before Phase 1
- Include PR UNDERSTANDING output format (intent, critical changes, risk areas, files to verify)
- Add explicit gate: 'Only AFTER completing Phase 0, proceed to Phase 1'
- Add 'Understand First' principle to Key Principles section

Covers: CONTEXT-01, CONTEXT-05

* feat(01-01): add related files and import graph to orchestrator prompt

- Add related files section categorizing tests vs dependencies/callers
- Add import graph section showing what files import/are imported by changed files
- Limit to 30 related files (15 tests, 15 deps) and 20 import entries
- Include actionable guidance for using the context

Covers: CONTEXT-02, CONTEXT-03

* feat(01-02): add investigation context to specialist agent descriptions

- security-reviewer: check related files for affected callers, verify tests
- quality-reviewer: check related files for pattern consistency
- logic-reviewer: check callers/dependents for broken assumptions
- codebase-fit-reviewer: use related files to understand existing patterns
- finding-validator: check related files for missed mitigations
- ai-triage-reviewer: unchanged (doesn't need related file guidance)

CONTEXT-04: Specialists now know which files to investigate beyond the diff

* feat(01-02): add specialist-specific delegation guidance to related files section

- Updated header: "Pass relevant files to specialists when delegating"
- Added per-specialist guidance for security, logic, quality, codebase-fit
- Added example delegation showing how to include related files in task

Orchestrator now knows HOW to pass investigation context to each specialist type

* feat(02-01): add VerificationEvidence class and update finding models

- Add VerificationEvidence class with required code_examined, line_range_examined, verification_method fields
- Add required verification field to BaseFinding
- Add required verification field to ParallelOrchestratorFinding
- Add is_impact_finding boolean field to ParallelOrchestratorFinding (default False)
- Add checked_for_handling_elsewhere boolean field to ParallelOrchestratorFinding (default False)
- Mark old evidence field as DEPRECATED in both BaseFinding and ParallelOrchestratorFinding

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* test(02-01): add tests for schema enforcement and verification evidence

- Add TestVerificationEvidence class with 5 tests for VerificationEvidence model
- Add TestParallelOrchestratorFindingVerification class with 6 tests for verification requirement
- Add TestVerificationSchemaGeneration class with 2 tests for JSON schema generation
- Update existing TestSecurityFinding and TestDeepAnalysisFinding to include verification field
- Import VerificationEvidence, ParallelOrchestratorFinding, BaseFinding in test imports

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* feat(03-02): add 'What the Diff Is For' section to orchestrator

- Reframe diff as question to investigate, not document to nitpick
- Add 3 questions to answer before delegation
- Include 'Delegate with Context' guidance
- Position after Phase 0, before Phase 1

* feat(03-01): add Understand Intent phase to all specialist prompts

- Add Phase 1: Understand the PR Intent to security, logic, quality, codebase_fit agents
- Force AI to understand PR purpose before searching for issues
- Prevents flagging intentional design decisions as bugs

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* feat(03-02): enhance delegation guidance with context requirements

- Add Context-Rich Delegation section with 3 requirements
- Include PR intent summary, specific concerns, files of interest
- Show anti-pattern vs good pattern comparison
- Update example delegation with specific verification items

* feat(03-01): add Evidence Requirements and Valid Outputs sections

- Add Evidence Requirements section documenting VerificationEvidence schema
- Document code_examined, line_range_examined, verification_method fields
- Document is_impact_finding and checked_for_handling_elsewhere fields
- Add Valid Outputs section allowing no-issues as valid output
- Document invalid outputs (forced issues, theoretical edge cases)

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* feat(03-01): update output format examples with verification object

- Add verification object with code_examined, line_range_examined, verification_method
- Add is_impact_finding and checked_for_handling_elsewhere fields
- Use domain-appropriate verification_method values per agent
- Security: direct_code_inspection for injection examples
- Logic: direct_code_inspection for off-by-one and race conditions
- Quality: direct_code_inspection + cross_file_trace for duplication
- Codebase fit: cross_file_trace for reinvention, direct_code_inspection for naming

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* feat(04-01): add _verify_line_numbers() method

- Pre-filter findings with invalid line numbers before AI validation
- Cache file line counts to avoid re-reading same file
- Reject findings where line > file length
- Log each rejection with finding ID and reason
- Conservative: allow findings if file read fails

* feat(04-02): add hypothesis-validation structure to finding validator

- Add "Hypothesis-Validation Structure (MANDATORY)" section with 4 steps
- Define TRUE/FALSE conditions for hypothesis testing
- Include worked example showing confirmed_valid conclusion path
- Include counter-example showing dismissed_false_positive path
- Reference structure from Investigation Process section

* feat(04-01): add _validate_findings() method

- Import FindingValidationResponse from pydantic_models
- Create finding-validator agent client with pr_finding_validator type
- Build validation prompt with findings JSON and changed files
- Filter findings by validation_status:
  - confirmed_valid: keep with validation evidence
  - dismissed_false_positive: exclude from results
  - needs_human_review: keep with [NEEDS REVIEW] prefix
- Fail-safe: return original findings on any error
- Log validation statistics

* feat(04-01): wire validation pipeline into review() method

- Stage 1: Line verification after cross-validation (cheap pre-filter)
- Stage 2: AI validation for findings that pass line check
- Update programmatic filter loop to use validated_by_ai
- Log validation statistics at each stage
- Uses project_root (worktree or fallback) for file access

* refactor(05-01): remove evidence filter and confidence routing from review()

- Remove _validate_finding_evidence() call from loop
- Remove _apply_confidence_routing() call
- Simplify loop to only check scope
- Replace routed_findings with direct validated_findings assignment

* feat(05-02): remove false positive patterns from validator

- Remove VAGUE_PATTERNS constant (10 patterns)
- Remove GENERIC_PATTERNS constant (6 patterns)
- Remove _is_false_positive() method (44 lines)
- Remove _is_false_positive call from _is_valid()
- Remove TestFalsePositiveDetection class (4 tests)
- Update test_low_severity_higher_threshold to use actionability score

REMOVE-04: VAGUE_PATTERNS, GENERIC_PATTERNS deleted
REMOVE-05: _is_false_positive() method deleted

* refactor(05-01): remove redundant functions and simplify scope check

- Remove ConfidenceTier enum (no longer used)
- Remove _validate_finding_evidence function (schema enforces evidence)
- Remove _apply_confidence_routing method (validation is binary)
- Remove 'from enum import Enum' import
- Simplify _is_finding_in_scope to use schema field is_impact_finding
  instead of keyword detection

* fix(pr-review): add Task tool to orchestrator configs for SDK subagents

The pr_orchestrator_parallel and pr_followup_parallel agents need the
Task tool in their tools list to invoke SDK subagents (security-reviewer,
logic-reviewer, etc.). Without Task, the SDK cannot spawn subagents,
resulting in "Agent type not found" errors.

Also fixes test_integration_phase4.py to set is_impact_finding as an
attribute rather than constructor arg, since PRReviewFinding doesn't
have this field (it's on ParallelOrchestratorFinding Pydantic model).

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* fix(pr-review): add explicit Task tool invocation syntax for specialist agents

The orchestrator was using the built-in general-purpose agent instead of
our custom specialist agents (security-reviewer, logic-reviewer, etc.)
because the prompt described agents but didn't show explicit Task tool
invocation syntax.

Changes:
- Add "CRITICAL: How to Invoke Specialist Agents" section with exact
  subagent_type values in a reference table
- Add Task tool invocation format with example syntax
- Add example showing parallel invocation of multiple specialists
- Add explicit "DO NOT USE" section warning against general-purpose
- Update example delegation to use Task tool syntax instead of prose
- Add example validation invocation for finding-validator

This ensures Claude uses our custom specialists instead of defaulting
to the built-in general-purpose agent.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* feat(pr-review): implement evidence-based validation and trigger-driven exploration

Major enhancements to the PR review system:

**Evidence-Based Validation:**
- Shift from confidence-based to evidence-based finding validation
- All findings now require VerificationEvidence with code_examined, line_range_examined
- finding-validator validates ALL findings (CRITICAL through LOW) before output
- Add dismissed_findings array for transparency - users see what was investigated

**Trigger-Driven Exploration (6 Semantic Triggers):**
- OUTPUT CONTRACT CHANGED - function returns different value/type/structure
- INPUT CONTRACT CHANGED - parameters added/removed/reordered
- BEHAVIORAL CONTRACT CHANGED - same I/O but different internal behavior
- SIDE EFFECT CONTRACT CHANGED - observable effects added/removed
- FAILURE CONTRACT CHANGED - error handling changed
- NULL/UNDEFINED CONTRACT CHANGED - null handling changed

Orchestrator detects triggers in Phase 1 and passes them to specialists
with explicit "TRIGGER:", "EXPLORATION REQUIRED:", "Stop when:" instructions.

**Implementation Changes:**
- Add _PRDebugLogger for comprehensive agent communication logging
- Add CI status integration to verdict logic (failing CI blocks merge)
- Extract with_working_dir() to shared agent_utils.py module
- Inject working directory into all subagent prompts
- Bump SDK requirement to >=0.1.22 for custom subagent support

**Frontend:**
- Tighten AUTH_FAILURE_PATTERNS to avoid false positives on AI auth discussion
- Update tests for new pattern requirements

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* fix(pr-review): wait for both queued AND in_progress CI checks

Previously, the CI wait logic only blocked on "in_progress" checks,
but not "queued" checks. This meant if a CI check (like CodeRabbit)
was queued but not yet running, the review would start immediately
and report "CI is pending" - which would be stale by the time the
contributor sees it.

Now we wait for ALL checks to reach "completed" status before
starting the review, ensuring the CI status in our review is accurate.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* chore: remove duplicate .planning entries from .gitignore

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* chore: remove docs/ from git tracking (already in .gitignore)

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* fix(pr-review): propagate is_impact_finding field to allow impact findings

The is_impact_finding field was defined in ParallelOrchestratorFinding
but never propagated to PRReviewFinding, causing ALL impact findings
(findings about callers/affected files outside the PR's changed files)
to be incorrectly filtered out as "not in scope".

Changes:
- Add is_impact_finding field to PRReviewFinding dataclass
- Extract and pass is_impact_finding in _create_finding_from_structured()
- Add to to_dict() and from_dict() for serialization

This enables the trigger-driven exploration feature to actually work,
allowing the review to report issues in files affected by contract changes.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* fix(pr-review): add Task tool invocation syntax to followup orchestrator

The follow-up review orchestrator was missing explicit Task tool
invocation syntax and examples. The AI didn't know HOW to invoke
the specialist agents (resolution-verifier, finding-validator, etc.),
causing resolution checking to never happen.

Added:
- Exact agent names table (subagent_type values)
- Task tool invocation format with examples
- Complete follow-up review workflow with Task calls
- DO NOT USE section (avoid general-purpose, Explore, Plan)
- Decision matrix for when to invoke each agent
- Explicit Task tool calls in Phase 2 workflow

This matches the main orchestrator prompt which has extensive
Task tool examples and works correctly.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* fix(pr-review): propagate is_impact_finding in follow-up reviewer

Applied the same is_impact_finding propagation fix to the follow-up
reviewer that was already applied to the main orchestrator reviewer.

Fixes:
1. Add is_impact_finding field to ParallelFollowupFinding Pydantic model
2. Propagate is_impact_finding when creating PRReviewFinding for new findings
3. Copy is_impact_finding from original finding for unresolved findings

Without this fix, impact findings (about callers/affected files outside
the PR's changed files) would be incorrectly filtered as "not in scope"
during follow-up reviews.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* feat(auth): enhance keychain service integration with config directory support

Added functionality to support profile-specific credentials by introducing a hash-based service name for macOS Keychain and updating Windows credential retrieval to utilize a provided config directory. This ensures that tokens are fetched from the correct profile-specific storage locations, improving credential management across different environments.

Changes include:
- New functions for calculating config directory hashes and generating keychain service names.
- Updated `get_token_from_keychain` and related functions to accept an optional config directory argument.
- Enhanced logging for better debugging when no token is found.

This aligns the backend credential handling with the frontend's expectations for profile-specific storage.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

---------

Co-authored-by: Claude Opus 4.5 <[email protected]>
2026-01-29 14:34:08 +01:00

559 lines
21 KiB
Python

"""
Tests for Output Validator Module
=================================
Tests validation, filtering, and enhancement of PR review findings.
"""
import pytest
from pathlib import Path
import sys
backend_path = Path(__file__).parent.parent / "apps" / "backend"
sys.path.insert(0, str(backend_path))
# Import directly to avoid loading the full runners module with its dependencies
import importlib.util
# Load file_lock first (models.py depends on it)
file_lock_spec = importlib.util.spec_from_file_location(
"file_lock",
backend_path / "runners" / "github" / "file_lock.py"
)
file_lock_module = importlib.util.module_from_spec(file_lock_spec)
sys.modules['file_lock'] = file_lock_module # Make it available for models imports
file_lock_spec.loader.exec_module(file_lock_module)
# Load models next
models_spec = importlib.util.spec_from_file_location(
"models",
backend_path / "runners" / "github" / "models.py"
)
models_module = importlib.util.module_from_spec(models_spec)
sys.modules['models'] = models_module # Make it available for validator imports
models_spec.loader.exec_module(models_module)
PRReviewFinding = models_module.PRReviewFinding
ReviewSeverity = models_module.ReviewSeverity
ReviewCategory = models_module.ReviewCategory
# Now load validator (it will find models in sys.modules)
validator_spec = importlib.util.spec_from_file_location(
"output_validator",
backend_path / "runners" / "github" / "output_validator.py"
)
validator_module = importlib.util.module_from_spec(validator_spec)
validator_spec.loader.exec_module(validator_module)
FindingValidator = validator_module.FindingValidator
@pytest.fixture
def sample_changed_files():
"""Sample changed files for testing."""
return {
"src/auth.py": """import os
import hashlib
def authenticate_user(username, password):
# TODO: Use proper password hashing
hashed = hashlib.md5(password.encode()).hexdigest()
stored_hash = get_stored_hash(username)
return hashed == stored_hash
def get_stored_hash(username):
# Vulnerable to SQL injection
query = f"SELECT password FROM users WHERE username = '{username}'"
return execute_query(query)
def execute_query(query):
pass
""",
"src/utils.py": """def process_data(data):
result = []
for item in data:
result.append(item * 2)
return result
def validate_input(user_input):
# Missing validation
return True
""",
"tests/test_auth.py": """import pytest
from src.auth import authenticate_user
def test_authentication():
# Basic test
assert authenticate_user("test", "password") == True
""",
}
@pytest.fixture
def validator(sample_changed_files, tmp_path):
"""Create a FindingValidator instance."""
return FindingValidator(tmp_path, sample_changed_files)
class TestFindingValidation:
"""Test finding validation logic."""
def test_valid_finding_passes(self, validator):
"""Test that a valid finding passes validation."""
finding = PRReviewFinding(
id="SEC001",
severity=ReviewSeverity.CRITICAL,
category=ReviewCategory.SECURITY,
title="SQL Injection Vulnerability",
description="The function get_stored_hash uses string formatting to construct SQL queries, making it vulnerable to SQL injection attacks. An attacker could manipulate the username parameter to execute arbitrary SQL.",
file="src/auth.py",
line=13,
suggested_fix="Use parameterized queries: `cursor.execute('SELECT password FROM users WHERE username = ?', (username,))`",
fixable=True,
)
result = validator.validate_findings([finding])
assert len(result) == 1
assert result[0].id == "SEC001"
def test_invalid_file_filtered(self, validator):
"""Test that findings for non-existent files are filtered."""
finding = PRReviewFinding(
id="TEST001",
severity=ReviewSeverity.LOW,
category=ReviewCategory.QUALITY,
title="Missing Test",
description="This file should have tests but doesn't exist in the changeset.",
file="src/nonexistent.py",
line=10,
)
result = validator.validate_findings([finding])
assert len(result) == 0
def test_short_title_filtered(self, validator):
"""Test that findings with short titles are filtered."""
finding = PRReviewFinding(
id="TEST002",
severity=ReviewSeverity.LOW,
category=ReviewCategory.STYLE,
title="Fix this", # Too short
description="This is a longer description that meets the minimum length requirement for validation.",
file="src/utils.py",
line=1,
)
result = validator.validate_findings([finding])
assert len(result) == 0
def test_short_description_filtered(self, validator):
"""Test that findings with short descriptions are filtered."""
finding = PRReviewFinding(
id="TEST003",
severity=ReviewSeverity.LOW,
category=ReviewCategory.STYLE,
title="Code Style Issue",
description="Short desc", # Too short
file="src/utils.py",
line=1,
)
result = validator.validate_findings([finding])
assert len(result) == 0
class TestLineNumberVerification:
"""Test line number verification and correction."""
def test_valid_line_number(self, validator):
"""Test that valid line numbers pass verification."""
finding = PRReviewFinding(
id="SEC001",
severity=ReviewSeverity.HIGH,
category=ReviewCategory.SECURITY,
title="Weak Password Hashing Algorithm",
description="The code uses MD5 for password hashing which is cryptographically broken. This makes passwords vulnerable to rainbow table attacks.",
file="src/auth.py",
line=5, # Line with hashlib.md5
suggested_fix="Use bcrypt or argon2: `import bcrypt; hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt())`",
)
assert validator._verify_line_number(finding)
def test_invalid_line_number(self, validator):
"""Test that invalid line numbers fail verification."""
finding = PRReviewFinding(
id="TEST001",
severity=ReviewSeverity.LOW,
category=ReviewCategory.QUALITY,
title="Code Quality Issue",
description="This line number is way out of bounds and should fail validation checks.",
file="src/auth.py",
line=999, # Out of bounds
)
assert not validator._verify_line_number(finding)
def test_auto_correct_line_number(self, validator):
"""Test auto-correction of line numbers."""
finding = PRReviewFinding(
id="SEC001",
severity=ReviewSeverity.HIGH,
category=ReviewCategory.SECURITY,
title="MD5 Password Hashing",
description="Using MD5 for password hashing is insecure. The hashlib.md5 function should be replaced with a modern algorithm.",
file="src/auth.py",
line=3, # Wrong line, but MD5 is on line 5
suggested_fix="Use bcrypt instead of MD5",
)
corrected = validator._auto_correct_line_number(finding)
# Should find a line with hashlib/md5 (line 4 imports hashlib, line 5 uses md5)
assert corrected.line in [4, 5] # Either import or usage line
def test_line_relevance_security_patterns(self, validator):
"""Test that security patterns are detected."""
finding = PRReviewFinding(
id="SEC002",
severity=ReviewSeverity.CRITICAL,
category=ReviewCategory.SECURITY,
title="SQL Injection",
description="Vulnerable to SQL injection through unsanitized user input",
file="src/auth.py",
line=13,
)
line_content = "query = f\"SELECT password FROM users WHERE username = '{username}'\""
assert validator._is_line_relevant(line_content, finding)
class TestActionabilityScoring:
"""Test actionability scoring."""
def test_high_actionability_score(self, validator):
"""Test that complete findings get high scores."""
finding = PRReviewFinding(
id="SEC001",
severity=ReviewSeverity.CRITICAL,
category=ReviewCategory.SECURITY,
title="SQL Injection Vulnerability in User Authentication",
description="The get_stored_hash function constructs SQL queries using f-strings, which is vulnerable to SQL injection. An attacker could manipulate the username parameter to execute arbitrary SQL commands, potentially compromising the entire database.",
file="src/auth.py",
line=13,
end_line=14,
suggested_fix="Replace the f-string with parameterized query: `cursor.execute('SELECT password FROM users WHERE username = ?', (username,))`",
fixable=True,
)
score = validator._score_actionability(finding)
assert score >= 0.8
def test_low_actionability_score(self, validator):
"""Test that incomplete findings get low scores."""
finding = PRReviewFinding(
id="QUAL001",
severity=ReviewSeverity.LOW,
category=ReviewCategory.QUALITY,
title="Code quality",
description="Could be better",
file="src/utils.py",
line=1,
)
score = validator._score_actionability(finding)
assert score <= 0.6
def test_security_findings_get_bonus(self, validator):
"""Test that security findings get actionability bonus."""
security_finding = PRReviewFinding(
id="SEC001",
severity=ReviewSeverity.HIGH,
category=ReviewCategory.SECURITY,
title="Security Vulnerability Found",
description="This is a security issue that needs to be addressed immediately for safety.",
file="src/auth.py",
line=5,
suggested_fix="Apply proper security measures",
)
quality_finding = PRReviewFinding(
id="QUAL001",
severity=ReviewSeverity.HIGH,
category=ReviewCategory.QUALITY,
title="Quality Issue Found",
description="This is a quality issue that needs to be addressed for better code.",
file="src/auth.py",
line=5,
suggested_fix="Apply proper quality measures",
)
sec_score = validator._score_actionability(security_finding)
qual_score = validator._score_actionability(quality_finding)
assert sec_score > qual_score
class TestConfidenceThreshold:
"""Test confidence threshold checks."""
def test_high_severity_lower_threshold(self, validator):
"""Test that high severity findings have lower threshold."""
finding = PRReviewFinding(
id="SEC001",
severity=ReviewSeverity.CRITICAL,
category=ReviewCategory.SECURITY,
title="Critical Security Issue",
description="This is a critical security vulnerability that must be fixed.",
file="src/auth.py",
line=5,
)
# Should pass with lower actionability due to critical severity
assert validator._meets_confidence_threshold(finding)
def test_low_severity_higher_threshold(self, validator):
"""Test that low severity findings need higher threshold."""
finding = PRReviewFinding(
id="STYLE001",
severity=ReviewSeverity.LOW,
category=ReviewCategory.STYLE,
title="Styl", # Very minimal (9 chars, just at min)
description="Could be improved with better formatting here",
file="src/utils.py",
line=1,
suggested_fix="", # No fix
)
# Score check: low severity with no fix gets low actionability
# With no fix, short title, and low severity: 0.5 (base) + 0.1 (file+line) = 0.6
# This barely meets the 0.6 threshold for low severity
score = validator._score_actionability(finding)
assert score <= 0.6 # Low actionability due to missing suggested fix
class TestFindingEnhancement:
"""Test finding enhancement."""
def test_enhance_adds_confidence(self, validator):
"""Test that enhancement adds confidence score."""
finding = PRReviewFinding(
id="SEC001",
severity=ReviewSeverity.HIGH,
category=ReviewCategory.SECURITY,
title="Security Vulnerability",
description="This is a security vulnerability that should be addressed immediately.",
file="src/auth.py",
line=5,
suggested_fix="Apply the recommended security fix here",
)
enhanced = validator._enhance(finding)
assert hasattr(enhanced, "confidence")
assert enhanced.confidence > 0
def test_enhance_sets_fixable(self, validator):
"""Test that enhancement sets fixable flag."""
finding = PRReviewFinding(
id="SEC001",
severity=ReviewSeverity.HIGH,
category=ReviewCategory.SECURITY,
title="Security Issue",
description="Security vulnerability that needs fixing",
file="src/auth.py",
line=5,
suggested_fix="Use parameterized queries instead of string concatenation",
fixable=False, # Initially false
)
enhanced = validator._enhance(finding)
assert enhanced.fixable # Should be set to True
def test_enhance_cleans_whitespace(self, validator):
"""Test that enhancement cleans whitespace."""
finding = PRReviewFinding(
id="TEST001",
severity=ReviewSeverity.MEDIUM,
category=ReviewCategory.QUALITY,
title=" Title with spaces ",
description=" Description with spaces ",
file="src/utils.py",
line=1,
suggested_fix=" Fix with spaces ",
)
enhanced = validator._enhance(finding)
assert enhanced.title == "Title with spaces"
assert enhanced.description == "Description with spaces"
assert enhanced.suggested_fix == "Fix with spaces"
class TestValidationStats:
"""Test validation statistics."""
def test_validation_stats(self, validator):
"""Test that validation stats are computed correctly."""
findings = [
PRReviewFinding(
id="SEC001",
severity=ReviewSeverity.CRITICAL,
category=ReviewCategory.SECURITY,
title="SQL Injection Vulnerability",
description="Critical SQL injection vulnerability in user authentication",
file="src/auth.py",
line=13,
suggested_fix="Use parameterized queries",
fixable=True,
),
PRReviewFinding(
id="STYLE001",
severity=ReviewSeverity.LOW,
category=ReviewCategory.STYLE,
title="Bad style", # Too short, will be filtered
description="Short",
file="src/utils.py",
line=1,
),
PRReviewFinding(
id="TEST001",
severity=ReviewSeverity.MEDIUM,
category=ReviewCategory.TEST,
title="Missing Test Coverage",
description="The authenticate_user function lacks comprehensive test coverage",
file="tests/test_auth.py",
line=5,
suggested_fix="Add tests for edge cases and error conditions",
),
]
validated = validator.validate_findings(findings)
stats = validator.get_validation_stats(findings, validated)
assert stats["total_findings"] == 3
assert stats["kept_findings"] == 2 # One filtered
assert stats["filtered_findings"] == 1
assert stats["filter_rate"] == pytest.approx(1/3)
assert stats["severity_distribution"]["critical"] == 1
assert stats["category_distribution"]["security"] == 1
assert stats["average_actionability"] > 0
# Both valid findings will have fixable=True after enhancement (both have good suggested fixes)
assert stats["fixable_count"] >= 1
class TestKeyTermExtraction:
"""Test key term extraction."""
def test_extract_from_title(self, validator):
"""Test extraction from title."""
finding = PRReviewFinding(
id="TEST001",
severity=ReviewSeverity.MEDIUM,
category=ReviewCategory.QUALITY,
title="Password Hashing Vulnerability",
description="Description",
file="src/auth.py",
line=1,
)
terms = validator._extract_key_terms(finding)
assert "Password" in terms or "password" in [t.lower() for t in terms]
assert "Hashing" in terms or "hashing" in [t.lower() for t in terms]
def test_extract_code_terms(self, validator):
"""Test extraction of code terms."""
finding = PRReviewFinding(
id="TEST001",
severity=ReviewSeverity.MEDIUM,
category=ReviewCategory.SECURITY,
title="Security Issue",
description="The `hashlib.md5` function is insecure",
file="src/auth.py",
line=1,
)
terms = validator._extract_key_terms(finding)
assert "hashlib.md5" in terms
def test_filter_common_words(self, validator):
"""Test that common words are filtered."""
finding = PRReviewFinding(
id="TEST001",
severity=ReviewSeverity.LOW,
category=ReviewCategory.QUALITY,
title="This Could Be Using Better Patterns",
description="Description with this and that",
file="src/utils.py",
line=1,
)
terms = validator._extract_key_terms(finding)
assert "this" not in [t.lower() for t in terms]
assert "that" not in [t.lower() for t in terms]
class TestIntegration:
"""Integration tests."""
def test_full_validation_pipeline(self, validator):
"""Test complete validation pipeline."""
findings = [
# Valid critical security finding
PRReviewFinding(
id="SEC001",
severity=ReviewSeverity.CRITICAL,
category=ReviewCategory.SECURITY,
title="SQL Injection in Authentication",
description="The get_stored_hash function uses f-string formatting to construct SQL queries, creating a critical SQL injection vulnerability.",
file="src/auth.py",
line=13,
suggested_fix="Use parameterized queries: cursor.execute('SELECT password FROM users WHERE username = ?', (username,))",
fixable=True,
),
# Valid security finding with wrong line (should be corrected)
PRReviewFinding(
id="SEC002",
severity=ReviewSeverity.HIGH,
category=ReviewCategory.SECURITY,
title="Weak Cryptographic Hash",
description="MD5 is cryptographically broken and should not be used for password hashing",
file="src/auth.py",
line=3, # Wrong, should be 5
suggested_fix="Use bcrypt.hashpw() or argon2 for password hashing",
),
# Invalid - vague low severity
PRReviewFinding(
id="STYLE001",
severity=ReviewSeverity.LOW,
category=ReviewCategory.STYLE,
title="Could Be Improved",
description="This code could be improved by considering better practices",
file="src/utils.py",
line=1,
),
# Invalid - non-existent file
PRReviewFinding(
id="TEST001",
severity=ReviewSeverity.MEDIUM,
category=ReviewCategory.TEST,
title="Missing Tests",
description="This file needs test coverage but it doesn't exist",
file="src/missing.py",
line=1,
),
]
validated = validator.validate_findings(findings)
# Should keep 2 valid findings
assert len(validated) == 2
# Check that line was corrected (should find hashlib or md5 reference)
sec002 = next(f for f in validated if f.id == "SEC002")
assert sec002.line in [4, 5] # Either import line or usage line
# Check that all validated findings have confidence
for finding in validated:
assert hasattr(finding, "confidence")
assert finding.confidence > 0
# Get stats
stats = validator.get_validation_stats(findings, validated)
assert stats["filter_rate"] == 0.5
assert stats["average_actionability"] > 0.6