Files
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

781 lines
28 KiB
Python

from __future__ import annotations
import json
import textwrap
from pathlib import Path
import pytest
from config.config import has_credentials_for_active_llm_provider
from tests.synthetic.rds_postgres.run_suite import run_scenario
from tests.synthetic.rds_postgres.scenario_loader import (
SUITE_DIR,
GoldenTrajectoryConfig,
load_all_scenarios,
load_scenario,
)
from tests.synthetic.rds_postgres.scoring import score_result
from tests.synthetic.schemas import VALID_EVIDENCE_SOURCES
def test_load_all_scenarios_reads_benchmark_cases() -> None:
fixtures = load_all_scenarios()
scenario_ids = [fixture.scenario_id for fixture in fixtures]
assert "000-healthy" in scenario_ids
assert "001-replication-lag" in scenario_ids
assert "002-connection-exhaustion" in scenario_ids
def test_scenario_metadata_is_valid() -> None:
fixtures = load_all_scenarios()
for fixture in fixtures:
meta = fixture.metadata
assert meta.schema_version, f"{fixture.scenario_id}: schema_version must be set"
assert meta.engine, f"{fixture.scenario_id}: engine must be set"
assert meta.failure_mode, f"{fixture.scenario_id}: failure_mode must be set"
assert meta.region, f"{fixture.scenario_id}: region must be set"
assert meta.available_evidence, (
f"{fixture.scenario_id}: available_evidence must not be empty"
)
unknown = set(meta.available_evidence) - VALID_EVIDENCE_SOURCES
assert not unknown, f"{fixture.scenario_id}: unknown evidence sources {unknown}"
def test_scenario_evidence_matches_available_evidence() -> None:
fixtures = load_all_scenarios()
for fixture in fixtures:
evidence_dict = fixture.evidence.as_dict()
assert set(evidence_dict.keys()) == set(fixture.metadata.available_evidence), (
f"{fixture.scenario_id}: evidence keys {set(evidence_dict.keys())} "
f"do not match available_evidence {fixture.metadata.available_evidence}"
)
def test_score_result_does_not_apply_failover_wording_to_storage_scenario() -> None:
fixture = load_scenario(SUITE_DIR / "008-storage-full-missing-metric")
final_state = {
"root_cause": (
"The RDS instance ran out of storage space, and storage space exhaustion is "
"confirmed by the RDS event plus collapsing WriteIOPS."
),
"root_cause_category": "storage_exhaustion",
"validated_claims": [
{"claim": 'RDS event states "DB instance ran out of storage space".'},
],
"non_validated_claims": [],
"causal_chain": ["Storage filled up, blocked writes, and caused the alert."],
"evidence": {
"grafana_logs": [
{"message": "DB instance ran out of storage space."},
]
},
"executed_hypotheses": [
{
"actions": [
"query_grafana_logs",
"query_grafana_metrics",
"query_grafana_alert_rules",
]
}
],
}
score = score_result(fixture, final_state)
assert score.passed is True
def test_score_result_accepts_equivalent_cpu_saturation_category() -> None:
"""Scenario 004 allows either generic CPU saturation or the specific bad-query label."""
fixture = load_scenario(SUITE_DIR / "004-cpu-saturation-bad-query")
final_state = {
"root_cause": (
"CPU saturation from a heavy query shown in Performance Insights "
"top SQL with AAS and avg load on the catalog database."
),
"root_cause_category": "cpu_saturation",
"validated_claims": [],
"non_validated_claims": [],
"causal_chain": [],
"evidence": {
"grafana_metrics": {"placeholder": True},
},
"executed_hypotheses": [
{
"actions": [
"query_grafana_metrics",
"query_grafana_logs",
"query_grafana_alert_rules",
]
}
],
"investigation_loop_count": 1,
}
score = score_result(fixture, final_state)
assert score.passed is True
assert score.actual_category == "cpu_saturation"
assert score.accepted_categories == ("cpu_saturation", "cpu_saturation_bad_query")
def test_score_result_accepts_replication_lag_wal_volume_equivalent() -> None:
"""Scenario 006 allows generic replication_lag or WAL-specific subcategory."""
fixture = load_scenario(SUITE_DIR / "006-replication-lag-cpu-redherring")
final_state = {
"root_cause": (
"Replication lag on the replica from WAL volume; the SELECT workload is a "
"red herring with avg load and AAS in Performance Insights. "
"UPDATE on primary drives WAL; SELECT analytics is unrelated to the lag."
),
"root_cause_category": "replication_lag_wal_volume",
"validated_claims": [],
"non_validated_claims": [],
"causal_chain": [],
"evidence": {
"grafana_metrics": {"placeholder": True},
},
"executed_hypotheses": [
{
"actions": [
"query_grafana_metrics",
"query_grafana_logs",
"query_grafana_alert_rules",
]
}
],
"investigation_loop_count": 1,
}
score = score_result(fixture, final_state)
assert score.passed is True
assert score.actual_category == "replication_lag_wal_volume"
assert score.accepted_categories == ("replication_lag", "replication_lag_wal_volume")
def test_score_result_keeps_failover_event_reasoning_requirement() -> None:
fixture = load_scenario(SUITE_DIR / "005-failover")
final_state = {
"root_cause": (
"A Multi-AZ automatic failover occurred after a health check failure on the "
"primary host, and workload resumed normally after failover completed. The "
"timeline shows failover initiated, failover in progress, failover completed, "
"and instance available as the primary evidence source."
),
"root_cause_category": "infrastructure",
"validated_claims": [
{"claim": "The timeline confirms the instance became available again."},
],
"non_validated_claims": [],
"causal_chain": [
"Health check failure triggered standby promotion and client reconnection."
],
"evidence": {
"grafana_logs": [
{"message": "Failover initiated."},
{"message": "Failover in progress."},
{"message": "Failover completed."},
{"message": "Instance available."},
]
},
"executed_hypotheses": [
{
"actions": [
"query_grafana_logs",
"query_grafana_metrics",
"query_grafana_alert_rules",
]
}
],
}
score = score_result(fixture, final_state)
assert score.failure_reason == "RDS events gathered but not used as primary reasoning signal"
def test_score_result_accepts_failover_event_reasoning() -> None:
fixture = load_scenario(SUITE_DIR / "005-failover")
final_state = {
"root_cause": (
"Based on the RDS event timeline (primary evidence source), a Multi-AZ "
"automatic failover occurred after a health check failure on the primary "
"host, and workload resumed normally after failover completed."
),
"root_cause_category": "infrastructure",
"validated_claims": [
{
"claim": (
"RDS events show the full sequence: failover initiated -> "
"failover in progress -> failover completed -> instance available."
)
},
],
"non_validated_claims": [],
"causal_chain": [
"Health check failure triggered failover, standby promotion, DNS update, "
"brief outage, and recovery."
],
"evidence": {
"grafana_logs": [
{"message": "Failover initiated."},
{"message": "Failover in progress."},
{"message": "Failover completed."},
{"message": "Instance available."},
]
},
"executed_hypotheses": [
{
"actions": [
"query_grafana_logs",
"query_grafana_metrics",
"query_grafana_alert_rules",
]
}
],
}
score = score_result(fixture, final_state)
assert score.passed is True
def test_score_result_uses_semantic_keyword_matching_for_write_heavy_workload() -> None:
fixture = load_scenario(SUITE_DIR / "001-replication-lag")
final_state = {
"root_cause": (
"Replication lag is driven by a write-heavy UPDATE on the orders table, "
"which increases WAL generation faster than the replica can replay; "
"Top SQL Activity and Avg Load confirm replay pressure."
),
"root_cause_category": "replication_lag",
"validated_claims": [
{"claim": "Replica lag and WAL replay pressure are both elevated."},
],
"non_validated_claims": [],
"causal_chain": [],
"evidence": {
"grafana_metrics": [{"metric_name": "ReplicaLag"}],
"grafana_logs": [{"message": "replica lag spike observed"}],
},
"executed_hypotheses": [
{
"actions": [
"query_grafana_metrics",
"query_grafana_logs",
"query_grafana_alert_rules",
]
}
],
}
score = score_result(fixture, final_state)
assert score.semantic_keyword_match is True
assert score.exact_keyword_match is False
assert score.gates["required_keyword_match"].status == "pass"
assert "write-heavy workload" in score.semantic_matched_keywords
assert score.passed is True
_ALL_SCENARIOS = load_all_scenarios()
_LLM_ATTEMPTS = 2
_SYNTHETIC_SKIP_LLM = (
"SKIPPED: missing API key for LLM_PROVIDER "
"(suite uses mock Grafana/RDS backends; configure the key for your selected provider)"
)
def _by_difficulty(level: int) -> list:
return [f for f in _ALL_SCENARIOS if f.metadata.scenario_difficulty == level]
def _should_assert_trajectory(fixture, actual_category: str) -> bool:
"""Keep trajectory assertions for lower-difficulty non-healthy scenarios."""
return fixture.metadata.scenario_difficulty <= 2 and actual_category != "healthy"
def _run_scenario_test(fixture) -> None:
"""Run scenario with real LLM and mock Grafana backend, then assert scoring."""
if not has_credentials_for_active_llm_provider():
pytest.skip(_SYNTHETIC_SKIP_LLM)
failures: list[str] = []
for attempt in range(1, _LLM_ATTEMPTS + 1):
final_state, score = run_scenario(fixture, use_mock_grafana=True)
try:
assert final_state["root_cause"]
assert score.passed is True, (
f"{fixture.scenario_id} FAILED: {score.failure_reason}\n"
f" actual_category={score.actual_category!r} "
f" missing_keywords={score.missing_keywords}"
)
if (
_should_assert_trajectory(fixture, score.actual_category)
and score.trajectory is not None
):
assert score.trajectory.sequencing_ok, (
f"{fixture.scenario_id} TRAJECTORY FAIL: "
f"sequencing={score.trajectory.sequencing_ok} "
f"calibration={score.trajectory.calibration_ok}\n"
f" expected={score.trajectory.expected_sequence}\n"
f" actual={score.trajectory.actual_sequence}"
)
return
except AssertionError as exc:
failures.append(f"attempt {attempt}/{_LLM_ATTEMPTS}: {exc}")
raise AssertionError("\n\n".join(failures))
@pytest.mark.synthetic
@pytest.mark.parametrize("fixture", _by_difficulty(1), ids=lambda f: f.scenario_id)
def test_level1_scenario(fixture) -> None:
"""Level 1 — single dominant signal, all evidence consistent."""
_run_scenario_test(fixture)
@pytest.mark.synthetic
@pytest.mark.parametrize("fixture", _by_difficulty(2), ids=lambda f: f.scenario_id)
def test_level2_scenario(fixture) -> None:
"""Level 2 — one confounder present, second evidence source needed to rule it out."""
_run_scenario_test(fixture)
@pytest.mark.synthetic
@pytest.mark.parametrize("fixture", _by_difficulty(3), ids=lambda f: f.scenario_id)
def test_level3_scenario(fixture) -> None:
"""Level 3 — absent or indirect evidence, key metric missing."""
_run_scenario_test(fixture)
@pytest.mark.synthetic
@pytest.mark.parametrize("fixture", _by_difficulty(4), ids=lambda f: f.scenario_id)
def test_level4_scenario(fixture) -> None:
"""Level 4 — compositional fault, two failure modes causally linked."""
_run_scenario_test(fixture)
# ---------------------------------------------------------------------------
# Scenario inheritance unit tests
# ---------------------------------------------------------------------------
def _write_minimal_answer_yml(scenario_dir: Path) -> None:
(scenario_dir / "answer.yml").write_text(
textwrap.dedent("""\
root_cause_category: test_category
required_keywords:
- test_keyword
model_response: "Test model response."
""")
)
class TestScenarioInheritance:
"""Verify base-inheritance and evidence-file fallback in scenario_loader."""
def test_metadata_inherited_from_base(self, tmp_path: Path) -> None:
"""Scenario with base: 000-healthy inherits metadata fields it omits."""
scenario_dir = tmp_path / "999-test-inherit"
scenario_dir.mkdir()
(scenario_dir / "scenario.yml").write_text(
textwrap.dedent("""\
base: 000-healthy
scenario_id: 999-test-inherit
failure_mode: cpu_saturation
severity: critical
""")
)
_write_minimal_answer_yml(scenario_dir)
# Symlink the suite directory so _resolve_base_dir can find 000-healthy.
# We place our scenario inside the real suite dir temporarily.
real_dir = SUITE_DIR / "999-test-inherit"
real_dir.mkdir(exist_ok=True)
try:
for f in scenario_dir.iterdir():
(real_dir / f.name).write_bytes(f.read_bytes())
fixture = load_scenario(real_dir)
assert fixture.metadata.scenario_id == "999-test-inherit"
assert fixture.metadata.failure_mode == "cpu_saturation"
assert fixture.metadata.severity == "critical"
# These should be inherited from 000-healthy:
assert fixture.metadata.engine == "postgres"
assert fixture.metadata.engine_version == "15"
assert fixture.metadata.instance_class == "db.r6g.2xlarge"
assert fixture.metadata.region == "us-east-1"
assert fixture.metadata.db_instance_identifier == "payments-prod"
assert fixture.metadata.db_cluster == "payments-cluster"
assert fixture.metadata.schema_version == "1.0"
assert "aws_cloudwatch_metrics" in fixture.metadata.available_evidence
finally:
for f in real_dir.iterdir():
f.unlink()
real_dir.rmdir()
def test_evidence_falls_back_to_base(self) -> None:
"""Scenario without evidence files loads them from the base."""
real_dir = SUITE_DIR / "999-test-fallback"
real_dir.mkdir(exist_ok=True)
try:
(real_dir / "scenario.yml").write_text(
textwrap.dedent("""\
base: 000-healthy
scenario_id: 999-test-fallback
failure_mode: healthy
severity: info
""")
)
_write_minimal_answer_yml(real_dir)
fixture = load_scenario(real_dir)
# Evidence should come from 000-healthy (non-None since base has all three)
assert fixture.evidence.aws_cloudwatch_metrics is not None
assert fixture.evidence.aws_rds_events is not None
assert fixture.evidence.aws_performance_insights is not None
# Alert should also fall back to 000-healthy's
assert fixture.alert["state"] == "normal"
assert "payments-prod" in fixture.alert["title"]
finally:
for f in real_dir.iterdir():
f.unlink()
real_dir.rmdir()
def test_local_evidence_overrides_base(self) -> None:
"""Scenario with its own evidence file uses it instead of the base's."""
real_dir = SUITE_DIR / "999-test-override"
real_dir.mkdir(exist_ok=True)
try:
(real_dir / "scenario.yml").write_text(
textwrap.dedent("""\
base: 000-healthy
scenario_id: 999-test-override
failure_mode: healthy
severity: info
""")
)
_write_minimal_answer_yml(real_dir)
custom_events = {
"events": [
{
"date": "2026-04-01T00:00:00Z",
"message": "Custom test event",
"source_identifier": "payments-prod",
"source_type": "db-instance",
"event_categories": ["notification"],
}
]
}
(real_dir / "aws_rds_events.json").write_text(json.dumps(custom_events))
fixture = load_scenario(real_dir)
assert fixture.evidence.aws_rds_events is not None
assert len(fixture.evidence.aws_rds_events) == 1
assert fixture.evidence.aws_rds_events[0]["message"] == "Custom test event"
finally:
for f in real_dir.iterdir():
f.unlink()
real_dir.rmdir()
def test_chained_inheritance_rejected(self) -> None:
"""Declaring base on a scenario that itself has a base raises ValueError."""
real_dir = SUITE_DIR / "999-test-chain-a"
real_dir_b = SUITE_DIR / "999-test-chain-b"
real_dir.mkdir(exist_ok=True)
real_dir_b.mkdir(exist_ok=True)
try:
(real_dir.joinpath("scenario.yml")).write_text(
textwrap.dedent("""\
base: 000-healthy
scenario_id: 999-test-chain-a
failure_mode: healthy
severity: info
""")
)
(real_dir_b / "scenario.yml").write_text(
textwrap.dedent("""\
base: 999-test-chain-a
scenario_id: 999-test-chain-b
failure_mode: healthy
severity: info
""")
)
_write_minimal_answer_yml(real_dir_b)
with pytest.raises(ValueError, match="Chained inheritance is not supported"):
load_scenario(real_dir_b)
finally:
for d in (real_dir, real_dir_b):
for f in d.iterdir():
f.unlink()
d.rmdir()
def test_missing_base_raises(self) -> None:
"""Referencing a non-existent base scenario raises ValueError."""
real_dir = SUITE_DIR / "999-test-missing-base"
real_dir.mkdir(exist_ok=True)
try:
(real_dir / "scenario.yml").write_text(
textwrap.dedent("""\
base: 999-nonexistent
scenario_id: 999-test-missing-base
failure_mode: healthy
severity: info
""")
)
_write_minimal_answer_yml(real_dir)
with pytest.raises(ValueError, match="Base scenario '999-nonexistent' not found"):
load_scenario(real_dir)
finally:
for f in real_dir.iterdir():
f.unlink()
real_dir.rmdir()
def test_no_base_works_unchanged(self) -> None:
"""Scenarios without a base field still load normally."""
fixture = load_scenario(SUITE_DIR / "000-healthy")
assert fixture.metadata.scenario_id == "000-healthy"
assert fixture.metadata.failure_mode == "healthy"
assert fixture.evidence.aws_cloudwatch_metrics is not None
def test_golden_trajectory_is_loaded_as_typed_config(self) -> None:
"""golden_trajectory is normalized into a typed config object."""
real_dir = SUITE_DIR / "999-test-golden-trajectory"
real_dir.mkdir(exist_ok=True)
try:
(real_dir / "scenario.yml").write_text(
textwrap.dedent("""\
base: 000-healthy
scenario_id: 999-test-golden-trajectory
failure_mode: replication_lag
severity: critical
""")
)
(real_dir / "answer.yml").write_text(
textwrap.dedent("""\
root_cause_category: resource_exhaustion
required_keywords:
- replication lag
model_response: "Replication lag from write pressure."
golden_trajectory:
ordered_actions:
- query_grafana_metrics
- query_grafana_logs
matching: strict
max_edit_distance: 1
max_extra_actions: 0
max_redundancy: 0
max_loops: 2
""")
)
fixture = load_scenario(real_dir)
expected = GoldenTrajectoryConfig(
ordered_actions=["query_grafana_metrics", "query_grafana_logs"],
matching="strict",
max_edit_distance=1,
max_extra_actions=0,
max_redundancy=0,
max_loops=2,
)
assert fixture.answer_key.golden_trajectory == expected
finally:
for f in real_dir.iterdir():
f.unlink()
real_dir.rmdir()
def test_golden_trajectory_requires_ordered_actions(self) -> None:
"""golden_trajectory block must include a non-empty ordered_actions list."""
real_dir = SUITE_DIR / "999-test-golden-trajectory-missing-actions"
real_dir.mkdir(exist_ok=True)
try:
(real_dir / "scenario.yml").write_text(
textwrap.dedent("""\
base: 000-healthy
scenario_id: 999-test-golden-trajectory-missing-actions
failure_mode: replication_lag
severity: critical
""")
)
(real_dir / "answer.yml").write_text(
textwrap.dedent("""\
root_cause_category: resource_exhaustion
required_keywords:
- replication lag
model_response: "Replication lag from write pressure."
golden_trajectory:
matching: strict
""")
)
with pytest.raises(
ValueError,
match="golden_trajectory.ordered_actions",
):
load_scenario(real_dir)
finally:
for f in real_dir.iterdir():
f.unlink()
real_dir.rmdir()
def test_golden_trajectory_rejects_boolean_numeric_fields(self) -> None:
"""Boolean values are rejected for numeric golden_trajectory limits."""
real_dir = SUITE_DIR / "999-test-golden-trajectory-bool-limit"
real_dir.mkdir(exist_ok=True)
try:
(real_dir / "scenario.yml").write_text(
textwrap.dedent("""\
base: 000-healthy
scenario_id: 999-test-golden-trajectory-bool-limit
failure_mode: replication_lag
severity: critical
""")
)
(real_dir / "answer.yml").write_text(
textwrap.dedent("""\
root_cause_category: resource_exhaustion
required_keywords:
- replication lag
model_response: "Replication lag from write pressure."
golden_trajectory:
ordered_actions:
- query_grafana_metrics
- query_grafana_logs
max_loops: true
""")
)
with pytest.raises(
ValueError,
match="golden_trajectory.max_loops",
):
load_scenario(real_dir)
finally:
for f in real_dir.iterdir():
f.unlink()
real_dir.rmdir()
def test_schema_v3_supports_k8s_semantic_evidence_sources(self) -> None:
"""schema_v3 scenarios can declare and load Kubernetes semantic evidence IDs."""
real_dir = SUITE_DIR / "999-test-schema-v3-k8s-sources"
real_dir.mkdir(exist_ok=True)
try:
(real_dir / "scenario.yml").write_text(
textwrap.dedent("""\
base: 000-healthy
schema_version: schema_v3
scenario_id: 999-test-schema-v3-k8s-sources
failure_mode: cpu_saturation
severity: critical
scenario_difficulty: 3
available_evidence:
- k8s_events
- k8s_rollout
""")
)
(real_dir / "answer.yml").write_text(
textwrap.dedent("""\
root_cause_category: cpu_saturation
required_keywords:
- cpu saturation
model_response: "CPU saturation."
required_evidence_sources:
- k8s_events
""")
)
(real_dir / "k8s_events.json").write_text(json.dumps({"events": []}))
(real_dir / "k8s_rollout.json").write_text(json.dumps({"status": "degraded"}))
fixture = load_scenario(real_dir)
assert fixture.metadata.schema_version == "schema_v3"
assert fixture.metadata.available_evidence == ["k8s_events", "k8s_rollout"]
assert fixture.evidence.k8s_events is not None
assert fixture.evidence.k8s_rollout is not None
assert fixture.answer_key.required_evidence_sources == ["k8s_events"]
finally:
for f in real_dir.iterdir():
f.unlink()
real_dir.rmdir()
def test_schema_v3_complex_scenario_requires_required_evidence_sources(self) -> None:
"""schema_v3 complex scenarios must declare non-empty required_evidence_sources."""
real_dir = SUITE_DIR / "999-test-schema-v3-complex-requires-sources"
real_dir.mkdir(exist_ok=True)
try:
(real_dir / "scenario.yml").write_text(
textwrap.dedent("""\
base: 000-healthy
schema_version: schema_v3
scenario_id: 999-test-schema-v3-complex-requires-sources
failure_mode: cpu_saturation
severity: critical
scenario_difficulty: 3
available_evidence:
- aws_cloudwatch_metrics
""")
)
_write_minimal_answer_yml(real_dir)
with pytest.raises(
ValueError,
match="required_evidence_sources",
):
load_scenario(real_dir)
finally:
for f in real_dir.iterdir():
f.unlink()
real_dir.rmdir()
def test_schema_v1_complex_scenario_keeps_backward_compatibility(self) -> None:
"""Legacy schema versions keep loading without required_evidence_sources."""
real_dir = SUITE_DIR / "999-test-schema-v1-complex-backcompat"
real_dir.mkdir(exist_ok=True)
try:
(real_dir / "scenario.yml").write_text(
textwrap.dedent("""\
base: 000-healthy
schema_version: "1.0"
scenario_id: 999-test-schema-v1-complex-backcompat
failure_mode: cpu_saturation
severity: critical
scenario_difficulty: 3
available_evidence:
- aws_cloudwatch_metrics
""")
)
_write_minimal_answer_yml(real_dir)
fixture = load_scenario(real_dir)
assert fixture.metadata.schema_version == "1.0"
assert fixture.answer_key.required_evidence_sources == []
finally:
for f in real_dir.iterdir():
f.unlink()
real_dir.rmdir()