"""Every tool that touches a browser goes through one funnel and can say which
browser it means.
Read from the SOURCE with `ast`, because the defect this guards does not fail:
a tool that reached the piece of work some other way - `work._open[...]`, a
`work.session(...)` of its own - would act on `main` whatever the caller asked,
or skip the sentence that says the browser is gone, and every test that drives
it with `main` open would stay green.
"""
from __future__ import annotations
import ast
import asyncio
import inspect
from invisible_playwright_mcp.mcp import server
NOT_DRIVING_A_PAGE = {"browser_open", "browser_close", "browser_list", "browser_status"}
ALLOWED = {"acting", "open", "close", "listing", "status"}
def _tools():
tree = ast.parse(inspect.getsource(server))
for node in tree.body:
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
continue
if any(isinstance(d, ast.Call) and isinstance(d.func, ast.Attribute)
and d.func.attr == "tool" for d in node.decorator_list):
yield node
def _reaches(node):
"""Every `work.<attr>` a tool body touches, with the call if it is one."""
for inner in ast.walk(node):
if (isinstance(inner, ast.Attribute) and isinstance(inner.value, ast.Name)
and inner.value.id == "work"):
yield inner.attr, inner
def _acting_calls(node):
for inner in ast.walk(node):
if (isinstance(inner, ast.Call) and isinstance(inner.func, ast.Attribute)
and isinstance(inner.func.value, ast.Name)
and inner.func.value.id == "work" and inner.func.attr == "acting"):
yield inner
def test_every_tool_that_drives_a_page_goes_through_the_funnel_and_names_its_browser():
"""Known-bad, two: have a tool call `actions.click(work._open["main"], ...)`;
drop `role=browser` from one `acting` call."""
driving = []
for tool in _tools():
if tool.name in NOT_DRIVING_A_PAGE:
continue
calls = list(_acting_calls(tool))
assert len(calls) == 1, (
"%s drives a page through %d `work.acting` calls; the funnel is one"
% (tool.name, len(calls)))
given = {k.arg: k.value for k in calls[0].keywords}
assert isinstance(given.get("role"), ast.Name) and given["role"].id == "browser", (
"%s does not hand `role=browser` to the funnel, so it acts on `main` "
"whatever the caller asked" % tool.name)
driving.append(tool.name)
assert len(driving) >= 12, "only %d tools drive a page: %r" % (len(driving), driving)
def test_no_tool_reaches_past_the_funnel():
for tool in _tools():
for attr, _ in _reaches(tool):
assert attr in ALLOWED, (
"%s reaches `work.%s`, which is not one of the five doors" % (tool.name, attr))
def test_the_tools_that_act_on_the_piece_of_work_name_a_browser_too():
"""`browser_open`, `browser_close` and `browser_status` take the role from
the caller and fall back to `main`; `browser_list` asks about the set and
takes none."""
for tool in _tools():
if tool.name not in NOT_DRIVING_A_PAGE or tool.name == "browser_list":
continue
calls = [c for c in ast.walk(tool)
if isinstance(c, ast.Call) and isinstance(c.func, ast.Attribute)
and isinstance(c.func.value, ast.Name) and c.func.value.id == "work"]
assert len(calls) == 1, tool.name
first = calls[0].args[0]
assert isinstance(first, ast.BoolOp) and any(
isinstance(v, ast.Name) and v.id == "browser" for v in first.values), (
"%s does not pass `browser or DEFAULT_BROWSER_ID`" % tool.name)
def test_every_tool_that_reaches_a_browser_offers_a_way_to_name_it():
"""Checked against the SCHEMA the server publishes, not the Python
signature: a parameter the client cannot see does not exist.
Known-bad: delete `browser` from any tool but `browser_list`.
"""
tools = {t.name: t for t in asyncio.run(server.mcp.list_tools())}
needing = {t.name for t in _tools()} - {"browser_list"}
assert len(needing) >= 15, sorted(needing)
mute = sorted(n for n in needing
if "browser" not in (tools[n].inputSchema.get("properties") or {}))
assert mute == [], (
"these tools reach a browser and offer no way to say which: %s" % mute)