test-driver/src/testdriver/browser.py
tegwick 44faf3de8e T07: agentic realization over a stdlib browser surface
Two decisions taken with the operator: stdlib HTML driver instead of
Playwright (F-0004), and a deterministic discovery runtime instead of a live
model. Both sit behind interfaces so the alternatives drop in later.

- html.py: stdlib DOM parse and query
- agentic.py: DiscoveryRuntime (agentic arm, ignores data-td by construction)
  and RecordedSelectorRuntime (control arm, uses the strongest identifier the
  page offers)
- browser.py: per-actor sessions over real HTTP, constructed per call so no
  actor inherits another's connection state
- cost/nondeterminism metrics recorded from the first run

F-0005 (CONCEPT_DRIFT): the H-001 result is a narrowing. Where test ids are
preserved, discovery 9/9 and recorded selectors 9/9 - the semantic action buys
nothing. Where they are dropped, discovery 2/3 and recorded 0/3. The concept
model presents semantic actions as generally superior; the evidence says
conditionally superior.

M21 and M22 added mid-task: the deciding side of the axis was N=1. M22 (field
names renamed) defeats the heuristic and is the first concrete evidence that a
live model would add capability, not just cost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1629012@bnt-lap001
Assistant-Session: 78d4fb13-8a1e-474b-87a3-9b9261c49a39
2026-08-22 23:50:29 +02:00

156 lines
5.5 KiB
Python

"""A browser-surface driver over the lab's HTML UI.
Each actor gets its **own** session — its own base URL binding, its own
credentials, its own cookie-equivalent. Nothing is shared between actors at
module or class level, because a shared client is exactly how isolation breaks in
practice and it never announces itself (F-0003).
The driver executes a `Plan` produced by an `ActorRuntime`. It does not decide
what to click and it does not decide whether the outcome was correct: the first
belongs to the runtime, the second to the observer and the oracle.
"""
from __future__ import annotations
import json
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass
from typing import Any
from .actions import SemanticAction, Surface
from .agentic import ActorRuntime, RealizationFailed
from .drivers import Realization, UnsupportedAction
from .html import Document
from .world import Actor
@dataclass(slots=True)
class Session:
"""One actor's private connection to the surface. Never shared."""
base_url: str
token: str
def _open(self, method: str, path: str, body: bytes | None, content_type: str):
request = urllib.request.Request(
urllib.parse.urljoin(self.base_url, path),
data=body,
method=method,
headers={
"Authorization": f"Bearer {self.token}",
**({"Content-Type": content_type} if body else {}),
},
)
try:
with urllib.request.urlopen(request, timeout=10) as response:
return response.status, response.read().decode()
except urllib.error.HTTPError as error:
return error.code, error.read().decode()
def get(self, path: str) -> tuple[int, str]:
return self._open("GET", path, None, "")
def post_form(self, path: str, fields: dict[str, str]) -> tuple[int, str]:
encoded = urllib.parse.urlencode(fields).encode()
return self._open("POST", path, encoded, "application/x-www-form-urlencoded")
def post_json(self, path: str, payload: dict[str, Any]) -> tuple[int, str]:
return self._open("POST", path, json.dumps(payload).encode(), "application/json")
class BrowserDriver:
"""Realizes semantic actions by finding and using controls on an HTML page."""
def __init__(
self,
base_url: str,
tokens: dict[str, str],
runtime: ActorRuntime,
resource_id: str,
) -> None:
self._base_url = base_url
self._tokens = tokens
self._runtime = runtime
self._resource_id = resource_id
self.surface = Surface(
id="browser", kind="html", description="lab browser UI"
)
def _session_for(self, actor: Actor) -> Session:
# Constructed per call: no actor can inherit another's connection state.
return Session(self._base_url, self._tokens[actor.id])
def _view_path(self) -> str:
return f"/resources/{self._resource_id}/view"
def realize(self, actor: Actor, action: SemanticAction) -> Realization:
action.check_surface(self.surface.id)
session = self._session_for(actor)
status, body = session.get(self._view_path())
if status != 200:
# A surface the actor cannot even load is not an adaptation problem.
return Realization(
self.surface.id,
{"action": action.name, "stage": "navigate", "status": status},
raised=f"NavigationFailed: {status}",
)
document = Document.parse(body)
args = {"resource_id": self._resource_id, **dict(action.args)}
try:
plan = self._runtime.plan(document, action.name, args)
except RealizationFailed as failure:
return Realization(
self.surface.id,
{
"action": action.name,
"stage": "discovery",
"runtime": getattr(self._runtime, "name", "?"),
"page_bytes": len(body),
},
raised=f"RealizationFailed: {failure}",
)
mechanics: dict[str, Any] = {
"action": action.describe(),
"stage": "submit",
"runtime": plan.metrics.runtime,
"rationale": plan.rationale,
"element_path": plan.element_path,
"target": plan.action,
"fields": sorted(plan.fields),
"metrics": plan.metrics.as_dict(),
"actor": actor.id,
}
status, response = session.post_form(plan.action, plan.fields)
mechanics["status"] = status
if status >= 400:
return Realization(
self.surface.id, mechanics, raised=f"Refused: {status} {response[:120]}"
)
return Realization(self.surface.id, mechanics)
def record_baseline_selectors(html: str) -> dict[str, str]:
"""Capture the control arm's selectors from a baseline page.
Deliberately generous: it takes the strongest identifier the page offers.
A weak control arm would make H-001 trivially true and worthless.
"""
document = Document.parse(html)
recordings: dict[str, str] = {}
for form in document.forms():
test_id = form.attrs.get("data-td", "")
target = form.attrs.get("action", "")
if not test_id:
continue
if target.endswith("/grant"):
recordings["grant_access"] = test_id
elif target.endswith("/revoke"):
recordings["revoke_access"] = test_id
return recordings