Files
tracer-cloud--opensre/tests/synthetic/schemas.py
T
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

657 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Centralized schema definitions for synthetic testing fixtures.
All scenario fixture files (alert.json, aws_cloudwatch_metrics.json, aws_rds_events.json,
aws_performance_insights.json, answer.yml, scenario.yml) must conform to these TypedDicts.
Validators enforce required fields so every scenario is structurally consistent.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, NotRequired
from typing_extensions import TypedDict
# ---------------------------------------------------------------------------
# Controlled vocabularies for scenario metadata
# ---------------------------------------------------------------------------
VALID_ENGINES = frozenset({"postgres", "mysql", "aurora-postgres", "aurora-mysql", "mariadb"})
VALID_FAILURE_MODES = frozenset(
{
"replication_lag",
"connection_exhaustion",
"storage_full",
"cpu_saturation",
"failover",
"healthy",
"application_load_spike",
}
)
VALID_EVIDENCE_SOURCES = frozenset(
{
"aws_cloudwatch_metrics",
"aws_rds_events",
"aws_performance_insights",
"ec2_instances_by_tag",
"elb_target_health",
"k8s_events",
"k8s_pod_metrics",
"k8s_node_metrics",
"k8s_dns_metrics",
"k8s_mesh_metrics",
"k8s_rollout",
}
)
# ---------------------------------------------------------------------------
# Alert fixture (alert.json)
# ---------------------------------------------------------------------------
class AlertLabels(TypedDict, total=False):
alertname: str
severity: str
pipeline_name: str
service: str
engine: str
class AlertAnnotations(TypedDict, total=False):
summary: str
error: str
suspected_symptom: str
db_instance_identifier: str
db_instance: str
db_cluster: str
read_replica: str
cloudwatch_region: str
rds_failure_mode: str
context_sources: str
class AlertFixture(TypedDict):
title: str
state: str
alert_source: str
commonLabels: AlertLabels
commonAnnotations: AlertAnnotations
# ---------------------------------------------------------------------------
# CloudWatch metrics fixture (aws_cloudwatch_metrics.json)
# Models the AWS GetMetricData API response shape.
# ---------------------------------------------------------------------------
class MetricDimension(TypedDict):
Name: str
Value: str
class MetricDataResult(TypedDict):
"""One metric query result, combining query context with response data."""
id: str
label: str
metric_name: str
dimensions: list[MetricDimension]
stat: str
unit: str
status_code: str
timestamps: list[str]
values: list[float]
class CloudWatchMetricsFixture(TypedDict):
namespace: str
period: int
start_time: str
end_time: str
metric_data_results: list[MetricDataResult]
# ---------------------------------------------------------------------------
# RDS events fixture (aws_rds_events.json)
# Models the AWS DescribeEvents API response shape.
# ---------------------------------------------------------------------------
class RDSEvent(TypedDict):
date: str
message: str
source_identifier: str
source_type: str
event_categories: list[str]
class RDSEventsFixture(TypedDict):
events: list[RDSEvent]
# ---------------------------------------------------------------------------
# Performance insights fixture (aws_performance_insights.json)
# Models the AWS GetResourceMetrics + DescribeDimensionKeys API response shape.
# ---------------------------------------------------------------------------
class DBLoadTimeSeries(TypedDict):
timestamps: list[str]
values: list[float]
unit: str
class TopSQLWaitEvent(TypedDict):
name: str
type: str
db_load_avg: float
class TopSQL(TypedDict):
statement: str
db_load_avg: float
wait_events: list[TopSQLWaitEvent]
calls_per_sec: float
class TopWaitEvent(TypedDict):
name: str
type: str
db_load_avg: float
class TopUser(TypedDict):
name: str
db_load_avg: float
class TopHost(TypedDict):
id: str
db_load_avg: float
class PerformanceInsightsFixture(TypedDict):
db_instance_identifier: str
start_time: str
end_time: str
db_load: DBLoadTimeSeries
top_sql: list[TopSQL]
top_wait_events: list[TopWaitEvent]
top_users: list[TopUser]
top_hosts: list[TopHost]
# ---------------------------------------------------------------------------
# Answer key (answer.yml)
# ---------------------------------------------------------------------------
VALID_TRAJECTORY_ACTIONS = frozenset(
{
"query_grafana_metrics",
"query_grafana_logs",
"query_grafana_alert_rules",
"describe_rds_instance",
"describe_rds_events",
"ec2_instances_by_tag",
"get_elb_target_health",
}
)
class AnswerKeySchema(TypedDict):
root_cause_category: str
required_keywords: list[str]
model_response: str
# Additional categories that pass the category gate alongside root_cause_category
equivalent_root_cause_categories: NotRequired[list[str]]
# Optional adversarial constraints (level 2+ scenarios)
forbidden_categories: NotRequired[list[str]] # root_cause_category must NOT be any of these
forbidden_keywords: NotRequired[list[str]] # none of these may appear in evidence_text
required_evidence_sources: NotRequired[
list[str]
] # these keys must be non-empty in final_state["evidence"]
# Trajectory efficiency (Axis 1)
optimal_trajectory: NotRequired[list[str]] # ordered action names the agent should call
max_investigation_loops: NotRequired[int] # how many investigation loops is acceptable
# Adversarial reasoning (Axis 2)
ruling_out_keywords: NotRequired[
list[str]
] # agent output must contain these tokens (proof it dismissed alternatives)
required_queries: NotRequired[
list[str]
] # metric names agent must have specifically requested via query_timeseries
golden_trajectory: NotRequired[GoldenTrajectorySchema]
class GoldenTrajectorySchema(TypedDict, total=False):
ordered_actions: list[str]
matching: str
max_edit_distance: int
max_extra_actions: int
max_redundancy: int
max_loops: int
# ---------------------------------------------------------------------------
# Scenario metadata (scenario.yml)
# ---------------------------------------------------------------------------
class TopologyTier(TypedDict):
"""Application tier description for EC2/RDS topology fixtures."""
name: str
instance_ids: list[str]
asg: NotRequired[str]
class TopologyMetadata(TypedDict, total=False):
"""Optional EC2/RDS topology block for non-K8s scenarios.
Present in fixtures that exercise the DNS → LB → Target Group → EC2 → RDS
request path. Absent in legacy RDS-only scenarios (000014).
"""
vpc_id: str
load_balancer_arn: str
target_group_arn: str
tiers: list[TopologyTier]
class ScenarioMetadataSchema(TypedDict):
schema_version: str
scenario_id: str
engine: str
engine_version: str
instance_class: str
region: str
db_instance_identifier: str
failure_mode: str
severity: str
available_evidence: list[str]
db_cluster: NotRequired[str]
scenario_difficulty: NotRequired[int] # 14 curriculum level
adversarial_signals: NotRequired[list[str]] # metrics that are intentional confounders
depends_on: NotRequired[str] # e.g. "healthy_rca_state" — CI skip flag
topology: NotRequired[TopologyMetadata]
# ---------------------------------------------------------------------------
# Typed evidence container
# ---------------------------------------------------------------------------
class EC2Instance(TypedDict, total=False):
instance_id: str
tier: str
asg: str
private_ip: str
vpc_id: str
subnet_id: str
state: str
instance_type: str
security_groups: list[str]
class EC2InstancesByTagFixture(TypedDict):
instances: list[EC2Instance]
class ELBTargetHealthEntry(TypedDict, total=False):
target_group_arn: str
instance_id: str
port: int
state: str
reason: str
description: str
class ELBTargetGroup(TypedDict, total=False):
TargetGroupArn: str
TargetGroupName: str
LoadBalancerArns: list[str]
class ELBTargetHealthFixture(TypedDict):
target_groups: list[ELBTargetGroup]
targets: list[ELBTargetHealthEntry]
class GenericEvidenceFixture(TypedDict, total=False):
"""Flexible schema for suite-specific evidence not strongly typed here."""
@dataclass(frozen=True)
class ScenarioEvidence:
"""Typed container for all evidence sources in a scenario fixture.
Each attribute is None when the corresponding file was not listed in
scenario.yml:available_evidence, making evidence presence explicit.
"""
aws_cloudwatch_metrics: CloudWatchMetricsFixture | None
aws_rds_events: list[RDSEvent] | None
aws_performance_insights: PerformanceInsightsFixture | None
ec2_instances_by_tag: EC2InstancesByTagFixture | None = None
elb_target_health: ELBTargetHealthFixture | None = None
k8s_events: GenericEvidenceFixture | None = None
k8s_pod_metrics: GenericEvidenceFixture | None = None
k8s_node_metrics: GenericEvidenceFixture | None = None
k8s_dns_metrics: GenericEvidenceFixture | None = None
k8s_mesh_metrics: GenericEvidenceFixture | None = None
k8s_rollout: GenericEvidenceFixture | None = None
def as_dict(self) -> dict[str, Any]:
"""Return only the non-None sources as a plain dict."""
result: dict[str, Any] = {}
if self.aws_cloudwatch_metrics is not None:
result["aws_cloudwatch_metrics"] = self.aws_cloudwatch_metrics
if self.aws_rds_events is not None:
result["aws_rds_events"] = self.aws_rds_events
if self.aws_performance_insights is not None:
result["aws_performance_insights"] = self.aws_performance_insights
if self.ec2_instances_by_tag is not None:
result["ec2_instances_by_tag"] = self.ec2_instances_by_tag
if self.elb_target_health is not None:
result["elb_target_health"] = self.elb_target_health
if self.k8s_events is not None:
result["k8s_events"] = self.k8s_events
if self.k8s_pod_metrics is not None:
result["k8s_pod_metrics"] = self.k8s_pod_metrics
if self.k8s_node_metrics is not None:
result["k8s_node_metrics"] = self.k8s_node_metrics
if self.k8s_dns_metrics is not None:
result["k8s_dns_metrics"] = self.k8s_dns_metrics
if self.k8s_mesh_metrics is not None:
result["k8s_mesh_metrics"] = self.k8s_mesh_metrics
if self.k8s_rollout is not None:
result["k8s_rollout"] = self.k8s_rollout
return result
def get(self, key: str) -> Any:
return self.as_dict().get(key)
# ---------------------------------------------------------------------------
# Validators — raise ValueError with a descriptive message on bad data
# ---------------------------------------------------------------------------
def validate_alert(data: dict[str, Any]) -> AlertFixture:
_require_str(data, "title", ctx="alert.json")
_require_str(data, "state", ctx="alert.json")
_require_str(data, "alert_source", ctx="alert.json")
if not isinstance(data.get("commonLabels"), dict):
raise ValueError("alert.json: 'commonLabels' must be an object")
if not isinstance(data.get("commonAnnotations"), dict):
raise ValueError("alert.json: 'commonAnnotations' must be an object")
return data # type: ignore[return-value]
def validate_cloudwatch_metrics(data: dict[str, Any]) -> CloudWatchMetricsFixture:
ctx = "aws_cloudwatch_metrics.json"
_require_str(data, "namespace", ctx=ctx)
_require_str(data, "start_time", ctx=ctx)
_require_str(data, "end_time", ctx=ctx)
if not isinstance(data.get("period"), int):
raise ValueError(f"{ctx}: 'period' must be an integer (seconds)")
results = data.get("metric_data_results")
if not isinstance(results, list) or not results:
raise ValueError(f"{ctx}: 'metric_data_results' must be a non-empty list")
for i, result in enumerate(results):
rctx = f"{ctx}:metric_data_results[{i}]"
for field in ("id", "label", "metric_name", "stat", "unit", "status_code"):
_require_str(result, field, ctx=rctx)
if not isinstance(result.get("dimensions"), list):
raise ValueError(f"{rctx}: 'dimensions' must be a list")
for dim in result["dimensions"]:
_require_str(dim, "Name", ctx=rctx)
_require_str(dim, "Value", ctx=rctx)
if not isinstance(result.get("timestamps"), list):
raise ValueError(f"{rctx}: 'timestamps' must be a list")
if not isinstance(result.get("values"), list):
raise ValueError(f"{rctx}: 'values' must be a list")
if len(result["timestamps"]) != len(result["values"]):
raise ValueError(f"{rctx}: 'timestamps' and 'values' must have the same length")
return data # type: ignore[return-value]
def validate_rds_events(data: dict[str, Any]) -> RDSEventsFixture:
if not isinstance(data.get("events"), list):
raise ValueError("aws_rds_events.json: 'events' must be a list")
for i, event in enumerate(data["events"]):
ctx = f"aws_rds_events.json:events[{i}]"
_require_str(event, "date", ctx=ctx)
_require_str(event, "message", ctx=ctx)
_require_str(event, "source_identifier", ctx=ctx)
_require_str(event, "source_type", ctx=ctx)
if not isinstance(event.get("event_categories"), list):
raise ValueError(f"{ctx}: 'event_categories' must be a list")
return data # type: ignore[return-value]
def validate_performance_insights(data: dict[str, Any]) -> PerformanceInsightsFixture:
ctx = "aws_performance_insights.json"
_require_str(data, "db_instance_identifier", ctx=ctx)
_require_str(data, "start_time", ctx=ctx)
_require_str(data, "end_time", ctx=ctx)
db_load = data.get("db_load")
if not isinstance(db_load, dict):
raise ValueError(f"{ctx}: 'db_load' must be an object")
if not isinstance(db_load.get("timestamps"), list):
raise ValueError(f"{ctx}: 'db_load.timestamps' must be a list")
if not isinstance(db_load.get("values"), list):
raise ValueError(f"{ctx}: 'db_load.values' must be a list")
if len(db_load["timestamps"]) != len(db_load["values"]):
raise ValueError(
f"{ctx}: 'db_load.timestamps' and 'db_load.values' must have the same length"
)
if not isinstance(data.get("top_sql"), list):
raise ValueError(f"{ctx}: 'top_sql' must be a list")
if not isinstance(data.get("top_wait_events"), list):
raise ValueError(f"{ctx}: 'top_wait_events' must be a list")
if not isinstance(data.get("top_users"), list):
raise ValueError(f"{ctx}: 'top_users' must be a list")
if not isinstance(data.get("top_hosts"), list):
raise ValueError(f"{ctx}: 'top_hosts' must be a list")
return data # type: ignore[return-value]
def validate_ec2_instances_by_tag(data: dict[str, Any]) -> EC2InstancesByTagFixture:
ctx = "ec2_instances_by_tag.json"
instances = data.get("instances")
if not isinstance(instances, list):
raise ValueError(f"{ctx}: 'instances' must be a list")
for i, inst in enumerate(instances):
ictx = f"{ctx}:instances[{i}]"
if not isinstance(inst, dict):
raise ValueError(f"{ictx}: must be an object")
_require_str(inst, "instance_id", ctx=ictx)
return data # type: ignore[return-value]
def validate_elb_target_health(data: dict[str, Any]) -> ELBTargetHealthFixture:
ctx = "elb_target_health.json"
if not isinstance(data.get("target_groups"), list):
raise ValueError(f"{ctx}: 'target_groups' must be a list")
if not isinstance(data.get("targets"), list):
raise ValueError(f"{ctx}: 'targets' must be a list")
for i, target in enumerate(data["targets"]):
tctx = f"{ctx}:targets[{i}]"
if not isinstance(target, dict):
raise ValueError(f"{tctx}: must be an object")
_require_str(target, "instance_id", ctx=tctx)
_require_str(target, "state", ctx=tctx)
return data # type: ignore[return-value]
def validate_generic_evidence(data: dict[str, Any], *, filename: str) -> GenericEvidenceFixture:
if not isinstance(data, dict):
raise ValueError(f"{filename}: expected an object")
return data # type: ignore[return-value]
def validate_answer_key(data: dict[str, Any]) -> AnswerKeySchema:
_require_str(data, "root_cause_category", ctx="answer.yml")
_require_non_empty_str_list(data, "required_keywords", "answer.yml", required=True)
_require_str(data, "model_response", ctx="answer.yml")
for opt_list_field in (
"forbidden_categories",
"forbidden_keywords",
"required_evidence_sources",
"equivalent_root_cause_categories",
):
val = data.get(opt_list_field)
if val is not None and not isinstance(val, list):
raise ValueError(f"answer.yml: '{opt_list_field}' must be a list when present")
required_sources = data.get("required_evidence_sources")
if required_sources is not None and required_sources:
if not all(isinstance(item, str) and item.strip() for item in required_sources):
raise ValueError(
"answer.yml: 'required_evidence_sources' must contain only non-empty strings"
)
unknown_sources = [item for item in required_sources if item not in VALID_EVIDENCE_SOURCES]
if unknown_sources:
raise ValueError(
"answer.yml: unknown source(s) in required_evidence_sources "
f"{unknown_sources}; expected subset of {sorted(VALID_EVIDENCE_SOURCES)}"
)
equiv = data.get("equivalent_root_cause_categories")
if (
equiv is not None
and equiv
and not all(isinstance(item, str) and item.strip() for item in equiv)
):
raise ValueError(
"answer.yml: 'equivalent_root_cause_categories' "
"must contain only non-empty strings when present"
)
trajectory = data.get("optimal_trajectory")
if trajectory is not None:
if not isinstance(trajectory, list) or not trajectory:
raise ValueError(
"answer.yml: 'optimal_trajectory' must be a non-empty list when present"
)
unknown_actions = [a for a in trajectory if a not in VALID_TRAJECTORY_ACTIONS]
if unknown_actions:
raise ValueError(
f"answer.yml: unknown action(s) in optimal_trajectory {unknown_actions}; "
f"expected subset of {sorted(VALID_TRAJECTORY_ACTIONS)}"
)
max_loops = data.get("max_investigation_loops")
if max_loops is not None and (not isinstance(max_loops, int) or max_loops < 1):
raise ValueError(
"answer.yml: 'max_investigation_loops' must be a positive integer when present"
)
for axis2_list_field in ("ruling_out_keywords", "required_queries"):
_require_non_empty_str_list(data, axis2_list_field, "answer.yml")
golden = data.get("golden_trajectory")
if golden is not None:
if not isinstance(golden, dict):
raise ValueError("answer.yml: 'golden_trajectory' must be an object when present")
ordered_actions = golden.get("ordered_actions")
if ordered_actions is not None:
if (
not isinstance(ordered_actions, list)
or not ordered_actions
or not all(isinstance(action, str) and action.strip() for action in ordered_actions)
):
raise ValueError(
"answer.yml: 'golden_trajectory.ordered_actions' must be a non-empty list "
"of strings when present"
)
unknown_actions = [a for a in ordered_actions if a not in VALID_TRAJECTORY_ACTIONS]
if unknown_actions:
raise ValueError(
"answer.yml: unknown action(s) in golden_trajectory.ordered_actions "
f"{unknown_actions}; expected subset of {sorted(VALID_TRAJECTORY_ACTIONS)}"
)
matching = golden.get("matching")
if matching is not None and matching not in {"strict", "lcs", "set"}:
raise ValueError(
"answer.yml: 'golden_trajectory.matching' must be one of "
"'strict', 'lcs', or 'set' when present"
)
for int_field in ("max_edit_distance", "max_extra_actions", "max_redundancy", "max_loops"):
value = golden.get(int_field)
if value is not None and (not isinstance(value, int) or value < 0):
raise ValueError(
f"answer.yml: 'golden_trajectory.{int_field}' must be a non-negative integer "
"when present"
)
return data # type: ignore[return-value]
def validate_scenario_metadata(data: dict[str, Any]) -> ScenarioMetadataSchema:
ctx = "scenario.yml"
for field in (
"schema_version",
"scenario_id",
"engine",
"engine_version",
"instance_class",
"region",
"db_instance_identifier",
"failure_mode",
"severity",
):
_require_str(data, field, ctx=ctx)
engine = data["engine"]
if engine not in VALID_ENGINES:
raise ValueError(
f"{ctx}: unknown engine {engine!r}; expected one of {sorted(VALID_ENGINES)}"
)
failure_mode = data["failure_mode"]
if failure_mode not in VALID_FAILURE_MODES:
raise ValueError(
f"{ctx}: unknown failure_mode {failure_mode!r}; expected one of {sorted(VALID_FAILURE_MODES)}"
)
sources = data.get("available_evidence")
if not isinstance(sources, list) or not sources:
raise ValueError(f"{ctx}: 'available_evidence' must be a non-empty list")
unknown = [s for s in sources if s not in VALID_EVIDENCE_SOURCES]
if unknown:
raise ValueError(
f"{ctx}: unknown evidence source(s) {unknown}; expected subset of {sorted(VALID_EVIDENCE_SOURCES)}"
)
return data # type: ignore[return-value]
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
def _require_str(obj: dict[str, Any], key: str, ctx: str = "") -> None:
value = obj.get(key)
prefix = f"{ctx}: " if ctx else ""
if not isinstance(value, str) or not value.strip():
raise ValueError(f"{prefix}missing or empty required string field '{key}'")
def _require_non_empty_str_list(
obj: dict[str, Any],
key: str,
ctx: str,
*,
required: bool = False,
) -> None:
value = obj.get(key)
if value is None:
if required:
raise ValueError(f"{ctx}: '{key}' must be a non-empty list")
return
if not isinstance(value, list) or not value:
raise ValueError(f"{ctx}: '{key}' must be a non-empty list")
if not all(isinstance(item, str) and item.strip() for item in value):
raise ValueError(f"{ctx}: all '{key}' entries must be non-empty strings")