"""Pure-logic guards for clustering, classification and the vocabularies. These are the cheap tests that catch the expensive mistakes: a cluster key that merges two vendors, a numeric pattern that matches an id, or a root cause with no remedy behind it. """ from __future__ import annotations import pytest from app.domains.automation_exception.collectors import ( COLLECTORS, SOURCE_REF_TYPES, ) from app.domains.automation_exception.rules import classify_cause, compute_severity from app.domains.automation_exception.signature import ( NO_ERROR_TEXT, canonical_signature, signature_hash, ) from app.domains.automation_exception.taxonomy import ( CAUSE_CREDENTIAL, CAUSE_DATA_QUALITY, CAUSE_PLATFORM_DEFECT, CAUSE_TARGET_MISSING, CAUSE_THROTTLED, CAUSE_UNKNOWN, CAUSE_VENDOR_POLICY, REMEDY_BY_CAUSE, REMEDY_INVESTIGATE, ROOT_CAUSES, SEVERITIES, SEVERITY_CRITICAL, SEVERITY_HIGH, SEVERITY_LOW, SEVERITY_ORDER, SOURCES, STATUSES, remedy_for, validate_cause, ) from app.notifications.categories import BUCKET_ACTION, BUCKET_SOCIAL class TestCanonicalSignature: def test_row_identity_is_stripped_so_rows_cluster(self): a = canonical_signature( "Feishu open_id not found for email alice@pingcap.com " "(request 3f2b8c1e-9a44-4d55-b6c1-0a1b2c3d4e5f)", ) b = canonical_signature( "Feishu open_id not found for email bob@example.com " "(request 7c9d1a2b-1111-2222-3333-444455556666)", ) assert a == b assert "" in a and "" in a def test_blank_and_none_get_a_real_label_not_an_empty_string(self): assert canonical_signature(None) == NO_ERROR_TEXT assert canonical_signature(" ") == NO_ERROR_TEXT def test_vendor_error_codes_survive_because_they_are_the_identity(self): # A 429 and a 404 must never collapse into one cluster. throttle = canonical_signature("HTTP 429 Too Many Requests") missing = canonical_signature("HTTP 404 user not found") assert throttle != missing assert "429" in throttle def test_signature_is_capped_but_still_readable(self): sig = canonical_signature("vendor said no. " + "x" * 5000) assert len(sig) <= 200 assert sig.startswith("vendor said no.") class TestSignatureHash: def test_same_sentence_different_scope_is_a_different_problem(self): sig = "rate limited" zoom = signature_hash(source="provisioning", scope_key="zoom", signature=sig) google = signature_hash( source="provisioning", scope_key="google", signature=sig, ) assert zoom != google def test_same_sentence_different_source_is_a_different_problem(self): sig = "401 unauthorized" a = signature_hash(source="provisioning", scope_key="zoom", signature=sig) b = signature_hash(source="identity_sync", scope_key="zoom", signature=sig) assert a != b def test_absent_scope_never_collides_with_empty_scope(self): # A collector that starts supplying a scope later must not merge # its new rows into its old ones. sig = "boom" none_scope = signature_hash(source="integration", scope_key=None, signature=sig) empty_scope = signature_hash(source="integration", scope_key="", signature=sig) assert none_scope != empty_scope def test_hash_is_stable_and_fits_the_column(self): h = signature_hash(source="integration", scope_key="x", signature="y") assert h == signature_hash(source="integration", scope_key="X", signature="y") assert len(h) == 32 class TestClassifyCause: @pytest.mark.parametrize( ("text", "expected"), [ ("Traceback (most recent call last): KeyError: 'x'", CAUSE_PLATFORM_DEFECT), ("401 Bad credentials", CAUSE_CREDENTIAL), ("invalid_grant: token has expired", CAUSE_CREDENTIAL), ("HTTP 429 Too Many Requests", CAUSE_THROTTLED), ("503 Service Unavailable", CAUSE_THROTTLED), ("plus_teams_only: SCIM not available in your plan", CAUSE_VENDOR_POLICY), ("No available license for this user", CAUSE_VENDOR_POLICY), ("404 user not found", CAUSE_TARGET_MISSING), ("Invalid email format", CAUSE_DATA_QUALITY), ("user already exists", CAUSE_DATA_QUALITY), ], ) def test_recognisable_vendor_wording(self, text, expected): assert classify_cause(text) == expected def test_numeric_codes_are_matched_on_word_boundaries(self): # The bug this guards: ``"429" in text`` also matches "4290". # A not-found filed as "rate limited" hands the admin a remedy of # "wait", for something that will never recover on its own. assert classify_cause("group 4290 not found") == CAUSE_TARGET_MISSING assert classify_cause("15034 rows; account not found") == CAUSE_TARGET_MISSING def test_a_credential_failure_wins_over_the_noise_around_it(self): # Vendors routinely pair a 401 with a permission or not-found # sentence; only a new credential fixes any of it. assert classify_cause( "401 unauthorized: user not found in directory", ) == CAUSE_CREDENTIAL def test_silence_when_unsure(self): assert classify_cause("the flux capacitor disagreed") is None assert classify_cause("") is None assert classify_cause(None) is None def test_an_unbounded_gap_does_not_pair_distant_words(self): # "user" at the top of a traceback must not pair with a # "not found" hundreds of characters later. assert classify_cause("user something\n" + ("x" * 200) + "\nnot found") is None class TestComputeSeverity: def test_a_leaver_whose_access_was_not_revoked_is_critical(self): assert compute_severity( source="provisioning", action="revoke", subject_is_leaver=True, ) == SEVERITY_CRITICAL def test_a_hire_with_no_account_is_critical(self): assert compute_severity( source="provisioning", action="grant", subject_is_pending_hire=True, ) == SEVERITY_CRITICAL def test_a_refused_credential_on_a_live_integration_is_critical(self): assert compute_severity( source="integration", root_cause=CAUSE_CREDENTIAL, integration_enabled=True, ) == SEVERITY_CRITICAL def test_the_same_credential_on_a_disabled_integration_is_low(self): assert compute_severity( source="integration", root_cause=CAUSE_CREDENTIAL, integration_enabled=False, ) == SEVERITY_LOW def test_notification_bucket_uses_the_real_vocabulary(self): # Guards a real bug caught in review: this set was first written # as {"actionable", "critical"} — words that appear nowhere in # the notifications domain, so every undelivered actionable # notification would have been rated ``normal``. actionable = compute_severity( source="notification", notification_bucket=BUCKET_ACTION, ) social = compute_severity( source="notification", notification_bucket=BUCKET_SOCIAL, ) assert actionable == SEVERITY_HIGH assert actionable != social def test_cron_severity_follows_the_declared_slo_tier(self): assert compute_severity(source="scheduled_job", slo_tier="P0") == SEVERITY_HIGH assert compute_severity(source="scheduled_job", slo_tier="P2") == SEVERITY_LOW class TestVocabularies: def test_every_root_cause_has_a_remedy(self): # A cause with no remedy is a row in the workbench with no next # step — the exact dead end this whole ledger exists to remove. assert set(REMEDY_BY_CAUSE) == set(ROOT_CAUSES) def test_unknown_cause_routes_to_investigation(self): assert remedy_for(CAUSE_UNKNOWN) == REMEDY_INVESTIGATE def test_remedy_for_tolerates_a_cause_this_build_does_not_know(self): # Forward compatibility during a rolling deploy: an older pod # reading a newer row must still render a next step. assert remedy_for("a_cause_from_the_future") == REMEDY_INVESTIGATE def test_validate_cause_rejects_anything_outside_the_set(self): with pytest.raises(ValueError, match="unknown root cause"): validate_cause("made_up") def test_severity_order_is_total_and_puts_critical_first(self): assert set(SEVERITY_ORDER) == set(SEVERITIES) assert SEVERITY_ORDER[SEVERITY_CRITICAL] == min(SEVERITY_ORDER.values()) def test_statuses_and_sources_are_non_empty_and_unique(self): assert len(set(STATUSES)) == len(STATUSES) assert len(set(SOURCES)) == len(SOURCES) class TestCollectorRegistry: def test_every_registered_collector_uses_a_known_source(self): for source, _fn in COLLECTORS: assert source in SOURCES def test_all_five_sources_have_a_collector(self): assert {source for source, _ in COLLECTORS} == set(SOURCES) def test_every_source_declares_the_row_type_it_points_at(self): # The read surfaces build deep links from this map; a missing # entry is a detail page with an evidence link that goes nowhere. assert set(SOURCE_REF_TYPES) == {source for source, _ in COLLECTORS} assert all(SOURCE_REF_TYPES.values())