From 5cff0ff3008a60701dddc4d11efb81f25a6f7a1e Mon Sep 17 00:00:00 2001 From: Li SiYi Date: Tue, 13 Oct 2026 14:25:12 +0000 Subject: [PATCH] fix: honor Cloud Run default zero traffic and project fixed structural reasons --- docs/notify-route-identity.zh-CN.md | 2 + scripts/notify_route_check.py | 42 +++++++++++++++- tests/test_notify_route_check.py | 76 +++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 2 deletions(-) diff --git a/docs/notify-route-identity.zh-CN.md b/docs/notify-route-identity.zh-CN.md index 314f0d0..15825fb 100644 --- a/docs/notify-route-identity.zh-CN.md +++ b/docs/notify-route-identity.zh-CN.md @@ -19,6 +19,8 @@ stdout 和 GitHub summary 使用 `qsl.notify_route_identity.v1`:固定 status/ `status=ok` / exit 0 仅表示诊断形成结果,**不表示 route 已就绪**。看逐服务 `bot_status=match|mismatch|unknown`、`same_bot_as_contract=true|false|null` 与固定 reason。scope 始终 `project_contract_only`,`canonical_route_status=unknown`。 +附加 `traffic_issue` / `traffic_detail` 只给出固定结构原因;依据 [Cloud Run REST v1 TrafficTarget](https://cloud.google.com/run/docs/reference/rest/v1/TrafficTarget) 的“defaults to zero if unspecified”,仅字典行完全缺少 percent 键时沿用 zero skip 并记 `percent_default_zero`(显式 null/bool/string/float 或越界仍拒绝),缺省零本身不证明 positive traffic、批准身份或就绪,也不作宽泛 parser 迁移。 + ## 尚未验收的入口 `same_chat_as_canonical` 和 `same_topic_as_canonical` 固定 null、对应 status unknown:当前没有批准的中央 private canonical reference,本批不创造新 secret、全局路由规则或 topic 值,也不额外读取 chat secret。既有 runtime chat 为 QSL_GLOBAL 优先 GLOBAL;heartbeat/guard 只遍历 GLOBAL 全列表;中央日报优先 QSL_GLOBAL/GLOBAL/plugin 且只取首 ID。它们需要按各自原 selector 在受保护比较边界取得三态证据,不能重新统一解析来使其匹配。 diff --git a/scripts/notify_route_check.py b/scripts/notify_route_check.py index 21570f6..194ec32 100644 --- a/scripts/notify_route_check.py +++ b/scripts/notify_route_check.py @@ -163,7 +163,7 @@ def _traffic(service: dict) -> tuple[list[str], str, str]: serving = [] total = 0 for row in rows: - percent = row.get("percent") if isinstance(row, dict) else None + percent = row.get("percent", 0) if isinstance(row, dict) else None if type(percent) is not int or not 0 <= percent <= 100: return [], "unknown", "traffic_invalid" if percent == 0: @@ -180,6 +180,42 @@ def _traffic(service: dict) -> tuple[list[str], str, str]: return serving, "single" if len(serving) == 1 else "split", "none" +def _traffic_shape(service: dict, reason: str) -> tuple[str, str]: + """Project fixed structural reasons without changing the parser's decision.""" + status = service.get("status") + if not isinstance(status, dict) or "traffic" not in status: + return "traffic_missing", "none" + rows = status["traffic"] + if not isinstance(rows, list): + return "not_list", "none" + detail = "none" + seen = set() + for row in rows: + if not isinstance(row, dict): + return "row_invalid", detail + if "percent" not in row: + # REST v1 TrafficTarget: only an unspecified percent defaults to zero. + detail = "percent_default_zero" + continue + percent = row["percent"] + if type(percent) is not int: + return "percent_type_invalid", detail + if not 0 <= percent <= 100: + return "percent_out_of_range", detail + if percent == 0: + continue + if "revisionName" not in row: + return "revision_missing", detail + name = row["revisionName"] + if not isinstance(name, str) or not re.fullmatch(_SERVICE, name): + return "revision_invalid", detail + if name in seen: + return "duplicate", detail + seen.add(name) + issue = {"traffic_incomplete": "incomplete", "no_positive_traffic": "no_positive", "none": "none"} + return issue.get(reason, "unavailable"), detail + + def _unknown_comparison() -> dict: return { "bot_status": "unknown", "same_bot_as_contract": None, @@ -190,6 +226,7 @@ def _unknown_comparison() -> dict: def _observe_service(service: str, ordinal: int, project: str, region: str, contract_id: int | None) -> dict: out = {"service_label": f"service#{ordinal}", "traffic_status": "unknown", + "traffic_issue": "unavailable", "traffic_detail": "unavailable", "reason_code": "service_describe_failed", **_unknown_comparison(), "revisions": []} try: spec = json.loads(_gcloud("run", "services", "describe", service, f"--project={project}", @@ -200,7 +237,8 @@ def _observe_service(service: str, ordinal: int, project: str, region: str, cont out["reason_code"] = "service_metadata_invalid" return out serving, traffic_status, reason = _traffic(spec) - out.update(traffic_status=traffic_status, reason_code=reason) + issue, detail = _traffic_shape(spec, reason) + out.update(traffic_status=traffic_status, reason_code=reason, traffic_issue=issue, traffic_detail=detail) if not serving: return out for index, name in enumerate(serving, start=1): diff --git a/tests/test_notify_route_check.py b/tests/test_notify_route_check.py index 5089841..9ee77f2 100644 --- a/tests/test_notify_route_check.py +++ b/tests/test_notify_route_check.py @@ -391,3 +391,79 @@ def test_workflow_protects_existing_inputs_before_headers_and_keeps_manual_main_ assert "SOURCE_SHA: ${{ github.sha }}" in protected assert "::add-mask::" not in protected assert "run: python3 scripts/notify_route_check.py" in protected + + +@pytest.mark.parametrize("use_default", [False, True]) +def test_documented_zero_target_does_not_hide_positive_serving_revision(tmp_path, use_default): + cloud = FakeCloud() + zero = {"tag": "PRIVATE_ZERO_TAG", "url": "https://PRIVATE_ZERO_URL"} + if not use_default: + zero["percent"] = 0 + cloud.service["status"]["traffic"].append(zero) + report, _, _ = observe(tmp_path, cloud) + row = report["services"][0] + assert row["traffic_status"] == "single" + assert row["same_bot_as_contract"] is False + assert row["traffic_issue"] == "none" + assert row["traffic_detail"] == ("percent_default_zero" if use_default else "none") + assert len(row["revisions"]) == 1 + assert row["revisions"][0]["same_bot_as_contract"] is False + public = json.dumps(report) + (tmp_path / "summary").read_text() + assert "PRIVATE_ZERO_TAG" not in public and "PRIVATE_ZERO_URL" not in public + + +@pytest.mark.parametrize("status,issue,detail", [ + (None, "traffic_missing", "none"), + ({}, "traffic_missing", "none"), + ({"traffic": None}, "not_list", "none"), + ({"traffic": {}}, "not_list", "none"), + ({"traffic": "PRIVATE_BAD_TRAFFIC"}, "not_list", "none"), + ({"traffic": [None]}, "row_invalid", "none"), + ({"traffic": ["PRIVATE_BAD_ROW"]}, "row_invalid", "none"), + ({"traffic": [{"tag": "PRIVATE_ZERO_TAG"}]}, "no_positive", "percent_default_zero"), + ({"traffic": [{"revisionName": REVISION}]}, "no_positive", "percent_default_zero"), + ({"traffic": [{"percent": None}]}, "percent_type_invalid", "none"), + ({"traffic": [{"percent": True}]}, "percent_type_invalid", "none"), + ({"traffic": [{"percent": "100"}]}, "percent_type_invalid", "none"), + ({"traffic": [{"percent": 100.0}]}, "percent_type_invalid", "none"), + ({"traffic": [{"percent": -1}]}, "percent_out_of_range", "none"), + ({"traffic": [{"percent": 101}]}, "percent_out_of_range", "none"), + ({"traffic": [{"percent": 100}]}, "revision_missing", "none"), + ({"traffic": [{"percent": 100, "revisionName": None}]}, "revision_invalid", "none"), + ({"traffic": [{"percent": 100, "revisionName": ""}]}, "revision_invalid", "none"), + ({"traffic": [{"percent": 100, "revisionName": "PRIVATE_BAD_REVISION/path"}]}, "revision_invalid", "none"), + ({"traffic": [{"percent": 50, "revisionName": REVISION}, + {"percent": 50, "revisionName": REVISION}]}, "duplicate", "none"), + ({"traffic": [{"percent": 50, "revisionName": REVISION}]}, "incomplete", "none"), + ({"traffic": []}, "no_positive", "none"), + ({"traffic": [{"percent": 0}]}, "no_positive", "none"), + ({"traffic": [{"tag": "PRIVATE_ZERO_TAG"}, {"percent": False}]}, + "percent_type_invalid", "percent_default_zero"), + ({"traffic": [{"percent": False}, {"tag": "PRIVATE_ZERO_TAG"}]}, "percent_type_invalid", "none"), +]) +def test_traffic_shape_reason_is_fixed_and_never_turns_bad_metadata_into_match(tmp_path, status, issue, detail): + cloud = FakeCloud() + cloud.service["status"] = status + report, _, _ = observe(tmp_path, cloud) + row = report["services"][0] + assert row["traffic_issue"] == issue + assert row["traffic_detail"] == detail + assert row["same_bot_as_contract"] is None + assert row["bot_status"] == "unknown" + assert row["revisions"] == [] + assert not any(args[:3] == ("run", "revisions", "describe") for args in cloud.calls) + summary = (tmp_path / "summary").read_text() + assert '"traffic_issue": "' + issue + '"' in summary + assert '"traffic_detail": "' + detail + '"' in summary + for private in ["PRIVATE_ZERO_TAG", "PRIVATE_ZERO_URL", "PRIVATE_BAD_TRAFFIC", "PRIVATE_BAD_ROW", + "PRIVATE_BAD_REVISION", "PRIVATE_BAD_STATUS"]: + assert private not in json.dumps(report) + summary + + +def test_unread_service_has_unavailable_shape_instead_of_inferred_default(tmp_path): + cloud = FakeCloud() + cloud.fail.add(("service", SERVICE)) + report, _, _ = observe(tmp_path, cloud) + row = report["services"][0] + assert row["traffic_issue"] == row["traffic_detail"] == "unavailable" + assert row["same_bot_as_contract"] is None