diff --git a/api/apps/chunk_app.py b/api/apps/chunk_app.py index 3b1c153aa..4d806eb32 100644 --- a/api/apps/chunk_app.py +++ b/api/apps/chunk_app.py @@ -240,6 +240,16 @@ async def rm(): req = await get_request_json() try: def _rm_sync(): + deleted_chunk_ids = req["chunk_ids"] + if isinstance(deleted_chunk_ids, list): + unique_chunk_ids = list(dict.fromkeys(deleted_chunk_ids)) + has_ids = len(unique_chunk_ids) > 0 + else: + unique_chunk_ids = [deleted_chunk_ids] + has_ids = deleted_chunk_ids not in (None, "") + if not has_ids: + return get_json_result(data=True) + e, doc = DocumentService.get_by_id(req["doc_id"]) if not e: return get_data_error_result(message="Document not found!") @@ -250,13 +260,6 @@ async def rm(): doc.kb_id) except Exception: return get_data_error_result(message="Chunk deleting failure") - deleted_chunk_ids = req["chunk_ids"] - if isinstance(deleted_chunk_ids, list): - unique_chunk_ids = list(dict.fromkeys(deleted_chunk_ids)) - has_ids = len(unique_chunk_ids) > 0 - else: - unique_chunk_ids = [deleted_chunk_ids] - has_ids = deleted_chunk_ids not in (None, "") if has_ids and deleted_count == 0: return get_data_error_result(message="Index updating failure") if deleted_count > 0 and deleted_count < len(unique_chunk_ids): diff --git a/api/apps/sdk/chat.py b/api/apps/sdk/chat.py index 786d1a733..e1142de25 100644 --- a/api/apps/sdk/chat.py +++ b/api/apps/sdk/chat.py @@ -235,16 +235,13 @@ async def delete_chats(tenant_id): success_count = 0 req = await get_request_json() if not req: - ids = None - else: - ids = req.get("ids") + return get_result() + + ids = req.get("ids") if not ids: - id_list = [] - dias = DialogService.query(tenant_id=tenant_id, status=StatusEnum.VALID.value) - for dia in dias: - id_list.append(dia.id) - else: - id_list = ids + return get_result() + + id_list = ids unique_id_list, duplicate_messages = check_duplicate_ids(id_list, "assistant") diff --git a/api/apps/sdk/dataset.py b/api/apps/sdk/dataset.py index 6538d3a33..caa75ec02 100644 --- a/api/apps/sdk/dataset.py +++ b/api/apps/sdk/dataset.py @@ -202,10 +202,8 @@ async def delete(tenant_id): items: type: string description: | - Specifies the datasets to delete: - - If `null`, all datasets will be deleted. - - If an array of IDs, only the specified datasets will be deleted. - - If an empty array, no datasets will be deleted. + List of dataset IDs to delete. + If `null` or an empty array is provided, no datasets will be deleted. responses: 200: description: Successful operation. @@ -218,22 +216,19 @@ async def delete(tenant_id): try: kb_id_instance_pairs = [] - if req["ids"] is None: - kbs = KnowledgebaseService.query(tenant_id=tenant_id) - for kb in kbs: - kb_id_instance_pairs.append((kb.id, kb)) + if req["ids"] is None or len(req["ids"]) == 0: + return get_result() - else: - error_kb_ids = [] - for kb_id in req["ids"]: - kb = KnowledgebaseService.get_or_none(id=kb_id, tenant_id=tenant_id) - if kb is None: - error_kb_ids.append(kb_id) - continue - kb_id_instance_pairs.append((kb_id, kb)) - if len(error_kb_ids) > 0: - return get_error_permission_result( - message=f"""User '{tenant_id}' lacks permission for datasets: '{", ".join(error_kb_ids)}'""") + error_kb_ids = [] + for kb_id in req["ids"]: + kb = KnowledgebaseService.get_or_none(id=kb_id, tenant_id=tenant_id) + if kb is None: + error_kb_ids.append(kb_id) + continue + kb_id_instance_pairs.append((kb_id, kb)) + if len(error_kb_ids) > 0: + return get_error_permission_result( + message=f"""User '{tenant_id}' lacks permission for datasets: '{", ".join(error_kb_ids)}'""") errors = [] success_count = 0 @@ -811,4 +806,4 @@ def trace_raptor(tenant_id,dataset_id): if not ok: return get_error_data_result(message="RAPTOR Task Not Found or Error Occurred") - return get_result(data=task.to_dict()) \ No newline at end of file + return get_result(data=task.to_dict()) diff --git a/api/apps/sdk/doc.py b/api/apps/sdk/doc.py index 7c1b3aa86..80d0a2e1e 100644 --- a/api/apps/sdk/doc.py +++ b/api/apps/sdk/doc.py @@ -727,7 +727,9 @@ async def delete(tenant_id, dataset_id): type: array items: type: string - description: List of document IDs to delete. + description: | + List of document IDs to delete. + If omitted, `null`, or an empty array is provided, no documents will be deleted. - in: header name: Authorization type: string @@ -743,16 +745,13 @@ async def delete(tenant_id, dataset_id): return get_error_data_result(message=f"You don't own the dataset {dataset_id}. ") req = await get_request_json() if not req: - doc_ids = None - else: - doc_ids = req.get("ids") + return get_result() + + doc_ids = req.get("ids") if not doc_ids: - doc_list = [] - docs = DocumentService.query(kb_id=dataset_id) - for doc in docs: - doc_list.append(doc.id) - else: - doc_list = doc_ids + return get_result() + + doc_list = doc_ids unique_doc_ids, duplicate_messages = check_duplicate_ids(doc_list, "document") doc_list = unique_doc_ids @@ -1318,7 +1317,9 @@ async def rm_chunk(tenant_id, dataset_id, document_id): type: array items: type: string - description: List of chunk IDs to remove. + description: | + List of chunk IDs to remove. + If omitted, `null`, or an empty array is provided, no chunks will be deleted. - in: header name: Authorization type: string @@ -1336,17 +1337,20 @@ async def rm_chunk(tenant_id, dataset_id, document_id): if not docs: raise LookupError(f"Can't find the document with ID {document_id}!") req = await get_request_json() + if not req: + return get_result() + + chunk_ids = req.get("chunk_ids") + if not chunk_ids: + return get_result() + condition = {"doc_id": document_id} - if "chunk_ids" in req: - unique_chunk_ids, duplicate_messages = check_duplicate_ids(req["chunk_ids"], "chunk") - condition["id"] = unique_chunk_ids - else: - unique_chunk_ids = [] - duplicate_messages = [] + unique_chunk_ids, duplicate_messages = check_duplicate_ids(chunk_ids, "chunk") + condition["id"] = unique_chunk_ids chunk_number = settings.docStoreConn.delete(condition, search.index_name(tenant_id), dataset_id) if chunk_number != 0: DocumentService.decrement_chunk_num(document_id, dataset_id, 1, chunk_number, 0) - if "chunk_ids" in req and chunk_number != len(unique_chunk_ids): + if chunk_number != len(unique_chunk_ids): if len(unique_chunk_ids) == 0: return get_result(message=f"deleted {chunk_number} chunks") return get_error_data_result(message=f"rm_chunk deleted chunks {chunk_number}, expect {len(unique_chunk_ids)}") diff --git a/api/apps/sdk/session.py b/api/apps/sdk/session.py index 9553baf1a..10439564d 100644 --- a/api/apps/sdk/session.py +++ b/api/apps/sdk/session.py @@ -739,18 +739,14 @@ async def delete(tenant_id, chat_id): errors = [] success_count = 0 req = await get_request_json() - convs = ConversationService.query(dialog_id=chat_id) if not req: - ids = None - else: - ids = req.get("ids") + return get_result() + ids = req.get("ids") if not ids: - conv_list = [] - for conv in convs: - conv_list.append(conv.id) - else: - conv_list = ids + return get_result() + + conv_list = ids unique_conv_ids, duplicate_messages = check_duplicate_ids(conv_list, "session") conv_list = unique_conv_ids @@ -791,21 +787,14 @@ async def delete_agent_session(tenant_id, agent_id): if not cvs: return get_error_data_result(f"You don't own the agent {agent_id}") - convs = API4ConversationService.query(dialog_id=agent_id) - if not convs: - return get_error_data_result(f"Agent {agent_id} has no sessions") - if not req: - ids = None - else: - ids = req.get("ids") + return get_result() + ids = req.get("ids") if not ids: - conv_list = [] - for conv in convs: - conv_list.append(conv.id) - else: - conv_list = ids + return get_result() + + conv_list = ids unique_conv_ids, duplicate_messages = check_duplicate_ids(conv_list, "session") conv_list = unique_conv_ids diff --git a/docs/references/http_api_reference.md b/docs/references/http_api_reference.md index 8a45106a3..a6ccf63fa 100644 --- a/docs/references/http_api_reference.md +++ b/docs/references/http_api_reference.md @@ -676,9 +676,8 @@ curl --request DELETE \ - `"ids"`: (*Body parameter*), `list[string]` or `null`, *Required* Specifies the datasets to delete: - - If `null`, all datasets will be deleted. - - If an array of IDs, only the specified datasets will be deleted. - - If an empty array, no datasets will be deleted. + - If omitted, or set to `null` or an empty array, no datasets are deleted. + - If an array of IDs is provided, only the datasets matching those IDs are deleted. #### Response @@ -1764,7 +1763,9 @@ curl --request DELETE \ - `dataset_id`: (*Path parameter*) The associated dataset ID. - `"ids"`: (*Body parameter*), `list[string]` - The IDs of the documents to delete. If it is not specified, all documents in the specified dataset will be deleted. + The IDs of the documents to delete. + - If omitted, or set to `null` or an empty array, no documents are deleted. + - If an array of IDs is provided, only the documents matching those IDs are deleted. #### Response @@ -2124,7 +2125,9 @@ curl --request DELETE \ - `document_ids`: (*Path parameter*) The associated document ID. - `"chunk_ids"`: (*Body parameter*), `list[string]` - The IDs of the chunks to delete. If it is not specified, all chunks of the specified document will be deleted. + The IDs of the chunks to delete. + - If omitted, or set to `null` or an empty array, no chunks are deleted. + - If an array of IDs is provided, only the chunks matching those IDs are deleted. #### Response @@ -2796,7 +2799,9 @@ curl --request DELETE \ ##### Request parameters - `"ids"`: (*Body parameter*), `list[string]` - The IDs of the chat assistants to delete. If it is not specified, all chat assistants in the system will be deleted. + The IDs of the chat assistants to delete. + - If omitted, or set to `null` or an empty array, no chat assistants are deleted. + - If an array of IDs is provided, only the chat assistants matching those IDs are deleted. #### Response @@ -3174,7 +3179,9 @@ curl --request DELETE \ - `chat_id`: (*Path parameter*) The ID of the associated chat assistant. - `"ids"`: (*Body Parameter*), `list[string]` - The IDs of the sessions to delete. If it is not specified, all sessions associated with the specified chat assistant will be deleted. + The IDs of the sessions to delete. + - If omitted, or set to `null` or an empty array, no sessions are deleted. + - If an array of IDs is provided, only the sessions matching those IDs are deleted. #### Response @@ -4538,7 +4545,9 @@ curl --request DELETE \ - `agent_id`: (*Path parameter*) The ID of the associated agent. - `"ids"`: (*Body Parameter*), `list[string]` - The IDs of the sessions to delete. If it is not specified, all sessions associated with the specified agent will be deleted. + The IDs of the sessions to delete. + - If omitted, or set to `null` or an empty array, no sessions are deleted. + - If an array of IDs is provided, only the sessions matching those IDs are deleted. #### Response diff --git a/docs/references/python_api_reference.md b/docs/references/python_api_reference.md index 80a8666e9..430e58a0f 100644 --- a/docs/references/python_api_reference.md +++ b/docs/references/python_api_reference.md @@ -240,9 +240,9 @@ Deletes datasets by ID. ##### ids: `list[str]` or `None`, *Required* The IDs of the datasets to delete. Defaults to `None`. - - If `None`, all datasets will be deleted. - - If an array of IDs, only the specified datasets will be deleted. - - If an empty array, no datasets will be deleted. + +- If omitted, or set to `null` or an empty array, no datasets are deleted. +- If an array of IDs is provided, only the datasets matching those IDs are deleted. #### Returns @@ -661,9 +661,12 @@ Deletes documents by ID. #### Parameters -##### ids: `list[list]` +##### ids: `list[str]` or `None` -The IDs of the documents to delete. Defaults to `None`. If it is not specified, all documents in the dataset will be deleted. +The IDs of the documents to delete. Defaults to `None`. + +- If omitted, or set to `null` or an empty array, no documents are deleted. +- If an array of IDs is provided, only the documents matching those IDs are deleted. #### Returns @@ -931,7 +934,10 @@ Deletes chunks by ID. ##### chunk_ids: `list[str]` -The IDs of the chunks to delete. Defaults to `None`. If it is not specified, all chunks of the current document will be deleted. +The IDs of the chunks to delete. Defaults to `None`. + +- If omitted, or set to `null` or an empty array, no chunks are deleted. +- If an array of IDs is provided, only the chunks matching those IDs are deleted. #### Returns @@ -1234,7 +1240,10 @@ Deletes chat assistants by ID. ##### ids: `list[str]` -The IDs of the chat assistants to delete. Defaults to `None`. If it is empty or not specified, all chat assistants in the system will be deleted. +The IDs of the chat assistants to delete. Defaults to `None`. + +- If omitted, or set to `null` or an empty array, no chat assistants are deleted. +- If an array of IDs is provided, only the chat assistants matching those IDs are deleted. #### Returns @@ -1463,7 +1472,10 @@ Deletes sessions of the current chat assistant by ID. ##### ids: `list[str]` -The IDs of the sessions to delete. Defaults to `None`. If it is not specified, all sessions associated with the current chat assistant will be deleted. +The IDs of the sessions to delete. Defaults to `None`. + +- If omitted, or set to `null` or an empty array, no sessions are deleted. +- If an array of IDs is provided, only the sessions matching those IDs are deleted. #### Returns @@ -1781,7 +1793,10 @@ Deletes sessions of an agent by ID. ##### ids: `list[str]` -The IDs of the sessions to delete. Defaults to `None`. If it is not specified, all sessions associated with the agent will be deleted. +The IDs of the sessions to delete. Defaults to `None`. + +- If omitted, or set to `null` or an empty array, no sessions are deleted. +- If an array of IDs is provided, only the sessions matching those IDs are deleted. #### Returns diff --git a/test/testcases/test_http_api/common.py b/test/testcases/test_http_api/common.py index 4e27f74a1..d6334543d 100644 --- a/test/testcases/test_http_api/common.py +++ b/test/testcases/test_http_api/common.py @@ -58,6 +58,23 @@ def delete_datasets(auth, payload=None, *, headers=HEADERS, data=None): return res.json() +def delete_all_datasets(auth, *, page_size=1000): + # Dataset DELETE now treats null/empty ids as a no-op, so cleanup must enumerate explicit ids. + page = 1 + dataset_ids = [] + while True: + res = list_datasets(auth, {"page": page, "page_size": page_size}) + data = res.get("data") or [] + dataset_ids.extend(dataset["id"] for dataset in data) + if len(data) < page_size: + break + page += 1 + + if not dataset_ids: + return {"code": 0, "message": ""} + return delete_datasets(auth, {"ids": dataset_ids}) + + def batch_create_datasets(auth, num): ids = [] for i in range(num): @@ -127,6 +144,23 @@ def delete_documents(auth, dataset_id, payload=None): return res.json() +def delete_all_documents(auth, dataset_id, *, page_size=1000): + # Document DELETE now treats missing/null/empty ids as a no-op, so cleanup must enumerate explicit ids. + page = 1 + document_ids = [] + while True: + res = list_documents(auth, dataset_id, {"page": page, "page_size": page_size}) + docs = (res.get("data") or {}).get("docs") or [] + document_ids.extend(doc["id"] for doc in docs) + if len(docs) < page_size: + break + page += 1 + + if not document_ids: + return {"code": 0, "message": ""} + return delete_documents(auth, dataset_id, {"ids": document_ids}) + + def parse_documents(auth, dataset_id, payload=None): url = f"{HOST_ADDRESS}{FILE_CHUNK_API_URL}".format(dataset_id=dataset_id) res = requests.post(url=url, headers=HEADERS, auth=auth, json=payload) @@ -176,6 +210,23 @@ def delete_chunks(auth, dataset_id, document_id, payload=None): return res.json() +def delete_all_chunks(auth, dataset_id, document_id, *, page_size=1000): + # Chunk DELETE now treats missing/null/empty ids as a no-op, so cleanup must enumerate explicit ids. + page = 1 + chunk_ids = [] + while True: + res = list_chunks(auth, dataset_id, document_id, {"page": page, "page_size": page_size}) + chunks = (res.get("data") or {}).get("chunks") or [] + chunk_ids.extend(chunk["id"] for chunk in chunks) + if len(chunks) < page_size: + break + page += 1 + + if not chunk_ids: + return {"code": 0, "message": ""} + return delete_chunks(auth, dataset_id, document_id, {"chunk_ids": chunk_ids}) + + def retrieval_chunks(auth, payload=None): url = f"{HOST_ADDRESS}{RETRIEVAL_API_URL}" res = requests.post(url=url, headers=HEADERS, auth=auth, json=payload) @@ -215,6 +266,23 @@ def delete_chat_assistants(auth, payload=None): return res.json() +def delete_all_chat_assistants(auth, *, page_size=1000): + # Chat DELETE now treats null/empty ids as a no-op, so cleanup must enumerate explicit ids. + page = 1 + chat_ids = [] + while True: + res = list_chat_assistants(auth, {"page": page, "page_size": page_size}) + data = res.get("data") or [] + chat_ids.extend(chat["id"] for chat in data) + if len(data) < page_size: + break + page += 1 + + if not chat_ids: + return {"code": 0, "message": ""} + return delete_chat_assistants(auth, {"ids": chat_ids}) + + def batch_create_chat_assistants(auth, num): chat_assistant_ids = [] for i in range(num): @@ -244,12 +312,27 @@ def update_session_with_chat_assistant(auth, chat_assistant_id, session_id, payl def delete_session_with_chat_assistants(auth, chat_assistant_id, payload=None): url = f"{HOST_ADDRESS}{SESSION_WITH_CHAT_ASSISTANT_API_URL}".format(chat_id=chat_assistant_id) - if payload is None: - payload = {} res = requests.delete(url=url, headers=HEADERS, auth=auth, json=payload) return res.json() +def delete_all_sessions_with_chat_assistant(auth, chat_assistant_id, *, page_size=1000): + # Session DELETE now treats missing/null/empty ids as a no-op, so cleanup must enumerate explicit ids. + page = 1 + session_ids = [] + while True: + res = list_session_with_chat_assistants(auth, chat_assistant_id, {"page": page, "page_size": page_size}) + data = res.get("data") or [] + session_ids.extend(session["id"] for session in data) + if len(data) < page_size: + break + page += 1 + + if not session_ids: + return {"code": 0, "message": ""} + return delete_session_with_chat_assistants(auth, chat_assistant_id, {"ids": session_ids}) + + def batch_add_sessions_with_chat_assistant(auth, chat_assistant_id, num): session_ids = [] for i in range(num): @@ -350,12 +433,27 @@ def list_agent_sessions(auth, agent_id, params=None): def delete_agent_sessions(auth, agent_id, payload=None): url = f"{HOST_ADDRESS}{SESSION_WITH_AGENT_API_URL}".format(agent_id=agent_id) - if payload is None: - payload = {} res = requests.delete(url=url, headers=HEADERS, auth=auth, json=payload) return res.json() +def delete_all_agent_sessions(auth, agent_id, *, page_size=1000): + # Agent session DELETE now treats missing/null/empty ids as a no-op, so cleanup must enumerate explicit ids. + page = 1 + session_ids = [] + while True: + res = list_agent_sessions(auth, agent_id, {"page": page, "page_size": page_size}) + data = res.get("data") or [] + session_ids.extend(session["id"] for session in data) + if len(data) < page_size: + break + page += 1 + + if not session_ids: + return {"code": 0, "message": ""} + return delete_agent_sessions(auth, agent_id, {"ids": session_ids}) + + def agent_completions(auth, agent_id, payload=None): url = f"{HOST_ADDRESS}{AGENT_API_URL}/{agent_id}/completions" res = requests.post(url=url, headers=HEADERS, auth=auth, json=payload) diff --git a/test/testcases/test_http_api/conftest.py b/test/testcases/test_http_api/conftest.py index eab05d09b..d3c571a6f 100644 --- a/test/testcases/test_http_api/conftest.py +++ b/test/testcases/test_http_api/conftest.py @@ -21,9 +21,9 @@ from common import ( batch_create_chat_assistants, batch_create_datasets, bulk_upload_documents, - delete_chat_assistants, - delete_datasets, - delete_session_with_chat_assistants, + delete_all_chat_assistants, + delete_all_datasets, + delete_all_sessions_with_chat_assistant, list_documents, parse_documents, ) @@ -89,7 +89,7 @@ def HttpApiAuth(token): @pytest.fixture(scope="function") def clear_datasets(request, HttpApiAuth): def cleanup(): - delete_datasets(HttpApiAuth, {"ids": None}) + delete_all_datasets(HttpApiAuth) request.addfinalizer(cleanup) @@ -97,7 +97,7 @@ def clear_datasets(request, HttpApiAuth): @pytest.fixture(scope="function") def clear_chat_assistants(request, HttpApiAuth): def cleanup(): - delete_chat_assistants(HttpApiAuth) + delete_all_chat_assistants(HttpApiAuth) request.addfinalizer(cleanup) @@ -106,7 +106,7 @@ def clear_chat_assistants(request, HttpApiAuth): def clear_session_with_chat_assistants(request, HttpApiAuth, add_chat_assistants): def cleanup(): for chat_assistant_id in chat_assistant_ids: - delete_session_with_chat_assistants(HttpApiAuth, chat_assistant_id) + delete_all_sessions_with_chat_assistant(HttpApiAuth, chat_assistant_id) request.addfinalizer(cleanup) @@ -116,7 +116,7 @@ def clear_session_with_chat_assistants(request, HttpApiAuth, add_chat_assistants @pytest.fixture(scope="class") def add_dataset(request, HttpApiAuth): def cleanup(): - delete_datasets(HttpApiAuth, {"ids": None}) + delete_all_datasets(HttpApiAuth) request.addfinalizer(cleanup) @@ -127,7 +127,7 @@ def add_dataset(request, HttpApiAuth): @pytest.fixture(scope="function") def add_dataset_func(request, HttpApiAuth): def cleanup(): - delete_datasets(HttpApiAuth, {"ids": None}) + delete_all_datasets(HttpApiAuth) request.addfinalizer(cleanup) @@ -154,7 +154,7 @@ def add_chunks(HttpApiAuth, add_document): @pytest.fixture(scope="class") def add_chat_assistants(request, HttpApiAuth, add_document): def cleanup(): - delete_chat_assistants(HttpApiAuth) + delete_all_chat_assistants(HttpApiAuth) request.addfinalizer(cleanup) diff --git a/test/testcases/test_http_api/test_chat_assistant_management/conftest.py b/test/testcases/test_http_api/test_chat_assistant_management/conftest.py index 772c0788b..b81b48edc 100644 --- a/test/testcases/test_http_api/test_chat_assistant_management/conftest.py +++ b/test/testcases/test_http_api/test_chat_assistant_management/conftest.py @@ -14,7 +14,7 @@ # limitations under the License. # import pytest -from common import batch_create_chat_assistants, delete_chat_assistants, list_chat_assistants, list_documents, parse_documents +from common import batch_create_chat_assistants, delete_all_chat_assistants, list_chat_assistants, list_documents, parse_documents from utils import wait_for @@ -30,7 +30,7 @@ def condition(_auth, _dataset_id): @pytest.fixture(scope="function") def add_chat_assistants_func(request, HttpApiAuth, add_document): def cleanup(): - delete_chat_assistants(HttpApiAuth) + delete_all_chat_assistants(HttpApiAuth) request.addfinalizer(cleanup) diff --git a/test/testcases/test_http_api/test_chat_assistant_management/test_chat_sdk_routes_unit.py b/test/testcases/test_http_api/test_chat_assistant_management/test_chat_sdk_routes_unit.py index cb3ca0ae8..5ca56b925 100644 --- a/test/testcases/test_http_api/test_chat_assistant_management/test_chat_sdk_routes_unit.py +++ b/test/testcases/test_http_api/test_chat_assistant_management/test_chat_sdk_routes_unit.py @@ -299,6 +299,15 @@ def test_update_internal_failure_paths(monkeypatch): def test_delete_duplicate_no_success_path(monkeypatch): module = _load_chat_module(monkeypatch) + _set_request_json(monkeypatch, module, {}) + monkeypatch.setattr( + module.DialogService, + "query", + lambda **_kwargs: (_ for _ in ()).throw(AssertionError("query must not run for empty delete payload")), + ) + res = _run(module.delete_chats.__wrapped__("tenant-1")) + assert res["code"] == module.RetCode.SUCCESS + _set_request_json(monkeypatch, module, {"ids": ["chat-1", "chat-1"]}) monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [SimpleNamespace(id="chat-1")]) monkeypatch.setattr(module.DialogService, "update_by_id", lambda *_args, **_kwargs: 0) diff --git a/test/testcases/test_http_api/test_chat_assistant_management/test_delete_chat_assistants.py b/test/testcases/test_http_api/test_chat_assistant_management/test_delete_chat_assistants.py index 670ab04d3..172c66492 100644 --- a/test/testcases/test_http_api/test_chat_assistant_management/test_delete_chat_assistants.py +++ b/test/testcases/test_http_api/test_chat_assistant_management/test_delete_chat_assistants.py @@ -44,8 +44,8 @@ class TestChatAssistantsDelete: @pytest.mark.parametrize( "payload, expected_code, expected_message, remaining", [ - pytest.param(None, 0, "", 0, marks=pytest.mark.p3), - pytest.param({"ids": []}, 0, "", 0, marks=pytest.mark.p3), + pytest.param(None, 0, "", 5, marks=pytest.mark.p3), + pytest.param({"ids": []}, 0, "", 5, marks=pytest.mark.p3), pytest.param({"ids": ["invalid_id"]}, 102, "Assistant(invalid_id) not found.", 5, marks=pytest.mark.p3), pytest.param({"ids": ["\n!?。;!?\"'"]}, 102, """Assistant(\n!?。;!?"\') not found.""", 5, marks=pytest.mark.p3), pytest.param("not json", 100, "AttributeError(\"'str' object has no attribute 'get'\")", 5, marks=pytest.mark.p3), diff --git a/test/testcases/test_http_api/test_chunk_management_within_dataset/conftest.py b/test/testcases/test_http_api/test_chunk_management_within_dataset/conftest.py index 7a06a23eb..48487ee9e 100644 --- a/test/testcases/test_http_api/test_chunk_management_within_dataset/conftest.py +++ b/test/testcases/test_http_api/test_chunk_management_within_dataset/conftest.py @@ -18,7 +18,7 @@ from time import sleep import pytest -from common import batch_add_chunks, delete_chunks, list_documents, parse_documents +from common import batch_add_chunks, delete_all_chunks, list_documents, parse_documents from utils import wait_for @@ -34,7 +34,7 @@ def condition(_auth, _dataset_id): @pytest.fixture(scope="function") def add_chunks_func(request, HttpApiAuth, add_document): def cleanup(): - delete_chunks(HttpApiAuth, dataset_id, document_id, {"chunk_ids": []}) + delete_all_chunks(HttpApiAuth, dataset_id, document_id) request.addfinalizer(cleanup) diff --git a/test/testcases/test_http_api/test_chunk_management_within_dataset/test_delete_chunks.py b/test/testcases/test_http_api/test_chunk_management_within_dataset/test_delete_chunks.py index 580a2974c..eae75afad 100644 --- a/test/testcases/test_http_api/test_chunk_management_within_dataset/test_delete_chunks.py +++ b/test/testcases/test_http_api/test_chunk_management_within_dataset/test_delete_chunks.py @@ -158,12 +158,12 @@ class TestChunksDeletion: @pytest.mark.parametrize( "payload, expected_code, expected_message, remaining", [ - pytest.param(None, 100, """TypeError("argument of type \'NoneType\' is not iterable")""", 5, marks=pytest.mark.skip), + pytest.param(None, 0, "", 5, marks=pytest.mark.p3), pytest.param({"chunk_ids": ["invalid_id"]}, 102, "rm_chunk deleted chunks 0, expect 1", 5, marks=pytest.mark.p3), pytest.param("not json", 100, """UnboundLocalError("local variable \'duplicate_messages\' referenced before assignment")""", 5, marks=pytest.mark.skip(reason="pull/6376")), pytest.param(lambda r: {"chunk_ids": r[:1]}, 0, "", 4, marks=pytest.mark.p3), pytest.param(lambda r: {"chunk_ids": r}, 0, "", 1, marks=pytest.mark.p1), - pytest.param({"chunk_ids": []}, 0, "", 0, marks=pytest.mark.p3), + pytest.param({"chunk_ids": []}, 0, "", 5, marks=pytest.mark.p3), ], ) def test_basic_scenarios( diff --git a/test/testcases/test_http_api/test_dataset_management/conftest.py b/test/testcases/test_http_api/test_dataset_management/conftest.py index d4ef989ff..3e03e50b9 100644 --- a/test/testcases/test_http_api/test_dataset_management/conftest.py +++ b/test/testcases/test_http_api/test_dataset_management/conftest.py @@ -16,13 +16,13 @@ import pytest -from common import batch_create_datasets, delete_datasets +from common import batch_create_datasets, delete_all_datasets @pytest.fixture(scope="class") def add_datasets(HttpApiAuth, request): def cleanup(): - delete_datasets(HttpApiAuth, {"ids": None}) + delete_all_datasets(HttpApiAuth) request.addfinalizer(cleanup) @@ -32,7 +32,7 @@ def add_datasets(HttpApiAuth, request): @pytest.fixture(scope="function") def add_datasets_func(HttpApiAuth, request): def cleanup(): - delete_datasets(HttpApiAuth, {"ids": None}) + delete_all_datasets(HttpApiAuth) request.addfinalizer(cleanup) diff --git a/test/testcases/test_http_api/test_dataset_management/test_delete_datasets.py b/test/testcases/test_http_api/test_dataset_management/test_delete_datasets.py index f8327704e..024085741 100644 --- a/test/testcases/test_http_api/test_dataset_management/test_delete_datasets.py +++ b/test/testcases/test_http_api/test_dataset_management/test_delete_datasets.py @@ -134,7 +134,7 @@ class TestDatasetsDelete: assert res["code"] == 0, res res = list_datasets(HttpApiAuth) - assert len(res["data"]) == 0, res + assert len(res["data"]) == 3, res @pytest.mark.p2 @pytest.mark.usefixtures("add_dataset_func") diff --git a/test/testcases/test_http_api/test_file_management_within_dataset/conftest.py b/test/testcases/test_http_api/test_file_management_within_dataset/conftest.py index cd1014382..efbbd5d43 100644 --- a/test/testcases/test_http_api/test_file_management_within_dataset/conftest.py +++ b/test/testcases/test_http_api/test_file_management_within_dataset/conftest.py @@ -16,13 +16,13 @@ import pytest -from common import bulk_upload_documents, delete_documents +from common import bulk_upload_documents, delete_all_documents @pytest.fixture(scope="function") def add_document_func(request, HttpApiAuth, add_dataset, ragflow_tmp_dir): def cleanup(): - delete_documents(HttpApiAuth, dataset_id, {"ids": None}) + delete_all_documents(HttpApiAuth, dataset_id) request.addfinalizer(cleanup) @@ -33,7 +33,7 @@ def add_document_func(request, HttpApiAuth, add_dataset, ragflow_tmp_dir): @pytest.fixture(scope="class") def add_documents(request, HttpApiAuth, add_dataset, ragflow_tmp_dir): def cleanup(): - delete_documents(HttpApiAuth, dataset_id, {"ids": None}) + delete_all_documents(HttpApiAuth, dataset_id) request.addfinalizer(cleanup) @@ -44,7 +44,7 @@ def add_documents(request, HttpApiAuth, add_dataset, ragflow_tmp_dir): @pytest.fixture(scope="function") def add_documents_func(request, HttpApiAuth, add_dataset_func, ragflow_tmp_dir): def cleanup(): - delete_documents(HttpApiAuth, dataset_id, {"ids": None}) + delete_all_documents(HttpApiAuth, dataset_id) request.addfinalizer(cleanup) diff --git a/test/testcases/test_http_api/test_file_management_within_dataset/test_delete_documents.py b/test/testcases/test_http_api/test_file_management_within_dataset/test_delete_documents.py index 74f5c0606..133a05df6 100644 --- a/test/testcases/test_http_api/test_file_management_within_dataset/test_delete_documents.py +++ b/test/testcases/test_http_api/test_file_management_within_dataset/test_delete_documents.py @@ -45,8 +45,8 @@ class TestDocumentsDeletion: @pytest.mark.parametrize( "payload, expected_code, expected_message, remaining", [ - (None, 0, "", 0), - ({"ids": []}, 0, "", 0), + (None, 0, "", 3), + ({"ids": []}, 0, "", 3), ({"ids": ["invalid_id"]}, 102, "Documents not found: ['invalid_id']", 3), ( {"ids": ["\n!?。;!?\"'"]}, diff --git a/test/testcases/test_http_api/test_file_management_within_dataset/test_doc_sdk_routes_unit.py b/test/testcases/test_http_api/test_file_management_within_dataset/test_doc_sdk_routes_unit.py index 23ac8fcf6..872563cca 100644 --- a/test/testcases/test_http_api/test_file_management_within_dataset/test_doc_sdk_routes_unit.py +++ b/test/testcases/test_http_api/test_file_management_within_dataset/test_doc_sdk_routes_unit.py @@ -692,6 +692,10 @@ class TestDocRoutesUnit: assert "don't own the dataset" in res["message"] monkeypatch.setattr(module.KnowledgebaseService, "accessible", lambda **_kwargs: True) + monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({})) + res = _run(module.delete.__wrapped__("tenant-1", "ds-1")) + assert res["code"] == module.RetCode.SUCCESS + monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"ids": ["doc-1"]})) monkeypatch.setattr(module, "check_duplicate_ids", lambda ids, _kind: (ids, [])) monkeypatch.setattr(module.FileService, "get_root_folder", lambda _tenant: {"id": "pf-1"}) @@ -871,7 +875,11 @@ class TestDocRoutesUnit: monkeypatch.setattr(module.DocumentService, "get_by_ids", lambda _ids: [_DummyDoc()]) monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({})) - _patch_docstore(monkeypatch, module, delete=lambda *_args, **_kwargs: 2) + _patch_docstore( + monkeypatch, + module, + delete=lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("delete must not run for empty chunk ids")), + ) monkeypatch.setattr(module.DocumentService, "decrement_chunk_num", lambda *_args, **_kwargs: None) res = _run(module.rm_chunk.__wrapped__("tenant-1", "ds-1", "doc-1")) assert res["code"] == 0 diff --git a/test/testcases/test_http_api/test_file_management_within_dataset/test_metadata_batch_update.py b/test/testcases/test_http_api/test_file_management_within_dataset/test_metadata_batch_update.py index 27b74d42f..9061ba390 100644 --- a/test/testcases/test_http_api/test_file_management_within_dataset/test_metadata_batch_update.py +++ b/test/testcases/test_http_api/test_file_management_within_dataset/test_metadata_batch_update.py @@ -63,4 +63,4 @@ class TestMetadataBatchUpdate: assert doc["meta_fields"].get("status") == "processed", f"Expected status='processed', got {doc['meta_fields'].get('status')}" # Cleanup - delete_documents(HttpApiAuth, dataset_id, {"ids": None}) + delete_documents(HttpApiAuth, dataset_id, {"ids": document_ids}) diff --git a/test/testcases/test_http_api/test_session_management/conftest.py b/test/testcases/test_http_api/test_session_management/conftest.py index 56eafab0a..3bae72395 100644 --- a/test/testcases/test_http_api/test_session_management/conftest.py +++ b/test/testcases/test_http_api/test_session_management/conftest.py @@ -14,14 +14,14 @@ # limitations under the License. # import pytest -from common import batch_add_sessions_with_chat_assistant, delete_session_with_chat_assistants +from common import batch_add_sessions_with_chat_assistant, delete_all_sessions_with_chat_assistant @pytest.fixture(scope="class") def add_sessions_with_chat_assistant(request, HttpApiAuth, add_chat_assistants): def cleanup(): for chat_assistant_id in chat_assistant_ids: - delete_session_with_chat_assistants(HttpApiAuth, chat_assistant_id) + delete_all_sessions_with_chat_assistant(HttpApiAuth, chat_assistant_id) request.addfinalizer(cleanup) @@ -33,7 +33,7 @@ def add_sessions_with_chat_assistant(request, HttpApiAuth, add_chat_assistants): def add_sessions_with_chat_assistant_func(request, HttpApiAuth, add_chat_assistants): def cleanup(): for chat_assistant_id in chat_assistant_ids: - delete_session_with_chat_assistants(HttpApiAuth, chat_assistant_id) + delete_all_sessions_with_chat_assistant(HttpApiAuth, chat_assistant_id) request.addfinalizer(cleanup) diff --git a/test/testcases/test_http_api/test_session_management/test_agent_completions.py b/test/testcases/test_http_api/test_session_management/test_agent_completions.py index e34cc21ec..bb65fd9f2 100644 --- a/test/testcases/test_http_api/test_session_management/test_agent_completions.py +++ b/test/testcases/test_http_api/test_session_management/test_agent_completions.py @@ -19,7 +19,7 @@ from common import ( create_agent, create_agent_session, delete_agent, - delete_agent_sessions, + delete_all_agent_sessions, list_agents, ) @@ -65,7 +65,7 @@ def agent_id(HttpApiAuth, request): agent_id = res["data"][0]["id"] def cleanup(): - delete_agent_sessions(HttpApiAuth, agent_id) + delete_all_agent_sessions(HttpApiAuth, agent_id) delete_agent(HttpApiAuth, agent_id) request.addfinalizer(cleanup) diff --git a/test/testcases/test_http_api/test_session_management/test_agent_sessions.py b/test/testcases/test_http_api/test_session_management/test_agent_sessions.py index cfcc1807b..883ae2af0 100644 --- a/test/testcases/test_http_api/test_session_management/test_agent_sessions.py +++ b/test/testcases/test_http_api/test_session_management/test_agent_sessions.py @@ -19,6 +19,7 @@ from common import ( create_agent, create_agent_session, delete_agent, + delete_all_agent_sessions, delete_agent_sessions, list_agent_sessions, list_agents, @@ -67,7 +68,7 @@ def agent_id(HttpApiAuth, request): agent_id = res["data"][0]["id"] def cleanup(): - delete_agent_sessions(HttpApiAuth, agent_id) + delete_all_agent_sessions(HttpApiAuth, agent_id) delete_agent(HttpApiAuth, agent_id) request.addfinalizer(cleanup) @@ -75,6 +76,19 @@ def agent_id(HttpApiAuth, request): class TestAgentSessions: + @pytest.mark.p2 + def test_delete_agent_sessions_empty_ids_noop(self, HttpApiAuth, agent_id): + res = create_agent_session(HttpApiAuth, agent_id, payload={}) + assert res["code"] == 0, res + session_id = res["data"]["id"] + + res = delete_agent_sessions(HttpApiAuth, agent_id, {"ids": []}) + assert res["code"] == 0, res + + res = list_agent_sessions(HttpApiAuth, agent_id, params={"id": session_id}) + assert res["code"] == 0, res + assert len(res["data"]) == 1, res + @pytest.mark.p2 def test_create_list_delete_agent_sessions(self, HttpApiAuth, agent_id): res = create_agent_session(HttpApiAuth, agent_id, payload={}) diff --git a/test/testcases/test_http_api/test_session_management/test_chat_completions.py b/test/testcases/test_http_api/test_session_management/test_chat_completions.py index fa2e225ca..000a90585 100644 --- a/test/testcases/test_http_api/test_session_management/test_chat_completions.py +++ b/test/testcases/test_http_api/test_session_management/test_chat_completions.py @@ -19,8 +19,8 @@ from common import ( chat_completions, create_chat_assistant, create_session_with_chat_assistant, - delete_chat_assistants, - delete_session_with_chat_assistants, + delete_all_chat_assistants, + delete_all_sessions_with_chat_assistant, list_documents, parse_documents, ) @@ -52,8 +52,8 @@ class TestChatCompletions: res = create_chat_assistant(HttpApiAuth, {"name": "chat_completion_test", "dataset_ids": [dataset_id]}) assert res["code"] == 0, res chat_id = res["data"]["id"] - request.addfinalizer(lambda: delete_session_with_chat_assistants(HttpApiAuth, chat_id)) - request.addfinalizer(lambda: delete_chat_assistants(HttpApiAuth)) + request.addfinalizer(lambda: delete_all_chat_assistants(HttpApiAuth)) + request.addfinalizer(lambda: delete_all_sessions_with_chat_assistant(HttpApiAuth, chat_id)) res = create_session_with_chat_assistant(HttpApiAuth, chat_id, {"name": "session_for_completion"}) assert res["code"] == 0, res @@ -85,8 +85,8 @@ class TestChatCompletions: res = create_chat_assistant(HttpApiAuth, {"name": "chat_completion_invalid_session", "dataset_ids": []}) assert res["code"] == 0, res chat_id = res["data"]["id"] - request.addfinalizer(lambda: delete_session_with_chat_assistants(HttpApiAuth, chat_id)) - request.addfinalizer(lambda: delete_chat_assistants(HttpApiAuth)) + request.addfinalizer(lambda: delete_all_chat_assistants(HttpApiAuth)) + request.addfinalizer(lambda: delete_all_sessions_with_chat_assistant(HttpApiAuth, chat_id)) res = chat_completions( HttpApiAuth, @@ -101,8 +101,8 @@ class TestChatCompletions: res = create_chat_assistant(HttpApiAuth, {"name": "chat_completion_invalid_meta", "dataset_ids": []}) assert res["code"] == 0, res chat_id = res["data"]["id"] - request.addfinalizer(lambda: delete_session_with_chat_assistants(HttpApiAuth, chat_id)) - request.addfinalizer(lambda: delete_chat_assistants(HttpApiAuth)) + request.addfinalizer(lambda: delete_all_chat_assistants(HttpApiAuth)) + request.addfinalizer(lambda: delete_all_sessions_with_chat_assistant(HttpApiAuth, chat_id)) res = create_session_with_chat_assistant(HttpApiAuth, chat_id, {"name": "session_for_meta"}) assert res["code"] == 0, res diff --git a/test/testcases/test_http_api/test_session_management/test_chat_completions_openai.py b/test/testcases/test_http_api/test_session_management/test_chat_completions_openai.py index ffaa3ee45..54d5fe29d 100644 --- a/test/testcases/test_http_api/test_session_management/test_chat_completions_openai.py +++ b/test/testcases/test_http_api/test_session_management/test_chat_completions_openai.py @@ -18,7 +18,7 @@ from common import ( bulk_upload_documents, chat_completions_openai, create_chat_assistant, - delete_chat_assistants, + delete_all_chat_assistants, list_documents, parse_documents, ) @@ -53,7 +53,7 @@ class TestChatCompletionsOpenAI: res = create_chat_assistant(HttpApiAuth, {"name": "openai_endpoint_test", "dataset_ids": [dataset_id]}) assert res["code"] == 0, res chat_id = res["data"]["id"] - request.addfinalizer(lambda: delete_chat_assistants(HttpApiAuth)) + request.addfinalizer(lambda: delete_all_chat_assistants(HttpApiAuth)) res = chat_completions_openai( HttpApiAuth, @@ -92,7 +92,7 @@ class TestChatCompletionsOpenAI: res = create_chat_assistant(HttpApiAuth, {"name": "openai_token_count_test", "dataset_ids": [dataset_id]}) assert res["code"] == 0, res chat_id = res["data"]["id"] - request.addfinalizer(lambda: delete_chat_assistants(HttpApiAuth)) + request.addfinalizer(lambda: delete_all_chat_assistants(HttpApiAuth)) # Use a message with known token count # "hello" is 1 token in cl100k_base encoding @@ -202,7 +202,7 @@ class TestChatCompletionsOpenAI: res = create_chat_assistant(HttpApiAuth, {"name": "openai_validation_case", "dataset_ids": []}) assert res["code"] == 0, res chat_id = res["data"]["id"] - request.addfinalizer(lambda: delete_chat_assistants(HttpApiAuth)) + request.addfinalizer(lambda: delete_all_chat_assistants(HttpApiAuth)) res = chat_completions_openai(HttpApiAuth, chat_id, payload) assert res.get("code") != 0, res diff --git a/test/testcases/test_http_api/test_session_management/test_delete_sessions_with_chat_assistant.py b/test/testcases/test_http_api/test_session_management/test_delete_sessions_with_chat_assistant.py index 818050819..637cb1f1d 100644 --- a/test/testcases/test_http_api/test_session_management/test_delete_sessions_with_chat_assistant.py +++ b/test/testcases/test_http_api/test_session_management/test_delete_sessions_with_chat_assistant.py @@ -141,12 +141,12 @@ class TestSessionWithChatAssistantDelete: @pytest.mark.parametrize( "payload, expected_code, expected_message, remaining", [ - pytest.param(None, 0, """TypeError("argument of type \'NoneType\' is not iterable")""", 0, marks=pytest.mark.skip), + pytest.param(None, 0, "", 5, marks=pytest.mark.p3), pytest.param({"ids": ["invalid_id"]}, 102, "The chat doesn't own the session invalid_id", 5, marks=pytest.mark.p3), pytest.param("not json", 100, """AttributeError("\'str\' object has no attribute \'get\'")""", 5, marks=pytest.mark.skip), pytest.param(lambda r: {"ids": r[:1]}, 0, "", 4, marks=pytest.mark.p3), pytest.param(lambda r: {"ids": r}, 0, "", 0, marks=pytest.mark.p1), - pytest.param({"ids": []}, 0, "", 0, marks=pytest.mark.p3), + pytest.param({"ids": []}, 0, "", 5, marks=pytest.mark.p3), ], ) def test_basic_scenarios( diff --git a/test/testcases/test_http_api/test_session_management/test_session_sdk_routes_unit.py b/test/testcases/test_http_api/test_session_management/test_session_sdk_routes_unit.py index c1bbd1034..6852024db 100644 --- a/test/testcases/test_http_api/test_session_management/test_session_sdk_routes_unit.py +++ b/test/testcases/test_http_api/test_session_management/test_session_sdk_routes_unit.py @@ -985,6 +985,10 @@ def test_delete_routes_partial_duplicate_unit(monkeypatch): module = _load_session_module(monkeypatch) monkeypatch.setattr(module.DialogService, "query", lambda **_kwargs: [SimpleNamespace(id="chat-1")]) + monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({})) + res = _run(inspect.unwrap(module.delete)("tenant-1", "chat-1")) + assert res["code"] == 0 + monkeypatch.setattr(module.ConversationService, "delete_by_id", lambda *_args, **_kwargs: True) def _conversation_query(**kwargs): @@ -1016,6 +1020,10 @@ def test_delete_routes_partial_duplicate_unit(monkeypatch): assert res["data"]["errors"] == ["Duplicate session ids: ok"] monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: [SimpleNamespace(id="agent-1")]) + monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({})) + res = _run(inspect.unwrap(module.delete_agent_session)("tenant-1", "agent-1")) + assert res["code"] == 0 + monkeypatch.setattr(module, "get_request_json", lambda: _AwaitableValue({"ids": ["session-1"]})) monkeypatch.setattr(module, "check_duplicate_ids", lambda ids, _kind: (ids, [])) diff --git a/test/testcases/test_sdk_api/common.py b/test/testcases/test_sdk_api/common.py index 3035383a4..84354fc91 100644 --- a/test/testcases/test_sdk_api/common.py +++ b/test/testcases/test_sdk_api/common.py @@ -25,6 +25,36 @@ def batch_create_datasets(client: RAGFlow, num: int) -> list[DataSet]: return [client.create_dataset(name=f"dataset_{i}") for i in range(num)] +def delete_all_datasets(client: RAGFlow, *, page_size: int = 1000) -> None: + # Dataset DELETE now treats null/empty ids as a no-op, so cleanup must enumerate explicit ids. + page = 1 + dataset_ids: list[str] = [] + while True: + datasets = client.list_datasets(page=page, page_size=page_size) + dataset_ids.extend(dataset.id for dataset in datasets) + if len(datasets) < page_size: + break + page += 1 + + if dataset_ids: + client.delete_datasets(ids=dataset_ids) + + +def delete_all_chats(client: RAGFlow, *, page_size: int = 1000) -> None: + # Chat DELETE now treats null/empty ids as a no-op, so cleanup must enumerate explicit ids. + page = 1 + chat_ids: list[str] = [] + while True: + chats = client.list_chats(page=page, page_size=page_size) + chat_ids.extend(chat.id for chat in chats) + if len(chats) < page_size: + break + page += 1 + + if chat_ids: + client.delete_chats(ids=chat_ids) + + # FILE MANAGEMENT WITHIN DATASET def bulk_upload_documents(dataset: DataSet, num: int, tmp_path: Path) -> list[Document]: document_infos = [] @@ -37,6 +67,51 @@ def bulk_upload_documents(dataset: DataSet, num: int, tmp_path: Path) -> list[Do return dataset.upload_documents(document_infos) +def delete_all_documents(dataset: DataSet, *, page_size: int = 1000) -> None: + # Document DELETE now treats missing/null/empty ids as a no-op, so cleanup must enumerate explicit ids. + page = 1 + document_ids: list[str] = [] + while True: + documents = dataset.list_documents(page=page, page_size=page_size) + document_ids.extend(document.id for document in documents) + if len(documents) < page_size: + break + page += 1 + + if document_ids: + dataset.delete_documents(ids=document_ids) + + +def delete_all_sessions(chat_assistant: Chat, *, page_size: int = 1000) -> None: + # Session DELETE now treats missing/null/empty ids as a no-op, so cleanup must enumerate explicit ids. + page = 1 + session_ids: list[str] = [] + while True: + sessions = chat_assistant.list_sessions(page=page, page_size=page_size) + session_ids.extend(session.id for session in sessions) + if len(sessions) < page_size: + break + page += 1 + + if session_ids: + chat_assistant.delete_sessions(ids=session_ids) + + +def delete_all_chunks(document: Document, *, page_size: int = 1000) -> None: + # Chunk DELETE now treats missing/null/empty ids as a no-op, so cleanup must enumerate explicit ids. + page = 1 + chunk_ids: list[str] = [] + while True: + chunks = document.list_chunks(page=page, page_size=page_size) + chunk_ids.extend(chunk.id for chunk in chunks) + if len(chunks) < page_size: + break + page += 1 + + if chunk_ids: + document.delete_chunks(ids=chunk_ids) + + # CHUNK MANAGEMENT WITHIN DATASET def batch_add_chunks(document: Document, num: int) -> list[Chunk]: return [document.add_chunk(content=f"chunk test {i}") for i in range(num)] diff --git a/test/testcases/test_sdk_api/conftest.py b/test/testcases/test_sdk_api/conftest.py index 11a258a5a..f4791306c 100644 --- a/test/testcases/test_sdk_api/conftest.py +++ b/test/testcases/test_sdk_api/conftest.py @@ -23,6 +23,10 @@ from common import ( batch_create_chat_assistants, batch_create_datasets, bulk_upload_documents, + delete_all_chats, + delete_all_chunks, + delete_all_datasets, + delete_all_sessions, ) from configs import HOST_ADDRESS, VERSION from pytest import FixtureRequest @@ -88,7 +92,7 @@ def client(token: str) -> RAGFlow: @pytest.fixture(scope="function") def clear_datasets(request: FixtureRequest, client: RAGFlow): def cleanup(): - client.delete_datasets(ids=None) + delete_all_datasets(client) request.addfinalizer(cleanup) @@ -96,7 +100,7 @@ def clear_datasets(request: FixtureRequest, client: RAGFlow): @pytest.fixture(scope="function") def clear_chat_assistants(request: FixtureRequest, client: RAGFlow): def cleanup(): - client.delete_chats(ids=None) + delete_all_chats(client) request.addfinalizer(cleanup) @@ -106,7 +110,7 @@ def clear_session_with_chat_assistants(request, add_chat_assistants): def cleanup(): for chat_assistant in chat_assistants: try: - chat_assistant.delete_sessions(ids=None) + delete_all_sessions(chat_assistant) except Exception: pass @@ -118,7 +122,7 @@ def clear_session_with_chat_assistants(request, add_chat_assistants): @pytest.fixture(scope="class") def add_dataset(request: FixtureRequest, client: RAGFlow) -> DataSet: def cleanup(): - client.delete_datasets(ids=None) + delete_all_datasets(client) request.addfinalizer(cleanup) return batch_create_datasets(client, 1)[0] @@ -127,7 +131,7 @@ def add_dataset(request: FixtureRequest, client: RAGFlow) -> DataSet: @pytest.fixture(scope="function") def add_dataset_func(request: FixtureRequest, client: RAGFlow) -> DataSet: def cleanup(): - client.delete_datasets(ids=None) + delete_all_datasets(client) request.addfinalizer(cleanup) return batch_create_datasets(client, 1)[0] @@ -142,7 +146,7 @@ def add_document(add_dataset: DataSet, ragflow_tmp_dir: Path) -> tuple[DataSet, def add_chunks(request: FixtureRequest, add_document: tuple[DataSet, Document]) -> tuple[DataSet, Document, list[Chunk]]: def cleanup(): try: - document.delete_chunks(ids=[]) + delete_all_chunks(document) except Exception: pass @@ -161,7 +165,7 @@ def add_chunks(request: FixtureRequest, add_document: tuple[DataSet, Document]) def add_chat_assistants(request, client, add_document) -> tuple[DataSet, Document, list[Chat]]: def cleanup(): try: - client.delete_chats(ids=None) + delete_all_chats(client) except Exception: pass diff --git a/test/testcases/test_sdk_api/test_chat_assistant_management/conftest.py b/test/testcases/test_sdk_api/test_chat_assistant_management/conftest.py index 79347d67a..c02065061 100644 --- a/test/testcases/test_sdk_api/test_chat_assistant_management/conftest.py +++ b/test/testcases/test_sdk_api/test_chat_assistant_management/conftest.py @@ -14,7 +14,7 @@ # limitations under the License. # import pytest -from common import batch_create_chat_assistants +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 @@ -32,7 +32,7 @@ def condition(_dataset: DataSet): @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(): - client.delete_chats(ids=None) + delete_all_chats(client) request.addfinalizer(cleanup) diff --git a/test/testcases/test_sdk_api/test_chat_assistant_management/test_delete_chat_assistants.py b/test/testcases/test_sdk_api/test_chat_assistant_management/test_delete_chat_assistants.py index 7f7203309..936d9cf5b 100644 --- a/test/testcases/test_sdk_api/test_chat_assistant_management/test_delete_chat_assistants.py +++ b/test/testcases/test_sdk_api/test_chat_assistant_management/test_delete_chat_assistants.py @@ -23,8 +23,8 @@ class TestChatAssistantsDelete: @pytest.mark.parametrize( "payload, expected_message, remaining", [ - pytest.param(None, "", 0, marks=pytest.mark.p3), - pytest.param({"ids": []}, "", 0, marks=pytest.mark.p3), + pytest.param(None, "", 5, marks=pytest.mark.p3), + pytest.param({"ids": []}, "", 5, marks=pytest.mark.p3), pytest.param({"ids": ["invalid_id"]}, "Assistant(invalid_id) not found.", 5, marks=pytest.mark.p3), pytest.param({"ids": ["\n!?。;!?\"'"]}, """Assistant(\n!?。;!?"\') not found.""", 5, marks=pytest.mark.p3), pytest.param(lambda r: {"ids": r[:1]}, "", 4, marks=pytest.mark.p3), diff --git a/test/testcases/test_sdk_api/test_chunk_management_within_dataset/conftest.py b/test/testcases/test_sdk_api/test_chunk_management_within_dataset/conftest.py index d9ed67838..835662d7a 100644 --- a/test/testcases/test_sdk_api/test_chunk_management_within_dataset/conftest.py +++ b/test/testcases/test_sdk_api/test_chunk_management_within_dataset/conftest.py @@ -18,7 +18,7 @@ from time import sleep import pytest -from common import batch_add_chunks +from common import batch_add_chunks, delete_all_chunks from pytest import FixtureRequest from ragflow_sdk import Chunk, DataSet, Document from utils import wait_for @@ -37,7 +37,7 @@ def condition(_dataset: DataSet): def add_chunks_func(request: FixtureRequest, add_document: tuple[DataSet, Document]) -> tuple[DataSet, Document, list[Chunk]]: def cleanup(): try: - document.delete_chunks(ids=[]) + delete_all_chunks(document) except Exception: pass diff --git a/test/testcases/test_sdk_api/test_chunk_management_within_dataset/test_delete_chunks.py b/test/testcases/test_sdk_api/test_chunk_management_within_dataset/test_delete_chunks.py index 319dac0e8..4fd59f01f 100644 --- a/test/testcases/test_sdk_api/test_chunk_management_within_dataset/test_delete_chunks.py +++ b/test/testcases/test_sdk_api/test_chunk_management_within_dataset/test_delete_chunks.py @@ -88,12 +88,12 @@ class TestChunksDeletion: @pytest.mark.parametrize( "payload, expected_message, remaining", [ - pytest.param(None, "TypeError", 5, marks=pytest.mark.skip), + pytest.param(None, "", 5, marks=pytest.mark.p3), pytest.param({"ids": ["invalid_id"]}, "rm_chunk deleted chunks 0, expect 1", 5, marks=pytest.mark.p3), pytest.param("not json", "UnboundLocalError", 5, marks=pytest.mark.skip(reason="pull/6376")), pytest.param(lambda r: {"ids": r[:1]}, "", 4, marks=pytest.mark.p3), pytest.param(lambda r: {"ids": r}, "", 1, marks=pytest.mark.p1), - pytest.param({"ids": []}, "", 0, marks=pytest.mark.p3), + pytest.param({"ids": []}, "", 5, marks=pytest.mark.p3), ], ) def test_basic_scenarios(self, add_chunks_func, payload, expected_message, remaining): @@ -107,7 +107,10 @@ class TestChunksDeletion: document.delete_chunks(**payload) assert expected_message in str(exception_info.value), str(exception_info.value) else: - document.delete_chunks(**payload) + if payload is None: + document.delete_chunks() + else: + document.delete_chunks(**payload) remaining_chunks = document.list_chunks() assert len(remaining_chunks) == remaining, str(remaining_chunks) diff --git a/test/testcases/test_sdk_api/test_dataset_mangement/conftest.py b/test/testcases/test_sdk_api/test_dataset_mangement/conftest.py index 8d53eac2e..998af9499 100644 --- a/test/testcases/test_sdk_api/test_dataset_mangement/conftest.py +++ b/test/testcases/test_sdk_api/test_dataset_mangement/conftest.py @@ -16,13 +16,13 @@ import pytest -from common import batch_create_datasets +from common import batch_create_datasets, delete_all_datasets @pytest.fixture(scope="class") def add_datasets(client, request): def cleanup(): - client.delete_datasets(**{"ids": None}) + delete_all_datasets(client) request.addfinalizer(cleanup) @@ -32,7 +32,7 @@ def add_datasets(client, request): @pytest.fixture(scope="function") def add_datasets_func(client, request): def cleanup(): - client.delete_datasets(**{"ids": None}) + delete_all_datasets(client) request.addfinalizer(cleanup) diff --git a/test/testcases/test_sdk_api/test_dataset_mangement/test_delete_datasets.py b/test/testcases/test_sdk_api/test_dataset_mangement/test_delete_datasets.py index d9a9069f4..dbf0e588e 100644 --- a/test/testcases/test_sdk_api/test_dataset_mangement/test_delete_datasets.py +++ b/test/testcases/test_sdk_api/test_dataset_mangement/test_delete_datasets.py @@ -95,7 +95,7 @@ class TestDatasetsDelete: client.delete_datasets(**payload) datasets = client.list_datasets() - assert len(datasets) == 0, str(datasets) + assert len(datasets) == 3, str(datasets) @pytest.mark.p2 @pytest.mark.usefixtures("add_dataset_func") diff --git a/test/testcases/test_sdk_api/test_file_management_within_dataset/conftest.py b/test/testcases/test_sdk_api/test_file_management_within_dataset/conftest.py index 32be9683a..b60f5f288 100644 --- a/test/testcases/test_sdk_api/test_file_management_within_dataset/conftest.py +++ b/test/testcases/test_sdk_api/test_file_management_within_dataset/conftest.py @@ -16,7 +16,7 @@ import pytest -from common import bulk_upload_documents +from common import bulk_upload_documents, delete_all_documents from pytest import FixtureRequest from ragflow_sdk import DataSet, Document @@ -27,7 +27,7 @@ def add_document_func(request: FixtureRequest, add_dataset: DataSet, ragflow_tmp documents = bulk_upload_documents(dataset, 1, ragflow_tmp_dir) def cleanup(): - dataset.delete_documents(ids=None) + delete_all_documents(dataset) request.addfinalizer(cleanup) return dataset, documents[0] @@ -39,7 +39,7 @@ def add_documents(request: FixtureRequest, add_dataset: DataSet, ragflow_tmp_dir documents = bulk_upload_documents(dataset, 5, ragflow_tmp_dir) def cleanup(): - dataset.delete_documents(ids=None) + delete_all_documents(dataset) request.addfinalizer(cleanup) return dataset, documents @@ -51,7 +51,7 @@ def add_documents_func(request: FixtureRequest, add_dataset_func: DataSet, ragfl documents = bulk_upload_documents(dataset, 3, ragflow_tmp_dir) def cleanup(): - dataset.delete_documents(ids=None) + delete_all_documents(dataset) request.addfinalizer(cleanup) return dataset, documents diff --git a/test/testcases/test_sdk_api/test_file_management_within_dataset/test_delete_documents.py b/test/testcases/test_sdk_api/test_file_management_within_dataset/test_delete_documents.py index 35f146a4d..9fa9d3b1e 100644 --- a/test/testcases/test_sdk_api/test_file_management_within_dataset/test_delete_documents.py +++ b/test/testcases/test_sdk_api/test_file_management_within_dataset/test_delete_documents.py @@ -24,8 +24,8 @@ class TestDocumentsDeletion: @pytest.mark.parametrize( "payload, expected_message, remaining", [ - ({"ids": None}, "", 0), - ({"ids": []}, "", 0), + ({"ids": None}, "", 3), + ({"ids": []}, "", 3), ({"ids": ["invalid_id"]}, "Documents not found: ['invalid_id']", 3), ({"ids": ["\n!?。;!?\"'"]}, "Documents not found: ['\\n!?。;!?\"\\'']", 3), ("not json", "must be a mapping", 3), diff --git a/test/testcases/test_sdk_api/test_session_management/conftest.py b/test/testcases/test_sdk_api/test_session_management/conftest.py index 3f1289ed6..7361b3484 100644 --- a/test/testcases/test_sdk_api/test_session_management/conftest.py +++ b/test/testcases/test_sdk_api/test_session_management/conftest.py @@ -14,7 +14,7 @@ # limitations under the License. # import pytest -from common import batch_add_sessions_with_chat_assistant +from common import batch_add_sessions_with_chat_assistant, delete_all_sessions from pytest import FixtureRequest from ragflow_sdk import Chat, DataSet, Document, Session @@ -24,7 +24,7 @@ def add_sessions_with_chat_assistant(request: FixtureRequest, add_chat_assistant def cleanup(): for chat_assistant in chat_assistants: try: - chat_assistant.delete_sessions(ids=None) + delete_all_sessions(chat_assistant) except Exception : pass @@ -39,7 +39,7 @@ def add_sessions_with_chat_assistant_func(request: FixtureRequest, add_chat_assi def cleanup(): for chat_assistant in chat_assistants: try: - chat_assistant.delete_sessions(ids=None) + delete_all_sessions(chat_assistant) except Exception : pass diff --git a/test/testcases/test_sdk_api/test_session_management/test_delete_sessions_with_chat_assistant.py b/test/testcases/test_sdk_api/test_session_management/test_delete_sessions_with_chat_assistant.py index 5d118af6c..e88b74c4c 100644 --- a/test/testcases/test_sdk_api/test_session_management/test_delete_sessions_with_chat_assistant.py +++ b/test/testcases/test_sdk_api/test_session_management/test_delete_sessions_with_chat_assistant.py @@ -84,12 +84,12 @@ class TestSessionWithChatAssistantDelete: @pytest.mark.parametrize( "payload, expected_message, remaining", [ - pytest.param(None, """TypeError("argument of type \'NoneType\' is not iterable")""", 0, marks=pytest.mark.skip), + pytest.param(None, "", 5, marks=pytest.mark.p3), pytest.param({"ids": ["invalid_id"]}, "The chat doesn't own the session invalid_id", 5, marks=pytest.mark.p3), pytest.param("not json", """AttributeError("\'str\' object has no attribute \'get\'")""", 5, marks=pytest.mark.skip), pytest.param(lambda r: {"ids": r[:1]}, "", 4, marks=pytest.mark.p3), pytest.param(lambda r: {"ids": r}, "", 0, marks=pytest.mark.p1), - pytest.param({"ids": []}, "", 0, marks=pytest.mark.p3), + pytest.param({"ids": []}, "", 5, marks=pytest.mark.p3), ], ) def test_basic_scenarios(self, add_sessions_with_chat_assistant_func, payload, expected_message, remaining): @@ -102,7 +102,10 @@ class TestSessionWithChatAssistantDelete: chat_assistant.delete_sessions(**payload) assert expected_message in str(exception_info.value) else: - chat_assistant.delete_sessions(**payload) + if payload is None: + chat_assistant.delete_sessions() + else: + chat_assistant.delete_sessions(**payload) sessions = chat_assistant.list_sessions() assert len(sessions) == remaining diff --git a/test/testcases/test_web_api/test_chunk_app/test_chunk_routes_unit.py b/test/testcases/test_web_api/test_chunk_app/test_chunk_routes_unit.py index 518250084..5837b3ff0 100644 --- a/test/testcases/test_web_api/test_chunk_app/test_chunk_routes_unit.py +++ b/test/testcases/test_web_api/test_chunk_app/test_chunk_routes_unit.py @@ -673,6 +673,20 @@ def test_rm_chunk_delete_exception_partial_compensation_and_cleanup_unit(monkeyp res = _run(module.rm()) assert res["message"] == "Document not found!", res + _set_request_json(monkeypatch, module, {"doc_id": "doc-1", "chunk_ids": []}) + monkeypatch.setattr( + module.DocumentService, + "get_by_id", + lambda _doc_id: (_ for _ in ()).throw(AssertionError("get_by_id must not run for empty delete payload")), + ) + monkeypatch.setattr( + module.settings.docStoreConn, + "delete", + lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("delete must not run for empty delete payload")), + ) + res = _run(module.rm()) + assert res["code"] == 0, res + monkeypatch.setattr(module.DocumentService, "get_by_id", lambda _doc_id: (True, _DummyDoc())) def _raise_delete(*_args, **_kwargs): diff --git a/test/testcases/test_web_api/test_chunk_app/test_rm_chunks.py b/test/testcases/test_web_api/test_chunk_app/test_rm_chunks.py index b611fcd45..6247eae08 100644 --- a/test/testcases/test_web_api/test_chunk_app/test_rm_chunks.py +++ b/test/testcases/test_web_api/test_chunk_app/test_rm_chunks.py @@ -165,7 +165,7 @@ class TestChunksDeletion: pytest.param("not json", 100, """UnboundLocalError("local variable \'duplicate_messages\' referenced before assignment")""", 5, marks=pytest.mark.skip(reason="pull/6376")), pytest.param(lambda r: {"chunk_ids": r[:1]}, 0, "", 3, marks=pytest.mark.p3), pytest.param(lambda r: {"chunk_ids": r}, 0, "", 0, marks=pytest.mark.p1), - pytest.param({"chunk_ids": []}, 0, "", 0, marks=pytest.mark.p3), + pytest.param({"chunk_ids": []}, 0, "", 5, marks=pytest.mark.p3), ], ) def test_basic_scenarios(self, WebApiAuth, add_chunks_func, payload, expected_code, expected_message, remaining): diff --git a/test/testcases/test_web_api/test_dataset_management/test_dataset_sdk_routes_unit.py b/test/testcases/test_web_api/test_dataset_management/test_dataset_sdk_routes_unit.py index bc81ac125..967c95ef7 100644 --- a/test/testcases/test_web_api/test_dataset_management/test_dataset_sdk_routes_unit.py +++ b/test/testcases/test_web_api/test_dataset_management/test_dataset_sdk_routes_unit.py @@ -472,14 +472,8 @@ def test_delete_route_error_summary_matrix_unit(monkeypatch): assert res["data"]["errors"], res req_state["ids"] = None - monkeypatch.setattr( - module.KnowledgebaseService, - "query", - lambda **_kwargs: (_ for _ in ()).throw(module.OperationalError("db down")), - ) res = _run(inspect.unwrap(module.delete)("tenant-1")) - assert res["code"] == module.RetCode.DATA_ERROR, res - assert res["message"] == "Database operation failed", res + assert res["code"] == module.RetCode.SUCCESS, res @pytest.mark.p2 diff --git a/test/testcases/test_web_api/test_kb_app/conftest.py b/test/testcases/test_web_api/test_kb_app/conftest.py index 0a435483c..8a2387391 100644 --- a/test/testcases/test_web_api/test_kb_app/conftest.py +++ b/test/testcases/test_web_api/test_kb_app/conftest.py @@ -14,7 +14,7 @@ # limitations under the License. # import pytest -from common import batch_create_datasets +from common import batch_create_datasets, list_kbs, rm_kb from libs.auth import RAGFlowWebApiAuth from pytest import FixtureRequest from ragflow_sdk import RAGFlow @@ -22,17 +22,31 @@ from ragflow_sdk import RAGFlow @pytest.fixture(scope="class") def add_datasets(request: FixtureRequest, client: RAGFlow, WebApiAuth: RAGFlowWebApiAuth) -> list[str]: + dataset_ids = batch_create_datasets(WebApiAuth, 5) + def cleanup(): - client.delete_datasets(ids=None) + # Web KB cleanup cannot call SDK dataset bulk delete with empty ids; deletion must stay explicit. + res = list_kbs(WebApiAuth, params={"page_size": 1000}) + existing_ids = {kb["id"] for kb in res["data"]["kbs"]} + for dataset_id in dataset_ids: + if dataset_id in existing_ids: + rm_kb(WebApiAuth, {"kb_id": dataset_id}) request.addfinalizer(cleanup) - return batch_create_datasets(WebApiAuth, 5) + return dataset_ids @pytest.fixture(scope="function") def add_datasets_func(request: FixtureRequest, client: RAGFlow, WebApiAuth: RAGFlowWebApiAuth) -> list[str]: + dataset_ids = batch_create_datasets(WebApiAuth, 3) + def cleanup(): - client.delete_datasets(ids=None) + # Web KB cleanup cannot call SDK dataset bulk delete with empty ids; deletion must stay explicit. + res = list_kbs(WebApiAuth, params={"page_size": 1000}) + existing_ids = {kb["id"] for kb in res["data"]["kbs"]} + for dataset_id in dataset_ids: + if dataset_id in existing_ids: + rm_kb(WebApiAuth, {"kb_id": dataset_id}) request.addfinalizer(cleanup) - return batch_create_datasets(WebApiAuth, 3) + return dataset_ids