mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-20 00:14:50 +00:00
refactor: rename the application package to docsgpt
The backend import package is now docsgpt, the name it will carry on PyPI; application was far too generic to install into anyone's site-packages. git mv plus a mechanical rewrite of every import, dotted string and path reference: 734 Python files, the compose files, Dockerfile, workflows, docs, setup scripts, devcontainer, k8s manifests, vscode config, pytest and coverage config, .gitignore. Behaviour is unchanged. Kept for one release: - A top-level application package whose meta-path finder resolves application.x.y to the already-imported docsgpt.x.y object, so old imports and entry points (celery -A application.app.celery, uvicorn application.asgi:asgi_app) keep working with a FutureWarning. - Celery registers every application.* task name as an alias of its docsgpt.* task on start-up, so messages queued by the previous release still run. The redbeat key prefix moves to redbeat:docsgpt:v2: so schedule entries the previous release wrote are left unread instead of firing twice. The backend image builds from the repository root (docker build -f docsgpt/Dockerfile .) so it can ship the alias package; a root .dockerignore allow-lists docsgpt/ and application/ and keeps caches, local data, .env files, the sample index files and the Dockerfile out. Compose and the image workflows point at the new context.
This commit is contained in:
1 parent
3e7f1f91b4
commit
574f96341e
984 files changed
+9847
-9682
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
from application.agents.agent_creator import AgentCreator
|
||||
from application.agents.classic_agent import ClassicAgent
|
||||
from docsgpt.agents.agent_creator import AgentCreator
|
||||
from docsgpt.agents.classic_agent import ClassicAgent
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from application.agents.agentic_agent import AgenticAgent
|
||||
from docsgpt.agents.agentic_agent import AgenticAgent
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def _no_tools(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tool_executor.ToolExecutor.get_tools",
|
||||
"docsgpt.agents.tool_executor.ToolExecutor.get_tools",
|
||||
lambda self: {},
|
||||
)
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Tests for application/agents/tools/api_body_serializer.py"""
|
||||
"""Tests for docsgpt/agents/tools/api_body_serializer.py"""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from application.agents.tools.api_body_serializer import (
|
||||
from docsgpt.agents.tools.api_body_serializer import (
|
||||
ContentType,
|
||||
RequestBodySerializer,
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests for application/agents/tools/api_tool.py"""
|
||||
"""Tests for docsgpt/agents/tools/api_tool.py"""
|
||||
|
||||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
@@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from application.agents.tools.api_tool import APITool
|
||||
from docsgpt.agents.tools.api_tool import APITool
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -45,7 +45,7 @@ class TestAPIToolInit:
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestMakeApiCall:
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_successful_get(self, mock_pinned, tool):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
@@ -60,7 +60,7 @@ class TestMakeApiCall:
|
||||
assert result["data"] == {"result": "ok"}
|
||||
assert result["message"] == "API call successful."
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_successful_post(self, mock_pinned, post_tool):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 201
|
||||
@@ -73,9 +73,9 @@ class TestMakeApiCall:
|
||||
|
||||
assert result["status_code"] == 201
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_ssrf_blocked(self, mock_pinned, tool):
|
||||
from application.security.safe_url import UnsafeUserUrlError
|
||||
from docsgpt.security.safe_url import UnsafeUserUrlError
|
||||
|
||||
mock_pinned.side_effect = UnsafeUserUrlError("blocked")
|
||||
|
||||
@@ -84,7 +84,7 @@ class TestMakeApiCall:
|
||||
assert result["status_code"] is None
|
||||
assert "URL validation error" in result["message"]
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_timeout_error(self, mock_pinned, tool):
|
||||
mock_pinned.side_effect = requests.exceptions.Timeout()
|
||||
|
||||
@@ -93,7 +93,7 @@ class TestMakeApiCall:
|
||||
assert result["status_code"] is None
|
||||
assert "timeout" in result["message"].lower()
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_connection_error(self, mock_pinned, tool):
|
||||
mock_pinned.side_effect = requests.exceptions.ConnectionError("refused")
|
||||
|
||||
@@ -102,7 +102,7 @@ class TestMakeApiCall:
|
||||
assert result["status_code"] is None
|
||||
assert "Connection error" in result["message"]
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_http_error(self, mock_pinned, tool):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 404
|
||||
@@ -126,7 +126,7 @@ class TestMakeApiCall:
|
||||
assert result["status_code"] is None
|
||||
assert "Unsupported" in result["message"]
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_put_method(self, mock_pinned):
|
||||
tool = APITool(config={"url": "https://example.com/item/1", "method": "PUT"})
|
||||
mock_resp = MagicMock()
|
||||
@@ -139,7 +139,7 @@ class TestMakeApiCall:
|
||||
result = tool.execute_action("update", name="new")
|
||||
assert result["status_code"] == 200
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_delete_method(self, mock_pinned):
|
||||
tool = APITool(config={"url": "https://example.com/item/1", "method": "DELETE"})
|
||||
mock_resp = MagicMock()
|
||||
@@ -151,7 +151,7 @@ class TestMakeApiCall:
|
||||
result = tool.execute_action("delete")
|
||||
assert result["status_code"] == 204
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_patch_method(self, mock_pinned):
|
||||
tool = APITool(config={"url": "https://example.com/item/1", "method": "PATCH"})
|
||||
mock_resp = MagicMock()
|
||||
@@ -164,7 +164,7 @@ class TestMakeApiCall:
|
||||
result = tool.execute_action("patch", field="val")
|
||||
assert result["status_code"] == 200
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_head_method(self, mock_pinned):
|
||||
tool = APITool(config={"url": "https://example.com", "method": "HEAD"})
|
||||
mock_resp = MagicMock()
|
||||
@@ -176,7 +176,7 @@ class TestMakeApiCall:
|
||||
result = tool.execute_action("check")
|
||||
assert result["status_code"] == 200
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_options_method(self, mock_pinned):
|
||||
tool = APITool(config={"url": "https://example.com", "method": "OPTIONS"})
|
||||
mock_resp = MagicMock()
|
||||
@@ -191,7 +191,7 @@ class TestMakeApiCall:
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestPathParamSubstitution:
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_path_params_substituted(self, mock_pinned):
|
||||
tool = APITool(
|
||||
config={
|
||||
|
||||
@@ -2,14 +2,14 @@ from contextlib import contextmanager
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
from application.agents.classic_agent import ClassicAgent
|
||||
from application.llm.anthropic import AnthropicLLM
|
||||
from application.llm.docsgpt_provider import DocsGPTAPILLM
|
||||
from application.llm.google_ai import GoogleLLM
|
||||
from application.llm.groq import GroqLLM
|
||||
from application.llm.novita import NovitaLLM
|
||||
from application.llm.open_router import OpenRouterLLM
|
||||
from application.llm.openai import OpenAILLM
|
||||
from docsgpt.agents.classic_agent import ClassicAgent
|
||||
from docsgpt.llm.anthropic import AnthropicLLM
|
||||
from docsgpt.llm.docsgpt_provider import DocsGPTAPILLM
|
||||
from docsgpt.llm.google_ai import GoogleLLM
|
||||
from docsgpt.llm.groq import GroqLLM
|
||||
from docsgpt.llm.novita import NovitaLLM
|
||||
from docsgpt.llm.open_router import OpenRouterLLM
|
||||
from docsgpt.llm.openai import OpenAILLM
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -229,7 +229,7 @@ class TestBaseAgentTools:
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
):
|
||||
from application.storage.db.repositories.user_tools import UserToolsRepository
|
||||
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
|
||||
|
||||
repo = UserToolsRepository(pg_conn)
|
||||
repo.create(user_id="test_user", name="tool1", status=True)
|
||||
@@ -240,13 +240,13 @@ class TestBaseAgentTools:
|
||||
yield pg_conn
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tool_executor.db_readonly", _use_pg_conn
|
||||
"docsgpt.agents.tool_executor.db_readonly", _use_pg_conn
|
||||
)
|
||||
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
tools = agent._get_user_tools("test_user")
|
||||
|
||||
from application.agents.default_tools import loaded_default_tools
|
||||
from docsgpt.agents.default_tools import loaded_default_tools
|
||||
|
||||
assert len(tools) == 2 + len(loaded_default_tools())
|
||||
assert "0" in tools
|
||||
@@ -263,7 +263,7 @@ class TestBaseAgentTools:
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
):
|
||||
from application.storage.db.repositories.user_tools import UserToolsRepository
|
||||
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
|
||||
|
||||
repo = UserToolsRepository(pg_conn)
|
||||
repo.create(user_id="test_user", name="tool1", status=True)
|
||||
@@ -274,13 +274,13 @@ class TestBaseAgentTools:
|
||||
yield pg_conn
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tool_executor.db_readonly", _use_pg_conn
|
||||
"docsgpt.agents.tool_executor.db_readonly", _use_pg_conn
|
||||
)
|
||||
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
tools = agent._get_user_tools("test_user")
|
||||
|
||||
from application.agents.default_tools import loaded_default_tools
|
||||
from docsgpt.agents.default_tools import loaded_default_tools
|
||||
|
||||
assert len(tools) == 1 + len(loaded_default_tools())
|
||||
names = {t["name"] for t in tools.values()}
|
||||
@@ -295,8 +295,8 @@ class TestBaseAgentTools:
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
):
|
||||
from application.storage.db.repositories.agents import AgentsRepository
|
||||
from application.storage.db.repositories.user_tools import UserToolsRepository
|
||||
from docsgpt.storage.db.repositories.agents import AgentsRepository
|
||||
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
|
||||
|
||||
tool_row = UserToolsRepository(pg_conn).create(
|
||||
user_id="alice", name="api_tool"
|
||||
@@ -316,13 +316,13 @@ class TestBaseAgentTools:
|
||||
yield pg_conn
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tool_executor.db_readonly", _use_pg_conn
|
||||
"docsgpt.agents.tool_executor.db_readonly", _use_pg_conn
|
||||
)
|
||||
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
tools = agent._get_tools("api_key_123")
|
||||
|
||||
from application.agents.default_tools import loaded_default_tools
|
||||
from docsgpt.agents.default_tools import loaded_default_tools
|
||||
|
||||
# Agent-bound: exactly agents.tools, no defaults.
|
||||
assert set(tools) == {tool_id}
|
||||
@@ -573,7 +573,7 @@ class TestBaseAgentToolExecution:
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
from application.agents.tool_executor import PERSISTED_RESULT_MAX_LEN
|
||||
from docsgpt.agents.tool_executor import PERSISTED_RESULT_MAX_LEN
|
||||
|
||||
agent.tool_calls = [
|
||||
{
|
||||
@@ -833,7 +833,7 @@ class TestCalculateContextTokens:
|
||||
messages = [{"role": "user", "content": "hello"}]
|
||||
|
||||
with patch(
|
||||
"application.api.answer.services.compression.token_counter.TokenCounter"
|
||||
"docsgpt.api.answer.services.compression.token_counter.TokenCounter"
|
||||
) as MockTC:
|
||||
MockTC.count_message_tokens.return_value = 42
|
||||
result = agent._calculate_current_context_tokens(messages)
|
||||
@@ -862,7 +862,7 @@ class TestCheckContextLimit:
|
||||
|
||||
with patch.object(agent, "_calculate_current_context_tokens", return_value=100):
|
||||
with patch(
|
||||
"application.core.model_utils.get_token_limit", return_value=10000
|
||||
"docsgpt.core.model_utils.get_token_limit", return_value=10000
|
||||
):
|
||||
result = agent._check_context_limit(messages)
|
||||
assert result is False
|
||||
@@ -879,7 +879,7 @@ class TestCheckContextLimit:
|
||||
# threshold = 10000 * 0.8 = 8000; tokens = 8001 → True
|
||||
with patch.object(agent, "_calculate_current_context_tokens", return_value=8001):
|
||||
with patch(
|
||||
"application.core.model_utils.get_token_limit", return_value=10000
|
||||
"docsgpt.core.model_utils.get_token_limit", return_value=10000
|
||||
):
|
||||
result = agent._check_context_limit(messages)
|
||||
assert result is True
|
||||
@@ -913,7 +913,7 @@ class TestValidateContextSize:
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
with patch.object(agent, "_calculate_current_context_tokens", return_value=10000):
|
||||
with patch(
|
||||
"application.core.model_utils.get_token_limit", return_value=10000
|
||||
"docsgpt.core.model_utils.get_token_limit", return_value=10000
|
||||
):
|
||||
# Should not raise
|
||||
agent._validate_context_size([{"role": "user", "content": "x"}])
|
||||
@@ -925,7 +925,7 @@ class TestValidateContextSize:
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
with patch.object(agent, "_calculate_current_context_tokens", return_value=100):
|
||||
with patch(
|
||||
"application.core.model_utils.get_token_limit", return_value=10000
|
||||
"docsgpt.core.model_utils.get_token_limit", return_value=10000
|
||||
):
|
||||
agent._validate_context_size([])
|
||||
assert agent.current_token_count == 100
|
||||
@@ -937,7 +937,7 @@ class TestValidateContextSize:
|
||||
# 8500 / 10000 = 85% → above 80% threshold but below 100%
|
||||
with patch.object(agent, "_calculate_current_context_tokens", return_value=8500):
|
||||
with patch(
|
||||
"application.core.model_utils.get_token_limit", return_value=10000
|
||||
"docsgpt.core.model_utils.get_token_limit", return_value=10000
|
||||
):
|
||||
agent._validate_context_size([])
|
||||
assert agent.current_token_count == 8500
|
||||
@@ -955,7 +955,7 @@ class TestTruncateTextMiddle:
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
with patch("application.utils.num_tokens_from_string", return_value=5):
|
||||
with patch("docsgpt.utils.num_tokens_from_string", return_value=5):
|
||||
result = agent._truncate_text_middle("short", max_tokens=100)
|
||||
assert result == "short"
|
||||
|
||||
@@ -968,7 +968,7 @@ class TestTruncateTextMiddle:
|
||||
def fake_tokens(text):
|
||||
return len(text) // 4
|
||||
|
||||
with patch("application.utils.num_tokens_from_string", side_effect=fake_tokens):
|
||||
with patch("docsgpt.utils.num_tokens_from_string", side_effect=fake_tokens):
|
||||
result = agent._truncate_text_middle(long_text, max_tokens=50)
|
||||
assert "[... content truncated to fit context limit ...]" in result
|
||||
assert len(result) < len(long_text)
|
||||
@@ -977,7 +977,7 @@ class TestTruncateTextMiddle:
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
with patch("application.utils.num_tokens_from_string", return_value=100):
|
||||
with patch("docsgpt.utils.num_tokens_from_string", return_value=100):
|
||||
result = agent._truncate_text_middle("some text", max_tokens=0)
|
||||
assert result == ""
|
||||
|
||||
@@ -1020,7 +1020,7 @@ class TestTruncateHistoryToFit:
|
||||
{"prompt": "q1", "response": "a1"},
|
||||
{"prompt": "q2", "response": "a2"},
|
||||
]
|
||||
with patch("application.utils.num_tokens_from_string", return_value=5):
|
||||
with patch("docsgpt.utils.num_tokens_from_string", return_value=5):
|
||||
result = agent._truncate_history_to_fit(history, 10000)
|
||||
assert len(result) == 2
|
||||
|
||||
@@ -1035,7 +1035,7 @@ class TestTruncateHistoryToFit:
|
||||
{"prompt": "new", "response": "new_ans"},
|
||||
]
|
||||
# Each message = 10 tokens (prompt + response), budget = 15 → only 1 fits
|
||||
with patch("application.utils.num_tokens_from_string", return_value=5):
|
||||
with patch("docsgpt.utils.num_tokens_from_string", return_value=5):
|
||||
result = agent._truncate_history_to_fit(history, 15)
|
||||
assert len(result) == 1
|
||||
assert result[0]["prompt"] == "new"
|
||||
@@ -1060,7 +1060,7 @@ class TestTruncateHistoryToFit:
|
||||
],
|
||||
}
|
||||
]
|
||||
with patch("application.utils.num_tokens_from_string", return_value=3):
|
||||
with patch("docsgpt.utils.num_tokens_from_string", return_value=3):
|
||||
result = agent._truncate_history_to_fit(history, 100)
|
||||
assert len(result) == 1
|
||||
|
||||
@@ -1080,8 +1080,8 @@ class TestBuildMessagesAdvanced:
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
with patch(
|
||||
"application.core.model_utils.get_token_limit", return_value=100000
|
||||
), patch("application.utils.num_tokens_from_string", return_value=10):
|
||||
"docsgpt.core.model_utils.get_token_limit", return_value=100000
|
||||
), patch("docsgpt.utils.num_tokens_from_string", return_value=10):
|
||||
messages = agent._build_messages("System prompt", "query")
|
||||
|
||||
system_content = messages[0]["content"]
|
||||
@@ -1099,8 +1099,8 @@ class TestBuildMessagesAdvanced:
|
||||
return len(text)
|
||||
|
||||
with patch(
|
||||
"application.core.model_utils.get_token_limit", return_value=200
|
||||
), patch("application.utils.num_tokens_from_string", side_effect=fake_tokens):
|
||||
"docsgpt.core.model_utils.get_token_limit", return_value=200
|
||||
), patch("docsgpt.utils.num_tokens_from_string", side_effect=fake_tokens):
|
||||
with patch.object(agent, "_truncate_text_middle", return_value="truncated"):
|
||||
with patch.object(agent, "_truncate_history_to_fit", return_value=[]):
|
||||
messages = agent._build_messages("sys", "A" * 500)
|
||||
@@ -1127,8 +1127,8 @@ class TestBuildMessagesAdvanced:
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
with patch(
|
||||
"application.core.model_utils.get_token_limit", return_value=100000
|
||||
), patch("application.utils.num_tokens_from_string", return_value=5):
|
||||
"docsgpt.core.model_utils.get_token_limit", return_value=100000
|
||||
), patch("docsgpt.utils.num_tokens_from_string", return_value=5):
|
||||
messages = agent._build_messages("sys", "q")
|
||||
|
||||
tool_msgs = [m for m in messages if m["role"] == "tool"]
|
||||
@@ -1391,7 +1391,7 @@ class TestBaseAgentContextBudget:
|
||||
def _agent_with_limit(params, monkeypatch, limit):
|
||||
agent = ClassicAgent(**params)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_token_limit",
|
||||
"docsgpt.core.model_utils.get_token_limit",
|
||||
lambda *a, **k: limit,
|
||||
)
|
||||
return agent
|
||||
@@ -1489,7 +1489,7 @@ class TestBaseAgentDocumentsInUserTurn:
|
||||
]
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_token_limit", lambda *a, **k: 1500
|
||||
"docsgpt.core.model_utils.get_token_limit", lambda *a, **k: 1500
|
||||
)
|
||||
messages = agent._build_messages("short system", "What is Python?")
|
||||
user = messages[-1]["content"]
|
||||
@@ -1531,7 +1531,7 @@ class TestBaseAgentDocumentBudgetOrdering:
|
||||
]
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_token_limit", lambda *a, **k: 4000
|
||||
"docsgpt.core.model_utils.get_token_limit", lambda *a, **k: 4000
|
||||
)
|
||||
huge_question = "please explain this in detail " * 900
|
||||
user = agent._build_messages("short system", huge_question)[-1]["content"]
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Tests for application/agents/tools/brave.py"""
|
||||
"""Tests for docsgpt/agents/tools/brave.py"""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from application.agents.tools.brave import BraveSearchTool
|
||||
from docsgpt.agents.tools.brave import BraveSearchTool
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -18,7 +18,7 @@ class TestBraveExecuteAction:
|
||||
with pytest.raises(ValueError, match="Unknown action"):
|
||||
tool.execute_action("invalid")
|
||||
|
||||
@patch("application.agents.tools.brave.requests.get")
|
||||
@patch("docsgpt.agents.tools.brave.requests.get")
|
||||
def test_web_search_success(self, mock_get, tool):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
@@ -34,7 +34,7 @@ class TestBraveExecuteAction:
|
||||
call_kwargs = mock_get.call_args
|
||||
assert call_kwargs[1]["headers"]["X-Subscription-Token"] == "test_api_key"
|
||||
|
||||
@patch("application.agents.tools.brave.requests.get")
|
||||
@patch("docsgpt.agents.tools.brave.requests.get")
|
||||
def test_web_search_failure(self, mock_get, tool):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 429
|
||||
@@ -45,7 +45,7 @@ class TestBraveExecuteAction:
|
||||
assert result["status_code"] == 429
|
||||
assert "failed" in result["message"].lower()
|
||||
|
||||
@patch("application.agents.tools.brave.requests.get")
|
||||
@patch("docsgpt.agents.tools.brave.requests.get")
|
||||
def test_image_search_success(self, mock_get, tool):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
@@ -57,7 +57,7 @@ class TestBraveExecuteAction:
|
||||
assert result["status_code"] == 200
|
||||
assert "results" in result
|
||||
|
||||
@patch("application.agents.tools.brave.requests.get")
|
||||
@patch("docsgpt.agents.tools.brave.requests.get")
|
||||
def test_image_search_failure(self, mock_get, tool):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 500
|
||||
@@ -67,7 +67,7 @@ class TestBraveExecuteAction:
|
||||
|
||||
assert result["status_code"] == 500
|
||||
|
||||
@patch("application.agents.tools.brave.requests.get")
|
||||
@patch("docsgpt.agents.tools.brave.requests.get")
|
||||
def test_count_capped_at_20(self, mock_get, tool):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
@@ -79,7 +79,7 @@ class TestBraveExecuteAction:
|
||||
params = mock_get.call_args[1]["params"]
|
||||
assert params["count"] == 20
|
||||
|
||||
@patch("application.agents.tools.brave.requests.get")
|
||||
@patch("docsgpt.agents.tools.brave.requests.get")
|
||||
def test_image_count_capped_at_100(self, mock_get, tool):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
@@ -91,7 +91,7 @@ class TestBraveExecuteAction:
|
||||
params = mock_get.call_args[1]["params"]
|
||||
assert params["count"] == 100
|
||||
|
||||
@patch("application.agents.tools.brave.requests.get")
|
||||
@patch("docsgpt.agents.tools.brave.requests.get")
|
||||
def test_freshness_param(self, mock_get, tool):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
@@ -103,7 +103,7 @@ class TestBraveExecuteAction:
|
||||
params = mock_get.call_args[1]["params"]
|
||||
assert params["freshness"] == "pd"
|
||||
|
||||
@patch("application.agents.tools.brave.requests.get")
|
||||
@patch("docsgpt.agents.tools.brave.requests.get")
|
||||
def test_offset_capped(self, mock_get, tool):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Tests for application/agents/workflows/cel_evaluator.py"""
|
||||
"""Tests for docsgpt/agents/workflows/cel_evaluator.py"""
|
||||
|
||||
import pytest
|
||||
|
||||
from application.agents.workflows.cel_evaluator import (
|
||||
from docsgpt.agents.workflows.cel_evaluator import (
|
||||
CelEvaluationError,
|
||||
_convert_value,
|
||||
build_activation,
|
||||
@@ -232,7 +232,7 @@ class TestCelErrorMessages:
|
||||
@pytest.mark.unit
|
||||
def test_long_error_is_truncated(self):
|
||||
"""Backstop for celpy messages that survive activation-stripping."""
|
||||
from application.agents.workflows.cel_evaluator import _summarize_cel_error
|
||||
from docsgpt.agents.workflows.cel_evaluator import _summarize_cel_error
|
||||
|
||||
summary = _summarize_cel_error(Exception("word " * 200))
|
||||
assert len(summary) <= 200
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from application.agents.classic_agent import ClassicAgent
|
||||
from application.agents.tools.internal_search import INTERNAL_TOOL_ID
|
||||
from docsgpt.agents.classic_agent import ClassicAgent
|
||||
from docsgpt.agents.tools.internal_search import INTERNAL_TOOL_ID
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -13,7 +13,7 @@ def _no_tools(monkeypatch):
|
||||
return {}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tool_executor.ToolExecutor.get_tools", _fake_get_tools
|
||||
"docsgpt.agents.tool_executor.ToolExecutor.get_tools", _fake_get_tools
|
||||
)
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ def _no_tools(monkeypatch):
|
||||
def _no_dir_structure(monkeypatch):
|
||||
"""Stub the DB-backed directory-structure lookup used by the search tool."""
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tools.internal_search.sources_have_directory_structure",
|
||||
"docsgpt.agents.tools.internal_search.sources_have_directory_structure",
|
||||
lambda source: False,
|
||||
)
|
||||
|
||||
@@ -117,7 +117,7 @@ class TestClassicAgent:
|
||||
}
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tool_executor.ToolExecutor.get_tools",
|
||||
"docsgpt.agents.tool_executor.ToolExecutor.get_tools",
|
||||
lambda self: fake_tools,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Tests for application/agents/tools/cryptoprice.py"""
|
||||
"""Tests for docsgpt/agents/tools/cryptoprice.py"""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from application.agents.tools.cryptoprice import CryptoPriceTool
|
||||
from docsgpt.agents.tools.cryptoprice import CryptoPriceTool
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -18,7 +18,7 @@ class TestCryptoPriceExecuteAction:
|
||||
with pytest.raises(ValueError, match="Unknown action"):
|
||||
tool.execute_action("invalid_action")
|
||||
|
||||
@patch("application.agents.tools.cryptoprice.requests.get")
|
||||
@patch("docsgpt.agents.tools.cryptoprice.requests.get")
|
||||
def test_successful_price_fetch(self, mock_get, tool):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
@@ -31,7 +31,7 @@ class TestCryptoPriceExecuteAction:
|
||||
assert result["price"] == 65000
|
||||
assert "successfully" in result["message"]
|
||||
|
||||
@patch("application.agents.tools.cryptoprice.requests.get")
|
||||
@patch("docsgpt.agents.tools.cryptoprice.requests.get")
|
||||
def test_currency_not_found(self, mock_get, tool):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
@@ -44,7 +44,7 @@ class TestCryptoPriceExecuteAction:
|
||||
assert "Couldn't find" in result["message"]
|
||||
assert "price" not in result
|
||||
|
||||
@patch("application.agents.tools.cryptoprice.requests.get")
|
||||
@patch("docsgpt.agents.tools.cryptoprice.requests.get")
|
||||
def test_api_failure(self, mock_get, tool):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 500
|
||||
@@ -55,7 +55,7 @@ class TestCryptoPriceExecuteAction:
|
||||
assert result["status_code"] == 500
|
||||
assert "Failed" in result["message"]
|
||||
|
||||
@patch("application.agents.tools.cryptoprice.requests.get")
|
||||
@patch("docsgpt.agents.tools.cryptoprice.requests.get")
|
||||
def test_symbol_case_insensitive(self, mock_get, tool):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests for application.agents.default_tools — the default chat tools."""
|
||||
"""Tests for docsgpt.agents.default_tools — the default chat tools."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -6,7 +6,7 @@ import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from application.agents import default_tools
|
||||
from docsgpt.agents import default_tools
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -100,7 +100,7 @@ class TestValidation:
|
||||
"DEFAULT_CHAT_TOOLS",
|
||||
["memory", "read_webpage", "future_tool_x"],
|
||||
)
|
||||
with caplog.at_level("DEBUG", logger="application.agents.default_tools"):
|
||||
with caplog.at_level("DEBUG", logger="docsgpt.agents.default_tools"):
|
||||
usable = default_tools.validate_default_chat_tools()
|
||||
assert "future_tool_x" not in usable
|
||||
assert "memory" in usable and "read_webpage" in usable
|
||||
@@ -117,7 +117,7 @@ class TestValidation:
|
||||
"DEFAULT_CHAT_TOOLS",
|
||||
["memory", "read_webpage", "future_tool_x"],
|
||||
)
|
||||
with caplog.at_level("DEBUG", logger="application.agents.default_tools"):
|
||||
with caplog.at_level("DEBUG", logger="docsgpt.agents.default_tools"):
|
||||
default_tools.loaded_default_tools()
|
||||
assert caplog.records == []
|
||||
|
||||
@@ -201,7 +201,7 @@ class TestSandboxToolsAreNotShippedDefaults:
|
||||
that legitimately enabled these — which is the documented way to turn
|
||||
them on when a sandbox runner exists.
|
||||
"""
|
||||
from application.core.settings import Settings
|
||||
from docsgpt.core.settings import Settings
|
||||
|
||||
shipped = Settings.model_fields["DEFAULT_CHAT_TOOLS"].default
|
||||
assert name not in shipped
|
||||
@@ -497,14 +497,14 @@ class TestBuiltinAgentTools:
|
||||
# ---------------------------------------------------------------------------
|
||||
@pytest.mark.unit
|
||||
class TestFkBoundToolsIsInSync:
|
||||
# Table name -> tool module name (``application/agents/tools/<name>``).
|
||||
# Table name -> tool module name (``docsgpt/agents/tools/<name>``).
|
||||
_TABLE_TO_TOOL = {
|
||||
"notes": "notes",
|
||||
"todos": "todo_list",
|
||||
}
|
||||
|
||||
def test_fk_bound_tools_matches_metadata(self):
|
||||
from application.storage.db.models import metadata
|
||||
from docsgpt.storage.db.models import metadata
|
||||
|
||||
fk_bound_tables = set()
|
||||
for tbl in metadata.tables.values():
|
||||
@@ -520,7 +520,7 @@ class TestFkBoundToolsIsInSync:
|
||||
assert not unmapped, (
|
||||
f"New FK-bound table(s) without a tool mapping: {sorted(unmapped)}. "
|
||||
"Add an entry to _TABLE_TO_TOOL here AND to "
|
||||
"application.agents.default_tools._FK_BOUND_TOOLS."
|
||||
"docsgpt.agents.default_tools._FK_BOUND_TOOLS."
|
||||
)
|
||||
derived_names = {
|
||||
self._TABLE_TO_TOOL[name] for name in fk_bound_tables
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Tests for application/agents/tools/duckduckgo.py"""
|
||||
"""Tests for docsgpt/agents/tools/duckduckgo.py"""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from application.agents.tools.duckduckgo import DuckDuckGoSearchTool
|
||||
from docsgpt.agents.tools.duckduckgo import DuckDuckGoSearchTool
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -88,7 +88,7 @@ class TestDuckDuckGoExecuteAction:
|
||||
call_kwargs = mock_client.images.call_args[1]
|
||||
assert call_kwargs["max_results"] == 50
|
||||
|
||||
@patch("application.agents.tools.duckduckgo.time.sleep")
|
||||
@patch("docsgpt.agents.tools.duckduckgo.time.sleep")
|
||||
@patch.object(DuckDuckGoSearchTool, "_get_ddgs_client")
|
||||
def test_rate_limit_retries(self, mock_client_factory, mock_sleep, tool):
|
||||
mock_client = MagicMock()
|
||||
|
||||
@@ -17,7 +17,7 @@ def _patch_db_readonly(pg_conn, monkeypatch):
|
||||
yield pg_conn
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.api.user.tools.routes.db_readonly", _use_pg_conn
|
||||
"docsgpt.api.user.tools.routes.db_readonly", _use_pg_conn
|
||||
)
|
||||
|
||||
|
||||
@@ -26,9 +26,9 @@ class TestGetArtifact:
|
||||
def test_note_artifact_success(
|
||||
self, pg_conn, _patch_db_readonly, flask_app, decoded_token
|
||||
):
|
||||
from application.api.user.tools.routes import GetArtifact
|
||||
from application.storage.db.repositories.notes import NotesRepository
|
||||
from application.storage.db.repositories.user_tools import UserToolsRepository
|
||||
from docsgpt.api.user.tools.routes import GetArtifact
|
||||
from docsgpt.storage.db.repositories.notes import NotesRepository
|
||||
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
|
||||
|
||||
tool_row = UserToolsRepository(pg_conn).create(
|
||||
user_id=decoded_token["sub"], name="notes_tool"
|
||||
@@ -57,9 +57,9 @@ class TestGetArtifact:
|
||||
def test_todo_artifact_success(
|
||||
self, pg_conn, _patch_db_readonly, flask_app, decoded_token
|
||||
):
|
||||
from application.api.user.tools.routes import GetArtifact
|
||||
from application.storage.db.repositories.todos import TodosRepository
|
||||
from application.storage.db.repositories.user_tools import UserToolsRepository
|
||||
from docsgpt.api.user.tools.routes import GetArtifact
|
||||
from docsgpt.storage.db.repositories.todos import TodosRepository
|
||||
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
|
||||
|
||||
tool_row = UserToolsRepository(pg_conn).create(
|
||||
user_id=decoded_token["sub"], name="todo_tool"
|
||||
@@ -96,9 +96,9 @@ class TestGetArtifact:
|
||||
self, pg_conn, _patch_db_readonly, flask_app, decoded_token
|
||||
):
|
||||
"""All todos are returned regardless of the 'all' query parameter."""
|
||||
from application.api.user.tools.routes import GetArtifact
|
||||
from application.storage.db.repositories.todos import TodosRepository
|
||||
from application.storage.db.repositories.user_tools import UserToolsRepository
|
||||
from docsgpt.api.user.tools.routes import GetArtifact
|
||||
from docsgpt.storage.db.repositories.todos import TodosRepository
|
||||
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
|
||||
|
||||
tool_row = UserToolsRepository(pg_conn).create(
|
||||
user_id=decoded_token["sub"], name="todo_tool"
|
||||
@@ -142,7 +142,7 @@ class TestGetArtifact:
|
||||
"""A non-UUID, non-legacy id falls through ``get_any`` on both
|
||||
repos without raising (id-shape dispatch keeps the UUID cast out
|
||||
of the path) and surfaces as a clean 404."""
|
||||
from application.api.user.tools.routes import GetArtifact
|
||||
from docsgpt.api.user.tools.routes import GetArtifact
|
||||
|
||||
with flask_app.app_context():
|
||||
with flask_app.test_request_context():
|
||||
@@ -155,7 +155,7 @@ class TestGetArtifact:
|
||||
def test_artifact_not_found_returns_404(
|
||||
self, _patch_db_readonly, flask_app, decoded_token
|
||||
):
|
||||
from application.api.user.tools.routes import GetArtifact
|
||||
from docsgpt.api.user.tools.routes import GetArtifact
|
||||
|
||||
non_existent_id = str(uuid.uuid4())
|
||||
|
||||
@@ -171,9 +171,9 @@ class TestGetArtifact:
|
||||
def test_other_user_artifact_returns_404(
|
||||
self, pg_conn, _patch_db_readonly, flask_app, decoded_token
|
||||
):
|
||||
from application.api.user.tools.routes import GetArtifact
|
||||
from application.storage.db.repositories.notes import NotesRepository
|
||||
from application.storage.db.repositories.user_tools import UserToolsRepository
|
||||
from docsgpt.api.user.tools.routes import GetArtifact
|
||||
from docsgpt.storage.db.repositories.notes import NotesRepository
|
||||
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
|
||||
|
||||
tool_row = UserToolsRepository(pg_conn).create(
|
||||
user_id="other_user", name="tool1"
|
||||
@@ -197,8 +197,8 @@ class TestGetArtifact:
|
||||
):
|
||||
"""Pre-cutover note artifact ids (Mongo ObjectIds) must resolve
|
||||
once the notes.legacy_mongo_id column is populated by backfill."""
|
||||
from application.api.user.tools.routes import GetArtifact
|
||||
from application.storage.db.repositories.user_tools import UserToolsRepository
|
||||
from docsgpt.api.user.tools.routes import GetArtifact
|
||||
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
|
||||
|
||||
tool_row = UserToolsRepository(pg_conn).create(
|
||||
user_id=decoded_token["sub"], name="notes_tool"
|
||||
@@ -233,9 +233,9 @@ class TestGetArtifact:
|
||||
``TodosRepository.get_any`` — that dispatch keeps the 24-hex
|
||||
ObjectId out of the bare ``CAST(:id AS uuid)`` path that would
|
||||
otherwise poison the readonly transaction."""
|
||||
from application.api.user.tools.routes import GetArtifact
|
||||
from application.storage.db.repositories.todos import TodosRepository
|
||||
from application.storage.db.repositories.user_tools import UserToolsRepository
|
||||
from docsgpt.api.user.tools.routes import GetArtifact
|
||||
from docsgpt.storage.db.repositories.todos import TodosRepository
|
||||
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
|
||||
|
||||
tool_row = UserToolsRepository(pg_conn).create(
|
||||
user_id=decoded_token["sub"], name="todo_tool"
|
||||
|
||||
@@ -18,7 +18,7 @@ import pytest
|
||||
|
||||
def _run(events, monkeypatch):
|
||||
"""Drive ``run_agent_headless`` over a canned agent event stream."""
|
||||
from application.agents import headless_runner as hr
|
||||
from docsgpt.agents import headless_runner as hr
|
||||
|
||||
agent = MagicMock(name="agent")
|
||||
agent.gen.return_value = iter(events)
|
||||
@@ -41,14 +41,14 @@ def _run(events, monkeypatch):
|
||||
classmethod(lambda cls, *a, **kw: agent),
|
||||
)
|
||||
|
||||
with patch("application.core.model_utils.validate_model_id", return_value=True), \
|
||||
patch("application.core.model_utils.get_default_model_id", return_value="m"), \
|
||||
with patch("docsgpt.core.model_utils.validate_model_id", return_value=True), \
|
||||
patch("docsgpt.core.model_utils.get_default_model_id", return_value="m"), \
|
||||
patch(
|
||||
"application.core.model_utils.get_provider_from_model_id",
|
||||
"docsgpt.core.model_utils.get_provider_from_model_id",
|
||||
return_value="openai",
|
||||
), \
|
||||
patch("application.core.model_utils.get_api_key_for_provider", return_value="k"), \
|
||||
patch("application.utils.calculate_doc_token_budget", return_value=1000):
|
||||
patch("docsgpt.core.model_utils.get_api_key_for_provider", return_value="k"), \
|
||||
patch("docsgpt.utils.calculate_doc_token_budget", return_value=1000):
|
||||
return hr.run_agent_headless(
|
||||
{"user_id": "u1", "id": "agent-1", "default_model_id": "m"},
|
||||
"do the thing",
|
||||
|
||||
@@ -19,7 +19,7 @@ import pytest
|
||||
|
||||
def _run(agent_config, monkeypatch, events=None):
|
||||
"""Drive ``run_agent_headless`` and capture the agent-factory kwargs."""
|
||||
from application.agents import headless_runner as hr
|
||||
from docsgpt.agents import headless_runner as hr
|
||||
|
||||
agent = MagicMock(name="agent")
|
||||
agent.gen.return_value = iter(events if events is not None else [{"answer": "ok"}])
|
||||
@@ -48,14 +48,14 @@ def _run(agent_config, monkeypatch, events=None):
|
||||
hr.AgentCreator, "create_agent", classmethod(_create_agent),
|
||||
)
|
||||
|
||||
with patch("application.core.model_utils.validate_model_id", return_value=True), \
|
||||
patch("application.core.model_utils.get_default_model_id", return_value="m"), \
|
||||
with patch("docsgpt.core.model_utils.validate_model_id", return_value=True), \
|
||||
patch("docsgpt.core.model_utils.get_default_model_id", return_value="m"), \
|
||||
patch(
|
||||
"application.core.model_utils.get_provider_from_model_id",
|
||||
"docsgpt.core.model_utils.get_provider_from_model_id",
|
||||
return_value="openai",
|
||||
), \
|
||||
patch("application.core.model_utils.get_api_key_for_provider", return_value="k"), \
|
||||
patch("application.utils.calculate_doc_token_budget", return_value=1000):
|
||||
patch("docsgpt.core.model_utils.get_api_key_for_provider", return_value="k"), \
|
||||
patch("docsgpt.utils.calculate_doc_token_budget", return_value=1000):
|
||||
outcome = hr.run_agent_headless(agent_config, "do the thing")
|
||||
return captured, outcome
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
from application.agents.tools.internal_search import (
|
||||
from docsgpt.agents.tools.internal_search import (
|
||||
INTERNAL_TOOL_ID,
|
||||
InternalSearchTool,
|
||||
add_internal_search_tool,
|
||||
@@ -24,7 +24,7 @@ class TestAddInternalSearchTool:
|
||||
}
|
||||
tools: dict = {}
|
||||
with patch(
|
||||
"application.agents.tools.internal_search."
|
||||
"docsgpt.agents.tools.internal_search."
|
||||
"sources_have_directory_structure",
|
||||
return_value=False,
|
||||
):
|
||||
@@ -254,7 +254,7 @@ class TestBuildHelpers:
|
||||
}
|
||||
|
||||
with patch(
|
||||
"application.agents.tools.internal_search.sources_have_directory_structure",
|
||||
"docsgpt.agents.tools.internal_search.sources_have_directory_structure",
|
||||
return_value=False,
|
||||
):
|
||||
add_internal_search_tool(tools_dict, retriever_config)
|
||||
@@ -284,7 +284,7 @@ class TestInternalSearchToolGetRetriever:
|
||||
def test_get_retriever_creates_dispatcher(self):
|
||||
# _get_retriever now builds the per-source Dispatcher (B1c) lazily and
|
||||
# caches it; the Dispatcher creates per-group retrievers at search time.
|
||||
from application.retriever.dispatcher import Dispatcher
|
||||
from docsgpt.retriever.dispatcher import Dispatcher
|
||||
|
||||
tool = InternalSearchTool({
|
||||
"source": {"active_docs": ["a"]},
|
||||
@@ -295,7 +295,7 @@ class TestInternalSearchToolGetRetriever:
|
||||
assert tool._retriever is None
|
||||
|
||||
with patch(
|
||||
"application.retriever.classic_rag.LLMCreator.create_llm",
|
||||
"docsgpt.retriever.classic_rag.LLMCreator.create_llm",
|
||||
return_value=Mock(),
|
||||
):
|
||||
result = tool._get_retriever()
|
||||
@@ -306,7 +306,7 @@ class TestInternalSearchToolGetRetriever:
|
||||
def test_get_retriever_kill_switch_falls_back_to_legacy(self, monkeypatch):
|
||||
# PER_SOURCE_RETRIEVAL_ENABLED=False → legacy single RetrieverCreator.
|
||||
monkeypatch.setattr(
|
||||
"application.retriever.dispatcher.settings.PER_SOURCE_RETRIEVAL_ENABLED",
|
||||
"docsgpt.retriever.dispatcher.settings.PER_SOURCE_RETRIEVAL_ENABLED",
|
||||
False,
|
||||
)
|
||||
tool = InternalSearchTool({
|
||||
@@ -316,7 +316,7 @@ class TestInternalSearchToolGetRetriever:
|
||||
})
|
||||
mock_retriever = Mock()
|
||||
with patch(
|
||||
"application.agents.tools.internal_search.RetrieverCreator"
|
||||
"docsgpt.agents.tools.internal_search.RetrieverCreator"
|
||||
) as mock_rc:
|
||||
mock_rc.create_retriever.return_value = mock_retriever
|
||||
result = tool._get_retriever()
|
||||
@@ -396,7 +396,7 @@ class TestSourcesHaveDirectoryStructure:
|
||||
"""Cover line 240, 254, 298: sources_have_directory_structure helper."""
|
||||
|
||||
def test_no_active_docs_returns_false(self):
|
||||
from application.agents.tools.internal_search import (
|
||||
from docsgpt.agents.tools.internal_search import (
|
||||
sources_have_directory_structure,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests for application/agents/tools/mcp_tool.py"""
|
||||
"""Tests for docsgpt/agents/tools/mcp_tool.py"""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
@@ -18,14 +18,14 @@ def _patch_mcp_globals(monkeypatch):
|
||||
import sys
|
||||
|
||||
# If the module is already loaded, just patch attributes directly
|
||||
if "application.agents.tools.mcp_tool" in sys.modules:
|
||||
mcp_mod = sys.modules["application.agents.tools.mcp_tool"]
|
||||
if "docsgpt.agents.tools.mcp_tool" in sys.modules:
|
||||
mcp_mod = sys.modules["docsgpt.agents.tools.mcp_tool"]
|
||||
else:
|
||||
# Break the circular import by pre-populating the tasks import
|
||||
# with a mock before mcp_tool tries to import it
|
||||
mock_tasks = MagicMock()
|
||||
monkeypatch.setitem(sys.modules, "application.api.user.tasks", mock_tasks)
|
||||
import application.agents.tools.mcp_tool as mcp_mod
|
||||
monkeypatch.setitem(sys.modules, "docsgpt.api.user.tasks", mock_tasks)
|
||||
import docsgpt.agents.tools.mcp_tool as mcp_mod
|
||||
|
||||
monkeypatch.setattr(mcp_mod, "_mcp_clients_cache", {})
|
||||
monkeypatch.setattr(mcp_mod, "validate_url", lambda url: url)
|
||||
@@ -55,7 +55,7 @@ def bearer_config():
|
||||
@pytest.mark.unit
|
||||
class TestMCPToolInit:
|
||||
def test_basic_init(self, mcp_config):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
with patch.object(MCPTool, "_setup_client"):
|
||||
tool = MCPTool(mcp_config)
|
||||
@@ -66,21 +66,21 @@ class TestMCPToolInit:
|
||||
assert tool.timeout == 10
|
||||
|
||||
def test_bearer_auth_credentials(self, bearer_config):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
with patch.object(MCPTool, "_setup_client"):
|
||||
tool = MCPTool(bearer_config)
|
||||
assert tool.auth_credentials["bearer_token"] == "tok_123"
|
||||
|
||||
def test_no_server_url_skips_setup(self):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
with patch.object(MCPTool, "_setup_client") as mock_setup:
|
||||
MCPTool({"server_url": "", "auth_type": "none"})
|
||||
mock_setup.assert_not_called()
|
||||
|
||||
def test_oauth_skips_setup(self):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
with patch.object(MCPTool, "_setup_client") as mock_setup:
|
||||
MCPTool(
|
||||
@@ -94,24 +94,24 @@ class TestMCPToolInit:
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestGenerateCacheKey:
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_none_auth(self, mock_setup, mcp_config):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool(mcp_config)
|
||||
assert "none" in tool._cache_key
|
||||
assert "mcp.example.com" in tool._cache_key
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_bearer_auth(self, mock_setup, bearer_config):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool(bearer_config)
|
||||
assert "bearer:" in tool._cache_key
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_api_key_auth(self, mock_setup):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool(
|
||||
{
|
||||
@@ -122,9 +122,9 @@ class TestGenerateCacheKey:
|
||||
)
|
||||
assert "apikey:" in tool._cache_key
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_basic_auth(self, mock_setup):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool(
|
||||
{
|
||||
@@ -138,17 +138,17 @@ class TestGenerateCacheKey:
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestCreateTransport:
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_http_transport(self, mock_setup, mcp_config):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool(mcp_config)
|
||||
transport = tool._create_transport()
|
||||
assert transport is not None
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_sse_transport(self, mock_setup):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool(
|
||||
{
|
||||
@@ -160,9 +160,9 @@ class TestCreateTransport:
|
||||
transport = tool._create_transport()
|
||||
assert transport is not None
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_auto_detects_sse(self, mock_setup):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool(
|
||||
{
|
||||
@@ -174,9 +174,9 @@ class TestCreateTransport:
|
||||
transport = tool._create_transport()
|
||||
assert transport is not None
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_stdio_transport_disabled(self, mock_setup):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool(
|
||||
{
|
||||
@@ -188,9 +188,9 @@ class TestCreateTransport:
|
||||
with pytest.raises(ValueError, match="STDIO transport is disabled"):
|
||||
tool._create_transport()
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_api_key_header_injected(self, mock_setup):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool(
|
||||
{
|
||||
@@ -207,9 +207,9 @@ class TestCreateTransport:
|
||||
transport = tool._create_transport()
|
||||
assert transport is not None
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_basic_auth_header_injected(self, mock_setup):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool(
|
||||
{
|
||||
@@ -225,18 +225,18 @@ class TestCreateTransport:
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestFormatTools:
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_format_list_of_dicts(self, mock_setup, mcp_config):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool(mcp_config)
|
||||
result = tool._format_tools([{"name": "tool1", "description": "desc"}])
|
||||
assert len(result) == 1
|
||||
assert result[0]["name"] == "tool1"
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_format_tools_with_name_attribute(self, mock_setup, mcp_config):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool(mcp_config)
|
||||
mock_tool = MagicMock()
|
||||
@@ -249,9 +249,9 @@ class TestFormatTools:
|
||||
assert result[0]["name"] == "my_tool"
|
||||
assert result[0]["inputSchema"] == {"type": "object", "properties": {}}
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_format_tools_response_object(self, mock_setup, mcp_config):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool(mcp_config)
|
||||
resp = MagicMock()
|
||||
@@ -260,9 +260,9 @@ class TestFormatTools:
|
||||
result = tool._format_tools(resp)
|
||||
assert len(result) == 1
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_format_empty(self, mock_setup, mcp_config):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool(mcp_config)
|
||||
assert tool._format_tools([]) == []
|
||||
@@ -271,9 +271,9 @@ class TestFormatTools:
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestFormatResult:
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_format_result_with_content(self, mock_setup, mcp_config):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool(mcp_config)
|
||||
mock_result = MagicMock()
|
||||
@@ -288,9 +288,9 @@ class TestFormatResult:
|
||||
assert result["content"][0]["text"] == "Hello"
|
||||
assert result["isError"] is False
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_format_raw_result(self, mock_setup, mcp_config):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool(mcp_config)
|
||||
raw = {"key": "value"}
|
||||
@@ -299,18 +299,18 @@ class TestFormatResult:
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestExecuteAction:
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_no_server_raises(self, mock_setup):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool({"server_url": "", "auth_type": "none"})
|
||||
with pytest.raises(Exception, match="No MCP server configured"):
|
||||
tool.execute_action("test_action")
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
def test_successful_execute(self, mock_run, mock_setup, mcp_config):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool(mcp_config)
|
||||
tool._client = MagicMock()
|
||||
@@ -321,10 +321,10 @@ class TestExecuteAction:
|
||||
mock_run.assert_called_once_with("call_tool", "test_action", param1="val1")
|
||||
assert result == {"key": "value"}
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
def test_empty_kwargs_cleaned(self, mock_run, mock_setup, mcp_config):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool(mcp_config)
|
||||
tool._client = MagicMock()
|
||||
@@ -340,18 +340,18 @@ class TestExecuteAction:
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestTestConnection:
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_no_server_url(self, mock_setup):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool({"server_url": "", "auth_type": "none"})
|
||||
result = tool.test_connection()
|
||||
assert result["success"] is False
|
||||
assert "No server URL" in result["message"]
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_invalid_scheme(self, mock_setup):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool(
|
||||
{"server_url": "ftp://bad.com", "auth_type": "none"}
|
||||
@@ -363,42 +363,42 @@ class TestTestConnection:
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestMapError:
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_timeout_error(self, mock_setup, mcp_config):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
import concurrent.futures
|
||||
|
||||
tool = MCPTool(mcp_config)
|
||||
err = tool._map_error("test", concurrent.futures.TimeoutError())
|
||||
assert "Timed out" in str(err)
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_connection_refused(self, mock_setup, mcp_config):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool(mcp_config)
|
||||
err = tool._map_error("test", ConnectionRefusedError())
|
||||
assert "Connection refused" in str(err)
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_403_forbidden(self, mock_setup, mcp_config):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool(mcp_config)
|
||||
err = tool._map_error("test", Exception("403 Forbidden"))
|
||||
assert "Access denied" in str(err)
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_ssl_error(self, mock_setup, mcp_config):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool(mcp_config)
|
||||
err = tool._map_error("test", Exception("SSL certificate verify failed"))
|
||||
assert "SSL" in str(err)
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_unknown_error_passthrough(self, mock_setup, mcp_config):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool(mcp_config)
|
||||
original = RuntimeError("something weird")
|
||||
@@ -408,17 +408,17 @@ class TestMapError:
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestGetActionsMetadata:
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_empty_tools(self, mock_setup, mcp_config):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool(mcp_config)
|
||||
tool.available_tools = []
|
||||
assert tool.get_actions_metadata() == []
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_tools_with_input_schema(self, mock_setup, mcp_config):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool(mcp_config)
|
||||
tool.available_tools = [
|
||||
@@ -437,18 +437,18 @@ class TestGetActionsMetadata:
|
||||
assert meta[0]["name"] == "search"
|
||||
assert "query" in meta[0]["parameters"]["properties"]
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_tools_without_schema(self, mock_setup, mcp_config):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool(mcp_config)
|
||||
tool.available_tools = [{"name": "ping", "description": "Ping"}]
|
||||
meta = tool.get_actions_metadata()
|
||||
assert meta[0]["parameters"]["properties"] == {}
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
|
||||
def test_config_requirements(self, mock_setup, mcp_config):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool(mcp_config)
|
||||
reqs = tool.get_config_requirements()
|
||||
@@ -460,7 +460,7 @@ class TestGetActionsMetadata:
|
||||
@pytest.mark.unit
|
||||
class TestMCPOAuthManager:
|
||||
def test_handle_callback_success(self):
|
||||
from application.agents.tools.mcp_tool import MCPOAuthManager
|
||||
from docsgpt.agents.tools.mcp_tool import MCPOAuthManager
|
||||
|
||||
mock_redis = MagicMock()
|
||||
manager = MCPOAuthManager(mock_redis)
|
||||
@@ -471,14 +471,14 @@ class TestMCPOAuthManager:
|
||||
mock_redis.setex.assert_called()
|
||||
|
||||
def test_handle_callback_no_redis(self):
|
||||
from application.agents.tools.mcp_tool import MCPOAuthManager
|
||||
from docsgpt.agents.tools.mcp_tool import MCPOAuthManager
|
||||
|
||||
manager = MCPOAuthManager(None)
|
||||
result = manager.handle_oauth_callback(state="abc", code="code")
|
||||
assert result is False
|
||||
|
||||
def test_handle_callback_error(self):
|
||||
from application.agents.tools.mcp_tool import MCPOAuthManager
|
||||
from docsgpt.agents.tools.mcp_tool import MCPOAuthManager
|
||||
|
||||
mock_redis = MagicMock()
|
||||
manager = MCPOAuthManager(mock_redis)
|
||||
@@ -489,7 +489,7 @@ class TestMCPOAuthManager:
|
||||
assert result is False
|
||||
|
||||
def test_get_oauth_status_no_task(self):
|
||||
from application.agents.tools.mcp_tool import MCPOAuthManager
|
||||
from docsgpt.agents.tools.mcp_tool import MCPOAuthManager
|
||||
|
||||
manager = MCPOAuthManager(MagicMock())
|
||||
result = manager.get_oauth_status("", "alice")
|
||||
@@ -500,7 +500,7 @@ class TestMCPOAuthManager:
|
||||
@pytest.mark.unit
|
||||
class TestDBTokenStorage:
|
||||
def test_get_base_url(self):
|
||||
from application.agents.tools.mcp_tool import DBTokenStorage
|
||||
from docsgpt.agents.tools.mcp_tool import DBTokenStorage
|
||||
|
||||
assert (
|
||||
DBTokenStorage.get_base_url("https://mcp.example.com/api/v1")
|
||||
@@ -508,7 +508,7 @@ class TestDBTokenStorage:
|
||||
)
|
||||
|
||||
def test_get_db_key(self):
|
||||
from application.agents.tools.mcp_tool import DBTokenStorage
|
||||
from docsgpt.agents.tools.mcp_tool import DBTokenStorage
|
||||
|
||||
mock_db = MagicMock()
|
||||
storage = DBTokenStorage(
|
||||
|
||||
@@ -13,14 +13,14 @@ def _no_db(monkeypatch):
|
||||
def fake_readonly():
|
||||
yield MagicMock()
|
||||
|
||||
monkeypatch.setattr("application.agents.tool_executor.db_readonly", fake_readonly)
|
||||
monkeypatch.setattr("docsgpt.agents.tool_executor.db_readonly", fake_readonly)
|
||||
|
||||
|
||||
class _FakeExecutorBase:
|
||||
"""Stands in for BaseAgent: provides the tool_executor the mixin scopes."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
from application.agents.tool_executor import ToolExecutor
|
||||
from docsgpt.agents.tool_executor import ToolExecutor
|
||||
|
||||
self.tool_executor = ToolExecutor()
|
||||
|
||||
@@ -29,7 +29,7 @@ class _FakeExecutorBase:
|
||||
class TestWorkflowNodeAgentFactory:
|
||||
|
||||
def test_raises_on_unsupported_type(self):
|
||||
from application.agents.workflows.node_agent import WorkflowNodeAgentFactory
|
||||
from docsgpt.agents.workflows.node_agent import WorkflowNodeAgentFactory
|
||||
|
||||
with pytest.raises(ValueError, match="Unsupported agent type"):
|
||||
WorkflowNodeAgentFactory.create(
|
||||
@@ -47,7 +47,7 @@ class TestWorkflowNodeMixinInit:
|
||||
so per-node tool filtering must live there (the old agent-method mixin was dead code)."""
|
||||
|
||||
def _mixed(self, **kwargs):
|
||||
from application.agents.workflows.node_agent import _WorkflowNodeMixin
|
||||
from docsgpt.agents.workflows.node_agent import _WorkflowNodeMixin
|
||||
|
||||
class TestMixin(_WorkflowNodeMixin, _FakeExecutorBase):
|
||||
pass
|
||||
@@ -76,7 +76,7 @@ class TestWorkflowNodeMixinInit:
|
||||
def test_scoped_executor_resolves_builtin_synthetic_ids(self, _no_db):
|
||||
"""A node whose Tools picker selected a builtin (Artifact / Read Document)
|
||||
must get that tool at runtime — the P0 this design replaced."""
|
||||
from application.agents.default_tools import default_tool_id
|
||||
from docsgpt.agents.default_tools import default_tool_id
|
||||
|
||||
obj = self._mixed(
|
||||
tool_ids=[
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Tests for application/agents/tools/ntfy.py"""
|
||||
"""Tests for docsgpt/agents/tools/ntfy.py"""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from application.agents.tools.ntfy import NtfyTool
|
||||
from docsgpt.agents.tools.ntfy import NtfyTool
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -23,7 +23,7 @@ class TestNtfyExecuteAction:
|
||||
with pytest.raises(ValueError, match="Unknown action"):
|
||||
tool.execute_action("bad_action")
|
||||
|
||||
@patch("application.agents.tools.ntfy.pinned_request")
|
||||
@patch("docsgpt.agents.tools.ntfy.pinned_request")
|
||||
def test_send_message_basic(self, mock_pinned_request, tool):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
@@ -44,7 +44,7 @@ class TestNtfyExecuteAction:
|
||||
assert args[1] == "https://ntfy.sh/test"
|
||||
assert kwargs["data"] == b"Hello"
|
||||
|
||||
@patch("application.agents.tools.ntfy.pinned_request")
|
||||
@patch("docsgpt.agents.tools.ntfy.pinned_request")
|
||||
def test_send_with_title_and_priority(self, mock_pinned_request, tool):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
@@ -63,7 +63,7 @@ class TestNtfyExecuteAction:
|
||||
assert headers["X-Title"] == "Warning"
|
||||
assert headers["X-Priority"] == "5"
|
||||
|
||||
@patch("application.agents.tools.ntfy.pinned_request")
|
||||
@patch("docsgpt.agents.tools.ntfy.pinned_request")
|
||||
def test_auth_header_with_token(self, mock_pinned_request, tool):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
@@ -79,7 +79,7 @@ class TestNtfyExecuteAction:
|
||||
headers = mock_pinned_request.call_args[1]["headers"]
|
||||
assert headers["Authorization"] == "Basic test_token"
|
||||
|
||||
@patch("application.agents.tools.ntfy.pinned_request")
|
||||
@patch("docsgpt.agents.tools.ntfy.pinned_request")
|
||||
def test_no_auth_without_token(self, mock_pinned_request, tool_no_token):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
@@ -115,7 +115,7 @@ class TestNtfyExecuteAction:
|
||||
priority="abc",
|
||||
)
|
||||
|
||||
@patch("application.agents.tools.ntfy.pinned_request")
|
||||
@patch("docsgpt.agents.tools.ntfy.pinned_request")
|
||||
def test_trailing_slash_stripped(self, mock_pinned_request, tool):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Tests for application/agents/tools/postgres.py"""
|
||||
"""Tests for docsgpt/agents/tools/postgres.py"""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from application.agents.tools.postgres import PostgresTool
|
||||
from docsgpt.agents.tools.postgres import PostgresTool
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -18,7 +18,7 @@ class TestPostgresExecuteAction:
|
||||
with pytest.raises(ValueError, match="Unknown action"):
|
||||
tool.execute_action("invalid")
|
||||
|
||||
@patch("application.agents.tools.postgres.psycopg.connect")
|
||||
@patch("docsgpt.agents.tools.postgres.psycopg.connect")
|
||||
def test_select_query(self, mock_connect, tool):
|
||||
mock_conn = MagicMock()
|
||||
mock_cur = MagicMock()
|
||||
@@ -37,7 +37,7 @@ class TestPostgresExecuteAction:
|
||||
assert result["response_data"]["data"][0] == {"id": 1, "name": "Alice"}
|
||||
mock_conn.close.assert_called_once()
|
||||
|
||||
@patch("application.agents.tools.postgres.psycopg.connect")
|
||||
@patch("docsgpt.agents.tools.postgres.psycopg.connect")
|
||||
def test_insert_query(self, mock_connect, tool):
|
||||
mock_conn = MagicMock()
|
||||
mock_cur = MagicMock()
|
||||
@@ -55,7 +55,7 @@ class TestPostgresExecuteAction:
|
||||
mock_conn.commit.assert_called_once()
|
||||
mock_conn.close.assert_called_once()
|
||||
|
||||
@patch("application.agents.tools.postgres.psycopg.connect")
|
||||
@patch("docsgpt.agents.tools.postgres.psycopg.connect")
|
||||
def test_db_error(self, mock_connect, tool):
|
||||
import psycopg
|
||||
|
||||
@@ -68,7 +68,7 @@ class TestPostgresExecuteAction:
|
||||
assert result["status_code"] == 500
|
||||
assert "Database error" in result["error"]
|
||||
|
||||
@patch("application.agents.tools.postgres.psycopg.connect")
|
||||
@patch("docsgpt.agents.tools.postgres.psycopg.connect")
|
||||
def test_get_schema(self, mock_connect, tool):
|
||||
mock_conn = MagicMock()
|
||||
mock_cur = MagicMock()
|
||||
@@ -89,7 +89,7 @@ class TestPostgresExecuteAction:
|
||||
assert result["schema"]["users"][0]["column_name"] == "id"
|
||||
mock_conn.close.assert_called_once()
|
||||
|
||||
@patch("application.agents.tools.postgres.psycopg.connect")
|
||||
@patch("docsgpt.agents.tools.postgres.psycopg.connect")
|
||||
def test_get_schema_db_error(self, mock_connect, tool):
|
||||
import psycopg
|
||||
|
||||
@@ -100,7 +100,7 @@ class TestPostgresExecuteAction:
|
||||
assert result["status_code"] == 500
|
||||
assert "Database error" in result["error"]
|
||||
|
||||
@patch("application.agents.tools.postgres.psycopg.connect")
|
||||
@patch("docsgpt.agents.tools.postgres.psycopg.connect")
|
||||
def test_connection_closed_on_error(self, mock_connect, tool):
|
||||
import psycopg
|
||||
|
||||
@@ -114,7 +114,7 @@ class TestPostgresExecuteAction:
|
||||
|
||||
mock_conn.close.assert_called_once()
|
||||
|
||||
@patch("application.agents.tools.postgres.psycopg.connect")
|
||||
@patch("docsgpt.agents.tools.postgres.psycopg.connect")
|
||||
def test_select_with_no_description(self, mock_connect, tool):
|
||||
mock_conn = MagicMock()
|
||||
mock_cur = MagicMock()
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"""Tests for application/agents/tools/read_webpage.py"""
|
||||
"""Tests for docsgpt/agents/tools/read_webpage.py"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from application.agents.tools.read_webpage import ReadWebpageTool
|
||||
from application.security.safe_url import ResponseTooLargeError
|
||||
from docsgpt.agents.tools.read_webpage import ReadWebpageTool
|
||||
from docsgpt.security.safe_url import ResponseTooLargeError
|
||||
|
||||
|
||||
class _FakeResponse:
|
||||
@@ -43,7 +43,7 @@ class TestReadWebpageExecuteAction:
|
||||
assert "Error" in result
|
||||
assert "URL parameter is missing" in result
|
||||
|
||||
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
def test_successful_fetch(self, mock_fetch, tool):
|
||||
mock_fetch.return_value = _fetch_result(
|
||||
b"<html><body><h1>Title</h1><p>Content</p></body></html>",
|
||||
@@ -55,7 +55,7 @@ class TestReadWebpageExecuteAction:
|
||||
assert "Title" in result
|
||||
assert "Content" in result
|
||||
|
||||
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
def test_request_error(self, mock_fetch, tool):
|
||||
mock_fetch.side_effect = requests.exceptions.ConnectionError("refused")
|
||||
|
||||
@@ -63,9 +63,9 @@ class TestReadWebpageExecuteAction:
|
||||
|
||||
assert "Error fetching URL" in result
|
||||
|
||||
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
def test_ssrf_blocked(self, mock_fetch, tool):
|
||||
from application.security.safe_url import UnsafeUserUrlError
|
||||
from docsgpt.security.safe_url import UnsafeUserUrlError
|
||||
|
||||
mock_fetch.side_effect = UnsafeUserUrlError("blocked")
|
||||
|
||||
@@ -74,7 +74,7 @@ class TestReadWebpageExecuteAction:
|
||||
assert "Error" in result
|
||||
assert "validation failed" in result
|
||||
|
||||
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
def test_http_error(self, mock_fetch, tool):
|
||||
mock_fetch.return_value = _fetch_result(b"", status_code=404)
|
||||
|
||||
@@ -88,7 +88,7 @@ class TestReadWebpageContentGuards:
|
||||
"""Regression tests for the PDF-as-text incident: a binary body must
|
||||
never come back as NUL-laden 'markdown'."""
|
||||
|
||||
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
def test_rejects_pdf_content_type(self, mock_fetch, tool):
|
||||
mock_fetch.return_value = _fetch_result(
|
||||
b"%PDF-1.7 binary...", content_type="application/pdf",
|
||||
@@ -99,7 +99,7 @@ class TestReadWebpageContentGuards:
|
||||
assert result.startswith("Error")
|
||||
assert "application/pdf" in result
|
||||
|
||||
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
def test_rejects_pdf_magic_without_content_type(self, mock_fetch, tool):
|
||||
# No Content-Type header at all — the 07-17 incident shape.
|
||||
mock_fetch.return_value = _fetch_result(b"%PDF-1.4\n%\xe2\xe3\xcf\xd3...")
|
||||
@@ -108,7 +108,7 @@ class TestReadWebpageContentGuards:
|
||||
|
||||
assert result.startswith("Error")
|
||||
|
||||
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
def test_rejects_nul_laden_body_despite_html_content_type(self, mock_fetch, tool):
|
||||
# Mislabeled binary: content-type lies, the NUL sniff must catch it.
|
||||
mock_fetch.return_value = _fetch_result(
|
||||
@@ -121,7 +121,7 @@ class TestReadWebpageContentGuards:
|
||||
assert result.startswith("Error")
|
||||
assert "\x00" not in result
|
||||
|
||||
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
def test_rejects_octet_stream(self, mock_fetch, tool):
|
||||
mock_fetch.return_value = _fetch_result(
|
||||
b"anything", content_type="application/octet-stream",
|
||||
@@ -132,7 +132,7 @@ class TestReadWebpageContentGuards:
|
||||
assert result.startswith("Error")
|
||||
assert "application/octet-stream" in result
|
||||
|
||||
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
def test_response_too_large(self, mock_fetch, tool):
|
||||
mock_fetch.side_effect = ResponseTooLargeError("body exceeds 10485760 bytes")
|
||||
|
||||
@@ -141,7 +141,7 @@ class TestReadWebpageContentGuards:
|
||||
assert result.startswith("Error")
|
||||
assert "large" in result.lower()
|
||||
|
||||
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
def test_utf8_decoded_without_declared_charset(self, mock_fetch, tool):
|
||||
# text/html with no charset param: requests' RFC-2616 ISO-8859-1
|
||||
# fallback would mojibake this; we must default to UTF-8.
|
||||
@@ -154,7 +154,7 @@ class TestReadWebpageContentGuards:
|
||||
|
||||
assert "café über" in result
|
||||
|
||||
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
def test_declared_charset_respected(self, mock_fetch, tool):
|
||||
mock_fetch.return_value = _fetch_result(
|
||||
"<html><body><p>café</p></body></html>".encode("latin-1"),
|
||||
@@ -165,7 +165,7 @@ class TestReadWebpageContentGuards:
|
||||
|
||||
assert "café" in result
|
||||
|
||||
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
def test_unknown_declared_charset_falls_back_to_utf8(self, mock_fetch, tool):
|
||||
mock_fetch.return_value = _fetch_result(
|
||||
"<html><body><p>ok</p></body></html>".encode("utf-8"),
|
||||
@@ -176,7 +176,7 @@ class TestReadWebpageContentGuards:
|
||||
|
||||
assert "ok" in result
|
||||
|
||||
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
def test_text_plain_allowed(self, mock_fetch, tool):
|
||||
mock_fetch.return_value = _fetch_result(
|
||||
b"plain text document", content_type="text/plain",
|
||||
@@ -186,7 +186,7 @@ class TestReadWebpageContentGuards:
|
||||
|
||||
assert "plain text document" in result
|
||||
|
||||
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
def test_utf16_page_with_declared_charset_allowed(self, mock_fetch, tool):
|
||||
# UTF-16 text is NUL-dense; a correct charset declaration must
|
||||
# win over the NUL sniff (which is for undeclared/mislabeled bodies).
|
||||
@@ -200,7 +200,7 @@ class TestReadWebpageContentGuards:
|
||||
assert "Hello world" in result
|
||||
assert "\x00" not in result
|
||||
|
||||
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
def test_pdf_magic_beats_declared_charset(self, mock_fetch, tool):
|
||||
# The magic-prefix check stays unconditional: a lying
|
||||
# ``text/html; charset=utf-8`` header must not sneak a PDF through.
|
||||
@@ -213,7 +213,7 @@ class TestReadWebpageContentGuards:
|
||||
|
||||
assert result.startswith("Error")
|
||||
|
||||
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
def test_nuls_past_sniff_window_are_stripped(self, mock_fetch, tool):
|
||||
# The sniff only sees the first KB; NULs beyond it must still
|
||||
# never leave the tool (self-contained, not reliant on the
|
||||
@@ -226,7 +226,7 @@ class TestReadWebpageContentGuards:
|
||||
assert "\x00" not in result
|
||||
assert "tail" in result
|
||||
|
||||
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
def test_text_csv_allowed(self, mock_fetch, tool):
|
||||
mock_fetch.return_value = _fetch_result(
|
||||
b"name,qty\nwidget,2", content_type="text/csv",
|
||||
@@ -236,7 +236,7 @@ class TestReadWebpageContentGuards:
|
||||
|
||||
assert "widget" in result
|
||||
|
||||
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
def test_redirect_reported_with_target(self, mock_fetch, tool):
|
||||
# allow_redirects=False (SSRF) means a 3xx would otherwise return
|
||||
# the redirect body as near-empty markdown with no hint.
|
||||
@@ -250,7 +250,7 @@ class TestReadWebpageContentGuards:
|
||||
assert result.startswith("Error")
|
||||
assert "https://example.com/moved-here" in result
|
||||
|
||||
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
def test_redirect_without_location_reported(self, mock_fetch, tool):
|
||||
mock_fetch.return_value = _fetch_result(b"", status_code=302)
|
||||
|
||||
@@ -259,7 +259,7 @@ class TestReadWebpageContentGuards:
|
||||
assert result.startswith("Error")
|
||||
assert "redirect" in result.lower()
|
||||
|
||||
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
|
||||
def test_rss_feed_allowed(self, mock_fetch, tool):
|
||||
mock_fetch.return_value = _fetch_result(
|
||||
b"<rss><channel><title>Feed title</title></channel></rss>",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Comprehensive tests for application/agents/research_agent.py
|
||||
"""Comprehensive tests for docsgpt/agents/research_agent.py
|
||||
|
||||
Covers: CitationManager, ResearchAgent (init, budget, timeout, phases:
|
||||
clarification, planning, research step, synthesis, _extract_text,
|
||||
@@ -11,7 +11,7 @@ from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from application.agents.research_agent import (
|
||||
from docsgpt.agents.research_agent import (
|
||||
COMPLEXITY_CAPS,
|
||||
CitationManager,
|
||||
ResearchAgent,
|
||||
@@ -732,7 +732,7 @@ class TestResearchAgentToolSetup:
|
||||
with patch.object(
|
||||
agent.tool_executor, "get_tools", return_value={}
|
||||
), patch(
|
||||
"application.agents.research_agent.add_internal_search_tool"
|
||||
"docsgpt.agents.research_agent.add_internal_search_tool"
|
||||
) as mock_add:
|
||||
tools = agent._setup_tools()
|
||||
mock_add.assert_called_once()
|
||||
@@ -746,7 +746,7 @@ class TestResearchAgentToolSetup:
|
||||
with patch.object(
|
||||
agent.tool_executor, "get_tools", return_value={}
|
||||
), patch(
|
||||
"application.agents.research_agent.add_internal_search_tool"
|
||||
"docsgpt.agents.research_agent.add_internal_search_tool"
|
||||
) as mock_add:
|
||||
tools = agent._setup_tools()
|
||||
mock_add.assert_called_once()
|
||||
@@ -1113,7 +1113,7 @@ class TestResearchStep:
|
||||
mock_response = Mock()
|
||||
mock_llm.gen = Mock(return_value=mock_response)
|
||||
|
||||
from application.llm.handlers.base import LLMResponse
|
||||
from docsgpt.llm.handlers.base import LLMResponse
|
||||
parsed = LLMResponse(
|
||||
content="Direct answer to the question",
|
||||
tool_calls=[],
|
||||
@@ -1142,7 +1142,7 @@ class TestResearchStep:
|
||||
mock_response2 = Mock()
|
||||
mock_llm.gen = Mock(side_effect=[mock_response1, mock_response2])
|
||||
|
||||
from application.llm.handlers.base import LLMResponse, ToolCall
|
||||
from docsgpt.llm.handlers.base import LLMResponse, ToolCall
|
||||
|
||||
tool_call = ToolCall(id="tc1", name="internal__search", arguments={"query": "python"})
|
||||
parsed_with_tool = LLMResponse(
|
||||
@@ -1238,7 +1238,7 @@ class TestResearchStep:
|
||||
agent._start_time = time.monotonic()
|
||||
mock_llm.token_usage = {"prompt_tokens": 10, "generated_tokens": 5}
|
||||
|
||||
from application.llm.handlers.base import LLMResponse, ToolCall
|
||||
from docsgpt.llm.handlers.base import LLMResponse, ToolCall
|
||||
|
||||
tool_call = ToolCall(id="tc1", name="internal__search", arguments={"query": "test"})
|
||||
|
||||
@@ -1299,7 +1299,7 @@ class TestExecuteStepToolsWithRefinement:
|
||||
"""Tool execution appends messages correctly."""
|
||||
agent = ResearchAgent(**agent_base_params)
|
||||
|
||||
from application.llm.handlers.base import ToolCall
|
||||
from docsgpt.llm.handlers.base import ToolCall
|
||||
|
||||
call = ToolCall(id="tc1", name="internal__search", arguments={"query": "test"})
|
||||
|
||||
@@ -1338,7 +1338,7 @@ class TestExecuteStepToolsWithRefinement:
|
||||
"""When search returns empty twice, adds refinement hint."""
|
||||
agent = ResearchAgent(**agent_base_params)
|
||||
|
||||
from application.llm.handlers.base import ToolCall
|
||||
from docsgpt.llm.handlers.base import ToolCall
|
||||
|
||||
call = ToolCall(id="tc1", name="internal__search", arguments={"query": "test"})
|
||||
|
||||
@@ -1370,7 +1370,7 @@ class TestExecuteStepToolsWithRefinement:
|
||||
"""Non-search tools don't trigger empty search logic."""
|
||||
agent = ResearchAgent(**agent_base_params)
|
||||
|
||||
from application.llm.handlers.base import ToolCall
|
||||
from docsgpt.llm.handlers.base import ToolCall
|
||||
|
||||
call = ToolCall(id="tc1", name="think__think", arguments={"thought": "hmm"})
|
||||
|
||||
|
||||
@@ -12,17 +12,17 @@ import pytest
|
||||
from sqlalchemy import text
|
||||
|
||||
# Pre-import to stabilise the ToolManager.load_tools walk's import order.
|
||||
import application.api.user.tools.mcp # noqa: F401
|
||||
import docsgpt.api.user.tools.mcp # noqa: F401
|
||||
|
||||
from application.agents.default_tools import ( # noqa: E402
|
||||
from docsgpt.agents.default_tools import ( # noqa: E402
|
||||
BUILTIN_AGENT_TOOLS,
|
||||
builtin_agent_tools_for_management,
|
||||
default_tool_id,
|
||||
resolve_tool_by_id,
|
||||
)
|
||||
from application.agents.tool_executor import ToolExecutor # noqa: E402
|
||||
from application.agents.tools.tool_manager import ToolManager # noqa: E402
|
||||
from application.storage.db.repositories.schedules import ( # noqa: E402
|
||||
from docsgpt.agents.tool_executor import ToolExecutor # noqa: E402
|
||||
from docsgpt.agents.tools.tool_manager import ToolManager # noqa: E402
|
||||
from docsgpt.storage.db.repositories.schedules import ( # noqa: E402
|
||||
SchedulesRepository,
|
||||
)
|
||||
|
||||
@@ -36,9 +36,9 @@ def patch_scheduler_sessions(pg_conn):
|
||||
yield pg_conn
|
||||
|
||||
with patch(
|
||||
"application.agents.tools.scheduler.db_session", _ctx,
|
||||
"docsgpt.agents.tools.scheduler.db_session", _ctx,
|
||||
), patch(
|
||||
"application.agents.tools.scheduler.db_readonly", _ctx,
|
||||
"docsgpt.agents.tools.scheduler.db_readonly", _ctx,
|
||||
):
|
||||
yield
|
||||
|
||||
@@ -98,7 +98,7 @@ class TestDualRegistration:
|
||||
"""``scheduler`` is in both registries; same uuid5 resolves either way."""
|
||||
|
||||
def test_scheduler_in_both_registries(self):
|
||||
from application.agents.default_tools import (
|
||||
from docsgpt.agents.default_tools import (
|
||||
BUILTIN_AGENT_TOOLS as BUILTINS,
|
||||
settings,
|
||||
)
|
||||
@@ -106,7 +106,7 @@ class TestDualRegistration:
|
||||
assert "scheduler" in settings.DEFAULT_CHAT_TOOLS
|
||||
|
||||
def test_same_synthetic_id_in_both_paths(self):
|
||||
from application.agents.default_tools import (
|
||||
from docsgpt.agents.default_tools import (
|
||||
builtin_agent_tool_ids,
|
||||
default_tool_ids,
|
||||
)
|
||||
@@ -117,7 +117,7 @@ class TestDualRegistration:
|
||||
|
||||
def test_synthesized_default_tools_includes_scheduler(self):
|
||||
"""Agentless chats see scheduler in the default-tools synthesis."""
|
||||
from application.agents.default_tools import synthesized_default_tools
|
||||
from docsgpt.agents.default_tools import synthesized_default_tools
|
||||
|
||||
rows = synthesized_default_tools(None)
|
||||
names = {r["name"] for r in rows}
|
||||
@@ -125,7 +125,7 @@ class TestDualRegistration:
|
||||
|
||||
def test_synthesized_builtin_agent_tools_includes_scheduler(self):
|
||||
"""Agent picker still sees scheduler via the builtin registry."""
|
||||
from application.agents.default_tools import (
|
||||
from docsgpt.agents.default_tools import (
|
||||
builtin_agent_tools_for_management,
|
||||
)
|
||||
|
||||
@@ -146,7 +146,7 @@ class TestEndToEndAgentPickerToLLMSchema:
|
||||
def _use_conn():
|
||||
yield pg_conn
|
||||
|
||||
with patch("application.agents.tool_executor.db_readonly", _use_conn):
|
||||
with patch("docsgpt.agents.tool_executor.db_readonly", _use_conn):
|
||||
executor = ToolExecutor(
|
||||
user_api_key=agent["key"], user="alice",
|
||||
agent_id=str(agent["id"]),
|
||||
|
||||
@@ -7,7 +7,7 @@ from zoneinfo import ZoneInfo
|
||||
|
||||
import pytest
|
||||
|
||||
from application.agents.scheduler_utils import (
|
||||
from docsgpt.agents.scheduler_utils import (
|
||||
ScheduleValidationError,
|
||||
clamp_once_horizon,
|
||||
cron_interval_seconds,
|
||||
@@ -120,7 +120,7 @@ class TestCronIntervalSeconds:
|
||||
assert cron_interval_seconds("* 9 * * *", None) == 60
|
||||
|
||||
def test_bursty_cron_rejected_when_floor_above_burst(self):
|
||||
from application.core.settings import settings as app_settings
|
||||
from docsgpt.core.settings import settings as app_settings
|
||||
burst = "* 9 * * *"
|
||||
cadence = cron_interval_seconds(burst, None)
|
||||
floor = max(0, int(app_settings.SCHEDULE_MIN_INTERVAL))
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Tests for application/agents/tools/spec_parser.py"""
|
||||
"""Tests for docsgpt/agents/tools/spec_parser.py"""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from application.agents.tools.spec_parser import (
|
||||
from docsgpt.agents.tools.spec_parser import (
|
||||
_extract_metadata,
|
||||
_generate_action_name,
|
||||
_get_base_url,
|
||||
@@ -473,14 +473,14 @@ paths:
|
||||
|
||||
def test_traverse_path_key_error(self):
|
||||
"""Cover lines 173-176: _traverse_path returns None on KeyError."""
|
||||
from application.agents.tools.spec_parser import _traverse_path
|
||||
from docsgpt.agents.tools.spec_parser import _traverse_path
|
||||
|
||||
result = _traverse_path({"a": {"b": 1}}, ["a", "c"])
|
||||
assert result is None
|
||||
|
||||
def test_traverse_path_non_dict_result(self):
|
||||
"""Cover line 175-176: _traverse_path returns None for non-dict result."""
|
||||
from application.agents.tools.spec_parser import _traverse_path
|
||||
from docsgpt.agents.tools.spec_parser import _traverse_path
|
||||
|
||||
result = _traverse_path({"a": "string_value"}, ["a"])
|
||||
assert result is None
|
||||
@@ -529,7 +529,7 @@ class TestSpecParserAdditionalCoverage:
|
||||
|
||||
def test_categorize_params_query_and_header(self):
|
||||
"""Cover lines 205, 209, 213: parameters categorized into query and header."""
|
||||
from application.agents.tools.spec_parser import _categorize_parameters
|
||||
from docsgpt.agents.tools.spec_parser import _categorize_parameters
|
||||
|
||||
parameters = [
|
||||
{"name": "q", "in": "query", "required": True, "description": "Query param"},
|
||||
@@ -543,7 +543,7 @@ class TestSpecParserAdditionalCoverage:
|
||||
|
||||
def test_categorize_params_skips_no_name(self):
|
||||
"""Cover line 205: parameters without name are skipped."""
|
||||
from application.agents.tools.spec_parser import _categorize_parameters
|
||||
from docsgpt.agents.tools.spec_parser import _categorize_parameters
|
||||
|
||||
parameters = [
|
||||
{"in": "query"}, # no name
|
||||
@@ -554,7 +554,7 @@ class TestSpecParserAdditionalCoverage:
|
||||
|
||||
def test_param_to_property_integer_type(self):
|
||||
"""Cover lines 216-217, 222, 228: _param_to_property with integer type."""
|
||||
from application.agents.tools.spec_parser import _param_to_property
|
||||
from docsgpt.agents.tools.spec_parser import _param_to_property
|
||||
|
||||
param = {
|
||||
"name": "count",
|
||||
@@ -569,7 +569,7 @@ class TestSpecParserAdditionalCoverage:
|
||||
|
||||
def test_param_to_property_number_type(self):
|
||||
"""Cover line 222: number type mapped to integer."""
|
||||
from application.agents.tools.spec_parser import _param_to_property
|
||||
from docsgpt.agents.tools.spec_parser import _param_to_property
|
||||
|
||||
param = {
|
||||
"schema": {"type": "number"},
|
||||
@@ -581,7 +581,7 @@ class TestSpecParserAdditionalCoverage:
|
||||
|
||||
def test_param_to_property_string_default(self):
|
||||
"""Cover line 222: unknown type defaults to string."""
|
||||
from application.agents.tools.spec_parser import _param_to_property
|
||||
from docsgpt.agents.tools.spec_parser import _param_to_property
|
||||
|
||||
param = {"description": "Desc", "required": False}
|
||||
prop = _param_to_property(param)
|
||||
@@ -589,7 +589,7 @@ class TestSpecParserAdditionalCoverage:
|
||||
|
||||
def test_param_to_property_description_truncated(self):
|
||||
"""Cover line 228: description truncated to 200 chars."""
|
||||
from application.agents.tools.spec_parser import _param_to_property
|
||||
from docsgpt.agents.tools.spec_parser import _param_to_property
|
||||
|
||||
param = {"description": "x" * 300, "required": False}
|
||||
prop = _param_to_property(param)
|
||||
@@ -603,7 +603,7 @@ class TestSpecParserAdditionalCoverage:
|
||||
# 184-190 (generate_action_name from path), 99 (swagger base URL)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
from application.agents.tools.spec_parser import _extract_actions # noqa: E402
|
||||
from docsgpt.agents.tools.spec_parser import _extract_actions # noqa: E402
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Tests for application/agents/tools/telegram.py"""
|
||||
"""Tests for docsgpt/agents/tools/telegram.py"""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from application.agents.tools.telegram import TelegramTool
|
||||
from docsgpt.agents.tools.telegram import TelegramTool
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -18,7 +18,7 @@ class TestTelegramExecuteAction:
|
||||
with pytest.raises(ValueError, match="Unknown action"):
|
||||
tool.execute_action("invalid")
|
||||
|
||||
@patch("application.agents.tools.telegram.requests.post")
|
||||
@patch("docsgpt.agents.tools.telegram.requests.post")
|
||||
def test_send_message(self, mock_post, tool):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
@@ -35,7 +35,7 @@ class TestTelegramExecuteAction:
|
||||
assert call_args[1]["data"]["text"] == "Hello"
|
||||
assert call_args[1]["data"]["chat_id"] == "12345"
|
||||
|
||||
@patch("application.agents.tools.telegram.requests.post")
|
||||
@patch("docsgpt.agents.tools.telegram.requests.post")
|
||||
def test_send_image(self, mock_post, tool):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
@@ -51,7 +51,7 @@ class TestTelegramExecuteAction:
|
||||
assert "bot123:ABC/sendPhoto" in call_args[0][0]
|
||||
assert call_args[1]["data"]["photo"] == "https://img.com/cat.jpg"
|
||||
|
||||
@patch("application.agents.tools.telegram.requests.post")
|
||||
@patch("docsgpt.agents.tools.telegram.requests.post")
|
||||
def test_api_error_status(self, mock_post, tool):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 403
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Tests for ThinkTool — the chain-of-thought pseudo-tool."""
|
||||
|
||||
import pytest
|
||||
from application.agents.tools.think import (
|
||||
from docsgpt.agents.tools.think import (
|
||||
THINK_TOOL_ENTRY,
|
||||
THINK_TOOL_ID,
|
||||
ThinkTool,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from application.agents.tools.tool_action_parser import ToolActionParser
|
||||
from docsgpt.agents.tools.tool_action_parser import ToolActionParser
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
|
||||
@@ -11,8 +11,8 @@ from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from application.agents.tools.artifact_generator import ArtifactGeneratorTool
|
||||
from application.agents.tools.code_executor import CodeExecutorTool
|
||||
from docsgpt.agents.tools.artifact_generator import ArtifactGeneratorTool
|
||||
from docsgpt.agents.tools.code_executor import CodeExecutorTool
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
|
||||
@@ -19,7 +19,7 @@ from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from application.agents.tool_executor import ToolExecutor
|
||||
from docsgpt.agents.tool_executor import ToolExecutor
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from application.agents.tool_executor import ToolExecutor
|
||||
from docsgpt.agents.tool_executor import ToolExecutor
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -42,12 +42,12 @@ class TestToolExecutorGetTools:
|
||||
yield pg_conn
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tool_executor.db_readonly", _use_pg_conn
|
||||
"docsgpt.agents.tool_executor.db_readonly", _use_pg_conn
|
||||
)
|
||||
|
||||
def test_get_tools_uses_api_key_when_present(self, pg_conn, monkeypatch):
|
||||
from application.storage.db.repositories.agents import AgentsRepository
|
||||
from application.storage.db.repositories.user_tools import UserToolsRepository
|
||||
from docsgpt.storage.db.repositories.agents import AgentsRepository
|
||||
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
|
||||
|
||||
tool = UserToolsRepository(pg_conn).create(user_id="alice", name="tool1")
|
||||
AgentsRepository(pg_conn).create(
|
||||
@@ -69,8 +69,8 @@ class TestToolExecutorGetTools:
|
||||
def test_api_key_agent_never_resolves_workflow_only_builtins(self, pg_conn, monkeypatch):
|
||||
"""read_document is workflow-only: a chat/scheduled agent carrying its
|
||||
builtin id must not get it; workflow nodes get it via allowed_tool_ids."""
|
||||
from application.agents.default_tools import default_tool_id
|
||||
from application.storage.db.repositories.agents import AgentsRepository
|
||||
from docsgpt.agents.default_tools import default_tool_id
|
||||
from docsgpt.storage.db.repositories.agents import AgentsRepository
|
||||
|
||||
read_doc_id = default_tool_id("read_document")
|
||||
artifact_id = default_tool_id("artifact_generator")
|
||||
@@ -94,8 +94,8 @@ class TestToolExecutorGetTools:
|
||||
assert node_names == {"read_document"}
|
||||
|
||||
def test_agentless_chat_synthesizes_defaults(self, pg_conn, monkeypatch):
|
||||
from application.agents.default_tools import loaded_default_tools
|
||||
from application.storage.db.repositories.user_tools import UserToolsRepository
|
||||
from docsgpt.agents.default_tools import loaded_default_tools
|
||||
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
|
||||
|
||||
UserToolsRepository(pg_conn).create(
|
||||
user_id="alice", name="tool1", status=True
|
||||
@@ -114,8 +114,8 @@ class TestToolExecutorGetTools:
|
||||
self, pg_conn, monkeypatch
|
||||
):
|
||||
"""``agent_id`` forces ``agents.tools``-only; no defaults synthesized."""
|
||||
from application.agents.default_tools import loaded_default_tools
|
||||
from application.storage.db.repositories.user_tools import UserToolsRepository
|
||||
from docsgpt.agents.default_tools import loaded_default_tools
|
||||
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
|
||||
|
||||
UserToolsRepository(pg_conn).create(
|
||||
user_id="alice", name="tool1", status=True
|
||||
@@ -129,7 +129,7 @@ class TestToolExecutorGetTools:
|
||||
assert not (set(loaded_default_tools()) & names)
|
||||
|
||||
def test_get_tools_defaults_to_local(self, pg_conn, monkeypatch):
|
||||
from application.agents.default_tools import loaded_default_tools
|
||||
from docsgpt.agents.default_tools import loaded_default_tools
|
||||
|
||||
self._patch_conn(monkeypatch, pg_conn)
|
||||
|
||||
@@ -141,9 +141,9 @@ class TestToolExecutorGetTools:
|
||||
|
||||
def test_api_key_path_excludes_defaults(self, pg_conn, monkeypatch):
|
||||
"""Agent-bound resolution returns exactly ``agents.tools``."""
|
||||
from application.agents.default_tools import loaded_default_tools
|
||||
from application.storage.db.repositories.agents import AgentsRepository
|
||||
from application.storage.db.repositories.user_tools import UserToolsRepository
|
||||
from docsgpt.agents.default_tools import loaded_default_tools
|
||||
from docsgpt.storage.db.repositories.agents import AgentsRepository
|
||||
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
|
||||
|
||||
tool = UserToolsRepository(pg_conn).create(user_id="alice", name="tool1")
|
||||
AgentsRepository(pg_conn).create(
|
||||
@@ -165,7 +165,7 @@ class TestToolExecutorGetTools:
|
||||
self, pg_conn, monkeypatch
|
||||
):
|
||||
"""Empty ``agents.tools`` invoked via API key yields no tools."""
|
||||
from application.storage.db.repositories.agents import AgentsRepository
|
||||
from docsgpt.storage.db.repositories.agents import AgentsRepository
|
||||
|
||||
AgentsRepository(pg_conn).create(
|
||||
user_id="bob",
|
||||
@@ -183,8 +183,8 @@ class TestToolExecutorGetTools:
|
||||
self, pg_conn, monkeypatch
|
||||
):
|
||||
"""Only ``read_webpage`` in ``agents.tools`` -> exactly that; no other defaults bolted on."""
|
||||
from application.agents.default_tools import default_tool_id
|
||||
from application.storage.db.repositories.agents import AgentsRepository
|
||||
from docsgpt.agents.default_tools import default_tool_id
|
||||
from docsgpt.storage.db.repositories.agents import AgentsRepository
|
||||
|
||||
read_webpage_id = default_tool_id("read_webpage")
|
||||
memory_id = default_tool_id("memory")
|
||||
@@ -210,8 +210,8 @@ class TestToolExecutorGetTools:
|
||||
self, pg_conn, monkeypatch
|
||||
):
|
||||
"""A default tool added explicitly to ``agents.tools`` resolves for every caller."""
|
||||
from application.agents.default_tools import default_tool_id
|
||||
from application.storage.db.repositories.agents import AgentsRepository
|
||||
from docsgpt.agents.default_tools import default_tool_id
|
||||
from docsgpt.storage.db.repositories.agents import AgentsRepository
|
||||
|
||||
memory_id = default_tool_id("memory")
|
||||
AgentsRepository(pg_conn).create(
|
||||
@@ -233,7 +233,7 @@ class TestToolExecutorGetTools:
|
||||
def test_no_dedup_between_explicit_and_default_memory(
|
||||
self, pg_conn, monkeypatch
|
||||
):
|
||||
from application.storage.db.repositories.user_tools import UserToolsRepository
|
||||
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
|
||||
|
||||
# Explicit ``memory`` row and the default ``memory`` coexist (separate stores).
|
||||
UserToolsRepository(pg_conn).create(
|
||||
@@ -603,7 +603,7 @@ class TestCheckPauseRemoteDevice:
|
||||
|
||||
def _patch_device(self, monkeypatch, device):
|
||||
"""Stub ``RemoteDeviceTool._load_device`` to return ``device``."""
|
||||
from application.agents.tools import remote_device
|
||||
from docsgpt.agents.tools import remote_device
|
||||
|
||||
monkeypatch.setattr(
|
||||
remote_device.RemoteDeviceTool,
|
||||
@@ -613,7 +613,7 @@ class TestCheckPauseRemoteDevice:
|
||||
|
||||
def _patch_sticky(self, monkeypatch, patterns):
|
||||
"""Stub the sticky lookup to match any normalized pattern in ``patterns``."""
|
||||
from application.agents.tools import remote_device
|
||||
from docsgpt.agents.tools import remote_device
|
||||
|
||||
monkeypatch.setattr(
|
||||
remote_device,
|
||||
@@ -789,7 +789,7 @@ class TestCheckPauseRemoteDeviceHeadless:
|
||||
}
|
||||
|
||||
def _patch_device(self, monkeypatch, device):
|
||||
from application.agents.tools import remote_device
|
||||
from docsgpt.agents.tools import remote_device
|
||||
|
||||
monkeypatch.setattr(
|
||||
remote_device.RemoteDeviceTool,
|
||||
@@ -798,7 +798,7 @@ class TestCheckPauseRemoteDeviceHeadless:
|
||||
)
|
||||
|
||||
def _patch_sticky(self, monkeypatch, patterns):
|
||||
from application.agents.tools import remote_device
|
||||
from docsgpt.agents.tools import remote_device
|
||||
|
||||
monkeypatch.setattr(
|
||||
remote_device,
|
||||
@@ -910,7 +910,7 @@ class TestToolExecutorExecute:
|
||||
executor = ToolExecutor()
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tool_executor.ToolActionParser",
|
||||
"docsgpt.agents.tool_executor.ToolActionParser",
|
||||
lambda _cls, **kw: Mock(parse_args=Mock(return_value=(None, None, {}))),
|
||||
)
|
||||
|
||||
@@ -937,7 +937,7 @@ class TestToolExecutorExecute:
|
||||
executor = ToolExecutor()
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tool_executor.ToolActionParser",
|
||||
"docsgpt.agents.tool_executor.ToolActionParser",
|
||||
lambda _cls, **kw: Mock(parse_args=Mock(return_value=("missing_id", "action", {}))),
|
||||
)
|
||||
|
||||
@@ -963,7 +963,7 @@ class TestToolExecutorExecute:
|
||||
executor = ToolExecutor(user="test_user")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tool_executor.ToolActionParser",
|
||||
"docsgpt.agents.tool_executor.ToolActionParser",
|
||||
lambda _cls, **kw: Mock(parse_args=Mock(return_value=("t1", "test_action", {"param1": "val"}))),
|
||||
)
|
||||
|
||||
@@ -998,7 +998,7 @@ class TestToolExecutorExecute:
|
||||
assert "completed" in statuses
|
||||
|
||||
def test_get_truncated_tool_calls(self):
|
||||
from application.agents.tool_executor import PERSISTED_RESULT_MAX_LEN
|
||||
from docsgpt.agents.tool_executor import PERSISTED_RESULT_MAX_LEN
|
||||
|
||||
executor = ToolExecutor()
|
||||
executor.tool_calls = [
|
||||
@@ -1044,7 +1044,7 @@ class TestToolExecutorExecute:
|
||||
assert truncated[0]["status"] == "error"
|
||||
|
||||
def test_result_status_reflects_in_band_tool_errors(self):
|
||||
from application.agents.tool_executor import result_status
|
||||
from docsgpt.agents.tool_executor import result_status
|
||||
|
||||
assert result_status({"status": "error", "error": "invalid spec"}) == "error"
|
||||
assert result_status({"error": "input artifact A9 not found"}) == "error"
|
||||
@@ -1056,7 +1056,7 @@ class TestToolExecutorExecute:
|
||||
executor = ToolExecutor(user="test_user")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tool_executor.ToolActionParser",
|
||||
"docsgpt.agents.tool_executor.ToolActionParser",
|
||||
lambda _cls, **kw: Mock(parse_args=Mock(return_value=("t1", "test_action", {}))),
|
||||
)
|
||||
|
||||
@@ -1097,7 +1097,7 @@ class TestToolExecutorExecute:
|
||||
executor = ToolExecutor(user="test_user")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tool_executor.ToolActionParser",
|
||||
"docsgpt.agents.tool_executor.ToolActionParser",
|
||||
lambda _cls, **kw: Mock(
|
||||
parse_args=Mock(return_value=("t1", "get_users", {"body_param": "val"}))
|
||||
),
|
||||
@@ -1144,7 +1144,7 @@ class TestToolExecutorExecute:
|
||||
executor = ToolExecutor(user="test_user")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tool_executor.ToolActionParser",
|
||||
"docsgpt.agents.tool_executor.ToolActionParser",
|
||||
lambda _cls, **kw: Mock(
|
||||
parse_args=Mock(return_value=("t1", "act", {}))
|
||||
),
|
||||
@@ -1190,7 +1190,7 @@ class TestToolExecutorExecute:
|
||||
executor = ToolExecutor(user="test_user")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tool_executor.ToolActionParser",
|
||||
"docsgpt.agents.tool_executor.ToolActionParser",
|
||||
lambda _cls, **kw: Mock(
|
||||
parse_args=Mock(return_value=("t1", "act", {"q": "v"}))
|
||||
),
|
||||
@@ -1239,10 +1239,10 @@ class TestToolExecutorExecute:
|
||||
mock_tool = Mock()
|
||||
mock_tm.load_tool.return_value = mock_tool
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tool_executor.ToolManager", lambda config: mock_tm
|
||||
"docsgpt.agents.tool_executor.ToolManager", lambda config: mock_tm
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tool_executor.decrypt_credentials",
|
||||
"docsgpt.agents.tool_executor.decrypt_credentials",
|
||||
lambda creds, user: {"api_key": "decrypted_key"},
|
||||
)
|
||||
|
||||
@@ -1267,7 +1267,7 @@ class TestToolExecutorExecute:
|
||||
mock_tm = Mock()
|
||||
mock_tm.load_tool.return_value = Mock()
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tool_executor.ToolManager", lambda config: mock_tm
|
||||
"docsgpt.agents.tool_executor.ToolManager", lambda config: mock_tm
|
||||
)
|
||||
captured = {}
|
||||
|
||||
@@ -1276,7 +1276,7 @@ class TestToolExecutorExecute:
|
||||
return {"api_key": "owner_secret"}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tool_executor.decrypt_credentials", _fake_decrypt
|
||||
"docsgpt.agents.tool_executor.decrypt_credentials", _fake_decrypt
|
||||
)
|
||||
|
||||
tool_data = {
|
||||
@@ -1298,7 +1298,7 @@ class TestToolExecutorExecute:
|
||||
mock_tool = Mock()
|
||||
mock_tm.load_tool.return_value = mock_tool
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tool_executor.ToolManager", lambda config: mock_tm
|
||||
"docsgpt.agents.tool_executor.ToolManager", lambda config: mock_tm
|
||||
)
|
||||
|
||||
tool_data = {
|
||||
@@ -1334,7 +1334,7 @@ class TestToolExecutorAdditionalCoverage:
|
||||
mock_tool.get_artifact_id.side_effect = RuntimeError("artifact error")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tool_executor.ToolManager",
|
||||
"docsgpt.agents.tool_executor.ToolManager",
|
||||
lambda config: Mock(load_tool=Mock(return_value=mock_tool)),
|
||||
)
|
||||
|
||||
@@ -1375,7 +1375,7 @@ class TestToolExecutorAdditionalCoverage:
|
||||
mock_tool = Mock()
|
||||
mock_tm.load_tool.return_value = mock_tool
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tool_executor.ToolManager", lambda config: mock_tm
|
||||
"docsgpt.agents.tool_executor.ToolManager", lambda config: mock_tm
|
||||
)
|
||||
|
||||
tool_data = {
|
||||
|
||||
@@ -11,8 +11,8 @@ from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from application.agents.default_tools import default_tool_id
|
||||
from application.agents.tool_executor import ToolExecutor
|
||||
from docsgpt.agents.default_tools import default_tool_id
|
||||
from docsgpt.agents.tool_executor import ToolExecutor
|
||||
|
||||
SYNTHESIZED_ID = default_tool_id("code_executor")
|
||||
STORED_ID = "9f1d4c2e-0000-4000-8000-000000000001"
|
||||
|
||||
@@ -14,7 +14,7 @@ from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from application.agents.tool_executor import ToolExecutor
|
||||
from docsgpt.agents.tool_executor import ToolExecutor
|
||||
|
||||
|
||||
def _action(name):
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from application.agents.tool_executor import ToolExecutor
|
||||
from docsgpt.agents.tool_executor import ToolExecutor
|
||||
|
||||
|
||||
def _call(name: str, args: dict | None = None, call_id: str = "c1"):
|
||||
@@ -97,7 +97,7 @@ class TestNormalModeUnchanged:
|
||||
# ---------------------------------------------------------------------------
|
||||
class TestHeadlessSchedulerExclusion:
|
||||
def test_synthesized_default_tools_drops_scheduler_in_headless(self):
|
||||
from application.agents.default_tools import (
|
||||
from docsgpt.agents.default_tools import (
|
||||
loaded_default_tools,
|
||||
synthesized_default_tools,
|
||||
)
|
||||
@@ -116,8 +116,8 @@ class TestHeadlessSchedulerExclusion:
|
||||
def test_get_user_tools_filters_scheduler_when_headless(
|
||||
self, monkeypatch,
|
||||
):
|
||||
from application.agents import tool_executor as te_module
|
||||
from application.agents.default_tools import (
|
||||
from docsgpt.agents import tool_executor as te_module
|
||||
from docsgpt.agents.default_tools import (
|
||||
default_tool_id,
|
||||
loaded_default_tools,
|
||||
)
|
||||
@@ -164,8 +164,8 @@ class TestHeadlessSchedulerExclusion:
|
||||
):
|
||||
"""An agent-bound headless run (e.g. webhook) skips scheduler even if
|
||||
the author added the synthetic id to ``agents.tools``."""
|
||||
from application.agents import tool_executor as te_module
|
||||
from application.agents.default_tools import default_tool_id
|
||||
from docsgpt.agents import tool_executor as te_module
|
||||
from docsgpt.agents.default_tools import default_tool_id
|
||||
|
||||
sched_id = default_tool_id("scheduler")
|
||||
from contextlib import contextmanager
|
||||
|
||||
@@ -7,7 +7,7 @@ source protects every lane at once.
|
||||
|
||||
import pytest
|
||||
|
||||
from application.agents.tool_executor import (
|
||||
from docsgpt.agents.tool_executor import (
|
||||
RESULT_FULL_MAX_CHARS,
|
||||
bound_result_full,
|
||||
sanitize_tool_result,
|
||||
|
||||
@@ -12,7 +12,7 @@ from unittest.mock import Mock
|
||||
import pytest
|
||||
from sqlalchemy import text
|
||||
|
||||
from application.agents.tool_executor import ToolExecutor
|
||||
from docsgpt.agents.tool_executor import ToolExecutor
|
||||
|
||||
|
||||
@contextmanager
|
||||
@@ -38,10 +38,10 @@ def _patch_db(monkeypatch, pg_conn):
|
||||
yield pg_conn
|
||||
|
||||
targets = (
|
||||
"application.agents.tool_executor",
|
||||
"application.agents.tools.notes",
|
||||
"application.agents.tools.todo_list",
|
||||
"application.storage.db.session",
|
||||
"docsgpt.agents.tool_executor",
|
||||
"docsgpt.agents.tools.notes",
|
||||
"docsgpt.agents.tools.todo_list",
|
||||
"docsgpt.storage.db.session",
|
||||
)
|
||||
for module in targets:
|
||||
monkeypatch.setattr(f"{module}.db_session", _use_pg, raising=False)
|
||||
@@ -94,7 +94,7 @@ class TestExecuteJournaling:
|
||||
"""No reserved message (``save_conversation=False``) → row lands ``confirmed``, not ``executed``."""
|
||||
executor = ToolExecutor(user="u")
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tool_executor.ToolActionParser",
|
||||
"docsgpt.agents.tool_executor.ToolActionParser",
|
||||
lambda _cls, **kw: Mock(
|
||||
parse_args=Mock(return_value=("t1", "test_action", {"q": "v"}))
|
||||
),
|
||||
@@ -119,7 +119,7 @@ class TestExecuteJournaling:
|
||||
self, pg_conn, mock_tool_manager, monkeypatch
|
||||
):
|
||||
"""The executor's message_id is carried onto the journal row, which stays ``executed``."""
|
||||
from application.storage.db.repositories.conversations import (
|
||||
from docsgpt.storage.db.repositories.conversations import (
|
||||
ConversationsRepository,
|
||||
)
|
||||
|
||||
@@ -138,7 +138,7 @@ class TestExecuteJournaling:
|
||||
executor = ToolExecutor(user="u")
|
||||
executor.message_id = message_uuid
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tool_executor.ToolActionParser",
|
||||
"docsgpt.agents.tool_executor.ToolActionParser",
|
||||
lambda _cls, **kw: Mock(
|
||||
parse_args=Mock(return_value=("t1", "test_action", {}))
|
||||
),
|
||||
@@ -163,7 +163,7 @@ class TestExecuteJournaling:
|
||||
that reuses a deterministic call id (``functions.create_artifact:0``)
|
||||
across turns must journal a distinct row per turn, not silently drop the
|
||||
later one on the table-wide ``call_id`` primary key."""
|
||||
from application.storage.db.repositories.conversations import (
|
||||
from docsgpt.storage.db.repositories.conversations import (
|
||||
ConversationsRepository,
|
||||
)
|
||||
|
||||
@@ -178,7 +178,7 @@ class TestExecuteJournaling:
|
||||
request_id="req-c2", status="pending",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tool_executor.ToolActionParser",
|
||||
"docsgpt.agents.tool_executor.ToolActionParser",
|
||||
lambda _cls, **kw: Mock(
|
||||
parse_args=Mock(return_value=("t1", "test_action", {}))
|
||||
),
|
||||
@@ -206,7 +206,7 @@ class TestExecuteJournaling:
|
||||
):
|
||||
executor = ToolExecutor(user="u")
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tool_executor.ToolActionParser",
|
||||
"docsgpt.agents.tool_executor.ToolActionParser",
|
||||
lambda _cls, **kw: Mock(
|
||||
parse_args=Mock(return_value=("t1", "test_action", {}))
|
||||
),
|
||||
@@ -229,7 +229,7 @@ class TestExecuteJournaling:
|
||||
@pytest.mark.unit
|
||||
class TestRepository:
|
||||
def test_proposed_then_confirmed_when_no_message(self, pg_conn):
|
||||
from application.storage.db.repositories.tool_call_attempts import (
|
||||
from docsgpt.storage.db.repositories.tool_call_attempts import (
|
||||
ToolCallAttemptsRepository,
|
||||
)
|
||||
|
||||
@@ -247,10 +247,10 @@ class TestRepository:
|
||||
assert row["result"] == {"result": {"out": "ok"}}
|
||||
|
||||
def test_mark_executed_with_message_stays_executed(self, pg_conn):
|
||||
from application.storage.db.repositories.conversations import (
|
||||
from docsgpt.storage.db.repositories.conversations import (
|
||||
ConversationsRepository,
|
||||
)
|
||||
from application.storage.db.repositories.tool_call_attempts import (
|
||||
from docsgpt.storage.db.repositories.tool_call_attempts import (
|
||||
ToolCallAttemptsRepository,
|
||||
)
|
||||
|
||||
@@ -277,7 +277,7 @@ class TestRepository:
|
||||
|
||||
def test_upsert_executed_without_message_confirms(self, pg_conn):
|
||||
"""``upsert_executed`` (DB-outage fallback) with no ``message_id`` lands ``confirmed``."""
|
||||
from application.storage.db.repositories.tool_call_attempts import (
|
||||
from docsgpt.storage.db.repositories.tool_call_attempts import (
|
||||
ToolCallAttemptsRepository,
|
||||
)
|
||||
|
||||
@@ -289,7 +289,7 @@ class TestRepository:
|
||||
assert row["result"] == {"result": {"out": "ok"}}
|
||||
|
||||
def test_mark_failed_sets_error(self, pg_conn):
|
||||
from application.storage.db.repositories.tool_call_attempts import (
|
||||
from docsgpt.storage.db.repositories.tool_call_attempts import (
|
||||
ToolCallAttemptsRepository,
|
||||
)
|
||||
|
||||
@@ -303,7 +303,7 @@ class TestRepository:
|
||||
def test_mark_failed_leaves_executed_row_untouched(self, pg_conn):
|
||||
"""A late error for a reused ``call_id`` ("call_0"-style) must
|
||||
not flip an already-executed row (see ``mark_failed``)."""
|
||||
from application.storage.db.repositories.tool_call_attempts import (
|
||||
from docsgpt.storage.db.repositories.tool_call_attempts import (
|
||||
ToolCallAttemptsRepository,
|
||||
)
|
||||
|
||||
@@ -320,7 +320,7 @@ class TestRepository:
|
||||
def test_mark_executed_guarded_to_proposed(self, pg_conn):
|
||||
"""A reused ``call_id`` must not flip an already-terminal row back
|
||||
to executed (the status guard mirrors ``mark_failed``)."""
|
||||
from application.storage.db.repositories.tool_call_attempts import (
|
||||
from docsgpt.storage.db.repositories.tool_call_attempts import (
|
||||
ToolCallAttemptsRepository,
|
||||
)
|
||||
|
||||
@@ -336,7 +336,7 @@ class TestRepository:
|
||||
def test_mark_executed_scoped_to_owner(self, pg_conn):
|
||||
"""A colliding ``call_id`` from another tenant can't flip — or
|
||||
read its result into — this user's proposed row."""
|
||||
from application.storage.db.repositories.tool_call_attempts import (
|
||||
from docsgpt.storage.db.repositories.tool_call_attempts import (
|
||||
ToolCallAttemptsRepository,
|
||||
)
|
||||
|
||||
@@ -353,7 +353,7 @@ class TestRepository:
|
||||
assert row["user_id"] == "victim"
|
||||
|
||||
def test_mark_failed_scoped_to_owner(self, pg_conn):
|
||||
from application.storage.db.repositories.tool_call_attempts import (
|
||||
from docsgpt.storage.db.repositories.tool_call_attempts import (
|
||||
ToolCallAttemptsRepository,
|
||||
)
|
||||
|
||||
@@ -368,7 +368,7 @@ class TestRepository:
|
||||
visible to per-user / per-agent analytics (was born unattributed)."""
|
||||
import uuid as _uuid
|
||||
|
||||
from application.storage.db.repositories.tool_call_attempts import (
|
||||
from docsgpt.storage.db.repositories.tool_call_attempts import (
|
||||
ToolCallAttemptsRepository,
|
||||
)
|
||||
|
||||
@@ -385,7 +385,7 @@ class TestRepository:
|
||||
def test_upsert_executed_wont_clobber_other_tenant(self, pg_conn):
|
||||
"""A colliding fallback upsert must not upgrade another tenant's
|
||||
proposed row or overwrite its result."""
|
||||
from application.storage.db.repositories.tool_call_attempts import (
|
||||
from docsgpt.storage.db.repositories.tool_call_attempts import (
|
||||
ToolCallAttemptsRepository,
|
||||
)
|
||||
|
||||
@@ -407,7 +407,7 @@ class TestDefaultToolJournaling:
|
||||
def test_synthetic_tool_id_is_journaled(
|
||||
self, pg_conn, mock_tool_manager, monkeypatch
|
||||
):
|
||||
from application.agents.default_tools import synthesize_default_tool
|
||||
from docsgpt.agents.default_tools import synthesize_default_tool
|
||||
|
||||
memory_row = synthesize_default_tool("memory")
|
||||
assert memory_row is not None
|
||||
@@ -415,7 +415,7 @@ class TestDefaultToolJournaling:
|
||||
|
||||
executor = ToolExecutor(user="u")
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tool_executor.ToolActionParser",
|
||||
"docsgpt.agents.tool_executor.ToolActionParser",
|
||||
lambda _cls, **kw: Mock(
|
||||
parse_args=Mock(
|
||||
return_value=(memory_row["id"], "memory_view", {"path": "/"})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
from application.agents.tools.base import Tool
|
||||
from application.agents.tools.tool_manager import ToolManager
|
||||
from docsgpt.agents.tools.base import Tool
|
||||
from docsgpt.agents.tools.tool_manager import ToolManager
|
||||
|
||||
|
||||
class MockTool(Tool):
|
||||
@@ -22,7 +22,7 @@ class MockTool(Tool):
|
||||
@pytest.mark.unit
|
||||
class TestToolManager:
|
||||
|
||||
@patch("application.agents.tools.tool_manager.pkgutil.iter_modules")
|
||||
@patch("docsgpt.agents.tools.tool_manager.pkgutil.iter_modules")
|
||||
def test_tool_manager_initialization(self, mock_iter):
|
||||
mock_iter.return_value = []
|
||||
|
||||
@@ -32,8 +32,8 @@ class TestToolManager:
|
||||
assert manager.config == config
|
||||
assert isinstance(manager.tools, dict)
|
||||
|
||||
@patch("application.agents.tools.tool_manager.pkgutil.iter_modules")
|
||||
@patch("application.agents.tools.tool_manager.importlib.import_module")
|
||||
@patch("docsgpt.agents.tools.tool_manager.pkgutil.iter_modules")
|
||||
@patch("docsgpt.agents.tools.tool_manager.importlib.import_module")
|
||||
def test_load_tools_skips_base_and_private(self, mock_import, mock_iter):
|
||||
mock_iter.return_value = [
|
||||
(None, "base", False),
|
||||
@@ -51,7 +51,7 @@ class TestToolManager:
|
||||
assert "base" not in manager.tools
|
||||
assert "__init__" not in manager.tools
|
||||
|
||||
@patch("application.agents.tools.tool_manager.pkgutil.iter_modules")
|
||||
@patch("docsgpt.agents.tools.tool_manager.pkgutil.iter_modules")
|
||||
def test_load_tools_creates_tool_instances(self, mock_iter):
|
||||
mock_iter.return_value = []
|
||||
|
||||
@@ -66,7 +66,7 @@ class TestToolManager:
|
||||
|
||||
def test_load_tool_with_user_id(self):
|
||||
with patch(
|
||||
"application.agents.tools.tool_manager.pkgutil.iter_modules",
|
||||
"docsgpt.agents.tools.tool_manager.pkgutil.iter_modules",
|
||||
return_value=[],
|
||||
):
|
||||
manager = ToolManager({})
|
||||
@@ -85,7 +85,7 @@ class TestToolManager:
|
||||
assert hasattr(tool, "execute_action")
|
||||
assert hasattr(tool, "get_actions_metadata")
|
||||
|
||||
@patch("application.agents.tools.tool_manager.pkgutil.iter_modules")
|
||||
@patch("docsgpt.agents.tools.tool_manager.pkgutil.iter_modules")
|
||||
def test_load_tool_updates_config(self, mock_iter):
|
||||
mock_iter.return_value = []
|
||||
|
||||
@@ -97,8 +97,8 @@ class TestToolManager:
|
||||
assert manager.config["test_tool"] == new_config
|
||||
assert "test_tool" in manager.config
|
||||
|
||||
@patch("application.agents.tools.tool_manager.pkgutil.iter_modules")
|
||||
@patch("application.agents.tools.tool_manager.importlib.import_module")
|
||||
@patch("docsgpt.agents.tools.tool_manager.pkgutil.iter_modules")
|
||||
@patch("docsgpt.agents.tools.tool_manager.importlib.import_module")
|
||||
def test_execute_action_on_loaded_tool(self, mock_import, mock_iter):
|
||||
mock_iter.return_value = [(None, "mock_tool", False)]
|
||||
|
||||
@@ -118,14 +118,14 @@ class TestToolManager:
|
||||
|
||||
def test_execute_action_tool_not_loaded(self):
|
||||
with patch(
|
||||
"application.agents.tools.tool_manager.pkgutil.iter_modules",
|
||||
"docsgpt.agents.tools.tool_manager.pkgutil.iter_modules",
|
||||
return_value=[],
|
||||
):
|
||||
manager = ToolManager({})
|
||||
with pytest.raises(ValueError, match="Tool 'nonexistent' not loaded"):
|
||||
manager.execute_action("nonexistent", "action")
|
||||
|
||||
@patch("application.agents.tools.tool_manager.importlib.import_module")
|
||||
@patch("docsgpt.agents.tools.tool_manager.importlib.import_module")
|
||||
def test_execute_action_with_user_id_for_mcp_tool(self, mock_import):
|
||||
mock_tool = MockTool({})
|
||||
|
||||
@@ -141,7 +141,7 @@ class TestToolManager:
|
||||
|
||||
mock_load.assert_called_once_with("mcp_tool", {}, "user123")
|
||||
|
||||
@patch("application.agents.tools.tool_manager.importlib.import_module")
|
||||
@patch("docsgpt.agents.tools.tool_manager.importlib.import_module")
|
||||
def test_execute_action_with_user_id_for_memory_tool(self, mock_import):
|
||||
mock_tool = MockTool({})
|
||||
|
||||
@@ -157,8 +157,8 @@ class TestToolManager:
|
||||
|
||||
mock_load.assert_called_once_with("memory", {}, "user456")
|
||||
|
||||
@patch("application.agents.tools.tool_manager.pkgutil.iter_modules")
|
||||
@patch("application.agents.tools.tool_manager.importlib.import_module")
|
||||
@patch("docsgpt.agents.tools.tool_manager.pkgutil.iter_modules")
|
||||
@patch("docsgpt.agents.tools.tool_manager.importlib.import_module")
|
||||
def test_get_all_actions_metadata(self, mock_import, mock_iter):
|
||||
mock_iter.return_value = [(None, "tool1", False), (None, "tool2", False)]
|
||||
|
||||
@@ -177,7 +177,7 @@ class TestToolManager:
|
||||
assert {"name": "action1"} in metadata
|
||||
assert {"name": "action2"} in metadata
|
||||
|
||||
@patch("application.agents.tools.tool_manager.pkgutil.iter_modules")
|
||||
@patch("docsgpt.agents.tools.tool_manager.pkgutil.iter_modules")
|
||||
def test_get_all_actions_metadata_empty(self, mock_iter):
|
||||
mock_iter.return_value = []
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests for application/agents/workflow_agent.py graph loading and saving.
|
||||
"""Tests for docsgpt/agents/workflow_agent.py graph loading and saving.
|
||||
|
||||
Tests _parse_embedded_workflow, _load_from_database, and _finalize_workflow_run
|
||||
against the ephemeral ``pg_conn`` fixture. Agent construction is bypassed via
|
||||
@@ -17,7 +17,7 @@ import pytest
|
||||
def _make_agent(*, workflow_id=None, workflow=None, workflow_owner=None,
|
||||
decoded_token=None):
|
||||
"""Construct a WorkflowAgent bypassing BaseAgent.__init__."""
|
||||
from application.agents.workflow_agent import WorkflowAgent
|
||||
from docsgpt.agents.workflow_agent import WorkflowAgent
|
||||
agent = WorkflowAgent.__new__(WorkflowAgent)
|
||||
agent.workflow_id = workflow_id
|
||||
agent.workflow_owner = workflow_owner
|
||||
@@ -36,9 +36,9 @@ def _patch_db(conn):
|
||||
yield conn
|
||||
|
||||
with patch(
|
||||
"application.agents.workflow_agent.db_readonly", _yield
|
||||
"docsgpt.agents.workflow_agent.db_readonly", _yield
|
||||
), patch(
|
||||
"application.agents.workflow_agent.db_session", _yield
|
||||
"docsgpt.agents.workflow_agent.db_session", _yield
|
||||
):
|
||||
yield
|
||||
|
||||
@@ -111,7 +111,7 @@ class TestLoadWorkflowGraph:
|
||||
assert got is not None
|
||||
|
||||
def test_uses_database_when_id_set(self, pg_conn):
|
||||
from application.storage.db.repositories.workflows import (
|
||||
from docsgpt.storage.db.repositories.workflows import (
|
||||
WorkflowsRepository,
|
||||
)
|
||||
user = "u-loadwf"
|
||||
@@ -135,7 +135,7 @@ class TestLoadFromDatabase:
|
||||
assert agent._load_from_database() is None
|
||||
|
||||
def test_owner_from_decoded_token(self, pg_conn):
|
||||
from application.storage.db.repositories.workflows import (
|
||||
from docsgpt.storage.db.repositories.workflows import (
|
||||
WorkflowsRepository,
|
||||
)
|
||||
user = "u-token-owner"
|
||||
@@ -159,7 +159,7 @@ class TestLoadFromDatabase:
|
||||
|
||||
def test_invalid_version_falls_back_to_1(self, pg_conn):
|
||||
"""When current_graph_version is invalid it falls back to 1."""
|
||||
from application.storage.db.repositories.workflows import (
|
||||
from docsgpt.storage.db.repositories.workflows import (
|
||||
WorkflowsRepository,
|
||||
)
|
||||
user = "u-bad-version"
|
||||
@@ -179,7 +179,7 @@ class TestLoadFromDatabase:
|
||||
|
||||
agent = _make_agent(workflow_id="x", workflow_owner="u")
|
||||
with patch(
|
||||
"application.agents.workflow_agent.db_readonly", _broken
|
||||
"docsgpt.agents.workflow_agent.db_readonly", _broken
|
||||
):
|
||||
got = agent._load_from_database()
|
||||
assert got is None
|
||||
@@ -214,10 +214,10 @@ class TestSaveWorkflowRun:
|
||||
agent._finalize_workflow_run(agent.workflow_owner, agent.workflow_owner, None, "q")
|
||||
|
||||
def test_creates_run_row(self, pg_conn):
|
||||
from application.storage.db.repositories.workflows import (
|
||||
from docsgpt.storage.db.repositories.workflows import (
|
||||
WorkflowsRepository,
|
||||
)
|
||||
from application.storage.db.repositories.workflow_runs import (
|
||||
from docsgpt.storage.db.repositories.workflow_runs import (
|
||||
WorkflowRunsRepository,
|
||||
)
|
||||
|
||||
@@ -259,7 +259,7 @@ class TestSaveWorkflowRun:
|
||||
yield
|
||||
|
||||
with patch(
|
||||
"application.agents.workflow_agent.db_session", _broken
|
||||
"docsgpt.agents.workflow_agent.db_session", _broken
|
||||
):
|
||||
# Should not raise
|
||||
agent._finalize_workflow_run(agent.workflow_owner, agent.workflow_owner, None, "q")
|
||||
@@ -267,13 +267,13 @@ class TestSaveWorkflowRun:
|
||||
|
||||
class TestDetermineRunStatus:
|
||||
def test_completed_when_no_engine(self):
|
||||
from application.agents.workflows.schemas import ExecutionStatus
|
||||
from docsgpt.agents.workflows.schemas import ExecutionStatus
|
||||
|
||||
agent = _make_agent()
|
||||
assert agent._determine_run_status() == ExecutionStatus.COMPLETED
|
||||
|
||||
def test_completed_when_log_empty(self):
|
||||
from application.agents.workflows.schemas import ExecutionStatus
|
||||
from docsgpt.agents.workflows.schemas import ExecutionStatus
|
||||
|
||||
agent = _make_agent()
|
||||
agent._engine = MagicMock()
|
||||
@@ -281,7 +281,7 @@ class TestDetermineRunStatus:
|
||||
assert agent._determine_run_status() == ExecutionStatus.COMPLETED
|
||||
|
||||
def test_failed_if_any_log_failed(self):
|
||||
from application.agents.workflows.schemas import ExecutionStatus
|
||||
from docsgpt.agents.workflows.schemas import ExecutionStatus
|
||||
|
||||
agent = _make_agent()
|
||||
agent._engine = MagicMock()
|
||||
@@ -343,14 +343,14 @@ class TestAgentNodeApprovalPause:
|
||||
"""A node agent whose tool pauses for approval must fail the node visibly, not emit empty output."""
|
||||
|
||||
def test_tool_calls_pending_raises_clear_error(self, monkeypatch):
|
||||
from application.agents.workflows import workflow_engine as we
|
||||
from application.agents.workflows.schemas import (
|
||||
from docsgpt.agents.workflows import workflow_engine as we
|
||||
from docsgpt.agents.workflows.schemas import (
|
||||
NodeType,
|
||||
Workflow,
|
||||
WorkflowGraph,
|
||||
WorkflowNode,
|
||||
)
|
||||
from application.agents.workflows.workflow_engine import WorkflowEngine
|
||||
from docsgpt.agents.workflows.workflow_engine import WorkflowEngine
|
||||
|
||||
# An ephemeral node agent whose LLM handler yields the pause signal and ends,
|
||||
# emitting no "answer". Previously the engine dropped it and the node completed
|
||||
|
||||
@@ -39,8 +39,8 @@ def _make_agent(**overrides):
|
||||
"json_schema": None,
|
||||
}
|
||||
defaults.update(overrides)
|
||||
with patch("application.agents.workflow_agent.log_activity", lambda **kw: lambda f: f):
|
||||
from application.agents.workflow_agent import WorkflowAgent
|
||||
with patch("docsgpt.agents.workflow_agent.log_activity", lambda **kw: lambda f: f):
|
||||
from docsgpt.agents.workflow_agent import WorkflowAgent
|
||||
|
||||
agent = WorkflowAgent(**defaults)
|
||||
return agent
|
||||
@@ -114,7 +114,7 @@ class TestDetermineRunStatusPg:
|
||||
|
||||
def test_completed_value_matches_pg_enum(self):
|
||||
"""The string stored in Postgres must match ExecutionStatus.COMPLETED.value."""
|
||||
from application.agents.workflows.schemas import ExecutionStatus
|
||||
from docsgpt.agents.workflows.schemas import ExecutionStatus
|
||||
|
||||
agent = _make_agent()
|
||||
agent._engine = MagicMock()
|
||||
@@ -125,7 +125,7 @@ class TestDetermineRunStatusPg:
|
||||
assert status.value == "completed"
|
||||
|
||||
def test_failed_value_matches_pg_enum(self):
|
||||
from application.agents.workflows.schemas import ExecutionStatus
|
||||
from docsgpt.agents.workflows.schemas import ExecutionStatus
|
||||
|
||||
agent = _make_agent()
|
||||
agent._engine = MagicMock()
|
||||
|
||||
@@ -7,16 +7,16 @@ from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from application.agents.agentic_agent import AgenticAgent
|
||||
from application.agents.classic_agent import ClassicAgent
|
||||
from application.agents.research_agent import ResearchAgent
|
||||
from application.agents.workflows.node_agent import (
|
||||
from docsgpt.agents.agentic_agent import AgenticAgent
|
||||
from docsgpt.agents.classic_agent import ClassicAgent
|
||||
from docsgpt.agents.research_agent import ResearchAgent
|
||||
from docsgpt.agents.workflows.node_agent import (
|
||||
WorkflowNodeAgenticAgent,
|
||||
WorkflowNodeAgentFactory,
|
||||
WorkflowNodeClassicAgent,
|
||||
WorkflowNodeResearchAgent,
|
||||
)
|
||||
from application.agents.workflows.schemas import (
|
||||
from docsgpt.agents.workflows.schemas import (
|
||||
AgentNodeConfig,
|
||||
AgentType,
|
||||
NodeType,
|
||||
@@ -24,7 +24,7 @@ from application.agents.workflows.schemas import (
|
||||
WorkflowGraph,
|
||||
WorkflowNode,
|
||||
)
|
||||
from application.agents.workflows.workflow_engine import WorkflowEngine
|
||||
from docsgpt.agents.workflows.workflow_engine import WorkflowEngine
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -205,7 +205,7 @@ class TestWorkflowEngineAgenticNode:
|
||||
staticmethod(capture_create),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_api_key_for_provider",
|
||||
"docsgpt.core.model_utils.get_api_key_for_provider",
|
||||
lambda _provider: None,
|
||||
)
|
||||
|
||||
@@ -243,7 +243,7 @@ class TestWorkflowEngineAgenticNode:
|
||||
staticmethod(capture_create),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_api_key_for_provider",
|
||||
"docsgpt.core.model_utils.get_api_key_for_provider",
|
||||
lambda _provider: None,
|
||||
)
|
||||
|
||||
@@ -276,7 +276,7 @@ class TestWorkflowEngineAgenticNode:
|
||||
staticmethod(capture_create),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_api_key_for_provider",
|
||||
"docsgpt.core.model_utils.get_api_key_for_provider",
|
||||
lambda _provider: None,
|
||||
)
|
||||
|
||||
@@ -309,7 +309,7 @@ class TestWorkflowEngineResearchNode:
|
||||
staticmethod(lambda **kwargs: StubNodeAgent(node_events)),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_api_key_for_provider",
|
||||
"docsgpt.core.model_utils.get_api_key_for_provider",
|
||||
lambda _provider: None,
|
||||
)
|
||||
|
||||
@@ -346,7 +346,7 @@ class TestWorkflowEngineResearchNode:
|
||||
staticmethod(capture_create),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_api_key_for_provider",
|
||||
"docsgpt.core.model_utils.get_api_key_for_provider",
|
||||
lambda _provider: None,
|
||||
)
|
||||
|
||||
@@ -375,7 +375,7 @@ class TestWorkflowEngineResearchNode:
|
||||
staticmethod(lambda **kwargs: StubNodeAgent(node_events)),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_api_key_for_provider",
|
||||
"docsgpt.core.model_utils.get_api_key_for_provider",
|
||||
lambda _provider: None,
|
||||
)
|
||||
|
||||
@@ -414,7 +414,7 @@ class TestWorkflowEngineClassicNodeNoRetrieverConfig:
|
||||
staticmethod(capture_create),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_api_key_for_provider",
|
||||
"docsgpt.core.model_utils.get_api_key_for_provider",
|
||||
lambda _provider: None,
|
||||
)
|
||||
|
||||
@@ -448,7 +448,7 @@ class TestWorkflowEngineStreamingEvents:
|
||||
staticmethod(lambda **kwargs: StubNodeAgent(node_events)),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_api_key_for_provider",
|
||||
"docsgpt.core.model_utils.get_api_key_for_provider",
|
||||
lambda _provider: None,
|
||||
)
|
||||
|
||||
@@ -475,7 +475,7 @@ class TestWorkflowEngineStreamingEvents:
|
||||
staticmethod(lambda **kwargs: StubNodeAgent(node_events)),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_api_key_for_provider",
|
||||
"docsgpt.core.model_utils.get_api_key_for_provider",
|
||||
lambda _provider: None,
|
||||
)
|
||||
|
||||
@@ -510,7 +510,7 @@ class TestWorkflowNodeSourceAuthorization:
|
||||
"""``_authorized_node_sources`` opens a connection; don't need a real one."""
|
||||
import contextlib
|
||||
|
||||
import application.storage.db.session as session
|
||||
import docsgpt.storage.db.session as session
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _conn():
|
||||
@@ -519,7 +519,7 @@ class TestWorkflowNodeSourceAuthorization:
|
||||
monkeypatch.setattr(session, "db_readonly", _conn)
|
||||
|
||||
def test_owner_sources_survive(self, monkeypatch):
|
||||
import application.api.user.team_sharing as ts
|
||||
import docsgpt.api.user.team_sharing as ts
|
||||
|
||||
self._stub_db(monkeypatch)
|
||||
monkeypatch.setattr(ts, "can_access", lambda *a, **k: True)
|
||||
@@ -527,7 +527,7 @@ class TestWorkflowNodeSourceAuthorization:
|
||||
assert engine._authorized_node_sources(["s1", "s2"]) == ["s1", "s2"]
|
||||
|
||||
def test_foreign_sources_are_dropped(self, monkeypatch):
|
||||
import application.api.user.team_sharing as ts
|
||||
import docsgpt.api.user.team_sharing as ts
|
||||
|
||||
self._stub_db(monkeypatch)
|
||||
monkeypatch.setattr(ts, "can_access", lambda conn, k, sid, u: sid == "mine")
|
||||
@@ -542,7 +542,7 @@ class TestWorkflowNodeSourceAuthorization:
|
||||
assert engine._authorized_node_sources(["s1"]) == []
|
||||
|
||||
def test_authorization_error_fails_closed(self, monkeypatch):
|
||||
import application.api.user.team_sharing as ts
|
||||
import docsgpt.api.user.team_sharing as ts
|
||||
|
||||
def _boom(*a, **k):
|
||||
raise RuntimeError("db down")
|
||||
@@ -576,7 +576,7 @@ class TestWorkflowNodeDocumentsReachTheAgent:
|
||||
return docs
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.retriever.retriever_creator.RetrieverCreator.create_retriever",
|
||||
"docsgpt.retriever.retriever_creator.RetrieverCreator.create_retriever",
|
||||
lambda *a, **k: _R(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
@@ -599,7 +599,7 @@ class TestWorkflowNodeDocumentsReachTheAgent:
|
||||
raise RuntimeError("vector store down")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.retriever.retriever_creator.RetrieverCreator.create_retriever",
|
||||
"docsgpt.retriever.retriever_creator.RetrieverCreator.create_retriever",
|
||||
_boom,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
|
||||
@@ -10,16 +10,16 @@ from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from application.agents.workflow_agent import WorkflowAgent
|
||||
from application.agents.workflows.cel_evaluator import evaluate_cel
|
||||
from application.agents.workflows.schemas import (
|
||||
from docsgpt.agents.workflow_agent import WorkflowAgent
|
||||
from docsgpt.agents.workflows.cel_evaluator import evaluate_cel
|
||||
from docsgpt.agents.workflows.schemas import (
|
||||
NodeType,
|
||||
Workflow,
|
||||
WorkflowEdge,
|
||||
WorkflowGraph,
|
||||
WorkflowNode,
|
||||
)
|
||||
from application.agents.workflows.workflow_engine import WorkflowEngine
|
||||
from docsgpt.agents.workflows.workflow_engine import WorkflowEngine
|
||||
|
||||
|
||||
def _engine() -> WorkflowEngine:
|
||||
@@ -118,14 +118,14 @@ def patch_sandbox(monkeypatch):
|
||||
return list(state["captured"])
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.sandbox.sandbox_creator.SandboxCreator.get_manager", _get_manager
|
||||
"docsgpt.sandbox.sandbox_creator.SandboxCreator.get_manager", _get_manager
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.sandbox.artifacts_capture.snapshot_signatures",
|
||||
"docsgpt.sandbox.artifacts_capture.snapshot_signatures",
|
||||
lambda *a, **k: state.__setitem__("snapshot_calls", state["snapshot_calls"] + 1) or {},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.sandbox.artifacts_capture.capture_artifacts", _capture
|
||||
"docsgpt.sandbox.artifacts_capture.capture_artifacts", _capture
|
||||
)
|
||||
state["manager_holder"] = manager_holder
|
||||
return state
|
||||
@@ -206,16 +206,16 @@ def test_execute_closes_run_session_once_at_end(monkeypatch):
|
||||
"""The run-scoped sandbox session opened by a code node is closed once when execute() ends."""
|
||||
manager = _FakeManager(_Result(ok=True, stdout="ok"))
|
||||
monkeypatch.setattr(
|
||||
"application.sandbox.sandbox_creator.SandboxCreator.get_manager", lambda: manager
|
||||
"docsgpt.sandbox.sandbox_creator.SandboxCreator.get_manager", lambda: manager
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.sandbox.sandbox_creator.SandboxCreator.peek_manager", lambda: manager
|
||||
"docsgpt.sandbox.sandbox_creator.SandboxCreator.peek_manager", lambda: manager
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.sandbox.artifacts_capture.snapshot_signatures", lambda *a, **k: {}
|
||||
"docsgpt.sandbox.artifacts_capture.snapshot_signatures", lambda *a, **k: {}
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.sandbox.artifacts_capture.capture_artifacts", lambda *a, **k: []
|
||||
"docsgpt.sandbox.artifacts_capture.capture_artifacts", lambda *a, **k: []
|
||||
)
|
||||
|
||||
start = WorkflowNode(
|
||||
@@ -354,7 +354,7 @@ def test_code_node_json_schema_merges_artifact_reference(patch_sandbox):
|
||||
|
||||
|
||||
def test_code_node_timeout_clamped_to_cap(patch_sandbox, monkeypatch):
|
||||
from application.core.settings import settings
|
||||
from docsgpt.core.settings import settings
|
||||
|
||||
monkeypatch.setattr(settings, "SANDBOX_EXEC_TIMEOUT", 30, raising=False)
|
||||
engine = _engine()
|
||||
@@ -378,11 +378,11 @@ def test_materialize_code_inputs_rejects_oversize(monkeypatch):
|
||||
"""A code-node input whose declared version ``size`` exceeds the cap raises before staging."""
|
||||
from contextlib import contextmanager
|
||||
|
||||
from application.core.settings import settings
|
||||
from docsgpt.core.settings import settings
|
||||
|
||||
monkeypatch.setattr(settings, "SANDBOX_MAX_INPUT_BYTES", 100, raising=False)
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tools.artifact_ref.resolve_artifact_id",
|
||||
"docsgpt.agents.tools.artifact_ref.resolve_artifact_id",
|
||||
lambda repo, raw, **k: str(raw),
|
||||
)
|
||||
|
||||
@@ -405,11 +405,11 @@ def test_materialize_code_inputs_rejects_oversize(monkeypatch):
|
||||
raise AssertionError("bytes must not be read when declared size exceeds the cap")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.storage.db.repositories.artifacts.ArtifactsRepository", _Repo
|
||||
"docsgpt.storage.db.repositories.artifacts.ArtifactsRepository", _Repo
|
||||
)
|
||||
monkeypatch.setattr("application.storage.db.session.db_readonly", _readonly)
|
||||
monkeypatch.setattr("docsgpt.storage.db.session.db_readonly", _readonly)
|
||||
monkeypatch.setattr(
|
||||
"application.storage.storage_creator.StorageCreator.get_storage",
|
||||
"docsgpt.storage.storage_creator.StorageCreator.get_storage",
|
||||
staticmethod(lambda: _Storage()),
|
||||
)
|
||||
|
||||
@@ -425,7 +425,7 @@ def test_materialize_code_inputs_dedupes_same_filename(monkeypatch):
|
||||
from contextlib import contextmanager
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tools.artifact_ref.resolve_artifact_id",
|
||||
"docsgpt.agents.tools.artifact_ref.resolve_artifact_id",
|
||||
lambda repo, raw, **k: str(raw),
|
||||
)
|
||||
|
||||
@@ -451,11 +451,11 @@ def test_materialize_code_inputs_dedupes_same_filename(monkeypatch):
|
||||
return io.BytesIO(path.encode())
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.storage.db.repositories.artifacts.ArtifactsRepository", _Repo
|
||||
"docsgpt.storage.db.repositories.artifacts.ArtifactsRepository", _Repo
|
||||
)
|
||||
monkeypatch.setattr("application.storage.db.session.db_readonly", _readonly)
|
||||
monkeypatch.setattr("docsgpt.storage.db.session.db_readonly", _readonly)
|
||||
monkeypatch.setattr(
|
||||
"application.storage.storage_creator.StorageCreator.get_storage",
|
||||
"docsgpt.storage.storage_creator.StorageCreator.get_storage",
|
||||
staticmethod(lambda: _Storage()),
|
||||
)
|
||||
|
||||
|
||||
@@ -3,16 +3,16 @@ from typing import Any, Dict, Optional
|
||||
|
||||
import pytest
|
||||
|
||||
from application.api.user.workflows import routes as workflow_routes
|
||||
from application.agents.workflows.node_agent import WorkflowNodeAgentFactory
|
||||
from application.agents.workflows.schemas import (
|
||||
from docsgpt.api.user.workflows import routes as workflow_routes
|
||||
from docsgpt.agents.workflows.node_agent import WorkflowNodeAgentFactory
|
||||
from docsgpt.agents.workflows.schemas import (
|
||||
NodeType,
|
||||
Workflow,
|
||||
WorkflowGraph,
|
||||
WorkflowNode,
|
||||
)
|
||||
from application.agents.workflows.workflow_engine import WorkflowEngine
|
||||
from application.api.user.workflows.routes import validate_workflow_structure
|
||||
from docsgpt.agents.workflows.workflow_engine import WorkflowEngine
|
||||
from docsgpt.api.user.workflows.routes import validate_workflow_structure
|
||||
|
||||
|
||||
class StubNodeAgent:
|
||||
@@ -81,7 +81,7 @@ def test_execute_agent_node_saves_structured_output_as_json(monkeypatch):
|
||||
staticmethod(lambda **kwargs: StubNodeAgent(node_events)),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_api_key_for_provider",
|
||||
"docsgpt.core.model_utils.get_api_key_for_provider",
|
||||
lambda _provider: None,
|
||||
)
|
||||
|
||||
@@ -111,11 +111,11 @@ def test_execute_agent_node_normalizes_wrapped_schema_before_agent_create(monkey
|
||||
staticmethod(create_node_agent),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_api_key_for_provider",
|
||||
"docsgpt.core.model_utils.get_api_key_for_provider",
|
||||
lambda _provider: None,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_model_capabilities",
|
||||
"docsgpt.core.model_utils.get_model_capabilities",
|
||||
lambda _model_id, **_kwargs: {"supports_structured_output": True},
|
||||
)
|
||||
|
||||
@@ -136,7 +136,7 @@ def test_execute_agent_node_falls_back_to_text_when_schema_not_configured(monkey
|
||||
staticmethod(lambda **kwargs: StubNodeAgent(node_events)),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_api_key_for_provider",
|
||||
"docsgpt.core.model_utils.get_api_key_for_provider",
|
||||
lambda _provider: None,
|
||||
)
|
||||
|
||||
@@ -381,11 +381,11 @@ def test_execute_agent_node_raises_when_structured_output_violates_schema(monkey
|
||||
staticmethod(lambda **kwargs: StubNodeAgent(node_events)),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_api_key_for_provider",
|
||||
"docsgpt.core.model_utils.get_api_key_for_provider",
|
||||
lambda _provider: None,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_model_capabilities",
|
||||
"docsgpt.core.model_utils.get_model_capabilities",
|
||||
lambda _model_id, **_kwargs: {"supports_structured_output": True},
|
||||
)
|
||||
|
||||
@@ -407,11 +407,11 @@ def test_execute_agent_node_raises_when_schema_set_and_response_not_json(monkeyp
|
||||
staticmethod(lambda **kwargs: StubNodeAgent(node_events)),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_api_key_for_provider",
|
||||
"docsgpt.core.model_utils.get_api_key_for_provider",
|
||||
lambda _provider: None,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_model_capabilities",
|
||||
"docsgpt.core.model_utils.get_model_capabilities",
|
||||
lambda _model_id, **_kwargs: {"supports_structured_output": True},
|
||||
)
|
||||
|
||||
@@ -445,15 +445,15 @@ class TestWorkflowEngineAdditionalCoverage:
|
||||
staticmethod(lambda **kwargs: StubNodeAgent(node_events)),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_api_key_for_provider",
|
||||
"docsgpt.core.model_utils.get_api_key_for_provider",
|
||||
lambda _: None,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_provider_from_model_id",
|
||||
"docsgpt.core.model_utils.get_provider_from_model_id",
|
||||
lambda _, **_kwargs: None,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_model_capabilities",
|
||||
"docsgpt.core.model_utils.get_model_capabilities",
|
||||
lambda _, **_kwargs: None,
|
||||
)
|
||||
|
||||
@@ -475,15 +475,15 @@ class TestWorkflowEngineAdditionalCoverage:
|
||||
staticmethod(lambda **kwargs: StubNodeAgent(node_events)),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_api_key_for_provider",
|
||||
"docsgpt.core.model_utils.get_api_key_for_provider",
|
||||
lambda _: "key",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_provider_from_model_id",
|
||||
"docsgpt.core.model_utils.get_provider_from_model_id",
|
||||
lambda _, **_kwargs: "openai",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_model_capabilities",
|
||||
"docsgpt.core.model_utils.get_model_capabilities",
|
||||
lambda _, **_kwargs: None,
|
||||
)
|
||||
|
||||
@@ -501,15 +501,15 @@ class TestWorkflowEngineAdditionalCoverage:
|
||||
node.config["model_id"] = "model-no-struct"
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_api_key_for_provider",
|
||||
"docsgpt.core.model_utils.get_api_key_for_provider",
|
||||
lambda _: "key",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_provider_from_model_id",
|
||||
"docsgpt.core.model_utils.get_provider_from_model_id",
|
||||
lambda _, **_kwargs: "openai",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_model_capabilities",
|
||||
"docsgpt.core.model_utils.get_model_capabilities",
|
||||
lambda _, **_kwargs: {"supports_structured_output": False},
|
||||
)
|
||||
|
||||
@@ -535,15 +535,15 @@ class TestWorkflowEngineAdditionalCoverage:
|
||||
staticmethod(lambda **kwargs: StubNodeAgent(node_events)),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_api_key_for_provider",
|
||||
"docsgpt.core.model_utils.get_api_key_for_provider",
|
||||
lambda _: None,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_provider_from_model_id",
|
||||
"docsgpt.core.model_utils.get_provider_from_model_id",
|
||||
lambda _, **_kwargs: None,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_model_capabilities",
|
||||
"docsgpt.core.model_utils.get_model_capabilities",
|
||||
lambda _, **_kwargs: {"supports_structured_output": True},
|
||||
)
|
||||
|
||||
@@ -566,15 +566,15 @@ class TestWorkflowEngineAdditionalCoverage:
|
||||
staticmethod(lambda **kwargs: StubNodeAgent(node_events)),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_api_key_for_provider",
|
||||
"docsgpt.core.model_utils.get_api_key_for_provider",
|
||||
lambda _: None,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_provider_from_model_id",
|
||||
"docsgpt.core.model_utils.get_provider_from_model_id",
|
||||
lambda _, **_kwargs: None,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_model_capabilities",
|
||||
"docsgpt.core.model_utils.get_model_capabilities",
|
||||
lambda _, **_kwargs: {"supports_structured_output": True},
|
||||
)
|
||||
|
||||
@@ -625,15 +625,15 @@ class TestAgentNodeProviderResolution:
|
||||
WorkflowNodeAgentFactory, "create", staticmethod(_capture)
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_api_key_for_provider",
|
||||
"docsgpt.core.model_utils.get_api_key_for_provider",
|
||||
lambda name: f"key-for-{name}",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_provider_from_model_id",
|
||||
"docsgpt.core.model_utils.get_provider_from_model_id",
|
||||
lambda _, **_kwargs: registry_provider,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_model_capabilities",
|
||||
"docsgpt.core.model_utils.get_model_capabilities",
|
||||
lambda _, **_kwargs: None,
|
||||
)
|
||||
list(engine._execute_agent_node(node))
|
||||
|
||||
@@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from application.agents.workflows.schemas import (
|
||||
from docsgpt.agents.workflows.schemas import (
|
||||
ExecutionStatus,
|
||||
NodeType,
|
||||
WorkflowEdge,
|
||||
@@ -14,7 +14,7 @@ from application.agents.workflows.schemas import (
|
||||
WorkflowNode,
|
||||
Workflow,
|
||||
)
|
||||
from application.agents.workflows.workflow_engine import WorkflowEngine
|
||||
from docsgpt.agents.workflows.workflow_engine import WorkflowEngine
|
||||
|
||||
|
||||
def _make_graph(nodes, edges):
|
||||
@@ -402,8 +402,8 @@ class TestNormalizeNodeJsonSchema:
|
||||
def test_invalid_schema_raises(self):
|
||||
graph = _make_graph([], [])
|
||||
engine = WorkflowEngine(graph, _make_agent())
|
||||
with patch("application.agents.workflows.workflow_engine.normalize_json_schema_payload") as mock_norm:
|
||||
from application.core.json_schema_utils import JsonSchemaValidationError
|
||||
with patch("docsgpt.agents.workflows.workflow_engine.normalize_json_schema_payload") as mock_norm:
|
||||
from docsgpt.core.json_schema_utils import JsonSchemaValidationError
|
||||
mock_norm.side_effect = JsonSchemaValidationError("bad schema")
|
||||
with pytest.raises(ValueError, match="Invalid JSON schema"):
|
||||
engine._normalize_node_json_schema({"bad": True}, "TestNode")
|
||||
@@ -430,7 +430,7 @@ class TestValidateStructuredOutput:
|
||||
def test_no_jsonschema_module(self):
|
||||
graph = _make_graph([], [])
|
||||
engine = WorkflowEngine(graph, _make_agent())
|
||||
with patch("application.agents.workflows.workflow_engine.jsonschema", None):
|
||||
with patch("docsgpt.agents.workflows.workflow_engine.jsonschema", None):
|
||||
engine._validate_structured_output({"type": "object"}, {}) # Should not raise
|
||||
|
||||
|
||||
@@ -449,7 +449,7 @@ class TestFormatTemplate:
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_render_error_returns_raw(self):
|
||||
from application.templates.template_engine import TemplateRenderError
|
||||
from docsgpt.templates.template_engine import TemplateRenderError
|
||||
graph = _make_graph([], [])
|
||||
engine = WorkflowEngine(graph, _make_agent())
|
||||
engine._build_template_context = MagicMock(return_value={})
|
||||
@@ -631,18 +631,18 @@ class TestAgentNodeExecution:
|
||||
mock_agent.gen.return_value = [{"answer": "response"}]
|
||||
|
||||
with patch(
|
||||
"application.agents.workflows.workflow_engine.WorkflowNodeAgentFactory"
|
||||
"docsgpt.agents.workflows.workflow_engine.WorkflowNodeAgentFactory"
|
||||
) as mock_factory, \
|
||||
patch(
|
||||
"application.core.model_utils.get_provider_from_model_id",
|
||||
"docsgpt.core.model_utils.get_provider_from_model_id",
|
||||
return_value="openai",
|
||||
), \
|
||||
patch(
|
||||
"application.core.model_utils.get_api_key_for_provider",
|
||||
"docsgpt.core.model_utils.get_api_key_for_provider",
|
||||
return_value="key",
|
||||
), \
|
||||
patch(
|
||||
"application.core.model_utils.get_model_capabilities",
|
||||
"docsgpt.core.model_utils.get_model_capabilities",
|
||||
return_value=None,
|
||||
):
|
||||
mock_factory.create.return_value = mock_agent
|
||||
@@ -674,18 +674,18 @@ class TestAgentNodeExecution:
|
||||
]
|
||||
|
||||
with patch(
|
||||
"application.agents.workflows.workflow_engine.WorkflowNodeAgentFactory"
|
||||
"docsgpt.agents.workflows.workflow_engine.WorkflowNodeAgentFactory"
|
||||
) as mock_factory, \
|
||||
patch(
|
||||
"application.core.model_utils.get_provider_from_model_id",
|
||||
"docsgpt.core.model_utils.get_provider_from_model_id",
|
||||
return_value="openai",
|
||||
), \
|
||||
patch(
|
||||
"application.core.model_utils.get_api_key_for_provider",
|
||||
"docsgpt.core.model_utils.get_api_key_for_provider",
|
||||
return_value="key",
|
||||
), \
|
||||
patch(
|
||||
"application.core.model_utils.get_model_capabilities",
|
||||
"docsgpt.core.model_utils.get_model_capabilities",
|
||||
return_value={"supports_structured_output": True},
|
||||
):
|
||||
mock_factory.create.return_value = mock_agent
|
||||
@@ -712,15 +712,15 @@ class TestAgentNodeExecution:
|
||||
engine.state = {"query": "test"}
|
||||
|
||||
with patch(
|
||||
"application.core.model_utils.get_provider_from_model_id",
|
||||
"docsgpt.core.model_utils.get_provider_from_model_id",
|
||||
return_value="openai",
|
||||
), \
|
||||
patch(
|
||||
"application.core.model_utils.get_api_key_for_provider",
|
||||
"docsgpt.core.model_utils.get_api_key_for_provider",
|
||||
return_value="key",
|
||||
), \
|
||||
patch(
|
||||
"application.core.model_utils.get_model_capabilities",
|
||||
"docsgpt.core.model_utils.get_model_capabilities",
|
||||
return_value={"supports_structured_output": False},
|
||||
):
|
||||
with pytest.raises(ValueError, match="does not support structured output"):
|
||||
@@ -744,18 +744,18 @@ class TestAgentNodeExecution:
|
||||
mock_agent.gen.return_value = [{"answer": "output text"}]
|
||||
|
||||
with patch(
|
||||
"application.agents.workflows.workflow_engine.WorkflowNodeAgentFactory"
|
||||
"docsgpt.agents.workflows.workflow_engine.WorkflowNodeAgentFactory"
|
||||
) as mock_factory, \
|
||||
patch(
|
||||
"application.core.model_utils.get_provider_from_model_id",
|
||||
"docsgpt.core.model_utils.get_provider_from_model_id",
|
||||
return_value="openai",
|
||||
), \
|
||||
patch(
|
||||
"application.core.model_utils.get_api_key_for_provider",
|
||||
"docsgpt.core.model_utils.get_api_key_for_provider",
|
||||
return_value="key",
|
||||
), \
|
||||
patch(
|
||||
"application.core.model_utils.get_model_capabilities",
|
||||
"docsgpt.core.model_utils.get_model_capabilities",
|
||||
return_value=None,
|
||||
):
|
||||
mock_factory.create.return_value = mock_agent
|
||||
@@ -771,11 +771,11 @@ class TestAgentNodeExecution:
|
||||
import jsonschema as js
|
||||
|
||||
with patch(
|
||||
"application.agents.workflows.workflow_engine.normalize_json_schema_payload",
|
||||
"docsgpt.agents.workflows.workflow_engine.normalize_json_schema_payload",
|
||||
return_value={"type": "invalid_schema_type"},
|
||||
), \
|
||||
patch(
|
||||
"application.agents.workflows.workflow_engine.jsonschema"
|
||||
"docsgpt.agents.workflows.workflow_engine.jsonschema"
|
||||
) as mock_js:
|
||||
mock_js.validate.side_effect = js.exceptions.SchemaError("bad schema")
|
||||
mock_js.exceptions = js.exceptions
|
||||
@@ -1038,15 +1038,15 @@ class TestNodeDocumentManifest:
|
||||
def fake_readonly():
|
||||
yield object()
|
||||
|
||||
monkeypatch.setattr("application.storage.db.session.db_readonly", fake_readonly)
|
||||
monkeypatch.setattr("docsgpt.storage.db.session.db_readonly", fake_readonly)
|
||||
monkeypatch.setattr(
|
||||
"application.storage.db.repositories.artifacts.ArtifactsRepository", FakeRepo
|
||||
"docsgpt.storage.db.repositories.artifacts.ArtifactsRepository", FakeRepo
|
||||
)
|
||||
return engine
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_manifest_lists_refs_filenames_and_mimes(self, monkeypatch):
|
||||
from application.agents.workflows.schemas import AgentNodeConfig
|
||||
from docsgpt.agents.workflows.schemas import AgentNodeConfig
|
||||
|
||||
engine = self._engine_with_docs(monkeypatch)
|
||||
config = AgentNodeConfig(agent_type="classic", input_documents=["*"])
|
||||
@@ -1058,7 +1058,7 @@ class TestNodeDocumentManifest:
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_manifest_empty_without_selection(self, monkeypatch):
|
||||
from application.agents.workflows.schemas import AgentNodeConfig
|
||||
from docsgpt.agents.workflows.schemas import AgentNodeConfig
|
||||
|
||||
engine = self._engine_with_docs(monkeypatch)
|
||||
config = AgentNodeConfig(agent_type="classic", input_documents=[])
|
||||
@@ -1068,7 +1068,7 @@ class TestNodeDocumentManifest:
|
||||
def test_manifest_failure_never_breaks_the_node(self, monkeypatch):
|
||||
from contextlib import contextmanager
|
||||
|
||||
from application.agents.workflows.schemas import AgentNodeConfig
|
||||
from docsgpt.agents.workflows.schemas import AgentNodeConfig
|
||||
|
||||
engine = WorkflowEngine(_make_graph([], []), _make_agent())
|
||||
engine.state["input_documents"] = [{"artifact_id": self._ART_1}]
|
||||
@@ -1078,6 +1078,6 @@ class TestNodeDocumentManifest:
|
||||
raise RuntimeError("db down")
|
||||
yield
|
||||
|
||||
monkeypatch.setattr("application.storage.db.session.db_readonly", broken_readonly)
|
||||
monkeypatch.setattr("docsgpt.storage.db.session.db_readonly", broken_readonly)
|
||||
config = AgentNodeConfig(agent_type="classic", input_documents=["*"])
|
||||
assert engine._node_document_manifest(config) == ""
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from application.agents.workflows.workflow_engine import WorkflowEngine
|
||||
from docsgpt.agents.workflows.workflow_engine import WorkflowEngine
|
||||
|
||||
|
||||
def _engine() -> WorkflowEngine:
|
||||
|
||||
@@ -15,16 +15,16 @@ import uuid
|
||||
import pytest
|
||||
from sqlalchemy import text
|
||||
|
||||
from application.agents.workflow_agent import WorkflowAgent, _MAX_INPUT_DOCUMENTS
|
||||
from application.agents.workflows.schemas import AgentNodeConfig
|
||||
from application.agents.workflows.workflow_engine import (
|
||||
from docsgpt.agents.workflow_agent import WorkflowAgent, _MAX_INPUT_DOCUMENTS
|
||||
from docsgpt.agents.workflows.schemas import AgentNodeConfig
|
||||
from docsgpt.agents.workflows.workflow_engine import (
|
||||
_EXTRACT_TRUNCATION_ID,
|
||||
WorkflowEngine,
|
||||
)
|
||||
from application.storage.db.repositories.artifacts import ArtifactsRepository
|
||||
from application.storage.db.repositories.workflow_runs import WorkflowRunsRepository
|
||||
from application.storage.local import LocalStorage
|
||||
from application.storage.storage_creator import StorageCreator
|
||||
from docsgpt.storage.db.repositories.artifacts import ArtifactsRepository
|
||||
from docsgpt.storage.db.repositories.workflow_runs import WorkflowRunsRepository
|
||||
from docsgpt.storage.local import LocalStorage
|
||||
from docsgpt.storage.storage_creator import StorageCreator
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
@@ -37,7 +37,7 @@ def _wire(pg_engine, tmp_path, monkeypatch) -> LocalStorage:
|
||||
"""Point storage + the db session at the ephemeral fixtures."""
|
||||
storage = LocalStorage(base_dir=str(tmp_path))
|
||||
monkeypatch.setattr(StorageCreator, "_instance", storage, raising=False)
|
||||
monkeypatch.setattr("application.storage.db.session.get_engine", lambda: pg_engine)
|
||||
monkeypatch.setattr("docsgpt.storage.db.session.get_engine", lambda: pg_engine)
|
||||
return storage
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ def _patch_engine(monkeypatch, probe=None) -> None:
|
||||
_RecordingEngine.instances = []
|
||||
_RecordingEngine.probe = probe
|
||||
monkeypatch.setattr(
|
||||
"application.agents.workflow_agent.WorkflowEngine", _RecordingEngine
|
||||
"docsgpt.agents.workflow_agent.WorkflowEngine", _RecordingEngine
|
||||
)
|
||||
|
||||
|
||||
@@ -228,7 +228,7 @@ def test_shared_agent_run_and_artifacts_owned_by_caller(pg_engine, tmp_path, mon
|
||||
# share token, so it needs a request context.
|
||||
from flask import Flask
|
||||
|
||||
from application.api.user.artifacts.authz import Principal, authorize_artifact
|
||||
from docsgpt.api.user.artifacts.authz import Principal, authorize_artifact
|
||||
|
||||
app = Flask(__name__)
|
||||
with app.test_request_context():
|
||||
@@ -366,13 +366,13 @@ def test_quota_exceeded_fails_run_and_does_not_execute(pg_engine, tmp_path, monk
|
||||
agent = _agent(wf_id, attachments)
|
||||
_patch_engine(monkeypatch)
|
||||
|
||||
from application.sandbox.artifacts_capture import QuotaExceeded
|
||||
from docsgpt.sandbox.artifacts_capture import QuotaExceeded
|
||||
|
||||
def _raise_quota(**kwargs):
|
||||
raise QuotaExceeded("artifact storage quota reached")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.sandbox.artifacts_capture.persist_new_artifact", _raise_quota
|
||||
"docsgpt.sandbox.artifacts_capture.persist_new_artifact", _raise_quota
|
||||
)
|
||||
|
||||
events = list(agent._gen_inner("summarize", log_context=None))
|
||||
@@ -400,7 +400,7 @@ def test_quota_exceeded_fails_run_and_does_not_execute(pg_engine, tmp_path, monk
|
||||
|
||||
def test_oversize_declared_attachment_skipped_with_notice(pg_engine, tmp_path, monkeypatch):
|
||||
"""A declared-oversize attachment is dropped with a surfaced notice; the run still proceeds."""
|
||||
from application.core.settings import settings
|
||||
from docsgpt.core.settings import settings
|
||||
|
||||
storage = _wire(pg_engine, tmp_path, monkeypatch)
|
||||
wf_id = _make_workflow(pg_engine)
|
||||
@@ -462,7 +462,7 @@ def test_extract_parse_opts_out_of_sync_subtask_guard(monkeypatch):
|
||||
engine.agent = agent
|
||||
engine.workflow_run_id = "run-extract"
|
||||
|
||||
import application.api.user.tasks as tasks
|
||||
import docsgpt.api.user.tasks as tasks
|
||||
|
||||
captured: dict = {}
|
||||
|
||||
@@ -495,7 +495,7 @@ def test_extract_parse_opts_out_of_sync_subtask_guard(monkeypatch):
|
||||
|
||||
def _engine_for_parse(monkeypatch, get_impl):
|
||||
"""Engine wired to a fake ``parse_document`` whose ``get`` is ``get_impl``."""
|
||||
import application.api.user.tasks as tasks
|
||||
import docsgpt.api.user.tasks as tasks
|
||||
|
||||
agent = _agent(str(uuid.uuid4()), [])
|
||||
engine = WorkflowEngine.__new__(WorkflowEngine)
|
||||
@@ -562,7 +562,7 @@ def test_parse_is_skipped_once_the_node_budget_is_spent(monkeypatch, caplog):
|
||||
|
||||
def test_parse_without_a_deadline_keeps_the_size_scaled_window(monkeypatch):
|
||||
"""The per-document scaling is the point of the feature; keep it intact."""
|
||||
from application.api.user.tasks import parse_timeout_for_size
|
||||
from docsgpt.api.user.tasks import parse_timeout_for_size
|
||||
|
||||
engine, captured = _engine_for_parse(
|
||||
monkeypatch, lambda timeout: {"status": "ok", "content": "md"}
|
||||
@@ -592,13 +592,13 @@ def test_node_extract_path_capped_with_truncation_note(pg_engine, tmp_path, monk
|
||||
engine = _RecordingEngine.instances[-1]
|
||||
|
||||
# Cap the blocking-extract path below the doc count so the overflow truncates.
|
||||
from application.core.settings import settings
|
||||
from docsgpt.core.settings import settings
|
||||
|
||||
monkeypatch.setattr(settings, "WORKFLOW_NODE_EXTRACT_MAX_FILES", 2, raising=False)
|
||||
|
||||
# Stub the parsing worker so each non-text doc "parses" without a broker, and
|
||||
# count the blocking calls to prove the overflow docs are never enqueued.
|
||||
import application.api.user.tasks as tasks
|
||||
import docsgpt.api.user.tasks as tasks
|
||||
|
||||
parse_calls = {"n": 0}
|
||||
|
||||
@@ -646,11 +646,11 @@ def test_node_extract_cap_bounds_parse_attempts_even_when_every_parse_times_out(
|
||||
list(agent._gen_inner("summarize", log_context=None))
|
||||
engine = _RecordingEngine.instances[-1]
|
||||
|
||||
from application.core.settings import settings
|
||||
from docsgpt.core.settings import settings
|
||||
|
||||
monkeypatch.setattr(settings, "WORKFLOW_NODE_EXTRACT_MAX_FILES", 2, raising=False)
|
||||
|
||||
import application.api.user.tasks as tasks
|
||||
import docsgpt.api.user.tasks as tasks
|
||||
|
||||
parse_calls = {"n": 0}
|
||||
|
||||
@@ -698,7 +698,7 @@ def _with_extraction(attachment: dict, content, extraction=_OK_EXTRACTION) -> di
|
||||
|
||||
def _forbid_parse(monkeypatch) -> None:
|
||||
"""Make any enqueue of the parsing worker a hard failure."""
|
||||
import application.api.user.tasks as tasks
|
||||
import docsgpt.api.user.tasks as tasks
|
||||
|
||||
def _apply_async(*a, **k):
|
||||
raise AssertionError("the document must not be re-parsed")
|
||||
@@ -708,7 +708,7 @@ def _forbid_parse(monkeypatch) -> None:
|
||||
|
||||
def _count_parses(monkeypatch, calls: dict) -> None:
|
||||
"""Stub the parsing worker so each blocking parse succeeds and is counted."""
|
||||
import application.api.user.tasks as tasks
|
||||
import docsgpt.api.user.tasks as tasks
|
||||
|
||||
class _R:
|
||||
def get(self, timeout=None, disable_sync_subtasks=True):
|
||||
@@ -758,7 +758,7 @@ def test_preextracted_attachment_text_is_reused_without_reparsing(
|
||||
|
||||
def test_preextracted_text_is_bounded_head_and_tail(pg_engine, tmp_path, monkeypatch):
|
||||
"""Reused text goes through the same head+tail window as the inline-text path."""
|
||||
from application.parser.document_reader import _TEXT_MAX_BYTES
|
||||
from docsgpt.parser.document_reader import _TEXT_MAX_BYTES
|
||||
|
||||
storage = _wire(pg_engine, tmp_path, monkeypatch)
|
||||
big = "A" * (_TEXT_MAX_BYTES * 3)
|
||||
@@ -841,7 +841,7 @@ def test_legacy_row_without_extraction_metadata_is_trusted(pg_engine, tmp_path,
|
||||
|
||||
def test_preextracted_docs_do_not_consume_the_extract_cap(pg_engine, tmp_path, monkeypatch):
|
||||
"""Reused text costs no blocking parse, so it must not spend the per-node parse budget."""
|
||||
from application.core.settings import settings
|
||||
from docsgpt.core.settings import settings
|
||||
|
||||
storage = _wire(pg_engine, tmp_path, monkeypatch)
|
||||
attachments = [
|
||||
|
||||
@@ -15,16 +15,16 @@ from typing import Any, Dict
|
||||
|
||||
import pytest
|
||||
|
||||
from application.agents.workflows.node_agent import WorkflowNodeAgentFactory
|
||||
from application.agents.workflows.schemas import (
|
||||
from docsgpt.agents.workflows.node_agent import WorkflowNodeAgentFactory
|
||||
from docsgpt.agents.workflows.schemas import (
|
||||
NodeType,
|
||||
Workflow,
|
||||
WorkflowGraph,
|
||||
WorkflowNode,
|
||||
)
|
||||
from application.agents.workflows.workflow_engine import WorkflowEngine
|
||||
from application.core.settings import settings
|
||||
from application.storage.db.repositories.artifacts import ArtifactsRepository
|
||||
from docsgpt.agents.workflows.workflow_engine import WorkflowEngine
|
||||
from docsgpt.core.settings import settings
|
||||
from docsgpt.storage.db.repositories.artifacts import ArtifactsRepository
|
||||
|
||||
RUN_ID = "11111111-1111-1111-1111-111111111111"
|
||||
|
||||
@@ -63,7 +63,7 @@ def _patch_repo(monkeypatch, artifacts: Dict[str, Dict[str, Any]], run_id: str =
|
||||
def _fake_db_readonly():
|
||||
yield object()
|
||||
|
||||
monkeypatch.setattr("application.storage.db.session.db_readonly", _fake_db_readonly)
|
||||
monkeypatch.setattr("docsgpt.storage.db.session.db_readonly", _fake_db_readonly)
|
||||
monkeypatch.setattr(ArtifactsRepository, "__init__", lambda self, conn=None: None)
|
||||
monkeypatch.setattr(ArtifactsRepository, "artifact_id_at_position", _at_position)
|
||||
monkeypatch.setattr(ArtifactsRepository, "get_artifact_in_parent", _in_parent)
|
||||
@@ -147,7 +147,7 @@ def test_raw_id_and_short_ref_pass_through(monkeypatch):
|
||||
|
||||
|
||||
def _node_config(**kwargs):
|
||||
from application.agents.workflows.schemas import AgentNodeConfig
|
||||
from docsgpt.agents.workflows.schemas import AgentNodeConfig
|
||||
|
||||
return AgentNodeConfig(**kwargs)
|
||||
|
||||
@@ -186,7 +186,7 @@ def test_auto_text_only_model_extracts_to_content(monkeypatch):
|
||||
_patch_repo(monkeypatch, {aid: rec})
|
||||
eng = _engine(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
"application.storage.storage_creator.StorageCreator.get_storage",
|
||||
"docsgpt.storage.storage_creator.StorageCreator.get_storage",
|
||||
staticmethod(lambda: _FakeStorage(b"hello world")),
|
||||
)
|
||||
cfg = _node_config(input_documents=[aid], file_passing="auto")
|
||||
@@ -203,7 +203,7 @@ def test_extract_always_inlines_text_even_for_vision_model(monkeypatch):
|
||||
_patch_repo(monkeypatch, {aid: rec})
|
||||
eng = _engine(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
"application.storage.storage_creator.StorageCreator.get_storage",
|
||||
"docsgpt.storage.storage_creator.StorageCreator.get_storage",
|
||||
staticmethod(lambda: _FakeStorage(b"# Title")),
|
||||
)
|
||||
cfg = _node_config(input_documents=[aid], file_passing="extract")
|
||||
@@ -280,7 +280,7 @@ def test_forged_uuid_is_rejected(monkeypatch):
|
||||
def test_native_file_cap_bounds_native_then_extracts(monkeypatch):
|
||||
"""More than the native cap: the first N go native, the rest are extracted."""
|
||||
monkeypatch.setattr(
|
||||
"application.core.settings.settings.WORKFLOW_NODE_NATIVE_MAX_FILES", 2, raising=False
|
||||
"docsgpt.core.settings.settings.WORKFLOW_NODE_NATIVE_MAX_FILES", 2, raising=False
|
||||
)
|
||||
artifacts = {}
|
||||
ids = []
|
||||
@@ -291,7 +291,7 @@ def test_native_file_cap_bounds_native_then_extracts(monkeypatch):
|
||||
_patch_repo(monkeypatch, artifacts)
|
||||
eng = _engine(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
"application.storage.storage_creator.StorageCreator.get_storage",
|
||||
"docsgpt.storage.storage_creator.StorageCreator.get_storage",
|
||||
staticmethod(lambda: _FakeStorage(b"img-bytes")),
|
||||
)
|
||||
# extract of a non-text image routes through the parsing worker; stub it so it returns text.
|
||||
@@ -312,7 +312,7 @@ def test_native_file_cap_bounds_native_then_extracts(monkeypatch):
|
||||
def test_oversize_file_is_skipped(monkeypatch):
|
||||
"""A file past the per-file byte ceiling is dropped, not attached."""
|
||||
monkeypatch.setattr(
|
||||
"application.core.settings.settings.SANDBOX_MAX_INPUT_BYTES", 5, raising=False
|
||||
"docsgpt.core.settings.settings.SANDBOX_MAX_INPUT_BYTES", 5, raising=False
|
||||
)
|
||||
aid, rec = _artifact(RUN_ID, "image/png", size=999)
|
||||
_patch_repo(monkeypatch, {aid: rec})
|
||||
@@ -328,7 +328,7 @@ def test_oversize_file_is_skipped(monkeypatch):
|
||||
def test_oversize_text_skipped_by_post_read_guard_when_size_missing(monkeypatch):
|
||||
"""A NULL/missing version size skips the pre-read cap; the post-read byte guard still drops it."""
|
||||
monkeypatch.setattr(
|
||||
"application.core.settings.settings.SANDBOX_MAX_INPUT_BYTES", 5, raising=False
|
||||
"docsgpt.core.settings.settings.SANDBOX_MAX_INPUT_BYTES", 5, raising=False
|
||||
)
|
||||
# size=None bypasses the ``isinstance(size, int)`` pre-read check; the actual
|
||||
# bytes (longer than the 5-byte cap) must be rejected after reading.
|
||||
@@ -336,7 +336,7 @@ def test_oversize_text_skipped_by_post_read_guard_when_size_missing(monkeypatch)
|
||||
_patch_repo(monkeypatch, {aid: rec})
|
||||
eng = _engine(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
"application.storage.storage_creator.StorageCreator.get_storage",
|
||||
"docsgpt.storage.storage_creator.StorageCreator.get_storage",
|
||||
staticmethod(lambda: _FakeStorage(b"way over the cap")),
|
||||
)
|
||||
cfg = _node_config(input_documents=[aid], file_passing="auto")
|
||||
@@ -349,14 +349,14 @@ def test_oversize_text_skipped_by_post_read_guard_when_size_missing(monkeypatch)
|
||||
@pytest.mark.unit
|
||||
def test_large_under_cap_text_is_windowed_not_inlined_whole(monkeypatch):
|
||||
"""A large-but-under-cap text file is bounded to a head+tail window, not inlined whole."""
|
||||
from application.parser.document_reader import _TEXT_MAX_BYTES as _MARKDOWN_MAX_BYTES
|
||||
from docsgpt.parser.document_reader import _TEXT_MAX_BYTES as _MARKDOWN_MAX_BYTES
|
||||
|
||||
big_text = ("A" * (_MARKDOWN_MAX_BYTES * 3)).encode("utf-8")
|
||||
aid, rec = _artifact(RUN_ID, "text/plain", filename="notes.txt", size=len(big_text))
|
||||
_patch_repo(monkeypatch, {aid: rec})
|
||||
eng = _engine(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
"application.storage.storage_creator.StorageCreator.get_storage",
|
||||
"docsgpt.storage.storage_creator.StorageCreator.get_storage",
|
||||
staticmethod(lambda: _FakeStorage(big_text)),
|
||||
)
|
||||
cfg = _node_config(input_documents=[aid], file_passing="auto")
|
||||
@@ -391,7 +391,7 @@ def test_duplicate_refs_attach_once(monkeypatch):
|
||||
|
||||
def _capture_parse(monkeypatch) -> dict:
|
||||
"""Patch parse_document.apply_async so nothing touches a broker; capture the call."""
|
||||
import application.api.user.tasks as tasks
|
||||
import docsgpt.api.user.tasks as tasks
|
||||
|
||||
captured: Dict[str, Any] = {}
|
||||
|
||||
@@ -411,7 +411,7 @@ def _capture_parse(monkeypatch) -> dict:
|
||||
@pytest.mark.unit
|
||||
def test_parse_window_scales_with_the_document_size(monkeypatch):
|
||||
"""The version's ``size`` reaches the parse: a longer await AND matching Celery limits."""
|
||||
from application.api.user.tasks import parse_task_time_limits, parse_timeout_for_size
|
||||
from docsgpt.api.user.tasks import parse_task_time_limits, parse_timeout_for_size
|
||||
|
||||
size = 8 * 1024 * 1024
|
||||
aid, rec = _artifact(RUN_ID, "application/pdf", filename="scan.pdf", size=size)
|
||||
@@ -456,7 +456,7 @@ def test_preextracted_text_skips_the_parse_worker(monkeypatch):
|
||||
eng = _engine(monkeypatch)
|
||||
eng.preextracted_text[aid] = "ALREADY OCRED"
|
||||
|
||||
import application.api.user.tasks as tasks
|
||||
import docsgpt.api.user.tasks as tasks
|
||||
|
||||
monkeypatch.setattr(
|
||||
tasks.parse_document,
|
||||
@@ -523,15 +523,15 @@ def _agent_node(input_documents=None, file_passing="auto", node_id="agent_1") ->
|
||||
def _patch_capabilities(monkeypatch):
|
||||
"""Stub provider/api-key resolution (capabilities are only fetched for json_schema nodes)."""
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_model_capabilities",
|
||||
"docsgpt.core.model_utils.get_model_capabilities",
|
||||
lambda model_id, user_id=None: None,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_provider_from_model_id",
|
||||
"docsgpt.core.model_utils.get_provider_from_model_id",
|
||||
lambda model_id, user_id=None: "openai",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_api_key_for_provider", lambda _p: "k"
|
||||
"docsgpt.core.model_utils.get_api_key_for_provider", lambda _p: "k"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -16,15 +16,15 @@ from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from application.agents.tool_executor import ToolExecutor
|
||||
from application.agents.workflows.node_agent import WorkflowNodeAgentFactory
|
||||
from application.agents.workflows.schemas import (
|
||||
from docsgpt.agents.tool_executor import ToolExecutor
|
||||
from docsgpt.agents.workflows.node_agent import WorkflowNodeAgentFactory
|
||||
from docsgpt.agents.workflows.schemas import (
|
||||
NodeType,
|
||||
Workflow,
|
||||
WorkflowGraph,
|
||||
WorkflowNode,
|
||||
)
|
||||
from application.agents.workflows.workflow_engine import WorkflowEngine
|
||||
from docsgpt.agents.workflows.workflow_engine import WorkflowEngine
|
||||
|
||||
|
||||
class _StubNodeAgent:
|
||||
@@ -85,7 +85,7 @@ def test_agent_node_run_scopes_tool_executor(monkeypatch):
|
||||
WorkflowNodeAgentFactory, "create", staticmethod(lambda **kwargs: stub)
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_api_key_for_provider", lambda _provider: None
|
||||
"docsgpt.core.model_utils.get_api_key_for_provider", lambda _provider: None
|
||||
)
|
||||
|
||||
list(engine._execute_agent_node(node))
|
||||
@@ -108,7 +108,7 @@ def test_agent_node_skips_run_scope_when_not_persisted(monkeypatch):
|
||||
WorkflowNodeAgentFactory, "create", staticmethod(lambda **kwargs: stub)
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_api_key_for_provider", lambda _provider: None
|
||||
"docsgpt.core.model_utils.get_api_key_for_provider", lambda _provider: None
|
||||
)
|
||||
|
||||
list(engine._execute_agent_node(node))
|
||||
@@ -135,7 +135,7 @@ def _capture_tool_config(monkeypatch) -> Dict[str, Any]:
|
||||
|
||||
mock_tm.load_tool.side_effect = _load_tool
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tool_executor.ToolManager", lambda config: mock_tm
|
||||
"docsgpt.agents.tool_executor.ToolManager", lambda config: mock_tm
|
||||
)
|
||||
return captured
|
||||
|
||||
@@ -184,15 +184,15 @@ def test_get_or_load_tool_omits_workflow_run_id_for_chat(monkeypatch):
|
||||
def test_run_scoped_ref_resolves_for_edit(pg_engine, tmp_path, monkeypatch):
|
||||
"""A1 created under a workflow_run_id resolves for edit_artifact -> v2."""
|
||||
pytest.importorskip("jsonschema")
|
||||
from application.agents.tools.artifact_generator import ArtifactGeneratorTool
|
||||
from application.storage.db.repositories.artifacts import ArtifactsRepository
|
||||
from application.storage.local import LocalStorage
|
||||
from application.storage.storage_creator import StorageCreator
|
||||
from docsgpt.agents.tools.artifact_generator import ArtifactGeneratorTool
|
||||
from docsgpt.storage.db.repositories.artifacts import ArtifactsRepository
|
||||
from docsgpt.storage.local import LocalStorage
|
||||
from docsgpt.storage.storage_creator import StorageCreator
|
||||
|
||||
storage = LocalStorage(base_dir=str(tmp_path))
|
||||
monkeypatch.setattr(StorageCreator, "_instance", storage, raising=False)
|
||||
monkeypatch.setattr(
|
||||
"application.storage.db.session.get_engine", lambda: pg_engine
|
||||
"docsgpt.storage.db.session.get_engine", lambda: pg_engine
|
||||
)
|
||||
# Skip the Jupyter-gateway renderer: the run-scoping under test is the ref
|
||||
# resolution + version append, not the rendered bytes.
|
||||
|
||||
@@ -4,7 +4,7 @@ from datetime import datetime, timezone
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from application.agents.workflows.schemas import (
|
||||
from docsgpt.agents.workflows.schemas import (
|
||||
AgentNodeConfig,
|
||||
AgentType,
|
||||
ConditionCase,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from application.agents.workflows.schemas import Workflow, WorkflowGraph
|
||||
from application.agents.workflows.workflow_engine import WorkflowEngine
|
||||
from docsgpt.agents.workflows.schemas import Workflow, WorkflowGraph
|
||||
from docsgpt.agents.workflows.workflow_engine import WorkflowEngine
|
||||
|
||||
|
||||
def create_engine() -> WorkflowEngine:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Comprehensive tests for application/agents/tools/api_body_serializer.py
|
||||
"""Comprehensive tests for docsgpt/agents/tools/api_body_serializer.py
|
||||
|
||||
Covers: ContentType enum, RequestBodySerializer (JSON, form-urlencoded,
|
||||
multipart, text/plain, XML, octet-stream, unknown types), encoding rules,
|
||||
@@ -10,7 +10,7 @@ from urllib.parse import parse_qs
|
||||
|
||||
import pytest
|
||||
|
||||
from application.agents.tools.api_body_serializer import (
|
||||
from docsgpt.agents.tools.api_body_serializer import (
|
||||
ContentType,
|
||||
RequestBodySerializer,
|
||||
)
|
||||
@@ -561,7 +561,7 @@ class TestApiBodySerializerMultipartParts:
|
||||
|
||||
def test_multipart_field_name_not_percent_encoded(self):
|
||||
"""RFC 7578: names are a quoted string, so 'file name' stays literal."""
|
||||
from application.agents.tools.api_body_serializer import (
|
||||
from docsgpt.agents.tools.api_body_serializer import (
|
||||
RequestBodySerializer,
|
||||
)
|
||||
|
||||
@@ -575,7 +575,7 @@ class TestApiBodySerializerMultipartParts:
|
||||
assert "file%20name" not in result
|
||||
|
||||
def test_multipart_field_name_escapes_quotes_and_newlines(self):
|
||||
from application.agents.tools.api_body_serializer import (
|
||||
from docsgpt.agents.tools.api_body_serializer import (
|
||||
RequestBodySerializer,
|
||||
)
|
||||
|
||||
@@ -589,7 +589,7 @@ class TestApiBodySerializerMultipartParts:
|
||||
|
||||
def test_multipart_dict_unknown_content_type(self):
|
||||
"""Cover line 226: dict with unknown content type uses str()."""
|
||||
from application.agents.tools.api_body_serializer import (
|
||||
from docsgpt.agents.tools.api_body_serializer import (
|
||||
RequestBodySerializer,
|
||||
)
|
||||
|
||||
@@ -604,7 +604,7 @@ class TestApiBodySerializerMultipartParts:
|
||||
|
||||
def test_multipart_string_json_content_type(self):
|
||||
"""Cover line 229: string value with application/json content type."""
|
||||
from application.agents.tools.api_body_serializer import (
|
||||
from docsgpt.agents.tools.api_body_serializer import (
|
||||
RequestBodySerializer,
|
||||
)
|
||||
|
||||
@@ -619,7 +619,7 @@ class TestApiBodySerializerMultipartParts:
|
||||
|
||||
def test_multipart_string_xml_content_type(self):
|
||||
"""Cover line 229: string value with application/xml content type."""
|
||||
from application.agents.tools.api_body_serializer import (
|
||||
from docsgpt.agents.tools.api_body_serializer import (
|
||||
RequestBodySerializer,
|
||||
)
|
||||
|
||||
@@ -634,7 +634,7 @@ class TestApiBodySerializerMultipartParts:
|
||||
|
||||
def test_multipart_string_unknown_content_type(self):
|
||||
"""Cover line 229: string with unknown content type falls through."""
|
||||
from application.agents.tools.api_body_serializer import (
|
||||
from docsgpt.agents.tools.api_body_serializer import (
|
||||
RequestBodySerializer,
|
||||
)
|
||||
|
||||
@@ -653,7 +653,7 @@ class TestApiBodySerializerOctetStreamCoverage:
|
||||
|
||||
def test_octet_stream_bytes_input(self):
|
||||
"""Cover line 271: _serialize_octet_stream with bytes input."""
|
||||
from application.agents.tools.api_body_serializer import (
|
||||
from docsgpt.agents.tools.api_body_serializer import (
|
||||
ContentType,
|
||||
RequestBodySerializer,
|
||||
)
|
||||
@@ -664,7 +664,7 @@ class TestApiBodySerializerOctetStreamCoverage:
|
||||
|
||||
def test_octet_stream_string_input(self):
|
||||
"""Cover line 275: _serialize_octet_stream with string input."""
|
||||
from application.agents.tools.api_body_serializer import (
|
||||
from docsgpt.agents.tools.api_body_serializer import (
|
||||
ContentType,
|
||||
RequestBodySerializer,
|
||||
)
|
||||
@@ -677,7 +677,7 @@ class TestApiBodySerializerOctetStreamCoverage:
|
||||
"""Cover line 279: _serialize_octet_stream with dict input (fallback to JSON)."""
|
||||
import json
|
||||
|
||||
from application.agents.tools.api_body_serializer import (
|
||||
from docsgpt.agents.tools.api_body_serializer import (
|
||||
ContentType,
|
||||
RequestBodySerializer,
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Comprehensive tests for application/agents/tools/api_tool.py
|
||||
"""Comprehensive tests for docsgpt/agents/tools/api_tool.py
|
||||
|
||||
Covers: APITool initialization, all HTTP methods, path param substitution,
|
||||
SSRF validation, error handling, response parsing, body serialization.
|
||||
@@ -10,7 +10,7 @@ from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from application.agents.tools.api_tool import APITool, DEFAULT_TIMEOUT
|
||||
from docsgpt.agents.tools.api_tool import APITool, DEFAULT_TIMEOUT
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -81,7 +81,7 @@ class TestAPIToolInit:
|
||||
@pytest.mark.unit
|
||||
class TestMakeApiCall:
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_successful_get(self, mock_pinned, get_tool):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
@@ -97,7 +97,7 @@ class TestMakeApiCall:
|
||||
assert result["message"] == "API call successful."
|
||||
assert mock_pinned.call_args[0][0] == "GET"
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_successful_post(self, mock_pinned, post_tool):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 201
|
||||
@@ -110,7 +110,7 @@ class TestMakeApiCall:
|
||||
assert result["status_code"] == 201
|
||||
assert mock_pinned.call_args[0][0] == "POST"
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_put_method(self, mock_pinned):
|
||||
tool = APITool(config={"url": "https://example.com/item/1", "method": "PUT"})
|
||||
mock_resp = MagicMock()
|
||||
@@ -124,7 +124,7 @@ class TestMakeApiCall:
|
||||
assert result["status_code"] == 200
|
||||
assert mock_pinned.call_args[0][0] == "PUT"
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_delete_method(self, mock_pinned):
|
||||
tool = APITool(config={"url": "https://example.com/item/1", "method": "DELETE"})
|
||||
mock_resp = MagicMock()
|
||||
@@ -137,7 +137,7 @@ class TestMakeApiCall:
|
||||
assert result["status_code"] == 204
|
||||
assert mock_pinned.call_args[0][0] == "DELETE"
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_patch_method(self, mock_pinned):
|
||||
tool = APITool(config={"url": "https://example.com/item/1", "method": "PATCH"})
|
||||
mock_resp = MagicMock()
|
||||
@@ -151,7 +151,7 @@ class TestMakeApiCall:
|
||||
assert result["status_code"] == 200
|
||||
assert mock_pinned.call_args[0][0] == "PATCH"
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_head_method(self, mock_pinned):
|
||||
tool = APITool(config={"url": "https://example.com", "method": "HEAD"})
|
||||
mock_resp = MagicMock()
|
||||
@@ -164,7 +164,7 @@ class TestMakeApiCall:
|
||||
assert result["status_code"] == 200
|
||||
assert mock_pinned.call_args[0][0] == "HEAD"
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_options_method(self, mock_pinned):
|
||||
tool = APITool(config={"url": "https://example.com", "method": "OPTIONS"})
|
||||
mock_resp = MagicMock()
|
||||
@@ -192,18 +192,18 @@ class TestMakeApiCall:
|
||||
@pytest.mark.unit
|
||||
class TestSSRFValidation:
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_ssrf_blocked(self, mock_pinned, get_tool):
|
||||
from application.security.safe_url import UnsafeUserUrlError
|
||||
from docsgpt.security.safe_url import UnsafeUserUrlError
|
||||
|
||||
mock_pinned.side_effect = UnsafeUserUrlError("blocked")
|
||||
result = get_tool.execute_action("any")
|
||||
assert result["status_code"] is None
|
||||
assert "URL validation error" in result["message"]
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_ssrf_blocked_with_path_params(self, mock_pinned):
|
||||
from application.security.safe_url import UnsafeUserUrlError
|
||||
from docsgpt.security.safe_url import UnsafeUserUrlError
|
||||
|
||||
tool = APITool(config={
|
||||
"url": "https://api.example.com/{host}/data",
|
||||
@@ -225,21 +225,21 @@ class TestSSRFValidation:
|
||||
@pytest.mark.unit
|
||||
class TestErrorHandling:
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_timeout_error(self, mock_pinned, get_tool):
|
||||
mock_pinned.side_effect = requests.exceptions.Timeout()
|
||||
result = get_tool.execute_action("any")
|
||||
assert result["status_code"] is None
|
||||
assert "timeout" in result["message"].lower()
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_connection_error(self, mock_pinned, get_tool):
|
||||
mock_pinned.side_effect = requests.exceptions.ConnectionError("refused")
|
||||
result = get_tool.execute_action("any")
|
||||
assert result["status_code"] is None
|
||||
assert "Connection error" in result["message"]
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_http_error_with_json(self, mock_pinned, get_tool):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 422
|
||||
@@ -253,7 +253,7 @@ class TestErrorHandling:
|
||||
assert result["status_code"] == 422
|
||||
assert result["data"] == {"error": "invalid_field"}
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_http_error_non_json_body(self, mock_pinned, get_tool):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 404
|
||||
@@ -268,19 +268,19 @@ class TestErrorHandling:
|
||||
assert result["status_code"] == 404
|
||||
assert result["data"] == "Not Found"
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_request_exception(self, mock_pinned, get_tool):
|
||||
mock_pinned.side_effect = requests.exceptions.RequestException("something")
|
||||
result = get_tool.execute_action("any")
|
||||
assert "API call failed" in result["message"]
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_unexpected_exception(self, mock_pinned, get_tool):
|
||||
mock_pinned.side_effect = RuntimeError("unexpected")
|
||||
result = get_tool.execute_action("any")
|
||||
assert "Unexpected error" in result["message"]
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_body_serialization_error(self, mock_pinned):
|
||||
tool = APITool(config={
|
||||
"url": "https://example.com",
|
||||
@@ -289,7 +289,7 @@ class TestErrorHandling:
|
||||
})
|
||||
|
||||
with patch(
|
||||
"application.agents.tools.api_tool.RequestBodySerializer.serialize",
|
||||
"docsgpt.agents.tools.api_tool.RequestBodySerializer.serialize",
|
||||
side_effect=ValueError("serialize fail"),
|
||||
):
|
||||
result = tool.execute_action("any", key="val")
|
||||
@@ -304,7 +304,7 @@ class TestErrorHandling:
|
||||
@pytest.mark.unit
|
||||
class TestPathParamSubstitution:
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_path_params_substituted(self, mock_pinned):
|
||||
tool = APITool(config={
|
||||
"url": "https://api.example.com/users/{user_id}/posts/{post_id}",
|
||||
@@ -324,7 +324,7 @@ class TestPathParamSubstitution:
|
||||
assert "/users/42/posts/7" in called_url
|
||||
assert "{user_id}" not in called_url
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_remaining_query_params_appended(self, mock_pinned):
|
||||
tool = APITool(config={
|
||||
"url": "https://api.example.com/items",
|
||||
@@ -344,7 +344,7 @@ class TestPathParamSubstitution:
|
||||
assert "page=2" in called_url
|
||||
assert "limit=10" in called_url
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_query_params_append_with_existing_query_string(self, mock_pinned):
|
||||
tool = APITool(config={
|
||||
"url": "https://api.example.com/items?existing=true",
|
||||
@@ -363,7 +363,7 @@ class TestPathParamSubstitution:
|
||||
called_url = mock_pinned.call_args[0][1]
|
||||
assert "&page=1" in called_url
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_empty_body_no_serialization(self, mock_pinned):
|
||||
tool = APITool(config={"url": "https://example.com", "method": "POST"})
|
||||
mock_resp = MagicMock()
|
||||
@@ -376,7 +376,7 @@ class TestPathParamSubstitution:
|
||||
result = tool.execute_action("create")
|
||||
assert result["status_code"] == 200
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_path_params_are_url_encoded(self, mock_pinned):
|
||||
tool = APITool(config={
|
||||
"url": "https://api.example.com/users/{user_id}/profile",
|
||||
@@ -396,7 +396,7 @@ class TestPathParamSubstitution:
|
||||
assert "../../admin" not in called_url
|
||||
assert "%2F" in called_url or "%2f" in called_url
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_path_params_query_injection_encoded(self, mock_pinned):
|
||||
tool = APITool(config={
|
||||
"url": "https://api.example.com/items/{item_id}",
|
||||
@@ -511,7 +511,7 @@ class TestAPIToolMetadata:
|
||||
def test_config_requirements_empty(self, get_tool):
|
||||
assert get_tool.get_config_requirements() == {}
|
||||
|
||||
@patch("application.agents.tools.api_tool.pinned_request")
|
||||
@patch("docsgpt.agents.tools.api_tool.pinned_request")
|
||||
def test_content_type_set_for_post_with_no_headers(self, mock_pinned):
|
||||
tool = APITool(config={
|
||||
"url": "https://example.com",
|
||||
|
||||
@@ -14,7 +14,7 @@ from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from application.agents.tools.artifact_generator import (
|
||||
from docsgpt.agents.tools.artifact_generator import (
|
||||
_KIND_INFO,
|
||||
_RENDERERS,
|
||||
ArtifactGeneratorTool,
|
||||
@@ -60,7 +60,7 @@ def test_validate_rejects_unknown_key():
|
||||
|
||||
def _patch_reversion(monkeypatch, tool):
|
||||
"""Stub render + persist so _reversion runs without a sandbox or DB; capture kwargs."""
|
||||
import application.agents.tools.artifact_generator as ag
|
||||
import docsgpt.agents.tools.artifact_generator as ag
|
||||
|
||||
captured: dict = {}
|
||||
|
||||
@@ -148,7 +148,7 @@ def test_render_cleans_scratch_but_leaves_session_open(monkeypatch):
|
||||
"""_render drops its per-render scratch dir but must NOT close the shared session."""
|
||||
manager = _FakeRenderManager()
|
||||
monkeypatch.setattr(
|
||||
"application.sandbox.sandbox_creator.SandboxCreator.get_manager", lambda: manager
|
||||
"docsgpt.sandbox.sandbox_creator.SandboxCreator.get_manager", lambda: manager
|
||||
)
|
||||
|
||||
out = _tool()._render("pdf", {"title": "t", "blocks": []})
|
||||
@@ -387,7 +387,7 @@ def test_spec_synopsis_covers_every_schema_key():
|
||||
The synopsis is a hand-written mirror of _SCHEMAS; this guards drift when a
|
||||
kind or key is added without updating what the model is told.
|
||||
"""
|
||||
from application.agents.tools.artifact_generator import _SCHEMAS, _SPEC_SYNOPSIS
|
||||
from docsgpt.agents.tools.artifact_generator import _SCHEMAS, _SPEC_SYNOPSIS
|
||||
|
||||
for kind, schema in _SCHEMAS.items():
|
||||
assert kind in _SPEC_SYNOPSIS
|
||||
@@ -398,7 +398,7 @@ def test_spec_synopsis_covers_every_schema_key():
|
||||
|
||||
|
||||
def test_create_and_rewrite_metadata_embed_spec_synopsis():
|
||||
from application.agents.tools.artifact_generator import _SPEC_SYNOPSIS
|
||||
from docsgpt.agents.tools.artifact_generator import _SPEC_SYNOPSIS
|
||||
|
||||
actions = {a["name"]: a for a in _tool().get_actions_metadata()}
|
||||
create_spec = actions["create_artifact"]["parameters"]["properties"]["spec"]
|
||||
@@ -413,7 +413,7 @@ def test_create_and_rewrite_metadata_embed_spec_synopsis():
|
||||
|
||||
|
||||
def test_spec_append_preserves_existing_items():
|
||||
from application.agents.tools.artifact_generator import _apply_spec_append
|
||||
from docsgpt.agents.tools.artifact_generator import _apply_spec_append
|
||||
|
||||
spec = {"title": "Brief", "blocks": [{"type": "heading", "text": "Overview"}]}
|
||||
out = _apply_spec_append(spec, {"blocks": [{"type": "heading", "text": "Risks"}]})
|
||||
@@ -424,14 +424,14 @@ def test_spec_append_preserves_existing_items():
|
||||
|
||||
|
||||
def test_spec_append_creates_missing_list():
|
||||
from application.agents.tools.artifact_generator import _apply_spec_append
|
||||
from docsgpt.agents.tools.artifact_generator import _apply_spec_append
|
||||
|
||||
out = _apply_spec_append({"title": "x"}, {"blocks": [{"type": "paragraph", "text": "p"}]})
|
||||
assert out["spec"]["blocks"] == [{"type": "paragraph", "text": "p"}]
|
||||
|
||||
|
||||
def test_spec_append_rejects_non_list_values_and_targets():
|
||||
from application.agents.tools.artifact_generator import _apply_spec_append
|
||||
from docsgpt.agents.tools.artifact_generator import _apply_spec_append
|
||||
|
||||
assert "error" in _apply_spec_append({}, {"blocks": "not-a-list"})
|
||||
assert "error" in _apply_spec_append({"title": "t"}, {"title": ["x"]})
|
||||
|
||||
@@ -8,7 +8,7 @@ from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from application.agents.tools.artifact_ref import make_ref, parse_ref, resolve_artifact_id
|
||||
from docsgpt.agents.tools.artifact_ref import make_ref, parse_ref, resolve_artifact_id
|
||||
|
||||
|
||||
class _FakeRepo:
|
||||
|
||||
@@ -15,13 +15,13 @@ from typing import Any, Dict, List, Optional
|
||||
|
||||
import pytest
|
||||
|
||||
import application.agents.tools.attachment_bridge as bridge_mod
|
||||
from application.agents.tools.attachment_bridge import (
|
||||
import docsgpt.agents.tools.attachment_bridge as bridge_mod
|
||||
from docsgpt.agents.tools.attachment_bridge import (
|
||||
AttachmentBridgeError,
|
||||
bridge_attachment,
|
||||
match_attachment,
|
||||
)
|
||||
from application.sandbox.artifacts_capture import QuotaExceeded
|
||||
from docsgpt.sandbox.artifacts_capture import QuotaExceeded
|
||||
|
||||
CONV = "11111111-1111-1111-1111-111111111111"
|
||||
USER = "user-1"
|
||||
@@ -232,7 +232,7 @@ def test_bridge_missing_upload_path_errors(monkeypatch):
|
||||
def test_bridge_rejects_oversize_attachment_before_reading(monkeypatch):
|
||||
# An oversize attachment is rejected via its authoritative ``size`` column,
|
||||
# BEFORE the bytes are buffered into worker memory (a memory-DoS guard).
|
||||
from application.core.settings import settings
|
||||
from docsgpt.core.settings import settings
|
||||
|
||||
_FakeArtifactsRepo.bridged = {}
|
||||
storage, calls = _patch_bridge(monkeypatch)
|
||||
@@ -248,7 +248,7 @@ def test_bridge_rejects_oversize_attachment_before_reading(monkeypatch):
|
||||
# code_executor wiring: fallback fires, stages bytes, succeeds
|
||||
# ---------------------------------------------------------------------------
|
||||
def _patch_code_executor_repo(monkeypatch):
|
||||
import application.agents.tools.code_executor as ce
|
||||
import docsgpt.agents.tools.code_executor as ce
|
||||
|
||||
monkeypatch.setattr(ce, "db_readonly", _fake_db)
|
||||
monkeypatch.setattr(ce, "ArtifactsRepository", _FakeArtifactsRepo)
|
||||
@@ -264,7 +264,7 @@ class _Manager:
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_code_executor_bridges_referenced_attachment_by_name(monkeypatch):
|
||||
from application.agents.tools.code_executor import CodeExecutorTool
|
||||
from docsgpt.agents.tools.code_executor import CodeExecutorTool
|
||||
|
||||
storage, calls = _patch_bridge(monkeypatch, storage=_FakeStorage(b"PDFDATA"), persisted_id="art-A")
|
||||
_patch_code_executor_repo(monkeypatch)
|
||||
@@ -291,7 +291,7 @@ def test_code_executor_bridges_referenced_attachment_by_name(monkeypatch):
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_code_executor_bridges_referenced_attachment_by_id(monkeypatch):
|
||||
from application.agents.tools.code_executor import CodeExecutorTool
|
||||
from docsgpt.agents.tools.code_executor import CodeExecutorTool
|
||||
|
||||
_patch_bridge(monkeypatch, storage=_FakeStorage(b"X"), persisted_id="art-B")
|
||||
_patch_code_executor_repo(monkeypatch)
|
||||
@@ -314,7 +314,7 @@ def test_code_executor_bridges_referenced_attachment_by_id(monkeypatch):
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_code_executor_idempotent_reuse_no_second_persist(monkeypatch):
|
||||
from application.agents.tools.code_executor import CodeExecutorTool
|
||||
from docsgpt.agents.tools.code_executor import CodeExecutorTool
|
||||
|
||||
_storage, calls = _patch_bridge(monkeypatch, persisted_id="art-A")
|
||||
_patch_code_executor_repo(monkeypatch)
|
||||
@@ -341,7 +341,7 @@ def test_code_executor_idempotent_reuse_no_second_persist(monkeypatch):
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_code_executor_rejects_foreign_attachment(monkeypatch):
|
||||
from application.agents.tools.code_executor import CodeExecutorTool
|
||||
from docsgpt.agents.tools.code_executor import CodeExecutorTool
|
||||
|
||||
_patch_bridge(monkeypatch)
|
||||
_patch_code_executor_repo(monkeypatch)
|
||||
@@ -358,7 +358,7 @@ def test_code_executor_rejects_foreign_attachment(monkeypatch):
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_code_executor_workflow_scope_does_not_bridge(monkeypatch):
|
||||
from application.agents.tools.code_executor import CodeExecutorTool
|
||||
from docsgpt.agents.tools.code_executor import CodeExecutorTool
|
||||
|
||||
_storage, calls = _patch_bridge(monkeypatch)
|
||||
_patch_code_executor_repo(monkeypatch)
|
||||
@@ -375,7 +375,7 @@ def test_code_executor_workflow_scope_does_not_bridge(monkeypatch):
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_code_executor_unresolvable_ref_still_errors(monkeypatch):
|
||||
from application.agents.tools.code_executor import CodeExecutorTool
|
||||
from docsgpt.agents.tools.code_executor import CodeExecutorTool
|
||||
|
||||
_patch_bridge(monkeypatch)
|
||||
_patch_code_executor_repo(monkeypatch)
|
||||
@@ -389,7 +389,7 @@ def test_code_executor_unresolvable_ref_still_errors(monkeypatch):
|
||||
@pytest.mark.unit
|
||||
def test_code_executor_existing_artifact_still_used(monkeypatch):
|
||||
"""A ref that resolves to a real artifact uses it directly (no bridge regression)."""
|
||||
from application.agents.tools.code_executor import CodeExecutorTool
|
||||
from docsgpt.agents.tools.code_executor import CodeExecutorTool
|
||||
|
||||
storage, calls = _patch_bridge(monkeypatch)
|
||||
_patch_code_executor_repo(monkeypatch)
|
||||
@@ -402,7 +402,7 @@ def test_code_executor_existing_artifact_still_used(monkeypatch):
|
||||
}
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tools.code_executor.StorageCreator.get_storage",
|
||||
"docsgpt.agents.tools.code_executor.StorageCreator.get_storage",
|
||||
staticmethod(lambda: storage),
|
||||
)
|
||||
tool = CodeExecutorTool(
|
||||
@@ -418,7 +418,7 @@ def test_code_executor_existing_artifact_still_used(monkeypatch):
|
||||
# read_document wiring
|
||||
# ---------------------------------------------------------------------------
|
||||
def _patch_read_document_repo(monkeypatch):
|
||||
import application.agents.tools.read_document as rd
|
||||
import docsgpt.agents.tools.read_document as rd
|
||||
|
||||
monkeypatch.setattr(rd, "db_readonly", _fake_db)
|
||||
monkeypatch.setattr(rd, "ArtifactsRepository", _FakeArtifactsRepo)
|
||||
@@ -426,8 +426,8 @@ def _patch_read_document_repo(monkeypatch):
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_read_document_bridges_attachment_then_enqueues(monkeypatch):
|
||||
import application.agents.tools.read_document as rd
|
||||
from application.agents.tools.read_document import ReadDocumentTool
|
||||
import docsgpt.agents.tools.read_document as rd
|
||||
from docsgpt.agents.tools.read_document import ReadDocumentTool
|
||||
|
||||
_storage, calls = _patch_bridge(monkeypatch, persisted_id="art-RD")
|
||||
_patch_read_document_repo(monkeypatch)
|
||||
@@ -440,7 +440,7 @@ def test_read_document_bridges_attachment_then_enqueues(monkeypatch):
|
||||
def get(self, timeout=None, disable_sync_subtasks=True):
|
||||
return {"status": "ok", "content": "parsed", "truncated": False}
|
||||
|
||||
import application.api.user.tasks as tasks
|
||||
import docsgpt.api.user.tasks as tasks
|
||||
|
||||
def _apply_async(args=None, queue=None, **kw):
|
||||
captured["args"] = args
|
||||
@@ -462,14 +462,14 @@ def test_read_document_bridges_attachment_then_enqueues(monkeypatch):
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_read_document_workflow_scope_does_not_bridge(monkeypatch):
|
||||
from application.agents.tools.read_document import ReadDocumentTool
|
||||
from docsgpt.agents.tools.read_document import ReadDocumentTool
|
||||
|
||||
_storage, calls = _patch_bridge(monkeypatch)
|
||||
_patch_read_document_repo(monkeypatch)
|
||||
att = _attachment(filename="wf.pdf")
|
||||
_FakeAttachmentsRepo.rows = {att["id"]: att}
|
||||
|
||||
import application.api.user.tasks as tasks
|
||||
import docsgpt.api.user.tasks as tasks
|
||||
monkeypatch.setattr(
|
||||
tasks.parse_document, "apply_async",
|
||||
lambda *a, **k: (_ for _ in ()).throw(AssertionError("must not enqueue")),
|
||||
|
||||
@@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
class TestBraveOptionalParams:
|
||||
def test_result_filter_added_to_params(self):
|
||||
from application.agents.tools.brave import BraveSearchTool
|
||||
from docsgpt.agents.tools.brave import BraveSearchTool
|
||||
|
||||
tool = BraveSearchTool(config={"token": "tk"})
|
||||
fake_response = MagicMock()
|
||||
@@ -14,7 +14,7 @@ class TestBraveOptionalParams:
|
||||
fake_response.json.return_value = {"web": {"results": []}}
|
||||
|
||||
with patch(
|
||||
"application.agents.tools.brave.requests.get",
|
||||
"docsgpt.agents.tools.brave.requests.get",
|
||||
return_value=fake_response,
|
||||
) as mock_get:
|
||||
tool.execute_action(
|
||||
@@ -28,7 +28,7 @@ class TestBraveOptionalParams:
|
||||
assert params.get("result_filter") == "news"
|
||||
|
||||
def test_extra_snippets_flag(self):
|
||||
from application.agents.tools.brave import BraveSearchTool
|
||||
from docsgpt.agents.tools.brave import BraveSearchTool
|
||||
|
||||
tool = BraveSearchTool(config={"token": "tk"})
|
||||
fake_response = MagicMock()
|
||||
@@ -36,7 +36,7 @@ class TestBraveOptionalParams:
|
||||
fake_response.json.return_value = {"web": {"results": []}}
|
||||
|
||||
with patch(
|
||||
"application.agents.tools.brave.requests.get",
|
||||
"docsgpt.agents.tools.brave.requests.get",
|
||||
return_value=fake_response,
|
||||
) as mock_get:
|
||||
tool.execute_action(
|
||||
@@ -46,7 +46,7 @@ class TestBraveOptionalParams:
|
||||
assert params.get("extra_snippets") == 1
|
||||
|
||||
def test_summary_flag(self):
|
||||
from application.agents.tools.brave import BraveSearchTool
|
||||
from docsgpt.agents.tools.brave import BraveSearchTool
|
||||
|
||||
tool = BraveSearchTool(config={"token": "tk"})
|
||||
fake_response = MagicMock()
|
||||
@@ -54,7 +54,7 @@ class TestBraveOptionalParams:
|
||||
fake_response.json.return_value = {"web": {"results": []}}
|
||||
|
||||
with patch(
|
||||
"application.agents.tools.brave.requests.get",
|
||||
"docsgpt.agents.tools.brave.requests.get",
|
||||
return_value=fake_response,
|
||||
) as mock_get:
|
||||
tool.execute_action("brave_web_search", query="x", summary=True)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Comprehensive tests for application/agents/tools/internal_search.py
|
||||
"""Comprehensive tests for docsgpt/agents/tools/internal_search.py
|
||||
|
||||
Covers: InternalSearchTool (search, list_files, path_filter, error handling,
|
||||
directory structure loading), build helpers, add_internal_search_tool,
|
||||
@@ -9,7 +9,7 @@ from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from application.agents.tools.internal_search import (
|
||||
from docsgpt.agents.tools.internal_search import (
|
||||
INTERNAL_TOOL_ENTRY,
|
||||
INTERNAL_TOOL_ID,
|
||||
InternalSearchTool,
|
||||
@@ -404,7 +404,7 @@ class TestBuildHelpers:
|
||||
}
|
||||
|
||||
with patch(
|
||||
"application.agents.tools.internal_search.sources_have_directory_structure",
|
||||
"docsgpt.agents.tools.internal_search.sources_have_directory_structure",
|
||||
return_value=False,
|
||||
):
|
||||
add_internal_search_tool(tools_dict, retriever_config)
|
||||
|
||||
@@ -12,21 +12,21 @@ def _patch_db(conn):
|
||||
yield conn
|
||||
|
||||
with patch(
|
||||
"application.storage.db.session.db_readonly", _yield
|
||||
"docsgpt.storage.db.session.db_readonly", _yield
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
class TestGetDirectoryStructureFromDb:
|
||||
def test_returns_none_for_empty_active_docs(self):
|
||||
from application.agents.tools.internal_search import InternalSearchTool
|
||||
from docsgpt.agents.tools.internal_search import InternalSearchTool
|
||||
|
||||
tool = InternalSearchTool({"source": {}})
|
||||
assert tool._get_directory_structure() is None
|
||||
|
||||
def test_loads_single_source_structure(self, pg_conn):
|
||||
from application.agents.tools.internal_search import InternalSearchTool
|
||||
from application.storage.db.repositories.sources import (
|
||||
from docsgpt.agents.tools.internal_search import InternalSearchTool
|
||||
from docsgpt.storage.db.repositories.sources import (
|
||||
SourcesRepository,
|
||||
)
|
||||
|
||||
@@ -47,8 +47,8 @@ class TestGetDirectoryStructureFromDb:
|
||||
assert "file.txt" in got
|
||||
|
||||
def test_loads_multiple_sources_merges(self, pg_conn):
|
||||
from application.agents.tools.internal_search import InternalSearchTool
|
||||
from application.storage.db.repositories.sources import (
|
||||
from docsgpt.agents.tools.internal_search import InternalSearchTool
|
||||
from docsgpt.storage.db.repositories.sources import (
|
||||
SourcesRepository,
|
||||
)
|
||||
|
||||
@@ -74,7 +74,7 @@ class TestGetDirectoryStructureFromDb:
|
||||
assert "src1" in got or "a.txt" in got
|
||||
|
||||
def test_skips_missing_source(self, pg_conn):
|
||||
from application.agents.tools.internal_search import InternalSearchTool
|
||||
from docsgpt.agents.tools.internal_search import InternalSearchTool
|
||||
|
||||
tool = InternalSearchTool({
|
||||
"source": {"active_docs": "00000000-0000-0000-0000-000000000000"},
|
||||
@@ -87,16 +87,16 @@ class TestGetDirectoryStructureFromDb:
|
||||
|
||||
class TestSourcesHaveDirectoryStructureDb:
|
||||
def test_false_no_active_docs(self):
|
||||
from application.agents.tools.internal_search import (
|
||||
from docsgpt.agents.tools.internal_search import (
|
||||
sources_have_directory_structure,
|
||||
)
|
||||
assert sources_have_directory_structure({}) is False
|
||||
|
||||
def test_true_when_source_has_structure(self, pg_conn):
|
||||
from application.agents.tools.internal_search import (
|
||||
from docsgpt.agents.tools.internal_search import (
|
||||
sources_have_directory_structure,
|
||||
)
|
||||
from application.storage.db.repositories.sources import (
|
||||
from docsgpt.storage.db.repositories.sources import (
|
||||
SourcesRepository,
|
||||
)
|
||||
|
||||
@@ -111,10 +111,10 @@ class TestSourcesHaveDirectoryStructureDb:
|
||||
assert got is True
|
||||
|
||||
def test_false_when_source_has_no_structure(self, pg_conn):
|
||||
from application.agents.tools.internal_search import (
|
||||
from docsgpt.agents.tools.internal_search import (
|
||||
sources_have_directory_structure,
|
||||
)
|
||||
from application.storage.db.repositories.sources import (
|
||||
from docsgpt.storage.db.repositories.sources import (
|
||||
SourcesRepository,
|
||||
)
|
||||
|
||||
@@ -126,10 +126,10 @@ class TestSourcesHaveDirectoryStructureDb:
|
||||
assert got is False
|
||||
|
||||
def test_legacy_id_lookup(self, pg_conn):
|
||||
from application.agents.tools.internal_search import (
|
||||
from docsgpt.agents.tools.internal_search import (
|
||||
sources_have_directory_structure,
|
||||
)
|
||||
from application.storage.db.repositories.sources import (
|
||||
from docsgpt.storage.db.repositories.sources import (
|
||||
SourcesRepository,
|
||||
)
|
||||
|
||||
|
||||
+120
-120
@@ -1,4 +1,4 @@
|
||||
"""Comprehensive tests for application/agents/tools/mcp_tool.py
|
||||
"""Comprehensive tests for docsgpt/agents/tools/mcp_tool.py
|
||||
|
||||
Covers: MCPTool init, cache key generation, transport creation, tool formatting,
|
||||
result formatting, execute_action, discover_tools, test_connection,
|
||||
@@ -26,12 +26,12 @@ def _patch_mcp_globals(monkeypatch):
|
||||
"""
|
||||
import sys
|
||||
|
||||
if "application.agents.tools.mcp_tool" in sys.modules:
|
||||
mcp_mod = sys.modules["application.agents.tools.mcp_tool"]
|
||||
if "docsgpt.agents.tools.mcp_tool" in sys.modules:
|
||||
mcp_mod = sys.modules["docsgpt.agents.tools.mcp_tool"]
|
||||
else:
|
||||
mock_tasks = MagicMock()
|
||||
monkeypatch.setitem(sys.modules, "application.api.user.tasks", mock_tasks)
|
||||
import application.agents.tools.mcp_tool as mcp_mod
|
||||
monkeypatch.setitem(sys.modules, "docsgpt.api.user.tasks", mock_tasks)
|
||||
import docsgpt.agents.tools.mcp_tool as mcp_mod
|
||||
|
||||
monkeypatch.setattr(mcp_mod, "_mcp_clients_cache", {})
|
||||
# Bypass DNS-resolving URL validation for tests using fake hostnames.
|
||||
@@ -60,7 +60,7 @@ def bearer_config():
|
||||
|
||||
|
||||
def _make_tool(config, **kwargs):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
with patch.object(MCPTool, "_setup_client"):
|
||||
return MCPTool(config, **kwargs)
|
||||
@@ -87,14 +87,14 @@ class TestMCPToolInit:
|
||||
assert tool.auth_credentials["bearer_token"] == "tok_123"
|
||||
|
||||
def test_no_server_url_skips_setup(self):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
with patch.object(MCPTool, "_setup_client") as mock_setup:
|
||||
MCPTool({"server_url": "", "auth_type": "none"})
|
||||
mock_setup.assert_not_called()
|
||||
|
||||
def test_oauth_skips_setup(self):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
with patch.object(MCPTool, "_setup_client") as mock_setup:
|
||||
MCPTool({
|
||||
@@ -104,10 +104,10 @@ class TestMCPToolInit:
|
||||
mock_setup.assert_not_called()
|
||||
|
||||
def test_encrypted_credentials_decryption(self):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
with patch.object(MCPTool, "_setup_client"), \
|
||||
patch("application.agents.tools.mcp_tool.decrypt_credentials",
|
||||
patch("docsgpt.agents.tools.mcp_tool.decrypt_credentials",
|
||||
return_value={"bearer_token": "decrypted_tok"}):
|
||||
tool = MCPTool(
|
||||
{
|
||||
@@ -140,27 +140,27 @@ class TestMCPToolInit:
|
||||
assert tool.custom_headers == {"X-Custom": "val"}
|
||||
|
||||
def test_rejects_metadata_ip(self, monkeypatch):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from application.core.url_validation import validate_url as real_validate_url
|
||||
import application.agents.tools.mcp_tool as mcp_mod
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.core.url_validation import validate_url as real_validate_url
|
||||
import docsgpt.agents.tools.mcp_tool as mcp_mod
|
||||
|
||||
monkeypatch.setattr(mcp_mod, "validate_url", real_validate_url)
|
||||
with pytest.raises(ValueError, match="Invalid MCP server URL"):
|
||||
MCPTool(config={"server_url": "http://169.254.169.254/latest/meta-data", "auth_type": "none"})
|
||||
|
||||
def test_rejects_localhost(self, monkeypatch):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from application.core.url_validation import validate_url as real_validate_url
|
||||
import application.agents.tools.mcp_tool as mcp_mod
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.core.url_validation import validate_url as real_validate_url
|
||||
import docsgpt.agents.tools.mcp_tool as mcp_mod
|
||||
|
||||
monkeypatch.setattr(mcp_mod, "validate_url", real_validate_url)
|
||||
with pytest.raises(ValueError, match="Invalid MCP server URL"):
|
||||
MCPTool(config={"server_url": "http://localhost:8080/mcp", "auth_type": "none"})
|
||||
|
||||
def test_rejects_private_ip(self, monkeypatch):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from application.core.url_validation import validate_url as real_validate_url
|
||||
import application.agents.tools.mcp_tool as mcp_mod
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.core.url_validation import validate_url as real_validate_url
|
||||
import docsgpt.agents.tools.mcp_tool as mcp_mod
|
||||
|
||||
monkeypatch.setattr(mcp_mod, "validate_url", real_validate_url)
|
||||
with pytest.raises(ValueError, match="Invalid MCP server URL"):
|
||||
@@ -174,7 +174,7 @@ class TestMCPToolInit:
|
||||
assert tool.server_url == "https://mcp.example.com/api"
|
||||
|
||||
def test_empty_server_url_allowed(self):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
with patch.object(MCPTool, "_setup_client"):
|
||||
tool = MCPTool(config={"server_url": "", "auth_type": "none"})
|
||||
@@ -198,7 +198,7 @@ class TestResolveRedirectUri:
|
||||
assert tool.redirect_uri == "https://my.app/callback"
|
||||
|
||||
def test_fallback_to_settings(self, monkeypatch):
|
||||
from application.core.settings import settings
|
||||
from docsgpt.core.settings import settings
|
||||
|
||||
monkeypatch.setattr(settings, "API_URL", "https://api.docsgpt.co")
|
||||
tool = _make_tool({
|
||||
@@ -488,7 +488,7 @@ class TestExecuteAction:
|
||||
with pytest.raises(Exception, match="No MCP server configured"):
|
||||
tool.execute_action("test_action")
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
def test_successful_execute(self, mock_run, mcp_config):
|
||||
tool = _make_tool(mcp_config)
|
||||
tool._client = MagicMock()
|
||||
@@ -499,7 +499,7 @@ class TestExecuteAction:
|
||||
mock_run.assert_called_once_with("call_tool", "test_action", param1="val1")
|
||||
assert result == {"key": "value"}
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
def test_empty_kwargs_cleaned(self, mock_run, mcp_config):
|
||||
tool = _make_tool(mcp_config)
|
||||
tool._client = MagicMock()
|
||||
@@ -512,9 +512,9 @@ class TestExecuteAction:
|
||||
assert "param2" not in call_kwargs
|
||||
assert call_kwargs["param3"] == "real"
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
def test_auth_error_retries_for_non_oauth(self, mock_run, mcp_config):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = _make_tool(mcp_config)
|
||||
tool._client = MagicMock()
|
||||
@@ -528,7 +528,7 @@ class TestExecuteAction:
|
||||
result = tool.execute_action("act")
|
||||
assert result == {"key": "retry_ok"}
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
def test_auth_error_raises_for_oauth(self, mock_run):
|
||||
tool = _make_tool({
|
||||
"server_url": "https://mcp.example.com",
|
||||
@@ -540,7 +540,7 @@ class TestExecuteAction:
|
||||
with pytest.raises(Exception, match="OAuth session expired"):
|
||||
tool.execute_action("act")
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
def test_non_auth_error_raises(self, mock_run, mcp_config):
|
||||
tool = _make_tool(mcp_config)
|
||||
tool._client = MagicMock()
|
||||
@@ -549,9 +549,9 @@ class TestExecuteAction:
|
||||
with pytest.raises(Exception, match="Failed to execute action"):
|
||||
tool.execute_action("act")
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
def test_no_client_calls_setup(self, mock_run, mcp_config):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = _make_tool(mcp_config)
|
||||
tool._client = None
|
||||
@@ -570,7 +570,7 @@ class TestExecuteAction:
|
||||
@pytest.mark.unit
|
||||
class TestDiscoverTools:
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
def test_discover_tools_success(self, mock_run, mcp_config):
|
||||
tool = _make_tool(mcp_config)
|
||||
tool._client = MagicMock()
|
||||
@@ -584,7 +584,7 @@ class TestDiscoverTools:
|
||||
tool = _make_tool({"server_url": "", "auth_type": "none"})
|
||||
assert tool.discover_tools() == []
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
def test_discover_tools_error(self, mock_run, mcp_config):
|
||||
tool = _make_tool(mcp_config)
|
||||
tool._client = MagicMock()
|
||||
@@ -614,7 +614,7 @@ class TestTestConnection:
|
||||
assert result["success"] is False
|
||||
assert "Invalid URL scheme" in result["message"]
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
def test_regular_connection_success(self, mock_run, mcp_config):
|
||||
tool = _make_tool(mcp_config)
|
||||
tool._client = MagicMock()
|
||||
@@ -627,7 +627,7 @@ class TestTestConnection:
|
||||
assert result["success"] is True
|
||||
assert result["tools_count"] == 1
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
def test_regular_connection_ping_fails_tools_work(self, mock_run, mcp_config):
|
||||
tool = _make_tool(mcp_config)
|
||||
tool._client = MagicMock()
|
||||
@@ -639,7 +639,7 @@ class TestTestConnection:
|
||||
result = tool.test_connection()
|
||||
assert result["success"] is True
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
def test_regular_connection_both_fail(self, mock_run, mcp_config):
|
||||
tool = _make_tool(mcp_config)
|
||||
tool._client = MagicMock()
|
||||
@@ -652,7 +652,7 @@ class TestTestConnection:
|
||||
assert result["success"] is False
|
||||
|
||||
def test_client_init_failure(self):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = _make_tool({
|
||||
"server_url": "https://good.example.com",
|
||||
@@ -796,7 +796,7 @@ class TestGetActionsMetadata:
|
||||
class TestSetupClient:
|
||||
|
||||
def test_setup_client_caches_client(self, mcp_config):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool.__new__(MCPTool)
|
||||
tool.config = mcp_config
|
||||
@@ -817,7 +817,7 @@ class TestSetupClient:
|
||||
|
||||
mock_client = MagicMock()
|
||||
with patch.object(MCPTool, "_create_transport", return_value=MagicMock()), \
|
||||
patch("application.agents.tools.mcp_tool.Client", return_value=mock_client):
|
||||
patch("docsgpt.agents.tools.mcp_tool.Client", return_value=mock_client):
|
||||
tool._setup_client()
|
||||
assert tool._client is mock_client
|
||||
|
||||
@@ -831,7 +831,7 @@ class TestSetupClient:
|
||||
class TestMCPOAuthManager:
|
||||
|
||||
def test_handle_callback_success(self):
|
||||
from application.agents.tools.mcp_tool import MCPOAuthManager
|
||||
from docsgpt.agents.tools.mcp_tool import MCPOAuthManager
|
||||
|
||||
mock_redis = MagicMock()
|
||||
manager = MCPOAuthManager(mock_redis)
|
||||
@@ -841,14 +841,14 @@ class TestMCPOAuthManager:
|
||||
mock_redis.setex.assert_called()
|
||||
|
||||
def test_handle_callback_no_redis(self):
|
||||
from application.agents.tools.mcp_tool import MCPOAuthManager
|
||||
from docsgpt.agents.tools.mcp_tool import MCPOAuthManager
|
||||
|
||||
manager = MCPOAuthManager(None)
|
||||
result = manager.handle_oauth_callback(state="abc", code="code")
|
||||
assert result is False
|
||||
|
||||
def test_handle_callback_no_state(self):
|
||||
from application.agents.tools.mcp_tool import MCPOAuthManager
|
||||
from docsgpt.agents.tools.mcp_tool import MCPOAuthManager
|
||||
|
||||
mock_redis = MagicMock()
|
||||
manager = MCPOAuthManager(mock_redis)
|
||||
@@ -856,7 +856,7 @@ class TestMCPOAuthManager:
|
||||
assert result is False
|
||||
|
||||
def test_handle_callback_error(self):
|
||||
from application.agents.tools.mcp_tool import MCPOAuthManager
|
||||
from docsgpt.agents.tools.mcp_tool import MCPOAuthManager
|
||||
|
||||
mock_redis = MagicMock()
|
||||
manager = MCPOAuthManager(mock_redis)
|
||||
@@ -867,14 +867,14 @@ class TestMCPOAuthManager:
|
||||
assert result is False
|
||||
|
||||
def test_get_oauth_status_no_task(self):
|
||||
from application.agents.tools.mcp_tool import MCPOAuthManager
|
||||
from docsgpt.agents.tools.mcp_tool import MCPOAuthManager
|
||||
|
||||
manager = MCPOAuthManager(MagicMock())
|
||||
result = manager.get_oauth_status("", "alice")
|
||||
assert result["status"] == "not_started"
|
||||
|
||||
def test_get_oauth_status_no_user(self):
|
||||
from application.agents.tools.mcp_tool import MCPOAuthManager
|
||||
from docsgpt.agents.tools.mcp_tool import MCPOAuthManager
|
||||
|
||||
manager = MCPOAuthManager(MagicMock())
|
||||
result = manager.get_oauth_status("task123", "")
|
||||
@@ -893,7 +893,7 @@ class TestMCPOAuthManager:
|
||||
through unchanged so ``mcp.py``'s ``connect_mcp`` can use them
|
||||
without further plumbing.
|
||||
"""
|
||||
from application.agents.tools.mcp_tool import MCPOAuthManager
|
||||
from docsgpt.agents.tools.mcp_tool import MCPOAuthManager
|
||||
|
||||
completed_envelope = json.dumps(
|
||||
{
|
||||
@@ -934,7 +934,7 @@ class TestMCPOAuthManager:
|
||||
# Scan window must cover the full bounded stream so a flood of
|
||||
# concurrent source-ingest events between popup-completed and
|
||||
# Save can't push the OAuth envelope out of view.
|
||||
from application.core.settings import settings
|
||||
from docsgpt.core.settings import settings
|
||||
|
||||
assert call_args.kwargs.get("count") >= settings.EVENTS_STREAM_MAXLEN
|
||||
|
||||
@@ -942,8 +942,8 @@ class TestMCPOAuthManager:
|
||||
"""Regression: count must scale with ``EVENTS_STREAM_MAXLEN``
|
||||
so the OAuth completion envelope is reachable even after
|
||||
concurrent source-ingest events flood the user stream."""
|
||||
from application.agents.tools.mcp_tool import MCPOAuthManager
|
||||
from application.core.settings import settings
|
||||
from docsgpt.agents.tools.mcp_tool import MCPOAuthManager
|
||||
from docsgpt.core.settings import settings
|
||||
|
||||
mock_redis = MagicMock()
|
||||
mock_redis.xrevrange.return_value = []
|
||||
@@ -955,7 +955,7 @@ class TestMCPOAuthManager:
|
||||
assert call_args.kwargs.get("count") >= settings.EVENTS_STREAM_MAXLEN
|
||||
|
||||
def test_get_oauth_status_returns_not_found_when_no_match(self):
|
||||
from application.agents.tools.mcp_tool import MCPOAuthManager
|
||||
from docsgpt.agents.tools.mcp_tool import MCPOAuthManager
|
||||
|
||||
mock_redis = MagicMock()
|
||||
# Stream is non-empty but has nothing matching this task.
|
||||
@@ -986,7 +986,7 @@ class TestDBTokenStorage:
|
||||
"""Covers the repository-backed DBTokenStorage post-PG migration.
|
||||
|
||||
Round-trip tests use the ephemeral ``pg_conn`` fixture and patch
|
||||
``db_session``/``db_readonly`` in ``application.agents.tools.mcp_tool``
|
||||
``db_session``/``db_readonly`` in ``docsgpt.agents.tools.mcp_tool``
|
||||
so the real INSERT/SELECT SQL runs. This is the shape that caught the
|
||||
``server_url`` NULL-column regression — ``get_tokens`` only succeeds
|
||||
if ``set_tokens`` populated the scalar column.
|
||||
@@ -996,7 +996,7 @@ class TestDBTokenStorage:
|
||||
def _patch_db(monkeypatch, pg_conn):
|
||||
from contextlib import contextmanager
|
||||
|
||||
import application.agents.tools.mcp_tool as mcp_mod
|
||||
import docsgpt.agents.tools.mcp_tool as mcp_mod
|
||||
|
||||
@contextmanager
|
||||
def _yield():
|
||||
@@ -1006,13 +1006,13 @@ class TestDBTokenStorage:
|
||||
monkeypatch.setattr(mcp_mod, "db_readonly", _yield, raising=False)
|
||||
# mcp_tool imports db_session/db_readonly *inside* the helper
|
||||
# methods, so also patch the origin module they come from.
|
||||
import application.storage.db.session as session_mod
|
||||
import docsgpt.storage.db.session as session_mod
|
||||
|
||||
monkeypatch.setattr(session_mod, "db_session", _yield)
|
||||
monkeypatch.setattr(session_mod, "db_readonly", _yield)
|
||||
|
||||
def test_get_base_url(self):
|
||||
from application.agents.tools.mcp_tool import DBTokenStorage
|
||||
from docsgpt.agents.tools.mcp_tool import DBTokenStorage
|
||||
|
||||
assert (
|
||||
DBTokenStorage.get_base_url("https://mcp.example.com/api/v1")
|
||||
@@ -1020,7 +1020,7 @@ class TestDBTokenStorage:
|
||||
)
|
||||
|
||||
def test_get_base_url_with_port(self):
|
||||
from application.agents.tools.mcp_tool import DBTokenStorage
|
||||
from docsgpt.agents.tools.mcp_tool import DBTokenStorage
|
||||
|
||||
assert (
|
||||
DBTokenStorage.get_base_url("http://localhost:8080/path")
|
||||
@@ -1028,7 +1028,7 @@ class TestDBTokenStorage:
|
||||
)
|
||||
|
||||
def test_pg_provider_includes_base_url(self):
|
||||
from application.agents.tools.mcp_tool import DBTokenStorage
|
||||
from docsgpt.agents.tools.mcp_tool import DBTokenStorage
|
||||
|
||||
storage = DBTokenStorage(
|
||||
server_url="https://mcp.example.com/api",
|
||||
@@ -1037,7 +1037,7 @@ class TestDBTokenStorage:
|
||||
assert storage._pg_provider() == "mcp:https://mcp.example.com"
|
||||
|
||||
def test_serialize_client_info(self):
|
||||
from application.agents.tools.mcp_tool import DBTokenStorage
|
||||
from docsgpt.agents.tools.mcp_tool import DBTokenStorage
|
||||
|
||||
storage = DBTokenStorage(
|
||||
server_url="https://mcp.example.com",
|
||||
@@ -1048,7 +1048,7 @@ class TestDBTokenStorage:
|
||||
assert result["redirect_uris"] == ["https://example.com/cb"]
|
||||
|
||||
def test_get_tokens_none_when_no_row(self, monkeypatch, pg_conn):
|
||||
from application.agents.tools.mcp_tool import DBTokenStorage
|
||||
from docsgpt.agents.tools.mcp_tool import DBTokenStorage
|
||||
|
||||
self._patch_db(monkeypatch, pg_conn)
|
||||
storage = DBTokenStorage(
|
||||
@@ -1069,7 +1069,7 @@ class TestDBTokenStorage:
|
||||
``get_by_user_and_server_url``) can resolve the row."""
|
||||
from mcp.shared.auth import OAuthToken
|
||||
|
||||
from application.agents.tools.mcp_tool import DBTokenStorage
|
||||
from docsgpt.agents.tools.mcp_tool import DBTokenStorage
|
||||
|
||||
self._patch_db(monkeypatch, pg_conn)
|
||||
storage = DBTokenStorage(
|
||||
@@ -1102,8 +1102,8 @@ class TestDBTokenStorage:
|
||||
column, not only into the JSONB blob."""
|
||||
from mcp.shared.auth import OAuthToken
|
||||
|
||||
from application.agents.tools.mcp_tool import DBTokenStorage
|
||||
from application.storage.db.repositories.connector_sessions import (
|
||||
from docsgpt.agents.tools.mcp_tool import DBTokenStorage
|
||||
from docsgpt.storage.db.repositories.connector_sessions import (
|
||||
ConnectorSessionsRepository,
|
||||
)
|
||||
|
||||
@@ -1137,8 +1137,8 @@ class TestDBTokenStorage:
|
||||
def test_clear_removes_row(self, monkeypatch, pg_conn):
|
||||
from mcp.shared.auth import OAuthToken
|
||||
|
||||
from application.agents.tools.mcp_tool import DBTokenStorage
|
||||
from application.storage.db.repositories.connector_sessions import (
|
||||
from docsgpt.agents.tools.mcp_tool import DBTokenStorage
|
||||
from docsgpt.storage.db.repositories.connector_sessions import (
|
||||
ConnectorSessionsRepository,
|
||||
)
|
||||
|
||||
@@ -1178,7 +1178,7 @@ class TestDBTokenStorage:
|
||||
class TestNonInteractiveOAuth:
|
||||
|
||||
def test_redirect_handler_raises(self):
|
||||
from application.agents.tools.mcp_tool import NonInteractiveOAuth
|
||||
from docsgpt.agents.tools.mcp_tool import NonInteractiveOAuth
|
||||
|
||||
mock_db = MagicMock()
|
||||
mock_collection = MagicMock()
|
||||
@@ -1204,7 +1204,7 @@ class TestNonInteractiveOAuth:
|
||||
loop.close()
|
||||
|
||||
def test_callback_handler_raises(self):
|
||||
from application.agents.tools.mcp_tool import NonInteractiveOAuth
|
||||
from docsgpt.agents.tools.mcp_tool import NonInteractiveOAuth
|
||||
|
||||
mock_db = MagicMock()
|
||||
mock_collection = MagicMock()
|
||||
@@ -1257,7 +1257,7 @@ class TestRunAsyncOperation:
|
||||
class TestResolveRedirectUriExtended:
|
||||
|
||||
def test_mcp_oauth_redirect_uri_setting(self, monkeypatch):
|
||||
from application.core.settings import settings
|
||||
from docsgpt.core.settings import settings
|
||||
|
||||
monkeypatch.setattr(settings, "MCP_OAUTH_REDIRECT_URI", "https://custom.redirect/callback/")
|
||||
# Ensure no configured redirect_uri in config
|
||||
@@ -1268,7 +1268,7 @@ class TestResolveRedirectUriExtended:
|
||||
assert tool.redirect_uri == "https://custom.redirect/callback"
|
||||
|
||||
def test_connector_redirect_base_uri_setting(self, monkeypatch):
|
||||
from application.core.settings import settings
|
||||
from docsgpt.core.settings import settings
|
||||
|
||||
monkeypatch.setattr(settings, "MCP_OAUTH_REDIRECT_URI", None, raising=False)
|
||||
monkeypatch.setattr(
|
||||
@@ -1282,7 +1282,7 @@ class TestResolveRedirectUriExtended:
|
||||
assert tool.redirect_uri == "https://connector.example.com/api/mcp_server/callback"
|
||||
|
||||
def test_connector_redirect_base_uri_invalid_url(self, monkeypatch):
|
||||
from application.core.settings import settings
|
||||
from docsgpt.core.settings import settings
|
||||
|
||||
monkeypatch.setattr(settings, "MCP_OAUTH_REDIRECT_URI", None, raising=False)
|
||||
# Provide a base URI that has no scheme
|
||||
@@ -1306,8 +1306,8 @@ class TestResolveRedirectUriExtended:
|
||||
class TestSetupClientExtended:
|
||||
|
||||
def test_cache_hit_returns_cached_client(self):
|
||||
import application.agents.tools.mcp_tool as mcp_mod
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
import docsgpt.agents.tools.mcp_tool as mcp_mod
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool.__new__(MCPTool)
|
||||
tool.config = {"server_url": "https://mcp.example.com", "auth_type": "none"}
|
||||
@@ -1336,8 +1336,8 @@ class TestSetupClientExtended:
|
||||
assert tool._client is cached_client
|
||||
|
||||
def test_expired_cache_creates_new_client(self):
|
||||
import application.agents.tools.mcp_tool as mcp_mod
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
import docsgpt.agents.tools.mcp_tool as mcp_mod
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool.__new__(MCPTool)
|
||||
tool.config = {"server_url": "https://mcp.example.com", "auth_type": "none"}
|
||||
@@ -1364,14 +1364,14 @@ class TestSetupClientExtended:
|
||||
|
||||
new_client = MagicMock()
|
||||
with patch.object(MCPTool, "_create_transport", return_value=MagicMock()), \
|
||||
patch("application.agents.tools.mcp_tool.Client", return_value=new_client):
|
||||
patch("docsgpt.agents.tools.mcp_tool.Client", return_value=new_client):
|
||||
tool._setup_client()
|
||||
assert tool._client is new_client
|
||||
assert "expired_cache_key" not in mcp_mod._mcp_clients_cache or \
|
||||
mcp_mod._mcp_clients_cache["expired_cache_key"]["client"] is new_client
|
||||
|
||||
def test_setup_client_oauth_query_mode(self):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool.__new__(MCPTool)
|
||||
tool.config = {"server_url": "https://mcp.example.com", "auth_type": "oauth"}
|
||||
@@ -1393,14 +1393,14 @@ class TestSetupClientExtended:
|
||||
|
||||
mock_client = MagicMock()
|
||||
with patch.object(MCPTool, "_create_transport", return_value=MagicMock()), \
|
||||
patch("application.agents.tools.mcp_tool.Client", return_value=mock_client), \
|
||||
patch("application.agents.tools.mcp_tool.get_redis_instance", return_value=MagicMock()), \
|
||||
patch("application.agents.tools.mcp_tool.NonInteractiveOAuth"):
|
||||
patch("docsgpt.agents.tools.mcp_tool.Client", return_value=mock_client), \
|
||||
patch("docsgpt.agents.tools.mcp_tool.get_redis_instance", return_value=MagicMock()), \
|
||||
patch("docsgpt.agents.tools.mcp_tool.NonInteractiveOAuth"):
|
||||
tool._setup_client()
|
||||
assert tool._client is mock_client
|
||||
|
||||
def test_setup_client_oauth_interactive_mode(self):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool.__new__(MCPTool)
|
||||
tool.config = {"server_url": "https://mcp.example.com", "auth_type": "oauth"}
|
||||
@@ -1423,14 +1423,14 @@ class TestSetupClientExtended:
|
||||
|
||||
mock_client = MagicMock()
|
||||
with patch.object(MCPTool, "_create_transport", return_value=MagicMock()), \
|
||||
patch("application.agents.tools.mcp_tool.Client", return_value=mock_client), \
|
||||
patch("application.agents.tools.mcp_tool.get_redis_instance", return_value=MagicMock()), \
|
||||
patch("application.agents.tools.mcp_tool.DocsGPTOAuth"):
|
||||
patch("docsgpt.agents.tools.mcp_tool.Client", return_value=mock_client), \
|
||||
patch("docsgpt.agents.tools.mcp_tool.get_redis_instance", return_value=MagicMock()), \
|
||||
patch("docsgpt.agents.tools.mcp_tool.DocsGPTOAuth"):
|
||||
tool._setup_client()
|
||||
assert tool._client is mock_client
|
||||
|
||||
def test_setup_client_bearer_auth(self):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = MCPTool.__new__(MCPTool)
|
||||
tool.config = {"server_url": "https://mcp.example.com", "auth_type": "bearer"}
|
||||
@@ -1451,8 +1451,8 @@ class TestSetupClientExtended:
|
||||
|
||||
mock_client = MagicMock()
|
||||
with patch.object(MCPTool, "_create_transport", return_value=MagicMock()), \
|
||||
patch("application.agents.tools.mcp_tool.Client", return_value=mock_client), \
|
||||
patch("application.agents.tools.mcp_tool.BearerAuth") as mock_bearer_auth:
|
||||
patch("docsgpt.agents.tools.mcp_tool.Client", return_value=mock_client), \
|
||||
patch("docsgpt.agents.tools.mcp_tool.BearerAuth") as mock_bearer_auth:
|
||||
tool._setup_client()
|
||||
mock_bearer_auth.assert_called_once_with("my_token")
|
||||
assert tool._client is mock_client
|
||||
@@ -1626,7 +1626,7 @@ class TestTestConnectionExtended:
|
||||
result = tool.test_connection()
|
||||
assert result["success"] is False
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
def test_no_tools_and_no_ping_fails(self, mock_run, mcp_config):
|
||||
tool = _make_tool(mcp_config)
|
||||
tool._client = MagicMock()
|
||||
@@ -1639,7 +1639,7 @@ class TestTestConnectionExtended:
|
||||
# ping_ok is True but tools is empty, should still succeed
|
||||
assert result["success"] is True
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
def test_ping_fails_no_tools_fails(self, mock_run, mcp_config):
|
||||
tool = _make_tool(mcp_config)
|
||||
tool._client = MagicMock()
|
||||
@@ -1652,7 +1652,7 @@ class TestTestConnectionExtended:
|
||||
assert "ping failed" in result["message"]
|
||||
|
||||
def test_oauth_connection_with_valid_tokens(self):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = _make_tool({
|
||||
"server_url": "https://mcp.example.com",
|
||||
@@ -1665,7 +1665,7 @@ class TestTestConnectionExtended:
|
||||
mock_token = MagicMock()
|
||||
mock_token.access_token = "valid_token"
|
||||
|
||||
with patch("application.agents.tools.mcp_tool.DBTokenStorage") as mock_storage_cls:
|
||||
with patch("docsgpt.agents.tools.mcp_tool.DBTokenStorage") as mock_storage_cls:
|
||||
mock_storage = MagicMock()
|
||||
|
||||
async def fake_get_tokens():
|
||||
@@ -1689,7 +1689,7 @@ class TestTestConnectionExtended:
|
||||
tool.user_id = "user1"
|
||||
tool._client = MagicMock()
|
||||
|
||||
with patch("application.agents.tools.mcp_tool.DBTokenStorage") as mock_storage_cls:
|
||||
with patch("docsgpt.agents.tools.mcp_tool.DBTokenStorage") as mock_storage_cls:
|
||||
mock_storage = MagicMock()
|
||||
|
||||
async def fake_get_tokens():
|
||||
@@ -1700,7 +1700,7 @@ class TestTestConnectionExtended:
|
||||
|
||||
mock_task_result = MagicMock()
|
||||
mock_task_result.id = "task_abc"
|
||||
with patch("application.agents.tools.mcp_tool.mcp_oauth_task") as mock_task:
|
||||
with patch("docsgpt.agents.tools.mcp_tool.mcp_oauth_task") as mock_task:
|
||||
mock_task.delay.return_value = mock_task_result
|
||||
result = tool.test_connection()
|
||||
assert result["success"] is False
|
||||
@@ -1708,7 +1708,7 @@ class TestTestConnectionExtended:
|
||||
assert result["task_id"] == "task_abc"
|
||||
|
||||
def test_oauth_connection_token_validation_fails(self):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = _make_tool({
|
||||
"server_url": "https://mcp.example.com",
|
||||
@@ -1721,7 +1721,7 @@ class TestTestConnectionExtended:
|
||||
mock_token = MagicMock()
|
||||
mock_token.access_token = "expired_token"
|
||||
|
||||
with patch("application.agents.tools.mcp_tool.DBTokenStorage") as mock_storage_cls:
|
||||
with patch("docsgpt.agents.tools.mcp_tool.DBTokenStorage") as mock_storage_cls:
|
||||
mock_storage = MagicMock()
|
||||
|
||||
async def fake_get_tokens():
|
||||
@@ -1734,7 +1734,7 @@ class TestTestConnectionExtended:
|
||||
mock_task_result.id = "task_retry"
|
||||
with patch.object(tool, "discover_tools", side_effect=Exception("401 Unauthorized")), \
|
||||
patch.object(MCPTool, "_setup_client"), \
|
||||
patch("application.agents.tools.mcp_tool.mcp_oauth_task") as mock_task:
|
||||
patch("docsgpt.agents.tools.mcp_tool.mcp_oauth_task") as mock_task:
|
||||
mock_task.delay.return_value = mock_task_result
|
||||
result = tool.test_connection()
|
||||
assert result["success"] is False
|
||||
@@ -1749,7 +1749,7 @@ class TestTestConnectionExtended:
|
||||
@pytest.mark.unit
|
||||
class TestExecuteActionExtended:
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
def test_execute_formats_result(self, mock_run, mcp_config):
|
||||
tool = _make_tool(mcp_config)
|
||||
tool._client = MagicMock()
|
||||
@@ -1765,9 +1765,9 @@ class TestExecuteActionExtended:
|
||||
assert result["content"][0]["type"] == "text"
|
||||
assert result["isError"] is False
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
def test_execute_auth_retry_second_attempt_fails(self, mock_run):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = _make_tool({
|
||||
"server_url": "https://mcp.example.com",
|
||||
@@ -1791,9 +1791,9 @@ class TestExecuteActionExtended:
|
||||
@pytest.mark.unit
|
||||
class TestDiscoverToolsExtended:
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
def test_discover_tools_no_client_calls_setup(self, mock_run, mcp_config):
|
||||
from application.agents.tools.mcp_tool import MCPTool
|
||||
from docsgpt.agents.tools.mcp_tool import MCPTool
|
||||
|
||||
tool = _make_tool(mcp_config)
|
||||
tool._client = None
|
||||
@@ -1813,7 +1813,7 @@ class TestDiscoverToolsExtended:
|
||||
@pytest.mark.unit
|
||||
class TestRegularConnectionExtended:
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
def test_regular_connection_message_format(self, mock_run, mcp_config):
|
||||
tool = _make_tool(mcp_config)
|
||||
tool._client = MagicMock()
|
||||
@@ -1827,7 +1827,7 @@ class TestRegularConnectionExtended:
|
||||
# Singular form for 1 tool
|
||||
assert "tools" not in result["message"]
|
||||
|
||||
@patch("application.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._run_async_operation")
|
||||
def test_regular_connection_multiple_tools(self, mock_run, mcp_config):
|
||||
tool = _make_tool(mcp_config)
|
||||
tool._client = MagicMock()
|
||||
@@ -1852,7 +1852,7 @@ class TestRegularConnectionExtended:
|
||||
class TestDocsGPTOAuthExtended:
|
||||
|
||||
def test_process_auth_url_success(self):
|
||||
from application.agents.tools.mcp_tool import DocsGPTOAuth
|
||||
from docsgpt.agents.tools.mcp_tool import DocsGPTOAuth
|
||||
|
||||
mock_db = MagicMock()
|
||||
mock_collection = MagicMock()
|
||||
@@ -1876,7 +1876,7 @@ class TestDocsGPTOAuthExtended:
|
||||
assert url == "https://auth.example.com/authorize?state=abc123&client_id=xyz"
|
||||
|
||||
def test_process_auth_url_no_state(self):
|
||||
from application.agents.tools.mcp_tool import DocsGPTOAuth
|
||||
from docsgpt.agents.tools.mcp_tool import DocsGPTOAuth
|
||||
|
||||
mock_db = MagicMock()
|
||||
mock_collection = MagicMock()
|
||||
@@ -1896,7 +1896,7 @@ class TestDocsGPTOAuthExtended:
|
||||
oauth._process_auth_url("https://auth.example.com/authorize?client_id=xyz")
|
||||
|
||||
def test_redirect_handler_stores_in_redis(self):
|
||||
from application.agents.tools.mcp_tool import DocsGPTOAuth
|
||||
from docsgpt.agents.tools.mcp_tool import DocsGPTOAuth
|
||||
|
||||
mock_db = MagicMock()
|
||||
mock_collection = MagicMock()
|
||||
@@ -1931,7 +1931,7 @@ class TestDocsGPTOAuthExtended:
|
||||
assert mock_redis.setex.call_count >= 2
|
||||
|
||||
def test_redirect_handler_no_redis(self):
|
||||
from application.agents.tools.mcp_tool import DocsGPTOAuth
|
||||
from docsgpt.agents.tools.mcp_tool import DocsGPTOAuth
|
||||
|
||||
mock_db = MagicMock()
|
||||
mock_collection = MagicMock()
|
||||
@@ -1960,7 +1960,7 @@ class TestDocsGPTOAuthExtended:
|
||||
assert oauth.extracted_state == "s1"
|
||||
|
||||
def test_callback_handler_no_redis_raises(self):
|
||||
from application.agents.tools.mcp_tool import DocsGPTOAuth
|
||||
from docsgpt.agents.tools.mcp_tool import DocsGPTOAuth
|
||||
|
||||
mock_db = MagicMock()
|
||||
mock_collection = MagicMock()
|
||||
@@ -1984,7 +1984,7 @@ class TestDocsGPTOAuthExtended:
|
||||
loop.close()
|
||||
|
||||
def test_callback_handler_receives_code(self):
|
||||
from application.agents.tools.mcp_tool import DocsGPTOAuth
|
||||
from docsgpt.agents.tools.mcp_tool import DocsGPTOAuth
|
||||
|
||||
mock_db = MagicMock()
|
||||
mock_collection = MagicMock()
|
||||
@@ -2016,7 +2016,7 @@ class TestDocsGPTOAuthExtended:
|
||||
loop.close()
|
||||
|
||||
def test_callback_handler_receives_error(self):
|
||||
from application.agents.tools.mcp_tool import DocsGPTOAuth
|
||||
from docsgpt.agents.tools.mcp_tool import DocsGPTOAuth
|
||||
|
||||
mock_db = MagicMock()
|
||||
mock_collection = MagicMock()
|
||||
@@ -2046,7 +2046,7 @@ class TestDocsGPTOAuthExtended:
|
||||
loop.close()
|
||||
|
||||
def test_init_scopes_as_string(self):
|
||||
from application.agents.tools.mcp_tool import DocsGPTOAuth
|
||||
from docsgpt.agents.tools.mcp_tool import DocsGPTOAuth
|
||||
|
||||
mock_db = MagicMock()
|
||||
mock_collection = MagicMock()
|
||||
@@ -2074,7 +2074,7 @@ class TestDocsGPTOAuthExtended:
|
||||
class TestDBTokenStorageExtended:
|
||||
|
||||
def test_get_tokens_with_valid_data(self):
|
||||
from application.agents.tools.mcp_tool import DBTokenStorage
|
||||
from docsgpt.agents.tools.mcp_tool import DBTokenStorage
|
||||
|
||||
mock_db = MagicMock()
|
||||
mock_collection = MagicMock()
|
||||
@@ -2101,7 +2101,7 @@ class TestDBTokenStorageExtended:
|
||||
loop.close()
|
||||
|
||||
def test_get_tokens_with_invalid_data(self):
|
||||
from application.agents.tools.mcp_tool import DBTokenStorage
|
||||
from docsgpt.agents.tools.mcp_tool import DBTokenStorage
|
||||
|
||||
mock_db = MagicMock()
|
||||
mock_collection = MagicMock()
|
||||
@@ -2124,7 +2124,7 @@ class TestDBTokenStorageExtended:
|
||||
loop.close()
|
||||
|
||||
def test_set_tokens(self):
|
||||
from application.agents.tools.mcp_tool import DBTokenStorage
|
||||
from docsgpt.agents.tools.mcp_tool import DBTokenStorage
|
||||
from mcp.shared.auth import OAuthToken
|
||||
|
||||
mock_db = MagicMock()
|
||||
@@ -2147,7 +2147,7 @@ class TestDBTokenStorageExtended:
|
||||
loop.close()
|
||||
|
||||
def test_get_client_info_none(self):
|
||||
from application.agents.tools.mcp_tool import DBTokenStorage
|
||||
from docsgpt.agents.tools.mcp_tool import DBTokenStorage
|
||||
|
||||
mock_db = MagicMock()
|
||||
mock_collection = MagicMock()
|
||||
@@ -2168,7 +2168,7 @@ class TestDBTokenStorageExtended:
|
||||
loop.close()
|
||||
|
||||
def test_get_client_info_no_client_info_key(self):
|
||||
from application.agents.tools.mcp_tool import DBTokenStorage
|
||||
from docsgpt.agents.tools.mcp_tool import DBTokenStorage
|
||||
|
||||
mock_db = MagicMock()
|
||||
mock_collection = MagicMock()
|
||||
@@ -2189,7 +2189,7 @@ class TestDBTokenStorageExtended:
|
||||
loop.close()
|
||||
|
||||
def test_get_client_info_with_valid_data(self):
|
||||
from application.agents.tools.mcp_tool import DBTokenStorage
|
||||
from docsgpt.agents.tools.mcp_tool import DBTokenStorage
|
||||
|
||||
mock_db = MagicMock()
|
||||
mock_collection = MagicMock()
|
||||
@@ -2216,7 +2216,7 @@ class TestDBTokenStorageExtended:
|
||||
loop.close()
|
||||
|
||||
def test_get_client_info_redirect_uri_mismatch(self):
|
||||
from application.agents.tools.mcp_tool import DBTokenStorage
|
||||
from docsgpt.agents.tools.mcp_tool import DBTokenStorage
|
||||
|
||||
mock_db = MagicMock()
|
||||
mock_collection = MagicMock()
|
||||
@@ -2244,7 +2244,7 @@ class TestDBTokenStorageExtended:
|
||||
loop.close()
|
||||
|
||||
def test_get_client_info_invalid_data(self):
|
||||
from application.agents.tools.mcp_tool import DBTokenStorage
|
||||
from docsgpt.agents.tools.mcp_tool import DBTokenStorage
|
||||
|
||||
mock_db = MagicMock()
|
||||
mock_collection = MagicMock()
|
||||
@@ -2267,7 +2267,7 @@ class TestDBTokenStorageExtended:
|
||||
loop.close()
|
||||
|
||||
def test_set_client_info(self):
|
||||
from application.agents.tools.mcp_tool import DBTokenStorage
|
||||
from docsgpt.agents.tools.mcp_tool import DBTokenStorage
|
||||
from mcp.shared.auth import OAuthClientInformationFull
|
||||
|
||||
mock_db = MagicMock()
|
||||
@@ -2293,7 +2293,7 @@ class TestDBTokenStorageExtended:
|
||||
loop.close()
|
||||
|
||||
def test_clear_all(self):
|
||||
from application.agents.tools.mcp_tool import DBTokenStorage
|
||||
from docsgpt.agents.tools.mcp_tool import DBTokenStorage
|
||||
|
||||
mock_db = MagicMock()
|
||||
mock_collection = MagicMock()
|
||||
@@ -2307,7 +2307,7 @@ class TestDBTokenStorageExtended:
|
||||
loop.close()
|
||||
|
||||
def test_serialize_client_info_without_redirect_uris(self):
|
||||
from application.agents.tools.mcp_tool import DBTokenStorage
|
||||
from docsgpt.agents.tools.mcp_tool import DBTokenStorage
|
||||
|
||||
mock_db = MagicMock()
|
||||
storage = DBTokenStorage(
|
||||
@@ -2329,7 +2329,7 @@ class TestDBTokenStorageExtended:
|
||||
class TestMCPOAuthManagerExtended:
|
||||
|
||||
def test_handle_callback_redis_setex_for_state(self):
|
||||
from application.agents.tools.mcp_tool import MCPOAuthManager
|
||||
from docsgpt.agents.tools.mcp_tool import MCPOAuthManager
|
||||
|
||||
mock_redis = MagicMock()
|
||||
manager = MCPOAuthManager(mock_redis)
|
||||
@@ -2340,7 +2340,7 @@ class TestMCPOAuthManagerExtended:
|
||||
assert mock_redis.setex.call_count == 2
|
||||
|
||||
def test_handle_callback_with_error_stores_error(self):
|
||||
from application.agents.tools.mcp_tool import MCPOAuthManager
|
||||
from docsgpt.agents.tools.mcp_tool import MCPOAuthManager
|
||||
|
||||
mock_redis = MagicMock()
|
||||
manager = MCPOAuthManager(mock_redis)
|
||||
@@ -2358,7 +2358,7 @@ class TestMCPOAuthManagerExtended:
|
||||
can present a clean "OAuth failed, try again" message rather
|
||||
than a 500.
|
||||
"""
|
||||
from application.agents.tools.mcp_tool import MCPOAuthManager
|
||||
from docsgpt.agents.tools.mcp_tool import MCPOAuthManager
|
||||
|
||||
mock_redis = MagicMock()
|
||||
mock_redis.xrevrange.side_effect = Exception("Redis went away")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Comprehensive tests for application/agents/tools/memory.py
|
||||
"""Comprehensive tests for docsgpt/agents/tools/memory.py
|
||||
|
||||
Covers: MemoryTool initialization, path validation, all actions
|
||||
(view, create, str_replace, insert, delete, rename), directory operations,
|
||||
@@ -87,19 +87,19 @@ def patched_memory(monkeypatch):
|
||||
"""Patch the memory tool to use the in-memory fake repo."""
|
||||
_FakeMemoriesRepo.reset()
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tools.memory.MemoriesRepository", _FakeMemoriesRepo
|
||||
"docsgpt.agents.tools.memory.MemoriesRepository", _FakeMemoriesRepo
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tools.memory.db_session", _noop_conn
|
||||
"docsgpt.agents.tools.memory.db_session", _noop_conn
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tools.memory.db_readonly", _noop_conn
|
||||
"docsgpt.agents.tools.memory.db_readonly", _noop_conn
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def memory_tool(patched_memory):
|
||||
from application.agents.tools.memory import MemoryTool
|
||||
from docsgpt.agents.tools.memory import MemoryTool
|
||||
# Real UUID so ``_pg_enabled()`` returns True.
|
||||
return MemoryTool(
|
||||
tool_config={"tool_id": str(uuid.uuid4())},
|
||||
@@ -116,7 +116,7 @@ def memory_tool(patched_memory):
|
||||
class TestMemoryToolInit:
|
||||
|
||||
def test_init_with_config(self, patched_memory):
|
||||
from application.agents.tools.memory import MemoryTool
|
||||
from docsgpt.agents.tools.memory import MemoryTool
|
||||
|
||||
tid = str(uuid.uuid4())
|
||||
tool = MemoryTool(tool_config={"tool_id": tid}, user_id="user1")
|
||||
@@ -124,13 +124,13 @@ class TestMemoryToolInit:
|
||||
assert tool.user_id == "user1"
|
||||
|
||||
def test_init_fallback_to_user_id(self, patched_memory):
|
||||
from application.agents.tools.memory import MemoryTool
|
||||
from docsgpt.agents.tools.memory import MemoryTool
|
||||
|
||||
tool = MemoryTool(tool_config={}, user_id="user1")
|
||||
assert tool.tool_id == "default_user1"
|
||||
|
||||
def test_init_no_user_no_config(self, patched_memory):
|
||||
from application.agents.tools.memory import MemoryTool
|
||||
from docsgpt.agents.tools.memory import MemoryTool
|
||||
|
||||
tool = MemoryTool()
|
||||
assert tool.tool_id is not None # UUID fallback
|
||||
@@ -181,7 +181,7 @@ class TestPathValidation:
|
||||
class TestNoUser:
|
||||
|
||||
def test_requires_user_id(self, patched_memory):
|
||||
from application.agents.tools.memory import MemoryTool
|
||||
from docsgpt.agents.tools.memory import MemoryTool
|
||||
|
||||
tool = MemoryTool(tool_config={"tool_id": str(uuid.uuid4())}, user_id=None)
|
||||
result = tool.execute_action("view", path="/")
|
||||
@@ -585,7 +585,7 @@ class TestSentinelShortCircuit:
|
||||
|
||||
def test_default_tool_id_short_circuits(self, patched_memory):
|
||||
"""A ``default_{user_id}`` sentinel tool_id must no-op."""
|
||||
from application.agents.tools.memory import MemoryTool
|
||||
from docsgpt.agents.tools.memory import MemoryTool
|
||||
|
||||
tool = MemoryTool(tool_config={}, user_id="user1")
|
||||
assert tool.tool_id == "default_user1"
|
||||
@@ -595,7 +595,7 @@ class TestSentinelShortCircuit:
|
||||
assert _FakeMemoriesRepo._store == {}
|
||||
|
||||
def test_non_uuid_tool_id_short_circuits(self, patched_memory):
|
||||
from application.agents.tools.memory import MemoryTool
|
||||
from docsgpt.agents.tools.memory import MemoryTool
|
||||
|
||||
tool = MemoryTool(tool_config={"tool_id": "not-a-uuid"}, user_id="user1")
|
||||
result = tool.execute_action("create", path="/x.txt", file_text="y")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests for application/agents/tools/notes.py using pg_conn."""
|
||||
"""Tests for docsgpt/agents/tools/notes.py using pg_conn."""
|
||||
|
||||
from contextlib import contextmanager
|
||||
from unittest.mock import patch
|
||||
@@ -12,15 +12,15 @@ def _patch_db(conn):
|
||||
yield conn
|
||||
|
||||
with patch(
|
||||
"application.storage.db.session.db_readonly", _yield
|
||||
"docsgpt.storage.db.session.db_readonly", _yield
|
||||
), patch(
|
||||
"application.storage.db.session.db_session", _yield
|
||||
"docsgpt.storage.db.session.db_session", _yield
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
def _make_tool(tool_id="default_test", user_id="u"):
|
||||
from application.agents.tools.notes import NotesTool
|
||||
from docsgpt.agents.tools.notes import NotesTool
|
||||
tool = NotesTool.__new__(NotesTool)
|
||||
tool.tool_id = tool_id
|
||||
tool.user_id = user_id
|
||||
@@ -60,7 +60,7 @@ class TestNotesToolExecuteGuards:
|
||||
|
||||
def test_unknown_action(self, pg_conn):
|
||||
# Real tool_id requires a user_tools row
|
||||
from application.storage.db.repositories.user_tools import (
|
||||
from docsgpt.storage.db.repositories.user_tools import (
|
||||
UserToolsRepository,
|
||||
)
|
||||
UserToolsRepository(pg_conn).create("u", "notes_tool")
|
||||
|
||||
@@ -17,8 +17,8 @@ from typing import Any, Dict, Optional
|
||||
|
||||
import pytest
|
||||
|
||||
import application.agents.tools.read_document as rd
|
||||
from application.agents.tools.read_document import ReadDocumentTool
|
||||
import docsgpt.agents.tools.read_document as rd
|
||||
from docsgpt.agents.tools.read_document import ReadDocumentTool
|
||||
|
||||
_ART_ID = str(uuid.uuid4())
|
||||
|
||||
@@ -86,7 +86,7 @@ class _FakeAsyncResult:
|
||||
|
||||
def _patch_task(monkeypatch, *, payload=None, exc=None):
|
||||
"""Patch parse_document.apply_async so no broker is touched; capture call args."""
|
||||
import application.api.user.tasks as tasks
|
||||
import docsgpt.api.user.tasks as tasks
|
||||
|
||||
captured: Dict[str, Any] = {}
|
||||
|
||||
@@ -225,7 +225,7 @@ def test_artifact_ref_sets_last_artifact_id(monkeypatch):
|
||||
@pytest.mark.unit
|
||||
def test_parse_window_scales_with_the_input_size(monkeypatch):
|
||||
"""A large input widens the await AND the task's per-call Celery time limits."""
|
||||
from application.api.user.tasks import parse_task_time_limits, parse_timeout_for_size
|
||||
from docsgpt.api.user.tasks import parse_task_time_limits, parse_timeout_for_size
|
||||
|
||||
size = 8 * 1024 * 1024
|
||||
_stub_repo(monkeypatch, found=True, conv="conv-1", run=None, size=size)
|
||||
@@ -263,7 +263,7 @@ def test_cross_tenant_rejected_before_enqueue(monkeypatch):
|
||||
_stub_repo(monkeypatch, found=True, conv="conv-OTHER", run=None)
|
||||
enqueued = {"called": False}
|
||||
|
||||
import application.api.user.tasks as tasks
|
||||
import docsgpt.api.user.tasks as tasks
|
||||
|
||||
def _apply_async(*a, **k):
|
||||
enqueued["called"] = True
|
||||
@@ -279,7 +279,7 @@ def test_cross_tenant_rejected_before_enqueue(monkeypatch):
|
||||
@pytest.mark.unit
|
||||
def test_missing_input_rejected_before_enqueue(monkeypatch):
|
||||
_stub_repo(monkeypatch, found=False, conv="conv-1", run=None)
|
||||
import application.api.user.tasks as tasks
|
||||
import docsgpt.api.user.tasks as tasks
|
||||
monkeypatch.setattr(
|
||||
tasks.parse_document, "apply_async",
|
||||
lambda *a, **k: (_ for _ in ()).throw(AssertionError("must not enqueue")),
|
||||
@@ -342,7 +342,7 @@ def test_json_schema_validation_fails_cleanly(monkeypatch):
|
||||
@pytest.mark.unit
|
||||
def test_malformed_json_schema_rejected_before_enqueue(monkeypatch):
|
||||
_stub_repo(monkeypatch, found=True, conv="conv-1", run=None)
|
||||
import application.api.user.tasks as tasks
|
||||
import docsgpt.api.user.tasks as tasks
|
||||
monkeypatch.setattr(
|
||||
tasks.parse_document, "apply_async",
|
||||
lambda *a, **k: (_ for _ in ()).throw(AssertionError("must not enqueue")),
|
||||
@@ -361,13 +361,13 @@ def test_dispatch_inline_when_in_worker(monkeypatch):
|
||||
# parsing queue self-deadlocks the worker that also serves it).
|
||||
monkeypatch.setattr(rd, "current_task", object())
|
||||
|
||||
import application.api.user.tasks as tasks
|
||||
import docsgpt.api.user.tasks as tasks
|
||||
monkeypatch.setattr(
|
||||
tasks.parse_document, "apply_async",
|
||||
lambda *a, **k: (_ for _ in ()).throw(AssertionError("must not enqueue inside a worker")),
|
||||
)
|
||||
|
||||
import application.worker as worker
|
||||
import docsgpt.worker as worker
|
||||
called: Dict[str, Any] = {}
|
||||
|
||||
def _fake_run(artifact_id, parent, user_id, options):
|
||||
@@ -391,7 +391,7 @@ def test_dispatch_enqueues_when_not_in_worker(monkeypatch):
|
||||
monkeypatch.setattr(rd, "current_task", None)
|
||||
captured = _patch_task(monkeypatch, payload={"status": "ok", "content": "queued", "truncated": False})
|
||||
|
||||
import application.worker as worker
|
||||
import docsgpt.worker as worker
|
||||
monkeypatch.setattr(
|
||||
worker, "run_parse_document",
|
||||
lambda *a, **k: (_ for _ in ()).throw(AssertionError("web path must dispatch, not inline")),
|
||||
@@ -418,14 +418,14 @@ def _inline(monkeypatch, run_parse, *, timeout=0.2) -> ReadDocumentTool:
|
||||
_stub_repo(monkeypatch, found=True, conv="conv-1", run=None)
|
||||
monkeypatch.setattr(rd, "current_task", object())
|
||||
|
||||
import application.api.user.tasks as tasks
|
||||
import docsgpt.api.user.tasks as tasks
|
||||
monkeypatch.setattr(
|
||||
tasks.parse_document, "apply_async",
|
||||
lambda *a, **k: (_ for _ in ()).throw(AssertionError("must not enqueue inside a worker")),
|
||||
)
|
||||
monkeypatch.setattr(tasks, "parse_timeout_for_size", lambda size: timeout)
|
||||
|
||||
import application.worker as worker
|
||||
import docsgpt.worker as worker
|
||||
monkeypatch.setattr(worker, "run_parse_document", run_parse)
|
||||
return _tool()
|
||||
|
||||
@@ -466,7 +466,7 @@ def test_inline_parse_times_out_on_the_signal_path(monkeypatch, caplog):
|
||||
before = signal.getsignal(signal.SIGALRM)
|
||||
tool = _inline(monkeypatch, _run, timeout=0.2)
|
||||
started = time.monotonic()
|
||||
with caplog.at_level(logging.WARNING, logger="application.agents.tools.read_document"):
|
||||
with caplog.at_level(logging.WARNING, logger="docsgpt.agents.tools.read_document"):
|
||||
out = tool.execute_action("read_document", input=_ART_ID, persist=False)
|
||||
elapsed = time.monotonic() - started
|
||||
|
||||
@@ -520,7 +520,7 @@ def test_the_inline_helper_thread_is_a_daemon():
|
||||
"""A timed-out parse is abandoned, so its thread must not outlive the process.
|
||||
|
||||
``concurrent.futures`` registers its (non-daemon) workers with an atexit
|
||||
hook that joins them, which is exactly why application/guardrails/engine.py
|
||||
hook that joins them, which is exactly why docsgpt/guardrails/engine.py
|
||||
uses raw daemon threads for the same abandon-on-timeout shape.
|
||||
"""
|
||||
seen: Dict[str, Any] = {}
|
||||
@@ -572,7 +572,7 @@ def test_inline_parse_times_out_on_the_thread_path(monkeypatch, caplog):
|
||||
def _call():
|
||||
box["out"] = tool.execute_action("read_document", input=_ART_ID, persist=False)
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="application.agents.tools.read_document"):
|
||||
with caplog.at_level(logging.WARNING, logger="docsgpt.agents.tools.read_document"):
|
||||
caller = threading.Thread(target=_call, name="fake-worker-pool-thread")
|
||||
caller.start()
|
||||
caller.join(2.0)
|
||||
@@ -681,7 +681,7 @@ def test_inline_timeout_is_not_swallowed_by_the_parser_catch_all(monkeypatch, ca
|
||||
seen["cleaned_up"] = True
|
||||
|
||||
tool = _inline(monkeypatch, _run, timeout=0.2)
|
||||
with caplog.at_level(logging.WARNING, logger="application.agents.tools.read_document"):
|
||||
with caplog.at_level(logging.WARNING, logger="docsgpt.agents.tools.read_document"):
|
||||
out = tool.execute_action("read_document", input=_ART_ID, persist=False)
|
||||
|
||||
assert out == {"status": "error", "error": f"{_TIMED_OUT} {int(0.2)}s."}
|
||||
|
||||
@@ -7,11 +7,11 @@ import requests
|
||||
|
||||
class TestReadWebpageErrors:
|
||||
def test_request_exception_returns_error_string(self):
|
||||
from application.agents.tools.read_webpage import ReadWebpageTool
|
||||
from docsgpt.agents.tools.read_webpage import ReadWebpageTool
|
||||
|
||||
tool = ReadWebpageTool(config={})
|
||||
with patch(
|
||||
"application.agents.tools.read_webpage.pinned_fetch_bytes",
|
||||
"docsgpt.agents.tools.read_webpage.pinned_fetch_bytes",
|
||||
side_effect=requests.exceptions.RequestException("bad url"),
|
||||
):
|
||||
got = tool.execute_action(
|
||||
@@ -22,17 +22,17 @@ class TestReadWebpageErrors:
|
||||
def test_generic_exception_returns_error_string(self):
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from application.agents.tools.read_webpage import ReadWebpageTool
|
||||
from docsgpt.agents.tools.read_webpage import ReadWebpageTool
|
||||
|
||||
tool = ReadWebpageTool(config={})
|
||||
response = MagicMock()
|
||||
response.headers = {"Content-Type": "text/html"}
|
||||
response.raise_for_status.return_value = None
|
||||
with patch(
|
||||
"application.agents.tools.read_webpage.markdownify",
|
||||
"docsgpt.agents.tools.read_webpage.markdownify",
|
||||
side_effect=RuntimeError("boom"),
|
||||
), patch(
|
||||
"application.agents.tools.read_webpage.pinned_fetch_bytes",
|
||||
"docsgpt.agents.tools.read_webpage.pinned_fetch_bytes",
|
||||
return_value=(b"<h1>hi</h1>", response),
|
||||
):
|
||||
got = tool.execute_action(
|
||||
@@ -45,7 +45,7 @@ class TestBaseAgentMinorBranches:
|
||||
"""Cover 2 missing lines in agents/base.py (116, 160)."""
|
||||
|
||||
def test_base_agent_with_llm_provided(self):
|
||||
from application.agents.classic_agent import ClassicAgent
|
||||
from docsgpt.agents.classic_agent import ClassicAgent
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
mock_llm = MagicMock()
|
||||
@@ -63,7 +63,7 @@ class TestWorkflowNodesMinor:
|
||||
"""Cover line 44 in workflow_nodes.py (likely default params branch)."""
|
||||
|
||||
def test_bulk_create_empty_list_returns_empty(self, pg_conn):
|
||||
from application.storage.db.repositories.workflow_nodes import (
|
||||
from docsgpt.storage.db.repositories.workflow_nodes import (
|
||||
WorkflowNodesRepository,
|
||||
)
|
||||
got = WorkflowNodesRepository(pg_conn).bulk_create(
|
||||
|
||||
@@ -11,13 +11,13 @@ import pytest
|
||||
from sqlalchemy import text
|
||||
|
||||
# Pre-import to stabilise the ToolManager.load_tools walk's import order
|
||||
# (avoids the mcp_tool ↔ application.api.user circular when ToolManager
|
||||
# (avoids the mcp_tool ↔ docsgpt.api.user circular when ToolManager
|
||||
# instantiation is the first reachable importer in a test process).
|
||||
import application.api.user.tools.mcp # noqa: F401
|
||||
import docsgpt.api.user.tools.mcp # noqa: F401
|
||||
|
||||
from application.agents.tools.scheduler import SchedulerTool # noqa: E402
|
||||
from application.core.settings import settings # noqa: E402
|
||||
from application.storage.db.repositories.schedules import SchedulesRepository # noqa: E402
|
||||
from docsgpt.agents.tools.scheduler import SchedulerTool # noqa: E402
|
||||
from docsgpt.core.settings import settings # noqa: E402
|
||||
from docsgpt.storage.db.repositories.schedules import SchedulesRepository # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -29,9 +29,9 @@ def patch_sessions(pg_conn):
|
||||
yield pg_conn
|
||||
|
||||
with patch(
|
||||
"application.agents.tools.scheduler.db_session", _ctx,
|
||||
"docsgpt.agents.tools.scheduler.db_session", _ctx,
|
||||
), patch(
|
||||
"application.agents.tools.scheduler.db_readonly", _ctx,
|
||||
"docsgpt.agents.tools.scheduler.db_readonly", _ctx,
|
||||
):
|
||||
yield
|
||||
|
||||
@@ -309,8 +309,8 @@ class TestAgentlessInvocation:
|
||||
self, pg_conn, patch_sessions,
|
||||
):
|
||||
"""Agentless schedule captures the user's non-approval tools at fire-time."""
|
||||
from application.agents.tools.scheduler import _safe_default_allowlist
|
||||
from application.storage.db.repositories.user_tools import (
|
||||
from docsgpt.agents.tools.scheduler import _safe_default_allowlist
|
||||
from docsgpt.storage.db.repositories.user_tools import (
|
||||
UserToolsRepository,
|
||||
)
|
||||
|
||||
@@ -352,8 +352,8 @@ class TestAllowlistSnapshotSemantics:
|
||||
):
|
||||
"""Schedule captures the allowlist at creation; a tool added later is
|
||||
visible at fire time (resolver re-queries) but isn't in the snapshot."""
|
||||
from application.agents.tools.scheduler import _safe_default_allowlist
|
||||
from application.storage.db.repositories.user_tools import (
|
||||
from docsgpt.agents.tools.scheduler import _safe_default_allowlist
|
||||
from docsgpt.storage.db.repositories.user_tools import (
|
||||
UserToolsRepository,
|
||||
)
|
||||
|
||||
@@ -394,8 +394,8 @@ class TestAllowlistSnapshotSemantics:
|
||||
):
|
||||
"""A tool deleted between schedule creation and fire is gone for the
|
||||
LLM at fire time (the resolver lists the current state)."""
|
||||
from application.agents.tools.scheduler import _safe_default_allowlist
|
||||
from application.storage.db.repositories.user_tools import (
|
||||
from docsgpt.agents.tools.scheduler import _safe_default_allowlist
|
||||
from docsgpt.storage.db.repositories.user_tools import (
|
||||
UserToolsRepository,
|
||||
)
|
||||
|
||||
@@ -424,13 +424,13 @@ class TestInternalFlag:
|
||||
assert SchedulerTool.internal is True
|
||||
|
||||
def test_not_in_tool_manager_auto_load(self):
|
||||
from application.agents.tools.tool_manager import ToolManager
|
||||
from docsgpt.agents.tools.tool_manager import ToolManager
|
||||
|
||||
tm = ToolManager(config={})
|
||||
assert "scheduler" not in tm.tools
|
||||
|
||||
def test_load_tool_special_case_still_works(self):
|
||||
from application.agents.tools.tool_manager import ToolManager
|
||||
from docsgpt.agents.tools.tool_manager import ToolManager
|
||||
|
||||
tm = ToolManager(config={})
|
||||
tool = tm.load_tool(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests for application/agents/tools/wiki.py.
|
||||
"""Tests for docsgpt/agents/tools/wiki.py.
|
||||
|
||||
A fake repository mirrors the WikiPagesRepository methods the tool calls;
|
||||
``db_session`` / ``db_readonly`` are stubbed with a no-op context manager and
|
||||
@@ -15,7 +15,7 @@ from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from application.storage.db.repositories.wiki_pages import WikiPageConflict
|
||||
from docsgpt.storage.db.repositories.wiki_pages import WikiPageConflict
|
||||
|
||||
|
||||
def _hash(content: str) -> str:
|
||||
@@ -142,21 +142,21 @@ def rebuild_mock():
|
||||
def patched_wiki(monkeypatch, reembed_mock, rebuild_mock):
|
||||
_FakeWikiRepo.reset()
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tools.wiki.WikiPagesRepository", _FakeWikiRepo
|
||||
"docsgpt.agents.tools.wiki.WikiPagesRepository", _FakeWikiRepo
|
||||
)
|
||||
monkeypatch.setattr("application.agents.tools.wiki.db_session", _noop_conn)
|
||||
monkeypatch.setattr("application.agents.tools.wiki.db_readonly", _noop_conn)
|
||||
monkeypatch.setattr("docsgpt.agents.tools.wiki.db_session", _noop_conn)
|
||||
monkeypatch.setattr("docsgpt.agents.tools.wiki.db_readonly", _noop_conn)
|
||||
monkeypatch.setattr(
|
||||
"application.agents.tools.wiki.rebuild_wiki_directory_structure", rebuild_mock
|
||||
"docsgpt.agents.tools.wiki.rebuild_wiki_directory_structure", rebuild_mock
|
||||
)
|
||||
task = MagicMock()
|
||||
task.delay = reembed_mock
|
||||
monkeypatch.setattr("application.api.user.tasks.reembed_wiki_page", task)
|
||||
monkeypatch.setattr("docsgpt.api.user.tasks.reembed_wiki_page", task)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def wiki_tool(patched_wiki):
|
||||
from application.agents.tools.wiki import WikiTool
|
||||
from docsgpt.agents.tools.wiki import WikiTool
|
||||
|
||||
return WikiTool(
|
||||
{
|
||||
@@ -176,7 +176,7 @@ def wiki_tool(patched_wiki):
|
||||
@pytest.mark.unit
|
||||
class TestBasics:
|
||||
def test_requires_source_id(self, patched_wiki):
|
||||
from application.agents.tools.wiki import WikiTool
|
||||
from docsgpt.agents.tools.wiki import WikiTool
|
||||
|
||||
tool = WikiTool({})
|
||||
assert "source_id" in tool.execute_action("view", path="/")
|
||||
@@ -246,7 +246,7 @@ class TestCreateView:
|
||||
).lower()
|
||||
|
||||
def test_create_oversize_rejected(self, wiki_tool, reembed_mock):
|
||||
from application.agents.tools.wiki import MAX_WIKI_PAGE_BYTES
|
||||
from docsgpt.agents.tools.wiki import MAX_WIKI_PAGE_BYTES
|
||||
|
||||
oversized = "a" * (MAX_WIKI_PAGE_BYTES + 1)
|
||||
result = wiki_tool.execute_action(
|
||||
@@ -441,7 +441,7 @@ class TestOptimisticConcurrency:
|
||||
@pytest.mark.unit
|
||||
class TestInjection:
|
||||
def test_add_wiki_tool_entry_has_id(self):
|
||||
from application.agents.tools.wiki import WIKI_TOOL_ID, add_wiki_tool
|
||||
from docsgpt.agents.tools.wiki import WIKI_TOOL_ID, add_wiki_tool
|
||||
|
||||
tools_dict = {}
|
||||
add_wiki_tool(
|
||||
@@ -459,7 +459,7 @@ class TestInjection:
|
||||
assert tools_dict[WIKI_TOOL_ID]["config"]["source_owner_id"] == "owner"
|
||||
|
||||
def test_add_wiki_tool_skips_without_owner(self):
|
||||
from application.agents.tools.wiki import WIKI_TOOL_ID, add_wiki_tool
|
||||
from docsgpt.agents.tools.wiki import WIKI_TOOL_ID, add_wiki_tool
|
||||
|
||||
tools_dict = {}
|
||||
add_wiki_tool(
|
||||
@@ -472,7 +472,7 @@ class TestInjection:
|
||||
@pytest.mark.unit
|
||||
class TestBuildAgentGating:
|
||||
def _processor(self, all_sources, caller="caller"):
|
||||
from application.api.answer.services.stream_processor import StreamProcessor
|
||||
from docsgpt.api.answer.services.stream_processor import StreamProcessor
|
||||
|
||||
proc = StreamProcessor.__new__(StreamProcessor)
|
||||
proc.all_sources = all_sources
|
||||
@@ -490,15 +490,15 @@ class TestBuildAgentGating:
|
||||
return {"id": sid, "config": {"kind": "wiki"}}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.api.answer.services.stream_processor.SourcesRepository",
|
||||
"docsgpt.api.answer.services.stream_processor.SourcesRepository",
|
||||
_SrcRepo,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.api.answer.services.stream_processor.db_readonly",
|
||||
"docsgpt.api.answer.services.stream_processor.db_readonly",
|
||||
_noop_conn,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.api.user.team_sharing.effective_write_owner",
|
||||
"docsgpt.api.user.team_sharing.effective_write_owner",
|
||||
lambda conn, rt, rid, uid: "owner-x",
|
||||
)
|
||||
cfg = proc._build_wiki_config()
|
||||
@@ -517,16 +517,16 @@ class TestBuildAgentGating:
|
||||
return {"id": sid, "config": {"kind": "wiki"}}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.api.answer.services.stream_processor.SourcesRepository",
|
||||
"docsgpt.api.answer.services.stream_processor.SourcesRepository",
|
||||
_SrcRepo,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.api.answer.services.stream_processor.db_readonly",
|
||||
"docsgpt.api.answer.services.stream_processor.db_readonly",
|
||||
_noop_conn,
|
||||
)
|
||||
# Viewer: effective_write_owner returns None.
|
||||
monkeypatch.setattr(
|
||||
"application.api.user.team_sharing.effective_write_owner",
|
||||
"docsgpt.api.user.team_sharing.effective_write_owner",
|
||||
lambda conn, rt, rid, uid: None,
|
||||
)
|
||||
assert proc._build_wiki_config() is None
|
||||
@@ -542,15 +542,15 @@ class TestBuildAgentGating:
|
||||
return {"id": sid, "config": {"kind": "classic"}}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.api.answer.services.stream_processor.SourcesRepository",
|
||||
"docsgpt.api.answer.services.stream_processor.SourcesRepository",
|
||||
_SrcRepo,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.api.answer.services.stream_processor.db_readonly",
|
||||
"docsgpt.api.answer.services.stream_processor.db_readonly",
|
||||
_noop_conn,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.api.user.team_sharing.effective_write_owner",
|
||||
"docsgpt.api.user.team_sharing.effective_write_owner",
|
||||
lambda conn, rt, rid, uid: "owner-x",
|
||||
)
|
||||
assert proc._build_wiki_config() is None
|
||||
@@ -566,15 +566,15 @@ class TestBuildAgentGating:
|
||||
return {"id": sid, "config": {"kind": "wiki"}}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.api.answer.services.stream_processor.SourcesRepository",
|
||||
"docsgpt.api.answer.services.stream_processor.SourcesRepository",
|
||||
_SrcRepo,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.api.answer.services.stream_processor.db_readonly",
|
||||
"docsgpt.api.answer.services.stream_processor.db_readonly",
|
||||
_noop_conn,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.api.user.team_sharing.effective_write_owner",
|
||||
"docsgpt.api.user.team_sharing.effective_write_owner",
|
||||
lambda conn, rt, rid, uid: "owner-x",
|
||||
)
|
||||
cfg = proc._build_wiki_config()
|
||||
|
||||
Reference in new issue
Block a user