Files
Pyra/tests/unit/test_config_tui.py
T
curo1305 ee6c32b035 feat(tui): keyboard-only ASCII redesign of config TUI
Remove all Button widgets — saves and plugin toggles are keyboard-only
(ctrl+s, e, d). Replace Header with a plain _TitleBar Static. Apply a
dark monochrome ASCII theme: +---+ borders on inputs, DataTable, and
tab panes; #0d0d0d background; grey/white palette. Disable mouse at the
driver level via run(mouse=False). Update save test to drive via ctrl+s.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 23:28:59 +02:00

165 lines
5.2 KiB
Python

import json
from unittest.mock import patch
from textual.widgets import DataTable, Input, Switch, TabbedContent
from pyra.config.schema import ProviderConfig, PyraConfig
def _make_cfg():
return PyraConfig(ai=ProviderConfig(provider_id="ollama", model="test"))
# ── Pure helper functions (no Textual, no fixtures) ───────────────────────────
def test_get_nested_one_level():
from pyra.config.tui import _get_nested
class Obj:
value = 42
assert _get_nested(Obj(), "value") == 42
def test_get_nested_two_levels():
from pyra.config.tui import _get_nested
cfg = _make_cfg()
assert _get_nested(cfg, "general.user_name") == "User"
assert _get_nested(cfg, "daemon.enabled") is False
def test_set_nested_two_levels():
from pyra.config.tui import _set_nested
cfg = _make_cfg()
_set_nested(cfg, "general.user_name", "Alice")
assert cfg.general.user_name == "Alice"
def test_set_nested_bool():
from pyra.config.tui import _set_nested
cfg = _make_cfg()
_set_nested(cfg, "daemon.enabled", True)
assert cfg.daemon.enabled is True
def test_fid_replaces_dots():
from pyra.config.tui import _fid
assert _fid("general.user_name") == "f-general-user_name"
assert _fid("daemon.enabled") == "f-daemon-enabled"
def test_pfid_format():
from pyra.config.tui import _pfid
assert _pfid("myplugin", "api_url") == "pf-myplugin-api_url"
assert _pfid("my-plugin", "some_key") == "pf-my-plugin-some_key"
def test_general_fields_non_empty():
from pyra.config.tui import GENERAL_FIELDS
assert len(GENERAL_FIELDS) >= 3
def test_general_fields_all_valid_types():
from pyra.config.tui import GENERAL_FIELDS
valid_types = {"text", "bool", "select"}
for f in GENERAL_FIELDS:
assert f.type in valid_types, f"Field '{f.path}' has unexpected type '{f.type}'"
# ── Textual ConfigApp ─────────────────────────────────────────────────────────
async def test_config_app_renders_all_general_fields(tmp_pyra_home):
from pyra.config.manager import save_config
from pyra.config.tui import GENERAL_FIELDS, ConfigApp, _fid
save_config(_make_cfg())
async with ConfigApp().run_test() as pilot:
for f in GENERAL_FIELDS:
wid = _fid(f.path)
if f.type == "bool":
assert pilot.app.query_one(f"#{wid}", Switch)
else:
assert pilot.app.query_one(f"#{wid}", Input)
async def test_general_tab_save_persists_new_value(tmp_pyra_home):
from textual.app import App as TextualApp, ComposeResult as CR
from pyra.config.manager import save_config as initial_save
from pyra.config.tui import _GeneralTab, _fid
initial_save(_make_cfg())
# Test _GeneralTab in isolation — avoids TabbedContent click-routing complexity
class _TestApp(TextualApp):
def compose(self) -> CR:
yield _GeneralTab()
saved = []
with patch("pyra.config.tui.save_config", side_effect=lambda c: saved.append(c)):
async with _TestApp().run_test() as pilot:
widget = pilot.app.query_one(f"#{_fid('general.user_name')}", Input)
widget.value = "Alice"
await pilot.pause() # flush reactive update before key press
await pilot.press("ctrl+s")
assert saved, "save_config was not called"
assert saved[-1].general.user_name == "Alice"
async def test_plugins_tab_shows_installed_plugin(tmp_pyra_home):
from pyra.config.manager import save_config
from pyra.config.tui import ConfigApp
save_config(_make_cfg())
plugin_dir = tmp_pyra_home / "plugins" / "testplugin"
plugin_dir.mkdir(parents=True, exist_ok=True)
(plugin_dir / "manifest.json").write_text(
json.dumps({"name": "testplugin", "version": "1.0", "description": "Test plugin"})
)
async with ConfigApp().run_test() as pilot:
table = pilot.app.query_one("#plugins-table", DataTable)
assert table.row_count == 1
async def test_plugin_config_tab_appears_for_plugin_with_config_fields(tmp_pyra_home):
from pyra.config.manager import save_config
from pyra.config.tui import ConfigApp
from pyra.plugins.base import BasePlugin, ConfigField
save_config(_make_cfg())
class FakePlugin(BasePlugin):
name = "fake"
description = "A fake plugin"
version = "1.0"
def config_fields(self):
return [ConfigField("url", "URL", "text", "http://example.com")]
fake_entries = [("fake", {"name": "fake", "version": "1.0"}, FakePlugin())]
with patch("pyra.config.tui._installed_plugins", return_value=fake_entries):
async with ConfigApp().run_test() as pilot:
content = pilot.app.query_one(TabbedContent)
tab_count = len(list(content.query("TabPane")))
# General + Plugins + fake plugin tab
assert tab_count == 3
async def test_q_key_exits_app(tmp_pyra_home):
from pyra.config.manager import save_config
from pyra.config.tui import ConfigApp
save_config(_make_cfg())
async with ConfigApp().run_test() as pilot:
await pilot.press("q")
# Reaching here means the app exited cleanly