Files
codex-app-server/cloud/tests/test_coverage85_mcp.py
T
host 576fd50ee3 coverage85-step2a: MCP/deploy/verify-multiuser 剩余缺口真实行为测试(+43 项)
- playwright/imagegen/websearch 三个 MCP:handle_request 全工具分发、
  通知/错误分支、main() 进程内 stdio 循环(pipe/StringIO + 假浏
  览器/假依赖)、LineReader 分块与 EOF、真实 RemoteBridge 构造、
  flat-copy 回落导入;三文件孤立覆盖率 98%(余 __main__ 与
  通知型 handler-error else 分支,见回执)。
- deploy.py:check_clean_tree 脏/暂存/放行、archive 失败、remote
  助手、transfer 全路失败与 sha 不符、git 源冲突与干跑、tar
  路径干跑/构建/部署成功与失败;孤立 92%(余行由既有 deploy
  测试覆盖,__main__ 除外)。
- verify-multiuser main():真实本地控制面 13/13 通过且清理归零、
  死端口与 stub 项目失败早退、清理触发器异常报 leftover。
生产代码零改动。
2026-11-03 02:48:54 +00:00

722 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""覆盖率 78%->85% 第 2 步:三个 stdio MCP(playwright / imagegen /
websearch)的剩余缺口真实行为测试。
只用标准库:playwright 侧传假浏览器(handle_request 本来就接受
browser 参数);imagegen/websearch 侧传假 generate/edit/search/fetch;
main() 循环在进程内跑(playwright 用真实 pipe fd + 假浏览器,
imagegen/websearch 用 StringIO stdin/stdout + 模块级假依赖)。
不跑真实浏览器、不联网、不调真实网关。flat-copy 回落导入用
sys.modules 封堵 cloud.* 后端、重新导入模块本体系测(事后恢复)。
"""
import importlib
import io
import json
import os
import sys
import tempfile
import threading
import unittest
from pathlib import Path
try:
from cloud import imagegen_mcp, playwright_mcp, websearch_mcp
except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud import imagegen_mcp, playwright_mcp, websearch_mcp
class FakeBrowser:
"""playwright_mcp handle_request 要调的全部工具方法。"""
def __init__(self, cwd=None):
self.calls = []
self.cwd = cwd
self.frame_sink = None
self.closed = False
def _ret(self, name, *args):
self.calls.append((name, args))
return {"tool": name, "args": [str(a) for a in args]}
def navigate(self, url):
return self._ret("navigate", url)
def snapshot(self):
return self._ret("snapshot")
def click(self, selector):
return self._ret("click", selector)
def fill(self, selector, text):
return self._ret("fill", selector, text)
def click_at(self, x, y):
return self._ret("click_at", x, y)
def mouse_move(self, x, y):
return self._ret("mouse_move", x, y)
def key_press(self, key):
return self._ret("key_press", key)
def type_text(self, text):
return self._ret("type_text", text)
def snapshot_refs(self):
return self._ret("snapshot_refs")
def click_ref(self, ref):
return self._ret("click_ref", ref)
def live_shot(self, output_filename=None):
return self._ret("live_shot", output_filename)
def evaluate(self, expression):
return self._ret("evaluate", expression)
def screenshot(self, output_filename=None):
return self._ret("screenshot", output_filename)
def close(self):
self.closed = True
return self._ret("close")
def set_frame_sink(self, sink):
self.frame_sink = sink
def cached_info(self):
return {"url": "http://example.test/", "title": "t",
"started": 1}
def pump_screencast(self):
return ""
class FakeBridge:
def __init__(self, browser, holder="agent", poll_error=None):
self.browser = browser
self.holder = holder
self.poll_error = poll_error
self.poll_calls = 0
def get_holder(self):
return self.holder
def poll(self, browser):
self.poll_calls += 1
if self.poll_error is not None:
raise self.poll_error
def _content_text(resp):
return json.loads(resp["result"]["content"][0]["text"])
class PlaywrightHandleTest(unittest.TestCase):
def setUp(self):
self._old_browser = playwright_mcp._BROWSER
self._old_bridge = playwright_mcp._BRIDGE
def tearDown(self):
playwright_mcp._BROWSER = self._old_browser
playwright_mcp._BRIDGE = self._old_bridge
def test_non_dict_request(self):
resp = playwright_mcp.handle_request("nope")
self.assertEqual(resp["error"]["code"], -32700)
self.assertIsNone(resp["id"])
def test_initialize_and_notifications(self):
resp = playwright_mcp.handle_request(
{"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"protocolVersion": "2025-01-01"}})
self.assertEqual(resp["result"]["protocolVersion"], "2025-01-01")
self.assertEqual(resp["result"]["serverInfo"]["name"], "playwright")
resp = playwright_mcp.handle_request(
{"jsonrpc": "2.0", "id": 2, "method": "initialize",
"params": {}})
self.assertEqual(resp["result"]["protocolVersion"], "2024-11-05")
self.assertIsNone(playwright_mcp.handle_request(
{"jsonrpc": "2.0", "method": "initialize"}))
self.assertIsNone(playwright_mcp.handle_request(
{"jsonrpc": "2.0", "method": "notifications/initialized"}))
self.assertIsNone(playwright_mcp.handle_request(
{"jsonrpc": "2.0", "method": "notifications/cancelled"}))
def test_ping_and_tools_list(self):
self.assertEqual(playwright_mcp.handle_request(
{"jsonrpc": "2.0", "id": 7, "method": "ping"})["result"], {})
self.assertIsNone(playwright_mcp.handle_request(
{"jsonrpc": "2.0", "method": "ping"}))
resp = playwright_mcp.handle_request(
{"jsonrpc": "2.0", "id": 8, "method": "tools/list"})
names = [t["name"] for t in resp["result"]["tools"]]
self.assertEqual(len(names), 14)
for want in ("browser_navigate", "browser_snapshot", "browser_click",
"browser_fill", "browser_evaluate", "browser_click_at",
"browser_live_shot", "browser_snapshot_refs",
"browser_click_ref", "browser_mouse_move",
"browser_key_press", "browser_type_text",
"browser_screenshot", "browser_close"):
self.assertIn(want, names)
self.assertIsNone(playwright_mcp.handle_request(
{"jsonrpc": "2.0", "method": "tools/list"}))
def _call(self, browser, name, args):
return playwright_mcp.handle_request(
{"jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": {"name": name, "arguments": args}}, browser)
def test_tool_dispatch_all(self):
cases = [
("browser_navigate", {"url": "http://example.test/"},
("navigate", ("http://example.test/",))),
("browser_snapshot", {}, ("snapshot", ())),
("browser_click", {"selector": "button.x"},
("click", ("button.x",))),
("browser_fill", {"selector": "input", "text": "hi"},
("fill", ("input", "hi"))),
("browser_click_at", {"x": 10, "y": 20},
("click_at", (10, 20))),
("browser_mouse_move", {"x": 1, "y": 2},
("mouse_move", (1, 2))),
("browser_key_press", {"key": "Enter"},
("key_press", ("Enter",))),
("browser_type_text", {"text": "abc"},
("type_text", ("abc",))),
("browser_snapshot_refs", {}, ("snapshot_refs", ())),
("browser_click_ref", {"ref": "e3"}, ("click_ref", ("e3",))),
("browser_live_shot", {"output_filename": "live.png"},
("live_shot", ("live.png",))),
("browser_live_shot", {}, ("live_shot", (None,))),
("browser_evaluate", {"expression": "1+1"},
("evaluate", ("1+1",))),
("browser_screenshot", {"output_filename": "s.png"},
("screenshot", ("s.png",))),
("browser_screenshot", {}, ("screenshot", (None,))),
]
for name, args, want_call in cases:
with self.subTest(tool=name, args=args):
browser = FakeBrowser()
playwright_mcp._BRIDGE = FakeBridge(browser)
resp = self._call(browser, name, args)
self.assertFalse(resp["result"]["isError"])
self.assertEqual(_content_text(resp)["tool"],
want_call[0])
self.assertIn(want_call, browser.calls)
def test_tool_close_is_idempotent(self):
browser = FakeBrowser()
playwright_mcp._BRIDGE = FakeBridge(browser)
self._call(browser, "browser_close", {})
self._call(browser, "browser_close", {})
self.assertTrue(browser.closed)
self.assertEqual(
[c for c in browser.calls if c[0] == "close"],
[("close", ()), ("close", ())])
def test_unknown_tool_and_unknown_method(self):
browser = FakeBrowser()
playwright_mcp._BRIDGE = FakeBridge(browser)
resp = self._call(browser, "browser_nope", {})
self.assertEqual(resp["error"]["code"], -32602)
self.assertIn("browser_nope", resp["error"]["message"])
resp = playwright_mcp.handle_request(
{"jsonrpc": "2.0", "id": 9, "method": "resources/list"})
self.assertEqual(resp["error"]["code"], -32601)
self.assertIsNone(playwright_mcp.handle_request(
{"jsonrpc": "2.0", "method": "resources/list"}))
self.assertIsNone(playwright_mcp.handle_request(
{"jsonrpc": "2.0", "method": "tools/call",
"params": {"name": "browser_snapshot", "arguments": {}}},
browser))
def test_tool_exception_becomes_is_error(self):
class Boom(FakeBrowser):
def snapshot(self):
raise RuntimeError("page gone")
browser = Boom()
playwright_mcp._BRIDGE = FakeBridge(browser)
resp = self._call(browser, "browser_snapshot", {})
self.assertTrue(resp["result"]["isError"])
self.assertIn("page gone",
resp["result"]["content"][0]["text"])
def test_user_holds_browser_blocks_tools(self):
browser = FakeBrowser()
playwright_mcp._BRIDGE = FakeBridge(browser, holder="user")
resp = self._call(browser, "browser_snapshot", {})
self.assertTrue(resp["result"]["isError"])
self.assertIn("user holds browser control",
resp["result"]["content"][0]["text"])
self.assertEqual(browser.calls, [])
def test_get_browser_variants(self):
fake = FakeBrowser()
self.assertIs(playwright_mcp._get_browser(fake), fake)
playwright_mcp._BROWSER = fake
self.assertIs(playwright_mcp._get_browser(), fake)
playwright_mcp._BROWSER = None
real = playwright_mcp._get_browser()
self.assertIsInstance(real, playwright_mcp.Browser)
self.assertIs(playwright_mcp._get_browser(), real)
def test_get_bridge_constructs_real_bridge_and_caches(self):
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
browser = FakeBrowser(cwd=tmp.name)
playwright_mcp._BRIDGE = None
bridge = playwright_mcp._get_bridge(browser)
self.assertIsNotNone(bridge)
self.assertIs(bridge.browser, browser)
self.assertEqual(bridge.get_holder(), "agent")
self.assertIs(playwright_mcp._get_bridge(browser), bridge)
resp = self._call(browser, "browser_snapshot", {})
self.assertFalse(resp["result"]["isError"])
playwright_mcp._drain_remote(browser)
self.assertTrue(os.listdir(tmp.name))
def test_get_bridge_none_and_drain(self):
browser = FakeBrowser()
saved = {}
for name in ("playwright_remote", "cloud.playwright_remote"):
saved[name] = sys.modules.get(name)
sys.modules[name] = None
try:
playwright_mcp._BRIDGE = None
self.assertIsNone(playwright_mcp._get_bridge(browser))
playwright_mcp._drain_remote(browser) # 桥为空直接返回
finally:
for name, val in saved.items():
if val is None:
sys.modules.pop(name, None)
else:
sys.modules[name] = val
err = io.StringIO()
old_err = sys.stderr
sys.stderr = err
try:
playwright_mcp._BRIDGE = FakeBridge(
browser, poll_error=RuntimeError("poll boom"))
playwright_mcp._drain_remote(browser)
finally:
sys.stderr = old_err
self.assertIn("bridge poll error", err.getvalue())
self.assertIn("poll boom", err.getvalue())
def test_line_reader_chunks_partial_and_errors(self):
rfd, wfd = os.pipe()
reader = playwright_mcp._LineReader(rfd)
# 空管道非阻塞读:BlockingIOError 分支,返回空且未 drained。
lines, drained = reader.poll()
self.assertEqual(lines, [])
self.assertFalse(drained)
os.write(wfd, b'{"a": 1}\n{"b": 2}')
lines, drained = reader.poll()
self.assertEqual(lines, ['{"a": 1}'])
self.assertFalse(drained)
os.close(wfd)
lines, drained = reader.poll()
self.assertEqual(lines, ['{"b": 2}'])
self.assertTrue(drained)
os.close(rfd)
# 已关闭 fd:OSError 分支,eof 且 drained。
rfd2, _wfd2 = os.pipe()
os.close(rfd2)
reader2 = playwright_mcp._LineReader.__new__(
playwright_mcp._LineReader)
reader2._fd = rfd2
reader2._buf = b""
reader2.eof = False
lines, drained = reader2.poll()
self.assertTrue(drained)
os.close(_wfd2)
def test_handle_line_parse_and_handler_errors(self):
out = io.StringIO()
old_out = sys.stdout
sys.stdout = out
try:
playwright_mcp._handle_line("not-json{", FakeBrowser())
playwright_mcp._handle_line(
json.dumps({"jsonrpc": "2.0", "id": 5, "method": "ping"}),
FakeBrowser())
# initialize 且 params 不是 dict:handle_request 内抛
# AttributeError,_handle_line 转 internal error。
playwright_mcp._handle_line(
json.dumps({"jsonrpc": "2.0", "id": 6, "method": "initialize",
"params": ["x"]}),
FakeBrowser())
finally:
sys.stdout = old_out
got = [json.loads(ln) for ln in out.getvalue().splitlines()]
self.assertEqual(got[0]["error"]["code"], -32700)
self.assertEqual(got[1]["result"], {})
self.assertEqual(got[2]["error"]["code"], -32603)
self.assertEqual(got[2]["id"], 6)
def test_main_loop_with_pipe_and_fake_browser(self):
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
browser = FakeBrowser(cwd=tmp.name)
bridge = FakeBridge(browser)
playwright_mcp._BROWSER = browser
playwright_mcp._BRIDGE = bridge
rfd, wfd = os.pipe()
lines_in = [
"",
json.dumps({"jsonrpc": "2.0", "id": 1, "method": "initialize"}),
json.dumps({"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"name": "browser_navigate",
"arguments": {"url": "http://x.test/"}}}),
"broken{",
]
def writer():
import time as _time
_time.sleep(0.05) # 先空转一轮:走 main 的 idle/sleep 分支
for ln in lines_in:
os.write(wfd, (ln + "\n").encode("utf-8"))
os.close(wfd)
threading.Thread(target=writer, daemon=True).start()
old_in, old_out = sys.stdin, sys.stdout
new_in = os.fdopen(rfd)
sys.stdin = new_in
sys.stdout = io.StringIO()
try:
playwright_mcp.main()
out = sys.stdout.getvalue()
finally:
sys.stdin = old_in
sys.stdout = old_out
new_in.close()
got = [json.loads(ln) for ln in out.splitlines()]
self.assertEqual(got[0]["result"]["serverInfo"]["name"], "playwright")
self.assertEqual(_content_text(got[1])["tool"], "navigate")
self.assertEqual(got[2]["error"]["code"], -32700)
self.assertIsNotNone(browser.frame_sink)
self.assertGreaterEqual(bridge.poll_calls, 1)
self.assertIn(("navigate", ("http://x.test/",)), browser.calls)
def test_main_loop_without_bridge(self):
browser = FakeBrowser()
playwright_mcp._BROWSER = browser
playwright_mcp._BRIDGE = None
saved = {}
for name in ("playwright_remote", "cloud.playwright_remote"):
saved[name] = sys.modules.get(name)
sys.modules[name] = None
rfd, wfd = os.pipe()
os.write(wfd, json.dumps(
{"jsonrpc": "2.0", "id": 1, "method": "ping"}).encode() + b"\n")
os.close(wfd)
old_in, old_out = sys.stdin, sys.stdout
new_in = os.fdopen(rfd)
sys.stdin = new_in
sys.stdout = io.StringIO()
try:
playwright_mcp.main()
out = sys.stdout.getvalue()
finally:
sys.stdin = old_in
sys.stdout = old_out
new_in.close()
for name, val in saved.items():
if val is None:
sys.modules.pop(name, None)
else:
sys.modules[name] = val
got = [json.loads(ln) for ln in out.splitlines()]
self.assertEqual(got[0]["result"], {})
self.assertIsNone(browser.frame_sink)
def test_frame_queue_real_and_fallback(self):
q = playwright_mcp._frame_queue()
q.put("frame-1")
self.assertEqual(q.get_nowait(), "frame-1")
with self.assertRaises(Exception):
q.get_nowait()
saved = {}
for name in ("playwright_remote", "cloud.playwright_remote"):
saved[name] = sys.modules.get(name)
sys.modules[name] = None
try:
q2 = playwright_mcp._frame_queue()
finally:
for name, val in saved.items():
if val is None:
sys.modules.pop(name, None)
else:
sys.modules[name] = val
q2.put("x")
self.assertEqual(q2.get_nowait(), "x")
with self.assertRaises(IndexError):
q2.get_nowait()
def test_flat_copy_fallback_import(self):
# 封堵 cloud.playwright_browser 后重新导入:走 sys.path 回落。
orig_browser = sys.modules.get("cloud.playwright_browser")
orig_mcp = sys.modules.get("cloud.playwright_mcp")
sys.modules.pop("cloud.playwright_mcp", None)
sys.modules["cloud.playwright_browser"] = None
try:
mod = importlib.import_module("cloud.playwright_mcp")
self.assertEqual(mod.SERVER_NAME, "playwright")
self.assertIsNotNone(mod.Browser)
finally:
sys.modules.pop("playwright_browser", None)
if orig_browser is not None:
sys.modules["cloud.playwright_browser"] = orig_browser
else:
sys.modules.pop("cloud.playwright_browser", None)
if orig_mcp is not None:
sys.modules["cloud.playwright_mcp"] = orig_mcp
else:
sys.modules.pop("cloud.playwright_mcp", None)
cloud_dir = str(Path(__file__).resolve().parents[1])
while cloud_dir in sys.path:
sys.path.remove(cloud_dir)
class ImageGenMcpTest(unittest.TestCase):
def test_handle_request_dispatch_and_errors(self):
calls = {}
def fake_generate(prompt, output_filename=None, size="1024x1024",
model=None):
calls["generate"] = (prompt, output_filename, size, model)
return {"path": "/tmp/a.png", "bytes": 10}
def fake_edit(prompt, input_filename, output_filename=None,
size="1024x1024", model=None):
calls["edit"] = (prompt, input_filename, output_filename, size,
model)
return {"path": "/tmp/b.png", "bytes": 20}
self.assertEqual(imagegen_mcp.handle_request("x")["error"]["code"],
-32700)
self.assertIsNone(imagegen_mcp.handle_request(
{"jsonrpc": "2.0", "method": "initialize"}))
self.assertIsNone(imagegen_mcp.handle_request(
{"jsonrpc": "2.0", "method": "notifications/cancelled"}))
self.assertEqual(imagegen_mcp.handle_request(
{"jsonrpc": "2.0", "id": 1, "method": "ping"})["result"], {})
self.assertIsNone(imagegen_mcp.handle_request(
{"jsonrpc": "2.0", "method": "ping"}))
resp = imagegen_mcp.handle_request(
{"jsonrpc": "2.0", "id": 2, "method": "tools/list"})
self.assertEqual([t["name"] for t in resp["result"]["tools"]],
["image_generate", "image_edit"])
self.assertIsNone(imagegen_mcp.handle_request(
{"jsonrpc": "2.0", "method": "tools/list"}))
resp = imagegen_mcp.handle_request(
{"jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": {"name": "image_generate",
"arguments": {"prompt": "a cat"}}},
generate_fn=fake_generate, edit_fn=fake_edit)
self.assertFalse(resp["result"]["isError"])
self.assertEqual(calls["generate"],
("a cat", None, "1024x1024", None))
resp = imagegen_mcp.handle_request(
{"jsonrpc": "2.0", "id": 4, "method": "tools/call",
"params": {"name": "image_edit",
"arguments": {"prompt": "redder",
"input_filename": "a.png",
"size": "1024x1536",
"model": "m1"}}},
generate_fn=fake_generate, edit_fn=fake_edit)
self.assertEqual(calls["edit"],
("redder", "a.png", None, "1024x1536", "m1"))
resp = imagegen_mcp.handle_request(
{"jsonrpc": "2.0", "id": 5, "method": "tools/call",
"params": {"name": "nope", "arguments": {}}},
generate_fn=fake_generate, edit_fn=fake_edit)
self.assertEqual(resp["error"]["code"], -32602)
self.assertIsNone(imagegen_mcp.handle_request(
{"jsonrpc": "2.0", "method": "tools/call",
"params": {"name": "image_generate",
"arguments": {"prompt": "x"}}},
generate_fn=fake_generate, edit_fn=fake_edit))
self.assertIsNone(imagegen_mcp.handle_request(
{"jsonrpc": "2.0", "method": "nope"}))
resp = imagegen_mcp.handle_request(
{"jsonrpc": "2.0", "id": 6, "method": "bogus"})
self.assertEqual(resp["error"]["code"], -32601)
def boom(*args, **kwargs):
raise RuntimeError("relay down")
resp = imagegen_mcp.handle_request(
{"jsonrpc": "2.0", "id": 7, "method": "tools/call",
"params": {"name": "image_generate",
"arguments": {"prompt": "x"}}},
generate_fn=boom, edit_fn=fake_edit)
self.assertTrue(resp["result"]["isError"])
self.assertIn("relay down", resp["result"]["content"][0]["text"])
def test_main_loop_with_fake_generate(self):
old_generate = imagegen_mcp.generate
imagegen_mcp.generate = lambda prompt, output_filename=None, \
size="1024x1024", model=None: {"path": "/tmp/g.png"}
old_in, old_out = sys.stdin, sys.stdout
sys.stdin = io.StringIO("\nbroken{\n" + json.dumps(
{"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {"name": "image_generate",
"arguments": {"prompt": "sunset"}}}) + "\n" + json.dumps(
{"jsonrpc": "2.0", "id": 2, "method": "initialize",
"params": ["x"]}) + "\n")
sys.stdout = io.StringIO()
try:
imagegen_mcp.main()
out = sys.stdout.getvalue()
finally:
sys.stdin, sys.stdout = old_in, old_out
imagegen_mcp.generate = old_generate
got = [json.loads(ln) for ln in out.splitlines()]
self.assertEqual(got[0]["error"]["code"], -32700)
self.assertFalse(got[1]["result"]["isError"])
self.assertEqual(got[2]["error"]["code"], -32603)
def test_flat_copy_fallback_import(self):
orig_backend = sys.modules.get("cloud.imagegen")
orig_mcp = sys.modules.get("cloud.imagegen_mcp")
sys.modules.pop("cloud.imagegen_mcp", None)
sys.modules["cloud.imagegen"] = None
try:
mod = importlib.import_module("cloud.imagegen_mcp")
self.assertEqual(mod.SERVER_NAME, "imagegen")
finally:
sys.modules.pop("imagegen", None)
if orig_backend is not None:
sys.modules["cloud.imagegen"] = orig_backend
else:
sys.modules.pop("cloud.imagegen", None)
if orig_mcp is not None:
sys.modules["cloud.imagegen_mcp"] = orig_mcp
else:
sys.modules.pop("cloud.imagegen_mcp", None)
cloud_dir = str(Path(__file__).resolve().parents[1])
while cloud_dir in sys.path:
sys.path.remove(cloud_dir)
class WebSearchMcpTest(unittest.TestCase):
def test_handle_request_dispatch_and_errors(self):
calls = {}
def fake_search(query, limit=5, provider="auto"):
calls["search"] = (query, limit, provider)
return {"results": [{"title": "t", "url": "http://x/"}]}
def fake_fetch(url, char_limit=15000):
calls["fetch"] = (url, char_limit)
return {"text": "hello"}
self.assertEqual(websearch_mcp.handle_request(42)["error"]["code"],
-32700)
self.assertIsNone(websearch_mcp.handle_request(
{"jsonrpc": "2.0", "method": "initialize"}))
self.assertIsNone(websearch_mcp.handle_request(
{"jsonrpc": "2.0", "method": "notifications/initialized"}))
self.assertEqual(websearch_mcp.handle_request(
{"jsonrpc": "2.0", "id": 1, "method": "ping"})["result"], {})
self.assertIsNone(websearch_mcp.handle_request(
{"jsonrpc": "2.0", "method": "ping"}))
resp = websearch_mcp.handle_request(
{"jsonrpc": "2.0", "id": 2, "method": "tools/list"})
self.assertEqual([t["name"] for t in resp["result"]["tools"]],
["web_search", "web_fetch"])
self.assertIsNone(websearch_mcp.handle_request(
{"jsonrpc": "2.0", "method": "tools/list"}))
resp = websearch_mcp.handle_request(
{"jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": {"name": "web_search",
"arguments": {"query": "muse"}}},
search_fn=fake_search, fetch_fn=fake_fetch)
self.assertEqual(calls["search"], ("muse", 5, "auto"))
resp = websearch_mcp.handle_request(
{"jsonrpc": "2.0", "id": 4, "method": "tools/call",
"params": {"name": "web_fetch",
"arguments": {"url": "http://x/",
"char_limit": 3000}}},
search_fn=fake_search, fetch_fn=fake_fetch)
self.assertEqual(calls["fetch"], ("http://x/", 3000))
resp = websearch_mcp.handle_request(
{"jsonrpc": "2.0", "id": 5, "method": "tools/call",
"params": {"name": "nope", "arguments": {}}},
search_fn=fake_search, fetch_fn=fake_fetch)
self.assertEqual(resp["error"]["code"], -32602)
self.assertIsNone(websearch_mcp.handle_request(
{"jsonrpc": "2.0", "method": "tools/call",
"params": {"name": "web_search",
"arguments": {"query": "x"}}},
search_fn=fake_search, fetch_fn=fake_fetch))
self.assertIsNone(websearch_mcp.handle_request(
{"jsonrpc": "2.0", "method": "bogus"}))
resp = websearch_mcp.handle_request(
{"jsonrpc": "2.0", "id": 6, "method": "bogus"})
self.assertEqual(resp["error"]["code"], -32601)
def boom(*args, **kwargs):
raise RuntimeError("provider down")
resp = websearch_mcp.handle_request(
{"jsonrpc": "2.0", "id": 7, "method": "tools/call",
"params": {"name": "web_search",
"arguments": {"query": "x"}}},
search_fn=boom, fetch_fn=fake_fetch)
self.assertTrue(resp["result"]["isError"])
self.assertIn("provider down", resp["result"]["content"][0]["text"])
def test_main_loop_with_fake_search(self):
old_search = websearch_mcp.search
websearch_mcp.search = lambda query, limit=5, provider="auto": \
{"results": []}
old_in, old_out = sys.stdin, sys.stdout
sys.stdin = io.StringIO("\nbroken{\n" + json.dumps(
{"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {"name": "web_search",
"arguments": {"query": "q"}}}) + "\n" + json.dumps(
{"jsonrpc": "2.0", "id": 2, "method": "initialize",
"params": ["x"]}) + "\n")
sys.stdout = io.StringIO()
try:
websearch_mcp.main()
out = sys.stdout.getvalue()
finally:
sys.stdin, sys.stdout = old_in, old_out
websearch_mcp.search = old_search
got = [json.loads(ln) for ln in out.splitlines()]
self.assertEqual(got[0]["error"]["code"], -32700)
self.assertFalse(got[1]["result"]["isError"])
self.assertEqual(got[2]["error"]["code"], -32603)
def test_flat_copy_fallback_import(self):
orig_backend = sys.modules.get("cloud.websearch")
orig_mcp = sys.modules.get("cloud.websearch_mcp")
sys.modules.pop("cloud.websearch_mcp", None)
sys.modules["cloud.websearch"] = None
try:
mod = importlib.import_module("cloud.websearch_mcp")
self.assertEqual(mod.SERVER_NAME, "websearch")
finally:
sys.modules.pop("websearch", None)
if orig_backend is not None:
sys.modules["cloud.websearch"] = orig_backend
else:
sys.modules.pop("cloud.websearch", None)
if orig_mcp is not None:
sys.modules["cloud.websearch_mcp"] = orig_mcp
else:
sys.modules.pop("cloud.websearch_mcp", None)
cloud_dir = str(Path(__file__).resolve().parents[1])
while cloud_dir in sys.path:
sys.path.remove(cloud_dir)
if __name__ == "__main__":
unittest.main()