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

2372 lines
87 KiB
Python

from __future__ import annotations
import json
import os
from unittest.mock import MagicMock
import pytest
from integrations.llm_cli.codex_oauth import CodexOAuthResult
from surfaces.cli.wizard import _integration_configurators, _ui, flow
from surfaces.cli.wizard import store as wizard_store
from surfaces.cli.wizard.env_sync import sync_provider_env
from surfaces.cli.wizard.probes import ProbeResult
from tests.integrations.llm_cli.testing_helpers import write_fake_runnable_cli_bin
@pytest.fixture(autouse=True)
def _stub_managed_llm_secret_persistence(monkeypatch: pytest.MonkeyPatch) -> None:
"""Wizard flow tests should not touch the developer's real keychain."""
monkeypatch.setattr(_ui, "save_api_key", lambda *_args, **_kwargs: None)
def test_run_wizard_advanced_remote_falls_back_to_local(monkeypatch, tmp_path, capsys) -> None:
# advanced -> falls back to local -> change provider? Yes -> pick anthropic -> skip integrations
select_responses = iter(
["advanced", "remote", "anthropic", "api_key", "claude-opus-4-7", "skip"]
)
confirm_responses = iter([True, True]) # "use local instead?" and "Change provider?"
def _mock_select(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(select_responses)
return m
def _mock_confirm(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(confirm_responses)
return m
def _mock_password(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = "secret-key"
return m
saved: dict[str, object] = {}
saved_llm_keys: list[tuple[str, str]] = []
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(flow.questionary, "confirm", _mock_confirm)
monkeypatch.setattr(flow.questionary, "password", _mock_password)
monkeypatch.setattr(_ui, "get_store_path", lambda: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(
flow, "probe_remote_target", lambda: ProbeResult("remote", True, "remote ok")
)
def _save_local_config(**kwargs):
saved.update(kwargs)
return tmp_path / "opensre.json"
monkeypatch.setattr(flow, "save_local_config", _save_local_config)
monkeypatch.setattr(flow, "sync_provider_env", lambda **_kwargs: tmp_path / ".env")
monkeypatch.setattr(
_ui,
"save_api_key",
lambda provider, value, **_kwargs: saved_llm_keys.append((provider, value)),
)
exit_code = flow.run_wizard()
assert exit_code == 0
assert saved["wizard_mode"] == "advanced"
assert saved["provider"] == "anthropic"
assert "api_key" not in saved
assert saved_llm_keys == [("anthropic", "secret-key")]
output = capsys.readouterr().out
assert "Summary" in output
assert "4/4" in output
assert "next" in output
assert "Done." in output
assert output.index("Summary") < output.index("Done.")
def test_prompt_value_can_signal_wizard_back(monkeypatch) -> None:
def _mock_password(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = None
return m
monkeypatch.setattr(_ui.questionary, "password", _mock_password)
with pytest.raises(_ui.WizardBack):
_ui._prompt_value("OpenAI API key", secret=True, back_on_cancel=True)
def test_run_wizard_no_saved_provider_shows_selection(monkeypatch, tmp_path) -> None:
"""With no saved config the provider list is shown immediately (no confirm prompt)."""
select_responses = iter(["quickstart", "anthropic", "api_key", "claude-opus-4-7", "skip"])
def _mock_select(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(select_responses)
return m
def _mock_password(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = "secret-key"
return m
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(flow.questionary, "password", _mock_password)
monkeypatch.setattr(_ui, "get_store_path", lambda: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(flow, "save_local_config", lambda **_kwargs: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "sync_provider_env", lambda **_kwargs: tmp_path / ".env")
monkeypatch.setattr(_ui, "save_llm_api_key", lambda *_args, **_kwargs: None)
exit_code = flow.run_wizard()
assert exit_code == 0
def test_run_wizard_shows_keyring_fix_steps_when_secure_storage_is_unavailable(
monkeypatch, tmp_path, capsys
) -> None:
select_responses = iter(["quickstart", "anthropic", "api_key", "claude-opus-4-7"])
def _mock_select(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(select_responses)
return m
def _mock_password(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = "secret-key"
return m
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(flow.questionary, "password", _mock_password)
monkeypatch.setattr(_ui, "get_store_path", lambda: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(
_ui,
"save_api_key",
lambda *_args, **_kwargs: (_ for _ in ()).throw(
RuntimeError("Secure local credential storage is unavailable on this machine.")
),
)
monkeypatch.setattr(
_ui,
"get_keyring_setup_instructions",
lambda _env_var: (
"Current keyring backend: keyring.backends.fail.Keyring.",
"Install it first: sudo apt update && sudo apt install -y gnome-keyring dbus-user-session",
"Start a D-Bus shell: dbus-run-session -- sh",
),
)
exit_code = flow.run_wizard()
assert exit_code == 1
output = capsys.readouterr().out
assert "OpenSRE could not save your API key to the local system keychain." in output
assert "Install it first: sudo apt update && sudo apt install -y gnome-keyring" in output
assert "dbus-user-session" in output
assert "Start a D-Bus shell: dbus-run-session -- sh" in output
def test_run_wizard_configures_optional_integrations(monkeypatch, tmp_path, capsys) -> None:
select_responses = iter(["quickstart", "anthropic", "api_key", "claude-opus-4-7", "grafana"])
saved_integrations: list[tuple[str, dict]] = []
synced_env_values: list[dict[str, str]] = []
def _mock_select(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(select_responses)
return m
password_responses = iter(
[
"llm-secret",
"grafana-token",
]
)
text_responses = iter(["https://grafana.example.com"])
def _mock_password(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(password_responses)
return m
def _mock_text(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(text_responses)
return m
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(flow.questionary, "password", _mock_password)
monkeypatch.setattr(flow.questionary, "text", _mock_text)
monkeypatch.setattr(_ui, "get_store_path", lambda: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(
_integration_configurators,
"validate_grafana_integration",
lambda **_kwargs: flow.IntegrationHealthResult(ok=True, detail="Grafana ok"),
)
monkeypatch.setattr(
_integration_configurators,
"validate_slack_webhook",
lambda **_kwargs: flow.IntegrationHealthResult(ok=True, detail="Slack ok"),
)
monkeypatch.setattr(flow, "save_local_config", lambda **_kwargs: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "sync_provider_env", lambda **_kwargs: tmp_path / ".env")
monkeypatch.setattr(_ui, "save_llm_api_key", lambda *_args, **_kwargs: None)
def _sync_env_values(values: dict[str, str], **_kwargs):
synced_env_values.append(values)
return tmp_path / ".env"
monkeypatch.setattr(_integration_configurators, "sync_env_values", _sync_env_values)
monkeypatch.setattr(
_integration_configurators,
"upsert_integration",
lambda service, payload: saved_integrations.append((service, payload)),
)
exit_code = flow.run_wizard()
assert exit_code == 0
assert saved_integrations == [
(
"grafana",
{
"credentials": {
"endpoint": "https://grafana.example.com",
"api_key": "grafana-token",
}
},
)
]
assert synced_env_values == [
{
"GRAFANA_INSTANCE_URL": "https://grafana.example.com",
},
]
output = capsys.readouterr().out
assert "Grafana" in output
def test_run_wizard_configures_honeycomb(monkeypatch, tmp_path) -> None:
select_responses = iter(["quickstart", "anthropic", "api_key", "claude-opus-4-7", "honeycomb"])
password_responses = iter(["llm-secret", "hny_test"])
text_responses = iter(["prod-api", "https://api.honeycomb.io"])
saved_integrations: list[tuple[str, dict]] = []
synced_env_values: list[dict[str, str]] = []
def _mock_select(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(select_responses)
return m
def _mock_password(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(password_responses)
return m
def _mock_text(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(text_responses)
return m
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(flow.questionary, "password", _mock_password)
monkeypatch.setattr(flow.questionary, "text", _mock_text)
monkeypatch.setattr(_ui, "get_store_path", lambda: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(
_integration_configurators,
"validate_honeycomb_integration",
lambda **_kwargs: flow.IntegrationHealthResult(ok=True, detail="Honeycomb ok"),
)
monkeypatch.setattr(flow, "save_local_config", lambda **_kwargs: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "sync_provider_env", lambda **_kwargs: tmp_path / ".env")
monkeypatch.setattr(_ui, "save_llm_api_key", lambda *_args, **_kwargs: None)
def _sync_env_values(values: dict[str, str], **_kwargs):
synced_env_values.append(values)
return tmp_path / ".env"
monkeypatch.setattr(_integration_configurators, "sync_env_values", _sync_env_values)
monkeypatch.setattr(
_integration_configurators,
"upsert_integration",
lambda service, payload: saved_integrations.append((service, payload)),
)
exit_code = flow.run_wizard()
assert exit_code == 0
assert saved_integrations == [
(
"honeycomb",
{
"credentials": {
"api_key": "hny_test",
"dataset": "prod-api",
"base_url": "https://api.honeycomb.io",
}
},
)
]
assert synced_env_values == [
{
"HONEYCOMB_DATASET": "prod-api",
"HONEYCOMB_API_URL": "https://api.honeycomb.io",
}
]
def test_run_wizard_configures_coralogix(monkeypatch, tmp_path) -> None:
select_responses = iter(["quickstart", "anthropic", "api_key", "claude-opus-4-7", "coralogix"])
password_responses = iter(["llm-secret", "cx_test"])
text_responses = iter(
[
"https://api.coralogix.com",
"payments",
"worker",
]
)
saved_integrations: list[tuple[str, dict]] = []
synced_env_values: list[dict[str, str]] = []
def _mock_select(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(select_responses)
return m
def _mock_password(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(password_responses)
return m
def _mock_text(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(text_responses)
return m
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(flow.questionary, "password", _mock_password)
monkeypatch.setattr(flow.questionary, "text", _mock_text)
monkeypatch.setattr(_ui, "get_store_path", lambda: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(
_integration_configurators,
"validate_coralogix_integration",
lambda **_kwargs: flow.IntegrationHealthResult(ok=True, detail="Coralogix ok"),
)
monkeypatch.setattr(flow, "save_local_config", lambda **_kwargs: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "sync_provider_env", lambda **_kwargs: tmp_path / ".env")
monkeypatch.setattr(_ui, "save_llm_api_key", lambda *_args, **_kwargs: None)
def _sync_env_values(values: dict[str, str], **_kwargs):
synced_env_values.append(values)
return tmp_path / ".env"
monkeypatch.setattr(_integration_configurators, "sync_env_values", _sync_env_values)
monkeypatch.setattr(
_integration_configurators,
"upsert_integration",
lambda service, payload: saved_integrations.append((service, payload)),
)
exit_code = flow.run_wizard()
assert exit_code == 0
assert saved_integrations == [
(
"coralogix",
{
"credentials": {
"api_key": "cx_test",
"base_url": "https://api.coralogix.com",
"application_name": "payments",
"subsystem_name": "worker",
}
},
)
]
assert synced_env_values == [
{
"CORALOGIX_API_URL": "https://api.coralogix.com",
"CORALOGIX_APPLICATION_NAME": "payments",
"CORALOGIX_SUBSYSTEM_NAME": "worker",
}
]
def test_run_wizard_configures_dagster(monkeypatch, tmp_path) -> None:
select_responses = iter(["quickstart", "anthropic", "api_key", "claude-opus-4-7", "dagster"])
password_responses = iter(["llm-secret", "dag_test_token"])
text_responses = iter(["http://localhost:3000"])
saved_integrations: list[tuple[str, dict]] = []
synced_env_values: list[dict[str, str]] = []
synced_secrets: list[tuple[str, str]] = []
def _mock_select(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(select_responses)
return m
def _mock_password(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(password_responses)
return m
def _mock_text(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(text_responses)
return m
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(flow.questionary, "password", _mock_password)
monkeypatch.setattr(flow.questionary, "text", _mock_text)
monkeypatch.setattr(_ui, "get_store_path", lambda: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(
_integration_configurators,
"validate_dagster_integration",
lambda **_kwargs: flow.IntegrationHealthResult(ok=True, detail="Dagster ok"),
)
monkeypatch.setattr(flow, "save_local_config", lambda **_kwargs: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "sync_provider_env", lambda **_kwargs: tmp_path / ".env")
monkeypatch.setattr(_ui, "save_llm_api_key", lambda *_args, **_kwargs: None)
def _sync_env_values(values: dict[str, str], **_kwargs):
synced_env_values.append(values)
return tmp_path / ".env"
monkeypatch.setattr(_integration_configurators, "sync_env_values", _sync_env_values)
monkeypatch.setattr(
_integration_configurators,
"sync_env_secret",
lambda key, value: synced_secrets.append((key, value)),
)
monkeypatch.setattr(
_integration_configurators,
"upsert_integration",
lambda service, payload: saved_integrations.append((service, payload)),
)
exit_code = flow.run_wizard()
assert exit_code == 0
assert saved_integrations == [
(
"dagster",
{
"credentials": {
"endpoint": "http://localhost:3000",
"api_token": "dag_test_token",
}
},
)
]
assert synced_env_values == [{"DAGSTER_ENDPOINT": "http://localhost:3000"}]
assert synced_secrets == [("DAGSTER_API_TOKEN", "dag_test_token")]
def test_run_wizard_configures_dagster_oss_skips_secret(monkeypatch, tmp_path) -> None:
"""OSS path: empty api_token must not call sync_env_secret."""
select_responses = iter(["quickstart", "anthropic", "api_key", "claude-opus-4-7", "dagster"])
password_responses = iter(["llm-secret", ""])
text_responses = iter(["http://localhost:3000"])
synced_env_values: list[dict[str, str]] = []
synced_secrets: list[tuple[str, str]] = []
def _mock_select(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(select_responses)
return m
def _mock_password(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(password_responses)
return m
def _mock_text(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(text_responses)
return m
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(flow.questionary, "password", _mock_password)
monkeypatch.setattr(flow.questionary, "text", _mock_text)
monkeypatch.setattr(_ui, "get_store_path", lambda: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(
_integration_configurators,
"validate_dagster_integration",
lambda **_kwargs: flow.IntegrationHealthResult(ok=True, detail="Dagster ok"),
)
monkeypatch.setattr(flow, "save_local_config", lambda **_kwargs: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "sync_provider_env", lambda **_kwargs: tmp_path / ".env")
monkeypatch.setattr(_ui, "save_llm_api_key", lambda *_args, **_kwargs: None)
monkeypatch.setattr(
_integration_configurators,
"sync_env_values",
lambda values, **_kwargs: synced_env_values.append(values) or (tmp_path / ".env"),
)
monkeypatch.setattr(
_integration_configurators,
"sync_env_secret",
lambda key, value: synced_secrets.append((key, value)),
)
monkeypatch.setattr(
_integration_configurators, "upsert_integration", lambda *_args, **_kwargs: None
)
exit_code = flow.run_wizard()
assert exit_code == 0
assert synced_env_values == [{"DAGSTER_ENDPOINT": "http://localhost:3000"}]
assert synced_secrets == [] # OSS path: no token
def test_run_wizard_configures_slack_persists_webhook(monkeypatch, tmp_path) -> None:
"""Regression: the wizard must upsert the Slack webhook to the store.
Previously `_configure_slack` validated the webhook and reported "Slack" in
the success summary but never called `upsert_integration`, silently
discarding the webhook (the catalog reads Slack store-first, so nothing was
readable afterwards). The webhook is a secret, so it belongs in the store,
not `.env` — `sync_env_values` is called with an empty mapping.
"""
select_responses = iter(["quickstart", "anthropic", "api_key", "claude-opus-4-7", "slack"])
# webhook_url is prompted with secret=True, so it comes from the password mock.
password_responses = iter(["llm-secret", "https://hooks.slack.com/services/T0/B0/XXXXX"])
saved_integrations: list[tuple[str, dict]] = []
synced_env_values: list[dict[str, str]] = []
def _mock_select(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(select_responses)
return m
def _mock_password(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(password_responses)
return m
def _mock_text(*_args, **_kwargs):
# Slack has no plain-text prompt; guard against an unexpected one.
m = MagicMock()
m.ask.return_value = ""
return m
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(flow.questionary, "password", _mock_password)
monkeypatch.setattr(flow.questionary, "text", _mock_text)
monkeypatch.setattr(_ui, "get_store_path", lambda: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(
_integration_configurators,
"validate_slack_webhook",
lambda **_kwargs: flow.IntegrationHealthResult(ok=True, detail="Slack ok"),
)
monkeypatch.setattr(flow, "save_local_config", lambda **_kwargs: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "sync_provider_env", lambda **_kwargs: tmp_path / ".env")
monkeypatch.setattr(_ui, "save_llm_api_key", lambda *_args, **_kwargs: None)
def _sync_env_values(values: dict[str, str], **_kwargs):
synced_env_values.append(values)
return tmp_path / ".env"
monkeypatch.setattr(_integration_configurators, "sync_env_values", _sync_env_values)
monkeypatch.setattr(
_integration_configurators,
"upsert_integration",
lambda service, payload: saved_integrations.append((service, payload)),
)
exit_code = flow.run_wizard()
assert exit_code == 0
assert saved_integrations == [
(
"slack",
{"credentials": {"webhook_url": "https://hooks.slack.com/services/T0/B0/XXXXX"}},
)
]
# Webhook is a secret: it goes to the store, not `.env`.
assert synced_env_values == [{}]
def test_run_wizard_dagster_retries_on_validation_failure(monkeypatch, tmp_path) -> None:
"""Two consecutive Dagster validation failures, then success.
Proves the retry loop recovers from N consecutive failures (not just one),
and that only the final successful attempt reaches the persistence layer.
"""
select_responses = iter(["quickstart", "anthropic", "api_key", "claude-opus-4-7", "dagster"])
# Two wrong tokens, then the correct one.
password_responses = iter(["llm-secret", "bad_token_1", "bad_token_2", "dag_good"])
# endpoint is prompted on each retry (three attempts total).
text_responses = iter(
["http://localhost:3000", "http://localhost:3000", "http://localhost:3000"]
)
saved_integrations: list[tuple[str, dict]] = []
synced_env_values: list[dict[str, str]] = []
synced_secrets: list[tuple[str, str]] = []
validation_call_count = 0
def _mock_select(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(select_responses)
return m
def _mock_password(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(password_responses)
return m
def _mock_text(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(text_responses)
return m
def _validate_dagster(**_kwargs):
nonlocal validation_call_count
validation_call_count += 1
if validation_call_count < 3:
return flow.IntegrationHealthResult(
ok=False, detail="Dagster GraphQL probe failed: HTTP 401"
)
return flow.IntegrationHealthResult(ok=True, detail="Dagster ok")
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(flow.questionary, "password", _mock_password)
monkeypatch.setattr(flow.questionary, "text", _mock_text)
monkeypatch.setattr(_ui, "get_store_path", lambda: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(
_integration_configurators, "validate_dagster_integration", _validate_dagster
)
monkeypatch.setattr(flow, "save_local_config", lambda **_kwargs: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "sync_provider_env", lambda **_kwargs: tmp_path / ".env")
monkeypatch.setattr(_ui, "save_llm_api_key", lambda *_args, **_kwargs: None)
def _sync_env_values(values: dict[str, str], **_kwargs):
synced_env_values.append(values)
return tmp_path / ".env"
monkeypatch.setattr(_integration_configurators, "sync_env_values", _sync_env_values)
monkeypatch.setattr(
_integration_configurators,
"sync_env_secret",
lambda key, value: synced_secrets.append((key, value)),
)
monkeypatch.setattr(
_integration_configurators,
"upsert_integration",
lambda service, payload: saved_integrations.append((service, payload)),
)
exit_code = flow.run_wizard()
assert exit_code == 0
assert validation_call_count == 3 # two failures, then success
# Only the successful attempt should reach the persistence layer:
assert saved_integrations == [
(
"dagster",
{
"credentials": {
"endpoint": "http://localhost:3000",
"api_token": "dag_good",
}
},
)
]
assert synced_env_values == [{"DAGSTER_ENDPOINT": "http://localhost:3000"}]
assert synced_secrets == [("DAGSTER_API_TOKEN", "dag_good")]
def test_run_wizard_configures_github_mcp_and_sentry(monkeypatch, tmp_path, capsys) -> None:
select_responses = iter(
[
"quickstart",
"anthropic",
"api_key",
"claude-opus-4-7",
"github",
flow.DEFAULT_GITHUB_MCP_MODE,
"token",
"auto",
"any",
"summary",
]
)
text_responses = iter(
[
flow.DEFAULT_GITHUB_MCP_URL,
"repos,issues,pull_requests,actions,search",
]
)
password_responses = iter(
[
"llm-secret",
"ghp_test",
]
)
saved_integrations: list[tuple[str, dict]] = []
synced_env_values: list[dict[str, str]] = []
def _mock_select(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(select_responses)
return m
def _mock_password(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(password_responses)
return m
def _mock_text(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(text_responses)
return m
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(flow.questionary, "password", _mock_password)
monkeypatch.setattr(flow.questionary, "text", _mock_text)
monkeypatch.setattr(_ui, "get_store_path", lambda: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(
_integration_configurators,
"validate_github_mcp_integration",
lambda **_kwargs: flow.IntegrationHealthResult(ok=True, detail="GitHub MCP ok"),
)
monkeypatch.setattr(flow, "save_local_config", lambda **_kwargs: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "sync_provider_env", lambda **_kwargs: tmp_path / ".env")
monkeypatch.setattr(_ui, "save_llm_api_key", lambda *_args, **_kwargs: None)
def _sync_env_values(values: dict[str, str], **_kwargs):
synced_env_values.append(values)
return tmp_path / ".env"
monkeypatch.setattr(
_integration_configurators,
"sync_env_values",
_sync_env_values,
)
monkeypatch.setattr(
_integration_configurators,
"upsert_integration",
lambda service, payload: saved_integrations.append((service, payload)),
)
exit_code = flow.run_wizard()
assert exit_code == 0
assert saved_integrations == [
(
"github",
{
"credentials": {
"url": flow.DEFAULT_GITHUB_MCP_URL,
"mode": flow.DEFAULT_GITHUB_MCP_MODE,
"auth_token": "ghp_test",
"command": "",
"args": [],
"toolsets": ["repos", "issues", "pull_requests", "actions", "search"],
}
},
),
]
assert synced_env_values == [
{
"GITHUB_MCP_URL": flow.DEFAULT_GITHUB_MCP_URL,
"GITHUB_MCP_MODE": flow.DEFAULT_GITHUB_MCP_MODE,
"GITHUB_MCP_COMMAND": "",
"GITHUB_MCP_ARGS": "",
"GITHUB_MCP_TOOLSETS": "repos,issues,pull_requests,actions,search",
},
]
output = capsys.readouterr().out
assert "GitHub MCP" in output
def test_run_wizard_reuses_saved_defaults_when_user_keeps_provider(monkeypatch, tmp_path) -> None:
"""When provider is already set and user declines to change, saved values are reused."""
saved: dict[str, object] = {}
saved_llm_keys: list[tuple[str, str]] = []
def _mock_select(*_args, choices=None, default=None, **_kwargs):
m = MagicMock()
prompt = str(_args[0]) if _args else ""
selected_value = "skip" if "integration" in prompt.lower() else default
if choices is not None:
for choice in choices:
if getattr(choice, "title", None) == selected_value:
selected_value = choice.value
break
m.ask.return_value = selected_value
return m
def _mock_confirm(*_args, **_kwargs):
m = MagicMock()
# "Change provider?" -> No (keep saved openai)
m.ask.return_value = False
return m
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(flow.questionary, "confirm", _mock_confirm)
monkeypatch.setattr(_ui, "get_store_path", lambda: tmp_path / "opensre.json")
monkeypatch.setattr(
_ui,
"load_local_config",
lambda _path: {
"wizard": {"mode": "quickstart"},
"targets": {
"local": {
"provider": "openai",
"model": "gpt-5.4-mini",
"api_key_env": "OPENAI_API_KEY",
"api_key": "saved-secret",
}
},
},
)
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(_ui, "has_llm_api_key", lambda _env: False)
def _save_local_config(**kwargs):
saved.update(kwargs)
return tmp_path / "opensre.json"
monkeypatch.setattr(flow, "save_local_config", _save_local_config)
monkeypatch.setattr(flow, "sync_provider_env", lambda **_kwargs: tmp_path / ".env")
monkeypatch.setattr(
_ui,
"save_api_key",
lambda provider, value, **_kwargs: saved_llm_keys.append((provider, value)),
)
exit_code = flow.run_wizard()
assert exit_code == 0
assert saved["wizard_mode"] == "quickstart"
assert saved["provider"] == "openai"
assert saved["model"] == "gpt-5.4-mini"
assert "api_key" not in saved
assert saved_llm_keys == [("openai", "saved-secret")]
def test_run_wizard_changes_model_when_user_keeps_provider(monkeypatch, tmp_path) -> None:
"""When provider is kept but user opts to change model, the new choice is saved."""
saved: dict[str, object] = {}
confirm_prompts: list[str] = []
def _mock_select(*_args, choices=None, default=None, **_kwargs):
m = MagicMock()
prompt = str(_args[0]) if _args else ""
if "Choose your LLM provider" in prompt:
m.ask.return_value = "openai"
elif "Choose OpenAI model" in prompt:
m.ask.return_value = "gpt-5.4-mini"
else:
m.ask.return_value = default
return m
def _mock_confirm(*_args, **_kwargs):
m = MagicMock()
prompt = str(_args[0]) if _args else ""
confirm_prompts.append(prompt)
if "Change provider?" in prompt:
m.ask.return_value = False
elif "Change model?" in prompt:
m.ask.return_value = True
else:
m.ask.return_value = False
return m
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(flow.questionary, "confirm", _mock_confirm)
monkeypatch.setattr(_ui, "get_store_path", lambda: tmp_path / "opensre.json")
monkeypatch.setattr(
_ui,
"load_local_config",
lambda _path: {
"wizard": {"mode": "quickstart"},
"targets": {
"local": {
"provider": "openai",
"model": "gpt-5.4-mini",
"api_key_env": "OPENAI_API_KEY",
"api_key": "saved-secret",
}
},
},
)
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(_ui, "has_llm_api_key", lambda _env: True)
def _save_local_config(**kwargs):
saved.update(kwargs)
return tmp_path / "opensre.json"
monkeypatch.setattr(flow, "save_local_config", _save_local_config)
monkeypatch.setattr(flow, "sync_provider_env", lambda **_kwargs: tmp_path / ".env")
exit_code = flow.run_wizard()
assert exit_code == 0
assert saved["provider"] == "openai"
assert saved["model"] == "gpt-5.4-mini"
assert "Change model?" in confirm_prompts
def test_run_wizard_persists_matching_local_config_and_env(monkeypatch, tmp_path) -> None:
select_responses = iter(["quickstart", "openai", "api_key", "gpt-5.4-mini", "skip"])
saved_llm_keys: list[tuple[str, str]] = []
def _mock_select(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(select_responses)
return m
def _mock_password(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = "openai-secret"
return m
store_path = tmp_path / "opensre.json"
env_path = tmp_path / ".env"
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(flow.questionary, "password", _mock_password)
monkeypatch.setattr(_ui, "get_store_path", lambda: store_path)
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(
flow,
"save_local_config",
lambda **kwargs: wizard_store.save_local_config(path=store_path, **kwargs),
)
monkeypatch.setattr(
flow,
"sync_provider_env",
lambda **kwargs: sync_provider_env(env_path=env_path, **kwargs),
)
monkeypatch.setattr(
_ui,
"save_api_key",
lambda provider, value, **_kwargs: saved_llm_keys.append((provider, value)),
)
exit_code = flow.run_wizard()
assert exit_code == 0
payload = json.loads(store_path.read_text(encoding="utf-8"))
env_values = env_path.read_text(encoding="utf-8")
assert payload["wizard"]["mode"] == "quickstart"
assert payload["targets"]["local"]["provider"] == "openai"
assert payload["targets"]["local"]["api_key_env"] == "OPENAI_API_KEY"
assert payload["targets"]["local"]["model_env"] == "OPENAI_REASONING_MODEL"
assert "api_key" not in payload["targets"]["local"]
assert "LLM_PROVIDER=openai\n" in env_values
assert "OPENAI_API_KEY=" not in env_values
assert saved_llm_keys == [("openai", "openai-secret")]
def test_run_wizard_codex_skips_api_key_and_runs_cli_onboarding(monkeypatch, tmp_path) -> None:
select_responses = iter(["quickstart", "codex", "", "skip"])
saved_llm_keys: list[tuple[str, str]] = []
cli_onboarding_providers: list[str] = []
def _mock_select(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(select_responses)
return m
def _cli_onboarding(provider, **_kwargs):
cli_onboarding_providers.append(provider.value)
return "ok"
store_path = tmp_path / "opensre.json"
env_path = tmp_path / ".env"
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(_ui, "get_store_path", lambda: store_path)
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(flow, "_run_cli_llm_onboarding", _cli_onboarding)
monkeypatch.setattr(
flow,
"save_local_config",
lambda **kwargs: wizard_store.save_local_config(path=store_path, **kwargs),
)
monkeypatch.setattr(
flow,
"sync_provider_env",
lambda **kwargs: sync_provider_env(env_path=env_path, **kwargs),
)
monkeypatch.setattr(
_ui,
"save_api_key",
lambda provider, value, **_kwargs: saved_llm_keys.append((provider, value)),
)
exit_code = flow.run_wizard()
assert exit_code == 0
assert cli_onboarding_providers == ["codex"]
assert saved_llm_keys == []
payload = json.loads(store_path.read_text(encoding="utf-8"))
env_values = env_path.read_text(encoding="utf-8")
assert payload["targets"]["local"]["provider"] == "codex"
assert payload["targets"]["local"]["api_key_env"] == ""
assert payload["targets"]["local"]["model_env"] == "CODEX_MODEL"
assert "LLM_PROVIDER=codex\n" in env_values
assert "CODEX_MODEL=\n" in env_values
assert "LLM_AUTH_METHOD=oauth\n" in env_values
def test_run_wizard_openai_oauth_is_onboarding_auth_method(monkeypatch, tmp_path) -> None:
select_responses = iter(["quickstart", "openai", "oauth", "gpt-5.5", "skip"])
saved_llm_keys: list[tuple[str, str]] = []
cli_onboarding_providers: list[str] = []
saved_summary: dict[str, object] = {}
def _mock_select(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(select_responses)
return m
def _cli_onboarding(provider, **kwargs):
cli_onboarding_providers.append(provider.value)
assert kwargs["display_label"] == "OpenAI OAuth"
return "ok"
store_path = tmp_path / "opensre.json"
env_path = tmp_path / ".env"
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(_ui, "get_store_path", lambda: store_path)
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(flow, "_run_cli_llm_onboarding", _cli_onboarding)
monkeypatch.setattr(
flow,
"save_local_config",
lambda **kwargs: wizard_store.save_local_config(path=store_path, **kwargs),
)
monkeypatch.setattr(
flow,
"sync_provider_env",
lambda **kwargs: sync_provider_env(env_path=env_path, **kwargs),
)
monkeypatch.setattr(
_ui,
"save_api_key",
lambda provider, value, **_kwargs: saved_llm_keys.append((provider, value)),
)
monkeypatch.setattr(
flow, "_render_saved_summary", lambda **kwargs: saved_summary.update(kwargs)
)
exit_code = flow.run_wizard()
assert exit_code == 0
assert cli_onboarding_providers == ["codex"]
assert saved_llm_keys == []
payload = json.loads(store_path.read_text(encoding="utf-8"))
env_values = env_path.read_text(encoding="utf-8")
assert payload["targets"]["local"]["provider"] == "openai"
assert payload["targets"]["local"]["auth_method"] == "oauth"
assert payload["targets"]["local"]["api_key_env"] == "OPENAI_API_KEY"
assert payload["targets"]["local"]["model_env"] == "CODEX_MODEL"
assert payload["targets"]["local"]["model"] == "gpt-5.5"
assert "LLM_PROVIDER=openai\n" in env_values
assert "LLM_AUTH_METHOD=oauth\n" in env_values
assert "CODEX_MODEL=gpt-5.5\n" in env_values
assert "OPENAI_API_KEY=" not in env_values
assert saved_summary["provider_label"] == "OpenAI OAuth"
assert saved_summary["credential_line"] == "OpenAI OAuth tokens (Codex CLI)"
def test_run_wizard_anthropic_oauth_is_onboarding_auth_method(monkeypatch, tmp_path) -> None:
select_responses = iter(["quickstart", "anthropic", "oauth", "", "skip"])
cli_onboarding_providers: list[str] = []
def _mock_select(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(select_responses)
return m
def _cli_onboarding(provider, **kwargs):
cli_onboarding_providers.append(provider.value)
assert kwargs["display_label"] == "Anthropic OAuth"
return "ok"
store_path = tmp_path / "opensre.json"
env_path = tmp_path / ".env"
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(_ui, "get_store_path", lambda: store_path)
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(flow, "_run_cli_llm_onboarding", _cli_onboarding)
monkeypatch.setattr(
flow,
"save_local_config",
lambda **kwargs: wizard_store.save_local_config(path=store_path, **kwargs),
)
monkeypatch.setattr(
flow,
"sync_provider_env",
lambda **kwargs: sync_provider_env(env_path=env_path, **kwargs),
)
exit_code = flow.run_wizard()
assert exit_code == 0
assert cli_onboarding_providers == ["claude-code"]
payload = json.loads(store_path.read_text(encoding="utf-8"))
env_values = env_path.read_text(encoding="utf-8")
assert payload["targets"]["local"]["provider"] == "anthropic"
assert payload["targets"]["local"]["auth_method"] == "oauth"
assert payload["targets"]["local"]["api_key_env"] == "ANTHROPIC_API_KEY"
assert payload["targets"]["local"]["model_env"] == "CLAUDE_CODE_MODEL"
assert "LLM_PROVIDER=anthropic\n" in env_values
assert "LLM_AUTH_METHOD=oauth\n" in env_values
assert "CLAUDE_CODE_MODEL=\n" in env_values
def test_run_wizard_claude_code_skips_api_key_and_runs_cli_onboarding(
monkeypatch, tmp_path
) -> None:
select_responses = iter(["quickstart", "claude-code", "", "skip"])
saved_llm_keys: list[tuple[str, str]] = []
cli_onboarding_providers: list[str] = []
def _mock_select(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(select_responses)
return m
def _cli_onboarding(provider, **_kwargs):
cli_onboarding_providers.append(provider.value)
return "ok"
store_path = tmp_path / "opensre.json"
env_path = tmp_path / ".env"
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(_ui, "get_store_path", lambda: store_path)
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(flow, "_run_cli_llm_onboarding", _cli_onboarding)
monkeypatch.setattr(
flow,
"save_local_config",
lambda **kwargs: wizard_store.save_local_config(path=store_path, **kwargs),
)
monkeypatch.setattr(
flow,
"sync_provider_env",
lambda **kwargs: sync_provider_env(env_path=env_path, **kwargs),
)
monkeypatch.setattr(
_ui,
"save_api_key",
lambda provider, value, **_kwargs: saved_llm_keys.append((provider, value)),
)
exit_code = flow.run_wizard()
assert exit_code == 0
assert cli_onboarding_providers == ["claude-code"]
assert saved_llm_keys == []
payload = json.loads(store_path.read_text(encoding="utf-8"))
env_values = env_path.read_text(encoding="utf-8")
assert payload["targets"]["local"]["provider"] == "claude-code"
assert payload["targets"]["local"]["api_key_env"] == ""
assert payload["targets"]["local"]["model_env"] == "CLAUDE_CODE_MODEL"
assert "LLM_PROVIDER=claude-code\n" in env_values
assert "CLAUDE_CODE_MODEL=\n" in env_values
def test_run_wizard_gemini_cli_skips_api_key_and_runs_cli_onboarding(monkeypatch, tmp_path) -> None:
select_responses = iter(["quickstart", "gemini-cli", "", "skip"])
saved_llm_keys: list[tuple[str, str]] = []
cli_onboarding_providers: list[str] = []
def _mock_select(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(select_responses)
return m
def _cli_onboarding(provider, **_kwargs):
cli_onboarding_providers.append(provider.value)
return "ok"
store_path = tmp_path / "opensre.json"
env_path = tmp_path / ".env"
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(_ui, "get_store_path", lambda: store_path)
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(flow, "_run_cli_llm_onboarding", _cli_onboarding)
monkeypatch.setattr(
flow,
"save_local_config",
lambda **kwargs: wizard_store.save_local_config(path=store_path, **kwargs),
)
monkeypatch.setattr(
flow,
"sync_provider_env",
lambda **kwargs: sync_provider_env(env_path=env_path, **kwargs),
)
monkeypatch.setattr(
_ui,
"save_api_key",
lambda provider, value, **_kwargs: saved_llm_keys.append((provider, value)),
)
exit_code = flow.run_wizard()
assert exit_code == 0
assert cli_onboarding_providers == ["gemini-cli"]
assert saved_llm_keys == []
payload = json.loads(store_path.read_text(encoding="utf-8"))
env_values = env_path.read_text(encoding="utf-8")
assert payload["targets"]["local"]["provider"] == "gemini-cli"
assert payload["targets"]["local"]["api_key_env"] == ""
assert payload["targets"]["local"]["model_env"] == "GEMINI_CLI_MODEL"
assert "LLM_PROVIDER=gemini-cli\n" in env_values
assert "GEMINI_CLI_MODEL=\n" in env_values
def test_run_cli_llm_onboarding_ok_when_logged_in(monkeypatch) -> None:
adapter = MagicMock()
adapter.name = "codex"
adapter.binary_env_key = "CODEX_BIN"
adapter.install_hint = "npm i -g @openai/codex"
adapter.auth_hint = "Run: codex login"
adapter.detect.return_value = MagicMock(
installed=True,
logged_in=True,
detail="Logged in.",
)
provider = MagicMock()
provider.label = "OpenAI Codex CLI"
provider.adapter_factory = lambda: adapter
result = flow._run_cli_llm_onboarding(provider)
assert result == "ok"
adapter.detect.assert_called_once()
def test_run_cli_llm_onboarding_repick_when_not_logged_in(monkeypatch) -> None:
adapter = MagicMock()
adapter.name = "codex"
adapter.binary_env_key = "CODEX_BIN"
adapter.install_hint = "npm i -g @openai/codex"
adapter.auth_hint = "Run: codex login"
adapter.detect.return_value = MagicMock(
installed=True,
logged_in=False,
detail="Not logged in.",
)
provider = MagicMock()
provider.label = "OpenAI Codex CLI"
provider.adapter_factory = lambda: adapter
monkeypatch.setattr(flow, "_choose", lambda *_args, **_kwargs: "repick")
result = flow._run_cli_llm_onboarding(provider)
assert result == "repick"
def test_run_cli_llm_onboarding_ok_after_login_retry(monkeypatch) -> None:
adapter = MagicMock()
adapter.name = "codex"
adapter.binary_env_key = "CODEX_BIN"
adapter.install_hint = "npm i -g @openai/codex"
adapter.auth_hint = "Run: codex login"
detect_calls: list[object] = []
def _detect():
detect_calls.append(None)
if len(detect_calls) == 1:
return MagicMock(
installed=True,
logged_in=False,
detail="Not logged in.",
)
return MagicMock(installed=True, logged_in=True, detail="Logged in.")
adapter.detect = _detect
provider = MagicMock()
provider.label = "OpenAI Codex CLI"
provider.adapter_factory = lambda: adapter
monkeypatch.setattr(flow, "_choose", lambda *_args, **_kwargs: "retry")
result = flow._run_cli_llm_onboarding(provider)
assert result == "ok"
assert len(detect_calls) == 2
def test_run_cli_llm_onboarding_launches_managed_codex_oauth(monkeypatch, tmp_path) -> None:
adapter = MagicMock()
adapter.name = "codex"
adapter.binary_env_key = "CODEX_BIN"
adapter.install_hint = "npm i -g @openai/codex"
adapter.auth_hint = "Run: codex login"
detect_calls: list[object] = []
def _detect():
detect_calls.append(None)
if len(detect_calls) == 1:
return MagicMock(
installed=True,
logged_in=False,
bin_path="/usr/local/bin/codex",
detail="Not logged in.",
)
return MagicMock(installed=True, logged_in=True, detail="Logged in.")
adapter.detect = _detect
provider = MagicMock()
provider.value = "codex"
provider.label = "OpenAI Codex CLI"
provider.adapter_factory = lambda: adapter
oauth_calls: list[object] = []
def _fake_codex_oauth_login() -> CodexOAuthResult:
oauth_calls.append(None)
return CodexOAuthResult(
account_id="account-123",
auth_path=tmp_path / "codex-home" / "auth.json",
detail="OpenAI OAuth tokens stored for Codex.",
)
monkeypatch.setattr(flow, "_choose", lambda *_args, **_kwargs: "login")
monkeypatch.setattr(flow, "run_codex_oauth_login", _fake_codex_oauth_login)
monkeypatch.setenv("OPENSRE_LLM_AUTH_METADATA_PATH", str(tmp_path / "llm-auth.json"))
result = flow._run_cli_llm_onboarding(provider)
assert result == "ok"
assert oauth_calls == [None]
assert len(detect_calls) == 1
def test_run_cli_llm_onboarding_surfaces_managed_codex_oauth_error(monkeypatch) -> None:
adapter = MagicMock()
adapter.name = "codex"
adapter.binary_env_key = "CODEX_BIN"
adapter.install_hint = "npm i -g @openai/codex"
adapter.auth_hint = "Run: codex login"
adapter.detect.return_value = MagicMock(
installed=True,
logged_in=None,
bin_path="/opt/homebrew/bin/codex",
detail="Auth status unknown.",
)
provider = MagicMock()
provider.value = "codex"
provider.label = "OpenAI Codex CLI"
provider.adapter_factory = lambda: adapter
choices: list[str] = ["login", "repick"]
def _choose(*_args, **_kwargs):
return choices.pop(0)
monkeypatch.setattr(flow, "_choose", _choose)
monkeypatch.setattr(
flow,
"run_codex_oauth_login",
MagicMock(side_effect=flow.CodexOAuthError("Could not bind localhost:1455.")),
)
monkeypatch.setattr(flow, "_run_interactive_login_process", MagicMock())
result = flow._run_cli_llm_onboarding(provider, display_label="OpenAI OAuth")
assert result == "repick"
flow._run_interactive_login_process.assert_not_called()
def test_run_cli_llm_onboarding_launches_claude_browser_login(monkeypatch) -> None:
adapter = MagicMock()
adapter.name = "claude-code"
adapter.binary_env_key = "CLAUDE_CODE_BIN"
adapter.install_hint = "npm i -g @anthropic-ai/claude-code"
adapter.auth_hint = "Run: claude auth login"
detect_calls: list[object] = []
def _detect():
detect_calls.append(None)
if len(detect_calls) == 1:
return MagicMock(
installed=True,
logged_in=False,
bin_path="/opt/homebrew/bin/claude",
detail="Not authenticated.",
)
return MagicMock(installed=True, logged_in=True, detail="Authenticated.")
adapter.detect = _detect
provider = MagicMock()
provider.value = "claude-code"
provider.label = "Anthropic Claude Code CLI"
provider.adapter_factory = lambda: adapter
login_commands: list[list[str]] = []
def _fake_login(command: list[str]) -> flow._LoginProcessResult:
login_commands.append(command)
return flow._LoginProcessResult(returncode=0)
monkeypatch.setattr(flow, "_choose", lambda *_args, **_kwargs: "login")
monkeypatch.setattr(flow, "_run_interactive_login_process", _fake_login)
result = flow._run_cli_llm_onboarding(provider)
assert result == "ok"
assert login_commands == [["/opt/homebrew/bin/claude", "auth", "login"]]
assert len(detect_calls) == 1
def test_run_cli_llm_onboarding_repick_when_auth_status_unclear(monkeypatch) -> None:
adapter = MagicMock()
adapter.name = "codex"
adapter.binary_env_key = "CODEX_BIN"
adapter.install_hint = "npm i -g @openai/codex"
adapter.auth_hint = "Run: codex login"
adapter.detect.return_value = MagicMock(
installed=True,
logged_in=None,
detail="Auth status unknown.",
)
provider = MagicMock()
provider.label = "OpenAI Codex CLI"
provider.adapter_factory = lambda: adapter
monkeypatch.setattr(flow, "_choose", lambda *_args, **_kwargs: "repick")
result = flow._run_cli_llm_onboarding(provider)
assert result == "repick"
def test_run_cli_llm_onboarding_ok_after_unclear_auth_retry(monkeypatch) -> None:
adapter = MagicMock()
adapter.name = "codex"
adapter.binary_env_key = "CODEX_BIN"
adapter.install_hint = "npm i -g @openai/codex"
adapter.auth_hint = "Run: codex login"
detect_calls: list[object] = []
def _detect():
detect_calls.append(None)
if len(detect_calls) == 1:
return MagicMock(
installed=True,
logged_in=None,
detail="Auth status unknown.",
)
return MagicMock(installed=True, logged_in=True, detail="Logged in.")
adapter.detect = _detect
provider = MagicMock()
provider.label = "OpenAI Codex CLI"
provider.adapter_factory = lambda: adapter
monkeypatch.setattr(flow, "_choose", lambda *_args, **_kwargs: "retry")
result = flow._run_cli_llm_onboarding(provider)
assert result == "ok"
assert len(detect_calls) == 2
def test_run_cli_llm_onboarding_repick_when_user_chooses_repick(monkeypatch) -> None:
adapter = MagicMock()
adapter.name = "codex"
adapter.binary_env_key = "CODEX_BIN"
adapter.install_hint = "npm i -g @openai/codex"
adapter.auth_hint = "Run: codex login"
adapter.detect.return_value = MagicMock(
installed=False,
logged_in=None,
detail="Not found.",
)
provider = MagicMock()
provider.label = "OpenAI Codex CLI"
provider.adapter_factory = lambda: adapter
monkeypatch.setattr(flow, "_choose", lambda *_args, **_kwargs: "repick")
result = flow._run_cli_llm_onboarding(provider)
assert result == "repick"
def test_run_cli_llm_onboarding_path_override_then_ok(monkeypatch, tmp_path) -> None:
fake_bin = write_fake_runnable_cli_bin(tmp_path, "codex")
adapter = MagicMock()
adapter.name = "codex"
adapter.binary_env_key = "CODEX_BIN"
adapter.install_hint = "npm i -g @openai/codex"
adapter.auth_hint = "Run: codex login"
detect_calls: list[object] = []
def _detect():
detect_calls.append(None)
if len(detect_calls) == 1:
return MagicMock(installed=False, logged_in=None, detail="Not found.")
return MagicMock(installed=True, logged_in=True, detail="Logged in.")
adapter.detect = _detect
provider = MagicMock()
provider.label = "OpenAI Codex CLI"
provider.adapter_factory = lambda: adapter
monkeypatch.setattr(flow, "_choose", lambda *_args, **_kwargs: "path")
monkeypatch.setattr(flow, "_prompt_value", lambda *_args, **_kwargs: str(fake_bin))
monkeypatch.setattr(flow, "sync_env_values", lambda *_args, **_kwargs: None)
original_codex_bin = os.environ.get("CODEX_BIN")
try:
result = flow._run_cli_llm_onboarding(provider)
assert result == "ok"
assert len(detect_calls) == 2
# os.environ must be updated in-process so the next detect() call in the
# retry loop resolves the new binary without a process restart.
assert os.environ.get("CODEX_BIN") == str(fake_bin)
finally:
if original_codex_bin is None:
os.environ.pop("CODEX_BIN", None)
else:
os.environ["CODEX_BIN"] = original_codex_bin
def test_run_cli_llm_onboarding_abort_after_max_retries(monkeypatch) -> None:
adapter = MagicMock()
adapter.name = "codex"
adapter.binary_env_key = "CODEX_BIN"
adapter.install_hint = "npm i -g @openai/codex"
adapter.auth_hint = "Run: codex login"
adapter.detect.return_value = MagicMock(
installed=False,
logged_in=None,
detail="Not found.",
)
provider = MagicMock()
provider.label = "OpenAI Codex CLI"
provider.adapter_factory = lambda: adapter
monkeypatch.setattr(flow, "_choose", lambda *_args, **_kwargs: "retry")
result = flow._run_cli_llm_onboarding(provider)
assert result == "abort"
assert adapter.detect.call_count == 10
def test_credential_line_for_saved_summary_cli_codex() -> None:
from surfaces.cli.wizard import config as wizard_config
codex = next(p for p in wizard_config.SUPPORTED_PROVIDERS if p.value == "codex")
assert flow._credential_line_for_saved_summary(codex) == "OpenAI Codex CLI (Run: codex login)"
def test_credential_line_for_saved_summary_cli_claude_code() -> None:
from surfaces.cli.wizard import config as wizard_config
claude_code = next(p for p in wizard_config.SUPPORTED_PROVIDERS if p.value == "claude-code")
assert flow._credential_line_for_saved_summary(claude_code) == (
"Anthropic Claude Code CLI (Run: claude auth login or set ANTHROPIC_API_KEY)"
)
def test_credential_line_for_saved_summary_cli_gemini_cli() -> None:
from surfaces.cli.wizard import config as wizard_config
gemini_cli = next(p for p in wizard_config.SUPPORTED_PROVIDERS if p.value == "gemini-cli")
assert flow._credential_line_for_saved_summary(gemini_cli) == (
"Google Gemini CLI (Run: gemini (interactive login) or set GEMINI_API_KEY)"
)
def test_credential_line_for_saved_summary_cli_copilot() -> None:
from surfaces.cli.wizard import config as wizard_config
copilot = next(p for p in wizard_config.SUPPORTED_PROVIDERS if p.value == "copilot")
line = flow._credential_line_for_saved_summary(copilot)
# PR #1533: hint surfaces both CLI paths (`copilot login`, `gh auth login`)
# before the env-var bypass, matching the new CLI-first probe order.
assert line.startswith("GitHub Copilot CLI (Run `copilot login`")
assert "gh auth login" in line
assert "COPILOT_GITHUB_TOKEN" in line
def test_credential_line_for_saved_summary_anthropic() -> None:
from surfaces.cli.wizard import config as wizard_config
anthropic = next(p for p in wizard_config.SUPPORTED_PROVIDERS if p.value == "anthropic")
assert flow._credential_line_for_saved_summary(anthropic) == "system keychain"
def test_credential_line_for_saved_summary_cli_without_factory() -> None:
from surfaces.cli.wizard.config import ModelOption, ProviderOption
p = ProviderOption(
value="codex",
label="Fake CLI",
group="Local CLI providers",
api_key_env="",
model_env="CODEX_MODEL",
default_model="",
models=(ModelOption(value="", label="default"),),
credential_kind="cli",
adapter_factory=None,
allow_custom_models=True,
)
assert flow._credential_line_for_saved_summary(p) == "Fake CLI (CLI)"
def test_run_wizard_configures_gitlab(monkeypatch, tmp_path) -> None:
select_responses = iter(["quickstart", "anthropic", "api_key", "claude-opus-4-7", "gitlab"])
password_responses = iter(["llm-secret", "glpat_test"])
text_responses = iter(["https://gitlab.example.com/api/v4"])
saved_integrations: list[tuple[str, dict]] = []
synced_env_values: list[dict[str, str]] = []
synced_env_secrets: list[tuple[str, str]] = []
def _mock_select(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(select_responses)
return m
def _mock_password(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(password_responses)
return m
def _mock_text(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(text_responses)
return m
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(flow.questionary, "password", _mock_password)
monkeypatch.setattr(flow.questionary, "text", _mock_text)
monkeypatch.setattr(_ui, "get_store_path", lambda: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(
_integration_configurators,
"validate_gitlab_integration",
lambda **_kwargs: flow.IntegrationHealthResult(ok=True, detail="GitLab ok"),
)
monkeypatch.setattr(flow, "save_local_config", lambda **_kwargs: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "sync_provider_env", lambda **_kwargs: tmp_path / ".env")
monkeypatch.setattr(_ui, "save_llm_api_key", lambda *_args, **_kwargs: None)
def _sync_env_values(values: dict[str, str], **_kwargs):
synced_env_values.append(values)
return tmp_path / ".env"
def _sync_env_secret(key: str, value: str) -> None:
synced_env_secrets.append((key, value))
monkeypatch.setattr(_integration_configurators, "sync_env_values", _sync_env_values)
monkeypatch.setattr(_integration_configurators, "sync_env_secret", _sync_env_secret)
monkeypatch.setattr(
_integration_configurators,
"upsert_integration",
lambda service, payload: saved_integrations.append((service, payload)),
)
exit_code = flow.run_wizard()
assert exit_code == 0
assert saved_integrations == [
(
"gitlab",
{
"credentials": {
"base_url": "https://gitlab.example.com/api/v4",
"auth_token": "glpat_test",
}
},
)
]
assert synced_env_secrets == [("GITLAB_ACCESS_TOKEN", "glpat_test")]
assert synced_env_values == [
{
"GITLAB_BASE_URL": "https://gitlab.example.com/api/v4",
}
]
def test_run_wizard_gitlab_retries_on_validation_failure(monkeypatch, tmp_path) -> None:
"""When GitLab validation fails the first time, the wizard retries and succeeds."""
select_responses = iter(["quickstart", "anthropic", "api_key", "claude-opus-4-7", "gitlab"])
# First attempt: wrong token; second attempt: correct token
password_responses = iter(["llm-secret", "bad_token", "glpat_good"])
# base_url is prompted on each retry
text_responses = iter(
[
"https://gitlab.com/api/v4",
"https://gitlab.com/api/v4",
]
)
saved_integrations: list[tuple[str, dict]] = []
synced_env_values: list[dict[str, str]] = []
synced_env_secrets: list[tuple[str, str]] = []
validation_call_count = 0
def _mock_select(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(select_responses)
return m
def _mock_password(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(password_responses)
return m
def _mock_text(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(text_responses)
return m
def _validate_gitlab(**_kwargs):
nonlocal validation_call_count
validation_call_count += 1
if validation_call_count == 1:
return flow.IntegrationHealthResult(ok=False, detail="Unauthorized")
return flow.IntegrationHealthResult(ok=True, detail="GitLab ok")
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(flow.questionary, "password", _mock_password)
monkeypatch.setattr(flow.questionary, "text", _mock_text)
monkeypatch.setattr(_ui, "get_store_path", lambda: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(_integration_configurators, "validate_gitlab_integration", _validate_gitlab)
monkeypatch.setattr(flow, "save_local_config", lambda **_kwargs: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "sync_provider_env", lambda **_kwargs: tmp_path / ".env")
monkeypatch.setattr(_ui, "save_llm_api_key", lambda *_args, **_kwargs: None)
def _sync_env_values(values: dict[str, str], **_kwargs):
synced_env_values.append(values)
return tmp_path / ".env"
def _sync_env_secret(key: str, value: str) -> None:
synced_env_secrets.append((key, value))
monkeypatch.setattr(_integration_configurators, "sync_env_values", _sync_env_values)
monkeypatch.setattr(_integration_configurators, "sync_env_secret", _sync_env_secret)
monkeypatch.setattr(
_integration_configurators,
"upsert_integration",
lambda service, payload: saved_integrations.append((service, payload)),
)
exit_code = flow.run_wizard()
assert exit_code == 0
assert validation_call_count == 2
assert saved_integrations == [
(
"gitlab",
{
"credentials": {
"base_url": "https://gitlab.com/api/v4",
"auth_token": "glpat_good",
}
},
)
]
assert synced_env_secrets == [("GITLAB_ACCESS_TOKEN", "glpat_good")]
assert synced_env_values == [
{
"GITLAB_BASE_URL": "https://gitlab.com/api/v4",
}
]
def test_run_wizard_switches_provider_and_keeps_store_and_env_in_sync(
monkeypatch, tmp_path
) -> None:
# Saved: anthropic. User says yes to "Change provider?" and picks openai.
select_responses = iter(["quickstart", "openai", "api_key", "gpt-5.4-mini", "skip"])
confirm_responses = iter([True]) # "Change provider?" -> Yes
saved_llm_keys: list[tuple[str, str]] = []
def _mock_select(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(select_responses)
return m
def _mock_confirm(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(confirm_responses)
return m
def _mock_password(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = "fresh-openai-key"
return m
store_path = tmp_path / "opensre.json"
env_path = tmp_path / ".env"
wizard_store.save_local_config(
wizard_mode="quickstart",
provider="anthropic",
model="claude-opus-4-5",
api_key_env="ANTHROPIC_API_KEY",
model_env="ANTHROPIC_MODEL",
probes={
"local": {"target": "local", "reachable": True, "detail": "ok"},
"remote": {"target": "remote", "reachable": False, "detail": "down"},
},
path=store_path,
)
env_path.write_text(
"LLM_PROVIDER=anthropic\n"
"ANTHROPIC_API_KEY=saved-anthropic-key\n"
"ANTHROPIC_MODEL=claude-opus-4-5\n",
encoding="utf-8",
)
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(flow.questionary, "confirm", _mock_confirm)
monkeypatch.setattr(flow.questionary, "password", _mock_password)
monkeypatch.setattr(_ui, "get_store_path", lambda: store_path)
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(
flow,
"save_local_config",
lambda **kwargs: wizard_store.save_local_config(path=store_path, **kwargs),
)
monkeypatch.setattr(
flow,
"sync_provider_env",
lambda **kwargs: sync_provider_env(env_path=env_path, **kwargs),
)
monkeypatch.setattr(
_ui,
"save_api_key",
lambda provider, value, **_kwargs: saved_llm_keys.append((provider, value)),
)
exit_code = flow.run_wizard()
assert exit_code == 0
payload = json.loads(store_path.read_text(encoding="utf-8"))
env_values = env_path.read_text(encoding="utf-8")
assert payload["targets"]["local"]["provider"] == "openai"
assert payload["targets"]["local"]["api_key_env"] == "OPENAI_API_KEY"
assert payload["targets"]["local"]["model_env"] == "OPENAI_REASONING_MODEL"
assert "api_key" not in payload["targets"]["local"]
assert "LLM_PROVIDER=openai\n" in env_values
assert "OPENAI_API_KEY=" not in env_values
assert "ANTHROPIC_API_KEY=" not in env_values
assert "OPENAI_REASONING_MODEL=" in env_values
assert saved_llm_keys == [("openai", "fresh-openai-key")]
def test_run_wizard_configures_opensearch(monkeypatch, tmp_path) -> None:
"""Happy path: user picks opensearch, enters URL + basic auth, all gets persisted."""
select_responses = iter(
["quickstart", "anthropic", "api_key", "claude-opus-4-7", "opensearch", "basic"]
)
password_responses = iter(["llm-secret", "secret-pass"])
text_responses = iter(["https://my-cluster.example.com", "admin"])
saved_integrations: list[tuple[str, dict]] = []
synced_env_values: list[dict[str, str]] = []
synced_env_secrets: list[tuple[str, str]] = []
def _mock_select(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(select_responses)
return m
def _mock_password(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(password_responses)
return m
def _mock_text(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(text_responses)
return m
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(flow.questionary, "password", _mock_password)
monkeypatch.setattr(flow.questionary, "text", _mock_text)
monkeypatch.setattr(_ui, "get_store_path", lambda: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(
_integration_configurators,
"validate_opensearch_integration",
lambda **_kwargs: flow.IntegrationHealthResult(ok=True, detail="OpenSearch ok"),
)
monkeypatch.setattr(flow, "save_local_config", lambda **_kwargs: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "sync_provider_env", lambda **_kwargs: tmp_path / ".env")
monkeypatch.setattr(_ui, "save_llm_api_key", lambda *_args, **_kwargs: None)
def _sync_env_values(values: dict[str, str], **_kwargs):
synced_env_values.append(values)
return tmp_path / ".env"
def _sync_env_secret(key: str, value: str) -> None:
synced_env_secrets.append((key, value))
monkeypatch.setattr(_integration_configurators, "sync_env_values", _sync_env_values)
monkeypatch.setattr(_integration_configurators, "sync_env_secret", _sync_env_secret)
monkeypatch.setattr(
_integration_configurators,
"upsert_integration",
lambda service, payload: saved_integrations.append((service, payload)),
)
monkeypatch.setattr(
flow,
"build_demo_action_response",
lambda: {"success": True, "topics": [], "guidance": []},
)
exit_code = flow.run_wizard()
assert exit_code == 0
assert saved_integrations == [
(
"opensearch",
{
"credentials": {
"url": "https://my-cluster.example.com",
"username": "admin",
"password": "secret-pass",
}
},
)
]
assert synced_env_secrets == [("OPENSEARCH_PASSWORD", "secret-pass")]
assert synced_env_values == [
{
"OPENSEARCH_URL": "https://my-cluster.example.com",
"OPENSEARCH_USERNAME": "admin",
}
]
def test_run_wizard_opensearch_retries_on_validation_failure(monkeypatch, tmp_path) -> None:
"""When OpenSearch validation fails the first time, the wizard retries and succeeds."""
select_responses = iter(
["quickstart", "anthropic", "api_key", "claude-opus-4-7", "opensearch", "basic", "basic"]
)
password_responses = iter(["llm-secret", "wrong-pass", "correct-pass"])
text_responses = iter(
[
"https://my-cluster.example.com",
"admin",
"https://my-cluster.example.com",
"admin",
]
)
saved_integrations: list[tuple[str, dict]] = []
synced_env_values: list[dict[str, str]] = []
validation_call_count = 0
def _mock_select(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(select_responses)
return m
def _mock_password(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(password_responses)
return m
def _mock_text(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(text_responses)
return m
def _validate_opensearch(**_kwargs):
nonlocal validation_call_count
validation_call_count += 1
if validation_call_count == 1:
return flow.IntegrationHealthResult(ok=False, detail="HTTP 401: unauthorized")
return flow.IntegrationHealthResult(ok=True, detail="OpenSearch ok")
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(flow.questionary, "password", _mock_password)
monkeypatch.setattr(flow.questionary, "text", _mock_text)
monkeypatch.setattr(_ui, "get_store_path", lambda: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(
_integration_configurators, "validate_opensearch_integration", _validate_opensearch
)
monkeypatch.setattr(flow, "save_local_config", lambda **_kwargs: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "sync_provider_env", lambda **_kwargs: tmp_path / ".env")
monkeypatch.setattr(_ui, "save_llm_api_key", lambda *_args, **_kwargs: None)
def _sync_env_values(values: dict[str, str], **_kwargs):
synced_env_values.append(values)
return tmp_path / ".env"
monkeypatch.setattr(_integration_configurators, "sync_env_values", _sync_env_values)
monkeypatch.setattr(
_integration_configurators,
"upsert_integration",
lambda service, payload: saved_integrations.append((service, payload)),
)
monkeypatch.setattr(
flow,
"build_demo_action_response",
lambda: {"success": True, "topics": [], "guidance": []},
)
exit_code = flow.run_wizard()
assert exit_code == 0
assert validation_call_count == 2
assert saved_integrations == [
(
"opensearch",
{
"credentials": {
"url": "https://my-cluster.example.com",
"username": "admin",
"password": "correct-pass",
}
},
)
]
def test_run_wizard_opensearch_rejects_empty_api_key(monkeypatch, tmp_path) -> None:
"""When user picks api_key auth but enters an empty key, the wizard rejects it.
Regression for the silent-credential-drop bug: on a cluster with security
disabled the validator probe would return 200, result.ok would be True,
and the integration would persist as URL-only — discarding the user's
chosen auth method without any visible error.
The wizard now guards against empty api_key explicitly (before the probe
runs) and re-prompts with a clear error message. Verified by checking
that validation is only called once the user supplies a non-empty key.
"""
# User picks: opensearch -> api_key auth -> (rejected, empty) -> api_key auth retry
select_responses = iter(
[
"quickstart",
"anthropic",
"api_key",
"claude-opus-4-7",
"opensearch",
"api_key",
"api_key",
]
)
# First api_key prompt: empty (rejected). Second: valid key.
password_responses = iter(["llm-secret", "", "valid-api-key"])
text_responses = iter(
[
"https://my-cluster.example.com",
"https://my-cluster.example.com",
]
)
saved_integrations: list[tuple[str, dict]] = []
synced_env_values: list[dict[str, str]] = []
validation_call_count = 0
def _mock_select(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(select_responses)
return m
def _mock_password(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(password_responses)
return m
def _mock_text(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(text_responses)
return m
def _validate_opensearch(**_kwargs):
nonlocal validation_call_count
validation_call_count += 1
return flow.IntegrationHealthResult(ok=True, detail="OpenSearch ok")
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(flow.questionary, "password", _mock_password)
monkeypatch.setattr(flow.questionary, "text", _mock_text)
monkeypatch.setattr(_ui, "get_store_path", lambda: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(
_integration_configurators, "validate_opensearch_integration", _validate_opensearch
)
monkeypatch.setattr(flow, "save_local_config", lambda **_kwargs: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "sync_provider_env", lambda **_kwargs: tmp_path / ".env")
monkeypatch.setattr(_ui, "save_llm_api_key", lambda *_args, **_kwargs: None)
def _sync_env_values(values: dict[str, str], **_kwargs):
synced_env_values.append(values)
return tmp_path / ".env"
monkeypatch.setattr(_integration_configurators, "sync_env_values", _sync_env_values)
monkeypatch.setattr(
_integration_configurators,
"upsert_integration",
lambda service, payload: saved_integrations.append((service, payload)),
)
monkeypatch.setattr(
flow,
"build_demo_action_response",
lambda: {"success": True, "topics": [], "guidance": []},
)
exit_code = flow.run_wizard()
assert exit_code == 0
# Validator should only be called once — after the user supplies a real key.
# The empty-key attempt must be blocked before reaching the probe.
assert validation_call_count == 1
assert saved_integrations == [
(
"opensearch",
{
"credentials": {
"url": "https://my-cluster.example.com",
"api_key": "valid-api-key",
}
},
)
]
def test_run_wizard_opensearch_rejects_empty_basic_password(monkeypatch, tmp_path) -> None:
"""When user picks basic auth but leaves password empty, the wizard rejects it.
Companion regression for the half-credential bug: ElasticsearchConfig.headers
silently drops the Authorization header when either half of Basic Auth is
empty, so the agent would send unauthenticated requests against a
security-enabled cluster and fail with a confusing 401.
The wizard now guards against half-populated Basic Auth before the probe
runs and re-prompts with a clear error message. Verified by checking that
validation is only called once the user supplies both halves.
"""
# User picks: opensearch -> basic auth -> (rejected, empty pass) -> basic auth retry
select_responses = iter(
["quickstart", "anthropic", "api_key", "claude-opus-4-7", "opensearch", "basic", "basic"]
)
# First password prompt: empty (rejected). Second attempt: valid password.
password_responses = iter(["llm-secret", "", "real-pass"])
text_responses = iter(
[
"https://my-cluster.example.com",
"admin",
"https://my-cluster.example.com",
"admin",
]
)
saved_integrations: list[tuple[str, dict]] = []
synced_env_values: list[dict[str, str]] = []
validation_call_count = 0
def _mock_select(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(select_responses)
return m
def _mock_password(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(password_responses)
return m
def _mock_text(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(text_responses)
return m
def _validate_opensearch(**_kwargs):
nonlocal validation_call_count
validation_call_count += 1
return flow.IntegrationHealthResult(ok=True, detail="OpenSearch ok")
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(flow.questionary, "password", _mock_password)
monkeypatch.setattr(flow.questionary, "text", _mock_text)
monkeypatch.setattr(_ui, "get_store_path", lambda: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(
_integration_configurators, "validate_opensearch_integration", _validate_opensearch
)
monkeypatch.setattr(flow, "save_local_config", lambda **_kwargs: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "sync_provider_env", lambda **_kwargs: tmp_path / ".env")
monkeypatch.setattr(_ui, "save_llm_api_key", lambda *_args, **_kwargs: None)
def _sync_env_values(values: dict[str, str], **_kwargs):
synced_env_values.append(values)
return tmp_path / ".env"
monkeypatch.setattr(_integration_configurators, "sync_env_values", _sync_env_values)
monkeypatch.setattr(
_integration_configurators,
"upsert_integration",
lambda service, payload: saved_integrations.append((service, payload)),
)
monkeypatch.setattr(
flow,
"build_demo_action_response",
lambda: {"success": True, "topics": [], "guidance": []},
)
exit_code = flow.run_wizard()
assert exit_code == 0
# Validator should only be called once — after the user supplies both halves.
# The empty-password attempt must be blocked before reaching the probe.
assert validation_call_count == 1
assert saved_integrations == [
(
"opensearch",
{
"credentials": {
"url": "https://my-cluster.example.com",
"username": "admin",
"password": "real-pass",
}
},
)
]
def test_run_wizard_configures_telegram(monkeypatch, tmp_path) -> None:
select_responses = iter(["quickstart", "anthropic", "api_key", "claude-opus-4-7", "telegram"])
password_responses = iter(["llm-secret", "123:ABC"])
text_responses = iter(["-1001234567890"])
saved_integrations: list[tuple[str, dict]] = []
synced_env_values: list[dict[str, str]] = []
synced_env_secrets: list[tuple[str, str]] = []
def _mock_select(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(select_responses)
return m
def _mock_password(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(password_responses)
return m
def _mock_text(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(text_responses)
return m
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(flow.questionary, "password", _mock_password)
monkeypatch.setattr(flow.questionary, "text", _mock_text)
monkeypatch.setattr(_ui, "get_store_path", lambda: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(
_integration_configurators,
"validate_telegram_bot",
lambda **_kwargs: flow.IntegrationHealthResult(
ok=True, detail="Connected to Telegram bot @opensre_bot."
),
)
monkeypatch.setattr(flow, "save_local_config", lambda **_kwargs: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "sync_provider_env", lambda **_kwargs: tmp_path / ".env")
monkeypatch.setattr(_ui, "save_llm_api_key", lambda *_args, **_kwargs: None)
def _sync_env_values(values: dict[str, str], **_kwargs):
synced_env_values.append(values)
return tmp_path / ".env"
def _sync_env_secret(key: str, value: str) -> None:
synced_env_secrets.append((key, value))
monkeypatch.setattr(_integration_configurators, "sync_env_values", _sync_env_values)
monkeypatch.setattr(_integration_configurators, "sync_env_secret", _sync_env_secret)
monkeypatch.setattr(
_integration_configurators,
"upsert_integration",
lambda service, payload: saved_integrations.append((service, payload)),
)
exit_code = flow.run_wizard()
assert exit_code == 0
assert saved_integrations == [
(
"telegram",
{
"credentials": {
"bot_token": "123:ABC",
"default_chat_id": "-1001234567890",
}
},
)
]
assert synced_env_secrets == [("TELEGRAM_BOT_TOKEN", "123:ABC")]
assert synced_env_values == [{"TELEGRAM_DEFAULT_CHAT_ID": "-1001234567890"}]
def test_run_wizard_telegram_retries_on_validation_failure(monkeypatch, tmp_path) -> None:
select_responses = iter(["quickstart", "anthropic", "api_key", "claude-opus-4-7", "telegram"])
password_responses = iter(["llm-secret", "bad-token", "123:GOOD"])
text_responses = iter(["-1001", "-1001"])
saved_integrations: list[tuple[str, dict]] = []
validation_call_count = 0
def _mock_select(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(select_responses)
return m
def _mock_password(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(password_responses)
return m
def _mock_text(*_args, **_kwargs):
m = MagicMock()
m.ask.return_value = next(text_responses)
return m
def _validate(**_kwargs):
nonlocal validation_call_count
validation_call_count += 1
if validation_call_count == 1:
return flow.IntegrationHealthResult(ok=False, detail="Telegram API check failed.")
return flow.IntegrationHealthResult(ok=True, detail="Connected to Telegram bot @bot.")
monkeypatch.setattr(_ui, "select_prompt", _mock_select)
monkeypatch.setattr(flow.questionary, "password", _mock_password)
monkeypatch.setattr(flow.questionary, "text", _mock_text)
monkeypatch.setattr(_ui, "get_store_path", lambda: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "probe_local_target", lambda _path: ProbeResult("local", True, "ok"))
monkeypatch.setattr(_integration_configurators, "validate_telegram_bot", _validate)
monkeypatch.setattr(flow, "save_local_config", lambda **_kwargs: tmp_path / "opensre.json")
monkeypatch.setattr(flow, "sync_provider_env", lambda **_kwargs: tmp_path / ".env")
monkeypatch.setattr(_ui, "save_llm_api_key", lambda *_args, **_kwargs: None)
monkeypatch.setattr(
_integration_configurators, "sync_env_values", lambda *_a, **_kw: tmp_path / ".env"
)
monkeypatch.setattr(_integration_configurators, "sync_env_secret", lambda *_a, **_kw: None)
monkeypatch.setattr(
_integration_configurators,
"upsert_integration",
lambda service, payload: saved_integrations.append((service, payload)),
)
exit_code = flow.run_wizard()
assert exit_code == 0
assert validation_call_count == 2
assert saved_integrations[0][0] == "telegram"
assert saved_integrations[0][1]["credentials"]["bot_token"] == "123:GOOD"