649 lines
23 KiB
Python
649 lines
23 KiB
Python
"""Coverage batch v2 (D4b): cloud/server.py small handler clusters,
|
|
second half -- costs/relay, task lists, followups, threads, files,
|
|
browser helpers, peer messages, reviews, skills/apps/MCP lists.
|
|
|
|
Same stubbing discipline as D2-D4: the method under test is never
|
|
stubbed; host-CLI collaborators (_run_host_cli, _extract_*) are
|
|
stubbed only to drive the handler's own error mapping.
|
|
"""
|
|
|
|
import os
|
|
import socket
|
|
import sqlite3
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import unittest
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest import mock
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(REPO_ROOT))
|
|
|
|
from cloud.server import (
|
|
CloudControlPlaneHandler,
|
|
_auth_store,
|
|
init_db,
|
|
)
|
|
from cloud.sub2api import Sub2apiError as _Sub2apiError
|
|
|
|
|
|
def _handler(db_path, upload_dir=None, tenants_root=None):
|
|
h = CloudControlPlaneHandler.__new__(CloudControlPlaneHandler)
|
|
h.server = SimpleNamespace(db_path=db_path, upload_dir=upload_dir,
|
|
tenants_root=tenants_root)
|
|
sent = []
|
|
h._send_json = lambda status, obj, extra_headers=None: sent.append(
|
|
(status, obj))
|
|
h.sent = sent
|
|
return h
|
|
|
|
|
|
def _conn(db_path):
|
|
c = sqlite3.connect(db_path)
|
|
c.row_factory = sqlite3.Row
|
|
return c
|
|
|
|
|
|
class _ProxyConn:
|
|
def __init__(self, real, fail_select_substr=None, fail_exc=None):
|
|
self._real = real
|
|
self._fail_select_substr = fail_select_substr
|
|
self._fail_exc = fail_exc
|
|
|
|
def execute(self, sql, params=()):
|
|
text = sql.strip()
|
|
if (self._fail_select_substr and text.startswith("SELECT")
|
|
and self._fail_select_substr in text):
|
|
raise self._fail_exc
|
|
return self._real.execute(sql, params)
|
|
|
|
def __getattr__(self, name):
|
|
return getattr(self._real, name)
|
|
|
|
|
|
class _Base(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
self.db_path = os.path.join(self.tmp.name, "test.db")
|
|
init_db(self.db_path)
|
|
self.upload_dir = os.path.join(self.tmp.name, "uploads")
|
|
os.makedirs(self.upload_dir, exist_ok=True)
|
|
|
|
def tearDown(self):
|
|
self.tmp.cleanup()
|
|
|
|
def _h(self, body=None, err=None):
|
|
h = _handler(self.db_path, upload_dir=self.upload_dir,
|
|
tenants_root=self.tmp.name)
|
|
h._read_json = lambda: (body, err)
|
|
return h
|
|
|
|
def _project(self, pid="p1", owner="u1", repo_url=None):
|
|
c = _conn(self.db_path)
|
|
c.execute(
|
|
"INSERT INTO projects(id, name, owner_id, created_at,"
|
|
" repo_url, auto_pr) VALUES(?, ?, ?, ?, ?, 0)",
|
|
(pid, "P", owner, time.time(), repo_url))
|
|
c.commit()
|
|
c.close()
|
|
with _auth_store(self.db_path) as store:
|
|
store.create_project(pid, owner)
|
|
|
|
def _member(self, pid, uid, role):
|
|
c = _conn(self.db_path)
|
|
c.execute(
|
|
"INSERT OR REPLACE INTO project_members(project_id,"
|
|
" user_id, role) VALUES(?, ?, ?)", (pid, uid, role))
|
|
c.commit()
|
|
c.close()
|
|
|
|
def _task(self, tid="t1", pid="p1", status="completed",
|
|
created_by="u1", **extra):
|
|
cols = ["id", "project_id", "prompt", "status", "created_by",
|
|
"created_at", "updated_at"]
|
|
vals = [tid, pid, "do work", status, created_by, time.time(),
|
|
time.time()]
|
|
for key, val in extra.items():
|
|
cols.append(key)
|
|
vals.append(val)
|
|
c = _conn(self.db_path)
|
|
c.execute(
|
|
"INSERT INTO tasks(%s) VALUES(%s)"
|
|
% (", ".join(cols), ", ".join("?" * len(cols))), vals)
|
|
c.commit()
|
|
c.close()
|
|
|
|
def _last(self, h):
|
|
self.assertTrue(h.sent, "handler sent no response")
|
|
return h.sent[-1]
|
|
|
|
|
|
class _ClosingConn:
|
|
def __init__(self):
|
|
self.conn = mock.Mock()
|
|
self.conn.close.side_effect = RuntimeError("close down")
|
|
|
|
|
|
class CostRelayTestCase(_Base):
|
|
def test_kb_search_q_required(self):
|
|
h = self._h()
|
|
h._kb_search("p1", {})
|
|
code, payload = self._last(h)
|
|
self.assertEqual(code, 400)
|
|
self.assertIn("q required", payload["error"])
|
|
|
|
def test_kb_search_bad_top_k(self):
|
|
h = self._h()
|
|
h._kb_search("p1", {"q": ["x"], "top_k": ["abc"]})
|
|
self.assertEqual(self._last(h)[0], 400)
|
|
|
|
def test_kb_search_close_failure_swallowed(self):
|
|
class _Store:
|
|
def __init__(self, db_path):
|
|
self.conn = _ClosingConn().conn
|
|
|
|
def search(self, project_id, q, top_k=5):
|
|
return []
|
|
h = self._h()
|
|
with mock.patch("cloud.handler_identity.KBStore", _Store):
|
|
h._kb_search("p1", {"q": ["x"]})
|
|
self.assertEqual(self._last(h)[0], 200)
|
|
|
|
def test_project_cost_bad_days(self):
|
|
h = self._h()
|
|
h._project_cost("p1", {"days": ["abc"]})
|
|
code, payload = self._last(h)
|
|
self.assertEqual(code, 400)
|
|
self.assertIn("days must be an integer", payload["error"])
|
|
|
|
def test_project_cost_close_failure_swallowed(self):
|
|
class _Store:
|
|
def __init__(self, db_path):
|
|
self._conn = mock.Mock()
|
|
self._conn.close.side_effect = RuntimeError("down")
|
|
|
|
def dual_column_report_by_project(self, project_id):
|
|
return {}
|
|
h = self._h()
|
|
with mock.patch("cloud.server.CostStore", _Store):
|
|
h._project_cost("p1", {})
|
|
self.assertEqual(self._last(h)[0], 200)
|
|
|
|
def test_task_cost_close_failure_swallowed(self):
|
|
self._project()
|
|
self._task()
|
|
class _Store:
|
|
def __init__(self, db_path):
|
|
self._conn = mock.Mock()
|
|
self._conn.close.side_effect = RuntimeError("down")
|
|
|
|
def dual_column_report_by_task(self, task_id):
|
|
return {}
|
|
h = self._h()
|
|
with mock.patch("cloud.server.CostStore", _Store):
|
|
h._task_cost("u1", "t1")
|
|
self.assertEqual(self._last(h)[0], 200)
|
|
|
|
def test_relay_models_sub2api_generic_error_502(self):
|
|
h = self._h()
|
|
with mock.patch.dict(os.environ, {"CLOUD_GATEWAY": "sub2api"}), \
|
|
mock.patch("cloud.server._validate_sub2api_key",
|
|
side_effect=RuntimeError("net down")):
|
|
h._relay_models("u1")
|
|
self.assertEqual(self._last(h)[0], 502)
|
|
|
|
def test_relay_models_sub2api_list_error_502(self):
|
|
h = self._h()
|
|
with mock.patch.dict(os.environ, {"CLOUD_GATEWAY": "sub2api"}), \
|
|
mock.patch("cloud.server._validate_sub2api_key",
|
|
return_value=("ok", "key", "sub", "kid")), \
|
|
mock.patch("cloud.handler_identity._list_sub2api_models",
|
|
side_effect=RuntimeError("gw down")):
|
|
h._relay_models("u1")
|
|
self.assertEqual(self._last(h)[0], 502)
|
|
|
|
def test_models_overview_no_memberships_200(self):
|
|
self._project()
|
|
h = self._h()
|
|
h._models_overview("u9")
|
|
code, payload = self._last(h)
|
|
self.assertEqual(code, 200)
|
|
self.assertEqual(payload["model_stats"], [])
|
|
|
|
def test_models_overview_close_failure_swallowed(self):
|
|
self._project()
|
|
conn = mock.Mock()
|
|
conn.execute.return_value.fetchall.return_value = []
|
|
conn.close.side_effect = RuntimeError("down")
|
|
|
|
class _Store:
|
|
def __init__(self, db_path):
|
|
self._conn = conn
|
|
h = self._h()
|
|
with mock.patch("cloud.server.CostStore", _Store):
|
|
h._models_overview("u1")
|
|
self.assertEqual(self._last(h)[0], 200)
|
|
|
|
|
|
class ListsFollowupsTestCase(_Base):
|
|
def test_list_tasks_project_required(self):
|
|
h = self._h()
|
|
h._list_tasks("u1", {})
|
|
code, payload = self._last(h)
|
|
self.assertEqual(code, 400)
|
|
self.assertIn("project_id required", payload["error"])
|
|
|
|
def test_list_tasks_executor_filter_skips(self):
|
|
self._project()
|
|
self._task()
|
|
h = self._h()
|
|
h._list_tasks("u1", {"project_id": ["p1"],
|
|
"executor": ["docker"]})
|
|
code, payload = self._last(h)
|
|
self.assertEqual(code, 200)
|
|
self.assertEqual(payload["tasks"], [])
|
|
|
|
def test_goal_convert_queued_none(self):
|
|
self._project()
|
|
self._task(status="completed")
|
|
h = self._h()
|
|
self.assertIsNone(h._goal_convert_queued("t1", "做目标"))
|
|
|
|
def test_goal_revive_terminal_none(self):
|
|
self._project()
|
|
self._task(status="completed", resume_message="old")
|
|
h = self._h()
|
|
self.assertIsNone(h._goal_revive_terminal("t1", "做目标"))
|
|
|
|
def test_task_agents_remote_host_200(self):
|
|
self._project()
|
|
self._task(worktree_host="other-host")
|
|
h = self._h()
|
|
h._task_agents("u1", "t1")
|
|
code, payload = self._last(h)
|
|
self.assertEqual(code, 200)
|
|
self.assertEqual(payload["agents"], [])
|
|
|
|
def test_create_followup_convert_race_409(self):
|
|
self._project()
|
|
self._task(status="queued")
|
|
h = self._h(body={"body": "/goal 做目标"})
|
|
h._goal_convert_queued = lambda tid, target: None
|
|
h._create_followup("u1", "t1")
|
|
code, payload = self._last(h)
|
|
self.assertEqual(code, 409)
|
|
self.assertIn("状态已变化", payload["error"])
|
|
|
|
def test_create_followup_revive_race_409(self):
|
|
self._project()
|
|
self._task(status="completed")
|
|
h = self._h(body={"body": "/goal 做目标"})
|
|
h._goal_revive_terminal = lambda tid, target: None
|
|
h._create_followup("u1", "t1")
|
|
code, payload = self._last(h)
|
|
self.assertEqual(code, 409)
|
|
self.assertIn("状态已变化", payload["error"])
|
|
|
|
def test_append_direct_trace_remote_host_returns(self):
|
|
h = self._h()
|
|
h._append_direct_trace({"id": "t1", "project_id": "p1",
|
|
"worktree_host": "other-host"},
|
|
"fu1", "hi")
|
|
|
|
def test_append_direct_trace_tenant_dir(self):
|
|
tenant_dir = os.path.join(self.tmp.name, "p1", "work",
|
|
"host-traces")
|
|
os.makedirs(tenant_dir, exist_ok=True)
|
|
open(os.path.join(tenant_dir, "t1.jsonl"), "w").close()
|
|
h = self._h()
|
|
h._append_direct_trace({"id": "t1", "project_id": "p1"},
|
|
"fu1", "hello tenant")
|
|
with open(os.path.join(tenant_dir, "t1.jsonl")) as f:
|
|
content = f.read()
|
|
self.assertIn("followup-direct", content)
|
|
|
|
def test_append_direct_trace_write_failure_swallowed(self):
|
|
h = self._h()
|
|
with mock.patch("os.makedirs",
|
|
side_effect=OSError("ro fs")):
|
|
h._append_direct_trace({"id": "t1", "project_id": "p1"},
|
|
"fu1", "hi")
|
|
|
|
def test_list_followups_missing_404(self):
|
|
h = self._h()
|
|
h._list_followups("u1", "ghost")
|
|
self.assertEqual(self._last(h)[0], 404)
|
|
|
|
def test_list_followups_forbidden_403(self):
|
|
self._project()
|
|
self._task()
|
|
h = self._h()
|
|
h._list_followups("u9", "t1")
|
|
self.assertEqual(self._last(h)[0], 403)
|
|
|
|
def test_task_timeline_forbidden_403(self):
|
|
self._project()
|
|
self._task()
|
|
h = self._h()
|
|
h._task_timeline("u9", "t1")
|
|
self.assertEqual(self._last(h)[0], 403)
|
|
|
|
def test_task_files_200_cap(self):
|
|
self._project()
|
|
self._task()
|
|
worktree = os.path.join(self.tmp.name, "p1", "work",
|
|
"host-worktrees", "t1")
|
|
os.makedirs(worktree, exist_ok=True)
|
|
for i in range(201):
|
|
with open(os.path.join(worktree, "f%03d.txt" % i), "w") as f:
|
|
f.write("x")
|
|
h = self._h()
|
|
h._task_files("u1", "t1", {})
|
|
code, payload = self._last(h)
|
|
self.assertEqual(code, 200)
|
|
self.assertEqual(len(payload["entries"]), 200)
|
|
|
|
def test_browser_worktree_for_cloud_409(self):
|
|
self._project()
|
|
self._task(executor="cloud")
|
|
h = self._h()
|
|
worktree, err = h._browser_worktree_for("u1", "t1")
|
|
self.assertIsNone(worktree)
|
|
self.assertEqual(err[0], 409)
|
|
|
|
def test_browser_body_dict_none_and_bad(self):
|
|
h = self._h()
|
|
self.assertIsNone(h._browser_body_dict(None))
|
|
self.assertIsNone(h._browser_body_dict(b"not json"))
|
|
self.assertEqual(h._browser_body_dict({"a": 1}), {"a": 1})
|
|
|
|
def test_browser_input_worktree_error(self):
|
|
h = self._h()
|
|
h._browser_worktree_for = lambda *a: (None, (404, "nope"))
|
|
h._browser_input("u1", "t1", {})
|
|
self.assertEqual(self._last(h)[0], 404)
|
|
|
|
def test_browser_input_state_error(self):
|
|
h = self._h()
|
|
h._browser_worktree_for = lambda *a: (self.tmp.name, None)
|
|
with mock.patch("cloud.playwright_remote.read_state",
|
|
return_value=(None, (502, "no session"))):
|
|
h._browser_input("u1", "t1", {})
|
|
self.assertEqual(self._last(h)[0], 502)
|
|
|
|
def test_browser_input_write_os_error_503(self):
|
|
h = self._h()
|
|
h._browser_worktree_for = lambda *a: (self.tmp.name, None)
|
|
with mock.patch("cloud.playwright_remote.read_state",
|
|
return_value=({"url": ""}, None)), \
|
|
mock.patch("cloud.playwright_remote.effective_holder",
|
|
return_value="user"), \
|
|
mock.patch("cloud.playwright_remote.append_input",
|
|
side_effect=OSError("unreachable")):
|
|
h._browser_input("u1", "t1", {"type": "type", "text": "hi"})
|
|
code, payload = self._last(h)
|
|
self.assertEqual(code, 503)
|
|
self.assertIn("browser input unreachable", payload["error"])
|
|
|
|
def test_chat_history_forbidden_403(self):
|
|
self._project()
|
|
self._task()
|
|
h = self._h()
|
|
h._task_chat_history("u9", "t1")
|
|
self.assertEqual(self._last(h)[0], 403)
|
|
|
|
def test_chat_history_legacy_query_failure_none(self):
|
|
self._project()
|
|
self._task()
|
|
h = self._h()
|
|
h._trace_snapshot = lambda task: ([], None)
|
|
h._db = lambda: _ProxyConn(
|
|
_conn(self.db_path),
|
|
fail_select_substr="FROM audit_log",
|
|
fail_exc=RuntimeError("audit down"))
|
|
h._task_chat_history("u1", "t1")
|
|
code, payload = self._last(h)
|
|
self.assertEqual(code, 200)
|
|
self.assertIsNone(payload["legacy_continue_count"])
|
|
|
|
def test_peer_message_read_error(self):
|
|
h = self._h(err="bad json")
|
|
h._create_peer_message("u1", "t1")
|
|
self.assertEqual(self._last(h)[0], 400)
|
|
|
|
def test_peer_message_sender_forbidden_403(self):
|
|
self._project()
|
|
self._project(pid="p2", owner="u2")
|
|
self._task(tid="t1", pid="p1")
|
|
self._task(tid="t2", pid="p2", created_by="u2")
|
|
h = self._h(body={"from_task_id": "t2", "body": "hi"})
|
|
h._create_peer_message("u1", "t1")
|
|
self.assertEqual(self._last(h)[0], 403)
|
|
|
|
def test_peer_message_body_too_long(self):
|
|
self._project()
|
|
self._task(tid="t1", pid="p1")
|
|
self._task(tid="t2", pid="p1")
|
|
h = self._h(body={"from_task_id": "t2", "body": "x" * 20001})
|
|
h._create_peer_message("u1", "t1")
|
|
code, payload = self._last(h)
|
|
self.assertEqual(code, 400)
|
|
self.assertIn("body too long", payload["error"])
|
|
|
|
def test_list_peer_messages_missing_404(self):
|
|
h = self._h()
|
|
h._list_peer_messages("u1", "ghost")
|
|
self.assertEqual(self._last(h)[0], 404)
|
|
|
|
def test_create_review_read_error(self):
|
|
h = self._h(err="bad json")
|
|
h._create_review("u1", "t1")
|
|
self.assertEqual(self._last(h)[0], 400)
|
|
|
|
def test_update_review_findings_read_error(self):
|
|
h = self._h(err="bad json")
|
|
h._update_review_findings("u1", "t1", "r1")
|
|
self.assertEqual(self._last(h)[0], 400)
|
|
|
|
def test_update_review_findings_missing_404(self):
|
|
h = self._h(body={})
|
|
h._update_review_findings("u1", "ghost", "r1")
|
|
self.assertEqual(self._last(h)[0], 404)
|
|
|
|
|
|
class ThreadListsTestCase(_Base):
|
|
def test_thread_guard_docker_409(self):
|
|
h = self._h()
|
|
code, msg = h._thread_task_guard({"executor": "docker"})
|
|
self.assertEqual(code, 409)
|
|
|
|
def test_thread_guard_remote_host_409(self):
|
|
h = self._h()
|
|
code, msg = h._thread_task_guard(
|
|
{"executor": None, "worktree_host": "other-host"})
|
|
self.assertEqual(code, 409)
|
|
|
|
def test_list_threads_no_tenant_home_502(self):
|
|
h = self._h()
|
|
threads, home, cwd, err, provider = h._list_task_threads(
|
|
{"id": "t1", "project_id": None})
|
|
self.assertEqual(err[0], 502)
|
|
|
|
def test_list_threads_extract_none_502(self):
|
|
h = self._h()
|
|
with mock.patch("cloud.server._run_host_cli",
|
|
return_value=(True, "garbage")), \
|
|
mock.patch("cloud.server._extract_thread_list",
|
|
return_value=None):
|
|
threads, home, cwd, err, provider = h._list_task_threads(
|
|
{"id": "t1", "project_id": "p1"})
|
|
self.assertEqual(err[0], 502)
|
|
self.assertIn("no thread list", err[1])
|
|
|
|
def test_task_thread_guard_409(self):
|
|
self._project()
|
|
self._task(executor="docker")
|
|
h = self._h()
|
|
h._task_thread("u1", "t1", {})
|
|
self.assertEqual(self._last(h)[0], 409)
|
|
|
|
def test_task_thread_cli_failure_502(self):
|
|
self._project()
|
|
self._task()
|
|
h = self._h()
|
|
h._resolve_task_thread = lambda task: (
|
|
{"id": "th1"}, "/tmp", "/tmp", None, None)
|
|
with mock.patch("cloud.server._run_host_cli",
|
|
return_value=(False, "host down")):
|
|
h._task_thread("u1", "t1", {})
|
|
code, payload = self._last(h)
|
|
self.assertEqual(code, 502)
|
|
self.assertIn("host down", payload["error"])
|
|
|
|
def test_task_thread_no_thread_502(self):
|
|
self._project()
|
|
self._task()
|
|
h = self._h()
|
|
h._resolve_task_thread = lambda task: (
|
|
{"id": "th1"}, "/tmp", "/tmp", None, None)
|
|
with mock.patch("cloud.server._run_host_cli",
|
|
return_value=(True, "garbage")):
|
|
h._task_thread("u1", "t1", {})
|
|
code, payload = self._last(h)
|
|
self.assertEqual(code, 502)
|
|
self.assertIn("no thread", payload["error"])
|
|
|
|
def test_task_thread_mutate_task_error(self):
|
|
h = self._h()
|
|
h._task_thread_mutate("u1", "ghost", ["archive"],
|
|
"thread_archived", "detail")
|
|
self.assertEqual(self._last(h)[0], 404)
|
|
|
|
def test_task_thread_rename_read_error(self):
|
|
self._project()
|
|
self._task()
|
|
h = self._h(err="bad json")
|
|
h._task_thread_rename("u1", "t1")
|
|
self.assertEqual(self._last(h)[0], 400)
|
|
|
|
def test_task_thread_rename_name_too_long(self):
|
|
self._project()
|
|
self._task()
|
|
h = self._h(body={"name": "x" * 201})
|
|
h._task_thread_rename("u1", "t1")
|
|
code, payload = self._last(h)
|
|
self.assertEqual(code, 400)
|
|
self.assertIn("name too long", payload["error"])
|
|
|
|
def test_project_host_home_none(self):
|
|
h = self._h()
|
|
self.assertEqual(h._project_host_home(None), (None, None))
|
|
|
|
def _list_case(self, method):
|
|
h = self._h()
|
|
method(h, None)
|
|
return h
|
|
|
|
def test_skills_list_home_none_502(self):
|
|
h = self._h()
|
|
h._skills_list(None)
|
|
self.assertEqual(self._last(h)[0], 502)
|
|
|
|
def test_skills_list_cli_failure_502(self):
|
|
h = self._h()
|
|
with mock.patch("cloud.server._run_host_cli",
|
|
return_value=(False, "host down")):
|
|
h._skills_list("p1")
|
|
self.assertEqual(self._last(h)[0], 502)
|
|
|
|
def test_skills_list_no_list_502(self):
|
|
h = self._h()
|
|
with mock.patch("cloud.server._run_host_cli",
|
|
return_value=(True, "garbage")):
|
|
h._skills_list("p1")
|
|
self.assertEqual(self._last(h)[0], 502)
|
|
|
|
def test_apps_list_home_none_502(self):
|
|
h = self._h()
|
|
h._apps_list(None)
|
|
self.assertEqual(self._last(h)[0], 502)
|
|
|
|
def test_apps_list_cli_failure_502(self):
|
|
h = self._h()
|
|
with mock.patch("cloud.server._run_host_cli",
|
|
return_value=(False, "host down")):
|
|
h._apps_list("p1")
|
|
self.assertEqual(self._last(h)[0], 502)
|
|
|
|
def test_apps_list_no_list_502(self):
|
|
h = self._h()
|
|
with mock.patch("cloud.server._run_host_cli",
|
|
return_value=(True, "garbage")):
|
|
h._apps_list("p1")
|
|
self.assertEqual(self._last(h)[0], 502)
|
|
|
|
def test_mcp_list_home_none_502(self):
|
|
h = self._h()
|
|
h._mcp_servers_list(None)
|
|
self.assertEqual(self._last(h)[0], 502)
|
|
|
|
def test_mcp_list_cli_failure_502(self):
|
|
h = self._h()
|
|
with mock.patch("cloud.server._run_host_cli",
|
|
return_value=(False, "host down")):
|
|
h._mcp_servers_list("p1")
|
|
self.assertEqual(self._last(h)[0], 502)
|
|
|
|
def test_mcp_list_no_list_502(self):
|
|
h = self._h()
|
|
with mock.patch("cloud.server._run_host_cli",
|
|
return_value=(True, "garbage")):
|
|
h._mcp_servers_list("p1")
|
|
self.assertEqual(self._last(h)[0], 502)
|
|
|
|
def test_mcp_list_next_cursor(self):
|
|
h = self._h()
|
|
with mock.patch("cloud.server._run_host_cli",
|
|
return_value=(True, {"servers": [],
|
|
"nextCursor": "n1"})):
|
|
h._mcp_servers_list("p1")
|
|
code, payload = self._last(h)
|
|
self.assertEqual(code, 200)
|
|
self.assertEqual(payload["nextCursor"], "n1")
|
|
|
|
def test_mcp_reload_home_none_502(self):
|
|
h = self._h()
|
|
h._mcp_servers_reload("u1", None)
|
|
self.assertEqual(self._last(h)[0], 502)
|
|
|
|
def test_mcp_reload_cli_failure_502(self):
|
|
h = self._h()
|
|
with mock.patch("cloud.server._run_host_cli",
|
|
return_value=(False, "host down")):
|
|
h._mcp_servers_reload("u1", "p1")
|
|
self.assertEqual(self._last(h)[0], 502)
|
|
|
|
def test_mcp_oauth_login_read_error(self):
|
|
h = self._h(err="bad json")
|
|
h._mcp_server_oauth_login("u1", "p1", "srv")
|
|
self.assertEqual(self._last(h)[0], 400)
|
|
|
|
def test_mcp_oauth_login_timeout_argv(self):
|
|
h = self._h(body={"timeoutSecs": 30})
|
|
with mock.patch("cloud.server._run_host_cli",
|
|
return_value=(True, {"authorizationUrl": "u",
|
|
"loginId": "l"})) as cli:
|
|
h._mcp_server_oauth_login("u1", "p1", "srv")
|
|
argv = cli.call_args[0][0]
|
|
self.assertIn("--timeout-secs", argv)
|
|
self.assertEqual(self._last(h)[0], 200)
|
|
|
|
def test_mcp_oauth_login_cli_failure_502(self):
|
|
h = self._h(body={})
|
|
with mock.patch("cloud.server._run_host_cli",
|
|
return_value=(False, "host down")):
|
|
h._mcp_server_oauth_login("u1", "p1", "srv")
|
|
self.assertEqual(self._last(h)[0], 502)
|