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

+2 -2
View File
@@ -1,6 +1,6 @@
import pytest
from application.agents.agent_creator import AgentCreator
from application.agents.classic_agent import ClassicAgent
from docsgpt.agents.agent_creator import AgentCreator
from docsgpt.agents.classic_agent import ClassicAgent
@pytest.mark.unit
+2 -2
View File
@@ -3,13 +3,13 @@
from unittest.mock import Mock
import pytest
from application.agents.agentic_agent import AgenticAgent
from docsgpt.agents.agentic_agent import AgenticAgent
@pytest.fixture
def _no_tools(monkeypatch):
monkeypatch.setattr(
"application.agents.tool_executor.ToolExecutor.get_tools",
"docsgpt.agents.tool_executor.ToolExecutor.get_tools",
lambda self: {},
)
+2 -2
View File
@@ -1,10 +1,10 @@
"""Tests for application/agents/tools/api_body_serializer.py"""
"""Tests for docsgpt/agents/tools/api_body_serializer.py"""
import json
import pytest
from application.agents.tools.api_body_serializer import (
from docsgpt.agents.tools.api_body_serializer import (
ContentType,
RequestBodySerializer,
)
+15 -15
View File
@@ -1,4 +1,4 @@
"""Tests for application/agents/tools/api_tool.py"""
"""Tests for docsgpt/agents/tools/api_tool.py"""
import json
from unittest.mock import MagicMock, patch
@@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch
import pytest
import requests
from application.agents.tools.api_tool import APITool
from docsgpt.agents.tools.api_tool import APITool
@pytest.fixture
@@ -45,7 +45,7 @@ class TestAPIToolInit:
@pytest.mark.unit
class TestMakeApiCall:
@patch("application.agents.tools.api_tool.pinned_request")
@patch("docsgpt.agents.tools.api_tool.pinned_request")
def test_successful_get(self, mock_pinned, tool):
mock_resp = MagicMock()
mock_resp.status_code = 200
@@ -60,7 +60,7 @@ class TestMakeApiCall:
assert result["data"] == {"result": "ok"}
assert result["message"] == "API call successful."
@patch("application.agents.tools.api_tool.pinned_request")
@patch("docsgpt.agents.tools.api_tool.pinned_request")
def test_successful_post(self, mock_pinned, post_tool):
mock_resp = MagicMock()
mock_resp.status_code = 201
@@ -73,9 +73,9 @@ class TestMakeApiCall:
assert result["status_code"] == 201
@patch("application.agents.tools.api_tool.pinned_request")
@patch("docsgpt.agents.tools.api_tool.pinned_request")
def test_ssrf_blocked(self, mock_pinned, tool):
from application.security.safe_url import UnsafeUserUrlError
from docsgpt.security.safe_url import UnsafeUserUrlError
mock_pinned.side_effect = UnsafeUserUrlError("blocked")
@@ -84,7 +84,7 @@ class TestMakeApiCall:
assert result["status_code"] is None
assert "URL validation error" in result["message"]
@patch("application.agents.tools.api_tool.pinned_request")
@patch("docsgpt.agents.tools.api_tool.pinned_request")
def test_timeout_error(self, mock_pinned, tool):
mock_pinned.side_effect = requests.exceptions.Timeout()
@@ -93,7 +93,7 @@ class TestMakeApiCall:
assert result["status_code"] is None
assert "timeout" in result["message"].lower()
@patch("application.agents.tools.api_tool.pinned_request")
@patch("docsgpt.agents.tools.api_tool.pinned_request")
def test_connection_error(self, mock_pinned, tool):
mock_pinned.side_effect = requests.exceptions.ConnectionError("refused")
@@ -102,7 +102,7 @@ class TestMakeApiCall:
assert result["status_code"] is None
assert "Connection error" in result["message"]
@patch("application.agents.tools.api_tool.pinned_request")
@patch("docsgpt.agents.tools.api_tool.pinned_request")
def test_http_error(self, mock_pinned, tool):
mock_resp = MagicMock()
mock_resp.status_code = 404
@@ -126,7 +126,7 @@ class TestMakeApiCall:
assert result["status_code"] is None
assert "Unsupported" in result["message"]
@patch("application.agents.tools.api_tool.pinned_request")
@patch("docsgpt.agents.tools.api_tool.pinned_request")
def test_put_method(self, mock_pinned):
tool = APITool(config={"url": "https://example.com/item/1", "method": "PUT"})
mock_resp = MagicMock()
@@ -139,7 +139,7 @@ class TestMakeApiCall:
result = tool.execute_action("update", name="new")
assert result["status_code"] == 200
@patch("application.agents.tools.api_tool.pinned_request")
@patch("docsgpt.agents.tools.api_tool.pinned_request")
def test_delete_method(self, mock_pinned):
tool = APITool(config={"url": "https://example.com/item/1", "method": "DELETE"})
mock_resp = MagicMock()
@@ -151,7 +151,7 @@ class TestMakeApiCall:
result = tool.execute_action("delete")
assert result["status_code"] == 204
@patch("application.agents.tools.api_tool.pinned_request")
@patch("docsgpt.agents.tools.api_tool.pinned_request")
def test_patch_method(self, mock_pinned):
tool = APITool(config={"url": "https://example.com/item/1", "method": "PATCH"})
mock_resp = MagicMock()
@@ -164,7 +164,7 @@ class TestMakeApiCall:
result = tool.execute_action("patch", field="val")
assert result["status_code"] == 200
@patch("application.agents.tools.api_tool.pinned_request")
@patch("docsgpt.agents.tools.api_tool.pinned_request")
def test_head_method(self, mock_pinned):
tool = APITool(config={"url": "https://example.com", "method": "HEAD"})
mock_resp = MagicMock()
@@ -176,7 +176,7 @@ class TestMakeApiCall:
result = tool.execute_action("check")
assert result["status_code"] == 200
@patch("application.agents.tools.api_tool.pinned_request")
@patch("docsgpt.agents.tools.api_tool.pinned_request")
def test_options_method(self, mock_pinned):
tool = APITool(config={"url": "https://example.com", "method": "OPTIONS"})
mock_resp = MagicMock()
@@ -191,7 +191,7 @@ class TestMakeApiCall:
@pytest.mark.unit
class TestPathParamSubstitution:
@patch("application.agents.tools.api_tool.pinned_request")
@patch("docsgpt.agents.tools.api_tool.pinned_request")
def test_path_params_substituted(self, mock_pinned):
tool = APITool(
config={
+40 -40
View File
@@ -2,14 +2,14 @@ from contextlib import contextmanager
from unittest.mock import Mock, patch
import pytest
from application.agents.classic_agent import ClassicAgent
from application.llm.anthropic import AnthropicLLM
from application.llm.docsgpt_provider import DocsGPTAPILLM
from application.llm.google_ai import GoogleLLM
from application.llm.groq import GroqLLM
from application.llm.novita import NovitaLLM
from application.llm.open_router import OpenRouterLLM
from application.llm.openai import OpenAILLM
from docsgpt.agents.classic_agent import ClassicAgent
from docsgpt.llm.anthropic import AnthropicLLM
from docsgpt.llm.docsgpt_provider import DocsGPTAPILLM
from docsgpt.llm.google_ai import GoogleLLM
from docsgpt.llm.groq import GroqLLM
from docsgpt.llm.novita import NovitaLLM
from docsgpt.llm.open_router import OpenRouterLLM
from docsgpt.llm.openai import OpenAILLM
@pytest.mark.unit
@@ -229,7 +229,7 @@ class TestBaseAgentTools:
mock_llm_creator,
mock_llm_handler_creator,
):
from application.storage.db.repositories.user_tools import UserToolsRepository
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
repo = UserToolsRepository(pg_conn)
repo.create(user_id="test_user", name="tool1", status=True)
@@ -240,13 +240,13 @@ class TestBaseAgentTools:
yield pg_conn
monkeypatch.setattr(
"application.agents.tool_executor.db_readonly", _use_pg_conn
"docsgpt.agents.tool_executor.db_readonly", _use_pg_conn
)
agent = ClassicAgent(**agent_base_params)
tools = agent._get_user_tools("test_user")
from application.agents.default_tools import loaded_default_tools
from docsgpt.agents.default_tools import loaded_default_tools
assert len(tools) == 2 + len(loaded_default_tools())
assert "0" in tools
@@ -263,7 +263,7 @@ class TestBaseAgentTools:
mock_llm_creator,
mock_llm_handler_creator,
):
from application.storage.db.repositories.user_tools import UserToolsRepository
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
repo = UserToolsRepository(pg_conn)
repo.create(user_id="test_user", name="tool1", status=True)
@@ -274,13 +274,13 @@ class TestBaseAgentTools:
yield pg_conn
monkeypatch.setattr(
"application.agents.tool_executor.db_readonly", _use_pg_conn
"docsgpt.agents.tool_executor.db_readonly", _use_pg_conn
)
agent = ClassicAgent(**agent_base_params)
tools = agent._get_user_tools("test_user")
from application.agents.default_tools import loaded_default_tools
from docsgpt.agents.default_tools import loaded_default_tools
assert len(tools) == 1 + len(loaded_default_tools())
names = {t["name"] for t in tools.values()}
@@ -295,8 +295,8 @@ class TestBaseAgentTools:
mock_llm_creator,
mock_llm_handler_creator,
):
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.user_tools import UserToolsRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
tool_row = UserToolsRepository(pg_conn).create(
user_id="alice", name="api_tool"
@@ -316,13 +316,13 @@ class TestBaseAgentTools:
yield pg_conn
monkeypatch.setattr(
"application.agents.tool_executor.db_readonly", _use_pg_conn
"docsgpt.agents.tool_executor.db_readonly", _use_pg_conn
)
agent = ClassicAgent(**agent_base_params)
tools = agent._get_tools("api_key_123")
from application.agents.default_tools import loaded_default_tools
from docsgpt.agents.default_tools import loaded_default_tools
# Agent-bound: exactly agents.tools, no defaults.
assert set(tools) == {tool_id}
@@ -573,7 +573,7 @@ class TestBaseAgentToolExecution:
):
agent = ClassicAgent(**agent_base_params)
from application.agents.tool_executor import PERSISTED_RESULT_MAX_LEN
from docsgpt.agents.tool_executor import PERSISTED_RESULT_MAX_LEN
agent.tool_calls = [
{
@@ -833,7 +833,7 @@ class TestCalculateContextTokens:
messages = [{"role": "user", "content": "hello"}]
with patch(
"application.api.answer.services.compression.token_counter.TokenCounter"
"docsgpt.api.answer.services.compression.token_counter.TokenCounter"
) as MockTC:
MockTC.count_message_tokens.return_value = 42
result = agent._calculate_current_context_tokens(messages)
@@ -862,7 +862,7 @@ class TestCheckContextLimit:
with patch.object(agent, "_calculate_current_context_tokens", return_value=100):
with patch(
"application.core.model_utils.get_token_limit", return_value=10000
"docsgpt.core.model_utils.get_token_limit", return_value=10000
):
result = agent._check_context_limit(messages)
assert result is False
@@ -879,7 +879,7 @@ class TestCheckContextLimit:
# threshold = 10000 * 0.8 = 8000; tokens = 8001 → True
with patch.object(agent, "_calculate_current_context_tokens", return_value=8001):
with patch(
"application.core.model_utils.get_token_limit", return_value=10000
"docsgpt.core.model_utils.get_token_limit", return_value=10000
):
result = agent._check_context_limit(messages)
assert result is True
@@ -913,7 +913,7 @@ class TestValidateContextSize:
agent = ClassicAgent(**agent_base_params)
with patch.object(agent, "_calculate_current_context_tokens", return_value=10000):
with patch(
"application.core.model_utils.get_token_limit", return_value=10000
"docsgpt.core.model_utils.get_token_limit", return_value=10000
):
# Should not raise
agent._validate_context_size([{"role": "user", "content": "x"}])
@@ -925,7 +925,7 @@ class TestValidateContextSize:
agent = ClassicAgent(**agent_base_params)
with patch.object(agent, "_calculate_current_context_tokens", return_value=100):
with patch(
"application.core.model_utils.get_token_limit", return_value=10000
"docsgpt.core.model_utils.get_token_limit", return_value=10000
):
agent._validate_context_size([])
assert agent.current_token_count == 100
@@ -937,7 +937,7 @@ class TestValidateContextSize:
# 8500 / 10000 = 85% → above 80% threshold but below 100%
with patch.object(agent, "_calculate_current_context_tokens", return_value=8500):
with patch(
"application.core.model_utils.get_token_limit", return_value=10000
"docsgpt.core.model_utils.get_token_limit", return_value=10000
):
agent._validate_context_size([])
assert agent.current_token_count == 8500
@@ -955,7 +955,7 @@ class TestTruncateTextMiddle:
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
):
agent = ClassicAgent(**agent_base_params)
with patch("application.utils.num_tokens_from_string", return_value=5):
with patch("docsgpt.utils.num_tokens_from_string", return_value=5):
result = agent._truncate_text_middle("short", max_tokens=100)
assert result == "short"
@@ -968,7 +968,7 @@ class TestTruncateTextMiddle:
def fake_tokens(text):
return len(text) // 4
with patch("application.utils.num_tokens_from_string", side_effect=fake_tokens):
with patch("docsgpt.utils.num_tokens_from_string", side_effect=fake_tokens):
result = agent._truncate_text_middle(long_text, max_tokens=50)
assert "[... content truncated to fit context limit ...]" in result
assert len(result) < len(long_text)
@@ -977,7 +977,7 @@ class TestTruncateTextMiddle:
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
):
agent = ClassicAgent(**agent_base_params)
with patch("application.utils.num_tokens_from_string", return_value=100):
with patch("docsgpt.utils.num_tokens_from_string", return_value=100):
result = agent._truncate_text_middle("some text", max_tokens=0)
assert result == ""
@@ -1020,7 +1020,7 @@ class TestTruncateHistoryToFit:
{"prompt": "q1", "response": "a1"},
{"prompt": "q2", "response": "a2"},
]
with patch("application.utils.num_tokens_from_string", return_value=5):
with patch("docsgpt.utils.num_tokens_from_string", return_value=5):
result = agent._truncate_history_to_fit(history, 10000)
assert len(result) == 2
@@ -1035,7 +1035,7 @@ class TestTruncateHistoryToFit:
{"prompt": "new", "response": "new_ans"},
]
# Each message = 10 tokens (prompt + response), budget = 15 → only 1 fits
with patch("application.utils.num_tokens_from_string", return_value=5):
with patch("docsgpt.utils.num_tokens_from_string", return_value=5):
result = agent._truncate_history_to_fit(history, 15)
assert len(result) == 1
assert result[0]["prompt"] == "new"
@@ -1060,7 +1060,7 @@ class TestTruncateHistoryToFit:
],
}
]
with patch("application.utils.num_tokens_from_string", return_value=3):
with patch("docsgpt.utils.num_tokens_from_string", return_value=3):
result = agent._truncate_history_to_fit(history, 100)
assert len(result) == 1
@@ -1080,8 +1080,8 @@ class TestBuildMessagesAdvanced:
agent = ClassicAgent(**agent_base_params)
with patch(
"application.core.model_utils.get_token_limit", return_value=100000
), patch("application.utils.num_tokens_from_string", return_value=10):
"docsgpt.core.model_utils.get_token_limit", return_value=100000
), patch("docsgpt.utils.num_tokens_from_string", return_value=10):
messages = agent._build_messages("System prompt", "query")
system_content = messages[0]["content"]
@@ -1099,8 +1099,8 @@ class TestBuildMessagesAdvanced:
return len(text)
with patch(
"application.core.model_utils.get_token_limit", return_value=200
), patch("application.utils.num_tokens_from_string", side_effect=fake_tokens):
"docsgpt.core.model_utils.get_token_limit", return_value=200
), patch("docsgpt.utils.num_tokens_from_string", side_effect=fake_tokens):
with patch.object(agent, "_truncate_text_middle", return_value="truncated"):
with patch.object(agent, "_truncate_history_to_fit", return_value=[]):
messages = agent._build_messages("sys", "A" * 500)
@@ -1127,8 +1127,8 @@ class TestBuildMessagesAdvanced:
agent = ClassicAgent(**agent_base_params)
with patch(
"application.core.model_utils.get_token_limit", return_value=100000
), patch("application.utils.num_tokens_from_string", return_value=5):
"docsgpt.core.model_utils.get_token_limit", return_value=100000
), patch("docsgpt.utils.num_tokens_from_string", return_value=5):
messages = agent._build_messages("sys", "q")
tool_msgs = [m for m in messages if m["role"] == "tool"]
@@ -1391,7 +1391,7 @@ class TestBaseAgentContextBudget:
def _agent_with_limit(params, monkeypatch, limit):
agent = ClassicAgent(**params)
monkeypatch.setattr(
"application.core.model_utils.get_token_limit",
"docsgpt.core.model_utils.get_token_limit",
lambda *a, **k: limit,
)
return agent
@@ -1489,7 +1489,7 @@ class TestBaseAgentDocumentsInUserTurn:
]
agent = ClassicAgent(**agent_base_params)
monkeypatch.setattr(
"application.core.model_utils.get_token_limit", lambda *a, **k: 1500
"docsgpt.core.model_utils.get_token_limit", lambda *a, **k: 1500
)
messages = agent._build_messages("short system", "What is Python?")
user = messages[-1]["content"]
@@ -1531,7 +1531,7 @@ class TestBaseAgentDocumentBudgetOrdering:
]
agent = ClassicAgent(**agent_base_params)
monkeypatch.setattr(
"application.core.model_utils.get_token_limit", lambda *a, **k: 4000
"docsgpt.core.model_utils.get_token_limit", lambda *a, **k: 4000
)
huge_question = "please explain this in detail " * 900
user = agent._build_messages("short system", huge_question)[-1]["content"]
+10 -10
View File
@@ -1,10 +1,10 @@
"""Tests for application/agents/tools/brave.py"""
"""Tests for docsgpt/agents/tools/brave.py"""
from unittest.mock import MagicMock, patch
import pytest
from application.agents.tools.brave import BraveSearchTool
from docsgpt.agents.tools.brave import BraveSearchTool
@pytest.fixture
@@ -18,7 +18,7 @@ class TestBraveExecuteAction:
with pytest.raises(ValueError, match="Unknown action"):
tool.execute_action("invalid")
@patch("application.agents.tools.brave.requests.get")
@patch("docsgpt.agents.tools.brave.requests.get")
def test_web_search_success(self, mock_get, tool):
mock_resp = MagicMock()
mock_resp.status_code = 200
@@ -34,7 +34,7 @@ class TestBraveExecuteAction:
call_kwargs = mock_get.call_args
assert call_kwargs[1]["headers"]["X-Subscription-Token"] == "test_api_key"
@patch("application.agents.tools.brave.requests.get")
@patch("docsgpt.agents.tools.brave.requests.get")
def test_web_search_failure(self, mock_get, tool):
mock_resp = MagicMock()
mock_resp.status_code = 429
@@ -45,7 +45,7 @@ class TestBraveExecuteAction:
assert result["status_code"] == 429
assert "failed" in result["message"].lower()
@patch("application.agents.tools.brave.requests.get")
@patch("docsgpt.agents.tools.brave.requests.get")
def test_image_search_success(self, mock_get, tool):
mock_resp = MagicMock()
mock_resp.status_code = 200
@@ -57,7 +57,7 @@ class TestBraveExecuteAction:
assert result["status_code"] == 200
assert "results" in result
@patch("application.agents.tools.brave.requests.get")
@patch("docsgpt.agents.tools.brave.requests.get")
def test_image_search_failure(self, mock_get, tool):
mock_resp = MagicMock()
mock_resp.status_code = 500
@@ -67,7 +67,7 @@ class TestBraveExecuteAction:
assert result["status_code"] == 500
@patch("application.agents.tools.brave.requests.get")
@patch("docsgpt.agents.tools.brave.requests.get")
def test_count_capped_at_20(self, mock_get, tool):
mock_resp = MagicMock()
mock_resp.status_code = 200
@@ -79,7 +79,7 @@ class TestBraveExecuteAction:
params = mock_get.call_args[1]["params"]
assert params["count"] == 20
@patch("application.agents.tools.brave.requests.get")
@patch("docsgpt.agents.tools.brave.requests.get")
def test_image_count_capped_at_100(self, mock_get, tool):
mock_resp = MagicMock()
mock_resp.status_code = 200
@@ -91,7 +91,7 @@ class TestBraveExecuteAction:
params = mock_get.call_args[1]["params"]
assert params["count"] == 100
@patch("application.agents.tools.brave.requests.get")
@patch("docsgpt.agents.tools.brave.requests.get")
def test_freshness_param(self, mock_get, tool):
mock_resp = MagicMock()
mock_resp.status_code = 200
@@ -103,7 +103,7 @@ class TestBraveExecuteAction:
params = mock_get.call_args[1]["params"]
assert params["freshness"] == "pd"
@patch("application.agents.tools.brave.requests.get")
@patch("docsgpt.agents.tools.brave.requests.get")
def test_offset_capped(self, mock_get, tool):
mock_resp = MagicMock()
mock_resp.status_code = 200
+3 -3
View File
@@ -1,8 +1,8 @@
"""Tests for application/agents/workflows/cel_evaluator.py"""
"""Tests for docsgpt/agents/workflows/cel_evaluator.py"""
import pytest
from application.agents.workflows.cel_evaluator import (
from docsgpt.agents.workflows.cel_evaluator import (
CelEvaluationError,
_convert_value,
build_activation,
@@ -232,7 +232,7 @@ class TestCelErrorMessages:
@pytest.mark.unit
def test_long_error_is_truncated(self):
"""Backstop for celpy messages that survive activation-stripping."""
from application.agents.workflows.cel_evaluator import _summarize_cel_error
from docsgpt.agents.workflows.cel_evaluator import _summarize_cel_error
summary = _summarize_cel_error(Exception("word " * 200))
assert len(summary) <= 200
+5 -5
View File
@@ -1,8 +1,8 @@
from unittest.mock import Mock
import pytest
from application.agents.classic_agent import ClassicAgent
from application.agents.tools.internal_search import INTERNAL_TOOL_ID
from docsgpt.agents.classic_agent import ClassicAgent
from docsgpt.agents.tools.internal_search import INTERNAL_TOOL_ID
@pytest.fixture
@@ -13,7 +13,7 @@ def _no_tools(monkeypatch):
return {}
monkeypatch.setattr(
"application.agents.tool_executor.ToolExecutor.get_tools", _fake_get_tools
"docsgpt.agents.tool_executor.ToolExecutor.get_tools", _fake_get_tools
)
@@ -21,7 +21,7 @@ def _no_tools(monkeypatch):
def _no_dir_structure(monkeypatch):
"""Stub the DB-backed directory-structure lookup used by the search tool."""
monkeypatch.setattr(
"application.agents.tools.internal_search.sources_have_directory_structure",
"docsgpt.agents.tools.internal_search.sources_have_directory_structure",
lambda source: False,
)
@@ -117,7 +117,7 @@ class TestClassicAgent:
}
}
monkeypatch.setattr(
"application.agents.tool_executor.ToolExecutor.get_tools",
"docsgpt.agents.tool_executor.ToolExecutor.get_tools",
lambda self: fake_tools,
)
+6 -6
View File
@@ -1,10 +1,10 @@
"""Tests for application/agents/tools/cryptoprice.py"""
"""Tests for docsgpt/agents/tools/cryptoprice.py"""
from unittest.mock import MagicMock, patch
import pytest
from application.agents.tools.cryptoprice import CryptoPriceTool
from docsgpt.agents.tools.cryptoprice import CryptoPriceTool
@pytest.fixture
@@ -18,7 +18,7 @@ class TestCryptoPriceExecuteAction:
with pytest.raises(ValueError, match="Unknown action"):
tool.execute_action("invalid_action")
@patch("application.agents.tools.cryptoprice.requests.get")
@patch("docsgpt.agents.tools.cryptoprice.requests.get")
def test_successful_price_fetch(self, mock_get, tool):
mock_resp = MagicMock()
mock_resp.status_code = 200
@@ -31,7 +31,7 @@ class TestCryptoPriceExecuteAction:
assert result["price"] == 65000
assert "successfully" in result["message"]
@patch("application.agents.tools.cryptoprice.requests.get")
@patch("docsgpt.agents.tools.cryptoprice.requests.get")
def test_currency_not_found(self, mock_get, tool):
mock_resp = MagicMock()
mock_resp.status_code = 200
@@ -44,7 +44,7 @@ class TestCryptoPriceExecuteAction:
assert "Couldn't find" in result["message"]
assert "price" not in result
@patch("application.agents.tools.cryptoprice.requests.get")
@patch("docsgpt.agents.tools.cryptoprice.requests.get")
def test_api_failure(self, mock_get, tool):
mock_resp = MagicMock()
mock_resp.status_code = 500
@@ -55,7 +55,7 @@ class TestCryptoPriceExecuteAction:
assert result["status_code"] == 500
assert "Failed" in result["message"]
@patch("application.agents.tools.cryptoprice.requests.get")
@patch("docsgpt.agents.tools.cryptoprice.requests.get")
def test_symbol_case_insensitive(self, mock_get, tool):
mock_resp = MagicMock()
mock_resp.status_code = 200
+8 -8
View File
@@ -1,4 +1,4 @@
"""Tests for application.agents.default_tools — the default chat tools."""
"""Tests for docsgpt.agents.default_tools — the default chat tools."""
from __future__ import annotations
@@ -6,7 +6,7 @@ import uuid
import pytest
from application.agents import default_tools
from docsgpt.agents import default_tools
@pytest.fixture(autouse=True)
@@ -100,7 +100,7 @@ class TestValidation:
"DEFAULT_CHAT_TOOLS",
["memory", "read_webpage", "future_tool_x"],
)
with caplog.at_level("DEBUG", logger="application.agents.default_tools"):
with caplog.at_level("DEBUG", logger="docsgpt.agents.default_tools"):
usable = default_tools.validate_default_chat_tools()
assert "future_tool_x" not in usable
assert "memory" in usable and "read_webpage" in usable
@@ -117,7 +117,7 @@ class TestValidation:
"DEFAULT_CHAT_TOOLS",
["memory", "read_webpage", "future_tool_x"],
)
with caplog.at_level("DEBUG", logger="application.agents.default_tools"):
with caplog.at_level("DEBUG", logger="docsgpt.agents.default_tools"):
default_tools.loaded_default_tools()
assert caplog.records == []
@@ -201,7 +201,7 @@ class TestSandboxToolsAreNotShippedDefaults:
that legitimately enabled these — which is the documented way to turn
them on when a sandbox runner exists.
"""
from application.core.settings import Settings
from docsgpt.core.settings import Settings
shipped = Settings.model_fields["DEFAULT_CHAT_TOOLS"].default
assert name not in shipped
@@ -497,14 +497,14 @@ class TestBuiltinAgentTools:
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestFkBoundToolsIsInSync:
# Table name -> tool module name (``application/agents/tools/<name>``).
# Table name -> tool module name (``docsgpt/agents/tools/<name>``).
_TABLE_TO_TOOL = {
"notes": "notes",
"todos": "todo_list",
}
def test_fk_bound_tools_matches_metadata(self):
from application.storage.db.models import metadata
from docsgpt.storage.db.models import metadata
fk_bound_tables = set()
for tbl in metadata.tables.values():
@@ -520,7 +520,7 @@ class TestFkBoundToolsIsInSync:
assert not unmapped, (
f"New FK-bound table(s) without a tool mapping: {sorted(unmapped)}. "
"Add an entry to _TABLE_TO_TOOL here AND to "
"application.agents.default_tools._FK_BOUND_TOOLS."
"docsgpt.agents.default_tools._FK_BOUND_TOOLS."
)
derived_names = {
self._TABLE_TO_TOOL[name] for name in fk_bound_tables
+3 -3
View File
@@ -1,10 +1,10 @@
"""Tests for application/agents/tools/duckduckgo.py"""
"""Tests for docsgpt/agents/tools/duckduckgo.py"""
from unittest.mock import MagicMock, patch
import pytest
from application.agents.tools.duckduckgo import DuckDuckGoSearchTool
from docsgpt.agents.tools.duckduckgo import DuckDuckGoSearchTool
@pytest.fixture
@@ -88,7 +88,7 @@ class TestDuckDuckGoExecuteAction:
call_kwargs = mock_client.images.call_args[1]
assert call_kwargs["max_results"] == 50
@patch("application.agents.tools.duckduckgo.time.sleep")
@patch("docsgpt.agents.tools.duckduckgo.time.sleep")
@patch.object(DuckDuckGoSearchTool, "_get_ddgs_client")
def test_rate_limit_retries(self, mock_client_factory, mock_sleep, tool):
mock_client = MagicMock()
+20 -20
View File
@@ -17,7 +17,7 @@ def _patch_db_readonly(pg_conn, monkeypatch):
yield pg_conn
monkeypatch.setattr(
"application.api.user.tools.routes.db_readonly", _use_pg_conn
"docsgpt.api.user.tools.routes.db_readonly", _use_pg_conn
)
@@ -26,9 +26,9 @@ class TestGetArtifact:
def test_note_artifact_success(
self, pg_conn, _patch_db_readonly, flask_app, decoded_token
):
from application.api.user.tools.routes import GetArtifact
from application.storage.db.repositories.notes import NotesRepository
from application.storage.db.repositories.user_tools import UserToolsRepository
from docsgpt.api.user.tools.routes import GetArtifact
from docsgpt.storage.db.repositories.notes import NotesRepository
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
tool_row = UserToolsRepository(pg_conn).create(
user_id=decoded_token["sub"], name="notes_tool"
@@ -57,9 +57,9 @@ class TestGetArtifact:
def test_todo_artifact_success(
self, pg_conn, _patch_db_readonly, flask_app, decoded_token
):
from application.api.user.tools.routes import GetArtifact
from application.storage.db.repositories.todos import TodosRepository
from application.storage.db.repositories.user_tools import UserToolsRepository
from docsgpt.api.user.tools.routes import GetArtifact
from docsgpt.storage.db.repositories.todos import TodosRepository
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
tool_row = UserToolsRepository(pg_conn).create(
user_id=decoded_token["sub"], name="todo_tool"
@@ -96,9 +96,9 @@ class TestGetArtifact:
self, pg_conn, _patch_db_readonly, flask_app, decoded_token
):
"""All todos are returned regardless of the 'all' query parameter."""
from application.api.user.tools.routes import GetArtifact
from application.storage.db.repositories.todos import TodosRepository
from application.storage.db.repositories.user_tools import UserToolsRepository
from docsgpt.api.user.tools.routes import GetArtifact
from docsgpt.storage.db.repositories.todos import TodosRepository
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
tool_row = UserToolsRepository(pg_conn).create(
user_id=decoded_token["sub"], name="todo_tool"
@@ -142,7 +142,7 @@ class TestGetArtifact:
"""A non-UUID, non-legacy id falls through ``get_any`` on both
repos without raising (id-shape dispatch keeps the UUID cast out
of the path) and surfaces as a clean 404."""
from application.api.user.tools.routes import GetArtifact
from docsgpt.api.user.tools.routes import GetArtifact
with flask_app.app_context():
with flask_app.test_request_context():
@@ -155,7 +155,7 @@ class TestGetArtifact:
def test_artifact_not_found_returns_404(
self, _patch_db_readonly, flask_app, decoded_token
):
from application.api.user.tools.routes import GetArtifact
from docsgpt.api.user.tools.routes import GetArtifact
non_existent_id = str(uuid.uuid4())
@@ -171,9 +171,9 @@ class TestGetArtifact:
def test_other_user_artifact_returns_404(
self, pg_conn, _patch_db_readonly, flask_app, decoded_token
):
from application.api.user.tools.routes import GetArtifact
from application.storage.db.repositories.notes import NotesRepository
from application.storage.db.repositories.user_tools import UserToolsRepository
from docsgpt.api.user.tools.routes import GetArtifact
from docsgpt.storage.db.repositories.notes import NotesRepository
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
tool_row = UserToolsRepository(pg_conn).create(
user_id="other_user", name="tool1"
@@ -197,8 +197,8 @@ class TestGetArtifact:
):
"""Pre-cutover note artifact ids (Mongo ObjectIds) must resolve
once the notes.legacy_mongo_id column is populated by backfill."""
from application.api.user.tools.routes import GetArtifact
from application.storage.db.repositories.user_tools import UserToolsRepository
from docsgpt.api.user.tools.routes import GetArtifact
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
tool_row = UserToolsRepository(pg_conn).create(
user_id=decoded_token["sub"], name="notes_tool"
@@ -233,9 +233,9 @@ class TestGetArtifact:
``TodosRepository.get_any`` — that dispatch keeps the 24-hex
ObjectId out of the bare ``CAST(:id AS uuid)`` path that would
otherwise poison the readonly transaction."""
from application.api.user.tools.routes import GetArtifact
from application.storage.db.repositories.todos import TodosRepository
from application.storage.db.repositories.user_tools import UserToolsRepository
from docsgpt.api.user.tools.routes import GetArtifact
from docsgpt.storage.db.repositories.todos import TodosRepository
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
tool_row = UserToolsRepository(pg_conn).create(
user_id=decoded_token["sub"], name="todo_tool"
+6 -6
View File
@@ -18,7 +18,7 @@ import pytest
def _run(events, monkeypatch):
"""Drive ``run_agent_headless`` over a canned agent event stream."""
from application.agents import headless_runner as hr
from docsgpt.agents import headless_runner as hr
agent = MagicMock(name="agent")
agent.gen.return_value = iter(events)
@@ -41,14 +41,14 @@ def _run(events, monkeypatch):
classmethod(lambda cls, *a, **kw: agent),
)
with patch("application.core.model_utils.validate_model_id", return_value=True), \
patch("application.core.model_utils.get_default_model_id", return_value="m"), \
with patch("docsgpt.core.model_utils.validate_model_id", return_value=True), \
patch("docsgpt.core.model_utils.get_default_model_id", return_value="m"), \
patch(
"application.core.model_utils.get_provider_from_model_id",
"docsgpt.core.model_utils.get_provider_from_model_id",
return_value="openai",
), \
patch("application.core.model_utils.get_api_key_for_provider", return_value="k"), \
patch("application.utils.calculate_doc_token_budget", return_value=1000):
patch("docsgpt.core.model_utils.get_api_key_for_provider", return_value="k"), \
patch("docsgpt.utils.calculate_doc_token_budget", return_value=1000):
return hr.run_agent_headless(
{"user_id": "u1", "id": "agent-1", "default_model_id": "m"},
"do the thing",
@@ -19,7 +19,7 @@ import pytest
def _run(agent_config, monkeypatch, events=None):
"""Drive ``run_agent_headless`` and capture the agent-factory kwargs."""
from application.agents import headless_runner as hr
from docsgpt.agents import headless_runner as hr
agent = MagicMock(name="agent")
agent.gen.return_value = iter(events if events is not None else [{"answer": "ok"}])
@@ -48,14 +48,14 @@ def _run(agent_config, monkeypatch, events=None):
hr.AgentCreator, "create_agent", classmethod(_create_agent),
)
with patch("application.core.model_utils.validate_model_id", return_value=True), \
patch("application.core.model_utils.get_default_model_id", return_value="m"), \
with patch("docsgpt.core.model_utils.validate_model_id", return_value=True), \
patch("docsgpt.core.model_utils.get_default_model_id", return_value="m"), \
patch(
"application.core.model_utils.get_provider_from_model_id",
"docsgpt.core.model_utils.get_provider_from_model_id",
return_value="openai",
), \
patch("application.core.model_utils.get_api_key_for_provider", return_value="k"), \
patch("application.utils.calculate_doc_token_budget", return_value=1000):
patch("docsgpt.core.model_utils.get_api_key_for_provider", return_value="k"), \
patch("docsgpt.utils.calculate_doc_token_budget", return_value=1000):
outcome = hr.run_agent_headless(agent_config, "do the thing")
return captured, outcome
+8 -8
View File
@@ -3,7 +3,7 @@
from unittest.mock import MagicMock, Mock, patch
import pytest
from application.agents.tools.internal_search import (
from docsgpt.agents.tools.internal_search import (
INTERNAL_TOOL_ID,
InternalSearchTool,
add_internal_search_tool,
@@ -24,7 +24,7 @@ class TestAddInternalSearchTool:
}
tools: dict = {}
with patch(
"application.agents.tools.internal_search."
"docsgpt.agents.tools.internal_search."
"sources_have_directory_structure",
return_value=False,
):
@@ -254,7 +254,7 @@ class TestBuildHelpers:
}
with patch(
"application.agents.tools.internal_search.sources_have_directory_structure",
"docsgpt.agents.tools.internal_search.sources_have_directory_structure",
return_value=False,
):
add_internal_search_tool(tools_dict, retriever_config)
@@ -284,7 +284,7 @@ class TestInternalSearchToolGetRetriever:
def test_get_retriever_creates_dispatcher(self):
# _get_retriever now builds the per-source Dispatcher (B1c) lazily and
# caches it; the Dispatcher creates per-group retrievers at search time.
from application.retriever.dispatcher import Dispatcher
from docsgpt.retriever.dispatcher import Dispatcher
tool = InternalSearchTool({
"source": {"active_docs": ["a"]},
@@ -295,7 +295,7 @@ class TestInternalSearchToolGetRetriever:
assert tool._retriever is None
with patch(
"application.retriever.classic_rag.LLMCreator.create_llm",
"docsgpt.retriever.classic_rag.LLMCreator.create_llm",
return_value=Mock(),
):
result = tool._get_retriever()
@@ -306,7 +306,7 @@ class TestInternalSearchToolGetRetriever:
def test_get_retriever_kill_switch_falls_back_to_legacy(self, monkeypatch):
# PER_SOURCE_RETRIEVAL_ENABLED=False → legacy single RetrieverCreator.
monkeypatch.setattr(
"application.retriever.dispatcher.settings.PER_SOURCE_RETRIEVAL_ENABLED",
"docsgpt.retriever.dispatcher.settings.PER_SOURCE_RETRIEVAL_ENABLED",
False,
)
tool = InternalSearchTool({
@@ -316,7 +316,7 @@ class TestInternalSearchToolGetRetriever:
})
mock_retriever = Mock()
with patch(
"application.agents.tools.internal_search.RetrieverCreator"
"docsgpt.agents.tools.internal_search.RetrieverCreator"
) as mock_rc:
mock_rc.create_retriever.return_value = mock_retriever
result = tool._get_retriever()
@@ -396,7 +396,7 @@ class TestSourcesHaveDirectoryStructure:
"""Cover line 240, 254, 298: sources_have_directory_structure helper."""
def test_no_active_docs_returns_false(self):
from application.agents.tools.internal_search import (
from docsgpt.agents.tools.internal_search import (
sources_have_directory_structure,
)
+77 -77
View File
@@ -1,4 +1,4 @@
"""Tests for application/agents/tools/mcp_tool.py"""
"""Tests for docsgpt/agents/tools/mcp_tool.py"""
from unittest.mock import MagicMock, patch
@@ -18,14 +18,14 @@ def _patch_mcp_globals(monkeypatch):
import sys
# If the module is already loaded, just patch attributes directly
if "application.agents.tools.mcp_tool" in sys.modules:
mcp_mod = sys.modules["application.agents.tools.mcp_tool"]
if "docsgpt.agents.tools.mcp_tool" in sys.modules:
mcp_mod = sys.modules["docsgpt.agents.tools.mcp_tool"]
else:
# Break the circular import by pre-populating the tasks import
# with a mock before mcp_tool tries to import it
mock_tasks = MagicMock()
monkeypatch.setitem(sys.modules, "application.api.user.tasks", mock_tasks)
import application.agents.tools.mcp_tool as mcp_mod
monkeypatch.setitem(sys.modules, "docsgpt.api.user.tasks", mock_tasks)
import docsgpt.agents.tools.mcp_tool as mcp_mod
monkeypatch.setattr(mcp_mod, "_mcp_clients_cache", {})
monkeypatch.setattr(mcp_mod, "validate_url", lambda url: url)
@@ -55,7 +55,7 @@ def bearer_config():
@pytest.mark.unit
class TestMCPToolInit:
def test_basic_init(self, mcp_config):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
with patch.object(MCPTool, "_setup_client"):
tool = MCPTool(mcp_config)
@@ -66,21 +66,21 @@ class TestMCPToolInit:
assert tool.timeout == 10
def test_bearer_auth_credentials(self, bearer_config):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
with patch.object(MCPTool, "_setup_client"):
tool = MCPTool(bearer_config)
assert tool.auth_credentials["bearer_token"] == "tok_123"
def test_no_server_url_skips_setup(self):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
with patch.object(MCPTool, "_setup_client") as mock_setup:
MCPTool({"server_url": "", "auth_type": "none"})
mock_setup.assert_not_called()
def test_oauth_skips_setup(self):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
with patch.object(MCPTool, "_setup_client") as mock_setup:
MCPTool(
@@ -94,24 +94,24 @@ class TestMCPToolInit:
@pytest.mark.unit
class TestGenerateCacheKey:
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_none_auth(self, mock_setup, mcp_config):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool(mcp_config)
assert "none" in tool._cache_key
assert "mcp.example.com" in tool._cache_key
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_bearer_auth(self, mock_setup, bearer_config):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool(bearer_config)
assert "bearer:" in tool._cache_key
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_api_key_auth(self, mock_setup):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool(
{
@@ -122,9 +122,9 @@ class TestGenerateCacheKey:
)
assert "apikey:" in tool._cache_key
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_basic_auth(self, mock_setup):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool(
{
@@ -138,17 +138,17 @@ class TestGenerateCacheKey:
@pytest.mark.unit
class TestCreateTransport:
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_http_transport(self, mock_setup, mcp_config):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool(mcp_config)
transport = tool._create_transport()
assert transport is not None
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_sse_transport(self, mock_setup):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool(
{
@@ -160,9 +160,9 @@ class TestCreateTransport:
transport = tool._create_transport()
assert transport is not None
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_auto_detects_sse(self, mock_setup):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool(
{
@@ -174,9 +174,9 @@ class TestCreateTransport:
transport = tool._create_transport()
assert transport is not None
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_stdio_transport_disabled(self, mock_setup):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool(
{
@@ -188,9 +188,9 @@ class TestCreateTransport:
with pytest.raises(ValueError, match="STDIO transport is disabled"):
tool._create_transport()
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_api_key_header_injected(self, mock_setup):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool(
{
@@ -207,9 +207,9 @@ class TestCreateTransport:
transport = tool._create_transport()
assert transport is not None
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_basic_auth_header_injected(self, mock_setup):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool(
{
@@ -225,18 +225,18 @@ class TestCreateTransport:
@pytest.mark.unit
class TestFormatTools:
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_format_list_of_dicts(self, mock_setup, mcp_config):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool(mcp_config)
result = tool._format_tools([{"name": "tool1", "description": "desc"}])
assert len(result) == 1
assert result[0]["name"] == "tool1"
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_format_tools_with_name_attribute(self, mock_setup, mcp_config):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool(mcp_config)
mock_tool = MagicMock()
@@ -249,9 +249,9 @@ class TestFormatTools:
assert result[0]["name"] == "my_tool"
assert result[0]["inputSchema"] == {"type": "object", "properties": {}}
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_format_tools_response_object(self, mock_setup, mcp_config):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool(mcp_config)
resp = MagicMock()
@@ -260,9 +260,9 @@ class TestFormatTools:
result = tool._format_tools(resp)
assert len(result) == 1
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_format_empty(self, mock_setup, mcp_config):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool(mcp_config)
assert tool._format_tools([]) == []
@@ -271,9 +271,9 @@ class TestFormatTools:
@pytest.mark.unit
class TestFormatResult:
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_format_result_with_content(self, mock_setup, mcp_config):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool(mcp_config)
mock_result = MagicMock()
@@ -288,9 +288,9 @@ class TestFormatResult:
assert result["content"][0]["text"] == "Hello"
assert result["isError"] is False
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_format_raw_result(self, mock_setup, mcp_config):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool(mcp_config)
raw = {"key": "value"}
@@ -299,18 +299,18 @@ class TestFormatResult:
@pytest.mark.unit
class TestExecuteAction:
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_no_server_raises(self, mock_setup):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool({"server_url": "", "auth_type": "none"})
with pytest.raises(Exception, match="No MCP server configured"):
tool.execute_action("test_action")
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("application.agents.tools.mcp_tool.MCPTool._run_async_operation")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._run_async_operation")
def test_successful_execute(self, mock_run, mock_setup, mcp_config):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool(mcp_config)
tool._client = MagicMock()
@@ -321,10 +321,10 @@ class TestExecuteAction:
mock_run.assert_called_once_with("call_tool", "test_action", param1="val1")
assert result == {"key": "value"}
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("application.agents.tools.mcp_tool.MCPTool._run_async_operation")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._run_async_operation")
def test_empty_kwargs_cleaned(self, mock_run, mock_setup, mcp_config):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool(mcp_config)
tool._client = MagicMock()
@@ -340,18 +340,18 @@ class TestExecuteAction:
@pytest.mark.unit
class TestTestConnection:
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_no_server_url(self, mock_setup):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool({"server_url": "", "auth_type": "none"})
result = tool.test_connection()
assert result["success"] is False
assert "No server URL" in result["message"]
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_invalid_scheme(self, mock_setup):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool(
{"server_url": "ftp://bad.com", "auth_type": "none"}
@@ -363,42 +363,42 @@ class TestTestConnection:
@pytest.mark.unit
class TestMapError:
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_timeout_error(self, mock_setup, mcp_config):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
import concurrent.futures
tool = MCPTool(mcp_config)
err = tool._map_error("test", concurrent.futures.TimeoutError())
assert "Timed out" in str(err)
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_connection_refused(self, mock_setup, mcp_config):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool(mcp_config)
err = tool._map_error("test", ConnectionRefusedError())
assert "Connection refused" in str(err)
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_403_forbidden(self, mock_setup, mcp_config):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool(mcp_config)
err = tool._map_error("test", Exception("403 Forbidden"))
assert "Access denied" in str(err)
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_ssl_error(self, mock_setup, mcp_config):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool(mcp_config)
err = tool._map_error("test", Exception("SSL certificate verify failed"))
assert "SSL" in str(err)
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_unknown_error_passthrough(self, mock_setup, mcp_config):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool(mcp_config)
original = RuntimeError("something weird")
@@ -408,17 +408,17 @@ class TestMapError:
@pytest.mark.unit
class TestGetActionsMetadata:
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_empty_tools(self, mock_setup, mcp_config):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool(mcp_config)
tool.available_tools = []
assert tool.get_actions_metadata() == []
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_tools_with_input_schema(self, mock_setup, mcp_config):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool(mcp_config)
tool.available_tools = [
@@ -437,18 +437,18 @@ class TestGetActionsMetadata:
assert meta[0]["name"] == "search"
assert "query" in meta[0]["parameters"]["properties"]
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_tools_without_schema(self, mock_setup, mcp_config):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool(mcp_config)
tool.available_tools = [{"name": "ping", "description": "Ping"}]
meta = tool.get_actions_metadata()
assert meta[0]["parameters"]["properties"] == {}
@patch("application.agents.tools.mcp_tool.MCPTool._setup_client")
@patch("docsgpt.agents.tools.mcp_tool.MCPTool._setup_client")
def test_config_requirements(self, mock_setup, mcp_config):
from application.agents.tools.mcp_tool import MCPTool
from docsgpt.agents.tools.mcp_tool import MCPTool
tool = MCPTool(mcp_config)
reqs = tool.get_config_requirements()
@@ -460,7 +460,7 @@ class TestGetActionsMetadata:
@pytest.mark.unit
class TestMCPOAuthManager:
def test_handle_callback_success(self):
from application.agents.tools.mcp_tool import MCPOAuthManager
from docsgpt.agents.tools.mcp_tool import MCPOAuthManager
mock_redis = MagicMock()
manager = MCPOAuthManager(mock_redis)
@@ -471,14 +471,14 @@ class TestMCPOAuthManager:
mock_redis.setex.assert_called()
def test_handle_callback_no_redis(self):
from application.agents.tools.mcp_tool import MCPOAuthManager
from docsgpt.agents.tools.mcp_tool import MCPOAuthManager
manager = MCPOAuthManager(None)
result = manager.handle_oauth_callback(state="abc", code="code")
assert result is False
def test_handle_callback_error(self):
from application.agents.tools.mcp_tool import MCPOAuthManager
from docsgpt.agents.tools.mcp_tool import MCPOAuthManager
mock_redis = MagicMock()
manager = MCPOAuthManager(mock_redis)
@@ -489,7 +489,7 @@ class TestMCPOAuthManager:
assert result is False
def test_get_oauth_status_no_task(self):
from application.agents.tools.mcp_tool import MCPOAuthManager
from docsgpt.agents.tools.mcp_tool import MCPOAuthManager
manager = MCPOAuthManager(MagicMock())
result = manager.get_oauth_status("", "alice")
@@ -500,7 +500,7 @@ class TestMCPOAuthManager:
@pytest.mark.unit
class TestDBTokenStorage:
def test_get_base_url(self):
from application.agents.tools.mcp_tool import DBTokenStorage
from docsgpt.agents.tools.mcp_tool import DBTokenStorage
assert (
DBTokenStorage.get_base_url("https://mcp.example.com/api/v1")
@@ -508,7 +508,7 @@ class TestDBTokenStorage:
)
def test_get_db_key(self):
from application.agents.tools.mcp_tool import DBTokenStorage
from docsgpt.agents.tools.mcp_tool import DBTokenStorage
mock_db = MagicMock()
storage = DBTokenStorage(
+5 -5
View File
@@ -13,14 +13,14 @@ def _no_db(monkeypatch):
def fake_readonly():
yield MagicMock()
monkeypatch.setattr("application.agents.tool_executor.db_readonly", fake_readonly)
monkeypatch.setattr("docsgpt.agents.tool_executor.db_readonly", fake_readonly)
class _FakeExecutorBase:
"""Stands in for BaseAgent: provides the tool_executor the mixin scopes."""
def __init__(self, *args, **kwargs):
from application.agents.tool_executor import ToolExecutor
from docsgpt.agents.tool_executor import ToolExecutor
self.tool_executor = ToolExecutor()
@@ -29,7 +29,7 @@ class _FakeExecutorBase:
class TestWorkflowNodeAgentFactory:
def test_raises_on_unsupported_type(self):
from application.agents.workflows.node_agent import WorkflowNodeAgentFactory
from docsgpt.agents.workflows.node_agent import WorkflowNodeAgentFactory
with pytest.raises(ValueError, match="Unsupported agent type"):
WorkflowNodeAgentFactory.create(
@@ -47,7 +47,7 @@ class TestWorkflowNodeMixinInit:
so per-node tool filtering must live there (the old agent-method mixin was dead code)."""
def _mixed(self, **kwargs):
from application.agents.workflows.node_agent import _WorkflowNodeMixin
from docsgpt.agents.workflows.node_agent import _WorkflowNodeMixin
class TestMixin(_WorkflowNodeMixin, _FakeExecutorBase):
pass
@@ -76,7 +76,7 @@ class TestWorkflowNodeMixinInit:
def test_scoped_executor_resolves_builtin_synthetic_ids(self, _no_db):
"""A node whose Tools picker selected a builtin (Artifact / Read Document)
must get that tool at runtime — the P0 this design replaced."""
from application.agents.default_tools import default_tool_id
from docsgpt.agents.default_tools import default_tool_id
obj = self._mixed(
tool_ids=[
+7 -7
View File
@@ -1,10 +1,10 @@
"""Tests for application/agents/tools/ntfy.py"""
"""Tests for docsgpt/agents/tools/ntfy.py"""
from unittest.mock import MagicMock, patch
import pytest
from application.agents.tools.ntfy import NtfyTool
from docsgpt.agents.tools.ntfy import NtfyTool
@pytest.fixture
@@ -23,7 +23,7 @@ class TestNtfyExecuteAction:
with pytest.raises(ValueError, match="Unknown action"):
tool.execute_action("bad_action")
@patch("application.agents.tools.ntfy.pinned_request")
@patch("docsgpt.agents.tools.ntfy.pinned_request")
def test_send_message_basic(self, mock_pinned_request, tool):
mock_resp = MagicMock()
mock_resp.status_code = 200
@@ -44,7 +44,7 @@ class TestNtfyExecuteAction:
assert args[1] == "https://ntfy.sh/test"
assert kwargs["data"] == b"Hello"
@patch("application.agents.tools.ntfy.pinned_request")
@patch("docsgpt.agents.tools.ntfy.pinned_request")
def test_send_with_title_and_priority(self, mock_pinned_request, tool):
mock_resp = MagicMock()
mock_resp.status_code = 200
@@ -63,7 +63,7 @@ class TestNtfyExecuteAction:
assert headers["X-Title"] == "Warning"
assert headers["X-Priority"] == "5"
@patch("application.agents.tools.ntfy.pinned_request")
@patch("docsgpt.agents.tools.ntfy.pinned_request")
def test_auth_header_with_token(self, mock_pinned_request, tool):
mock_resp = MagicMock()
mock_resp.status_code = 200
@@ -79,7 +79,7 @@ class TestNtfyExecuteAction:
headers = mock_pinned_request.call_args[1]["headers"]
assert headers["Authorization"] == "Basic test_token"
@patch("application.agents.tools.ntfy.pinned_request")
@patch("docsgpt.agents.tools.ntfy.pinned_request")
def test_no_auth_without_token(self, mock_pinned_request, tool_no_token):
mock_resp = MagicMock()
mock_resp.status_code = 200
@@ -115,7 +115,7 @@ class TestNtfyExecuteAction:
priority="abc",
)
@patch("application.agents.tools.ntfy.pinned_request")
@patch("docsgpt.agents.tools.ntfy.pinned_request")
def test_trailing_slash_stripped(self, mock_pinned_request, tool):
mock_resp = MagicMock()
mock_resp.status_code = 200
+9 -9
View File
@@ -1,10 +1,10 @@
"""Tests for application/agents/tools/postgres.py"""
"""Tests for docsgpt/agents/tools/postgres.py"""
from unittest.mock import MagicMock, patch
import pytest
from application.agents.tools.postgres import PostgresTool
from docsgpt.agents.tools.postgres import PostgresTool
@pytest.fixture
@@ -18,7 +18,7 @@ class TestPostgresExecuteAction:
with pytest.raises(ValueError, match="Unknown action"):
tool.execute_action("invalid")
@patch("application.agents.tools.postgres.psycopg.connect")
@patch("docsgpt.agents.tools.postgres.psycopg.connect")
def test_select_query(self, mock_connect, tool):
mock_conn = MagicMock()
mock_cur = MagicMock()
@@ -37,7 +37,7 @@ class TestPostgresExecuteAction:
assert result["response_data"]["data"][0] == {"id": 1, "name": "Alice"}
mock_conn.close.assert_called_once()
@patch("application.agents.tools.postgres.psycopg.connect")
@patch("docsgpt.agents.tools.postgres.psycopg.connect")
def test_insert_query(self, mock_connect, tool):
mock_conn = MagicMock()
mock_cur = MagicMock()
@@ -55,7 +55,7 @@ class TestPostgresExecuteAction:
mock_conn.commit.assert_called_once()
mock_conn.close.assert_called_once()
@patch("application.agents.tools.postgres.psycopg.connect")
@patch("docsgpt.agents.tools.postgres.psycopg.connect")
def test_db_error(self, mock_connect, tool):
import psycopg
@@ -68,7 +68,7 @@ class TestPostgresExecuteAction:
assert result["status_code"] == 500
assert "Database error" in result["error"]
@patch("application.agents.tools.postgres.psycopg.connect")
@patch("docsgpt.agents.tools.postgres.psycopg.connect")
def test_get_schema(self, mock_connect, tool):
mock_conn = MagicMock()
mock_cur = MagicMock()
@@ -89,7 +89,7 @@ class TestPostgresExecuteAction:
assert result["schema"]["users"][0]["column_name"] == "id"
mock_conn.close.assert_called_once()
@patch("application.agents.tools.postgres.psycopg.connect")
@patch("docsgpt.agents.tools.postgres.psycopg.connect")
def test_get_schema_db_error(self, mock_connect, tool):
import psycopg
@@ -100,7 +100,7 @@ class TestPostgresExecuteAction:
assert result["status_code"] == 500
assert "Database error" in result["error"]
@patch("application.agents.tools.postgres.psycopg.connect")
@patch("docsgpt.agents.tools.postgres.psycopg.connect")
def test_connection_closed_on_error(self, mock_connect, tool):
import psycopg
@@ -114,7 +114,7 @@ class TestPostgresExecuteAction:
mock_conn.close.assert_called_once()
@patch("application.agents.tools.postgres.psycopg.connect")
@patch("docsgpt.agents.tools.postgres.psycopg.connect")
def test_select_with_no_description(self, mock_connect, tool):
mock_conn = MagicMock()
mock_cur = MagicMock()
+24 -24
View File
@@ -1,12 +1,12 @@
"""Tests for application/agents/tools/read_webpage.py"""
"""Tests for docsgpt/agents/tools/read_webpage.py"""
from unittest.mock import patch
import pytest
import requests
from application.agents.tools.read_webpage import ReadWebpageTool
from application.security.safe_url import ResponseTooLargeError
from docsgpt.agents.tools.read_webpage import ReadWebpageTool
from docsgpt.security.safe_url import ResponseTooLargeError
class _FakeResponse:
@@ -43,7 +43,7 @@ class TestReadWebpageExecuteAction:
assert "Error" in result
assert "URL parameter is missing" in result
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
def test_successful_fetch(self, mock_fetch, tool):
mock_fetch.return_value = _fetch_result(
b"<html><body><h1>Title</h1><p>Content</p></body></html>",
@@ -55,7 +55,7 @@ class TestReadWebpageExecuteAction:
assert "Title" in result
assert "Content" in result
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
def test_request_error(self, mock_fetch, tool):
mock_fetch.side_effect = requests.exceptions.ConnectionError("refused")
@@ -63,9 +63,9 @@ class TestReadWebpageExecuteAction:
assert "Error fetching URL" in result
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
def test_ssrf_blocked(self, mock_fetch, tool):
from application.security.safe_url import UnsafeUserUrlError
from docsgpt.security.safe_url import UnsafeUserUrlError
mock_fetch.side_effect = UnsafeUserUrlError("blocked")
@@ -74,7 +74,7 @@ class TestReadWebpageExecuteAction:
assert "Error" in result
assert "validation failed" in result
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
def test_http_error(self, mock_fetch, tool):
mock_fetch.return_value = _fetch_result(b"", status_code=404)
@@ -88,7 +88,7 @@ class TestReadWebpageContentGuards:
"""Regression tests for the PDF-as-text incident: a binary body must
never come back as NUL-laden 'markdown'."""
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
def test_rejects_pdf_content_type(self, mock_fetch, tool):
mock_fetch.return_value = _fetch_result(
b"%PDF-1.7 binary...", content_type="application/pdf",
@@ -99,7 +99,7 @@ class TestReadWebpageContentGuards:
assert result.startswith("Error")
assert "application/pdf" in result
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
def test_rejects_pdf_magic_without_content_type(self, mock_fetch, tool):
# No Content-Type header at all — the 07-17 incident shape.
mock_fetch.return_value = _fetch_result(b"%PDF-1.4\n%\xe2\xe3\xcf\xd3...")
@@ -108,7 +108,7 @@ class TestReadWebpageContentGuards:
assert result.startswith("Error")
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
def test_rejects_nul_laden_body_despite_html_content_type(self, mock_fetch, tool):
# Mislabeled binary: content-type lies, the NUL sniff must catch it.
mock_fetch.return_value = _fetch_result(
@@ -121,7 +121,7 @@ class TestReadWebpageContentGuards:
assert result.startswith("Error")
assert "\x00" not in result
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
def test_rejects_octet_stream(self, mock_fetch, tool):
mock_fetch.return_value = _fetch_result(
b"anything", content_type="application/octet-stream",
@@ -132,7 +132,7 @@ class TestReadWebpageContentGuards:
assert result.startswith("Error")
assert "application/octet-stream" in result
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
def test_response_too_large(self, mock_fetch, tool):
mock_fetch.side_effect = ResponseTooLargeError("body exceeds 10485760 bytes")
@@ -141,7 +141,7 @@ class TestReadWebpageContentGuards:
assert result.startswith("Error")
assert "large" in result.lower()
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
def test_utf8_decoded_without_declared_charset(self, mock_fetch, tool):
# text/html with no charset param: requests' RFC-2616 ISO-8859-1
# fallback would mojibake this; we must default to UTF-8.
@@ -154,7 +154,7 @@ class TestReadWebpageContentGuards:
assert "café über" in result
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
def test_declared_charset_respected(self, mock_fetch, tool):
mock_fetch.return_value = _fetch_result(
"<html><body><p>café</p></body></html>".encode("latin-1"),
@@ -165,7 +165,7 @@ class TestReadWebpageContentGuards:
assert "café" in result
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
def test_unknown_declared_charset_falls_back_to_utf8(self, mock_fetch, tool):
mock_fetch.return_value = _fetch_result(
"<html><body><p>ok</p></body></html>".encode("utf-8"),
@@ -176,7 +176,7 @@ class TestReadWebpageContentGuards:
assert "ok" in result
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
def test_text_plain_allowed(self, mock_fetch, tool):
mock_fetch.return_value = _fetch_result(
b"plain text document", content_type="text/plain",
@@ -186,7 +186,7 @@ class TestReadWebpageContentGuards:
assert "plain text document" in result
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
def test_utf16_page_with_declared_charset_allowed(self, mock_fetch, tool):
# UTF-16 text is NUL-dense; a correct charset declaration must
# win over the NUL sniff (which is for undeclared/mislabeled bodies).
@@ -200,7 +200,7 @@ class TestReadWebpageContentGuards:
assert "Hello world" in result
assert "\x00" not in result
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
def test_pdf_magic_beats_declared_charset(self, mock_fetch, tool):
# The magic-prefix check stays unconditional: a lying
# ``text/html; charset=utf-8`` header must not sneak a PDF through.
@@ -213,7 +213,7 @@ class TestReadWebpageContentGuards:
assert result.startswith("Error")
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
def test_nuls_past_sniff_window_are_stripped(self, mock_fetch, tool):
# The sniff only sees the first KB; NULs beyond it must still
# never leave the tool (self-contained, not reliant on the
@@ -226,7 +226,7 @@ class TestReadWebpageContentGuards:
assert "\x00" not in result
assert "tail" in result
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
def test_text_csv_allowed(self, mock_fetch, tool):
mock_fetch.return_value = _fetch_result(
b"name,qty\nwidget,2", content_type="text/csv",
@@ -236,7 +236,7 @@ class TestReadWebpageContentGuards:
assert "widget" in result
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
def test_redirect_reported_with_target(self, mock_fetch, tool):
# allow_redirects=False (SSRF) means a 3xx would otherwise return
# the redirect body as near-empty markdown with no hint.
@@ -250,7 +250,7 @@ class TestReadWebpageContentGuards:
assert result.startswith("Error")
assert "https://example.com/moved-here" in result
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
def test_redirect_without_location_reported(self, mock_fetch, tool):
mock_fetch.return_value = _fetch_result(b"", status_code=302)
@@ -259,7 +259,7 @@ class TestReadWebpageContentGuards:
assert result.startswith("Error")
assert "redirect" in result.lower()
@patch("application.agents.tools.read_webpage.pinned_fetch_bytes")
@patch("docsgpt.agents.tools.read_webpage.pinned_fetch_bytes")
def test_rss_feed_allowed(self, mock_fetch, tool):
mock_fetch.return_value = _fetch_result(
b"<rss><channel><title>Feed title</title></channel></rss>",
+10 -10
View File
@@ -1,4 +1,4 @@
"""Comprehensive tests for application/agents/research_agent.py
"""Comprehensive tests for docsgpt/agents/research_agent.py
Covers: CitationManager, ResearchAgent (init, budget, timeout, phases:
clarification, planning, research step, synthesis, _extract_text,
@@ -11,7 +11,7 @@ from unittest.mock import Mock, patch
import pytest
from application.agents.research_agent import (
from docsgpt.agents.research_agent import (
COMPLEXITY_CAPS,
CitationManager,
ResearchAgent,
@@ -732,7 +732,7 @@ class TestResearchAgentToolSetup:
with patch.object(
agent.tool_executor, "get_tools", return_value={}
), patch(
"application.agents.research_agent.add_internal_search_tool"
"docsgpt.agents.research_agent.add_internal_search_tool"
) as mock_add:
tools = agent._setup_tools()
mock_add.assert_called_once()
@@ -746,7 +746,7 @@ class TestResearchAgentToolSetup:
with patch.object(
agent.tool_executor, "get_tools", return_value={}
), patch(
"application.agents.research_agent.add_internal_search_tool"
"docsgpt.agents.research_agent.add_internal_search_tool"
) as mock_add:
tools = agent._setup_tools()
mock_add.assert_called_once()
@@ -1113,7 +1113,7 @@ class TestResearchStep:
mock_response = Mock()
mock_llm.gen = Mock(return_value=mock_response)
from application.llm.handlers.base import LLMResponse
from docsgpt.llm.handlers.base import LLMResponse
parsed = LLMResponse(
content="Direct answer to the question",
tool_calls=[],
@@ -1142,7 +1142,7 @@ class TestResearchStep:
mock_response2 = Mock()
mock_llm.gen = Mock(side_effect=[mock_response1, mock_response2])
from application.llm.handlers.base import LLMResponse, ToolCall
from docsgpt.llm.handlers.base import LLMResponse, ToolCall
tool_call = ToolCall(id="tc1", name="internal__search", arguments={"query": "python"})
parsed_with_tool = LLMResponse(
@@ -1238,7 +1238,7 @@ class TestResearchStep:
agent._start_time = time.monotonic()
mock_llm.token_usage = {"prompt_tokens": 10, "generated_tokens": 5}
from application.llm.handlers.base import LLMResponse, ToolCall
from docsgpt.llm.handlers.base import LLMResponse, ToolCall
tool_call = ToolCall(id="tc1", name="internal__search", arguments={"query": "test"})
@@ -1299,7 +1299,7 @@ class TestExecuteStepToolsWithRefinement:
"""Tool execution appends messages correctly."""
agent = ResearchAgent(**agent_base_params)
from application.llm.handlers.base import ToolCall
from docsgpt.llm.handlers.base import ToolCall
call = ToolCall(id="tc1", name="internal__search", arguments={"query": "test"})
@@ -1338,7 +1338,7 @@ class TestExecuteStepToolsWithRefinement:
"""When search returns empty twice, adds refinement hint."""
agent = ResearchAgent(**agent_base_params)
from application.llm.handlers.base import ToolCall
from docsgpt.llm.handlers.base import ToolCall
call = ToolCall(id="tc1", name="internal__search", arguments={"query": "test"})
@@ -1370,7 +1370,7 @@ class TestExecuteStepToolsWithRefinement:
"""Non-search tools don't trigger empty search logic."""
agent = ResearchAgent(**agent_base_params)
from application.llm.handlers.base import ToolCall
from docsgpt.llm.handlers.base import ToolCall
call = ToolCall(id="tc1", name="think__think", arguments={"thought": "hmm"})
+12 -12
View File
@@ -12,17 +12,17 @@ import pytest
from sqlalchemy import text
# Pre-import to stabilise the ToolManager.load_tools walk's import order.
import application.api.user.tools.mcp # noqa: F401
import docsgpt.api.user.tools.mcp # noqa: F401
from application.agents.default_tools import ( # noqa: E402
from docsgpt.agents.default_tools import ( # noqa: E402
BUILTIN_AGENT_TOOLS,
builtin_agent_tools_for_management,
default_tool_id,
resolve_tool_by_id,
)
from application.agents.tool_executor import ToolExecutor # noqa: E402
from application.agents.tools.tool_manager import ToolManager # noqa: E402
from application.storage.db.repositories.schedules import ( # noqa: E402
from docsgpt.agents.tool_executor import ToolExecutor # noqa: E402
from docsgpt.agents.tools.tool_manager import ToolManager # noqa: E402
from docsgpt.storage.db.repositories.schedules import ( # noqa: E402
SchedulesRepository,
)
@@ -36,9 +36,9 @@ def patch_scheduler_sessions(pg_conn):
yield pg_conn
with patch(
"application.agents.tools.scheduler.db_session", _ctx,
"docsgpt.agents.tools.scheduler.db_session", _ctx,
), patch(
"application.agents.tools.scheduler.db_readonly", _ctx,
"docsgpt.agents.tools.scheduler.db_readonly", _ctx,
):
yield
@@ -98,7 +98,7 @@ class TestDualRegistration:
"""``scheduler`` is in both registries; same uuid5 resolves either way."""
def test_scheduler_in_both_registries(self):
from application.agents.default_tools import (
from docsgpt.agents.default_tools import (
BUILTIN_AGENT_TOOLS as BUILTINS,
settings,
)
@@ -106,7 +106,7 @@ class TestDualRegistration:
assert "scheduler" in settings.DEFAULT_CHAT_TOOLS
def test_same_synthetic_id_in_both_paths(self):
from application.agents.default_tools import (
from docsgpt.agents.default_tools import (
builtin_agent_tool_ids,
default_tool_ids,
)
@@ -117,7 +117,7 @@ class TestDualRegistration:
def test_synthesized_default_tools_includes_scheduler(self):
"""Agentless chats see scheduler in the default-tools synthesis."""
from application.agents.default_tools import synthesized_default_tools
from docsgpt.agents.default_tools import synthesized_default_tools
rows = synthesized_default_tools(None)
names = {r["name"] for r in rows}
@@ -125,7 +125,7 @@ class TestDualRegistration:
def test_synthesized_builtin_agent_tools_includes_scheduler(self):
"""Agent picker still sees scheduler via the builtin registry."""
from application.agents.default_tools import (
from docsgpt.agents.default_tools import (
builtin_agent_tools_for_management,
)
@@ -146,7 +146,7 @@ class TestEndToEndAgentPickerToLLMSchema:
def _use_conn():
yield pg_conn
with patch("application.agents.tool_executor.db_readonly", _use_conn):
with patch("docsgpt.agents.tool_executor.db_readonly", _use_conn):
executor = ToolExecutor(
user_api_key=agent["key"], user="alice",
agent_id=str(agent["id"]),
+2 -2
View File
@@ -7,7 +7,7 @@ from zoneinfo import ZoneInfo
import pytest
from application.agents.scheduler_utils import (
from docsgpt.agents.scheduler_utils import (
ScheduleValidationError,
clamp_once_horizon,
cron_interval_seconds,
@@ -120,7 +120,7 @@ class TestCronIntervalSeconds:
assert cron_interval_seconds("* 9 * * *", None) == 60
def test_bursty_cron_rejected_when_floor_above_burst(self):
from application.core.settings import settings as app_settings
from docsgpt.core.settings import settings as app_settings
burst = "* 9 * * *"
cadence = cron_interval_seconds(burst, None)
floor = max(0, int(app_settings.SCHEDULE_MIN_INTERVAL))
+11 -11
View File
@@ -1,10 +1,10 @@
"""Tests for application/agents/tools/spec_parser.py"""
"""Tests for docsgpt/agents/tools/spec_parser.py"""
import json
import pytest
from application.agents.tools.spec_parser import (
from docsgpt.agents.tools.spec_parser import (
_extract_metadata,
_generate_action_name,
_get_base_url,
@@ -473,14 +473,14 @@ paths:
def test_traverse_path_key_error(self):
"""Cover lines 173-176: _traverse_path returns None on KeyError."""
from application.agents.tools.spec_parser import _traverse_path
from docsgpt.agents.tools.spec_parser import _traverse_path
result = _traverse_path({"a": {"b": 1}}, ["a", "c"])
assert result is None
def test_traverse_path_non_dict_result(self):
"""Cover line 175-176: _traverse_path returns None for non-dict result."""
from application.agents.tools.spec_parser import _traverse_path
from docsgpt.agents.tools.spec_parser import _traverse_path
result = _traverse_path({"a": "string_value"}, ["a"])
assert result is None
@@ -529,7 +529,7 @@ class TestSpecParserAdditionalCoverage:
def test_categorize_params_query_and_header(self):
"""Cover lines 205, 209, 213: parameters categorized into query and header."""
from application.agents.tools.spec_parser import _categorize_parameters
from docsgpt.agents.tools.spec_parser import _categorize_parameters
parameters = [
{"name": "q", "in": "query", "required": True, "description": "Query param"},
@@ -543,7 +543,7 @@ class TestSpecParserAdditionalCoverage:
def test_categorize_params_skips_no_name(self):
"""Cover line 205: parameters without name are skipped."""
from application.agents.tools.spec_parser import _categorize_parameters
from docsgpt.agents.tools.spec_parser import _categorize_parameters
parameters = [
{"in": "query"}, # no name
@@ -554,7 +554,7 @@ class TestSpecParserAdditionalCoverage:
def test_param_to_property_integer_type(self):
"""Cover lines 216-217, 222, 228: _param_to_property with integer type."""
from application.agents.tools.spec_parser import _param_to_property
from docsgpt.agents.tools.spec_parser import _param_to_property
param = {
"name": "count",
@@ -569,7 +569,7 @@ class TestSpecParserAdditionalCoverage:
def test_param_to_property_number_type(self):
"""Cover line 222: number type mapped to integer."""
from application.agents.tools.spec_parser import _param_to_property
from docsgpt.agents.tools.spec_parser import _param_to_property
param = {
"schema": {"type": "number"},
@@ -581,7 +581,7 @@ class TestSpecParserAdditionalCoverage:
def test_param_to_property_string_default(self):
"""Cover line 222: unknown type defaults to string."""
from application.agents.tools.spec_parser import _param_to_property
from docsgpt.agents.tools.spec_parser import _param_to_property
param = {"description": "Desc", "required": False}
prop = _param_to_property(param)
@@ -589,7 +589,7 @@ class TestSpecParserAdditionalCoverage:
def test_param_to_property_description_truncated(self):
"""Cover line 228: description truncated to 200 chars."""
from application.agents.tools.spec_parser import _param_to_property
from docsgpt.agents.tools.spec_parser import _param_to_property
param = {"description": "x" * 300, "required": False}
prop = _param_to_property(param)
@@ -603,7 +603,7 @@ class TestSpecParserAdditionalCoverage:
# 184-190 (generate_action_name from path), 99 (swagger base URL)
# ---------------------------------------------------------------------------
from application.agents.tools.spec_parser import _extract_actions # noqa: E402
from docsgpt.agents.tools.spec_parser import _extract_actions # noqa: E402
@pytest.mark.unit
+5 -5
View File
@@ -1,10 +1,10 @@
"""Tests for application/agents/tools/telegram.py"""
"""Tests for docsgpt/agents/tools/telegram.py"""
from unittest.mock import MagicMock, patch
import pytest
from application.agents.tools.telegram import TelegramTool
from docsgpt.agents.tools.telegram import TelegramTool
@pytest.fixture
@@ -18,7 +18,7 @@ class TestTelegramExecuteAction:
with pytest.raises(ValueError, match="Unknown action"):
tool.execute_action("invalid")
@patch("application.agents.tools.telegram.requests.post")
@patch("docsgpt.agents.tools.telegram.requests.post")
def test_send_message(self, mock_post, tool):
mock_resp = MagicMock()
mock_resp.status_code = 200
@@ -35,7 +35,7 @@ class TestTelegramExecuteAction:
assert call_args[1]["data"]["text"] == "Hello"
assert call_args[1]["data"]["chat_id"] == "12345"
@patch("application.agents.tools.telegram.requests.post")
@patch("docsgpt.agents.tools.telegram.requests.post")
def test_send_image(self, mock_post, tool):
mock_resp = MagicMock()
mock_resp.status_code = 200
@@ -51,7 +51,7 @@ class TestTelegramExecuteAction:
assert "bot123:ABC/sendPhoto" in call_args[0][0]
assert call_args[1]["data"]["photo"] == "https://img.com/cat.jpg"
@patch("application.agents.tools.telegram.requests.post")
@patch("docsgpt.agents.tools.telegram.requests.post")
def test_api_error_status(self, mock_post, tool):
mock_resp = MagicMock()
mock_resp.status_code = 403
+1 -1
View File
@@ -1,7 +1,7 @@
"""Tests for ThinkTool — the chain-of-thought pseudo-tool."""
import pytest
from application.agents.tools.think import (
from docsgpt.agents.tools.think import (
THINK_TOOL_ENTRY,
THINK_TOOL_ID,
ThinkTool,
+1 -1
View File
@@ -1,7 +1,7 @@
from unittest.mock import Mock
import pytest
from application.agents.tools.tool_action_parser import ToolActionParser
from docsgpt.agents.tools.tool_action_parser import ToolActionParser
@pytest.mark.unit
+2 -2
View File
@@ -11,8 +11,8 @@ from __future__ import annotations
import pytest
from application.agents.tools.artifact_generator import ArtifactGeneratorTool
from application.agents.tools.code_executor import CodeExecutorTool
from docsgpt.agents.tools.artifact_generator import ArtifactGeneratorTool
from docsgpt.agents.tools.code_executor import CodeExecutorTool
@pytest.mark.unit
+1 -1
View File
@@ -19,7 +19,7 @@ from __future__ import annotations
import pytest
from application.agents.tool_executor import ToolExecutor
from docsgpt.agents.tool_executor import ToolExecutor
@pytest.mark.unit
+40 -40
View File
@@ -3,7 +3,7 @@
from unittest.mock import Mock
import pytest
from application.agents.tool_executor import ToolExecutor
from docsgpt.agents.tool_executor import ToolExecutor
@pytest.mark.unit
@@ -42,12 +42,12 @@ class TestToolExecutorGetTools:
yield pg_conn
monkeypatch.setattr(
"application.agents.tool_executor.db_readonly", _use_pg_conn
"docsgpt.agents.tool_executor.db_readonly", _use_pg_conn
)
def test_get_tools_uses_api_key_when_present(self, pg_conn, monkeypatch):
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.user_tools import UserToolsRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
tool = UserToolsRepository(pg_conn).create(user_id="alice", name="tool1")
AgentsRepository(pg_conn).create(
@@ -69,8 +69,8 @@ class TestToolExecutorGetTools:
def test_api_key_agent_never_resolves_workflow_only_builtins(self, pg_conn, monkeypatch):
"""read_document is workflow-only: a chat/scheduled agent carrying its
builtin id must not get it; workflow nodes get it via allowed_tool_ids."""
from application.agents.default_tools import default_tool_id
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.storage.db.repositories.agents import AgentsRepository
read_doc_id = default_tool_id("read_document")
artifact_id = default_tool_id("artifact_generator")
@@ -94,8 +94,8 @@ class TestToolExecutorGetTools:
assert node_names == {"read_document"}
def test_agentless_chat_synthesizes_defaults(self, pg_conn, monkeypatch):
from application.agents.default_tools import loaded_default_tools
from application.storage.db.repositories.user_tools import UserToolsRepository
from docsgpt.agents.default_tools import loaded_default_tools
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
UserToolsRepository(pg_conn).create(
user_id="alice", name="tool1", status=True
@@ -114,8 +114,8 @@ class TestToolExecutorGetTools:
self, pg_conn, monkeypatch
):
"""``agent_id`` forces ``agents.tools``-only; no defaults synthesized."""
from application.agents.default_tools import loaded_default_tools
from application.storage.db.repositories.user_tools import UserToolsRepository
from docsgpt.agents.default_tools import loaded_default_tools
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
UserToolsRepository(pg_conn).create(
user_id="alice", name="tool1", status=True
@@ -129,7 +129,7 @@ class TestToolExecutorGetTools:
assert not (set(loaded_default_tools()) & names)
def test_get_tools_defaults_to_local(self, pg_conn, monkeypatch):
from application.agents.default_tools import loaded_default_tools
from docsgpt.agents.default_tools import loaded_default_tools
self._patch_conn(monkeypatch, pg_conn)
@@ -141,9 +141,9 @@ class TestToolExecutorGetTools:
def test_api_key_path_excludes_defaults(self, pg_conn, monkeypatch):
"""Agent-bound resolution returns exactly ``agents.tools``."""
from application.agents.default_tools import loaded_default_tools
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.user_tools import UserToolsRepository
from docsgpt.agents.default_tools import loaded_default_tools
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
tool = UserToolsRepository(pg_conn).create(user_id="alice", name="tool1")
AgentsRepository(pg_conn).create(
@@ -165,7 +165,7 @@ class TestToolExecutorGetTools:
self, pg_conn, monkeypatch
):
"""Empty ``agents.tools`` invoked via API key yields no tools."""
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
AgentsRepository(pg_conn).create(
user_id="bob",
@@ -183,8 +183,8 @@ class TestToolExecutorGetTools:
self, pg_conn, monkeypatch
):
"""Only ``read_webpage`` in ``agents.tools`` -> exactly that; no other defaults bolted on."""
from application.agents.default_tools import default_tool_id
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.storage.db.repositories.agents import AgentsRepository
read_webpage_id = default_tool_id("read_webpage")
memory_id = default_tool_id("memory")
@@ -210,8 +210,8 @@ class TestToolExecutorGetTools:
self, pg_conn, monkeypatch
):
"""A default tool added explicitly to ``agents.tools`` resolves for every caller."""
from application.agents.default_tools import default_tool_id
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.storage.db.repositories.agents import AgentsRepository
memory_id = default_tool_id("memory")
AgentsRepository(pg_conn).create(
@@ -233,7 +233,7 @@ class TestToolExecutorGetTools:
def test_no_dedup_between_explicit_and_default_memory(
self, pg_conn, monkeypatch
):
from application.storage.db.repositories.user_tools import UserToolsRepository
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
# Explicit ``memory`` row and the default ``memory`` coexist (separate stores).
UserToolsRepository(pg_conn).create(
@@ -603,7 +603,7 @@ class TestCheckPauseRemoteDevice:
def _patch_device(self, monkeypatch, device):
"""Stub ``RemoteDeviceTool._load_device`` to return ``device``."""
from application.agents.tools import remote_device
from docsgpt.agents.tools import remote_device
monkeypatch.setattr(
remote_device.RemoteDeviceTool,
@@ -613,7 +613,7 @@ class TestCheckPauseRemoteDevice:
def _patch_sticky(self, monkeypatch, patterns):
"""Stub the sticky lookup to match any normalized pattern in ``patterns``."""
from application.agents.tools import remote_device
from docsgpt.agents.tools import remote_device
monkeypatch.setattr(
remote_device,
@@ -789,7 +789,7 @@ class TestCheckPauseRemoteDeviceHeadless:
}
def _patch_device(self, monkeypatch, device):
from application.agents.tools import remote_device
from docsgpt.agents.tools import remote_device
monkeypatch.setattr(
remote_device.RemoteDeviceTool,
@@ -798,7 +798,7 @@ class TestCheckPauseRemoteDeviceHeadless:
)
def _patch_sticky(self, monkeypatch, patterns):
from application.agents.tools import remote_device
from docsgpt.agents.tools import remote_device
monkeypatch.setattr(
remote_device,
@@ -910,7 +910,7 @@ class TestToolExecutorExecute:
executor = ToolExecutor()
monkeypatch.setattr(
"application.agents.tool_executor.ToolActionParser",
"docsgpt.agents.tool_executor.ToolActionParser",
lambda _cls, **kw: Mock(parse_args=Mock(return_value=(None, None, {}))),
)
@@ -937,7 +937,7 @@ class TestToolExecutorExecute:
executor = ToolExecutor()
monkeypatch.setattr(
"application.agents.tool_executor.ToolActionParser",
"docsgpt.agents.tool_executor.ToolActionParser",
lambda _cls, **kw: Mock(parse_args=Mock(return_value=("missing_id", "action", {}))),
)
@@ -963,7 +963,7 @@ class TestToolExecutorExecute:
executor = ToolExecutor(user="test_user")
monkeypatch.setattr(
"application.agents.tool_executor.ToolActionParser",
"docsgpt.agents.tool_executor.ToolActionParser",
lambda _cls, **kw: Mock(parse_args=Mock(return_value=("t1", "test_action", {"param1": "val"}))),
)
@@ -998,7 +998,7 @@ class TestToolExecutorExecute:
assert "completed" in statuses
def test_get_truncated_tool_calls(self):
from application.agents.tool_executor import PERSISTED_RESULT_MAX_LEN
from docsgpt.agents.tool_executor import PERSISTED_RESULT_MAX_LEN
executor = ToolExecutor()
executor.tool_calls = [
@@ -1044,7 +1044,7 @@ class TestToolExecutorExecute:
assert truncated[0]["status"] == "error"
def test_result_status_reflects_in_band_tool_errors(self):
from application.agents.tool_executor import result_status
from docsgpt.agents.tool_executor import result_status
assert result_status({"status": "error", "error": "invalid spec"}) == "error"
assert result_status({"error": "input artifact A9 not found"}) == "error"
@@ -1056,7 +1056,7 @@ class TestToolExecutorExecute:
executor = ToolExecutor(user="test_user")
monkeypatch.setattr(
"application.agents.tool_executor.ToolActionParser",
"docsgpt.agents.tool_executor.ToolActionParser",
lambda _cls, **kw: Mock(parse_args=Mock(return_value=("t1", "test_action", {}))),
)
@@ -1097,7 +1097,7 @@ class TestToolExecutorExecute:
executor = ToolExecutor(user="test_user")
monkeypatch.setattr(
"application.agents.tool_executor.ToolActionParser",
"docsgpt.agents.tool_executor.ToolActionParser",
lambda _cls, **kw: Mock(
parse_args=Mock(return_value=("t1", "get_users", {"body_param": "val"}))
),
@@ -1144,7 +1144,7 @@ class TestToolExecutorExecute:
executor = ToolExecutor(user="test_user")
monkeypatch.setattr(
"application.agents.tool_executor.ToolActionParser",
"docsgpt.agents.tool_executor.ToolActionParser",
lambda _cls, **kw: Mock(
parse_args=Mock(return_value=("t1", "act", {}))
),
@@ -1190,7 +1190,7 @@ class TestToolExecutorExecute:
executor = ToolExecutor(user="test_user")
monkeypatch.setattr(
"application.agents.tool_executor.ToolActionParser",
"docsgpt.agents.tool_executor.ToolActionParser",
lambda _cls, **kw: Mock(
parse_args=Mock(return_value=("t1", "act", {"q": "v"}))
),
@@ -1239,10 +1239,10 @@ class TestToolExecutorExecute:
mock_tool = Mock()
mock_tm.load_tool.return_value = mock_tool
monkeypatch.setattr(
"application.agents.tool_executor.ToolManager", lambda config: mock_tm
"docsgpt.agents.tool_executor.ToolManager", lambda config: mock_tm
)
monkeypatch.setattr(
"application.agents.tool_executor.decrypt_credentials",
"docsgpt.agents.tool_executor.decrypt_credentials",
lambda creds, user: {"api_key": "decrypted_key"},
)
@@ -1267,7 +1267,7 @@ class TestToolExecutorExecute:
mock_tm = Mock()
mock_tm.load_tool.return_value = Mock()
monkeypatch.setattr(
"application.agents.tool_executor.ToolManager", lambda config: mock_tm
"docsgpt.agents.tool_executor.ToolManager", lambda config: mock_tm
)
captured = {}
@@ -1276,7 +1276,7 @@ class TestToolExecutorExecute:
return {"api_key": "owner_secret"}
monkeypatch.setattr(
"application.agents.tool_executor.decrypt_credentials", _fake_decrypt
"docsgpt.agents.tool_executor.decrypt_credentials", _fake_decrypt
)
tool_data = {
@@ -1298,7 +1298,7 @@ class TestToolExecutorExecute:
mock_tool = Mock()
mock_tm.load_tool.return_value = mock_tool
monkeypatch.setattr(
"application.agents.tool_executor.ToolManager", lambda config: mock_tm
"docsgpt.agents.tool_executor.ToolManager", lambda config: mock_tm
)
tool_data = {
@@ -1334,7 +1334,7 @@ class TestToolExecutorAdditionalCoverage:
mock_tool.get_artifact_id.side_effect = RuntimeError("artifact error")
monkeypatch.setattr(
"application.agents.tool_executor.ToolManager",
"docsgpt.agents.tool_executor.ToolManager",
lambda config: Mock(load_tool=Mock(return_value=mock_tool)),
)
@@ -1375,7 +1375,7 @@ class TestToolExecutorAdditionalCoverage:
mock_tool = Mock()
mock_tm.load_tool.return_value = mock_tool
monkeypatch.setattr(
"application.agents.tool_executor.ToolManager", lambda config: mock_tm
"docsgpt.agents.tool_executor.ToolManager", lambda config: mock_tm
)
tool_data = {
@@ -11,8 +11,8 @@ from __future__ import annotations
import pytest
from application.agents.default_tools import default_tool_id
from application.agents.tool_executor import ToolExecutor
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.agents.tool_executor import ToolExecutor
SYNTHESIZED_ID = default_tool_id("code_executor")
STORED_ID = "9f1d4c2e-0000-4000-8000-000000000001"
@@ -14,7 +14,7 @@ from unittest.mock import Mock
import pytest
from application.agents.tool_executor import ToolExecutor
from docsgpt.agents.tool_executor import ToolExecutor
def _action(name):
+6 -6
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
from types import SimpleNamespace
from application.agents.tool_executor import ToolExecutor
from docsgpt.agents.tool_executor import ToolExecutor
def _call(name: str, args: dict | None = None, call_id: str = "c1"):
@@ -97,7 +97,7 @@ class TestNormalModeUnchanged:
# ---------------------------------------------------------------------------
class TestHeadlessSchedulerExclusion:
def test_synthesized_default_tools_drops_scheduler_in_headless(self):
from application.agents.default_tools import (
from docsgpt.agents.default_tools import (
loaded_default_tools,
synthesized_default_tools,
)
@@ -116,8 +116,8 @@ class TestHeadlessSchedulerExclusion:
def test_get_user_tools_filters_scheduler_when_headless(
self, monkeypatch,
):
from application.agents import tool_executor as te_module
from application.agents.default_tools import (
from docsgpt.agents import tool_executor as te_module
from docsgpt.agents.default_tools import (
default_tool_id,
loaded_default_tools,
)
@@ -164,8 +164,8 @@ class TestHeadlessSchedulerExclusion:
):
"""An agent-bound headless run (e.g. webhook) skips scheduler even if
the author added the synthetic id to ``agents.tools``."""
from application.agents import tool_executor as te_module
from application.agents.default_tools import default_tool_id
from docsgpt.agents import tool_executor as te_module
from docsgpt.agents.default_tools import default_tool_id
sched_id = default_tool_id("scheduler")
from contextlib import contextmanager
+1 -1
View File
@@ -7,7 +7,7 @@ source protects every lane at once.
import pytest
from application.agents.tool_executor import (
from docsgpt.agents.tool_executor import (
RESULT_FULL_MAX_CHARS,
bound_result_full,
sanitize_tool_result,
+24 -24
View File
@@ -12,7 +12,7 @@ from unittest.mock import Mock
import pytest
from sqlalchemy import text
from application.agents.tool_executor import ToolExecutor
from docsgpt.agents.tool_executor import ToolExecutor
@contextmanager
@@ -38,10 +38,10 @@ def _patch_db(monkeypatch, pg_conn):
yield pg_conn
targets = (
"application.agents.tool_executor",
"application.agents.tools.notes",
"application.agents.tools.todo_list",
"application.storage.db.session",
"docsgpt.agents.tool_executor",
"docsgpt.agents.tools.notes",
"docsgpt.agents.tools.todo_list",
"docsgpt.storage.db.session",
)
for module in targets:
monkeypatch.setattr(f"{module}.db_session", _use_pg, raising=False)
@@ -94,7 +94,7 @@ class TestExecuteJournaling:
"""No reserved message (``save_conversation=False``) → row lands ``confirmed``, not ``executed``."""
executor = ToolExecutor(user="u")
monkeypatch.setattr(
"application.agents.tool_executor.ToolActionParser",
"docsgpt.agents.tool_executor.ToolActionParser",
lambda _cls, **kw: Mock(
parse_args=Mock(return_value=("t1", "test_action", {"q": "v"}))
),
@@ -119,7 +119,7 @@ class TestExecuteJournaling:
self, pg_conn, mock_tool_manager, monkeypatch
):
"""The executor's message_id is carried onto the journal row, which stays ``executed``."""
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -138,7 +138,7 @@ class TestExecuteJournaling:
executor = ToolExecutor(user="u")
executor.message_id = message_uuid
monkeypatch.setattr(
"application.agents.tool_executor.ToolActionParser",
"docsgpt.agents.tool_executor.ToolActionParser",
lambda _cls, **kw: Mock(
parse_args=Mock(return_value=("t1", "test_action", {}))
),
@@ -163,7 +163,7 @@ class TestExecuteJournaling:
that reuses a deterministic call id (``functions.create_artifact:0``)
across turns must journal a distinct row per turn, not silently drop the
later one on the table-wide ``call_id`` primary key."""
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -178,7 +178,7 @@ class TestExecuteJournaling:
request_id="req-c2", status="pending",
)
monkeypatch.setattr(
"application.agents.tool_executor.ToolActionParser",
"docsgpt.agents.tool_executor.ToolActionParser",
lambda _cls, **kw: Mock(
parse_args=Mock(return_value=("t1", "test_action", {}))
),
@@ -206,7 +206,7 @@ class TestExecuteJournaling:
):
executor = ToolExecutor(user="u")
monkeypatch.setattr(
"application.agents.tool_executor.ToolActionParser",
"docsgpt.agents.tool_executor.ToolActionParser",
lambda _cls, **kw: Mock(
parse_args=Mock(return_value=("t1", "test_action", {}))
),
@@ -229,7 +229,7 @@ class TestExecuteJournaling:
@pytest.mark.unit
class TestRepository:
def test_proposed_then_confirmed_when_no_message(self, pg_conn):
from application.storage.db.repositories.tool_call_attempts import (
from docsgpt.storage.db.repositories.tool_call_attempts import (
ToolCallAttemptsRepository,
)
@@ -247,10 +247,10 @@ class TestRepository:
assert row["result"] == {"result": {"out": "ok"}}
def test_mark_executed_with_message_stays_executed(self, pg_conn):
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
from application.storage.db.repositories.tool_call_attempts import (
from docsgpt.storage.db.repositories.tool_call_attempts import (
ToolCallAttemptsRepository,
)
@@ -277,7 +277,7 @@ class TestRepository:
def test_upsert_executed_without_message_confirms(self, pg_conn):
"""``upsert_executed`` (DB-outage fallback) with no ``message_id`` lands ``confirmed``."""
from application.storage.db.repositories.tool_call_attempts import (
from docsgpt.storage.db.repositories.tool_call_attempts import (
ToolCallAttemptsRepository,
)
@@ -289,7 +289,7 @@ class TestRepository:
assert row["result"] == {"result": {"out": "ok"}}
def test_mark_failed_sets_error(self, pg_conn):
from application.storage.db.repositories.tool_call_attempts import (
from docsgpt.storage.db.repositories.tool_call_attempts import (
ToolCallAttemptsRepository,
)
@@ -303,7 +303,7 @@ class TestRepository:
def test_mark_failed_leaves_executed_row_untouched(self, pg_conn):
"""A late error for a reused ``call_id`` ("call_0"-style) must
not flip an already-executed row (see ``mark_failed``)."""
from application.storage.db.repositories.tool_call_attempts import (
from docsgpt.storage.db.repositories.tool_call_attempts import (
ToolCallAttemptsRepository,
)
@@ -320,7 +320,7 @@ class TestRepository:
def test_mark_executed_guarded_to_proposed(self, pg_conn):
"""A reused ``call_id`` must not flip an already-terminal row back
to executed (the status guard mirrors ``mark_failed``)."""
from application.storage.db.repositories.tool_call_attempts import (
from docsgpt.storage.db.repositories.tool_call_attempts import (
ToolCallAttemptsRepository,
)
@@ -336,7 +336,7 @@ class TestRepository:
def test_mark_executed_scoped_to_owner(self, pg_conn):
"""A colliding ``call_id`` from another tenant can't flip — or
read its result into — this user's proposed row."""
from application.storage.db.repositories.tool_call_attempts import (
from docsgpt.storage.db.repositories.tool_call_attempts import (
ToolCallAttemptsRepository,
)
@@ -353,7 +353,7 @@ class TestRepository:
assert row["user_id"] == "victim"
def test_mark_failed_scoped_to_owner(self, pg_conn):
from application.storage.db.repositories.tool_call_attempts import (
from docsgpt.storage.db.repositories.tool_call_attempts import (
ToolCallAttemptsRepository,
)
@@ -368,7 +368,7 @@ class TestRepository:
visible to per-user / per-agent analytics (was born unattributed)."""
import uuid as _uuid
from application.storage.db.repositories.tool_call_attempts import (
from docsgpt.storage.db.repositories.tool_call_attempts import (
ToolCallAttemptsRepository,
)
@@ -385,7 +385,7 @@ class TestRepository:
def test_upsert_executed_wont_clobber_other_tenant(self, pg_conn):
"""A colliding fallback upsert must not upgrade another tenant's
proposed row or overwrite its result."""
from application.storage.db.repositories.tool_call_attempts import (
from docsgpt.storage.db.repositories.tool_call_attempts import (
ToolCallAttemptsRepository,
)
@@ -407,7 +407,7 @@ class TestDefaultToolJournaling:
def test_synthetic_tool_id_is_journaled(
self, pg_conn, mock_tool_manager, monkeypatch
):
from application.agents.default_tools import synthesize_default_tool
from docsgpt.agents.default_tools import synthesize_default_tool
memory_row = synthesize_default_tool("memory")
assert memory_row is not None
@@ -415,7 +415,7 @@ class TestDefaultToolJournaling:
executor = ToolExecutor(user="u")
monkeypatch.setattr(
"application.agents.tool_executor.ToolActionParser",
"docsgpt.agents.tool_executor.ToolActionParser",
lambda _cls, **kw: Mock(
parse_args=Mock(
return_value=(memory_row["id"], "memory_view", {"path": "/"})
+16 -16
View File
@@ -1,8 +1,8 @@
from unittest.mock import Mock, patch
import pytest
from application.agents.tools.base import Tool
from application.agents.tools.tool_manager import ToolManager
from docsgpt.agents.tools.base import Tool
from docsgpt.agents.tools.tool_manager import ToolManager
class MockTool(Tool):
@@ -22,7 +22,7 @@ class MockTool(Tool):
@pytest.mark.unit
class TestToolManager:
@patch("application.agents.tools.tool_manager.pkgutil.iter_modules")
@patch("docsgpt.agents.tools.tool_manager.pkgutil.iter_modules")
def test_tool_manager_initialization(self, mock_iter):
mock_iter.return_value = []
@@ -32,8 +32,8 @@ class TestToolManager:
assert manager.config == config
assert isinstance(manager.tools, dict)
@patch("application.agents.tools.tool_manager.pkgutil.iter_modules")
@patch("application.agents.tools.tool_manager.importlib.import_module")
@patch("docsgpt.agents.tools.tool_manager.pkgutil.iter_modules")
@patch("docsgpt.agents.tools.tool_manager.importlib.import_module")
def test_load_tools_skips_base_and_private(self, mock_import, mock_iter):
mock_iter.return_value = [
(None, "base", False),
@@ -51,7 +51,7 @@ class TestToolManager:
assert "base" not in manager.tools
assert "__init__" not in manager.tools
@patch("application.agents.tools.tool_manager.pkgutil.iter_modules")
@patch("docsgpt.agents.tools.tool_manager.pkgutil.iter_modules")
def test_load_tools_creates_tool_instances(self, mock_iter):
mock_iter.return_value = []
@@ -66,7 +66,7 @@ class TestToolManager:
def test_load_tool_with_user_id(self):
with patch(
"application.agents.tools.tool_manager.pkgutil.iter_modules",
"docsgpt.agents.tools.tool_manager.pkgutil.iter_modules",
return_value=[],
):
manager = ToolManager({})
@@ -85,7 +85,7 @@ class TestToolManager:
assert hasattr(tool, "execute_action")
assert hasattr(tool, "get_actions_metadata")
@patch("application.agents.tools.tool_manager.pkgutil.iter_modules")
@patch("docsgpt.agents.tools.tool_manager.pkgutil.iter_modules")
def test_load_tool_updates_config(self, mock_iter):
mock_iter.return_value = []
@@ -97,8 +97,8 @@ class TestToolManager:
assert manager.config["test_tool"] == new_config
assert "test_tool" in manager.config
@patch("application.agents.tools.tool_manager.pkgutil.iter_modules")
@patch("application.agents.tools.tool_manager.importlib.import_module")
@patch("docsgpt.agents.tools.tool_manager.pkgutil.iter_modules")
@patch("docsgpt.agents.tools.tool_manager.importlib.import_module")
def test_execute_action_on_loaded_tool(self, mock_import, mock_iter):
mock_iter.return_value = [(None, "mock_tool", False)]
@@ -118,14 +118,14 @@ class TestToolManager:
def test_execute_action_tool_not_loaded(self):
with patch(
"application.agents.tools.tool_manager.pkgutil.iter_modules",
"docsgpt.agents.tools.tool_manager.pkgutil.iter_modules",
return_value=[],
):
manager = ToolManager({})
with pytest.raises(ValueError, match="Tool 'nonexistent' not loaded"):
manager.execute_action("nonexistent", "action")
@patch("application.agents.tools.tool_manager.importlib.import_module")
@patch("docsgpt.agents.tools.tool_manager.importlib.import_module")
def test_execute_action_with_user_id_for_mcp_tool(self, mock_import):
mock_tool = MockTool({})
@@ -141,7 +141,7 @@ class TestToolManager:
mock_load.assert_called_once_with("mcp_tool", {}, "user123")
@patch("application.agents.tools.tool_manager.importlib.import_module")
@patch("docsgpt.agents.tools.tool_manager.importlib.import_module")
def test_execute_action_with_user_id_for_memory_tool(self, mock_import):
mock_tool = MockTool({})
@@ -157,8 +157,8 @@ class TestToolManager:
mock_load.assert_called_once_with("memory", {}, "user456")
@patch("application.agents.tools.tool_manager.pkgutil.iter_modules")
@patch("application.agents.tools.tool_manager.importlib.import_module")
@patch("docsgpt.agents.tools.tool_manager.pkgutil.iter_modules")
@patch("docsgpt.agents.tools.tool_manager.importlib.import_module")
def test_get_all_actions_metadata(self, mock_import, mock_iter):
mock_iter.return_value = [(None, "tool1", False), (None, "tool2", False)]
@@ -177,7 +177,7 @@ class TestToolManager:
assert {"name": "action1"} in metadata
assert {"name": "action2"} in metadata
@patch("application.agents.tools.tool_manager.pkgutil.iter_modules")
@patch("docsgpt.agents.tools.tool_manager.pkgutil.iter_modules")
def test_get_all_actions_metadata_empty(self, mock_iter):
mock_iter.return_value = []
+17 -17
View File
@@ -1,4 +1,4 @@
"""Tests for application/agents/workflow_agent.py graph loading and saving.
"""Tests for docsgpt/agents/workflow_agent.py graph loading and saving.
Tests _parse_embedded_workflow, _load_from_database, and _finalize_workflow_run
against the ephemeral ``pg_conn`` fixture. Agent construction is bypassed via
@@ -17,7 +17,7 @@ import pytest
def _make_agent(*, workflow_id=None, workflow=None, workflow_owner=None,
decoded_token=None):
"""Construct a WorkflowAgent bypassing BaseAgent.__init__."""
from application.agents.workflow_agent import WorkflowAgent
from docsgpt.agents.workflow_agent import WorkflowAgent
agent = WorkflowAgent.__new__(WorkflowAgent)
agent.workflow_id = workflow_id
agent.workflow_owner = workflow_owner
@@ -36,9 +36,9 @@ def _patch_db(conn):
yield conn
with patch(
"application.agents.workflow_agent.db_readonly", _yield
"docsgpt.agents.workflow_agent.db_readonly", _yield
), patch(
"application.agents.workflow_agent.db_session", _yield
"docsgpt.agents.workflow_agent.db_session", _yield
):
yield
@@ -111,7 +111,7 @@ class TestLoadWorkflowGraph:
assert got is not None
def test_uses_database_when_id_set(self, pg_conn):
from application.storage.db.repositories.workflows import (
from docsgpt.storage.db.repositories.workflows import (
WorkflowsRepository,
)
user = "u-loadwf"
@@ -135,7 +135,7 @@ class TestLoadFromDatabase:
assert agent._load_from_database() is None
def test_owner_from_decoded_token(self, pg_conn):
from application.storage.db.repositories.workflows import (
from docsgpt.storage.db.repositories.workflows import (
WorkflowsRepository,
)
user = "u-token-owner"
@@ -159,7 +159,7 @@ class TestLoadFromDatabase:
def test_invalid_version_falls_back_to_1(self, pg_conn):
"""When current_graph_version is invalid it falls back to 1."""
from application.storage.db.repositories.workflows import (
from docsgpt.storage.db.repositories.workflows import (
WorkflowsRepository,
)
user = "u-bad-version"
@@ -179,7 +179,7 @@ class TestLoadFromDatabase:
agent = _make_agent(workflow_id="x", workflow_owner="u")
with patch(
"application.agents.workflow_agent.db_readonly", _broken
"docsgpt.agents.workflow_agent.db_readonly", _broken
):
got = agent._load_from_database()
assert got is None
@@ -214,10 +214,10 @@ class TestSaveWorkflowRun:
agent._finalize_workflow_run(agent.workflow_owner, agent.workflow_owner, None, "q")
def test_creates_run_row(self, pg_conn):
from application.storage.db.repositories.workflows import (
from docsgpt.storage.db.repositories.workflows import (
WorkflowsRepository,
)
from application.storage.db.repositories.workflow_runs import (
from docsgpt.storage.db.repositories.workflow_runs import (
WorkflowRunsRepository,
)
@@ -259,7 +259,7 @@ class TestSaveWorkflowRun:
yield
with patch(
"application.agents.workflow_agent.db_session", _broken
"docsgpt.agents.workflow_agent.db_session", _broken
):
# Should not raise
agent._finalize_workflow_run(agent.workflow_owner, agent.workflow_owner, None, "q")
@@ -267,13 +267,13 @@ class TestSaveWorkflowRun:
class TestDetermineRunStatus:
def test_completed_when_no_engine(self):
from application.agents.workflows.schemas import ExecutionStatus
from docsgpt.agents.workflows.schemas import ExecutionStatus
agent = _make_agent()
assert agent._determine_run_status() == ExecutionStatus.COMPLETED
def test_completed_when_log_empty(self):
from application.agents.workflows.schemas import ExecutionStatus
from docsgpt.agents.workflows.schemas import ExecutionStatus
agent = _make_agent()
agent._engine = MagicMock()
@@ -281,7 +281,7 @@ class TestDetermineRunStatus:
assert agent._determine_run_status() == ExecutionStatus.COMPLETED
def test_failed_if_any_log_failed(self):
from application.agents.workflows.schemas import ExecutionStatus
from docsgpt.agents.workflows.schemas import ExecutionStatus
agent = _make_agent()
agent._engine = MagicMock()
@@ -343,14 +343,14 @@ class TestAgentNodeApprovalPause:
"""A node agent whose tool pauses for approval must fail the node visibly, not emit empty output."""
def test_tool_calls_pending_raises_clear_error(self, monkeypatch):
from application.agents.workflows import workflow_engine as we
from application.agents.workflows.schemas import (
from docsgpt.agents.workflows import workflow_engine as we
from docsgpt.agents.workflows.schemas import (
NodeType,
Workflow,
WorkflowGraph,
WorkflowNode,
)
from application.agents.workflows.workflow_engine import WorkflowEngine
from docsgpt.agents.workflows.workflow_engine import WorkflowEngine
# An ephemeral node agent whose LLM handler yields the pause signal and ends,
# emitting no "answer". Previously the engine dropped it and the node completed
+4 -4
View File
@@ -39,8 +39,8 @@ def _make_agent(**overrides):
"json_schema": None,
}
defaults.update(overrides)
with patch("application.agents.workflow_agent.log_activity", lambda **kw: lambda f: f):
from application.agents.workflow_agent import WorkflowAgent
with patch("docsgpt.agents.workflow_agent.log_activity", lambda **kw: lambda f: f):
from docsgpt.agents.workflow_agent import WorkflowAgent
agent = WorkflowAgent(**defaults)
return agent
@@ -114,7 +114,7 @@ class TestDetermineRunStatusPg:
def test_completed_value_matches_pg_enum(self):
"""The string stored in Postgres must match ExecutionStatus.COMPLETED.value."""
from application.agents.workflows.schemas import ExecutionStatus
from docsgpt.agents.workflows.schemas import ExecutionStatus
agent = _make_agent()
agent._engine = MagicMock()
@@ -125,7 +125,7 @@ class TestDetermineRunStatusPg:
assert status.value == "completed"
def test_failed_value_matches_pg_enum(self):
from application.agents.workflows.schemas import ExecutionStatus
from docsgpt.agents.workflows.schemas import ExecutionStatus
agent = _make_agent()
agent._engine = MagicMock()
+21 -21
View File
@@ -7,16 +7,16 @@ from unittest.mock import MagicMock
import pytest
from application.agents.agentic_agent import AgenticAgent
from application.agents.classic_agent import ClassicAgent
from application.agents.research_agent import ResearchAgent
from application.agents.workflows.node_agent import (
from docsgpt.agents.agentic_agent import AgenticAgent
from docsgpt.agents.classic_agent import ClassicAgent
from docsgpt.agents.research_agent import ResearchAgent
from docsgpt.agents.workflows.node_agent import (
WorkflowNodeAgenticAgent,
WorkflowNodeAgentFactory,
WorkflowNodeClassicAgent,
WorkflowNodeResearchAgent,
)
from application.agents.workflows.schemas import (
from docsgpt.agents.workflows.schemas import (
AgentNodeConfig,
AgentType,
NodeType,
@@ -24,7 +24,7 @@ from application.agents.workflows.schemas import (
WorkflowGraph,
WorkflowNode,
)
from application.agents.workflows.workflow_engine import WorkflowEngine
from docsgpt.agents.workflows.workflow_engine import WorkflowEngine
# ---------------------------------------------------------------------------
@@ -205,7 +205,7 @@ class TestWorkflowEngineAgenticNode:
staticmethod(capture_create),
)
monkeypatch.setattr(
"application.core.model_utils.get_api_key_for_provider",
"docsgpt.core.model_utils.get_api_key_for_provider",
lambda _provider: None,
)
@@ -243,7 +243,7 @@ class TestWorkflowEngineAgenticNode:
staticmethod(capture_create),
)
monkeypatch.setattr(
"application.core.model_utils.get_api_key_for_provider",
"docsgpt.core.model_utils.get_api_key_for_provider",
lambda _provider: None,
)
@@ -276,7 +276,7 @@ class TestWorkflowEngineAgenticNode:
staticmethod(capture_create),
)
monkeypatch.setattr(
"application.core.model_utils.get_api_key_for_provider",
"docsgpt.core.model_utils.get_api_key_for_provider",
lambda _provider: None,
)
@@ -309,7 +309,7 @@ class TestWorkflowEngineResearchNode:
staticmethod(lambda **kwargs: StubNodeAgent(node_events)),
)
monkeypatch.setattr(
"application.core.model_utils.get_api_key_for_provider",
"docsgpt.core.model_utils.get_api_key_for_provider",
lambda _provider: None,
)
@@ -346,7 +346,7 @@ class TestWorkflowEngineResearchNode:
staticmethod(capture_create),
)
monkeypatch.setattr(
"application.core.model_utils.get_api_key_for_provider",
"docsgpt.core.model_utils.get_api_key_for_provider",
lambda _provider: None,
)
@@ -375,7 +375,7 @@ class TestWorkflowEngineResearchNode:
staticmethod(lambda **kwargs: StubNodeAgent(node_events)),
)
monkeypatch.setattr(
"application.core.model_utils.get_api_key_for_provider",
"docsgpt.core.model_utils.get_api_key_for_provider",
lambda _provider: None,
)
@@ -414,7 +414,7 @@ class TestWorkflowEngineClassicNodeNoRetrieverConfig:
staticmethod(capture_create),
)
monkeypatch.setattr(
"application.core.model_utils.get_api_key_for_provider",
"docsgpt.core.model_utils.get_api_key_for_provider",
lambda _provider: None,
)
@@ -448,7 +448,7 @@ class TestWorkflowEngineStreamingEvents:
staticmethod(lambda **kwargs: StubNodeAgent(node_events)),
)
monkeypatch.setattr(
"application.core.model_utils.get_api_key_for_provider",
"docsgpt.core.model_utils.get_api_key_for_provider",
lambda _provider: None,
)
@@ -475,7 +475,7 @@ class TestWorkflowEngineStreamingEvents:
staticmethod(lambda **kwargs: StubNodeAgent(node_events)),
)
monkeypatch.setattr(
"application.core.model_utils.get_api_key_for_provider",
"docsgpt.core.model_utils.get_api_key_for_provider",
lambda _provider: None,
)
@@ -510,7 +510,7 @@ class TestWorkflowNodeSourceAuthorization:
"""``_authorized_node_sources`` opens a connection; don't need a real one."""
import contextlib
import application.storage.db.session as session
import docsgpt.storage.db.session as session
@contextlib.contextmanager
def _conn():
@@ -519,7 +519,7 @@ class TestWorkflowNodeSourceAuthorization:
monkeypatch.setattr(session, "db_readonly", _conn)
def test_owner_sources_survive(self, monkeypatch):
import application.api.user.team_sharing as ts
import docsgpt.api.user.team_sharing as ts
self._stub_db(monkeypatch)
monkeypatch.setattr(ts, "can_access", lambda *a, **k: True)
@@ -527,7 +527,7 @@ class TestWorkflowNodeSourceAuthorization:
assert engine._authorized_node_sources(["s1", "s2"]) == ["s1", "s2"]
def test_foreign_sources_are_dropped(self, monkeypatch):
import application.api.user.team_sharing as ts
import docsgpt.api.user.team_sharing as ts
self._stub_db(monkeypatch)
monkeypatch.setattr(ts, "can_access", lambda conn, k, sid, u: sid == "mine")
@@ -542,7 +542,7 @@ class TestWorkflowNodeSourceAuthorization:
assert engine._authorized_node_sources(["s1"]) == []
def test_authorization_error_fails_closed(self, monkeypatch):
import application.api.user.team_sharing as ts
import docsgpt.api.user.team_sharing as ts
def _boom(*a, **k):
raise RuntimeError("db down")
@@ -576,7 +576,7 @@ class TestWorkflowNodeDocumentsReachTheAgent:
return docs
monkeypatch.setattr(
"application.retriever.retriever_creator.RetrieverCreator.create_retriever",
"docsgpt.retriever.retriever_creator.RetrieverCreator.create_retriever",
lambda *a, **k: _R(),
)
monkeypatch.setattr(
@@ -599,7 +599,7 @@ class TestWorkflowNodeDocumentsReachTheAgent:
raise RuntimeError("vector store down")
monkeypatch.setattr(
"application.retriever.retriever_creator.RetrieverCreator.create_retriever",
"docsgpt.retriever.retriever_creator.RetrieverCreator.create_retriever",
_boom,
)
monkeypatch.setattr(
+21 -21
View File
@@ -10,16 +10,16 @@ from types import SimpleNamespace
import pytest
from application.agents.workflow_agent import WorkflowAgent
from application.agents.workflows.cel_evaluator import evaluate_cel
from application.agents.workflows.schemas import (
from docsgpt.agents.workflow_agent import WorkflowAgent
from docsgpt.agents.workflows.cel_evaluator import evaluate_cel
from docsgpt.agents.workflows.schemas import (
NodeType,
Workflow,
WorkflowEdge,
WorkflowGraph,
WorkflowNode,
)
from application.agents.workflows.workflow_engine import WorkflowEngine
from docsgpt.agents.workflows.workflow_engine import WorkflowEngine
def _engine() -> WorkflowEngine:
@@ -118,14 +118,14 @@ def patch_sandbox(monkeypatch):
return list(state["captured"])
monkeypatch.setattr(
"application.sandbox.sandbox_creator.SandboxCreator.get_manager", _get_manager
"docsgpt.sandbox.sandbox_creator.SandboxCreator.get_manager", _get_manager
)
monkeypatch.setattr(
"application.sandbox.artifacts_capture.snapshot_signatures",
"docsgpt.sandbox.artifacts_capture.snapshot_signatures",
lambda *a, **k: state.__setitem__("snapshot_calls", state["snapshot_calls"] + 1) or {},
)
monkeypatch.setattr(
"application.sandbox.artifacts_capture.capture_artifacts", _capture
"docsgpt.sandbox.artifacts_capture.capture_artifacts", _capture
)
state["manager_holder"] = manager_holder
return state
@@ -206,16 +206,16 @@ def test_execute_closes_run_session_once_at_end(monkeypatch):
"""The run-scoped sandbox session opened by a code node is closed once when execute() ends."""
manager = _FakeManager(_Result(ok=True, stdout="ok"))
monkeypatch.setattr(
"application.sandbox.sandbox_creator.SandboxCreator.get_manager", lambda: manager
"docsgpt.sandbox.sandbox_creator.SandboxCreator.get_manager", lambda: manager
)
monkeypatch.setattr(
"application.sandbox.sandbox_creator.SandboxCreator.peek_manager", lambda: manager
"docsgpt.sandbox.sandbox_creator.SandboxCreator.peek_manager", lambda: manager
)
monkeypatch.setattr(
"application.sandbox.artifacts_capture.snapshot_signatures", lambda *a, **k: {}
"docsgpt.sandbox.artifacts_capture.snapshot_signatures", lambda *a, **k: {}
)
monkeypatch.setattr(
"application.sandbox.artifacts_capture.capture_artifacts", lambda *a, **k: []
"docsgpt.sandbox.artifacts_capture.capture_artifacts", lambda *a, **k: []
)
start = WorkflowNode(
@@ -354,7 +354,7 @@ def test_code_node_json_schema_merges_artifact_reference(patch_sandbox):
def test_code_node_timeout_clamped_to_cap(patch_sandbox, monkeypatch):
from application.core.settings import settings
from docsgpt.core.settings import settings
monkeypatch.setattr(settings, "SANDBOX_EXEC_TIMEOUT", 30, raising=False)
engine = _engine()
@@ -378,11 +378,11 @@ def test_materialize_code_inputs_rejects_oversize(monkeypatch):
"""A code-node input whose declared version ``size`` exceeds the cap raises before staging."""
from contextlib import contextmanager
from application.core.settings import settings
from docsgpt.core.settings import settings
monkeypatch.setattr(settings, "SANDBOX_MAX_INPUT_BYTES", 100, raising=False)
monkeypatch.setattr(
"application.agents.tools.artifact_ref.resolve_artifact_id",
"docsgpt.agents.tools.artifact_ref.resolve_artifact_id",
lambda repo, raw, **k: str(raw),
)
@@ -405,11 +405,11 @@ def test_materialize_code_inputs_rejects_oversize(monkeypatch):
raise AssertionError("bytes must not be read when declared size exceeds the cap")
monkeypatch.setattr(
"application.storage.db.repositories.artifacts.ArtifactsRepository", _Repo
"docsgpt.storage.db.repositories.artifacts.ArtifactsRepository", _Repo
)
monkeypatch.setattr("application.storage.db.session.db_readonly", _readonly)
monkeypatch.setattr("docsgpt.storage.db.session.db_readonly", _readonly)
monkeypatch.setattr(
"application.storage.storage_creator.StorageCreator.get_storage",
"docsgpt.storage.storage_creator.StorageCreator.get_storage",
staticmethod(lambda: _Storage()),
)
@@ -425,7 +425,7 @@ def test_materialize_code_inputs_dedupes_same_filename(monkeypatch):
from contextlib import contextmanager
monkeypatch.setattr(
"application.agents.tools.artifact_ref.resolve_artifact_id",
"docsgpt.agents.tools.artifact_ref.resolve_artifact_id",
lambda repo, raw, **k: str(raw),
)
@@ -451,11 +451,11 @@ def test_materialize_code_inputs_dedupes_same_filename(monkeypatch):
return io.BytesIO(path.encode())
monkeypatch.setattr(
"application.storage.db.repositories.artifacts.ArtifactsRepository", _Repo
"docsgpt.storage.db.repositories.artifacts.ArtifactsRepository", _Repo
)
monkeypatch.setattr("application.storage.db.session.db_readonly", _readonly)
monkeypatch.setattr("docsgpt.storage.db.session.db_readonly", _readonly)
monkeypatch.setattr(
"application.storage.storage_creator.StorageCreator.get_storage",
"docsgpt.storage.storage_creator.StorageCreator.get_storage",
staticmethod(lambda: _Storage()),
)
+31 -31
View File
@@ -3,16 +3,16 @@ from typing import Any, Dict, Optional
import pytest
from application.api.user.workflows import routes as workflow_routes
from application.agents.workflows.node_agent import WorkflowNodeAgentFactory
from application.agents.workflows.schemas import (
from docsgpt.api.user.workflows import routes as workflow_routes
from docsgpt.agents.workflows.node_agent import WorkflowNodeAgentFactory
from docsgpt.agents.workflows.schemas import (
NodeType,
Workflow,
WorkflowGraph,
WorkflowNode,
)
from application.agents.workflows.workflow_engine import WorkflowEngine
from application.api.user.workflows.routes import validate_workflow_structure
from docsgpt.agents.workflows.workflow_engine import WorkflowEngine
from docsgpt.api.user.workflows.routes import validate_workflow_structure
class StubNodeAgent:
@@ -81,7 +81,7 @@ def test_execute_agent_node_saves_structured_output_as_json(monkeypatch):
staticmethod(lambda **kwargs: StubNodeAgent(node_events)),
)
monkeypatch.setattr(
"application.core.model_utils.get_api_key_for_provider",
"docsgpt.core.model_utils.get_api_key_for_provider",
lambda _provider: None,
)
@@ -111,11 +111,11 @@ def test_execute_agent_node_normalizes_wrapped_schema_before_agent_create(monkey
staticmethod(create_node_agent),
)
monkeypatch.setattr(
"application.core.model_utils.get_api_key_for_provider",
"docsgpt.core.model_utils.get_api_key_for_provider",
lambda _provider: None,
)
monkeypatch.setattr(
"application.core.model_utils.get_model_capabilities",
"docsgpt.core.model_utils.get_model_capabilities",
lambda _model_id, **_kwargs: {"supports_structured_output": True},
)
@@ -136,7 +136,7 @@ def test_execute_agent_node_falls_back_to_text_when_schema_not_configured(monkey
staticmethod(lambda **kwargs: StubNodeAgent(node_events)),
)
monkeypatch.setattr(
"application.core.model_utils.get_api_key_for_provider",
"docsgpt.core.model_utils.get_api_key_for_provider",
lambda _provider: None,
)
@@ -381,11 +381,11 @@ def test_execute_agent_node_raises_when_structured_output_violates_schema(monkey
staticmethod(lambda **kwargs: StubNodeAgent(node_events)),
)
monkeypatch.setattr(
"application.core.model_utils.get_api_key_for_provider",
"docsgpt.core.model_utils.get_api_key_for_provider",
lambda _provider: None,
)
monkeypatch.setattr(
"application.core.model_utils.get_model_capabilities",
"docsgpt.core.model_utils.get_model_capabilities",
lambda _model_id, **_kwargs: {"supports_structured_output": True},
)
@@ -407,11 +407,11 @@ def test_execute_agent_node_raises_when_schema_set_and_response_not_json(monkeyp
staticmethod(lambda **kwargs: StubNodeAgent(node_events)),
)
monkeypatch.setattr(
"application.core.model_utils.get_api_key_for_provider",
"docsgpt.core.model_utils.get_api_key_for_provider",
lambda _provider: None,
)
monkeypatch.setattr(
"application.core.model_utils.get_model_capabilities",
"docsgpt.core.model_utils.get_model_capabilities",
lambda _model_id, **_kwargs: {"supports_structured_output": True},
)
@@ -445,15 +445,15 @@ class TestWorkflowEngineAdditionalCoverage:
staticmethod(lambda **kwargs: StubNodeAgent(node_events)),
)
monkeypatch.setattr(
"application.core.model_utils.get_api_key_for_provider",
"docsgpt.core.model_utils.get_api_key_for_provider",
lambda _: None,
)
monkeypatch.setattr(
"application.core.model_utils.get_provider_from_model_id",
"docsgpt.core.model_utils.get_provider_from_model_id",
lambda _, **_kwargs: None,
)
monkeypatch.setattr(
"application.core.model_utils.get_model_capabilities",
"docsgpt.core.model_utils.get_model_capabilities",
lambda _, **_kwargs: None,
)
@@ -475,15 +475,15 @@ class TestWorkflowEngineAdditionalCoverage:
staticmethod(lambda **kwargs: StubNodeAgent(node_events)),
)
monkeypatch.setattr(
"application.core.model_utils.get_api_key_for_provider",
"docsgpt.core.model_utils.get_api_key_for_provider",
lambda _: "key",
)
monkeypatch.setattr(
"application.core.model_utils.get_provider_from_model_id",
"docsgpt.core.model_utils.get_provider_from_model_id",
lambda _, **_kwargs: "openai",
)
monkeypatch.setattr(
"application.core.model_utils.get_model_capabilities",
"docsgpt.core.model_utils.get_model_capabilities",
lambda _, **_kwargs: None,
)
@@ -501,15 +501,15 @@ class TestWorkflowEngineAdditionalCoverage:
node.config["model_id"] = "model-no-struct"
monkeypatch.setattr(
"application.core.model_utils.get_api_key_for_provider",
"docsgpt.core.model_utils.get_api_key_for_provider",
lambda _: "key",
)
monkeypatch.setattr(
"application.core.model_utils.get_provider_from_model_id",
"docsgpt.core.model_utils.get_provider_from_model_id",
lambda _, **_kwargs: "openai",
)
monkeypatch.setattr(
"application.core.model_utils.get_model_capabilities",
"docsgpt.core.model_utils.get_model_capabilities",
lambda _, **_kwargs: {"supports_structured_output": False},
)
@@ -535,15 +535,15 @@ class TestWorkflowEngineAdditionalCoverage:
staticmethod(lambda **kwargs: StubNodeAgent(node_events)),
)
monkeypatch.setattr(
"application.core.model_utils.get_api_key_for_provider",
"docsgpt.core.model_utils.get_api_key_for_provider",
lambda _: None,
)
monkeypatch.setattr(
"application.core.model_utils.get_provider_from_model_id",
"docsgpt.core.model_utils.get_provider_from_model_id",
lambda _, **_kwargs: None,
)
monkeypatch.setattr(
"application.core.model_utils.get_model_capabilities",
"docsgpt.core.model_utils.get_model_capabilities",
lambda _, **_kwargs: {"supports_structured_output": True},
)
@@ -566,15 +566,15 @@ class TestWorkflowEngineAdditionalCoverage:
staticmethod(lambda **kwargs: StubNodeAgent(node_events)),
)
monkeypatch.setattr(
"application.core.model_utils.get_api_key_for_provider",
"docsgpt.core.model_utils.get_api_key_for_provider",
lambda _: None,
)
monkeypatch.setattr(
"application.core.model_utils.get_provider_from_model_id",
"docsgpt.core.model_utils.get_provider_from_model_id",
lambda _, **_kwargs: None,
)
monkeypatch.setattr(
"application.core.model_utils.get_model_capabilities",
"docsgpt.core.model_utils.get_model_capabilities",
lambda _, **_kwargs: {"supports_structured_output": True},
)
@@ -625,15 +625,15 @@ class TestAgentNodeProviderResolution:
WorkflowNodeAgentFactory, "create", staticmethod(_capture)
)
monkeypatch.setattr(
"application.core.model_utils.get_api_key_for_provider",
"docsgpt.core.model_utils.get_api_key_for_provider",
lambda name: f"key-for-{name}",
)
monkeypatch.setattr(
"application.core.model_utils.get_provider_from_model_id",
"docsgpt.core.model_utils.get_provider_from_model_id",
lambda _, **_kwargs: registry_provider,
)
monkeypatch.setattr(
"application.core.model_utils.get_model_capabilities",
"docsgpt.core.model_utils.get_model_capabilities",
lambda _, **_kwargs: None,
)
list(engine._execute_agent_node(node))
+29 -29
View File
@@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch
import pytest
from application.agents.workflows.schemas import (
from docsgpt.agents.workflows.schemas import (
ExecutionStatus,
NodeType,
WorkflowEdge,
@@ -14,7 +14,7 @@ from application.agents.workflows.schemas import (
WorkflowNode,
Workflow,
)
from application.agents.workflows.workflow_engine import WorkflowEngine
from docsgpt.agents.workflows.workflow_engine import WorkflowEngine
def _make_graph(nodes, edges):
@@ -402,8 +402,8 @@ class TestNormalizeNodeJsonSchema:
def test_invalid_schema_raises(self):
graph = _make_graph([], [])
engine = WorkflowEngine(graph, _make_agent())
with patch("application.agents.workflows.workflow_engine.normalize_json_schema_payload") as mock_norm:
from application.core.json_schema_utils import JsonSchemaValidationError
with patch("docsgpt.agents.workflows.workflow_engine.normalize_json_schema_payload") as mock_norm:
from docsgpt.core.json_schema_utils import JsonSchemaValidationError
mock_norm.side_effect = JsonSchemaValidationError("bad schema")
with pytest.raises(ValueError, match="Invalid JSON schema"):
engine._normalize_node_json_schema({"bad": True}, "TestNode")
@@ -430,7 +430,7 @@ class TestValidateStructuredOutput:
def test_no_jsonschema_module(self):
graph = _make_graph([], [])
engine = WorkflowEngine(graph, _make_agent())
with patch("application.agents.workflows.workflow_engine.jsonschema", None):
with patch("docsgpt.agents.workflows.workflow_engine.jsonschema", None):
engine._validate_structured_output({"type": "object"}, {}) # Should not raise
@@ -449,7 +449,7 @@ class TestFormatTemplate:
@pytest.mark.unit
def test_render_error_returns_raw(self):
from application.templates.template_engine import TemplateRenderError
from docsgpt.templates.template_engine import TemplateRenderError
graph = _make_graph([], [])
engine = WorkflowEngine(graph, _make_agent())
engine._build_template_context = MagicMock(return_value={})
@@ -631,18 +631,18 @@ class TestAgentNodeExecution:
mock_agent.gen.return_value = [{"answer": "response"}]
with patch(
"application.agents.workflows.workflow_engine.WorkflowNodeAgentFactory"
"docsgpt.agents.workflows.workflow_engine.WorkflowNodeAgentFactory"
) as mock_factory, \
patch(
"application.core.model_utils.get_provider_from_model_id",
"docsgpt.core.model_utils.get_provider_from_model_id",
return_value="openai",
), \
patch(
"application.core.model_utils.get_api_key_for_provider",
"docsgpt.core.model_utils.get_api_key_for_provider",
return_value="key",
), \
patch(
"application.core.model_utils.get_model_capabilities",
"docsgpt.core.model_utils.get_model_capabilities",
return_value=None,
):
mock_factory.create.return_value = mock_agent
@@ -674,18 +674,18 @@ class TestAgentNodeExecution:
]
with patch(
"application.agents.workflows.workflow_engine.WorkflowNodeAgentFactory"
"docsgpt.agents.workflows.workflow_engine.WorkflowNodeAgentFactory"
) as mock_factory, \
patch(
"application.core.model_utils.get_provider_from_model_id",
"docsgpt.core.model_utils.get_provider_from_model_id",
return_value="openai",
), \
patch(
"application.core.model_utils.get_api_key_for_provider",
"docsgpt.core.model_utils.get_api_key_for_provider",
return_value="key",
), \
patch(
"application.core.model_utils.get_model_capabilities",
"docsgpt.core.model_utils.get_model_capabilities",
return_value={"supports_structured_output": True},
):
mock_factory.create.return_value = mock_agent
@@ -712,15 +712,15 @@ class TestAgentNodeExecution:
engine.state = {"query": "test"}
with patch(
"application.core.model_utils.get_provider_from_model_id",
"docsgpt.core.model_utils.get_provider_from_model_id",
return_value="openai",
), \
patch(
"application.core.model_utils.get_api_key_for_provider",
"docsgpt.core.model_utils.get_api_key_for_provider",
return_value="key",
), \
patch(
"application.core.model_utils.get_model_capabilities",
"docsgpt.core.model_utils.get_model_capabilities",
return_value={"supports_structured_output": False},
):
with pytest.raises(ValueError, match="does not support structured output"):
@@ -744,18 +744,18 @@ class TestAgentNodeExecution:
mock_agent.gen.return_value = [{"answer": "output text"}]
with patch(
"application.agents.workflows.workflow_engine.WorkflowNodeAgentFactory"
"docsgpt.agents.workflows.workflow_engine.WorkflowNodeAgentFactory"
) as mock_factory, \
patch(
"application.core.model_utils.get_provider_from_model_id",
"docsgpt.core.model_utils.get_provider_from_model_id",
return_value="openai",
), \
patch(
"application.core.model_utils.get_api_key_for_provider",
"docsgpt.core.model_utils.get_api_key_for_provider",
return_value="key",
), \
patch(
"application.core.model_utils.get_model_capabilities",
"docsgpt.core.model_utils.get_model_capabilities",
return_value=None,
):
mock_factory.create.return_value = mock_agent
@@ -771,11 +771,11 @@ class TestAgentNodeExecution:
import jsonschema as js
with patch(
"application.agents.workflows.workflow_engine.normalize_json_schema_payload",
"docsgpt.agents.workflows.workflow_engine.normalize_json_schema_payload",
return_value={"type": "invalid_schema_type"},
), \
patch(
"application.agents.workflows.workflow_engine.jsonschema"
"docsgpt.agents.workflows.workflow_engine.jsonschema"
) as mock_js:
mock_js.validate.side_effect = js.exceptions.SchemaError("bad schema")
mock_js.exceptions = js.exceptions
@@ -1038,15 +1038,15 @@ class TestNodeDocumentManifest:
def fake_readonly():
yield object()
monkeypatch.setattr("application.storage.db.session.db_readonly", fake_readonly)
monkeypatch.setattr("docsgpt.storage.db.session.db_readonly", fake_readonly)
monkeypatch.setattr(
"application.storage.db.repositories.artifacts.ArtifactsRepository", FakeRepo
"docsgpt.storage.db.repositories.artifacts.ArtifactsRepository", FakeRepo
)
return engine
@pytest.mark.unit
def test_manifest_lists_refs_filenames_and_mimes(self, monkeypatch):
from application.agents.workflows.schemas import AgentNodeConfig
from docsgpt.agents.workflows.schemas import AgentNodeConfig
engine = self._engine_with_docs(monkeypatch)
config = AgentNodeConfig(agent_type="classic", input_documents=["*"])
@@ -1058,7 +1058,7 @@ class TestNodeDocumentManifest:
@pytest.mark.unit
def test_manifest_empty_without_selection(self, monkeypatch):
from application.agents.workflows.schemas import AgentNodeConfig
from docsgpt.agents.workflows.schemas import AgentNodeConfig
engine = self._engine_with_docs(monkeypatch)
config = AgentNodeConfig(agent_type="classic", input_documents=[])
@@ -1068,7 +1068,7 @@ class TestNodeDocumentManifest:
def test_manifest_failure_never_breaks_the_node(self, monkeypatch):
from contextlib import contextmanager
from application.agents.workflows.schemas import AgentNodeConfig
from docsgpt.agents.workflows.schemas import AgentNodeConfig
engine = WorkflowEngine(_make_graph([], []), _make_agent())
engine.state["input_documents"] = [{"artifact_id": self._ART_1}]
@@ -1078,6 +1078,6 @@ class TestNodeDocumentManifest:
raise RuntimeError("db down")
yield
monkeypatch.setattr("application.storage.db.session.db_readonly", broken_readonly)
monkeypatch.setattr("docsgpt.storage.db.session.db_readonly", broken_readonly)
config = AgentNodeConfig(agent_type="classic", input_documents=["*"])
assert engine._node_document_manifest(config) == ""
@@ -2,7 +2,7 @@
from __future__ import annotations
from application.agents.workflows.workflow_engine import WorkflowEngine
from docsgpt.agents.workflows.workflow_engine import WorkflowEngine
def _engine() -> WorkflowEngine:
+24 -24
View File
@@ -15,16 +15,16 @@ import uuid
import pytest
from sqlalchemy import text
from application.agents.workflow_agent import WorkflowAgent, _MAX_INPUT_DOCUMENTS
from application.agents.workflows.schemas import AgentNodeConfig
from application.agents.workflows.workflow_engine import (
from docsgpt.agents.workflow_agent import WorkflowAgent, _MAX_INPUT_DOCUMENTS
from docsgpt.agents.workflows.schemas import AgentNodeConfig
from docsgpt.agents.workflows.workflow_engine import (
_EXTRACT_TRUNCATION_ID,
WorkflowEngine,
)
from application.storage.db.repositories.artifacts import ArtifactsRepository
from application.storage.db.repositories.workflow_runs import WorkflowRunsRepository
from application.storage.local import LocalStorage
from application.storage.storage_creator import StorageCreator
from docsgpt.storage.db.repositories.artifacts import ArtifactsRepository
from docsgpt.storage.db.repositories.workflow_runs import WorkflowRunsRepository
from docsgpt.storage.local import LocalStorage
from docsgpt.storage.storage_creator import StorageCreator
pytestmark = pytest.mark.integration
@@ -37,7 +37,7 @@ def _wire(pg_engine, tmp_path, monkeypatch) -> LocalStorage:
"""Point storage + the db session at the ephemeral fixtures."""
storage = LocalStorage(base_dir=str(tmp_path))
monkeypatch.setattr(StorageCreator, "_instance", storage, raising=False)
monkeypatch.setattr("application.storage.db.session.get_engine", lambda: pg_engine)
monkeypatch.setattr("docsgpt.storage.db.session.get_engine", lambda: pg_engine)
return storage
@@ -120,7 +120,7 @@ def _patch_engine(monkeypatch, probe=None) -> None:
_RecordingEngine.instances = []
_RecordingEngine.probe = probe
monkeypatch.setattr(
"application.agents.workflow_agent.WorkflowEngine", _RecordingEngine
"docsgpt.agents.workflow_agent.WorkflowEngine", _RecordingEngine
)
@@ -228,7 +228,7 @@ def test_shared_agent_run_and_artifacts_owned_by_caller(pg_engine, tmp_path, mon
# share token, so it needs a request context.
from flask import Flask
from application.api.user.artifacts.authz import Principal, authorize_artifact
from docsgpt.api.user.artifacts.authz import Principal, authorize_artifact
app = Flask(__name__)
with app.test_request_context():
@@ -366,13 +366,13 @@ def test_quota_exceeded_fails_run_and_does_not_execute(pg_engine, tmp_path, monk
agent = _agent(wf_id, attachments)
_patch_engine(monkeypatch)
from application.sandbox.artifacts_capture import QuotaExceeded
from docsgpt.sandbox.artifacts_capture import QuotaExceeded
def _raise_quota(**kwargs):
raise QuotaExceeded("artifact storage quota reached")
monkeypatch.setattr(
"application.sandbox.artifacts_capture.persist_new_artifact", _raise_quota
"docsgpt.sandbox.artifacts_capture.persist_new_artifact", _raise_quota
)
events = list(agent._gen_inner("summarize", log_context=None))
@@ -400,7 +400,7 @@ def test_quota_exceeded_fails_run_and_does_not_execute(pg_engine, tmp_path, monk
def test_oversize_declared_attachment_skipped_with_notice(pg_engine, tmp_path, monkeypatch):
"""A declared-oversize attachment is dropped with a surfaced notice; the run still proceeds."""
from application.core.settings import settings
from docsgpt.core.settings import settings
storage = _wire(pg_engine, tmp_path, monkeypatch)
wf_id = _make_workflow(pg_engine)
@@ -462,7 +462,7 @@ def test_extract_parse_opts_out_of_sync_subtask_guard(monkeypatch):
engine.agent = agent
engine.workflow_run_id = "run-extract"
import application.api.user.tasks as tasks
import docsgpt.api.user.tasks as tasks
captured: dict = {}
@@ -495,7 +495,7 @@ def test_extract_parse_opts_out_of_sync_subtask_guard(monkeypatch):
def _engine_for_parse(monkeypatch, get_impl):
"""Engine wired to a fake ``parse_document`` whose ``get`` is ``get_impl``."""
import application.api.user.tasks as tasks
import docsgpt.api.user.tasks as tasks
agent = _agent(str(uuid.uuid4()), [])
engine = WorkflowEngine.__new__(WorkflowEngine)
@@ -562,7 +562,7 @@ def test_parse_is_skipped_once_the_node_budget_is_spent(monkeypatch, caplog):
def test_parse_without_a_deadline_keeps_the_size_scaled_window(monkeypatch):
"""The per-document scaling is the point of the feature; keep it intact."""
from application.api.user.tasks import parse_timeout_for_size
from docsgpt.api.user.tasks import parse_timeout_for_size
engine, captured = _engine_for_parse(
monkeypatch, lambda timeout: {"status": "ok", "content": "md"}
@@ -592,13 +592,13 @@ def test_node_extract_path_capped_with_truncation_note(pg_engine, tmp_path, monk
engine = _RecordingEngine.instances[-1]
# Cap the blocking-extract path below the doc count so the overflow truncates.
from application.core.settings import settings
from docsgpt.core.settings import settings
monkeypatch.setattr(settings, "WORKFLOW_NODE_EXTRACT_MAX_FILES", 2, raising=False)
# Stub the parsing worker so each non-text doc "parses" without a broker, and
# count the blocking calls to prove the overflow docs are never enqueued.
import application.api.user.tasks as tasks
import docsgpt.api.user.tasks as tasks
parse_calls = {"n": 0}
@@ -646,11 +646,11 @@ def test_node_extract_cap_bounds_parse_attempts_even_when_every_parse_times_out(
list(agent._gen_inner("summarize", log_context=None))
engine = _RecordingEngine.instances[-1]
from application.core.settings import settings
from docsgpt.core.settings import settings
monkeypatch.setattr(settings, "WORKFLOW_NODE_EXTRACT_MAX_FILES", 2, raising=False)
import application.api.user.tasks as tasks
import docsgpt.api.user.tasks as tasks
parse_calls = {"n": 0}
@@ -698,7 +698,7 @@ def _with_extraction(attachment: dict, content, extraction=_OK_EXTRACTION) -> di
def _forbid_parse(monkeypatch) -> None:
"""Make any enqueue of the parsing worker a hard failure."""
import application.api.user.tasks as tasks
import docsgpt.api.user.tasks as tasks
def _apply_async(*a, **k):
raise AssertionError("the document must not be re-parsed")
@@ -708,7 +708,7 @@ def _forbid_parse(monkeypatch) -> None:
def _count_parses(monkeypatch, calls: dict) -> None:
"""Stub the parsing worker so each blocking parse succeeds and is counted."""
import application.api.user.tasks as tasks
import docsgpt.api.user.tasks as tasks
class _R:
def get(self, timeout=None, disable_sync_subtasks=True):
@@ -758,7 +758,7 @@ def test_preextracted_attachment_text_is_reused_without_reparsing(
def test_preextracted_text_is_bounded_head_and_tail(pg_engine, tmp_path, monkeypatch):
"""Reused text goes through the same head+tail window as the inline-text path."""
from application.parser.document_reader import _TEXT_MAX_BYTES
from docsgpt.parser.document_reader import _TEXT_MAX_BYTES
storage = _wire(pg_engine, tmp_path, monkeypatch)
big = "A" * (_TEXT_MAX_BYTES * 3)
@@ -841,7 +841,7 @@ def test_legacy_row_without_extraction_metadata_is_trusted(pg_engine, tmp_path,
def test_preextracted_docs_do_not_consume_the_extract_cap(pg_engine, tmp_path, monkeypatch):
"""Reused text costs no blocking parse, so it must not spend the per-node parse budget."""
from application.core.settings import settings
from docsgpt.core.settings import settings
storage = _wire(pg_engine, tmp_path, monkeypatch)
attachments = [
+22 -22
View File
@@ -15,16 +15,16 @@ from typing import Any, Dict
import pytest
from application.agents.workflows.node_agent import WorkflowNodeAgentFactory
from application.agents.workflows.schemas import (
from docsgpt.agents.workflows.node_agent import WorkflowNodeAgentFactory
from docsgpt.agents.workflows.schemas import (
NodeType,
Workflow,
WorkflowGraph,
WorkflowNode,
)
from application.agents.workflows.workflow_engine import WorkflowEngine
from application.core.settings import settings
from application.storage.db.repositories.artifacts import ArtifactsRepository
from docsgpt.agents.workflows.workflow_engine import WorkflowEngine
from docsgpt.core.settings import settings
from docsgpt.storage.db.repositories.artifacts import ArtifactsRepository
RUN_ID = "11111111-1111-1111-1111-111111111111"
@@ -63,7 +63,7 @@ def _patch_repo(monkeypatch, artifacts: Dict[str, Dict[str, Any]], run_id: str =
def _fake_db_readonly():
yield object()
monkeypatch.setattr("application.storage.db.session.db_readonly", _fake_db_readonly)
monkeypatch.setattr("docsgpt.storage.db.session.db_readonly", _fake_db_readonly)
monkeypatch.setattr(ArtifactsRepository, "__init__", lambda self, conn=None: None)
monkeypatch.setattr(ArtifactsRepository, "artifact_id_at_position", _at_position)
monkeypatch.setattr(ArtifactsRepository, "get_artifact_in_parent", _in_parent)
@@ -147,7 +147,7 @@ def test_raw_id_and_short_ref_pass_through(monkeypatch):
def _node_config(**kwargs):
from application.agents.workflows.schemas import AgentNodeConfig
from docsgpt.agents.workflows.schemas import AgentNodeConfig
return AgentNodeConfig(**kwargs)
@@ -186,7 +186,7 @@ def test_auto_text_only_model_extracts_to_content(monkeypatch):
_patch_repo(monkeypatch, {aid: rec})
eng = _engine(monkeypatch)
monkeypatch.setattr(
"application.storage.storage_creator.StorageCreator.get_storage",
"docsgpt.storage.storage_creator.StorageCreator.get_storage",
staticmethod(lambda: _FakeStorage(b"hello world")),
)
cfg = _node_config(input_documents=[aid], file_passing="auto")
@@ -203,7 +203,7 @@ def test_extract_always_inlines_text_even_for_vision_model(monkeypatch):
_patch_repo(monkeypatch, {aid: rec})
eng = _engine(monkeypatch)
monkeypatch.setattr(
"application.storage.storage_creator.StorageCreator.get_storage",
"docsgpt.storage.storage_creator.StorageCreator.get_storage",
staticmethod(lambda: _FakeStorage(b"# Title")),
)
cfg = _node_config(input_documents=[aid], file_passing="extract")
@@ -280,7 +280,7 @@ def test_forged_uuid_is_rejected(monkeypatch):
def test_native_file_cap_bounds_native_then_extracts(monkeypatch):
"""More than the native cap: the first N go native, the rest are extracted."""
monkeypatch.setattr(
"application.core.settings.settings.WORKFLOW_NODE_NATIVE_MAX_FILES", 2, raising=False
"docsgpt.core.settings.settings.WORKFLOW_NODE_NATIVE_MAX_FILES", 2, raising=False
)
artifacts = {}
ids = []
@@ -291,7 +291,7 @@ def test_native_file_cap_bounds_native_then_extracts(monkeypatch):
_patch_repo(monkeypatch, artifacts)
eng = _engine(monkeypatch)
monkeypatch.setattr(
"application.storage.storage_creator.StorageCreator.get_storage",
"docsgpt.storage.storage_creator.StorageCreator.get_storage",
staticmethod(lambda: _FakeStorage(b"img-bytes")),
)
# extract of a non-text image routes through the parsing worker; stub it so it returns text.
@@ -312,7 +312,7 @@ def test_native_file_cap_bounds_native_then_extracts(monkeypatch):
def test_oversize_file_is_skipped(monkeypatch):
"""A file past the per-file byte ceiling is dropped, not attached."""
monkeypatch.setattr(
"application.core.settings.settings.SANDBOX_MAX_INPUT_BYTES", 5, raising=False
"docsgpt.core.settings.settings.SANDBOX_MAX_INPUT_BYTES", 5, raising=False
)
aid, rec = _artifact(RUN_ID, "image/png", size=999)
_patch_repo(monkeypatch, {aid: rec})
@@ -328,7 +328,7 @@ def test_oversize_file_is_skipped(monkeypatch):
def test_oversize_text_skipped_by_post_read_guard_when_size_missing(monkeypatch):
"""A NULL/missing version size skips the pre-read cap; the post-read byte guard still drops it."""
monkeypatch.setattr(
"application.core.settings.settings.SANDBOX_MAX_INPUT_BYTES", 5, raising=False
"docsgpt.core.settings.settings.SANDBOX_MAX_INPUT_BYTES", 5, raising=False
)
# size=None bypasses the ``isinstance(size, int)`` pre-read check; the actual
# bytes (longer than the 5-byte cap) must be rejected after reading.
@@ -336,7 +336,7 @@ def test_oversize_text_skipped_by_post_read_guard_when_size_missing(monkeypatch)
_patch_repo(monkeypatch, {aid: rec})
eng = _engine(monkeypatch)
monkeypatch.setattr(
"application.storage.storage_creator.StorageCreator.get_storage",
"docsgpt.storage.storage_creator.StorageCreator.get_storage",
staticmethod(lambda: _FakeStorage(b"way over the cap")),
)
cfg = _node_config(input_documents=[aid], file_passing="auto")
@@ -349,14 +349,14 @@ def test_oversize_text_skipped_by_post_read_guard_when_size_missing(monkeypatch)
@pytest.mark.unit
def test_large_under_cap_text_is_windowed_not_inlined_whole(monkeypatch):
"""A large-but-under-cap text file is bounded to a head+tail window, not inlined whole."""
from application.parser.document_reader import _TEXT_MAX_BYTES as _MARKDOWN_MAX_BYTES
from docsgpt.parser.document_reader import _TEXT_MAX_BYTES as _MARKDOWN_MAX_BYTES
big_text = ("A" * (_MARKDOWN_MAX_BYTES * 3)).encode("utf-8")
aid, rec = _artifact(RUN_ID, "text/plain", filename="notes.txt", size=len(big_text))
_patch_repo(monkeypatch, {aid: rec})
eng = _engine(monkeypatch)
monkeypatch.setattr(
"application.storage.storage_creator.StorageCreator.get_storage",
"docsgpt.storage.storage_creator.StorageCreator.get_storage",
staticmethod(lambda: _FakeStorage(big_text)),
)
cfg = _node_config(input_documents=[aid], file_passing="auto")
@@ -391,7 +391,7 @@ def test_duplicate_refs_attach_once(monkeypatch):
def _capture_parse(monkeypatch) -> dict:
"""Patch parse_document.apply_async so nothing touches a broker; capture the call."""
import application.api.user.tasks as tasks
import docsgpt.api.user.tasks as tasks
captured: Dict[str, Any] = {}
@@ -411,7 +411,7 @@ def _capture_parse(monkeypatch) -> dict:
@pytest.mark.unit
def test_parse_window_scales_with_the_document_size(monkeypatch):
"""The version's ``size`` reaches the parse: a longer await AND matching Celery limits."""
from application.api.user.tasks import parse_task_time_limits, parse_timeout_for_size
from docsgpt.api.user.tasks import parse_task_time_limits, parse_timeout_for_size
size = 8 * 1024 * 1024
aid, rec = _artifact(RUN_ID, "application/pdf", filename="scan.pdf", size=size)
@@ -456,7 +456,7 @@ def test_preextracted_text_skips_the_parse_worker(monkeypatch):
eng = _engine(monkeypatch)
eng.preextracted_text[aid] = "ALREADY OCRED"
import application.api.user.tasks as tasks
import docsgpt.api.user.tasks as tasks
monkeypatch.setattr(
tasks.parse_document,
@@ -523,15 +523,15 @@ def _agent_node(input_documents=None, file_passing="auto", node_id="agent_1") ->
def _patch_capabilities(monkeypatch):
"""Stub provider/api-key resolution (capabilities are only fetched for json_schema nodes)."""
monkeypatch.setattr(
"application.core.model_utils.get_model_capabilities",
"docsgpt.core.model_utils.get_model_capabilities",
lambda model_id, user_id=None: None,
)
monkeypatch.setattr(
"application.core.model_utils.get_provider_from_model_id",
"docsgpt.core.model_utils.get_provider_from_model_id",
lambda model_id, user_id=None: "openai",
)
monkeypatch.setattr(
"application.core.model_utils.get_api_key_for_provider", lambda _p: "k"
"docsgpt.core.model_utils.get_api_key_for_provider", lambda _p: "k"
)
@@ -16,15 +16,15 @@ from unittest.mock import Mock
import pytest
from application.agents.tool_executor import ToolExecutor
from application.agents.workflows.node_agent import WorkflowNodeAgentFactory
from application.agents.workflows.schemas import (
from docsgpt.agents.tool_executor import ToolExecutor
from docsgpt.agents.workflows.node_agent import WorkflowNodeAgentFactory
from docsgpt.agents.workflows.schemas import (
NodeType,
Workflow,
WorkflowGraph,
WorkflowNode,
)
from application.agents.workflows.workflow_engine import WorkflowEngine
from docsgpt.agents.workflows.workflow_engine import WorkflowEngine
class _StubNodeAgent:
@@ -85,7 +85,7 @@ def test_agent_node_run_scopes_tool_executor(monkeypatch):
WorkflowNodeAgentFactory, "create", staticmethod(lambda **kwargs: stub)
)
monkeypatch.setattr(
"application.core.model_utils.get_api_key_for_provider", lambda _provider: None
"docsgpt.core.model_utils.get_api_key_for_provider", lambda _provider: None
)
list(engine._execute_agent_node(node))
@@ -108,7 +108,7 @@ def test_agent_node_skips_run_scope_when_not_persisted(monkeypatch):
WorkflowNodeAgentFactory, "create", staticmethod(lambda **kwargs: stub)
)
monkeypatch.setattr(
"application.core.model_utils.get_api_key_for_provider", lambda _provider: None
"docsgpt.core.model_utils.get_api_key_for_provider", lambda _provider: None
)
list(engine._execute_agent_node(node))
@@ -135,7 +135,7 @@ def _capture_tool_config(monkeypatch) -> Dict[str, Any]:
mock_tm.load_tool.side_effect = _load_tool
monkeypatch.setattr(
"application.agents.tool_executor.ToolManager", lambda config: mock_tm
"docsgpt.agents.tool_executor.ToolManager", lambda config: mock_tm
)
return captured
@@ -184,15 +184,15 @@ def test_get_or_load_tool_omits_workflow_run_id_for_chat(monkeypatch):
def test_run_scoped_ref_resolves_for_edit(pg_engine, tmp_path, monkeypatch):
"""A1 created under a workflow_run_id resolves for edit_artifact -> v2."""
pytest.importorskip("jsonschema")
from application.agents.tools.artifact_generator import ArtifactGeneratorTool
from application.storage.db.repositories.artifacts import ArtifactsRepository
from application.storage.local import LocalStorage
from application.storage.storage_creator import StorageCreator
from docsgpt.agents.tools.artifact_generator import ArtifactGeneratorTool
from docsgpt.storage.db.repositories.artifacts import ArtifactsRepository
from docsgpt.storage.local import LocalStorage
from docsgpt.storage.storage_creator import StorageCreator
storage = LocalStorage(base_dir=str(tmp_path))
monkeypatch.setattr(StorageCreator, "_instance", storage, raising=False)
monkeypatch.setattr(
"application.storage.db.session.get_engine", lambda: pg_engine
"docsgpt.storage.db.session.get_engine", lambda: pg_engine
)
# Skip the Jupyter-gateway renderer: the run-scoping under test is the ref
# resolution + version append, not the rendered bytes.
+1 -1
View File
@@ -4,7 +4,7 @@ from datetime import datetime, timezone
import pytest
from pydantic import ValidationError
from application.agents.workflows.schemas import (
from docsgpt.agents.workflows.schemas import (
AgentNodeConfig,
AgentType,
ConditionCase,
+2 -2
View File
@@ -1,7 +1,7 @@
from types import SimpleNamespace
from application.agents.workflows.schemas import Workflow, WorkflowGraph
from application.agents.workflows.workflow_engine import WorkflowEngine
from docsgpt.agents.workflows.schemas import Workflow, WorkflowGraph
from docsgpt.agents.workflows.workflow_engine import WorkflowEngine
def create_engine() -> WorkflowEngine:
+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()
+27 -27
View File
@@ -1,4 +1,4 @@
"""Tests for application/api/answer/routes/answer.py"""
"""Tests for docsgpt/api/answer/routes/answer.py"""
import json
import uuid
@@ -15,7 +15,7 @@ _AGENT_ID = "507f1f77bcf86cd799439012"
def mock_stream_processor():
"""Create a mock StreamProcessor."""
with patch(
"application.api.answer.routes.answer.StreamProcessor"
"docsgpt.api.answer.routes.answer.StreamProcessor"
) as MockProcessor:
processor = MagicMock()
processor.decoded_token = {"sub": "test_user"}
@@ -35,7 +35,7 @@ def answer_client(mock_mongo_db, flask_app):
"""Create a test client with the answer route registered."""
from flask_restx import Api
from application.api.answer.routes.answer import answer_ns
from docsgpt.api.answer.routes.answer import answer_ns
api = Api(flask_app)
api.add_namespace(answer_ns)
@@ -61,13 +61,13 @@ class TestAnswerResourcePost:
return_value=iter([]),
):
with patch(
"application.api.answer.routes.answer.AnswerResource.validate_request",
"docsgpt.api.answer.routes.answer.AnswerResource.validate_request",
return_value=None,
), patch(
"application.api.answer.routes.answer.AnswerResource.check_usage",
"docsgpt.api.answer.routes.answer.AnswerResource.check_usage",
return_value=None,
), patch(
"application.api.answer.routes.answer.AnswerResource.complete_stream",
"docsgpt.api.answer.routes.answer.AnswerResource.complete_stream",
return_value=iter(
[
f'data: {json.dumps({"type": "answer", "answer": "Hello"})}\n\n',
@@ -76,7 +76,7 @@ class TestAnswerResourcePost:
]
),
), patch(
"application.api.answer.routes.answer.AnswerResource.process_response_stream",
"docsgpt.api.answer.routes.answer.AnswerResource.process_response_stream",
return_value={"conversation_id": conv_id, "answer": "Hello", "sources": [], "tool_calls": [], "thought": "", "error": None},
):
resp = answer_client.post(
@@ -92,7 +92,7 @@ class TestAnswerResourcePost:
def test_unauthorized_returns_401(self, answer_client, mock_stream_processor):
mock_stream_processor.decoded_token = None
with patch(
"application.api.answer.routes.answer.AnswerResource.validate_request",
"docsgpt.api.answer.routes.answer.AnswerResource.validate_request",
return_value=None,
):
resp = answer_client.post(
@@ -106,10 +106,10 @@ class TestAnswerResourcePost:
def test_usage_exceeded_returns_error(self, answer_client, mock_stream_processor):
with patch(
"application.api.answer.routes.answer.AnswerResource.validate_request",
"docsgpt.api.answer.routes.answer.AnswerResource.validate_request",
return_value=None,
), patch(
"application.api.answer.routes.answer.AnswerResource.check_usage",
"docsgpt.api.answer.routes.answer.AnswerResource.check_usage",
) as mock_check:
with flask_app_context(answer_client):
mock_check.return_value = ({"error": "Usage limit exceeded"}, 429)
@@ -123,16 +123,16 @@ class TestAnswerResourcePost:
def test_stream_error_returns_400(self, answer_client, mock_stream_processor):
with patch(
"application.api.answer.routes.answer.AnswerResource.validate_request",
"docsgpt.api.answer.routes.answer.AnswerResource.validate_request",
return_value=None,
), patch(
"application.api.answer.routes.answer.AnswerResource.check_usage",
"docsgpt.api.answer.routes.answer.AnswerResource.check_usage",
return_value=None,
), patch(
"application.api.answer.routes.answer.AnswerResource.complete_stream",
"docsgpt.api.answer.routes.answer.AnswerResource.complete_stream",
return_value=iter([]),
), patch(
"application.api.answer.routes.answer.AnswerResource.process_response_stream",
"docsgpt.api.answer.routes.answer.AnswerResource.process_response_stream",
return_value={"conversation_id": None, "answer": None, "sources": None, "tool_calls": None, "thought": None, "error": "Stream error"},
):
resp = answer_client.post(
@@ -154,7 +154,7 @@ class TestAnswerResourcePost:
traceback on the ERROR channel. ``/stream`` and
``/v1/chat/completions`` both answer 409 for the identical condition.
"""
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ResumeInProgressError,
)
@@ -163,7 +163,7 @@ class TestAnswerResourcePost:
)
with patch(
"application.api.answer.routes.answer.AnswerResource.validate_request",
"docsgpt.api.answer.routes.answer.AnswerResource.validate_request",
return_value=None,
):
resp = answer_client.post(
@@ -186,13 +186,13 @@ class TestAnswerResourcePost:
def test_exception_returns_500(self, answer_client, mock_stream_processor):
with patch(
"application.api.answer.routes.answer.AnswerResource.validate_request",
"docsgpt.api.answer.routes.answer.AnswerResource.validate_request",
return_value=None,
), patch(
"application.api.answer.routes.answer.AnswerResource.check_usage",
"docsgpt.api.answer.routes.answer.AnswerResource.check_usage",
return_value=None,
), patch(
"application.api.answer.routes.answer.AnswerResource.complete_stream",
"docsgpt.api.answer.routes.answer.AnswerResource.complete_stream",
side_effect=RuntimeError("unexpected"),
):
resp = answer_client.post(
@@ -208,16 +208,16 @@ class TestAnswerResourcePost:
):
conv_id = str(uuid.uuid4())
with patch(
"application.api.answer.routes.answer.AnswerResource.validate_request",
"docsgpt.api.answer.routes.answer.AnswerResource.validate_request",
return_value=None,
), patch(
"application.api.answer.routes.answer.AnswerResource.check_usage",
"docsgpt.api.answer.routes.answer.AnswerResource.check_usage",
return_value=None,
), patch(
"application.api.answer.routes.answer.AnswerResource.complete_stream",
"docsgpt.api.answer.routes.answer.AnswerResource.complete_stream",
return_value=iter([]),
), patch(
"application.api.answer.routes.answer.AnswerResource.process_response_stream",
"docsgpt.api.answer.routes.answer.AnswerResource.process_response_stream",
return_value={"conversation_id": conv_id, "answer": '{"key": "val"}', "sources": [], "tool_calls": [], "thought": "", "error": None, "extra": {"structured": True, "schema": {"type": "object"}}},
):
resp = answer_client.post(
@@ -235,16 +235,16 @@ class TestAnswerResourcePost:
):
conv_id = str(uuid.uuid4())
with patch(
"application.api.answer.routes.answer.AnswerResource.validate_request",
"docsgpt.api.answer.routes.answer.AnswerResource.validate_request",
return_value=None,
), patch(
"application.api.answer.routes.answer.AnswerResource.check_usage",
"docsgpt.api.answer.routes.answer.AnswerResource.check_usage",
return_value=None,
), patch(
"application.api.answer.routes.answer.AnswerResource.complete_stream",
"docsgpt.api.answer.routes.answer.AnswerResource.complete_stream",
return_value=iter([]),
), patch(
"application.api.answer.routes.answer.AnswerResource.process_response_stream",
"docsgpt.api.answer.routes.answer.AnswerResource.process_response_stream",
return_value={"conversation_id": conv_id, "answer": "answer text", "sources": [{"title": "src"}], "tool_calls": [{"tool": "t"}], "thought": "thinking...", "error": None},
):
resp = answer_client.post(
+36 -36
View File
@@ -12,7 +12,7 @@ class TestBaseAnswerValidation:
def test_validate_request_passes_with_required_fields(
self, mock_mongo_db, flask_app
):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -23,7 +23,7 @@ class TestBaseAnswerValidation:
assert result is None
def test_validate_request_fails_without_question(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -36,7 +36,7 @@ class TestBaseAnswerValidation:
assert "question" in result.json["message"].lower()
def test_validate_with_conversation_id_required(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -49,7 +49,7 @@ class TestBaseAnswerValidation:
assert "conversation_id" in result.json["message"].lower()
def test_validate_passes_with_all_required_fields(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -65,7 +65,7 @@ class TestUsageChecking:
pass
def test_returns_none_when_no_api_key(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -88,7 +88,7 @@ class TestGPTModelRetrieval:
pass
def test_initializes_gpt_model(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -102,7 +102,7 @@ class TestConversationServiceIntegration:
pass
def test_initializes_conversation_service(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -117,7 +117,7 @@ class TestCompleteStreamMethod:
pass
def test_streams_answer_chunks(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -149,7 +149,7 @@ class TestCompleteStreamMethod:
assert '"answer": "world!"' in answer_chunks[1]
def test_streams_sources(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -180,7 +180,7 @@ class TestCompleteStreamMethod:
assert '"title": "doc1.txt"' in source_chunks[0]
def test_handles_error_during_streaming(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -209,7 +209,7 @@ class TestCompleteStreamMethod:
Without the flag, sanitize_api_error substring-matches "quota" and rewrites the
message into a misleading rate-limit notice.
"""
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -248,7 +248,7 @@ class TestCompleteStreamMethod:
emitted as ``type: error`` -- the client treats an error event as terminal and
disables reconnect -- and its text must not be run through sanitize_api_error.
"""
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -277,7 +277,7 @@ class TestCompleteStreamMethod:
def test_non_user_facing_error_is_sanitized(self, mock_mongo_db, flask_app):
"""A raw error without the flag is still routed through sanitize_api_error."""
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -303,7 +303,7 @@ class TestCompleteStreamMethod:
assert "Rate limit exceeded" in error_chunks[0]
def test_saves_conversation_when_enabled(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -354,7 +354,7 @@ class TestCompleteStreamMethod:
so tools needing a conversation home (``scheduler`` in an agentless
chat) see it on the very first call.
"""
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -439,7 +439,7 @@ class TestCompleteStreamMethod:
"""A pure ``requires_client_execution`` pause must NOT publish a
``tool.approval.required`` event — the client resolves it, so the
notification would be non-actionable noise."""
from application.api.answer.routes import base as base_mod
from docsgpt.api.answer.routes import base as base_mod
with flask_app.app_context(), patch.object(
base_mod, "publish_user_event"
@@ -465,7 +465,7 @@ class TestCompleteStreamMethod:
):
"""A pause with an ``awaiting_approval`` call publishes once, and the
payload surfaces only the approval call (not the client-side one)."""
from application.api.answer.routes import base as base_mod
from docsgpt.api.answer.routes import base as base_mod
with flask_app.app_context(), patch.object(
base_mod, "publish_user_event"
@@ -505,7 +505,7 @@ class TestProcessResponseStream:
def test_processes_complete_stream(self, mock_mongo_db, flask_app):
import json
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -529,7 +529,7 @@ class TestProcessResponseStream:
def test_handles_stream_error(self, mock_mongo_db, flask_app):
import json
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -544,7 +544,7 @@ class TestProcessResponseStream:
assert result["error"] == "Test error"
def test_handles_malformed_stream_data(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -564,7 +564,7 @@ class TestErrorStreamGenerate:
pass
def test_generates_error_stream(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -588,9 +588,9 @@ def _patch_base_db(conn):
yield conn
with patch(
"application.api.answer.routes.base.db_readonly", _yield
"docsgpt.api.answer.routes.base.db_readonly", _yield
), patch(
"application.api.answer.routes.base.db_session", _yield
"docsgpt.api.answer.routes.base.db_session", _yield
):
yield
@@ -598,7 +598,7 @@ def _patch_base_db(conn):
@pytest.mark.unit
class TestCheckUsagePgConn:
def test_invalid_api_key_returns_401(self, pg_conn, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with _patch_base_db(pg_conn), flask_app.app_context():
resource = BaseAnswerResource()
@@ -607,8 +607,8 @@ class TestCheckUsagePgConn:
assert result.status_code == 401
def test_no_limits_returns_none(self, pg_conn, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.storage.db.repositories.agents import AgentsRepository
AgentsRepository(pg_conn).create(
"owner", "a", "published", key="k1",
@@ -620,8 +620,8 @@ class TestCheckUsagePgConn:
assert result is None
def test_within_limit_returns_none(self, pg_conn, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.storage.db.repositories.agents import AgentsRepository
AgentsRepository(pg_conn).create(
"owner", "a", "published", key="k2",
@@ -633,9 +633,9 @@ class TestCheckUsagePgConn:
assert result is None
def test_token_limit_exceeded_returns_429(self, pg_conn, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.token_usage import (
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.token_usage import (
TokenUsageRepository,
)
@@ -655,9 +655,9 @@ class TestCheckUsagePgConn:
assert result.status_code == 429
def test_request_limit_exceeded_returns_429(self, pg_conn, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.token_usage import (
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.token_usage import (
TokenUsageRepository,
)
@@ -678,8 +678,8 @@ class TestCheckUsagePgConn:
def test_string_True_limited_token_mode_parsed(self, pg_conn, flask_app):
"""Legacy Mongo sometimes stored ``limited_token_mode`` as the
string 'True'; verify the parse branch."""
from application.api.answer.routes.base import BaseAnswerResource
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.storage.db.repositories.agents import AgentsRepository
# Store bool=False in DB (limited_token_mode default). Test uses
# string 'True' by mutating the row directly.
+26 -26
View File
@@ -1,6 +1,6 @@
"""Tests for /api/search route (application/api/answer/routes/search.py).
"""Tests for /api/search route (docsgpt/api/answer/routes/search.py).
Retrieval logic lives in ``application/services/search_service.py`` and
Retrieval logic lives in ``docsgpt/services/search_service.py`` and
has its own unit tests in ``tests/services/test_search_service.py``. The
tests below focus on what the route specifically owns:
@@ -21,7 +21,7 @@ import pytest
@pytest.mark.unit
class TestSearchResourceValidation:
def test_returns_400_when_question_missing(self, flask_app):
from application.api.answer.routes.search import SearchResource
from docsgpt.api.answer.routes.search import SearchResource
with flask_app.app_context():
with flask_app.test_request_context(json={"api_key": "test_key"}):
@@ -30,7 +30,7 @@ class TestSearchResourceValidation:
assert "question" in result.json["error"]
def test_returns_400_when_api_key_missing(self, flask_app):
from application.api.answer.routes.search import SearchResource
from docsgpt.api.answer.routes.search import SearchResource
with flask_app.app_context():
with flask_app.test_request_context(json={"question": "test query"}):
@@ -49,13 +49,13 @@ class TestSearchResourceExceptionMapping:
"""
def test_invalid_api_key_returns_401(self, flask_app):
from application.api.answer.routes.search import SearchResource
from application.services.search_service import InvalidAPIKey
from docsgpt.api.answer.routes.search import SearchResource
from docsgpt.services.search_service import InvalidAPIKey
with flask_app.app_context(), flask_app.test_request_context(
json={"question": "q", "api_key": "bad"}
), patch(
"application.api.answer.routes.search.search",
"docsgpt.api.answer.routes.search.search",
side_effect=InvalidAPIKey(),
):
result = SearchResource().post()
@@ -63,13 +63,13 @@ class TestSearchResourceExceptionMapping:
assert result.json == {"error": "Invalid API key"}
def test_search_failed_returns_500(self, flask_app):
from application.api.answer.routes.search import SearchResource
from application.services.search_service import SearchFailed
from docsgpt.api.answer.routes.search import SearchResource
from docsgpt.services.search_service import SearchFailed
with flask_app.app_context(), flask_app.test_request_context(
json={"question": "q", "api_key": "k"}
), patch(
"application.api.answer.routes.search.search",
"docsgpt.api.answer.routes.search.search",
side_effect=SearchFailed("boom"),
):
result = SearchResource().post()
@@ -77,13 +77,13 @@ class TestSearchResourceExceptionMapping:
assert result.json == {"error": "Search failed"}
def test_happy_path_passes_service_result_through(self, flask_app):
from application.api.answer.routes.search import SearchResource
from docsgpt.api.answer.routes.search import SearchResource
hits = [{"text": "t", "title": "T", "source": "s"}]
with flask_app.app_context(), flask_app.test_request_context(
json={"question": "q", "api_key": "k", "chunks": 7}
), patch(
"application.api.answer.routes.search.search",
"docsgpt.api.answer.routes.search.search",
return_value=hits,
) as mock_search:
result = SearchResource().post()
@@ -92,12 +92,12 @@ class TestSearchResourceExceptionMapping:
mock_search.assert_called_once_with("k", "q", 7)
def test_default_chunks_is_5(self, flask_app):
from application.api.answer.routes.search import SearchResource
from docsgpt.api.answer.routes.search import SearchResource
with flask_app.app_context(), flask_app.test_request_context(
json={"question": "q", "api_key": "k"} # no chunks field
), patch(
"application.api.answer.routes.search.search",
"docsgpt.api.answer.routes.search.search",
return_value=[],
) as mock_search:
SearchResource().post()
@@ -121,14 +121,14 @@ def _patch_search_db(conn):
yield conn
with patch(
"application.services.search_service.db_readonly", _yield
"docsgpt.services.search_service.db_readonly", _yield
):
yield
class TestSearchResourcePgConn:
def test_invalid_api_key_returns_401(self, pg_conn, flask_app):
from application.api.answer.routes.search import SearchResource
from docsgpt.api.answer.routes.search import SearchResource
with _patch_search_db(pg_conn), flask_app.app_context():
with flask_app.test_request_context(
@@ -138,8 +138,8 @@ class TestSearchResourcePgConn:
assert result.status_code == 401
def test_no_sources_returns_empty(self, pg_conn, flask_app):
from application.api.answer.routes.search import SearchResource
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.answer.routes.search import SearchResource
from docsgpt.storage.db.repositories.agents import AgentsRepository
AgentsRepository(pg_conn).create(
"u", "a", "published", key="no-src-key",
@@ -153,9 +153,9 @@ class TestSearchResourcePgConn:
assert result.json == []
def test_search_returns_results(self, pg_conn, flask_app):
from application.api.answer.routes.search import SearchResource
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.api.answer.routes.search import SearchResource
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
src = SourcesRepository(pg_conn).create("src", user_id="u")
AgentsRepository(pg_conn).create(
@@ -170,7 +170,7 @@ class TestSearchResourcePgConn:
]
with _patch_search_db(pg_conn), patch(
"application.services.search_service.VectorCreator.create_vectorstore",
"docsgpt.services.search_service.VectorCreator.create_vectorstore",
return_value=fake_vs,
), flask_app.app_context():
with flask_app.test_request_context(
@@ -181,9 +181,9 @@ class TestSearchResourcePgConn:
assert len(result.json) == 1
def test_search_uses_extra_source_ids(self, pg_conn, flask_app):
from application.api.answer.routes.search import SearchResource
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.api.answer.routes.search import SearchResource
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
src1 = SourcesRepository(pg_conn).create("s1", user_id="u")
src2 = SourcesRepository(pg_conn).create("s2", user_id="u")
@@ -198,7 +198,7 @@ class TestSearchResourcePgConn:
{"text": "one", "metadata": {"title": "A"}},
]
with _patch_search_db(pg_conn), patch(
"application.services.search_service.VectorCreator.create_vectorstore",
"docsgpt.services.search_service.VectorCreator.create_vectorstore",
return_value=fake_vs,
), flask_app.app_context():
with flask_app.test_request_context(
+14 -14
View File
@@ -1,4 +1,4 @@
"""Tests for application/api/answer/routes/stream.py"""
"""Tests for docsgpt/api/answer/routes/stream.py"""
import json
import uuid
@@ -15,7 +15,7 @@ _AGENT_ID = "507f1f77bcf86cd799439012"
def mock_stream_processor():
"""Create a mock StreamProcessor for stream tests."""
with patch(
"application.api.answer.routes.stream.StreamProcessor"
"docsgpt.api.answer.routes.stream.StreamProcessor"
) as MockProcessor:
processor = MagicMock()
processor.decoded_token = {"sub": "test_user"}
@@ -35,7 +35,7 @@ def stream_client(mock_mongo_db, flask_app):
"""Create a test client with the stream route registered."""
from flask_restx import Api
from application.api.answer.routes.stream import answer_ns
from docsgpt.api.answer.routes.stream import answer_ns
api = Api(flask_app)
api.add_namespace(answer_ns)
@@ -59,13 +59,13 @@ class TestStreamResourcePost:
yield f'data: {json.dumps({"type": "end"})}\n\n'
with patch(
"application.api.answer.routes.stream.StreamResource.validate_request",
"docsgpt.api.answer.routes.stream.StreamResource.validate_request",
return_value=None,
), patch(
"application.api.answer.routes.stream.StreamResource.check_usage",
"docsgpt.api.answer.routes.stream.StreamResource.check_usage",
return_value=None,
), patch(
"application.api.answer.routes.stream.StreamResource.complete_stream",
"docsgpt.api.answer.routes.stream.StreamResource.complete_stream",
side_effect=fake_stream,
):
resp = stream_client.post(
@@ -84,7 +84,7 @@ class TestStreamResourcePost:
):
mock_stream_processor.decoded_token = None
with patch(
"application.api.answer.routes.stream.StreamResource.validate_request",
"docsgpt.api.answer.routes.stream.StreamResource.validate_request",
return_value=None,
):
resp = stream_client.post(
@@ -101,10 +101,10 @@ class TestStreamResourcePost:
self, stream_client, mock_stream_processor
):
with patch(
"application.api.answer.routes.stream.StreamResource.validate_request",
"docsgpt.api.answer.routes.stream.StreamResource.validate_request",
return_value=None,
), patch(
"application.api.answer.routes.stream.StreamResource.check_usage",
"docsgpt.api.answer.routes.stream.StreamResource.check_usage",
) as mock_check:
mock_check.return_value = ({"error": "Usage limit exceeded"}, 429)
resp = stream_client.post(
@@ -119,7 +119,7 @@ class TestStreamResourcePost:
):
mock_stream_processor.build_agent.side_effect = ValueError("bad data")
with patch(
"application.api.answer.routes.stream.StreamResource.validate_request",
"docsgpt.api.answer.routes.stream.StreamResource.validate_request",
return_value=None,
):
resp = stream_client.post(
@@ -137,7 +137,7 @@ class TestStreamResourcePost:
):
mock_stream_processor.build_agent.side_effect = RuntimeError("crash")
with patch(
"application.api.answer.routes.stream.StreamResource.validate_request",
"docsgpt.api.answer.routes.stream.StreamResource.validate_request",
return_value=None,
):
resp = stream_client.post(
@@ -172,13 +172,13 @@ class TestStreamResourcePost:
conv_id = str(uuid.uuid4())
with patch(
"application.api.answer.routes.stream.StreamResource.validate_request",
"docsgpt.api.answer.routes.stream.StreamResource.validate_request",
return_value=None,
), patch(
"application.api.answer.routes.stream.StreamResource.check_usage",
"docsgpt.api.answer.routes.stream.StreamResource.check_usage",
return_value=None,
), patch(
"application.api.answer.routes.stream.StreamResource.complete_stream",
"docsgpt.api.answer.routes.stream.StreamResource.complete_stream",
side_effect=fake_stream,
) as mock_complete:
resp = stream_client.post(
@@ -14,16 +14,16 @@ from unittest.mock import MagicMock, patch
import pytest
from application.api.answer.services.compression import CompressionService
from application.api.answer.services.compression.orchestrator import (
from docsgpt.api.answer.services.compression import CompressionService
from docsgpt.api.answer.services.compression.orchestrator import (
CompressionOrchestrator,
)
from application.api.answer.services.compression.threshold_checker import (
from docsgpt.api.answer.services.compression.threshold_checker import (
CompressionThresholdChecker,
)
from application.api.answer.services.compression.token_counter import TokenCounter
from application.api.answer.services.compression.types import CompressionResult
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.compression.token_counter import TokenCounter
from docsgpt.api.answer.services.compression.types import CompressionResult
from docsgpt.api.answer.services.conversation_service import (
COMPRESSION_SUMMARY_PROMPT,
)
@@ -106,16 +106,16 @@ class TestTurnStartReuse:
assert result.last_compression_at is None
@patch(
"application.api.answer.services.compression.orchestrator.get_provider_from_model_id",
"docsgpt.api.answer.services.compression.orchestrator.get_provider_from_model_id",
return_value="openai",
)
@patch(
"application.api.answer.services.compression.orchestrator.get_api_key_for_provider",
"docsgpt.api.answer.services.compression.orchestrator.get_api_key_for_provider",
return_value="sk",
)
@patch("application.api.answer.services.compression.orchestrator.LLMCreator")
@patch("application.api.answer.services.compression.orchestrator.CompressionService")
@patch("application.api.answer.services.compression.orchestrator.settings")
@patch("docsgpt.api.answer.services.compression.orchestrator.LLMCreator")
@patch("docsgpt.api.answer.services.compression.orchestrator.CompressionService")
@patch("docsgpt.api.answer.services.compression.orchestrator.settings")
def test_over_threshold_compresses_only_the_tail(
self,
mock_settings,
@@ -172,7 +172,7 @@ class TestEffectiveTokenCount:
)
@patch(
"application.api.answer.services.compression.threshold_checker.get_token_limit",
"docsgpt.api.answer.services.compression.threshold_checker.get_token_limit",
return_value=1000,
)
def test_should_compress_uses_effective_count(self, _limit):
@@ -252,7 +252,7 @@ class TestSummaryRowMarker:
who types the label text as a question keeps that turn."""
def test_marked_row_is_a_summary_row(self):
from application.api.answer.services.compression.types import (
from docsgpt.api.answer.services.compression.types import (
COMPRESSION_SUMMARY_MARKER,
is_compression_summary_row,
)
@@ -261,7 +261,7 @@ class TestSummaryRowMarker:
assert is_compression_summary_row(row) is True
def test_legacy_row_without_metadata_is_a_summary_row(self):
from application.api.answer.services.compression.types import is_compression_summary_row
from docsgpt.api.answer.services.compression.types import is_compression_summary_row
assert is_compression_summary_row({"prompt": COMPRESSION_SUMMARY_PROMPT, "response": "S"}) is True
assert is_compression_summary_row(
@@ -269,7 +269,7 @@ class TestSummaryRowMarker:
) is True
def test_user_turn_with_the_label_text_is_kept(self):
from application.api.answer.services.compression.types import is_compression_summary_row
from docsgpt.api.answer.services.compression.types import is_compression_summary_row
real_turn = {
"prompt": COMPRESSION_SUMMARY_PROMPT,
@@ -309,16 +309,16 @@ class TestIncrementalTailExcludesSummaryRows:
svc.compress_conversation(conv, compress_up_to_index=2, start_index=2)
@patch(
"application.api.answer.services.compression.orchestrator.get_provider_from_model_id",
"docsgpt.api.answer.services.compression.orchestrator.get_provider_from_model_id",
return_value="openai",
)
@patch(
"application.api.answer.services.compression.orchestrator.get_api_key_for_provider",
"docsgpt.api.answer.services.compression.orchestrator.get_api_key_for_provider",
return_value="sk",
)
@patch("application.api.answer.services.compression.orchestrator.LLMCreator")
@patch("application.api.answer.services.compression.orchestrator.CompressionService")
@patch("application.api.answer.services.compression.orchestrator.settings")
@patch("docsgpt.api.answer.services.compression.orchestrator.LLMCreator")
@patch("docsgpt.api.answer.services.compression.orchestrator.CompressionService")
@patch("docsgpt.api.answer.services.compression.orchestrator.settings")
def test_orchestrator_reuses_summary_when_only_summary_rows_follow_the_point(
self, mock_settings, MockCompressionService, MockLLMCreator, _key, _provider,
orchestrator, conversation_service, threshold_checker,
@@ -355,16 +355,16 @@ class TestAbsolutePersistIndex:
assert metadata.query_index == 19
@patch(
"application.api.answer.services.compression.orchestrator.get_provider_from_model_id",
"docsgpt.api.answer.services.compression.orchestrator.get_provider_from_model_id",
return_value="openai",
)
@patch(
"application.api.answer.services.compression.orchestrator.get_api_key_for_provider",
"docsgpt.api.answer.services.compression.orchestrator.get_api_key_for_provider",
return_value="sk",
)
@patch("application.api.answer.services.compression.orchestrator.LLMCreator")
@patch("application.api.answer.services.compression.orchestrator.CompressionService")
@patch("application.api.answer.services.compression.orchestrator.settings")
@patch("docsgpt.api.answer.services.compression.orchestrator.LLMCreator")
@patch("docsgpt.api.answer.services.compression.orchestrator.CompressionService")
@patch("docsgpt.api.answer.services.compression.orchestrator.settings")
def test_mid_execution_builds_on_the_carried_summary_and_persists_the_absolute_index(
self, mock_settings, MockCompressionService, MockLLMCreator, _key, _provider,
orchestrator, conversation_service,
@@ -461,16 +461,16 @@ class TestUnusableSavedPoints:
assert [q["prompt"] for q in recent] == ["q2", "q3", "q4"]
@patch(
"application.api.answer.services.compression.orchestrator.get_provider_from_model_id",
"docsgpt.api.answer.services.compression.orchestrator.get_provider_from_model_id",
return_value="openai",
)
@patch(
"application.api.answer.services.compression.orchestrator.get_api_key_for_provider",
"docsgpt.api.answer.services.compression.orchestrator.get_api_key_for_provider",
return_value="sk",
)
@patch("application.api.answer.services.compression.orchestrator.LLMCreator")
@patch("application.api.answer.services.compression.orchestrator.CompressionService")
@patch("application.api.answer.services.compression.orchestrator.settings")
@patch("docsgpt.api.answer.services.compression.orchestrator.LLMCreator")
@patch("docsgpt.api.answer.services.compression.orchestrator.CompressionService")
@patch("docsgpt.api.answer.services.compression.orchestrator.settings")
def test_recompression_starts_after_the_latest_usable_point(
self, mock_settings, MockCompressionService, MockLLMCreator, _key, _provider,
orchestrator, conversation_service, threshold_checker,
@@ -1,9 +1,9 @@
"""Tests for application/api/answer/services/compression/message_builder.py"""
"""Tests for docsgpt/api/answer/services/compression/message_builder.py"""
import pytest
from application.api.answer.services.compression.message_builder import MessageBuilder
from docsgpt.api.answer.services.compression.message_builder import MessageBuilder
@pytest.mark.unit
@@ -1,13 +1,13 @@
"""Tests for application/api/answer/services/compression/orchestrator.py"""
"""Tests for docsgpt/api/answer/services/compression/orchestrator.py"""
from unittest.mock import MagicMock, patch
import pytest
from application.api.answer.services.compression.orchestrator import (
from docsgpt.api.answer.services.compression.orchestrator import (
CompressionOrchestrator,
)
from application.api.answer.services.compression.types import (
from docsgpt.api.answer.services.compression.types import (
CompressionMetadata,
CompressionResult,
)
@@ -172,14 +172,14 @@ class TestCompressIfNeeded:
@pytest.mark.unit
class TestPerformCompression:
@patch(
"application.api.answer.services.compression.orchestrator.get_provider_from_model_id"
"docsgpt.api.answer.services.compression.orchestrator.get_provider_from_model_id"
)
@patch(
"application.api.answer.services.compression.orchestrator.get_api_key_for_provider"
"docsgpt.api.answer.services.compression.orchestrator.get_api_key_for_provider"
)
@patch("application.api.answer.services.compression.orchestrator.LLMCreator")
@patch("application.api.answer.services.compression.orchestrator.CompressionService")
@patch("application.api.answer.services.compression.orchestrator.settings")
@patch("docsgpt.api.answer.services.compression.orchestrator.LLMCreator")
@patch("docsgpt.api.answer.services.compression.orchestrator.CompressionService")
@patch("docsgpt.api.answer.services.compression.orchestrator.settings")
def test_successful_compression(
self,
mock_settings,
@@ -228,13 +228,13 @@ class TestPerformCompression:
mock_svc_instance.compress_and_save.assert_called_once()
@patch(
"application.api.answer.services.compression.orchestrator.get_provider_from_model_id"
"docsgpt.api.answer.services.compression.orchestrator.get_provider_from_model_id"
)
@patch(
"application.api.answer.services.compression.orchestrator.get_api_key_for_provider"
"docsgpt.api.answer.services.compression.orchestrator.get_api_key_for_provider"
)
@patch("application.api.answer.services.compression.orchestrator.LLMCreator")
@patch("application.api.answer.services.compression.orchestrator.settings")
@patch("docsgpt.api.answer.services.compression.orchestrator.LLMCreator")
@patch("docsgpt.api.answer.services.compression.orchestrator.settings")
def test_uses_compression_model_override(
self,
mock_settings,
@@ -253,7 +253,7 @@ class TestPerformCompression:
conversation = {"queries": [{"prompt": "q", "response": "r"}], "agent_id": "a"}
with patch(
"application.api.answer.services.compression.orchestrator.CompressionService"
"docsgpt.api.answer.services.compression.orchestrator.CompressionService"
) as MockCS:
mock_svc = MagicMock()
mock_svc.compress_and_save.return_value = MagicMock(
@@ -280,14 +280,14 @@ class TestPerformCompression:
)
@patch(
"application.api.answer.services.compression.orchestrator.get_provider_from_model_id"
"docsgpt.api.answer.services.compression.orchestrator.get_provider_from_model_id"
)
@patch(
"application.api.answer.services.compression.orchestrator.get_api_key_for_provider"
"docsgpt.api.answer.services.compression.orchestrator.get_api_key_for_provider"
)
@patch("application.api.answer.services.compression.orchestrator.LLMCreator")
@patch("application.api.answer.services.compression.orchestrator.CompressionService")
@patch("application.api.answer.services.compression.orchestrator.settings")
@patch("docsgpt.api.answer.services.compression.orchestrator.LLMCreator")
@patch("docsgpt.api.answer.services.compression.orchestrator.CompressionService")
@patch("docsgpt.api.answer.services.compression.orchestrator.settings")
def test_no_queries_returns_no_compression(
self,
mock_settings,
@@ -327,9 +327,9 @@ class TestPerformCompression:
}
with patch(
"application.api.answer.services.compression.orchestrator.settings"
"docsgpt.api.answer.services.compression.orchestrator.settings"
) as mock_settings, patch(
"application.api.answer.services.compression.orchestrator.get_provider_from_model_id",
"docsgpt.api.answer.services.compression.orchestrator.get_provider_from_model_id",
side_effect=RuntimeError("provider error"),
):
mock_settings.COMPRESSION_MODEL_OVERRIDE = None
@@ -1,11 +1,11 @@
"""Tests for application/api/answer/services/compression/service.py"""
"""Tests for docsgpt/api/answer/services/compression/service.py"""
from unittest.mock import MagicMock, patch
import pytest
from application.api.answer.services.compression.service import CompressionService
from application.api.answer.services.compression.types import CompressionMetadata
from docsgpt.api.answer.services.compression.service import CompressionService
from docsgpt.api.answer.services.compression.types import CompressionMetadata
@pytest.fixture
@@ -48,11 +48,11 @@ def sample_conversation():
@pytest.mark.unit
class TestCompressionServiceInit:
@patch("application.api.answer.services.compression.service.settings")
@patch("docsgpt.api.answer.services.compression.service.settings")
def test_default_prompt_builder(self, mock_settings, mock_llm):
mock_settings.COMPRESSION_PROMPT_VERSION = "v1.0"
with patch(
"application.api.answer.services.compression.service.CompressionPromptBuilder"
"docsgpt.api.answer.services.compression.service.CompressionPromptBuilder"
):
svc = CompressionService(llm=mock_llm, model_id="gpt-4")
assert svc.llm is mock_llm
@@ -81,7 +81,7 @@ class TestCompressConversation:
)
with patch(
"application.api.answer.services.compression.service.TokenCounter"
"docsgpt.api.answer.services.compression.service.TokenCounter"
) as MockTC:
MockTC.count_query_tokens.return_value = 1000
MockTC.count_message_tokens.return_value = 100
@@ -144,7 +144,7 @@ class TestCompressConversation:
)
with patch(
"application.api.answer.services.compression.service.TokenCounter"
"docsgpt.api.answer.services.compression.service.TokenCounter"
) as MockTC:
MockTC.count_query_tokens.return_value = 500
MockTC.count_message_tokens.return_value = 50
@@ -170,7 +170,7 @@ class TestCompressConversation:
)
with patch(
"application.api.answer.services.compression.service.TokenCounter"
"docsgpt.api.answer.services.compression.service.TokenCounter"
) as MockTC:
MockTC.count_query_tokens.return_value = 1000
MockTC.count_message_tokens.return_value = 0
@@ -195,7 +195,7 @@ class TestCompressConversation:
)
with patch(
"application.api.answer.services.compression.service.TokenCounter"
"docsgpt.api.answer.services.compression.service.TokenCounter"
) as MockTC:
MockTC.count_query_tokens.return_value = 100
with pytest.raises(RuntimeError, match="LLM error"):
@@ -222,7 +222,7 @@ class TestCompressAndSave:
)
with patch(
"application.api.answer.services.compression.service.TokenCounter"
"docsgpt.api.answer.services.compression.service.TokenCounter"
) as MockTC:
MockTC.count_query_tokens.return_value = 500
MockTC.count_message_tokens.return_value = 50
@@ -487,7 +487,7 @@ class TestBoundRecentQueries:
self, mock_llm, monkeypatch
):
monkeypatch.setattr(
"application.api.answer.services.compression.service.settings.COMPRESSION_RECENT_FIELD_MAX_TOKENS",
"docsgpt.api.answer.services.compression.service.settings.COMPRESSION_RECENT_FIELD_MAX_TOKENS",
50,
raising=False,
)
@@ -507,7 +507,7 @@ class TestBoundRecentQueries:
def test_oversized_tool_result_is_trimmed(self, mock_llm, monkeypatch):
monkeypatch.setattr(
"application.api.answer.services.compression.service.settings.COMPRESSION_RECENT_FIELD_MAX_TOKENS",
"docsgpt.api.answer.services.compression.service.settings.COMPRESSION_RECENT_FIELD_MAX_TOKENS",
50,
raising=False,
)
@@ -536,7 +536,7 @@ class TestBoundRecentQueries:
def test_zero_cap_disables_bounding(self, mock_llm, monkeypatch):
monkeypatch.setattr(
"application.api.answer.services.compression.service.settings.COMPRESSION_RECENT_FIELD_MAX_TOKENS",
"docsgpt.api.answer.services.compression.service.settings.COMPRESSION_RECENT_FIELD_MAX_TOKENS",
0,
raising=False,
)
@@ -7,18 +7,18 @@ import pytest
class TestCompressionThresholdChecker:
def _make_checker(self, pct=0.7):
from application.api.answer.services.compression.threshold_checker import (
from docsgpt.api.answer.services.compression.threshold_checker import (
CompressionThresholdChecker,
)
return CompressionThresholdChecker(threshold_percentage=pct)
@patch(
"application.api.answer.services.compression.threshold_checker.get_token_limit",
"docsgpt.api.answer.services.compression.threshold_checker.get_token_limit",
return_value=8000,
)
@patch(
"application.api.answer.services.compression.threshold_checker.TokenCounter.count_message_tokens",
"docsgpt.api.answer.services.compression.threshold_checker.TokenCounter.count_message_tokens",
return_value=6000,
)
def test_check_message_tokens_above_threshold(self, mock_count, mock_limit):
@@ -26,11 +26,11 @@ class TestCompressionThresholdChecker:
assert checker.check_message_tokens([{"role": "user"}], "gpt-4") is True
@patch(
"application.api.answer.services.compression.threshold_checker.get_token_limit",
"docsgpt.api.answer.services.compression.threshold_checker.get_token_limit",
return_value=8000,
)
@patch(
"application.api.answer.services.compression.threshold_checker.TokenCounter.count_message_tokens",
"docsgpt.api.answer.services.compression.threshold_checker.TokenCounter.count_message_tokens",
return_value=1000,
)
def test_check_message_tokens_below_threshold(self, mock_count, mock_limit):
@@ -38,7 +38,7 @@ class TestCompressionThresholdChecker:
assert checker.check_message_tokens([{"role": "user"}], "gpt-4") is False
@patch(
"application.api.answer.services.compression.threshold_checker.TokenCounter.count_message_tokens",
"docsgpt.api.answer.services.compression.threshold_checker.TokenCounter.count_message_tokens",
side_effect=Exception("Token error"),
)
def test_check_message_tokens_exception_returns_false(self, mock_count):
@@ -1,10 +1,10 @@
"""Tests for application/api/answer/services/compression/types.py"""
"""Tests for docsgpt/api/answer/services/compression/types.py"""
from datetime import datetime, timezone
import pytest
from application.api.answer.services.compression.types import (
from docsgpt.api.answer.services.compression.types import (
CompressionMetadata,
CompressionResult,
)
@@ -1,4 +1,4 @@
"""Tests for application/api/answer/services/continuation_service.py using pg_conn."""
"""Tests for docsgpt/api/answer/services/continuation_service.py using pg_conn."""
from contextlib import contextmanager
from unittest.mock import patch
@@ -15,10 +15,10 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.answer.services.continuation_service.db_readonly",
"docsgpt.api.answer.services.continuation_service.db_readonly",
_yield,
), patch(
"application.api.answer.services.continuation_service.db_session",
"docsgpt.api.answer.services.continuation_service.db_session",
_yield,
):
yield
@@ -26,21 +26,21 @@ def _patch_db(conn):
class TestMakeSerializable:
def test_uuid_becomes_string(self):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
_make_serializable,
)
u = uuid4()
assert _make_serializable(u) == str(u)
def test_dict_keys_stringified(self):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
_make_serializable,
)
got = _make_serializable({42: "a", "b": 2})
assert got == {"42": "a", "b": 2}
def test_list_elements_recursively_serialized(self):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
_make_serializable,
)
u = uuid4()
@@ -52,7 +52,7 @@ class TestMakeSerializable:
# the shared serialization module — base64 is lossless and round-
# trippable (UTF-8-replace silently corrupted binary payloads).
import base64
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
_make_serializable,
)
got = _make_serializable(b"hello")
@@ -60,7 +60,7 @@ class TestMakeSerializable:
def test_bytes_arbitrary_binary_roundtrips(self):
import base64
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
_make_serializable,
)
raw = b"\xff\xfe\x00\x10"
@@ -69,7 +69,7 @@ class TestMakeSerializable:
assert base64.b64decode(got) == raw
def test_passes_through_primitives(self):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
_make_serializable,
)
assert _make_serializable("hello") == "hello"
@@ -83,7 +83,7 @@ class TestMakeSerializable:
# rows, which would otherwise blow up json.dumps in pending_tool_state.
import json
from datetime import datetime, timezone
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
_make_serializable,
)
@@ -97,7 +97,7 @@ class TestMakeSerializable:
# where each tool row has timestamp fields buried under string keys.
import json
from datetime import datetime, timezone
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
_make_serializable,
)
@@ -116,7 +116,7 @@ class TestMakeSerializable:
def test_date_becomes_iso_string(self):
from datetime import date
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
_make_serializable,
)
assert _make_serializable(date(2026, 5, 2)) == "2026-05-02"
@@ -124,10 +124,10 @@ class TestMakeSerializable:
class TestContinuationServiceSaveLoad:
def test_save_and_load_state(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -152,7 +152,7 @@ class TestContinuationServiceSaveLoad:
assert loaded["messages"] == [{"role": "user", "content": "hi"}]
def test_load_state_returns_none_when_not_found(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
@@ -164,11 +164,11 @@ class TestContinuationServiceSaveLoad:
assert got is None
def test_claim_state_is_atomic_and_duplicate_is_conflict(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
ResumeInProgressError,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -193,10 +193,10 @@ class TestContinuationServiceSaveLoad:
service.claim_state(conv_id, user)
def test_expired_state_is_neither_loaded_nor_claimed(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -226,10 +226,10 @@ class TestContinuationServiceSaveLoad:
assert service.claim_state(conv_id, user) is None
def test_save_state_no_client_tools(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -253,10 +253,10 @@ class TestContinuationServiceSaveLoad:
assert loaded is not None
def test_delete_state_returns_true_when_deleted(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -279,7 +279,7 @@ class TestContinuationServiceSaveLoad:
assert got is True
def test_delete_state_returns_false_when_missing(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
@@ -301,7 +301,7 @@ class TestContinuationServiceLegacyIdHandling:
LEGACY_OBJECTID = "507f1f77bcf86cd799439011"
def test_load_state_unresolvable_legacy_id_returns_none(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
@@ -311,7 +311,7 @@ class TestContinuationServiceLegacyIdHandling:
assert got is None
def test_delete_state_unresolvable_legacy_id_returns_false(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
@@ -323,7 +323,7 @@ class TestContinuationServiceLegacyIdHandling:
def test_save_state_unresolvable_legacy_id_raises(self, pg_conn):
import pytest
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
@@ -341,10 +341,10 @@ class TestContinuationServiceLegacyIdHandling:
)
def test_load_state_resolves_backfilled_legacy_id(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -1,4 +1,4 @@
"""Tests for application/api/answer/services/conversation_service.py."""
"""Tests for docsgpt/api/answer/services/conversation_service.py."""
from contextlib import contextmanager
from datetime import datetime, timezone
@@ -14,10 +14,10 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.answer.services.conversation_service.db_session",
"docsgpt.api.answer.services.conversation_service.db_session",
_yield,
), patch(
"application.api.answer.services.conversation_service.db_readonly",
"docsgpt.api.answer.services.conversation_service.db_readonly",
_yield,
):
yield
@@ -25,13 +25,13 @@ def _patch_db(conn):
class TestConversationServiceGet:
def test_returns_none_when_no_conversation_id(self):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
assert ConversationService().get_conversation("", "u") is None
def test_returns_none_when_no_user_id(self):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
assert (
@@ -42,7 +42,7 @@ class TestConversationServiceGet:
)
def test_returns_none_when_not_found(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
with _patch_db(pg_conn):
@@ -52,10 +52,10 @@ class TestConversationServiceGet:
assert got is None
def test_returns_conversation_with_messages(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -74,7 +74,7 @@ class TestConversationServiceGet:
assert len(got["queries"]) == 1
def test_handles_exception_returns_none(self):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -84,7 +84,7 @@ class TestConversationServiceGet:
yield
with patch(
"application.api.answer.services.conversation_service.db_readonly",
"docsgpt.api.answer.services.conversation_service.db_readonly",
_broken,
):
got = ConversationService().get_conversation("abc", "u")
@@ -93,7 +93,7 @@ class TestConversationServiceGet:
class TestConversationServiceSave:
def test_raises_for_none_token(self):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
with pytest.raises(ValueError):
@@ -105,7 +105,7 @@ class TestConversationServiceSave:
)
def test_raises_when_no_user_in_token(self):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
with pytest.raises(ValueError):
@@ -117,10 +117,10 @@ class TestConversationServiceSave:
)
def test_creates_new_conversation(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -141,10 +141,10 @@ class TestConversationServiceSave:
assert got["name"] == "Title"
def test_appends_message_to_existing(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -166,10 +166,10 @@ class TestConversationServiceSave:
assert any(m["prompt"] == "q-new" for m in messages)
def test_updates_message_at_index(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -193,7 +193,7 @@ class TestConversationServiceSave:
assert messages[0]["prompt"] == "updated"
def test_raises_when_conversation_missing(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
with _patch_db(pg_conn), pytest.raises(ValueError):
@@ -206,10 +206,10 @@ class TestConversationServiceSave:
)
def test_save_with_empty_llm_title_falls_back(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -231,11 +231,11 @@ class TestConversationServiceSave:
class TestSaveUserQuestion:
def test_creates_conversation_and_reserves_message(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
TERMINATED_RESPONSE_PLACEHOLDER,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -261,10 +261,10 @@ class TestSaveUserQuestion:
assert messages[0]["request_id"] == result["request_id"]
def test_appends_to_existing_conversation(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -285,7 +285,7 @@ class TestSaveUserQuestion:
assert msgs[0]["prompt"] == "follow-up"
def test_raises_when_token_missing(self):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
with pytest.raises(ValueError):
@@ -302,10 +302,10 @@ class TestSaveUserQuestion:
the end. Pre-fix the WAL path appended unconditionally and the
old answer survived alongside the regenerated one.
"""
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -356,10 +356,10 @@ class TestSaveUserQuestion:
"""``index=0`` is a valid edge: it should drop every prior
message and reseat the placeholder at position 0.
"""
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -398,10 +398,10 @@ class TestSaveUserQuestion:
the create-then-reserve path silently treats it as a no-op
rather than truncating a freshly-created conversation.
"""
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -421,7 +421,7 @@ class TestSaveUserQuestion:
assert msgs[0]["prompt"] == "brand new q"
def test_raises_when_conversation_unauthorized(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
with _patch_db(pg_conn), pytest.raises(ValueError):
@@ -434,10 +434,10 @@ class TestSaveUserQuestion:
class TestFinalizeMessage:
def test_finalizes_complete(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -449,7 +449,7 @@ class TestFinalizeMessage:
question="q",
decoded_token={"sub": user},
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
MessageUpdateOutcome,
)
assert svc.finalize_message(
@@ -477,10 +477,10 @@ class TestFinalizeMessage:
def test_finalize_strips_null_bytes(self, pg_conn):
"""A NUL-laden tool result (07-17 PDF incident) must not kill the
conversation save — Postgres rejects \\x00 in text and jsonb."""
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
MessageUpdateOutcome,
)
@@ -515,10 +515,10 @@ class TestFinalizeMessage:
assert msgs[0]["metadata"] == {"key": "val"}
def test_finalizes_failed_records_error(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -531,7 +531,7 @@ class TestFinalizeMessage:
decoded_token={"sub": user},
)
err = RuntimeError("provider down")
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
MessageUpdateOutcome,
)
assert svc.finalize_message(
@@ -552,7 +552,7 @@ class TestFinalizeMessage:
rows as 'confirmed' for the same message_id."""
from sqlalchemy import text as sql_text
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -572,7 +572,7 @@ class TestFinalizeMessage:
),
{"cid": "c1", "mid": res["message_id"]},
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
MessageUpdateOutcome,
)
assert svc.finalize_message(
@@ -586,10 +586,10 @@ class TestFinalizeMessage:
assert status == "confirmed"
def test_finalize_returns_not_found_for_unknown_message(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
MessageUpdateOutcome,
)
with _patch_db(pg_conn):
@@ -614,10 +614,10 @@ class TestFinalizeMessage:
from sqlalchemy import text as sql_text
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories import conversations as conv_module
from docsgpt.storage.db.repositories import conversations as conv_module
user = "u-fin-rollback"
@@ -632,10 +632,10 @@ class TestFinalizeMessage:
raise
with patch(
"application.api.answer.services.conversation_service.db_session",
"docsgpt.api.answer.services.conversation_service.db_session",
_savepoint_session,
), patch(
"application.api.answer.services.conversation_service.db_readonly",
"docsgpt.api.answer.services.conversation_service.db_readonly",
_savepoint_session,
):
svc = ConversationService()
@@ -686,10 +686,10 @@ class TestFinalizeMessage:
assert msg_status == "pending"
def test_finalize_generates_title_when_provided(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -703,7 +703,7 @@ class TestFinalizeMessage:
question="long question that becomes the fallback name",
decoded_token={"sub": user},
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
MessageUpdateOutcome,
)
assert svc.finalize_message(
@@ -730,10 +730,10 @@ class TestSaveUserQuestionFinalizeFailedFlow:
"""LLM fails immediately; question stays queryable with status='failed' + error metadata."""
def test_failed_llm_leaves_question_persisted(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -769,10 +769,10 @@ class TestSaveUserQuestionFinalizeFailedFlow:
class TestCompressionMetadata:
def test_update_compression_metadata(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -792,7 +792,7 @@ class TestCompressionMetadata:
)
def test_update_compression_raises_on_error(self):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -802,13 +802,13 @@ class TestCompressionMetadata:
yield
with patch(
"application.api.answer.services.conversation_service.db_session",
"docsgpt.api.answer.services.conversation_service.db_session",
_broken,
), pytest.raises(RuntimeError):
ConversationService().update_compression_metadata("abc", {})
def test_append_compression_message_skips_empty_summary(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -818,10 +818,10 @@ class TestCompressionMetadata:
)
def test_append_compression_message_appends_summary(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -844,7 +844,7 @@ class TestCompressionMetadata:
assert any(m["response"] == "A summary" for m in messages)
def test_append_compression_message_swallows_error(self):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -854,7 +854,7 @@ class TestCompressionMetadata:
yield
with patch(
"application.api.answer.services.conversation_service.db_session",
"docsgpt.api.answer.services.conversation_service.db_session",
_broken,
):
ConversationService().append_compression_message(
@@ -864,7 +864,7 @@ class TestCompressionMetadata:
def test_get_compression_metadata_returns_none_for_missing(
self, pg_conn,
):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
with _patch_db(pg_conn):
@@ -881,7 +881,7 @@ class TestCompressionMetadata:
would raise and pollute logs with a stack trace every call."""
import logging
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -896,7 +896,7 @@ class TestCompressionMetadata:
)
def test_get_compression_metadata_handles_exception(self):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -906,7 +906,7 @@ class TestCompressionMetadata:
yield
with patch(
"application.api.answer.services.conversation_service.db_readonly",
"docsgpt.api.answer.services.conversation_service.db_readonly",
_broken,
):
got = ConversationService().get_compression_metadata("abc")
@@ -2,7 +2,7 @@
import pytest
from application.api.answer.services.persistence_policy import (
from docsgpt.api.answer.services.persistence_policy import (
VISIBILITY_HIDDEN,
VISIBILITY_LISTED,
resolve_persistence,
@@ -5,7 +5,7 @@ import pytest
class TestTemplateEngine:
def test_render_simple_template(self):
from application.templates.template_engine import TemplateEngine
from docsgpt.templates.template_engine import TemplateEngine
engine = TemplateEngine()
result = engine.render("Hello {{ name }}", {"name": "World"})
@@ -13,7 +13,7 @@ class TestTemplateEngine:
assert result == "Hello World"
def test_render_with_namespace(self):
from application.templates.template_engine import TemplateEngine
from docsgpt.templates.template_engine import TemplateEngine
engine = TemplateEngine()
context = {
@@ -27,7 +27,7 @@ class TestTemplateEngine:
assert result == "Alice is a admin on 2025-10-22"
def test_render_empty_template(self):
from application.templates.template_engine import TemplateEngine
from docsgpt.templates.template_engine import TemplateEngine
engine = TemplateEngine()
result = engine.render("", {"key": "value"})
@@ -35,7 +35,7 @@ class TestTemplateEngine:
assert result == ""
def test_render_template_without_variables(self):
from application.templates.template_engine import TemplateEngine
from docsgpt.templates.template_engine import TemplateEngine
engine = TemplateEngine()
result = engine.render("Just plain text", {})
@@ -43,7 +43,7 @@ class TestTemplateEngine:
assert result == "Just plain text"
def test_render_undefined_variable_returns_empty_string(self):
from application.templates.template_engine import TemplateEngine
from docsgpt.templates.template_engine import TemplateEngine
engine = TemplateEngine()
@@ -51,7 +51,7 @@ class TestTemplateEngine:
assert result == "Hello "
def test_render_syntax_error_raises_error(self):
from application.templates.template_engine import (
from docsgpt.templates.template_engine import (
TemplateEngine,
TemplateRenderError,
)
@@ -62,7 +62,7 @@ class TestTemplateEngine:
engine.render("Hello {{ name", {"name": "World"})
def test_render_blocks_unsafe_attribute_access(self):
from application.templates.template_engine import (
from docsgpt.templates.template_engine import (
TemplateEngine,
TemplateRenderError,
)
@@ -74,25 +74,25 @@ class TestTemplateEngine:
engine.render(payload, {})
def test_validate_template_valid(self):
from application.templates.template_engine import TemplateEngine
from docsgpt.templates.template_engine import TemplateEngine
engine = TemplateEngine()
assert engine.validate_template("Valid {{ variable }}") is True
def test_validate_template_invalid(self):
from application.templates.template_engine import TemplateEngine
from docsgpt.templates.template_engine import TemplateEngine
engine = TemplateEngine()
assert engine.validate_template("Invalid {{ variable") is False
def test_validate_empty_template(self):
from application.templates.template_engine import TemplateEngine
from docsgpt.templates.template_engine import TemplateEngine
engine = TemplateEngine()
assert engine.validate_template("") is True
def test_extract_variables(self):
from application.templates.template_engine import TemplateEngine
from docsgpt.templates.template_engine import TemplateEngine
engine = TemplateEngine()
template = "{{ user.name }} and {{ user.email }}"
@@ -106,7 +106,7 @@ class TestTemplateEngine:
class TestSystemNamespace:
def test_system_namespace_build(self):
from application.templates.namespaces import SystemNamespace
from docsgpt.templates.namespaces import SystemNamespace
builder = SystemNamespace()
context = builder.build(
@@ -120,7 +120,7 @@ class TestSystemNamespace:
assert "timestamp" in context
def test_system_namespace_generates_request_id(self):
from application.templates.namespaces import SystemNamespace
from docsgpt.templates.namespaces import SystemNamespace
builder = SystemNamespace()
context = builder.build(user_id="user_123")
@@ -129,13 +129,13 @@ class TestSystemNamespace:
assert len(context["request_id"]) > 0
def test_system_namespace_name(self):
from application.templates.namespaces import SystemNamespace
from docsgpt.templates.namespaces import SystemNamespace
builder = SystemNamespace()
assert builder.namespace_name == "system"
def test_system_namespace_date_format(self):
from application.templates.namespaces import SystemNamespace
from docsgpt.templates.namespaces import SystemNamespace
builder = SystemNamespace()
context = builder.build()
@@ -150,7 +150,7 @@ class TestSystemNamespace:
class TestPassthroughNamespace:
def test_passthrough_namespace_build(self):
from application.templates.namespaces import PassthroughNamespace
from docsgpt.templates.namespaces import PassthroughNamespace
builder = PassthroughNamespace()
passthrough_data = {"company": "Acme", "user_name": "John", "count": 42}
@@ -162,7 +162,7 @@ class TestPassthroughNamespace:
assert context["count"] == 42
def test_passthrough_namespace_empty(self):
from application.templates.namespaces import PassthroughNamespace
from docsgpt.templates.namespaces import PassthroughNamespace
builder = PassthroughNamespace()
context = builder.build(passthrough_data=None)
@@ -170,7 +170,7 @@ class TestPassthroughNamespace:
assert context == {}
def test_passthrough_namespace_filters_unsafe_values(self):
from application.templates.namespaces import PassthroughNamespace
from docsgpt.templates.namespaces import PassthroughNamespace
builder = PassthroughNamespace()
passthrough_data = {
@@ -190,7 +190,7 @@ class TestPassthroughNamespace:
assert "unsafe_list" not in context
def test_passthrough_namespace_allows_none_values(self):
from application.templates.namespaces import PassthroughNamespace
from docsgpt.templates.namespaces import PassthroughNamespace
builder = PassthroughNamespace()
passthrough_data = {"nullable_field": None}
@@ -200,7 +200,7 @@ class TestPassthroughNamespace:
assert context["nullable_field"] is None
def test_passthrough_namespace_name(self):
from application.templates.namespaces import PassthroughNamespace
from docsgpt.templates.namespaces import PassthroughNamespace
builder = PassthroughNamespace()
assert builder.namespace_name == "passthrough"
@@ -210,7 +210,7 @@ class TestPassthroughNamespace:
class TestSourceNamespace:
def test_source_namespace_build_with_docs(self):
from application.templates.namespaces import SourceNamespace
from docsgpt.templates.namespaces import SourceNamespace
builder = SourceNamespace()
docs = [
@@ -227,7 +227,7 @@ class TestSourceNamespace:
assert context["summaries"] == docs_together
def test_source_namespace_build_empty(self):
from application.templates.namespaces import SourceNamespace
from docsgpt.templates.namespaces import SourceNamespace
builder = SourceNamespace()
context = builder.build(docs=None, docs_together=None)
@@ -235,7 +235,7 @@ class TestSourceNamespace:
assert context == {}
def test_source_namespace_build_docs_only(self):
from application.templates.namespaces import SourceNamespace
from docsgpt.templates.namespaces import SourceNamespace
builder = SourceNamespace()
docs = [{"text": "Doc 1"}]
@@ -247,7 +247,7 @@ class TestSourceNamespace:
assert "content" not in context
def test_source_namespace_build_docs_together_only(self):
from application.templates.namespaces import SourceNamespace
from docsgpt.templates.namespaces import SourceNamespace
builder = SourceNamespace()
docs_together = "Content here"
@@ -259,7 +259,7 @@ class TestSourceNamespace:
assert "documents" not in context
def test_source_namespace_name(self):
from application.templates.namespaces import SourceNamespace
from docsgpt.templates.namespaces import SourceNamespace
builder = SourceNamespace()
assert builder.namespace_name == "source"
@@ -269,7 +269,7 @@ class TestSourceNamespace:
class TestToolsNamespace:
def test_tools_namespace_build_with_memory_data(self):
from application.templates.namespaces import ToolsNamespace
from docsgpt.templates.namespaces import ToolsNamespace
builder = ToolsNamespace()
tools_data = {
@@ -282,7 +282,7 @@ class TestToolsNamespace:
assert context["memory"]["available"] is True
def test_tools_namespace_build_empty(self):
from application.templates.namespaces import ToolsNamespace
from docsgpt.templates.namespaces import ToolsNamespace
builder = ToolsNamespace()
context = builder.build(tools_data=None)
@@ -290,7 +290,7 @@ class TestToolsNamespace:
assert context == {}
def test_tools_namespace_build_multiple_tools(self):
from application.templates.namespaces import ToolsNamespace
from docsgpt.templates.namespaces import ToolsNamespace
builder = ToolsNamespace()
tools_data = {
@@ -309,7 +309,7 @@ class TestToolsNamespace:
assert context["api"]["status"] == "success"
def test_tools_namespace_filters_unsafe_values(self):
from application.templates.namespaces import ToolsNamespace
from docsgpt.templates.namespaces import ToolsNamespace
builder = ToolsNamespace()
@@ -324,13 +324,13 @@ class TestToolsNamespace:
assert "unsafe_tool" not in context
def test_tools_namespace_name(self):
from application.templates.namespaces import ToolsNamespace
from docsgpt.templates.namespaces import ToolsNamespace
builder = ToolsNamespace()
assert builder.namespace_name == "tools"
def test_tools_namespace_with_empty_dict(self):
from application.templates.namespaces import ToolsNamespace
from docsgpt.templates.namespaces import ToolsNamespace
builder = ToolsNamespace()
context = builder.build(tools_data={})
@@ -342,7 +342,7 @@ class TestToolsNamespace:
class TestNamespaceManagerWithTools:
def test_namespace_manager_includes_tools_in_context(self):
from application.templates.namespaces import NamespaceManager
from docsgpt.templates.namespaces import NamespaceManager
manager = NamespaceManager()
tools_data = {"memory": {"root": "content", "available": True}}
@@ -353,7 +353,7 @@ class TestNamespaceManagerWithTools:
assert context["tools"]["memory"]["root"] == "content"
def test_namespace_manager_build_context_all_namespaces(self):
from application.templates.namespaces import NamespaceManager
from docsgpt.templates.namespaces import NamespaceManager
manager = NamespaceManager()
context = manager.build_context(
@@ -371,7 +371,7 @@ class TestNamespaceManagerWithTools:
assert context["tools"]["memory"]["root"] == "notes"
def test_namespace_manager_build_context_partial_data(self):
from application.templates.namespaces import NamespaceManager
from docsgpt.templates.namespaces import NamespaceManager
manager = NamespaceManager()
context = manager.build_context(request_id="req_123")
@@ -386,13 +386,13 @@ class TestPromptRendererArtifactParent:
def test_render_resolves_artifact_by_id_with_conversation_parent(self):
from unittest.mock import patch
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
row = {"id": "art-1", "current_version": 1, "kind": "document", "title": "Report"}
version = {"mime_type": "text/html", "filename": "report.html", "size": 12}
with patch(
"application.storage.db.repositories.artifacts.ArtifactsRepository"
) as repo_cls, patch("application.storage.db.session.db_readonly"):
"docsgpt.storage.db.repositories.artifacts.ArtifactsRepository"
) as repo_cls, patch("docsgpt.storage.db.session.db_readonly"):
repo = repo_cls.return_value
repo.get_artifact_in_parent.return_value = row
repo.get_version.return_value = version
@@ -407,7 +407,7 @@ class TestPromptRendererArtifactParent:
assert kwargs.get("conversation_id") == "conv-1"
def test_render_without_parent_yields_empty_lookup(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
# No artifact_parent (e.g. a first-turn conversation_id is None) must not error;
# the lookup returns an empty mapping and the attribute renders blank.
@@ -418,7 +418,7 @@ class TestPromptRendererArtifactParent:
assert rendered == "[]"
def test_namespace_manager_get_builder(self):
from application.templates.namespaces import NamespaceManager, SystemNamespace
from docsgpt.templates.namespaces import NamespaceManager, SystemNamespace
manager = NamespaceManager()
builder = manager.get_builder("system")
@@ -426,7 +426,7 @@ class TestPromptRendererArtifactParent:
assert isinstance(builder, SystemNamespace)
def test_namespace_manager_get_builder_nonexistent(self):
from application.templates.namespaces import NamespaceManager
from docsgpt.templates.namespaces import NamespaceManager
manager = NamespaceManager()
builder = manager.get_builder("nonexistent")
@@ -436,7 +436,7 @@ class TestPromptRendererArtifactParent:
def test_namespace_manager_handles_builder_exceptions(self):
from unittest.mock import patch
from application.templates.namespaces import NamespaceManager
from docsgpt.templates.namespaces import NamespaceManager
manager = NamespaceManager()
@@ -456,7 +456,7 @@ class TestPromptRendererArtifactParent:
class TestPromptRenderer:
def test_render_prompt_with_template_syntax(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Hello {{ system.user_id }}, today is {{ system.date }}"
@@ -467,7 +467,7 @@ class TestPromptRenderer:
assert "202" in result
def test_render_prompt_with_passthrough_data(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Company: {{ passthrough.company }}\nUser: {{ passthrough.user_name }}"
@@ -479,7 +479,7 @@ class TestPromptRenderer:
assert "User: John" in result
def test_render_prompt_with_source_docs(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Use this information:\n{{ source.content }}"
@@ -491,7 +491,7 @@ class TestPromptRenderer:
assert "Important document content" in result
def test_render_prompt_empty_content(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
result = renderer.render_prompt("")
@@ -499,7 +499,7 @@ class TestPromptRenderer:
assert result == ""
def test_render_prompt_legacy_format_with_summaries(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Context: {summaries}\nQuestion: What is this?"
@@ -510,7 +510,7 @@ class TestPromptRenderer:
assert "Context: This is the document content" in result
def test_render_prompt_legacy_format_without_docs(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Context: {summaries}\nQuestion: What is this?"
@@ -522,7 +522,7 @@ class TestPromptRenderer:
assert "Question: What is this?" in result
def test_render_prompt_combined_namespace_variables(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "User: {{ passthrough.user }}, Date: {{ system.date }}, Docs: {{ source.content }}"
@@ -540,7 +540,7 @@ class TestPromptRenderer:
assert "Doc content" in result
def test_render_prompt_with_tools_data(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Memory contents:\n{{ tools.memory.root }}\n\nStatus: {{ tools.memory.available }}"
@@ -557,7 +557,7 @@ class TestPromptRenderer:
assert "Status: True" in result
def test_render_prompt_with_all_namespaces(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = """
@@ -583,7 +583,7 @@ Memory: {{ tools.memory.root }}
assert "Notes content" in result
def test_render_prompt_undefined_variable_returns_empty_string(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Hello {{ undefined_var }}"
@@ -592,7 +592,7 @@ Memory: {{ tools.memory.root }}
assert result == "Hello "
def test_render_prompt_with_undefined_variable_in_template(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Hello {{ undefined_name }}"
@@ -601,19 +601,19 @@ Memory: {{ tools.memory.root }}
assert result == "Hello "
def test_validate_template_valid(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
assert renderer.validate_template("Valid {{ variable }}") is True
def test_validate_template_invalid(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
assert renderer.validate_template("Invalid {{ variable") is False
def test_extract_variables(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
template = "{{ var1 }} and {{ var2 }}"
@@ -623,7 +623,7 @@ Memory: {{ tools.memory.root }}
assert isinstance(result, set)
def test_uses_template_syntax_detection(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
@@ -632,7 +632,7 @@ Memory: {{ tools.memory.root }}
assert renderer._uses_template_syntax("Plain text") is False
def test_apply_legacy_substitutions(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Use {summaries} to answer"
@@ -643,7 +643,7 @@ Memory: {{ tools.memory.root }}
assert "Use Important info to answer" in result
def test_apply_legacy_substitutions_without_docs(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Use {summaries} to answer"
@@ -658,7 +658,7 @@ Memory: {{ tools.memory.root }}
class TestPromptRendererIntegration:
def test_render_prompt_real_world_scenario(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "You are helping {{ passthrough.company }}.\n\nUser: {{ passthrough.user_name }}\n\nRequest ID: {{ system.request_id }}\n\nDate: {{ system.date }}\n\nReference Documents:\n\n{{ source.content }}\n\nPlease answer the question professionally."
@@ -681,7 +681,7 @@ class TestPromptRendererIntegration:
assert "professionally" in result
def test_render_prompt_multiple_doc_references(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = """Documents: {{ source.content }} \n\nAlso summaries: {{ source.summaries }}"""
@@ -700,7 +700,7 @@ class TestPromptRendererIntegration:
class TestStreamProcessorPromptRendering:
def test_stream_processor_pre_fetch_docs_none_doc_mode(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
request_data = {"question": "Test question", "isNoneDoc": True}
processor = StreamProcessor(request_data, None)
@@ -711,8 +711,8 @@ class TestStreamProcessorPromptRendering:
assert docs_list is None
def test_pre_fetch_tools_disabled_globally(self, monkeypatch):
from application.api.answer.services.stream_processor import StreamProcessor
from application.core.settings import settings
from docsgpt.api.answer.services.stream_processor import StreamProcessor
from docsgpt.core.settings import settings
monkeypatch.setattr(settings, "ENABLE_TOOL_PREFETCH", False)
@@ -724,7 +724,7 @@ class TestStreamProcessorPromptRendering:
assert result is None
def test_pre_fetch_tools_disabled_per_request(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
request_data = {"question": "test", "disable_tool_prefetch": True}
processor = StreamProcessor(request_data, {"sub": "user1"})
@@ -736,8 +736,8 @@ class TestStreamProcessorPromptRendering:
def test_pre_fetch_tools_skips_tool_with_no_actions(self, mock_mongo_db):
from unittest.mock import MagicMock, patch
from application.api.answer.services.stream_processor import StreamProcessor
from application.core.settings import settings
from docsgpt.api.answer.services.stream_processor import StreamProcessor
from docsgpt.core.settings import settings
db = mock_mongo_db[settings.MONGO_DB_NAME]
db["user_tools"].insert_one(
@@ -753,7 +753,7 @@ class TestStreamProcessorPromptRendering:
processor = StreamProcessor(request_data, {"sub": "user1"})
with patch(
"application.agents.tools.tool_manager.ToolManager"
"docsgpt.agents.tools.tool_manager.ToolManager"
) as mock_manager_class:
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
@@ -773,8 +773,8 @@ class TestStreamProcessorPromptRendering:
def test_pre_fetch_tools_enabled_by_default(self, mock_mongo_db, monkeypatch):
from unittest.mock import MagicMock, patch
from application.api.answer.services.stream_processor import StreamProcessor
from application.core.settings import settings
from docsgpt.api.answer.services.stream_processor import StreamProcessor
from docsgpt.core.settings import settings
db = mock_mongo_db[settings.MONGO_DB_NAME]
db["user_tools"].insert_one(
@@ -790,7 +790,7 @@ class TestStreamProcessorPromptRendering:
processor = StreamProcessor(request_data, {"sub": "user1"})
with patch(
"application.agents.tools.tool_manager.ToolManager"
"docsgpt.agents.tools.tool_manager.ToolManager"
) as mock_manager_class:
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
@@ -812,7 +812,7 @@ class TestStreamProcessorPromptRendering:
assert "memory_ls" in result["memory"]
def test_pre_fetch_tools_no_tools_configured(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
request_data = {"question": "test"}
processor = StreamProcessor(request_data, {"sub": "user1"})
@@ -824,8 +824,8 @@ class TestStreamProcessorPromptRendering:
def test_pre_fetch_tools_memory_returns_error(self, mock_mongo_db):
from unittest.mock import MagicMock, patch
from application.api.answer.services.stream_processor import StreamProcessor
from application.core.settings import settings
from docsgpt.api.answer.services.stream_processor import StreamProcessor
from docsgpt.core.settings import settings
db = mock_mongo_db[settings.MONGO_DB_NAME]
db["user_tools"].insert_one(
@@ -841,7 +841,7 @@ class TestStreamProcessorPromptRendering:
processor = StreamProcessor(request_data, {"sub": "user1"})
with patch(
"application.agents.tools.tool_manager.ToolManager"
"docsgpt.agents.tools.tool_manager.ToolManager"
) as mock_manager_class:
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
@@ -864,8 +864,8 @@ class TestStreamProcessorPromptRendering:
def test_pre_fetch_tools_memory_returns_empty(self, mock_mongo_db):
from unittest.mock import MagicMock, patch
from application.api.answer.services.stream_processor import StreamProcessor
from application.core.settings import settings
from docsgpt.api.answer.services.stream_processor import StreamProcessor
from docsgpt.core.settings import settings
db = mock_mongo_db[settings.MONGO_DB_NAME]
db["user_tools"].insert_one(
@@ -881,7 +881,7 @@ class TestStreamProcessorPromptRendering:
processor = StreamProcessor(request_data, {"sub": "user1"})
with patch(
"application.agents.tools.tool_manager.ToolManager"
"docsgpt.agents.tools.tool_manager.ToolManager"
) as mock_manager_class:
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
@@ -1,4 +1,4 @@
"""Tests for application/api/answer/services/stream_processor.py.
"""Tests for docsgpt/api/answer/services/stream_processor.py.
The previous suite was tightly coupled to Mongo (mock_mongo_db fixture,
bson.ObjectId, bson.DBRef, find_one, etc.) which no longer exist after the
@@ -24,7 +24,7 @@ class TestStreamProcessorInitialization:
pass
def test_initializes_with_decoded_token(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
conv_id = _STATIC_OID
request_data = {
@@ -41,7 +41,7 @@ class TestStreamProcessorInitialization:
assert processor.conversation_id == request_data["conversation_id"]
def test_initializes_without_token(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
request_data = {"question": "Test question"}
@@ -52,7 +52,7 @@ class TestStreamProcessorInitialization:
assert processor.data == request_data
def test_initializes_default_attributes(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
processor = StreamProcessor({"question": "Test"}, {"sub": "user_123"})
@@ -66,7 +66,7 @@ class TestStreamProcessorInitialization:
assert processor.shared_token is None
def test_extracts_conversation_id_from_request(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
conv_id = _STATIC_OID
request_data = {"question": "Test", "conversation_id": conv_id}
@@ -81,7 +81,7 @@ class TestStreamProcessorHistoryLoading:
pass
def test_uses_request_history_when_no_conversation_id(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
request_data = {
"question": "What is Python?",
@@ -98,7 +98,7 @@ class TestStreamProcessorAgentConfiguration:
pass
def test_uses_default_config_without_api_key(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
request_data = {"question": "Test"}
@@ -110,7 +110,7 @@ class TestStreamProcessorAgentConfiguration:
def test_embedded_workflow_without_saved_id(self):
"""A preview run with no saved workflow id carries no ``workflow_id``."""
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
request_data = {
"question": "Test",
@@ -126,7 +126,7 @@ class TestStreamProcessorAgentConfiguration:
def test_embedded_workflow_with_saved_id_persists_run(self):
"""A saved workflow id alongside the embedded graph is captured so the
run can persist a ``workflow_runs`` row for artifact listing."""
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
request_data = {
"question": "Test",
@@ -152,7 +152,7 @@ class TestStreamProcessorDocPrefetch:
def test_prefetch_skipped_when_no_active_docs(self):
from unittest.mock import MagicMock
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
processor = StreamProcessor(
{"question": "Hi there"},
@@ -170,7 +170,7 @@ class TestStreamProcessorDocPrefetch:
def test_prefetch_skipped_when_active_docs_is_default(self):
from unittest.mock import MagicMock
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
processor = StreamProcessor(
{"question": "Hi", "active_docs": "default"},
@@ -194,7 +194,7 @@ class TestStreamProcessorAttachments:
pass
def test_handles_empty_attachments(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
request_data = {"question": "Simple question"}
@@ -225,7 +225,7 @@ class TestBuildContinuationFromMessages:
def _make_processor():
from unittest.mock import MagicMock
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
processor = StreamProcessor({"question": ""}, {"sub": "user_123"})
fake_agent = MagicMock()
@@ -1,4 +1,4 @@
"""Targeted tests for application/api/answer/services/stream_processor.py.
"""Targeted tests for docsgpt/api/answer/services/stream_processor.py.
Tests the ``get_prompt`` helper and simpler StreamProcessor methods against
real ephemeral Postgres.
@@ -9,7 +9,7 @@ from unittest.mock import patch
import pytest
import application.api.answer.services.stream_processor as sp_mod
import docsgpt.api.answer.services.stream_processor as sp_mod
@contextmanager
@@ -19,46 +19,46 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.answer.services.stream_processor.db_readonly", _yield
"docsgpt.api.answer.services.stream_processor.db_readonly", _yield
), patch(
"application.api.answer.services.stream_processor.db_session", _yield
"docsgpt.api.answer.services.stream_processor.db_session", _yield
):
yield
class TestGetPrompt:
def test_default_preset(self):
from application.api.answer.services.stream_processor import get_prompt
from docsgpt.api.answer.services.stream_processor import get_prompt
got = get_prompt("default")
assert isinstance(got, str)
assert len(got) > 0
def test_creative_preset(self):
from application.api.answer.services.stream_processor import get_prompt
from docsgpt.api.answer.services.stream_processor import get_prompt
got = get_prompt("creative")
assert isinstance(got, str) and len(got) > 0
def test_strict_preset(self):
from application.api.answer.services.stream_processor import get_prompt
from docsgpt.api.answer.services.stream_processor import get_prompt
got = get_prompt("strict")
assert isinstance(got, str) and len(got) > 0
def test_agentic_default_preset(self):
from application.api.answer.services.stream_processor import get_prompt
from docsgpt.api.answer.services.stream_processor import get_prompt
got = get_prompt("agentic_default")
assert isinstance(got, str) and len(got) > 0
def test_none_defaults_to_default(self):
from application.api.answer.services.stream_processor import get_prompt
from docsgpt.api.answer.services.stream_processor import get_prompt
got = get_prompt(None)
assert isinstance(got, str) and len(got) > 0
def test_empty_string_defaults_to_default(self):
from application.api.answer.services.stream_processor import get_prompt
from docsgpt.api.answer.services.stream_processor import get_prompt
assert get_prompt("") == get_prompt("default")
def test_non_string_id_converted(self):
from application.api.answer.services.stream_processor import get_prompt
from docsgpt.api.answer.services.stream_processor import get_prompt
# A UUID object would be stringified; use an int to test the branch
with pytest.raises(ValueError):
# Int converts to str "42" which isn't a preset, and will
@@ -66,18 +66,18 @@ class TestGetPrompt:
get_prompt(42)
def test_unknown_prompt_id_raises(self, pg_conn):
from application.api.answer.services.stream_processor import get_prompt
from docsgpt.api.answer.services.stream_processor import get_prompt
with _patch_db(pg_conn), pytest.raises(ValueError):
get_prompt("00000000-0000-0000-0000-000000000000")
def test_legacy_id_unknown_raises(self, pg_conn):
from application.api.answer.services.stream_processor import get_prompt
from docsgpt.api.answer.services.stream_processor import get_prompt
with _patch_db(pg_conn), pytest.raises(ValueError):
get_prompt("507f1f77bcf86cd799439011")
def test_uuid_lookup_returns_content(self, pg_conn):
from application.api.answer.services.stream_processor import get_prompt
from application.storage.db.repositories.prompts import (
from docsgpt.api.answer.services.stream_processor import get_prompt
from docsgpt.storage.db.repositories.prompts import (
PromptsRepository,
)
@@ -91,7 +91,7 @@ class TestGetPrompt:
class TestStreamProcessorInit:
def test_basic_init(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
data = {"question": "hi", "conversation_id": "conv-1"}
@@ -103,7 +103,7 @@ class TestStreamProcessorInit:
assert sp.conversation_id == "conv-1"
def test_init_no_token(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"question": "hi"}, None)
@@ -112,7 +112,7 @@ class TestStreamProcessorInit:
assert sp.conversation_id is None
def test_init_sets_agent_id_from_data(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
data = {"question": "hi", "agent_id": "agent-xyz"}
@@ -122,7 +122,7 @@ class TestStreamProcessorInit:
class TestLoadConversationHistory:
def test_no_conversation_id_uses_request_history(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
import json as _json
@@ -136,10 +136,10 @@ class TestLoadConversationHistory:
assert len(sp.history) == 1
def test_loads_existing_conversation_history(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -162,7 +162,7 @@ class TestLoadConversationHistory:
)
# Also patch conversation_service.get_conversation's DB accessor
with _patch_db(pg_conn), patch(
"application.api.answer.services.conversation_service.db_readonly",
"docsgpt.api.answer.services.conversation_service.db_readonly",
) as mock_readonly:
@contextmanager
def _yield():
@@ -172,10 +172,10 @@ class TestLoadConversationHistory:
assert len(sp.history) == 2
def test_unauthorized_conversation_raises(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -186,7 +186,7 @@ class TestLoadConversationHistory:
{"sub": "hacker"},
)
with _patch_db(pg_conn), patch(
"application.api.answer.services.conversation_service.db_readonly"
"docsgpt.api.answer.services.conversation_service.db_readonly"
) as mock_readonly:
@contextmanager
def _yield():
@@ -198,7 +198,7 @@ class TestLoadConversationHistory:
class TestHasActiveDocs:
def test_false_when_no_active_docs(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"question": "q"}, {"sub": "u"})
@@ -207,7 +207,7 @@ class TestHasActiveDocs:
assert sp._has_active_docs() is False
def test_true_when_source_active(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"question": "q"}, {"sub": "u"})
@@ -215,7 +215,7 @@ class TestHasActiveDocs:
assert sp._has_active_docs() is True
def test_false_when_source_empty(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"question": "q"}, {"sub": "u"})
@@ -223,7 +223,7 @@ class TestHasActiveDocs:
assert sp._has_active_docs() is False
def test_default_returns_false(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"question": "q"}, {"sub": "u"})
@@ -234,7 +234,7 @@ class TestHasActiveDocs:
class TestProcessAttachments:
def test_no_attachments(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"question": "q"}, {"sub": "u"})
@@ -244,10 +244,10 @@ class TestProcessAttachments:
assert sp.attachments == []
def test_retrieves_attachments_by_id(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
from application.storage.db.repositories.attachments import (
from docsgpt.storage.db.repositories.attachments import (
AttachmentsRepository,
)
@@ -269,7 +269,7 @@ class TestProcessAttachments:
class TestGetAttachmentsContent:
def test_empty_list(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"question": "q"}, {"sub": "u"})
@@ -278,7 +278,7 @@ class TestGetAttachmentsContent:
assert got == []
def test_skips_missing_attachments(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"question": "q"}, {"sub": "u"})
@@ -291,14 +291,14 @@ class TestGetAttachmentsContent:
class TestResolveAgentId:
def test_returns_agent_id_from_request(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"agent_id": "req-agent"}, {"sub": "u"})
assert sp._resolve_agent_id() == "req-agent"
def test_returns_none_if_not_set(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -307,7 +307,7 @@ class TestResolveAgentId:
class TestGetAgentKey:
def test_returns_tuple_for_none_agent_id(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -315,7 +315,7 @@ class TestGetAgentKey:
assert key is None and is_shared is False and tok is None
def test_raises_for_missing_agent(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -325,10 +325,10 @@ class TestGetAgentKey:
)
def test_returns_key_for_owned_agent(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
agent = AgentsRepository(pg_conn).create(
"owner", "a", "published", key="the-key",
@@ -340,10 +340,10 @@ class TestGetAgentKey:
assert shared is False
def test_raises_on_unauthorized_access(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
agent = AgentsRepository(pg_conn).create(
"owner", "a", "published", key="k", shared=False,
@@ -372,7 +372,7 @@ def _stub_db_readonly(monkeypatch, sp_mod):
class TestConfigureSource:
def test_agent_data_with_sources_list(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -386,7 +386,7 @@ class TestConfigureSource:
assert sp.source == {"active_docs": ["s1"]}
def test_agent_data_with_single_source(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -395,7 +395,7 @@ class TestConfigureSource:
assert sp.source == {"active_docs": "src-1"}
def test_agent_data_default_source(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -449,7 +449,7 @@ class TestConfigureSource:
assert sp.source == {}
def test_request_active_docs_default(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"active_docs": "default"}, {"sub": "u"})
@@ -457,7 +457,7 @@ class TestConfigureSource:
assert sp.source == {}
def test_no_data_empty_source(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -467,7 +467,7 @@ class TestConfigureSource:
class TestConfigureRetriever:
def test_defaults(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -476,7 +476,7 @@ class TestConfigureRetriever:
assert sp.retriever_config["chunks"] == 2
def test_agent_overrides(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -486,7 +486,7 @@ class TestConfigureRetriever:
assert sp.retriever_config["chunks"] == 5
def test_agent_wins_over_request_on_agent_bound(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor(
@@ -498,7 +498,7 @@ class TestConfigureRetriever:
assert sp.retriever_config["chunks"] == 5
def test_body_wins_on_agentless(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor(
@@ -510,7 +510,7 @@ class TestConfigureRetriever:
def test_agent_bound_drops_body_chunks_and_retriever(self):
# Missing agent values fall back to system defaults, not body's.
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor(
@@ -522,7 +522,7 @@ class TestConfigureRetriever:
assert sp.retriever_config["chunks"] == 2
def test_invalid_agent_chunks_falls_back(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -531,7 +531,7 @@ class TestConfigureRetriever:
assert sp.retriever_config["chunks"] == 2
def test_invalid_request_chunks_falls_back(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"chunks": "abc"}, {"sub": "u"})
@@ -539,7 +539,7 @@ class TestConfigureRetriever:
assert sp.retriever_config["chunks"] == 2
def test_isnonedoc_without_api_key_sets_chunks_to_0(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"isNoneDoc": True}, {"sub": "u"})
@@ -550,10 +550,10 @@ class TestConfigureRetriever:
class TestGetPromptContent:
def test_gets_from_agent_config(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
from application.storage.db.repositories.prompts import (
from docsgpt.storage.db.repositories.prompts import (
PromptsRepository,
)
@@ -573,10 +573,10 @@ class TestGetPromptContent:
assert "{{ system.persona }}" in content
def test_templated_custom_prompt_is_left_alone(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
from application.storage.db.repositories.prompts import (
from docsgpt.storage.db.repositories.prompts import (
PromptsRepository,
)
@@ -592,7 +592,7 @@ class TestGetPromptContent:
assert sp._persona is None
def test_returns_none_on_missing(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
@@ -605,7 +605,7 @@ class TestGetPromptContent:
assert content is None
def test_caches_prompt_content(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -614,7 +614,7 @@ class TestGetPromptContent:
assert sp._get_prompt_content() == "cached"
def test_agentic_agent_gets_agentic_preset(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -624,7 +624,7 @@ class TestGetPromptContent:
assert "source.summaries" not in content
def test_research_agent_gets_agentic_preset(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -633,7 +633,7 @@ class TestGetPromptContent:
assert "`search` tool" in content
def test_classic_agent_gets_classic_preset(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -644,7 +644,7 @@ class TestGetPromptContent:
assert "documents are provided with the question" in content
def test_null_prompt_id_agentic_agent_gets_agentic_preset(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
# PG ``agents.prompt_id`` is NULL for agents that never chose a
@@ -657,7 +657,7 @@ class TestGetPromptContent:
assert "source.summaries" not in content
def test_null_prompt_id_classic_agent_gets_default_preset(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -671,7 +671,7 @@ class TestGetPromptContent:
class TestPreFetchDocs:
def test_skips_when_no_active_docs(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -680,7 +680,7 @@ class TestPreFetchDocs:
assert docs is None and raw is None
def test_skips_when_isnonedoc_no_agent(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"isNoneDoc": True}, {"sub": "u"})
@@ -690,13 +690,13 @@ class TestPreFetchDocs:
assert docs is None and raw is None
def test_handles_retriever_exception(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
sp.source = {"active_docs": "src"}
with patch(
"application.api.answer.services.stream_processor.StreamProcessor.create_retriever",
"docsgpt.api.answer.services.stream_processor.StreamProcessor.create_retriever",
side_effect=RuntimeError("boom"),
):
docs, raw = sp.pre_fetch_docs("q")
@@ -705,48 +705,48 @@ class TestPreFetchDocs:
class TestPreFetchTools:
def test_disabled_globally_returns_none(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
with patch(
"application.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
"docsgpt.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
False,
):
got = sp.pre_fetch_tools()
assert got is None
def test_disabled_per_request(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"disable_tool_prefetch": True}, {"sub": "u"})
with patch(
"application.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
"docsgpt.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
True,
):
got = sp.pre_fetch_tools()
assert got is None
def test_no_template_skips_default_tool_prefetch(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "no-tools-user"})
sp._prompt_content = "No template syntax here"
with _patch_db(pg_conn), patch(
"application.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
"docsgpt.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
True,
):
got = sp.pre_fetch_tools()
assert got is None
def test_unresolvable_prompt_prefetches_only_explicit_rows(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
from application.storage.db.repositories.user_tools import (
from docsgpt.storage.db.repositories.user_tools import (
UserToolsRepository,
)
@@ -766,7 +766,7 @@ class TestPreFetchTools:
return {"ok": True}
with _patch_db(pg_conn), patch(
"application.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
"docsgpt.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
True,
), patch.object(sp, "_fetch_tool_data", _fake_fetch):
got = sp.pre_fetch_tools()
@@ -778,8 +778,8 @@ class TestPreFetchTools:
def test_default_tool_prefetched_when_template_references_it(
self, pg_conn
):
from application.agents.default_tools import default_tool_id
from application.api.answer.services.stream_processor import (
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
@@ -792,7 +792,7 @@ class TestPreFetchTools:
return {"ok": True}
with _patch_db(pg_conn), patch(
"application.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
"docsgpt.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
True,
), patch.object(sp, "_fetch_tool_data", _fake_fetch):
got = sp.pre_fetch_tools()
@@ -807,11 +807,11 @@ class TestPreFetchTools:
assert got.get("read_webpage") == {"ok": True}
def test_explicit_row_keeps_name_key_over_default(self, pg_conn):
from application.agents.default_tools import default_tool_id
from application.api.answer.services.stream_processor import (
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
from application.storage.db.repositories.user_tools import (
from docsgpt.storage.db.repositories.user_tools import (
UserToolsRepository,
)
@@ -825,7 +825,7 @@ class TestPreFetchTools:
return {"is_default": bool(tool_doc.get("default"))}
with _patch_db(pg_conn), patch(
"application.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
"docsgpt.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
True,
), patch.object(sp, "_fetch_tool_data", _fake_fetch):
got = sp.pre_fetch_tools()
@@ -838,8 +838,8 @@ class TestPreFetchTools:
def test_fetch_tool_data_executes_referenced_memory_view(self, pg_conn):
from unittest.mock import MagicMock
from application.agents.default_tools import synthesize_default_tool
from application.api.answer.services.stream_processor import (
from docsgpt.agents.default_tools import synthesize_default_tool
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
@@ -853,7 +853,7 @@ class TestPreFetchTools:
mock_manager.load_tool.return_value = mock_tool
with patch(
"application.agents.tools.tool_manager.ToolManager",
"docsgpt.agents.tools.tool_manager.ToolManager",
return_value=mock_manager,
):
got = sp._fetch_tool_data(tool_doc, {"memory_view"})
@@ -864,14 +864,14 @@ class TestPreFetchTools:
mock_tool.execute_action.assert_called_once_with("memory_view")
def test_agent_bound_invocation_omits_default_tool_prefetch(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"agent_id": "agent-xyz"}, {"sub": "u-ag"})
sp._required_tool_actions = {"read_webpage": {None}}
with _patch_db(pg_conn), patch(
"application.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
"docsgpt.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
True,
):
got = sp.pre_fetch_tools()
@@ -881,11 +881,11 @@ class TestPreFetchTools:
"""An explicit row and the synthesized default of the same name
coexist: name key stays on the explicit, default reachable by
synthetic id only."""
from application.agents.default_tools import default_tool_id
from application.api.answer.services.stream_processor import (
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
from application.storage.db.repositories.user_tools import (
from docsgpt.storage.db.repositories.user_tools import (
UserToolsRepository,
)
@@ -906,7 +906,7 @@ class TestPreFetchTools:
}
with _patch_db(pg_conn), patch(
"application.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
"docsgpt.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
True,
), patch.object(sp, "_fetch_tool_data", _fake_fetch):
got = sp.pre_fetch_tools()
@@ -921,7 +921,7 @@ class TestValidateAndSetModelAgentAuthority:
"""Agent-bound chats: agent's ``default_model_id`` is authoritative."""
def test_agent_bound_ignores_body_model_id(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"model_id": "body-model"}, {"sub": "caller"})
@@ -937,10 +937,10 @@ class TestValidateAndSetModelAgentAuthority:
return True
with patch(
"application.api.answer.services.stream_processor.validate_model_id",
"docsgpt.api.answer.services.stream_processor.validate_model_id",
side_effect=_fake_validate,
), patch(
"application.api.answer.services.stream_processor.get_default_model_id",
"docsgpt.api.answer.services.stream_processor.get_default_model_id",
return_value="global-default",
):
sp._validate_and_set_model()
@@ -950,17 +950,17 @@ class TestValidateAndSetModelAgentAuthority:
assert ("agent-model", "owner") in captured
def test_agent_bound_no_default_falls_back_to_system(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"model_id": "body-model"}, {"sub": "u"})
sp._agent_data = {"user": "u"}
sp.agent_config = {"default_model_id": "", "user_id": "u"}
with patch(
"application.api.answer.services.stream_processor.validate_model_id",
"docsgpt.api.answer.services.stream_processor.validate_model_id",
return_value=False,
), patch(
"application.api.answer.services.stream_processor.get_default_model_id",
"docsgpt.api.answer.services.stream_processor.get_default_model_id",
return_value="global-default",
):
sp._validate_and_set_model()
@@ -968,13 +968,13 @@ class TestValidateAndSetModelAgentAuthority:
assert sp.model_user_id is None
def test_agentless_body_model_still_wins(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"model_id": "body-model"}, {"sub": "u"})
sp._agent_data = None
with patch(
"application.api.answer.services.stream_processor.validate_model_id",
"docsgpt.api.answer.services.stream_processor.validate_model_id",
return_value=True,
):
sp._validate_and_set_model()
@@ -986,14 +986,14 @@ class TestGetDataFromApiKeySourceUnion:
"""`_get_data_from_api_key`: primary ∪ extras, deduplicated, primary first."""
def _make_sp(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
return StreamProcessor({}, {"sub": "u"})
def test_union_primary_and_extras(self, pg_conn):
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
owner = "u-merge-both"
sources_repo = SourcesRepository(pg_conn)
@@ -1023,8 +1023,8 @@ class TestGetDataFromApiKeySourceUnion:
assert data["source"] == str(primary["id"])
def test_only_primary(self, pg_conn):
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
owner = "u-merge-primary-only"
primary = SourcesRepository(pg_conn).create(
@@ -1045,8 +1045,8 @@ class TestGetDataFromApiKeySourceUnion:
assert data["source"] == str(primary["id"])
def test_only_extras(self, pg_conn):
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
owner = "u-merge-extras-only"
e1 = SourcesRepository(pg_conn).create(name="e1", user_id=owner)
@@ -1067,8 +1067,8 @@ class TestGetDataFromApiKeySourceUnion:
assert data["source"] is None
def test_dedupe_primary_repeated_in_extras(self, pg_conn):
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
owner = "u-merge-dedupe"
primary = SourcesRepository(pg_conn).create(
@@ -1096,11 +1096,11 @@ class TestAgentBoundFieldsAuthoritative:
"""End-to-end regression: agent's source/model/chunks/retriever win."""
def test_agent_values_win_over_body(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
owner = "u-regr-agent-authority"
primary = SourcesRepository(pg_conn).create(
@@ -1130,10 +1130,10 @@ class TestAgentBoundFieldsAuthoritative:
sp = StreamProcessor(body, {"sub": owner})
with _patch_db(pg_conn), patch(
"application.api.answer.services.stream_processor.validate_model_id",
"docsgpt.api.answer.services.stream_processor.validate_model_id",
return_value=True,
), patch(
"application.api.answer.services.stream_processor.get_default_model_id",
"docsgpt.api.answer.services.stream_processor.get_default_model_id",
return_value="system-default",
):
sp._configure_agent()
@@ -1,6 +1,6 @@
"""Token-usage attribution tests for the always-on inline-persist model.
Persistence is owned by the per-call decorator in ``application.usage``.
Persistence is owned by the per-call decorator in ``docsgpt.usage``.
``finalize_message`` no longer writes ``token_usage`` rows. These tests
exercise the decorator path through ``stream_token_usage`` /
``gen_token_usage``:
@@ -63,7 +63,7 @@ class TestDecoratorAlwaysPersists:
"""Per-call inline persistence — no opt-in flag."""
def test_primary_stream_writes_agent_stream_row(self, pg_conn):
from application.usage import stream_token_usage
from docsgpt.usage import stream_token_usage
user_id = _seed_user(pg_conn)
@@ -81,7 +81,7 @@ class TestDecoratorAlwaysPersists:
yield "chunk-b"
llm = _PrimaryLLM()
with _patch_db_session_for(("application.usage",), pg_conn):
with _patch_db_session_for(("docsgpt.usage",), pg_conn):
for _ in llm._raw(
"m", [{"role": "user", "content": "hi"}], True, None,
):
@@ -102,7 +102,7 @@ class TestDecoratorAlwaysPersists:
def test_side_channel_source_flows_to_row(self, pg_conn):
"""``_token_usage_source`` overrides the default ``agent_stream``."""
from application.usage import stream_token_usage
from docsgpt.usage import stream_token_usage
user_id = _seed_user(pg_conn)
@@ -120,7 +120,7 @@ class TestDecoratorAlwaysPersists:
yield "chunk"
llm = _RagLLM()
with _patch_db_session_for(("application.usage",), pg_conn):
with _patch_db_session_for(("docsgpt.usage",), pg_conn):
for _ in llm._raw("m", [{"role": "user", "content": "q"}], True, None):
pass
@@ -137,7 +137,7 @@ class TestDecoratorAlwaysPersists:
"""``_request_id`` on the LLM (stamped by the route) lands in
``token_usage.request_id`` so ``count_in_range`` can DISTINCT it.
"""
from application.usage import stream_token_usage
from docsgpt.usage import stream_token_usage
user_id = _seed_user(pg_conn)
request_id = f"req-{uuid.uuid4().hex[:12]}"
@@ -156,7 +156,7 @@ class TestDecoratorAlwaysPersists:
yield "chunk"
llm = _PrimaryLLM()
with _patch_db_session_for(("application.usage",), pg_conn):
with _patch_db_session_for(("docsgpt.usage",), pg_conn):
# Call twice — the route invokes the LLM once per tool round.
for _ in llm._raw("m", [{"role": "user", "content": "q"}], True, None):
pass
@@ -173,7 +173,7 @@ class TestDecoratorAlwaysPersists:
assert all(r[0] == request_id for r in rows)
def test_zero_count_call_is_skipped(self, pg_conn):
from application.usage import gen_token_usage
from docsgpt.usage import gen_token_usage
user_id = _seed_user(pg_conn)
@@ -190,7 +190,7 @@ class TestDecoratorAlwaysPersists:
return None # empty result → 0 generated tokens, 0 prompt tokens
llm = _EmptyLLM()
with _patch_db_session_for(("application.usage",), pg_conn):
with _patch_db_session_for(("docsgpt.usage",), pg_conn):
llm._raw("m", [], False, None)
n = pg_conn.execute(
@@ -206,7 +206,7 @@ class TestDecoratorAlwaysPersists:
constraint; the decorator skips before that to keep the stream
running.
"""
from application.usage import stream_token_usage
from docsgpt.usage import stream_token_usage
class _OrphanLLM:
decoded_token = None
@@ -222,8 +222,8 @@ class TestDecoratorAlwaysPersists:
llm = _OrphanLLM()
with _patch_db_session_for(
("application.usage",), pg_conn,
), caplog.at_level(logging.WARNING, logger="application.usage"):
("docsgpt.usage",), pg_conn,
), caplog.at_level(logging.WARNING, logger="docsgpt.usage"):
for _ in llm._raw("m", [{"role": "user", "content": "q"}], True, None):
pass
+59 -59
View File
@@ -1,4 +1,4 @@
"""Unit tests for application/api/answer/routes/base.py — BaseAnswerResource.
"""Unit tests for docsgpt/api/answer/routes/base.py — BaseAnswerResource.
Additional coverage beyond tests/api/answer/routes/test_base.py:
- _prepare_tool_calls_for_logging: truncation, non-dict items
@@ -22,21 +22,21 @@ class TestPrepareToolCallsForLogging:
pass
def test_empty_list(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
assert resource._prepare_tool_calls_for_logging([]) == []
def test_none_returns_empty(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
assert resource._prepare_tool_calls_for_logging(None) == []
def test_truncates_long_result(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -45,7 +45,7 @@ class TestPrepareToolCallsForLogging:
assert len(prepared[0]["result"]) == 100
def test_truncates_result_full(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -54,7 +54,7 @@ class TestPrepareToolCallsForLogging:
assert len(prepared[0]["result_full"]) == 50
def test_non_dict_items_wrapped(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -64,7 +64,7 @@ class TestPrepareToolCallsForLogging:
assert prepared[1] == {"result": "42"}
def test_preserves_short_results(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -79,7 +79,7 @@ class TestCompleteStreamToolCalls:
pass
def test_streams_tool_calls(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -105,7 +105,7 @@ class TestCompleteStreamToolCalls:
assert len(tool_chunks) == 1
def test_streams_thought_events(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -137,7 +137,7 @@ class TestCompleteStreamStructuredOutput:
pass
def test_streams_structured_answer(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -173,7 +173,7 @@ class TestCompleteStreamMetadata:
pass
def test_metadata_collected(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -205,7 +205,7 @@ class TestCompleteStreamIsNoneDoc:
pass
def test_isNoneDoc_sets_source_to_none(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -238,7 +238,7 @@ class TestCompleteStreamErrorType:
pass
def test_error_type_event_sanitized(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -263,7 +263,7 @@ class TestCompleteStreamErrorType:
assert len(error_chunks) == 1
def test_non_error_type_event_passed_through(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -293,7 +293,7 @@ class TestProcessResponseStreamExtended:
pass
def test_handles_structured_answer(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -307,7 +307,7 @@ class TestProcessResponseStreamExtended:
assert result.get("extra", {}).get("structured") is True
def test_handles_tool_calls_event(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -321,7 +321,7 @@ class TestProcessResponseStreamExtended:
assert result["tool_calls"] == [{"name": "t1"}]
def test_incomplete_stream(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -332,7 +332,7 @@ class TestProcessResponseStreamExtended:
assert result["error"] == "Stream ended unexpectedly"
def test_handles_thought_event(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -350,7 +350,7 @@ class TestProcessResponseStreamExtended:
informational ``message_id`` event) without breaking JSON
decoding.
"""
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -372,7 +372,7 @@ class TestProcessResponseStreamExtended:
assert result["conversation_id"] == "conv-1"
def test_skips_keepalive_comment_lines(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -397,7 +397,7 @@ class TestCompleteStreamCompressionMetadata:
"""Cover lines 307-319 (compression metadata persistence in complete_stream)."""
def test_compression_metadata_persisted(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -442,7 +442,7 @@ class TestCompleteStreamCompressionMetadata:
def test_compression_metadata_error_handled(self, mock_mongo_db, flask_app):
"""Cover lines 318-322: compression metadata persistence error."""
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -485,7 +485,7 @@ class TestCompleteStreamLogTruncation:
"""Cover line 354: log data truncation for long values."""
def test_long_response_truncated_in_log(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -514,7 +514,7 @@ class TestCompleteStreamGeneratorExit:
"""Cover lines 360-416 (GeneratorExit handling in complete_stream)."""
def test_generator_exit_saves_partial_response(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -558,7 +558,7 @@ class TestCompleteStreamGeneratorExit:
def test_generator_exit_with_compression_metadata(self, mock_mongo_db, flask_app):
"""Cover lines 393-411: GeneratorExit with compression metadata."""
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -598,7 +598,7 @@ class TestCompleteStreamGeneratorExit:
def test_generator_exit_save_error_handled(self, mock_mongo_db, flask_app):
"""Cover lines 412-415: exception during partial save."""
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -647,7 +647,7 @@ class TestCompleteStreamGeneratorExit:
``end`` for a row whose DB status is still non-terminal and the
UI parks on a blank successful answer.
"""
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -679,7 +679,7 @@ class TestCompleteStreamGeneratorExit:
return True
with patch(
"application.api.answer.routes.base.record_event",
"docsgpt.api.answer.routes.base.record_event",
side_effect=_capture_record,
):
gen = resource.complete_stream(
@@ -727,8 +727,8 @@ class TestCompleteStreamGeneratorExit:
``end`` (the row matches ``complete``). Only the empty-response
branch flips to ``error``.
"""
from application.api.answer.routes.base import BaseAnswerResource
from application.storage.db.repositories.conversations import (
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.storage.db.repositories.conversations import (
MessageUpdateOutcome,
)
@@ -761,7 +761,7 @@ class TestCompleteStreamGeneratorExit:
return True
with patch(
"application.api.answer.routes.base.record_event",
"docsgpt.api.answer.routes.base.record_event",
side_effect=_capture_record,
):
gen = resource.complete_stream(
@@ -804,8 +804,8 @@ class TestCompleteStreamGeneratorExit:
as a failure and journaled ``error`` — a reconnect would then
replay a successful completion as a failed answer.
"""
from application.api.answer.routes.base import BaseAnswerResource
from application.storage.db.repositories.conversations import (
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.storage.db.repositories.conversations import (
MessageUpdateOutcome,
)
@@ -841,7 +841,7 @@ class TestCompleteStreamGeneratorExit:
return True
with patch(
"application.api.answer.routes.base.record_event",
"docsgpt.api.answer.routes.base.record_event",
side_effect=_capture_record,
):
gen = resource.complete_stream(
@@ -890,24 +890,24 @@ def _patch_db_session(conn):
yield conn
with patch(
"application.api.answer.services.conversation_service.db_session",
"docsgpt.api.answer.services.conversation_service.db_session",
_yield,
), patch(
"application.api.answer.services.conversation_service.db_readonly",
"docsgpt.api.answer.services.conversation_service.db_readonly",
_yield,
), patch(
# ``record_event`` opens its own short-lived ``db_session`` for
# cross-connection visibility. In tests we route it back to the
# same ``pg_conn`` so the journal write can see the message row
# the conversation_service just wrote in this transaction.
"application.streaming.message_journal.db_session",
"docsgpt.streaming.message_journal.db_session",
_yield,
), patch(
# ``complete_stream`` reads ``latest_sequence_no`` via
# ``db_readonly`` to seed continuation runs. Same patch reason
# as the journal — keep the read on the same pg_conn so it sees
# uncommitted writes from this transaction.
"application.api.answer.routes.base.db_readonly",
"docsgpt.api.answer.routes.base.db_readonly",
_yield,
), patch(
# The terminal ``stream_answer`` user_logs write opens its own
@@ -916,7 +916,7 @@ def _patch_db_session(conn):
# inserted (via the ``ensure_user_exists`` trigger) until the
# statement timeout fires — ~30s per test, swallowed by the
# caller's except, so it only ever showed up as slowness.
"application.api.answer.routes.base.db_session",
"docsgpt.api.answer.routes.base.db_session",
_yield,
):
yield
@@ -941,8 +941,8 @@ class TestCompleteStreamWalAcceptance:
def test_failed_llm_persists_question_with_failed_status(
self, pg_conn, flask_app,
):
from application.api.answer.routes.base import BaseAnswerResource
from application.storage.db.repositories.conversations import (
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -993,8 +993,8 @@ class TestCompleteStreamWalAcceptance:
answer with no error and no retry affordance — the user saw a blank
message and re-sent the prompt. Reproduces the 2026-08-01 report.
"""
from application.api.answer.routes.base import BaseAnswerResource
from application.storage.db.repositories.conversations import (
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -1044,8 +1044,8 @@ class TestCompleteStreamWalAcceptance:
then a downstream node failed, the row stays ``complete`` so the
partial answer still renders; the error was already surfaced live.
"""
from application.api.answer.routes.base import BaseAnswerResource
from application.storage.db.repositories.conversations import (
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -1095,8 +1095,8 @@ class TestCompleteStreamWalAcceptance:
column default ``complete`` — leaving exactly the blank bubble this
changeset removes on the other branch.
"""
from application.api.answer.routes.base import BaseAnswerResource
from application.storage.db.repositories.conversations import (
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -1143,7 +1143,7 @@ class TestCompleteStreamWalAcceptance:
``pending_tool_state`` row would deep-link the user to a 404.
Gate the publish on save_state actually committing.
"""
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -1173,10 +1173,10 @@ class TestCompleteStreamWalAcceptance:
published.append(args)
with _patch_db_session(pg_conn), patch(
"application.api.answer.routes.base.publish_user_event",
"docsgpt.api.answer.routes.base.publish_user_event",
side_effect=_capture,
), patch(
"application.api.answer.services.continuation_service."
"docsgpt.api.answer.services.continuation_service."
"ContinuationService.save_state",
side_effect=RuntimeError("PG outage"),
):
@@ -1213,8 +1213,8 @@ class TestCompleteStreamWalAcceptance:
from sqlalchemy import text as sql_text
from application.api.answer.routes.base import BaseAnswerResource
from application.storage.db.repositories.message_events import (
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.storage.db.repositories.message_events import (
MessageEventsRepository,
)
@@ -1296,8 +1296,8 @@ class TestCompleteStreamWalAcceptance:
row, so client-side correlation, ``token_usage`` joins, and
``count_in_range``'s DISTINCT all line up.
"""
from application.api.answer.routes.base import BaseAnswerResource
from application.storage.db.repositories.conversations import (
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -1357,7 +1357,7 @@ class TestStreamingHeartbeatSeed:
def test_heartbeat_seeded_at_generation_start_and_on_first_chunk(
self, mock_mongo_db, flask_app,
):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -1417,7 +1417,7 @@ class TestStreamingHeartbeatSeed:
self, mock_mongo_db, flask_app,
):
"""No DB-backed message row → no heartbeat call (and no error)."""
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -1471,11 +1471,11 @@ class TestCompleteStreamSupersededLookupIsGuarded:
):
import logging
from application.api.answer.routes.base import BaseAnswerResource
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
MessageUpdateOutcome,
)
@@ -1,4 +1,4 @@
"""Unit tests for application/api/answer/services/conversation_service.py.
"""Unit tests for docsgpt/api/answer/services/conversation_service.py.
Additional coverage beyond tests/api/answer/services/test_conversation_service.py:
- save_conversation: index-based update, metadata persistence, agent key tracking
@@ -18,7 +18,7 @@ class TestConversationServiceGetExtended:
pass
def test_handles_exception_gracefully(self, mock_mongo_db):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -33,7 +33,7 @@ class TestSaveConversationExtended:
pass
def test_raises_for_none_token(self, mock_mongo_db):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -71,7 +71,7 @@ class TestGetCompressionMetadata:
pass
def test_returns_none_for_missing_conversation(self, mock_mongo_db):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -80,7 +80,7 @@ class TestGetCompressionMetadata:
assert result is None
def test_handles_invalid_id(self, mock_mongo_db):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -100,7 +100,7 @@ class TestConversationServiceGaps:
def test_append_compression_message_empty_summary_skips(self, mock_mongo_db):
"""Cover: empty summary does not insert."""
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -12,12 +12,12 @@ from unittest.mock import MagicMock, patch
import pytest
from application.agents.classic_agent import ClassicAgent
from docsgpt.agents.classic_agent import ClassicAgent
def _agent(**kwargs):
with patch("application.llm.llm_creator.LLMCreator.create_llm"), patch(
"application.llm.handlers.handler_creator.LLMHandlerCreator.create_handler"
with patch("docsgpt.llm.llm_creator.LLMCreator.create_llm"), patch(
"docsgpt.llm.handlers.handler_creator.LLMHandlerCreator.create_handler"
):
return ClassicAgent(
endpoint="stream",
@@ -13,8 +13,8 @@ from unittest.mock import MagicMock, patch
import pytest
from application.agents.classic_agent import ClassicAgent
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.agents.classic_agent import ClassicAgent
from docsgpt.api.answer.services.stream_processor import StreamProcessor
DOCS = [
{"text": "Clause 4: reporting is due within 30 days.", "filename": "aml.pdf"},
@@ -72,8 +72,8 @@ class TestRetrievalReachesTheAgent:
@pytest.mark.unit
class TestDocumentsLandInTheUserTurn:
def _agent(self, **kwargs):
with patch("application.llm.llm_creator.LLMCreator.create_llm"), patch(
"application.llm.handlers.handler_creator.LLMHandlerCreator.create_handler"
with patch("docsgpt.llm.llm_creator.LLMCreator.create_llm"), patch(
"docsgpt.llm.handlers.handler_creator.LLMHandlerCreator.create_handler"
):
return ClassicAgent(
endpoint="stream",
@@ -110,7 +110,7 @@ class TestChunksPrecedence:
"""
def _sp(self, request_chunks=None, source_chunks=None):
from application.storage.db.source_config import RetrievalConfig
from docsgpt.storage.db.source_config import RetrievalConfig
sp = _processor()
sp._agent_data = None
@@ -31,9 +31,9 @@ def _patch_journal_session(conn):
yield conn
with patch(
"application.streaming.message_journal.db_session", _yield
"docsgpt.streaming.message_journal.db_session", _yield
), patch(
"application.streaming.event_replay.db_readonly", _yield
"docsgpt.streaming.event_replay.db_readonly", _yield
):
yield
@@ -78,8 +78,8 @@ class TestSnapshotPlusTailRoundTrip:
snapshot back via the same repo and formats it for the wire — the
exact primitive the async reader replays through.
"""
from application.streaming.event_replay import read_snapshot_lines
from application.streaming.message_journal import record_event
from docsgpt.streaming.event_replay import read_snapshot_lines
from docsgpt.streaming.message_journal import record_event
_, message_id = _seed_message(pg_conn)
@@ -99,8 +99,8 @@ class TestSnapshotPlusTailRoundTrip:
assert terminal is True
def test_snapshot_resumes_past_last_event_id(self, pg_conn):
from application.streaming.event_replay import read_snapshot_lines
from application.streaming.message_journal import record_event
from docsgpt.streaming.event_replay import read_snapshot_lines
from docsgpt.streaming.message_journal import record_event
_, message_id = _seed_message(pg_conn)
@@ -121,7 +121,7 @@ class TestSnapshotPlusTailRoundTrip:
"""The async route's ownership gate runs real SQL against
``conversation_messages`` — the owner passes, everyone else 404s.
"""
from application.api import async_sse
from docsgpt.api import async_sse
user_id, message_id = _seed_message(pg_conn)
@@ -129,7 +129,7 @@ class TestSnapshotPlusTailRoundTrip:
def _yield():
yield pg_conn
with patch("application.api.async_sse.db_readonly", _yield):
with patch("docsgpt.api.async_sse.db_readonly", _yield):
assert async_sse._user_owns_message(message_id, user_id) is True
assert async_sse._user_owns_message(message_id, "different-user") is False
# A well-formed but unknown id is also not owned.
@@ -139,8 +139,8 @@ class TestSnapshotPlusTailRoundTrip:
"""``read_snapshot_lines(..., user_id=)`` re-asserts ownership at the
data layer: the owner gets the journal rows, a non-owner gets none.
"""
from application.streaming.event_replay import read_snapshot_lines
from application.streaming.message_journal import record_event
from docsgpt.streaming.event_replay import read_snapshot_lines
from docsgpt.streaming.message_journal import record_event
user_id, message_id = _seed_message(pg_conn)
@@ -166,7 +166,7 @@ class TestSnapshotPlusTailRoundTrip:
own row; a non-owner reads as missing (terminal), not as the row's
real status.
"""
from application.streaming.event_replay import _check_producer_liveness
from docsgpt.streaming.event_replay import _check_producer_liveness
# Seed a row and flip it to a terminal status the watchdog reports.
user_id, message_id = _seed_message(pg_conn)
@@ -181,7 +181,7 @@ class TestSnapshotPlusTailRoundTrip:
def _yield():
yield pg_conn
with patch("application.streaming.event_replay.db_readonly", _yield):
with patch("docsgpt.streaming.event_replay.db_readonly", _yield):
owner = _check_producer_liveness(message_id, user_id, 90.0)
other = _check_producer_liveness(message_id, "different-user", 90.0)
@@ -16,7 +16,7 @@ from __future__ import annotations
import pytest
import application.api.answer.services.stream_processor as sp_mod
import docsgpt.api.answer.services.stream_processor as sp_mod
StreamProcessor = sp_mod.StreamProcessor
@@ -15,7 +15,7 @@ from unittest.mock import MagicMock, patch
import pytest
from application.storage.db.repositories.conversations import HeartbeatState
from docsgpt.storage.db.repositories.conversations import HeartbeatState
def _reservation(message_id="11111111-1111-1111-1111-111111111111"):
@@ -59,8 +59,8 @@ def _service(state=HeartbeatState.STAMPED):
@pytest.mark.unit
class TestHeartbeatTicker:
def _run(self, flask_app, gen, service=None, interval=0.05):
from application.api.answer.routes import base as base_mod
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes import base as base_mod
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -99,8 +99,8 @@ class TestHeartbeatTicker:
assert service.heartbeat_message_state.call_count == settled
def test_ticker_stops_on_client_abort(self, mock_mongo_db, flask_app):
from application.api.answer.routes import base as base_mod
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes import base as base_mod
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -131,8 +131,8 @@ class TestHeartbeatTicker:
def test_no_ticker_without_a_reserved_row(self, mock_mongo_db, flask_app):
"""Headless/continuation rounds have no row to stamp."""
from application.api.answer.routes import base as base_mod
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes import base as base_mod
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -177,8 +177,8 @@ class TestSupersededStreamCancellation:
"""A deleted row must stop the work, not just quiet the logs."""
def _run_with_missing_row(self, flask_app, gen, interval=0.05):
from application.api.answer.routes import base as base_mod
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes import base as base_mod
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -227,8 +227,8 @@ class TestSupersededStreamCancellation:
def test_live_row_is_never_cancelled(self, mock_mongo_db, flask_app):
"""The common case must be untouched."""
from application.api.answer.routes import base as base_mod
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes import base as base_mod
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -255,8 +255,8 @@ class TestSupersededStreamCancellation:
def test_db_error_does_not_cancel_the_stream(self, mock_mongo_db, flask_app):
"""A transient blip must never be mistaken for a deleted row."""
from application.api.answer.routes import base as base_mod
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes import base as base_mod
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
File diff suppressed because it is too large. Load diff
@@ -6,8 +6,8 @@ from unittest.mock import MagicMock, patch
import pytest
from application.api.answer.services.stream_processor import StreamProcessor
from application.storage.db.source_config import RetrievalConfig
from docsgpt.api.answer.services.stream_processor import StreamProcessor
from docsgpt.storage.db.source_config import RetrievalConfig
def _processor() -> StreamProcessor:
@@ -164,12 +164,12 @@ class TestNonAgentSourceConfig:
"config": {"retrieval": {"exposure": "agentic_tool", "chunks": 7}}
}
with patch(
"application.api.answer.services.stream_processor.db_readonly"
"docsgpt.api.answer.services.stream_processor.db_readonly"
), patch(
"application.api.answer.services.stream_processor.can_access",
"docsgpt.api.answer.services.stream_processor.can_access",
return_value=True,
), patch(
"application.api.answer.services.stream_processor.SourcesRepository"
"docsgpt.api.answer.services.stream_processor.SourcesRepository"
) as repo:
# Read unscoped after the access check, so a team grantee gets the
# source's real config instead of silently falling back to defaults.
+1 -1
View File
@@ -21,7 +21,7 @@ def mock_request_token(monkeypatch, decoded_token):
return wrapper
monkeypatch.setattr("application.auth.api_key_required", lambda: mock_decorator)
monkeypatch.setattr("docsgpt.auth.api_key_required", lambda: mock_decorator)
return decoded_token
+12 -12
View File
@@ -16,7 +16,7 @@ import pytest
@pytest.fixture
def client():
from application.app import app as flask_app
from docsgpt.app import app as flask_app
flask_app.config["TESTING"] = True
return flask_app.test_client()
@@ -29,18 +29,18 @@ def _fake_conn():
@contextmanager
def _admin(**route_patches):
"""Authenticate as admin and patch application.api.admin.routes.* members."""
"""Authenticate as admin and patch docsgpt.api.admin.routes.* members."""
with ExitStack() as stack:
stack.enter_context(
patch("application.app.handle_auth", return_value={"sub": "admin1"})
patch("docsgpt.app.handle_auth", return_value={"sub": "admin1"})
)
stack.enter_context(
patch("application.app.resolve_roles", return_value=["admin", "user"])
patch("docsgpt.app.resolve_roles", return_value=["admin", "user"])
)
stack.enter_context(patch("application.api.admin.routes.db_readonly", _fake_conn))
stack.enter_context(patch("application.api.admin.routes.db_session", _fake_conn))
stack.enter_context(patch("docsgpt.api.admin.routes.db_readonly", _fake_conn))
stack.enter_context(patch("docsgpt.api.admin.routes.db_session", _fake_conn))
for name, value in route_patches.items():
stack.enter_context(patch(f"application.api.admin.routes.{name}", value))
stack.enter_context(patch(f"docsgpt.api.admin.routes.{name}", value))
yield
@@ -51,13 +51,13 @@ def _body(resp):
@pytest.mark.unit
class TestGuard:
def test_non_admin_forbidden(self, client):
with patch("application.app.handle_auth", return_value={"sub": "u"}), patch(
"application.app.resolve_roles", return_value=["user"]
with patch("docsgpt.app.handle_auth", return_value={"sub": "u"}), patch(
"docsgpt.app.resolve_roles", return_value=["user"]
):
assert client.get("/api/admin/overview").status_code == 403
def test_unauthenticated(self, client):
with patch("application.app.handle_auth", return_value=None):
with patch("docsgpt.app.handle_auth", return_value=None):
assert client.get("/api/admin/overview").status_code == 401
@@ -170,7 +170,7 @@ class TestUserLifecycle:
with _admin(
UsersRepository=Mock(return_value=users),
AuthEventsRepository=Mock(return_value=events),
), patch("application.api.admin.routes.denylist") as dl:
), patch("docsgpt.api.admin.routes.denylist") as dl:
resp = client.patch("/api/admin/users/bob", json={"active": False})
assert resp.status_code == 200
dl.deny_user.assert_called_once_with("bob")
@@ -179,7 +179,7 @@ class TestUserLifecycle:
def test_force_logout(self, client):
events = Mock()
with _admin(AuthEventsRepository=Mock(return_value=events)), patch(
"application.api.admin.routes.denylist"
"docsgpt.api.admin.routes.denylist"
) as dl:
dl.deny_user.return_value = True
resp = client.post("/api/admin/users/bob/revoke-sessions")
+14 -14
View File
@@ -1,4 +1,4 @@
"""Tests for agent YAML export/import (application.api.user.agents.portability).
"""Tests for agent YAML export/import (docsgpt.api.user.agents.portability).
These exercise real SQL against the ephemeral ``pg_conn`` fixture, calling
the dependency-injected serialize/plan/apply functions directly so the
@@ -11,8 +11,8 @@ from unittest.mock import Mock
import pytest
from application.agents.default_tools import default_tool_id
from application.api.user.agents.portability import (
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.api.user.agents.portability import (
API_VERSION,
AgentImportError,
agent_to_yaml,
@@ -22,10 +22,10 @@ from application.api.user.agents.portability import (
plan_import,
serialize_agent,
)
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.prompts import PromptsRepository
from application.storage.db.repositories.sources import SourcesRepository
from application.storage.db.repositories.user_tools import UserToolsRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.prompts import PromptsRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
pytestmark = pytest.mark.integration
@@ -243,7 +243,7 @@ def test_apply_creates_tool_with_supplied_secret(pg_conn, monkeypatch):
}
fake_tool.get_actions_metadata.return_value = []
monkeypatch.setattr(
"application.api.user.agents.portability._tool_instance",
"docsgpt.api.user.agents.portability._tool_instance",
lambda tool_type: fake_tool,
)
doc = _doc(
@@ -380,7 +380,7 @@ def test_import_rejects_ssrf_tool_url(pg_conn, monkeypatch):
fake_tool.get_config_requirements.return_value = {}
fake_tool.get_actions_metadata.return_value = []
monkeypatch.setattr(
"application.api.user.agents.portability._tool_instance",
"docsgpt.api.user.agents.portability._tool_instance",
lambda tool_type: fake_tool,
)
doc = _doc(
@@ -473,12 +473,12 @@ def test_tool_skipped_when_encryption_fails(pg_conn, monkeypatch):
}
fake_tool.get_actions_metadata.return_value = []
monkeypatch.setattr(
"application.api.user.agents.portability._tool_instance",
"docsgpt.api.user.agents.portability._tool_instance",
lambda tool_type: fake_tool,
)
# Force credential encryption to fail (returns "").
monkeypatch.setattr(
"application.api.user.tools.routes.encrypt_credentials",
"docsgpt.api.user.tools.routes.encrypt_credentials",
lambda creds, user_id: "",
)
doc = _doc(
@@ -534,7 +534,7 @@ def _fake_telegram(monkeypatch):
[_SEND_MESSAGE_META, _GET_UPDATES_META]
)
monkeypatch.setattr(
"application.api.user.agents.portability._tool_instance",
"docsgpt.api.user.agents.portability._tool_instance",
lambda tool_type: fake_tool,
)
return fake_tool
@@ -711,12 +711,12 @@ def _fake_api_tool(monkeypatch):
fake_tool.get_config_requirements.return_value = {}
fake_tool.get_actions_metadata.return_value = []
monkeypatch.setattr(
"application.api.user.agents.portability._tool_instance",
"docsgpt.api.user.agents.portability._tool_instance",
lambda tool_type: fake_tool,
)
# The sanitizer SSRF-gates each action URL; DNS isn't available in tests.
monkeypatch.setattr(
"application.api.user.agents.portability.validate_url", lambda url: url
"docsgpt.api.user.agents.portability.validate_url", lambda url: url
)
return fake_tool
+14 -14
View File
@@ -16,7 +16,7 @@ import pytest
@pytest.fixture
def client():
from application.app import app as flask_app
from docsgpt.app import app as flask_app
flask_app.config["TESTING"] = True
return flask_app.test_client()
@@ -31,19 +31,19 @@ def _patches(sub, repo, team_access, *, prompt_name="Resolved Prompt", source_de
if source_details is None:
source_details = []
return [
patch("application.app.handle_auth", return_value={"sub": sub}),
patch("application.app.resolve_roles", return_value=["user"]),
patch("application.api.user.agents.routes.db_readonly", lambda: _cm(Mock())),
patch("application.api.user.agents.routes.AgentsRepository", return_value=repo),
patch("application.api.user.agents.routes.team_access_for", return_value=team_access),
patch("docsgpt.app.handle_auth", return_value={"sub": sub}),
patch("docsgpt.app.resolve_roles", return_value=["user"]),
patch("docsgpt.api.user.agents.routes.db_readonly", lambda: _cm(Mock())),
patch("docsgpt.api.user.agents.routes.AgentsRepository", return_value=repo),
patch("docsgpt.api.user.agents.routes.team_access_for", return_value=team_access),
# Resolve names by id (owner-agnostic) — patched so the test never
# touches the DB; the route is what we're asserting wires them in.
patch(
"application.api.user.agents.routes.resolve_prompt_name",
"docsgpt.api.user.agents.routes.resolve_prompt_name",
return_value=prompt_name,
),
patch(
"application.api.user.agents.routes.resolve_source_details",
"docsgpt.api.user.agents.routes.resolve_source_details",
return_value=source_details,
),
]
@@ -160,12 +160,12 @@ class TestGetAgentResolvesNames:
def _update_patches(sub, repo, team_access, can_access_mock):
return [
patch("application.app.handle_auth", return_value={"sub": sub}),
patch("application.app.resolve_roles", return_value=["user"]),
patch("application.api.user.agents.routes.db_session", lambda: _cm(Mock())),
patch("application.api.user.agents.routes.AgentsRepository", return_value=repo),
patch("application.api.user.agents.routes.team_access_for", return_value=team_access),
patch("application.api.user.agents.routes.can_access", can_access_mock),
patch("docsgpt.app.handle_auth", return_value={"sub": sub}),
patch("docsgpt.app.resolve_roles", return_value=["user"]),
patch("docsgpt.api.user.agents.routes.db_session", lambda: _cm(Mock())),
patch("docsgpt.api.user.agents.routes.AgentsRepository", return_value=repo),
patch("docsgpt.api.user.agents.routes.team_access_for", return_value=team_access),
patch("docsgpt.api.user.agents.routes.can_access", can_access_mock),
]
+7 -7
View File
@@ -1,4 +1,4 @@
"""Tests for ``application/api/async_sse.py``.
"""Tests for ``docsgpt/api/async_sse.py``.
Native-async reconnect endpoint: GET /api/messages/<id>/events. Auth gate,
ownership gate, malformed-id rejection, Last-Event-ID normalisation, and the
@@ -15,19 +15,19 @@ import pytest
from starlette.applications import Starlette
from starlette.testclient import TestClient
from application.api.async_sse import (
from docsgpt.api.async_sse import (
_MESSAGE_ID_RE,
_normalise_last_event_id,
async_sse_routes,
)
from application.core.settings import settings
from docsgpt.core.settings import settings
VALID_UUID = "67d65e8f-e7fb-4df1-9e6e-99ea6c830206"
_AUTH = "application.api.async_sse.handle_auth"
_OWNS = "application.api.async_sse._user_owns_message"
_STREAM = "application.api.async_sse.build_message_event_stream_async"
_AREDIS = "application.api.async_sse.get_async_redis_instance"
_AUTH = "docsgpt.api.async_sse.handle_auth"
_OWNS = "docsgpt.api.async_sse._user_owns_message"
_STREAM = "docsgpt.api.async_sse.build_message_event_stream_async"
_AREDIS = "docsgpt.api.async_sse.get_async_redis_instance"
def _client() -> TestClient:
+8 -8
View File
@@ -1,4 +1,4 @@
"""Tests for application/api/connector/routes.py"""
"""Tests for docsgpt/api/connector/routes.py"""
import base64
import json
@@ -89,8 +89,8 @@ class _InMemoryCollection:
@pytest.fixture
def app():
with patch("application.app.handle_auth", return_value={"sub": "test_user"}):
from application.app import app as flask_app
with patch("docsgpt.app.handle_auth", return_value={"sub": "test_user"}):
from docsgpt.app import app as flask_app
flask_app.config["TESTING"] = True
yield flask_app
@@ -104,8 +104,8 @@ def client(app):
def mock_sessions(monkeypatch):
sessions = _InMemoryCollection()
sources = _InMemoryCollection()
monkeypatch.setattr("application.api.connector.routes.sessions_collection", sessions)
monkeypatch.setattr("application.api.connector.routes.sources_collection", sources)
monkeypatch.setattr("docsgpt.api.connector.routes.sessions_collection", sessions)
monkeypatch.setattr("docsgpt.api.connector.routes.sources_collection", sources)
return {"sessions": sessions, "sources": sources}
@@ -124,7 +124,7 @@ class TestConnectorAuth:
@pytest.mark.unit
def test_unauthorized(self, client, app):
with patch("application.app.handle_auth", return_value=None):
with patch("docsgpt.app.handle_auth", return_value=None):
resp = client.get("/api/connectors/auth?provider=google_drive")
data = json.loads(resp.data)
# decoded_token is None -> 401
@@ -212,7 +212,7 @@ class TestBuildCallbackRedirect:
@pytest.mark.unit
def test_builds_url(self):
from application.api.connector.routes import build_callback_redirect
from docsgpt.api.connector.routes import build_callback_redirect
url = build_callback_redirect({"status": "success", "message": "OK"})
assert url.startswith("/api/connectors/callback-status?")
assert "status=success" in url
@@ -228,7 +228,7 @@ class TestConnectorsCallback:
def _patch_connector_creator(self):
"""Patch ConnectorCreator at both module-level and local-import locations."""
return patch(
"application.parser.connectors.connector_creator.ConnectorCreator",
"docsgpt.parser.connectors.connector_creator.ConnectorCreator",
)
+57 -57
View File
@@ -1,4 +1,4 @@
"""Tests for application/api/connector/routes.py.
"""Tests for docsgpt/api/connector/routes.py.
Directly instantiates Resource classes via ``test_request_context`` instead
of registering the blueprint (the flask_restx ``api`` is a module-level
@@ -27,9 +27,9 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.connector.routes.db_session", _yield
"docsgpt.api.connector.routes.db_session", _yield
), patch(
"application.api.connector.routes.db_readonly", _yield
"docsgpt.api.connector.routes.db_readonly", _yield
):
yield
@@ -40,7 +40,7 @@ def _encode_state(payload):
class TestBuildCallbackRedirect:
def test_builds_safe_url_with_params(self):
from application.api.connector.routes import build_callback_redirect
from docsgpt.api.connector.routes import build_callback_redirect
got = build_callback_redirect({"status": "success", "provider": "x"})
assert got.startswith("/api/connectors/callback-status?")
@@ -50,7 +50,7 @@ class TestBuildCallbackRedirect:
class TestConnectorAuth:
def test_returns_400_missing_provider(self, app):
from application.api.connector.routes import ConnectorAuth
from docsgpt.api.connector.routes import ConnectorAuth
with app.test_request_context("/api/connectors/auth"):
from flask import request
@@ -59,10 +59,10 @@ class TestConnectorAuth:
assert r.status_code == 400
def test_returns_400_unsupported_provider(self, app):
from application.api.connector.routes import ConnectorAuth
from docsgpt.api.connector.routes import ConnectorAuth
with patch(
"application.api.connector.routes.ConnectorCreator.is_supported",
"docsgpt.api.connector.routes.ConnectorCreator.is_supported",
return_value=False,
), app.test_request_context("/api/connectors/auth?provider=nope"):
from flask import request
@@ -71,10 +71,10 @@ class TestConnectorAuth:
assert r.status_code == 400
def test_returns_401_unauthenticated(self, app):
from application.api.connector.routes import ConnectorAuth
from docsgpt.api.connector.routes import ConnectorAuth
with patch(
"application.api.connector.routes.ConnectorCreator.is_supported",
"docsgpt.api.connector.routes.ConnectorCreator.is_supported",
return_value=True,
), app.test_request_context(
"/api/connectors/auth?provider=google_drive"
@@ -85,16 +85,16 @@ class TestConnectorAuth:
assert r.status_code == 401
def test_generates_authorization_url(self, app, pg_conn):
from application.api.connector.routes import ConnectorAuth
from docsgpt.api.connector.routes import ConnectorAuth
fake_auth = MagicMock()
fake_auth.get_authorization_url.return_value = "https://ex/auth?state=x"
with _patch_db(pg_conn), patch(
"application.api.connector.routes.ConnectorCreator.is_supported",
"docsgpt.api.connector.routes.ConnectorCreator.is_supported",
return_value=True,
), patch(
"application.api.connector.routes.ConnectorCreator.create_auth",
"docsgpt.api.connector.routes.ConnectorCreator.create_auth",
return_value=fake_auth,
), app.test_request_context(
"/api/connectors/auth?provider=google_drive"
@@ -109,11 +109,11 @@ class TestConnectorAuth:
class TestConnectorsCallback:
def test_invalid_provider_redirects_to_error(self, app):
from application.api.connector.routes import ConnectorsCallback
from docsgpt.api.connector.routes import ConnectorsCallback
state = _encode_state({"provider": "bogus", "object_id": "x"})
with patch(
"application.api.connector.routes.ConnectorCreator.is_supported",
"docsgpt.api.connector.routes.ConnectorCreator.is_supported",
return_value=False,
), app.test_request_context(f"/api/connectors/callback?state={state}"):
r = ConnectorsCallback().get()
@@ -121,11 +121,11 @@ class TestConnectorsCallback:
assert "callback-status" in r.location
def test_access_denied_redirects_cancelled(self, app):
from application.api.connector.routes import ConnectorsCallback
from docsgpt.api.connector.routes import ConnectorsCallback
state = _encode_state({"provider": "google_drive", "object_id": "x"})
with patch(
"application.api.connector.routes.ConnectorCreator.is_supported",
"docsgpt.api.connector.routes.ConnectorCreator.is_supported",
return_value=True,
), app.test_request_context(
f"/api/connectors/callback?state={state}&error=access_denied"
@@ -135,11 +135,11 @@ class TestConnectorsCallback:
assert "cancelled" in r.location
def test_error_redirects_error(self, app):
from application.api.connector.routes import ConnectorsCallback
from docsgpt.api.connector.routes import ConnectorsCallback
state = _encode_state({"provider": "google_drive", "object_id": "x"})
with patch(
"application.api.connector.routes.ConnectorCreator.is_supported",
"docsgpt.api.connector.routes.ConnectorCreator.is_supported",
return_value=True,
), app.test_request_context(
f"/api/connectors/callback?state={state}&error=other"
@@ -149,11 +149,11 @@ class TestConnectorsCallback:
assert "status=error" in r.location
def test_missing_code_redirects_error(self, app):
from application.api.connector.routes import ConnectorsCallback
from docsgpt.api.connector.routes import ConnectorsCallback
state = _encode_state({"provider": "google_drive", "object_id": "x"})
with patch(
"application.api.connector.routes.ConnectorCreator.is_supported",
"docsgpt.api.connector.routes.ConnectorCreator.is_supported",
return_value=True,
), app.test_request_context(
f"/api/connectors/callback?state={state}"
@@ -163,8 +163,8 @@ class TestConnectorsCallback:
assert "status=error" in r.location
def test_successful_callback_updates_session(self, app, pg_conn):
from application.api.connector.routes import ConnectorsCallback
from application.storage.db.repositories.connector_sessions import (
from docsgpt.api.connector.routes import ConnectorsCallback
from docsgpt.storage.db.repositories.connector_sessions import (
ConnectorSessionsRepository,
)
@@ -190,10 +190,10 @@ class TestConnectorsCallback:
)
with _patch_db(pg_conn), patch(
"application.api.connector.routes.ConnectorCreator.is_supported",
"docsgpt.api.connector.routes.ConnectorCreator.is_supported",
return_value=True,
), patch(
"application.api.connector.routes.ConnectorCreator.create_auth",
"docsgpt.api.connector.routes.ConnectorCreator.create_auth",
return_value=fake_auth,
), app.test_request_context(
f"/api/connectors/callback?state={state}&code=auth-code"
@@ -203,17 +203,17 @@ class TestConnectorsCallback:
assert "status=success" in r.location
def test_token_exchange_failure_redirects_error(self, app, pg_conn):
from application.api.connector.routes import ConnectorsCallback
from docsgpt.api.connector.routes import ConnectorsCallback
state = _encode_state({"provider": "google_drive", "object_id": ""})
fake_auth = MagicMock()
fake_auth.exchange_code_for_tokens.side_effect = RuntimeError("fail")
with _patch_db(pg_conn), patch(
"application.api.connector.routes.ConnectorCreator.is_supported",
"docsgpt.api.connector.routes.ConnectorCreator.is_supported",
return_value=True,
), patch(
"application.api.connector.routes.ConnectorCreator.create_auth",
"docsgpt.api.connector.routes.ConnectorCreator.create_auth",
return_value=fake_auth,
), app.test_request_context(
f"/api/connectors/callback?state={state}&code=auth-code"
@@ -224,7 +224,7 @@ class TestConnectorsCallback:
class TestConnectorFiles:
def test_returns_400_missing_fields(self, app):
from application.api.connector.routes import ConnectorFiles
from docsgpt.api.connector.routes import ConnectorFiles
with app.test_request_context(
"/api/connectors/files",
@@ -237,7 +237,7 @@ class TestConnectorFiles:
assert r.status_code == 400
def test_returns_401_unauthenticated(self, app):
from application.api.connector.routes import ConnectorFiles
from docsgpt.api.connector.routes import ConnectorFiles
with app.test_request_context(
"/api/connectors/files",
@@ -250,7 +250,7 @@ class TestConnectorFiles:
assert r.status_code == 401
def test_returns_401_invalid_session(self, app, pg_conn):
from application.api.connector.routes import ConnectorFiles
from docsgpt.api.connector.routes import ConnectorFiles
with _patch_db(pg_conn), app.test_request_context(
"/api/connectors/files",
@@ -263,8 +263,8 @@ class TestConnectorFiles:
assert r.status_code == 401
def test_lists_files_successfully(self, app, pg_conn):
from application.api.connector.routes import ConnectorFiles
from application.storage.db.repositories.connector_sessions import (
from docsgpt.api.connector.routes import ConnectorFiles
from docsgpt.storage.db.repositories.connector_sessions import (
ConnectorSessionsRepository,
)
@@ -292,7 +292,7 @@ class TestConnectorFiles:
fake_loader.next_page_token = None
with _patch_db(pg_conn), patch(
"application.api.connector.routes.ConnectorCreator.create_connector",
"docsgpt.api.connector.routes.ConnectorCreator.create_connector",
return_value=fake_loader,
), app.test_request_context(
"/api/connectors/files",
@@ -308,7 +308,7 @@ class TestConnectorFiles:
class TestConnectorValidateSession:
def test_returns_400_missing_fields(self, app):
from application.api.connector.routes import ConnectorValidateSession
from docsgpt.api.connector.routes import ConnectorValidateSession
with app.test_request_context(
"/api/connectors/validate-session",
@@ -321,7 +321,7 @@ class TestConnectorValidateSession:
assert r.status_code == 400
def test_returns_401_unauthenticated(self, app):
from application.api.connector.routes import ConnectorValidateSession
from docsgpt.api.connector.routes import ConnectorValidateSession
with app.test_request_context(
"/api/connectors/validate-session",
@@ -334,7 +334,7 @@ class TestConnectorValidateSession:
assert r.status_code == 401
def test_returns_401_invalid_session(self, app, pg_conn):
from application.api.connector.routes import ConnectorValidateSession
from docsgpt.api.connector.routes import ConnectorValidateSession
with _patch_db(pg_conn), app.test_request_context(
"/api/connectors/validate-session",
@@ -347,8 +347,8 @@ class TestConnectorValidateSession:
assert r.status_code == 401
def test_valid_session_returns_tokens(self, app, pg_conn):
from application.api.connector.routes import ConnectorValidateSession
from application.storage.db.repositories.connector_sessions import (
from docsgpt.api.connector.routes import ConnectorValidateSession
from docsgpt.storage.db.repositories.connector_sessions import (
ConnectorSessionsRepository,
)
@@ -368,7 +368,7 @@ class TestConnectorValidateSession:
fake_auth.is_token_expired.return_value = False
with _patch_db(pg_conn), patch(
"application.api.connector.routes.ConnectorCreator.create_auth",
"docsgpt.api.connector.routes.ConnectorCreator.create_auth",
return_value=fake_auth,
), app.test_request_context(
"/api/connectors/validate-session",
@@ -382,8 +382,8 @@ class TestConnectorValidateSession:
assert r.json["access_token"] == "at"
def test_expired_token_refreshes(self, app, pg_conn):
from application.api.connector.routes import ConnectorValidateSession
from application.storage.db.repositories.connector_sessions import (
from docsgpt.api.connector.routes import ConnectorValidateSession
from docsgpt.storage.db.repositories.connector_sessions import (
ConnectorSessionsRepository,
)
@@ -404,7 +404,7 @@ class TestConnectorValidateSession:
fake_auth.sanitize_token_info.return_value = {"access_token": "new-at"}
with _patch_db(pg_conn), patch(
"application.api.connector.routes.ConnectorCreator.create_auth",
"docsgpt.api.connector.routes.ConnectorCreator.create_auth",
return_value=fake_auth,
), app.test_request_context(
"/api/connectors/validate-session",
@@ -420,7 +420,7 @@ class TestConnectorValidateSession:
class TestConnectorDisconnect:
def test_returns_400_missing_provider(self, app):
from application.api.connector.routes import ConnectorDisconnect
from docsgpt.api.connector.routes import ConnectorDisconnect
with app.test_request_context(
"/api/connectors/disconnect", method="POST", json={}
@@ -429,8 +429,8 @@ class TestConnectorDisconnect:
assert r.status_code == 400
def test_disconnects_session(self, app, pg_conn):
from application.api.connector.routes import ConnectorDisconnect
from application.storage.db.repositories.connector_sessions import (
from docsgpt.api.connector.routes import ConnectorDisconnect
from docsgpt.storage.db.repositories.connector_sessions import (
ConnectorSessionsRepository,
)
@@ -449,7 +449,7 @@ class TestConnectorDisconnect:
assert r.json["success"] is True
def test_disconnect_without_session_token_succeeds(self, app):
from application.api.connector.routes import ConnectorDisconnect
from docsgpt.api.connector.routes import ConnectorDisconnect
with app.test_request_context(
"/api/connectors/disconnect",
@@ -462,7 +462,7 @@ class TestConnectorDisconnect:
class TestConnectorSync:
def test_returns_401_unauthenticated(self, app):
from application.api.connector.routes import ConnectorSync
from docsgpt.api.connector.routes import ConnectorSync
with app.test_request_context(
"/api/connectors/sync",
@@ -475,7 +475,7 @@ class TestConnectorSync:
assert r.status_code == 401
def test_returns_400_missing_fields(self, app):
from application.api.connector.routes import ConnectorSync
from docsgpt.api.connector.routes import ConnectorSync
with app.test_request_context(
"/api/connectors/sync", method="POST", json={"source_id": "x"}
@@ -486,7 +486,7 @@ class TestConnectorSync:
assert r.status_code == 400
def test_returns_404_source_not_found(self, app, pg_conn):
from application.api.connector.routes import ConnectorSync
from docsgpt.api.connector.routes import ConnectorSync
with _patch_db(pg_conn), app.test_request_context(
"/api/connectors/sync",
@@ -502,8 +502,8 @@ class TestConnectorSync:
assert r.status_code == 404
def test_returns_400_missing_provider(self, app, pg_conn):
from application.api.connector.routes import ConnectorSync
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.api.connector.routes import ConnectorSync
from docsgpt.storage.db.repositories.sources import SourcesRepository
user = "u-noprov"
src = SourcesRepository(pg_conn).create(
@@ -521,8 +521,8 @@ class TestConnectorSync:
assert r.status_code == 400
def test_triggers_sync_task(self, app, pg_conn):
from application.api.connector.routes import ConnectorSync
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.api.connector.routes import ConnectorSync
from docsgpt.storage.db.repositories.sources import SourcesRepository
user = "u-sync-trigger"
src = SourcesRepository(pg_conn).create(
@@ -532,7 +532,7 @@ class TestConnectorSync:
fake_task = MagicMock(id="task-abc")
with _patch_db(pg_conn), patch(
"application.api.connector.routes.ingest_connector_task.delay",
"docsgpt.api.connector.routes.ingest_connector_task.delay",
return_value=fake_task,
), app.test_request_context(
"/api/connectors/sync",
@@ -548,7 +548,7 @@ class TestConnectorSync:
class TestConnectorCallbackStatus:
def test_returns_html_for_success(self, app):
from application.api.connector.routes import ConnectorCallbackStatus
from docsgpt.api.connector.routes import ConnectorCallbackStatus
with app.test_request_context(
"/api/connectors/callback-status?"
@@ -561,7 +561,7 @@ class TestConnectorCallbackStatus:
assert b"hello" in r.data
def test_returns_html_for_error(self, app):
from application.api.connector.routes import ConnectorCallbackStatus
from docsgpt.api.connector.routes import ConnectorCallbackStatus
with app.test_request_context(
"/api/connectors/callback-status?status=error&message=oops"
@@ -571,7 +571,7 @@ class TestConnectorCallbackStatus:
assert b"oops" in r.data
def test_unknown_status_coerces_to_error(self, app):
from application.api.connector.routes import ConnectorCallbackStatus
from docsgpt.api.connector.routes import ConnectorCallbackStatus
with app.test_request_context(
"/api/connectors/callback-status?status=weird"
+40 -40
View File
@@ -1,4 +1,4 @@
"""Tests for application/api/events/routes.py — the SSE endpoint.
"""Tests for docsgpt/api/events/routes.py — the SSE endpoint.
The SSE generator runs in a separate thread under the WSGI test client;
we drive it with mocked Redis (the ``pubsub.get_message`` and ``xrange``
@@ -20,11 +20,11 @@ from flask import Flask, request
def _make_app():
"""Mount the events blueprint on a bare Flask app + JWT shim.
The shim mimics ``application/app.py`` populating
The shim mimics ``docsgpt/app.py`` populating
``request.decoded_token`` so the SSE handler's auth gate sees a
user-id without requiring the full app stack.
"""
from application.api.events.routes import events
from docsgpt.api.events.routes import events
app = Flask(__name__)
app.register_blueprint(events)
@@ -103,7 +103,7 @@ class TestAuthGate:
assert r.status_code == 401
def test_rejects_when_decoded_token_missing_sub(self):
from application.api.events import routes as events_module
from docsgpt.api.events import routes as events_module
app = _make_app()
@@ -123,7 +123,7 @@ class TestAuthGate:
class TestStreamShape:
def test_returns_event_stream_mimetype_and_no_buffering_header(self):
from application.api.events import routes as events_module
from docsgpt.api.events import routes as events_module
app = _make_app()
with patch.object(events_module, "get_redis_instance", return_value=None):
@@ -138,7 +138,7 @@ class TestStreamShape:
assert b": connected" in body
def test_emits_push_disabled_when_setting_off(self):
from application.api.events import routes as events_module
from docsgpt.api.events import routes as events_module
app = _make_app()
with patch.object(events_module, "get_redis_instance", return_value=None), \
@@ -155,7 +155,7 @@ class TestStreamShape:
class TestConcurrencyCap:
def test_returns_429_when_user_over_cap(self):
from application.api.events import routes as events_module
from docsgpt.api.events import routes as events_module
app = _make_app()
redis_client = MagicMock()
@@ -171,7 +171,7 @@ class TestConcurrencyCap:
redis_client.decr.assert_called_once_with("user:alice:sse_count")
def test_skips_cap_when_zero_disabled(self):
from application.api.events import routes as events_module
from docsgpt.api.events import routes as events_module
app = _make_app()
redis_client = MagicMock()
@@ -201,7 +201,7 @@ class TestConcurrencyCap:
class TestReplayAndTail:
def test_replay_yields_xrange_entries_with_injected_id(self):
from application.api.events import routes as events_module
from docsgpt.api.events import routes as events_module
app = _make_app()
redis_client = MagicMock()
@@ -227,7 +227,7 @@ class TestReplayAndTail:
# Topic.subscribe yields an immediate timeout so the generator
# keeps running long enough to flush replay; subsequent calls
# also return None.
from application.api.events.routes import _SSE_LINE_SPLIT # noqa: F401
from docsgpt.api.events.routes import _SSE_LINE_SPLIT # noqa: F401
# Fake the broadcast Topic to invoke on_subscribe immediately
# then yield None ticks until close.
@@ -265,7 +265,7 @@ class TestReplayAndTail:
must still reach the client. Prior to the fix the in-loop flush
was the only flush, so the backlog was silently dropped.
"""
from application.api.events import routes as events_module
from docsgpt.api.events import routes as events_module
app = _make_app()
redis_client = MagicMock()
@@ -321,7 +321,7 @@ class TestReplayAndTail:
redis_client.xrange.assert_called_once()
def test_invalid_last_event_id_emits_truncation_notice(self):
from application.api.events import routes as events_module
from docsgpt.api.events import routes as events_module
app = _make_app()
redis_client = MagicMock()
@@ -358,7 +358,7 @@ class TestReplayAndTail:
just refuse to use the bogus id for ordering, so it ships
without an SSE ``id:`` header and ``max_replayed_id`` stays put.
"""
from application.api.events import routes as events_module
from docsgpt.api.events import routes as events_module
app = _make_app()
redis_client = MagicMock()
@@ -435,17 +435,17 @@ class TestReplayRateLimit:
"""Enumeration defenses on the per-user backlog."""
def test_allow_replay_returns_true_when_budget_disabled(self):
from application.api.events.routes import _allow_replay
from docsgpt.api.events.routes import _allow_replay
with patch("application.api.events.routes.settings") as mock_settings:
with patch("docsgpt.api.events.routes.settings") as mock_settings:
mock_settings.EVENTS_REPLAY_BUDGET_REQUESTS_PER_WINDOW = 0
mock_settings.EVENTS_REPLAY_BUDGET_WINDOW_SECONDS = 60
assert _allow_replay(MagicMock(), "alice", "1735682400000-0") is True
def test_allow_replay_returns_true_when_redis_unavailable(self):
from application.api.events.routes import _allow_replay
from docsgpt.api.events.routes import _allow_replay
with patch("application.api.events.routes.settings") as mock_settings:
with patch("docsgpt.api.events.routes.settings") as mock_settings:
mock_settings.EVENTS_REPLAY_BUDGET_REQUESTS_PER_WINDOW = 5
mock_settings.EVENTS_REPLAY_BUDGET_WINDOW_SECONDS = 60
assert _allow_replay(None, "alice", "1735682400000-0") is True
@@ -455,9 +455,9 @@ class TestReplayRateLimit:
live and must never 429 on the replay budget, no matter how many
tabs open at once (30 fresh connects/min used to exhaust it).
"""
from application.api.events.routes import _allow_replay
from docsgpt.api.events.routes import _allow_replay
with patch("application.api.events.routes.settings") as mock_settings:
with patch("docsgpt.api.events.routes.settings") as mock_settings:
mock_settings.EVENTS_REPLAY_BUDGET_REQUESTS_PER_WINDOW = 3
mock_settings.EVENTS_REPLAY_BUDGET_WINDOW_SECONDS = 60
redis = MagicMock()
@@ -470,9 +470,9 @@ class TestReplayRateLimit:
redis.xlen.assert_not_called()
def test_allow_replay_passes_until_budget_exhausted(self):
from application.api.events.routes import _allow_replay
from docsgpt.api.events.routes import _allow_replay
with patch("application.api.events.routes.settings") as mock_settings:
with patch("docsgpt.api.events.routes.settings") as mock_settings:
mock_settings.EVENTS_REPLAY_BUDGET_REQUESTS_PER_WINDOW = 3
mock_settings.EVENTS_REPLAY_BUDGET_WINDOW_SECONDS = 60
redis = MagicMock()
@@ -499,9 +499,9 @@ class TestReplayRateLimit:
assert call.args[1] == 60
def test_allow_replay_fail_open_on_redis_error(self):
from application.api.events.routes import _allow_replay
from docsgpt.api.events.routes import _allow_replay
with patch("application.api.events.routes.settings") as mock_settings:
with patch("docsgpt.api.events.routes.settings") as mock_settings:
mock_settings.EVENTS_REPLAY_BUDGET_REQUESTS_PER_WINDOW = 5
mock_settings.EVENTS_REPLAY_BUDGET_WINDOW_SECONDS = 60
redis = MagicMock()
@@ -517,9 +517,9 @@ class TestReplayRateLimit:
until an operator DEL'd the key. The fix calls EXPIRE on every
successful INCR so the next call still re-seeds the TTL.
"""
from application.api.events.routes import _allow_replay
from docsgpt.api.events.routes import _allow_replay
with patch("application.api.events.routes.settings") as mock_settings:
with patch("docsgpt.api.events.routes.settings") as mock_settings:
mock_settings.EVENTS_REPLAY_BUDGET_REQUESTS_PER_WINDOW = 5
mock_settings.EVENTS_REPLAY_BUDGET_WINDOW_SECONDS = 60
redis = MagicMock()
@@ -549,7 +549,7 @@ class TestReplayRateLimit:
assert call.args[1] == 60
def test_replay_backlog_passes_count_to_xrange(self):
from application.api.events.routes import _replay_backlog
from docsgpt.api.events.routes import _replay_backlog
redis = MagicMock()
redis.xrange.return_value = []
@@ -567,7 +567,7 @@ class TestReplayRateLimit:
the cursor pinned so the next reconnect (after the budget
window slides) can replay normally.
"""
from application.api.events import routes as events_module
from docsgpt.api.events import routes as events_module
app = _make_app()
redis_client = MagicMock()
@@ -603,7 +603,7 @@ class TestReplayRateLimit:
class TestFormatHelpers:
def test_format_sse_two_terminating_newlines(self):
from application.api.events.routes import _format_sse
from docsgpt.api.events.routes import _format_sse
out = _format_sse("hello", event_id="1-0")
assert out.endswith("\n\n")
@@ -625,7 +625,7 @@ class TestFormatHelpers:
],
)
def test_normalize_last_event_id(self, candidate, expected):
from application.api.events.routes import _normalize_last_event_id
from docsgpt.api.events.routes import _normalize_last_event_id
assert _normalize_last_event_id(candidate) == expected
@@ -635,43 +635,43 @@ class TestFormatHelpers:
class TestReplayPolicy:
def test_replay_floor_id_disabled_when_setting_zero(self):
from application.api.events.routes import _replay_floor_id
from docsgpt.api.events.routes import _replay_floor_id
with patch("application.api.events.routes.settings") as mock_settings:
with patch("docsgpt.api.events.routes.settings") as mock_settings:
mock_settings.EVENTS_REPLAY_MAX_AGE_HOURS = 0
assert _replay_floor_id() is None
def test_replay_floor_id_is_ms_stream_id(self):
from application.api.events.routes import _replay_floor_id
from docsgpt.api.events.routes import _replay_floor_id
with patch("application.api.events.routes.settings") as mock_settings:
with patch("docsgpt.api.events.routes.settings") as mock_settings:
mock_settings.EVENTS_REPLAY_MAX_AGE_HOURS = 48
with patch(
"application.api.events.routes.time.time",
"docsgpt.api.events.routes.time.time",
return_value=1_800_000_000.0,
):
floor = _replay_floor_id()
assert floor == f"{(1_800_000_000 - 48 * 3600) * 1000}-0"
def test_replay_backlog_uses_cursor_when_newer_than_floor(self):
from application.api.events.routes import _replay_backlog
from docsgpt.api.events.routes import _replay_backlog
redis = MagicMock()
redis.xrange.return_value = []
with patch(
"application.api.events.routes._replay_floor_id",
"docsgpt.api.events.routes._replay_floor_id",
return_value="1000-0",
):
list(_replay_backlog(redis, "alice", "2000-0", 200))
assert redis.xrange.call_args.kwargs["min"] == "(2000-0"
def test_replay_backlog_clamps_start_to_age_floor(self):
from application.api.events.routes import _replay_backlog
from docsgpt.api.events.routes import _replay_backlog
redis = MagicMock()
redis.xrange.return_value = []
with patch(
"application.api.events.routes._replay_floor_id",
"docsgpt.api.events.routes._replay_floor_id",
return_value="5000-0",
):
list(_replay_backlog(redis, "alice", "2000-0", 200))
@@ -683,7 +683,7 @@ class TestReplayPolicy:
def test_no_cursor_connect_never_replays(self):
"""A fresh session (no Last-Event-ID) starts live: no XRANGE, no
weeks-old backlog on every tab-open."""
from application.api.events import routes as events_module
from docsgpt.api.events import routes as events_module
app = _make_app()
redis_client = MagicMock()
@@ -711,7 +711,7 @@ class TestReplayPolicy:
"""An age-clamped snapshot has a gap the client can't see from
entry ids alone — it must get the truncation notice so it
refetches full state instead of trusting a partial replay."""
from application.api.events import routes as events_module
from docsgpt.api.events import routes as events_module
app = _make_app()
redis_client = MagicMock()
+43 -43
View File
@@ -1,4 +1,4 @@
"""Tests for application/api/internal/routes.py.
"""Tests for docsgpt/api/internal/routes.py.
Uses the ephemeral ``pg_conn`` fixture so the sources repository writes
happen against a real Postgres schema.
@@ -22,13 +22,13 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.internal.routes.db_session", _yield
"docsgpt.api.internal.routes.db_session", _yield
):
yield
def _make_app():
from application.api.internal.routes import internal
from docsgpt.api.internal.routes import internal
app = Flask(__name__)
app.register_blueprint(internal)
@@ -40,7 +40,7 @@ class TestVerifyInternalKey:
def test_rejects_when_internal_key_not_configured(self):
app = _make_app()
with patch(
"application.api.internal.routes.settings.INTERNAL_KEY", ""
"docsgpt.api.internal.routes.settings.INTERNAL_KEY", ""
):
with app.test_client() as c:
r = c.get("/api/download")
@@ -49,7 +49,7 @@ class TestVerifyInternalKey:
def test_rejects_when_key_missing(self):
app = _make_app()
with patch(
"application.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
"docsgpt.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
):
with app.test_client() as c:
r = c.get("/api/download")
@@ -58,7 +58,7 @@ class TestVerifyInternalKey:
def test_rejects_when_key_mismatch(self):
app = _make_app()
with patch(
"application.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
"docsgpt.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
):
with app.test_client() as c:
r = c.get("/api/download", headers={"X-Internal-Key": "wrong"})
@@ -69,12 +69,12 @@ class TestDownloadFile:
def test_returns_404_for_missing_file(self, tmp_path):
app = _make_app()
with patch(
"application.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
"docsgpt.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
), patch(
"application.api.internal.routes.settings.UPLOAD_FOLDER",
"docsgpt.api.internal.routes.settings.UPLOAD_FOLDER",
str(tmp_path),
), patch(
"application.api.internal.routes.current_dir", ""
"docsgpt.api.internal.routes.current_dir", ""
):
with app.test_client() as c:
r = c.get(
@@ -90,12 +90,12 @@ class TestDownloadFile:
(user_dir / "hello.txt").write_text("hi there")
with patch(
"application.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
"docsgpt.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
), patch(
"application.api.internal.routes.settings.UPLOAD_FOLDER",
"docsgpt.api.internal.routes.settings.UPLOAD_FOLDER",
str(tmp_path),
), patch(
"application.api.internal.routes.current_dir", ""
"docsgpt.api.internal.routes.current_dir", ""
):
with app.test_client() as c:
r = c.get(
@@ -120,7 +120,7 @@ class TestUploadIndex:
def test_rejects_without_auth(self):
app = _make_app()
with patch(
"application.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
"docsgpt.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
):
with app.test_client() as c:
r = c.post("/api/upload_index")
@@ -129,7 +129,7 @@ class TestUploadIndex:
def test_rejects_missing_user(self):
app = _make_app()
with patch(
"application.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
"docsgpt.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
):
with app.test_client() as c:
r = c.post("/api/upload_index", headers=_AUTH, data={})
@@ -140,7 +140,7 @@ class TestUploadIndex:
app = _make_app()
form = {"user": "alice"}
with patch(
"application.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
"docsgpt.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
):
with app.test_client() as c:
r = c.post("/api/upload_index", headers=_AUTH, data=form)
@@ -149,18 +149,18 @@ class TestUploadIndex:
def test_creates_new_source_for_non_faiss_store(self, pg_conn):
"""For non-faiss VECTOR_STORE the route skips file uploads entirely."""
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
app = _make_app()
form = {**self._base_form(source_id="legacy-source-1")}
with patch(
"application.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
"docsgpt.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
), patch(
"application.api.internal.routes.settings.VECTOR_STORE", "milvus"
"docsgpt.api.internal.routes.settings.VECTOR_STORE", "milvus"
), patch(
"application.api.internal.routes.settings.EMBEDDINGS_NAME", "emb"
"docsgpt.api.internal.routes.settings.EMBEDDINGS_NAME", "emb"
), patch(
"application.api.internal.routes.StorageCreator.get_storage",
"docsgpt.api.internal.routes.StorageCreator.get_storage",
return_value=MagicMock(),
), _patch_db(pg_conn):
with app.test_client() as c:
@@ -173,7 +173,7 @@ class TestUploadIndex:
assert found is not None
def test_updates_existing_source(self, pg_conn):
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
repo = SourcesRepository(pg_conn)
created = repo.create(
@@ -185,13 +185,13 @@ class TestUploadIndex:
app = _make_app()
form = {**self._base_form(source_id="legacy-src-2"), "tokens": "999"}
with patch(
"application.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
"docsgpt.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
), patch(
"application.api.internal.routes.settings.VECTOR_STORE", "milvus"
"docsgpt.api.internal.routes.settings.VECTOR_STORE", "milvus"
), patch(
"application.api.internal.routes.settings.EMBEDDINGS_NAME", "emb"
"docsgpt.api.internal.routes.settings.EMBEDDINGS_NAME", "emb"
), patch(
"application.api.internal.routes.StorageCreator.get_storage",
"docsgpt.api.internal.routes.StorageCreator.get_storage",
return_value=MagicMock(),
), _patch_db(pg_conn):
with app.test_client() as c:
@@ -211,13 +211,13 @@ class TestUploadIndex:
"file_name_map": json.dumps({"a.txt": "Original A.txt"}),
}
with patch(
"application.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
"docsgpt.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
), patch(
"application.api.internal.routes.settings.VECTOR_STORE", "milvus"
"docsgpt.api.internal.routes.settings.VECTOR_STORE", "milvus"
), patch(
"application.api.internal.routes.settings.EMBEDDINGS_NAME", "emb"
"docsgpt.api.internal.routes.settings.EMBEDDINGS_NAME", "emb"
), patch(
"application.api.internal.routes.StorageCreator.get_storage",
"docsgpt.api.internal.routes.StorageCreator.get_storage",
return_value=MagicMock(),
), _patch_db(pg_conn):
with app.test_client() as c:
@@ -232,13 +232,13 @@ class TestUploadIndex:
"file_name_map": "also-not-json",
}
with patch(
"application.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
"docsgpt.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
), patch(
"application.api.internal.routes.settings.VECTOR_STORE", "milvus"
"docsgpt.api.internal.routes.settings.VECTOR_STORE", "milvus"
), patch(
"application.api.internal.routes.settings.EMBEDDINGS_NAME", "emb"
"docsgpt.api.internal.routes.settings.EMBEDDINGS_NAME", "emb"
), patch(
"application.api.internal.routes.StorageCreator.get_storage",
"docsgpt.api.internal.routes.StorageCreator.get_storage",
return_value=MagicMock(),
), _patch_db(pg_conn):
with app.test_client() as c:
@@ -249,11 +249,11 @@ class TestUploadIndex:
app = _make_app()
form = self._base_form(source_id="legacy-src-5")
with patch(
"application.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
"docsgpt.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
), patch(
"application.api.internal.routes.settings.VECTOR_STORE", "faiss"
"docsgpt.api.internal.routes.settings.VECTOR_STORE", "faiss"
), patch(
"application.api.internal.routes.StorageCreator.get_storage",
"docsgpt.api.internal.routes.StorageCreator.get_storage",
return_value=MagicMock(),
):
with app.test_client() as c:
@@ -273,11 +273,11 @@ class TestUploadIndex:
"file_faiss": (io.BytesIO(b"faiss-data"), "index.faiss"),
}
with patch(
"application.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
"docsgpt.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
), patch(
"application.api.internal.routes.settings.VECTOR_STORE", "faiss"
"docsgpt.api.internal.routes.settings.VECTOR_STORE", "faiss"
), patch(
"application.api.internal.routes.StorageCreator.get_storage",
"docsgpt.api.internal.routes.StorageCreator.get_storage",
return_value=MagicMock(),
):
with app.test_client() as c:
@@ -299,13 +299,13 @@ class TestUploadIndex:
"file_pkl": (io.BytesIO(b"pkl-data"), "index.pkl"),
}
with patch(
"application.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
"docsgpt.api.internal.routes.settings.INTERNAL_KEY", _TEST_KEY
), patch(
"application.api.internal.routes.settings.VECTOR_STORE", "faiss"
"docsgpt.api.internal.routes.settings.VECTOR_STORE", "faiss"
), patch(
"application.api.internal.routes.settings.EMBEDDINGS_NAME", "emb"
"docsgpt.api.internal.routes.settings.EMBEDDINGS_NAME", "emb"
), patch(
"application.api.internal.routes.StorageCreator.get_storage",
"docsgpt.api.internal.routes.StorageCreator.get_storage",
return_value=fake_storage,
), _patch_db(pg_conn):
with app.test_client() as c:
+25 -25
View File
@@ -11,7 +11,7 @@ import pytest
@pytest.fixture
def client():
from application.app import app as flask_app
from docsgpt.app import app as flask_app
flask_app.config["TESTING"] = True
return flask_app.test_client()
@@ -25,8 +25,8 @@ def _fake_readonly():
@pytest.mark.unit
class TestMeEndpoint:
def test_returns_user_id_and_roles(self, client):
with patch("application.app.handle_auth", return_value={"sub": "u1"}), patch(
"application.app.resolve_roles", return_value=["user"]
with patch("docsgpt.app.handle_auth", return_value={"sub": "u1"}), patch(
"docsgpt.app.resolve_roles", return_value=["user"]
):
resp = client.get("/api/user/me")
assert resp.status_code == 200
@@ -35,15 +35,15 @@ class TestMeEndpoint:
assert data["roles"] == ["user"]
def test_echoes_admin_role(self, client):
with patch("application.app.handle_auth", return_value={"sub": "a1"}), patch(
"application.app.resolve_roles", return_value=["admin", "user"]
with patch("docsgpt.app.handle_auth", return_value={"sub": "a1"}), patch(
"docsgpt.app.resolve_roles", return_value=["admin", "user"]
):
resp = client.get("/api/user/me")
data = json.loads(resp.data)
assert data["roles"] == ["admin", "user"]
def test_unauthenticated_returns_401(self, client):
with patch("application.app.handle_auth", return_value=None):
with patch("docsgpt.app.handle_auth", return_value=None):
resp = client.get("/api/user/me")
assert resp.status_code == 401
@@ -51,24 +51,24 @@ class TestMeEndpoint:
@pytest.mark.unit
class TestAdminUsersEndpoint:
def test_non_admin_forbidden(self, client):
with patch("application.app.handle_auth", return_value={"sub": "u1"}), patch(
"application.app.resolve_roles", return_value=["user"]
with patch("docsgpt.app.handle_auth", return_value={"sub": "u1"}), patch(
"docsgpt.app.resolve_roles", return_value=["user"]
):
resp = client.get("/api/admin/users")
assert resp.status_code == 403
def test_unauthenticated_401(self, client):
with patch("application.app.handle_auth", return_value=None):
with patch("docsgpt.app.handle_auth", return_value=None):
resp = client.get("/api/admin/users")
assert resp.status_code == 401
@staticmethod
def _admin_get(client, repo, query=""):
# The list endpoint resolves users (with last_seen) via AdminStatsRepository.
with patch("application.app.handle_auth", return_value={"sub": "a1"}), patch(
"application.app.resolve_roles", return_value=["admin", "user"]
), patch("application.api.admin.routes.db_readonly", _fake_readonly), patch(
"application.api.admin.routes.AdminStatsRepository", return_value=repo
with patch("docsgpt.app.handle_auth", return_value={"sub": "a1"}), patch(
"docsgpt.app.resolve_roles", return_value=["admin", "user"]
), patch("docsgpt.api.admin.routes.db_readonly", _fake_readonly), patch(
"docsgpt.api.admin.routes.AdminStatsRepository", return_value=repo
):
return client.get(f"/api/admin/users{query}")
@@ -142,10 +142,10 @@ class TestChokepointOverwritesForgedRoles:
"""
def test_forged_admin_ignored_in_session_jwt_mode(self, client):
from application.api.user import authz
from docsgpt.api.user import authz
forged = lambda *a, **k: {"sub": "attacker", "roles": ["admin"]} # noqa: E731
with patch("application.app.handle_auth", side_effect=forged), patch.object(
with patch("docsgpt.app.handle_auth", side_effect=forged), patch.object(
authz.settings, "AUTH_TYPE", "session_jwt"
):
admin_resp = client.get("/api/admin/users")
@@ -154,10 +154,10 @@ class TestChokepointOverwritesForgedRoles:
assert json.loads(me_resp.data)["roles"] == ["user"]
def test_forged_admin_ignored_in_no_auth_with_local_admin_off(self, client):
from application.api.user import authz
from docsgpt.api.user import authz
forged = lambda *a, **k: {"sub": "local", "roles": ["admin"]} # noqa: E731
with patch("application.app.handle_auth", side_effect=forged), patch.object(
with patch("docsgpt.app.handle_auth", side_effect=forged), patch.object(
authz.settings, "AUTH_TYPE", None
), patch.object(authz.settings, "LOCAL_MODE_ADMIN", False):
admin_resp = client.get("/api/admin/users")
@@ -168,13 +168,13 @@ class TestChokepointOverwritesForgedRoles:
def test_forged_admin_ignored_in_oidc_mode_without_grant(self, client):
# OIDC is the only privilege-bearing mode: drive a forged admin claim
# through the real chokepoint with NO DB grant — must resolve to user.
from application.api.user import authz
from docsgpt.api.user import authz
repo = Mock()
repo.role_names_for.return_value = [] # no persisted grant
forged = lambda *a, **k: {"sub": "attacker", "roles": ["admin"]} # noqa: E731
with patch("application.app.handle_auth", side_effect=forged), patch(
"application.app.oidc_session_denied", return_value=False
with patch("docsgpt.app.handle_auth", side_effect=forged), patch(
"docsgpt.app.oidc_session_denied", return_value=False
), patch.object(authz.settings, "AUTH_TYPE", "oidc"), patch.object(
authz, "db_readonly", _fake_readonly
), patch.object(authz, "UserRolesRepository", return_value=repo):
@@ -187,20 +187,20 @@ class TestChokepointOverwritesForgedRoles:
def test_oidc_db_grant_yields_admin_end_to_end(self, client):
# The DB grant (not any claim) is the source of admin, end-to-end.
from application.api.user import authz
from docsgpt.api.user import authz
authz_repo = Mock()
authz_repo.role_names_for.return_value = ["admin"]
admin_repo = Mock()
admin_repo.list_users.return_value = (0, [])
with patch(
"application.app.handle_auth", return_value={"sub": "alice"}
), patch("application.app.oidc_session_denied", return_value=False), patch.object(
"docsgpt.app.handle_auth", return_value={"sub": "alice"}
), patch("docsgpt.app.oidc_session_denied", return_value=False), patch.object(
authz.settings, "AUTH_TYPE", "oidc"
), patch.object(authz, "db_readonly", _fake_readonly), patch.object(
authz, "UserRolesRepository", return_value=authz_repo
), patch("application.api.admin.routes.db_readonly", _fake_readonly), patch(
"application.api.admin.routes.AdminStatsRepository", return_value=admin_repo
), patch("docsgpt.api.admin.routes.db_readonly", _fake_readonly), patch(
"docsgpt.api.admin.routes.AdminStatsRepository", return_value=admin_repo
):
admin_resp = client.get("/api/admin/users")
me_resp = client.get("/api/user/me")
+64 -64
View File
@@ -18,7 +18,7 @@ import pytest
@pytest.fixture
def client():
from application.app import app as flask_app
from docsgpt.app import app as flask_app
flask_app.config["TESTING"] = True
return flask_app.test_client()
@@ -38,10 +38,10 @@ def _auth(sub="u1", roles=("user",), team_role=None):
members = Mock()
members.role_for.return_value = team_role
return [
patch("application.app.handle_auth", return_value={"sub": sub}),
patch("application.app.resolve_roles", return_value=list(roles)),
patch("application.api.user.team_authz.db_readonly", lambda: _cm(Mock())),
patch("application.api.user.team_authz.TeamMembersRepository", return_value=members),
patch("docsgpt.app.handle_auth", return_value={"sub": sub}),
patch("docsgpt.app.resolve_roles", return_value=list(roles)),
patch("docsgpt.api.user.team_authz.db_readonly", lambda: _cm(Mock())),
patch("docsgpt.api.user.team_authz.TeamMembersRepository", return_value=members),
]
@@ -58,7 +58,7 @@ def _stop(patches):
@pytest.mark.unit
class TestTeamCreation:
def test_unauthenticated_401(self, client):
with patch("application.app.handle_auth", return_value=None):
with patch("docsgpt.app.handle_auth", return_value=None):
resp = client.post("/api/teams", json={"name": "Acme"})
assert resp.status_code == 401
@@ -68,10 +68,10 @@ class TestTeamCreation:
teams_repo.create.return_value = {"id": "t1", "name": "Acme", "slug": "acme"}
members_repo = Mock()
patches = _auth(sub="alice") + [
patch("application.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch("application.api.user.teams.routes.TeamsRepository", return_value=teams_repo),
patch("docsgpt.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch("docsgpt.api.user.teams.routes.TeamsRepository", return_value=teams_repo),
patch(
"application.api.user.teams.routes.TeamMembersRepository",
"docsgpt.api.user.teams.routes.TeamMembersRepository",
return_value=members_repo,
),
]
@@ -113,10 +113,10 @@ class TestTeamAccessControl:
members_repo.list_members.return_value = []
members_repo.role_for.return_value = "team_member"
patches = _auth(sub="bob", team_role="team_member") + [
patch("application.api.user.teams.routes.db_readonly", lambda: _cm(Mock())),
patch("application.api.user.teams.routes.TeamsRepository", return_value=teams_repo),
patch("docsgpt.api.user.teams.routes.db_readonly", lambda: _cm(Mock())),
patch("docsgpt.api.user.teams.routes.TeamsRepository", return_value=teams_repo),
patch(
"application.api.user.teams.routes.TeamMembersRepository",
"docsgpt.api.user.teams.routes.TeamMembersRepository",
return_value=members_repo,
),
]
@@ -152,9 +152,9 @@ class TestTeamAccessControl:
def test_team_admin_can_add_member(self, client):
members_repo = Mock()
patches = _auth(sub="alice", team_role="team_admin") + [
patch("application.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch("docsgpt.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch(
"application.api.user.teams.routes.TeamMembersRepository",
"docsgpt.api.user.teams.routes.TeamMembersRepository",
return_value=members_repo,
),
]
@@ -173,13 +173,13 @@ class TestTeamAccessControl:
users_repo = Mock()
users_repo.find_by_email.return_value = {"user_id": "resolved-sub"}
patches = _auth(sub="alice", team_role="team_admin") + [
patch("application.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch("docsgpt.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch(
"application.api.user.teams.routes.TeamMembersRepository",
"docsgpt.api.user.teams.routes.TeamMembersRepository",
return_value=members_repo,
),
patch(
"application.api.user.teams.routes.UsersRepository",
"docsgpt.api.user.teams.routes.UsersRepository",
return_value=users_repo,
),
]
@@ -199,9 +199,9 @@ class TestTeamAccessControl:
users_repo = Mock()
users_repo.find_by_email.return_value = None
patches = _auth(sub="alice", team_role="team_admin") + [
patch("application.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch("docsgpt.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch(
"application.api.user.teams.routes.UsersRepository",
"docsgpt.api.user.teams.routes.UsersRepository",
return_value=users_repo,
),
]
@@ -219,8 +219,8 @@ class TestTeamAccessControl:
teams_repo = Mock()
teams_repo.get.return_value = {"id": "team-1", "owner_id": "alice"}
patches = _auth(sub="bob", roles=("user",), team_role="team_admin") + [
patch("application.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch("application.api.user.teams.routes.TeamsRepository", return_value=teams_repo),
patch("docsgpt.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch("docsgpt.api.user.teams.routes.TeamsRepository", return_value=teams_repo),
]
_apply(patches)
try:
@@ -236,8 +236,8 @@ class TestTeamAccessControl:
class TestSharingAuthz:
def test_share_requires_ownership(self, client):
patches = _auth(sub="bob", team_role="team_member") + [
patch("application.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch("application.api.user.teams.routes.owns_resource", return_value=False),
patch("docsgpt.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch("docsgpt.api.user.teams.routes.owns_resource", return_value=False),
]
_apply(patches)
try:
@@ -256,10 +256,10 @@ class TestSharingAuthz:
grants_repo = Mock()
grants_repo.grant.return_value = {"id": "g1", "access_level": "viewer"}
patches = _auth(sub="alice", team_role="team_member") + [
patch("application.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch("application.api.user.teams.routes.owns_resource", return_value=True),
patch("docsgpt.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch("docsgpt.api.user.teams.routes.owns_resource", return_value=True),
patch(
"application.api.user.teams.routes.TeamResourceGrantsRepository",
"docsgpt.api.user.teams.routes.TeamResourceGrantsRepository",
return_value=grants_repo,
),
]
@@ -281,7 +281,7 @@ class TestSharingAuthz:
def test_invalid_resource_type_rejected(self, client):
patches = _auth(sub="alice", team_role="team_member") + [
patch("application.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch("docsgpt.api.user.teams.routes.db_session", lambda: _cm(Mock())),
]
_apply(patches)
try:
@@ -298,10 +298,10 @@ class TestSharingAuthz:
members_repo = Mock()
members_repo.is_member.return_value = False
patches = _auth(sub="alice", team_role="team_member") + [
patch("application.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch("application.api.user.teams.routes.owns_resource", return_value=True),
patch("docsgpt.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch("docsgpt.api.user.teams.routes.owns_resource", return_value=True),
patch(
"application.api.user.teams.routes.TeamMembersRepository",
"docsgpt.api.user.teams.routes.TeamMembersRepository",
return_value=members_repo,
),
]
@@ -325,14 +325,14 @@ class TestSharingAuthz:
grants_repo = Mock()
grants_repo.grant.return_value = {"id": "g1", "target_user_id": "bob"}
patches = _auth(sub="alice", team_role="team_member") + [
patch("application.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch("application.api.user.teams.routes.owns_resource", return_value=True),
patch("docsgpt.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch("docsgpt.api.user.teams.routes.owns_resource", return_value=True),
patch(
"application.api.user.teams.routes.TeamMembersRepository",
"docsgpt.api.user.teams.routes.TeamMembersRepository",
return_value=members_repo,
),
patch(
"application.api.user.teams.routes.TeamResourceGrantsRepository",
"docsgpt.api.user.teams.routes.TeamResourceGrantsRepository",
return_value=grants_repo,
),
]
@@ -356,8 +356,8 @@ class TestSharingAuthz:
# Team grants are UUID-only (post-cutover); a legacy/non-UUID id must be
# rejected cleanly, not cast-and-poison the txn into a generic error.
patches = _auth(sub="alice", team_role="team_member") + [
patch("application.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch("application.api.user.teams.routes.owns_resource", return_value=True),
patch("docsgpt.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch("docsgpt.api.user.teams.routes.owns_resource", return_value=True),
]
_apply(patches)
try:
@@ -385,8 +385,8 @@ class TestAdminOversight:
teams_repo = Mock()
teams_repo.list_all.return_value = [{"id": "t1", "member_count": 3}]
patches = _auth(sub="root", roles=("admin", "user")) + [
patch("application.api.user.teams.routes.db_readonly", lambda: _cm(Mock())),
patch("application.api.user.teams.routes.TeamsRepository", return_value=teams_repo),
patch("docsgpt.api.user.teams.routes.db_readonly", lambda: _cm(Mock())),
patch("docsgpt.api.user.teams.routes.TeamsRepository", return_value=teams_repo),
]
_apply(patches)
try:
@@ -407,17 +407,17 @@ class TestTeamNotifications:
teams_repo.get.return_value = {"id": "team-1", "name": "Acme"}
publish = Mock()
patches = _auth(sub="alice", team_role="team_admin") + [
patch("application.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch("application.api.user.teams.routes.db_readonly", lambda: _cm(Mock())),
patch("docsgpt.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch("docsgpt.api.user.teams.routes.db_readonly", lambda: _cm(Mock())),
patch(
"application.api.user.teams.routes.TeamMembersRepository",
"docsgpt.api.user.teams.routes.TeamMembersRepository",
return_value=members_repo,
),
patch(
"application.api.user.teams.routes.TeamsRepository",
"docsgpt.api.user.teams.routes.TeamsRepository",
return_value=teams_repo,
),
patch("application.api.user.teams.routes.publish_user_event", publish),
patch("docsgpt.api.user.teams.routes.publish_user_event", publish),
]
_apply(patches)
try:
@@ -442,17 +442,17 @@ class TestTeamNotifications:
teams_repo.get.return_value = {"id": "team-1", "name": "Acme"}
publish = Mock()
patches = _auth(sub="alice", team_role="team_admin") + [
patch("application.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch("application.api.user.teams.routes.db_readonly", lambda: _cm(Mock())),
patch("docsgpt.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch("docsgpt.api.user.teams.routes.db_readonly", lambda: _cm(Mock())),
patch(
"application.api.user.teams.routes.TeamMembersRepository",
"docsgpt.api.user.teams.routes.TeamMembersRepository",
return_value=members_repo,
),
patch(
"application.api.user.teams.routes.TeamsRepository",
"docsgpt.api.user.teams.routes.TeamsRepository",
return_value=teams_repo,
),
patch("application.api.user.teams.routes.publish_user_event", publish),
patch("docsgpt.api.user.teams.routes.publish_user_event", publish),
]
_apply(patches)
try:
@@ -478,26 +478,26 @@ class TestTeamNotifications:
teams_repo.get.return_value = {"id": "team-1", "name": "Acme"}
publish = Mock()
patches = _auth(sub="alice", team_role="team_member") + [
patch("application.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch("application.api.user.teams.routes.db_readonly", lambda: _cm(Mock())),
patch("application.api.user.teams.routes.owns_resource", return_value=True),
patch("docsgpt.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch("docsgpt.api.user.teams.routes.db_readonly", lambda: _cm(Mock())),
patch("docsgpt.api.user.teams.routes.owns_resource", return_value=True),
patch(
"application.api.user.teams.routes.TeamResourceGrantsRepository",
"docsgpt.api.user.teams.routes.TeamResourceGrantsRepository",
return_value=grants_repo,
),
patch(
"application.api.user.teams.routes.TeamMembersRepository",
"docsgpt.api.user.teams.routes.TeamMembersRepository",
return_value=members_repo,
),
patch(
"application.api.user.teams.routes.TeamsRepository",
"docsgpt.api.user.teams.routes.TeamsRepository",
return_value=teams_repo,
),
patch(
"application.api.user.teams.routes._resource_display_name",
"docsgpt.api.user.teams.routes._resource_display_name",
return_value="My Agent",
),
patch("application.api.user.teams.routes.publish_user_event", publish),
patch("docsgpt.api.user.teams.routes.publish_user_event", publish),
]
_apply(patches)
try:
@@ -529,26 +529,26 @@ class TestTeamNotifications:
teams_repo.get.return_value = {"id": "team-1", "name": "Acme"}
publish = Mock()
patches = _auth(sub="alice", team_role="team_member") + [
patch("application.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch("application.api.user.teams.routes.db_readonly", lambda: _cm(Mock())),
patch("application.api.user.teams.routes.owns_resource", return_value=True),
patch("docsgpt.api.user.teams.routes.db_session", lambda: _cm(Mock())),
patch("docsgpt.api.user.teams.routes.db_readonly", lambda: _cm(Mock())),
patch("docsgpt.api.user.teams.routes.owns_resource", return_value=True),
patch(
"application.api.user.teams.routes.TeamResourceGrantsRepository",
"docsgpt.api.user.teams.routes.TeamResourceGrantsRepository",
return_value=grants_repo,
),
patch(
"application.api.user.teams.routes.TeamMembersRepository",
"docsgpt.api.user.teams.routes.TeamMembersRepository",
return_value=members_repo,
),
patch(
"application.api.user.teams.routes.TeamsRepository",
"docsgpt.api.user.teams.routes.TeamsRepository",
return_value=teams_repo,
),
patch(
"application.api.user.teams.routes._resource_display_name",
"docsgpt.api.user.teams.routes._resource_display_name",
return_value="My Agent",
),
patch("application.api.user.teams.routes.publish_user_event", publish),
patch("docsgpt.api.user.teams.routes.publish_user_event", publish),
]
_apply(patches)
try:
Loaded 100 of 984 files, more files were not shown because too many files have changed in this diff. Show more