Files
Aperant/tests/test_risk_classifier.py
53111dbb95 auto-claude: 147-remove-outdated-compatibility-shims (#1465)
* auto-claude: subtask-1-1 - Remove validation_strategy backward compatibility shim

- Delete apps/backend/validation_strategy.py shim file that re-exported from spec.validation_strategy
- Update docstring in spec/validation_strategy.py to show correct import path

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

* auto-claude: subtask-1-2 - Remove service_orchestrator shim

- Deleted apps/backend/service_orchestrator.py backward compatibility shim file
- Updated docstring in services/orchestrator.py to use correct import path
  (from services.orchestrator import instead of from service_orchestrator import)
- Verified no external imports of the shim remain in the codebase
- Import from services.orchestrator works correctly

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

* auto-claude: subtask-2-1 - Remove Chunk/ChunkStatus aliases from implementation_plan

Removed backwards compatibility aliases as part of cleaning up outdated
compatibility shims:

- Removed ChunkStatus = SubtaskStatus from enums.py
- Removed Chunk = Subtask from subtask.py
- Removed Chunk/ChunkStatus exports from __init__.py
- Updated all test files to use canonical names (Subtask, SubtaskStatus)

This completes subtasks 2-1, 2-2, and 2-3 together since the test files
depend on all three changes being made atomically.

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

* auto-claude: subtask-2-4 - Remove deprecated use_orchestrator_review field from GitHub runner models

* auto-claude: subtask-3-1 - Update apps/frontend/src/main/index.ts to use platform imports

* auto-claude: subtask-3-2 - Update python-detector.ts to use platform imports

- Import isWindows from ./platform module
- Replace all process.platform === 'win32' checks with isWindows() calls
- Remove redundant local isWindows variable declarations

* auto-claude: subtask-3-3 - Update apps/frontend/src/main/claude-cli-utils.ts to use platform imports

* auto-claude: subtask-3-4 - Update apps/frontend/src/main/config-paths.ts to use platform imports

* auto-claude: subtask-3-5 - Update apps/frontend/src/main/memory-service.ts to use platform imports

* auto-claude: subtask-4-1 - Update claude-code-handlers.ts to use platform imports

Replace all direct process.platform checks with centralized platform
abstraction functions from ../platform module:
- isWindows() for Windows platform checks
- isMacOS() for macOS/Darwin platform checks
- isLinux() for Linux platform checks

This removes 6 instances of process.platform === '...' comparisons and
1 local isWindows variable assignment, replacing them with the platform
abstraction layer for better cross-platform consistency.

* auto-claude: subtask-4-2 - Update apps/frontend/src/main/ipc-handlers/mcp-handlers.ts to use platform imports

* auto-claude: subtask-4-3 - Update apps/frontend/src/main/ipc-handlers/memory-handlers.ts to use platform imports

- Replace process.platform checks with platform module functions
- Add getOllamaExecutablePaths(), getOllamaInstallCommand(), and getWhichCommand() to platform/paths.ts
- Export new functions from platform/index.ts
- Migrate checkOllamaInstalled() to use platform module for path resolution
- Migrate getOllamaInstallCommand() to delegate to platform module
- Update debug log to use getCurrentOS() instead of process.platform

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

* auto-claude: subtask-4-4 - Update apps/frontend/src/main/ipc-handlers/termina

* auto-claude: subtask-4-5 - Update apps/frontend/src/main/ipc-handlers/github/

- Replace direct process.platform check with getWhichCommand() from platform abstraction
- Import getWhichCommand from ../../platform for cross-platform which/where command

* auto-claude: subtask-4-6 - Update apps/frontend/src/main/ipc-handlers/github/utils/subprocess-runner.ts to use platform imports

* auto-claude: subtask-5-1 - Update apps/frontend/src/main/agent/agent-process.ts

Replace direct process.platform checks with platform abstraction:
- Import isWindows from ../platform module
- Replace `process.platform !== 'win32'` with `!isWindows()`
- Replace `process.platform === 'win32'` with `isWindows()`

This ensures consistent platform detection using the centralized
platform abstraction layer.

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

* auto-claude: subtask-5-2 - Update apps/frontend/src/main/agent/agent-queue.ts

* auto-claude: subtask-5-3 - Update apps/frontend/src/main/terminal/pty-daemon.ts to use platform imports

* auto-claude: subtask-5-4 - Update pty-daemon-client.ts to use platform imports

Replace direct process.platform === 'win32' check with isWindows()
from the platform abstraction layer for consistent cross-platform
handling of socket paths.

* auto-claude: subtask-5-5 - Update apps/frontend/src/main/insights/config.ts to use platform imports

- Import isWindows() from '../platform'
- Replace process.platform === 'win32' checks with isWindows()
- Maintains case-insensitive path comparison on Windows

* auto-claude: subtask-5-6 - Update apps/frontend/src/main/changelog/version-suggester.ts to use platform imports

* auto-claude: subtask-5-7 - Update apps/frontend/src/main/changelog/generator.

* fix: Remove unused import and fix test import paths

- Remove unused `isWindows` import from memory-handlers.ts
- Fix test_service_orchestrator.py to import from services.orchestrator
  instead of the removed service_orchestrator shim
- Fix case sensitivity in path ("Apps" -> "apps")

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

* fix: Fix test import paths for case sensitivity and removed shims

- Fix path case sensitivity: "Apps" -> "apps" in 21 test files
- Update test_validation_strategy.py to import from spec.validation_strategy

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

---------

Co-authored-by: Claude Opus 4.5 <[email protected]>
2026-01-25 12:31:06 +01:00

589 lines
20 KiB
Python

#!/usr/bin/env python3
"""
Tests for Risk Classifier Module
================================
Tests the risk_classifier.py module functionality including:
- Loading and parsing complexity_assessment.json
- Validation recommendations parsing
- Risk level determination
- Backward compatibility with older assessments
"""
import json
import pytest
import tempfile
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))
from risk_classifier import (
RiskClassifier,
RiskAssessment,
ValidationRecommendations,
ComplexityAnalysis,
ScopeAnalysis,
IntegrationAnalysis,
InfrastructureAnalysis,
KnowledgeAnalysis,
RiskAnalysis,
AssessmentFlags,
load_risk_assessment,
get_validation_requirements,
)
# =============================================================================
# FIXTURES
# =============================================================================
@pytest.fixture
def temp_spec_dir():
"""Create a temporary spec directory."""
with tempfile.TemporaryDirectory() as tmpdir:
yield Path(tmpdir)
@pytest.fixture
def classifier():
"""Create a fresh RiskClassifier instance."""
return RiskClassifier()
def create_assessment_file(
spec_dir: Path, assessment_data: dict
) -> Path:
"""Helper to create a complexity_assessment.json file."""
assessment_file = spec_dir / "complexity_assessment.json"
with open(assessment_file, "w", encoding="utf-8") as f:
json.dump(assessment_data, f, indent=2)
return assessment_file
# =============================================================================
# SAMPLE DATA
# =============================================================================
SIMPLE_ASSESSMENT = {
"complexity": "simple",
"workflow_type": "simple",
"confidence": 0.95,
"reasoning": "Single file UI change with no dependencies.",
"analysis": {
"scope": {
"estimated_files": 1,
"estimated_services": 1,
"is_cross_cutting": False,
"notes": "CSS-only change",
},
"integrations": {
"external_services": [],
"new_dependencies": [],
"research_needed": False,
},
"infrastructure": {
"docker_changes": False,
"database_changes": False,
"config_changes": False,
},
"knowledge": {
"patterns_exist": True,
"research_required": False,
"unfamiliar_tech": [],
},
"risk": {
"level": "low",
"concerns": [],
},
},
"recommended_phases": ["discovery", "quick_spec", "validation"],
"flags": {
"needs_research": False,
"needs_self_critique": False,
"needs_infrastructure_setup": False,
},
"validation_recommendations": {
"risk_level": "low",
"skip_validation": False,
"minimal_mode": True,
"test_types_required": ["unit"],
"security_scan_required": False,
"staging_deployment_required": False,
"reasoning": "Simple CSS change with minimal testing needs.",
},
}
COMPLEX_ASSESSMENT = {
"complexity": "complex",
"workflow_type": "feature",
"confidence": 0.90,
"reasoning": "Multiple integrations with infrastructure changes.",
"analysis": {
"scope": {
"estimated_files": 12,
"estimated_services": 3,
"is_cross_cutting": True,
"notes": "Touches multiple services",
},
"integrations": {
"external_services": ["Stripe", "Auth0"],
"new_dependencies": ["stripe", "@auth0/auth0-spa-js"],
"research_needed": True,
"notes": "Payment and auth integration",
},
"infrastructure": {
"docker_changes": True,
"database_changes": True,
"config_changes": True,
"notes": "New container and DB migrations",
},
"knowledge": {
"patterns_exist": False,
"research_required": True,
"unfamiliar_tech": ["Stripe webhooks", "Auth0 rules"],
},
"risk": {
"level": "high",
"concerns": ["Payment security", "Auth vulnerabilities", "Data integrity"],
},
},
"recommended_phases": [
"discovery",
"requirements",
"research",
"context",
"spec_writing",
"self_critique",
"planning",
"validation",
],
"flags": {
"needs_research": True,
"needs_self_critique": True,
"needs_infrastructure_setup": True,
},
"validation_recommendations": {
"risk_level": "critical",
"skip_validation": False,
"minimal_mode": False,
"test_types_required": ["unit", "integration", "e2e", "security"],
"security_scan_required": True,
"staging_deployment_required": True,
"reasoning": "Payment and auth integration requires comprehensive testing.",
},
}
TRIVIAL_ASSESSMENT = {
"complexity": "simple",
"workflow_type": "simple",
"confidence": 0.98,
"reasoning": "Documentation-only change.",
"analysis": {
"scope": {
"estimated_files": 1,
"estimated_services": 0,
"is_cross_cutting": False,
},
"integrations": {
"external_services": [],
"new_dependencies": [],
"research_needed": False,
},
"infrastructure": {
"docker_changes": False,
"database_changes": False,
"config_changes": False,
},
"risk": {
"level": "low",
"concerns": [],
},
},
"recommended_phases": ["discovery", "quick_spec", "validation"],
"flags": {
"needs_research": False,
"needs_self_critique": False,
},
"validation_recommendations": {
"risk_level": "trivial",
"skip_validation": True,
"minimal_mode": True,
"test_types_required": [],
"security_scan_required": False,
"staging_deployment_required": False,
"reasoning": "README update only - no functional code changes.",
},
}
# Assessment without validation_recommendations (backward compatibility)
LEGACY_ASSESSMENT = {
"complexity": "standard",
"workflow_type": "feature",
"confidence": 0.85,
"reasoning": "New API endpoint.",
"analysis": {
"scope": {
"estimated_files": 5,
"estimated_services": 1,
"is_cross_cutting": False,
},
"integrations": {
"external_services": [],
"new_dependencies": [],
"research_needed": False,
},
"infrastructure": {
"docker_changes": False,
"database_changes": False,
"config_changes": False,
},
"knowledge": {
"patterns_exist": True,
"research_required": False,
"unfamiliar_tech": [],
},
"risk": {
"level": "medium",
"concerns": [],
},
},
"recommended_phases": [
"discovery",
"requirements",
"context",
"spec_writing",
"planning",
"validation",
],
"flags": {
"needs_research": False,
"needs_self_critique": False,
},
# No validation_recommendations - should be inferred
}
# =============================================================================
# TESTS: LOADING
# =============================================================================
class TestLoadAssessment:
"""Tests for loading complexity_assessment.json."""
def test_load_valid_assessment(self, temp_spec_dir, classifier):
"""Loads a valid complexity_assessment.json file."""
create_assessment_file(temp_spec_dir, SIMPLE_ASSESSMENT)
assessment = classifier.load_assessment(temp_spec_dir)
assert assessment is not None
assert assessment.complexity == "simple"
assert assessment.workflow_type == "simple"
assert assessment.confidence == 0.95
def test_load_nonexistent_file(self, temp_spec_dir, classifier):
"""Returns None when file doesn't exist."""
assessment = classifier.load_assessment(temp_spec_dir)
assert assessment is None
def test_load_invalid_json(self, temp_spec_dir, classifier):
"""Returns None for invalid JSON."""
assessment_file = temp_spec_dir / "complexity_assessment.json"
assessment_file.write_text("invalid json {{{")
assessment = classifier.load_assessment(temp_spec_dir)
assert assessment is None
def test_caches_loaded_assessment(self, temp_spec_dir, classifier):
"""Caches loaded assessments."""
create_assessment_file(temp_spec_dir, SIMPLE_ASSESSMENT)
# Load twice
assessment1 = classifier.load_assessment(temp_spec_dir)
assessment2 = classifier.load_assessment(temp_spec_dir)
# Should be same object from cache
assert assessment1 is assessment2
def test_clear_cache(self, temp_spec_dir, classifier):
"""Cache can be cleared."""
create_assessment_file(temp_spec_dir, SIMPLE_ASSESSMENT)
assessment1 = classifier.load_assessment(temp_spec_dir)
classifier.clear_cache()
assessment2 = classifier.load_assessment(temp_spec_dir)
# After cache clear, should be different objects
assert assessment1 is not assessment2
# =============================================================================
# TESTS: PARSING
# =============================================================================
class TestParseAssessment:
"""Tests for parsing assessment data into objects."""
def test_parses_scope(self, temp_spec_dir, classifier):
"""Parses scope analysis correctly."""
create_assessment_file(temp_spec_dir, COMPLEX_ASSESSMENT)
assessment = classifier.load_assessment(temp_spec_dir)
assert assessment.analysis.scope.estimated_files == 12
assert assessment.analysis.scope.estimated_services == 3
assert assessment.analysis.scope.is_cross_cutting is True
def test_parses_integrations(self, temp_spec_dir, classifier):
"""Parses integrations analysis correctly."""
create_assessment_file(temp_spec_dir, COMPLEX_ASSESSMENT)
assessment = classifier.load_assessment(temp_spec_dir)
assert "Stripe" in assessment.analysis.integrations.external_services
assert "stripe" in assessment.analysis.integrations.new_dependencies
assert assessment.analysis.integrations.research_needed is True
def test_parses_infrastructure(self, temp_spec_dir, classifier):
"""Parses infrastructure analysis correctly."""
create_assessment_file(temp_spec_dir, COMPLEX_ASSESSMENT)
assessment = classifier.load_assessment(temp_spec_dir)
assert assessment.analysis.infrastructure.docker_changes is True
assert assessment.analysis.infrastructure.database_changes is True
assert assessment.analysis.infrastructure.config_changes is True
def test_parses_flags(self, temp_spec_dir, classifier):
"""Parses flags correctly."""
create_assessment_file(temp_spec_dir, COMPLEX_ASSESSMENT)
assessment = classifier.load_assessment(temp_spec_dir)
assert assessment.flags.needs_research is True
assert assessment.flags.needs_self_critique is True
assert assessment.flags.needs_infrastructure_setup is True
def test_parses_validation_recommendations(self, temp_spec_dir, classifier):
"""Parses validation recommendations correctly."""
create_assessment_file(temp_spec_dir, COMPLEX_ASSESSMENT)
assessment = classifier.load_assessment(temp_spec_dir)
assert assessment.validation.risk_level == "critical"
assert assessment.validation.skip_validation is False
assert assessment.validation.security_scan_required is True
assert "e2e" in assessment.validation.test_types_required
# =============================================================================
# TESTS: BACKWARD COMPATIBILITY
# =============================================================================
class TestBackwardCompatibility:
"""Tests for backward compatibility with older assessments."""
def test_infers_validation_from_analysis(self, temp_spec_dir, classifier):
"""Infers validation recommendations when not present."""
create_assessment_file(temp_spec_dir, LEGACY_ASSESSMENT)
assessment = classifier.load_assessment(temp_spec_dir)
# Should have inferred validation recommendations
assert assessment.validation is not None
assert assessment.validation.risk_level == "medium"
assert "unit" in assessment.validation.test_types_required
def test_infers_medium_risk_test_types(self, temp_spec_dir, classifier):
"""Infers unit + integration for medium risk."""
create_assessment_file(temp_spec_dir, LEGACY_ASSESSMENT)
assessment = classifier.load_assessment(temp_spec_dir)
assert "unit" in assessment.validation.test_types_required
assert "integration" in assessment.validation.test_types_required
def test_handles_missing_sections(self, temp_spec_dir, classifier):
"""Handles assessments with missing optional sections."""
minimal_assessment = {
"complexity": "simple",
"workflow_type": "simple",
"confidence": 0.9,
}
create_assessment_file(temp_spec_dir, minimal_assessment)
assessment = classifier.load_assessment(temp_spec_dir)
assert assessment is not None
assert assessment.complexity == "simple"
# Should have defaults for missing sections
assert assessment.analysis.scope.estimated_files == 0
# =============================================================================
# TESTS: CONVENIENCE METHODS
# =============================================================================
class TestConvenienceMethods:
"""Tests for convenience query methods."""
def test_should_skip_validation_true(self, temp_spec_dir, classifier):
"""Returns True for trivial tasks."""
create_assessment_file(temp_spec_dir, TRIVIAL_ASSESSMENT)
assert classifier.should_skip_validation(temp_spec_dir) is True
def test_should_skip_validation_false(self, temp_spec_dir, classifier):
"""Returns False for non-trivial tasks."""
create_assessment_file(temp_spec_dir, SIMPLE_ASSESSMENT)
assert classifier.should_skip_validation(temp_spec_dir) is False
def test_should_skip_validation_no_file(self, temp_spec_dir, classifier):
"""Returns False when file doesn't exist."""
assert classifier.should_skip_validation(temp_spec_dir) is False
def test_should_use_minimal_mode(self, temp_spec_dir, classifier):
"""Returns True for minimal mode tasks."""
create_assessment_file(temp_spec_dir, SIMPLE_ASSESSMENT)
assert classifier.should_use_minimal_mode(temp_spec_dir) is True
def test_get_required_test_types(self, temp_spec_dir, classifier):
"""Returns correct test types."""
create_assessment_file(temp_spec_dir, COMPLEX_ASSESSMENT)
test_types = classifier.get_required_test_types(temp_spec_dir)
assert "unit" in test_types
assert "integration" in test_types
assert "e2e" in test_types
assert "security" in test_types
def test_get_required_test_types_default(self, temp_spec_dir, classifier):
"""Returns unit tests as default when file doesn't exist."""
test_types = classifier.get_required_test_types(temp_spec_dir)
assert test_types == ["unit"]
def test_requires_security_scan(self, temp_spec_dir, classifier):
"""Correctly identifies security scan requirement."""
create_assessment_file(temp_spec_dir, COMPLEX_ASSESSMENT)
assert classifier.requires_security_scan(temp_spec_dir) is True
create_assessment_file(temp_spec_dir, SIMPLE_ASSESSMENT)
classifier.clear_cache()
assert classifier.requires_security_scan(temp_spec_dir) is False
def test_requires_staging_deployment(self, temp_spec_dir, classifier):
"""Correctly identifies staging deployment requirement."""
create_assessment_file(temp_spec_dir, COMPLEX_ASSESSMENT)
assert classifier.requires_staging_deployment(temp_spec_dir) is True
def test_get_risk_level(self, temp_spec_dir, classifier):
"""Returns correct risk level."""
create_assessment_file(temp_spec_dir, COMPLEX_ASSESSMENT)
assert classifier.get_risk_level(temp_spec_dir) == "critical"
classifier.clear_cache()
create_assessment_file(temp_spec_dir, SIMPLE_ASSESSMENT)
assert classifier.get_risk_level(temp_spec_dir) == "low"
def test_get_complexity(self, temp_spec_dir, classifier):
"""Returns correct complexity level."""
create_assessment_file(temp_spec_dir, COMPLEX_ASSESSMENT)
assert classifier.get_complexity(temp_spec_dir) == "complex"
classifier.clear_cache()
create_assessment_file(temp_spec_dir, SIMPLE_ASSESSMENT)
assert classifier.get_complexity(temp_spec_dir) == "simple"
# =============================================================================
# TESTS: VALIDATION SUMMARY
# =============================================================================
class TestValidationSummary:
"""Tests for get_validation_summary method."""
def test_returns_full_summary(self, temp_spec_dir, classifier):
"""Returns complete validation summary."""
create_assessment_file(temp_spec_dir, COMPLEX_ASSESSMENT)
summary = classifier.get_validation_summary(temp_spec_dir)
assert summary["risk_level"] == "critical"
assert summary["complexity"] == "complex"
assert summary["skip_validation"] is False
assert summary["security_scan"] is True
assert summary["staging_deployment"] is True
assert "unit" in summary["test_types"]
def test_returns_unknown_for_missing_file(self, temp_spec_dir, classifier):
"""Returns unknown values when file doesn't exist."""
summary = classifier.get_validation_summary(temp_spec_dir)
assert summary["risk_level"] == "unknown"
assert summary["complexity"] == "unknown"
assert summary["confidence"] == 0.0
# =============================================================================
# TESTS: CONVENIENCE FUNCTIONS
# =============================================================================
class TestConvenienceFunctions:
"""Tests for module-level convenience functions."""
def test_load_risk_assessment(self, temp_spec_dir):
"""load_risk_assessment function works."""
create_assessment_file(temp_spec_dir, SIMPLE_ASSESSMENT)
assessment = load_risk_assessment(temp_spec_dir)
assert assessment is not None
assert assessment.complexity == "simple"
def test_get_validation_requirements(self, temp_spec_dir):
"""get_validation_requirements function works."""
create_assessment_file(temp_spec_dir, COMPLEX_ASSESSMENT)
requirements = get_validation_requirements(temp_spec_dir)
assert requirements["risk_level"] == "critical"
assert "unit" in requirements["test_types"]
# =============================================================================
# TESTS: DATACLASS PROPERTIES
# =============================================================================
class TestDataclassProperties:
"""Tests for dataclass properties."""
def test_risk_assessment_risk_level_property(self, temp_spec_dir, classifier):
"""RiskAssessment.risk_level property works."""
create_assessment_file(temp_spec_dir, COMPLEX_ASSESSMENT)
assessment = classifier.load_assessment(temp_spec_dir)
assert assessment.risk_level == "critical"
assert assessment.risk_level == assessment.validation.risk_level