From 0663ee554268cf8a4aef0820df3c5674301b2f3a Mon Sep 17 00:00:00 2001 From: guke Date: Wed, 5 Aug 2026 14:52:47 +0800 Subject: [PATCH] =?UTF-8?q?fix(compare-alert):=20worker=20import=20?= =?UTF-8?q?=E6=8E=92=E5=BA=8F=20+=20build=5Fhits=20=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=20+=20=E5=85=B1=E4=BA=AB=E9=A2=84=E7=AE=97?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- app/core/compare_alert_worker.py | 7 ++++--- tests/test_compare_alert_stuck_worker.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/app/core/compare_alert_worker.py b/app/core/compare_alert_worker.py index 0de40d9..1615d09 100644 --- a/app/core/compare_alert_worker.py +++ b/app/core/compare_alert_worker.py @@ -12,6 +12,7 @@ import logging import os import time from collections.abc import Iterator +from dataclasses import replace as _dc_replace from datetime import datetime from pathlib import Path @@ -25,10 +26,9 @@ from app.integrations import feishu_notifier from app.models.app_config import AppConfig from app.models.comparison import ComparisonRecord from app.models.user import User -from dataclasses import replace as _dc_replace - from app.services import trace_stuck from app.services.compare_alert import ( + AlertHit, classify_cancelled_fallback, classify_record, make_hit, @@ -73,6 +73,7 @@ def _send(title: str, content: list) -> None: def _trace_dir(base: Path, trace_url: str | None) -> Path | None: + """URL → trace 目录 Path;trace_url 缺失/无法解析 → None。""" name = trace_stuck.dir_name_from_trace_url(trace_url) if not name: return None @@ -91,7 +92,7 @@ def build_hits( timeout_keywords: tuple[str, ...], unrecognized_keywords: tuple[str, ...], biz_exclude_keywords: tuple[str, ...], -) -> list: +) -> list[AlertHit]: """编排:cancelled 走 trace 优先(读到确认没卡则不报,读不到回退保底);failed 附卡点。 trace 读取限量 max_trace_reads 次/轮;任何 trace 异常都在 trace_stuck 内部降级为 diff --git a/tests/test_compare_alert_stuck_worker.py b/tests/test_compare_alert_stuck_worker.py index 18bfcb5..2331337 100644 --- a/tests/test_compare_alert_stuck_worker.py +++ b/tests/test_compare_alert_stuck_worker.py @@ -92,3 +92,22 @@ def test_max_trace_reads_zero_skips_trace(tmp_path): hits = build_hits([rec], work_log_dir=str(tmp_path), **kw) # 没读 trace → 回退保底 → deep(95s) → 深度放弃 assert len(hits) == 1 and "深度放弃" in hits[0].reason + + +def test_shared_reads_budget_across_branches(tmp_path): + # cancelled 和 failed 共用 max_trace_reads 预算;预算=1 时 cancelled 先消耗,failed 拿不到卡点 + for i in range(18): + _frame(tmp_path / "20260804_c" / "meituan", i, "add_one_dish", "meal_detail_popup") + for i in range(20): + _frame(tmp_path / "20260804_d" / "meituan", i, "add_one_dish", "meal_detail_popup") + cancelled = _Rec(status="cancelled", trace_url="https://x/traces/20260804_c/", + total_ms=5000, step_count=3) + failed = _Rec(status="failed", fail_reason="启动超时", + trace_url="https://x/traces/20260804_d/") + kw = {**_KW, "max_trace_reads": 1} + hits = build_hits([cancelled, failed], work_log_dir=str(tmp_path), **kw) + assert len(hits) == 2 + # cancelled 消耗了唯一预算 → 报卡死 + assert hits[0].alert_type == "T5" and "卡在" in hits[0].reason + # failed 超预算 → 仍报 T2,但不附卡点 + assert hits[1].alert_type == "T2" and "卡在" not in hits[1].reason