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。
生产代码零改动。
This commit is contained in:
host committed 2026-11-03 02:48:54 +00:00
1 parent 81a58daca2
commit 576fd50ee3
3 files changed
+1293

No files matched your search

+721
View File
@@ -0,0 +1,721 @@
"""覆盖率 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()