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
230 lines
7.6 KiB
Python
230 lines
7.6 KiB
Python
"""Unit tests for the ``--axis2`` CLI dispatch in ``run_suite``.
|
|
|
|
Pins the wiring fix for #1672: ``--axis2`` was previously parsed by
|
|
``parse_args`` but not consumed by ``run_suite``, so the documented invocation
|
|
``python -m tests.synthetic.rds_postgres.run_suite --axis2`` silently produced
|
|
the default per-scenario flow with no gap report. These tests would have caught
|
|
that regression and now lock the new behavior in place.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
import tests.synthetic.rds_postgres.run_suite as run_suite_module
|
|
from tests.synthetic.mock_grafana_backend.selective_backend import SelectiveGrafanaBackend
|
|
from tests.synthetic.rds_postgres.scenario_loader import SUITE_DIR, load_scenario
|
|
|
|
|
|
def _make_score(fixture: Any, *, passed: bool) -> Any:
|
|
base = run_suite_module.score_result(
|
|
fixture,
|
|
{
|
|
"root_cause": "",
|
|
"root_cause_category": "unknown",
|
|
"validated_claims": [],
|
|
"non_validated_claims": [],
|
|
"causal_chain": [],
|
|
"evidence": {},
|
|
"executed_hypotheses": [],
|
|
"investigation_loop_count": 0,
|
|
"report": "",
|
|
},
|
|
)
|
|
# Mutate only the ``passed`` field on a frozen dataclass via ``replace`` to
|
|
# produce both pass and fail variants for combined-exit-code assertions.
|
|
from dataclasses import replace
|
|
|
|
return replace(base, passed=passed)
|
|
|
|
|
|
def _empty_state() -> dict[str, Any]:
|
|
return {
|
|
"root_cause": "",
|
|
"root_cause_category": "unknown",
|
|
"validated_claims": [],
|
|
"non_validated_claims": [],
|
|
"causal_chain": [],
|
|
"evidence": {},
|
|
"executed_hypotheses": [],
|
|
"investigation_loop_count": 0,
|
|
"report": "",
|
|
}
|
|
|
|
|
|
def test_axis2_runs_each_fixture_twice_and_prints_gap_report(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
capsys: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
"""``--axis2`` runs every fixture twice and emits ``print_gap_report``."""
|
|
fixture = load_scenario(SUITE_DIR / "001-replication-lag")
|
|
|
|
call_args: list[dict[str, Any]] = []
|
|
|
|
def _fake_run_scenario(
|
|
f: Any,
|
|
use_mock_grafana: bool = False,
|
|
grafana_backend: Any = None,
|
|
) -> tuple[dict[str, Any], Any]:
|
|
call_args.append(
|
|
{
|
|
"scenario_id": f.scenario_id,
|
|
"use_mock_grafana": use_mock_grafana,
|
|
"grafana_backend_type": type(grafana_backend).__name__
|
|
if grafana_backend is not None
|
|
else None,
|
|
}
|
|
)
|
|
return _empty_state(), _make_score(f, passed=True)
|
|
|
|
print_gap_calls: list[tuple[int, int, int]] = []
|
|
|
|
def _fake_print_gap_report(
|
|
axis1_results: list[Any],
|
|
axis2_results: list[Any],
|
|
all_fixtures: list[Any],
|
|
) -> None:
|
|
print_gap_calls.append((len(axis1_results), len(axis2_results), len(all_fixtures)))
|
|
|
|
monkeypatch.setattr(run_suite_module, "load_all_scenarios", lambda _suite_dir: [fixture])
|
|
monkeypatch.setattr(run_suite_module, "run_scenario", _fake_run_scenario)
|
|
monkeypatch.setattr(run_suite_module, "print_gap_report", _fake_print_gap_report)
|
|
|
|
results = run_suite_module.run_suite(
|
|
[
|
|
"--scenario",
|
|
fixture.scenario_id,
|
|
"--axis2",
|
|
"--observations-dir",
|
|
str(tmp_path),
|
|
]
|
|
)
|
|
|
|
assert len(call_args) == 2, "each fixture must run twice (axis 1 + axis 2)"
|
|
# Axis 1: full mock backend, no explicit grafana_backend.
|
|
assert call_args[0]["use_mock_grafana"] is True
|
|
assert call_args[0]["grafana_backend_type"] is None
|
|
# Axis 2: SelectiveGrafanaBackend injected, mock_grafana False so the
|
|
# selective backend is the only path the agent can hit.
|
|
assert call_args[1]["use_mock_grafana"] is False
|
|
assert call_args[1]["grafana_backend_type"] == SelectiveGrafanaBackend.__name__
|
|
|
|
assert print_gap_calls == [(1, 1, 1)]
|
|
# Combined return so main()'s exit-code reflects either axis.
|
|
assert len(results) == 2
|
|
|
|
captured = capsys.readouterr()
|
|
assert "Axis 1 vs Axis 2" not in captured.out, (
|
|
"stub print_gap_report swallowed output, so the real banner must not leak"
|
|
)
|
|
|
|
|
|
def test_axis2_combined_results_surface_axis2_failures(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""A fully-failing axis 2 run must produce a non-zero ``main()`` exit code."""
|
|
fixture = load_scenario(SUITE_DIR / "001-replication-lag")
|
|
|
|
invocation = {"count": 0}
|
|
|
|
def _fake_run_scenario(
|
|
f: Any,
|
|
use_mock_grafana: bool = False, # noqa: ARG001
|
|
grafana_backend: Any = None, # noqa: ARG001
|
|
) -> tuple[dict[str, Any], Any]:
|
|
invocation["count"] += 1
|
|
# Axis 1 passes, axis 2 fails — combined return must include the failure.
|
|
passed = invocation["count"] == 1
|
|
return _empty_state(), _make_score(f, passed=passed)
|
|
|
|
monkeypatch.setattr(run_suite_module, "load_all_scenarios", lambda _suite_dir: [fixture])
|
|
monkeypatch.setattr(run_suite_module, "run_scenario", _fake_run_scenario)
|
|
monkeypatch.setattr(run_suite_module, "print_gap_report", lambda *_args, **_kwargs: None)
|
|
|
|
exit_code = run_suite_module.main(
|
|
[
|
|
"--scenario",
|
|
fixture.scenario_id,
|
|
"--axis2",
|
|
"--observations-dir",
|
|
str(tmp_path),
|
|
]
|
|
)
|
|
assert exit_code == 1, "axis 2 failure must propagate through main()'s exit code"
|
|
|
|
|
|
def test_axis2_with_json_emits_structured_payload_with_both_axes(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
capsys: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
"""``--axis2 --json`` emits ``{"axis1": [...], "axis2": [...]}`` on stdout."""
|
|
fixture = load_scenario(SUITE_DIR / "001-replication-lag")
|
|
|
|
monkeypatch.setattr(run_suite_module, "load_all_scenarios", lambda _suite_dir: [fixture])
|
|
monkeypatch.setattr(
|
|
run_suite_module,
|
|
"run_scenario",
|
|
lambda f, **_kwargs: (_empty_state(), _make_score(f, passed=True)),
|
|
)
|
|
# The gap-report path must NOT run when --json is set.
|
|
print_gap_calls: list[None] = []
|
|
monkeypatch.setattr(
|
|
run_suite_module,
|
|
"print_gap_report",
|
|
lambda *_args, **_kwargs: print_gap_calls.append(None),
|
|
)
|
|
|
|
run_suite_module.run_suite(
|
|
[
|
|
"--scenario",
|
|
fixture.scenario_id,
|
|
"--axis2",
|
|
"--json",
|
|
"--observations-dir",
|
|
str(tmp_path),
|
|
]
|
|
)
|
|
|
|
assert print_gap_calls == [], "--json must short-circuit the human-readable gap report"
|
|
|
|
captured = capsys.readouterr()
|
|
payload = json.loads(captured.out)
|
|
assert set(payload.keys()) == {"axis1", "axis2"}
|
|
assert len(payload["axis1"]) == 1
|
|
assert len(payload["axis2"]) == 1
|
|
assert payload["axis1"][0]["scenario_id"] == fixture.scenario_id
|
|
assert payload["axis2"][0]["scenario_id"] == fixture.scenario_id
|
|
|
|
|
|
def test_main_rejects_ollama_before_synthetic_execution(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
capsys: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
monkeypatch.setenv("LLM_PROVIDER", "ollama")
|
|
|
|
exit_code = run_suite_module.main(
|
|
[
|
|
"--scenario",
|
|
"001-replication-lag",
|
|
"--axis2",
|
|
"--observations-dir",
|
|
str(tmp_path),
|
|
]
|
|
)
|
|
|
|
assert exit_code == 1
|
|
captured = capsys.readouterr()
|
|
assert captured.out == ""
|
|
assert "RDS PostgreSQL synthetic tests are not supported with LLM_PROVIDER=ollama" in (
|
|
captured.err
|
|
)
|
|
assert "Local Ollama models" in captured.err
|