oterm/tests/app/test_oterm_app.py
Yiorgis Gozadinos 79854dfdef
Add a Go to chat search, and focus the prompt when switching chats
ctrl+f, or Go to chat in the command palette, opens a fuzzy search over
the chat tab names and switches to the chat picked.

Switching with ctrl+tab or ctrl+shift+tab set the active tab but left
nothing focused, so typing went nowhere until the prompt was clicked.
Both now focus the new chat's prompt, as Go to chat does.

Closes #52.
2026-10-03 14:14:05 +03:00

935 lines
31 KiB
Python

import pytest
from textual.widgets import TabbedContent, TabPane
from oterm.types import ChatModel, MessageModel
from tests._helpers import wait_until
@pytest.fixture(autouse=True)
def stub_network(monkeypatch):
"""Keep OTerm.on_mount from touching the network or spawning MCP servers."""
import oterm.app.oterm as oterm_mod
import oterm.tools.mcp.setup as mcp_setup_mod
import oterm.utils as utils_mod
async def _up_to_date():
from importlib import metadata
from packaging.version import parse
v = parse(metadata.version("oterm"))
return True, v, v
async def _no_mcp():
return {}
monkeypatch.setattr(utils_mod, "is_up_to_date", _up_to_date)
monkeypatch.setattr(oterm_mod, "is_up_to_date", _up_to_date)
monkeypatch.setattr(mcp_setup_mod, "setup_mcp_servers", _no_mcp)
async def _no_teardown():
return None
monkeypatch.setattr(oterm_mod, "teardown_mcp_servers", _no_teardown)
@pytest.fixture
def fresh_app(tmp_data_dir, app_config, stub_network, monkeypatch):
"""Build a fresh OTerm app wired to an isolated data dir, splash off.
Patches ``push_screen_wait`` to return ``None`` so the empty-store path
that auto-fires the new-chat modal during ``on_mount`` doesn't leave a
modal pinned on the stack and swallowing app-level key bindings.
"""
app_config.set("splash-screen", False)
from oterm.app.oterm import OTerm
async def _no_modal(self, screen):
return None
monkeypatch.setattr(OTerm, "push_screen_wait", _no_modal)
return OTerm()
class TestStartup:
async def test_empty_store_shows_welcome(self, fresh_app):
"""On empty store, on_mount shows the EmptyState welcome and hides tabs."""
from oterm.app.widgets.empty_state import EmptyState
app = fresh_app
async with app.run_test() as pilot:
await pilot.pause()
assert app.query_one(TabbedContent).tab_count == 0
assert app.query_one(EmptyState).display is True
assert app.query_one(TabbedContent).display is False
async def test_existing_chats_loaded_into_tabs(
self, tmp_data_dir, app_config, stub_network, store
):
app_config.set("splash-screen", False)
chat_a = ChatModel(name="alpha", model="m")
chat_a.id = await store.save_chat(chat_a)
chat_b = ChatModel(name="beta", model="m")
chat_b.id = await store.save_chat(chat_b)
from oterm.app.oterm import OTerm
app = OTerm()
async with app.run_test() as pilot:
await pilot.pause()
tabs = app.query_one(TabbedContent)
assert tabs.tab_count == 2
from oterm.app.widgets.empty_state import EmptyState
assert app.query_one(EmptyState).display is False
assert tabs.display is True
class TestCycleChat:
async def test_cycle_forward_and_back(
self, tmp_data_dir, app_config, stub_network, store
):
app_config.set("splash-screen", False)
ids = []
for n in ("a", "b", "c"):
cm = ChatModel(name=n, model="m")
cm.id = await store.save_chat(cm)
ids.append(cm.id)
from oterm.app.oterm import OTerm
app = OTerm()
async with app.run_test() as pilot:
await pilot.pause()
tabs = app.query_one(TabbedContent)
tabs.active = f"chat-{ids[0]}"
await pilot.pause()
app.action_cycle_chat(1)
await pilot.pause()
assert tabs.active == f"chat-{ids[1]}"
app.action_cycle_chat(-1)
await pilot.pause()
assert tabs.active == f"chat-{ids[0]}"
async def test_cycle_key_focuses_the_prompt_of_the_new_chat(
self, tmp_data_dir, app_config, stub_network, store
):
from oterm.app.widgets.prompt import PostableTextArea
app_config.set("splash-screen", False)
for n in ("a", "b"):
cm = ChatModel(name=n, model="m")
cm.id = await store.save_chat(cm)
from oterm.app.oterm import OTerm
app = OTerm()
async with app.run_test() as pilot:
await pilot.pause()
tabs = app.query_one(TabbedContent)
before = tabs.active
await pilot.press("ctrl+tab")
await pilot.pause()
assert tabs.active != before
assert isinstance(app.focused, PostableTextArea)
assert tabs.active_pane in app.focused.ancestors
async def test_cycle_with_no_active_pane_is_noop(self, fresh_app, monkeypatch):
app = fresh_app
async with app.run_test() as pilot:
await pilot.pause()
tabs = app.query_one(TabbedContent)
# Remove all panes to simulate no active pane.
for pane in list(tabs.query(TabPane)):
await tabs.remove_pane(pane.id or "")
await pilot.pause()
assert tabs.active_pane is None
# Should return silently with no exception
app.action_cycle_chat(1)
class TestDeleteChat:
async def test_delete_removes_pane_and_db_row(
self, tmp_data_dir, app_config, stub_network, store
):
app_config.set("splash-screen", False)
cm = ChatModel(name="doomed", model="m")
cm.id = await store.save_chat(cm)
from oterm.app.oterm import OTerm
from oterm.app.widgets.empty_state import EmptyState
app = OTerm()
async with app.run_test() as pilot:
await pilot.pause()
tabs = app.query_one(TabbedContent)
assert tabs.tab_count == 1
await app.action_delete_chat()
await pilot.pause()
assert tabs.tab_count == 0
assert await store.get_chat(cm.id) is None
assert app.query_one(EmptyState).display is True
assert tabs.display is False
async def test_delete_without_active_pane_is_noop(self, fresh_app):
app = fresh_app
async with app.run_test() as pilot:
await pilot.pause()
tabs = app.query_one(TabbedContent)
for pane in list(tabs.query(TabPane)):
await tabs.remove_pane(pane.id or "")
await pilot.pause()
await app.action_delete_chat() # silently no-op
class TestClearChat:
async def test_clear_on_active_chat_wipes_messages(
self, tmp_data_dir, app_config, stub_network, store
):
app_config.set("splash-screen", False)
cm = ChatModel(name="c", model="m")
cm.id = await store.save_chat(cm)
await store.save_message(MessageModel(chat_id=cm.id, role="user", text="hi"))
from oterm.app.oterm import OTerm
app = OTerm()
async with app.run_test() as pilot:
await pilot.pause()
await app.action_clear_chat()
await pilot.pause()
assert await store.get_messages(cm.id) == []
class TestExportChat:
async def test_export_pushes_screen_when_active_pane_has_chat(
self, tmp_data_dir, app_config, stub_network, store
):
from oterm.app.chat_export import ChatExport
app_config.set("splash-screen", False)
cm = ChatModel(name="C One", model="m")
cm.id = await store.save_chat(cm)
from oterm.app.oterm import OTerm
app = OTerm()
async with app.run_test() as pilot:
await pilot.pause()
await app.action_export_chat()
await pilot.pause()
assert isinstance(app.screen, ChatExport)
class TestShowLogs:
async def test_show_logs_pushes_screen(self, fresh_app):
from oterm.app.log_viewer import LogViewer
app = fresh_app
async with app.run_test() as pilot:
await pilot.pause()
await app.action_show_logs()
await pilot.pause()
assert isinstance(app.screen, LogViewer)
class TestThemePersistence:
async def test_theme_change_persists_to_app_config(self, fresh_app, app_config):
app = fresh_app
async with app.run_test() as pilot:
await pilot.pause()
app.theme = "textual-light"
await pilot.pause()
assert app_config.get("theme") == "textual-light"
class TestLoadTools:
async def test_discovers_entry_point_tools(
self, tmp_data_dir, app_config, stub_network
):
import oterm.tools as tools_mod
import oterm.tools.mcp.setup as mcp_setup_mod
def fake_tool() -> str:
"""Sample."""
return "x"
from oterm.tools import make_tool_def
called: list[tuple] = []
def fake_discover():
return [make_tool_def(fake_tool)]
async def fake_mcp_setup():
called.append(("mcp",))
return {}
original_discover = tools_mod.discover_tools
original_builtins = tools_mod.builtin_tools
tools_mod.discover_tools = fake_discover # ty: ignore[invalid-assignment]
mcp_setup_mod.setup_mcp_servers = fake_mcp_setup # ty: ignore[invalid-assignment]
try:
from oterm.app.oterm import OTerm
app = OTerm()
async with app.run_test() as pilot:
await pilot.pause()
names = {t["name"] for t in tools_mod.builtin_tools}
assert "fake_tool" in names
assert called
finally:
tools_mod.discover_tools = original_discover
tools_mod.builtin_tools = original_builtins
class TestNewChat:
async def test_new_chat_from_modal_creates_tab(
self, tmp_data_dir, app_config, stub_network, store, monkeypatch
):
"""action_new_chat awaits a ChatEdit modal; feed back a valid JSON payload."""
app_config.set("splash-screen", False)
from oterm.app.oterm import OTerm
async def fake_push_screen_wait(self, screen):
return ChatModel(model="llama3", provider="ollama")
monkeypatch.setattr(OTerm, "push_screen_wait", fake_push_screen_wait)
app = OTerm()
async with app.run_test() as pilot:
await pilot.pause()
initial = app.query_one(TabbedContent).tab_count
app.action_new_chat()
# The @work decorator runs the action in a worker; give it time.
for _ in range(30):
await pilot.pause()
if app.query_one(TabbedContent).tab_count > initial:
break
assert app.query_one(TabbedContent).tab_count == initial + 1
chats = await store.get_chats()
assert any(c.model == "llama3" for c in chats)
from oterm.app.widgets.empty_state import EmptyState
assert app.query_one(EmptyState).display is False
assert app.query_one(TabbedContent).display is True
@pytest.mark.parametrize(
"chats,available,expected",
[
(["ollama", "anthropic"], ["ollama", "anthropic"], "anthropic"),
(["anthropic"], ["groq", "ollama"], "ollama"),
],
ids=["most_recent_chat", "unavailable_falls_back"],
)
async def test_new_chat_provider_comes_from_the_last_chat(
self,
tmp_data_dir,
app_config,
stub_network,
store,
monkeypatch,
chats,
available,
expected,
):
"""A new chat starts on the most recently created chat's provider, unless
that provider is no longer available."""
import oterm.app.oterm as oterm_mod
from oterm.app.oterm import OTerm
app_config.set("splash-screen", False)
for provider in chats:
await store.save_chat(
ChatModel(name=provider, model="m", provider=provider)
)
monkeypatch.setattr(oterm_mod, "get_available_providers", lambda: available)
seen: list[str] = []
async def capture(self, screen):
seen.append(screen.provider)
return None
monkeypatch.setattr(OTerm, "push_screen_wait", capture)
app = OTerm()
async with app.run_test() as pilot:
await pilot.pause()
app.action_new_chat()
await wait_until(pilot, lambda: bool(seen))
assert seen == [expected]
async def test_new_chat_cancelled_modal_noop(self, fresh_app, store, monkeypatch):
from oterm.app.oterm import OTerm
async def fake_push_screen_wait(self, screen):
return None
monkeypatch.setattr(OTerm, "push_screen_wait", fake_push_screen_wait)
app = fresh_app
async with app.run_test() as pilot:
await pilot.pause()
initial = app.query_one(TabbedContent).tab_count
app.action_new_chat()
for _ in range(10):
await pilot.pause()
assert app.query_one(TabbedContent).tab_count == initial
class TestQuit:
async def test_ctrl_q_exits(self, fresh_app):
app = fresh_app
async with app.run_test() as pilot:
await pilot.pause()
assert app.is_running
await pilot.press("ctrl+q")
# run_test teardown will exit; we just verify the binding works.
# After action_quit, app.exit() has been called.
for _ in range(10):
await pilot.pause()
if not app.is_running:
break
assert not app.is_running
class TestActionDelegates:
"""OTerm.action_* methods that just forward to ChatContainer.action_*."""
async def test_edit_chat_delegates_to_active_container(
self, tmp_data_dir, app_config, stub_network, store, monkeypatch
):
from oterm.app.oterm import OTerm
from oterm.app.widgets.chat import ChatContainer
app_config.set("splash-screen", False)
cm = ChatModel(name="c", model="m")
cm.id = await store.save_chat(cm)
app = OTerm()
async with app.run_test() as pilot:
await pilot.pause()
called: list[bool] = []
def spy(self):
called.append(True)
monkeypatch.setattr(ChatContainer, "action_edit_chat", spy)
await app.action_edit_chat()
await pilot.pause()
assert called == [True]
async def test_rename_chat_delegates(
self, tmp_data_dir, app_config, stub_network, store, monkeypatch
):
from oterm.app.oterm import OTerm
from oterm.app.widgets.chat import ChatContainer
app_config.set("splash-screen", False)
cm = ChatModel(name="c", model="m")
cm.id = await store.save_chat(cm)
app = OTerm()
async with app.run_test() as pilot:
await pilot.pause()
called: list[bool] = []
def spy(self):
called.append(True)
monkeypatch.setattr(ChatContainer, "action_rename_chat", spy)
await app.action_rename_chat()
await pilot.pause()
assert called == [True]
async def test_toggle_thinking_delegates(
self, tmp_data_dir, app_config, stub_network, store, monkeypatch
):
from oterm.app.oterm import OTerm
from oterm.app.widgets.chat import ChatContainer
app_config.set("splash-screen", False)
cm = ChatModel(name="c", model="m")
cm.id = await store.save_chat(cm)
app = OTerm()
async with app.run_test() as pilot:
await pilot.pause()
called: list[bool] = []
def spy(self):
called.append(True)
monkeypatch.setattr(ChatContainer, "action_toggle_thinking", spy)
await app.action_toggle_thinking()
await pilot.pause()
assert called == [True]
async def test_copy_message_delegates(
self, tmp_data_dir, app_config, stub_network, store, monkeypatch
):
from oterm.app.oterm import OTerm
from oterm.app.widgets.chat import ChatContainer
app_config.set("splash-screen", False)
cm = ChatModel(name="c", model="m")
cm.id = await store.save_chat(cm)
app = OTerm()
async with app.run_test() as pilot:
await pilot.pause()
called: list[bool] = []
def spy(self):
called.append(True)
monkeypatch.setattr(ChatContainer, "action_copy_message", spy)
await app.action_copy_message()
await pilot.pause()
assert called == [True]
async def test_regenerate_last_message_delegates(
self, tmp_data_dir, app_config, stub_network, store, monkeypatch
):
from oterm.app.oterm import OTerm
from oterm.app.widgets.chat import ChatContainer
app_config.set("splash-screen", False)
cm = ChatModel(name="c", model="m")
cm.id = await store.save_chat(cm)
app = OTerm()
async with app.run_test() as pilot:
await pilot.pause()
called: list[bool] = []
async def spy(self):
called.append(True)
monkeypatch.setattr(ChatContainer, "action_regenerate_llm_message", spy)
await app.action_regenerate_last_message()
await pilot.pause()
assert called == [True]
async def test_action_delegates_are_noop_without_active_pane(self, fresh_app):
from textual.widgets import TabbedContent
app = fresh_app
async with app.run_test() as pilot:
await pilot.pause()
tabs = app.query_one(TabbedContent)
for pane in list(tabs.query("TabPane")):
await tabs.remove_pane(pane.id or "")
await pilot.pause()
# None of these should raise.
await app.action_edit_chat()
await app.action_toggle_thinking()
await app.action_rename_chat()
await app.action_export_chat()
await app.action_regenerate_last_message()
await app.action_clear_chat()
await app.action_prompt_history()
await app.action_copy_message()
async def test_prompt_history_delegates_to_active_container(
self, tmp_data_dir, app_config, stub_network, store, monkeypatch
):
from oterm.app.oterm import OTerm
from oterm.app.widgets.chat import ChatContainer
app_config.set("splash-screen", False)
cm = ChatModel(name="c", model="m")
cm.id = await store.save_chat(cm)
app = OTerm()
async with app.run_test() as pilot:
await pilot.pause()
called: list[bool] = []
async def spy(self):
called.append(True)
monkeypatch.setattr(ChatContainer, "action_history", spy)
await app.action_prompt_history()
await pilot.pause()
assert called == [True]
class TestNoneIdGuards:
"""Chats lacking a database id (e.g. from a failed save) are skipped."""
async def test_delete_with_id_none_is_noop(
self, tmp_data_dir, app_config, stub_network, store
):
from textual.widgets import TabbedContent, TabPane
from oterm.app.oterm import OTerm
from oterm.app.widgets.chat import ChatContainer
app_config.set("splash-screen", False)
app = OTerm()
async with app.run_test() as pilot:
await pilot.pause()
tabs = app.query_one(TabbedContent)
# Replace the auto-created chat with one whose chat_model.id is None.
for pane in list(tabs.query("TabPane")):
await tabs.remove_pane(pane.id or "")
await pilot.pause()
cm = ChatModel(model="m", provider="ollama")
assert cm.id is None
pane = TabPane("orphan", id="chat-orphan")
pane.compose_add_child(ChatContainer(chat_model=cm, messages=[]))
await tabs.add_pane(pane)
tabs.active = "chat-orphan"
await pilot.pause()
await app.action_delete_chat()
await pilot.pause()
# Pane is preserved because the guard skipped the delete.
assert tabs.tab_count == 1
async def test_export_with_id_none_is_noop(
self, tmp_data_dir, app_config, stub_network, store
):
from textual.widgets import TabbedContent, TabPane
from oterm.app.oterm import OTerm
from oterm.app.widgets.chat import ChatContainer
app_config.set("splash-screen", False)
app = OTerm()
async with app.run_test() as pilot:
await pilot.pause()
tabs = app.query_one(TabbedContent)
for pane in list(tabs.query("TabPane")):
await tabs.remove_pane(pane.id or "")
await pilot.pause()
cm = ChatModel(model="m", provider="ollama")
pane = TabPane("orphan", id="chat-orphan")
pane.compose_add_child(ChatContainer(chat_model=cm, messages=[]))
await tabs.add_pane(pane)
tabs.active = "chat-orphan"
await pilot.pause()
original_screen = app.screen
await app.action_export_chat()
await pilot.pause()
# Without a chat id, no ChatExport screen is pushed.
assert app.screen is original_screen
async def test_saved_chat_with_id_none_is_skipped_on_load(
self, tmp_data_dir, app_config, stub_network, monkeypatch
):
from textual.widgets import TabbedContent
from oterm.store.store import Store
app_config.set("splash-screen", False)
async def fake_get_chats(self):
return [ChatModel(id=None, name="orphan", model="m")]
monkeypatch.setattr(Store, "get_chats", fake_get_chats)
from oterm.app.oterm import OTerm
app = OTerm()
async with app.run_test() as pilot:
await pilot.pause()
# The orphan chat is skipped; no panes added during the load loop.
tabs = app.query_one(TabbedContent)
assert tabs.tab_count == 0
class TestThemeEdgeCases:
async def test_theme_change_to_same_value_does_not_rewrite_config(
self, fresh_app, app_config, monkeypatch
):
app = fresh_app
async with app.run_test() as pilot:
await pilot.pause()
current_theme = app.theme
app_config.set("theme", current_theme)
writes: list[tuple] = []
original_set = app_config.set
def track_set(key, value):
writes.append((key, value))
original_set(key, value)
monkeypatch.setattr(app_config, "set", track_set)
# on_theme_change is the watcher; calling it directly with the same
# value must short-circuit (no config write).
app.on_theme_change(current_theme, current_theme)
await pilot.pause()
assert writes == []
class TestPerformChecks:
async def test_outdated_version_notifies(
self, tmp_data_dir, app_config, stub_network, monkeypatch
):
import oterm.app.oterm as oterm_mod
async def _outdated():
from packaging.version import parse
return False, parse("0.0.0"), parse("99.0.0")
monkeypatch.setattr(oterm_mod, "is_up_to_date", _outdated)
app_config.set("splash-screen", False)
from oterm.app.oterm import OTerm
app = OTerm()
async with app.run_test() as pilot:
await pilot.pause()
# perform_checks fires as part of on_mount after splash_done
for _ in range(20):
await pilot.pause()
if any("available" in n.message for n in list(app._notifications)):
break
assert any("available" in n.message for n in list(app._notifications))
class TestTheme:
async def test_dark_theme_from_config(self, tmp_data_dir, app_config, stub_network):
app_config.set("splash-screen", False)
app_config.set("theme", "dark")
from oterm.app.oterm import OTerm
app = OTerm()
async with app.run_test() as pilot:
await pilot.pause()
assert app.theme == "textual-dark"
async def test_light_theme_from_config(
self, tmp_data_dir, app_config, stub_network
):
app_config.set("splash-screen", False)
app_config.set("theme", "light")
from oterm.app.oterm import OTerm
app = OTerm()
async with app.run_test() as pilot:
await pilot.pause()
assert app.theme == "textual-light"
async def test_custom_named_theme_from_config(
self, tmp_data_dir, app_config, stub_network
):
app_config.set("splash-screen", False)
app_config.set("theme", "solarized-dark")
from oterm.app.oterm import OTerm
app = OTerm()
async with app.run_test() as pilot:
await pilot.pause()
assert app.theme == "solarized-dark"
class TestKeymap:
async def test_keymap_remaps_new_chat_binding(
self, tmp_data_dir, app_config, stub_network, monkeypatch
):
import oterm.app.oterm as oterm_mod
calls: list[bool] = []
monkeypatch.setattr(
oterm_mod.OTerm,
"action_new_chat",
lambda self: calls.append(True),
)
app_config.set("splash-screen", False)
app_config.set("keymap", {"new.chat": "f5"})
app = oterm_mod.OTerm()
async with app.run_test() as pilot:
await pilot.pause()
calls.clear() # ignore the on_mount auto-new-chat call
await pilot.press("f5")
await pilot.pause()
assert calls == [True]
async def test_copy_message_key_reaches_the_action_from_the_prompt(
self, tmp_data_dir, app_config, stub_network, store, monkeypatch
):
"""The prompt is focused on mount, so the default must not be a key
PostableTextArea already claims (it inherits all of TextArea.BINDINGS)."""
import oterm.app.oterm as oterm_mod
from oterm.app.widgets.prompt import PostableTextArea
app_config.set("splash-screen", False)
cm = ChatModel(name="c", model="m")
cm.id = await store.save_chat(cm)
calls: list[bool] = []
async def spy(self):
calls.append(True)
monkeypatch.setattr(oterm_mod.OTerm, "action_copy_message", spy)
app = oterm_mod.OTerm()
async with app.run_test() as pilot:
await pilot.pause()
tabs = app.query_one(TabbedContent)
assert tabs.active_pane is not None
prompt = tabs.active_pane.query_one(PostableTextArea)
prompt.focus()
await pilot.pause()
assert app.focused is prompt
key = next(
b.key for b in oterm_mod.OTerm.BINDINGS if b.id == "copy.message"
)
await pilot.press(key)
await pilot.pause()
assert calls == [True]
async def test_keymap_remaps_toggle_thinking_binding(
self, tmp_data_dir, app_config, stub_network, monkeypatch
):
import oterm.app.oterm as oterm_mod
calls: list[bool] = []
async def spy(self):
calls.append(True)
monkeypatch.setattr(oterm_mod.OTerm, "action_toggle_thinking", spy)
app_config.set("splash-screen", False)
app_config.set("keymap", {"toggle.thinking": "f6"})
app = oterm_mod.OTerm()
async with app.run_test() as pilot:
await pilot.pause()
await pilot.press("f6")
await pilot.pause()
assert calls == [True]
class TestSplashCallback:
async def test_splash_dismissal_triggers_post_splash_flow(
self, tmp_data_dir, app_config, stub_network, monkeypatch
):
"""With splash on, on_mount defers the post-splash flow until splash dismisses."""
from textual.screen import Screen
import oterm.app.oterm as oterm_mod
class _StubSplash(Screen):
async def on_mount(self):
self.dismiss("")
monkeypatch.setattr(oterm_mod, "splash", _StubSplash())
checks_calls: list[bool] = []
monkeypatch.setattr(
oterm_mod.OTerm,
"perform_checks",
lambda self: checks_calls.append(True),
)
app_config.set("splash-screen", True)
app = oterm_mod.OTerm()
async with app.run_test() as pilot:
for _ in range(30):
await pilot.pause()
if checks_calls:
break
assert checks_calls == [True]
class TestSystemCommands:
async def test_system_commands_includes_all_actions(self, fresh_app):
app = fresh_app
async with app.run_test() as pilot:
await pilot.pause()
cmds = [c.title for c in app.get_system_commands(app.screen)]
for title in (
"New chat",
"Edit chat parameters",
"Toggle thinking",
"Copy message",
"Rename chat",
"Clear chat",
"Delete chat",
"Export chat",
"Regenerate last message",
"Prompt history",
"Show logs",
"Go to chat",
):
assert title in cmds
class TestGoToChat:
async def test_go_to_chat_key_opens_the_chat_picked_from_the_prompt(
self, tmp_data_dir, app_config, stub_network, store
):
from textual.command import CommandList, CommandPalette
import oterm.app.oterm as oterm_mod
from oterm.app.widgets.prompt import PostableTextArea
app_config.set("splash-screen", False)
ids = []
for name in ("alpha", "beta", "gamma"):
cm = ChatModel(name=name, model="m")
cm.id = await store.save_chat(cm)
ids.append(cm.id)
app = oterm_mod.OTerm()
async with app.run_test() as pilot:
await pilot.pause()
tabs = app.query_one(TabbedContent)
tabs.active = f"chat-{ids[0]}"
await pilot.pause()
assert tabs.active_pane is not None
tabs.active_pane.query_one(PostableTextArea).focus()
await pilot.pause()
key = next(b.key for b in oterm_mod.OTerm.BINDINGS if b.id == "go.to.chat")
await pilot.press(key)
await pilot.pause()
await pilot.press(*"beta")
await wait_until(
pilot,
lambda: (
isinstance(app.screen, CommandPalette)
and app.screen.query_one(CommandList).option_count == 1
),
)
await pilot.press("enter")
await wait_until(pilot, lambda: tabs.active == f"chat-{ids[1]}")
assert isinstance(app.focused, PostableTextArea)
assert tabs.active_pane in app.focused.ancestors