Skip to content

Commit 82bc9c3

Browse files
authored
Merge pull request #176 from reacher-z/test/tui-tests
test: tui tests with mock questionary
2 parents c8ff4d8 + 128b4b4 commit 82bc9c3

1 file changed

Lines changed: 338 additions & 0 deletions

File tree

tests/test_tui_helpers.py

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
"""Executable host-side tests for TUI helper behavior."""
2+
3+
from __future__ import annotations
4+
5+
import subprocess
6+
import sys
7+
from pathlib import Path
8+
9+
import pytest
10+
11+
from clawbench import tui
12+
13+
14+
class _TTY:
15+
def __init__(self, is_tty: bool) -> None:
16+
self._is_tty = is_tty
17+
self.output = ""
18+
19+
def isatty(self) -> bool:
20+
return self._is_tty
21+
22+
def write(self, text: str) -> int:
23+
self.output += text
24+
return len(text)
25+
26+
def flush(self) -> None:
27+
return None
28+
29+
30+
class _Prompt:
31+
def __init__(self, value: object) -> None:
32+
self._value = value
33+
34+
def ask(self) -> object:
35+
return self._value
36+
37+
38+
class _QuestionaryDriver:
39+
def __init__(
40+
self,
41+
*,
42+
select: list[object] | None = None,
43+
checkbox: list[object] | None = None,
44+
text: list[object] | None = None,
45+
confirm: list[object] | None = None,
46+
) -> None:
47+
self._answers = {
48+
"select": list(select or []),
49+
"checkbox": list(checkbox or []),
50+
"text": list(text or []),
51+
"confirm": list(confirm or []),
52+
}
53+
54+
def select(self, *_args: object, **_kwargs: object) -> _Prompt:
55+
return _Prompt(self._pop("select"))
56+
57+
def checkbox(self, *_args: object, **_kwargs: object) -> _Prompt:
58+
return _Prompt(self._pop("checkbox"))
59+
60+
def text(self, *_args: object, **_kwargs: object) -> _Prompt:
61+
return _Prompt(self._pop("text"))
62+
63+
def confirm(self, *_args: object, **_kwargs: object) -> _Prompt:
64+
return _Prompt(self._pop("confirm"))
65+
66+
def assert_done(self) -> None:
67+
leftovers = {k: v for k, v in self._answers.items() if v}
68+
assert leftovers == {}
69+
70+
def _pop(self, kind: str) -> object:
71+
assert self._answers[kind], f"no queued {kind} answer"
72+
return self._answers[kind].pop(0)
73+
74+
75+
def _install_tui_main_fakes(
76+
monkeypatch: pytest.MonkeyPatch,
77+
driver: _QuestionaryDriver,
78+
) -> list[list[str]]:
79+
commands: list[list[str]] = []
80+
cases_by_dir = {
81+
"test-cases/v1": [
82+
"001-daily-life-food-uber-eats",
83+
"002-daily-life-shopping-amazon",
84+
],
85+
"test-cases/v2": [
86+
"v2-0001-daily-life-food-uber-eats",
87+
"v2-0002-daily-life-shopping-amazon",
88+
],
89+
"test-cases/v1-lite": ["001-daily-life-food-uber-eats"],
90+
"test-cases/claw-eval": ["ce-T050-regulatory-research"],
91+
}
92+
93+
monkeypatch.setattr(tui.sys, "argv", ["clawbench"])
94+
monkeypatch.setattr(tui, "_require_tty", lambda: None)
95+
monkeypatch.setattr(tui, "ensure_workspace_templates", lambda: None)
96+
monkeypatch.setattr(tui.os, "chdir", lambda _path: None)
97+
monkeypatch.setattr(tui, "_load_saved_theme", lambda: "dark")
98+
monkeypatch.setattr(tui, "load_models", lambda: ["model-a"])
99+
monkeypatch.setattr(tui, "_check_engine", lambda: ("docker", "ready", ""))
100+
monkeypatch.setattr(
101+
tui,
102+
"_fix_engine",
103+
lambda *_args: pytest.fail("engine fix should not run in TUI flow tests"),
104+
)
105+
monkeypatch.setattr(tui, "load_cases", lambda cases_dir: cases_by_dir[cases_dir])
106+
monkeypatch.setattr(tui, "_recommend_concurrent", lambda: 2)
107+
monkeypatch.setattr(tui.questionary, "select", driver.select)
108+
monkeypatch.setattr(tui.questionary, "checkbox", driver.checkbox)
109+
monkeypatch.setattr(tui.questionary, "text", driver.text)
110+
monkeypatch.setattr(tui.questionary, "confirm", driver.confirm)
111+
monkeypatch.setattr(
112+
tui,
113+
"run_cmd",
114+
lambda cmd, **_kwargs: commands.append(cmd),
115+
)
116+
return commands
117+
118+
119+
def test_tui_dataset_and_case_helpers_use_known_suites() -> None:
120+
assert tui._dataset_cases_dir_name("v2") == "test-cases/v2"
121+
assert "V2" in tui._dataset_summary("v2")
122+
assert tui._case_numeric_id("v2-1065b-daily-life-home-services-handy") == 1065
123+
assert (
124+
tui._case_display("v2-1065b-daily-life-home-services-handy")
125+
== "1065b v2-1065b-daily-life-home-services-handy"
126+
)
127+
128+
129+
def test_tui_load_cases_reads_flat_and_directory_suites() -> None:
130+
v1_cases = tui.load_cases("test-cases/v1")
131+
claw_eval_cases = tui.load_cases("test-cases/claw-eval")
132+
133+
assert "001-daily-life-food-uber-eats" in v1_cases
134+
assert "ce-T050-regulatory-research" in claw_eval_cases
135+
136+
137+
def test_tui_theme_save_and_load_round_trips(
138+
monkeypatch: pytest.MonkeyPatch,
139+
tmp_path: Path,
140+
) -> None:
141+
config_dir = tmp_path / "clawbench-config"
142+
config_file = config_dir / "tui.json"
143+
monkeypatch.setattr(tui, "CONFIG_DIR", config_dir)
144+
monkeypatch.setattr(tui, "CONFIG_FILE", config_file)
145+
146+
assert tui._load_saved_theme() is None
147+
tui._save_theme("light")
148+
assert tui._load_saved_theme() == "light"
149+
config_file.write_text('{"theme": "invalid"}', encoding="utf-8")
150+
assert tui._load_saved_theme() is None
151+
152+
153+
def test_tui_require_tty_exits_for_non_interactive_streams(
154+
monkeypatch: pytest.MonkeyPatch,
155+
) -> None:
156+
monkeypatch.setattr(tui.sys, "stdin", _TTY(False))
157+
monkeypatch.setattr(tui.sys, "stdout", _TTY(False))
158+
159+
with pytest.raises(SystemExit) as exc:
160+
tui._require_tty()
161+
162+
assert exc.value.code == 1
163+
164+
165+
def test_tui_engine_detection_prefers_env_then_path(
166+
monkeypatch: pytest.MonkeyPatch,
167+
) -> None:
168+
monkeypatch.setenv("CONTAINER_ENGINE", "podman")
169+
monkeypatch.setattr(
170+
tui.shutil, "which", lambda cmd: cmd if cmd == "podman" else None
171+
)
172+
assert tui._engine_from_env_or_path() == "podman"
173+
174+
monkeypatch.setenv("CONTAINER_ENGINE", "docker")
175+
assert tui._engine_from_env_or_path() == "podman"
176+
177+
monkeypatch.delenv("CONTAINER_ENGINE", raising=False)
178+
monkeypatch.setattr(tui.shutil, "which", lambda cmd: cmd)
179+
assert tui._engine_from_env_or_path() == "docker"
180+
181+
monkeypatch.setattr(tui.shutil, "which", lambda _cmd: None)
182+
assert tui._engine_from_env_or_path() is None
183+
184+
185+
def test_tui_check_engine_classifies_docker_ready_and_not_running(
186+
monkeypatch: pytest.MonkeyPatch,
187+
) -> None:
188+
monkeypatch.setattr(tui, "_engine_from_env_or_path", lambda: "docker")
189+
190+
def ready_run(cmd: list[str], **_kwargs: object) -> subprocess.CompletedProcess:
191+
assert cmd == ["docker", "info"]
192+
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
193+
194+
monkeypatch.setattr(tui.subprocess, "run", ready_run)
195+
assert tui._check_engine() == ("docker", "ready", "")
196+
197+
def failed_run(cmd: list[str], **_kwargs: object) -> subprocess.CompletedProcess:
198+
assert cmd == ["docker", "info"]
199+
return subprocess.CompletedProcess(cmd, 1, stdout="", stderr="daemon down")
200+
201+
monkeypatch.setattr(tui.subprocess, "run", failed_run)
202+
assert tui._check_engine() == ("docker", "docker_not_running", "daemon down")
203+
204+
205+
def test_tui_check_engine_classifies_podman_machine_states(
206+
monkeypatch: pytest.MonkeyPatch,
207+
) -> None:
208+
monkeypatch.setattr(tui, "_engine_from_env_or_path", lambda: "podman")
209+
monkeypatch.setattr(tui.platform, "system", lambda: "Windows")
210+
calls: list[list[str]] = []
211+
212+
def no_machine_run(
213+
cmd: list[str], **_kwargs: object
214+
) -> subprocess.CompletedProcess:
215+
calls.append(cmd)
216+
return subprocess.CompletedProcess(cmd, 0, stdout="[]", stderr="")
217+
218+
monkeypatch.setattr(tui.subprocess, "run", no_machine_run)
219+
assert tui._check_engine() == ("podman", "podman_no_machine", "")
220+
assert calls == [["podman", "machine", "list", "--format", "json"]]
221+
222+
def low_memory_run(
223+
cmd: list[str], **_kwargs: object
224+
) -> subprocess.CompletedProcess:
225+
if cmd == ["podman", "machine", "list", "--format", "json"]:
226+
return subprocess.CompletedProcess(
227+
cmd, 0, stdout='[{"Running": true}]', stderr=""
228+
)
229+
if cmd == ["podman", "ps"]:
230+
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
231+
return subprocess.CompletedProcess(cmd, 0, stdout="2048", stderr="")
232+
233+
monkeypatch.setattr(tui.subprocess, "run", low_memory_run)
234+
assert tui._check_engine() == ("podman", "podman_low_memory", "2048")
235+
236+
237+
def test_tui_recommend_concurrent_returns_positive_value(
238+
monkeypatch: pytest.MonkeyPatch,
239+
) -> None:
240+
monkeypatch.setattr(tui.multiprocessing, "cpu_count", lambda: 8)
241+
242+
assert tui._recommend_concurrent() >= 1
243+
244+
245+
def test_tui_main_single_run_flow_builds_runner_command(
246+
monkeypatch: pytest.MonkeyPatch,
247+
) -> None:
248+
driver = _QuestionaryDriver(
249+
select=[
250+
"v1",
251+
"single",
252+
"model-a",
253+
"codex",
254+
"001-daily-life-food-uber-eats",
255+
"exit",
256+
],
257+
confirm=[True],
258+
)
259+
commands = _install_tui_main_fakes(monkeypatch, driver)
260+
261+
tui.main()
262+
263+
assert commands == [
264+
[
265+
sys.executable,
266+
"-m",
267+
"clawbench.runner.run",
268+
"test-cases/v1/001-daily-life-food-uber-eats",
269+
"model-a",
270+
"--harness",
271+
"codex",
272+
]
273+
]
274+
driver.assert_done()
275+
276+
277+
def test_tui_main_batch_range_flow_builds_dry_run_command(
278+
monkeypatch: pytest.MonkeyPatch,
279+
) -> None:
280+
driver = _QuestionaryDriver(
281+
select=["v2", "batch", "openclaw", "range", "exit"],
282+
checkbox=[["model-a"]],
283+
text=["1-2", "3"],
284+
confirm=[True, True],
285+
)
286+
commands = _install_tui_main_fakes(monkeypatch, driver)
287+
288+
tui.main()
289+
290+
assert commands == [
291+
[
292+
sys.executable,
293+
"-m",
294+
"clawbench.runner.batch",
295+
"--models",
296+
"model-a",
297+
"--cases-suite",
298+
"v2",
299+
"--case-range",
300+
"1-2",
301+
"--max-concurrent",
302+
"3",
303+
"--harness",
304+
"openclaw",
305+
"--dry-run",
306+
]
307+
]
308+
driver.assert_done()
309+
310+
311+
def test_tui_main_dataset_switch_then_human_flow_builds_runner_command(
312+
monkeypatch: pytest.MonkeyPatch,
313+
) -> None:
314+
driver = _QuestionaryDriver(
315+
select=[
316+
"v1",
317+
"dataset",
318+
"claw-eval",
319+
"human",
320+
"ce-T050-regulatory-research",
321+
"exit",
322+
],
323+
confirm=[True],
324+
)
325+
commands = _install_tui_main_fakes(monkeypatch, driver)
326+
327+
tui.main()
328+
329+
assert commands == [
330+
[
331+
sys.executable,
332+
"-m",
333+
"clawbench.runner.run",
334+
"test-cases/claw-eval/ce-T050-regulatory-research",
335+
"--human",
336+
]
337+
]
338+
driver.assert_done()

0 commit comments

Comments
 (0)