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

190 lines
6.4 KiB
Python

"""Pytest fixtures for co-located turn tests."""
from __future__ import annotations
import os
import sys
from collections.abc import Iterator
from pathlib import Path
import pytest
from pydantic import ValidationError
from config.config import (
get_configured_llm_provider,
get_llm_provider_api_key_env,
resolve_llm_settings,
)
from config.grafana_cloud import load_env
from config.llm_auth.credentials import status as credential_status
from config.llm_auth.provider_catalog import provider_spec
from config.platform_bootstrap import ensure_project_platform_package
from tests.core.agent._ci_gates import (
running_in_github_actions,
)
ensure_project_platform_package()
def _repo_root() -> Path:
for parent in Path(__file__).resolve().parents:
if (parent / "pyproject.toml").is_file():
return parent
return Path(__file__).resolve().parents[3]
_PROJECT_ROOT = _repo_root()
_ENV_PATH = _PROJECT_ROOT / ".env"
_TURN_TEST_DEFAULT_ENV = {
"OPENSRE_SENTRY_DISABLED": "1",
"OPENSRE_NO_TELEMETRY": "1",
"OPENSRE_INVESTIGATION_SOURCE": "test",
}
def _skip_or_fail_live_llm(message: str) -> None:
if running_in_github_actions():
pytest.fail(message)
pytest.skip(message)
def pytest_addoption(parser: pytest.Parser) -> None:
"""Register selection flags for the live turn scenario suite.
The suite is downsampled by default (everywhere, including CI) to a small
representative subset, then sharded. Use these to change the subset or run
the full suite on demand. ``TURN_MAX_RUNS`` separately caps each scenario's
majority-vote ``runs`` (default 1; set ``0``/``all`` to honour fixtures).
"""
group = parser.getgroup("turn scenarios")
group.addoption(
"--turn-select",
action="store",
default=None,
help=(
"Choose which live turn scenarios run. Use 'all' to run the FULL "
"suite (the default is a small representative downsample). Otherwise "
"'<mode>:<n>' where mode is 'complex' (most complex) or 'sample' "
"(random), and n is a count, a fraction, or a percentage (e.g. "
"'complex:5', 'sample:0.1', 'sample:10%'). Also settable via the "
"TURN_SELECT env var."
),
)
group.addoption(
"--turn-select-seed",
action="store",
default=None,
help="Random seed for '--turn-select=sample:*' (default: TURN_SELECT_SEED or 1337).",
)
def pytest_configure(config: pytest.Config) -> None: # noqa: ARG001
"""Load project settings for co-located turn tests."""
load_env(_ENV_PATH, override=False)
@pytest.fixture(autouse=True)
def _turn_test_env_defaults(monkeypatch: pytest.MonkeyPatch) -> None:
"""Mirror test-suite defaults while keeping env mutations isolated per test."""
for key, value in _TURN_TEST_DEFAULT_ENV.items():
monkeypatch.setenv(key, value)
@pytest.fixture(autouse=True)
def _disable_system_keyring(
request: pytest.FixtureRequest,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Keep tests isolated from any real developer keychain entries."""
if request.node.get_closest_marker("live_llm") is not None:
return
monkeypatch.setenv("OPENSRE_DISABLE_KEYRING", "1")
@pytest.fixture(autouse=True)
def _resolve_live_llm_configuration(
request: pytest.FixtureRequest,
monkeypatch: pytest.MonkeyPatch,
) -> Iterator[None]:
"""Let live LLM turn tests run with Anthropic or OpenAI credentials."""
if request.node.get_closest_marker("live_llm") is None:
yield
return
try:
settings = resolve_llm_settings()
except ValidationError as exc:
provider = get_configured_llm_provider()
env_var = get_llm_provider_api_key_env(provider)
msg = exc.errors()[0].get("msg", str(exc)) if exc.errors() else str(exc)
hint = f" configured provider={provider!r}"
if env_var is not None:
hint += f", required key={env_var}"
_skip_or_fail_live_llm(
f"Live LLM turn tests require usable LLM configuration:{hint}. {msg}"
)
auth = credential_status(settings.provider)
if not auth.configured or auth.stale:
_skip_or_fail_live_llm(
"Live LLM turn tests require usable LLM credentials:"
f" configured provider={settings.provider!r}, auth={auth.source}, detail={auth.detail}"
)
spec = provider_spec(settings.provider)
if spec is not None and spec.credential_kind == "api_key" and spec.api_key_env:
from config.llm_credentials import resolve_llm_api_key
if not resolve_llm_api_key(spec.api_key_env):
_skip_or_fail_live_llm(
"Live LLM turn tests require a resolvable API key:"
f" provider={settings.provider!r}, env={spec.api_key_env}"
)
from core.llm.factory import reset_llm_clients
monkeypatch.setenv("LLM_PROVIDER", settings.provider)
reset_llm_clients()
yield
reset_llm_clients()
@pytest.fixture(autouse=True)
def _repl_execution_policy_auto_yes(monkeypatch: pytest.MonkeyPatch) -> None:
"""Elevated REPL actions prompt for confirmation; stdin is non-TTY under pytest."""
monkeypatch.setattr(
"surfaces.interactive_shell.ui.execution_confirm.DEFAULT_CONFIRM_FN",
lambda _prompt: "y",
)
monkeypatch.setattr(sys.stdin, "isatty", lambda: True)
_LIVE_LLM_SKIPS_IN_CI: list[str] = []
def _is_xdist_worker() -> bool:
"""True on pytest-xdist worker processes (not the controller)."""
return os.getenv("PYTEST_XDIST_WORKER") is not None
def pytest_runtest_logreport(report: pytest.TestReport) -> None:
"""Fail the run if any live_llm test skips in CI (controller-only under xdist)."""
if _is_xdist_worker() or not running_in_github_actions():
return
if report.when != "call" or not report.skipped:
return
if "live_llm" not in report.keywords:
return
_LIVE_LLM_SKIPS_IN_CI.append(f"{report.nodeid}: {report.longrepr}")
def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None:
if _is_xdist_worker() or not _LIVE_LLM_SKIPS_IN_CI:
return
terminal = session.config.pluginmanager.get_plugin("terminalreporter")
if terminal is not None:
terminal.write_line("live_llm tests must not skip in CI (fix credentials or shard config):")
for line in _LIVE_LLM_SKIPS_IN_CI:
terminal.write_line(f" - {line}")
session.exitstatus = 1