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:
Alex committed 2026-09-07 10:20:43 +01:00
1 parent 3e7f1f91b4
commit 574f96341e
984 files changed
+9847 -9682

No files matched your search

+11 -11
View File
@@ -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,
)
+28 -28
View File
@@ -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"]})
+1 -1
View File
@@ -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:
+19 -19
View File
@@ -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 -6
View File
@@ -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)
+3 -3
View File
@@ -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)
+14 -14
View File
@@ -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
View File
@@ -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")
+11 -11
View File
@@ -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")
+5 -5
View File
@@ -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")
+16 -16
View File
@@ -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(
+15 -15
View File
@@ -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(
+25 -25
View File
@@ -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()