mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-11-10 21:32:43 +08:00
test(sdk): audit chat assistant tests (#21654)
This commit is contained in:
1 parent
3f6939db1a
commit
66e308c8ea
8 files changed
+1117
-694
No files matched your search
@@ -0,0 +1,49 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
# Chat assistant SDK tests
|
||||
|
||||
Run from the repository root with Python 3.13, the current `sdk/python` dependencies,
|
||||
pytest and Pillow installed. These tests deliberately exclude ancestor fixtures
|
||||
that provision models, require provider keys, or rewrite marker selection.
|
||||
Both `-c` and `--confcutdir` are required:
|
||||
|
||||
```sh
|
||||
folder=test/testcases/test_sdk_api/test_chat_assistant_management
|
||||
python -m pytest -c "$folder/pytest.ini" --confcutdir="$folder" "$folder" --collect-only -q
|
||||
python -m pytest -c "$folder/pytest.ini" --confcutdir="$folder" "$folder" -m 'not integration' -q
|
||||
```
|
||||
|
||||
Mocked tests need no backend or credentials and block unmocked network requests.
|
||||
Live tests fail setup unless all three environment variables are configured:
|
||||
|
||||
- `RAGFLOW_SDK_ISOLATED=1`: confirms a dedicated disposable Go deployment and tenant.
|
||||
- `RAGFLOW_SDK_BASE_URL`: the deployment base URL.
|
||||
- `RAGFLOW_SDK_API_KEY`: the dedicated tenant owner's API key.
|
||||
|
||||
Provision that environment independently; a reachable server does not establish
|
||||
isolation. Use an empty tenant without shared resources, with a configured default
|
||||
chat model, embedding model, document engine and storage. Dataset association tests
|
||||
upload documents and add manual chunks; they never start parsing or poll jobs.
|
||||
Run serially, without xdist or another client sharing the tenant. The initial
|
||||
preflight rejects accessible chats/datasets; delete-all cases additionally check
|
||||
the complete current chat IDs immediately before deletion.
|
||||
|
||||
Run the live suite by replacing `-m 'not integration'` with `-m integration`.
|
||||
Cleanup deletes only IDs created by each test and reports cleanup errors. No model
|
||||
configuration or tenant-wide cleanup runs automatically. Targeted source-defect
|
||||
skips retain their expected assertions and need rechecking when their owning
|
||||
SDK or Go implementation changes; a skip is not a live reproduction.
|
||||
@@ -1,5 +1,5 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
# 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.
|
||||
@@ -13,40 +13,109 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
"""Chat-only fixtures: explicit isolation, bounded requests and owned cleanup."""
|
||||
|
||||
import os
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from common import batch_create_chat_assistants, delete_all_chats
|
||||
from pytest import FixtureRequest
|
||||
from ragflow_sdk import Chat, DataSet, Document, RAGFlow
|
||||
from utils import wait_for
|
||||
import requests
|
||||
from ragflow_sdk import RAGFlow
|
||||
|
||||
|
||||
@wait_for(200, 1, "Document parsing timeout")
|
||||
def condition(_dataset: DataSet):
|
||||
documents = _dataset.list_documents(page_size=100)
|
||||
for document in documents:
|
||||
if document.run != "DONE":
|
||||
return False
|
||||
return True
|
||||
@pytest.fixture(autouse=True)
|
||||
def request_boundary(request, monkeypatch):
|
||||
send = requests.sessions.Session.request
|
||||
|
||||
def bounded_request(session, *args, **kwargs):
|
||||
if request.node.get_closest_marker("integration") is None:
|
||||
pytest.fail("Service-free chat tests must not open a network connection")
|
||||
kwargs.setdefault("timeout", 30)
|
||||
return send(session, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(requests.sessions.Session, "request", bounded_request)
|
||||
|
||||
|
||||
def _ensure_parsed(dataset: DataSet, document: Document):
|
||||
"""Trigger parsing only if the document is not already done or in progress."""
|
||||
if document.run == "DONE":
|
||||
return
|
||||
try:
|
||||
dataset.async_parse_documents([document.id])
|
||||
except Exception:
|
||||
pass # Already being processed
|
||||
condition(dataset)
|
||||
@pytest.fixture(scope="session")
|
||||
def integration_settings():
|
||||
required = ("RAGFLOW_SDK_BASE_URL", "RAGFLOW_SDK_API_KEY")
|
||||
if os.getenv("RAGFLOW_SDK_ISOLATED") != "1" or any(not os.getenv(key) for key in required):
|
||||
pytest.fail(
|
||||
"Integration environment required: set RAGFLOW_SDK_ISOLATED=1, "
|
||||
"RAGFLOW_SDK_BASE_URL and RAGFLOW_SDK_API_KEY for a dedicated, empty Go test tenant. "
|
||||
"Run service-free tests with -m 'not integration'."
|
||||
)
|
||||
if os.getenv("PYTEST_XDIST_WORKER"):
|
||||
pytest.fail("Chat integrations require a dedicated tenant and serial execution")
|
||||
return {"base_url": os.environ[required[0]], "api_key": os.environ[required[1]]}
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def add_chat_assistants_func(request: FixtureRequest, client: RAGFlow, add_document: tuple[DataSet, Document]) -> tuple[DataSet, Document, list[Chat]]:
|
||||
def cleanup():
|
||||
delete_all_chats(client)
|
||||
@pytest.fixture(scope="session")
|
||||
def isolated_tenant(integration_settings):
|
||||
# The dedicated-tenant declaration is mandatory; emptiness alone is not isolation.
|
||||
probe = RAGFlow(integration_settings["api_key"], integration_settings["base_url"])
|
||||
response = requests.get(f"{probe.api_url}/chats", headers=probe.authorization_header, params={"page_size": 1}, timeout=30)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
assert payload["code"] == 0, payload
|
||||
assert payload["data"]["total"] == 0, "Refusing integrations: tenant has accessible chats"
|
||||
response = requests.get(f"{probe.api_url}/datasets", headers=probe.authorization_header, params={"page_size": 1}, timeout=30)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
assert payload["code"] == 0, payload
|
||||
assert payload["data"] == [], "Refusing integrations: tenant has accessible datasets"
|
||||
return integration_settings
|
||||
|
||||
request.addfinalizer(cleanup)
|
||||
|
||||
dataset, document = add_document
|
||||
_ensure_parsed(dataset, document)
|
||||
return dataset, document, batch_create_chat_assistants(client, 5)
|
||||
@pytest.fixture
|
||||
def client(isolated_tenant, monkeypatch):
|
||||
rag = RAGFlow(isolated_tenant["api_key"], isolated_tenant["base_url"])
|
||||
chats, datasets = [], []
|
||||
create_chat, create_dataset = rag.create_chat, rag.create_dataset
|
||||
|
||||
def tracked_chat(*args, **kwargs):
|
||||
chat = create_chat(*args, **kwargs)
|
||||
chats.append(chat.id)
|
||||
return chat
|
||||
|
||||
def tracked_dataset(*args, **kwargs):
|
||||
dataset = create_dataset(*args, **kwargs)
|
||||
datasets.append(dataset.id)
|
||||
return dataset
|
||||
|
||||
monkeypatch.setattr(rag, "create_chat", tracked_chat)
|
||||
monkeypatch.setattr(rag, "create_dataset", tracked_dataset)
|
||||
yield rag
|
||||
# Continue cleaning other owned resources if one cleanup operation fails.
|
||||
errors = []
|
||||
for chat_id in chats:
|
||||
try:
|
||||
if rag.list_chats(id=chat_id):
|
||||
rag.delete_chats(ids=[chat_id])
|
||||
assert rag.list_chats(id=chat_id) == [], f"Chat cleanup failed: {chat_id}"
|
||||
except Exception as exc: # noqa: BLE001 - SDK raises plain Exception; report every cleanup failure.
|
||||
errors.append(str(exc))
|
||||
for dataset_id in datasets:
|
||||
try:
|
||||
if rag.list_datasets(id=dataset_id):
|
||||
rag.delete_datasets(ids=[dataset_id])
|
||||
assert rag.list_datasets(id=dataset_id) == [], f"Dataset cleanup failed: {dataset_id}"
|
||||
except Exception as exc: # noqa: BLE001 - SDK raises plain Exception; report every cleanup failure.
|
||||
errors.append(str(exc))
|
||||
assert not errors, "Owned resource cleanup failed: " + "; ".join(errors)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def chat_factory(client):
|
||||
def create(name=None, **kwargs):
|
||||
return client.create_chat(name=name if name is not None else f"sdk-chat-{uuid4().hex}", **kwargs)
|
||||
|
||||
return create
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dataset_factory(client):
|
||||
def create(name=None, **kwargs):
|
||||
return client.create_dataset(name=name if name is not None else f"sdk-chat-dataset-{uuid4().hex}", **kwargs)
|
||||
|
||||
return create
|
||||
@@ -0,0 +1,24 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
[pytest]
|
||||
pythonpath = ../../../../sdk/python
|
||||
addopts = --strict-markers
|
||||
markers =
|
||||
integration: requires an explicitly isolated Go backend and dedicated empty tenant
|
||||
p1: high priority
|
||||
p2: medium priority
|
||||
p3: low priority
|
||||
+201
-51
@@ -14,10 +14,14 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from copy import deepcopy
|
||||
from inspect import unwrap
|
||||
|
||||
import conftest as chat_fixtures
|
||||
import pytest
|
||||
from configs import HOST_ADDRESS
|
||||
from ragflow_sdk import RAGFlow
|
||||
from ragflow_sdk.modules.chat import Chat
|
||||
import requests
|
||||
from beartype.roar import BeartypeCallHintParamViolation
|
||||
from ragflow_sdk import Chat, RAGFlow
|
||||
from ragflow_sdk.modules.session import Session
|
||||
|
||||
|
||||
@@ -26,63 +30,209 @@ class _DummyResponse:
|
||||
self._payload = payload
|
||||
|
||||
def json(self):
|
||||
return self._payload
|
||||
return deepcopy(self._payload)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def auth():
|
||||
return "unit-auth"
|
||||
@pytest.fixture
|
||||
def sdk_client():
|
||||
return RAGFlow("unit-token", "http://sdk.invalid")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def set_tenant_info():
|
||||
return None
|
||||
@pytest.fixture
|
||||
def wire(monkeypatch):
|
||||
state = {"response": {"code": 0, "data": {}}, "calls": []}
|
||||
|
||||
def send(_session, method, url, **kwargs):
|
||||
state["calls"].append((method.upper(), url, kwargs))
|
||||
return _DummyResponse(state["response"])
|
||||
|
||||
monkeypatch.setattr(requests.sessions.Session, "request", send)
|
||||
return state
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_chat_create_session_raises_server_error_message(monkeypatch):
|
||||
client = RAGFlow("token", HOST_ADDRESS)
|
||||
chat = Chat(client, {"id": "chat-1"})
|
||||
|
||||
monkeypatch.setattr(
|
||||
chat,
|
||||
"post",
|
||||
lambda *_args, **_kwargs: _DummyResponse({"code": 102, "message": "`name` can not be empty"}),
|
||||
)
|
||||
|
||||
with pytest.raises(Exception) as exception_info:
|
||||
chat.create_session(name="")
|
||||
assert "`name` can not be empty" in str(exception_info.value), str(exception_info.value)
|
||||
@pytest.mark.parametrize(
|
||||
"options",
|
||||
[
|
||||
{},
|
||||
{"dataset_ids": None},
|
||||
{"icon": "data:image/png;base64,aWNvbg==", "dataset_ids": ["dataset-1"]},
|
||||
{"llm_id": "model@instance@provider", "llm_setting": {"temperature": 0}, "prompt_config": {"quote": False}, "top_n": 4},
|
||||
{"llm_id": "", "llm_setting": {}, "prompt_config": {}},
|
||||
],
|
||||
)
|
||||
def test_create_chat_request_and_response(sdk_client, wire, options):
|
||||
wire["response"]["data"] = {"id": "chat-1", "name": "unit-chat", "llm_setting": {"temperature": 0}}
|
||||
chat = sdk_client.create_chat("unit-chat", **options)
|
||||
expected = {"name": "unit-chat", "icon": "", "dataset_ids": []}
|
||||
expected.update(options)
|
||||
expected["dataset_ids"] = options.get("dataset_ids") or []
|
||||
method, url, args = wire["calls"].pop()
|
||||
assert (method, url) == ("POST", "http://sdk.invalid/api/v1/chats")
|
||||
assert args["json"] == expected
|
||||
assert args["headers"] == {"Authorization": "Bearer unit-token"}
|
||||
assert isinstance(chat, Chat)
|
||||
assert (chat.id, chat.name, chat.to_json()["llm_setting"]["temperature"]) == ("chat-1", "unit-chat", 0)
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_chat_list_sessions_forwards_restful_query_params(monkeypatch):
|
||||
client = RAGFlow("token", HOST_ADDRESS)
|
||||
chat = Chat(client, {"id": "chat-1"})
|
||||
calls = []
|
||||
@pytest.mark.parametrize("owner_ids", [None, "owner-1", ["owner-1", "owner-2"]])
|
||||
def test_list_chats_request_and_response(sdk_client, wire, owner_ids):
|
||||
wire["response"]["data"] = {"total": 1, "chats": [{"id": "chat-1", "dataset_ids": ["dataset-1"], "prompt_config": {"quote": False}}]}
|
||||
options = {"page": 2, "page_size": 3, "orderby": "name", "desc": False, "id": "chat-1", "name": "exact", "keywords": "fuzzy", "owner_ids": owner_ids}
|
||||
chats = sdk_client.list_chats(**options)
|
||||
method, url, args = wire["calls"].pop()
|
||||
assert (method, url) == ("GET", "http://sdk.invalid/api/v1/chats")
|
||||
assert args["params"] == options
|
||||
assert len(chats) == 1 and isinstance(chats[0], Chat)
|
||||
assert chats[0].id == "chat-1"
|
||||
assert chats[0].dataset_ids == ["dataset-1"]
|
||||
assert chats[0].to_json()["prompt_config"]["quote"] is False
|
||||
|
||||
def _ok_get(path, params=None):
|
||||
calls.append((path, params))
|
||||
return _DummyResponse(
|
||||
{
|
||||
"code": 0,
|
||||
"data": [
|
||||
{"id": "session-1", "chat_id": "chat-1", "name": "one"},
|
||||
{"id": "session-2", "chat_id": "chat-1", "name": "two"},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
monkeypatch.setattr(chat, "get", _ok_get)
|
||||
def test_list_chats_empty(sdk_client, wire):
|
||||
wire["response"]["data"] = {"total": 0, "chats": []}
|
||||
assert sdk_client.list_chats(page_size=0) == []
|
||||
assert wire["calls"][0][2]["params"]["page_size"] == 0
|
||||
|
||||
sessions = chat.list_sessions(page=2, page_size=2, orderby="create_time", desc=False, id="session-1", name="one", user_id="user-1")
|
||||
assert len(sessions) == 2, str(sessions)
|
||||
assert all(isinstance(item, Session) for item in sessions), str(sessions)
|
||||
assert calls[-1][0] == "/chats/chat-1/sessions"
|
||||
assert calls[-1][1]["page_size"] == 2
|
||||
assert calls[-1][1]["name"] == "one"
|
||||
assert calls[-1][1]["user_id"] == "user-1"
|
||||
|
||||
all_sessions = chat.list_sessions(page_size=0)
|
||||
assert len(all_sessions) == 2, str(all_sessions)
|
||||
assert calls[-1][1]["page_size"] == 0
|
||||
def test_get_chat_request_and_response(sdk_client, wire):
|
||||
wire["response"]["data"] = {"id": "chat-1", "name": "detail", "llm_id": "model", "llm_setting": {"max_tokens": 64}, "prompt_config": {"system": "sys"}}
|
||||
chat = sdk_client.get_chat("chat-1")
|
||||
assert wire["calls"][0][:2] == ("GET", "http://sdk.invalid/api/v1/chats/chat-1")
|
||||
assert isinstance(chat, Chat)
|
||||
assert (chat.id, chat.name, chat.llm_id) == ("chat-1", "detail", "model")
|
||||
assert chat.to_json()["llm_setting"]["max_tokens"] == 64
|
||||
assert chat.to_json()["prompt_config"]["system"] == "sys"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("options", [{}, {"ids": None}, {"ids": []}, {"ids": ["chat-1"]}, {"delete_all": True}, {"ids": [], "delete_all": True}, {"ids": ["chat-1"], "delete_all": True}])
|
||||
def test_delete_chats_explicit_payload(sdk_client, wire, options):
|
||||
sdk_client.delete_chats(**options)
|
||||
method, url, args = wire["calls"].pop()
|
||||
assert (method, url) == ("DELETE", "http://sdk.invalid/api/v1/chats")
|
||||
assert args["json"] == {"ids": options.get("ids"), "delete_all": options.get("delete_all", False)}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("operation", ["create", "list", "get", "delete", "update", "create_session", "list_sessions", "delete_sessions"])
|
||||
def test_chat_operations_propagate_server_errors(sdk_client, wire, operation):
|
||||
wire["response"] = {"code": 102, "message": "contract sentinel error"}
|
||||
chat = Chat(sdk_client, {"id": "chat-1"})
|
||||
operations = {
|
||||
"create": lambda: sdk_client.create_chat("name"),
|
||||
"list": sdk_client.list_chats,
|
||||
"get": lambda: sdk_client.get_chat("chat-1"),
|
||||
"delete": lambda: sdk_client.delete_chats(["chat-1"]),
|
||||
"update": lambda: chat.update({"name": "renamed"}),
|
||||
"create_session": chat.create_session,
|
||||
"list_sessions": chat.list_sessions,
|
||||
"delete_sessions": lambda: chat.delete_sessions(["session-1"]),
|
||||
}
|
||||
with pytest.raises(Exception, match="^contract sentinel error$"):
|
||||
operations[operation]()
|
||||
assert len(wire["calls"]) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("payload", [None, [], "bad", 1, False])
|
||||
def test_update_rejects_non_dict(sdk_client, payload):
|
||||
with pytest.raises(BeartypeCallHintParamViolation):
|
||||
Chat(sdk_client, {"id": "chat-1"}).update(payload)
|
||||
|
||||
|
||||
def test_update_rejects_non_dict_without_type_wrapper(sdk_client):
|
||||
chat = Chat(sdk_client, {"id": "chat-1"})
|
||||
with pytest.raises(Exception, match="`update_message` must be a dict"):
|
||||
unwrap(Chat.update)(chat, "bad")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("payload", [{}, {"name": "renamed"}, {"llm_setting": {"temperature": 0}}, {"prompt_config": {"quote": False}}])
|
||||
def test_update_uses_patch_for_partial_payload(sdk_client, wire, payload):
|
||||
chat = Chat(sdk_client, {"id": "chat-1"})
|
||||
assert chat.update(payload) is None
|
||||
method, url, args = wire["calls"].pop()
|
||||
assert (method, url) == ("PATCH", "http://sdk.invalid/api/v1/chats/chat-1")
|
||||
assert args["json"] == payload
|
||||
|
||||
|
||||
@pytest.mark.parametrize("operation, options", [("list", {"page": "bad"}), ("list", {"page_size": []}), ("list", {"desc": "false"}), ("get", {"chat_id": 123}), ("delete", {"ids": "chat-1"})])
|
||||
def test_sdk_rejects_typed_arguments_before_http(sdk_client, operation, options):
|
||||
call = {"list": sdk_client.list_chats, "get": sdk_client.get_chat, "delete": sdk_client.delete_chats}[operation]
|
||||
with pytest.raises(BeartypeCallHintParamViolation):
|
||||
call(**options)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"options",
|
||||
[{"name": 1}, {"dataset_ids": "dataset-1"}, {"llm_setting": "invalid"}, {"prompt_config": "invalid"}, {"icon": 1}, {"llm_id": 123}],
|
||||
)
|
||||
def test_create_chat_rejects_typed_arguments_before_http(sdk_client, options):
|
||||
with pytest.raises(BeartypeCallHintParamViolation):
|
||||
sdk_client.create_chat(**{"name": "typed-chat", **options})
|
||||
|
||||
|
||||
def test_chat_create_session_raises_server_error_message(sdk_client, wire):
|
||||
wire["response"] = {"code": 102, "message": "`name` can not be empty"}
|
||||
with pytest.raises(Exception, match="`name` can not be empty"):
|
||||
Chat(sdk_client, {"id": "chat-1"}).create_session(name="")
|
||||
method, url, args = wire["calls"][0]
|
||||
assert (method, url) == ("POST", "http://sdk.invalid/api/v1/chats/chat-1/sessions")
|
||||
assert args["json"] == {"name": ""}
|
||||
|
||||
|
||||
def test_chat_list_sessions_forwards_restful_query_params(sdk_client, wire):
|
||||
wire["response"]["data"] = [{"id": "session-1", "chat_id": "chat-1", "name": "one"}, {"id": "session-2", "chat_id": "chat-1", "name": "two"}]
|
||||
chat = Chat(sdk_client, {"id": "chat-1"})
|
||||
options = {"page": 2, "page_size": 2, "orderby": "create_time", "desc": False, "id": "session-1", "name": "one", "user_id": "user-1"}
|
||||
sessions = chat.list_sessions(**options)
|
||||
method, url, args = wire["calls"].pop()
|
||||
assert (method, url) == ("GET", "http://sdk.invalid/api/v1/chats/chat-1/sessions")
|
||||
assert args["params"] == options
|
||||
assert all(isinstance(item, Session) for item in sessions)
|
||||
assert [item.id for item in sessions] == ["session-1", "session-2"]
|
||||
assert len(chat.list_sessions(page_size=0)) == 2
|
||||
assert wire["calls"][-1][2]["params"]["page_size"] == 0
|
||||
|
||||
|
||||
def test_integration_settings_require_explicit_isolation(monkeypatch):
|
||||
for key in ("RAGFLOW_SDK_BASE_URL", "RAGFLOW_SDK_API_KEY", "RAGFLOW_SDK_ISOLATED"):
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
with pytest.raises(pytest.fail.Exception, match="Integration environment required"):
|
||||
unwrap(chat_fixtures.integration_settings)()
|
||||
|
||||
|
||||
def test_fixture_cleanup_only_deletes_owned_resources(monkeypatch):
|
||||
chats = {"unrelated-chat"}
|
||||
datasets = {"unrelated-dataset"}
|
||||
deleted = []
|
||||
|
||||
class FakeClient:
|
||||
def create_chat(self, **kwargs):
|
||||
chats.add("owned-chat")
|
||||
return type("Chat", (), {"id": "owned-chat"})()
|
||||
|
||||
def create_dataset(self, **kwargs):
|
||||
datasets.add("owned-dataset")
|
||||
return type("Dataset", (), {"id": "owned-dataset"})()
|
||||
|
||||
def list_chats(self, id):
|
||||
return [id] if id in chats else []
|
||||
|
||||
def list_datasets(self, id):
|
||||
return [id] if id in datasets else []
|
||||
|
||||
def delete_chats(self, ids):
|
||||
deleted.extend(ids)
|
||||
chats.difference_update(ids)
|
||||
|
||||
def delete_datasets(self, ids):
|
||||
deleted.extend(ids)
|
||||
datasets.difference_update(ids)
|
||||
|
||||
monkeypatch.setattr(chat_fixtures, "RAGFlow", lambda *_args: FakeClient())
|
||||
lifecycle = unwrap(chat_fixtures.client)({"api_key": "fake", "base_url": "http://sdk.invalid"}, monkeypatch)
|
||||
client = next(lifecycle)
|
||||
client.create_chat(name="owned")
|
||||
client.create_dataset(name="owned")
|
||||
with pytest.raises(StopIteration):
|
||||
next(lifecycle)
|
||||
assert deleted == ["owned-chat", "owned-dataset"]
|
||||
assert chats == {"unrelated-chat"}
|
||||
assert datasets == {"unrelated-dataset"}
|
||||
+226
-177
@@ -1,5 +1,5 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
# 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.
|
||||
@@ -14,184 +14,233 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import base64
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from common import valid_chat_llm_id
|
||||
from configs import CHAT_ASSISTANT_NAME_LIMIT
|
||||
from utils import encode_avatar
|
||||
from utils.file_utils import create_image_file
|
||||
from PIL import Image
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("clear_chat_assistants")
|
||||
class TestChatAssistantCreate:
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.usefixtures("add_chunks")
|
||||
@pytest.mark.parametrize(
|
||||
"name, expected_message",
|
||||
[
|
||||
("valid_name", ""),
|
||||
pytest.param("a" * (CHAT_ASSISTANT_NAME_LIMIT + 1), "", marks=pytest.mark.skip(reason="issues/")),
|
||||
pytest.param(1, "", marks=pytest.mark.skip(reason="issues/")),
|
||||
("", "`name` is required"),
|
||||
("duplicated_name", "duplicated chat name in creating chat"),
|
||||
("case insensitive", "duplicated chat name in creating chat"),
|
||||
],
|
||||
)
|
||||
def test_name(self, client, name, expected_message):
|
||||
if name == "duplicated_name":
|
||||
client.create_chat(name=name)
|
||||
elif name == "case insensitive":
|
||||
client.create_chat(name=name.upper())
|
||||
|
||||
if expected_message:
|
||||
with pytest.raises(Exception) as exception_info:
|
||||
client.create_chat(name=name)
|
||||
assert expected_message in str(exception_info.value)
|
||||
else:
|
||||
chat_assistant = client.create_chat(name=name)
|
||||
assert chat_assistant.name == name
|
||||
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.parametrize(
|
||||
"dataset_ids, expected_message",
|
||||
[
|
||||
([], ""),
|
||||
(lambda r: [r], ""),
|
||||
(["invalid_dataset_id"], "You don't own the dataset invalid_dataset_id"),
|
||||
("invalid_dataset_id", "violates type hint list[str] | None"),
|
||||
],
|
||||
)
|
||||
def test_dataset_ids(self, client, add_chunks, dataset_ids, expected_message):
|
||||
dataset, _, _ = add_chunks
|
||||
if callable(dataset_ids):
|
||||
dataset_ids = dataset_ids(dataset.id)
|
||||
|
||||
if expected_message:
|
||||
with pytest.raises(Exception) as exception_info:
|
||||
client.create_chat(name="ragflow test", dataset_ids=dataset_ids)
|
||||
assert expected_message in str(exception_info.value)
|
||||
else:
|
||||
chat_assistant = client.create_chat(name="ragflow test", dataset_ids=dataset_ids)
|
||||
assert chat_assistant.name == "ragflow test"
|
||||
|
||||
@pytest.mark.p3
|
||||
def test_icon(self, client, tmp_path):
|
||||
fn = create_image_file(tmp_path / "ragflow_test.png")
|
||||
chat_assistant = client.create_chat(name="icon_test", icon=encode_avatar(fn), dataset_ids=[])
|
||||
assert chat_assistant.name == "icon_test"
|
||||
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.parametrize(
|
||||
"llm_setting, expected_message",
|
||||
[
|
||||
({}, ""),
|
||||
({"temperature": 0}, ""),
|
||||
({"temperature": 1}, ""),
|
||||
pytest.param({"temperature": -1}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"temperature": 10}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"temperature": "a"}, "", marks=pytest.mark.skip),
|
||||
({"top_p": 0}, ""),
|
||||
({"top_p": 1}, ""),
|
||||
pytest.param({"top_p": -1}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"top_p": 10}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"top_p": "a"}, "", marks=pytest.mark.skip),
|
||||
({"presence_penalty": 0}, ""),
|
||||
({"presence_penalty": 1}, ""),
|
||||
pytest.param({"presence_penalty": -1}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"presence_penalty": 10}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"presence_penalty": "a"}, "", marks=pytest.mark.skip),
|
||||
({"frequency_penalty": 0}, ""),
|
||||
({"frequency_penalty": 1}, ""),
|
||||
pytest.param({"frequency_penalty": -1}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"frequency_penalty": 10}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"frequency_penalty": "a"}, "", marks=pytest.mark.skip),
|
||||
({"max_token": 0}, ""),
|
||||
({"max_token": 1024}, ""),
|
||||
pytest.param({"max_token": -1}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"max_token": 10}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"max_token": "a"}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"unknown": "unknown"}, "", marks=pytest.mark.skip),
|
||||
],
|
||||
)
|
||||
def test_llm_setting(self, client, add_chunks, llm_setting, expected_message):
|
||||
dataset, _, _ = add_chunks
|
||||
|
||||
if expected_message:
|
||||
with pytest.raises(Exception) as exception_info:
|
||||
client.create_chat(name="llm_test", dataset_ids=[dataset.id], llm_setting=llm_setting or None)
|
||||
assert expected_message in str(exception_info.value)
|
||||
else:
|
||||
chat_assistant = client.create_chat(name="llm_test", dataset_ids=[dataset.id], llm_setting=llm_setting or None)
|
||||
for k, v in llm_setting.items():
|
||||
assert getattr(chat_assistant.llm_setting, k) == v
|
||||
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.parametrize(
|
||||
"llm_id, expected_message",
|
||||
[
|
||||
(valid_chat_llm_id, ""),
|
||||
("unknown", "`llm_id` unknown doesn't exist"),
|
||||
],
|
||||
)
|
||||
def test_llm_id(self, client, add_chunks, llm_id, expected_message):
|
||||
dataset, _, _ = add_chunks
|
||||
if callable(llm_id):
|
||||
llm_id = llm_id(client)
|
||||
|
||||
if expected_message:
|
||||
with pytest.raises(Exception) as exception_info:
|
||||
client.create_chat(name="llm_test", dataset_ids=[dataset.id], llm_id=llm_id)
|
||||
assert expected_message in str(exception_info.value)
|
||||
else:
|
||||
chat_assistant = client.create_chat(name="llm_test", dataset_ids=[dataset.id], llm_id=llm_id)
|
||||
assert chat_assistant.llm_id == llm_id
|
||||
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.parametrize(
|
||||
"prompt_config, expected_message",
|
||||
[
|
||||
({"empty_response": "Hello World"}, ""),
|
||||
({"empty_response": ""}, ""),
|
||||
({"empty_response": "!@#$%^&*()"}, ""),
|
||||
({"empty_response": "中文测试"}, ""),
|
||||
pytest.param({"empty_response": 123}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"empty_response": True}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"empty_response": " "}, "", marks=pytest.mark.skip),
|
||||
({"prologue": "Hello World"}, ""),
|
||||
({"prologue": ""}, ""),
|
||||
({"prologue": "!@#$%^&*()"}, ""),
|
||||
({"prologue": "中文测试"}, ""),
|
||||
pytest.param({"prologue": 123}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"prologue": True}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"prologue": " "}, "", marks=pytest.mark.skip),
|
||||
({"quote": True}, ""),
|
||||
({"quote": False}, ""),
|
||||
({"system": "Hello World {knowledge}"}, ""),
|
||||
({"system": "{knowledge}"}, ""),
|
||||
({"system": "!@#$%^&*() {knowledge}"}, ""),
|
||||
({"system": "中文测试 {knowledge}"}, ""),
|
||||
({"system": "Hello World"}, ""),
|
||||
({"system": "Hello World", "parameters": []}, ""),
|
||||
pytest.param({"system": 123}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"unknown": "unknown"}, "", marks=pytest.mark.skip),
|
||||
],
|
||||
)
|
||||
def test_prompt_config(self, client, add_chunks, prompt_config, expected_message):
|
||||
dataset, _, _ = add_chunks
|
||||
|
||||
if expected_message:
|
||||
with pytest.raises(Exception) as exception_info:
|
||||
client.create_chat(name="prompt_test", dataset_ids=[dataset.id], prompt_config=prompt_config)
|
||||
assert expected_message in str(exception_info.value)
|
||||
else:
|
||||
chat_assistant = client.create_chat(name="prompt_test", dataset_ids=[dataset.id], prompt_config=prompt_config)
|
||||
for k, v in prompt_config.items():
|
||||
assert getattr(chat_assistant.prompt_config, k) == v
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.parametrize(
|
||||
"name, expected",
|
||||
[
|
||||
("valid_name", None),
|
||||
("a" * 255, None),
|
||||
("a" * 256, "larger than 255"),
|
||||
("", "`name` is required"),
|
||||
(" ", "`name` is required"),
|
||||
],
|
||||
)
|
||||
def test_name(chat_factory, name, expected):
|
||||
if expected:
|
||||
with pytest.raises(Exception, match=expected):
|
||||
chat_factory(name=name)
|
||||
else:
|
||||
chat = chat_factory(name=name)
|
||||
assert chat.id and chat.name == name
|
||||
|
||||
|
||||
class TestChatAssistantCreate2:
|
||||
@pytest.mark.p3
|
||||
def test_unparsed_document(self, client, add_document):
|
||||
dataset, _ = add_document
|
||||
with pytest.raises(Exception) as exception_info:
|
||||
client.create_chat(name="prompt_test", dataset_ids=[dataset.id])
|
||||
assert "doesn't own parsed file" in str(exception_info.value)
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.parametrize("case_variant", [False, True], ids=["same-case", "case-insensitive"])
|
||||
def test_duplicate_name(chat_factory, case_variant):
|
||||
name = f"sdk-chat-duplicate-{uuid4().hex}"
|
||||
first = chat_factory(name=name)
|
||||
second = chat_factory(name=name.upper() if case_variant else name)
|
||||
assert first.id != second.id
|
||||
assert second.name == f"{name.upper() if case_variant else name}(1)"
|
||||
|
||||
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.parametrize("dataset_ids", [[], ["invalid_dataset_id"]])
|
||||
def test_dataset_ids(chat_factory, dataset_ids):
|
||||
if dataset_ids == []:
|
||||
chat = chat_factory(dataset_ids=dataset_ids)
|
||||
assert chat.dataset_ids == []
|
||||
else:
|
||||
with pytest.raises(Exception, match="Resource not found"):
|
||||
chat_factory(dataset_ids=dataset_ids)
|
||||
|
||||
|
||||
@pytest.mark.p3
|
||||
def test_dataset_association(chat_factory, dataset_factory):
|
||||
dataset = dataset_factory()
|
||||
# A manually added chunk satisfies the Go chat endpoint without starting a parser.
|
||||
document = dataset.upload_documents([{"display_name": "chat-association.txt", "blob": b"chat association"}])[0]
|
||||
document.add_chunk(content="chat association")
|
||||
|
||||
chat = chat_factory(dataset_ids=[dataset.id])
|
||||
assert chat.dataset_ids == [dataset.id]
|
||||
|
||||
|
||||
@pytest.mark.p3
|
||||
def test_icon(chat_factory, client, tmp_path):
|
||||
image = tmp_path / "chat-icon.png"
|
||||
Image.new("RGB", (1, 1), color="blue").save(image)
|
||||
icon = base64.b64encode(image.read_bytes()).decode("ascii")
|
||||
chat = chat_factory(icon=icon)
|
||||
assert chat.icon == icon
|
||||
assert client.get_chat(chat.id).icon == icon
|
||||
|
||||
|
||||
@pytest.mark.p3
|
||||
def test_default_settings(chat_factory, client):
|
||||
chat = chat_factory()
|
||||
persisted = client.get_chat(chat.id)
|
||||
assert persisted.dataset_ids == []
|
||||
assert persisted.llm_id == chat.llm_id
|
||||
assert persisted.prompt_config.system == ""
|
||||
assert persisted.prompt_config.quote is True
|
||||
assert persisted.prompt_config.parameters == [{"key": "knowledge", "optional": False}]
|
||||
|
||||
|
||||
@pytest.mark.p3
|
||||
def test_optional_fields(chat_factory, client):
|
||||
chat = chat_factory(description="SDK chat", top_n=8, similarity_threshold=0.4)
|
||||
persisted = client.get_chat(chat.id)
|
||||
assert persisted.description == "SDK chat"
|
||||
assert persisted.top_n == 8
|
||||
assert persisted.similarity_threshold == 0.4
|
||||
|
||||
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.parametrize(
|
||||
"llm_setting",
|
||||
[
|
||||
{},
|
||||
{"temperature": 0},
|
||||
{"temperature": 1},
|
||||
{"top_p": 0},
|
||||
{"top_p": 1},
|
||||
{"presence_penalty": 0},
|
||||
{"presence_penalty": 1},
|
||||
{"frequency_penalty": 0},
|
||||
{"frequency_penalty": 1},
|
||||
{"max_tokens": 1024},
|
||||
],
|
||||
)
|
||||
def test_llm_setting(chat_factory, client, llm_setting):
|
||||
chat = chat_factory(llm_setting=llm_setting)
|
||||
persisted = client.get_chat(chat.id)
|
||||
assert chat.llm_setting.to_json() == llm_setting
|
||||
assert persisted.llm_setting.to_json() == llm_setting
|
||||
|
||||
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.parametrize(
|
||||
"llm_setting",
|
||||
[
|
||||
{"temperature": "invalid"},
|
||||
{"top_p": "invalid"},
|
||||
{"presence_penalty": "invalid"},
|
||||
{"frequency_penalty": "invalid"},
|
||||
{"max_tokens": "invalid"},
|
||||
],
|
||||
)
|
||||
@pytest.mark.skip(reason="Go ChatService.Create accepts invalid generation parameter types without validation")
|
||||
def test_invalid_llm_setting(chat_factory, llm_setting):
|
||||
with pytest.raises(Exception, match="llm_setting"):
|
||||
chat_factory(llm_setting=llm_setting)
|
||||
|
||||
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.skip(reason="Go ChatService.Create discards non-object llm_setting rather than rejecting it")
|
||||
def test_create_rejects_invalid_llm_setting_object(client):
|
||||
response = client.post("/chats", {"name": f"sdk-chat-{uuid4().hex}", "llm_setting": "invalid"}).json()
|
||||
try:
|
||||
assert response["code"] != 0
|
||||
assert "`llm_setting` should be an object" in response["message"]
|
||||
finally:
|
||||
if (response.get("data") or {}).get("id"):
|
||||
client.delete_chats(ids=[response["data"]["id"]])
|
||||
|
||||
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.parametrize("valid", [True, False], ids=["configured", "unknown"])
|
||||
def test_llm_id(chat_factory, client, valid):
|
||||
llm_id = chat_factory().llm_id if valid else "sdk-no-such-model"
|
||||
if valid:
|
||||
assert llm_id, "Explicit llm_id test requires a configured default chat model"
|
||||
chat = chat_factory(llm_id=llm_id)
|
||||
assert chat.llm_id == llm_id
|
||||
assert client.get_chat(chat.id).llm_id == llm_id
|
||||
else:
|
||||
with pytest.raises(Exception, match="`llm_id` .* doesn't exist"):
|
||||
chat_factory(llm_id=llm_id)
|
||||
|
||||
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.parametrize(
|
||||
"prompt_config",
|
||||
[
|
||||
{"empty_response": "Hello World"},
|
||||
{"empty_response": ""},
|
||||
{"empty_response": "!@#$%^&*()"},
|
||||
{"empty_response": "中文测试"},
|
||||
{"empty_response": " "},
|
||||
{"prologue": "Hello World"},
|
||||
{"prologue": ""},
|
||||
{"prologue": "!@#$%^&*()"},
|
||||
{"prologue": "中文测试"},
|
||||
{"prologue": " "},
|
||||
{"quote": True},
|
||||
{"quote": False},
|
||||
{"system": "Hello World {knowledge}"},
|
||||
{"system": "{knowledge}"},
|
||||
{"system": "!@#$%^&*() {knowledge}"},
|
||||
{"system": "中文测试 {knowledge}"},
|
||||
{"system": "Hello World"},
|
||||
{"system": "Hello World", "parameters": []},
|
||||
],
|
||||
)
|
||||
def test_prompt_config(chat_factory, client, prompt_config):
|
||||
chat = chat_factory(prompt_config=prompt_config)
|
||||
persisted = client.get_chat(chat.id)
|
||||
for key, value in prompt_config.items():
|
||||
assert getattr(chat.prompt_config, key) == value
|
||||
assert getattr(persisted.prompt_config, key) == value
|
||||
|
||||
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.parametrize(
|
||||
"prompt_config",
|
||||
[
|
||||
{"empty_response": 123},
|
||||
{"empty_response": True},
|
||||
{"prologue": 123},
|
||||
{"prologue": True},
|
||||
{"system": 123},
|
||||
{"parameters": "invalid"},
|
||||
{"quote": "invalid"},
|
||||
],
|
||||
)
|
||||
@pytest.mark.skip(reason="Go validatePromptConfigParameters only checks duplicate keys; Create persists invalid prompt field types")
|
||||
def test_invalid_prompt_fields(chat_factory, prompt_config):
|
||||
with pytest.raises(Exception, match="prompt_config"):
|
||||
chat_factory(prompt_config=prompt_config)
|
||||
|
||||
|
||||
@pytest.mark.p3
|
||||
def test_create_rejects_invalid_prompt_config_object(client):
|
||||
response = client.post("/chats", {"name": f"sdk-chat-{uuid4().hex}", "prompt_config": "invalid"}).json()
|
||||
try:
|
||||
assert response["code"] != 0
|
||||
assert "`prompt_config` should be an object" in response["message"]
|
||||
finally:
|
||||
if (response.get("data") or {}).get("id"):
|
||||
client.delete_chats(ids=[response["data"]["id"]])
|
||||
|
||||
|
||||
@pytest.mark.p3
|
||||
def test_duplicate_prompt_parameters(chat_factory):
|
||||
with pytest.raises(Exception, match="`parameters` contains duplicate key: knowledge"):
|
||||
chat_factory(prompt_config={"parameters": [{"key": "knowledge"}, {"key": "knowledge"}]})
|
||||
|
||||
|
||||
@pytest.mark.p3
|
||||
def test_unparsed_dataset(chat_factory, dataset_factory):
|
||||
dataset = dataset_factory()
|
||||
with pytest.raises(Exception, match="doesn't own parsed file"):
|
||||
chat_factory(dataset_ids=[dataset.id])
|
||||
+126
-79
@@ -1,5 +1,5 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
# 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.
|
||||
@@ -13,104 +13,151 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from common import batch_create_chat_assistants
|
||||
from ragflow_sdk import RAGFlow
|
||||
|
||||
|
||||
@pytest.mark.p3
|
||||
def test_delete_chats_nonzero_response_raises(monkeypatch):
|
||||
class DummyResponse:
|
||||
def json(self):
|
||||
return {"code": 1, "message": "boom"}
|
||||
|
||||
client = RAGFlow("token", "http://sdk.invalid")
|
||||
monkeypatch.setattr(client, "delete", lambda *_args, **_kwargs: DummyResponse())
|
||||
with pytest.raises(Exception, match="boom"):
|
||||
client.delete_chats(ids=["chat-1"])
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
@pytest.mark.parametrize("error", ["Chat(invalid_id) not found.", "Duplicate chat ids: chat-1"])
|
||||
def test_delete_chats_partial_success_returns_no_value(monkeypatch, error):
|
||||
class DummyResponse:
|
||||
def json(self):
|
||||
return {"code": 0, "data": {"success_count": 1, "errors": [error]}}
|
||||
|
||||
client = RAGFlow("token", "http://sdk.invalid")
|
||||
calls = []
|
||||
|
||||
def delete(path, payload):
|
||||
calls.append((path, payload))
|
||||
return DummyResponse()
|
||||
|
||||
monkeypatch.setattr(client, "delete", delete)
|
||||
assert client.delete_chats(ids=["chat-1", "invalid_id"]) is None
|
||||
assert calls == [("/chats", {"ids": ["chat-1", "invalid_id"], "delete_all": False})]
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestChatAssistantsDelete:
|
||||
@pytest.mark.parametrize(
|
||||
"payload, expected_message, remaining",
|
||||
[
|
||||
pytest.param(None, "", 5, marks=pytest.mark.p3),
|
||||
pytest.param({"ids": []}, "", 5, marks=pytest.mark.p3),
|
||||
pytest.param({"ids": ["invalid_id"]}, "Chat(invalid_id) not found.", 5, marks=pytest.mark.p3),
|
||||
pytest.param({"ids": ["\n!?。;!?\"'"]}, """Chat(\n!?。;!?"\') not found.""", 5, marks=pytest.mark.p3),
|
||||
pytest.param(lambda r: {"ids": r[:1]}, "", 4, marks=pytest.mark.p3),
|
||||
pytest.param(lambda r: {"ids": r}, "", 0, marks=pytest.mark.p3),
|
||||
],
|
||||
)
|
||||
def test_basic_scenarios(self, client, add_chat_assistants_func, payload, expected_message, remaining):
|
||||
_, _, chat_assistants = add_chat_assistants_func
|
||||
if callable(payload):
|
||||
payload = payload([chat_assistant.id for chat_assistant in chat_assistants])
|
||||
|
||||
if expected_message:
|
||||
with pytest.raises(Exception) as exception_info:
|
||||
client.delete_chats(**payload)
|
||||
assert expected_message in str(exception_info.value)
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.parametrize("selection", ["omitted", "empty", "single", "bulk"])
|
||||
def test_basic_scenarios(self, client, chat_factory, selection):
|
||||
chats = [chat_factory() for _ in range(3)]
|
||||
ids = [chat.id for chat in chats]
|
||||
if selection == "omitted":
|
||||
client.delete_chats()
|
||||
deleted = set()
|
||||
elif selection == "empty":
|
||||
client.delete_chats(ids=[])
|
||||
deleted = set()
|
||||
elif selection == "single":
|
||||
client.delete_chats(ids=ids[:1])
|
||||
deleted = set(ids[:1])
|
||||
else:
|
||||
if payload is None:
|
||||
client.delete_chats(payload)
|
||||
else:
|
||||
client.delete_chats(**payload)
|
||||
client.delete_chats(ids=ids)
|
||||
deleted = set(ids)
|
||||
|
||||
assistants = client.list_chats()
|
||||
assert len(assistants) == remaining
|
||||
for chat in chats:
|
||||
assert bool(client.list_chats(id=chat.id)) == (chat.id not in deleted)
|
||||
|
||||
@pytest.mark.p3
|
||||
def test_delete_chats_nonzero_response_raises(self, client, monkeypatch):
|
||||
class _DummyResponse:
|
||||
def json(self):
|
||||
return {"code": 1, "message": "boom"}
|
||||
@pytest.mark.parametrize("invalid_id", ["invalid_id", "\n!?。;!?\"'"])
|
||||
def test_invalid_id(self, client, chat_factory, invalid_id):
|
||||
chat = chat_factory()
|
||||
with pytest.raises(Exception, match="not found"):
|
||||
client.delete_chats(ids=[invalid_id])
|
||||
assert [item.id for item in client.list_chats(id=chat.id)] == [chat.id]
|
||||
|
||||
monkeypatch.setattr(client, "delete", lambda *_args, **_kwargs: _DummyResponse())
|
||||
with pytest.raises(Exception) as exception_info:
|
||||
client.delete_chats(ids=["chat-1"])
|
||||
assert "boom" in str(exception_info.value), str(exception_info.value)
|
||||
@pytest.mark.p2
|
||||
def test_single_chat_endpoint(self, client, chat_factory):
|
||||
chat = chat_factory()
|
||||
response = client.delete(f"/chats/{chat.id}", None).json()
|
||||
assert response["code"] == 0, response
|
||||
assert response["data"] is True
|
||||
assert client.list_chats(id=chat.id) == []
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
[
|
||||
pytest.param(lambda r: {"ids": ["invalid_id"] + r}, marks=pytest.mark.p3),
|
||||
pytest.param(lambda r: {"ids": r[:1] + ["invalid_id"] + r[1:5]}, marks=pytest.mark.p3),
|
||||
pytest.param(lambda r: {"ids": r + ["invalid_id"]}, marks=pytest.mark.p3),
|
||||
],
|
||||
)
|
||||
def test_delete_partial_invalid_id(self, client, add_chat_assistants_func, payload):
|
||||
_, _, chat_assistants = add_chat_assistants_func
|
||||
payload = payload([chat_assistant.id for chat_assistant in chat_assistants])
|
||||
client.delete_chats(**payload)
|
||||
@pytest.mark.p2
|
||||
@pytest.mark.parametrize("selection", ["omitted", "none", "empty"])
|
||||
def test_delete_all_on_verified_empty_tenant(self, client, chat_factory, isolated_tenant, selection):
|
||||
chats = [chat_factory() for _ in range(2)]
|
||||
ids = {chat.id for chat in chats}
|
||||
# The session preflight verifies an empty dedicated tenant; refuse deletion if it changed.
|
||||
response = client.get("/chats", {"page_size": 0}).json()
|
||||
assert response["code"] == 0, response
|
||||
assert response["data"]["total"] == len(ids)
|
||||
assert {chat["id"] for chat in response["data"]["chats"]} == ids
|
||||
assert {chat.id for chat in client.list_chats(page_size=0)} == ids
|
||||
if selection == "omitted":
|
||||
client.delete_chats(delete_all=True)
|
||||
elif selection == "none":
|
||||
client.delete_chats(ids=None, delete_all=True)
|
||||
else:
|
||||
client.delete_chats(ids=[], delete_all=True)
|
||||
assert all(client.list_chats(id=chat_id) == [] for chat_id in ids)
|
||||
|
||||
assistants = client.list_chats()
|
||||
assert len(assistants) == 0
|
||||
@pytest.mark.p2
|
||||
def test_explicit_ids_take_precedence_over_delete_all(self, client, chat_factory):
|
||||
chats = [chat_factory() for _ in range(2)]
|
||||
client.delete_chats(ids=[chats[0].id], delete_all=True)
|
||||
assert client.list_chats(id=chats[0].id) == []
|
||||
assert [chat.id for chat in client.list_chats(id=chats[1].id)] == [chats[1].id]
|
||||
|
||||
@pytest.mark.p3
|
||||
def test_repeated_deletion(self, client, add_chat_assistants_func):
|
||||
_, _, chat_assistants = add_chat_assistants_func
|
||||
chat_ids = [chat.id for chat in chat_assistants]
|
||||
client.delete_chats(ids=chat_ids)
|
||||
|
||||
with pytest.raises(Exception) as exception_info:
|
||||
client.delete_chats(ids=chat_ids)
|
||||
assert "not found" in str(exception_info.value)
|
||||
@pytest.mark.parametrize("invalid_position", ["first", "middle", "last"])
|
||||
def test_delete_partial_invalid_id(self, client, chat_factory, invalid_position):
|
||||
chats = [chat_factory() for _ in range(3)]
|
||||
ids = [chat.id for chat in chats]
|
||||
position = {"first": 0, "middle": 1, "last": len(ids)}[invalid_position]
|
||||
ids.insert(position, "invalid_id")
|
||||
response = client.delete("/chats", {"ids": ids}).json()
|
||||
assert response["code"] == 0, response
|
||||
assert response["data"]["success_count"] == len(chats)
|
||||
assert "Chat(invalid_id) not found." in response["data"]["errors"]
|
||||
assert all(client.list_chats(id=chat.id) == [] for chat in chats)
|
||||
|
||||
@pytest.mark.p3
|
||||
def test_duplicate_deletion(self, client, add_chat_assistants_func):
|
||||
_, _, chat_assistants = add_chat_assistants_func
|
||||
chat_ids = [chat.id for chat in chat_assistants]
|
||||
client.delete_chats(ids=chat_ids + chat_ids)
|
||||
|
||||
assistants = client.list_chats()
|
||||
assert len(assistants) == 0
|
||||
def test_repeated_deletion(self, client, chat_factory):
|
||||
chat = chat_factory()
|
||||
client.delete_chats(ids=[chat.id])
|
||||
with pytest.raises(Exception, match="not found"):
|
||||
client.delete_chats(ids=[chat.id])
|
||||
assert client.list_chats(id=chat.id) == []
|
||||
|
||||
@pytest.mark.p3
|
||||
def test_concurrent_deletion(self, client):
|
||||
count = 100
|
||||
chat_ids = [client.create_chat(name=f"test_{i}").id for i in range(count)]
|
||||
def test_duplicate_deletion(self, client, chat_factory):
|
||||
chat = chat_factory()
|
||||
response = client.delete("/chats", {"ids": [chat.id, chat.id]}).json()
|
||||
assert response["code"] == 0, response
|
||||
assert response["data"]["success_count"] == 1
|
||||
assert f"Duplicate chat ids: {chat.id}" in response["data"]["errors"]
|
||||
assert client.list_chats(id=chat.id) == []
|
||||
|
||||
@pytest.mark.p3
|
||||
def test_concurrent_deletion(self, client, chat_factory):
|
||||
chats = [chat_factory(name=f"sdk-chat-delete-{uuid4().hex}") for _ in range(20)]
|
||||
with ThreadPoolExecutor(max_workers=5) as executor:
|
||||
futures = [executor.submit(client.delete_chats, ids=[chat_ids[i]]) for i in range(count)]
|
||||
responses = list(as_completed(futures))
|
||||
|
||||
assert len(responses) == count
|
||||
assert all(future.exception() is None for future in futures)
|
||||
futures = [executor.submit(client.delete_chats, ids=[chat.id]) for chat in chats]
|
||||
for future in as_completed(futures):
|
||||
assert future.result() is None
|
||||
assert all(client.list_chats(id=chat.id) == [] for chat in chats)
|
||||
|
||||
@pytest.mark.p3
|
||||
def test_delete_1k(self, client):
|
||||
chat_assistants = batch_create_chat_assistants(client, 1_000)
|
||||
client.delete_chats(ids=[chat_assistants.id for chat_assistants in chat_assistants])
|
||||
|
||||
assistants = client.list_chats()
|
||||
assert len(assistants) == 0
|
||||
def test_bulk_delete_spans_default_list_page(self, client, chat_factory):
|
||||
chats = [chat_factory() for _ in range(31)]
|
||||
client.delete_chats(ids=[chat.id for chat in chats])
|
||||
assert all(client.list_chats(id=chat.id) == [] for chat in chats)
|
||||
+129
-187
@@ -1,5 +1,5 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
# 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.
|
||||
@@ -13,220 +13,162 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from beartype.roar import BeartypeCallHintParamViolation
|
||||
from ragflow_sdk import RAGFlow
|
||||
from ragflow_sdk.modules.chat import Chat
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("add_chat_assistants")
|
||||
@pytest.fixture
|
||||
def chats(chat_factory):
|
||||
prefix = f"sdk-chat-list-{uuid4().hex}"
|
||||
return [chat_factory(name=f"{prefix}-{index}") for index in range(5)]
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
{"page": "a"},
|
||||
{"page": "3"},
|
||||
{"page_size": "a"},
|
||||
{"page_size": "1"},
|
||||
{"desc": None},
|
||||
{"desc": "true"},
|
||||
{"desc": "False"},
|
||||
{"desc": "unknown"},
|
||||
],
|
||||
)
|
||||
def test_invalid_sdk_parameter_types_before_get(monkeypatch, params):
|
||||
client = RAGFlow("token", "http://sdk.invalid")
|
||||
calls = []
|
||||
monkeypatch.setattr(client, "get", lambda *args, **kwargs: calls.append((args, kwargs)))
|
||||
|
||||
with pytest.raises(BeartypeCallHintParamViolation, match="not instance of"):
|
||||
client.list_chats(**params)
|
||||
assert calls == []
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestChatAssistantsList:
|
||||
@pytest.mark.p3
|
||||
def test_default(self, client):
|
||||
assistants = client.list_chats()
|
||||
assert len(assistants) == 5
|
||||
def test_default(self, client, chats):
|
||||
listed = client.list_chats()
|
||||
assert sorted(chat.id for chat in listed) == sorted(chat.id for chat in chats)
|
||||
assert all(isinstance(chat, Chat) for chat in listed)
|
||||
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.parametrize(
|
||||
"params, expected_page_size, expected_message",
|
||||
[
|
||||
({"page": 0, "page_size": 2}, 2, ""),
|
||||
({"page": 2, "page_size": 2}, 2, ""),
|
||||
({"page": 3, "page_size": 2}, 1, ""),
|
||||
({"page": "3", "page_size": 2}, 0, "not instance of"),
|
||||
pytest.param(
|
||||
{"page": -1, "page_size": 2},
|
||||
0,
|
||||
"1064",
|
||||
marks=pytest.mark.skip(reason="issues/5851"),
|
||||
),
|
||||
pytest.param(
|
||||
{"page": "a", "page_size": 2},
|
||||
0,
|
||||
"""ValueError("invalid literal for int() with base 10: \'a\'")""",
|
||||
marks=pytest.mark.skip(reason="issues/5851"),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_page(self, client, params, expected_page_size, expected_message):
|
||||
if expected_message:
|
||||
with pytest.raises(Exception) as exception_info:
|
||||
client.list_chats(**params)
|
||||
assert expected_message in str(exception_info.value)
|
||||
else:
|
||||
assistants = client.list_chats(**params)
|
||||
assert len(assistants) == expected_page_size
|
||||
@pytest.mark.parametrize("page, expected", [(1, (0, 1)), (2, (2, 3)), (3, (4,))])
|
||||
def test_page(self, client, chats, page, expected):
|
||||
prefix = chats[0].name.rsplit("-", 1)[0]
|
||||
listed = client.list_chats(page=page, page_size=2, orderby="name", desc=False, keywords=prefix)
|
||||
assert [chat.id for chat in listed] == [chats[index].id for index in expected]
|
||||
|
||||
@pytest.mark.p2
|
||||
@pytest.mark.parametrize("page", [0, -1])
|
||||
def test_nonpositive_page_is_unpaginated(self, client, chats, page):
|
||||
prefix = chats[0].name.rsplit("-", 1)[0]
|
||||
listed = client.list_chats(page=page, page_size=2, keywords=prefix)
|
||||
assert {chat.id for chat in listed} == {chat.id for chat in chats}
|
||||
|
||||
@pytest.mark.p1
|
||||
@pytest.mark.parametrize(
|
||||
"params, expected_page_size, expected_message",
|
||||
[
|
||||
({"page_size": 0}, 5, ""),
|
||||
({"page_size": 1}, 1, ""),
|
||||
({"page_size": 6}, 5, ""),
|
||||
({"page_size": "1"}, 0, "not instance of"),
|
||||
pytest.param(
|
||||
{"page_size": -1},
|
||||
0,
|
||||
"1064",
|
||||
marks=pytest.mark.skip(reason="issues/5851"),
|
||||
),
|
||||
pytest.param(
|
||||
{"page_size": "a"},
|
||||
0,
|
||||
"""ValueError("invalid literal for int() with base 10: \'a\'")""",
|
||||
marks=pytest.mark.skip(reason="issues/5851"),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_page_size(self, client, params, expected_page_size, expected_message):
|
||||
if expected_message:
|
||||
with pytest.raises(Exception) as exception_info:
|
||||
client.list_chats(**params)
|
||||
assert expected_message in str(exception_info.value)
|
||||
else:
|
||||
assistants = client.list_chats(**params)
|
||||
assert len(assistants) == expected_page_size
|
||||
@pytest.mark.parametrize("page_size", [1, 2, 6])
|
||||
def test_page_size(self, client, chats, page_size):
|
||||
prefix = chats[0].name.rsplit("-", 1)[0]
|
||||
listed = client.list_chats(page_size=page_size, orderby="name", desc=False, keywords=prefix)
|
||||
assert [chat.id for chat in listed] == [chat.id for chat in chats[:page_size]]
|
||||
|
||||
@pytest.mark.p2
|
||||
@pytest.mark.parametrize("page_size", [0, -1])
|
||||
def test_nonpositive_page_size_is_unpaginated(self, client, chats, page_size):
|
||||
prefix = chats[0].name.rsplit("-", 1)[0]
|
||||
listed = client.list_chats(page_size=page_size, keywords=prefix)
|
||||
assert {chat.id for chat in listed} == {chat.id for chat in chats}
|
||||
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.parametrize(
|
||||
"params, expected_message",
|
||||
[
|
||||
({"orderby": "create_time"}, ""),
|
||||
({"orderby": "update_time"}, ""),
|
||||
pytest.param({"orderby": "name", "desc": "False"}, "", marks=pytest.mark.skip(reason="issues/5851")),
|
||||
pytest.param({"orderby": "unknown"}, "orderby should be create_time or update_time", marks=pytest.mark.skip(reason="issues/5851")),
|
||||
],
|
||||
)
|
||||
def test_orderby(self, client, params, expected_message):
|
||||
if expected_message:
|
||||
with pytest.raises(Exception) as exception_info:
|
||||
client.list_chats(**params)
|
||||
assert expected_message in str(exception_info.value)
|
||||
else:
|
||||
client.list_chats(**params)
|
||||
@pytest.mark.parametrize("orderby", ["create_time", "update_time", "name"])
|
||||
@pytest.mark.parametrize("desc", [True, False])
|
||||
def test_orderby(self, client, chats, orderby, desc):
|
||||
prefix = chats[0].name.rsplit("-", 1)[0]
|
||||
listed = client.list_chats(orderby=orderby, desc=desc, keywords=prefix)
|
||||
assert {chat.id for chat in listed} == {chat.id for chat in chats}
|
||||
values = [getattr(chat, orderby) for chat in listed]
|
||||
assert values == sorted(values, reverse=desc)
|
||||
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.parametrize(
|
||||
"params, expected_message",
|
||||
[
|
||||
({"desc": None}, "not instance of"),
|
||||
({"desc": "true"}, "not instance of"),
|
||||
({"desc": "True"}, "not instance of"),
|
||||
({"desc": True}, ""),
|
||||
({"desc": "false"}, "not instance of"),
|
||||
({"desc": "False"}, "not instance of"),
|
||||
({"desc": False}, ""),
|
||||
({"desc": "False", "orderby": "update_time"}, "not instance of"),
|
||||
pytest.param(
|
||||
{"desc": "unknown"},
|
||||
"desc should be true or false",
|
||||
marks=pytest.mark.skip(reason="issues/5851"),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_desc(self, client, params, expected_message):
|
||||
if expected_message:
|
||||
with pytest.raises(Exception) as exception_info:
|
||||
client.list_chats(**params)
|
||||
assert expected_message in str(exception_info.value)
|
||||
else:
|
||||
client.list_chats(**params)
|
||||
def test_invalid_orderby(self, client):
|
||||
with pytest.raises(Exception, match="invalid orderby field: unknown"):
|
||||
client.list_chats(orderby="unknown")
|
||||
|
||||
@pytest.mark.p1
|
||||
@pytest.mark.parametrize(
|
||||
"params, expected_num, expected_message",
|
||||
[
|
||||
({"keywords": None}, 5, ""),
|
||||
({"keywords": ""}, 5, ""),
|
||||
({"keywords": "test_chat_assistant_1"}, 1, ""),
|
||||
({"keywords": "unknown"}, 0, ""),
|
||||
],
|
||||
)
|
||||
def test_keywords(self, client, params, expected_num, expected_message):
|
||||
if expected_message:
|
||||
with pytest.raises(Exception) as exception_info:
|
||||
client.list_chats(**params)
|
||||
assert expected_message in str(exception_info.value)
|
||||
@pytest.mark.parametrize("term, expected", [(None, 5), ("", 5), ("-1", 1), ("-missing", 0)])
|
||||
def test_keywords(self, client, chats, term, expected):
|
||||
prefix = chats[0].name.rsplit("-", 1)[0]
|
||||
keywords = f"{prefix}{term}" if term else term
|
||||
listed = client.list_chats(keywords=keywords)
|
||||
if term in (None, ""):
|
||||
assert sorted(chat.id for chat in listed) == sorted(chat.id for chat in chats)
|
||||
else:
|
||||
assistants = client.list_chats(**params)
|
||||
if params["keywords"] in [None, ""]:
|
||||
assert len(assistants) == expected_num
|
||||
else:
|
||||
assert len(assistants) == expected_num
|
||||
if expected_num:
|
||||
assert assistants[0].name == params["keywords"]
|
||||
assert len(listed) == expected
|
||||
assert {chat.id for chat in listed} <= {chat.id for chat in chats}
|
||||
if expected:
|
||||
assert listed[0].name == chats[1].name
|
||||
|
||||
@pytest.mark.p1
|
||||
def test_exact_id_and_name_filters(self, client, add_chat_assistants):
|
||||
_, _, chat_assistants = add_chat_assistants
|
||||
target = chat_assistants[1]
|
||||
def test_exact_id_and_name_filters(self, client, chats):
|
||||
target = chats[1]
|
||||
assert [chat.id for chat in client.list_chats(id=target.id)] == [target.id]
|
||||
assert [chat.id for chat in client.list_chats(name=target.name)] == [target.id]
|
||||
assert [chat.id for chat in client.list_chats(name=target.name, keywords="missing")] == [target.id]
|
||||
assert client.list_chats(id=uuid4().hex) == []
|
||||
|
||||
assistants = client.list_chats(id=target.id)
|
||||
assert len(assistants) == 1
|
||||
assert assistants[0].id == target.id
|
||||
|
||||
assistants = client.list_chats(name=target.name)
|
||||
assert len(assistants) == 1
|
||||
assert assistants[0].name == target.name
|
||||
|
||||
assistants = client.list_chats(name=target.name, keywords="unknown")
|
||||
assert len(assistants) == 1
|
||||
assert assistants[0].name == target.name
|
||||
@pytest.mark.p2
|
||||
@pytest.mark.parametrize("owner_format", ["string", "list", "other"])
|
||||
def test_owner_ids(self, client, chats, owner_format):
|
||||
owner_id = chats[0].tenant_id
|
||||
owners = [owner_id] if owner_format == "list" else owner_id
|
||||
if owner_format == "other":
|
||||
owners = uuid4().hex
|
||||
prefix = chats[0].name.rsplit("-", 1)[0]
|
||||
listed = client.list_chats(owner_ids=owners, keywords=prefix)
|
||||
assert {chat.id for chat in listed} == (set() if owner_format == "other" else {chat.id for chat in chats})
|
||||
|
||||
@pytest.mark.p1
|
||||
@pytest.mark.parametrize(
|
||||
"chat_assistant_id, expected_message",
|
||||
[
|
||||
(lambda r: r[0], ""),
|
||||
("unknown", "no authorization"),
|
||||
],
|
||||
)
|
||||
def test_get_chat(self, client, add_chat_assistants, chat_assistant_id, expected_message):
|
||||
_, _, chat_assistants = add_chat_assistants
|
||||
chat_id = chat_assistant_id([chat.id for chat in chat_assistants]) if callable(chat_assistant_id) else chat_assistant_id
|
||||
|
||||
if expected_message:
|
||||
with pytest.raises(Exception) as exception_info:
|
||||
client.get_chat(chat_id)
|
||||
assert expected_message in str(exception_info.value)
|
||||
else:
|
||||
assistant = client.get_chat(chat_id)
|
||||
assert assistant.id == chat_id
|
||||
def test_get_chat(self, client, chats):
|
||||
target = chats[0]
|
||||
result = client.get_chat(target.id)
|
||||
assert isinstance(result, Chat)
|
||||
assert result.id == target.id
|
||||
assert result.name == target.name
|
||||
with pytest.raises(Exception, match="Resource not found"):
|
||||
client.get_chat(uuid4().hex)
|
||||
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.parametrize(
|
||||
"chat_assistant_id, keywords, expected_num, expected_message",
|
||||
[
|
||||
(lambda r: r[0], "test_chat_assistant_0", 1, ""),
|
||||
(lambda r: r[0], "test_chat_assistant_1", 1, ""),
|
||||
(lambda r: r[0], "unknown", 0, ""),
|
||||
],
|
||||
)
|
||||
def test_get_and_keywords_are_separate_lookups(self, client, add_chat_assistants, chat_assistant_id, keywords, expected_num, expected_message):
|
||||
_, _, chat_assistants = add_chat_assistants
|
||||
chat_id = chat_assistant_id([chat.id for chat in chat_assistants]) if callable(chat_assistant_id) else chat_assistant_id
|
||||
|
||||
if expected_message:
|
||||
with pytest.raises(Exception) as exception_info:
|
||||
client.get_chat(chat_id)
|
||||
assert expected_message in str(exception_info.value)
|
||||
else:
|
||||
client.get_chat(chat_id)
|
||||
assistants = client.list_chats(keywords=keywords)
|
||||
assert len(assistants) == expected_num
|
||||
def test_get_and_keywords_are_separate_lookups(self, client, chats):
|
||||
assert client.get_chat(chats[0].id).id == chats[0].id
|
||||
assert [chat.id for chat in client.list_chats(keywords=chats[1].name)] == [chats[1].id]
|
||||
assert client.get_chat(chats[0].id).name == chats[0].name
|
||||
|
||||
@pytest.mark.p3
|
||||
def test_concurrent_list(self, client):
|
||||
count = 100
|
||||
def test_concurrent_list(self, client, chats):
|
||||
prefix = chats[0].name.rsplit("-", 1)[0]
|
||||
expected_ids = {chat.id for chat in chats}
|
||||
with ThreadPoolExecutor(max_workers=5) as executor:
|
||||
futures = [executor.submit(client.list_chats) for _ in range(count)]
|
||||
responses = list(as_completed(futures))
|
||||
assert len(responses) == count, responses
|
||||
futures = [executor.submit(client.list_chats, keywords=prefix) for _ in range(20)]
|
||||
for future in as_completed(futures):
|
||||
assert {chat.id for chat in future.result()} == expected_ids
|
||||
|
||||
@pytest.mark.p3
|
||||
def test_list_chats_after_deleting_associated_dataset(self, client, add_chat_assistants):
|
||||
dataset, _, _ = add_chat_assistants
|
||||
def test_list_chats_after_deleting_associated_dataset(self, client, chat_factory, dataset_factory):
|
||||
dataset = dataset_factory()
|
||||
document = dataset.upload_documents([{"display_name": "chat-association.txt", "blob": b"chat association"}])[0]
|
||||
document.add_chunk(content="chat association")
|
||||
chat = chat_factory(dataset_ids=[dataset.id])
|
||||
client.delete_datasets(ids=[dataset.id])
|
||||
|
||||
assistants = client.list_chats()
|
||||
assert len(assistants) == 5
|
||||
listed = client.list_chats(id=chat.id)
|
||||
assert [result.id for result in listed] == [chat.id]
|
||||
assert listed[0].dataset_ids == []
|
||||
+264
-171
@@ -1,5 +1,5 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
# 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.
|
||||
@@ -14,196 +14,289 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import base64
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from common import valid_chat_llm_id
|
||||
from configs import CHAT_ASSISTANT_NAME_LIMIT
|
||||
from utils import encode_avatar
|
||||
from utils.file_utils import create_image_file
|
||||
from PIL import Image
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
CHAT_NAME_MAX_BYTES = 255
|
||||
|
||||
|
||||
class TestChatAssistantUpdate:
|
||||
@pytest.mark.p3
|
||||
def test_update_rejects_non_dict(self, add_chat_assistants_func):
|
||||
_, _, chat_assistants = add_chat_assistants_func
|
||||
chat_assistant = chat_assistants[0]
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.parametrize(
|
||||
"name_case, expected_message",
|
||||
[
|
||||
("unique", None),
|
||||
("max_length", None),
|
||||
("too_long", "larger than 255"),
|
||||
("non_string", "chat name must be a string"),
|
||||
("empty", "`name` cannot be empty"),
|
||||
("duplicate", "duplicated chat name"),
|
||||
("duplicate_case_insensitive", "duplicated chat name"),
|
||||
],
|
||||
)
|
||||
def test_name(client, chat_factory, name_case, expected_message):
|
||||
chat = chat_factory()
|
||||
original_name = chat.name
|
||||
name = {
|
||||
"unique": f"renamed-{uuid4().hex}",
|
||||
"max_length": "a" * CHAT_NAME_MAX_BYTES,
|
||||
"too_long": "a" * (CHAT_NAME_MAX_BYTES + 1),
|
||||
"non_string": 1,
|
||||
"empty": "",
|
||||
}.get(name_case)
|
||||
if name_case.startswith("duplicate"):
|
||||
other = chat_factory()
|
||||
name = other.name.upper() if name_case.endswith("insensitive") else other.name
|
||||
|
||||
with pytest.raises(Exception) as exception_info:
|
||||
chat_assistant.update.__wrapped__(chat_assistant, "bad")
|
||||
assert "`update_message` must be a dict" in str(exception_info.value)
|
||||
if expected_message:
|
||||
with pytest.raises(Exception, match=expected_message):
|
||||
chat.update({"name": name})
|
||||
assert client.get_chat(chat.id).name == original_name
|
||||
else:
|
||||
chat.update({"name": name})
|
||||
assert client.get_chat(chat.id).name == name
|
||||
|
||||
@pytest.mark.p3
|
||||
def test_update_raises_on_nonzero_response(self, add_chat_assistants_func, monkeypatch):
|
||||
_, _, chat_assistants = add_chat_assistants_func
|
||||
chat_assistant = chat_assistants[0]
|
||||
|
||||
class _DummyResponse:
|
||||
def json(self):
|
||||
return {"code": 1, "message": "boom"}
|
||||
@pytest.mark.p3
|
||||
def test_partial_patch_preserves_unmentioned_fields(client, chat_factory):
|
||||
chat = chat_factory()
|
||||
before = client.get_chat(chat.id)
|
||||
chat.update({"top_n": before.top_n + 1})
|
||||
after = client.get_chat(chat.id)
|
||||
assert after.top_n == before.top_n + 1
|
||||
assert after.name == before.name
|
||||
assert after.icon == before.icon
|
||||
assert after.dataset_ids == before.dataset_ids
|
||||
assert after.llm_id == before.llm_id
|
||||
assert after.llm_setting.to_json() == before.llm_setting.to_json()
|
||||
assert after.prompt_config.to_json() == before.prompt_config.to_json()
|
||||
|
||||
monkeypatch.setattr(chat_assistant, "patch", lambda *_args, **_kwargs: _DummyResponse())
|
||||
|
||||
with pytest.raises(Exception) as exception_info:
|
||||
chat_assistant.update({"name": "error-case"})
|
||||
assert "boom" in str(exception_info.value)
|
||||
@pytest.mark.p3
|
||||
def test_empty_patch_preserves_settings(client, chat_factory):
|
||||
chat = chat_factory()
|
||||
before = client.get_chat(chat.id)
|
||||
chat.update({})
|
||||
after = client.get_chat(chat.id)
|
||||
assert after.name == before.name
|
||||
assert after.dataset_ids == before.dataset_ids
|
||||
assert after.llm_setting.to_json() == before.llm_setting.to_json()
|
||||
assert after.prompt_config.to_json() == before.prompt_config.to_json()
|
||||
|
||||
@pytest.mark.p3
|
||||
def test_update_uses_patch_for_partial_payload(self, add_chat_assistants_func, monkeypatch):
|
||||
_, _, chat_assistants = add_chat_assistants_func
|
||||
chat_assistant = chat_assistants[0]
|
||||
captured = {}
|
||||
|
||||
class _DummyResponse:
|
||||
def json(self):
|
||||
return {"code": 0, "message": "ok"}
|
||||
@pytest.mark.p3
|
||||
def test_icon(client, chat_factory, tmp_path):
|
||||
chat = chat_factory()
|
||||
image = tmp_path / "ragflow_test.png"
|
||||
Image.new("RGB", (1, 1), color="blue").save(image)
|
||||
icon = base64.b64encode(image.read_bytes()).decode("ascii")
|
||||
|
||||
def _patch(path, payload):
|
||||
captured["path"] = path
|
||||
captured["payload"] = payload
|
||||
return _DummyResponse()
|
||||
chat.update({"icon": icon})
|
||||
|
||||
monkeypatch.setattr(chat_assistant, "patch", _patch)
|
||||
monkeypatch.setattr(chat_assistant, "put", lambda *_args, **_kwargs: pytest.fail("update() should not use PUT"))
|
||||
updated = client.get_chat(chat.id)
|
||||
assert updated.icon == icon
|
||||
assert updated.name == chat.name
|
||||
|
||||
chat_assistant.update({"name": "renamed"})
|
||||
|
||||
assert captured["path"] == f"/chats/{chat_assistant.id}"
|
||||
assert captured["payload"] == {"name": "renamed"}
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.parametrize(
|
||||
"valid, expected_message",
|
||||
[
|
||||
(True, None),
|
||||
(False, "`llm_id` unknown doesn't exist"),
|
||||
],
|
||||
)
|
||||
def test_llm_id(client, chat_factory, valid, expected_message):
|
||||
configured_model = chat_factory().llm_id
|
||||
chat = chat_factory(llm_id="")
|
||||
llm_id = configured_model if valid else "unknown"
|
||||
if expected_message:
|
||||
with pytest.raises(Exception, match=expected_message):
|
||||
chat.update({"llm_id": llm_id})
|
||||
assert client.get_chat(chat.id).llm_id == chat.llm_id
|
||||
else:
|
||||
assert llm_id, "Explicit llm_id test requires a configured default chat model"
|
||||
assert client.get_chat(chat.id).llm_id != llm_id
|
||||
chat.update({"llm_id": llm_id})
|
||||
assert client.get_chat(chat.id).llm_id == llm_id
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload, expected_message",
|
||||
[
|
||||
pytest.param({"name": "valid_name"}, "", marks=pytest.mark.p3),
|
||||
pytest.param({"name": "a" * (CHAT_ASSISTANT_NAME_LIMIT + 1)}, "", marks=pytest.mark.skip(reason="issues/")),
|
||||
pytest.param({"name": 1}, "", marks=pytest.mark.skip(reason="issues/")),
|
||||
pytest.param({"name": ""}, "`name` cannot be empty", marks=pytest.mark.p3),
|
||||
pytest.param({"name": "test_chat_assistant_1"}, "duplicated chat name", marks=pytest.mark.p3),
|
||||
pytest.param({"name": "TEST_CHAT_ASSISTANT_1"}, "duplicated chat name", marks=pytest.mark.p3),
|
||||
],
|
||||
)
|
||||
def test_name(self, client, add_chat_assistants_func, payload, expected_message):
|
||||
_, _, chat_assistants = add_chat_assistants_func
|
||||
chat_assistant = chat_assistants[0]
|
||||
|
||||
if expected_message:
|
||||
with pytest.raises(Exception) as exception_info:
|
||||
chat_assistant.update(payload)
|
||||
assert expected_message in str(exception_info.value)
|
||||
else:
|
||||
chat_assistant.update(payload)
|
||||
updated_chat = client.get_chat(chat_assistant.id)
|
||||
assert updated_chat.name == payload["name"], str(updated_chat)
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.parametrize(
|
||||
"setting",
|
||||
[
|
||||
{"temperature": 0},
|
||||
{"temperature": 1},
|
||||
{"top_p": 0},
|
||||
{"top_p": 1},
|
||||
{"presence_penalty": 0},
|
||||
{"presence_penalty": 1},
|
||||
{"frequency_penalty": 0},
|
||||
{"frequency_penalty": 1},
|
||||
{"max_tokens": 0},
|
||||
{"max_tokens": 10},
|
||||
{"max_tokens": 1024},
|
||||
],
|
||||
)
|
||||
def test_llm_setting(client, chat_factory, setting):
|
||||
chat = chat_factory()
|
||||
before = client.get_chat(chat.id).llm_setting.to_json()
|
||||
|
||||
@pytest.mark.p3
|
||||
def test_icon(self, client, add_chat_assistants_func, tmp_path):
|
||||
dataset, _, chat_assistants = add_chat_assistants_func
|
||||
chat_assistant = chat_assistants[0]
|
||||
chat.update({"llm_setting": setting})
|
||||
|
||||
fn = create_image_file(tmp_path / "ragflow_test.png")
|
||||
payload = {"name": "icon_test", "icon": encode_avatar(fn), "dataset_ids": [dataset.id]}
|
||||
assert client.get_chat(chat.id).llm_setting.to_json() == {**before, **setting}
|
||||
|
||||
chat_assistant.update(payload)
|
||||
updated_chat = client.get_chat(chat_assistant.id)
|
||||
assert updated_chat.name == payload["name"], str(updated_chat)
|
||||
assert updated_chat.icon is not None, str(updated_chat)
|
||||
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.parametrize(
|
||||
"llm_setting, expected_message",
|
||||
[
|
||||
({"model_name": valid_chat_llm_id}, ""),
|
||||
({"model_name": "unknown"}, "`llm_id` unknown doesn't exist"),
|
||||
({"temperature": 0}, ""),
|
||||
({"temperature": 1}, ""),
|
||||
pytest.param({"temperature": -1}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"temperature": 10}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"temperature": "a"}, "", marks=pytest.mark.skip),
|
||||
({"top_p": 0}, ""),
|
||||
({"top_p": 1}, ""),
|
||||
pytest.param({"top_p": -1}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"top_p": 10}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"top_p": "a"}, "", marks=pytest.mark.skip),
|
||||
({"presence_penalty": 0}, ""),
|
||||
({"presence_penalty": 1}, ""),
|
||||
pytest.param({"presence_penalty": -1}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"presence_penalty": 10}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"presence_penalty": "a"}, "", marks=pytest.mark.skip),
|
||||
({"frequency_penalty": 0}, ""),
|
||||
({"frequency_penalty": 1}, ""),
|
||||
pytest.param({"frequency_penalty": -1}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"frequency_penalty": 10}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"frequency_penalty": "a"}, "", marks=pytest.mark.skip),
|
||||
({"max_token": 0}, ""),
|
||||
({"max_token": 1024}, ""),
|
||||
pytest.param({"max_token": -1}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"max_token": 10}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"max_token": "a"}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"unknown": "unknown"}, "", marks=pytest.mark.skip),
|
||||
],
|
||||
)
|
||||
def test_llm_setting(self, client, add_chat_assistants_func, llm_setting, expected_message):
|
||||
dataset, _, chat_assistants = add_chat_assistants_func
|
||||
chat_assistant = chat_assistants[0]
|
||||
llm_setting = dict(llm_setting)
|
||||
llm_id = llm_setting.pop("model_name", None)
|
||||
if callable(llm_id):
|
||||
llm_id = llm_id(client)
|
||||
payload = {"name": "llm_test", "dataset_ids": [dataset.id], "llm_setting": llm_setting}
|
||||
if llm_id is not None:
|
||||
payload["llm_id"] = llm_id
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.parametrize(
|
||||
"field",
|
||||
[
|
||||
pytest.param(
|
||||
field,
|
||||
marks=pytest.mark.skip(reason="Go PATCH persists string-valued numeric llm_setting fields (internal/service/chat.go:updateChatREST)"),
|
||||
)
|
||||
for field in (
|
||||
"temperature",
|
||||
"top_p",
|
||||
"presence_penalty",
|
||||
"frequency_penalty",
|
||||
"max_tokens",
|
||||
)
|
||||
],
|
||||
)
|
||||
def test_llm_setting_rejects_non_numeric(client, chat_factory, field):
|
||||
chat = chat_factory()
|
||||
before = client.get_chat(chat.id).llm_setting.to_json()
|
||||
with pytest.raises(Exception, match=field):
|
||||
chat.update({"llm_setting": {field: "invalid"}})
|
||||
assert client.get_chat(chat.id).llm_setting.to_json() == before
|
||||
|
||||
if expected_message:
|
||||
with pytest.raises(Exception) as exception_info:
|
||||
chat_assistant.update(payload)
|
||||
assert expected_message in str(exception_info.value)
|
||||
else:
|
||||
chat_assistant.update(payload)
|
||||
updated_chat = client.get_chat(chat_assistant.id)
|
||||
if llm_id:
|
||||
assert updated_chat.llm_id == llm_id, str(updated_chat)
|
||||
for k, v in llm_setting.items():
|
||||
assert getattr(updated_chat.llm_setting, k) == v, str(updated_chat)
|
||||
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.parametrize(
|
||||
"prompt_config, expected_message",
|
||||
[
|
||||
({"empty_response": "Hello World"}, ""),
|
||||
({"empty_response": ""}, ""),
|
||||
({"empty_response": "!@#$%^&*()"}, ""),
|
||||
({"empty_response": "中文测试"}, ""),
|
||||
pytest.param({"empty_response": 123}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"empty_response": True}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"empty_response": " "}, "", marks=pytest.mark.skip),
|
||||
({"prologue": "Hello World"}, ""),
|
||||
({"prologue": ""}, ""),
|
||||
({"prologue": "!@#$%^&*()"}, ""),
|
||||
({"prologue": "中文测试"}, ""),
|
||||
pytest.param({"prologue": 123}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"prologue": True}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"prologue": " "}, "", marks=pytest.mark.skip),
|
||||
({"quote": True}, ""),
|
||||
({"quote": False}, ""),
|
||||
({"system": "Hello World {knowledge}"}, ""),
|
||||
({"system": "{knowledge}"}, ""),
|
||||
({"system": "!@#$%^&*() {knowledge}"}, ""),
|
||||
({"system": "中文测试 {knowledge}"}, ""),
|
||||
({"system": "Hello World"}, ""),
|
||||
({"system": "Hello World", "parameters": []}, ""),
|
||||
pytest.param({"system": 123}, "", marks=pytest.mark.skip),
|
||||
pytest.param({"unknown": "unknown"}, "", marks=pytest.mark.skip),
|
||||
],
|
||||
)
|
||||
def test_prompt_config(self, client, add_chat_assistants_func, prompt_config, expected_message):
|
||||
dataset, _, chat_assistants = add_chat_assistants_func
|
||||
chat_assistant = chat_assistants[0]
|
||||
payload = {"name": "prompt_test", "prompt_config": prompt_config, "dataset_ids": [dataset.id]}
|
||||
@pytest.mark.p3
|
||||
def test_dataset_association_updates(client, chat_factory, dataset_factory):
|
||||
chat = chat_factory()
|
||||
first, second = dataset_factory(), dataset_factory()
|
||||
for dataset in (first, second):
|
||||
document = dataset.upload_documents([{"display_name": "chat-association.txt", "blob": b"chat association"}])[0]
|
||||
document.add_chunk(content="chat association")
|
||||
|
||||
if expected_message:
|
||||
with pytest.raises(Exception) as exception_info:
|
||||
chat_assistant.update(payload)
|
||||
assert expected_message in str(exception_info.value)
|
||||
else:
|
||||
chat_assistant.update(payload)
|
||||
updated_chat = client.get_chat(chat_assistant.id)
|
||||
for k, v in prompt_config.items():
|
||||
assert getattr(updated_chat.prompt_config, k) == v, str(updated_chat)
|
||||
chat.update({"dataset_ids": [first.id]})
|
||||
assert client.get_chat(chat.id).dataset_ids == [first.id]
|
||||
|
||||
chat.update({"dataset_ids": [second.id]})
|
||||
assert client.get_chat(chat.id).dataset_ids == [second.id]
|
||||
|
||||
chat.update({"dataset_ids": []})
|
||||
assert client.get_chat(chat.id).dataset_ids == []
|
||||
|
||||
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.parametrize(
|
||||
"field, value, expected_message",
|
||||
[
|
||||
("llm_setting", "invalid", "`llm_setting` should be an object"),
|
||||
("prompt_config", "invalid", "`prompt_config` should be an object"),
|
||||
("dataset_ids", "invalid", "`dataset_ids` should be a list"),
|
||||
],
|
||||
)
|
||||
def test_rejects_non_object_settings_and_dataset_ids(client, chat_factory, field, value, expected_message):
|
||||
chat = chat_factory()
|
||||
original = client.get_chat(chat.id)
|
||||
before = original.dataset_ids if field == "dataset_ids" else getattr(original, field).to_json()
|
||||
with pytest.raises(Exception, match=expected_message):
|
||||
chat.update({field: value})
|
||||
persisted = client.get_chat(chat.id)
|
||||
after = persisted.dataset_ids if field == "dataset_ids" else getattr(persisted, field).to_json()
|
||||
assert after == before
|
||||
|
||||
|
||||
@pytest.mark.p3
|
||||
def test_rejects_unparsed_dataset(client, chat_factory, dataset_factory):
|
||||
chat = chat_factory()
|
||||
dataset = dataset_factory()
|
||||
with pytest.raises(Exception, match="doesn't own parsed file"):
|
||||
chat.update({"dataset_ids": [dataset.id]})
|
||||
assert client.get_chat(chat.id).dataset_ids == []
|
||||
|
||||
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.parametrize(
|
||||
"prompt_config",
|
||||
[
|
||||
{"empty_response": "Hello World"},
|
||||
{"empty_response": ""},
|
||||
{"empty_response": "!@#$%^&*()"},
|
||||
{"empty_response": "中文测试"},
|
||||
{"empty_response": " "},
|
||||
{"prologue": "Hello World"},
|
||||
{"prologue": ""},
|
||||
{"prologue": "!@#$%^&*()"},
|
||||
{"prologue": "中文测试"},
|
||||
{"prologue": " "},
|
||||
{"quote": True},
|
||||
{"quote": False},
|
||||
{"system": "Hello World {knowledge}"},
|
||||
{"system": "{knowledge}"},
|
||||
{"system": "!@#$%^&*() {knowledge}"},
|
||||
{"system": "中文测试 {knowledge}"},
|
||||
{"system": "Hello World"},
|
||||
{"system": "Hello World", "parameters": []},
|
||||
],
|
||||
)
|
||||
def test_prompt_config(client, chat_factory, prompt_config):
|
||||
chat = chat_factory()
|
||||
before = client.get_chat(chat.id).prompt_config.to_json()
|
||||
|
||||
chat.update({"prompt_config": prompt_config})
|
||||
|
||||
assert client.get_chat(chat.id).prompt_config.to_json() == {**before, **prompt_config}
|
||||
|
||||
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.parametrize(
|
||||
"field, value",
|
||||
[
|
||||
pytest.param(
|
||||
field,
|
||||
value,
|
||||
id=f"{field}-{value}",
|
||||
marks=pytest.mark.skip(reason="Go PATCH merges prompt_config without validating documented string fields (internal/service/chat.go:updateChatREST)"),
|
||||
)
|
||||
for field, values in (
|
||||
("empty_response", (123, True)),
|
||||
("prologue", (123, True)),
|
||||
("system", (123,)),
|
||||
)
|
||||
for value in values
|
||||
],
|
||||
)
|
||||
def test_prompt_config_rejects_invalid_text(client, chat_factory, field, value):
|
||||
chat = chat_factory()
|
||||
before = client.get_chat(chat.id).prompt_config.to_json()
|
||||
with pytest.raises(Exception, match=field):
|
||||
chat.update({"prompt_config": {field: value}})
|
||||
assert client.get_chat(chat.id).prompt_config.to_json() == before
|
||||
|
||||
|
||||
@pytest.mark.p3
|
||||
@pytest.mark.skip(reason="Go PATCH replaces nested prompt_config.reference_metadata instead of deep-merging it (internal/service/chat.go:mergeJSONMap)")
|
||||
def test_prompt_config_preserves_nested_reference_metadata(client, chat_factory):
|
||||
chat = chat_factory(prompt_config={"reference_metadata": {"include": False, "fields": ["source"]}})
|
||||
before = client.get_chat(chat.id).prompt_config.reference_metadata.to_json()
|
||||
assert before == {"include": False, "fields": ["source"]}
|
||||
|
||||
chat.update({"prompt_config": {"reference_metadata": {"include": True}}})
|
||||
after = client.get_chat(chat.id).prompt_config.reference_metadata.to_json()
|
||||
assert after == {"include": True, "fields": ["source"]}
|
||||
|
||||
|
||||
@pytest.mark.p3
|
||||
def test_prompt_config_rejects_duplicate_parameter_keys(client, chat_factory):
|
||||
chat = chat_factory()
|
||||
before = client.get_chat(chat.id).prompt_config.to_json()
|
||||
with pytest.raises(Exception, match="`parameters` contains duplicate key: knowledge"):
|
||||
chat.update({"prompt_config": {"parameters": [{"key": "knowledge"}, {"key": "knowledge"}]}})
|
||||
assert client.get_chat(chat.id).prompt_config.to_json() == before
|
||||
Reference in new issue
Block a user