aba28293b7
cli.py — plugin_install() now asks "Configure now?" after a successful install, runs the plugin's setup wizard, and offers to enable inline. Failing to install short-circuits before the prompt is shown. wizard.py — _offer_telegram_setup_if_selected() runs install + wizard + enable automatically at the end of pyra setup when the user selected "Communication bots". Adds load_config import (was missing alongside save_config). Tests: test_plugin_install_decline_setup, test_plugin_install_error_does_not_prompt. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
184 lines
6.2 KiB
Python
184 lines
6.2 KiB
Python
import pytest
|
|
from click.testing import CliRunner
|
|
|
|
from pyra.cli import main
|
|
|
|
|
|
def test_memory_write_creates_file(tmp_pyra_home):
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, ["memory", "write", "user/note.md", "Hello world"])
|
|
assert result.exit_code == 0
|
|
assert (tmp_pyra_home / "memory" / "user" / "note.md").exists()
|
|
assert "Hello world" in (tmp_pyra_home / "memory" / "user" / "note.md").read_text()
|
|
|
|
|
|
def test_memory_write_updates_db(tmp_pyra_home):
|
|
runner = CliRunner()
|
|
runner.invoke(main, ["memory", "write", "user/note.md", "DB test content"])
|
|
from pyra.memory import database
|
|
rows = database.list_all()
|
|
assert any(r["path"] == "user/note.md" for r in rows)
|
|
|
|
|
|
def test_memory_append_adds_content(tmp_pyra_home):
|
|
runner = CliRunner()
|
|
runner.invoke(main, ["memory", "write", "user/note.md", "First line"])
|
|
runner.invoke(main, ["memory", "append", "user/note.md", "Second line"])
|
|
content = (tmp_pyra_home / "memory" / "user" / "note.md").read_text()
|
|
assert "First line" in content
|
|
assert "Second line" in content
|
|
|
|
|
|
def test_memory_read_existing(tmp_pyra_home):
|
|
from pyra.memory.writer import write_memory
|
|
write_memory("user/note.md", "Readable content")
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, ["memory", "read", "user/note.md"])
|
|
assert result.exit_code == 0
|
|
|
|
|
|
def test_memory_read_missing_exits_cleanly(tmp_pyra_home):
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, ["memory", "read", "user/does_not_exist.md"])
|
|
assert result.exit_code == 0
|
|
|
|
|
|
def test_memory_read_blocked_path_exits_cleanly(tmp_pyra_home):
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, ["memory", "read", "../../../../vault/secrets/api_keys.json"])
|
|
assert result.exit_code == 0
|
|
|
|
|
|
def test_memory_list_empty(tmp_pyra_home):
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, ["memory", "list"])
|
|
assert result.exit_code == 0
|
|
|
|
|
|
def test_memory_list_populated(tmp_pyra_home):
|
|
from pyra.memory.writer import write_memory
|
|
write_memory("user/profile.md", "# Profile")
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, ["memory", "list"])
|
|
assert result.exit_code == 0
|
|
|
|
|
|
def test_plugin_list_no_config(tmp_pyra_home):
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, ["plugin", "list"])
|
|
assert result.exit_code == 0
|
|
|
|
|
|
def _make_config():
|
|
from pyra.config.schema import PyraConfig, ProviderConfig
|
|
return PyraConfig(ai=ProviderConfig(provider_id="lmstudio", model="gemma"))
|
|
|
|
|
|
def test_plugin_enable_not_installed(tmp_pyra_home):
|
|
from pyra.config.manager import save_config
|
|
save_config(_make_config())
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, ["plugin", "enable", "nonexistent"])
|
|
assert result.exit_code == 0
|
|
|
|
|
|
def test_plugin_enable_and_disable(tmp_pyra_home):
|
|
from pyra.config.manager import load_config, save_config
|
|
|
|
save_config(_make_config())
|
|
|
|
plugin_dir = tmp_pyra_home / "plugins" / "myplugin"
|
|
plugin_dir.mkdir(parents=True, exist_ok=True)
|
|
(plugin_dir / "manifest.json").write_text('{"name": "myplugin", "version": "1.0"}')
|
|
|
|
runner = CliRunner()
|
|
|
|
runner.invoke(main, ["plugin", "enable", "myplugin"])
|
|
cfg = load_config()
|
|
assert "myplugin" in cfg.plugins.enabled
|
|
|
|
runner.invoke(main, ["plugin", "disable", "myplugin"])
|
|
cfg = load_config()
|
|
assert "myplugin" not in cfg.plugins.enabled
|
|
|
|
|
|
def test_daemon_commands_exit_cleanly(tmp_pyra_home):
|
|
runner = CliRunner()
|
|
for cmd in ["start", "stop", "status", "restart"]:
|
|
result = runner.invoke(main, ["daemon", cmd])
|
|
assert result.exit_code == 0, f"daemon {cmd} exited with {result.exit_code}"
|
|
|
|
|
|
def test_main_calls_setup_wizard_when_no_config(tmp_pyra_home, monkeypatch):
|
|
setup_calls = []
|
|
monkeypatch.setattr("pyra.setup.wizard.run_setup", lambda: setup_calls.append(1))
|
|
monkeypatch.setattr("pyra.chat.session.start_chat", lambda: None)
|
|
|
|
runner = CliRunner()
|
|
runner.invoke(main, [])
|
|
|
|
assert len(setup_calls) == 1, "run_setup should be called once when no config exists"
|
|
|
|
|
|
def test_main_skips_setup_when_config_exists(tmp_pyra_home, monkeypatch):
|
|
from pyra.config.manager import save_config
|
|
save_config(_make_config())
|
|
|
|
setup_calls = []
|
|
monkeypatch.setattr("pyra.setup.wizard.run_setup", lambda: setup_calls.append(1))
|
|
monkeypatch.setattr("pyra.chat.session.start_chat", lambda: None)
|
|
|
|
runner = CliRunner()
|
|
runner.invoke(main, [])
|
|
|
|
assert len(setup_calls) == 0, "run_setup should NOT be called when config already exists"
|
|
|
|
|
|
def test_config_slash_command_registered():
|
|
from pyra.chat.session import _STATIC_COMMANDS
|
|
assert "/config" in _STATIC_COMMANDS
|
|
|
|
|
|
# ── plugin install ────────────────────────────────────────────────────────────
|
|
|
|
def test_plugin_install_decline_setup(tmp_pyra_home, monkeypatch):
|
|
"""Declining 'Configure now?' shows manual instructions and exits cleanly."""
|
|
from unittest.mock import MagicMock
|
|
import questionary
|
|
|
|
monkeypatch.setattr(
|
|
"pyra.plugins.install.install_bundled_plugin", lambda *a, **kw: None
|
|
)
|
|
monkeypatch.setattr(
|
|
questionary, "confirm",
|
|
lambda *a, **kw: MagicMock(ask=lambda: False),
|
|
)
|
|
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, ["plugin", "install", "telegram_bot"])
|
|
assert result.exit_code == 0
|
|
assert "Installed" in result.output
|
|
assert "Configure later" in result.output
|
|
|
|
|
|
def test_plugin_install_error_does_not_prompt(tmp_pyra_home, monkeypatch):
|
|
"""If install fails, the configure prompt is never shown."""
|
|
from unittest.mock import MagicMock
|
|
import questionary
|
|
|
|
monkeypatch.setattr(
|
|
"pyra.plugins.install.install_bundled_plugin",
|
|
lambda *a, **kw: (_ for _ in ()).throw(FileNotFoundError("not found")),
|
|
)
|
|
confirm_calls = []
|
|
monkeypatch.setattr(
|
|
questionary, "confirm",
|
|
lambda *a, **kw: confirm_calls.append(1) or MagicMock(ask=lambda: False),
|
|
)
|
|
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, ["plugin", "install", "telegram_bot"])
|
|
assert result.exit_code == 0
|
|
assert "Error" in result.output
|
|
assert len(confirm_calls) == 0 # prompt never reached
|