Files
opensquilla--opensquilla/tests/test_cli/test_onboard_cmd_review_fixes.py
2026-07-13 13:12:33 +08:00

475 lines
15 KiB
Python

"""Regression tests for review findings on the headless onboard CLI.
Covers: keep-current --router on re-saves (S1), EOFError cancellation
productization (S4), restart guidance at the bare-onboard boundary (F17),
wizard-phase OSError diagnosis (F27), validation-error redaction (F29), and
the single-preflight-load contract of the --provider path (F41).
All tests are offline and use synthetic dummy data only.
"""
from __future__ import annotations
import errno
import re
import tomllib
import pytest
from typer.testing import CliRunner
from opensquilla.cli.main import app
from opensquilla.onboarding.config_store import load_config
runner = CliRunner()
_ANSI_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
def _plain(value: str) -> str:
return " ".join(_ANSI_RE.sub("", value).split())
# ---------------------------------------------------------------------------
# S1: bare `onboard --provider` re-saves keep the stored router state when
# --router is omitted; explicit --router and first-run behavior are unchanged.
# ---------------------------------------------------------------------------
def test_onboard_provider_key_rotation_keeps_router_disabled_and_model(
tmp_path, monkeypatch
):
target = tmp_path / "c.toml"
target.write_text(
"[llm]\n"
'provider = "openrouter"\n'
'model = "custom/model-x"\n'
'api_key = "sk-old"\n'
"\n"
"[squilla_router]\n"
"enabled = false\n",
encoding="utf-8",
)
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(
app,
["onboard", "--provider", "openrouter", "--api-key", "sk-new", "--minimal"],
)
assert result.exit_code == 0, result.output
cfg = load_config(target)
assert cfg.squilla_router.enabled is False
assert cfg.llm.model == "custom/model-x"
assert cfg.llm.api_key == "sk-new"
data = tomllib.loads(target.read_text(encoding="utf-8"))
assert data["squilla_router"]["enabled"] is False
def test_onboard_provider_key_rotation_keeps_hand_customized_tiers(
tmp_path, monkeypatch
):
target = tmp_path / "c.toml"
target.write_text(
"[llm]\n"
'provider = "openrouter"\n'
'model = "custom/model-x"\n'
'api_key = "sk-old"\n'
"\n"
"[squilla_router.tiers.c0]\n"
'provider = "openrouter"\n'
'model = "my-org/custom-c0"\n'
"[squilla_router.tiers.c1]\n"
'provider = "openrouter"\n'
'model = "my-org/custom-c1"\n'
"[squilla_router.tiers.c2]\n"
'provider = "openrouter"\n'
'model = "my-org/custom-c2"\n'
"[squilla_router.tiers.c3]\n"
'provider = "openrouter"\n'
'model = "my-org/custom-c3"\n',
encoding="utf-8",
)
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(
app,
["onboard", "--provider", "openrouter", "--api-key", "sk-new", "--minimal"],
)
assert result.exit_code == 0, result.output
cfg = load_config(target)
for tier in ("c0", "c1", "c2", "c3"):
assert cfg.squilla_router.tiers[tier]["model"] == f"my-org/custom-{tier}"
assert cfg.llm.api_key == "sk-new"
def test_onboard_provider_explicit_router_flag_still_applies_on_resave(
tmp_path, monkeypatch
):
target = tmp_path / "c.toml"
target.write_text(
"[llm]\n"
'provider = "openrouter"\n'
'model = "custom/model-x"\n'
'api_key = "sk-old"\n'
"\n"
"[squilla_router]\n"
"enabled = false\n",
encoding="utf-8",
)
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(
app,
[
"onboard",
"--provider",
"openrouter",
"--api-key",
"sk-new",
"--router",
"recommended",
"--minimal",
],
)
assert result.exit_code == 0, result.output
cfg = load_config(target)
assert cfg.squilla_router.enabled is True
assert cfg.squilla_router.tier_profile == "openrouter"
def test_onboard_provider_first_run_still_applies_recommended_router(
tmp_path, monkeypatch
):
# Fresh/unconfigured install: an omitted --router keeps today's first-run
# behavior and applies the recommended profile.
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
monkeypatch.delenv("OPENSQUILLA_LLM_API_KEY", raising=False)
result = runner.invoke(
app,
["onboard", "--provider", "openrouter", "--api-key", "sk", "--minimal"],
)
assert result.exit_code == 0, result.output
data = tomllib.loads(target.read_text(encoding="utf-8"))
assert data["squilla_router"]["tier_profile"] == "openrouter"
cfg = load_config(target)
assert cfg.squilla_router.enabled is True
def test_onboard_provider_first_run_with_env_key_still_applies_router(
tmp_path, monkeypatch
):
# An env-absorbed credential must not make a fresh install look
# configured: the recommended profile still applies on first setup.
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
monkeypatch.setenv("OPENSQUILLA_LLM_API_KEY", "sk-from-env")
result = runner.invoke(
app,
["onboard", "--provider", "openrouter", "--api-key", "sk", "--minimal"],
)
assert result.exit_code == 0, result.output
data = tomllib.loads(target.read_text(encoding="utf-8"))
assert data["squilla_router"]["tier_profile"] == "openrouter"
# ---------------------------------------------------------------------------
# S4: EOFError (Ctrl+D / exhausted piped stdin) must be productized exactly
# like Esc/Ctrl+C — one short "Setup cancelled" line, exit 130.
# ---------------------------------------------------------------------------
def test_onboard_wizard_eof_is_productized_as_cancellation(tmp_path, monkeypatch):
from opensquilla.cli import onboard_cmd
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
def eof(_options):
raise EOFError
monkeypatch.setattr(onboard_cmd, "run_interactive_onboard", eof)
result = runner.invoke(app, ["onboard"])
assert result.exit_code == 130
assert "Setup cancelled" in _plain(result.stderr)
assert "Aborted" not in result.output + result.stderr
assert "Traceback" not in result.output + result.stderr
assert not target.exists()
def test_configure_wizard_eof_is_productized_as_cancellation(tmp_path, monkeypatch):
from opensquilla.cli import onboard_cmd
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
def eof(_section, *, config_path=None):
raise EOFError
monkeypatch.setattr(onboard_cmd, "run_interactive_configure", eof)
result = runner.invoke(app, ["onboard", "configure", "router"])
assert result.exit_code == 130
assert "Setup cancelled" in _plain(result.stderr)
assert "Aborted" not in result.output + result.stderr
assert "Traceback" not in result.output + result.stderr
assert not target.exists()
# ---------------------------------------------------------------------------
# F17: bare `onboard` must surface PersistResult.restart_required with the
# same structured guidance `onboard configure` prints.
# ---------------------------------------------------------------------------
def test_bare_onboard_prints_restart_guidance_when_result_requires_it(
tmp_path, monkeypatch
):
from opensquilla.cli import onboard_cmd
from opensquilla.onboarding.config_store import PersistResult
target = tmp_path / "c.toml"
target.write_text(
'[llm]\nprovider = "openrouter"\nmodel = "m"\napi_key = "sk"\n',
encoding="utf-8",
)
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
def hub_edit(_options):
# Simulates onboard -> "Change specific sections" -> a channel edit:
# the aggregated result carries the sticky restart flag.
return PersistResult(
path=target,
backup_path=None,
restart_required=True,
warnings=[],
)
monkeypatch.setattr(onboard_cmd, "run_interactive_onboard", hub_edit)
result = runner.invoke(app, ["onboard"])
assert result.exit_code == 0, result.output
plain = _plain(result.stdout)
assert "restart required" in plain
assert "opensquilla gateway restart" in plain
def test_bare_onboard_prints_no_restart_guidance_without_flag(tmp_path, monkeypatch):
from opensquilla.cli import onboard_cmd
from opensquilla.onboarding.config_store import PersistResult
target = tmp_path / "c.toml"
target.write_text(
'[llm]\nprovider = "openrouter"\nmodel = "m"\napi_key = "sk"\n',
encoding="utf-8",
)
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
monkeypatch.setattr(
onboard_cmd,
"run_interactive_onboard",
lambda _options: PersistResult(
path=target,
backup_path=None,
restart_required=False,
warnings=[],
),
)
result = runner.invoke(app, ["onboard"])
assert result.exit_code == 0, result.output
assert "restart required" not in _plain(result.stdout)
# ---------------------------------------------------------------------------
# F27: an OSError raised INSIDE the wizard (e.g. disk full during the final
# persist) is a write failure, not a config-load error — the CLI must not
# claim the config failed to load or suggest editing/moving it.
# ---------------------------------------------------------------------------
def test_onboard_wizard_write_oserror_is_not_misdiagnosed_as_load_error(
tmp_path, monkeypatch
):
from opensquilla.cli import onboard_cmd
target = tmp_path / "c.toml"
target.write_text(
'[llm]\nprovider = "openrouter"\nmodel = "m"\napi_key = "sk"\n',
encoding="utf-8",
)
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
def disk_full(_options):
raise OSError(errno.ENOSPC, "No space left on device")
monkeypatch.setattr(onboard_cmd, "run_interactive_onboard", disk_full)
result = runner.invoke(app, ["onboard"])
assert result.exit_code == 2
plain = _plain(result.stderr)
assert "No space left on device" in plain
assert "OpenSquilla config error" not in plain
assert "edit or move this config" not in plain
assert "Traceback" not in result.output + result.stderr
def test_onboard_prevalidates_config_before_entering_wizard(tmp_path, monkeypatch):
from opensquilla.cli import onboard_cmd
target = tmp_path / "c.toml"
target.write_text("not toml :::", encoding="utf-8")
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
def must_not_run(_options): # pragma: no cover - the pre-check exits first
raise AssertionError("the wizard must not start over a corrupt config")
monkeypatch.setattr(onboard_cmd, "run_interactive_onboard", must_not_run)
result = runner.invoke(app, ["onboard"])
assert result.exit_code == 2
assert "OpenSquilla config error" in result.stderr
# ---------------------------------------------------------------------------
# F29: a validator message that interpolates the offending input must be
# masked by the free-text redactor before it reaches stderr.
# ---------------------------------------------------------------------------
def test_onboard_provider_validation_error_redacts_secret_shaped_values(
tmp_path, monkeypatch
):
from pydantic import BaseModel, ValidationError, field_validator
from opensquilla.cli import onboard_cmd
secret = "sk-live-SECRETSECRETSECRETSECRET123456"
class _Probe(BaseModel):
api_key: str
@field_validator("api_key")
@classmethod
def _reject(cls, value: str) -> str:
raise ValueError(f"key {value!r} looks malformed")
try:
_Probe(api_key=secret)
except ValidationError as exc:
captured = exc
else: # pragma: no cover - the construction above always fails
raise AssertionError("expected a ValidationError")
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
class _RaisingEngine:
def __init__(self, *_a, **_kw):
pass
def apply(self, *_a, **_kw):
raise captured
def persist(self, *_a, **_kw): # pragma: no cover - apply raises first
raise AssertionError("persist must not run after a failed apply")
monkeypatch.setattr(onboard_cmd, "SetupEngine", _RaisingEngine)
result = runner.invoke(
app,
["onboard", "--provider", "openrouter", "--api-key", secret, "--minimal"],
)
assert result.exit_code == 2
combined = result.output + result.stderr
assert secret not in combined
assert "***" in _plain(result.stderr)
assert "api_key" in _plain(result.stderr)
assert not target.exists()
def test_format_validation_error_masks_interpolated_secret_unit():
from pydantic import BaseModel, ValidationError, field_validator
from opensquilla.cli.onboard_cmd import _format_validation_error
secret = "sk-live-UNITSECRETUNITSECRET987654"
class _Probe(BaseModel):
api_key: str
@field_validator("api_key")
@classmethod
def _reject(cls, value: str) -> str:
raise ValueError(f"key {value!r} looks malformed")
with pytest.raises(ValidationError) as excinfo:
_Probe(api_key=secret)
rendered = _format_validation_error(excinfo.value)
assert secret not in rendered
assert "***" in rendered
assert "api_key" in rendered
# ---------------------------------------------------------------------------
# F41: the `onboard --provider` path must reuse its preflight load for the
# save instead of loading the config again inside the engine.
# ---------------------------------------------------------------------------
def test_onboard_provider_reuses_preflight_load_for_the_save(tmp_path, monkeypatch):
from opensquilla.cli import onboard_cmd
from opensquilla.onboarding import setup_engine
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
def engine_must_not_load(*_a, **_kw): # pragma: no cover - regression guard
raise AssertionError(
"SetupEngine must receive the preflight-loaded config, not reload it"
)
monkeypatch.setattr(setup_engine, "load_config", engine_must_not_load)
load_calls: list[object] = []
real_load = onboard_cmd.load_config
def counting_load(path=None):
load_calls.append(path)
return real_load(path)
monkeypatch.setattr(onboard_cmd, "load_config", counting_load)
result = runner.invoke(
app,
[
"onboard",
"--provider",
"openrouter",
"--model",
"deepseek/deepseek-v4-flash",
"--api-key",
"sk",
"--minimal",
],
)
assert result.exit_code == 0, result.output
# One preflight load feeding the engine + one post-save load for the
# handoff summary; the previous shape performed three full loads.
assert len(load_calls) == 2
assert "openrouter" in target.read_text(encoding="utf-8")