2723 lines
89 KiB
Python
2723 lines
89 KiB
Python
"""CLI tests for `opensquilla onboard` and `configure`."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json as _json
|
|
import platform
|
|
import re
|
|
import shlex
|
|
import tomllib
|
|
|
|
import pytest
|
|
from typer.testing import CliRunner
|
|
|
|
from opensquilla.cli.main import app
|
|
|
|
runner = CliRunner()
|
|
_ANSI_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
|
|
|
|
|
|
def _plain_text(value: str) -> str:
|
|
return " ".join(_ANSI_RE.sub("", value).replace("│", " ").split())
|
|
|
|
|
|
def _compact_text(value: str) -> str:
|
|
return "".join(_ANSI_RE.sub("", value).replace("│", " ").split())
|
|
|
|
|
|
def _env_hint(env_key: str) -> str:
|
|
if platform.system().lower().startswith("win"):
|
|
return f'PowerShell: $env:{env_key} = "<your-key>"'
|
|
return f'export {env_key}="<your-key>"'
|
|
|
|
|
|
def _env_command(env_key: str) -> str:
|
|
if platform.system().lower().startswith("win"):
|
|
return f'$env:{env_key} = "<your-key>"'
|
|
return f'export {env_key}="<your-key>"'
|
|
|
|
|
|
def _config_arg(path) -> str:
|
|
return shlex.quote(str(path))
|
|
|
|
|
|
def _effective_config(path):
|
|
"""Load the effective config exactly as the gateway would.
|
|
|
|
The sparse TOML persist omits values equal to model defaults, so raw-TOML
|
|
asserts are reserved for keys a save actually writes; everything else is
|
|
checked against the loaded (effective) configuration.
|
|
"""
|
|
from opensquilla.onboarding.config_store import load_config
|
|
|
|
return load_config(path)
|
|
|
|
|
|
def test_onboard_noninteractive_provider(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"onboard",
|
|
"--provider", "openrouter",
|
|
"--model", "deepseek/deepseek-v4-flash",
|
|
"--api-key", "sk",
|
|
"--skip-channels", "--skip-search",
|
|
],
|
|
)
|
|
assert result.exit_code == 0, result.stdout
|
|
assert "openrouter" in target.read_text()
|
|
assert "sk" not in result.stdout
|
|
assert "onboarding.config_persisted" not in result.stdout
|
|
assert "openrouter ·" not in result.stdout
|
|
assert "OpenSquilla Setup Handoff" in result.stdout
|
|
assert "Provider Configured" not in result.stdout
|
|
assert "LLM: openrouter / deepseek/deepseek-v4-pro" in result.stdout
|
|
|
|
|
|
def test_onboard_finish_commands_remain_copyable_with_long_config_path(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
from opensquilla.cli import onboard_cmd
|
|
|
|
target = tmp_path / "very-long-config-name-for-onboard-output.toml"
|
|
monkeypatch.setattr(onboard_cmd.console, "width", 48)
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"onboard",
|
|
"--provider",
|
|
"openrouter",
|
|
"--model",
|
|
"deepseek/deepseek-v4-flash",
|
|
"--api-key",
|
|
"sk",
|
|
"--router",
|
|
"disabled",
|
|
"--minimal",
|
|
"--config",
|
|
str(target),
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert f"opensquilla gateway run --config {_config_arg(target)}" in result.stdout
|
|
assert f"opensquilla gateway start --json --config {_config_arg(target)}" in result.stdout
|
|
assert (
|
|
f"opensquilla gateway restart --json --config {_config_arg(target)}"
|
|
in result.stdout
|
|
)
|
|
|
|
|
|
def test_onboard_status_paths_remain_copyable_with_long_config_path(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
from opensquilla.cli import onboard_cmd
|
|
|
|
target = tmp_path / "very-long-config-name-for-onboard-status-output.toml"
|
|
monkeypatch.setattr(onboard_cmd.console, "width", 48)
|
|
|
|
result = runner.invoke(app, ["onboard", "status", "--config", str(target)])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert (
|
|
f"Guided CLI: opensquilla onboard --if-needed --config {_config_arg(target)}"
|
|
in result.stdout
|
|
)
|
|
assert f"Web UI: opensquilla gateway run --config {_config_arg(target)}" in result.stdout
|
|
assert (
|
|
"Provider recipes: "
|
|
f"opensquilla onboard catalog providers --config {_config_arg(target)}"
|
|
in result.stdout
|
|
)
|
|
|
|
|
|
def test_onboard_accepts_skip_image_generation_option(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"onboard",
|
|
"--provider",
|
|
"openrouter",
|
|
"--model",
|
|
"deepseek/deepseek-v4-flash",
|
|
"--api-key",
|
|
"sk",
|
|
"--skip-channels",
|
|
"--skip-search",
|
|
"--skip-image-generation",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
# A save marker: the flag-accepting run must actually persist the
|
|
# provider config; a fresh-config default check alone is vacuous.
|
|
# (Values equal to model defaults are omitted by the sparse persist, so
|
|
# the stored key and router profile are the observable markers.)
|
|
assert target.exists()
|
|
data = tomllib.loads(target.read_text())
|
|
assert data["llm"]["api_key"] == "sk"
|
|
assert data["squilla_router"]["tier_profile"] == "openrouter"
|
|
assert "image_generation" not in data or not data["image_generation"].get("enabled")
|
|
assert _effective_config(target).image_generation.enabled is False
|
|
|
|
|
|
def test_onboard_passes_skip_migration_to_interactive_flow(tmp_path, monkeypatch):
|
|
from opensquilla.cli import onboard_cmd
|
|
from opensquilla.onboarding.config_store import PersistResult
|
|
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
captured: dict[str, bool] = {}
|
|
|
|
def fake_run_interactive_onboard(options):
|
|
captured["skip_migration"] = options.skip_migration
|
|
return PersistResult(
|
|
path=target,
|
|
backup_path=None,
|
|
restart_required=False,
|
|
warnings=[],
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
onboard_cmd,
|
|
"run_interactive_onboard",
|
|
fake_run_interactive_onboard,
|
|
)
|
|
|
|
result = runner.invoke(app, ["onboard", "--skip-migration"])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert captured["skip_migration"] is True
|
|
|
|
|
|
def test_onboard_noninteractive_provider_can_use_env_key_and_router(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"onboard",
|
|
"--provider",
|
|
"deepseek",
|
|
"--model",
|
|
"deepseek-chat",
|
|
"--api-key-env",
|
|
"DEEPSEEK_API_KEY",
|
|
"--proxy",
|
|
"http://127.0.0.1:7890",
|
|
"--router",
|
|
"recommended",
|
|
"--minimal",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
data = tomllib.loads(target.read_text())
|
|
assert data["llm"]["provider"] == "deepseek"
|
|
assert data["llm"]["api_key_env"] == "DEEPSEEK_API_KEY"
|
|
assert data["llm"]["proxy"] == "http://127.0.0.1:7890"
|
|
assert "api_key" not in data["llm"]
|
|
assert data["squilla_router"]["tier_profile"] == "deepseek"
|
|
assert "tiers" not in data["squilla_router"]
|
|
assert "DEEPSEEK_API_KEY" in result.stdout
|
|
assert "warning" in result.stdout.lower()
|
|
assert "not set in this shell" in result.stdout
|
|
|
|
|
|
def test_onboard_noninteractive_provider_can_omit_model_for_router_profile(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"onboard",
|
|
"--provider",
|
|
"deepseek",
|
|
"--api-key-env",
|
|
"DEEPSEEK_API_KEY",
|
|
"--minimal",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
data = tomllib.loads(target.read_text())
|
|
assert data["llm"]["provider"] == "deepseek"
|
|
assert data["llm"]["model"] == "deepseek-v4-flash"
|
|
assert data["squilla_router"]["tier_profile"] == "deepseek"
|
|
|
|
|
|
def test_onboard_noninteractive_provider_without_router_profile_seeds_router_tiers(
|
|
tmp_path, monkeypatch
|
|
):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"onboard",
|
|
"--provider",
|
|
"anthropic",
|
|
"--model",
|
|
"claude-3-5-sonnet-latest",
|
|
"--api-key-env",
|
|
"ANTHROPIC_API_KEY",
|
|
"--minimal",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
data = tomllib.loads(target.read_text())
|
|
assert data["llm"]["provider"] == "anthropic"
|
|
assert data["llm"]["api_key_env"] == "ANTHROPIC_API_KEY"
|
|
assert "api_key" not in data["llm"]
|
|
assert _effective_config(target).squilla_router.enabled is True
|
|
assert "tier_profile" not in data["squilla_router"]
|
|
for tier in ("c0", "c1", "c2", "c3"):
|
|
assert data["squilla_router"]["tiers"][tier]["provider"] == "anthropic"
|
|
assert data["squilla_router"]["tiers"][tier]["model"] == "claude-3-5-sonnet-latest"
|
|
|
|
|
|
def test_onboard_noninteractive_provider_error_is_productized(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"onboard",
|
|
"--provider",
|
|
"ollama",
|
|
"--minimal",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 2
|
|
assert "model is required" in result.stderr
|
|
assert "--model <model-id>" in result.stderr
|
|
assert "Traceback" not in result.stdout + result.stderr
|
|
assert not target.exists()
|
|
|
|
|
|
def test_onboard_if_needed_skips_when_configured(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
runner.invoke(
|
|
app,
|
|
[
|
|
"onboard",
|
|
"--provider", "openrouter",
|
|
"--model", "x", "--api-key", "k",
|
|
"--skip-channels", "--skip-search",
|
|
],
|
|
)
|
|
mtime_before = target.stat().st_mtime
|
|
result = runner.invoke(app, ["onboard", "--if-needed"])
|
|
assert result.exit_code == 0
|
|
assert "core setup is ready" in result.stdout.lower()
|
|
assert "Optional next moves:" in result.stdout
|
|
assert "Channel recipes:" in result.stdout
|
|
assert "Image recipes:" in result.stdout
|
|
assert target.stat().st_mtime == mtime_before
|
|
|
|
|
|
def test_onboard_if_needed_uses_explicit_config_path(tmp_path, monkeypatch):
|
|
default_target = tmp_path / "default.toml"
|
|
target = tmp_path / "custom.toml"
|
|
target.write_text(
|
|
'[llm]\n'
|
|
'provider = "openrouter"\n'
|
|
'model = "deepseek/deepseek-v4-flash"\n'
|
|
'api_key_env = "CUSTOM_LLM_KEY"\n',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(default_target))
|
|
monkeypatch.setenv("CUSTOM_LLM_KEY", "sk-from-custom-env")
|
|
|
|
result = runner.invoke(app, ["onboard", "--if-needed", "--config", str(target)])
|
|
|
|
assert result.exit_code == 0
|
|
assert "core setup is ready" in result.stdout.lower()
|
|
assert "Optional next moves:" in result.stdout
|
|
assert f"--config{_config_arg(target)}" in "".join(result.stdout.split())
|
|
assert not default_target.exists()
|
|
|
|
|
|
def test_onboard_if_needed_skips_when_key_comes_from_env(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
target.write_text(
|
|
'[llm]\n'
|
|
'provider = "openrouter"\n'
|
|
'model = "deepseek/deepseek-v4-flash"\n'
|
|
'api_key_env = "OPENROUTER_API_KEY"\n',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-from-env")
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(app, ["onboard", "--if-needed"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "core setup is ready" in result.stdout.lower()
|
|
assert "Optional next moves:" in result.stdout
|
|
|
|
|
|
def test_onboard_if_needed_does_not_treat_env_as_config_without_config_file(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-from-env")
|
|
|
|
result = runner.invoke(app, ["onboard", "--if-needed"])
|
|
|
|
assert result.exit_code == 2
|
|
assert "requires a TTY" in result.stdout
|
|
assert "already complete" not in result.stdout.lower()
|
|
assert not target.exists()
|
|
|
|
|
|
def test_onboard_if_needed_accepts_default_env_key(tmp_path, monkeypatch):
|
|
# A config file that names the provider but omits api_key_env is ready when
|
|
# the provider's default env var (OPENROUTER_API_KEY) resolves: onboarding
|
|
# status mirrors the runtime, which reads that variable directly.
|
|
target = tmp_path / "c.toml"
|
|
target.write_text(
|
|
'[llm]\n'
|
|
'provider = "openrouter"\n'
|
|
'model = "deepseek/deepseek-v4-flash"\n',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-from-env")
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(app, ["onboard", "--if-needed"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "core setup is ready" in result.stdout.lower()
|
|
assert "requires a TTY" not in result.stdout
|
|
|
|
|
|
def test_onboard_if_needed_does_not_accept_settings_env_without_config_reference(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "c.toml"
|
|
target.write_text(
|
|
'[llm]\n'
|
|
'provider = "openrouter"\n'
|
|
'model = "deepseek/deepseek-v4-flash"\n',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("OPENSQUILLA_LLM_API_KEY", "sk-from-settings-env")
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(app, ["onboard", "--if-needed"])
|
|
|
|
assert result.exit_code == 2
|
|
assert "requires a TTY" in result.stdout
|
|
assert "already complete" not in result.stdout.lower()
|
|
|
|
|
|
def test_onboard_if_needed_does_not_accept_settings_env_with_empty_config(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "c.toml"
|
|
target.write_text("", encoding="utf-8")
|
|
monkeypatch.setenv("OPENSQUILLA_LLM_API_KEY", "sk-from-settings-env")
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(app, ["onboard", "--if-needed"])
|
|
|
|
assert result.exit_code == 2
|
|
assert "requires a TTY" in result.stdout
|
|
assert "already complete" not in result.stdout.lower()
|
|
|
|
|
|
def test_onboard_if_needed_requires_referenced_env_even_with_settings_env(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "c.toml"
|
|
target.write_text(
|
|
'[llm]\n'
|
|
'provider = "openrouter"\n'
|
|
'model = "deepseek/deepseek-v4-flash"\n'
|
|
'api_key_env = "OPENROUTER_API_KEY"\n',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
|
|
monkeypatch.setenv("OPENSQUILLA_LLM_API_KEY", "sk-from-settings-env")
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(app, ["onboard", "--if-needed"])
|
|
|
|
assert result.exit_code == 2
|
|
assert "requires a TTY" in result.stdout
|
|
assert "already complete" not in result.stdout.lower()
|
|
|
|
|
|
def test_onboard_status_uses_explicit_config_path(tmp_path, monkeypatch):
|
|
default_target = tmp_path / "default.toml"
|
|
target = tmp_path / "custom.toml"
|
|
target.write_text(
|
|
'[llm]\n'
|
|
'provider = "openrouter"\n'
|
|
'model = "deepseek/deepseek-v4-flash"\n'
|
|
'api_key_env = "CUSTOM_LLM_KEY"\n',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(default_target))
|
|
monkeypatch.delenv("CUSTOM_LLM_KEY", raising=False)
|
|
|
|
result = runner.invoke(app, ["onboard", "status", "--json", "--config", str(target)])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
payload = _json.loads(result.stdout)
|
|
assert payload["configPath"] == str(target)
|
|
assert payload["sections"]["llm"] == "degraded"
|
|
assert not default_target.exists()
|
|
|
|
|
|
@pytest.mark.parametrize("system_name", ["Linux", "Windows"])
|
|
def test_onboard_status_json_exposes_provider_section_alias(
|
|
tmp_path,
|
|
monkeypatch,
|
|
system_name,
|
|
):
|
|
monkeypatch.setattr(platform, "system", lambda: system_name)
|
|
target = tmp_path / "custom.toml"
|
|
target.write_text(
|
|
'[llm]\n'
|
|
'provider = "openrouter"\n'
|
|
'model = "deepseek/deepseek-v4-flash"\n'
|
|
'api_key_env = "CUSTOM_LLM_KEY"\n',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.delenv("CUSTOM_LLM_KEY", raising=False)
|
|
|
|
result = runner.invoke(app, ["onboard", "status", "--json", "--config", str(target)])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
payload = _json.loads(result.stdout)
|
|
assert payload["sections"]["provider"] == payload["sections"]["llm"]
|
|
assert payload["sectionDetails"]["provider"] == payload["sectionDetails"]["llm"]
|
|
assert payload["sectionDetails"]["provider"]["label"] == "Provider"
|
|
assert payload["sectionAliases"] == {"llm": "provider"}
|
|
assert payload["envRecoveryCommands"] == [
|
|
{
|
|
"section": "llm",
|
|
"label": "Set provider key",
|
|
"command": _env_command("CUSTOM_LLM_KEY"),
|
|
}
|
|
]
|
|
|
|
|
|
def test_onboard_status_reports_invalid_config_without_traceback(tmp_path):
|
|
target = tmp_path / "bad.toml"
|
|
target.write_text("[search]\n", encoding="utf-8")
|
|
|
|
result = runner.invoke(app, ["onboard", "status", "--config", str(target)])
|
|
|
|
assert result.exit_code == 2
|
|
assert "OpenSquilla config error" in result.stderr
|
|
assert _compact_text(str(target)) in _compact_text(result.stderr)
|
|
assert "search" in result.stderr
|
|
assert "Fix:" in result.stderr
|
|
assert (
|
|
f"opensquillaonboard--if-needed--config{_config_arg(target)}"
|
|
in "".join(result.stderr.split())
|
|
)
|
|
assert "Traceback" not in result.stderr
|
|
assert "pydantic_core" not in result.stderr
|
|
|
|
|
|
def test_onboard_status_table_keeps_explicit_config_path_in_next_step(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
default_target = tmp_path / "default.toml"
|
|
target = tmp_path / "custom.toml"
|
|
target.write_text(
|
|
'[llm]\n'
|
|
'provider = "openrouter"\n'
|
|
'model = "deepseek/deepseek-v4-flash"\n'
|
|
'api_key_env = "CUSTOM_LLM_KEY"\n',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(default_target))
|
|
monkeypatch.delenv("CUSTOM_LLM_KEY", raising=False)
|
|
|
|
result = runner.invoke(app, ["onboard", "status", "--config", str(target)])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert (
|
|
f"opensquillaonboard--if-needed--config{_config_arg(target)}"
|
|
in "".join(result.stdout.split())
|
|
)
|
|
assert not default_target.exists()
|
|
|
|
|
|
def test_onboard_status_table_offers_cli_web_and_recipe_setup_paths(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
default_target = tmp_path / "default.toml"
|
|
target = tmp_path / "custom.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(default_target))
|
|
|
|
result = runner.invoke(app, ["onboard", "status", "--config", str(target)])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
collapsed = "".join(result.stdout.split())
|
|
assert "Setup paths:" in result.stdout
|
|
assert "Guided CLI:" in result.stdout
|
|
assert (
|
|
f"opensquillaonboard--if-needed--config{_config_arg(target)}"
|
|
in collapsed
|
|
)
|
|
assert "Web UI:" in result.stdout
|
|
assert (
|
|
f"opensquillagatewayrun--config{_config_arg(target)}"
|
|
in collapsed
|
|
)
|
|
assert "http://127.0.0.1:18791/control/setup" in result.stdout
|
|
assert "Explore options:" in result.stdout
|
|
assert (
|
|
f"opensquillaonboardcatalog--config{_config_arg(target)}"
|
|
in collapsed
|
|
)
|
|
assert "opensquilla onboard catalog --json" not in result.stdout
|
|
assert "Provider recipes:" in result.stdout
|
|
assert (
|
|
"opensquillaonboardcatalogproviders"
|
|
f"--config{_config_arg(target)}"
|
|
in collapsed
|
|
)
|
|
assert "--provider<id>" not in collapsed
|
|
assert "--model<model>" not in collapsed
|
|
assert "--api-key-env<ENV_NAME>" not in collapsed
|
|
assert not default_target.exists()
|
|
|
|
|
|
def test_onboard_status_points_missing_provider_to_provider_recipes(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
default_target = tmp_path / "default.toml"
|
|
target = tmp_path / "custom.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(default_target))
|
|
|
|
result = runner.invoke(app, ["onboard", "status", "--config", str(target)])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
collapsed = "".join(result.stdout.split())
|
|
assert "Provider recipes:" in result.stdout
|
|
assert (
|
|
f"opensquillaonboardcatalogproviders--config{_config_arg(target)}"
|
|
in collapsed
|
|
)
|
|
assert "Headless provider:" not in result.stdout
|
|
assert "--provider <id>" not in result.stdout
|
|
assert "--model <model>" not in result.stdout
|
|
assert "--api-key-env <ENV_NAME>" not in result.stdout
|
|
assert not default_target.exists()
|
|
|
|
|
|
def test_onboard_status_leads_with_recommended_next_move(tmp_path, monkeypatch):
|
|
default_target = tmp_path / "default.toml"
|
|
target = tmp_path / "custom.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(default_target))
|
|
|
|
result = runner.invoke(app, ["onboard", "status", "--config", str(target)])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
collapsed = "".join(result.stdout.split())
|
|
assert "Recommended next move:" in result.stdout
|
|
assert "Guided CLI:" in result.stdout
|
|
assert (
|
|
f"GuidedCLI:opensquillaonboard--if-needed--config{_config_arg(target)}"
|
|
in collapsed
|
|
)
|
|
assert result.stdout.count("Guided CLI:") == 1
|
|
assert result.stdout.index("Recommended next move:") < result.stdout.index(
|
|
"Setup paths:"
|
|
)
|
|
assert not default_target.exists()
|
|
|
|
|
|
def test_onboard_status_web_path_uses_gateway_and_control_ui_config(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "custom.toml"
|
|
target.write_text(
|
|
'host = "0.0.0.0"\n'
|
|
"port = 19999\n"
|
|
"\n"
|
|
"[control_ui]\n"
|
|
'base_path = "/ops"\n',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(tmp_path / "default.toml"))
|
|
|
|
result = runner.invoke(app, ["onboard", "status", "--config", str(target)])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert "Web UI:" in result.stdout
|
|
assert "http://127.0.0.1:19999/ops/setup" in result.stdout
|
|
assert "http://0.0.0.0:19999" not in result.stdout
|
|
|
|
|
|
def test_onboard_status_does_not_offer_disabled_web_ui_path(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "custom.toml"
|
|
target.write_text(
|
|
"[control_ui]\n"
|
|
"enabled = false\n",
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(tmp_path / "default.toml"))
|
|
|
|
result = runner.invoke(app, ["onboard", "status", "--config", str(target)])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert "Guided CLI:" in result.stdout
|
|
assert "Provider recipes:" in result.stdout
|
|
assert "Web UI:" not in result.stdout
|
|
assert "/control/setup" not in result.stdout
|
|
|
|
|
|
def test_onboard_status_table_uses_product_labels_and_scope_column(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
default_target = tmp_path / "default.toml"
|
|
target = tmp_path / "custom.toml"
|
|
target.write_text(
|
|
'[llm]\n'
|
|
'provider = "openrouter"\n'
|
|
'model = "deepseek/deepseek-v4-flash"\n'
|
|
'api_key_env = "CUSTOM_LLM_KEY"\n',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(default_target))
|
|
monkeypatch.delenv("CUSTOM_LLM_KEY", raising=False)
|
|
|
|
result = runner.invoke(app, ["onboard", "status", "--config", str(target)])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert "OpenSquilla setup readiness" in result.stdout
|
|
assert "OpenSquilla ready:" in result.stdout
|
|
assert "Needs onboarding:" not in result.stdout
|
|
assert "Provider" in result.stdout
|
|
assert "Web search" in result.stdout
|
|
assert "Image generation" in result.stdout
|
|
assert "Memory embedding" in result.stdout
|
|
assert "Required" in result.stdout
|
|
assert "Optional" in result.stdout
|
|
assert "Later" in result.stdout
|
|
assert "env key not visible" in result.stdout
|
|
assert "llm" not in result.stdout
|
|
assert "missing_env" not in result.stdout
|
|
assert "image_generation" not in result.stdout
|
|
assert not default_target.exists()
|
|
|
|
|
|
@pytest.mark.parametrize("system_name", ["Linux", "Windows"])
|
|
def test_onboard_status_prioritizes_missing_env_recovery(
|
|
tmp_path,
|
|
monkeypatch,
|
|
system_name,
|
|
):
|
|
monkeypatch.setattr(platform, "system", lambda: system_name)
|
|
target = tmp_path / "custom.toml"
|
|
target.write_text(
|
|
'[llm]\n'
|
|
'provider = "openrouter"\n'
|
|
'model = "deepseek/deepseek-v4-flash"\n'
|
|
'api_key_env = "CUSTOM_LLM_KEY"\n',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.delenv("CUSTOM_LLM_KEY", raising=False)
|
|
|
|
result = runner.invoke(app, ["onboard", "status", "--config", str(target)])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert f"Set provider key: {_env_hint('CUSTOM_LLM_KEY')}" in _plain_text(
|
|
result.stdout
|
|
)
|
|
assert result.stdout.index("Set provider key:") < result.stdout.index("Guided CLI:")
|
|
|
|
|
|
def test_onboard_status_prints_action_guide_without_squeezing_detail(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "custom.toml"
|
|
target.write_text(
|
|
'[llm]\n'
|
|
'provider = "openrouter"\n'
|
|
'model = "deepseek/deepseek-v4-flash"\n'
|
|
'api_key_env = "CUSTOM_LLM_KEY"\n',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.delenv("CUSTOM_LLM_KEY", raising=False)
|
|
|
|
result = runner.invoke(app, ["onboard", "status", "--config", str(target)])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert "Action guide:" in result.stdout
|
|
assert "Fix: blocked or action-required" in result.stdout
|
|
assert "Review: ready" in result.stdout
|
|
assert "Configure: optional later" in result.stdout
|
|
assert "┃ Action" not in result.stdout.split("Action guide:", 1)[0]
|
|
assert "env key not visible" in result.stdout
|
|
|
|
|
|
def test_onboard_help_exposes_configure_command():
|
|
result = runner.invoke(app, ["onboard", "--help"])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert "status" in result.stdout
|
|
assert "catalog" in result.stdout
|
|
compact = _compact_text(result.stdout)
|
|
assert "Listonboardingsetupoptionsforeveryconfigurablesection." in compact
|
|
assert "configure" in result.stdout
|
|
assert (
|
|
"Reconfigureprovider,router,ensemble,channels,search,imagegeneration,"
|
|
"ormemory."
|
|
in compact
|
|
)
|
|
|
|
|
|
def test_onboard_catalog_json_exposes_all_setup_options(tmp_path, monkeypatch):
|
|
target = tmp_path / "custom.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(app, ["onboard", "catalog", "--json", "--config", str(target)])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
payload = _json.loads(result.stdout)
|
|
assert {
|
|
"providers",
|
|
"routerProfiles",
|
|
"searchProviders",
|
|
"channels",
|
|
"imageGenerationProviders",
|
|
"memoryEmbeddingProviders",
|
|
} <= payload.keys()
|
|
assert any(row["providerId"] == "openrouter" for row in payload["providers"])
|
|
assert any(row["providerId"] == "duckduckgo" for row in payload["searchProviders"])
|
|
assert any(row["providerId"] == "openrouter" for row in payload["imageGenerationProviders"])
|
|
assert any(row["providerId"] == "auto" for row in payload["memoryEmbeddingProviders"])
|
|
assert any(row["type"] == "discord" for row in payload["channels"])
|
|
assert not target.exists()
|
|
|
|
|
|
def test_onboard_catalog_help_names_short_capability_section_aliases():
|
|
result = runner.invoke(app, ["onboard", "catalog", "--help"])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
compact = _compact_text(result.stdout)
|
|
assert "image(aliasforimage-generation)" in compact
|
|
assert "memory(aliasformemory-embedding)" in compact
|
|
|
|
|
|
def test_onboard_catalog_can_focus_one_setup_section():
|
|
result = runner.invoke(app, ["onboard", "catalog", "search", "--json"])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
payload = _json.loads(result.stdout)
|
|
assert list(payload) == ["searchProviders"]
|
|
assert any(row["providerId"] == "duckduckgo" for row in payload["searchProviders"])
|
|
assert all("requiresApiKey" in row for row in payload["searchProviders"])
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("alias", "section_key", "expected"),
|
|
[
|
|
("image", "imageGenerationProviders", "openrouter/google/gemini"),
|
|
("memory", "memoryEmbeddingProviders", "openai-compatible"),
|
|
],
|
|
)
|
|
def test_onboard_catalog_accepts_short_capability_section_aliases(
|
|
alias,
|
|
section_key,
|
|
expected,
|
|
):
|
|
result = runner.invoke(app, ["onboard", "catalog", alias, "--json"])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
payload = _json.loads(result.stdout)
|
|
assert list(payload) == [section_key]
|
|
assert expected in result.stdout
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("section", "expected"),
|
|
[
|
|
("providers", ["openrouter", "OPENROUTER_API_KEY", "deepseek/deepseek-v4-pro"]),
|
|
("router", ["recommended", "openrouter-mix", "c0", "c3"]),
|
|
("search", ["duckduckgo", "brave", "BRAVE_SEARCH_API_KEY"]),
|
|
("channels", ["discord", "Bot token", "opensquilla channels describe discord --json"]),
|
|
("image-generation", ["openrouter", "openrouter/google/gemini", "OPENROUTER_API_KEY"]),
|
|
("memory-embedding", ["auto", "local", "openai-compatible", "FTS-only"]),
|
|
],
|
|
)
|
|
def test_onboard_catalog_focused_human_output_lists_usable_options(section, expected):
|
|
result = runner.invoke(app, ["onboard", "catalog", section])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
for text in expected:
|
|
assert text in result.stdout
|
|
|
|
|
|
def test_onboard_catalog_provider_rows_name_squillarouter_tier_support():
|
|
result = runner.invoke(app, ["onboard", "catalog", "providers"])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert "route SquillaRouter ready" in result.stdout
|
|
assert "route Direct only" in result.stdout
|
|
openrouter_row = next(
|
|
line for line in result.stdout.splitlines() if line.startswith("- openrouter:")
|
|
)
|
|
anthropic_row = next(
|
|
line for line in result.stdout.splitlines() if line.startswith("- anthropic:")
|
|
)
|
|
assert openrouter_row.index("route SquillaRouter ready") < openrouter_row.index(
|
|
"key OPENROUTER_API_KEY"
|
|
)
|
|
assert anthropic_row.index("route Direct only") < anthropic_row.index(
|
|
"key ANTHROPIC_API_KEY"
|
|
)
|
|
assert "SquillaRouter tiers yes" not in result.stdout
|
|
assert "SquillaRouter tiers no" not in result.stdout
|
|
assert "router yes" not in result.stdout
|
|
assert "router no" not in result.stdout
|
|
|
|
|
|
def test_onboard_catalog_focused_commands_are_actionable_with_config_path(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "catalog-target.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(app, ["onboard", "catalog", "channels", "--config", str(target)])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert (
|
|
"Try: opensquilla onboard configure channels --channel-type discord "
|
|
"--name <name> --token <token>"
|
|
) in result.stdout
|
|
assert f"--config {_config_arg(target)}" in result.stdout
|
|
assert "opensquilla onboard configure channels --channel-type <type>\n" not in result.stdout
|
|
assert "Configure with:" not in result.stdout
|
|
assert not target.exists()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"section, forbidden",
|
|
[
|
|
("providers", "opensquilla onboard configure provider --provider <id>"),
|
|
("search", "--search-provider <provider>"),
|
|
("channels", "opensquilla onboard configure channels --channel-type <type>"),
|
|
("image-generation", "--image-provider <provider>"),
|
|
("memory-embedding", "--memory-provider <provider>"),
|
|
],
|
|
)
|
|
def test_onboard_catalog_focused_output_uses_recipes_not_generic_templates(
|
|
section,
|
|
forbidden,
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "focused-catalog.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(app, ["onboard", "catalog", section, "--config", str(target)])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert "Copy a Try line" in result.stdout
|
|
assert "Configure with:" not in result.stdout
|
|
assert forbidden not in result.stdout
|
|
assert f"--config {_config_arg(target)}" in result.stdout
|
|
assert not target.exists()
|
|
|
|
|
|
def test_onboard_catalog_router_focus_offers_a_real_recipe(tmp_path, monkeypatch):
|
|
target = tmp_path / "router-catalog.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(app, ["onboard", "catalog", "router", "--config", str(target)])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert "Try: opensquilla onboard configure router --router recommended --default-tier c1" in (
|
|
result.stdout
|
|
)
|
|
assert "Configure with:" not in result.stdout
|
|
assert f"--config {_config_arg(target)}" in result.stdout
|
|
assert not target.exists()
|
|
|
|
|
|
def test_onboard_catalog_focused_provider_examples_match_key_requirements(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "provider-catalog.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["onboard", "catalog", "providers", "--config", str(target)],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert (
|
|
"Try: opensquilla onboard configure provider --provider openrouter "
|
|
"--model deepseek/deepseek-v4-pro --api-key-env OPENROUTER_API_KEY "
|
|
f"--config {_config_arg(target)}"
|
|
) in result.stdout
|
|
assert (
|
|
"Try: opensquilla onboard configure provider --provider anthropic "
|
|
"--model <model> --api-key-env ANTHROPIC_API_KEY "
|
|
f"--config {_config_arg(target)}"
|
|
) in result.stdout
|
|
assert (
|
|
"Try: opensquilla onboard configure provider --provider ollama "
|
|
"--model <model> "
|
|
f"--config {_config_arg(target)}"
|
|
) in result.stdout
|
|
ollama_line = next(
|
|
line
|
|
for line in result.stdout.splitlines()
|
|
if line.startswith(" Try: opensquilla onboard configure provider --provider ollama")
|
|
)
|
|
assert "--api-key-env" not in ollama_line
|
|
assert not target.exists()
|
|
|
|
|
|
def test_onboard_catalog_focused_capability_examples_match_key_requirements(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "capability-catalog.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
search = runner.invoke(
|
|
app,
|
|
["onboard", "catalog", "search", "--config", str(target)],
|
|
)
|
|
memory = runner.invoke(
|
|
app,
|
|
["onboard", "catalog", "memory-embedding", "--config", str(target)],
|
|
)
|
|
|
|
assert search.exit_code == 0, search.stdout
|
|
assert (
|
|
"Try: opensquilla onboard configure search --search-provider brave "
|
|
"--api-key-env BRAVE_SEARCH_API_KEY "
|
|
f"--config {_config_arg(target)}"
|
|
) in search.stdout
|
|
assert (
|
|
"Try: opensquilla onboard configure search --search-provider duckduckgo "
|
|
f"--config {_config_arg(target)}"
|
|
) in search.stdout
|
|
duckduckgo_line = next(
|
|
line
|
|
for line in search.stdout.splitlines()
|
|
if line.startswith(
|
|
" Try: opensquilla onboard configure search --search-provider duckduckgo"
|
|
)
|
|
)
|
|
assert "--api-key-env" not in duckduckgo_line
|
|
|
|
assert memory.exit_code == 0, memory.stdout
|
|
assert (
|
|
"Try: opensquilla onboard configure memory --memory-provider auto "
|
|
f"--config {_config_arg(target)}"
|
|
) in memory.stdout
|
|
assert (
|
|
"Try: opensquilla onboard configure memory --memory-provider openai "
|
|
"--api-key-env OPENAI_API_KEY "
|
|
f"--config {_config_arg(target)}"
|
|
) in memory.stdout
|
|
auto_line = next(
|
|
line
|
|
for line in memory.stdout.splitlines()
|
|
if line.startswith(
|
|
" Try: opensquilla onboard configure memory --memory-provider auto"
|
|
)
|
|
)
|
|
assert "--api-key-env" not in auto_line
|
|
assert "--model" not in auto_line
|
|
assert not target.exists()
|
|
|
|
|
|
def test_onboard_catalog_focused_channel_examples_include_required_fields(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "channel-catalog.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["onboard", "catalog", "channels", "--config", str(target)],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert (
|
|
"Try: opensquilla onboard configure channels --channel-type discord "
|
|
"--name <name> --token <token> "
|
|
f"--config {_config_arg(target)}"
|
|
) in result.stdout
|
|
assert (
|
|
"Try: opensquilla onboard configure channels --channel-type feishu "
|
|
"--name <name> --field app_id=<value> --field app_secret=<value> "
|
|
f"--config {_config_arg(target)}"
|
|
) in result.stdout
|
|
assert "opensquilla channels describe feishu --json" in result.stdout
|
|
assert not target.exists()
|
|
|
|
|
|
def test_onboard_catalog_focused_image_and_metadata_search_examples_are_specific(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "image-catalog.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
image = runner.invoke(
|
|
app,
|
|
["onboard", "catalog", "image-generation", "--config", str(target)],
|
|
)
|
|
search = runner.invoke(
|
|
app,
|
|
["onboard", "catalog", "search", "--config", str(target)],
|
|
)
|
|
|
|
assert image.exit_code == 0, image.stdout
|
|
assert (
|
|
"Try: opensquilla onboard configure image "
|
|
"--image-provider openrouter "
|
|
"--primary openrouter/google/gemini-3.1-flash-image-preview "
|
|
"--api-key-env OPENROUTER_API_KEY "
|
|
f"--config {_config_arg(target)}"
|
|
) in image.stdout
|
|
|
|
assert search.exit_code == 0, search.stdout
|
|
assert "- exa: Exa | ready | key EXA_API_KEY | cloud" in search.stdout
|
|
assert (
|
|
"Try: opensquilla onboard configure search --search-provider exa "
|
|
"--api-key-env EXA_API_KEY "
|
|
f"--config {_config_arg(target)}"
|
|
) in search.stdout
|
|
assert "- perplexity: Perplexity | metadata only" in search.stdout
|
|
assert "Try: not configurable in this build" in search.stdout
|
|
assert not target.exists()
|
|
|
|
|
|
def test_onboard_catalog_overview_commands_keep_active_config_path(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "catalog-overview.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(app, ["onboard", "catalog", "--config", str(target)])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
compact = result.stdout.replace(" ", "").replace("\n", "")
|
|
assert (
|
|
f"opensquillaonboardcatalogproviders--config{_config_arg(target)}".replace(
|
|
" ", ""
|
|
)
|
|
in compact
|
|
)
|
|
assert (
|
|
f"opensquillaonboardcatalogchannels--config{_config_arg(target)}".replace(
|
|
" ", ""
|
|
)
|
|
in compact
|
|
)
|
|
assert (
|
|
f"opensquillaonboardcatalogrouter--config{_config_arg(target)}".replace(" ", "")
|
|
in compact
|
|
)
|
|
assert (
|
|
f"opensquillaonboardcatalogsearch--config{_config_arg(target)}".replace(" ", "")
|
|
in compact
|
|
)
|
|
assert (
|
|
f"opensquillaonboardcatalogimage--config{_config_arg(target)}".replace(
|
|
" ", ""
|
|
)
|
|
in compact
|
|
)
|
|
assert (
|
|
f"opensquillaonboardcatalogmemory--config{_config_arg(target)}".replace(
|
|
" ", ""
|
|
)
|
|
in compact
|
|
)
|
|
assert "Open section" in result.stdout
|
|
assert "option-specific Try commands" in result.stdout
|
|
assert not target.exists()
|
|
|
|
|
|
def test_onboard_catalog_overview_hides_generic_configure_templates(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "catalog-overview.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(app, ["onboard", "catalog", "--config", str(target)])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert "Configure templates:" not in result.stdout
|
|
assert "opensquilla onboard configure provider --provider <id>" not in result.stdout
|
|
assert "--api-key-env <ENV_NAME>" not in result.stdout
|
|
assert "--search-provider <provider>" not in result.stdout
|
|
assert "--memory-provider <provider>" not in result.stdout
|
|
assert not target.exists()
|
|
|
|
|
|
def test_onboard_configure_help_groups_options_by_setup_area():
|
|
result = runner.invoke(app, ["onboard", "configure", "--help"])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
expected_panels = [
|
|
"Target section",
|
|
"Text provider",
|
|
"Shared keys and endpoints",
|
|
"Router",
|
|
"Search",
|
|
"Channels",
|
|
"Image generation",
|
|
"Memory embedding",
|
|
"Global",
|
|
]
|
|
for panel in expected_panels:
|
|
assert panel in result.stdout
|
|
|
|
expected_option_locations = [
|
|
("Text provider", "--provider"),
|
|
("Shared keys and endpoints", "--model"),
|
|
("Shared keys and endpoints", "--api-key-env"),
|
|
("Shared keys and endpoints", "--base-url"),
|
|
("Shared keys and endpoints", "--proxy"),
|
|
("Router", "--router"),
|
|
("Search", "--search-provider"),
|
|
("Channels", "--channel-type"),
|
|
("Image generation", "--image-provider"),
|
|
("Memory embedding", "--memory-provider"),
|
|
("Global", "--config"),
|
|
]
|
|
for panel, option in expected_option_locations:
|
|
assert result.stdout.index(panel) < result.stdout.index(option)
|
|
|
|
compact = _compact_text(result.stdout)
|
|
assert "APIkeyforprovider,search,imagegeneration,ormemoryembedding." in compact
|
|
assert "image(aliasforimage-generation)" in compact
|
|
assert "memory(aliasformemory-embedding)" in compact
|
|
|
|
|
|
def test_onboard_status_summarizes_blocking_and_optional_sections(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "custom.toml"
|
|
target.write_text(
|
|
'[llm]\n'
|
|
'provider = "openrouter"\n'
|
|
'model = "deepseek/deepseek-v4-flash"\n'
|
|
'api_key_env = "CUSTOM_LLM_KEY"\n',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.delenv("CUSTOM_LLM_KEY", raising=False)
|
|
|
|
result = runner.invoke(app, ["onboard", "status", "--config", str(target)])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert "OpenSquilla Setup Cockpit" in result.stdout
|
|
assert "Blocking setup: Provider" in result.stdout
|
|
status_plain = _plain_text(result.stdout)
|
|
assert "Optional later: LLM ensemble, Channels, Image generation, Voice audio" in status_plain
|
|
assert "llm" not in result.stdout
|
|
assert "image_generation" not in result.stdout
|
|
|
|
|
|
def test_onboard_status_explains_router_ready_before_provider_setup(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "custom.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(tmp_path / "default.toml"))
|
|
|
|
result = runner.invoke(app, ["onboard", "status", "--config", str(target)])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert "Router" in result.stdout
|
|
collapsed = "".join(result.stdout.split())
|
|
assert "Provider first" in result.stdout
|
|
assert "usesSquillaRouterafter" in collapsed
|
|
assert "providersetup" in collapsed
|
|
|
|
json_result = runner.invoke(
|
|
app,
|
|
["onboard", "status", "--json", "--config", str(target)],
|
|
)
|
|
assert json_result.exit_code == 0, json_result.stdout
|
|
payload = _json.loads(json_result.stdout)
|
|
assert (
|
|
payload["sectionDetails"]["router"]["detail"]
|
|
== "uses SquillaRouter after provider setup"
|
|
)
|
|
|
|
|
|
def test_onboard_status_table_hides_image_generation_internal_source(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "custom.toml"
|
|
target.write_text(
|
|
'[llm]\n'
|
|
'provider = "openrouter"\n'
|
|
'model = "deepseek/deepseek-v4-flash"\n'
|
|
'api_key = "sk-or"\n'
|
|
'\n'
|
|
'[image_generation]\n'
|
|
'enabled = true\n'
|
|
'primary = "openrouter/google/gemini-3.1-flash-image-preview"\n',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
|
|
|
|
result = runner.invoke(app, ["onboard", "status", "--config", str(target)])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert "Image generation" in result.stdout
|
|
assert "openrouter (same provider key)" in result.stdout
|
|
assert "llm_fallback" not in result.stdout
|
|
|
|
|
|
def test_onboard_status_table_names_missing_env_keys_for_optional_capabilities(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "custom.toml"
|
|
target.write_text(
|
|
'search_provider = "brave"\n'
|
|
'search_api_key_env = "BRAVE_SEARCH_API_KEY"\n'
|
|
"\n"
|
|
"[llm]\n"
|
|
'provider = "openrouter"\n'
|
|
'model = "deepseek/deepseek-v4-flash"\n'
|
|
'api_key = "sk-or"\n'
|
|
"\n"
|
|
"[image_generation]\n"
|
|
"enabled = true\n"
|
|
'primary = "openai/gpt-image-1"\n'
|
|
"\n"
|
|
"[image_generation.providers.openai]\n"
|
|
'api_key_env = "OPENAI_IMAGE_KEY"\n'
|
|
"\n"
|
|
"[memory.embedding]\n"
|
|
'provider = "openai"\n'
|
|
"\n"
|
|
"[memory.embedding.remote]\n"
|
|
'api_key_env = "OPENAI_EMBEDDINGS_API_KEY"\n',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.delenv("BRAVE_SEARCH_API_KEY", raising=False)
|
|
monkeypatch.delenv("OPENAI_IMAGE_KEY", raising=False)
|
|
monkeypatch.delenv("OPENAI_EMBEDDINGS_API_KEY", raising=False)
|
|
|
|
result = runner.invoke(app, ["onboard", "status", "--config", str(target)])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert "Blocking setup: Memory embedding" in result.stdout
|
|
assert "Web search" in result.stdout
|
|
assert "env key not visible" in result.stdout
|
|
assert "BRAVE_SEARCH_API_KEY" in result.stdout
|
|
assert "Image generation" in result.stdout
|
|
assert "openai" in result.stdout
|
|
assert "OPENAI_IMAGE_KEY" in result.stdout
|
|
assert "Memory embedding" in result.stdout
|
|
assert "OPENAI_EMBEDDINGS_API_KEY" in result.stdout
|
|
stdout_plain = _plain_text(result.stdout)
|
|
assert f"Set search key: {_env_hint('BRAVE_SEARCH_API_KEY')}" in stdout_plain
|
|
assert f"Set image key: {_env_hint('OPENAI_IMAGE_KEY')}" in stdout_plain
|
|
assert (
|
|
f"Set memory key: {_env_hint('OPENAI_EMBEDDINGS_API_KEY')}" in stdout_plain
|
|
)
|
|
assert "Fix now:" in result.stdout
|
|
assert result.stdout.index("Fix now:") < result.stdout.index("Set memory key:")
|
|
assert result.stdout.index("Set image key:") < result.stdout.index("Setup paths:")
|
|
assert "Set search key:" not in result.stdout.split("Setup paths:", 1)[1]
|
|
assert "Web UI after env fix:" in result.stdout
|
|
assert result.stdout.index("Set memory key:") < result.stdout.index("Set search key:")
|
|
assert "not configured" not in result.stdout
|
|
|
|
from opensquilla.cli.onboard_cmd import _status_cockpit_summary
|
|
from opensquilla.onboarding.config_store import load_config
|
|
from opensquilla.onboarding.status import get_onboarding_status
|
|
|
|
summary = _status_cockpit_summary(get_onboarding_status(load_config(target)))
|
|
assert summary == (
|
|
"Blocking setup: Memory embedding"
|
|
" · Optional later: LLM ensemble, Web search, Channels, Image generation, Voice audio"
|
|
)
|
|
|
|
|
|
def test_onboard_status_offers_optional_capability_paths_when_core_ready(
|
|
tmp_path,
|
|
):
|
|
target = tmp_path / "custom.toml"
|
|
target.write_text(
|
|
"[llm]\n"
|
|
'provider = "openrouter"\n'
|
|
'model = "deepseek/deepseek-v4-flash"\n'
|
|
'api_key = "sk-or"\n',
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = runner.invoke(app, ["onboard", "status", "--config", str(target)])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert "OpenSquilla ready: yes" in result.stdout
|
|
assert "Optional next moves:" in result.stdout
|
|
assert "Web UI:" in result.stdout
|
|
assert "Explore options:" in result.stdout
|
|
assert "Setup catalog:" not in result.stdout
|
|
assert "opensquilla onboard catalog --json" not in result.stdout
|
|
assert "Channel recipes:" in result.stdout
|
|
compact = "".join(result.stdout.split())
|
|
assert f"opensquillaonboardcatalog--config{_config_arg(target)}" in compact
|
|
assert f"opensquillaonboardcatalogchannels--config{_config_arg(target)}" in compact
|
|
assert "opensquillachannelsdescribe<type>--json" not in compact
|
|
assert "opensquillaonboardconfigurechannels--channel-type<type>" not in compact
|
|
assert "Image recipes:" in result.stdout
|
|
assert (
|
|
f"opensquillaonboardcatalogimage--config{_config_arg(target)}"
|
|
in compact
|
|
)
|
|
assert "opensquillaonboardconfigureimage--image-provider<provider>" not in compact
|
|
assert f"--config{_config_arg(target)}" in compact
|
|
assert "Provider recipes:" not in result.stdout
|
|
|
|
|
|
def test_onboard_status_offers_ready_next_moves_when_all_sections_ready(
|
|
tmp_path,
|
|
):
|
|
target = tmp_path / "custom.toml"
|
|
target.write_text(
|
|
'search_provider = "duckduckgo"\n'
|
|
"\n"
|
|
"[llm]\n"
|
|
'provider = "openrouter"\n'
|
|
'model = "deepseek/deepseek-v4-flash"\n'
|
|
'api_key = "sk-or"\n'
|
|
"\n"
|
|
"[image_generation]\n"
|
|
"enabled = true\n"
|
|
'primary = "openrouter/google/gemini-3.1-flash-image-preview"\n'
|
|
"\n"
|
|
"[[channels.channels]]\n"
|
|
'type = "slack"\n'
|
|
'name = "w"\n'
|
|
'token = "xoxb-test"\n'
|
|
"\n"
|
|
"[audio]\n"
|
|
"enabled = true\n"
|
|
"\n"
|
|
"[audio.providers.elevenlabs]\n"
|
|
'api_key = "el-test"\n',
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = runner.invoke(app, ["onboard", "status", "--config", str(target)])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert "OpenSquilla ready: yes" in result.stdout
|
|
assert "Optional next moves:" in result.stdout
|
|
assert "Headless ensemble:" in result.stdout
|
|
assert "opensquilla onboard configure ensemble --enabled" in result.stdout
|
|
assert "Ready next moves:" not in result.stdout
|
|
assert "Recommended next move:" not in result.stdout
|
|
|
|
|
|
def test_onboard_if_needed_non_tty_hint_keeps_explicit_config_path(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
default_target = tmp_path / "default.toml"
|
|
target = tmp_path / "custom.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(default_target))
|
|
|
|
result = runner.invoke(app, ["onboard", "--if-needed", "--config", str(target)])
|
|
|
|
assert result.exit_code == 2
|
|
assert f"--config {_config_arg(target)}" in result.stdout
|
|
assert "Guided CLI:" in result.stdout
|
|
assert "Web UI:" in result.stdout
|
|
assert "http://127.0.0.1:18791/control/setup" in result.stdout
|
|
assert "Provider recipes:" in result.stdout
|
|
assert "Check status:" in result.stdout
|
|
assert not default_target.exists()
|
|
|
|
|
|
def test_onboard_if_needed_non_tty_hint_prioritizes_runnable_paths(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "custom.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(app, ["onboard", "--if-needed"])
|
|
|
|
assert result.exit_code == 2
|
|
assert "Use a runnable setup path from this shell:" in result.stdout
|
|
assert result.stdout.index("Web UI:") < result.stdout.index("Provider recipes:")
|
|
assert result.stdout.index("Provider recipes:") < result.stdout.index("Guided CLI:")
|
|
assert "Guided CLI: opensquilla onboard --if-needed" in result.stdout
|
|
assert "(interactive terminal only)" in result.stdout
|
|
assert not target.exists()
|
|
|
|
|
|
def test_onboard_if_needed_non_tty_hint_stays_provider_neutral(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "custom.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(app, ["onboard", "--if-needed"])
|
|
|
|
assert result.exit_code == 2
|
|
assert "Provider recipes:" in result.stdout
|
|
assert "opensquilla onboard catalog providers" in result.stdout
|
|
assert "opensquilla onboard configure provider --provider <id>" not in result.stdout
|
|
assert "--model <model>" not in result.stdout
|
|
assert "--api-key-env <ENV_NAME>" not in result.stdout
|
|
assert "openrouter" not in result.stdout.lower()
|
|
assert "brave" not in result.stdout.lower()
|
|
assert "slack" not in result.stdout.lower()
|
|
assert "configure search" not in result.stdout
|
|
assert "channels add" not in result.stdout
|
|
assert not target.exists()
|
|
|
|
|
|
def test_onboard_if_needed_non_tty_hint_hides_disabled_web_ui_path(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "custom.toml"
|
|
target.write_text("[control_ui]\nenabled = false\n", encoding="utf-8")
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(app, ["onboard", "--if-needed"])
|
|
|
|
assert result.exit_code == 2
|
|
assert "Guided CLI:" in result.stdout
|
|
assert "Provider recipes:" in result.stdout
|
|
assert "Check status:" in result.stdout
|
|
assert result.stdout.index("Provider recipes:") < result.stdout.index("Guided CLI:")
|
|
assert "Web UI:" not in result.stdout
|
|
assert "/control/setup" not in result.stdout
|
|
|
|
|
|
def test_onboard_if_needed_non_tty_hint_targets_blocking_memory_section(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "custom.toml"
|
|
target.write_text(
|
|
"[llm]\n"
|
|
'provider = "openrouter"\n'
|
|
'model = "deepseek/deepseek-v4-flash"\n'
|
|
'api_key = "sk-or"\n'
|
|
"\n"
|
|
"[memory.embedding]\n"
|
|
'provider = "openai"\n'
|
|
"\n"
|
|
"[memory.embedding.remote]\n"
|
|
'api_key_env = "OPENAI_EMBEDDINGS_API_KEY"\n',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.delenv("OPENAI_EMBEDDINGS_API_KEY", raising=False)
|
|
|
|
result = runner.invoke(app, ["onboard", "--if-needed", "--config", str(target)])
|
|
|
|
assert result.exit_code == 2
|
|
assert "Use a runnable setup path from this shell:" in result.stdout
|
|
assert "Headless memory embedding:" in result.stdout
|
|
assert (
|
|
"opensquilla onboard configure memory --memory-provider auto"
|
|
in result.stdout
|
|
)
|
|
assert f"--config {_config_arg(target)}" in result.stdout
|
|
assert "Provider recipes:" not in result.stdout
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("section", "label", "command"),
|
|
[
|
|
(
|
|
"provider",
|
|
"Provider recipes:",
|
|
"opensquilla onboard catalog providers",
|
|
),
|
|
(
|
|
"router",
|
|
"Headless router:",
|
|
"opensquilla onboard configure router --router recommended --default-tier c1",
|
|
),
|
|
(
|
|
"ensemble",
|
|
"Headless ensemble:",
|
|
"opensquilla onboard configure ensemble --enabled",
|
|
),
|
|
(
|
|
"channels",
|
|
"Channel recipes:",
|
|
"opensquilla onboard catalog channels",
|
|
),
|
|
(
|
|
"search",
|
|
"Headless search:",
|
|
"opensquilla onboard configure search --search-provider duckduckgo",
|
|
),
|
|
(
|
|
"image-generation",
|
|
"Image recipes:",
|
|
"opensquilla onboard catalog image",
|
|
),
|
|
(
|
|
"memory-embedding",
|
|
"Headless memory embedding:",
|
|
"opensquilla onboard configure memory --memory-provider auto",
|
|
),
|
|
],
|
|
)
|
|
def test_configure_without_tty_hint_targets_selected_section(
|
|
tmp_path,
|
|
monkeypatch,
|
|
section,
|
|
label,
|
|
command,
|
|
):
|
|
target = tmp_path / "custom.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(app, ["configure", section, "--config", str(target)])
|
|
|
|
# A non-TTY fallthrough is a failure to configure anything: exit 2 like
|
|
# bare `onboard`, with the hint still naming runnable setup paths.
|
|
assert result.exit_code == 2
|
|
assert label in result.stdout
|
|
assert command in result.stdout
|
|
assert f"--config {_config_arg(target)}" in result.stdout
|
|
assert "opensquilla onboard configure provider --provider <id>" not in result.stdout
|
|
assert "opensquilla channels describe <type>" not in result.stdout
|
|
assert "--image-provider <provider>" not in result.stdout
|
|
assert not target.exists()
|
|
|
|
|
|
def test_onboard_configure_provider_alias_uses_setup_engine(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"onboard",
|
|
"configure",
|
|
"provider",
|
|
"--provider",
|
|
"openrouter",
|
|
"--model",
|
|
"deepseek/deepseek-v4-flash",
|
|
"--api-key-env",
|
|
"OPENROUTER_API_KEY",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
data = tomllib.loads(target.read_text())
|
|
assert _effective_config(target).llm.provider == "openrouter"
|
|
assert data["llm"]["api_key_env"] == "OPENROUTER_API_KEY"
|
|
assert "api_key" not in data["llm"]
|
|
|
|
|
|
def test_configure_provider_noninteractive_uses_setup_engine(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"configure",
|
|
"provider",
|
|
"--provider",
|
|
"openrouter",
|
|
"--model",
|
|
"deepseek/deepseek-v4-flash",
|
|
"--api-key-env",
|
|
"OPENROUTER_API_KEY",
|
|
"--proxy",
|
|
"http://127.0.0.1:7890",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
data = tomllib.loads(target.read_text())
|
|
assert _effective_config(target).llm.provider == "openrouter"
|
|
assert data["llm"]["api_key_env"] == "OPENROUTER_API_KEY"
|
|
assert data["llm"]["proxy"] == "http://127.0.0.1:7890"
|
|
assert "api_key" not in data["llm"]
|
|
|
|
|
|
def test_configure_provider_uses_explicit_config_path(tmp_path, monkeypatch):
|
|
default_target = tmp_path / "default.toml"
|
|
target = tmp_path / "custom.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(default_target))
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"configure",
|
|
"provider",
|
|
"--provider",
|
|
"openrouter",
|
|
"--model",
|
|
"deepseek/deepseek-v4-flash",
|
|
"--api-key-env",
|
|
"OPENROUTER_API_KEY",
|
|
"--config",
|
|
str(target),
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
data = tomllib.loads(target.read_text())
|
|
assert _effective_config(target).llm.provider == "openrouter"
|
|
assert data["llm"]["api_key_env"] == "OPENROUTER_API_KEY"
|
|
assert not default_target.exists()
|
|
|
|
|
|
def test_configure_provider_can_omit_model_for_router_profile(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"configure",
|
|
"provider",
|
|
"--provider",
|
|
"deepseek",
|
|
"--api-key-env",
|
|
"DEEPSEEK_API_KEY",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
data = tomllib.loads(target.read_text())
|
|
assert data["llm"]["provider"] == "deepseek"
|
|
assert data["llm"]["model"] == "deepseek-v4-flash"
|
|
|
|
|
|
def test_configure_provider_recomputes_existing_router_profile(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
target.write_text(
|
|
'[llm]\nprovider = "deepseek"\nmodel = "deepseek-chat"\n'
|
|
'[squilla_router]\ntier_profile = "deepseek"\n',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"configure",
|
|
"provider",
|
|
"--provider",
|
|
"openai",
|
|
"--model",
|
|
"gpt-5.4-mini",
|
|
"--api-key-env",
|
|
"OPENAI_API_KEY",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
data = tomllib.loads(target.read_text())
|
|
assert data["llm"]["provider"] == "openai"
|
|
assert data["squilla_router"]["tier_profile"] == "openai"
|
|
assert "tiers" not in data["squilla_router"]
|
|
|
|
|
|
def test_configure_saved_path_escapes_rich_markup_chars(tmp_path, monkeypatch):
|
|
root = tmp_path / "opensquilla-[review]"
|
|
root.mkdir()
|
|
target = root / "config[dev].toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"configure",
|
|
"provider",
|
|
"--provider",
|
|
"openrouter",
|
|
"--model",
|
|
"deepseek/deepseek-v4-flash",
|
|
"--api-key-env",
|
|
"OPENROUTER_API_KEY",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert str(target) in result.stdout
|
|
|
|
|
|
def test_configure_provider_errors_go_to_stderr(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(app, ["configure", "provider", "--provider", "not-a-provider"])
|
|
|
|
assert result.exit_code == 2
|
|
assert "unknown provider" in result.stderr
|
|
assert "unknown provider" not in result.stdout
|
|
|
|
|
|
def test_configure_provider_reports_invalid_config_without_schema_traceback(tmp_path):
|
|
target = tmp_path / "bad.toml"
|
|
target.write_text("[search]\n", encoding="utf-8")
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"configure",
|
|
"provider",
|
|
"--provider",
|
|
"deepseek",
|
|
"--model",
|
|
"deepseek-chat",
|
|
"--api-key-env",
|
|
"DEEPSEEK_API_KEY",
|
|
"--config",
|
|
str(target),
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 2
|
|
assert "OpenSquilla config error" in result.stderr
|
|
assert _compact_text(str(target)) in _compact_text(result.stderr)
|
|
assert "search" in result.stderr
|
|
assert (
|
|
f"opensquillaonboard--if-needed--config{_config_arg(target)}"
|
|
in "".join(result.stderr.split())
|
|
)
|
|
assert "pydantic.dev" not in result.stderr
|
|
assert "Traceback" not in result.stderr
|
|
|
|
|
|
def test_configure_router_noninteractive_can_disable(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
target.write_text(
|
|
'[llm]\nprovider = "openrouter"\nmodel = "deepseek/deepseek-v4-flash"\n',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(app, ["configure", "router", "--router", "disabled"])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
data = tomllib.loads(target.read_text())
|
|
assert data["squilla_router"]["enabled"] is False
|
|
|
|
|
|
def test_configure_router_noninteractive_can_set_default_tier(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
target.write_text(
|
|
'[llm]\nprovider = "deepseek"\nmodel = "deepseek-chat"\n',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["configure", "router", "--router", "recommended", "--default-tier", "c2"],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
data = tomllib.loads(target.read_text())
|
|
router = _effective_config(target).squilla_router
|
|
assert router.enabled is True
|
|
assert router.tier_profile == "deepseek"
|
|
assert data["squilla_router"]["default_tier"] == "c2"
|
|
|
|
|
|
def test_configure_router_rejects_invalid_default_tier_without_writing(
|
|
tmp_path, monkeypatch
|
|
):
|
|
target = tmp_path / "c.toml"
|
|
target.write_text(
|
|
'[llm]\nprovider = "ollama"\nmodel = "llama3"\n',
|
|
encoding="utf-8",
|
|
)
|
|
before = target.read_text(encoding="utf-8")
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["configure", "router", "--router", "recommended", "--default-tier", "bad"],
|
|
)
|
|
|
|
assert result.exit_code == 2
|
|
assert "defaultTier must reference a text tier" in result.output
|
|
assert "Traceback" not in result.output
|
|
assert target.read_text(encoding="utf-8") == before
|
|
|
|
|
|
def test_configure_router_invalid_mode_reports_clean_error(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
target.write_text(
|
|
'[llm]\nprovider = "deepseek"\nmodel = "deepseek-chat"\n'
|
|
'[squilla_router]\ntier_profile = "deepseek"\n',
|
|
encoding="utf-8",
|
|
)
|
|
before = target.read_text(encoding="utf-8")
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(app, ["configure", "router", "--router", "openrouter-mix"])
|
|
|
|
assert result.exit_code == 2
|
|
assert "openrouter-mix router mode is only valid" in result.output
|
|
assert "Traceback" not in result.output
|
|
assert target.read_text(encoding="utf-8") == before
|
|
|
|
|
|
def test_configure_search_noninteractive(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"configure",
|
|
"search",
|
|
"--search-provider",
|
|
"duckduckgo",
|
|
"--max-results",
|
|
"7",
|
|
"--proxy",
|
|
"http://127.0.0.1:7890",
|
|
"--use-env-proxy",
|
|
"--fallback-policy",
|
|
"network",
|
|
"--diagnostics",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
data = tomllib.loads(target.read_text())
|
|
assert _effective_config(target).search_provider == "duckduckgo"
|
|
assert data["search_max_results"] == 7
|
|
assert data["search_proxy"] == "http://127.0.0.1:7890"
|
|
assert data["search_use_env_proxy"] is True
|
|
assert data["search_fallback_policy"] == "network"
|
|
assert data["search_diagnostics"] is True
|
|
|
|
|
|
def test_configure_search_can_use_env_key_reference(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
monkeypatch.delenv("BRAVE_SEARCH_API_KEY", raising=False)
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"configure",
|
|
"search",
|
|
"--search-provider",
|
|
"brave",
|
|
"--api-key-env",
|
|
"BRAVE_SEARCH_API_KEY",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
data = tomllib.loads(target.read_text())
|
|
assert data["search_provider"] == "brave"
|
|
assert data["search_api_key_env"] == "BRAVE_SEARCH_API_KEY"
|
|
assert "search_api_key" not in data
|
|
assert "warning" in result.stdout.lower()
|
|
assert "BRAVE_SEARCH_API_KEY" in result.stdout
|
|
|
|
|
|
def test_configure_image_generation_missing_env_is_blocked(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"configure",
|
|
"image-generation",
|
|
"--image-provider",
|
|
"openrouter",
|
|
"--primary",
|
|
"openrouter/google/gemini-3.1-flash-image-preview",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 2
|
|
assert "requires an api_key" in result.stderr
|
|
|
|
|
|
def test_configure_channel_noninteractive_adds_slack(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"configure",
|
|
"channel",
|
|
"--channel-type",
|
|
"slack",
|
|
"--name",
|
|
"work",
|
|
"--token",
|
|
"xoxb-secret",
|
|
"--field",
|
|
"signing_secret=ss",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
data = tomllib.loads(target.read_text())
|
|
assert data["channels"]["channels"][0]["type"] == "slack"
|
|
assert data["channels"]["channels"][0]["name"] == "work"
|
|
|
|
|
|
def test_configure_channels_noninteractive_accepts_spec_fields_for_feishu(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"configure",
|
|
"channels",
|
|
"--channel-type",
|
|
"feishu",
|
|
"--name",
|
|
"feishu-main",
|
|
"--field",
|
|
"app_id=cli_123",
|
|
"--field",
|
|
"app_secret=secret_123",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
data = tomllib.loads(target.read_text())
|
|
entry = data["channels"]["channels"][0]
|
|
assert entry["type"] == "feishu"
|
|
assert entry["name"] == "feishu-main"
|
|
assert entry["app_id"] == "cli_123"
|
|
assert entry["app_secret"] == "secret_123"
|
|
assert "secret_123" not in result.stdout
|
|
|
|
|
|
def test_configure_channels_rejects_unknown_field(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"configure",
|
|
"channels",
|
|
"--channel-type",
|
|
"feishu",
|
|
"--name",
|
|
"feishu-main",
|
|
"--field",
|
|
"not_a_field=value",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 2
|
|
assert "unknown field" in result.output.lower()
|
|
assert not target.exists()
|
|
|
|
|
|
def test_configure_channels_token_only_does_not_complete_feishu(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"configure",
|
|
"channels",
|
|
"--channel-type",
|
|
"feishu",
|
|
"--name",
|
|
"feishu-main",
|
|
"--token",
|
|
"secret",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 2
|
|
assert "app_id" in result.output or "app_secret" in result.output
|
|
assert not target.exists()
|
|
|
|
|
|
def test_configure_image_generation_noninteractive_uses_env(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-image-env")
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"configure",
|
|
"image-generation",
|
|
"--image-provider",
|
|
"openrouter",
|
|
"--primary",
|
|
"openrouter/google/gemini-3.1-flash-image-preview",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
data = tomllib.loads(target.read_text())
|
|
assert data["image_generation"]["enabled"] is True
|
|
assert _effective_config(target).image_generation.providers.openrouter.api_key == ""
|
|
|
|
|
|
def test_configure_image_generation_can_use_nondefault_env_reference(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
|
|
monkeypatch.setenv("OPENSQUILLA_TEST_IMAGE_KEY", "sk-image-env")
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"configure",
|
|
"image-generation",
|
|
"--image-provider",
|
|
"openrouter",
|
|
"--primary",
|
|
"openrouter/google/gemini-3.1-flash-image-preview",
|
|
"--api-key-env",
|
|
"OPENSQUILLA_TEST_IMAGE_KEY",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
data = tomllib.loads(target.read_text())
|
|
provider = data["image_generation"]["providers"]["openrouter"]
|
|
assert _effective_config(target).image_generation.providers.openrouter.api_key == ""
|
|
assert provider["api_key_env"] == "OPENSQUILLA_TEST_IMAGE_KEY"
|
|
|
|
|
|
def test_configure_image_generation_can_save_missing_env_reference(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
monkeypatch.delenv("OPENSQUILLA_TEST_IMAGE_KEY", raising=False)
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"configure",
|
|
"image-generation",
|
|
"--image-provider",
|
|
"openrouter",
|
|
"--primary",
|
|
"openrouter/google/gemini-3.1-flash-image-preview",
|
|
"--api-key-env",
|
|
"OPENSQUILLA_TEST_IMAGE_KEY",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
data = tomllib.loads(target.read_text())
|
|
provider = data["image_generation"]["providers"]["openrouter"]
|
|
assert provider["api_key_env"] == "OPENSQUILLA_TEST_IMAGE_KEY"
|
|
assert "warning" in result.stdout.lower()
|
|
assert "OPENSQUILLA_TEST_IMAGE_KEY" in result.stdout
|
|
|
|
|
|
def test_configure_image_generation_can_disable_from_cli(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
monkeypatch.setenv("OPENSQUILLA_TEST_IMAGE_KEY", "sk-image-env")
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"configure",
|
|
"image-generation",
|
|
"--image-provider",
|
|
"openrouter",
|
|
"--primary",
|
|
"openrouter/google/gemini-3.1-flash-image-preview",
|
|
"--api-key-env",
|
|
"OPENSQUILLA_TEST_IMAGE_KEY",
|
|
"--no-image-enabled",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
data = tomllib.loads(target.read_text())
|
|
provider = data["image_generation"]["providers"]["openrouter"]
|
|
# The explicit off decision must be IN THE FILE, not just resolvable as
|
|
# the model default — keep-current on a later rotation depends on it.
|
|
assert data["image_generation"]["enabled"] is False
|
|
assert _effective_config(target).image_generation.enabled is False
|
|
assert data["image_generation"]["primary"] == (
|
|
"openrouter/google/gemini-3.1-flash-image-preview"
|
|
)
|
|
assert provider["api_key_env"] == "OPENSQUILLA_TEST_IMAGE_KEY"
|
|
|
|
|
|
def test_configure_image_first_time_disable_survives_key_rotation(
|
|
tmp_path, monkeypatch
|
|
):
|
|
"""R1 end-to-end: an explicit --no-image-enabled at FIRST configure (fresh
|
|
config, value equal to the model default) must be written to the file and
|
|
must survive a later key rotation that omits the flag — the rotation used
|
|
to fall into configure-implies-enable and flip the tool back on."""
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
first = runner.invoke(
|
|
app,
|
|
[
|
|
"configure",
|
|
"image",
|
|
"--image-provider",
|
|
"openai",
|
|
"--primary",
|
|
"openai/gpt-image-1",
|
|
"--api-key",
|
|
"sk-image-1",
|
|
"--no-image-enabled",
|
|
],
|
|
)
|
|
assert first.exit_code == 0, first.output
|
|
data = tomllib.loads(target.read_text())
|
|
assert data["image_generation"]["enabled"] is False
|
|
|
|
rotation = runner.invoke(
|
|
app,
|
|
[
|
|
"configure",
|
|
"image",
|
|
"--image-provider",
|
|
"openai",
|
|
"--api-key",
|
|
"sk-image-2",
|
|
],
|
|
)
|
|
assert rotation.exit_code == 0, rotation.output
|
|
data = tomllib.loads(target.read_text())
|
|
assert data["image_generation"]["enabled"] is False
|
|
assert data["image_generation"]["providers"]["openai"]["api_key"] == "sk-image-2"
|
|
assert _effective_config(target).image_generation.enabled is False
|
|
|
|
|
|
def test_configure_image_generation_can_disable_without_provider_from_cli(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
|
|
# Seed the non-default state so a silent no-op cannot masquerade as a
|
|
# save: the command must actually flip the on-disk value.
|
|
target.write_text("[image_generation]\nenabled = true\n", encoding="utf-8")
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"configure",
|
|
"image-generation",
|
|
"--no-image-enabled",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
data = tomllib.loads(target.read_text())
|
|
assert data["image_generation"]["enabled"] is False
|
|
assert _effective_config(target).image_generation.enabled is False
|
|
assert "requires an api_key" not in result.output
|
|
|
|
|
|
def test_configure_accepts_short_capability_section_aliases(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
|
|
|
|
image = runner.invoke(app, ["configure", "image", "--no-image-enabled"])
|
|
memory = runner.invoke(
|
|
app,
|
|
[
|
|
"configure",
|
|
"memory",
|
|
"--memory-provider",
|
|
"local",
|
|
"--onnx-dir",
|
|
"models/bge",
|
|
],
|
|
)
|
|
|
|
assert image.exit_code == 0, image.output
|
|
assert memory.exit_code == 0, memory.output
|
|
data = tomllib.loads(target.read_text())
|
|
assert _effective_config(target).image_generation.enabled is False
|
|
assert data["memory"]["embedding"]["provider"] == "local"
|
|
assert data["memory"]["embedding"]["local"]["onnx_dir"] == "models/bge"
|
|
|
|
|
|
def test_configure_image_generation_can_disable_provider_without_key_from_cli(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"configure",
|
|
"image-generation",
|
|
"--image-provider",
|
|
"openrouter",
|
|
"--primary",
|
|
"openrouter/google/gemini-3.1-flash-image-preview",
|
|
"--no-image-enabled",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.output
|
|
data = tomllib.loads(target.read_text())
|
|
assert _effective_config(target).image_generation.enabled is False
|
|
assert data["image_generation"]["primary"] == (
|
|
"openrouter/google/gemini-3.1-flash-image-preview"
|
|
)
|
|
assert "requires an api_key" not in result.output
|
|
|
|
|
|
def test_configure_memory_embedding_noninteractive(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"configure",
|
|
"memory-embedding",
|
|
"--memory-provider",
|
|
"local",
|
|
"--onnx-dir",
|
|
"models/bge",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
data = tomllib.loads(target.read_text())
|
|
assert data["memory"]["embedding"]["provider"] == "local"
|
|
assert data["memory"]["embedding"]["local"]["onnx_dir"] == "models/bge"
|
|
|
|
|
|
def test_configure_memory_embedding_can_use_env_key_reference(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
monkeypatch.delenv("OPENAI_EMBEDDINGS_API_KEY", raising=False)
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"configure",
|
|
"memory-embedding",
|
|
"--memory-provider",
|
|
"openai",
|
|
"--model",
|
|
"text-embedding-3-small",
|
|
"--api-key-env",
|
|
"OPENAI_EMBEDDINGS_API_KEY",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
data = tomllib.loads(target.read_text())
|
|
remote = data["memory"]["embedding"]["remote"]
|
|
assert data["memory"]["embedding"]["provider"] == "openai"
|
|
assert remote["model"] == "text-embedding-3-small"
|
|
assert remote["api_key_env"] == "OPENAI_EMBEDDINGS_API_KEY"
|
|
assert "api_key" not in remote
|
|
assert "warning" in result.stdout.lower()
|
|
assert "OPENAI_EMBEDDINGS_API_KEY" in result.stdout
|
|
|
|
|
|
def test_onboard_without_tty_prints_hint_without_writing_config(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(app, ["onboard"])
|
|
|
|
assert result.exit_code == 2
|
|
assert "requires a TTY" in result.stdout
|
|
assert "Provider recipes:" in result.stdout
|
|
assert "opensquilla onboard catalog providers" in result.stdout
|
|
assert "--api-key-env" not in result.stdout
|
|
assert "--api-key $" not in result.stdout
|
|
assert "Check status:" in result.stdout
|
|
assert not target.exists()
|
|
|
|
|
|
def test_init_help_mentions_onboard():
|
|
result = runner.invoke(app, ["init", "--help"])
|
|
assert "onboard" in result.stdout.lower()
|
|
|
|
|
|
def test_configure_ensemble_noninteractive(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"configure",
|
|
"ensemble",
|
|
"--enabled",
|
|
"--selection-mode",
|
|
"router_dynamic",
|
|
"--model-options",
|
|
"prov/model-a, prov/model-b",
|
|
"--min-successful-proposers",
|
|
"2",
|
|
"--all-failed-policy",
|
|
"error",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.output
|
|
data = tomllib.loads(target.read_text())
|
|
ensemble = data["llm_ensemble"]
|
|
assert ensemble["enabled"] is True
|
|
assert ensemble["selection_mode"] == "router_dynamic"
|
|
assert ensemble["model_options"] == ["prov/model-a", "prov/model-b"]
|
|
assert ensemble["min_successful_proposers"] == 2
|
|
assert ensemble["all_failed_policy"] == "error"
|
|
|
|
|
|
def test_onboard_configure_ensemble_alias_uses_setup_engine(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
# Seed the non-default state: `--disabled` must observably flip the file,
|
|
# not exit 0 while persisting/applying nothing.
|
|
target.write_text("[llm_ensemble]\nenabled = true\n", encoding="utf-8")
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["onboard", "configure", "ensemble", "--disabled"],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.output
|
|
data = tomllib.loads(target.read_text())
|
|
assert data["llm_ensemble"]["enabled"] is False
|
|
assert _effective_config(target).llm_ensemble.enabled is False
|
|
|
|
|
|
def test_configure_ensemble_omitted_flags_keep_stored_values(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
target.write_text(
|
|
"[llm_ensemble]\n"
|
|
"enabled = true\n"
|
|
'selection_mode = "router_dynamic"\n'
|
|
'model_options = ["stored/model-a", "stored/model-b"]\n'
|
|
"min_successful_proposers = 3\n"
|
|
'all_failed_policy = "error"\n',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["configure", "ensemble", "--min-successful-proposers", "2"],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.output
|
|
data = tomllib.loads(target.read_text())
|
|
ensemble = data["llm_ensemble"]
|
|
# Only the passed flag changed; everything else kept the stored values.
|
|
assert ensemble["min_successful_proposers"] == 2
|
|
assert ensemble["enabled"] is True
|
|
assert ensemble["selection_mode"] == "router_dynamic"
|
|
assert ensemble["model_options"] == ["stored/model-a", "stored/model-b"]
|
|
assert ensemble["all_failed_policy"] == "error"
|
|
|
|
|
|
def test_configure_ensemble_rejects_invalid_selection_mode(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["configure", "ensemble", "--selection-mode", "not-a-mode"],
|
|
)
|
|
|
|
assert result.exit_code == 2
|
|
assert "selection_mode must be one of" in result.stderr
|
|
assert "Traceback" not in result.output + result.stderr
|
|
assert not target.exists()
|
|
|
|
|
|
def test_onboard_status_includes_ensemble_section(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(app, ["onboard", "status", "--json", "--config", str(target)])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
payload = _json.loads(result.stdout)
|
|
assert payload["sections"]["ensemble"] == "optional"
|
|
detail = payload["sectionDetails"]["ensemble"]
|
|
assert detail["label"] == "LLM ensemble"
|
|
assert detail["required"] is False
|
|
assert detail["blocking"] is False
|
|
|
|
|
|
def test_onboard_status_table_shows_disabled_ensemble_detail(tmp_path, monkeypatch):
|
|
target = tmp_path / "c.toml"
|
|
target.write_text("[llm_ensemble]\nenabled = false\n", encoding="utf-8")
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
result = runner.invoke(app, ["onboard", "status", "--json", "--config", str(target)])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
payload = _json.loads(result.stdout)
|
|
assert payload["sections"]["ensemble"] == "optional"
|
|
assert payload["sectionDetails"]["ensemble"]["detail"] == "disabled"
|
|
|
|
|
|
def _fake_probe_result(ok: bool, **overrides):
|
|
from opensquilla.onboarding.probe import ProviderProbeResult
|
|
|
|
async def fake_probe(**kwargs):
|
|
fake_probe.calls.append(kwargs)
|
|
return ProviderProbeResult(
|
|
ok=ok,
|
|
provider_id=kwargs["provider_id"],
|
|
model=kwargs["model"],
|
|
**overrides,
|
|
)
|
|
|
|
fake_probe.calls = []
|
|
return fake_probe
|
|
|
|
|
|
def test_onboard_probe_flag_exits_zero_when_probe_passes(tmp_path, monkeypatch):
|
|
from opensquilla.onboarding import probe as probe_module
|
|
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
fake_probe = _fake_probe_result(ok=True)
|
|
monkeypatch.setattr(probe_module, "probe_llm_provider", fake_probe)
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"onboard",
|
|
"--provider", "openrouter",
|
|
"--model", "deepseek/deepseek-v4-flash",
|
|
"--api-key", "sk",
|
|
"--minimal",
|
|
"--probe",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.output
|
|
assert "Probe OK" in result.stdout
|
|
assert [call["provider_id"] for call in fake_probe.calls] == ["openrouter"]
|
|
# The probe verifies exactly the credentials that were just saved.
|
|
assert fake_probe.calls[0]["api_key"] == "sk"
|
|
assert "sk" in target.read_text()
|
|
|
|
|
|
def test_onboard_probe_flag_exits_nonzero_when_probe_fails(tmp_path, monkeypatch):
|
|
from opensquilla.onboarding import probe as probe_module
|
|
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
fake_probe = _fake_probe_result(
|
|
ok=False,
|
|
failure_kind="auth_invalid",
|
|
message="Incorrect API key provided",
|
|
)
|
|
monkeypatch.setattr(probe_module, "probe_llm_provider", fake_probe)
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"onboard",
|
|
"--provider", "openrouter",
|
|
"--model", "deepseek/deepseek-v4-flash",
|
|
"--api-key", "sk-bad",
|
|
"--minimal",
|
|
"--probe",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 1
|
|
assert "Probe failed" in result.stderr
|
|
assert "auth_invalid" in result.stderr
|
|
assert "Incorrect API key provided" in result.stderr
|
|
# The probe is a CI gate, not a rollback: the config stays saved.
|
|
assert "openrouter" in target.read_text()
|
|
|
|
|
|
def test_onboard_without_probe_flag_never_probes(tmp_path, monkeypatch):
|
|
from opensquilla.onboarding import probe as probe_module
|
|
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
def unexpected_probe(**_kwargs):
|
|
raise AssertionError("the probe must only run behind --probe")
|
|
|
|
monkeypatch.setattr(probe_module, "probe_llm_provider", unexpected_probe)
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"onboard",
|
|
"--provider", "openrouter",
|
|
"--model", "deepseek/deepseek-v4-flash",
|
|
"--api-key", "sk",
|
|
"--minimal",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.output
|
|
assert "Probe" not in result.stdout
|
|
|
|
|
|
def test_advertised_router_command_actionable_for_non_registry_provider(
|
|
tmp_path, monkeypatch
|
|
):
|
|
"""X1: the advertised next-steps router one-liner against a hand-edited,
|
|
non-registry llm.provider used to exit 2 with the cryptic "router tier
|
|
'c0' must be an object"; the error must now name the provider and the
|
|
two runnable ways out (exit code stays 2)."""
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
target.write_text(
|
|
'[llm]\nprovider = "acme-llm"\nmodel = "acme-model"\n', encoding="utf-8"
|
|
)
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"onboard",
|
|
"configure",
|
|
"router",
|
|
"--router",
|
|
"recommended",
|
|
"--default-tier",
|
|
"c1",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 2, result.output
|
|
text = _plain_text(result.output)
|
|
assert "acme-llm" in text
|
|
assert "opensquilla onboard configure provider --provider" in text
|
|
assert "--router disabled" in text
|
|
assert "must be an object" not in text
|
|
|
|
|
|
def test_onboard_interactive_value_error_is_productized(tmp_path, monkeypatch):
|
|
"""R9 boundary: a mutation-level ValueError escaping the wizard (e.g.
|
|
"model is required" for a provider without a derivable default) must
|
|
exit 2 with a productized message, not a raw traceback."""
|
|
from opensquilla.cli import onboard_cmd
|
|
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
def boom(_options):
|
|
raise ValueError("model is required")
|
|
|
|
monkeypatch.setattr(onboard_cmd, "run_interactive_onboard", boom)
|
|
|
|
result = runner.invoke(app, ["onboard"])
|
|
|
|
assert result.exit_code == 2, result.output
|
|
assert "model is required" in _plain_text(result.output)
|
|
assert "Traceback" not in result.output
|
|
|
|
|
|
def test_configure_interactive_value_error_is_productized(tmp_path, monkeypatch):
|
|
"""R8 boundary: a mutation ValueError from the configure wizard (e.g. a
|
|
blank required channel secret) must exit 2 like the headless path."""
|
|
from opensquilla.cli import onboard_cmd
|
|
|
|
target = tmp_path / "c.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
|
|
|
|
def boom(_section, config_path=None):
|
|
raise ValueError("channel field 'token' requires a non-empty value")
|
|
|
|
monkeypatch.setattr(onboard_cmd, "run_interactive_configure", boom)
|
|
|
|
result = runner.invoke(app, ["configure", "channels"])
|
|
|
|
assert result.exit_code == 2, result.output
|
|
assert "requires a non-empty value" in _plain_text(result.output)
|
|
assert "Traceback" not in result.output
|