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

880 lines
28 KiB
Python

from __future__ import annotations
import errno
import json
import os
import re
import select
import shutil
import site
import subprocess
import sys
import sysconfig
import time
from dataclasses import dataclass
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from threading import Thread
import pytest
from config.constants.paths import REPO_ROOT
from config.version import get_opensre_version
_SCRIPT_NAME = "opensre.exe" if os.name == "nt" else "opensre"
_ANSI_RE = re.compile(r"\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
_CLEARED_ENV_KEYS = (
"ANTHROPIC_API_KEY",
"AWS_ACCESS_KEY_ID",
"AWS_DEFAULT_REGION",
"AWS_EXTERNAL_ID",
"AWS_REGION",
"AWS_ROLE_ARN",
"AWS_SECRET_ACCESS_KEY",
"AWS_SESSION_TOKEN",
"CORALOGIX_API_KEY",
"CORALOGIX_API_URL",
"CORALOGIX_APPLICATION_NAME",
"CORALOGIX_SUBSYSTEM_NAME",
"DD_API_KEY",
"DD_APP_KEY",
"DD_SITE",
"GEMINI_API_KEY",
"GOOGLE_CREDENTIALS_FILE",
"GOOGLE_DRIVE_FOLDER_ID",
"GRAFANA_INSTANCE_URL",
"GRAFANA_READ_TOKEN",
"HONEYCOMB_API_KEY",
"HONEYCOMB_API_URL",
"HONEYCOMB_DATASET",
"JWT_TOKEN",
"NVIDIA_API_KEY",
"OPENAI_API_KEY",
"OPENROUTER_API_KEY",
"OPENCLAW_MCP_ARGS",
"OPENCLAW_MCP_AUTH_TOKEN",
"OPENCLAW_MCP_COMMAND",
"OPENCLAW_MCP_MODE",
"OPENCLAW_MCP_URL",
"OPENSRE_LLM_AUTH_METADATA_PATH",
"OPENSRE_PROJECT_ENV_PATH",
"OPENSRE_RELEASES_API_URL",
"OPENSRE_WIZARD_STORE_PATH",
"SLACK_WEBHOOK_URL",
"TELEGRAM_BOT_TOKEN",
"TELEGRAM_DEFAULT_CHAT_ID",
"TRACER_API_URL",
"TRACER_WEB_APP_URL",
)
@dataclass(frozen=True)
class CliResult:
args: tuple[str, ...]
exit_code: int
stdout: str
stderr: str = ""
@dataclass(frozen=True)
class PtyAction:
#: Wait until this substring appears, or any substring if a tuple is given.
expect: str | tuple[str, ...]
send: bytes
timeout: float = 10.0
#: If > 0, send this many ``stagger_key`` keypresses one at a time
#: (prompt_toolkit may coalesce a single burst), then send ``send``
#: (usually ``\\r``).
stagger_j: int = 0
#: The single-keypress navigation byte sent ``stagger_j`` times. Defaults to
#: ``j`` (vim-down). Use ``k`` (vim-up) to reach the last option in one step,
#: since questionary select menus wrap around.
stagger_key: bytes = b"j"
@dataclass
class CliSandbox:
home: Path
project_env_path: Path
env: dict[str, str]
@property
def integration_store_path(self) -> Path:
return self.home / ".opensre" / "integrations.json"
@property
def wizard_store_path(self) -> Path:
return self.home / ".opensre" / "opensre.json"
def seed_integrations(self, integrations: list[dict[str, object]]) -> None:
self.integration_store_path.parent.mkdir(parents=True, exist_ok=True)
self.integration_store_path.write_text(
json.dumps({"version": 1, "integrations": integrations}, indent=2) + "\n",
encoding="utf-8",
)
def seed_wizard_store(
self,
*,
provider: str = "anthropic",
model: str = "claude-opus-4-7",
) -> None:
self.wizard_store_path.parent.mkdir(parents=True, exist_ok=True)
self.wizard_store_path.write_text(
json.dumps(
{
"version": 1,
"wizard": {
"mode": "quickstart",
"configured_target": "local",
"updated_at": "2026-01-01T00:00:00+00:00",
},
"targets": {
"local": {
"provider": provider,
"model": model,
"api_key_env": f"{provider.upper()}_API_KEY",
"model_env": f"{provider.upper()}_REASONING_MODEL",
"updated_at": "2026-01-01T00:00:00+00:00",
}
},
"probes": {"local": {}, "remote": {}},
},
indent=2,
)
+ "\n",
encoding="utf-8",
)
def seed_project_env(
self,
*,
provider: str = "anthropic",
model: str = "claude-opus-4-7",
) -> None:
model_env = f"{provider.upper()}_REASONING_MODEL"
self.project_env_path.write_text(
f"LLM_PROVIDER={provider}\n{model_env}={model}\n",
encoding="utf-8",
)
def read_integrations(self) -> list[dict[str, object]]:
if not self.integration_store_path.exists():
return []
payload = json.loads(self.integration_store_path.read_text(encoding="utf-8"))
return list(payload.get("integrations", []))
def read_project_env(self) -> str:
if not self.project_env_path.exists():
return ""
return self.project_env_path.read_text(encoding="utf-8")
def read_wizard_store(self) -> dict[str, object]:
return json.loads(self.wizard_store_path.read_text(encoding="utf-8"))
def _clean_terminal_output(text: str) -> str:
if not text:
return ""
cleaned = _ANSI_RE.sub("", text)
cleaned = cleaned.replace("\r", "\n").replace("\x00", "")
cleaned = re.sub(r"\n{3,}", "\n\n", cleaned)
return cleaned
def _opensre_executable() -> Path:
"""Return the ``opensre`` entrypoint from the active test interpreter's venv.
Prefer the script adjacent to ``sys.executable`` over ``shutil.which`` so CI
and local xdist workers never invoke a stale global ``opensre`` on ``PATH``
that predates the ``remote`` command (smoke tests would otherwise see
``Error: No such command 'remote'``).
"""
candidates: list[Path] = [
Path(sys.executable).with_name(_SCRIPT_NAME),
Path(sysconfig.get_path("scripts")) / _SCRIPT_NAME,
]
resolved = shutil.which(_SCRIPT_NAME)
if resolved:
candidates.append(Path(resolved))
for candidate in candidates:
if candidate.exists():
return candidate
pytest.skip("installed opensre executable is unavailable in this environment")
raise AssertionError("pytest.skip should have interrupted control flow")
def _is_python_script(path: Path) -> bool:
"""Return True when an executable should be launched via Python."""
if path.suffix in {".py", ".pyw"}:
return True
try:
first_line = path.read_text(encoding="utf-8", errors="ignore").splitlines()[0]
except (OSError, IndexError):
return False
return first_line.startswith("#!") and "python" in first_line.lower()
def _cli_env(home: Path, project_env_path: Path) -> dict[str, str]:
env = os.environ.copy()
# Blank values block ``load_dotenv(override=False)`` from re-importing the repo
# ``.env`` when subprocesses run with ``cwd=REPO_ROOT``.
for key in _CLEARED_ENV_KEYS:
env[key] = ""
existing_pythonpath = env.get("PYTHONPATH", "")
pythonpath_parts = [str(REPO_ROOT)]
user_site = site.getusersitepackages()
if user_site:
pythonpath_parts.append(user_site)
if existing_pythonpath:
pythonpath_parts.append(existing_pythonpath)
env["PYTHONPATH"] = os.pathsep.join(pythonpath_parts)
env["HOME"] = str(home)
env["USERPROFILE"] = str(home)
env["OPENSRE_NO_TELEMETRY"] = "1"
env["OPENSRE_PROJECT_ENV_PATH"] = str(project_env_path)
env["PYTHONUTF8"] = "1"
env["PYTHONIOENCODING"] = "utf-8"
env["TERM"] = "xterm-256color"
env.pop("OPENSRE_DISABLE_KEYRING", None)
env["PYTHON_KEYRING_BACKEND"] = "tests.shared.keyring_backend.MemoryKeyring"
return env
@pytest.fixture()
def cli_sandbox(tmp_path: Path) -> CliSandbox:
home = tmp_path / "home"
home.mkdir()
project_env_path = tmp_path / "project.env"
return CliSandbox(
home=home,
project_env_path=project_env_path,
env=_cli_env(home, project_env_path),
)
def _run_cli(
sandbox: CliSandbox,
*args: str,
timeout: float = 15.0,
extra_env: dict[str, str] | None = None,
) -> CliResult:
executable = _opensre_executable()
command = [str(executable), *args]
if executable.suffix != ".exe" and _is_python_script(executable):
command = [sys.executable, str(executable), *args]
env = sandbox.env.copy()
if extra_env:
env.update(extra_env)
completed = subprocess.run(
command,
cwd=REPO_ROOT,
env=env,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=timeout,
check=False,
)
return CliResult(
args=tuple(args),
exit_code=int(completed.returncode),
stdout=_clean_terminal_output(completed.stdout),
stderr=_clean_terminal_output(completed.stderr),
)
def _read_pty_chunk(master_fd: int, timeout: float) -> bytes:
ready, _, _ = select.select([master_fd], [], [], timeout)
if not ready:
return b""
try:
return os.read(master_fd, 4096)
except OSError as exc:
if exc.errno == errno.EIO:
return b""
raise
def _wait_for_output(
process: subprocess.Popen[bytes],
master_fd: int,
buffer: bytearray,
expected: str | tuple[str, ...],
*,
timeout: float,
) -> None:
def _matches(cleaned: str) -> bool:
if isinstance(expected, str):
return expected in cleaned
return any(sub in cleaned for sub in expected)
deadline = time.monotonic() + timeout
while not _matches(_clean_terminal_output(buffer.decode("utf-8", errors="replace"))):
if time.monotonic() > deadline:
cleaned = _clean_terminal_output(buffer.decode("utf-8", errors="replace"))
raise AssertionError(f"Timed out waiting for {expected!r}.\nCurrent output:\n{cleaned}")
chunk = _read_pty_chunk(master_fd, 0.1)
if chunk:
buffer.extend(chunk)
continue
if process.poll() is not None:
break
cleaned = _clean_terminal_output(buffer.decode("utf-8", errors="replace"))
if not _matches(cleaned):
raise AssertionError(
f"Process exited before showing {expected!r}.\nCurrent output:\n{cleaned}"
)
def _run_cli_pty(
sandbox: CliSandbox,
*args: str,
actions: list[PtyAction],
timeout: float = 20.0,
extra_env: dict[str, str] | None = None,
) -> CliResult:
executable = _opensre_executable()
command = [str(executable), *args]
if executable.suffix != ".exe" and _is_python_script(executable):
command = [sys.executable, str(executable), *args]
master_fd, slave_fd = os.openpty()
env = sandbox.env.copy()
if extra_env:
env.update(extra_env)
process = subprocess.Popen(
command,
cwd=REPO_ROOT,
env=env,
stdin=slave_fd,
stdout=slave_fd,
stderr=slave_fd,
close_fds=True,
)
os.close(slave_fd)
buffer = bytearray()
try:
for action in actions:
_wait_for_output(process, master_fd, buffer, action.expect, timeout=action.timeout)
if action.stagger_j:
for _ in range(action.stagger_j):
os.write(master_fd, action.stagger_key)
time.sleep(0.05)
os.write(master_fd, action.send)
deadline = time.monotonic() + timeout
while True:
chunk = _read_pty_chunk(master_fd, 0.1)
if chunk:
buffer.extend(chunk)
continue
if process.poll() is not None:
break
if time.monotonic() > deadline:
process.kill()
cleaned = _clean_terminal_output(buffer.decode("utf-8", errors="replace"))
raise AssertionError(f"Timed out waiting for CLI exit.\nCurrent output:\n{cleaned}")
for _ in range(5):
chunk = _read_pty_chunk(master_fd, 0.05)
if not chunk:
break
buffer.extend(chunk)
finally:
os.close(master_fd)
return CliResult(
args=tuple(args),
exit_code=int(process.wait(timeout=2.0)),
stdout=_clean_terminal_output(buffer.decode("utf-8", errors="replace")),
)
class _ReleaseHandler(BaseHTTPRequestHandler):
def do_GET(self) -> None:
payload = json.dumps(
{
"tag_name": "main-build",
"body": "- Version: `9999.0.0`\n- Commit: `deadbeef`\n",
}
).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def log_message(self, _format: str, *_args: object) -> None:
return
@pytest.fixture()
def release_api_url() -> str:
try:
server = ThreadingHTTPServer(("127.0.0.1", 0), _ReleaseHandler)
except OSError as exc:
if exc.errno in {errno.EPERM, errno.EACCES}:
pytest.skip("localhost HTTP server binding is not permitted in this environment")
raise
thread = Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
yield f"http://127.0.0.1:{server.server_port}/releases/tags/main-build"
finally:
server.shutdown()
thread.join(timeout=5.0)
server.server_close()
def test_opensre_landing_page_smoke(cli_sandbox: CliSandbox) -> None:
result = _run_cli(cli_sandbox)
assert result.exit_code == 0
assert "Quick start:" in result.stdout
assert "opensre investigate -i alert.json" in result.stdout
def test_opensre_help_smoke(cli_sandbox: CliSandbox) -> None:
result = _run_cli(cli_sandbox, "-h")
assert result.exit_code == 0
assert "Welcome back" not in result.stdout
assert "Commands:" in result.stdout
assert "integrations" in result.stdout
assert "--interactive / --no-interactive" in result.stdout
assert "--layout [classic|pinned]" in result.stdout
assert "update" in result.stdout
def test_opensre_version_smoke(cli_sandbox: CliSandbox) -> None:
result = _run_cli(cli_sandbox, "--version")
assert result.exit_code == 0
assert get_opensre_version() in result.stdout
def test_health_smoke_uses_real_datadog_store_config(cli_sandbox: CliSandbox) -> None:
cli_sandbox.seed_integrations(
[
{
"id": "datadog-local",
"service": "datadog",
"status": "active",
"credentials": {
"api_key": "",
"app_key": "",
"site": "datadoghq.com",
},
}
]
)
result = _run_cli(cli_sandbox, "health")
assert result.exit_code == 0
assert "OpenSRE Health" in result.stdout
assert "datadog" in result.stdout
assert "Missing API key or application key." in result.stdout
def test_update_check_smoke_uses_local_stub(cli_sandbox: CliSandbox, release_api_url: str) -> None:
result = _run_cli(
cli_sandbox,
"update",
"--check",
extra_env={"OPENSRE_RELEASES_API_URL": release_api_url},
)
assert result.exit_code == 1
assert "current:" in result.stdout
assert "latest:" in result.stdout
assert "9999.0.0" in result.stdout
def test_investigate_print_template_smoke(cli_sandbox: CliSandbox) -> None:
result = _run_cli(cli_sandbox, "investigate", "--print-template", "generic")
assert result.exit_code == 0
payload = json.loads(result.stdout)
assert payload["alert_source"] == "generic"
assert payload["message"]
def test_investigate_onboard_handoff_smoke(cli_sandbox: CliSandbox, tmp_path: Path) -> None:
"""project.env written by onboard is read by investigate before it reaches the LLM.
Only the project .env handoff is exercised here — investigate reads
LLM_PROVIDER and model env vars from that file, not from the wizard store.
No real API key is required. The test proves the plumbing works by asserting
the CLI reaches the LLM credential check (exit 1 + ANTHROPIC_API_KEY named
in the error) rather than crashing in config loading or file parsing.
LLM_PROVIDER is passed explicitly via extra_env so the assertion holds even
on CI runners where the variable is set to a different provider in the
parent environment.
"""
cli_sandbox.seed_project_env(provider="anthropic", model="claude-opus-4-7")
alert_path = tmp_path / "alert.json"
alert_path.write_text(
json.dumps(
{
"alert_name": "High CPU on orders-rds-prod",
"pipeline_name": "orders",
"severity": "critical",
"message": "CPU utilisation exceeded 90% for 5 minutes.",
}
),
encoding="utf-8",
)
result = _run_cli(
cli_sandbox,
"investigate",
"-i",
str(alert_path),
extra_env={"LLM_PROVIDER": "anthropic"},
)
# Exit 1 is expected — no real API key in CI.
assert result.exit_code == 1
# The failure must name the missing credential specifically — not a generic
# "run opensre onboard" fallback that would also appear when config loading
# itself is broken (the scenario this test is designed to catch).
combined = result.stdout + result.stderr
assert "ANTHROPIC_API_KEY" in combined, (
f"Expected a missing-credential error naming ANTHROPIC_API_KEY, got:\n{combined}"
)
def test_integrations_list_and_show_smoke(cli_sandbox: CliSandbox) -> None:
cli_sandbox.seed_integrations(
[
{
"id": "datadog-local",
"service": "datadog",
"status": "active",
"credentials": {
"api_key": "dd-api-key",
"app_key": "dd-app-key",
"site": "datadoghq.com",
},
}
]
)
list_result = _run_cli(cli_sandbox, "integrations", "list")
show_result = _run_cli(cli_sandbox, "integrations", "show", "datadog")
assert list_result.exit_code == 0
assert "datadog" in list_result.stdout
assert "datadog-local" in list_result.stdout
assert show_result.exit_code == 0
assert '"service": "datadog"' in show_result.stdout
assert '"api_key": "dd-a****"' in show_result.stdout
assert '"app_key": "dd-a****"' in show_result.stdout
def test_integrations_verify_datadog_smoke(cli_sandbox: CliSandbox) -> None:
cli_sandbox.seed_integrations(
[
{
"id": "datadog-local",
"service": "datadog",
"status": "active",
"credentials": {
"api_key": "",
"app_key": "",
"site": "datadoghq.com",
},
}
]
)
result = _run_cli(cli_sandbox, "integrations", "verify", "datadog")
assert result.exit_code == 1
assert "datadog" in result.stdout
assert "Missing API key or application key." in result.stdout
def test_tests_inventory_commands_smoke(cli_sandbox: CliSandbox) -> None:
list_result = _run_cli(cli_sandbox, "tests", "list", "--category", "ci-safe")
run_result = _run_cli(cli_sandbox, "tests", "run", "make:test-cov", "--dry-run")
assert list_result.exit_code == 0
assert "make:test-cov" in list_result.stdout
assert "make:test-full" in list_result.stdout
assert run_result.exit_code == 0
assert "make test-cov" in run_result.stdout
@pytest.mark.skipif(os.name == "nt", reason="interactive smoke uses POSIX PTYs")
def test_onboard_interactive_smoke(cli_sandbox: CliSandbox) -> None:
# The select list wraps, and "Skip for now" is always the last option. A single
# vim-up (`k`) keypress from the first option wraps to it, so this stays correct
# regardless of how many integrations the picker lists.
result = _run_cli_pty(
cli_sandbox,
"onboard",
actions=[
PtyAction(expect="How do you want to get started?", send=b"\r"),
PtyAction(expect="Choose your LLM provider", send=b"\r"),
PtyAction(
expect="Choose Anthropic auth method",
send=b"\r",
stagger_j=1,
),
PtyAction(expect="Anthropic API key", send=b"smoke-test-key\r"),
PtyAction(expect="Choose Anthropic model", send=b"\r"),
PtyAction(
expect="Choose an integration to configure",
send=b"\r",
stagger_j=1,
stagger_key=b"k",
),
],
timeout=30.0,
extra_env={"OPENSRE_AUTO_LAUNCH": "0"},
)
assert result.exit_code == 0
assert "Done." in result.stdout
assert "next" in result.stdout
store = cli_sandbox.read_wizard_store()
assert store["targets"]["local"]["provider"] == "anthropic"
assert "api_key" not in store["targets"]["local"]
assert "LLM_PROVIDER=anthropic" in cli_sandbox.read_project_env()
assert "ANTHROPIC_API_KEY=" not in cli_sandbox.read_project_env()
assert "ANTHROPIC_REASONING_MODEL=" in cli_sandbox.read_project_env()
@pytest.mark.parametrize(
("_cli_binary", "provider_key", "provider_label", "uses_oauth", "pty_timeout"),
[
pytest.param(
"codex",
"openai",
"OpenAI OAuth",
True,
60.0,
marks=pytest.mark.skipif(
shutil.which("codex") is None,
reason="OpenAI Codex CLI not on PATH",
),
),
pytest.param(
"opencode",
"opencode",
"OpenCode CLI",
False,
120.0,
marks=pytest.mark.skipif(
shutil.which("opencode") is None,
reason="OpenCode CLI not on PATH",
),
),
],
)
@pytest.mark.skipif(os.name == "nt", reason="interactive smoke uses POSIX PTYs")
def test_onboard_interactive_smoke_cli_provider_repick_when_unauthenticated(
cli_sandbox: CliSandbox,
_cli_binary: str,
provider_key: str,
provider_label: str,
uses_oauth: bool,
pty_timeout: float,
) -> None:
"""PTY: quickstart → local CLI LLM → repick when unauthenticated, then finish as Anthropic.
Navigates from the default provider using the current runtime provider list order.
Fresh HOME has no CLI auth, so either ``requires login`` or ``Could not verify … login``
is accepted before choosing repick. Skips when the CLI binary for each parametrized
case is not on PATH.
"""
from surfaces.cli.wizard.flow import _onboarding_provider_options
stagger_j = next(
(
i
for i, provider in enumerate(_onboarding_provider_options())
if provider.value == provider_key
),
-1,
)
assert stagger_j >= 0, f"Provider '{provider_key}' missing from onboarding providers"
login_prompt: tuple[str, ...] = (
f"{provider_label} requires login. What next?",
f"Could not verify {provider_label} login. What next?",
)
model_prompt: tuple[str, ...] = (
f"Choose {provider_label} model",
"Model",
)
actions = [
PtyAction(expect="How do you want to get started?", send=b"\r"),
PtyAction(expect="Choose your LLM provider", send=b"\r", stagger_j=stagger_j),
]
if uses_oauth:
actions.append(PtyAction(expect="Choose OpenAI auth method", send=b"\r"))
actions.extend(
[
PtyAction(expect=model_prompt, send=b"\r", timeout=30.0),
PtyAction(
expect=login_prompt,
send=b"\r",
stagger_j=2 if uses_oauth else 1,
timeout=90.0,
),
PtyAction(expect="Choose your LLM provider", send=b"\r"),
PtyAction(
expect="Choose Anthropic auth method",
send=b"\r",
stagger_j=1,
),
PtyAction(expect="Anthropic API key", send=b"smoke-test-key\r"),
PtyAction(expect="Choose Anthropic model", send=b"\r"),
PtyAction(
expect="Choose an integration to configure",
send=b"\r",
stagger_j=1,
stagger_key=b"k",
),
]
)
try:
result = _run_cli_pty(
cli_sandbox,
"onboard",
actions=actions,
timeout=pty_timeout,
extra_env={
"OPENSRE_AUTO_LAUNCH": "0",
"OPENAI_API_KEY": "",
"OPENAI_ORG_ID": "",
"OPENAI_PROJECT_ID": "",
"OPENAI_BASE_URL": "",
},
)
except AssertionError as exc:
msg = str(exc)
if (
_cli_binary == "codex"
and "Choose OpenAI OAuth model" in msg
and "requires login" in msg
):
pytest.skip(
"OpenAI Codex CLI appears already authenticated; unauth repick flow skipped"
)
if (
_cli_binary == "opencode"
and "environment provider key(s)" in msg
and "OpenCode:" in msg
):
pytest.skip("OpenCode CLI is already authenticated via env; unauth repick flow skipped")
raise
assert result.exit_code == 0
assert "Done." in result.stdout
assert "next" in result.stdout
store = cli_sandbox.read_wizard_store()
assert store["targets"]["local"]["provider"] == "anthropic"
assert "api_key" not in store["targets"]["local"]
env_body = cli_sandbox.read_project_env()
assert "LLM_PROVIDER=anthropic\n" in env_body
assert "ANTHROPIC_API_KEY=" not in env_body
assert "ANTHROPIC_REASONING_MODEL=" in env_body
@pytest.mark.skipif(os.name == "nt", reason="interactive smoke uses POSIX PTYs")
def test_integrations_setup_datadog_interactive_smoke(cli_sandbox: CliSandbox) -> None:
result = _run_cli_pty(
cli_sandbox,
"integrations",
"setup",
"datadog",
actions=[
PtyAction(expect="API key", send=b"dd-api-key\r"),
PtyAction(expect="Application key", send=b"dd-app-key\r"),
PtyAction(expect="Site", send=b"\r"),
],
# Setup runs verify against the Datadog API; CI runners can exceed 20s.
timeout=45.0,
)
assert "Saved" in result.stdout
# Setup saves credentials then runs verify; placeholder keys fail the Datadog API check.
assert result.exit_code in (0, 1)
integrations = cli_sandbox.read_integrations()
assert len(integrations) == 1
assert integrations[0]["service"] == "datadog"
# v2 store shape: credentials live inside the default instance.
assert integrations[0]["instances"][0]["credentials"]["site"] == "datadoghq.com"
@pytest.mark.skipif(os.name == "nt", reason="interactive smoke uses POSIX PTYs")
def test_integrations_remove_datadog_interactive_smoke(cli_sandbox: CliSandbox) -> None:
cli_sandbox.seed_integrations(
[
{
"id": "datadog-local",
"service": "datadog",
"status": "active",
"credentials": {
"api_key": "dd-api-key",
"app_key": "dd-app-key",
"site": "datadoghq.com",
},
}
]
)
result = _run_cli_pty(
cli_sandbox,
"integrations",
"remove",
"datadog",
actions=[PtyAction(expect="Remove 'datadog'?", send=b"y\r")],
)
assert result.exit_code == 0
assert "Removed 'datadog'." in result.stdout
assert cli_sandbox.read_integrations() == []
@pytest.mark.skipif(os.name == "nt", reason="interactive smoke uses POSIX PTYs")
def test_tests_interactive_launcher_smoke(cli_sandbox: CliSandbox) -> None:
# The prompt instruction reads "Esc exit"; Escape is the PTY-safe way to
# dismiss the prompt in automation (no SIGINT/raw-mode race conditions).
result = _run_cli_pty(
cli_sandbox,
"tests",
actions=[PtyAction(expect="Choose a test category:", send=b"\x1b")],
)
assert result.exit_code == 0
assert "Choose a test category:" in result.stdout
def test_gateway_help_smoke(cli_sandbox: CliSandbox) -> None:
result = _run_cli(cli_sandbox, "gateway", "-h")
assert result.exit_code == 0
assert "telegram" in result.stdout