|
| 1 | +from datetime import datetime, timedelta, timezone |
| 2 | + |
| 3 | +from src.core.clustering.clusterer import ClusterData |
| 4 | +from src.core.explain.evidence import EvidencePacket |
| 5 | +from src.core.timeline.builder import build_timeline |
| 6 | + |
| 7 | +BASE = datetime(2026, 1, 1, 22, 0, 0, tzinfo=timezone.utc) |
| 8 | + |
| 9 | + |
| 10 | +def _cluster(message: str, offset_minutes: int, fingerprint: str = "") -> ClusterData: |
| 11 | + ts = BASE + timedelta(minutes=offset_minutes) |
| 12 | + return ClusterData( |
| 13 | + fingerprint=fingerprint or message, |
| 14 | + representative_message=message, |
| 15 | + count=10, |
| 16 | + services={"api": 10}, |
| 17 | + levels={"error": 10}, |
| 18 | + first_seen=ts, |
| 19 | + last_seen=ts, |
| 20 | + baseline_count=0, |
| 21 | + change_ratio=0.0, |
| 22 | + importance_score=1.0, |
| 23 | + ) |
| 24 | + |
| 25 | + |
| 26 | +def _packet(primary: ClusterData, secondary: list[ClusterData]) -> EvidencePacket: |
| 27 | + return EvidencePacket( |
| 28 | + window_start=BASE, |
| 29 | + window_end=BASE + timedelta(hours=1), |
| 30 | + total_logs=100, |
| 31 | + primary_cluster=primary, |
| 32 | + secondary_clusters=secondary, |
| 33 | + trigger_candidates=[], |
| 34 | + evidence_items=[], |
| 35 | + services_affected=["api"], |
| 36 | + ) |
| 37 | + |
| 38 | + |
| 39 | +class TestTimelineOrdering: |
| 40 | + def test_error_effect_sorts_before_earlier_symptom(self): |
| 41 | + """Regression for #65: severity ranks before timestamp. |
| 42 | +
|
| 43 | + A 500/error effect must sort ahead of a queue-backlog symptom even |
| 44 | + when the symptom appeared first — the docstring promises "error |
| 45 | + effects before … symptoms last", not chronological order. |
| 46 | + """ |
| 47 | + primary = _cluster("primary 500 error", 0) |
| 48 | + symptom = _cluster("queue backlog growing", 1) # earlier |
| 49 | + effect = _cluster("checkout 500 error failed", 5) # later, more severe |
| 50 | + |
| 51 | + timeline = build_timeline(_packet(primary, [symptom, effect])) |
| 52 | + categories = [e.category for e in timeline] |
| 53 | + |
| 54 | + # primary error first, then the effect, then the symptom last |
| 55 | + assert categories == ["error", "effect", "symptom"] |
| 56 | + # the later-but-more-severe effect precedes the earlier symptom |
| 57 | + eff_idx = next(i for i, e in enumerate(timeline) if "checkout" in e.description) |
| 58 | + sym_idx = next(i for i, e in enumerate(timeline) if "backlog" in e.description) |
| 59 | + assert eff_idx < sym_idx |
| 60 | + |
| 61 | + def test_timestamp_breaks_ties_within_category(self): |
| 62 | + """Equally-severe effects still order by timestamp.""" |
| 63 | + primary = _cluster("primary 500 error", 0) |
| 64 | + later = _cluster("checkout 500 error late", 9) |
| 65 | + earlier = _cluster("payment 500 error early", 3) |
| 66 | + |
| 67 | + timeline = build_timeline(_packet(primary, [later, earlier])) |
| 68 | + effects = [e.description for e in timeline if e.category == "effect"] |
| 69 | + assert effects.index("payment 500 error early") < effects.index("checkout 500 error late") |
0 commit comments