676 lines
27 KiB
Python
676 lines
27 KiB
Python
"""Run the repository-owned front-end seat suite against a connected T2G bench."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
import time
|
|
import urllib.error
|
|
import urllib.parse
|
|
import urllib.request
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
DEFAULT_SUITE = ROOT / "tests" / "fixtures" / "seat_frontend_hil_suite.json"
|
|
DEFAULT_OUTPUT = ROOT / "artifacts" / "hil" / "seat_frontend_hil_latest.json"
|
|
MOTION_AXES = {
|
|
"longitudinal",
|
|
"cushion_tilt",
|
|
"backrest_recline",
|
|
"legrest_height",
|
|
}
|
|
EXPECTED_CAPABILITIES = {
|
|
"heating": "seat.set_heating",
|
|
"ventilation": "seat.set_ventilation",
|
|
"longitudinal": "seat.set_longitudinal_position",
|
|
"cushion_tilt": "seat.set_cushion_tilt",
|
|
"backrest_recline": "seat.set_backrest_recline",
|
|
"legrest_height": "seat.set_legrest_height",
|
|
}
|
|
|
|
|
|
def utc_now() -> str:
|
|
return datetime.now(UTC).isoformat().replace("+00:00", "Z")
|
|
|
|
|
|
def request_json(
|
|
url: str,
|
|
*,
|
|
payload: dict[str, Any] | None = None,
|
|
timeout_s: int = 180,
|
|
) -> tuple[int, dict[str, Any]]:
|
|
data = None
|
|
method = "GET"
|
|
headers: dict[str, str] = {}
|
|
if payload is not None:
|
|
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
method = "POST"
|
|
headers["Content-Type"] = "application/json; charset=utf-8"
|
|
request = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
try:
|
|
with urllib.request.urlopen(request, timeout=timeout_s) as response:
|
|
return response.status, json.loads(response.read().decode("utf-8"))
|
|
except urllib.error.HTTPError as exc:
|
|
body = exc.read().decode("utf-8", errors="replace")
|
|
try:
|
|
detail = json.loads(body)
|
|
except json.JSONDecodeError:
|
|
detail = {"message": body}
|
|
return exc.code, detail
|
|
|
|
|
|
def dig(value: dict[str, Any], *path: str) -> Any:
|
|
current: Any = value
|
|
for item in path:
|
|
if not isinstance(current, dict) or item not in current:
|
|
return None
|
|
current = current[item]
|
|
return current
|
|
|
|
|
|
def failed_hardware_result(result: dict[str, Any]) -> dict[str, Any] | None:
|
|
items = dig(result, "execution_report", "spec", "results") or []
|
|
for item in reversed(items):
|
|
if item.get("status") != "succeeded":
|
|
return item
|
|
return None
|
|
|
|
|
|
def expand_case_entries(
|
|
entries: list[dict[str, Any]], *, group: str | None = None
|
|
) -> list[dict[str, Any]]:
|
|
"""Expand compact input-equivalence families into individually reported cases."""
|
|
|
|
expanded: list[dict[str, Any]] = []
|
|
for entry in entries:
|
|
base = {key: value for key, value in entry.items() if key != "variants"}
|
|
if group is not None:
|
|
base["group"] = group
|
|
variants = entry.get("variants")
|
|
if not variants:
|
|
expanded.append(base)
|
|
continue
|
|
for variant in variants:
|
|
case = {**base, **variant}
|
|
if "id" not in case or "utterance" not in case:
|
|
raise ValueError("each case variant requires id and utterance")
|
|
expanded.append(case)
|
|
return expanded
|
|
|
|
|
|
def expected_operation_errors(
|
|
result: dict[str, Any],
|
|
expected: dict[str, int],
|
|
expected_motion: dict[str, Any] | None = None,
|
|
expected_dynamic_mode: dict[str, Any] | None = None,
|
|
) -> list[str]:
|
|
errors: list[str] = []
|
|
seat_config = (result.get("function_configs") or {}).get("seat") or result.get(
|
|
"function_config"
|
|
) or {}
|
|
operations = dig(seat_config, "spec", "operations") or []
|
|
by_capability = {item.get("capability"): item for item in operations}
|
|
for field, expected_value in expected.items():
|
|
capability = EXPECTED_CAPABILITIES.get(field)
|
|
if capability is None:
|
|
continue
|
|
operation = by_capability.get(capability)
|
|
if operation is None:
|
|
errors.append(f"expected capability was not generated: {capability}")
|
|
continue
|
|
if dig(operation, "target", "instance") != "passenger" or dig(
|
|
operation, "target", "zone"
|
|
) != "front_right":
|
|
errors.append(f"{capability} did not target passenger/front_right")
|
|
parameter = "level" if field in {"heating", "ventilation"} else "position_pct"
|
|
actual_value = dig(operation, "parameters", parameter)
|
|
if field in {"heating", "ventilation"} and expected_value == 0:
|
|
if dig(operation, "parameters", "state") != "off":
|
|
errors.append(f"{capability} state is not off")
|
|
elif actual_value != expected_value:
|
|
errors.append(
|
|
f"{capability} {parameter}={actual_value}, expected={expected_value}"
|
|
)
|
|
if expected_motion:
|
|
axis = str(expected_motion["axis"])
|
|
capability = EXPECTED_CAPABILITIES[axis]
|
|
operation = by_capability.get(capability)
|
|
motion = operation.get("motion") if operation else None
|
|
if not isinstance(motion, dict):
|
|
errors.append(f"{capability} did not preserve motion metadata")
|
|
else:
|
|
for field, expected_value in expected_motion.items():
|
|
if field == "axis":
|
|
continue
|
|
actual_value = motion.get(field)
|
|
if actual_value != expected_value:
|
|
errors.append(
|
|
f"{capability} motion.{field}={actual_value}, "
|
|
f"expected={expected_value}"
|
|
)
|
|
if expected_dynamic_mode:
|
|
expected_mode_id = int(expected_dynamic_mode["mode_id"])
|
|
capability = (
|
|
"seat.stop_dynamic_mode"
|
|
if expected_mode_id == 0
|
|
else "seat.start_dynamic_mode"
|
|
)
|
|
operation = by_capability.get(capability)
|
|
if operation is None:
|
|
errors.append(f"expected capability was not generated: {capability}")
|
|
elif expected_mode_id and dig(operation, "parameters", "mode_id") != expected_mode_id:
|
|
errors.append(
|
|
f"{capability} mode_id={dig(operation, 'parameters', 'mode_id')}, "
|
|
f"expected={expected_mode_id}"
|
|
)
|
|
|
|
hardware_results = dig(result, "execution_report", "spec", "results") or []
|
|
telemetry = next(
|
|
(
|
|
item.get("telemetry", {})
|
|
for item in hardware_results
|
|
if item.get("domain") == "seat"
|
|
and item.get("telemetry", {}).get("dynamic_mode_id") is not None
|
|
),
|
|
None,
|
|
)
|
|
if telemetry is None:
|
|
errors.append("dynamic-mode execution did not include physical 0x306 telemetry")
|
|
else:
|
|
if telemetry.get("dynamic_mode_id") != expected_mode_id:
|
|
errors.append(
|
|
f"dynamic telemetry mode_id={telemetry.get('dynamic_mode_id')}, "
|
|
f"expected={expected_mode_id}"
|
|
)
|
|
dynamic_state = telemetry.get("dynamic_state")
|
|
accepted_states = {0} if expected_mode_id == 0 else {1, 2}
|
|
if dynamic_state not in accepted_states:
|
|
errors.append(
|
|
f"dynamic telemetry state={dynamic_state}, "
|
|
f"expected one of {sorted(accepted_states)}"
|
|
)
|
|
return errors
|
|
|
|
|
|
def physical_fingerprint(state_result: dict[str, Any]) -> dict[str, Any]:
|
|
physical = state_result.get("physical_state", {})
|
|
return {
|
|
"source": physical.get("source"),
|
|
"complete": physical.get("complete"),
|
|
"heating": dig(physical, "heating", "level"),
|
|
"ventilation": dig(physical, "ventilation", "level"),
|
|
"position": {
|
|
axis: dig(physical, "position", axis, "position_pct")
|
|
for axis in sorted(MOTION_AXES)
|
|
},
|
|
}
|
|
|
|
|
|
def physical_state_request(
|
|
base_url: str, vehicle_id: str, conversation_id: str
|
|
) -> tuple[int, dict[str, Any]]:
|
|
query = urllib.parse.urlencode(
|
|
{"conversation_id": conversation_id, "vehicle_id": vehicle_id}
|
|
)
|
|
return request_json(
|
|
f"{base_url}/api/hardware/seat-state?{query}", timeout_s=20
|
|
)
|
|
|
|
|
|
def validate_physical_state(
|
|
state_result: dict[str, Any], expected: dict[str, int], tolerance: int
|
|
) -> list[str]:
|
|
errors: list[str] = []
|
|
physical = state_result.get("physical_state", {})
|
|
synchronized = state_result.get("actuator_state", {}).get("passenger", {})
|
|
if physical.get("source") != "physical_can":
|
|
errors.append("physical state source is not physical_can")
|
|
if not physical.get("complete"):
|
|
errors.append("physical state is incomplete: " + ",".join(physical.get("missing", [])))
|
|
for feature in ("heating", "ventilation"):
|
|
if feature not in expected:
|
|
continue
|
|
actual = dig(physical, feature, "level")
|
|
twin = dig(synchronized, feature, "level")
|
|
if actual != expected[feature]:
|
|
errors.append(f"{feature} physical={actual}, expected={expected[feature]}")
|
|
if twin != actual:
|
|
errors.append(f"{feature} twin={twin}, physical={actual}")
|
|
if dig(synchronized, feature, "feedback_source") != "physical_can":
|
|
errors.append(f"{feature} twin feedback source is not physical_can")
|
|
for axis in MOTION_AXES:
|
|
if axis not in expected:
|
|
continue
|
|
actual = dig(physical, "position", axis, "position_pct")
|
|
twin = dig(synchronized, "position", axis, "position_pct")
|
|
if not isinstance(actual, (int, float)) or abs(actual - expected[axis]) > tolerance:
|
|
errors.append(f"{axis} physical={actual}, expected={expected[axis]}±{tolerance}")
|
|
if twin != actual:
|
|
errors.append(f"{axis} twin={twin}, physical={actual}")
|
|
if dig(synchronized, "position", axis, "feedback_source") != "physical_can":
|
|
errors.append(f"{axis} twin feedback source is not physical_can")
|
|
return errors
|
|
|
|
|
|
def safety_block_for_case(
|
|
case: dict[str, Any], safety_blocks: dict[str, Any]
|
|
) -> dict[str, Any] | None:
|
|
"""Refuse only motions covered by a documented, evidence-backed bench hazard."""
|
|
|
|
expected = case.get("expected", {})
|
|
for axis, policy in safety_blocks.items():
|
|
target = expected.get(axis)
|
|
if target is None:
|
|
continue
|
|
if policy.get("block_all_targets") is True:
|
|
return {
|
|
"axis": axis,
|
|
"target_pct": target,
|
|
"block_all_targets": True,
|
|
"reason": policy.get("reason", "documented hardware safety block"),
|
|
"evidence": policy.get("evidence", {}),
|
|
}
|
|
minimum = policy.get("minimum_safe_target_pct")
|
|
if isinstance(minimum, (int, float)) and target < minimum:
|
|
return {
|
|
"axis": axis,
|
|
"target_pct": target,
|
|
"minimum_safe_target_pct": minimum,
|
|
"reason": policy.get("reason", "documented hardware safety boundary"),
|
|
"evidence": policy.get("evidence", {}),
|
|
}
|
|
return None
|
|
|
|
|
|
def run_case(
|
|
base_url: str,
|
|
vehicle_id: str,
|
|
conversation_id: str,
|
|
case: dict[str, Any],
|
|
tolerance: int,
|
|
) -> dict[str, Any]:
|
|
started = time.monotonic()
|
|
record: dict[str, Any] = {
|
|
"id": case["id"],
|
|
"group": case.get("group", "mode"),
|
|
"utterance": case["utterance"],
|
|
"started_at": utc_now(),
|
|
"status": "failed",
|
|
"errors": [],
|
|
}
|
|
payload = {
|
|
"utterance": case["utterance"],
|
|
"provider": case.get("provider", "local"),
|
|
"conversation_id": conversation_id,
|
|
"execution_mode": "hardware",
|
|
"vehicle_id": vehicle_id,
|
|
}
|
|
try:
|
|
status_code, result = request_json(
|
|
f"{base_url}/api/requests", payload=payload, timeout_s=900
|
|
)
|
|
record["http_status"] = status_code
|
|
record["request_id"] = result.get("request_id")
|
|
record["provider_used"] = result.get("provider_used")
|
|
if status_code != 201:
|
|
record["errors"].append(result.get("message", f"HTTP {status_code}"))
|
|
elif result.get("status") != "succeeded":
|
|
failed = failed_hardware_result(result)
|
|
record["errors"].append(
|
|
failed.get("message", "request failed") if failed else "request failed"
|
|
)
|
|
if failed:
|
|
record["failure_telemetry"] = failed.get("telemetry", {})
|
|
elif dig(result, "execution_report", "spec", "status") != "succeeded":
|
|
failed = failed_hardware_result(result)
|
|
record["errors"].append(
|
|
failed.get("message", "hardware execution failed")
|
|
if failed
|
|
else "hardware execution failed"
|
|
)
|
|
if failed:
|
|
record["failure_telemetry"] = failed.get("telemetry", {})
|
|
else:
|
|
expected_provider_prefix = case.get("expected_provider_prefix")
|
|
if expected_provider_prefix and not str(result.get("provider_used", "")).startswith(
|
|
str(expected_provider_prefix)
|
|
):
|
|
record["errors"].append(
|
|
f"provider_used={result.get('provider_used')}, "
|
|
f"expected prefix={expected_provider_prefix}"
|
|
)
|
|
record["errors"].extend(
|
|
expected_operation_errors(
|
|
result,
|
|
case.get("expected", {}),
|
|
case.get("expected_motion"),
|
|
case.get("expected_dynamic_mode"),
|
|
)
|
|
)
|
|
seat_config = (result.get("function_configs") or {}).get(
|
|
"seat"
|
|
) or result.get("function_config") or {}
|
|
operations = dig(seat_config, "spec", "operations") or []
|
|
seat_operations = [
|
|
item for item in operations if str(item.get("capability", "")).startswith("seat.")
|
|
]
|
|
invalid_targets = [
|
|
item.get("target")
|
|
for item in seat_operations
|
|
if dig(item, "target", "instance") != "passenger"
|
|
or dig(item, "target", "zone") != "front_right"
|
|
]
|
|
if invalid_targets:
|
|
record["errors"].append(f"non-passenger targets generated: {invalid_targets}")
|
|
visible_notices = [
|
|
*[str(item) for item in result.get("decision_summary", [])],
|
|
*[
|
|
str(item.get("message", ""))
|
|
for item in dig(result, "execution_report", "spec", "results") or []
|
|
],
|
|
*[
|
|
str(item.get("telemetry", {}).get("user_notice", ""))
|
|
for item in dig(result, "execution_report", "spec", "results") or []
|
|
],
|
|
]
|
|
visible_notice_text = "\n".join(visible_notices)
|
|
for expected_notice in case.get("expected_notice_contains", []):
|
|
if expected_notice not in visible_notice_text:
|
|
record["errors"].append(
|
|
f"visible execution notice does not contain {expected_notice!r}"
|
|
)
|
|
expected_mode = case.get("mode_id")
|
|
matched_modes = [item.get("id") for item in result.get("matched_modes", [])]
|
|
record["matched_modes"] = matched_modes
|
|
if expected_mode and expected_mode not in matched_modes:
|
|
record["errors"].append(
|
|
f"mode {expected_mode} was not matched: {matched_modes}"
|
|
)
|
|
state_code, state = physical_state_request(
|
|
base_url, vehicle_id, conversation_id
|
|
)
|
|
record["state_http_status"] = state_code
|
|
if state_code != 200:
|
|
record["errors"].append(
|
|
state.get("message", f"physical state HTTP {state_code}")
|
|
)
|
|
else:
|
|
record["errors"].extend(
|
|
validate_physical_state(state, case.get("expected", {}), tolerance)
|
|
)
|
|
expected_dynamic_mode = case.get("expected_dynamic_mode")
|
|
if expected_dynamic_mode:
|
|
actual_dynamic_mode = dig(
|
|
state, "actuator_state", "passenger", "dynamic_mode"
|
|
) or {}
|
|
for field in ("mode_id", "state"):
|
|
expected_value = expected_dynamic_mode[field]
|
|
if actual_dynamic_mode.get(field) != expected_value:
|
|
record["errors"].append(
|
|
f"dynamic_mode.{field}={actual_dynamic_mode.get(field)}, "
|
|
f"expected={expected_value}"
|
|
)
|
|
record["physical_state"] = state.get("physical_state")
|
|
record["status"] = "passed" if not record["errors"] else "failed"
|
|
except Exception as exc: # continue the suite after transport and parse errors
|
|
record["errors"].append(f"{type(exc).__name__}: {exc}")
|
|
record["finished_at"] = utc_now()
|
|
record["duration_ms"] = round((time.monotonic() - started) * 1000)
|
|
return record
|
|
|
|
|
|
def run_rejection_case(
|
|
base_url: str,
|
|
vehicle_id: str,
|
|
conversation_id: str,
|
|
case: dict[str, Any],
|
|
) -> dict[str, Any]:
|
|
started = time.monotonic()
|
|
record: dict[str, Any] = {
|
|
"id": case["id"],
|
|
"group": case.get("group", "safety"),
|
|
"utterance": case["utterance"],
|
|
"started_at": utc_now(),
|
|
"status": "failed",
|
|
"errors": [],
|
|
}
|
|
try:
|
|
before_code, before = physical_state_request(base_url, vehicle_id, conversation_id)
|
|
if before_code != 200:
|
|
record["errors"].append(f"pre-state HTTP {before_code}")
|
|
payload = {
|
|
"utterance": case["utterance"],
|
|
"provider": case.get("provider", "local"),
|
|
"conversation_id": conversation_id,
|
|
"execution_mode": "hardware",
|
|
"vehicle_id": vehicle_id,
|
|
}
|
|
status_code, result = request_json(
|
|
f"{base_url}/api/requests", payload=payload, timeout_s=60
|
|
)
|
|
record["http_status"] = status_code
|
|
record["request_id"] = result.get("request_id")
|
|
expected_http = int(case.get("expected_http_status", 201))
|
|
if status_code != expected_http:
|
|
record["errors"].append(f"HTTP {status_code}, expected {expected_http}")
|
|
if expected_http == 201:
|
|
expected_status = case.get("expected_status", "needs_confirmation")
|
|
if result.get("status") != expected_status:
|
|
record["errors"].append(
|
|
f"status={result.get('status')}, expected={expected_status}"
|
|
)
|
|
if result.get("function_config") is not None:
|
|
record["errors"].append("rejected request generated a function_config")
|
|
if dig(result, "execution_report", "spec", "mode") == "hil":
|
|
record["errors"].append("rejected request produced a HIL execution report")
|
|
else:
|
|
expected_error = case.get("expected_error")
|
|
if expected_error and result.get("error") != expected_error:
|
|
record["errors"].append(
|
|
f"error={result.get('error')}, expected={expected_error}"
|
|
)
|
|
after_code, after = physical_state_request(base_url, vehicle_id, conversation_id)
|
|
if after_code != 200:
|
|
record["errors"].append(f"post-state HTTP {after_code}")
|
|
before_fingerprint = physical_fingerprint(before)
|
|
after_fingerprint = physical_fingerprint(after)
|
|
record["physical_state_before"] = before_fingerprint
|
|
record["physical_state_after"] = after_fingerprint
|
|
if before_fingerprint != after_fingerprint:
|
|
record["errors"].append("physical state changed for a rejected request")
|
|
record["status"] = "passed" if not record["errors"] else "failed"
|
|
except Exception as exc:
|
|
record["errors"].append(f"{type(exc).__name__}: {exc}")
|
|
record["finished_at"] = utc_now()
|
|
record["duration_ms"] = round((time.monotonic() - started) * 1000)
|
|
return record
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--suite", type=Path, default=DEFAULT_SUITE)
|
|
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
|
|
parser.add_argument("--base-url", default="http://127.0.0.1:8000")
|
|
parser.add_argument("--start-at")
|
|
parser.add_argument(
|
|
"--case",
|
|
action="append",
|
|
dest="case_ids",
|
|
help="Run only this case ID; repeat the option to select an ordered subset.",
|
|
)
|
|
parser.add_argument(
|
|
"--only-group",
|
|
choices=("thermal", "motion", "combined", "mode", "model", "safety"),
|
|
)
|
|
parser.add_argument(
|
|
"--confirm-hardware",
|
|
action="store_true",
|
|
help="Required acknowledgement that a secured, clear T2G bench is connected.",
|
|
)
|
|
args = parser.parse_args()
|
|
if not args.confirm_hardware:
|
|
parser.error("--confirm-hardware is required")
|
|
|
|
suite = json.loads(args.suite.read_text(encoding="utf-8"))
|
|
runtime_code, runtime = request_json(f"{args.base_url}/api/runtime", timeout_s=10)
|
|
if runtime_code != 200 or not runtime.get("hardware_execution_allowed"):
|
|
raise SystemExit("hardware execution is not enabled by the running front-end service")
|
|
vehicle_id = suite.get("vehicle_id", "denza_n7")
|
|
conversation_id = "conv-hil-full-" + datetime.now(UTC).strftime("%Y%m%d%H%M%S")
|
|
tolerance = int(suite["execution_policy"].get("position_tolerance_pct", 2))
|
|
cases = (
|
|
expand_case_entries(list(suite.get("cases", [])))
|
|
+ expand_case_entries(list(suite.get("mode_cases", [])), group="mode")
|
|
+ expand_case_entries(list(suite.get("model_cases", [])), group="model")
|
|
+ expand_case_entries(list(suite.get("rejection_cases", [])), group="safety")
|
|
)
|
|
if args.only_group:
|
|
cases = [item for item in cases if item.get("group") == args.only_group]
|
|
if args.case_ids:
|
|
if args.start_at:
|
|
parser.error("--case and --start-at cannot be used together")
|
|
by_id = {item["id"]: item for item in cases}
|
|
missing = [item for item in args.case_ids if item not in by_id]
|
|
if missing:
|
|
raise SystemExit("unknown --case ID(s): " + ", ".join(missing))
|
|
cases = [by_id[item] for item in args.case_ids]
|
|
if args.start_at:
|
|
ids = [item["id"] for item in cases]
|
|
if args.start_at not in ids:
|
|
raise SystemExit(f"unknown --start-at case {args.start_at}")
|
|
cases = cases[ids.index(args.start_at) :]
|
|
|
|
report: dict[str, Any] = {
|
|
"suite": suite["suite"],
|
|
"started_at": utc_now(),
|
|
"base_url": args.base_url,
|
|
"vehicle_id": vehicle_id,
|
|
"conversation_id": conversation_id,
|
|
"runtime": {
|
|
"hardware_execution_allowed": runtime.get("hardware_execution_allowed"),
|
|
"firmware_profiles": dig(runtime, "hardware", "firmware_profiles"),
|
|
},
|
|
"results": [],
|
|
}
|
|
sync_case = {
|
|
"id": "PREFLIGHT-POSITION-MEMORY-SYNC",
|
|
"group": "preflight",
|
|
**suite["final_state"],
|
|
}
|
|
print(
|
|
"[PREFLIGHT] Four-axis physical homing and Hall-memory synchronization",
|
|
flush=True,
|
|
)
|
|
sync_result = run_case(
|
|
args.base_url, vehicle_id, conversation_id, sync_case, tolerance
|
|
)
|
|
report["position_memory_sync"] = sync_result
|
|
print(
|
|
f" {sync_result['status'].upper()} {sync_result['duration_ms']} ms"
|
|
+ (
|
|
""
|
|
if sync_result["status"] == "passed"
|
|
else ": " + " | ".join(sync_result["errors"])
|
|
),
|
|
flush=True,
|
|
)
|
|
if sync_result["status"] != "passed":
|
|
report["final_state"] = sync_result
|
|
report["finished_at"] = utc_now()
|
|
report["summary"] = {
|
|
"total": 0,
|
|
"passed": 0,
|
|
"failed": 0,
|
|
"safety_blocked": 0,
|
|
"executed": 0,
|
|
"position_memory_sync_passed": False,
|
|
"final_state_passed": False,
|
|
}
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
args.output.write_text(
|
|
json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
)
|
|
print(json.dumps(report["summary"], ensure_ascii=False), flush=True)
|
|
print(str(args.output.resolve()), flush=True)
|
|
return 1
|
|
|
|
for index, case in enumerate(cases, start=1):
|
|
print(f"[{index:02d}/{len(cases):02d}] {case['id']} {case['utterance']}", flush=True)
|
|
safety_block = safety_block_for_case(
|
|
case, suite["execution_policy"].get("known_safety_blocks", {})
|
|
)
|
|
if safety_block is not None:
|
|
result = {
|
|
"id": case["id"],
|
|
"group": case.get("group", "mode"),
|
|
"utterance": case["utterance"],
|
|
"status": "safety_blocked",
|
|
"started_at": utc_now(),
|
|
"finished_at": utc_now(),
|
|
"duration_ms": 0,
|
|
"errors": [],
|
|
"safety_block": safety_block,
|
|
}
|
|
report["results"].append(result)
|
|
print(
|
|
" SAFETY_BLOCKED: "
|
|
+ (
|
|
f"all {safety_block['axis']} motion is blocked by current bench evidence"
|
|
if safety_block.get("block_all_targets")
|
|
else f"{safety_block['axis']} target {safety_block['target_pct']}% is below "
|
|
f"the verified {safety_block['minimum_safe_target_pct']}% boundary"
|
|
),
|
|
flush=True,
|
|
)
|
|
continue
|
|
if case.get("expected_status") or case.get("expected_http_status"):
|
|
result = run_rejection_case(
|
|
args.base_url, vehicle_id, conversation_id, case
|
|
)
|
|
else:
|
|
result = run_case(
|
|
args.base_url, vehicle_id, conversation_id, case, tolerance
|
|
)
|
|
report["results"].append(result)
|
|
suffix = "" if result["status"] == "passed" else ": " + " | ".join(result["errors"])
|
|
print(f" {result['status'].upper()} {result['duration_ms']} ms{suffix}", flush=True)
|
|
|
|
final_case = {
|
|
"id": "FINAL-THERMAL-OFF",
|
|
"group": "cleanup",
|
|
**suite["final_state"],
|
|
}
|
|
final_result = run_case(
|
|
args.base_url, vehicle_id, conversation_id, final_case, tolerance
|
|
)
|
|
report["final_state"] = final_result
|
|
report["finished_at"] = utc_now()
|
|
report["summary"] = {
|
|
"total": len(report["results"]),
|
|
"passed": sum(item["status"] == "passed" for item in report["results"]),
|
|
"failed": sum(item["status"] == "failed" for item in report["results"]),
|
|
"safety_blocked": sum(
|
|
item["status"] == "safety_blocked" for item in report["results"]
|
|
),
|
|
"executed": sum(
|
|
item["status"] != "safety_blocked" for item in report["results"]
|
|
),
|
|
"position_memory_sync_passed": sync_result["status"] == "passed",
|
|
"final_state_passed": final_result["status"] == "passed",
|
|
}
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
args.output.write_text(
|
|
json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
)
|
|
print(json.dumps(report["summary"], ensure_ascii=False), flush=True)
|
|
print(str(args.output.resolve()), flush=True)
|
|
return 0 if report["summary"]["failed"] == 0 and final_result["status"] == "passed" else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|