"""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")