mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-20 02:16:39 +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,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