tests: improve RAGFlow coverage based on Codecov report (#13200)

### What problem does this PR solve?

Codecov’s coverage report shows that several RAGFlow code paths are
currently untested or under-tested. This makes it easier for regressions
to slip in during refactors and feature work.
This PR adds targeted automated tests to cover the files and branches
highlighted by Codecov, improving confidence in core behavior while
keeping runtime functionality unchanged.

### Type of change

- [x] Other (please describe): Test coverage improvement (adds/extends
unit and integration tests to address Codecov-reported gaps)
This commit is contained in:
6ba3i
2026-02-25 19:12:11 +08:00
committed by GitHub
parent 2a5ddf064d
commit 38011f2c16
56 changed files with 11453 additions and 17 deletions

View File

@@ -304,6 +304,12 @@ def metadata_summary(auth, dataset_id, params=None):
return res.json()
def metadata_batch_update(auth, dataset_id, payload=None):
url = f"{HOST_ADDRESS}{DATASETS_API_URL}/{dataset_id}/metadata/update"
res = requests.post(url=url, headers=HEADERS, auth=auth, json=payload)
return res.json()
# CHAT COMPLETIONS AND RELATED QUESTIONS
def related_questions(auth, payload=None):
url = f"{HOST_ADDRESS}/api/{VERSION}/sessions/related_questions"

View File

@@ -0,0 +1,350 @@
#
# Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import asyncio
import importlib.util
import sys
from copy import deepcopy
from pathlib import Path
from types import ModuleType, SimpleNamespace
import pytest
class _DummyManager:
def route(self, *_args, **_kwargs):
def decorator(func):
return func
return decorator
class _AwaitableValue:
def __init__(self, value):
self._value = value
def __await__(self):
async def _co():
return self._value
return _co().__await__()
class _DummyKB:
def __init__(self, embd_id="embd@factory", chunk_num=1):
self.embd_id = embd_id
self.chunk_num = chunk_num
def to_json(self):
return {"id": "kb-1"}
class _DummyDialogRecord:
def __init__(self):
self._data = {
"id": "chat-1",
"name": "chat-name",
"prompt_config": {
"system": "Answer with {knowledge}",
"parameters": [{"key": "knowledge", "optional": False}],
"prologue": "hello",
"quote": True,
},
"llm_setting": {"temperature": 0.1},
"llm_id": "glm-4",
"similarity_threshold": 0.2,
"vector_similarity_weight": 0.3,
"top_n": 6,
"rerank_id": "",
"top_k": 1024,
"kb_ids": ["kb-1"],
"icon": "icon.png",
}
def to_json(self):
return deepcopy(self._data)
def _run(coro):
return asyncio.run(coro)
def _load_chat_module(monkeypatch):
repo_root = Path(__file__).resolve().parents[4]
common_pkg = ModuleType("common")
common_pkg.__path__ = [str(repo_root / "common")]
monkeypatch.setitem(sys.modules, "common", common_pkg)
deepdoc_pkg = ModuleType("deepdoc")
deepdoc_parser_pkg = ModuleType("deepdoc.parser")
deepdoc_parser_pkg.__path__ = []
class _StubPdfParser:
pass
class _StubExcelParser:
pass
class _StubDocxParser:
pass
deepdoc_parser_pkg.PdfParser = _StubPdfParser
deepdoc_parser_pkg.ExcelParser = _StubExcelParser
deepdoc_parser_pkg.DocxParser = _StubDocxParser
deepdoc_pkg.parser = deepdoc_parser_pkg
monkeypatch.setitem(sys.modules, "deepdoc", deepdoc_pkg)
monkeypatch.setitem(sys.modules, "deepdoc.parser", deepdoc_parser_pkg)
deepdoc_excel_module = ModuleType("deepdoc.parser.excel_parser")
deepdoc_excel_module.RAGFlowExcelParser = _StubExcelParser
monkeypatch.setitem(sys.modules, "deepdoc.parser.excel_parser", deepdoc_excel_module)
deepdoc_parser_utils = ModuleType("deepdoc.parser.utils")
deepdoc_parser_utils.get_text = lambda *_args, **_kwargs: ""
monkeypatch.setitem(sys.modules, "deepdoc.parser.utils", deepdoc_parser_utils)
monkeypatch.setitem(sys.modules, "xgboost", ModuleType("xgboost"))
module_name = "test_chat_sdk_routes_unit_module"
module_path = repo_root / "api" / "apps" / "sdk" / "chat.py"
spec = importlib.util.spec_from_file_location(module_name, module_path)
module = importlib.util.module_from_spec(spec)
module.manager = _DummyManager()
monkeypatch.setitem(sys.modules, module_name, module)
spec.loader.exec_module(module)
return module
def _set_request_json(monkeypatch, module, payload):
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue(deepcopy(payload)))
@pytest.mark.p2
def test_create_internal_failure_paths(monkeypatch):
module = _load_chat_module(monkeypatch)
_set_request_json(monkeypatch, module, {"name": "chat-a", "dataset_ids": ["kb-1", "kb-2"]})
monkeypatch.setattr(module.KnowledgebaseService, "accessible", lambda **_kwargs: [SimpleNamespace(id="kb")])
monkeypatch.setattr(module.KnowledgebaseService, "query", lambda **_kwargs: [_DummyKB(chunk_num=1)])
monkeypatch.setattr(module.KnowledgebaseService, "get_by_ids", lambda _ids: [_DummyKB(embd_id="embd-a@x"), _DummyKB(embd_id="embd-b@y")])
monkeypatch.setattr(module.TenantLLMService, "split_model_name_and_factory", lambda model: (model.split("@")[0], "factory"))
res = _run(module.create.__wrapped__("tenant-1"))
assert res["code"] == module.RetCode.AUTHENTICATION_ERROR
assert "different embedding models" in res["message"]
_set_request_json(monkeypatch, module, {"name": "chat-a", "dataset_ids": []})
monkeypatch.setattr(module.TenantService, "get_by_id", lambda _tid: (False, None))
res = _run(module.create.__wrapped__("tenant-1"))
assert res["message"] == "Tenant not found!"
monkeypatch.setattr(module.TenantService, "get_by_id", lambda _tid: (True, SimpleNamespace(llm_id="glm-4")))
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [])
monkeypatch.setattr(module.DialogService, "save", lambda **_kwargs: False)
res = _run(module.create.__wrapped__("tenant-1"))
assert res["message"] == "Fail to new a chat!"
monkeypatch.setattr(module.DialogService, "save", lambda **_kwargs: True)
monkeypatch.setattr(module.DialogService, "get_by_id", lambda _id: (False, None))
res = _run(module.create.__wrapped__("tenant-1"))
assert res["message"] == "Fail to new a chat!"
_set_request_json(
monkeypatch,
module,
{"name": "chat-rerank", "dataset_ids": [], "prompt": {"rerank_model": "unknown-rerank-model"}},
)
monkeypatch.setattr(module.TenantService, "get_by_id", lambda _tid: (True, SimpleNamespace(llm_id="glm-4")))
rerank_query_calls = []
def _mock_tenant_llm_query(**kwargs):
rerank_query_calls.append(kwargs)
return False
monkeypatch.setattr(module.TenantLLMService, "query", _mock_tenant_llm_query)
res = _run(module.create.__wrapped__("tenant-1"))
assert "`rerank_model` unknown-rerank-model doesn't exist" in res["message"]
assert rerank_query_calls[-1]["model_type"] == "rerank"
assert rerank_query_calls[-1]["llm_name"] == "unknown-rerank-model"
_set_request_json(monkeypatch, module, {"name": "chat-tenant", "dataset_ids": [], "tenant_id": "tenant-forbidden"})
res = _run(module.create.__wrapped__("tenant-1"))
assert res["message"] == "`tenant_id` must not be provided."
@pytest.mark.p2
def test_update_internal_failure_paths(monkeypatch):
module = _load_chat_module(monkeypatch)
_set_request_json(monkeypatch, module, {"name": "anything"})
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [])
res = _run(module.update.__wrapped__("tenant-1", "chat-1"))
assert res["message"] == "You do not own the chat"
_set_request_json(monkeypatch, module, {"name": "chat-name"})
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [SimpleNamespace(id="chat-1")])
monkeypatch.setattr(module.TenantService, "get_by_id", lambda _tid: (False, None))
res = _run(module.update.__wrapped__("tenant-1", "chat-1"))
assert res["message"] == "Tenant not found!"
_set_request_json(monkeypatch, module, {"dataset_ids": ["kb-1", "kb-2"]})
monkeypatch.setattr(module.TenantService, "get_by_id", lambda _tid: (True, SimpleNamespace(id="tenant-1")))
monkeypatch.setattr(module.KnowledgebaseService, "accessible", lambda **_kwargs: [SimpleNamespace(id="kb")])
monkeypatch.setattr(module.KnowledgebaseService, "query", lambda **_kwargs: [_DummyKB(chunk_num=1)])
monkeypatch.setattr(module.KnowledgebaseService, "get_by_ids", lambda _ids: [_DummyKB(embd_id="embd-a@x"), _DummyKB(embd_id="embd-b@y")])
monkeypatch.setattr(module.TenantLLMService, "split_model_name_and_factory", lambda model: (model.split("@")[0], "factory"))
res = _run(module.update.__wrapped__("tenant-1", "chat-1"))
assert res["code"] == module.RetCode.AUTHENTICATION_ERROR
assert "different embedding models" in res["message"]
_set_request_json(monkeypatch, module, {"avatar": "new-avatar"})
monkeypatch.setattr(module.DialogService, "get_by_id", lambda _id: (True, _DummyDialogRecord()))
monkeypatch.setattr(module.DialogService, "update_by_id", lambda *_args, **_kwargs: False)
res = _run(module.update.__wrapped__("tenant-1", "chat-1"))
assert res["message"] == "Chat not found!"
monkeypatch.setattr(module.TenantService, "get_by_id", lambda _tid: (True, SimpleNamespace(id="tenant-1")))
monkeypatch.setattr(module.DialogService, "get_by_id", lambda _id: (True, _DummyDialogRecord()))
monkeypatch.setattr(module.DialogService, "update_by_id", lambda *_args, **_kwargs: True)
monkeypatch.setattr(
module.DialogService,
"query",
lambda **kwargs: (
[SimpleNamespace(id="chat-1")]
if kwargs.get("id") == "chat-1"
else ([SimpleNamespace(id="dup")] if kwargs.get("name") == "dup-name" else [])
),
)
monkeypatch.setattr(
module.TenantLLMService,
"split_model_name_and_factory",
lambda model: (model.split("@")[0], "factory"),
)
monkeypatch.setattr(
module.TenantLLMService,
"query",
lambda **kwargs: kwargs.get("llm_name") in {"glm-4", "allowed-rerank"},
)
_set_request_json(monkeypatch, module, {"show_quotation": True})
res = _run(module.update.__wrapped__("tenant-1", "chat-1"))
assert res["code"] == 0
_set_request_json(monkeypatch, module, {"dataset_ids": ["kb-no-owner"]})
monkeypatch.setattr(module.KnowledgebaseService, "accessible", lambda **_kwargs: [])
res = _run(module.update.__wrapped__("tenant-1", "chat-1"))
assert "You don't own the dataset kb-no-owner" in res["message"]
_set_request_json(monkeypatch, module, {"dataset_ids": ["kb-unparsed"]})
monkeypatch.setattr(module.KnowledgebaseService, "accessible", lambda **_kwargs: [SimpleNamespace(id="kb-unparsed")])
monkeypatch.setattr(module.KnowledgebaseService, "query", lambda **_kwargs: [_DummyKB(chunk_num=0)])
res = _run(module.update.__wrapped__("tenant-1", "chat-1"))
assert "doesn't own parsed file" in res["message"]
_set_request_json(monkeypatch, module, {"llm": {"model_name": "unknown-model", "model_type": "unsupported"}})
res = _run(module.update.__wrapped__("tenant-1", "chat-1"))
assert "`model_name` unknown-model doesn't exist" in res["message"]
_set_request_json(
monkeypatch,
module,
{"prompt": {"prompt": "No placeholder", "variables": [{"key": "knowledge", "optional": False}], "rerank_model": "unknown-rerank"}},
)
res = _run(module.update.__wrapped__("tenant-1", "chat-1"))
assert "`rerank_model` unknown-rerank doesn't exist" in res["message"]
_set_request_json(
monkeypatch,
module,
{"prompt": {"prompt": "No placeholder", "variables": [{"key": "knowledge", "optional": False}]}},
)
res = _run(module.update.__wrapped__("tenant-1", "chat-1"))
assert "Parameter 'knowledge' is not used" in res["message"]
_set_request_json(
monkeypatch,
module,
{"prompt": {"prompt": "Optional-only prompt", "variables": [{"key": "maybe", "optional": True}]}},
)
res = _run(module.update.__wrapped__("tenant-1", "chat-1"))
assert res["code"] == 0
_set_request_json(monkeypatch, module, {"name": ""})
res = _run(module.update.__wrapped__("tenant-1", "chat-1"))
assert res["message"] == "`name` cannot be empty."
_set_request_json(monkeypatch, module, {"name": "dup-name"})
res = _run(module.update.__wrapped__("tenant-1", "chat-1"))
assert res["message"] == "Duplicated chat name in updating chat."
_set_request_json(monkeypatch, module, {"llm": {"model_name": "glm-4", "temperature": 0.9}})
res = _run(module.update.__wrapped__("tenant-1", "chat-1"))
assert res["code"] == 0
@pytest.mark.p2
def test_delete_duplicate_no_success_path(monkeypatch):
module = _load_chat_module(monkeypatch)
_set_request_json(monkeypatch, module, {"ids": ["chat-1", "chat-1"]})
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [SimpleNamespace(id="chat-1")])
monkeypatch.setattr(module.DialogService, "update_by_id", lambda *_args, **_kwargs: 0)
res = _run(module.delete_chats.__wrapped__("tenant-1"))
assert res["code"] == module.RetCode.DATA_ERROR
assert "Duplicate assistant ids: chat-1" in res["message"]
_set_request_json(monkeypatch, module, {"ids": ["missing-chat"]})
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [])
res = _run(module.delete_chats.__wrapped__("tenant-1"))
assert res["code"] == module.RetCode.DATA_ERROR
assert "Assistant(missing-chat) not found." in res["message"]
_set_request_json(monkeypatch, module, {"ids": ["chat-1", "chat-1"]})
monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [SimpleNamespace(id="chat-1")])
monkeypatch.setattr(module.DialogService, "update_by_id", lambda *_args, **_kwargs: 1)
res = _run(module.delete_chats.__wrapped__("tenant-1"))
assert res["code"] == 0
assert res["data"]["success_count"] == 1
@pytest.mark.p2
def test_list_missing_kb_warning_and_desc_false(monkeypatch, caplog):
module = _load_chat_module(monkeypatch)
monkeypatch.setattr(module, "request", SimpleNamespace(args={"desc": "False"}))
monkeypatch.setattr(module.DialogService, "get_list", lambda *_args, **_kwargs: [
{
"id": "chat-1",
"name": "chat-name",
"prompt_config": {"system": "Answer with {knowledge}", "parameters": [{"key": "knowledge", "optional": False}], "do_refer": True},
"similarity_threshold": 0.2,
"vector_similarity_weight": 0.3,
"top_n": 6,
"rerank_id": "",
"llm_setting": {"temperature": 0.1},
"llm_id": "glm-4",
"kb_ids": ["missing-kb"],
"icon": "icon.png",
}
])
monkeypatch.setattr(module.KnowledgebaseService, "query", lambda **_kwargs: [])
with caplog.at_level("WARNING"):
res = module.list_chat.__wrapped__("tenant-1")
assert res["code"] == 0
assert res["data"][0]["datasets"] == []
assert res["data"][0]["avatar"] == "icon.png"
assert "does not exist" in caplog.text

View File

@@ -231,6 +231,22 @@ class TestChatAssistantCreate:
else:
assert res["message"] == expected_message
@pytest.mark.p2
def test_create_additional_guards_p2(self, HttpApiAuth):
tenant_payload = {"name": "guard-tenant-id", "dataset_ids": [], "tenant_id": "tenant-should-not-pass"}
res = create_chat_assistant(HttpApiAuth, tenant_payload)
assert res["code"] == 102
assert res["message"] == "`tenant_id` must not be provided."
rerank_payload = {
"name": "guard-rerank-id",
"dataset_ids": [],
"prompt": {"rerank_model": "unknown-rerank-model"},
}
res = create_chat_assistant(HttpApiAuth, rerank_payload)
assert res["code"] == 102
assert "`rerank_model` unknown-rerank-model doesn't exist" in res["message"]
class TestChatAssistantCreate2:
@pytest.mark.p2

View File

@@ -125,3 +125,20 @@ class TestChatAssistantsDelete:
res = list_chat_assistants(HttpApiAuth)
assert len(res["data"]) == 0
@pytest.mark.p2
def test_delete_all_errors_no_success_p2(self, HttpApiAuth, add_chat_assistants_func):
delete_payload = {"ids": ["missing-1", "missing-2"]}
res = delete_chat_assistants(HttpApiAuth, delete_payload)
assert res["code"] == 102
assert "Assistant(missing-1) not found." in res["message"]
assert "Assistant(missing-2) not found." in res["message"]
@pytest.mark.p2
def test_delete_duplicate_partial_success_p2(self, HttpApiAuth, add_chat_assistants_func):
_, _, chat_assistant_ids = add_chat_assistants_func
payload = {"ids": [chat_assistant_ids[0], chat_assistant_ids[0]]}
res = delete_chat_assistants(HttpApiAuth, payload)
assert res["code"] == 0
assert res["data"]["success_count"] == 1
assert "Duplicate assistant ids" in res["data"]["errors"][0]

View File

@@ -312,3 +312,9 @@ class TestChatAssistantsList:
res = list_chat_assistants(HttpApiAuth)
assert res["code"] == 0
assert len(res["data"]) == 5
@pytest.mark.p2
def test_desc_false_parse_branch_p2(self, HttpApiAuth):
res = list_chat_assistants(HttpApiAuth, params={"desc": "False", "orderby": "create_time"})
assert res["code"] == 0
assert is_sorted(res["data"], "create_time", False)

View File

@@ -14,7 +14,7 @@
# limitations under the License.
#
import pytest
from common import list_chat_assistants, update_chat_assistant
from common import create_chat_assistant, list_chat_assistants, update_chat_assistant
from configs import CHAT_ASSISTANT_NAME_LIMIT, INVALID_API_TOKEN
from libs.auth import RAGFlowHttpApiAuth
from utils import encode_avatar
@@ -229,3 +229,64 @@ class TestChatAssistantUpdate:
)
else:
assert expected_message in res["message"]
@pytest.mark.p2
def test_update_mapping_and_validation_branches_p2(self, HttpApiAuth, add_chat_assistants_func, chat_assistant_llm_model_type):
dataset_id, _, chat_assistant_ids = add_chat_assistants_func
chat_id = chat_assistant_ids[0]
res = update_chat_assistant(HttpApiAuth, "invalid-chat-id", {"name": "anything"})
assert res["code"] == 102
assert res["message"] == "You do not own the chat"
res = update_chat_assistant(HttpApiAuth, chat_id, {"show_quotation": False, "dataset_ids": [dataset_id]})
assert res["code"] == 0
res = update_chat_assistant(
HttpApiAuth,
chat_id,
{"llm": {"model_name": "unknown-llm-model", "model_type": chat_assistant_llm_model_type}},
)
assert res["code"] == 102
assert "`model_name` unknown-llm-model doesn't exist" in res["message"]
res = update_chat_assistant(
HttpApiAuth,
chat_id,
{"prompt": {"rerank_model": "unknown-rerank-model"}},
)
assert res["code"] == 102
assert "`rerank_model` unknown-rerank-model doesn't exist" in res["message"]
res = update_chat_assistant(HttpApiAuth, chat_id, {"name": ""})
assert res["code"] == 102
assert res["message"] == "`name` cannot be empty."
res = update_chat_assistant(HttpApiAuth, chat_id, {"name": "test_chat_assistant_1"})
assert res["code"] == 102
assert res["message"] == "Duplicated chat name in updating chat."
res = update_chat_assistant(
HttpApiAuth,
chat_id,
{"prompt": {"prompt": "No required placeholder", "variables": [{"key": "knowledge", "optional": False}]}},
)
assert res["code"] == 102
assert "Parameter 'knowledge' is not used" in res["message"]
res = update_chat_assistant(HttpApiAuth, chat_id, {"avatar": "raw-avatar-value"})
assert res["code"] == 0
listed = list_chat_assistants(HttpApiAuth, {"id": chat_id})
assert listed["code"] == 0
assert listed["data"][0]["avatar"] == "raw-avatar-value"
@pytest.mark.p2
def test_update_unparsed_dataset_guard_p2(self, HttpApiAuth, add_dataset_func, clear_chat_assistants):
dataset_id = add_dataset_func
create_res = create_chat_assistant(HttpApiAuth, {"name": "update-unparsed-target", "dataset_ids": []})
assert create_res["code"] == 0
chat_id = create_res["data"]["id"]
res = update_chat_assistant(HttpApiAuth, chat_id, {"dataset_ids": [dataset_id]})
assert res["code"] == 102
assert "doesn't own parsed file" in res["message"]

View File

@@ -0,0 +1,219 @@
#
# Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import asyncio
import importlib.util
import inspect
import sys
from copy import deepcopy
from pathlib import Path
from types import ModuleType, SimpleNamespace
import pytest
class _DummyManager:
def route(self, *_args, **_kwargs):
def decorator(func):
return func
return decorator
class _AwaitableValue:
def __init__(self, value):
self._value = value
def __await__(self):
async def _co():
return self._value
return _co().__await__()
class _DummyKB:
def __init__(self, tenant_id="tenant-1", embd_id="embd-1"):
self.tenant_id = tenant_id
self.embd_id = embd_id
class _DummyRetriever:
async def retrieval(self, *_args, **_kwargs):
return {
"chunks": [
{"doc_id": "doc-1", "content_with_weight": "chunk-content", "similarity": 0.8, "docnm_kwd": "doc-title", "vector": [0.1]}
]
}
def retrieval_by_children(self, chunks, _tenant_ids):
return chunks
def _run(coro):
return asyncio.run(coro)
def _load_dify_retrieval_module(monkeypatch):
repo_root = Path(__file__).resolve().parents[4]
common_pkg = ModuleType("common")
common_pkg.__path__ = [str(repo_root / "common")]
monkeypatch.setitem(sys.modules, "common", common_pkg)
deepdoc_pkg = ModuleType("deepdoc")
deepdoc_parser_pkg = ModuleType("deepdoc.parser")
deepdoc_parser_pkg.__path__ = []
class _StubPdfParser:
pass
class _StubExcelParser:
pass
class _StubDocxParser:
pass
deepdoc_parser_pkg.PdfParser = _StubPdfParser
deepdoc_parser_pkg.ExcelParser = _StubExcelParser
deepdoc_parser_pkg.DocxParser = _StubDocxParser
deepdoc_pkg.parser = deepdoc_parser_pkg
monkeypatch.setitem(sys.modules, "deepdoc", deepdoc_pkg)
monkeypatch.setitem(sys.modules, "deepdoc.parser", deepdoc_parser_pkg)
deepdoc_excel_module = ModuleType("deepdoc.parser.excel_parser")
deepdoc_excel_module.RAGFlowExcelParser = _StubExcelParser
monkeypatch.setitem(sys.modules, "deepdoc.parser.excel_parser", deepdoc_excel_module)
deepdoc_parser_utils = ModuleType("deepdoc.parser.utils")
deepdoc_parser_utils.get_text = lambda *_args, **_kwargs: ""
monkeypatch.setitem(sys.modules, "deepdoc.parser.utils", deepdoc_parser_utils)
monkeypatch.setitem(sys.modules, "xgboost", ModuleType("xgboost"))
module_name = "test_dify_retrieval_routes_unit_module"
module_path = repo_root / "api" / "apps" / "sdk" / "dify_retrieval.py"
spec = importlib.util.spec_from_file_location(module_name, module_path)
module = importlib.util.module_from_spec(spec)
module.manager = _DummyManager()
monkeypatch.setitem(sys.modules, module_name, module)
spec.loader.exec_module(module)
return module
def _set_request_json(monkeypatch, module, payload):
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue(deepcopy(payload)))
@pytest.mark.p2
def test_retrieval_success_with_metadata_and_kg(monkeypatch):
module = _load_dify_retrieval_module(monkeypatch)
_set_request_json(
monkeypatch,
module,
{
"knowledge_id": "kb-1",
"query": "hello",
"use_kg": True,
"retrieval_setting": {"score_threshold": 0.1, "top_k": 3},
"metadata_condition": {"conditions": [{"name": "author", "comparison_operator": "is", "value": "alice"}], "logic": "and"},
},
)
monkeypatch.setattr(module, "jsonify", lambda payload: payload)
monkeypatch.setattr(module.DocMetadataService, "get_meta_by_kbs", lambda _kb_ids: [{"doc_id": "doc-1"}])
monkeypatch.setattr(module.KnowledgebaseService, "get_by_id", lambda _kb_id: (True, _DummyKB()))
monkeypatch.setattr(module, "LLMBundle", lambda *_args, **_kwargs: object())
monkeypatch.setattr(module, "convert_conditions", lambda cond: cond.get("conditions", []))
monkeypatch.setattr(module, "meta_filter", lambda *_args, **_kwargs: [])
retriever = _DummyRetriever()
monkeypatch.setattr(module.settings, "retriever", retriever)
class _DummyKgRetriever:
async def retrieval(self, *_args, **_kwargs):
return {
"doc_id": "doc-2",
"content_with_weight": "kg-content",
"similarity": 0.9,
"docnm_kwd": "kg-title",
}
monkeypatch.setattr(module.settings, "kg_retriever", _DummyKgRetriever())
monkeypatch.setattr(
module.DocumentService,
"get_by_id",
lambda doc_id: (True, SimpleNamespace(meta_fields={"origin": f"meta-{doc_id}"})),
)
monkeypatch.setattr(module, "label_question", lambda *_args, **_kwargs: [])
res = _run(inspect.unwrap(module.retrieval)("tenant-1"))
assert "records" in res, res
assert len(res["records"]) == 2, res
top = res["records"][0]
assert top["title"] == "kg-title", res
assert top["metadata"]["doc_id"] == "doc-2", res
assert "score" in top, res
@pytest.mark.p2
def test_retrieval_kb_not_found(monkeypatch):
module = _load_dify_retrieval_module(monkeypatch)
_set_request_json(monkeypatch, module, {"knowledge_id": "kb-missing", "query": "hello"})
monkeypatch.setattr(module.DocMetadataService, "get_meta_by_kbs", lambda _kb_ids: [])
monkeypatch.setattr(module.KnowledgebaseService, "get_by_id", lambda _kb_id: (False, None))
res = _run(inspect.unwrap(module.retrieval)("tenant-1"))
assert res["code"] == module.RetCode.NOT_FOUND, res
assert "Knowledgebase not found" in res["message"], res
@pytest.mark.p2
def test_retrieval_not_found_exception_mapping(monkeypatch):
module = _load_dify_retrieval_module(monkeypatch)
_set_request_json(monkeypatch, module, {"knowledge_id": "kb-1", "query": "hello"})
monkeypatch.setattr(module.DocMetadataService, "get_meta_by_kbs", lambda _kb_ids: [])
monkeypatch.setattr(module.KnowledgebaseService, "get_by_id", lambda _kb_id: (True, _DummyKB()))
monkeypatch.setattr(module, "LLMBundle", lambda *_args, **_kwargs: object())
monkeypatch.setattr(module, "label_question", lambda *_args, **_kwargs: [])
class _BrokenRetriever:
async def retrieval(self, *_args, **_kwargs):
raise RuntimeError("chunk_not_found_error")
monkeypatch.setattr(module.settings, "retriever", _BrokenRetriever())
res = _run(inspect.unwrap(module.retrieval)("tenant-1"))
assert res["code"] == module.RetCode.NOT_FOUND, res
assert "No chunk found" in res["message"], res
@pytest.mark.p2
def test_retrieval_generic_exception_mapping(monkeypatch):
module = _load_dify_retrieval_module(monkeypatch)
_set_request_json(monkeypatch, module, {"knowledge_id": "kb-1", "query": "hello"})
monkeypatch.setattr(module.DocMetadataService, "get_meta_by_kbs", lambda _kb_ids: [])
monkeypatch.setattr(module.KnowledgebaseService, "get_by_id", lambda _kb_id: (True, _DummyKB()))
monkeypatch.setattr(module, "LLMBundle", lambda *_args, **_kwargs: object())
monkeypatch.setattr(module, "label_question", lambda *_args, **_kwargs: [])
class _BrokenRetriever:
async def retrieval(self, *_args, **_kwargs):
raise RuntimeError("boom")
monkeypatch.setattr(module.settings, "retriever", _BrokenRetriever())
res = _run(inspect.unwrap(module.retrieval)("tenant-1"))
assert res["code"] == module.RetCode.SERVER_ERROR, res
assert "boom" in res["message"], res

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,992 @@
#
# Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import asyncio
import importlib.util
import sys
from pathlib import Path
from types import ModuleType, SimpleNamespace
import numpy as np
import pytest
class _DummyManager:
def route(self, *_args, **_kwargs):
def decorator(func):
return func
return decorator
class _AwaitableValue:
def __init__(self, value):
self._value = value
def __await__(self):
async def _co():
return self._value
return _co().__await__()
class _DummyFiles(dict):
def getlist(self, key):
return self.get(key, [])
class _DummyArgs(dict):
def getlist(self, key):
v = self.get(key, [])
if v is None:
return []
if isinstance(v, list):
return v
return [v]
class _DummyDoc:
def __init__(
self,
*,
doc_id="doc-1",
kb_id="kb-1",
name="doc.txt",
chunk_num=1,
token_num=2,
progress=0,
process_duration=0,
parser_id="naive",
doc_type=1,
status=True,
run=0,
):
self.id = doc_id
self.kb_id = kb_id
self.name = name
self.chunk_num = chunk_num
self.token_num = token_num
self.progress = progress
self.process_duration = process_duration
self.parser_id = parser_id
self.type = doc_type
self.status = status
self.run = run
def to_dict(self):
return {
"id": self.id,
"kb_id": self.kb_id,
"name": self.name,
"chunk_num": self.chunk_num,
"token_num": self.token_num,
"progress": self.progress,
"process_duration": self.process_duration,
"parser_id": self.parser_id,
"run": self.run,
"status": self.status,
}
class _ToggleBoolDocList:
def __init__(self, value):
self._calls = 0
self._value = value
def __getitem__(self, item):
return self._value
def __bool__(self):
self._calls += 1
return self._calls == 1
def _run(coro):
return asyncio.run(coro)
def _load_doc_module(monkeypatch):
repo_root = Path(__file__).resolve().parents[4]
common_pkg = ModuleType("common")
common_pkg.__path__ = [str(repo_root / "common")]
monkeypatch.setitem(sys.modules, "common", common_pkg)
deepdoc_pkg = ModuleType("deepdoc")
deepdoc_parser_pkg = ModuleType("deepdoc.parser")
deepdoc_parser_pkg.__path__ = []
class _StubPdfParser:
pass
class _StubExcelParser:
pass
class _StubDocxParser:
pass
deepdoc_parser_pkg.PdfParser = _StubPdfParser
deepdoc_parser_pkg.ExcelParser = _StubExcelParser
deepdoc_parser_pkg.DocxParser = _StubDocxParser
deepdoc_pkg.parser = deepdoc_parser_pkg
monkeypatch.setitem(sys.modules, "deepdoc", deepdoc_pkg)
monkeypatch.setitem(sys.modules, "deepdoc.parser", deepdoc_parser_pkg)
deepdoc_excel_module = ModuleType("deepdoc.parser.excel_parser")
deepdoc_excel_module.RAGFlowExcelParser = _StubExcelParser
monkeypatch.setitem(sys.modules, "deepdoc.parser.excel_parser", deepdoc_excel_module)
deepdoc_parser_utils = ModuleType("deepdoc.parser.utils")
deepdoc_parser_utils.get_text = lambda *_args, **_kwargs: ""
monkeypatch.setitem(sys.modules, "deepdoc.parser.utils", deepdoc_parser_utils)
monkeypatch.setitem(sys.modules, "xgboost", ModuleType("xgboost"))
module_path = repo_root / "api" / "apps" / "sdk" / "doc.py"
spec = importlib.util.spec_from_file_location("test_doc_sdk_routes_unit", module_path)
module = importlib.util.module_from_spec(spec)
module.manager = _DummyManager()
spec.loader.exec_module(module)
return module
def _patch_send_file(monkeypatch, module):
async def _fake_send_file(file_obj, **kwargs):
return {"file": file_obj, "filename": kwargs.get("attachment_filename")}
monkeypatch.setattr(module, "send_file", _fake_send_file)
def _patch_storage(monkeypatch, module, *, file_stream=b"abc"):
storage = SimpleNamespace(get=lambda *_args, **_kwargs: file_stream, rm=lambda *_args, **_kwargs: None)
monkeypatch.setattr(module.settings, "STORAGE_IMPL", storage)
def _patch_docstore(monkeypatch, module, **kwargs):
defaults = {
"delete": lambda *_args, **_kwargs: 0,
"update": lambda *_args, **_kwargs: None,
"get": lambda *_args, **_kwargs: {},
"insert": lambda *_args, **_kwargs: None,
"index_exist": lambda *_args, **_kwargs: False,
}
defaults.update(kwargs)
monkeypatch.setattr(module.settings, "docStoreConn", SimpleNamespace(**defaults))
@pytest.mark.p2
class TestDocRoutesUnit:
def test_chunk_positions_validation_error(self, monkeypatch):
module = _load_doc_module(monkeypatch)
with pytest.raises(ValueError) as exc_info:
module.Chunk(positions=[[1, 2, 3, 4]])
assert "length of 5" in str(exc_info.value)
def test_upload_validation_and_upload_error(self, monkeypatch):
module = _load_doc_module(monkeypatch)
class _FileObj:
def __init__(self, name):
self.filename = name
monkeypatch.setattr(module, "request", SimpleNamespace(form=_AwaitableValue({}), files=_AwaitableValue(_DummyFiles({"file": [_FileObj("")]}))))
res = _run(module.upload.__wrapped__("ds-1", "tenant-1"))
assert res["code"] == module.RetCode.ARGUMENT_ERROR
assert res["message"] == "No file selected!"
long_name = "a" * (module.FILE_NAME_LEN_LIMIT + 1)
monkeypatch.setattr(module, "request", SimpleNamespace(form=_AwaitableValue({}), files=_AwaitableValue(_DummyFiles({"file": [_FileObj(long_name)]}))))
res = _run(module.upload.__wrapped__("ds-1", "tenant-1"))
assert res["code"] == module.RetCode.ARGUMENT_ERROR
assert "bytes or less" in res["message"]
monkeypatch.setattr(module, "request", SimpleNamespace(form=_AwaitableValue({}), files=_AwaitableValue(_DummyFiles({"file": [_FileObj("ok.txt")]}))))
monkeypatch.setattr(module.KnowledgebaseService, "get_by_id", lambda _id: (True, SimpleNamespace()))
monkeypatch.setattr(module.FileService, "upload_document", lambda *_args, **_kwargs: (["upload failed"], []))
res = _run(module.upload.__wrapped__("ds-1", "tenant-1"))
assert res["code"] == module.RetCode.SERVER_ERROR
assert res["message"] == "upload failed"
def test_update_doc_guards_and_error_paths(self, monkeypatch):
module = _load_doc_module(monkeypatch)
doc = _DummyDoc()
monkeypatch.setattr(module.KnowledgebaseService, "query", lambda **_kwargs: [])
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({}))
res = _run(module.update_doc.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert res["message"] == "You don't own the dataset."
monkeypatch.setattr(module.KnowledgebaseService, "query", lambda **_kwargs: [1])
monkeypatch.setattr(module.KnowledgebaseService, "get_by_id", lambda _id: (False, None))
res = _run(module.update_doc.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert res["message"] == "Can't find this dataset!"
monkeypatch.setattr(module.KnowledgebaseService, "get_by_id", lambda _id: (True, SimpleNamespace(tenant_id="tenant-1")))
monkeypatch.setattr(module.DocumentService, "query", lambda **_kwargs: [])
res = _run(module.update_doc.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert "doesn't own the document" in res["message"]
monkeypatch.setattr(module.DocumentService, "query", lambda **_kwargs: [doc])
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"chunk_count": 100}))
res = _run(module.update_doc.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert "chunk_count" in res["message"]
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"token_count": 100}))
res = _run(module.update_doc.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert "token_count" in res["message"]
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"progress": 100}))
res = _run(module.update_doc.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert "progress" in res["message"]
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"meta_fields": []}))
res = _run(module.update_doc.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert res["message"] == "meta_fields must be a dictionary"
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"meta_fields": {"k": "v"}}))
monkeypatch.setattr(module.DocMetadataService, "update_document_metadata", lambda *_args, **_kwargs: False)
res = _run(module.update_doc.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert res["message"] == "Failed to update metadata"
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"name": "a" * (module.FILE_NAME_LEN_LIMIT + 1)}))
res = _run(module.update_doc.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert res["code"] == module.RetCode.ARGUMENT_ERROR
assert "bytes or less" in res["message"]
monkeypatch.setattr(module.DocumentService, "update_by_id", lambda *_args, **_kwargs: False)
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"name": "new.txt"}))
res = _run(module.update_doc.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert "Document rename" in res["message"]
def test_update_doc_chunk_method_enabled_and_db_error(self, monkeypatch):
module = _load_doc_module(monkeypatch)
visual_doc = _DummyDoc(parser_id="naive", doc_type=module.FileType.VISUAL)
kb = SimpleNamespace(tenant_id="tenant-1")
monkeypatch.setattr(module.KnowledgebaseService, "query", lambda **_kwargs: [1])
monkeypatch.setattr(module.KnowledgebaseService, "get_by_id", lambda _id: (True, kb))
monkeypatch.setattr(module.DocumentService, "query", lambda **_kwargs: [visual_doc])
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"chunk_method": "naive"}))
res = _run(module.update_doc.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert res["message"] == "Not supported yet!"
doc = _DummyDoc(token_num=2, chunk_num=1, parser_id="naive")
monkeypatch.setattr(module.DocumentService, "query", lambda **_kwargs: [doc])
monkeypatch.setattr(module.DocumentService, "update_by_id", lambda *_args, **_kwargs: False)
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"chunk_method": "manual"}))
res = _run(module.update_doc.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert res["message"] == "Document not found!"
monkeypatch.setattr(module.DocumentService, "update_by_id", lambda *_args, **_kwargs: True)
monkeypatch.setattr(module.DocumentService, "update_parser_config", lambda *_args, **_kwargs: None)
monkeypatch.setattr(module.DocumentService, "increment_chunk_num", lambda *_args, **_kwargs: False)
_patch_docstore(monkeypatch, module, delete=lambda *_args, **_kwargs: None)
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"chunk_method": "manual"}))
res = _run(module.update_doc.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert res["message"] == "Document not found!"
monkeypatch.setattr(module.DocumentService, "increment_chunk_num", lambda *_args, **_kwargs: True)
doc_for_enabled = _DummyDoc(status=False)
monkeypatch.setattr(module.DocumentService, "query", lambda **_kwargs: [doc_for_enabled])
monkeypatch.setattr(module.DocumentService, "get_by_id", lambda _id: (True, doc_for_enabled))
monkeypatch.setattr(module.DocumentService, "update_by_id", lambda *_args, **_kwargs: False)
_patch_docstore(monkeypatch, module, update=lambda *_args, **_kwargs: None, delete=lambda *_args, **_kwargs: None)
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"enabled": True}))
res = _run(module.update_doc.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert "Document update" in res["message"]
monkeypatch.setattr(module.DocumentService, "update_by_id", lambda *_args, **_kwargs: True)
_patch_docstore(monkeypatch, module, update=lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("boom")), delete=lambda *_args, **_kwargs: None)
monkeypatch.setattr(module, "server_error_response", lambda e: {"code": 500, "message": str(e)})
res = _run(module.update_doc.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert res["code"] == 500
assert "boom" in res["message"]
monkeypatch.setattr(module.DocumentService, "get_by_id", lambda _id: (False, None))
_patch_docstore(monkeypatch, module, update=lambda *_args, **_kwargs: None, delete=lambda *_args, **_kwargs: None)
res = _run(module.update_doc.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert res["message"] == "Dataset created failed"
# cover token reset + docStore deletion branch
doc_reset = _DummyDoc(token_num=3, chunk_num=2, parser_id="naive", run=0)
monkeypatch.setattr(module.DocumentService, "query", lambda **_kwargs: [doc_reset])
monkeypatch.setattr(module.DocumentService, "update_by_id", lambda *_args, **_kwargs: True)
monkeypatch.setattr(module.DocumentService, "increment_chunk_num", lambda *_args, **_kwargs: True)
monkeypatch.setattr(module.DocumentService, "get_by_id", lambda _id: (True, doc_reset))
_patch_docstore(monkeypatch, module, delete=lambda *_args, **_kwargs: None, update=lambda *_args, **_kwargs: None)
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"chunk_method": "manual"}))
res = _run(module.update_doc.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert res["code"] == 0
def _raise_operational_error(_id):
raise module.OperationalError("db down")
monkeypatch.setattr(module.DocumentService, "get_by_id", _raise_operational_error)
res = _run(module.update_doc.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert res["message"] == "Database operation failed"
def test_download_and_download_doc_errors(self, monkeypatch):
module = _load_doc_module(monkeypatch)
_patch_send_file(monkeypatch, module)
_patch_storage(monkeypatch, module, file_stream=b"")
res = _run(module.download.__wrapped__("tenant-1", "ds-1", ""))
assert res["message"] == "Specify document_id please."
monkeypatch.setattr(module.KnowledgebaseService, "query", lambda **_kwargs: [])
res = _run(module.download.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert "do not own the dataset" in res["message"]
monkeypatch.setattr(module.KnowledgebaseService, "query", lambda **_kwargs: [1])
monkeypatch.setattr(module.DocumentService, "query", lambda **_kwargs: [])
res = _run(module.download.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert "not own the document" in res["message"]
monkeypatch.setattr(module.DocumentService, "query", lambda **_kwargs: [_DummyDoc()])
monkeypatch.setattr(module.File2DocumentService, "get_storage_address", lambda **_kwargs: ("b", "n"))
res = _run(module.download.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert res["message"] == "This file is empty."
monkeypatch.setattr(module, "request", SimpleNamespace(headers={"Authorization": "Bearer"}))
res = _run(module.download_doc("doc-1"))
assert "Authorization is not valid" in res["message"]
monkeypatch.setattr(module, "request", SimpleNamespace(headers={"Authorization": "Bearer token"}))
monkeypatch.setattr(module.APIToken, "query", lambda **_kwargs: [])
res = _run(module.download_doc("doc-1"))
assert "API key is invalid" in res["message"]
monkeypatch.setattr(module.APIToken, "query", lambda **_kwargs: [SimpleNamespace()])
res = _run(module.download_doc(""))
assert res["message"] == "Specify document_id please."
monkeypatch.setattr(module.DocumentService, "query", lambda **_kwargs: [])
res = _run(module.download_doc("doc-1"))
assert "not own the document" in res["message"]
monkeypatch.setattr(module.DocumentService, "query", lambda **_kwargs: [_DummyDoc()])
monkeypatch.setattr(module.File2DocumentService, "get_storage_address", lambda **_kwargs: ("b", "n"))
_patch_storage(monkeypatch, module, file_stream=b"")
res = _run(module.download_doc("doc-1"))
assert res["message"] == "This file is empty."
_patch_storage(monkeypatch, module, file_stream=b"abc")
res = _run(module.download_doc("doc-1"))
assert res["filename"] == "doc.txt"
def test_list_docs_metadata_filters(self, monkeypatch):
module = _load_doc_module(monkeypatch)
monkeypatch.setattr(module.KnowledgebaseService, "accessible", lambda **_kwargs: False)
monkeypatch.setattr(module, "request", SimpleNamespace(args=_DummyArgs()))
res = module.list_docs.__wrapped__("ds-1", "tenant-1")
assert "don't own the dataset" in res["message"]
monkeypatch.setattr(module.KnowledgebaseService, "accessible", lambda **_kwargs: True)
monkeypatch.setattr(
module,
"request",
SimpleNamespace(
args=_DummyArgs(
{
"metadata_condition": "{bad json",
}
)
),
)
res = module.list_docs.__wrapped__("ds-1", "tenant-1")
assert res["message"] == "metadata_condition must be valid JSON."
monkeypatch.setattr(module, "request", SimpleNamespace(args=_DummyArgs({"metadata_condition": "[1]"})))
res = module.list_docs.__wrapped__("ds-1", "tenant-1")
assert res["message"] == "metadata_condition must be an object."
monkeypatch.setattr(module.DocMetadataService, "get_flatted_meta_by_kbs", lambda _kbs: [{"doc_id": "x"}])
monkeypatch.setattr(module, "meta_filter", lambda *_args, **_kwargs: [])
monkeypatch.setattr(module, "convert_conditions", lambda cond: cond)
monkeypatch.setattr(
module,
"request",
SimpleNamespace(args=_DummyArgs({"metadata_condition": '{"conditions":[{"field":"x","op":"eq","value":"y"}]}'})),
)
res = module.list_docs.__wrapped__("ds-1", "tenant-1")
assert res["code"] == module.RetCode.SUCCESS
assert res["data"]["total"] == 0
monkeypatch.setattr(
module.DocumentService,
"get_list",
lambda *_args, **_kwargs: ([{"id": "doc-1", "create_time": 100, "run": "0"}], 1),
)
monkeypatch.setattr(
module,
"request",
SimpleNamespace(
args=_DummyArgs(
{
"create_time_from": "101",
"create_time_to": "200",
}
)
),
)
res = module.list_docs.__wrapped__("ds-1", "tenant-1")
assert res["code"] == 0
assert res["data"]["docs"] == []
def test_metadata_summary_and_batch_update(self, monkeypatch):
module = _load_doc_module(monkeypatch)
monkeypatch.setattr(module, "convert_conditions", lambda cond: cond)
monkeypatch.setattr(module.KnowledgebaseService, "accessible", lambda **_kwargs: False)
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"selector": {}}))
res = _run(module.metadata_batch_update.__wrapped__("ds-1", "tenant-1"))
assert "don't own the dataset" in res["message"]
monkeypatch.setattr(module.KnowledgebaseService, "accessible", lambda **_kwargs: False)
res = _run(module.metadata_summary.__wrapped__("ds-1", "tenant-1"))
assert "don't own the dataset" in res["message"]
monkeypatch.setattr(module.KnowledgebaseService, "accessible", lambda **_kwargs: True)
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"doc_ids": ["d1"]}))
monkeypatch.setattr(module.DocMetadataService, "get_metadata_summary", lambda *_args, **_kwargs: {"k": 1})
res = _run(module.metadata_summary.__wrapped__("ds-1", "tenant-1"))
assert res["code"] == 0
assert res["data"]["summary"] == {"k": 1}
monkeypatch.setattr(module.DocMetadataService, "get_metadata_summary", lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("x")))
monkeypatch.setattr(module, "server_error_response", lambda e: {"code": 500, "message": str(e)})
res = _run(module.metadata_summary.__wrapped__("ds-1", "tenant-1"))
assert res["code"] == 500
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"selector": [1]}))
res = _run(module.metadata_batch_update.__wrapped__("ds-1", "tenant-1"))
assert res["message"] == "selector must be an object."
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"selector": {}, "updates": {"k": "v"}, "deletes": []}))
res = _run(module.metadata_batch_update.__wrapped__("ds-1", "tenant-1"))
assert res["message"] == "updates and deletes must be lists."
monkeypatch.setattr(
module,
"get_request_json",
lambda: _AwaitableValue({"selector": {"metadata_condition": [1]}, "updates": [], "deletes": []}),
)
res = _run(module.metadata_batch_update.__wrapped__("ds-1", "tenant-1"))
assert res["message"] == "metadata_condition must be an object."
monkeypatch.setattr(
module,
"get_request_json",
lambda: _AwaitableValue({"selector": {"document_ids": "doc-1"}, "updates": [], "deletes": []}),
)
res = _run(module.metadata_batch_update.__wrapped__("ds-1", "tenant-1"))
assert res["message"] == "document_ids must be a list."
monkeypatch.setattr(
module,
"get_request_json",
lambda: _AwaitableValue({"selector": {}, "updates": [{"key": ""}], "deletes": []}),
)
res = _run(module.metadata_batch_update.__wrapped__("ds-1", "tenant-1"))
assert "Each update requires key and value." in res["message"]
monkeypatch.setattr(
module,
"get_request_json",
lambda: _AwaitableValue({"selector": {}, "updates": [], "deletes": [{"x": "y"}]}),
)
res = _run(module.metadata_batch_update.__wrapped__("ds-1", "tenant-1"))
assert "Each delete requires key." in res["message"]
monkeypatch.setattr(
module,
"get_request_json",
lambda: _AwaitableValue(
{
"selector": {"document_ids": ["bad"], "metadata_condition": {"conditions": []}},
"updates": [{"key": "k", "value": "v"}],
"deletes": [],
}
),
)
monkeypatch.setattr(module.KnowledgebaseService, "list_documents_by_ids", lambda _ids: ["doc-1"])
res = _run(module.metadata_batch_update.__wrapped__("ds-1", "tenant-1"))
assert "do not belong to dataset" in res["message"]
monkeypatch.setattr(
module,
"get_request_json",
lambda: _AwaitableValue(
{
"selector": {"document_ids": ["doc-1"], "metadata_condition": {"conditions": [{"f": "x"}]}},
"updates": [{"key": "k", "value": "v"}],
"deletes": [],
}
),
)
monkeypatch.setattr(module, "meta_filter", lambda *_args, **_kwargs: [])
monkeypatch.setattr(module.DocMetadataService, "get_flatted_meta_by_kbs", lambda _kbs: [])
res = _run(module.metadata_batch_update.__wrapped__("ds-1", "tenant-1"))
assert res["code"] == 0
assert res["data"]["updated"] == 0
assert res["data"]["matched_docs"] == 0
monkeypatch.setattr(module, "meta_filter", lambda *_args, **_kwargs: ["doc-1"])
monkeypatch.setattr(module.DocMetadataService, "batch_update_metadata", lambda *_args, **_kwargs: 1)
res = _run(module.metadata_batch_update.__wrapped__("ds-1", "tenant-1"))
assert res["code"] == 0
assert res["data"]["updated"] == 1
assert res["data"]["matched_docs"] == 1
def test_delete_branches(self, monkeypatch):
module = _load_doc_module(monkeypatch)
monkeypatch.setattr(module.KnowledgebaseService, "accessible", lambda **_kwargs: False)
res = _run(module.delete.__wrapped__("tenant-1", "ds-1"))
assert "don't own the dataset" in res["message"]
monkeypatch.setattr(module.KnowledgebaseService, "accessible", lambda **_kwargs: True)
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"ids": ["doc-1"]}))
monkeypatch.setattr(module, "check_duplicate_ids", lambda ids, _kind: (ids, []))
monkeypatch.setattr(module.FileService, "get_root_folder", lambda _tenant: {"id": "pf-1"})
monkeypatch.setattr(module.FileService, "init_knowledgebase_docs", lambda *_args, **_kwargs: None)
monkeypatch.setattr(module.DocumentService, "get_by_id", lambda _id: (True, _DummyDoc()))
monkeypatch.setattr(module.DocumentService, "get_tenant_id", lambda _id: None)
res = _run(module.delete.__wrapped__("tenant-1", "ds-1"))
assert res["message"] == "Tenant not found!"
monkeypatch.setattr(module.DocumentService, "get_tenant_id", lambda _id: "tenant-1")
monkeypatch.setattr(module.File2DocumentService, "get_storage_address", lambda **_kwargs: ("b", "n"))
monkeypatch.setattr(module.DocumentService, "remove_document", lambda *_args, **_kwargs: False)
res = _run(module.delete.__wrapped__("tenant-1", "ds-1"))
assert "Document removal" in res["message"]
def _raise_get_by_id(_id):
raise RuntimeError("boom")
monkeypatch.setattr(module.DocumentService, "get_by_id", _raise_get_by_id)
res = _run(module.delete.__wrapped__("tenant-1", "ds-1"))
assert res["code"] == module.RetCode.SERVER_ERROR
assert "boom" in res["message"]
monkeypatch.setattr(module, "check_duplicate_ids", lambda _ids, _kind: ([], ["Duplicate document ids: doc-1"]))
monkeypatch.setattr(module.DocumentService, "get_by_id", lambda _id: (False, None))
res = _run(module.delete.__wrapped__("tenant-1", "ds-1"))
assert res["code"] == module.RetCode.DATA_ERROR
assert "Duplicate document ids" in res["message"]
def test_parse_branches(self, monkeypatch):
module = _load_doc_module(monkeypatch)
monkeypatch.setattr(module.KnowledgebaseService, "accessible", lambda **_kwargs: False)
res = _run(module.parse.__wrapped__("tenant-1", "ds-1"))
assert "don't own the dataset" in res["message"]
monkeypatch.setattr(module.KnowledgebaseService, "accessible", lambda **_kwargs: True)
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"document_ids": ["doc-1"]}))
monkeypatch.setattr(module, "check_duplicate_ids", lambda ids, _kind: (ids, []))
toggle_doc = _ToggleBoolDocList(_DummyDoc(progress=0))
monkeypatch.setattr(module.DocumentService, "query", lambda **_kwargs: toggle_doc)
res = _run(module.parse.__wrapped__("tenant-1", "ds-1"))
assert "don't own the document" in res["message"]
monkeypatch.setattr(module.DocumentService, "query", lambda **_kwargs: [_DummyDoc(progress=0.5)])
res = _run(module.parse.__wrapped__("tenant-1", "ds-1"))
assert "currently being processed" in res["message"]
monkeypatch.setattr(module.DocumentService, "query", lambda **_kwargs: [_DummyDoc(progress=0)])
monkeypatch.setattr(module.DocumentService, "update_by_id", lambda *_args, **_kwargs: True)
monkeypatch.setattr(module.DocumentService, "get_by_id", lambda _id: (True, _DummyDoc()))
monkeypatch.setattr(module.File2DocumentService, "get_storage_address", lambda **_kwargs: ("b", "n"))
_patch_docstore(monkeypatch, module, delete=lambda *_args, **_kwargs: None)
monkeypatch.setattr(module.TaskService, "filter_delete", lambda *_args, **_kwargs: None)
monkeypatch.setattr(module, "queue_tasks", lambda *_args, **_kwargs: None)
monkeypatch.setattr(module, "check_duplicate_ids", lambda ids, _kind: (ids, ["Duplicate document ids: doc-1"]))
res = _run(module.parse.__wrapped__("tenant-1", "ds-1"))
assert res["code"] == 0
assert res["data"]["success_count"] == 1
assert "Duplicate document ids" in res["data"]["errors"][0]
monkeypatch.setattr(module, "check_duplicate_ids", lambda _ids, _kind: ([], ["Duplicate document ids: doc-1"]))
res = _run(module.parse.__wrapped__("tenant-1", "ds-1"))
assert res["code"] == module.RetCode.DATA_ERROR
assert "Duplicate document ids" in res["message"]
def test_stop_parsing_branches(self, monkeypatch):
module = _load_doc_module(monkeypatch)
monkeypatch.setattr(module.KnowledgebaseService, "accessible", lambda **_kwargs: False)
res = _run(module.stop_parsing.__wrapped__("tenant-1", "ds-1"))
assert "don't own the dataset" in res["message"]
monkeypatch.setattr(module.KnowledgebaseService, "accessible", lambda **_kwargs: True)
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({}))
res = _run(module.stop_parsing.__wrapped__("tenant-1", "ds-1"))
assert "`document_ids` is required" in res["message"]
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"document_ids": ["doc-1"]}))
monkeypatch.setattr(module, "check_duplicate_ids", lambda ids, _kind: (ids, []))
monkeypatch.setattr(module.DocumentService, "query", lambda **_kwargs: [])
res = _run(module.stop_parsing.__wrapped__("tenant-1", "ds-1"))
assert "don't own the document" in res["message"]
monkeypatch.setattr(module.DocumentService, "query", lambda **_kwargs: [_DummyDoc(progress=1)])
res = _run(module.stop_parsing.__wrapped__("tenant-1", "ds-1"))
assert "progress at 0 or 1" in res["message"]
monkeypatch.setattr(module.DocumentService, "query", lambda **_kwargs: [_DummyDoc(progress=0.3)])
monkeypatch.setattr(module, "cancel_all_task_of", lambda *_args, **_kwargs: None)
monkeypatch.setattr(module.DocumentService, "update_by_id", lambda *_args, **_kwargs: True)
_patch_docstore(monkeypatch, module, delete=lambda *_args, **_kwargs: None)
monkeypatch.setattr(module, "check_duplicate_ids", lambda ids, _kind: (ids, ["Duplicate document ids: doc-1"]))
res = _run(module.stop_parsing.__wrapped__("tenant-1", "ds-1"))
assert res["code"] == 0
assert res["data"]["success_count"] == 1
assert "Duplicate document ids" in res["data"]["errors"][0]
monkeypatch.setattr(module, "check_duplicate_ids", lambda _ids, _kind: ([], ["Duplicate document ids: doc-1"]))
res = _run(module.stop_parsing.__wrapped__("tenant-1", "ds-1"))
assert res["code"] == module.RetCode.DATA_ERROR
assert "Duplicate document ids" in res["message"]
monkeypatch.setattr(module, "check_duplicate_ids", lambda ids, _kind: (ids, []))
monkeypatch.setattr(module.DocumentService, "query", lambda **_kwargs: [_DummyDoc(progress=0.3)])
res = _run(module.stop_parsing.__wrapped__("tenant-1", "ds-1"))
assert res["code"] == 0
def test_list_chunks_branches(self, monkeypatch):
module = _load_doc_module(monkeypatch)
monkeypatch.setattr(module.KnowledgebaseService, "accessible", lambda **_kwargs: False)
res = _run(module.list_chunks.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert "don't own the dataset" in res["message"]
monkeypatch.setattr(module.KnowledgebaseService, "accessible", lambda **_kwargs: True)
monkeypatch.setattr(module.DocumentService, "query", lambda **_kwargs: [])
res = _run(module.list_chunks.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert "don't own the document" in res["message"]
monkeypatch.setattr(module.DocumentService, "query", lambda **_kwargs: [_DummyDoc()])
monkeypatch.setattr(module, "request", SimpleNamespace(args=_DummyArgs({"id": "chunk-1"})))
_patch_docstore(monkeypatch, module, get=lambda *_args, **_kwargs: None)
res = _run(module.list_chunks.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert "Chunk not found" in res["message"]
_patch_docstore(monkeypatch, module, get=lambda *_args, **_kwargs: {"id_vec": [1], "content_with_weight_vec": [2]})
res = _run(module.list_chunks.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert "Chunk `chunk-1` not found." in res["message"]
_patch_docstore(
monkeypatch,
module,
get=lambda *_args, **_kwargs: {
"chunk_id": "chunk-1",
"content_with_weight": "x",
"doc_id": "doc-1",
"docnm_kwd": "doc",
"position_int": [[1, 2, 3, 4, 5]],
},
)
res = _run(module.list_chunks.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert res["code"] == 0
assert res["data"]["total"] == 1
assert res["data"]["chunks"][0]["id"] == "chunk-1"
def test_add_chunk_access_guard(self, monkeypatch):
module = _load_doc_module(monkeypatch)
monkeypatch.setattr(module.KnowledgebaseService, "accessible", lambda **_kwargs: False)
res = _run(module.add_chunk.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert "don't own the dataset" in res["message"]
def test_rm_chunk_branches(self, monkeypatch):
module = _load_doc_module(monkeypatch)
monkeypatch.setattr(module.KnowledgebaseService, "accessible", lambda **_kwargs: False)
res = _run(module.rm_chunk.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert "don't own the dataset" in res["message"]
monkeypatch.setattr(module.KnowledgebaseService, "accessible", lambda **_kwargs: True)
monkeypatch.setattr(module.DocumentService, "get_by_ids", lambda _ids: [])
with pytest.raises(LookupError):
_run(module.rm_chunk.__wrapped__("tenant-1", "ds-1", "doc-1"))
monkeypatch.setattr(module.DocumentService, "get_by_ids", lambda _ids: [_DummyDoc()])
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({}))
_patch_docstore(monkeypatch, module, delete=lambda *_args, **_kwargs: 2)
monkeypatch.setattr(module.DocumentService, "decrement_chunk_num", lambda *_args, **_kwargs: None)
res = _run(module.rm_chunk.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert res["code"] == 0
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"chunk_ids": ["c1", "c1"]}))
monkeypatch.setattr(module, "check_duplicate_ids", lambda _ids, _kind: (["c1"], ["Duplicate chunk ids: c1"]))
_patch_docstore(monkeypatch, module, delete=lambda *_args, **_kwargs: 1)
res = _run(module.rm_chunk.__wrapped__("tenant-1", "ds-1", "doc-1"))
assert res["code"] == 0
assert res["data"]["errors"] == ["Duplicate chunk ids: c1"]
def test_update_chunk_branches(self, monkeypatch):
module = _load_doc_module(monkeypatch)
_patch_docstore(monkeypatch, module, get=lambda *_args, **_kwargs: None)
res = _run(module.update_chunk.__wrapped__("tenant-1", "ds-1", "doc-1", "chunk-1"))
assert "Can't find this chunk" in res["message"]
_patch_docstore(monkeypatch, module, get=lambda *_args, **_kwargs: {"content_with_weight": "q\na"})
monkeypatch.setattr(module.KnowledgebaseService, "accessible", lambda **_kwargs: False)
res = _run(module.update_chunk.__wrapped__("tenant-1", "ds-1", "doc-1", "chunk-1"))
assert "don't own the dataset" in res["message"]
monkeypatch.setattr(module.KnowledgebaseService, "accessible", lambda **_kwargs: True)
monkeypatch.setattr(module.DocumentService, "query", lambda **_kwargs: [])
res = _run(module.update_chunk.__wrapped__("tenant-1", "ds-1", "doc-1", "chunk-1"))
assert "don't own the document" in res["message"]
doc = _DummyDoc(parser_id="naive")
monkeypatch.setattr(module.DocumentService, "query", lambda **_kwargs: [doc])
monkeypatch.setattr(module.rag_tokenizer, "tokenize", lambda text: text or "")
monkeypatch.setattr(module.rag_tokenizer, "fine_grained_tokenize", lambda text: text or "")
monkeypatch.setattr(module.rag_tokenizer, "is_chinese", lambda _text: False)
monkeypatch.setattr(module.DocumentService, "get_embd_id", lambda _doc_id: "embd")
class _EmbedModel:
def encode(self, _texts):
return [np.array([0.2, 0.8]), np.array([0.3, 0.7])], 1
monkeypatch.setattr(module.TenantLLMService, "model_instance", lambda *_args, **_kwargs: _EmbedModel())
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"positions": "bad"}))
res = _run(module.update_chunk.__wrapped__("tenant-1", "ds-1", "doc-1", "chunk-1"))
assert "`positions` should be a list" in res["message"]
_patch_docstore(monkeypatch, module, get=lambda *_args, **_kwargs: {"content_with_weight": "x"}, update=lambda *_args, **_kwargs: None)
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"positions": [[1, 2, 3, 4, 5]]}))
res = _run(module.update_chunk.__wrapped__("tenant-1", "ds-1", "doc-1", "chunk-1"))
assert res["code"] == 0
qa_doc = _DummyDoc(parser_id=module.ParserType.QA)
monkeypatch.setattr(module.DocumentService, "query", lambda **_kwargs: [qa_doc])
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"content": "no-separator"}))
res = _run(module.update_chunk.__wrapped__("tenant-1", "ds-1", "doc-1", "chunk-1"))
assert "Q&A must be separated" in res["message"]
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"content": "Q?\nA!"}))
_patch_docstore(monkeypatch, module, get=lambda *_args, **_kwargs: {"content_with_weight": "Q?\nA!"}, update=lambda *_args, **_kwargs: None)
monkeypatch.setattr(module, "beAdoc", lambda d, *_args, **_kwargs: d)
res = _run(module.update_chunk.__wrapped__("tenant-1", "ds-1", "doc-1", "chunk-1"))
assert res["code"] == 0
def test_retrieval_validation_matrix(self, monkeypatch):
module = _load_doc_module(monkeypatch)
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"dataset_ids": "bad"}))
res = _run(module.retrieval_test.__wrapped__("tenant-1"))
assert "`dataset_ids` should be a list" in res["message"]
monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"dataset_ids": ["ds-1"]}))
monkeypatch.setattr(module.KnowledgebaseService, "accessible", lambda **_kwargs: False)
res = _run(module.retrieval_test.__wrapped__("tenant-1"))
assert "don't own the dataset" in res["message"]
monkeypatch.setattr(module.KnowledgebaseService, "accessible", lambda **_kwargs: True)
monkeypatch.setattr(module.KnowledgebaseService, "get_by_ids", lambda _ids: [SimpleNamespace(embd_id="m1"), SimpleNamespace(embd_id="m2")])
monkeypatch.setattr(module.TenantLLMService, "split_model_name_and_factory", lambda embd_id: (embd_id, "f"))
res = _run(module.retrieval_test.__wrapped__("tenant-1"))
assert "different embedding models" in res["message"]
monkeypatch.setattr(module.KnowledgebaseService, "get_by_ids", lambda _ids: [SimpleNamespace(embd_id="m1", tenant_id="tenant-1")])
res = _run(module.retrieval_test.__wrapped__("tenant-1"))
assert "`question` is required." in res["message"]
monkeypatch.setattr(
module,
"get_request_json",
lambda: _AwaitableValue({"dataset_ids": ["ds-1"], "question": " "}),
)
res = _run(module.retrieval_test.__wrapped__("tenant-1"))
assert res["code"] == 0
assert res["data"]["chunks"] == []
monkeypatch.setattr(
module,
"get_request_json",
lambda: _AwaitableValue({"dataset_ids": ["ds-1"], "question": "q", "document_ids": "bad"}),
)
res = _run(module.retrieval_test.__wrapped__("tenant-1"))
assert "`documents` should be a list" in res["message"]
monkeypatch.setattr(
module,
"get_request_json",
lambda: _AwaitableValue({"dataset_ids": ["ds-1"], "question": "q", "document_ids": ["not-owned"]}),
)
monkeypatch.setattr(module.KnowledgebaseService, "list_documents_by_ids", lambda _ids: ["doc-1"])
res = _run(module.retrieval_test.__wrapped__("tenant-1"))
assert "don't own the document" in res["message"]
monkeypatch.setattr(
module,
"get_request_json",
lambda: _AwaitableValue({"dataset_ids": ["ds-1"], "question": "q", "metadata_condition": {"logic": "and"}}),
)
monkeypatch.setattr(module.DocMetadataService, "get_meta_by_kbs", lambda _ids: [])
monkeypatch.setattr(module, "meta_filter", lambda *_args, **_kwargs: [])
res = _run(module.retrieval_test.__wrapped__("tenant-1"))
assert "code" in res
monkeypatch.setattr(
module,
"get_request_json",
lambda: _AwaitableValue({"dataset_ids": ["ds-1"], "question": "q", "highlight": "True"}),
)
monkeypatch.setattr(module.KnowledgebaseService, "get_by_ids", lambda _ids: [SimpleNamespace(embd_id="m1", tenant_id="tenant-1")])
monkeypatch.setattr(module.KnowledgebaseService, "get_by_id", lambda _id: (True, SimpleNamespace(tenant_id="tenant-1", embd_id="m1")))
class _Retriever:
async def retrieval(self, *_args, **_kwargs):
return {"chunks": [], "total": 0}
def retrieval_by_children(self, chunks, *_args, **_kwargs):
return chunks
monkeypatch.setattr(module, "LLMBundle", lambda *_args, **_kwargs: SimpleNamespace())
monkeypatch.setattr(module, "label_question", lambda *_args, **_kwargs: {})
monkeypatch.setattr(module.settings, "retriever", _Retriever())
res = _run(module.retrieval_test.__wrapped__("tenant-1"))
assert res["code"] == 0
assert res["data"]["chunks"] == []
monkeypatch.setattr(
module,
"get_request_json",
lambda: _AwaitableValue({"dataset_ids": ["ds-1"], "question": "q", "highlight": True}),
)
res = _run(module.retrieval_test.__wrapped__("tenant-1"))
assert res["code"] == 0
monkeypatch.setattr(
module,
"get_request_json",
lambda: _AwaitableValue({"dataset_ids": ["ds-1"], "question": "q", "highlight": "yes"}),
)
res = _run(module.retrieval_test.__wrapped__("tenant-1"))
assert "`highlight` should be a boolean" in res["message"]
monkeypatch.setattr(
module,
"get_request_json",
lambda: _AwaitableValue({"dataset_ids": ["ds-1"], "question": "q", "highlight": 1}),
)
res = _run(module.retrieval_test.__wrapped__("tenant-1"))
assert "`highlight` should be a boolean" in res["message"]
monkeypatch.setattr(
module,
"get_request_json",
lambda: _AwaitableValue({"dataset_ids": ["ds-1"], "question": "q"}),
)
monkeypatch.setattr(module.KnowledgebaseService, "get_by_id", lambda _id: (False, None))
res = _run(module.retrieval_test.__wrapped__("tenant-1"))
assert "Dataset not found!" in res["message"]
feature_calls = {"cross": None, "keyword": None, "retrieval_question": None}
async def _cross_languages(_tenant_id, _dialog, question, langs):
feature_calls["cross"] = tuple(langs)
return f"{question}-xl"
async def _keyword_extraction(_chat_mdl, question):
feature_calls["keyword"] = question
return "-kw"
class _FeatureRetriever:
async def retrieval(self, question, *_args, **_kwargs):
feature_calls["retrieval_question"] = question
return {
"chunks": [
{
"chunk_id": "c1",
"content_with_weight": "content",
"doc_id": "doc-1",
"kb_id": "ds-1",
"vector": [1, 2],
}
],
"total": 1,
}
async def retrieval_by_toc(self, question, chunks, tenant_ids, _chat_mdl, size):
assert question == "q-xl-kw"
assert chunks and tenant_ids
assert size == 30
return [
{
"chunk_id": "toc-1",
"content_with_weight": "toc content",
"doc_id": "doc-toc",
"kb_id": "ds-1",
}
]
def retrieval_by_children(self, chunks, _tenant_ids):
return chunks + [
{
"chunk_id": "child-1",
"content_with_weight": "child content",
"doc_id": "doc-child",
"kb_id": "ds-1",
}
]
class _FeatureKgRetriever:
async def retrieval(self, *_args, **_kwargs):
return {
"chunk_id": "kg-1",
"content_with_weight": "kg content",
"doc_id": "doc-kg",
"kb_id": "ds-1",
}
monkeypatch.setattr(
module,
"get_request_json",
lambda: _AwaitableValue(
{
"dataset_ids": ["ds-1"],
"question": "q",
"rerank_id": "rerank-1",
"cross_languages": ["fr"],
"keyword": True,
"toc_enhance": True,
"use_kg": True,
}
),
)
monkeypatch.setattr(module.KnowledgebaseService, "get_by_id", lambda _id: (True, SimpleNamespace(tenant_id="tenant-1", embd_id="m1")))
monkeypatch.setattr(module, "cross_languages", _cross_languages)
monkeypatch.setattr(module, "keyword_extraction", _keyword_extraction)
monkeypatch.setattr(module.settings, "retriever", _FeatureRetriever())
monkeypatch.setattr(module.settings, "kg_retriever", _FeatureKgRetriever())
monkeypatch.setattr(module, "label_question", lambda *_args, **_kwargs: {})
monkeypatch.setattr(module, "LLMBundle", lambda *_args, **_kwargs: SimpleNamespace())
res = _run(module.retrieval_test.__wrapped__("tenant-1"))
assert res["code"] == 0
assert feature_calls["cross"] == ("fr",)
assert feature_calls["keyword"] == "q-xl"
assert feature_calls["retrieval_question"] == "q-xl-kw"
assert res["data"]["chunks"][0]["id"] == "kg-1"
assert res["data"]["chunks"][0]["content"] == "kg content"
assert any(chunk["id"] == "toc-1" for chunk in res["data"]["chunks"])
assert any(chunk["id"] == "child-1" for chunk in res["data"]["chunks"])
class _NotFoundRetriever:
async def retrieval(self, *_args, **_kwargs):
raise Exception("boom not_found boom")
def retrieval_by_children(self, chunks, *_args, **_kwargs):
return chunks
monkeypatch.setattr(
module,
"get_request_json",
lambda: _AwaitableValue({"dataset_ids": ["ds-1"], "question": "q"}),
)
monkeypatch.setattr(module.settings, "retriever", _NotFoundRetriever())
res = _run(module.retrieval_test.__wrapped__("tenant-1"))
assert res["code"] == module.RetCode.DATA_ERROR
assert "No chunk found! Check the chunk status please!" in res["message"]

View File

@@ -14,6 +14,7 @@
# limitations under the License.
#
import pytest
import requests
from common import (
create_agent,
create_agent_session,
@@ -22,6 +23,7 @@ from common import (
list_agent_sessions,
list_agents,
)
from configs import HOST_ADDRESS, VERSION
AGENT_TITLE = "test_agent_http"
MINIMAL_DSL = {
@@ -87,3 +89,33 @@ class TestAgentSessions:
res = delete_agent_sessions(HttpApiAuth, agent_id, {"ids": [session_id]})
assert res["code"] == 0, res
@pytest.mark.p2
def test_agent_crud_validation_contract(self, HttpApiAuth, agent_id):
res = list_agents(HttpApiAuth, {"id": "missing-agent-id", "title": "missing-agent-title"})
assert res["code"] == 102, res
assert "doesn't exist" in res["message"], res
res = list_agents(HttpApiAuth, {"title": AGENT_TITLE, "desc": "true", "page_size": 1})
assert res["code"] == 0, res
res = create_agent(HttpApiAuth, {"title": "missing-dsl-agent"})
assert res["code"] == 101, res
assert "No DSL data in request" in res["message"], res
res = create_agent(HttpApiAuth, {"dsl": MINIMAL_DSL})
assert res["code"] == 101, res
assert "No title in request" in res["message"], res
res = create_agent(HttpApiAuth, {"title": AGENT_TITLE, "dsl": MINIMAL_DSL})
assert res["code"] == 102, res
assert "already exists" in res["message"], res
update_url = f"{HOST_ADDRESS}/api/{VERSION}/agents/invalid-agent-id"
res = requests.put(update_url, auth=HttpApiAuth, json={"title": "updated", "dsl": MINIMAL_DSL}).json()
assert res["code"] == 103, res
assert "Only owner of canvas authorized" in res["message"], res
res = delete_agent(HttpApiAuth, "invalid-agent-id")
assert res["code"] == 103, res
assert "Only owner of canvas authorized" in res["message"], res

View File

@@ -130,3 +130,80 @@ class TestChatCompletionsOpenAI:
)
# Should return an error (format may vary based on implementation)
assert "error" in res or res.get("code") != 0, f"Should return error for invalid chat: {res}"
@pytest.mark.p2
@pytest.mark.parametrize(
"payload, requires_valid_chat, expected_message",
[
(
{
"model": "model",
"messages": [{"role": "user", "content": "hello"}],
"extra_body": "invalid_extra_body",
},
False,
"extra_body must be an object.",
),
(
{
"model": "model",
"messages": [{"role": "user", "content": "hello"}],
"extra_body": {"reference_metadata": "invalid_reference_metadata"},
},
False,
"reference_metadata must be an object.",
),
(
{
"model": "model",
"messages": [{"role": "user", "content": "hello"}],
"extra_body": {"reference_metadata": {"fields": "author"}},
},
False,
"reference_metadata.fields must be an array.",
),
(
{
"model": "model",
"messages": [],
},
False,
"You have to provide messages.",
),
(
{
"model": "model",
"messages": [{"role": "assistant", "content": "hello"}],
},
False,
"The last content of this conversation is not from user.",
),
(
{
"model": "model",
"messages": [{"role": "user", "content": "hello"}],
"extra_body": {"metadata_condition": "invalid"},
},
True,
"metadata_condition must be an object.",
),
],
)
def test_openai_chat_completion_request_validation(
self,
HttpApiAuth,
request,
payload,
requires_valid_chat,
expected_message,
):
chat_id = "invalid_chat_id"
if requires_valid_chat:
res = create_chat_assistant(HttpApiAuth, {"name": "openai_validation_case", "dataset_ids": []})
assert res["code"] == 0, res
chat_id = res["data"]["id"]
request.addfinalizer(lambda: delete_chat_assistants(HttpApiAuth))
res = chat_completions_openai(HttpApiAuth, chat_id, payload)
assert res.get("code") != 0, res
assert expected_message in res.get("message", ""), res