hkblockchaintechnology commited on
Commit
24c5a49
·
verified ·
1 Parent(s): e187916

Publish tests/ from marimo

Browse files
tests/test_act.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+
3
+ from tiny_computer_agent import (
4
+ Action, DryRunExecutor, Element, InputUnavailable, Observation,
5
+ PyAutoGuiExecutor, resolve_point,
6
+ )
7
+
8
+ FRAME = Observation(elements=(Element("Save", x=320, y=120, width=80, height=24),))
9
+
10
+
11
+ def test_resolve_point_uses_pixels_when_given():
12
+ assert resolve_point(Action("click", x=1, y=2), FRAME) == (1, 2)
13
+
14
+
15
+ def test_resolve_point_uses_the_element_index_for_labels():
16
+ assert resolve_point(Action("click", label="Save"), FRAME) == (360, 132)
17
+
18
+
19
+ def test_resolve_point_is_none_for_unmatched_labels_and_other_verbs():
20
+ assert resolve_point(Action("click", label="Quit"), FRAME) is None
21
+ assert resolve_point(Action("type", text="hi"), FRAME) is None
22
+
23
+
24
+ def test_dry_run_records_instead_of_acting():
25
+ executor = DryRunExecutor()
26
+ result = executor.execute(Action("click", label="Save"), FRAME)
27
+ assert result.ok
28
+ assert "(dry-run)" in result.note
29
+ assert "(360, 132)" in result.note
30
+ assert len(executor.performed) == 1
31
+
32
+
33
+ def test_dry_run_fails_where_live_input_would_refuse():
34
+ """A dry run must never give false confidence about a doomed click."""
35
+ dry = DryRunExecutor().execute(Action("click", label="Quit"), FRAME)
36
+ live = PyAutoGuiExecutor().execute(Action("click", label="Quit"), FRAME)
37
+ assert dry.ok is False and live.ok is False
38
+ assert "absent from the element index" in dry.note
39
+
40
+
41
+ def test_live_executor_refuses_to_guess_pixels():
42
+ """The dangerous path: a label with no element behind it must never click."""
43
+ result = PyAutoGuiExecutor().execute(Action("click", label="Quit"), FRAME)
44
+ assert result.ok is False
45
+ assert "refusing to guess pixels" in result.note
46
+
47
+
48
+ def test_live_executor_treats_done_as_a_no_op():
49
+ result = PyAutoGuiExecutor().execute(Action("done", reason="finished"))
50
+ assert result.ok and "goal reported met" in result.note
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")
tests/test_actions.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+
3
+ from tiny_computer_agent import ACTION_KINDS, ACTION_MENU, Action, ActionError, parse_action
4
+
5
+
6
+ def test_click_by_label():
7
+ action = Action("click", label="Save")
8
+ assert action.kind == "click"
9
+ assert "label='Save'" in action.describe()
10
+
11
+
12
+ def test_click_by_pixels():
13
+ assert Action("click", x=10, y=20).x == 10
14
+
15
+
16
+ @pytest.mark.parametrize(
17
+ "kwargs, message",
18
+ [
19
+ ({"kind": "fly"}, "unknown action"),
20
+ ({"kind": "click"}, "click needs a label"),
21
+ ({"kind": "click", "x": -1, "y": 5}, "non-negative"),
22
+ ({"kind": "type", "text": ""}, "type needs non-empty text"),
23
+ ({"kind": "key"}, "key needs a chord"),
24
+ ({"kind": "scroll", "amount": 0}, "non-zero amount"),
25
+ ({"kind": "wait", "seconds": 0}, "positive number of seconds"),
26
+ ],
27
+ )
28
+ def test_validation_rejects_bad_actions(kwargs, message):
29
+ with pytest.raises(ActionError, match=message):
30
+ Action(**kwargs)
31
+
32
+ # the same rule must hold for anything a model sends back
33
+ with pytest.raises(ActionError):
34
+ parse_action(kwargs)
35
+
36
+
37
+ def test_parse_action_ignores_chatty_extra_keys():
38
+ action = parse_action({"kind": "type", "text": "hello", "confidence": 0.9, "thought": "..."})
39
+ assert action.text == "hello"
40
+
41
+
42
+ def test_parse_action_rejects_a_non_mapping():
43
+ with pytest.raises(ActionError, match="expected a mapping"):
44
+ parse_action(["click", "Save"])
45
+
46
+
47
+ def test_menu_documents_every_verb():
48
+ for kind in ACTION_KINDS:
49
+ assert f'"kind": "{kind}"' in ACTION_MENU
50
+
51
+
52
+ def test_round_trip_to_dict():
53
+ action = Action("scroll", amount=-3, reason="more content below")
54
+ assert parse_action(action.to_dict()).amount == -3
tests/test_cli.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+
3
+ from tiny_computer_agent import __main__ as cli
4
+
5
+
6
+ def test_demo_runs_dry_run_by_default(capsys):
7
+ assert cli.main([]) == 0
8
+ out = capsys.readouterr().out
9
+ assert "dry-run" in out
10
+ assert "(dry-run) would click" in out
11
+ assert "result: success" in out
12
+
13
+
14
+ def test_real_input_needs_the_allow_env_var(monkeypatch, capsys):
15
+ monkeypatch.delenv(cli.ALLOW_REAL_INPUT_ENV, raising=False)
16
+ assert cli.main(["--real-input"]) == 2
17
+ assert "refusing --real-input" in capsys.readouterr().err
18
+
19
+
20
+ def test_bad_script_json_is_reported(capsys):
21
+ assert cli.main(["--script", "{not json"]) == 2
22
+ assert "not valid JSON" in capsys.readouterr().err
23
+
24
+
25
+ def test_script_replay(capsys):
26
+ assert cli.main(["--script", '[{"kind": "done", "reason": "nothing to do"}]']) == 0
27
+ assert "nothing to do" in capsys.readouterr().out
28
+
29
+
30
+ 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
tests/test_loop.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+
3
+ from tiny_computer_agent import (
4
+ Action, AgentLoop, DryRunExecutor, Element, Episode, FakeScreen,
5
+ GoalUnreachable, MockBrain, PlannerBrain, Policy,
6
+ )
7
+ from tiny_computer_agent.__main__ import demo_screen
8
+
9
+
10
+ def test_scripted_episode_succeeds_and_records_every_step():
11
+ screen = FakeScreen([(Element("Save", x=320, y=120, width=80, height=24),)])
12
+ brain = MockBrain([
13
+ {"kind": "type", "text": "report"},
14
+ {"kind": "key", "keys": "ctrl+s"},
15
+ {"kind": "done", "reason": "file saved"},
16
+ ])
17
+ episode = AgentLoop(screen=screen, brain=brain).run("save the report")
18
+
19
+ assert episode.success
20
+ assert [s.action.kind for s in episode.steps] == ["type", "key", "done"]
21
+ assert "file saved" in episode.stop_reason
22
+ assert len(episode.to_rows()) == 3
23
+
24
+
25
+ def test_planner_solves_a_real_click_task_with_no_model():
26
+ episode = AgentLoop(screen=demo_screen(), brain=PlannerBrain()).run(
27
+ "click 'Save' to save the document"
28
+ )
29
+ assert episode.success
30
+ clicks = [s.action for s in episode.steps if s.action.kind == "click"]
31
+ assert [c.label for c in clicks] == ["Save"]
32
+
33
+
34
+ def test_screen_is_reread_after_every_executed_action():
35
+ """The verify half of the loop: never decide twice on the same frame."""
36
+ screen = FakeScreen([(Element("Save"),)])
37
+ brain = MockBrain([{"kind": "click", "label": "Save"}, {"kind": "click", "label": "Save"}])
38
+ episode = AgentLoop(screen=screen, brain=brain).run("click Save")
39
+
40
+ executed = [s for s in episode.steps if s.result is not None and s.action.kind != "done"]
41
+ assert len(executed) == 2
42
+ assert screen.captures == len(executed) + 1 # one initial read, one per action
43
+ assert episode.steps[0].observation.index == 0
44
+ assert episode.steps[1].observation.index == 1
45
+
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")
53
+
54
+ assert episode.success is False
55
+ assert len(episode.steps) == 3
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(
62
+ screen=FakeScreen([(Element("Terminal"),)]),
63
+ brain=MockBrain([{"kind": "type", "text": "sudo rm -rf /"}]),
64
+ executor=executor,
65
+ ).run("clean up")
66
+
67
+ assert episode.success is False
68
+ assert "policy refusal" in episode.stop_reason
69
+ assert episode.steps[-1].error.startswith("refused:")
70
+ assert executor.performed == []
71
+
72
+
73
+ def test_kill_switch_aborts_before_the_first_action(tmp_path):
74
+ marker = tmp_path / "STOP"
75
+ marker.write_text("stop")
76
+ episode = AgentLoop(
77
+ screen=FakeScreen([(Element("Save"),)]),
78
+ brain=MockBrain([{"kind": "click", "label": "Save"}]),
79
+ policy=Policy(kill_switch_path=str(marker)),
80
+ ).run("click Save")
81
+
82
+ assert episode.steps == []
83
+ assert episode.stop_reason == "kill switch engaged"
84
+
85
+
86
+ def test_a_broken_brain_stops_the_run_instead_of_crashing_it():
87
+ class Exploding:
88
+ name = "exploding"
89
+
90
+ def decide(self, goal, observation):
91
+ raise RuntimeError("model died")
92
+
93
+ episode = AgentLoop(screen=FakeScreen([(Element("Save"),)]), brain=Exploding()).run("save")
94
+
95
+ assert episode.success is False
96
+ assert episode.stop_reason == "brain error"
97
+ assert "RuntimeError: model died" in episode.steps[-1].error
98
+
99
+
100
+ def test_failed_action_ends_the_episode():
101
+ episode = AgentLoop(
102
+ screen=FakeScreen([(Element("Save"),)]),
103
+ brain=MockBrain([{"kind": "click", "label": "Nowhere"}]),
104
+ ).run("click Nowhere")
105
+
106
+ assert episode.success is False
107
+ assert "action failed" in episode.stop_reason
108
+
109
+
110
+ def test_on_step_callback_sees_every_step_and_render_mentions_the_goal():
111
+ seen = []
112
+ episode = AgentLoop(
113
+ screen=demo_screen(), brain=PlannerBrain(), on_step=seen.append
114
+ ).run("press Save")
115
+
116
+ assert len(seen) == len(episode.steps)
117
+ assert "press Save" in episode.render()
118
+ assert "success" in episode.render()
119
+
120
+
121
+ def test_a_brain_that_gives_up_is_a_failure_not_a_success():
122
+ """The harness owns success: "I give up" must never read as "goal met"."""
123
+ episode = AgentLoop(
124
+ screen=FakeScreen([(Element("Files"),)]),
125
+ brain=PlannerBrain(max_waits=0),
126
+ ).run("click 'Save'")
127
+
128
+ assert episode.success is False
129
+ assert episode.stop_reason.startswith("brain gave up:")
130
+ assert episode.steps[-1].error.startswith("gave up:")
131
+
132
+
133
+ def test_planner_raises_rather_than_inventing_a_target():
134
+ with pytest.raises(GoalUnreachable, match="no element name"):
135
+ PlannerBrain().decide("do the thing", FakeScreen([(Element("Save"),)]).capture())
tests/test_observe.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+
3
+ from tiny_computer_agent import Element, FakeScreen, Observation
4
+
5
+
6
+ def test_element_center_is_the_middle_of_the_box():
7
+ assert Element("Save", x=320, y=120, width=80, height=24).center == (360, 132)
8
+
9
+
10
+ def test_element_center_falls_back_to_corner_without_size():
11
+ assert Element("dot", x=5, y=7).center == (5, 7)
12
+
13
+
14
+ def test_element_rejects_empty_label():
15
+ with pytest.raises(ValueError, match="non-empty"):
16
+ Element(" ")
17
+
18
+
19
+ def test_element_rejects_negative_geometry():
20
+ with pytest.raises(ValueError, match="non-negative"):
21
+ Element("Save", x=-5)
22
+
23
+
24
+ def test_fake_screen_advances_then_holds_the_last_frame():
25
+ screen = FakeScreen([(Element("A"),), (Element("B"),)])
26
+ assert [e.label for f in range(4) for e in screen.capture().elements] == ["A", "B", "B", "B"]
27
+ assert screen.captures == 4
28
+
29
+
30
+ def test_find_prefers_exact_match_over_substring():
31
+ observation = Observation(elements=(
32
+ Element("Save as"), Element("Save"), Element("Save all"),
33
+ ))
34
+ assert observation.find("Save").label == "Save" # exact wins
35
+ assert observation.find("save as").label == "Save as" # case-insensitive
36
+ assert observation.find("ave a").label == "Save as" # substring fallback
37
+ assert observation.find("Quit") is None
38
+
39
+
40
+ def test_summary_lists_elements_and_truncates():
41
+ observation = Observation(
42
+ index=2,
43
+ elements=tuple(Element(f"button {i}") for i in range(5)),
44
+ text=" Untitled\n document ",
45
+ )
46
+ summary = observation.summary(max_elements=3)
47
+ assert "step 2" in summary
48
+ assert "Untitled document" in summary # whitespace collapsed
49
+ assert "button 0" in summary
50
+ assert "and 2 more" in summary
51
+
52
+
53
+ def test_summary_admits_when_there_is_no_index():
54
+ assert "no element index" in Observation().summary()
tests/test_safety.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+
3
+ from tiny_computer_agent import (
4
+ Action, Element, Observation, Policy, PolicyViolation,
5
+ )
6
+
7
+ FRAME = Observation(elements=(Element("Terminal"), Element("Save")))
8
+
9
+
10
+ def test_allows_an_ordinary_click():
11
+ Policy().check(Action("click", label="Save"), FRAME)
12
+
13
+
14
+ def test_refuses_a_denied_key_chord():
15
+ with pytest.raises(PolicyViolation, match="deny list"):
16
+ Policy().check(Action("key", keys="ctrl+alt+t"))
17
+
18
+
19
+ @pytest.mark.parametrize("text", ["sudo apt install foo", "rm -rf /", "my password is hunter2"])
20
+ def test_refuses_denied_text(text):
21
+ with pytest.raises(PolicyViolation, match="deny list"):
22
+ Policy().check(Action("type", text=text))
23
+
24
+
25
+ def test_refuses_clicking_a_denied_widget():
26
+ with pytest.raises(PolicyViolation, match="Terminal"):
27
+ Policy().check(Action("click", label="Terminal"), FRAME)
28
+
29
+
30
+ def test_deny_label_is_case_insensitive():
31
+ with pytest.raises(PolicyViolation):
32
+ Policy().check(Action("click", label="terminal"), FRAME)
33
+
34
+
35
+ def test_kill_switch_is_off_until_the_file_exists(tmp_path):
36
+ marker = tmp_path / "STOP"
37
+ policy = Policy(kill_switch_path=str(marker))
38
+ assert policy.should_stop() is False
39
+ marker.write_text("stop")
40
+ assert policy.should_stop() is True
41
+
42
+
43
+ def test_no_kill_switch_path_means_never_stops():
44
+ assert Policy().should_stop() is False
45
+
46
+
47
+ def test_rejects_a_useless_budget():
48
+ with pytest.raises(ValueError, match="step_budget"):
49
+ Policy(step_budget=0)
50
+
51
+
52
+ def test_deny_lists_are_configurable():
53
+ lax = Policy(deny_text_patterns=(), deny_labels=(), deny_keys=())
54
+ lax.check(Action("type", text="sudo rm -rf /"))
55
+ lax.check(Action("click", label="Terminal"), FRAME)