hkblockchaintechnology commited on
Commit
2bef145
·
verified ·
1 Parent(s): 3c4fcca

Publish tests/ from marimo

Browse files
tests/test_cli.py CHANGED
@@ -53,7 +53,10 @@ 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
 
53
  """The whole pipeline - OCR index, menu, action - on a still image."""
54
  from tiny_computer_agent.perception import detector_available
55
 
56
+ # detector_available() defaults to "auto", which is True whenever the grid
57
+ # fallback exists - so it never skips. Ask for the OCR engine specifically:
58
+ # the pipeline below needs it to find the button.
59
+ if not detector_available("ocr")[0]:
60
  pytest.skip("no OCR engine installed")
61
  assert cli.main(["--image", "example", "--brain", "planner"]) == 0
62
  out = capsys.readouterr().out
tests/test_perception.py CHANGED
@@ -94,7 +94,7 @@ def test_the_synthetic_desktop_looks_like_the_intended_screen():
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())
@@ -108,7 +108,7 @@ def test_real_ocr_reads_a_real_screenshot():
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
 
94
  assert image.size == (720, 460)
95
 
96
 
97
+ @pytest.mark.skipif(not detector_available("ocr")[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())
 
108
  assert 260 <= save.center[1] <= 310
109
 
110
 
111
+ @pytest.mark.skipif(not detector_available("ocr")[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
tests/test_trace.py ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Traces record a run, replay it offline, and count how often the choice was right."""
2
+
3
+ import json
4
+
5
+ import pytest
6
+
7
+ from tiny_computer_agent import (
8
+ Action, ActionError, AgentLoop, DecisionMenu, Element, MockBrain, PlannerBrain,
9
+ )
10
+ from tiny_computer_agent.trace import (
11
+ DONE, NOTHING, TRACE_VERSION, Scenario, Scoreboard, Trace, TraceStep, benchmark, benchmark_traces, load_traces, run_scenario, score, score_replay,
12
+ score_trace,
13
+ )
14
+
15
+
16
+ # -- fixtures --------------------------------------------------------------
17
+
18
+ def frame(*labels):
19
+ return tuple(Element(label, x=i * 100, y=10, width=80, height=24) for i, label in enumerate(labels))
20
+
21
+
22
+ def topbar(*labels):
23
+ return tuple(Element(label, x=i * 100, y=6, width=80, height=20) for i, label in enumerate(labels))
24
+
25
+
26
+ def button(label, x, y=270):
27
+ return Element(label, x=x, y=y, width=90, height=30)
28
+
29
+
30
+ SAVE = Scenario(
31
+ "click 'Save' to save the document",
32
+ frames=[topbar("Files", "Edit", "View"),
33
+ (Element("Name", x=200, y=195, width=200, height=26), button("Save", 200)),
34
+ topbar("Files", "Edit", "View") + (button("Save", 200),)],
35
+ expected="Save", name="save",
36
+ )
37
+ CANCEL = Scenario(
38
+ "click 'Cancel' to discard the change",
39
+ frames=[(button("Save", 200), button("Cancel", 400))],
40
+ expected="Cancel", name="cancel",
41
+ )
42
+ DENIED = Scenario(
43
+ "click 'Terminal' to open a shell",
44
+ frames=[topbar("Terminal", "Files")],
45
+ expected="Terminal", name="terminal (denied by policy)",
46
+ )
47
+
48
+
49
+ class FirstItemBrain:
50
+ """A baseline: always click the first item in reading order. No model, no idea."""
51
+
52
+ name = "first-item"
53
+
54
+ def decide(self, goal, observation):
55
+ menu = DecisionMenu(observation)
56
+ if not menu:
57
+ return Action("done", reason="nothing to click")
58
+ return Action("click", label=menu.items[0].label, reason="first item in reading order")
59
+
60
+
61
+ def oracle(scenario):
62
+ return MockBrain([
63
+ {"kind": "click", "label": scenario.expected},
64
+ {"kind": "done", "reason": "clicked the right thing"},
65
+ ])
66
+
67
+
68
+ # -- recording -------------------------------------------------------------
69
+
70
+ def test_a_trace_records_the_frames_and_the_decision_without_pixels():
71
+ episode = AgentLoop(screen=SAVE.screen(), brain=PlannerBrain()).run(SAVE.goal)
72
+ trace = Trace.record(episode, expected="Save", brain="planner", scenario="save")
73
+
74
+ assert trace.expected == "Save" and trace.brain == "planner" and trace.scenario == "save"
75
+ assert trace.frames == len(episode.steps)
76
+ assert trace.choice == "Save"
77
+ # the element index survives; the pixels deliberately do not
78
+ assert trace.steps[-1].elements and all(isinstance(e, Element) for e in trace.steps[-1].elements)
79
+ assert all(step.observation().frame is None for step in trace.steps)
80
+
81
+
82
+ def test_waits_are_not_choices():
83
+ episode = AgentLoop(
84
+ screen=SAVE.screen(),
85
+ brain=MockBrain([{"kind": "wait", "seconds": 0.1}, {"kind": "click", "label": "Save"}]),
86
+ ).run(SAVE.goal)
87
+ trace = Trace.record(episode, expected="Save")
88
+ assert trace.steps[0].action.kind == "wait"
89
+ assert trace.choice == "Save" # the click, not the wait
90
+ assert trace.decided == "Save"
91
+
92
+
93
+ def test_a_brain_that_says_done_before_choosing_is_recorded_bluntly():
94
+ episode = AgentLoop(
95
+ screen=SAVE.screen(), brain=MockBrain([{"kind": "done", "reason": "assumed"}]),
96
+ ).run(SAVE.goal)
97
+ trace = Trace.record(episode, expected="Save")
98
+ assert trace.choice is None
99
+ assert trace.decided == DONE
100
+ assert score_trace(trace).correct is False
101
+
102
+
103
+ def test_a_trace_that_never_committed_reads_as_nothing():
104
+ trace = Trace(goal="click something")
105
+ assert trace.decided == NOTHING and trace.choice is None
106
+ assert trace.frames == 0
107
+
108
+
109
+ # -- persistence -----------------------------------------------------------
110
+
111
+ def test_a_trace_round_trips_through_json():
112
+ trace = run_scenario(SAVE, PlannerBrain())
113
+ again = Trace.from_json(trace.to_json())
114
+ assert again.to_dict() == trace.to_dict()
115
+ assert again.to_json() == trace.to_json()
116
+ assert json.loads(trace.to_json())["trace_version"] == TRACE_VERSION
117
+
118
+
119
+ def test_a_trace_saves_and_loads_from_a_directory(tmp_path):
120
+ run_scenario(SAVE, PlannerBrain()).save(tmp_path / "b.json")
121
+ run_scenario(CANCEL, PlannerBrain()).save(tmp_path / "a.json")
122
+ names = [t.name for t in load_traces(tmp_path)] # directories load in name order
123
+ assert names == ["cancel", "save"]
124
+ assert load_traces(tmp_path / "a.json")[0].name == "cancel"
125
+
126
+
127
+ def test_a_newer_trace_version_is_refused_rather_than_misread():
128
+ with pytest.raises(ValueError, match="newer than this build"):
129
+ Trace.from_dict({"trace_version": TRACE_VERSION + 1, "goal": "g"})
130
+
131
+
132
+ def test_a_corrupt_action_in_a_trace_is_reported_on_load():
133
+ data = {"goal": "g", "steps": [{"index": 0, "action": {"kind": "format_disk"}}]}
134
+ with pytest.raises(ActionError, match="unknown action"):
135
+ Trace.from_dict(data)
136
+
137
+
138
+ def test_unknown_keys_are_ignored_so_a_trace_stays_forward_compatible():
139
+ trace = Trace.from_dict({"goal": "g", "expected": "Save", "extra": 1,
140
+ "steps": [{"index": 0, "future_field": True}]})
141
+ assert trace.goal == "g" and trace.frames == 1
142
+
143
+
144
+ # -- scoring ---------------------------------------------------------------
145
+
146
+ def test_score_matches_the_choice_to_the_expected_answer():
147
+ board = benchmark([SAVE, CANCEL], {"planner": lambda s: PlannerBrain()})
148
+ assert board.accuracy == 1.0
149
+ assert all(v.correct for v in board)
150
+
151
+
152
+ def test_score_forgives_ocr_spacing_the_way_the_index_does():
153
+ trace = Trace(goal="g", expected="Save",
154
+ steps=[TraceStep(0, action=Action("click", label="Save ."))])
155
+ assert score_trace(trace).correct is True
156
+
157
+
158
+ def test_a_wrong_choice_is_scored_with_the_alternative_it_picked():
159
+ verdict = score(SAVE, FirstItemBrain())
160
+ assert verdict.correct is False
161
+ assert verdict.chosen == "Files"
162
+ assert "not 'Save'" in verdict.detail
163
+
164
+
165
+ def test_a_trace_with_no_expectation_cannot_be_scored_and_says_so():
166
+ verdict = score_trace(run_scenario(SAVE, PlannerBrain()))
167
+ # run_scenario always records ground truth, so drop it to check the guard
168
+ unjudged = score_trace(Trace(goal="g", steps=[TraceStep(0, action=Action("click", label="Save"))]))
169
+ assert verdict.correct is True
170
+ assert unjudged.correct is False and "no expected answer" in unjudged.detail
171
+
172
+
173
+ def test_a_refused_decision_is_scored_on_the_choice_but_flagged():
174
+ """A denied click is still what the brain chose - and it must not look clean."""
175
+ verdict = score(DENIED, FirstItemBrain())
176
+ assert verdict.chosen == "Terminal" and verdict.correct is True
177
+ assert verdict.blocked is True
178
+ assert "refused" in verdict.line()
179
+
180
+
181
+ def test_the_scoreboard_counts_blocked_decisions_separately_from_accuracy():
182
+ board = benchmark([SAVE, DENIED], {"first-item": lambda s: FirstItemBrain()})
183
+ assert board.total == 2 and board.blocked == 1
184
+ assert "stopped by the harness" in board.render()
185
+
186
+
187
+ # -- replay ----------------------------------------------------------------
188
+
189
+ def test_replay_answers_the_recorded_frames_with_a_different_brain():
190
+ recorded = run_scenario(SAVE, PlannerBrain())
191
+ assert recorded.choice == "Save"
192
+ replayed = recorded.replay(FirstItemBrain())
193
+ assert replayed.brain == "first-item"
194
+ assert replayed.choice == "Files" # same frames, a worse brain
195
+ assert replayed.frames == recorded.frames
196
+ assert replayed.steps[0].elements == recorded.steps[0].elements
197
+
198
+
199
+ def test_replay_keeps_the_ground_truth_so_the_replay_can_be_scored():
200
+ recorded = run_scenario(CANCEL, FirstItemBrain())
201
+ replayed = recorded.replay(PlannerBrain())
202
+ assert replayed.expected == "Cancel"
203
+ assert score_trace(replayed).correct is True
204
+
205
+
206
+ def test_a_brain_that_explodes_during_replay_is_a_scored_error_not_a_crash():
207
+ class Exploding:
208
+ name = "exploding"
209
+
210
+ def decide(self, goal, observation):
211
+ raise RuntimeError("model died")
212
+
213
+ replayed = run_scenario(SAVE, PlannerBrain()).replay(Exploding())
214
+ assert replayed.decided == NOTHING
215
+ assert "RuntimeError: model died" in replayed.steps[0].error
216
+ assert score_trace(replayed).correct is False
217
+
218
+
219
+ def test_score_replay_is_the_single_trace_form_of_the_roadmap_step():
220
+ recorded = run_scenario(SAVE, PlannerBrain())
221
+ assert score_replay(recorded, PlannerBrain()).correct is True
222
+ assert score_replay(recorded, FirstItemBrain()).correct is False
223
+
224
+
225
+ def test_benchmark_traces_replays_a_recorded_suite_against_one_brain():
226
+ traces = [run_scenario(SAVE, PlannerBrain())] # a trace of a run that got it right
227
+ assert benchmark_traces(traces, PlannerBrain()).accuracy == 1.0
228
+ # the same frames, a worse brain: the trace is the fixed part, the brain is the variable
229
+ replayed = benchmark_traces(traces, FirstItemBrain())
230
+ assert replayed.brains() == ["first-item"]
231
+ assert replayed.accuracy == 0.0
232
+
233
+
234
+ # -- the suite, and its table ---------------------------------------------
235
+
236
+ def test_benchmark_scores_every_brain_on_every_scenario_with_a_fresh_brain_each_time():
237
+ seen = []
238
+
239
+ def make(scenario):
240
+ seen.append(scenario.name)
241
+ return PlannerBrain()
242
+
243
+ board = benchmark([SAVE, CANCEL], {"planner": make, "oracle": oracle, "first-item": lambda s: FirstItemBrain()})
244
+ assert board.total == 6
245
+ assert board.brains() == ["planner", "oracle", "first-item"]
246
+ assert board.by_brain() == {"planner": 1.0, "oracle": 1.0, "first-item": 0.0}
247
+ assert seen == ["save", "cancel"] # a fresh brain per scenario
248
+ assert {row["correct"] for row in board.to_rows()} == {True, False}
249
+
250
+
251
+ def test_the_scoreboard_table_has_the_columns_a_notebook_wants():
252
+ board = benchmark([SAVE], {"planner": lambda s: PlannerBrain()})
253
+ assert board.to_rows() == [
254
+ {"brain": "planner", "scenario": "save", "expected": "Save", "chose": "Save", "correct": True}
255
+ ]
256
+ assert board.confusion() == {("Save", "Save"): 1}
257
+
258
+
259
+ def test_an_empty_scoreboard_is_zero_not_a_division_error():
260
+ board = Scoreboard()
261
+ assert board.total == 0 and board.accuracy == 0.0 and board.brains() == []
262
+ assert board.to_rows() == [] and "0/0" in board.render()
263
+
264
+
265
+ # -- scenarios -------------------------------------------------------------
266
+
267
+ def test_a_scenario_needs_at_least_one_frame():
268
+ with pytest.raises(ValueError, match="at least one frame"):
269
+ Scenario("g", frames=[], expected="Save")
270
+
271
+
272
+ def test_a_scenario_hands_out_a_fresh_screen_every_time():
273
+ scenario = Scenario("g", frames=[frame("Save")], expected="Save")
274
+ first, second = scenario.screen(), scenario.screen()
275
+ assert first is not second
276
+ assert first.captures == second.captures == 0
277
+ assert scenario.frames[0][0].label == "Save"
278
+
279
+
280
+ def test_render_is_a_replayable_summary_of_the_trace():
281
+ trace = run_scenario(DENIED, FirstItemBrain())
282
+ text = trace.render()
283
+ assert "trace: terminal (denied by policy)" in text
284
+ assert "brain: first-item" in text
285
+ assert "expected: 'Terminal'" in text and "chose: 'Terminal'" in text
286
+ assert "the harness refused it" in text
287
+ assert "click" in text
288
+
289
+
290
+ # -- the command line ------------------------------------------------------
291
+
292
+ def test_cli_records_a_trace_and_scores_it(tmp_path, capsys):
293
+ from tiny_computer_agent import __main__ as cli
294
+
295
+ target = tmp_path / "files.json"
296
+ assert cli.main(["--script", '[{"kind": "click", "label": "Files"}]',
297
+ "--trace", str(target), "--expected", "Files"]) == 0
298
+ out = capsys.readouterr().out
299
+ assert "trace: wrote" in out and "2 frames" in out
300
+ assert "correct" in out
301
+
302
+ trace = Trace.load(target)
303
+ assert trace.expected == "Files" and trace.choice == "Files"
304
+ assert score_trace(trace).correct is True
305
+
306
+
307
+ def test_cli_writes_a_trace_even_without_an_expected_answer(tmp_path, capsys):
308
+ from tiny_computer_agent import __main__ as cli
309
+
310
+ target = tmp_path / "unscored.json"
311
+ assert cli.main(["--script", '[{"kind": "click", "label": "Files"}]',
312
+ "--trace", str(target)]) == 0
313
+ assert "score:" not in capsys.readouterr().out
314
+ trace = Trace.load(target)
315
+ assert trace.expected is None and score_trace(trace).correct is False