Publish tests/ from marimo
Browse files- tests/test_cli.py +4 -1
- tests/test_perception.py +2 -2
- tests/test_trace.py +315 -0
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 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|