fix(awooop): 等待 source correlation review 回寫
This commit is contained in:
171
apps/api/tests/test_awooop_source_correlation_apply_smoke.py
Normal file
171
apps/api/tests/test_awooop_source_correlation_apply_smoke.py
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import sys
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
SCRIPT_PATH = (
|
||||||
|
Path(__file__).resolve().parents[3]
|
||||||
|
/ "scripts"
|
||||||
|
/ "awooop_source_correlation_apply_smoke.py"
|
||||||
|
)
|
||||||
|
SPEC = importlib.util.spec_from_file_location(
|
||||||
|
"awooop_source_correlation_apply_smoke",
|
||||||
|
SCRIPT_PATH,
|
||||||
|
)
|
||||||
|
awooop_source_correlation_apply_smoke = importlib.util.module_from_spec(SPEC)
|
||||||
|
assert SPEC and SPEC.loader
|
||||||
|
sys.dont_write_bytecode = True
|
||||||
|
sys.modules[SPEC.name] = awooop_source_correlation_apply_smoke
|
||||||
|
SPEC.loader.exec_module(awooop_source_correlation_apply_smoke)
|
||||||
|
|
||||||
|
|
||||||
|
def test_failed_check_summary_lists_preflight_failures() -> None:
|
||||||
|
payload = {
|
||||||
|
"checks": [
|
||||||
|
{
|
||||||
|
"name": "source_review_work_item",
|
||||||
|
"passed": True,
|
||||||
|
"detail": "source_correlation_review",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "accepted_review_recorded",
|
||||||
|
"passed": False,
|
||||||
|
"detail": "missing",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "target_incident_present",
|
||||||
|
"passed": False,
|
||||||
|
"detail": "missing target_incident_id",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
assert awooop_source_correlation_apply_smoke._failed_check_summary(payload) == (
|
||||||
|
"accepted_review_recorded=missing, "
|
||||||
|
"target_incident_present=missing target_incident_id"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_source_review_readback_state_detects_accepted_review() -> None:
|
||||||
|
recurrence = {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"work_item": {
|
||||||
|
"kind": "source_correlation_review",
|
||||||
|
"work_item_id": (
|
||||||
|
"source-evidence:sentry:upstream_canary:"
|
||||||
|
"awoooi-source-link-canary-gitea-cd-4294-1"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"source_correlation_review": {
|
||||||
|
"work_item_id": (
|
||||||
|
"source-evidence:sentry:upstream_canary:"
|
||||||
|
"awoooi-source-link-canary-gitea-cd-4294-1"
|
||||||
|
),
|
||||||
|
"decision": "accepted",
|
||||||
|
"review_id": "review-1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
state = awooop_source_correlation_apply_smoke._source_review_readback_state(
|
||||||
|
recurrence,
|
||||||
|
work_item_id=(
|
||||||
|
"source-evidence:sentry:upstream_canary:"
|
||||||
|
"awoooi-source-link-canary-gitea-cd-4294-1"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert state == {
|
||||||
|
"found": True,
|
||||||
|
"decision": "accepted",
|
||||||
|
"review_id": "review-1",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_source_review_readback_state_reports_missing_review() -> None:
|
||||||
|
recurrence = {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"work_item": {
|
||||||
|
"kind": "source_correlation_review",
|
||||||
|
"work_item_id": "source-evidence:sentry:upstream_canary:item-1",
|
||||||
|
},
|
||||||
|
"source_correlation_review": None,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
state = awooop_source_correlation_apply_smoke._source_review_readback_state(
|
||||||
|
recurrence,
|
||||||
|
work_item_id="source-evidence:sentry:upstream_canary:item-1",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert state == {
|
||||||
|
"found": True,
|
||||||
|
"decision": "missing",
|
||||||
|
"review_id": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_wait_for_review_readback_retries_until_accepted(monkeypatch) -> None:
|
||||||
|
work_item_id = "source-evidence:sentry:upstream_canary:item-1"
|
||||||
|
calls: list[str] = []
|
||||||
|
recurrences = [
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"work_item": {
|
||||||
|
"kind": "source_correlation_review",
|
||||||
|
"work_item_id": work_item_id,
|
||||||
|
},
|
||||||
|
"source_correlation_review": None,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"work_item": {
|
||||||
|
"kind": "source_correlation_review",
|
||||||
|
"work_item_id": work_item_id,
|
||||||
|
},
|
||||||
|
"source_correlation_review": {
|
||||||
|
"work_item_id": work_item_id,
|
||||||
|
"decision": "accepted",
|
||||||
|
"review_id": "review-2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
def fake_http_json(url: str, **_: object) -> dict[str, object]:
|
||||||
|
calls.append(url)
|
||||||
|
return recurrences[min(len(calls) - 1, len(recurrences) - 1)]
|
||||||
|
|
||||||
|
monkeypatch.setattr(awooop_source_correlation_apply_smoke, "_http_json", fake_http_json)
|
||||||
|
monkeypatch.setattr(awooop_source_correlation_apply_smoke.time, "sleep", lambda _: None)
|
||||||
|
|
||||||
|
state = awooop_source_correlation_apply_smoke._wait_for_review_readback(
|
||||||
|
args=SimpleNamespace(
|
||||||
|
api_url="https://awoooi.wooo.work",
|
||||||
|
project_id="awoooi",
|
||||||
|
provider="sentry",
|
||||||
|
limit=300,
|
||||||
|
review_readback_attempts=2,
|
||||||
|
review_readback_interval_seconds=0,
|
||||||
|
),
|
||||||
|
work_item_id=work_item_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert state == {
|
||||||
|
"found": True,
|
||||||
|
"decision": "accepted",
|
||||||
|
"review_id": "review-2",
|
||||||
|
}
|
||||||
|
assert len(calls) == 2
|
||||||
@@ -301,6 +301,88 @@ def _refresh_candidate_result(item: dict[str, Any]) -> dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _failed_check_summary(payload: dict[str, Any]) -> str:
|
||||||
|
checks = payload.get("checks")
|
||||||
|
if not isinstance(checks, list):
|
||||||
|
return "-"
|
||||||
|
failed: list[str] = []
|
||||||
|
for check in checks:
|
||||||
|
if not isinstance(check, dict) or check.get("passed") is True:
|
||||||
|
continue
|
||||||
|
name = str(check.get("name") or "unknown").strip() or "unknown"
|
||||||
|
detail = str(check.get("detail") or "failed").strip() or "failed"
|
||||||
|
failed.append(f"{name}={detail}")
|
||||||
|
return ", ".join(failed) if failed else "-"
|
||||||
|
|
||||||
|
|
||||||
|
def _source_review_readback_state(
|
||||||
|
recurrence: dict[str, Any],
|
||||||
|
*,
|
||||||
|
work_item_id: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
items = recurrence.get("items")
|
||||||
|
if not isinstance(items, list):
|
||||||
|
return {"found": False, "decision": "missing_items", "review_id": None}
|
||||||
|
for item in items:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
work_item = item.get("work_item")
|
||||||
|
if not isinstance(work_item, dict):
|
||||||
|
continue
|
||||||
|
source_review = (
|
||||||
|
item.get("source_correlation_review")
|
||||||
|
if isinstance(item.get("source_correlation_review"), dict)
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
candidate_ids = {
|
||||||
|
_source_review_work_item_id(item),
|
||||||
|
str(work_item.get("work_item_id") or "").strip(),
|
||||||
|
str(source_review.get("work_item_id") or "").strip(),
|
||||||
|
}
|
||||||
|
if work_item_id not in candidate_ids:
|
||||||
|
continue
|
||||||
|
return {
|
||||||
|
"found": True,
|
||||||
|
"decision": str(source_review.get("decision") or "missing").strip()
|
||||||
|
or "missing",
|
||||||
|
"review_id": source_review.get("review_id"),
|
||||||
|
}
|
||||||
|
return {"found": False, "decision": "not_found", "review_id": None}
|
||||||
|
|
||||||
|
|
||||||
|
def _wait_for_review_readback(
|
||||||
|
*,
|
||||||
|
args: argparse.Namespace,
|
||||||
|
work_item_id: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
recurrence_url = _url(
|
||||||
|
args.api_url,
|
||||||
|
"/api/v1/platform/events/dossier/recurrence",
|
||||||
|
{
|
||||||
|
"project_id": args.project_id,
|
||||||
|
"provider": args.provider,
|
||||||
|
"limit": args.limit,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
last_state: dict[str, Any] = {}
|
||||||
|
attempts = max(int(args.review_readback_attempts or 1), 1)
|
||||||
|
for attempt in range(attempts):
|
||||||
|
recurrence = _http_json(recurrence_url)
|
||||||
|
last_state = _source_review_readback_state(
|
||||||
|
recurrence,
|
||||||
|
work_item_id=work_item_id,
|
||||||
|
)
|
||||||
|
if last_state.get("found") and last_state.get("decision") == "accepted":
|
||||||
|
return last_state
|
||||||
|
if attempt + 1 < attempts:
|
||||||
|
time.sleep(max(float(args.review_readback_interval_seconds or 0), 0.0))
|
||||||
|
raise SmokeError(
|
||||||
|
"accepted review did not appear in recurrence read model: "
|
||||||
|
f"work_item_id={work_item_id} found={last_state.get('found')} "
|
||||||
|
f"decision={last_state.get('decision')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _apply_source_correlation_item(
|
def _apply_source_correlation_item(
|
||||||
*,
|
*,
|
||||||
args: argparse.Namespace,
|
args: argparse.Namespace,
|
||||||
@@ -332,6 +414,7 @@ def _apply_source_correlation_item(
|
|||||||
"accepted review was not recorded: "
|
"accepted review was not recorded: "
|
||||||
f"status={review.get('review_record_status')} allowed={review.get('allowed')}"
|
f"status={review.get('review_record_status')} allowed={review.get('allowed')}"
|
||||||
)
|
)
|
||||||
|
_wait_for_review_readback(args=args, work_item_id=work_item_id)
|
||||||
|
|
||||||
apply_url = _url(
|
apply_url = _url(
|
||||||
args.api_url,
|
args.api_url,
|
||||||
@@ -352,7 +435,9 @@ def _apply_source_correlation_item(
|
|||||||
if apply_result.get("apply_status") != "applied":
|
if apply_result.get("apply_status") != "applied":
|
||||||
raise SmokeError(
|
raise SmokeError(
|
||||||
"source correlation apply did not complete: "
|
"source correlation apply did not complete: "
|
||||||
f"status={apply_result.get('apply_status')} allowed={apply_result.get('allowed')}"
|
f"status={apply_result.get('apply_status')} "
|
||||||
|
f"allowed={apply_result.get('allowed')} "
|
||||||
|
f"failed_checks={_failed_check_summary(apply_result)}"
|
||||||
)
|
)
|
||||||
if apply_result.get("writes_incident_state") is not False:
|
if apply_result.get("writes_incident_state") is not False:
|
||||||
raise SmokeError("apply unexpectedly wrote Incident state")
|
raise SmokeError("apply unexpectedly wrote Incident state")
|
||||||
@@ -536,6 +621,8 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
|
|||||||
parser.add_argument("--min-applied", type=int, default=1)
|
parser.add_argument("--min-applied", type=int, default=1)
|
||||||
parser.add_argument("--status-attempts", type=int, default=8)
|
parser.add_argument("--status-attempts", type=int, default=8)
|
||||||
parser.add_argument("--status-interval-seconds", type=float, default=2.0)
|
parser.add_argument("--status-interval-seconds", type=float, default=2.0)
|
||||||
|
parser.add_argument("--review-readback-attempts", type=int, default=8)
|
||||||
|
parser.add_argument("--review-readback-interval-seconds", type=float, default=2.0)
|
||||||
parser.add_argument("--allow-non-canary", action="store_true")
|
parser.add_argument("--allow-non-canary", action="store_true")
|
||||||
parser.add_argument("--allow-existing-apply", action="store_true")
|
parser.add_argument("--allow-existing-apply", action="store_true")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
|||||||
Reference in New Issue
Block a user