Files
2026-07-13 13:12:33 +08:00

2616 lines
93 KiB
Python

"""Tests for non-interactive onboarding flow halves."""
from __future__ import annotations
import types
from io import StringIO
from rich.console import Console
def test_wait_for_setup_start_flushes_visible_prompt_before_accepting_enter(monkeypatch):
from opensquilla.onboarding import flow
events: list[str] = []
class _Console:
class _File:
def flush(self):
events.append("flush")
file = _File()
def print(self, message: str):
assert "Press Enter to start setup" in message
events.append("print")
monkeypatch.setattr(flow, "console", _Console())
monkeypatch.setattr(flow, "_flush_stdin_typeahead", lambda: events.append("clear"))
monkeypatch.setattr("builtins.input", lambda: events.append("input"))
flow._wait_for_setup_start()
assert events == ["print", "flush", "clear", "input"]
def test_flush_stdin_typeahead_uses_msvcrt_on_windows(monkeypatch):
from opensquilla.onboarding import flow
drained: list[str] = []
fake_msvcrt = types.SimpleNamespace(
kbhit=lambda: len(drained) < 2,
getwch=lambda: drained.append("key"),
)
monkeypatch.setattr(flow.os, "name", "nt")
monkeypatch.setitem(__import__("sys").modules, "msvcrt", fake_msvcrt)
flow._flush_stdin_typeahead()
assert drained == ["key", "key"]
def test_flush_stdin_typeahead_uses_termios_on_unix_tty(monkeypatch):
from opensquilla.onboarding import flow
calls: list[object] = []
fake_stdin = types.SimpleNamespace(isatty=lambda: True)
fake_termios = types.SimpleNamespace(
TCIFLUSH=123,
tcflush=lambda stream, selector: calls.extend([stream, selector]),
)
monkeypatch.setattr(flow.os, "name", "posix")
monkeypatch.setattr(flow.sys, "stdin", fake_stdin)
monkeypatch.setitem(__import__("sys").modules, "termios", fake_termios)
flow._flush_stdin_typeahead()
assert calls == [fake_stdin, 123]
def test_interactive_provider_choice_offers_all_runtime_supported_providers():
from opensquilla.onboarding.flow import OnboardOptions, _ask_provider_choice
captured: dict[str, object] = {}
class _Question:
def ask(self) -> str:
return "openrouter (OpenRouter)"
class _Questionary:
def select(
self, _message: str, *, choices: list[str], default: str, **kwargs
) -> _Question:
captured["choices"] = choices
captured["default"] = default
captured["kwargs"] = kwargs
return _Question()
_ask_provider_choice(_Questionary(), OnboardOptions())
choices = captured["choices"]
assert choices[0] == "tokenrhythm (TokenRhythm)"
assert choices[1] == "openrouter (OpenRouter)"
assert captured["default"] == "tokenrhythm (TokenRhythm)"
offered = {choice.split(" ")[0] for choice in choices}
from tests.test_onboarding.test_provider_specs import EXPECTED_SUPPORTED
assert offered == EXPECTED_SUPPORTED
# Experimental providers are offered, but with a visible caveat.
assert any("(experimental)" in choice for choice in choices)
# ~30 entries: the select must let the operator type to filter.
kwargs = captured["kwargs"]
assert kwargs["use_search_filter"] is True
assert kwargs["use_jk_keys"] is False
def test_interactive_router_supported_provider_does_not_prompt_for_model(monkeypatch):
from opensquilla.onboarding import flow
from opensquilla.onboarding.flow import OnboardOptions, _ask_provider_fields
from opensquilla.onboarding.provider_specs import get_provider_setup_spec
# The router-supported pre-save probe is exercised in
# test_flow_provider_verify.py; here it degrades silently (offline).
monkeypatch.setattr(flow, "_run_provider_probe", lambda **_kw: None)
class _Questionary:
def text(self, message: str, **_kwargs):
if message == "Model id":
raise AssertionError("router-supported providers should not prompt for model")
raise AssertionError(f"unexpected text prompt: {message}")
answers = _ask_provider_fields(
_Questionary(),
get_provider_setup_spec("openrouter"),
OnboardOptions(api_key_env="OPENROUTER_API_KEY"),
)
assert answers["model"] == ""
assert answers["api_key_env"] == "OPENROUTER_API_KEY"
def test_interactive_provider_fields_default_to_pasted_api_key(monkeypatch):
from opensquilla.onboarding import flow
from opensquilla.onboarding.flow import OnboardOptions, _ask_provider_fields
from opensquilla.onboarding.provider_specs import get_provider_setup_spec
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
monkeypatch.setattr(flow, "_run_provider_probe", lambda **_kw: None)
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary:
def select(self, message: str, **kwargs):
assert message == "LLM API key source"
assert kwargs.get("choices") == [
"Paste API key now",
"Use environment variable OPENROUTER_API_KEY",
]
assert kwargs.get("default") == "Paste API key now"
return _Answer("Paste API key now")
def text(self, message: str, **_kwargs):
raise AssertionError(f"unexpected text prompt: {message}")
def password(self, message: str, **_kwargs):
assert message == "API key"
return _Answer("sk-live")
def confirm(self, message: str, **_kwargs):
raise AssertionError(f"unexpected confirm prompt: {message}")
def checkbox(self, message: str, **_kwargs):
raise AssertionError(f"unexpected checkbox prompt: {message}")
answers = _ask_provider_fields(
_Questionary(),
get_provider_setup_spec("openrouter"),
OnboardOptions(),
)
assert answers["model"] == ""
assert answers["api_key"] == "sk-live"
assert answers["api_key_env"] == ""
def test_interactive_provider_fields_explains_detected_env_key(monkeypatch):
from opensquilla.onboarding import flow
from opensquilla.onboarding.flow import OnboardOptions, _ask_provider_fields
from opensquilla.onboarding.provider_specs import get_provider_setup_spec
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-from-env")
monkeypatch.setattr(flow, "_run_provider_probe", lambda **_kw: None)
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary:
def select(self, message: str, **kwargs):
assert message == "LLM API key source"
assert kwargs.get("choices") == [
"Paste API key now",
"Use environment variable OPENROUTER_API_KEY (detected)",
]
assert kwargs.get("default") == (
"Use environment variable OPENROUTER_API_KEY (detected)"
)
return _Answer("Use environment variable OPENROUTER_API_KEY (detected)")
def password(self, message: str, **_kwargs):
raise AssertionError(f"unexpected password prompt: {message}")
answers = _ask_provider_fields(
_Questionary(),
get_provider_setup_spec("openrouter"),
OnboardOptions(),
)
assert answers["api_key"] == ""
assert answers["api_key_env"] == "OPENROUTER_API_KEY"
def test_interactive_provider_fields_requires_pasted_api_key(monkeypatch):
from opensquilla.onboarding import flow
from opensquilla.onboarding.flow import OnboardOptions, _ask_provider_fields
from opensquilla.onboarding.provider_specs import get_provider_setup_spec
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
monkeypatch.setattr(flow, "_run_provider_probe", lambda **_kw: None)
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary:
def select(self, message: str, **kwargs):
assert message == "LLM API key source"
assert kwargs.get("default") == "Paste API key now"
return _Answer("Paste API key now")
def password(self, message: str, **kwargs):
assert message == "API key"
validate = kwargs.get("validate")
assert validate is not None
assert validate("") is not True
assert validate("sk-live") is True
return _Answer("sk-live")
answers = _ask_provider_fields(
_Questionary(),
get_provider_setup_spec("openrouter"),
OnboardOptions(),
)
assert answers["api_key"] == "sk-live"
assert answers["api_key_env"] == ""
def test_interactive_provider_fields_rejects_terminal_paste_escape(monkeypatch):
from opensquilla.onboarding import flow
from opensquilla.onboarding.flow import OnboardOptions, _ask_provider_fields
from opensquilla.onboarding.provider_specs import get_provider_setup_spec
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
monkeypatch.setattr(flow, "_run_provider_probe", lambda **_kw: None)
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary:
def select(self, message: str, **kwargs):
assert message == "LLM API key source"
assert kwargs.get("default") == "Paste API key now"
return _Answer("Paste API key now")
def password(self, message: str, **kwargs):
assert message == "API key"
validate = kwargs.get("validate")
assert validate is not None
assert validate("[2;2~") is not True
assert validate("\x1b[200~sk-live\x1b[201~") is not True
assert validate("sk-live-with-[2;2~-literal-suffix") is True
assert validate("sk-live") is True
return _Answer("sk-live")
answers = _ask_provider_fields(
_Questionary(),
get_provider_setup_spec("openrouter"),
OnboardOptions(),
)
assert answers["api_key"] == "sk-live"
def test_interactive_onboard_prompts_router_defaults_before_persist(tmp_path, monkeypatch):
import sys
import types
from opensquilla.onboarding import flow
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
monkeypatch.setattr(flow, "_is_tty", lambda: True)
monkeypatch.setattr(flow, "_wait_for_setup_start", lambda: calls.append("start gate"))
monkeypatch.setattr(flow, "detect_default_sources", lambda: [])
monkeypatch.setattr(flow, "_run_provider_probe", lambda **_kw: None)
calls: list[str] = []
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary(types.SimpleNamespace):
def select(self, message: str, **kwargs):
calls.append(message)
if message == "LLM provider":
return _Answer("openrouter (OpenRouter)")
if message == "LLM API key source":
assert kwargs.get("default") == "Paste API key now"
return _Answer("Use environment variable OPENROUTER_API_KEY")
if message == "Router mode":
assert kwargs.get("choices") == ["SquillaRouter", "Disabled"]
assert kwargs.get("default") == "SquillaRouter"
return _Answer("SquillaRouter")
if message == "Default text model":
assert kwargs.get("choices") == [
"Route c0",
"Route c1",
"Route c2",
"Route c3",
]
assert kwargs.get("default") == "Route c1"
return _Answer("Route c2")
raise AssertionError(f"unexpected select prompt: {message}")
def text(self, message: str, **kwargs):
calls.append(message)
raise AssertionError(f"unexpected text prompt: {message}")
def password(self, message: str, **_kwargs):
raise AssertionError(f"unexpected password prompt: {message}")
def confirm(self, message: str, **_kwargs):
calls.append(message)
if message == "Edit router tier models now?":
return _Answer(False)
if message in {
"Configure a messaging channel now?",
"Configure web search now?",
"Enable image generation now?",
}:
return _Answer(False)
raise AssertionError(f"unexpected confirm prompt: {message}")
monkeypatch.setitem(sys.modules, "questionary", _Questionary())
flow.run_interactive_onboard(flow.OnboardOptions())
assert calls[0] == "start gate"
assert calls[1] == "LLM provider"
assert calls.index("Router mode") < calls.index("Configure a messaging channel now?")
# Persistence is sparse (default-equal values may be omitted from the
# TOML), so pin the reloaded semantic state rather than raw file lines.
saved = flow.load_config(target)
assert saved.llm.api_key == ""
assert saved.llm.api_key_env == "OPENROUTER_API_KEY"
assert saved.squilla_router.default_tier == "c2"
assert saved.llm.model == "z-ai/glm-5.2"
def test_interactive_onboard_migration_defaults_to_all_sources_and_keeps_imported_provider(
tmp_path, monkeypatch
):
import sys
import tomllib
import types
from opensquilla.onboarding import flow
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-imported-env")
monkeypatch.setattr(flow, "_is_tty", lambda: True)
monkeypatch.setattr(flow, "_wait_for_setup_start", lambda: calls.append("start gate"))
detected = [
flow.DetectedMigrationSource("openclaw", tmp_path / ".openclaw"),
flow.DetectedMigrationSource("hermes", tmp_path / ".hermes"),
]
monkeypatch.setattr(flow, "detect_default_sources", lambda: detected)
calls: list[str] = []
batches: list[tuple[tuple[str, ...], bool, bool]] = []
def fake_run_migration_batch(_detected, selected, options):
batches.append((tuple(selected), options.apply, options.migrate_secrets))
if options.apply:
target.write_text(
"\n".join(
[
"[llm]",
'provider = "openrouter"',
'model = "anthropic/claude-sonnet-4.5"',
'api_key_env = "OPENROUTER_API_KEY"',
"",
]
),
encoding="utf-8",
)
return flow.MigrationBatchResult(
selected=tuple(selected),
apply=options.apply,
reports={
name: {
"output_dir": str(tmp_path / "reports" / name),
"items": [
{
"kind": "config",
"status": "applied" if options.apply else "planned",
}
],
}
for name in selected
},
)
monkeypatch.setattr(flow, "run_migration_batch", fake_run_migration_batch)
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Choice:
def __init__(self, title, value, checked=False, description=None):
self.title = title
self.value = value
self.checked = checked
self.description = description
class _Questionary(types.SimpleNamespace):
Choice = _Choice
def checkbox(self, message: str, choices, **kwargs):
calls.append(message)
assert message == "Select sources to import"
assert kwargs.get("instruction") == (
"Space select | Enter continue | A toggle all"
)
assert [choice.value for choice in choices] == ["openclaw", "hermes"]
assert [choice.title for choice in choices] == ["OpenClaw", "Hermes Agent"]
assert [choice.description for choice in choices] == [
str(tmp_path / ".openclaw"),
str(tmp_path / ".hermes"),
]
assert all(choice.checked for choice in choices)
return _Answer([choice.value for choice in choices])
def confirm(self, message: str, **kwargs):
calls.append(message)
if message == "Review migration options now?":
assert kwargs.get("default") is True
return _Answer(True)
if message == "Import saved API keys/tokens from detected legacy .env files?":
assert kwargs.get("default") is False
return _Answer(False)
if message == "Apply this migration now?":
assert kwargs.get("default") is True
return _Answer(True)
if message == "Use imported provider credentials?":
assert kwargs.get("default") is True
return _Answer(True)
if message == "Edit router tier models now?":
return _Answer(False)
if message in {
"Configure a messaging channel now?",
"Configure web search now?",
"Enable image generation now?",
}:
return _Answer(False)
raise AssertionError(f"unexpected confirm prompt: {message}")
def select(self, message: str, **_kwargs):
calls.append(message)
if message == "Router mode":
return _Answer("SquillaRouter")
if message == "Default text model":
return _Answer(_kwargs.get("default"))
raise AssertionError(f"unexpected select prompt: {message}")
def text(self, message: str, **_kwargs):
calls.append(message)
raise AssertionError(f"unexpected text prompt: {message}")
def password(self, message: str, **_kwargs):
calls.append(message)
raise AssertionError(f"unexpected password prompt: {message}")
monkeypatch.setitem(sys.modules, "questionary", _Questionary())
flow.run_interactive_onboard(flow.OnboardOptions())
assert batches == [
(("openclaw", "hermes"), False, False),
(("openclaw", "hermes"), True, False),
]
assert "LLM provider" not in calls
assert "Router mode" in calls
data = tomllib.loads(target.read_text())
# Sparse persistence may omit default-equal keys from the raw TOML, so
# the enabled/provider pins go through the reloaded semantic state.
saved = flow.load_config(target)
assert saved.llm.provider == "openrouter"
assert saved.llm.api_key_env == "OPENROUTER_API_KEY"
assert saved.llm.model == "deepseek/deepseek-v4-pro"
assert saved.squilla_router.enabled is True
assert saved.squilla_router.tier_profile == "openrouter"
assert "api_key" not in data.get("llm", {})
def test_interactive_onboard_imported_provider_prefers_inline_key_over_env(
tmp_path, monkeypatch
):
import sys
import tomllib
import types
from opensquilla.onboarding import flow
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
monkeypatch.setattr(flow, "_is_tty", lambda: True)
monkeypatch.setattr(flow, "_wait_for_setup_start", lambda: None)
monkeypatch.setattr(
flow,
"detect_default_sources",
lambda: [flow.DetectedMigrationSource("openclaw", tmp_path / ".openclaw")],
)
calls: list[str] = []
def fake_run_migration_batch(_detected, selected, options):
if options.apply:
target.write_text(
"\n".join(
[
"[llm]",
'provider = "openrouter"',
'model = "anthropic/claude-sonnet-4.5"',
'api_key = "sk-imported"',
'api_key_env = "OPENROUTER_API_KEY"',
"",
]
),
encoding="utf-8",
)
return flow.MigrationBatchResult(
selected=tuple(selected),
apply=options.apply,
reports={
"openclaw": {
"output_dir": str(tmp_path / "reports" / "openclaw"),
"items": [{"kind": "config", "status": "planned"}],
}
},
)
monkeypatch.setattr(flow, "run_migration_batch", fake_run_migration_batch)
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary(types.SimpleNamespace):
def confirm(self, message: str, **kwargs):
calls.append(message)
if message == "Review migration options now?":
return _Answer(True)
if message == "Import saved API keys/tokens from detected legacy .env files?":
return _Answer(True)
if message == "Apply this migration now?":
return _Answer(True)
if message == "Use imported provider credentials?":
return _Answer(True)
if message == "Edit router tier models now?":
return _Answer(False)
if message in {
"Configure a messaging channel now?",
"Configure web search now?",
"Enable image generation now?",
}:
return _Answer(False)
raise AssertionError(f"unexpected confirm prompt: {message}")
def select(self, message: str, **kwargs):
calls.append(message)
if message == "Router mode":
return _Answer("SquillaRouter")
if message == "Default text model":
return _Answer(kwargs.get("default"))
raise AssertionError(f"unexpected select prompt: {message}")
def text(self, message: str, **_kwargs):
calls.append(message)
raise AssertionError(f"unexpected text prompt: {message}")
def password(self, message: str, **_kwargs):
calls.append(message)
raise AssertionError(f"unexpected password prompt: {message}")
monkeypatch.setitem(sys.modules, "questionary", _Questionary())
flow.run_interactive_onboard(flow.OnboardOptions())
assert "LLM provider" not in calls
data = tomllib.loads(target.read_text())
assert data["llm"]["provider"] == "openrouter"
assert data["llm"]["api_key"] == "sk-imported"
assert data["llm"].get("api_key_env", "") == ""
assert data["llm"]["model"] == "deepseek/deepseek-v4-pro"
def test_interactive_onboard_imported_provider_finalize_error_continues_setup(
tmp_path, monkeypatch
):
import sys
import tomllib
import types
from opensquilla.onboarding import flow
target = tmp_path / "c.toml"
console_output = StringIO()
monkeypatch.setattr(
flow,
"console",
Console(file=console_output, force_terminal=False, highlight=False),
)
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-env")
monkeypatch.setattr(flow, "_is_tty", lambda: True)
monkeypatch.setattr(flow, "_wait_for_setup_start", lambda: None)
monkeypatch.setattr(
flow,
"detect_default_sources",
lambda: [flow.DetectedMigrationSource("openclaw", tmp_path / ".openclaw")],
)
monkeypatch.setattr(
flow,
"_use_imported_provider_credentials_with_router_defaults",
lambda *_args, **_kwargs: (_ for _ in ()).throw(ValueError("bad imported provider")),
)
monkeypatch.setattr(flow, "_run_provider_probe", lambda **_kw: None)
calls: list[str] = []
def fake_run_migration_batch(_detected, selected, options):
if options.apply:
target.write_text(
"\n".join(
[
"[llm]",
'provider = "openrouter"',
'model = "anthropic/claude-sonnet-4.5"',
'api_key_env = "OPENROUTER_API_KEY"',
"",
]
),
encoding="utf-8",
)
return flow.MigrationBatchResult(
selected=tuple(selected),
apply=options.apply,
reports={
"openclaw": {
"output_dir": str(tmp_path / "reports" / "openclaw"),
"items": [{"kind": "config", "status": "planned"}],
}
},
)
monkeypatch.setattr(flow, "run_migration_batch", fake_run_migration_batch)
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary(types.SimpleNamespace):
def confirm(self, message: str, **kwargs):
calls.append(message)
if message == "Review migration options now?":
return _Answer(True)
if message == "Import saved API keys/tokens from detected legacy .env files?":
return _Answer(False)
if message == "Apply this migration now?":
return _Answer(True)
if message == "Use imported provider credentials?":
return _Answer(True)
if message == "Edit router tier models now?":
return _Answer(False)
if message in {
"Configure a messaging channel now?",
"Configure web search now?",
"Enable image generation now?",
}:
return _Answer(False)
raise AssertionError(f"unexpected confirm prompt: {message}")
def select(self, message: str, **kwargs):
calls.append(message)
if message == "LLM provider":
return _Answer("openrouter (OpenRouter)")
if message == "LLM API key source":
return _Answer("Use environment variable OPENROUTER_API_KEY")
if message == "Router mode":
return _Answer("SquillaRouter")
if message == "Default text model":
return _Answer(kwargs.get("default"))
raise AssertionError(f"unexpected select prompt: {message}")
def text(self, message: str, **_kwargs):
calls.append(message)
raise AssertionError(f"unexpected text prompt: {message}")
def password(self, message: str, **_kwargs):
calls.append(message)
raise AssertionError(f"unexpected password prompt: {message}")
monkeypatch.setitem(sys.modules, "questionary", _Questionary())
flow.run_interactive_onboard(flow.OnboardOptions())
assert "LLM provider" in calls
out = console_output.getvalue()
assert "Imported provider settings could not be finalized" in out
assert "Continue provider setup to finish onboarding" in out
data = tomllib.loads(target.read_text())
assert data["llm"]["provider"] == "openrouter"
assert data["llm"]["api_key_env"] == "OPENROUTER_API_KEY"
assert data["llm"]["model"] == "deepseek/deepseek-v4-pro"
def test_onboard_migration_selection_summary_lists_checked_sources(tmp_path, monkeypatch):
from opensquilla.onboarding import flow
console_output = StringIO()
monkeypatch.setattr(
flow,
"console",
Console(file=console_output, force_terminal=False, highlight=False),
)
detected = [
flow.DetectedMigrationSource("openclaw", tmp_path / ".openclaw"),
flow.DetectedMigrationSource("hermes", tmp_path / ".hermes"),
]
flow._print_selected_migration_sources(detected, ["openclaw", "hermes"])
out = console_output.getvalue()
assert "Selected migration sources" in out
assert "☑ OpenClaw" in out
assert "☑ Hermes Agent" in out
unwrapped_out = out.replace("\n", "")
assert str(tmp_path / ".openclaw") in unwrapped_out
assert str(tmp_path / ".hermes") in unwrapped_out
def test_onboard_migration_source_prompt_uses_clear_continue_language(tmp_path):
from opensquilla.onboarding import flow
captured: dict[str, object] = {}
class _Answer:
def ask(self):
return ["openclaw", "hermes"]
class _Choice:
def __init__(self, title, value, checked=False, description=None):
self.title = title
self.value = value
self.checked = checked
self.description = description
class _Questionary:
Choice = _Choice
def checkbox(self, message: str, **kwargs):
captured["message"] = message
captured["instruction"] = kwargs.get("instruction")
return _Answer()
selected = flow._ask_migration_sources(
_Questionary(),
[
flow.DetectedMigrationSource("openclaw", tmp_path / ".openclaw"),
flow.DetectedMigrationSource("hermes", tmp_path / ".hermes"),
],
)
assert selected == ["openclaw", "hermes"]
assert captured == {
"message": "Select sources to import",
"instruction": "Space select | Enter continue | A toggle all",
}
def test_onboard_migration_preview_hides_unwritten_report_path(tmp_path, monkeypatch):
from opensquilla.onboarding import flow
console_output = StringIO()
monkeypatch.setattr(
flow,
"console",
Console(file=console_output, force_terminal=False, highlight=False),
)
missing_report_dir = tmp_path / "dry-run-report"
flow._print_migration_summary(
flow.MigrationBatchResult(
selected=("openclaw",),
apply=False,
reports={
"openclaw": {
"output_dir": str(missing_report_dir),
"items": [{"kind": "config", "status": "planned"}],
}
},
),
title="Migration preview",
)
out = console_output.getvalue()
assert "Migration preview" in out
assert "planned=1" in out
assert str(missing_report_dir) not in out
def test_interactive_onboard_migration_preview_failure_continues_provider_setup(
tmp_path, monkeypatch
):
import sys
import types
from opensquilla.onboarding import flow
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
monkeypatch.setattr(flow, "_is_tty", lambda: True)
monkeypatch.setattr(flow, "_wait_for_setup_start", lambda: calls.append("start gate"))
detected = [flow.DetectedMigrationSource("openclaw", tmp_path / ".openclaw")]
monkeypatch.setattr(flow, "detect_default_sources", lambda: detected)
monkeypatch.setattr(flow, "_run_provider_probe", lambda **_kw: None)
calls: list[str] = []
batches: list[tuple[tuple[str, ...], bool]] = []
def fake_run_migration_batch(_detected, selected, options):
batches.append((tuple(selected), options.apply))
return flow.MigrationBatchResult(
selected=tuple(selected),
apply=options.apply,
reports={
"openclaw": {
"output_dir": str(tmp_path / "reports" / "openclaw"),
"items": [{"kind": "source", "status": "error", "reason": "bad source"}],
}
},
)
monkeypatch.setattr(flow, "run_migration_batch", fake_run_migration_batch)
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary(types.SimpleNamespace):
def confirm(self, message: str, **kwargs):
calls.append(message)
if message == "Review migration options now?":
assert kwargs.get("default") is True
return _Answer(True)
if message == "Import saved API keys/tokens from detected legacy .env files?":
assert kwargs.get("default") is False
return _Answer(False)
if message == "Edit router tier models now?":
return _Answer(False)
if message in {
"Configure a messaging channel now?",
"Configure web search now?",
"Enable image generation now?",
}:
return _Answer(False)
raise AssertionError(f"unexpected confirm prompt: {message}")
def select(self, message: str, **kwargs):
calls.append(message)
if message == "LLM provider":
return _Answer("openrouter (OpenRouter)")
if message == "LLM API key source":
return _Answer("Use environment variable OPENROUTER_API_KEY")
if message == "Router mode":
return _Answer("SquillaRouter")
if message == "Default text model":
return _Answer(kwargs.get("default"))
raise AssertionError(f"unexpected select prompt: {message}")
def text(self, message: str, **_kwargs):
calls.append(message)
raise AssertionError(f"unexpected text prompt: {message}")
def password(self, message: str, **_kwargs):
calls.append(message)
raise AssertionError(f"unexpected password prompt: {message}")
monkeypatch.setitem(sys.modules, "questionary", _Questionary())
flow.run_interactive_onboard(flow.OnboardOptions())
assert batches == [(("openclaw",), False)]
assert "Apply this migration now?" not in calls
assert "LLM provider" in calls
# Sparse persistence may omit default-equal keys; pin the reloaded state.
saved = flow.load_config(target)
assert saved.llm.provider == "openrouter"
assert saved.llm.api_key_env == "OPENROUTER_API_KEY"
def test_interactive_onboard_migration_prompts_for_missing_imported_provider_key(
tmp_path, monkeypatch
):
import sys
import tomllib
import types
from opensquilla.onboarding import flow
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
monkeypatch.delenv("IMPORTED_OPENROUTER_KEY", raising=False)
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
monkeypatch.setattr(flow, "_is_tty", lambda: True)
monkeypatch.setattr(flow, "_wait_for_setup_start", lambda: calls.append("start gate"))
detected = [flow.DetectedMigrationSource("openclaw", tmp_path / ".openclaw")]
monkeypatch.setattr(flow, "detect_default_sources", lambda: detected)
calls: list[str] = []
def fake_run_migration_batch(_detected, selected, options):
if options.apply:
target.write_text(
"\n".join(
[
"[llm]",
'provider = "openrouter"',
'model = "anthropic/claude-sonnet-4.5"',
'api_key_env = "IMPORTED_OPENROUTER_KEY"',
"",
]
),
encoding="utf-8",
)
return flow.MigrationBatchResult(
selected=tuple(selected),
apply=options.apply,
reports={
"openclaw": {
"output_dir": str(tmp_path / "reports" / "openclaw"),
"items": [{"kind": "config", "status": "planned"}],
}
},
)
monkeypatch.setattr(flow, "run_migration_batch", fake_run_migration_batch)
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary(types.SimpleNamespace):
def confirm(self, message: str, **kwargs):
calls.append(message)
if message == "Review migration options now?":
return _Answer(True)
if message == "Import saved API keys/tokens from detected legacy .env files?":
assert kwargs.get("default") is False
return _Answer(False)
if message == "Apply this migration now?":
return _Answer(True)
if message == "Edit router tier models now?":
return _Answer(False)
if message in {
"Configure a messaging channel now?",
"Configure web search now?",
"Enable image generation now?",
}:
return _Answer(False)
raise AssertionError(f"unexpected confirm prompt: {message}")
def select(self, message: str, **kwargs):
calls.append(message)
if message == "LLM API key source":
assert "Use environment variable IMPORTED_OPENROUTER_KEY" in kwargs.get(
"choices", []
)
assert "Use environment variable OPENROUTER_API_KEY" in kwargs.get(
"choices", []
)
assert kwargs.get("default") == "Paste API key now"
return _Answer("Paste API key now")
if message == "Router mode":
return _Answer("SquillaRouter")
if message == "Default text model":
return _Answer(kwargs.get("default"))
raise AssertionError(f"unexpected select prompt: {message}")
def password(self, message: str, **_kwargs):
calls.append(message)
if message == "API key":
return _Answer("sk-new")
raise AssertionError(f"unexpected password prompt: {message}")
def text(self, message: str, **_kwargs):
calls.append(message)
raise AssertionError(f"unexpected text prompt: {message}")
monkeypatch.setitem(sys.modules, "questionary", _Questionary())
flow.run_interactive_onboard(flow.OnboardOptions())
assert "LLM provider" not in calls
assert "Router mode" in calls
data = tomllib.loads(target.read_text())
assert data["llm"]["provider"] == "openrouter"
assert data["llm"]["api_key"] == "sk-new"
assert data["llm"]["model"] == "deepseek/deepseek-v4-pro"
def test_interactive_onboard_can_enable_image_generation(tmp_path, monkeypatch):
import sys
import tomllib
import types
from opensquilla.onboarding import flow
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-image-env")
monkeypatch.setattr(flow, "_is_tty", lambda: True)
monkeypatch.setattr(flow, "_wait_for_setup_start", lambda: None)
monkeypatch.setattr(flow, "detect_default_sources", lambda: [])
monkeypatch.setattr(flow, "_run_provider_probe", lambda **_kw: None)
calls: list[str] = []
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary(types.SimpleNamespace):
def select(self, message: str, **kwargs):
calls.append(message)
if message == "LLM provider":
return _Answer("openrouter (OpenRouter)")
if message == "LLM API key source":
assert kwargs.get("default") == (
"Use environment variable OPENROUTER_API_KEY (detected)"
)
return _Answer("Use environment variable OPENROUTER_API_KEY (detected)")
if message == "Router mode":
return _Answer("SquillaRouter")
if message == "Default text model":
return _Answer(kwargs.get("default"))
if message == "Image generation provider":
assert kwargs.get("default") == "openrouter (OpenRouter Images)"
return _Answer("openrouter (OpenRouter Images)")
if message == "Image API key source":
assert (
"Use environment variable OPENROUTER_API_KEY"
in kwargs.get("choices", [])
)
assert "Reuse matching LLM provider key" not in kwargs.get("choices", [])
assert kwargs.get("default") == "Use environment variable OPENROUTER_API_KEY"
return _Answer("Use environment variable OPENROUTER_API_KEY")
raise AssertionError(f"unexpected select prompt: {message}")
def text(self, message: str, **kwargs):
calls.append(message)
if message == "Primary image model":
return _Answer(kwargs.get("default"))
if message == "Image base URL":
return _Answer(kwargs.get("default"))
raise AssertionError(f"unexpected text prompt: {message}")
def password(self, message: str, **_kwargs):
if message == "API key":
return _Answer("sk-llm")
raise AssertionError(f"unexpected password prompt: {message}")
def confirm(self, message: str, **_kwargs):
calls.append(message)
if message == "Edit router tier models now?":
return _Answer(False)
if message in {
"Configure a messaging channel now?",
"Configure web search now?",
}:
return _Answer(False)
if message == "Enable image generation now?":
return _Answer(True)
if message == "Image generation enabled?":
return _Answer(True)
raise AssertionError(f"unexpected confirm prompt: {message}")
monkeypatch.setitem(sys.modules, "questionary", _Questionary())
flow.run_interactive_onboard(flow.OnboardOptions())
assert calls.index("Enable image generation now?") > calls.index("Configure web search now?")
data = tomllib.loads(target.read_text())
assert data["image_generation"]["enabled"] is True
assert (
data["image_generation"]["primary"]
== "openrouter/google/gemini-3.1-flash-image-preview"
)
def test_onboard_if_needed_core_ready_repairs_memory_embedding_without_provider_setup(
tmp_path,
monkeypatch,
):
import sys
import tomllib
import types
from opensquilla.gateway.config import (
GatewayConfig,
LlmProviderConfig,
MemoryEmbeddingConfig,
)
from opensquilla.onboarding import flow
from opensquilla.onboarding.config_store import persist_config
target = tmp_path / "c.toml"
cfg = GatewayConfig(config_path=str(target))
cfg.llm = LlmProviderConfig(
provider="openrouter",
model="deepseek/deepseek-v4-flash",
api_key="sk-core",
)
cfg.memory.embedding = MemoryEmbeddingConfig(provider="openai")
persist_config(cfg, path=target, backup=False)
calls: list[str] = []
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
monkeypatch.setenv("OPENAI_API_KEY", "sk-memory-env")
monkeypatch.setattr(flow, "_is_tty", lambda: True)
monkeypatch.setattr(flow, "_wait_for_setup_start", lambda: calls.append("start gate"))
monkeypatch.setattr(flow, "detect_default_sources", lambda: [])
banner_calls: list[tuple[str, str]] = []
monkeypatch.setattr(
flow,
"banner_panel",
lambda title, subtitle: banner_calls.append((title, subtitle)) or title,
)
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary(types.SimpleNamespace):
def confirm(self, message: str, **kwargs):
calls.append(message)
if message == "Configure memory embeddings now?":
assert kwargs.get("default") is True
return _Answer(True)
raise AssertionError(f"unexpected confirm prompt: {message}")
def select(self, message: str, **kwargs):
calls.append(message)
if message == "Memory embedding provider":
return _Answer("openai (OpenAI)")
if message == "Memory API key source":
assert "Use environment variable OPENAI_API_KEY (detected)" in kwargs.get(
"choices", []
)
return _Answer("Use environment variable OPENAI_API_KEY (detected)")
raise AssertionError(f"unexpected select prompt: {message}")
def text(self, message: str, **kwargs):
calls.append(message)
if message == "Memory embedding model":
return _Answer(kwargs.get("default"))
if message == "Memory embedding base URL":
return _Answer(kwargs.get("default"))
raise AssertionError(f"unexpected text prompt: {message}")
def password(self, message: str, **_kwargs):
raise AssertionError(f"unexpected password prompt: {message}")
monkeypatch.setitem(sys.modules, "questionary", _Questionary())
flow.run_interactive_onboard(flow.OnboardOptions(if_needed=True))
assert "LLM provider" not in calls
assert calls.index("Configure memory embeddings now?") < calls.index(
"Memory embedding provider"
)
assert banner_calls == [
(
"OpenSquilla Onboarding",
"Migration · Provider · SquillaRouter · Channels · Capabilities",
)
]
data = tomllib.loads(target.read_text())
assert data["memory"]["embedding"]["provider"] == "openai"
assert data["memory"]["embedding"]["remote"]["api_key_env"] == "OPENAI_API_KEY"
assert "api_key" not in data["memory"]["embedding"]["remote"]
def test_interactive_configure_image_generation_persists(tmp_path, monkeypatch):
import sys
import types
from opensquilla.onboarding import flow
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
monkeypatch.setenv("OPENAI_API_KEY", "sk-image-env")
monkeypatch.setattr(flow, "_is_tty", lambda: True)
calls: list[str] = []
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary(types.SimpleNamespace):
def select(self, message: str, **kwargs):
calls.append(message)
if message == "Image generation provider":
return _Answer("openai (OpenAI Images)")
if message == "Image API key source":
return _Answer("Use environment variable OPENAI_API_KEY")
raise AssertionError(f"unexpected select prompt: {message}")
def text(self, message: str, **kwargs):
calls.append(message)
if message == "Primary image model":
return _Answer(kwargs.get("default"))
if message == "Image base URL":
return _Answer(kwargs.get("default"))
raise AssertionError(f"unexpected text prompt: {message}")
def password(self, message: str, **_kwargs):
raise AssertionError(f"unexpected password prompt: {message}")
def confirm(self, message: str, **kwargs):
calls.append(message)
if message == "Image generation enabled?":
assert kwargs.get("default") is True
return _Answer(True)
raise AssertionError(f"unexpected confirm prompt: {message}")
monkeypatch.setitem(sys.modules, "questionary", _Questionary())
flow.run_interactive_configure("image-generation")
assert calls == [
"Image generation provider",
"Primary image model",
"Image API key source",
"Image base URL",
"Image generation enabled?",
]
# Sparse persistence may omit default-equal keys; pin the reloaded state.
saved = flow.load_config(target)
assert saved.image_generation.enabled is True
assert saved.image_generation.primary == "openai/gpt-image-1"
def test_interactive_configure_image_generation_uses_explicit_config_path(
tmp_path,
monkeypatch,
):
import sys
import types
from opensquilla.onboarding import flow
default_target = tmp_path / "default.toml"
target = tmp_path / "custom.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(default_target))
monkeypatch.setenv("OPENAI_API_KEY", "sk-image-env")
monkeypatch.setattr(flow, "_is_tty", lambda: True)
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary(types.SimpleNamespace):
def select(self, message: str, **kwargs):
if message == "Image generation provider":
return _Answer("openai (OpenAI Images)")
if message == "Image API key source":
return _Answer("Use environment variable OPENAI_API_KEY")
raise AssertionError(f"unexpected select prompt: {message}")
def text(self, message: str, **kwargs):
if message == "Primary image model":
return _Answer(kwargs.get("default"))
if message == "Image base URL":
return _Answer(kwargs.get("default"))
raise AssertionError(f"unexpected text prompt: {message}")
def password(self, message: str, **_kwargs):
raise AssertionError(f"unexpected password prompt: {message}")
def confirm(self, message: str, **kwargs):
if message == "Image generation enabled?":
assert kwargs.get("default") is True
return _Answer(True)
raise AssertionError(f"unexpected confirm prompt: {message}")
monkeypatch.setitem(sys.modules, "questionary", _Questionary())
flow.run_interactive_configure("image-generation", config_path=target)
# Sparse persistence may omit default-equal keys; pin the reloaded state.
saved = flow.load_config(target)
assert saved.image_generation.enabled is True
assert saved.image_generation.providers.openai.api_key_env == "OPENAI_API_KEY"
assert not default_target.exists()
def test_router_tier_overrides_edit_only_selected_tiers():
from opensquilla.gateway.config import GatewayConfig
from opensquilla.onboarding.flow import _router_tier_overrides
calls: list[str] = []
selections = iter(["Route c2", "Done"])
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary:
def select(self, message: str, **kwargs):
calls.append(message)
assert message == "Tier to edit"
assert kwargs.get("choices") == [
"Done",
"Route c0",
"Route c1",
"Route c2",
"Route c3",
"Image model",
]
return _Answer(next(selections))
def text(self, message: str, **kwargs):
calls.append(message)
if message == "c2 provider":
assert kwargs.get("default") == "openrouter"
return _Answer("openrouter")
if message == "c2 model":
assert kwargs.get("default") == "z-ai/glm-5.2"
return _Answer("custom/reasoner")
raise AssertionError(f"unexpected text prompt: {message}")
# Pin the packaged openrouter ladder: the prompt list and per-tier
# defaults asserted above include its curated image tier.
overrides = _router_tier_overrides(
_Questionary(), GatewayConfig(llm={"provider": "openrouter"})
)
assert calls == ["Tier to edit", "c2 provider", "c2 model", "Tier to edit"]
assert overrides == {"c2": {"provider": "openrouter", "model": "custom/reasoner"}}
def test_interactive_feishu_websocket_prompts_only_core_fields(tmp_path, monkeypatch):
import sys
import types
from opensquilla.onboarding import flow
target = tmp_path / "c.toml"
console_output = StringIO()
monkeypatch.setattr(
flow,
"console",
Console(file=console_output, force_terminal=False, highlight=False),
)
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
monkeypatch.setattr(flow, "_is_tty", lambda: True)
monkeypatch.setattr(flow.importlib.util, "find_spec", lambda name: None)
calls: list[str] = []
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary(types.SimpleNamespace):
def select(self, message: str, **kwargs):
calls.append(message)
if message == "Channel type":
return _Answer("feishu")
if message == "Connection mode":
return _Answer(kwargs.get("default") or "websocket")
raise AssertionError(f"unexpected select prompt: {message}")
def text(self, message: str, **kwargs):
calls.append(message)
if message == "Channel name":
assert kwargs.get("default") == "feishu"
return _Answer("feishu")
if message == "App id":
return _Answer("cli_test")
raise AssertionError(f"unexpected text prompt: {message}")
def password(self, message: str, **_kwargs):
calls.append(message)
if message == "App secret":
return _Answer("secret")
raise AssertionError(f"unexpected password prompt: {message}")
def confirm(self, message: str, **_kwargs):
calls.append(message)
raise AssertionError(f"unexpected confirm prompt: {message}")
monkeypatch.setitem(sys.modules, "questionary", _Questionary())
flow.run_interactive_channel_add(None)
out = console_output.getvalue()
normalized_out = " ".join(out.split())
assert "Feishu websocket mode requires the base lark-oapi dependency" in out
assert "Portable zip:" in out
assert "latest recommended portable package" in out
assert "OPENSQUILLA_INSTALL_EXTRAS" not in normalized_out
assert "https://opensquilla.ai/install.ps1" in normalized_out
assert "https://opensquilla.ai/install.sh" in normalized_out
assert "bash -s --" in normalized_out
assert "uv sync --extra recommended" in normalized_out
assert "--extra feishu" not in normalized_out
assert "Restarting alone will not install Python packages." in out
assert calls == ["Channel type", "Channel name", "App id", "App secret", "Connection mode"]
data = target.read_text()
assert 'type = "feishu"' in data
assert 'app_id = "cli_test"' in data
assert 'connection_mode = "websocket"' in data
def test_interactive_channel_add_uses_explicit_config_path(tmp_path, monkeypatch):
import sys
import types
from opensquilla.onboarding import flow
default_target = tmp_path / "default.toml"
target = tmp_path / "custom.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(default_target))
monkeypatch.setattr(flow, "_is_tty", lambda: True)
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary(types.SimpleNamespace):
def select(self, message: str, **kwargs):
if message == "Channel type":
return _Answer("slack")
if message == "Connection mode":
return _Answer("webhook")
raise AssertionError(f"unexpected select prompt: {message}")
def text(self, message: str, **kwargs):
if message == "Channel name":
return _Answer("slack-main")
raise AssertionError(f"unexpected text prompt: {message}")
def password(self, message: str, **_kwargs):
if message == "Bot token (xoxb-...)":
return _Answer("xoxb-test")
if message == "Signing secret":
return _Answer("signing-secret")
raise AssertionError(f"unexpected password prompt: {message}")
def confirm(self, message: str, **_kwargs):
raise AssertionError(f"unexpected confirm prompt: {message}")
def checkbox(self, message: str, **_kwargs):
raise AssertionError(f"unexpected checkbox prompt: {message}")
monkeypatch.setitem(sys.modules, "questionary", _Questionary())
flow.run_interactive_channel_add(None, config_path=target)
data = target.read_text()
assert 'type = "slack"' in data
assert 'connection_mode = "webhook"' in data
assert 'signing_secret = "signing-secret"' in data
assert not default_target.exists()
def test_interactive_slack_channel_add_can_select_socket_mode(tmp_path, monkeypatch):
import sys
import types
from opensquilla.onboarding import flow
target = tmp_path / "socket.toml"
monkeypatch.setattr(flow, "_is_tty", lambda: True)
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary(types.SimpleNamespace):
def select(self, message: str, **kwargs):
if message == "Channel type":
return _Answer("slack")
if message == "Connection mode":
return _Answer("socket")
raise AssertionError(f"unexpected select prompt: {message}")
def text(self, message: str, **kwargs):
if message == "Channel name":
return _Answer("slack-socket")
raise AssertionError(f"unexpected text prompt: {message}")
def password(self, message: str, **_kwargs):
if message == "Bot token (xoxb-...)":
return _Answer("xoxb-test")
if message == "App-level token (xapp-...)":
return _Answer("xapp-test")
raise AssertionError(f"unexpected password prompt: {message}")
def confirm(self, message: str, **_kwargs):
raise AssertionError(f"unexpected confirm prompt: {message}")
def checkbox(self, message: str, **_kwargs):
raise AssertionError(f"unexpected checkbox prompt: {message}")
monkeypatch.setitem(sys.modules, "questionary", _Questionary())
flow.run_interactive_channel_add(None, config_path=target)
data = target.read_text()
assert 'type = "slack"' in data
assert 'connection_mode = "socket"' in data
assert 'app_token = "xapp-test"' in data
assert "signing_secret" not in data
def test_optional_onboarding_section_receives_explicit_config_path(tmp_path):
from opensquilla.onboarding import flow
target = tmp_path / "custom.toml"
seen = {}
def runner(*, config_path=None):
seen["config_path"] = config_path
flow._run_optional_section(
section="search",
label="search",
runner=runner,
config_path=target,
)
assert seen["config_path"] == target
def test_channel_saved_output_separates_configured_from_connected(monkeypatch):
from opensquilla.onboarding import flow
from opensquilla.onboarding.flow import _print_channel_saved
console_output = StringIO()
monkeypatch.setattr(
flow,
"console",
Console(file=console_output, force_terminal=False, highlight=False),
)
_print_channel_saved("feishu")
out = console_output.getvalue()
assert "configured, not connected yet" in out
assert "Restart the gateway process" in out
assert "opensquilla channels status feishu --json" in out
def test_search_provider_key_defaults_to_pasted_key_with_brave_hint(monkeypatch):
from opensquilla.onboarding.flow import _ask_search_fields
from opensquilla.onboarding.search_specs import get_search_provider_setup_spec
monkeypatch.delenv("BRAVE_SEARCH_API_KEY", raising=False)
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary:
def select(self, message: str, **kwargs):
if message == "Search fallback policy":
return _Answer(kwargs.get("default"))
raise AssertionError(f"unexpected select prompt: {message}")
def confirm(self, message: str, **kwargs):
if message == "Use environment proxy for search?":
return _Answer(False)
if message == (
"Enable search diagnostics? Include provider attempt/error details "
"for troubleshooting?"
):
return _Answer(False)
raise AssertionError(f"unexpected confirm prompt: {message}")
def password(self, message: str, **_kwargs):
assert message == (
"Brave Search API key "
"(create one at https://api-dashboard.search.brave.com/app/keys)"
)
return _Answer("brave-secret")
def text(self, message: str, **kwargs):
if message == "Max search results":
return _Answer(kwargs.get("default"))
if message == "Search HTTP proxy":
return _Answer("")
raise AssertionError(f"unexpected text prompt: {message}")
answers = _ask_search_fields(
_Questionary(),
get_search_provider_setup_spec("brave"),
)
assert answers["api_key"] == "brave-secret"
assert answers["api_key_env"] == ""
def test_search_provider_detected_env_prefers_env_but_can_use_manual_key(monkeypatch):
from opensquilla.onboarding.flow import _ask_search_fields
from opensquilla.onboarding.search_specs import get_search_provider_setup_spec
monkeypatch.setenv("BRAVE_SEARCH_API_KEY", "from-env")
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary:
def select(self, message: str, **kwargs):
if message == "Search fallback policy":
return _Answer(kwargs.get("default"))
raise AssertionError(f"unexpected select prompt: {message}")
def confirm(self, message: str, **kwargs):
if message == "Use BRAVE_SEARCH_API_KEY from environment?":
assert kwargs.get("default") is True
return _Answer(False)
if message == "Use environment proxy for search?":
return _Answer(False)
if message == (
"Enable search diagnostics? Include provider attempt/error details "
"for troubleshooting?"
):
return _Answer(False)
raise AssertionError(f"unexpected confirm prompt: {message}")
def password(self, message: str, **_kwargs):
assert message == (
"Brave Search API key "
"(create one at https://api-dashboard.search.brave.com/app/keys)"
)
return _Answer("manual-brave-secret")
def text(self, message: str, **kwargs):
if message == "Max search results":
return _Answer(kwargs.get("default"))
if message == "Search HTTP proxy":
return _Answer("")
raise AssertionError(f"unexpected text prompt: {message}")
answers = _ask_search_fields(
_Questionary(),
get_search_provider_setup_spec("brave"),
)
assert answers["api_key"] == "manual-brave-secret"
assert answers["api_key_env"] == ""
def test_search_provider_can_use_detected_env_when_requested(monkeypatch):
from opensquilla.onboarding.flow import _ask_search_fields
from opensquilla.onboarding.search_specs import get_search_provider_setup_spec
monkeypatch.setenv("BRAVE_SEARCH_API_KEY", "from-env")
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary:
def select(self, message: str, **kwargs):
if message == "Search fallback policy":
return _Answer(kwargs.get("default"))
raise AssertionError(f"unexpected select prompt: {message}")
def confirm(self, message: str, **kwargs):
if message == "Use BRAVE_SEARCH_API_KEY from environment?":
assert kwargs.get("default") is True
return _Answer(True)
if message == "Use environment proxy for search?":
return _Answer(False)
if message == (
"Enable search diagnostics? Include provider attempt/error details "
"for troubleshooting?"
):
return _Answer(False)
raise AssertionError(f"unexpected confirm prompt: {message}")
def password(self, message: str, **_kwargs):
raise AssertionError(f"unexpected password prompt: {message}")
def text(self, message: str, **kwargs):
if message == "Max search results":
return _Answer(kwargs.get("default"))
if message == "Search HTTP proxy":
return _Answer("")
raise AssertionError(f"unexpected text prompt: {message}")
answers = _ask_search_fields(
_Questionary(),
get_search_provider_setup_spec("brave"),
)
assert answers["api_key"] == ""
assert answers["api_key_env"] == "BRAVE_SEARCH_API_KEY"
def test_search_fallback_choice_names_duckduckgo_and_persists_value(monkeypatch):
from opensquilla.onboarding.flow import _ask_search_fields
from opensquilla.onboarding.search_specs import get_search_provider_setup_spec
monkeypatch.delenv("BRAVE_SEARCH_API_KEY", raising=False)
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary:
def select(self, message: str, **kwargs):
if message == "Search fallback policy":
choices = kwargs.get("choices")
assert choices == [
"off - no fallback; surface the original provider error",
"network - retry with DuckDuckGo on timeout/network errors",
]
assert kwargs.get("default") == choices[0]
return _Answer(choices[1])
raise AssertionError(f"unexpected select prompt: {message}")
def confirm(self, message: str, **_kwargs):
if message == "Use environment proxy for search?":
return _Answer(False)
if message == (
"Enable search diagnostics? Include provider attempt/error details "
"for troubleshooting?"
):
return _Answer(False)
raise AssertionError(f"unexpected confirm prompt: {message}")
def password(self, message: str, **_kwargs):
return _Answer("brave-secret")
def text(self, message: str, **kwargs):
if message == "Max search results":
return _Answer(kwargs.get("default"))
if message == "Search HTTP proxy":
return _Answer("")
raise AssertionError(f"unexpected text prompt: {message}")
answers = _ask_search_fields(
_Questionary(),
get_search_provider_setup_spec("brave"),
)
assert answers["fallback_policy"] == "network"
def test_search_provider_can_use_masked_api_key_prompt(monkeypatch):
from opensquilla.onboarding.flow import _ask_search_fields
from opensquilla.onboarding.search_specs import get_search_provider_setup_spec
monkeypatch.delenv("BRAVE_SEARCH_API_KEY", raising=False)
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary:
def select(self, message: str, **kwargs):
if message == "Search fallback policy":
return _Answer(kwargs.get("default"))
raise AssertionError(f"unexpected select prompt: {message}")
def password(self, message: str, **_kwargs):
assert message == (
"Brave Search API key "
"(create one at https://api-dashboard.search.brave.com/app/keys)"
)
return _Answer("brave-secret")
def text(self, message: str, **kwargs):
if message == "Max search results":
return _Answer(kwargs.get("default"))
if message == "Search HTTP proxy":
return _Answer("")
raise AssertionError(f"unexpected text prompt: {message}")
def confirm(self, message: str, **_kwargs):
if message == "Use environment proxy for search?":
return _Answer(False)
if message == (
"Enable search diagnostics? Include provider attempt/error details "
"for troubleshooting?"
):
return _Answer(False)
raise AssertionError(f"unexpected confirm prompt: {message}")
answers = _ask_search_fields(
_Questionary(),
get_search_provider_setup_spec("brave"),
)
assert answers["api_key"] == "brave-secret"
assert answers["api_key_env"] == ""
def test_noninteractive_provider_configure_writes_config(tmp_path, monkeypatch):
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
from opensquilla.onboarding.flow import run_noninteractive_provider_configure
result = run_noninteractive_provider_configure(
"openrouter",
{"model": "deepseek/deepseek-v4-flash", "api_key": "sk"},
)
assert result.path == target
assert "openrouter" in target.read_text()
def test_flow_module_exposes_no_engine_bypassing_noninteractive_writers():
"""Headless channel/search writes go through the CLI's SetupEngine path;
the old module-level helpers persisted config while bypassing the
engine's restart/warning accumulation and must stay deleted."""
from opensquilla.onboarding import flow
assert not hasattr(flow, "run_noninteractive_channel_add")
assert not hasattr(flow, "run_noninteractive_search_configure")
def test_interactive_configure_search_uses_explicit_config_path(tmp_path, monkeypatch):
import sys
import tomllib
import types
from opensquilla.onboarding import flow
default_target = tmp_path / "default.toml"
target = tmp_path / "custom.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(default_target))
monkeypatch.setattr(flow, "_is_tty", lambda: True)
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary(types.SimpleNamespace):
def select(self, message: str, **kwargs):
if message == "Search provider":
return _Answer("duckduckgo (DuckDuckGo)")
if message == "Search fallback policy":
return _Answer(kwargs.get("default"))
raise AssertionError(f"unexpected select prompt: {message}")
def text(self, message: str, **kwargs):
if message == "Max search results":
return _Answer("7")
if message == "Search HTTP proxy":
return _Answer("")
raise AssertionError(f"unexpected text prompt: {message}")
def password(self, message: str, **_kwargs):
raise AssertionError(f"unexpected password prompt: {message}")
def confirm(self, message: str, **_kwargs):
if message in {
"Use environment proxy for search?",
flow._SEARCH_DIAGNOSTICS_PROMPT,
}:
return _Answer(False)
raise AssertionError(f"unexpected confirm prompt: {message}")
monkeypatch.setitem(sys.modules, "questionary", _Questionary())
flow.run_interactive_configure("search", config_path=target)
data = tomllib.loads(target.read_text())
# Sparse persistence may omit default-equal keys (duckduckgo is the
# default provider); pin the reloaded semantic state instead.
saved = flow.load_config(target)
assert saved.search_provider == "duckduckgo"
assert data["search_max_results"] == 7
assert not default_target.exists()
def test_interactive_configure_memory_embedding_is_in_section_menu(
tmp_path,
monkeypatch,
):
import sys
import tomllib
import types
from opensquilla.onboarding import flow
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
monkeypatch.setenv("OPENAI_API_KEY", "sk-memory-env")
monkeypatch.setattr(flow, "_is_tty", lambda: True)
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary(types.SimpleNamespace):
def select(self, message: str, **kwargs):
if message == "Section":
titles = kwargs["choices"]
target_title = next(
(t for t in titles if t.startswith("Memory embedding")), None
)
if target_title is not None and not getattr(self, "_dispatched", False):
self._dispatched = True
return _Answer(target_title)
done = next(
t for t in titles if t in ("Done", "Exit (nothing changed)")
)
return _Answer(done)
if message == "Memory embedding provider":
return _Answer("openai (OpenAI)")
if message == "Memory API key source":
return _Answer("Use environment variable OPENAI_API_KEY")
raise AssertionError(f"unexpected select prompt: {message}")
def text(self, message: str, **kwargs):
if message == "Memory embedding model":
return _Answer("text-embedding-3-small")
if message == "Memory embedding base URL":
return _Answer(kwargs.get("default"))
raise AssertionError(f"unexpected text prompt: {message}")
def password(self, message: str, **_kwargs):
raise AssertionError(f"unexpected password prompt: {message}")
monkeypatch.setitem(sys.modules, "questionary", _Questionary())
flow.run_interactive_configure(config_path=target)
data = tomllib.loads(target.read_text())
remote = data["memory"]["embedding"]["remote"]
assert data["memory"]["embedding"]["provider"] == "openai"
assert remote["api_key_env"] == "OPENAI_API_KEY"
assert "api_key" not in remote
def test_interactive_memory_embedding_configure_without_tty_prints_hint(
tmp_path,
monkeypatch,
capsys,
):
from opensquilla.onboarding import flow
target = tmp_path / "c.toml"
monkeypatch.setattr(flow, "_is_tty", lambda: False)
result = flow.run_interactive_memory_embedding_configure(config_path=target)
assert result.warnings == ["tty_required"]
assert not target.exists()
out = capsys.readouterr().out
assert "Headless memory embedding:" in out
assert "opensquilla onboard configure memory-embedding --config" in out
def test_interactive_configure_provider_accepts_singular_section_alias(
tmp_path,
monkeypatch,
):
from opensquilla.onboarding import flow
from opensquilla.onboarding.config_store import PersistResult
target = tmp_path / "custom.toml"
seen = {}
monkeypatch.setattr(flow, "_is_tty", lambda: True)
def fake_run_interactive_onboard(options):
seen["config_path"] = options.config_path
return PersistResult(
path=target,
backup_path=None,
restart_required=False,
warnings=[],
)
monkeypatch.setattr(flow, "run_interactive_onboard", fake_run_interactive_onboard)
result = flow.run_interactive_configure("provider", config_path=target)
assert result is not None
assert seen["config_path"] == target
def test_interactive_configure_router_persists(tmp_path, monkeypatch):
import sys
import tomllib
import types
from opensquilla.onboarding import flow
target = tmp_path / "c.toml"
target.write_text(
'[llm]\nprovider = "openrouter"\nmodel = "deepseek/deepseek-v4-flash"\n',
encoding="utf-8",
)
monkeypatch.setattr(flow, "_is_tty", lambda: True)
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary(types.SimpleNamespace):
def select(self, message: str, **kwargs):
if message == "Router mode":
return _Answer("Disabled")
raise AssertionError(f"unexpected select prompt: {message}")
def text(self, message: str, **_kwargs):
raise AssertionError(f"unexpected text prompt: {message}")
def password(self, message: str, **_kwargs):
raise AssertionError(f"unexpected password prompt: {message}")
def confirm(self, message: str, **_kwargs):
raise AssertionError(f"unexpected confirm prompt: {message}")
def checkbox(self, message: str, **_kwargs):
raise AssertionError(f"unexpected checkbox prompt: {message}")
monkeypatch.setitem(sys.modules, "questionary", _Questionary())
flow.run_interactive_configure("router", config_path=target)
data = tomllib.loads(target.read_text())
assert data["squilla_router"]["enabled"] is False
def test_interactive_configure_provider_receives_explicit_config_path(
tmp_path,
monkeypatch,
):
from opensquilla.onboarding import flow
from opensquilla.onboarding.config_store import PersistResult
target = tmp_path / "custom.toml"
seen = {}
monkeypatch.setattr(flow, "_is_tty", lambda: True)
def fake_run_interactive_onboard(options):
seen["config_path"] = options.config_path
return PersistResult(
path=target,
backup_path=None,
restart_required=False,
warnings=[],
)
monkeypatch.setattr(flow, "run_interactive_onboard", fake_run_interactive_onboard)
result = flow.run_interactive_configure("providers", config_path=target)
assert result is not None
assert seen["config_path"] == target
def test_interactive_configure_without_tty_does_not_create_config(
tmp_path, monkeypatch, capsys
):
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
from opensquilla.onboarding import flow
monkeypatch.setattr(flow, "_is_tty", lambda: False)
result = flow.run_interactive_configure("providers")
assert result is None
out = capsys.readouterr().out
assert "Guided CLI:" in out
assert "Provider recipes:" in out
assert "Headless provider:" not in out
assert "Check status:" in out
assert not target.exists()
def test_interactive_configure_provider_scopes_out_migration_and_optional_sections(
tmp_path,
monkeypatch,
):
"""``onboard configure provider`` is a scoped key swap: it must not
re-trigger the legacy-migration pre-step or the optional image-generation
prompt that the full first-run wizard walks through."""
from opensquilla.onboarding import flow
from opensquilla.onboarding.config_store import PersistResult
target = tmp_path / "custom.toml"
seen = {}
monkeypatch.setattr(flow, "_is_tty", lambda: True)
def fake_run_interactive_onboard(options):
seen["options"] = options
return PersistResult(
path=target,
backup_path=None,
restart_required=False,
warnings=[],
)
monkeypatch.setattr(flow, "run_interactive_onboard", fake_run_interactive_onboard)
result = flow.run_interactive_configure("provider", config_path=target)
assert result is not None
options = seen["options"]
assert options.skip_migration is True
assert options.skip_image_generation is True
assert options.skip_channels is True
assert options.skip_search is True
assert options.config_path == target
def test_scoped_section_run_skips_banner_start_gate_and_trailing_prompts(
tmp_path,
monkeypatch,
):
"""A scoped section entry (``configure provider``, the hub's Provider
item) is not a first run: it must not re-print the onboarding banner,
must not block on the raw "Press Enter to start setup" ``input()`` gate
(where Ctrl+C would escape the hub's cancel handling entirely), and must
stop at its own section instead of appending the full walk's trailing
action-required prompts."""
import sys
import types
from opensquilla.onboarding import flow
target = tmp_path / "c.toml"
# Memory embedding needs action: on a FULL walk this appends the
# "Configure memory embeddings now?" prompt after the provider save.
target.write_text(
"[llm]\n"
'provider = "openrouter"\n'
'model = "dummy/model"\n'
'api_key = "sk-dummy"\n'
"\n"
"[memory.embedding]\n"
'provider = "openai"\n',
encoding="utf-8",
)
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
monkeypatch.setattr(flow, "_is_tty", lambda: True)
monkeypatch.setattr(flow, "_run_provider_probe", lambda **_kw: None)
banner_calls: list[tuple[str, str]] = []
monkeypatch.setattr(
flow,
"banner_panel",
lambda title, subtitle: banner_calls.append((title, subtitle)) or title,
)
gate_calls: list[str] = []
monkeypatch.setattr(flow, "_wait_for_setup_start", lambda: gate_calls.append("gate"))
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary(types.SimpleNamespace):
def select(self, message: str, **kwargs):
if message == "LLM provider":
# Pin OpenRouter explicitly: this test exercises scoped-section
# mechanics, not the provider choice, and the picker default is
# TokenRhythm (whose offline probe would add a "Save anyway?"
# prompt this walkthrough does not expect).
return _Answer("openrouter (OpenRouter)")
if message in {"Router mode", "Default text model"}:
choices = list(kwargs.get("choices") or [])
return _Answer(kwargs.get("default") or choices[0])
if message == "LLM API key source":
return _Answer("Paste API key now")
raise AssertionError(f"unexpected select prompt: {message}")
def password(self, message: str, **_kwargs):
return _Answer("sk-scoped-rotation")
def text(self, message: str, **_kwargs):
raise AssertionError(f"unexpected text prompt: {message}")
def confirm(self, message: str, **kwargs):
if message == "Edit router tier models now?":
return _Answer(False)
raise AssertionError(
f"a scoped section edit must not prompt: {message}"
)
def checkbox(self, message: str, **_kwargs):
raise AssertionError(f"unexpected checkbox prompt: {message}")
monkeypatch.setitem(sys.modules, "questionary", _Questionary())
result = flow.run_interactive_configure("provider", config_path=target)
assert result is not None
assert banner_calls == [], "scoped edits must not re-print the first-run banner"
assert gate_calls == [], "scoped edits must not block on the Enter start gate"
saved = flow.load_config(target)
assert saved.llm.api_key == "sk-scoped-rotation"
def test_full_walk_folds_optional_section_restart_flag_into_result(
tmp_path,
monkeypatch,
):
"""An optional section's ``restart_required`` (e.g. memory embedding)
must reach the CLI's restart guidance: ``_run_optional_section`` used to
drop the runner's PersistResult, so the full walk returned only the
provider-stage result with ``restart_required=False``."""
import sys
import types
from opensquilla.onboarding import flow
from opensquilla.onboarding.config_store import PersistResult
target = tmp_path / "c.toml"
# Memory embedding action-required after the provider save (remote
# provider, no key), llm NOT configured so the walk runs linearly.
target.write_text('[memory.embedding]\nprovider = "openai"\n', encoding="utf-8")
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
monkeypatch.setattr(flow, "_is_tty", lambda: True)
monkeypatch.setattr(flow, "_wait_for_setup_start", lambda: None)
monkeypatch.setattr(flow, "detect_default_sources", lambda: [])
monkeypatch.setattr(flow, "_run_provider_probe", lambda **_kw: None)
embedding_runs: list[object] = []
def _fake_embedding_runner(*, config_path=None):
embedding_runs.append(config_path)
return PersistResult(
path=target,
backup_path=None,
restart_required=True,
warnings=["w-embedding"],
)
monkeypatch.setattr(
flow, "run_interactive_memory_embedding_configure", _fake_embedding_runner
)
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary(types.SimpleNamespace):
def select(self, message: str, **kwargs):
if message == "LLM provider":
# Pin OpenRouter explicitly: this test exercises scoped-section
# mechanics, not the provider choice, and the picker default is
# TokenRhythm (whose offline probe would add a "Save anyway?"
# prompt this walkthrough does not expect).
return _Answer("openrouter (OpenRouter)")
if message in {"Router mode", "Default text model"}:
choices = list(kwargs.get("choices") or [])
return _Answer(kwargs.get("default") or choices[0])
if message == "LLM API key source":
return _Answer("Paste API key now")
raise AssertionError(f"unexpected select prompt: {message}")
def password(self, message: str, **_kwargs):
return _Answer("sk-full-walk")
def text(self, message: str, **_kwargs):
raise AssertionError(f"unexpected text prompt: {message}")
def confirm(self, message: str, **kwargs):
if message == "Edit router tier models now?":
return _Answer(False)
if message in {
"Configure a messaging channel now?",
"Configure web search now?",
"Enable image generation now?",
}:
return _Answer(False)
if message == "Configure memory embeddings now?":
return _Answer(True)
raise AssertionError(f"unexpected confirm prompt: {message}")
def checkbox(self, message: str, **_kwargs):
raise AssertionError(f"unexpected checkbox prompt: {message}")
monkeypatch.setitem(sys.modules, "questionary", _Questionary())
result = flow.run_interactive_onboard(flow.OnboardOptions(config_path=target))
assert embedding_runs == [target]
assert result.restart_required is True, (
"the embedding section's restart flag must survive to the CLI boundary"
)
assert "w-embedding" in result.warnings
def test_interactive_configure_dispatches_short_image_and_memory_aliases(
tmp_path,
monkeypatch,
):
"""The CLI help advertises ``configure image`` / ``configure memory``;
the wizard dispatch must consume the setup engine's alias sets instead
of a hand-copied subset that dropped the short spellings and answered
"not yet supported"."""
from opensquilla.onboarding import flow
from opensquilla.onboarding.config_store import PersistResult
target = tmp_path / "custom.toml"
dispatched: list[str] = []
monkeypatch.setattr(flow, "_is_tty", lambda: True)
def _fake_runner(name):
def runner(config_path=None):
dispatched.append(name)
assert config_path == target
return PersistResult(
path=target,
backup_path=None,
restart_required=False,
warnings=[],
)
return runner
monkeypatch.setattr(
flow,
"run_interactive_image_generation_configure",
_fake_runner("image-generation"),
)
monkeypatch.setattr(
flow,
"run_interactive_memory_embedding_configure",
_fake_runner("memory-embedding"),
)
assert flow.run_interactive_configure("image", config_path=target) is not None
assert flow.run_interactive_configure("memory", config_path=target) is not None
assert dispatched == ["image-generation", "memory-embedding"]
class _RecordingConsole:
def __init__(self):
self.messages: list[str] = []
def print(self, message="", *_a, **_kw):
self.messages.append(str(message))
def joined(self) -> str:
return "\n".join(self.messages)
def test_interactive_configure_unknown_section_names_explicit_config_path(
tmp_path,
monkeypatch,
):
"""The unsupported-section notice must name the config file this run
would actually edit (the explicit ``--config`` path here), not a
hardcoded ``~/.opensquilla/config.toml``."""
from opensquilla.onboarding import flow
target = tmp_path / "custom.toml"
recorder = _RecordingConsole()
monkeypatch.setattr(flow, "console", recorder)
monkeypatch.setattr(flow, "_is_tty", lambda: True)
result = flow.run_interactive_configure(
"definitely-not-a-section", config_path=target
)
assert result is None
joined = recorder.joined()
assert str(target) in joined
assert "~/.opensquilla/config.toml" not in joined
def test_interactive_configure_unknown_section_honours_env_config_path(
tmp_path,
monkeypatch,
):
from opensquilla.onboarding import flow
target = tmp_path / "env-config.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
recorder = _RecordingConsole()
monkeypatch.setattr(flow, "console", recorder)
monkeypatch.setattr(flow, "_is_tty", lambda: True)
result = flow.run_interactive_configure("definitely-not-a-section")
assert result is None
joined = recorder.joined()
assert str(target) in joined
assert "~/.opensquilla/config.toml" not in joined
def test_interactive_memory_embedding_configure_reports_mutation_restart_required(
tmp_path,
monkeypatch,
):
"""The embedding mutation reports ``restart_required`` when the setup
actually changed (embedding edits only take effect after a full gateway
restart); the interactive runner used to hardcode ``False`` over it."""
import sys
import types
from opensquilla.onboarding import flow
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
monkeypatch.setenv("OPENAI_API_KEY", "sk-memory-env")
monkeypatch.setattr(flow, "_is_tty", lambda: True)
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary(types.SimpleNamespace):
def select(self, message: str, **kwargs):
if message == "Memory embedding provider":
return _Answer("openai (OpenAI)")
if message == "Memory API key source":
return _Answer("Use environment variable OPENAI_API_KEY (detected)")
raise AssertionError(f"unexpected select prompt: {message}")
def text(self, message: str, **kwargs):
if message in {"Memory embedding model", "Memory embedding base URL"}:
return _Answer(kwargs.get("default"))
raise AssertionError(f"unexpected text prompt: {message}")
def password(self, message: str, **_kwargs):
raise AssertionError(f"unexpected password prompt: {message}")
def confirm(self, message: str, **_kwargs):
raise AssertionError(f"unexpected confirm prompt: {message}")
def checkbox(self, message: str, **_kwargs):
raise AssertionError(f"unexpected checkbox prompt: {message}")
monkeypatch.setitem(sys.modules, "questionary", _Questionary())
result = flow.run_interactive_memory_embedding_configure(config_path=target)
assert result.restart_required is True
def test_flow_config_cli_arg_is_powershell_safe_on_windows(monkeypatch):
"""flow's resume hints must quote --config with the shared platform-aware
helper: shlex's '"'"' escape is invalid PowerShell, while next_steps and
onboard status already print the PowerShell-quoted form of the same
command in the same session."""
from opensquilla.onboarding import flow, next_steps
monkeypatch.setattr(next_steps.platform, "system", lambda: "Windows")
arg = flow._config_cli_arg("C:\\Setup Files\\config.toml")
assert arg == " --config 'C:\\Setup Files\\config.toml'"
assert arg == next_steps._config_cli_arg("C:\\Setup Files\\config.toml")
quoted = flow._config_cli_arg("C:\\it's.toml")
assert quoted == " --config 'C:\\it''s.toml'"
assert '\'"\'"\'' not in quoted
def test_declared_questionary_floor_supports_use_search_filter():
"""The provider select passes ``use_search_filter=True`` — a questionary
2.1 kwarg that crashes 2.0.x installs at prompt construction with
``TypeError: PromptSession.__init__() got an unexpected keyword argument``.
The declared dependency floor must therefore exclude every 2.0.x release
the wizard cannot run on (dev/CI lockfiles mask this, downstream installs
resolving the floor do not)."""
import tomllib
from pathlib import Path
from packaging.requirements import Requirement
pyproject = Path(__file__).resolve().parents[2] / "pyproject.toml"
data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
requirement = next(
Requirement(dep)
for dep in data["project"]["dependencies"]
if Requirement(dep).name == "questionary"
)
assert not requirement.specifier.contains("2.0.1"), (
"questionary 2.0.x lacks use_search_filter; the floor must be >=2.1"
)
assert requirement.specifier.contains("2.1.0")