Spaces:
Sleeping
Sleeping
| """ | |
| Tests for the shared heuristics module (server/heuristics.py). | |
| These tests verify that component, severity, priority, and info-request logic | |
| produce the expected outputs for representative ticket inputs. | |
| """ | |
| from __future__ import annotations | |
| import pytest | |
| from datetime import datetime | |
| from models import TicketModel | |
| from server.heuristics import ( | |
| infer_component, | |
| infer_severity, | |
| infer_priority, | |
| needs_more_info, | |
| suggested_info_type, | |
| severity_to_priority, | |
| COMPONENT_TEAM_MAP, | |
| ) | |
| AVAILABLE_COMPONENTS = [ | |
| "api-gateway", "auth-service", "user-service", "payment-service", | |
| "web-app", "ios-app", "android-app", "database", "cache", "cdn", | |
| ] | |
| AVAILABLE_TEAMS = [ | |
| "backend-api", "frontend-web", "mobile-ios", | |
| "mobile-android", "infrastructure", "data-platform", | |
| ] | |
| def _ticket(**kwargs) -> TicketModel: | |
| """Helper to construct a minimal TicketModel with sane defaults.""" | |
| defaults = dict( | |
| ticket_id="BUG-TEST", | |
| title="Test ticket", | |
| description="A test description", | |
| reporter_type="user", | |
| service="api", | |
| component_candidates=["api-gateway"], | |
| created_at=datetime(2024, 1, 1), | |
| customer_tier="pro", | |
| repro_steps_present=True, | |
| logs_present=True, | |
| attachments_count=0, | |
| suspected_duplicate_ids=[], | |
| ) | |
| defaults.update(kwargs) | |
| return TicketModel(**defaults) | |
| # --------------------------------------------------------------------------- | |
| # severity_to_priority | |
| # --------------------------------------------------------------------------- | |
| class TestSeverityToPriority: | |
| def test_sev0_maps_to_p0(self): | |
| assert severity_to_priority("sev0") == "p0" | |
| def test_sev3_maps_to_p3(self): | |
| assert severity_to_priority("sev3") == "p3" | |
| def test_unknown_defaults_to_p2(self): | |
| assert severity_to_priority("unknown") == "p2" | |
| # --------------------------------------------------------------------------- | |
| # infer_component | |
| # --------------------------------------------------------------------------- | |
| class TestInferComponent: | |
| def test_returns_first_candidate_when_no_available(self): | |
| t = _ticket(component_candidates=["ios-app", "auth-service"]) | |
| result = infer_component(t, []) | |
| assert result == "api-gateway" # fallback default | |
| def test_keyword_match_wins(self): | |
| t = _ticket( | |
| title="iOS login broken", | |
| description="tapping login does nothing on iPhone", | |
| service="mobile-app", | |
| component_candidates=["ios-app", "auth-service"], | |
| ) | |
| result = infer_component(t, AVAILABLE_COMPONENTS) | |
| assert result == "ios-app" | |
| def test_payment_keyword_picks_payment_service(self): | |
| t = _ticket( | |
| title="Payment timeout", | |
| description="checkout is failing, billing error", | |
| service="payments", | |
| component_candidates=["payment-service", "api-gateway"], | |
| ) | |
| result = infer_component(t, AVAILABLE_COMPONENTS) | |
| assert result == "payment-service" | |
| def test_cdn_keyword(self): | |
| t = _ticket( | |
| title="Images broken", | |
| description="CDN asset fails to load", | |
| service="web-app", | |
| component_candidates=["cdn", "web-app"], | |
| ) | |
| result = infer_component(t, AVAILABLE_COMPONENTS) | |
| assert result == "cdn" | |
| def test_filters_out_unavailable_candidates(self): | |
| t = _ticket(component_candidates=["nonexistent-service", "database"]) | |
| result = infer_component(t, AVAILABLE_COMPONENTS) | |
| assert result == "database" | |
| # --------------------------------------------------------------------------- | |
| # infer_severity | |
| # --------------------------------------------------------------------------- | |
| class TestInferSeverity: | |
| def test_security_keyword_is_sev0(self): | |
| t = _ticket(description="unauthorized access detected, security breach") | |
| assert infer_severity(t, "auth-service") == "sev0" | |
| def test_double_charge_is_sev0(self): | |
| t = _ticket(description="customer was double charged") | |
| assert infer_severity(t, "payment-service") == "sev0" | |
| def test_timeout_is_sev1(self): | |
| t = _ticket(description="service is timing out after 30s") | |
| assert infer_severity(t, "api-gateway") == "sev1" | |
| def test_outage_is_sev1(self): | |
| t = _ticket(description="full outage detected by monitoring") | |
| assert infer_severity(t, "api-gateway") == "sev1" | |
| def test_crash_is_sev2(self): | |
| t = _ticket(description="app crashes on launch every time") | |
| assert infer_severity(t, "android-app") == "sev2" | |
| def test_slow_db_free_tier_is_sev3(self): | |
| t = _ticket( | |
| description="dashboard is slow", | |
| customer_tier="free", | |
| ) | |
| assert infer_severity(t, "database") == "sev3" | |
| def test_slow_db_enterprise_is_sev2(self): | |
| t = _ticket( | |
| description="dashboard is slow", | |
| customer_tier="enterprise", | |
| ) | |
| assert infer_severity(t, "database") == "sev2" | |
| def test_incorrect_tax_enterprise_is_sev1(self): | |
| t = _ticket(description="incorrect tax calculation on invoice", customer_tier="enterprise") | |
| assert infer_severity(t, "payment-service") == "sev1" | |
| def test_incorrect_tax_free_is_sev2(self): | |
| t = _ticket(description="incorrect tax calculation on invoice", customer_tier="free") | |
| assert infer_severity(t, "payment-service") == "sev2" | |
| def test_low_signal_free_is_sev3(self): | |
| t = _ticket(description="something is wrong, signal quality is low", customer_tier="free") | |
| assert infer_severity(t, "web-app") == "sev3" | |
| def test_no_keywords_default_sev3(self): | |
| t = _ticket(title="Minor UI glitch", description="button looks slightly off") | |
| assert infer_severity(t, "web-app") == "sev3" | |
| # --------------------------------------------------------------------------- | |
| # infer_priority | |
| # --------------------------------------------------------------------------- | |
| class TestInferPriority: | |
| def test_sev2_enterprise_upgrades_to_p1(self): | |
| t = _ticket(customer_tier="enterprise") | |
| assert infer_priority(t, "sev2") == "p1" | |
| def test_sev2_monitoring_upgrades_to_p1(self): | |
| t = _ticket(reporter_type="monitoring", customer_tier="free") | |
| assert infer_priority(t, "sev2") == "p1" | |
| def test_sev2_payment_keyword_upgrades_to_p1(self): | |
| t = _ticket(description="payment gateway slow", customer_tier="free") | |
| assert infer_priority(t, "sev2") == "p1" | |
| def test_sev3_free_stays_p3(self): | |
| t = _ticket(customer_tier="free") | |
| assert infer_priority(t, "sev3") == "p3" | |
| # --------------------------------------------------------------------------- | |
| # needs_more_info | |
| # --------------------------------------------------------------------------- | |
| class TestNeedsMoreInfo: | |
| def test_no_repro_no_logs_needs_info(self): | |
| t = _ticket(repro_steps_present=False, logs_present=False) | |
| assert needs_more_info(t) is True | |
| def test_duplicate_overrides_info_need(self): | |
| t = _ticket( | |
| repro_steps_present=False, | |
| logs_present=False, | |
| suspected_duplicate_ids=["BUG-999"], | |
| ) | |
| assert needs_more_info(t) is False | |
| def test_user_without_logs_needs_info(self): | |
| t = _ticket(reporter_type="user", repro_steps_present=True, logs_present=False) | |
| assert needs_more_info(t) is True | |
| def test_monitoring_without_repro_ok(self): | |
| """Monitoring reporters don't need manual repro steps.""" | |
| t = _ticket(reporter_type="monitoring", repro_steps_present=False, logs_present=True) | |
| assert needs_more_info(t) is False | |
| def test_low_signal_flag_needs_info(self): | |
| t = _ticket( | |
| description="signal quality is low on this ticket", | |
| repro_steps_present=True, | |
| logs_present=True, | |
| ) | |
| assert needs_more_info(t) is True | |
| def test_full_info_not_needed(self): | |
| t = _ticket(repro_steps_present=True, logs_present=True, reporter_type="user") | |
| assert needs_more_info(t) is False | |
| # --------------------------------------------------------------------------- | |
| # suggested_info_type | |
| # --------------------------------------------------------------------------- | |
| class TestSuggestedInfoType: | |
| def test_neither_present_returns_both(self): | |
| t = _ticket(repro_steps_present=False, logs_present=False) | |
| assert suggested_info_type(t) == "both" | |
| def test_only_logs_missing_returns_logs(self): | |
| t = _ticket(repro_steps_present=True, logs_present=False) | |
| assert suggested_info_type(t) == "logs" | |
| def test_only_repro_missing_returns_repro_steps(self): | |
| t = _ticket(repro_steps_present=False, logs_present=True) | |
| assert suggested_info_type(t) == "repro_steps" | |
| # --------------------------------------------------------------------------- | |
| # COMPONENT_TEAM_MAP sanity check | |
| # --------------------------------------------------------------------------- | |
| class TestComponentTeamMap: | |
| def test_all_components_have_teams(self): | |
| for component in AVAILABLE_COMPONENTS: | |
| assert component in COMPONENT_TEAM_MAP, f"{component} missing from COMPONENT_TEAM_MAP" | |
| def test_teams_are_valid(self): | |
| for team in COMPONENT_TEAM_MAP.values(): | |
| assert team in AVAILABLE_TEAMS, f"Team '{team}' not in AVAILABLE_TEAMS" | |