mirror of
https://github.com/ggozad/oterm.git
synced 2026-10-29 01:21:24 +01:00
- Drop throttle and debounce; LogViewer polls with set_interval. - OTerm actions share an _active_chat() lookup. - ChatEdit dismisses with a ChatModel instead of JSON, drops the supported-settings filter (every parameter it offers is in the base ModelSettings, so the filter never removed anything), and holds its state in plain attributes instead of unwatched reactives. - ToolMeta is a TypedDict; resolve_api_key and BUILTIN_OPENAI_COMPAT are public since agent.py imports them. - Ollama URL helpers derive from one another and share one client constructor. - Store maps rows to ChatModel in one place; build_user_prompt decodes images in one place. - Remove mutable default arguments, unused attributes and parameters, and four tcss rules that match nothing. - Rename supress_logging to suppress_logging. - Delete comments that restate the code; correct ones that were false (the logging comment in cli, the pydantic-ai 2.0 note in the 0.18.0 upgrade, a stale Textual version in cancel_streams).
77 lines
2.4 KiB
Python
77 lines
2.4 KiB
Python
from textual.app import App
|
|
from textual.widgets import Input
|
|
|
|
from oterm.app.chat_export import ChatExport, slugify
|
|
from oterm.types import ChatModel, MessageModel
|
|
|
|
|
|
class TestSlugify:
|
|
def test_lowercases_and_hyphenates(self):
|
|
assert slugify("Hello World!") == "hello-world"
|
|
|
|
def test_strips_diacritics(self):
|
|
assert slugify("café crème") == "cafe-creme"
|
|
|
|
def test_collapses_multiple_whitespace(self):
|
|
assert slugify("a b c") == "a-b-c"
|
|
|
|
|
|
class _Host(App):
|
|
pass
|
|
|
|
|
|
async def test_escape_cancels(tmp_path):
|
|
app = _Host()
|
|
async with app.run_test() as pilot:
|
|
received: list[str | None] = []
|
|
app.push_screen(
|
|
ChatExport(chat_id=1, file_name=str(tmp_path / "chat.md")),
|
|
lambda r: received.append(r),
|
|
)
|
|
await pilot.pause()
|
|
await pilot.press("escape")
|
|
await pilot.pause()
|
|
assert received == [None]
|
|
|
|
|
|
async def test_submit_writes_file_and_dismisses(tmp_path, store):
|
|
chat_id = await store.save_chat(ChatModel(name="c", model="m"))
|
|
for role, text in (("user", "hi"), ("assistant", "hello")):
|
|
await store.save_message(MessageModel(chat_id=chat_id, role=role, text=text))
|
|
|
|
out = tmp_path / "chat.md"
|
|
app = _Host()
|
|
async with app.run_test() as pilot:
|
|
received: list[str | None] = []
|
|
screen = ChatExport(chat_id=chat_id, file_name=str(out))
|
|
app.push_screen(screen, lambda r: received.append(r))
|
|
await pilot.pause()
|
|
|
|
inp = screen.query_one("#chat-name-input", Input)
|
|
inp.value = str(out)
|
|
await pilot.press("enter")
|
|
await pilot.pause()
|
|
|
|
content = out.read_text()
|
|
assert "*user*" in content
|
|
assert "hi" in content
|
|
assert "*assistant*" in content
|
|
assert "hello" in content
|
|
# Dismissed with no value: on_submit ends with dismiss().
|
|
assert received == [None]
|
|
|
|
|
|
async def test_empty_file_name_is_ignored(tmp_path, store):
|
|
await store.save_chat(ChatModel(name="c", model="m"))
|
|
app = _Host()
|
|
async with app.run_test() as pilot:
|
|
received: list[str | None] = []
|
|
screen = ChatExport(chat_id=1, file_name="")
|
|
app.push_screen(screen, lambda r: received.append(r))
|
|
await pilot.pause()
|
|
|
|
inp = screen.query_one("#chat-name-input", Input)
|
|
inp.value = ""
|
|
await pilot.press("enter")
|
|
await pilot.pause()
|
|
assert received == []
|