test(sdk): audit chat assistant tests (#21654)

This commit is contained in:
Sbaaoui Idriss authored and GitHub committed 2026-10-29 14:11:36 +08:00
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
@@ -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"}
@@ -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])
@@ -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)
@@ -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 == []
@@ -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