Ctrl K
ComunidadGuías…

tests/test_browser_identity_transport.py

Rama dev · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import asyncio
import json
from types import SimpleNamespace

import pytest

from src import browser_identity as browser
from src.agent_runtime.resources import ResourceIdentityError
from tests.test_browser_resource_identity import producer, observed, authority
from tests.test_runtime_resource_integration import approval_for, dispatch


@pytest.mark.parametrize("phase", ["timeout", "cancel", "spawn_cancel"])
async def test_client_is_killed_before_resend_deadline_without_retry(monkeypatch, phase):
    calls = []
    class Child:
        returncode = None
        killed = False
        async def wait(self):
            if self.killed:
                self.returncode = -9
                return -9
            await asyncio.Future()
        def kill(self): self.killed = True
    child = Child()
    started, release = asyncio.Event(), asyncio.Event()
    async def spawn(*args, **kwargs):
        calls.append(args); started.set()
        if phase == "spawn_cancel": await release.wait()
        return child
    monkeypatch.setattr(browser.asyncio, "create_subprocess_exec", spawn)
    original = asyncio.wait_for
    async def bounded(awaitable, timeout):
        assert timeout == browser.CLIENT_DEADLINE_S and timeout < 30
        return await original(awaitable, .01 if phase == "timeout" else timeout)
    monkeypatch.setattr(browser.asyncio, "wait_for", bounded)
    task = asyncio.create_task(browser.run_client(["trusted-producer", "session", "info"], env={}, cwd="/"))
    await started.wait()
    if phase != "timeout": task.cancel()
    release.set()
    with pytest.raises((asyncio.TimeoutError, asyncio.CancelledError)): await task
    assert child.killed and len(calls) == 1


async def test_sidecar_allowlist_has_no_enable_mutation_or_arbitrary_cdp():
    client = browser.CDPSidecar("ws://127.0.0.1:1234/devtools/browser/12345678-1234-1234-1234-123456789abc")
    for method in ("Page.enable", "Runtime.evaluate", "Page.navigate", "Target.closeTarget", "Browser.close"):
        with pytest.raises(ValueError): await client.call(method)


@pytest.mark.parametrize("envelope", [[], None, {"id": True, "result": {}}, {"id": "1", "result": {}}, {"id": 1, "error": {}, "result": {}}])
async def test_sidecar_rejects_malformed_identity_envelopes(monkeypatch, envelope):
    client = browser.CDPSidecar("ws://127.0.0.1:1234/devtools/browser/12345678-1234-1234-1234-123456789abc")
    async def send(*args): pass
    async def receive(): return envelope
    monkeypatch.setattr(client, "_send", send)
    monkeypatch.setattr(client, "_message", receive)
    with pytest.raises(ResourceIdentityError):
        await client.call("Target.getTargets")


@pytest.mark.parametrize("field", ["namespace", "runtimeError", "restoreKey"])
async def test_missing_nullable_lifecycle_fields_are_not_valid_observations(producer, field):
    record = await observed(producer)
    info = await record.command("session", "info")
    del (info["runtime"] if field == "restoreKey" else info)[field]
    with pytest.raises(ResourceIdentityError): browser.daemon_observation(record, info)


async def test_observation_cancellation_while_waiting_for_lock_invalidates_session(producer):
    record = await observed(producer)
    await record.lock.acquire()
    task = asyncio.create_task(browser.observe_registered(record))
    await asyncio.sleep(0)
    task.cancel()
    with pytest.raises(asyncio.CancelledError): await task
    record.lock.release()
    assert record.session is None and record.pages == ()


@pytest.mark.parametrize("args", [{"action": "click", "page": "t1"}, {"action": "batch", "commands": [["click", "e1"]]}])
async def test_central_dispatch_cannot_bypass_page_denial(producer, args):
    await observed(producer)
    current = authority()
    producer.calls.clear(); producer.cdp_calls.clear()
    _, result = await dispatch(current, "private_browser", json.dumps(args))
    assert result["failure_kind"] == browser.PAGE_FAILURE and result["executed"] is False
    assert not producer.calls and not producer.cdp_calls


@pytest.mark.parametrize("replacement", ["browser", "daemon"])
async def test_exact_approval_revalidates_before_claim(producer, replacement):
    record = await observed(producer)
    current = authority()
    content = '{"action":"session_info"}'
    approval = approval_for(current, "private_browser", content)
    if replacement == "daemon":
        producer.pid += 1
    else:
        record._endpoint = "ws://127.0.0.1:1234/devtools/browser/87654321-1234-1234-1234-123456789abc"
    _, result = await dispatch(current, "private_browser", content, approval)
    assert result["exit_code"] == 1 and not approval._claimed
    assert record.session is None and record.pages == ()


async def test_metadata_revalidation_never_auto_launches_or_calls_get_cdp_url(producer):
    await observed(producer)
    current = authority()
    producer.calls.clear()
    _, result = await dispatch(current, "private_browser", '{"action":"session_info"}')
    assert result["exit_code"] == 0
    assert producer.calls and all(command == ("session", "info") for command in producer.calls)


@pytest.mark.parametrize("status", ["EOF", "connection reset", "EAGAIN", "read timeout"])
async def test_page_failures_never_enter_producer_internal_retry_path(producer, status, monkeypatch):
    async def forbidden(*args, **kwargs):
        pytest.fail("Producer retry hazard reached: " + status)
    monkeypatch.setattr(browser, "run_client", forbidden)
    producer.calls.clear()
    from src.agent_tools.web_tools import PrivateBrowserTool
    result = await PrivateBrowserTool().execute('{"action":"wait","page":"t1","timeout_ms":120000}', {})
    assert result["executed"] is False and result["retryable"] is False
    assert producer.calls == []


async def test_page_scoped_child_still_cannot_execute_even_matching_observation(producer):
    await observed(producer)
    from dataclasses import replace
    parent = replace(authority(), browser_sessions=())
    child = parent.intersect(authority())
    _, result = await dispatch(child, "private_browser", '{"action":"click","page":"t1","ref":"e1"}')
    assert result["failure_kind"] == browser.PAGE_FAILURE and result["executed"] is False


async def test_observed_url_or_alias_change_is_not_resource_authority(producer):
    record = await observed(producer)
    from dataclasses import replace
    original = record.pages[0]
    metadata = replace(original, resolved_alias="t99", observed_url="https://different.example")
    assert original.authority_key() == metadata.authority_key()
    metadata.validate()


def test_raw_global_playwright_and_native_backend_are_not_substitutable():
    from src.agent_runtime.resources import ExternalResource
    from src.agent_runtime.authority import ExactOperation
    assert not browser.native_browser(ExactOperation.normalize("private_browser", '{"action":"session_info"}'),
        ExternalResource("mcp", "endpoint", "server", "tool", "epoch"))


@pytest.mark.parametrize("tool", ["browser_click", "browser_snapshot", "browser_evaluate", "browser_navigate", "browser_run_code"])
async def test_raw_mcp_browser_execution_cannot_evade_disabled_page_contract(tool):
    from src.agent_runtime.authority import RequestAuthority, OperationGrant
    name = "mcp__builtin_browser__" + tool
    current = RequestAuthority("request", "alice", "thread", "", (OperationGrant(name),))
    _, result = await dispatch(current, name, '{}')
    assert result["failure_kind"] == browser.PAGE_FAILURE and result["executed"] is False