Download tests/test_watchdog.py from hkblockchaintechnology/tiny-computer-agent: direct link, hf CLI and curl.
- Browser
- Download file 6.17 kB
-
https://huggingface.co/hkblockchaintechnology/tiny-computer-agent/resolve/main/tests/test_watchdog.py
- Command line
-
hf download hf://hkblockchaintechnology/tiny-computer-agent/tests/test_watchdog.py
-
curl -L -o test_watchdog.py https://huggingface.co/hkblockchaintechnology/tiny-computer-agent/resolve/main/tests/test_watchdog.py
6.17 kB
| """A call that never returns must not be a harness that never returns. | |
| The loop stops a *repeated* action and a step budget stops a long episode, but nothing | |
| stopped a single call that hangs: no step, no reason, no evidence, and a console that looks | |
| like it is thinking. These tests pin the watchdog's two promises - the loop stops waiting and | |
| says which call it stopped waiting for - and its one honest limit, that a Python thread | |
| cannot be killed, only abandoned. | |
| """ | |
| import time | |
| import pytest | |
| from tiny_computer_agent import ( | |
| AgentLoop, DryRunExecutor, Element, FakeScreen, MockBrain, Watchdog, WatchdogTimeout, | |
| ) | |
| # -- the unit: one guarded call ------------------------------------------------ # | |
| def test_a_call_that_returns_passes_its_value_through(): | |
| watchdog = Watchdog(step_timeout=5) | |
| assert watchdog.call("decide", lambda a, b: a + b, 2, 3) == 5 | |
| assert watchdog.orphans == [] | |
| def test_a_call_that_overruns_is_abandoned_and_named(): | |
| watchdog = Watchdog(step_timeout=0.05) | |
| started = time.monotonic() | |
| with pytest.raises(WatchdogTimeout) as raised: | |
| watchdog.call("execute", lambda: time.sleep(3)) | |
| elapsed = time.monotonic() - started | |
| assert elapsed < 1.5, "the watchdog waited for the deadline, not for the call" | |
| assert raised.value.where == "execute" | |
| assert raised.value.seconds == 0.05 | |
| assert "execute did not finish within 0.05s" in str(raised.value) | |
| assert watchdog.orphans == ["execute"], "an abandoned call is recorded, not forgiven" | |
| def test_the_wall_clock_refuses_the_next_call_before_it_starts(): | |
| """A spent wall clock is reported as un-attempted, never as a timeout of what was next.""" | |
| watchdog = Watchdog(step_timeout=5, wall_clock=0).start() | |
| called = [] | |
| with pytest.raises(WatchdogTimeout) as raised: | |
| watchdog.call("decide", lambda: called.append("ran")) | |
| assert called == [], "nothing may be attempted once the wall clock is spent" | |
| assert raised.value.abandoned is False | |
| assert "not attempted" in str(raised.value) | |
| def test_no_per_call_deadline_runs_the_call_inline(): | |
| """The honest default for a local desktop: this process *is* the thing being watched.""" | |
| watchdog = Watchdog(step_timeout=None) | |
| assert watchdog.call("execute", lambda: "inline") == "inline" | |
| assert watchdog.threads_started == 0 | |
| def test_an_exception_inside_a_guarded_call_reaches_the_caller_unchanged(): | |
| def broken(): | |
| raise ValueError("the executor is broken, not slow") | |
| with pytest.raises(ValueError, match="not slow"): | |
| Watchdog(step_timeout=5).call("execute", broken) | |
| def test_render_names_the_limits_and_every_abandoned_call(): | |
| watchdog = Watchdog(step_timeout=0.05, wall_clock=60).start() | |
| with pytest.raises(WatchdogTimeout): | |
| watchdog.call("decide", lambda: time.sleep(3)) | |
| line = watchdog.render() | |
| assert "per call <= 0.05s" in line | |
| assert "wall clock <= 60s" in line | |
| assert "abandoned: decide" in line | |
| def test_the_clock_starts_once_and_is_not_restarted_by_a_re_run(): | |
| watchdog = Watchdog(wall_clock=60).start() | |
| time.sleep(0.02) | |
| first = watchdog.elapsed | |
| watchdog.start() # re-running a goal must not hand it a fresh budget | |
| assert watchdog.elapsed >= first | |
| def test_remaining_counts_down_and_then_reports_expired(): | |
| watchdog = Watchdog(wall_clock=0.05).start() | |
| assert watchdog.remaining is not None and watchdog.remaining <= 0.05 | |
| time.sleep(0.06) | |
| assert watchdog.expired is True | |
| assert Watchdog(wall_clock=None).remaining is None | |
| # -- the loop: a hung stage becomes a step, not a stall ------------------------- # | |
| class SlowExecutor(DryRunExecutor): | |
| seconds = 2.0 | |
| def execute(self, action, observation): | |
| time.sleep(self.seconds) | |
| return super().execute(action, observation) | |
| class SlowScreen(FakeScreen): | |
| def capture(self): | |
| time.sleep(2.0) | |
| return super().capture() | |
| class SlowBrain(MockBrain): | |
| def decide(self, goal, observation): | |
| time.sleep(2.0) | |
| return super().decide(goal, observation) | |
| def _screen(): | |
| return FakeScreen([(Element("Save", x=320, y=120, width=80, height=24),)]) | |
| def test_a_hung_executor_ends_the_episode_with_a_named_reason(): | |
| loop = AgentLoop(screen=_screen(), brain=MockBrain([{"kind": "click", "label": "Save"}]), | |
| executor=SlowExecutor(), watchdog=Watchdog(step_timeout=0.05)) | |
| started = time.monotonic() | |
| episode = loop.run("click 'Save' to save the document") | |
| assert time.monotonic() - started < 2.0, "the loop must not wait for the hung call" | |
| assert episode.success is False | |
| assert episode.stop_reason.startswith("watchdog stopped the episode") | |
| assert "execute did not finish within 0.05s" in episode.steps[-1].error | |
| assert loop.watchdog.orphans == ["execute"] | |
| def test_a_hung_brain_ends_the_episode_too(): | |
| loop = AgentLoop(screen=_screen(), brain=SlowBrain([{"kind": "done", "reason": "x"}]), | |
| watchdog=Watchdog(step_timeout=0.05)) | |
| episode = loop.run("save") | |
| assert episode.success is False | |
| assert "decide did not finish within 0.05s" in episode.steps[-1].error | |
| assert loop.watchdog.orphans == ["decide"] | |
| def test_a_hung_capture_is_recorded_before_the_episode_starts(): | |
| loop = AgentLoop(screen=SlowScreen([(Element("Save", x=1, y=1, width=10, height=10),)]), | |
| brain=MockBrain([{"kind": "done", "reason": "x"}]), | |
| watchdog=Watchdog(step_timeout=0.05)) | |
| episode = loop.run("save") | |
| assert episode.success is False | |
| assert "watchdog stopped the episode" in episode.stop_reason | |
| assert "capture did not finish within 0.05s" in episode.stop_reason | |
| def test_without_a_watchdog_a_slow_but_finite_call_still_completes(): | |
| """Guarding is opt-in: a local desktop should not pay for a thread per step.""" | |
| loop = AgentLoop(screen=_screen(), brain=MockBrain([{"kind": "done", "reason": "fine"}]), | |
| executor=SlowExecutor()) | |
| loop.executor.seconds = 0.05 | |
| episode = loop.run("save") | |
| assert episode.success is True | |
| assert loop.watchdog is None | |