hkblockchaintechnology commited on
Commit
8b9b8c8
·
verified ·
1 Parent(s): 57356d8

Publish tests/ from marimo

Browse files
tests/test_act.py CHANGED
@@ -51,11 +51,16 @@ def test_live_executor_treats_done_as_a_no_op():
51
 
52
 
53
  def test_live_executor_needs_pyautogui_installed():
54
- """Real input on a machine without pyautogui fails loudly, not silently."""
 
 
 
 
 
55
  try:
56
  import pyautogui # noqa: F401
57
- except ImportError:
58
  with pytest.raises(InputUnavailable, match="pyautogui"):
59
  PyAutoGuiExecutor().execute(Action("type", text="hello"))
60
  else:
61
- pytest.skip("pyautogui is installed here")
 
51
 
52
 
53
  def test_live_executor_needs_pyautogui_installed():
54
+ """Real input on a machine that cannot run pyautogui fails loudly, not silently.
55
+
56
+ pyautogui installed is not the same as pyautogui usable: with no display it
57
+ raises KeyError('DISPLAY') on import, so this must catch Exception rather than
58
+ ImportError or the failure escapes as a traceback instead of InputUnavailable.
59
+ """
60
  try:
61
  import pyautogui # noqa: F401
62
+ except Exception:
63
  with pytest.raises(InputUnavailable, match="pyautogui"):
64
  PyAutoGuiExecutor().execute(Action("type", text="hello"))
65
  else:
66
+ pytest.skip("pyautogui is usable here")
tests/test_live_desktop.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The end-to-end test: a real display, real capture, real OCR, a real click.
2
+
3
+ This is the only test that proves the loop drives a *computer* rather than a
4
+ synthetic screenshot. It is opt-in because it moves the real mouse and clicks:
5
+
6
+ TINY_COMPUTER_AGENT_LIVE_TEST=1 python -m pytest tests/test_live_desktop.py
7
+
8
+ Run it on a throwaway desktop (VM, container with Xvfb), never your working
9
+ session. With Xvfb it needs no hardware:
10
+
11
+ Xvfb :99 -screen 0 1280x800x24 &
12
+ DISPLAY=:99 TINY_COMPUTER_AGENT_LIVE_TEST=1 python -m pytest tests/test_live_desktop.py
13
+ """
14
+
15
+ import os
16
+ import pathlib
17
+ import subprocess
18
+ import sys
19
+ import time
20
+
21
+ import pytest
22
+
23
+ from tiny_computer_agent.act import PyAutoGuiExecutor
24
+ from tiny_computer_agent.actions import Action
25
+ from tiny_computer_agent.desktop import display_available
26
+ from tiny_computer_agent.observe import GrabScreen
27
+ from tiny_computer_agent.perception import OcrDetector, detector_available
28
+
29
+ OPT_IN = os.environ.get("TINY_COMPUTER_AGENT_LIVE_TEST") == "1"
30
+
31
+ pytestmark = pytest.mark.skipif(
32
+ not OPT_IN,
33
+ reason="moves the real mouse: set TINY_COMPUTER_AGENT_LIVE_TEST=1 on a throwaway desktop",
34
+ )
35
+
36
+ APP = '''import pathlib, tkinter as tk
37
+
38
+ MARKER = pathlib.Path("%(marker)s")
39
+ WINDOW = (120, 120)
40
+ BUTTON = (20, 155, 110, 40) # x, y, width, height inside the window
41
+
42
+ root = tk.Tk()
43
+ root.title("Document")
44
+ root.geometry("420x230+%%d+%%d" %% WINDOW)
45
+ root.configure(bg="#f5f6f8")
46
+ title = tk.Label(root, text="Untitled document - unsaved changes", bg="#f5f6f8")
47
+ title.place(x=20, y=18)
48
+ tk.Label(root, text="File name", bg="#f5f6f8").place(x=20, y=70)
49
+ entry = tk.Entry(root)
50
+ entry.place(x=20, y=95, width=300, height=28)
51
+ entry.insert(0, "report.txt")
52
+
53
+ def chosen(what):
54
+ MARKER.write_text(what)
55
+ title.config(text="Saved to disk")
56
+ for widget in buttons:
57
+ widget.place_forget()
58
+
59
+ buttons = [tk.Button(root, text="Save", command=lambda: chosen("Save"))]
60
+ buttons[0].place(x=BUTTON[0], y=BUTTON[1], width=BUTTON[2], height=BUTTON[3])
61
+ root.update()
62
+ root.mainloop()
63
+ '''
64
+
65
+
66
+ @pytest.fixture
67
+ def save_dialog(tmp_path):
68
+ """A real Tk window with a real Save button, on the real display."""
69
+ if not display_available()[0]:
70
+ pytest.skip("no display session to drive")
71
+ if not detector_available("ocr")[0]:
72
+ pytest.skip("this test needs OCR to find the button")
73
+ marker = tmp_path / "clicked.txt"
74
+ app = tmp_path / "app.py"
75
+ app.write_text(APP % {"marker": marker})
76
+ process = subprocess.Popen([sys.executable, str(app)])
77
+ try:
78
+ time.sleep(2.5) # let Tk map the window
79
+ yield marker, (120, 120), (20, 155, 110, 40)
80
+ finally:
81
+ process.terminate()
82
+
83
+
84
+ def test_ocr_finds_a_real_button_at_the_right_pixels(save_dialog):
85
+ """Pixels in, a label whose coordinates really are the button's."""
86
+ marker, window, (bx, by, bw, bh) = save_dialog
87
+ observation = GrabScreen(monitor=1).capture()
88
+ elements = OcrDetector().detect(observation.frame)
89
+
90
+ save = [e for e in elements if e.matches("Save")]
91
+ assert save, f"OCR found no Save button, only {[e.label for e in elements]}"
92
+ centre = save[0].center
93
+ assert window[0] + bx <= centre[0] <= window[0] + bx + bw, f"{centre} outside the button"
94
+ assert window[1] + by <= centre[1] <= window[1] + by + bh, f"{centre} outside the button"
95
+
96
+
97
+ def test_a_real_click_lands_on_the_button(save_dialog):
98
+ """The whole loop: see it, choose it, click it, and the app reacts."""
99
+ from tiny_computer_agent import AgentLoop, PlannerBrain, Policy
100
+
101
+ marker, _, _ = save_dialog
102
+ episode = AgentLoop(
103
+ screen=GrabScreen(monitor=1),
104
+ brain=PlannerBrain(),
105
+ executor=PyAutoGuiExecutor(),
106
+ policy=Policy(dry_run=False, step_budget=2),
107
+ verify=lambda observation: "saved" in observation.text.lower(),
108
+ ).run("click 'Save' to save the document")
109
+
110
+ assert marker.exists(), "no click landed on the button"
111
+ assert marker.read_text() == "Save"
112
+ assert episode.success, episode.render()
tests/test_verify.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Success must be evidence, not a claim."""
2
+
3
+ import pytest
4
+
5
+ from tiny_computer_agent import (
6
+ Action, AgentLoop, DryRunExecutor, Element, MockBrain, Observation, Policy,
7
+ )
8
+
9
+
10
+ class ScriptedTextScreen:
11
+ """Screen whose *text* changes, so a verifier has something to see."""
12
+
13
+ def __init__(self, texts):
14
+ self.texts = list(texts)
15
+ self.captures = 0
16
+
17
+ def capture(self) -> Observation:
18
+ text = self.texts[min(self.captures, len(self.texts) - 1)]
19
+ self.captures += 1
20
+ return Observation(index=self.captures - 1, elements=(Element("Save"),), text=text)
21
+
22
+
23
+ WAITS = [{"kind": "wait", "seconds": 0.1}] * 4
24
+ # A script longer than the budget, so the budget - not a scripted "done" - ends the run.
25
+ ENDLESS_WAITS = WAITS * 3
26
+
27
+
28
+ def run_with_budget(screen, brain, verify=None, budget=3):
29
+ return AgentLoop(screen=screen, brain=brain, policy=Policy(step_budget=budget),
30
+ verify=verify).run("save")
31
+
32
+
33
+ def test_without_a_verifier_a_successful_action_can_look_like_a_failure():
34
+ """The bug this feature exists for: the click worked, the episode still failed."""
35
+ changing = ScriptedTextScreen(["one", "two", "three", "four"])
36
+ episode = run_with_budget(changing, MockBrain(ENDLESS_WAITS))
37
+ assert episode.success is False
38
+ assert "budget" in episode.stop_reason
39
+
40
+
41
+ def test_a_verifier_turns_evidence_into_success():
42
+ screen = ScriptedTextScreen(["Unsaved changes", "before", "Saved"])
43
+ episode = AgentLoop(
44
+ screen=screen, brain=MockBrain(WAITS), verify=lambda obs: "saved" in obs.text.lower()
45
+ ).run("save the document")
46
+
47
+ assert episode.success is True
48
+ assert episode.stop_reason == "verified after 2 action(s)"
49
+ assert len(episode.steps) == 2
50
+
51
+
52
+ def test_a_verifier_that_never_matches_leaves_it_a_failure():
53
+ episode = AgentLoop(
54
+ screen=ScriptedTextScreen(["nothing to see"]), brain=MockBrain(WAITS),
55
+ verify=lambda obs: "saved" in obs.text.lower(),
56
+ ).run("save")
57
+ assert episode.success is False
58
+
59
+
60
+ def test_a_broken_verifier_cannot_fake_a_success():
61
+ def explode(observation):
62
+ raise RuntimeError("verifier bug")
63
+
64
+ episode = run_with_budget(
65
+ ScriptedTextScreen(["one", "two", "three", "four"]), MockBrain(ENDLESS_WAITS), verify=explode
66
+ )
67
+ assert episode.success is False
68
+ assert "budget" in episode.stop_reason
69
+
70
+
71
+ def test_a_model_claiming_done_still_wins_when_there_is_no_verifier():
72
+ """Unchanged behaviour: with no verifier the brain's "done" is all we have."""
73
+ episode = AgentLoop(
74
+ screen=ScriptedTextScreen(["x"]),
75
+ brain=MockBrain([{"kind": "click", "label": "Save"}, {"kind": "done", "reason": "saved it"}]),
76
+ ).run("save")
77
+ assert episode.success is True and "saved it" in episode.stop_reason
78
+
79
+
80
+ def test_verify_does_not_disturb_dry_run():
81
+ executor = DryRunExecutor()
82
+ AgentLoop(
83
+ screen=ScriptedTextScreen(["a", "b", "Saved"]), brain=MockBrain(WAITS), executor=executor,
84
+ verify=lambda obs: "saved" in obs.text.lower(),
85
+ ).run("save")
86
+ assert all(a.kind == "wait" for a in executor.performed)
87
+
88
+
89
+ def test_cli_verify_text_flag(capsys):
90
+ from tiny_computer_agent import __main__ as cli
91
+
92
+ # the demo screen's text says "Untitled document - unsaved changes"
93
+ assert cli.main(["--script", '[{"kind": "wait", "seconds": 0.1}]',
94
+ "--verify-text", "Unsaved", "--max-steps", "2"]) == 0
95
+ out = capsys.readouterr().out
96
+ assert "verified after" in out
97
+
98
+ # without it, the same run exhausts its budget and fails
99
+ assert cli.main(["--script", '[{"kind": "wait", "seconds": 0.1}]', "--max-steps", "1"]) == 1