mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-12 17:22:38 +02:00
test(e2e): add ClickUp MCP backend fake
This commit is contained in:
parent
4b347caefc
commit
e211028866
5 changed files with 209 additions and 0 deletions
|
|
@ -33,6 +33,9 @@ JIRA_CANARY_SUMMARY = "E2E Canary Jira Issue"
|
||||||
JIRA_CANARY_KEY = "E2E-101"
|
JIRA_CANARY_KEY = "E2E-101"
|
||||||
SLACK_CANARY_TOKEN = "SURFSENSE_E2E_CANARY_TOKEN_SLACK_001"
|
SLACK_CANARY_TOKEN = "SURFSENSE_E2E_CANARY_TOKEN_SLACK_001"
|
||||||
SLACK_CANARY_CHANNEL = "slack-e2e-canary"
|
SLACK_CANARY_CHANNEL = "slack-e2e-canary"
|
||||||
|
CLICKUP_CANARY_TOKEN = "SURFSENSE_E2E_CANARY_TOKEN_CLICKUP_001"
|
||||||
|
CLICKUP_CANARY_TITLE = "E2E Canary ClickUp Task"
|
||||||
|
CLICKUP_CANARY_TASK_ID = "fake-clickup-task-canary-001"
|
||||||
NO_RELEVANT_CONTENT_SENTINEL = "No relevant indexed content found."
|
NO_RELEVANT_CONTENT_SENTINEL = "No relevant indexed content found."
|
||||||
NO_RELEVANT_CONTENT_QUERY = "E2E_NO_RELEVANT_CONTENT_SMOKE"
|
NO_RELEVANT_CONTENT_QUERY = "E2E_NO_RELEVANT_CONTENT_SMOKE"
|
||||||
|
|
||||||
|
|
@ -122,6 +125,11 @@ class FakeChatLLM(BaseChatModel):
|
||||||
and SLACK_CANARY_TOKEN in latest_tool_text
|
and SLACK_CANARY_TOKEN in latest_tool_text
|
||||||
):
|
):
|
||||||
return f"Slack live tool content found: {SLACK_CANARY_TOKEN}"
|
return f"Slack live tool content found: {SLACK_CANARY_TOKEN}"
|
||||||
|
if (
|
||||||
|
latest_tool_name in {"clickup_search", "clickup_get_task"}
|
||||||
|
and CLICKUP_CANARY_TOKEN in latest_tool_text
|
||||||
|
):
|
||||||
|
return f"ClickUp live tool content found: {CLICKUP_CANARY_TOKEN}"
|
||||||
|
|
||||||
wants_gmail = _contains_any(
|
wants_gmail = _contains_any(
|
||||||
latest_human,
|
latest_human,
|
||||||
|
|
@ -170,6 +178,10 @@ class FakeChatLLM(BaseChatModel):
|
||||||
latest_human,
|
latest_human,
|
||||||
("slack", SLACK_CANARY_TOKEN),
|
("slack", SLACK_CANARY_TOKEN),
|
||||||
)
|
)
|
||||||
|
wants_clickup = _contains_any(
|
||||||
|
latest_human,
|
||||||
|
("clickup", CLICKUP_CANARY_TITLE),
|
||||||
|
)
|
||||||
has_gmail_evidence = (
|
has_gmail_evidence = (
|
||||||
GMAIL_CANARY_SUBJECT in prompt_text
|
GMAIL_CANARY_SUBJECT in prompt_text
|
||||||
or GMAIL_CANARY_MESSAGE_ID in prompt_text
|
or GMAIL_CANARY_MESSAGE_ID in prompt_text
|
||||||
|
|
@ -222,7 +234,14 @@ class FakeChatLLM(BaseChatModel):
|
||||||
or "C_FAKE_SLACK_CANARY" in prompt_text
|
or "C_FAKE_SLACK_CANARY" in prompt_text
|
||||||
or "T_FAKE_SLACK_TEAM" in prompt_text
|
or "T_FAKE_SLACK_TEAM" in prompt_text
|
||||||
)
|
)
|
||||||
|
has_clickup_evidence = (
|
||||||
|
CLICKUP_CANARY_TITLE in prompt_text
|
||||||
|
or CLICKUP_CANARY_TOKEN in prompt_text
|
||||||
|
or CLICKUP_CANARY_TASK_ID in prompt_text
|
||||||
|
)
|
||||||
|
|
||||||
|
if wants_clickup and has_clickup_evidence:
|
||||||
|
return f"ClickUp content found: {CLICKUP_CANARY_TOKEN}"
|
||||||
if wants_slack and has_slack_evidence:
|
if wants_slack and has_slack_evidence:
|
||||||
return f"Slack content found: {SLACK_CANARY_TOKEN}"
|
return f"Slack content found: {SLACK_CANARY_TOKEN}"
|
||||||
if wants_jira and has_jira_evidence:
|
if wants_jira and has_jira_evidence:
|
||||||
|
|
@ -254,6 +273,7 @@ class FakeChatLLM(BaseChatModel):
|
||||||
and not has_onedrive_evidence
|
and not has_onedrive_evidence
|
||||||
and not has_dropbox_evidence
|
and not has_dropbox_evidence
|
||||||
and not has_slack_evidence
|
and not has_slack_evidence
|
||||||
|
and not has_clickup_evidence
|
||||||
):
|
):
|
||||||
return f"Notion content found: {NOTION_CANARY_TOKEN}"
|
return f"Notion content found: {NOTION_CANARY_TOKEN}"
|
||||||
if (
|
if (
|
||||||
|
|
@ -267,6 +287,7 @@ class FakeChatLLM(BaseChatModel):
|
||||||
and not has_onedrive_evidence
|
and not has_onedrive_evidence
|
||||||
and not has_dropbox_evidence
|
and not has_dropbox_evidence
|
||||||
and not has_slack_evidence
|
and not has_slack_evidence
|
||||||
|
and not has_clickup_evidence
|
||||||
):
|
):
|
||||||
return f"Confluence content found: {CONFLUENCE_CANARY_TOKEN}"
|
return f"Confluence content found: {CONFLUENCE_CANARY_TOKEN}"
|
||||||
if (
|
if (
|
||||||
|
|
@ -280,6 +301,7 @@ class FakeChatLLM(BaseChatModel):
|
||||||
and not has_onedrive_evidence
|
and not has_onedrive_evidence
|
||||||
and not has_dropbox_evidence
|
and not has_dropbox_evidence
|
||||||
and not has_slack_evidence
|
and not has_slack_evidence
|
||||||
|
and not has_clickup_evidence
|
||||||
):
|
):
|
||||||
return f"Jira content found: {JIRA_CANARY_TOKEN}"
|
return f"Jira content found: {JIRA_CANARY_TOKEN}"
|
||||||
if (
|
if (
|
||||||
|
|
@ -293,6 +315,7 @@ class FakeChatLLM(BaseChatModel):
|
||||||
and not has_onedrive_evidence
|
and not has_onedrive_evidence
|
||||||
and not has_dropbox_evidence
|
and not has_dropbox_evidence
|
||||||
and not has_slack_evidence
|
and not has_slack_evidence
|
||||||
|
and not has_clickup_evidence
|
||||||
):
|
):
|
||||||
return f"Linear content found: {LINEAR_CANARY_TOKEN}"
|
return f"Linear content found: {LINEAR_CANARY_TOKEN}"
|
||||||
if (
|
if (
|
||||||
|
|
@ -306,6 +329,7 @@ class FakeChatLLM(BaseChatModel):
|
||||||
and not has_onedrive_evidence
|
and not has_onedrive_evidence
|
||||||
and not has_dropbox_evidence
|
and not has_dropbox_evidence
|
||||||
and not has_slack_evidence
|
and not has_slack_evidence
|
||||||
|
and not has_clickup_evidence
|
||||||
):
|
):
|
||||||
return f"Calendar content found: {CALENDAR_CANARY_TOKEN}"
|
return f"Calendar content found: {CALENDAR_CANARY_TOKEN}"
|
||||||
if (
|
if (
|
||||||
|
|
@ -318,6 +342,7 @@ class FakeChatLLM(BaseChatModel):
|
||||||
and not has_onedrive_evidence
|
and not has_onedrive_evidence
|
||||||
and not has_dropbox_evidence
|
and not has_dropbox_evidence
|
||||||
and not has_slack_evidence
|
and not has_slack_evidence
|
||||||
|
and not has_clickup_evidence
|
||||||
):
|
):
|
||||||
return f"Gmail content found: {GMAIL_CANARY_TOKEN}"
|
return f"Gmail content found: {GMAIL_CANARY_TOKEN}"
|
||||||
if (
|
if (
|
||||||
|
|
@ -331,6 +356,7 @@ class FakeChatLLM(BaseChatModel):
|
||||||
and not has_drive_evidence
|
and not has_drive_evidence
|
||||||
and not has_dropbox_evidence
|
and not has_dropbox_evidence
|
||||||
and not has_slack_evidence
|
and not has_slack_evidence
|
||||||
|
and not has_clickup_evidence
|
||||||
):
|
):
|
||||||
return f"OneDrive content found: {ONEDRIVE_CANARY_TOKEN}"
|
return f"OneDrive content found: {ONEDRIVE_CANARY_TOKEN}"
|
||||||
if (
|
if (
|
||||||
|
|
@ -344,6 +370,7 @@ class FakeChatLLM(BaseChatModel):
|
||||||
and not has_drive_evidence
|
and not has_drive_evidence
|
||||||
and not has_onedrive_evidence
|
and not has_onedrive_evidence
|
||||||
and not has_slack_evidence
|
and not has_slack_evidence
|
||||||
|
and not has_clickup_evidence
|
||||||
):
|
):
|
||||||
return f"Dropbox content found: {DROPBOX_CANARY_TOKEN}"
|
return f"Dropbox content found: {DROPBOX_CANARY_TOKEN}"
|
||||||
if (
|
if (
|
||||||
|
|
@ -356,6 +383,7 @@ class FakeChatLLM(BaseChatModel):
|
||||||
and not has_onedrive_evidence
|
and not has_onedrive_evidence
|
||||||
and not has_dropbox_evidence
|
and not has_dropbox_evidence
|
||||||
and not has_slack_evidence
|
and not has_slack_evidence
|
||||||
|
and not has_clickup_evidence
|
||||||
):
|
):
|
||||||
return f"Drive content found: {DRIVE_CANARY_TOKEN}"
|
return f"Drive content found: {DRIVE_CANARY_TOKEN}"
|
||||||
if (
|
if (
|
||||||
|
|
@ -369,8 +397,23 @@ class FakeChatLLM(BaseChatModel):
|
||||||
and not has_drive_evidence
|
and not has_drive_evidence
|
||||||
and not has_onedrive_evidence
|
and not has_onedrive_evidence
|
||||||
and not has_dropbox_evidence
|
and not has_dropbox_evidence
|
||||||
|
and not has_clickup_evidence
|
||||||
):
|
):
|
||||||
return f"Slack content found: {SLACK_CANARY_TOKEN}"
|
return f"Slack content found: {SLACK_CANARY_TOKEN}"
|
||||||
|
if (
|
||||||
|
has_clickup_evidence
|
||||||
|
and not has_confluence_evidence
|
||||||
|
and not has_jira_evidence
|
||||||
|
and not has_linear_evidence
|
||||||
|
and not has_notion_evidence
|
||||||
|
and not has_calendar_evidence
|
||||||
|
and not has_gmail_evidence
|
||||||
|
and not has_drive_evidence
|
||||||
|
and not has_onedrive_evidence
|
||||||
|
and not has_dropbox_evidence
|
||||||
|
and not has_slack_evidence
|
||||||
|
):
|
||||||
|
return f"ClickUp content found: {CLICKUP_CANARY_TOKEN}"
|
||||||
return NO_RELEVANT_CONTENT_SENTINEL
|
return NO_RELEVANT_CONTENT_SENTINEL
|
||||||
|
|
||||||
def _tool_call_message_for(self, messages: list[BaseMessage]) -> AIMessage | None:
|
def _tool_call_message_for(self, messages: list[BaseMessage]) -> AIMessage | None:
|
||||||
|
|
@ -493,6 +536,21 @@ class FakeChatLLM(BaseChatModel):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if latest_tool is None and _contains_any(
|
||||||
|
latest_human,
|
||||||
|
("clickup", CLICKUP_CANARY_TITLE),
|
||||||
|
):
|
||||||
|
return AIMessage(
|
||||||
|
content="",
|
||||||
|
tool_calls=[
|
||||||
|
{
|
||||||
|
"name": "clickup_search",
|
||||||
|
"args": {"query": CLICKUP_CANARY_TITLE, "limit": 5},
|
||||||
|
"id": "call_e2e_search_clickup_tasks",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _generate(
|
def _generate(
|
||||||
|
|
|
||||||
134
surfsense_backend/tests/e2e/fakes/clickup_module.py
Normal file
134
surfsense_backend/tests/e2e/fakes/clickup_module.py
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
"""Strict ClickUp MCP OAuth/tool fakes for Playwright E2E."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from tests.e2e.fakes import mcp_oauth_runtime, mcp_runtime
|
||||||
|
|
||||||
|
_FIXTURE_PATH = Path(__file__).parent / "fixtures" / "clickup_tasks.json"
|
||||||
|
|
||||||
|
_AUTHORIZATION_URL = "https://mcp.clickup.com/authorize"
|
||||||
|
_REGISTRATION_URL = "https://mcp.clickup.com/register"
|
||||||
|
_TOKEN_URL = "https://mcp.clickup.com/token"
|
||||||
|
_MCP_URL = "https://mcp.clickup.com/mcp"
|
||||||
|
|
||||||
|
_CLIENT_ID = "fake-clickup-mcp-client-id"
|
||||||
|
_CLIENT_SECRET = "fake-clickup-mcp-client-secret"
|
||||||
|
_ACCESS_TOKEN = "fake-clickup-mcp-access-token"
|
||||||
|
_REFRESH_TOKEN = "fake-clickup-mcp-refresh-token"
|
||||||
|
_OAUTH_CODE = "fake-clickup-oauth-code"
|
||||||
|
|
||||||
|
|
||||||
|
def _load_fixture() -> dict[str, Any]:
|
||||||
|
with _FIXTURE_PATH.open() as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
_FIXTURE = _load_fixture()
|
||||||
|
|
||||||
|
|
||||||
|
def _task_text(task: dict[str, Any]) -> str:
|
||||||
|
return (
|
||||||
|
f"{task['name']}\n"
|
||||||
|
f"id: {task['id']}\n"
|
||||||
|
f"workspace: {task['workspace_name']} ({task['workspace_id']})\n"
|
||||||
|
f"list: {task['list_name']}\n"
|
||||||
|
f"status: {task['status']}\n"
|
||||||
|
f"description: {task['description']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _list_tools() -> SimpleNamespace:
|
||||||
|
return SimpleNamespace(
|
||||||
|
tools=[
|
||||||
|
SimpleNamespace(
|
||||||
|
name="clickup_search",
|
||||||
|
description="Search ClickUp tasks visible to the authenticated user.",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"query": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Text to search for in ClickUp tasks.",
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Maximum number of tasks to return.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["query"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SimpleNamespace(
|
||||||
|
name="clickup_get_task",
|
||||||
|
description="Get a ClickUp task by id.",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"task_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ClickUp task id.",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["task_id"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _call_tool(
|
||||||
|
tool_name: str, arguments: dict[str, Any] | None = None
|
||||||
|
) -> SimpleNamespace:
|
||||||
|
arguments = arguments or {}
|
||||||
|
task = _FIXTURE["tasks"][0]
|
||||||
|
|
||||||
|
if tool_name == "clickup_search":
|
||||||
|
query = str(arguments.get("query", ""))
|
||||||
|
if query and task["name"].lower() not in query.lower():
|
||||||
|
raise ValueError(f"Unexpected ClickUp task query: {query!r}")
|
||||||
|
return SimpleNamespace(content=[SimpleNamespace(text=_task_text(task))])
|
||||||
|
|
||||||
|
if tool_name == "clickup_get_task":
|
||||||
|
task_id = arguments.get("task_id")
|
||||||
|
if task_id != task["id"]:
|
||||||
|
raise ValueError(f"Unexpected ClickUp task id: {task_id!r}")
|
||||||
|
return SimpleNamespace(content=[SimpleNamespace(text=_task_text(task))])
|
||||||
|
|
||||||
|
raise NotImplementedError(f"Unexpected ClickUp MCP tool call: {tool_name!r}")
|
||||||
|
|
||||||
|
|
||||||
|
def install(active_patches: list[Any]) -> None:
|
||||||
|
"""Register ClickUp MCP OAuth/tool handlers with the shared dispatchers."""
|
||||||
|
del active_patches
|
||||||
|
mcp_oauth_runtime.register_service(
|
||||||
|
mcp_url=_MCP_URL,
|
||||||
|
discovery_metadata={
|
||||||
|
"issuer": "https://mcp.clickup.com",
|
||||||
|
"authorization_endpoint": _AUTHORIZATION_URL,
|
||||||
|
"token_endpoint": _TOKEN_URL,
|
||||||
|
"registration_endpoint": _REGISTRATION_URL,
|
||||||
|
"code_challenge_methods_supported": ["S256"],
|
||||||
|
"grant_types_supported": ["authorization_code", "refresh_token"],
|
||||||
|
"response_types_supported": ["code"],
|
||||||
|
},
|
||||||
|
client_id=_CLIENT_ID,
|
||||||
|
client_secret=_CLIENT_SECRET,
|
||||||
|
token_endpoint=_TOKEN_URL,
|
||||||
|
registration_endpoint=_REGISTRATION_URL,
|
||||||
|
oauth_code=_OAUTH_CODE,
|
||||||
|
access_token=_ACCESS_TOKEN,
|
||||||
|
refresh_token=_REFRESH_TOKEN,
|
||||||
|
scope="read write",
|
||||||
|
redirect_uri_substring="/api/v1/auth/mcp/clickup/connector/callback",
|
||||||
|
)
|
||||||
|
mcp_runtime.register(
|
||||||
|
url=_MCP_URL,
|
||||||
|
expected_bearer=_ACCESS_TOKEN,
|
||||||
|
list_tools=_list_tools,
|
||||||
|
call_tool=_call_tool,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"id": "fake-clickup-task-canary-001",
|
||||||
|
"name": "E2E Canary ClickUp Task",
|
||||||
|
"list_name": "SurfSense E2E ClickUp List",
|
||||||
|
"workspace_id": "fake-clickup-workspace-001",
|
||||||
|
"workspace_name": "SurfSense E2E ClickUp Workspace",
|
||||||
|
"status": "open",
|
||||||
|
"description": "Canary task body containing SURFSENSE_E2E_CANARY_TOKEN_CLICKUP_001"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -109,6 +109,7 @@ from unittest.mock import patch # noqa: E402
|
||||||
|
|
||||||
from app.app import app # noqa: E402
|
from app.app import app # noqa: E402
|
||||||
from tests.e2e.fakes import ( # noqa: E402
|
from tests.e2e.fakes import ( # noqa: E402
|
||||||
|
clickup_module as _fake_clickup_module,
|
||||||
confluence_indexer as _fake_confluence_indexer,
|
confluence_indexer as _fake_confluence_indexer,
|
||||||
confluence_oauth as _fake_confluence_oauth,
|
confluence_oauth as _fake_confluence_oauth,
|
||||||
dropbox_api as _fake_dropbox_api,
|
dropbox_api as _fake_dropbox_api,
|
||||||
|
|
@ -200,6 +201,7 @@ _fake_dropbox_api.install(_active_patches)
|
||||||
_fake_notion_module.install(_active_patches)
|
_fake_notion_module.install(_active_patches)
|
||||||
_fake_linear_module.install(_active_patches)
|
_fake_linear_module.install(_active_patches)
|
||||||
_fake_jira_module.install(_active_patches)
|
_fake_jira_module.install(_active_patches)
|
||||||
|
_fake_clickup_module.install(_active_patches)
|
||||||
_fake_mcp_runtime.install(_active_patches)
|
_fake_mcp_runtime.install(_active_patches)
|
||||||
_fake_mcp_oauth_runtime.install(_active_patches)
|
_fake_mcp_oauth_runtime.install(_active_patches)
|
||||||
_fake_slack_module.install(_active_patches)
|
_fake_slack_module.install(_active_patches)
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ from unittest.mock import patch # noqa: E402
|
||||||
|
|
||||||
from app.celery_app import celery_app # noqa: E402
|
from app.celery_app import celery_app # noqa: E402
|
||||||
from tests.e2e.fakes import ( # noqa: E402
|
from tests.e2e.fakes import ( # noqa: E402
|
||||||
|
clickup_module as _fake_clickup_module,
|
||||||
confluence_indexer as _fake_confluence_indexer,
|
confluence_indexer as _fake_confluence_indexer,
|
||||||
confluence_oauth as _fake_confluence_oauth,
|
confluence_oauth as _fake_confluence_oauth,
|
||||||
dropbox_api as _fake_dropbox_api,
|
dropbox_api as _fake_dropbox_api,
|
||||||
|
|
@ -184,6 +185,7 @@ _fake_dropbox_api.install(_active_patches)
|
||||||
_fake_notion_module.install(_active_patches)
|
_fake_notion_module.install(_active_patches)
|
||||||
_fake_linear_module.install(_active_patches)
|
_fake_linear_module.install(_active_patches)
|
||||||
_fake_jira_module.install(_active_patches)
|
_fake_jira_module.install(_active_patches)
|
||||||
|
_fake_clickup_module.install(_active_patches)
|
||||||
_fake_mcp_runtime.install(_active_patches)
|
_fake_mcp_runtime.install(_active_patches)
|
||||||
_fake_mcp_oauth_runtime.install(_active_patches)
|
_fake_mcp_oauth_runtime.install(_active_patches)
|
||||||
_fake_slack_module.install(_active_patches)
|
_fake_slack_module.install(_active_patches)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue