File size: 2,050 Bytes
e8b22d2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
"""Deterministic test doubles for browser-control environments."""

from __future__ import annotations

from dataclasses import dataclass, field

from browser_control.environment import BrowserObservation, BrowserStep


@dataclass(slots=True)
class FakeClickTestEnvironment:
    """A one-button task with explicit lifecycle and reward semantics."""

    button_id: str = "13"
    actions: list[str] = field(default_factory=list, init=False)
    _active: bool = field(default=False, init=False, repr=False)
    _done: bool = field(default=False, init=False, repr=False)

    @property
    def expected_action(self) -> str:
        return f"click('{self.button_id}')"

    def reset(self, *, seed: int | None = None) -> BrowserStep:
        del seed
        self.actions.clear()
        self._active = True
        self._done = False
        return BrowserStep(
            observation=self._observation(),
            reward=0.0,
            terminated=False,
            info={"task": "click-test", "backend": "fake"},
        )

    def step(self, action: str) -> BrowserStep:
        if not self._active:
            raise RuntimeError("reset() must be called before step()")
        if self._done:
            raise RuntimeError("step() cannot be called after the episode is done")

        self.actions.append(action)
        succeeded = action == self.expected_action
        self._done = True
        error = None if succeeded else f"invalid action: {action}"
        return BrowserStep(
            observation=self._observation(error=error),
            reward=1.0 if succeeded else 0.0,
            terminated=True,
            info={"task": "click-test", "success": succeeded, "backend": "fake"},
        )

    def close(self) -> None:
        self._active = False

    def _observation(self, *, error: str | None = None) -> BrowserObservation:
        return BrowserObservation(
            goal="Click the button.",
            accessibility_tree=f"[{self.button_id}] button 'Click Me!'",
            last_action_error=error,
        )