feat(tester): add integration-first journey execution
This commit is contained in:
@@ -13,7 +13,9 @@ from __future__ import annotations
|
||||
from typing import Any, Iterable, Mapping
|
||||
|
||||
|
||||
TEST_TYPES = ("journey", "integration")
|
||||
CLIENT_SUITES = ("client-smoke", "client-regression", "safety")
|
||||
INTEGRATION_SUITES = ("api-contract", "authorization", "build-contract")
|
||||
PLATFORMS = ("android", "ios")
|
||||
FAILURE_POLICIES = ("stop", "continue")
|
||||
RESULTS = ("pass", "fail", "environment-fail", "skipped", "not-run")
|
||||
@@ -28,6 +30,33 @@ def client_suite(case: Mapping[str, Any]) -> str:
|
||||
return str(value or alias or "")
|
||||
|
||||
|
||||
def integration_suite(case: Mapping[str, Any]) -> str:
|
||||
"""Return the integration suite, accepting ``suite`` during migration."""
|
||||
value = case.get("integration_suite")
|
||||
alias = case.get("suite")
|
||||
if value and alias and str(value) != str(alias):
|
||||
return ""
|
||||
return str(value or alias or "")
|
||||
|
||||
|
||||
def test_type(case: Mapping[str, Any]) -> str:
|
||||
"""Return an explicit type or infer one for an old catalog entry."""
|
||||
declared = str(case.get("test_type") or "").strip().lower()
|
||||
if declared:
|
||||
return declared
|
||||
if client_suite(case) or case.get("journey") is not None:
|
||||
return "journey"
|
||||
if integration_suite(case):
|
||||
return "integration"
|
||||
return ""
|
||||
|
||||
|
||||
def suite(case: Mapping[str, Any]) -> str:
|
||||
"""Return the report/selection suite for either test type."""
|
||||
kind = test_type(case)
|
||||
return client_suite(case) if kind == "journey" else integration_suite(case)
|
||||
|
||||
|
||||
def normalize_platforms(value: Any) -> list[str]:
|
||||
if isinstance(value, str):
|
||||
values = [value]
|
||||
@@ -57,16 +86,23 @@ def contract_errors(case: Mapping[str, Any], sessions: Mapping[str, Any] | None
|
||||
schema. Once opted in, all lifecycle inputs are explicit so a missing
|
||||
fixture/cleanup can stop the run before any case result is fabricated.
|
||||
"""
|
||||
suite = client_suite(case)
|
||||
journey = case.get("journey")
|
||||
if not suite and journey is None:
|
||||
kind = test_type(case)
|
||||
declared_kind = str(case.get("test_type") or "").strip().lower()
|
||||
if declared_kind and declared_kind not in TEST_TYPES:
|
||||
return [f"{case.get('id') or '?'}: test_type 必须是 journey 或 integration"]
|
||||
if not kind:
|
||||
return []
|
||||
if kind == "integration":
|
||||
return integration_contract_errors(case)
|
||||
|
||||
suite_name = client_suite(case)
|
||||
journey = case.get("journey")
|
||||
errors: list[str] = []
|
||||
cid = str(case.get("id") or "?")
|
||||
where = f"{cid}: client journey"
|
||||
if case.get("client_suite") and case.get("suite") and str(case["client_suite"]) != str(case["suite"]):
|
||||
errors.append(f"{where}: client_suite 与 suite 不一致")
|
||||
if suite not in CLIENT_SUITES:
|
||||
if suite_name not in CLIENT_SUITES:
|
||||
errors.append(f"{where}: client_suite 必须是 {', '.join(CLIENT_SUITES)}")
|
||||
|
||||
if not isinstance(journey, Mapping):
|
||||
@@ -116,11 +152,69 @@ def contract_errors(case: Mapping[str, Any], sessions: Mapping[str, Any] | None
|
||||
errors.append(f"{where}: session 必须声明可复用客户端会话")
|
||||
elif sessions is not None and session not in sessions:
|
||||
errors.append(f"{where}: session={session} 未在 catalog.sessions 声明")
|
||||
if suite == "safety" and str(case.get("tag") or "") == "smoke":
|
||||
if suite_name == "safety" and str(case.get("tag") or "") == "smoke":
|
||||
errors.append(f"{where}: safety 不得标记为 smoke")
|
||||
return errors
|
||||
|
||||
|
||||
def integration_contract_errors(case: Mapping[str, Any]) -> list[str]:
|
||||
"""Validate the non-client half of the tester catalog.
|
||||
|
||||
Integration cases deliberately do not require a browser/mobile session. A
|
||||
``how`` block is the legacy call declaration; newer catalogs may use
|
||||
``request``/``input``/``call`` or an ``integration`` mapping as well.
|
||||
``expected`` remains the common protocol/data assertion list.
|
||||
"""
|
||||
cid = str(case.get("id") or "?")
|
||||
where = f"{cid}: integration"
|
||||
errors: list[str] = []
|
||||
value = case.get("integration_suite")
|
||||
alias = case.get("suite")
|
||||
if value and alias and str(value) != str(alias):
|
||||
errors.append(f"{where}: integration_suite 与 suite 不一致")
|
||||
suite_name = integration_suite(case)
|
||||
if suite_name not in INTEGRATION_SUITES:
|
||||
errors.append(
|
||||
f"{where}: integration_suite 必须是 {', '.join(INTEGRATION_SUITES)}"
|
||||
)
|
||||
|
||||
fixture = case.get("fixture")
|
||||
if not isinstance(fixture, Mapping) or not fixture:
|
||||
errors.append(f"{where}: fixture 缺失,必须在 preflight 前声明")
|
||||
|
||||
declared_call = any(
|
||||
key in case and case.get(key) not in (None, "", [], {})
|
||||
for key in ("request", "input", "call", "integration")
|
||||
)
|
||||
how = case.get("how")
|
||||
if not declared_call and not isinstance(how, Mapping):
|
||||
errors.append(f"{where}: 必须声明 request/input/call/integration 或 how")
|
||||
|
||||
# Integration writes need an explicit rollback/cleanup contract. Read-only
|
||||
# probes may state why cleanup is unnecessary, matching test-preflight.
|
||||
risk = str(case.get("risk") or "")
|
||||
cleanup = case.get("cleanup")
|
||||
cleanup_steps = isinstance(cleanup, Mapping) and (
|
||||
bool(cleanup.get("steps")) or bool(str(cleanup.get("run") or "").strip())
|
||||
)
|
||||
cleanup_noop = isinstance(cleanup, Mapping) and cleanup.get("required") is False and bool(
|
||||
str(cleanup.get("reason") or "").strip()
|
||||
)
|
||||
if risk != "readonly" and not (cleanup_steps or cleanup_noop):
|
||||
errors.append(f"{where}: 写入型 integration 必须声明 cleanup/rollback steps/run")
|
||||
elif cleanup is not None and not (cleanup_steps or cleanup_noop):
|
||||
errors.append(f"{where}: cleanup 必须有 steps/run 或 required=false + reason")
|
||||
|
||||
forbidden = []
|
||||
if case.get("client_suite") or case.get("journey") is not None:
|
||||
forbidden.append("client_suite/journey")
|
||||
if case.get("session") or case.get("platform"):
|
||||
forbidden.append("session/platform")
|
||||
if forbidden:
|
||||
errors.append(f"{where}: integration 不应声明 " + ", ".join(forbidden))
|
||||
return errors
|
||||
|
||||
|
||||
def preflight(cases: Iterable[Mapping[str, Any]], sessions: Mapping[str, Any] | None = None) -> dict[str, Any]:
|
||||
"""Return a contract preflight result; no per-case result is created on block."""
|
||||
cases = list(cases)
|
||||
|
||||
Reference in New Issue
Block a user