test_browser_governance.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. from __future__ import annotations
  2. from pathlib import Path
  3. from typing import Any, Dict, Optional
  4. import pytest
  5. from agent.tools.builtin.browser import downloads, providers
  6. class _FakeResponse:
  7. def __init__(self, status_code: int, chunks: tuple[bytes, ...] = ()) -> None:
  8. self.status_code = status_code
  9. self._chunks = chunks
  10. async def __aenter__(self) -> "_FakeResponse":
  11. return self
  12. async def __aexit__(self, *_args: object) -> None:
  13. return None
  14. async def aiter_bytes(self):
  15. for chunk in self._chunks:
  16. yield chunk
  17. class _FakeClient:
  18. response = _FakeResponse(200)
  19. def __init__(self, **_kwargs: object) -> None:
  20. pass
  21. async def __aenter__(self) -> "_FakeClient":
  22. return self
  23. async def __aexit__(self, *_args: object) -> None:
  24. return None
  25. def stream(self, _method: str, _url: str) -> _FakeResponse:
  26. return self.response
  27. class _ContainerProvider:
  28. async def create(self, url: str, account_name: str) -> Dict[str, Any]:
  29. return {"success": True, "url": url, "account_name": account_name}
  30. class _CookieProvider:
  31. def get_cookie_row(self, cookie_type: str) -> Optional[Dict[str, Any]]:
  32. return {"type": cookie_type, "cookies": "session=test"}
  33. @pytest.fixture(autouse=True)
  34. def _reset_providers() -> None:
  35. yield
  36. providers.reset_browser_providers()
  37. @pytest.mark.asyncio
  38. async def test_download_accepts_partial_content_and_sanitizes_name(
  39. monkeypatch: pytest.MonkeyPatch,
  40. tmp_path: Path,
  41. ) -> None:
  42. _FakeClient.response = _FakeResponse(206, (b"abc", b"def"))
  43. monkeypatch.setattr(downloads.httpx, "AsyncClient", _FakeClient)
  44. result = await downloads.download_direct_url(
  45. "https://files.example.invalid/path/report.txt",
  46. "../../escaped.txt",
  47. download_dir=tmp_path,
  48. )
  49. assert result.error is None
  50. assert result.metadata["status_code"] == 206
  51. assert result.metadata["bytes"] == 6
  52. assert Path(result.metadata["path"]) == tmp_path / "escaped.txt"
  53. assert (tmp_path / "escaped.txt").read_bytes() == b"abcdef"
  54. assert not (tmp_path / ".escaped.txt.part").exists()
  55. @pytest.mark.asyncio
  56. async def test_download_failure_always_returns_tool_result(
  57. monkeypatch: pytest.MonkeyPatch,
  58. tmp_path: Path,
  59. ) -> None:
  60. _FakeClient.response = _FakeResponse(404)
  61. monkeypatch.setattr(downloads.httpx, "AsyncClient", _FakeClient)
  62. stale_partial = tmp_path / ".missing.bin.part"
  63. stale_partial.write_bytes(b"stale")
  64. result = await downloads.download_direct_url(
  65. "https://files.example.invalid/missing.bin",
  66. "",
  67. download_dir=tmp_path,
  68. )
  69. assert result.error == "HTTP 404"
  70. assert result.metadata["status_code"] == 404
  71. assert not list(tmp_path.iterdir())
  72. @pytest.mark.asyncio
  73. async def test_download_infers_and_decodes_url_filename(
  74. monkeypatch: pytest.MonkeyPatch,
  75. tmp_path: Path,
  76. ) -> None:
  77. _FakeClient.response = _FakeResponse(200, (b"content",))
  78. monkeypatch.setattr(downloads.httpx, "AsyncClient", _FakeClient)
  79. result = await downloads.download_direct_url(
  80. "https://files.example.invalid/%E6%8A%A5%E5%91%8A.txt",
  81. "",
  82. download_dir=tmp_path,
  83. )
  84. assert result.error is None
  85. assert Path(result.metadata["path"]).name == "报告.txt"
  86. assert (tmp_path / "报告.txt").read_bytes() == b"content"
  87. @pytest.mark.asyncio
  88. async def test_container_provider_normalizes_unexpected_failures(
  89. monkeypatch: pytest.MonkeyPatch,
  90. ) -> None:
  91. class _BrokenClient:
  92. def __init__(self, **_kwargs: object) -> None:
  93. raise TypeError("malformed service response")
  94. monkeypatch.setattr(providers.httpx, "AsyncClient", _BrokenClient)
  95. provider = providers.HttpContainerProvider(
  96. base_url="https://browser.example.invalid",
  97. startup_delay=0,
  98. retry_delay=0,
  99. )
  100. result = await provider.create("https://example.invalid", "account")
  101. assert result["success"] is False
  102. assert result["error"] == "malformed service response"
  103. def test_browser_providers_are_host_replaceable() -> None:
  104. container = _ContainerProvider()
  105. cookie = _CookieProvider()
  106. providers.configure_browser_providers(
  107. container_provider=container,
  108. cookie_provider=cookie,
  109. )
  110. assert providers.get_browser_container_provider() is container
  111. assert providers.get_browser_cookie_provider() is cookie
  112. def test_base_class_no_longer_knows_host_endpoint_or_cookie_table() -> None:
  113. source = (
  114. Path(__file__).parents[1]
  115. / "agent"
  116. / "tools"
  117. / "builtin"
  118. / "browser"
  119. / "baseClass.py"
  120. ).read_text(encoding="utf-8")
  121. assert "47.84.182.56" not in source
  122. assert "agent_channel_cookies" not in source