185 lines
6.3 KiB
Python
185 lines
6.3 KiB
Python
"""Tests for the Textual terminal UI."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from openharness.api.client import ApiMessageCompleteEvent
|
|
from openharness.api.usage import UsageSnapshot
|
|
from openharness.engine.messages import ConversationMessage, TextBlock, ToolUseBlock
|
|
from openharness.ui.textual_app import OpenHarnessTerminalApp
|
|
|
|
|
|
class StaticApiClient:
|
|
"""Fake streaming client for UI tests."""
|
|
|
|
def __init__(self, text: str) -> None:
|
|
self._text = text
|
|
|
|
async def stream_message(self, request):
|
|
del request
|
|
yield ApiMessageCompleteEvent(
|
|
message=ConversationMessage(role="assistant", content=[TextBlock(text=self._text)]),
|
|
usage=UsageSnapshot(input_tokens=2, output_tokens=3),
|
|
stop_reason=None,
|
|
)
|
|
|
|
|
|
class ScriptedApiClient:
|
|
"""Fake client that yields a scripted sequence of assistant turns."""
|
|
|
|
def __init__(self, messages) -> None:
|
|
self._messages = list(messages)
|
|
|
|
async def stream_message(self, request):
|
|
del request
|
|
message = self._messages.pop(0)
|
|
yield ApiMessageCompleteEvent(
|
|
message=message,
|
|
usage=UsageSnapshot(input_tokens=2, output_tokens=3),
|
|
stop_reason=None,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_textual_app_handles_commands(tmp_path, monkeypatch):
|
|
monkeypatch.chdir(tmp_path)
|
|
monkeypatch.setenv("OPENHARNESS_CONFIG_DIR", str(tmp_path / "config"))
|
|
monkeypatch.setenv("OPENHARNESS_DATA_DIR", str(tmp_path / "data"))
|
|
|
|
app = OpenHarnessTerminalApp(api_client=StaticApiClient("unused"))
|
|
async with app.run_test() as pilot:
|
|
composer = app.query_one("#composer")
|
|
composer.value = "/version"
|
|
await pilot.press("enter")
|
|
await pilot.pause()
|
|
|
|
assert any("OpenHarness" in line for line in app.transcript_lines)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_textual_app_runs_one_model_turn(tmp_path, monkeypatch):
|
|
monkeypatch.chdir(tmp_path)
|
|
monkeypatch.setenv("OPENHARNESS_CONFIG_DIR", str(tmp_path / "config"))
|
|
monkeypatch.setenv("OPENHARNESS_DATA_DIR", str(tmp_path / "data"))
|
|
|
|
app = OpenHarnessTerminalApp(api_client=StaticApiClient("hello from textual"))
|
|
async with app.run_test() as pilot:
|
|
composer = app.query_one("#composer")
|
|
composer.value = "hi"
|
|
await pilot.press("enter")
|
|
await pilot.pause()
|
|
|
|
assert any("user> hi" in line for line in app.transcript_lines)
|
|
assert any("assistant> hello from textual" in line for line in app.transcript_lines)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_textual_app_handles_ask_user_tool(tmp_path, monkeypatch):
|
|
monkeypatch.chdir(tmp_path)
|
|
monkeypatch.setenv("OPENHARNESS_CONFIG_DIR", str(tmp_path / "config"))
|
|
monkeypatch.setenv("OPENHARNESS_DATA_DIR", str(tmp_path / "data"))
|
|
|
|
app = OpenHarnessTerminalApp(
|
|
api_client=ScriptedApiClient(
|
|
[
|
|
ConversationMessage(
|
|
role="assistant",
|
|
content=[
|
|
ToolUseBlock(
|
|
id="toolu_ask",
|
|
name="ask_user_question",
|
|
input={"question": "Pick a color"},
|
|
)
|
|
],
|
|
),
|
|
ConversationMessage(
|
|
role="assistant",
|
|
content=[TextBlock(text="chosen green")],
|
|
),
|
|
]
|
|
)
|
|
)
|
|
|
|
async def _answer(question: str) -> str:
|
|
assert question == "Pick a color"
|
|
return "green"
|
|
|
|
app._ask_question = _answer
|
|
async with app.run_test() as pilot:
|
|
composer = app.query_one("#composer")
|
|
composer.value = "hi"
|
|
await pilot.press("enter")
|
|
await pilot.pause()
|
|
|
|
assert any("tool-result> ask_user_question: green" in line for line in app.transcript_lines)
|
|
assert any("assistant> chosen green" in line for line in app.transcript_lines)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_textual_sidebar_refresh_is_snapshot_based(tmp_path, monkeypatch):
|
|
monkeypatch.chdir(tmp_path)
|
|
monkeypatch.setenv("OPENHARNESS_CONFIG_DIR", str(tmp_path / "config"))
|
|
monkeypatch.setenv("OPENHARNESS_DATA_DIR", str(tmp_path / "data"))
|
|
|
|
app = OpenHarnessTerminalApp(api_client=StaticApiClient("hello"))
|
|
async with app.run_test():
|
|
status_bar = app.query_one("#status-bar")
|
|
tasks_panel = app.query_one("#tasks-panel")
|
|
|
|
status_updates: list[str] = []
|
|
task_updates: list[str] = []
|
|
|
|
original_status_update = status_bar.update
|
|
original_task_update = tasks_panel.update
|
|
|
|
def _tracked_status_update(renderable):
|
|
status_updates.append(str(renderable))
|
|
return original_status_update(renderable)
|
|
|
|
def _tracked_task_update(renderable):
|
|
task_updates.append(str(renderable))
|
|
return original_task_update(renderable)
|
|
|
|
monkeypatch.setattr(status_bar, "update", _tracked_status_update)
|
|
monkeypatch.setattr(tasks_panel, "update", _tracked_task_update)
|
|
|
|
app._refresh_sidebars()
|
|
app._refresh_sidebars()
|
|
assert status_updates == []
|
|
assert task_updates == []
|
|
|
|
app._bundle.app_state.set(model="gpt-5.4")
|
|
app._refresh_sidebars()
|
|
assert len(status_updates) == 1
|
|
assert task_updates == []
|
|
|
|
app._refresh_sidebars()
|
|
assert len(status_updates) == 1
|
|
assert task_updates == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_textual_current_response_update_is_deduplicated(tmp_path, monkeypatch):
|
|
monkeypatch.chdir(tmp_path)
|
|
monkeypatch.setenv("OPENHARNESS_CONFIG_DIR", str(tmp_path / "config"))
|
|
monkeypatch.setenv("OPENHARNESS_DATA_DIR", str(tmp_path / "data"))
|
|
|
|
app = OpenHarnessTerminalApp(api_client=StaticApiClient("hello"))
|
|
async with app.run_test():
|
|
current_response = app.query_one("#current-response")
|
|
updates: list[str] = []
|
|
original_update = current_response.update
|
|
|
|
def _tracked_update(renderable):
|
|
updates.append(str(renderable))
|
|
return original_update(renderable)
|
|
|
|
monkeypatch.setattr(current_response, "update", _tracked_update)
|
|
|
|
app._set_current_response("same message")
|
|
app._set_current_response("same message")
|
|
app._set_current_response("next message")
|
|
|
|
assert updates == ["same message", "next message"]
|