hkblockchaintechnology commited on
Commit
627c6cf
·
verified ·
1 Parent(s): 59022ca

Publish tests/ from marimo

Browse files
tests/test_brain.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+
3
+ from tiny_computer_agent import (
4
+ Action, BrainError, DecisionMenu, Element, GoalUnreachable, ModelBrain,
5
+ Observation, extract_json_object, extract_json_objects, parse_decision,
6
+ )
7
+
8
+ FRAME = Observation(elements=(
9
+ Element("Files", x=10, y=5, width=40, height=20),
10
+ Element("Save", x=200, y=260, width=60, height=20),
11
+ Element("Cancel", x=300, y=260, width=60, height=20),
12
+ ), text="untitled document")
13
+
14
+
15
+ def test_menu_is_numbered_in_reading_order_and_capped():
16
+ observation = Observation(elements=tuple(
17
+ Element(f"b{i}", x=(i % 2) * 100, y=(i // 2) * 50) for i in range(6)
18
+ ))
19
+ menu = DecisionMenu(observation, max_items=3)
20
+ assert [item.index for item in menu] == [1, 2, 3]
21
+ assert len(menu) == 3 and bool(menu)
22
+ assert "1." in menu.render()
23
+
24
+
25
+ def test_menu_lookup_tolerates_what_models_send():
26
+ menu = DecisionMenu(FRAME)
27
+ assert menu.item(1).label == "Files"
28
+ assert menu.item("2").label == "Save"
29
+ assert menu.item(2.0).label == "Save"
30
+ assert menu.item(0) is None and menu.item(99) is None and menu.item("x") is None
31
+
32
+
33
+ def test_menu_resolves_labels_through_normalisation():
34
+ menu = DecisionMenu(Observation(elements=(Element("Save ."),)))
35
+ assert menu.find("save").label == "Save ."
36
+ assert menu.find("nothing").__class__ is type(None)
37
+
38
+
39
+ def test_json_objects_are_extracted_in_order_and_the_last_wins():
40
+ raw = 'First {"kind": "click", "index": 1} then {"kind": "done", "reason": "ok"}'
41
+ assert [o["kind"] for o in extract_json_objects(raw)] == ["click", "done"]
42
+ # a model that echoes its prompt puts its own answer last
43
+ assert extract_json_object(raw)["kind"] == "done"
44
+ with pytest.raises(BrainError, match="no JSON object"):
45
+ extract_json_object("no json here")
46
+
47
+
48
+ def test_parse_decision_json_index():
49
+ action = parse_decision('{"kind": "click", "index": 2, "reason": "saves"}', FRAME)
50
+ assert action.kind == "click" and action.label == "Save"
51
+
52
+
53
+ def test_parse_decision_echo_of_the_prompt_is_rejected():
54
+ echoed = '{"kind": "click", "index": 1} {"kind": "click", "index": 2}'
55
+ assert parse_decision(echoed, FRAME).label == "Save" # last object wins
56
+ with pytest.raises(BrainError, match="echo"):
57
+ parse_decision(DecisionMenu(FRAME).render(), FRAME) # 3+ labels mentioned
58
+
59
+
60
+ def test_parse_decision_bare_number_and_prose_label():
61
+ assert parse_decision("2", FRAME).label == "Save"
62
+ assert parse_decision('Click on "Save" in the dialogue.', FRAME).label == "Save"
63
+ assert parse_decision("The goal is already done.", FRAME).kind == "done"
64
+
65
+
66
+ def test_parse_decision_rejects_an_index_outside_the_menu():
67
+ with pytest.raises(BrainError, match="not in the"):
68
+ parse_decision('{"kind": "click", "index": 99}', FRAME)
69
+
70
+
71
+ def test_parse_decision_rejects_a_label_that_is_not_on_screen():
72
+ with pytest.raises(BrainError, match="not on screen"):
73
+ parse_decision('{"kind": "click", "label": "Format disk"}', FRAME)
74
+
75
+
76
+ def test_parse_decision_tolerates_a_garbled_verb_when_a_label_is_present():
77
+ # 256M models really do emit {"kind": "file", "label": "Save"}
78
+ action = parse_decision('{"kind": "file", "label": "Save"}', FRAME)
79
+ assert action.kind == "click" and action.label == "Save"
80
+
81
+
82
+ def test_parse_decision_empty_and_useless_replies():
83
+ for raw in ["", " ", "I am not sure what to do, sorry."]:
84
+ with pytest.raises(BrainError):
85
+ parse_decision(raw, FRAME)
86
+
87
+
88
+ class _ScriptedModel(ModelBrain):
89
+ name = "scripted"
90
+
91
+ def __init__(self, replies, **kwargs):
92
+ super().__init__("fake/model", **kwargs)
93
+ self._replies = list(replies)
94
+ self.prompts = []
95
+
96
+ def _generate(self, prompt, observation):
97
+ self.prompts.append(prompt)
98
+ return self._replies.pop(0)
99
+
100
+
101
+ def test_model_brain_tells_the_model_what_it_already_tried():
102
+ brain = _ScriptedModel(['{"kind": "click", "index": 2}', '{"kind": "done", "reason": "ok"}'])
103
+ first = brain.decide("save", FRAME)
104
+ assert first.label == "Save"
105
+ assert "Already attempted" not in brain.prompts[0] # nothing tried yet
106
+
107
+ second = brain.decide("save", FRAME) # same screen -> no progress
108
+ prompt = brain.prompts[1]
109
+ assert "Already attempted" in prompt
110
+ assert "screen did NOT change" in prompt
111
+ assert "Choose a DIFFERENT action" in prompt
112
+ assert second.kind == "done"
113
+
114
+
115
+ def test_model_brain_history_is_bounded():
116
+ brain = _ScriptedModel(['{"kind": "wait", "seconds": 1}'] * 6)
117
+ for _ in range(6):
118
+ brain.decide("wait", FRAME)
119
+ assert len(brain.history) == 3
120
+
121
+
122
+ def test_prompt_carries_the_goal_the_menu_and_the_done_option():
123
+ brain = _ScriptedModel(['{"kind": "done"}'])
124
+ brain.decide("save the document", FRAME)
125
+ prompt = brain.prompts[0]
126
+ assert "save the document" in prompt
127
+ assert "2. Save (at" in prompt
128
+ assert '"kind": "done"' in prompt
129
+ assert "<number from the list>" in prompt # placeholders, not worked examples
130
+
131
+
132
+ def test_text_only_brains_stop_when_there_is_nothing_to_click():
133
+ class NeedsIndex(_ScriptedModel):
134
+ requires_index = True
135
+
136
+ with pytest.raises(GoalUnreachable, match="no clickable elements"):
137
+ NeedsIndex(['{"kind": "done"}']).decide("save", Observation(elements=()))
138
+
139
+
140
+ def test_a_right_answer_in_the_wrong_field_is_recovered():
141
+ """Real 0.5B output: named the correct element but put it in "reason"."""
142
+ action = parse_decision('{"kind": "click", "index": 0, "reason": "Save"}', FRAME)
143
+ assert action.kind == "click" and action.label == "Save"
144
+ assert "recovered" in action.reason
145
+
146
+
147
+ def test_recovery_still_refuses_an_unresolvable_index():
148
+ with pytest.raises(BrainError, match="not in the"):
149
+ parse_decision('{"kind": "click", "index": 42, "reason": "no idea"}', FRAME)
150
+
151
+
152
+ def test_recovery_is_not_ambiguous():
153
+ # two labels named and no usable index -> refuse rather than guess
154
+ with pytest.raises(BrainError):
155
+ parse_decision('{"kind": "click", "index": 0, "reason": "Save or Cancel"}', FRAME)
tests/test_cli.py CHANGED
@@ -31,3 +31,42 @@ def test_failure_exit_code(capsys):
31
  # the planner gives up when the target never appears, so the CLI exits non-zero
32
  assert cli.main(["click 'Nonexistent button'"]) == 1
33
  assert "gave up" in capsys.readouterr().out
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  # the planner gives up when the target never appears, so the CLI exits non-zero
32
  assert cli.main(["click 'Nonexistent button'"]) == 1
33
  assert "gave up" in capsys.readouterr().out
34
+
35
+
36
+ def test_doctor_exits_with_a_status(capsys):
37
+ assert cli.main(["--doctor"]) in (0, 1)
38
+ assert "preflight" in capsys.readouterr().out
39
+
40
+
41
+ def test_live_is_refused_without_a_display(capsys, monkeypatch):
42
+ from tiny_computer_agent.desktop import display_available
43
+
44
+ if display_available()[0]:
45
+ pytest.skip("this machine has a display, so --live would really run")
46
+ assert cli.main(["--live"]) == 2
47
+ captured = capsys.readouterr()
48
+ assert "refusing to run live" in captured.err
49
+ assert "no DISPLAY" in captured.out
50
+
51
+
52
+ def test_screenshot_replay_needs_no_display(capsys):
53
+ """The whole pipeline - OCR index, menu, action - on a still image."""
54
+ from tiny_computer_agent.perception import detector_available
55
+
56
+ if not detector_available()[0]:
57
+ pytest.skip("no OCR engine installed")
58
+ assert cli.main(["--image", "example", "--brain", "planner"]) == 0
59
+ out = capsys.readouterr().out
60
+ assert "screenshot replay" in out
61
+ assert "elements detected" in out
62
+ assert "would click" in out
63
+
64
+
65
+ def test_a_missing_image_is_reported_not_crashed(capsys):
66
+ assert cli.main(["--image", "/nonexistent/shot.png"]) == 2
67
+ assert "could not start" in capsys.readouterr().err
68
+
69
+
70
+ def test_vlm_brain_requires_a_live_screen(capsys):
71
+ assert cli.main(["--brain", "vlm"]) == 2
72
+ assert "needs --live" in capsys.readouterr().err
tests/test_desktop.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+
3
+ import platform
4
+
5
+ from tiny_computer_agent import (
6
+ AgentLoop, DryRunExecutor, Element, FakeScreen, MockBrain, PlannerBrain,
7
+ PyAutoGuiExecutor,
8
+ )
9
+ from tiny_computer_agent.desktop import (
10
+ DEFAULT_KILL_SWITCH, Check, build_agent, display_available, doctor, run_checks,
11
+ )
12
+
13
+ FRAME = FakeScreen([(Element("Save"),)])
14
+
15
+
16
+ class StubDetector:
17
+ name = "stub"
18
+
19
+ def detect(self, image):
20
+ return ()
21
+
22
+
23
+ def test_display_detection_follows_the_environment(monkeypatch):
24
+ monkeypatch.delenv("DISPLAY", raising=False)
25
+ monkeypatch.delenv("WAYLAND_DISPLAY", raising=False)
26
+ ok, detail = display_available()
27
+ if platform.system() in ("Linux", "FreeBSD"):
28
+ assert ok is False and "DISPLAY" in detail
29
+ monkeypatch.setenv("DISPLAY", ":0")
30
+ assert display_available()[0] is True
31
+
32
+
33
+ def test_checks_cover_every_piece_the_harness_needs():
34
+ names = [c.name for c in run_checks(brain="planner", detector="ocr")]
35
+ for expected in ("display server", "pillow", "element detector (ocr)",
36
+ "execution mode", "kill switch"):
37
+ assert expected in names
38
+
39
+
40
+ def test_pyautogui_is_only_required_for_real_input():
41
+ dry = {c.name: c for c in run_checks(real_input=False)}
42
+ live = {c.name: c for c in run_checks(real_input=True)}
43
+ assert dry["input backend (pyautogui)"].required is False
44
+ assert live["input backend (pyautogui)"].required is True
45
+ assert live["execution mode"].detail.startswith("REAL")
46
+
47
+
48
+ def test_the_planner_needs_no_model_runtime():
49
+ checks = {c.name: c for c in run_checks(brain="planner")}
50
+ assert checks["model runtime"].ok
51
+ assert "no weights" in checks["model runtime"].detail
52
+
53
+
54
+ def test_a_model_brain_reports_its_runtime():
55
+ checks = {c.name: c for c in run_checks(brain="text-planner")}
56
+ assert "model runtime" in checks
57
+ assert "transformers" in checks["model runtime"].detail
58
+
59
+
60
+ def test_doctor_gives_a_verdict_and_a_fix(monkeypatch):
61
+ monkeypatch.delenv("DISPLAY", raising=False)
62
+ monkeypatch.delenv("WAYLAND_DISPLAY", raising=False)
63
+ report = doctor(brain="planner")
64
+ assert "preflight" in report
65
+ assert "fix:" in report or "READY" in report
66
+
67
+
68
+ def test_a_check_renders_its_fix_hint():
69
+ rendered = Check("display server", False, "no display", fix="run on the desktop").render()
70
+ assert "[FAIL]" in rendered and "fix: run on the desktop" in rendered
71
+
72
+
73
+ def test_build_agent_defaults_to_dry_run_and_the_planner():
74
+ loop = build_agent(screen=FRAME, detector=StubDetector(), brain="planner")
75
+ assert isinstance(loop, AgentLoop)
76
+ assert isinstance(loop.brain, PlannerBrain)
77
+ assert isinstance(loop.executor, DryRunExecutor)
78
+ assert loop.policy.dry_run is True
79
+ assert loop.policy.step_budget == 10
80
+
81
+
82
+ def test_build_agent_real_input_swaps_the_executor():
83
+ loop = build_agent(screen=FRAME, detector=StubDetector(), brain=MockBrain([]),
84
+ dry_run=False, max_steps=3)
85
+ assert isinstance(loop.executor, PyAutoGuiExecutor)
86
+ assert loop.policy.dry_run is False
87
+
88
+
89
+ def test_build_agent_wires_the_kill_switch(tmp_path):
90
+ marker = tmp_path / "STOP"
91
+ loop = build_agent(screen=FRAME, detector=StubDetector(), brain=MockBrain([]),
92
+ kill_switch_path=str(marker))
93
+ assert loop.policy.kill_switch_path == str(marker)
94
+ assert DEFAULT_KILL_SWITCH.endswith("tiny-computer-agent.STOP")
tests/test_grid_fallback.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The grid fallback: an action space that needs nothing, and its guard rails."""
2
+
3
+ import pytest
4
+ from PIL import Image
5
+
6
+ from tiny_computer_agent import Action, Element, Observation, Policy, PolicyViolation
7
+ from tiny_computer_agent.perception import (
8
+ AutoDetector, GridDetector, OcrDetector, detector_available, make_detector,
9
+ )
10
+
11
+ CELL = Element("cell 12", kind="grid", x=600, y=0, width=60, height=57)
12
+
13
+
14
+ class FixedOcr:
15
+ name = "fixed"
16
+
17
+ def __init__(self, elements=()):
18
+ self.elements = tuple(elements)
19
+
20
+ def detect(self, image):
21
+ return self.elements
22
+
23
+
24
+ def test_grid_detector_needs_nothing_at_all():
25
+ elements = GridDetector(rows=2, cols=2).detect(Image.new("RGB", (100, 80)))
26
+ assert [e.label for e in elements] == ["cell 1", "cell 2", "cell 3", "cell 4"]
27
+ assert all(e.kind == "grid" for e in elements)
28
+ assert elements[0].center == (25, 20)
29
+ assert elements[3].center == (75, 60)
30
+
31
+
32
+ def test_grid_detector_rejects_a_useless_shape():
33
+ with pytest.raises(ValueError):
34
+ GridDetector(rows=0)
35
+
36
+
37
+ def test_grid_detector_is_always_available_and_default_off():
38
+ assert detector_available("grid")[0] is True
39
+ assert isinstance(make_detector("grid"), GridDetector)
40
+
41
+
42
+ def test_auto_uses_ocr_when_it_finds_text():
43
+ detector = AutoDetector(ocr=FixedOcr((Element("Save"),)), grid=GridDetector(rows=1, cols=1))
44
+ assert [e.label for e in detector.detect(Image.new("RGB", (10, 10)))] == ["Save"]
45
+
46
+
47
+ def test_auto_falls_back_to_the_grid_when_ocr_finds_nothing():
48
+ detector = AutoDetector(ocr=FixedOcr(()), grid=GridDetector(rows=1, cols=2))
49
+ assert [e.label for e in detector.detect(Image.new("RGB", (10, 10)))] == ["cell 1", "cell 2"]
50
+
51
+
52
+ def test_a_grid_click_is_fine_while_planning():
53
+ Policy(dry_run=True).check(Action("click", label="cell 12"), Observation(elements=(CELL,)))
54
+
55
+
56
+ def test_a_grid_click_is_refused_with_real_input():
57
+ """A blind click on a real desktop must be a deliberate choice, not a default."""
58
+ with pytest.raises(PolicyViolation, match="blind click"):
59
+ Policy(dry_run=False).check(Action("click", label="cell 12"), Observation(elements=(CELL,)))
60
+
61
+
62
+ def test_a_grid_click_is_refused_even_when_the_index_is_stale():
63
+ with pytest.raises(PolicyViolation, match="blind click"):
64
+ Policy(dry_run=False).check(Action("click", label="cell 47"), Observation())
65
+
66
+
67
+ def test_a_grid_click_is_allowed_when_asked_for():
68
+ Policy(dry_run=False, allow_grid_clicks=True).check(
69
+ Action("click", label="cell 12"), Observation(elements=(CELL,))
70
+ )
71
+
72
+
73
+ def test_an_ordinary_click_is_unaffected_by_the_grid_guard():
74
+ Policy(dry_run=False).check(
75
+ Action("click", label="Save"),
76
+ Observation(elements=(Element("Save", x=1, y=2, width=3, height=4),)),
77
+ )
78
+
79
+
80
+ @pytest.mark.skipif(not detector_available("ocr")[0], reason="no OCR engine installed")
81
+ def test_real_ocr_beats_the_grid_on_a_real_screenshot():
82
+ from tiny_computer_agent.demo import fake_desktop
83
+
84
+ elements = AutoDetector().detect(fake_desktop())
85
+ assert elements and any(e.kind != "grid" for e in elements)
86
+ assert isinstance(OcrDetector(), OcrDetector)
tests/test_loop.py CHANGED
@@ -46,7 +46,8 @@ def test_screen_is_reread_after_every_executed_action():
46
 
47
  def test_step_budget_is_enforced():
48
  screen = FakeScreen([(Element("Save"),)])
49
- brain = MockBrain([{"kind": "click", "label": "Save"}] * 10)
 
50
  episode = AgentLoop(
51
  screen=screen, brain=brain, policy=Policy(step_budget=3)
52
  ).run("click Save")
@@ -56,6 +57,52 @@ def test_step_budget_is_enforced():
56
  assert "budget of 3" in episode.stop_reason
57
 
58
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  def test_policy_refusal_stops_the_run_and_nothing_executes():
60
  executor = DryRunExecutor()
61
  episode = AgentLoop(
 
46
 
47
  def test_step_budget_is_enforced():
48
  screen = FakeScreen([(Element("Save"),)])
49
+ # distinct actions, so the budget is what stops it rather than the no-progress guard
50
+ brain = MockBrain([{"kind": "wait", "seconds": 0.5 + i} for i in range(10)])
51
  episode = AgentLoop(
52
  screen=screen, brain=brain, policy=Policy(step_budget=3)
53
  ).run("click Save")
 
57
  assert "budget of 3" in episode.stop_reason
58
 
59
 
60
+ def test_a_flailing_agent_is_stopped_instead_of_burning_the_budget():
61
+ """Repeating one action against an unchanged screen is stuck, not persistent."""
62
+ brain = MockBrain([{"kind": "click", "label": "Save"}] * 10)
63
+ episode = AgentLoop(
64
+ screen=FakeScreen([(Element("Save"),)]), brain=brain,
65
+ policy=Policy(step_budget=10),
66
+ ).run("click Save")
67
+
68
+ assert episode.success is False
69
+ assert "no progress" in episode.stop_reason
70
+ assert len(episode.steps) < 10
71
+ assert "unchanged screen" in episode.steps[-1].error
72
+
73
+
74
+ def test_progress_resets_the_repeat_counter():
75
+ """Two different actions on an unchanged screen is not flailing."""
76
+ brain = MockBrain([
77
+ {"kind": "wait", "seconds": 0.5},
78
+ {"kind": "click", "label": "Save"},
79
+ {"kind": "wait", "seconds": 0.5},
80
+ {"kind": "click", "label": "Save"},
81
+ {"kind": "done", "reason": "assumed saved"},
82
+ ])
83
+ episode = AgentLoop(
84
+ screen=FakeScreen([(Element("Save"),)]), brain=brain,
85
+ policy=Policy(step_budget=10),
86
+ ).run("save")
87
+
88
+ assert episode.success
89
+ assert len(episode.steps) == 5
90
+
91
+
92
+ def test_a_dead_screen_is_reported_as_an_episode_not_a_traceback():
93
+ class Blind:
94
+ name = "blind"
95
+
96
+ def capture(self):
97
+ raise RuntimeError("X connection failed")
98
+
99
+ episode = AgentLoop(screen=Blind(), brain=MockBrain([{"kind": "done"}])).run("anything")
100
+
101
+ assert episode.success is False
102
+ assert episode.stop_reason.startswith("capture failed: RuntimeError")
103
+ assert episode.steps == []
104
+
105
+
106
  def test_policy_refusal_stops_the_run_and_nothing_executes():
107
  executor = DryRunExecutor()
108
  episode = AgentLoop(
tests/test_perception.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+
3
+ from tiny_computer_agent import Element, FakeScreen, Observation
4
+ from tiny_computer_agent.demo import fake_desktop, write_fake_desktop
5
+ from tiny_computer_agent.observe import ImageScreen
6
+ from tiny_computer_agent.perception import (
7
+ DetectorUnavailable, IndexedScreen, OcrDetector, detector_available, make_detector,
8
+ )
9
+
10
+ BOX = [[10, 10], [80, 10], [80, 30], [10, 30]]
11
+
12
+
13
+ class FakeDetector:
14
+ name = "fake"
15
+
16
+ def __init__(self, elements=(), error=None):
17
+ self.elements = tuple(elements)
18
+ self.error = error
19
+
20
+ def detect(self, image):
21
+ if self.error is not None:
22
+ raise self.error
23
+ return self.elements
24
+
25
+
26
+ def test_low_confidence_boxes_are_dropped():
27
+ assert OcrDetector(min_score=0.9)._to_element(BOX, "Save", 0.5) is None
28
+
29
+
30
+ def test_prose_fragments_and_punctuation_are_not_widgets():
31
+ detector = OcrDetector(max_label_chars=20, min_label_chars=2)
32
+ assert detector._to_element(BOX, "a long sentence that is not a widget", 1.0) is None
33
+ assert detector._to_element(BOX, "S", 1.0) is None
34
+ assert detector._to_element(BOX, " ... ", 1.0) is None
35
+ assert detector._to_element(BOX, " first ", 1.0).label == "first"
36
+
37
+
38
+ def test_a_box_becomes_a_clickable_element():
39
+ element = OcrDetector()._to_element(BOX, "Save", 0.99)
40
+ assert (element.x, element.y, element.width, element.height) == (10, 10, 70, 20)
41
+ assert element.center == (45, 20)
42
+
43
+
44
+ def test_indexed_screen_replaces_elements_with_detected_ones():
45
+ screen = IndexedScreen(
46
+ FakeScreen([(Element("stale"),)]), FakeDetector((Element("Save"), Element("Cancel")))
47
+ )
48
+ observation = screen.capture()
49
+ assert [e.label for e in observation.elements] == ["Save", "Cancel"]
50
+ assert observation.text == "Save Cancel" # text filled from the index
51
+
52
+
53
+ def test_indexed_screen_keeps_text_from_the_source():
54
+ screen = IndexedScreen(FakeScreen([(Element("x"),)], text="hello"),
55
+ FakeDetector((Element("Save"),)))
56
+ assert screen.capture().text == "hello"
57
+
58
+
59
+ def test_a_perception_error_does_not_kill_the_run():
60
+ screen = IndexedScreen(FakeScreen([(Element("x"),)]), FakeDetector(error=ValueError("boom")))
61
+ observation = screen.capture()
62
+ assert observation.elements == ()
63
+ assert "perception error" in observation.text
64
+
65
+
66
+ def test_a_missing_engine_is_reported_loudly():
67
+ screen = IndexedScreen(FakeScreen([(Element("x"),)]),
68
+ FakeDetector(error=DetectorUnavailable("no engine")))
69
+ with pytest.raises(DetectorUnavailable):
70
+ screen.capture()
71
+
72
+
73
+ def test_unknown_detector_names_are_reported():
74
+ available, detail = detector_available("nope")
75
+ assert available is False and "unknown detector" in detail
76
+ with pytest.raises(DetectorUnavailable):
77
+ make_detector("nope")
78
+
79
+
80
+ def test_image_screen_replays_a_still_and_holds_it(tmp_path):
81
+ path = write_fake_desktop(tmp_path / "screen.png")
82
+ screen = ImageScreen(str(path))
83
+ assert screen.capture().frame.size == screen.capture().frame.size
84
+ assert isinstance(screen.capture().text, str)
85
+
86
+
87
+ def test_image_screen_rejects_something_empty():
88
+ with pytest.raises(ValueError):
89
+ ImageScreen([])
90
+
91
+
92
+ def test_the_synthetic_desktop_looks_like_the_intended_screen():
93
+ image = fake_desktop()
94
+ assert image.size == (720, 460)
95
+
96
+
97
+ @pytest.mark.skipif(not detector_available()[0], reason="no OCR engine installed")
98
+ def test_real_ocr_reads_a_real_screenshot():
99
+ """The whole point: pixels in, labelled elements with usable coordinates out."""
100
+ elements = OcrDetector().detect(fake_desktop())
101
+ labels = [e.label for e in elements]
102
+ assert len(elements) >= 3, labels
103
+ assert any(e.matches("Save") for e in elements), labels
104
+
105
+ save = next(e for e in elements if e.matches("Save"))
106
+ # the Save button is drawn at (200..380, 265..300)
107
+ assert 200 <= save.center[0] <= 380
108
+ assert 260 <= save.center[1] <= 310
109
+
110
+
111
+ @pytest.mark.skipif(not detector_available()[0], reason="no OCR engine installed")
112
+ def test_ocr_index_plugs_straight_into_a_click():
113
+ """Label -> pixel resolution on coordinates the detector actually produced."""
114
+ from tiny_computer_agent import Action, resolve_point
115
+
116
+ observation = Observation(elements=OcrDetector().detect(fake_desktop()))
117
+ point = resolve_point(Action("click", label="Save"), observation)
118
+ assert point is not None
119
+ assert 200 <= point[0] <= 380