Feat: update and add new tests for web api apps (#12714)

### What problem does this PR solve?

This PR adds missing web API tests (system, search, KB, LLM, plugin,
connector). It also addresses a contract mismatch that was causing test
failures: metadata updates did not persist new keys (update‑only
behavior).

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
- [x] Other (please describe): Test coverage expansion and test helper
instrumentation
This commit is contained in:
6ba3i
2026-01-20 19:12:15 +08:00
committed by GitHub
parent aee9860970
commit 960ecd3158
14 changed files with 1623 additions and 11 deletions

View File

@ -13,6 +13,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
import json
import os
import time
import uuid
from pathlib import Path
import requests
@ -30,6 +34,137 @@ DIALOG_APP_URL = f"/{VERSION}/dialog"
# SESSION_WITH_AGENT_API_URL = "/api/v1/agents/{agent_id}/sessions"
MEMORY_API_URL = f"/api/{VERSION}/memories"
MESSAGE_API_URL = f"/api/{VERSION}/messages"
API_APP_URL = f"/{VERSION}/api"
SYSTEM_APP_URL = f"/{VERSION}/system"
LLM_APP_URL = f"/{VERSION}/llm"
PLUGIN_APP_URL = f"/{VERSION}/plugin"
SEARCH_APP_URL = f"/{VERSION}/search"
def _http_debug_enabled():
return os.getenv("TEST_HTTP_DEBUG") == "1"
def _redact_payload(payload):
if not isinstance(payload, dict):
return payload
redacted = {}
for key, value in payload.items():
if any(token in key.lower() for token in ("api_key", "password", "token", "secret", "authorization")):
redacted[key] = "***redacted***"
else:
redacted[key] = value
return redacted
def _log_http_debug(method, url, req_id, payload, status, text, resp_json, elapsed_ms):
if not _http_debug_enabled():
return
payload_summary = _redact_payload(payload)
print(f"[HTTP DEBUG] {method} {url} req_id={req_id} elapsed_ms={elapsed_ms:.1f}")
print(f"[HTTP DEBUG] request_payload={json.dumps(payload_summary, default=str)}")
print(f"[HTTP DEBUG] status={status}")
print(f"[HTTP DEBUG] response_text={text}")
print(f"[HTTP DEBUG] response_json={json.dumps(resp_json, default=str) if resp_json is not None else None}")
# API APP
def api_new_token(auth, payload=None, *, headers=HEADERS, data=None):
if payload is None:
payload = {}
res = requests.post(url=f"{HOST_ADDRESS}{API_APP_URL}/new_token", headers=headers, auth=auth, json=payload, data=data)
return res.json()
def api_token_list(auth, params=None, *, headers=HEADERS):
res = requests.get(url=f"{HOST_ADDRESS}{API_APP_URL}/token_list", headers=headers, auth=auth, params=params)
return res.json()
def api_rm_token(auth, payload=None, *, headers=HEADERS, data=None):
res = requests.post(url=f"{HOST_ADDRESS}{API_APP_URL}/rm", headers=headers, auth=auth, json=payload, data=data)
return res.json()
def api_stats(auth, params=None, *, headers=HEADERS):
res = requests.get(url=f"{HOST_ADDRESS}{API_APP_URL}/stats", headers=headers, auth=auth, params=params)
return res.json()
# SYSTEM APP
def system_new_token(auth, payload=None, *, headers=HEADERS, data=None):
res = requests.post(url=f"{HOST_ADDRESS}{SYSTEM_APP_URL}/new_token", headers=headers, auth=auth, json=payload, data=data)
return res.json()
def system_token_list(auth, params=None, *, headers=HEADERS):
res = requests.get(url=f"{HOST_ADDRESS}{SYSTEM_APP_URL}/token_list", headers=headers, auth=auth, params=params)
return res.json()
def system_delete_token(auth, token, *, headers=HEADERS):
res = requests.delete(url=f"{HOST_ADDRESS}{SYSTEM_APP_URL}/token/{token}", headers=headers, auth=auth)
return res.json()
def system_status(auth, params=None, *, headers=HEADERS):
res = requests.get(url=f"{HOST_ADDRESS}{SYSTEM_APP_URL}/status", headers=headers, auth=auth, params=params)
return res.json()
def system_version(auth, params=None, *, headers=HEADERS):
res = requests.get(url=f"{HOST_ADDRESS}{SYSTEM_APP_URL}/version", headers=headers, auth=auth, params=params)
return res.json()
def system_config(auth=None, params=None, *, headers=HEADERS):
res = requests.get(url=f"{HOST_ADDRESS}{SYSTEM_APP_URL}/config", headers=headers, auth=auth, params=params)
return res.json()
# LLM APP
def llm_factories(auth, params=None, *, headers=HEADERS):
res = requests.get(url=f"{HOST_ADDRESS}{LLM_APP_URL}/factories", headers=headers, auth=auth, params=params)
return res.json()
def llm_list(auth, params=None, *, headers=HEADERS):
res = requests.get(url=f"{HOST_ADDRESS}{LLM_APP_URL}/list", headers=headers, auth=auth, params=params)
return res.json()
# PLUGIN APP
def plugin_llm_tools(auth, params=None, *, headers=HEADERS):
res = requests.get(url=f"{HOST_ADDRESS}{PLUGIN_APP_URL}/llm_tools", headers=headers, auth=auth, params=params)
return res.json()
# SEARCH APP
def search_create(auth, payload=None, *, headers=HEADERS, data=None):
res = requests.post(url=f"{HOST_ADDRESS}{SEARCH_APP_URL}/create", headers=headers, auth=auth, json=payload, data=data)
return res.json()
def search_update(auth, payload=None, *, headers=HEADERS, data=None):
res = requests.post(url=f"{HOST_ADDRESS}{SEARCH_APP_URL}/update", headers=headers, auth=auth, json=payload, data=data)
return res.json()
def search_detail(auth, params=None, *, headers=HEADERS):
res = requests.get(url=f"{HOST_ADDRESS}{SEARCH_APP_URL}/detail", headers=headers, auth=auth, params=params)
return res.json()
def search_list(auth, params=None, payload=None, *, headers=HEADERS, data=None):
if payload is None:
payload = {}
res = requests.post(url=f"{HOST_ADDRESS}{SEARCH_APP_URL}/list", headers=headers, auth=auth, params=params, json=payload, data=data)
return res.json()
def search_rm(auth, payload=None, *, headers=HEADERS, data=None):
res = requests.post(url=f"{HOST_ADDRESS}{SEARCH_APP_URL}/rm", headers=headers, auth=auth, json=payload, data=data)
return res.json()
# KB APP
@ -60,6 +195,77 @@ def detail_kb(auth, params=None, *, headers=HEADERS):
return res.json()
def kb_get_meta(auth, params=None, *, headers=HEADERS):
res = requests.get(url=f"{HOST_ADDRESS}{KB_APP_URL}/get_meta", headers=headers, auth=auth, params=params)
return res.json()
def kb_basic_info(auth, params=None, *, headers=HEADERS):
res = requests.get(url=f"{HOST_ADDRESS}{KB_APP_URL}/basic_info", headers=headers, auth=auth, params=params)
return res.json()
def kb_update_metadata_setting(auth, payload=None, *, headers=HEADERS, data=None):
res = requests.post(url=f"{HOST_ADDRESS}{KB_APP_URL}/update_metadata_setting", headers=headers, auth=auth, json=payload, data=data)
return res.json()
def kb_list_pipeline_logs(auth, params=None, payload=None, *, headers=HEADERS, data=None):
if payload is None:
payload = {}
res = requests.post(url=f"{HOST_ADDRESS}{KB_APP_URL}/list_pipeline_logs", headers=headers, auth=auth, params=params, json=payload, data=data)
return res.json()
def kb_list_pipeline_dataset_logs(auth, params=None, payload=None, *, headers=HEADERS, data=None):
if payload is None:
payload = {}
res = requests.post(url=f"{HOST_ADDRESS}{KB_APP_URL}/list_pipeline_dataset_logs", headers=headers, auth=auth, params=params, json=payload, data=data)
return res.json()
def kb_delete_pipeline_logs(auth, params=None, payload=None, *, headers=HEADERS, data=None):
if payload is None:
payload = {}
res = requests.post(url=f"{HOST_ADDRESS}{KB_APP_URL}/delete_pipeline_logs", headers=headers, auth=auth, params=params, json=payload, data=data)
return res.json()
def kb_pipeline_log_detail(auth, params=None, *, headers=HEADERS):
res = requests.get(url=f"{HOST_ADDRESS}{KB_APP_URL}/pipeline_log_detail", headers=headers, auth=auth, params=params)
return res.json()
def kb_run_graphrag(auth, payload=None, *, headers=HEADERS, data=None):
res = requests.post(url=f"{HOST_ADDRESS}{KB_APP_URL}/run_graphrag", headers=headers, auth=auth, json=payload, data=data)
return res.json()
def kb_trace_graphrag(auth, params=None, *, headers=HEADERS):
res = requests.get(url=f"{HOST_ADDRESS}{KB_APP_URL}/trace_graphrag", headers=headers, auth=auth, params=params)
return res.json()
def kb_run_raptor(auth, payload=None, *, headers=HEADERS, data=None):
res = requests.post(url=f"{HOST_ADDRESS}{KB_APP_URL}/run_raptor", headers=headers, auth=auth, json=payload, data=data)
return res.json()
def kb_trace_raptor(auth, params=None, *, headers=HEADERS):
res = requests.get(url=f"{HOST_ADDRESS}{KB_APP_URL}/trace_raptor", headers=headers, auth=auth, params=params)
return res.json()
def kb_run_mindmap(auth, payload=None, *, headers=HEADERS, data=None):
res = requests.post(url=f"{HOST_ADDRESS}{KB_APP_URL}/run_mindmap", headers=headers, auth=auth, json=payload, data=data)
return res.json()
def kb_trace_mindmap(auth, params=None, *, headers=HEADERS):
res = requests.get(url=f"{HOST_ADDRESS}{KB_APP_URL}/trace_mindmap", headers=headers, auth=auth, params=params)
return res.json()
def list_tags_from_kbs(auth, params=None, *, headers=HEADERS):
res = requests.get(url=f"{HOST_ADDRESS}{KB_APP_URL}/tags", headers=headers, auth=auth, params=params)
return res.json()
@ -76,7 +282,7 @@ def rm_tags(auth, dataset_id, payload=None, *, headers=HEADERS, data=None):
def rename_tags(auth, dataset_id, payload=None, *, headers=HEADERS, data=None):
res = requests.post(url=f"{HOST_ADDRESS}{KB_APP_URL}/{dataset_id}/rename_tags", headers=headers, auth=auth, json=payload, data=data)
res = requests.post(url=f"{HOST_ADDRESS}{KB_APP_URL}/{dataset_id}/rename_tag", headers=headers, auth=auth, json=payload, data=data)
return res.json()
@ -154,6 +360,46 @@ def parse_documents(auth, payload=None, *, headers=HEADERS, data=None):
return res.json()
def document_filter(auth, payload=None, *, headers=HEADERS, data=None):
res = requests.post(url=f"{HOST_ADDRESS}{DOCUMENT_APP_URL}/filter", headers=headers, auth=auth, json=payload, data=data)
return res.json()
def document_infos(auth, payload=None, *, headers=HEADERS, data=None):
res = requests.post(url=f"{HOST_ADDRESS}{DOCUMENT_APP_URL}/infos", headers=headers, auth=auth, json=payload, data=data)
return res.json()
def document_metadata_summary(auth, payload=None, *, headers=HEADERS, data=None):
res = requests.post(url=f"{HOST_ADDRESS}{DOCUMENT_APP_URL}/metadata/summary", headers=headers, auth=auth, json=payload, data=data)
return res.json()
def document_metadata_update(auth, payload=None, *, headers=HEADERS, data=None):
res = requests.post(url=f"{HOST_ADDRESS}{DOCUMENT_APP_URL}/metadata/update", headers=headers, auth=auth, json=payload, data=data)
return res.json()
def document_update_metadata_setting(auth, payload=None, *, headers=HEADERS, data=None):
res = requests.post(url=f"{HOST_ADDRESS}{DOCUMENT_APP_URL}/update_metadata_setting", headers=headers, auth=auth, json=payload, data=data)
return res.json()
def document_change_status(auth, payload=None, *, headers=HEADERS, data=None):
res = requests.post(url=f"{HOST_ADDRESS}{DOCUMENT_APP_URL}/change_status", headers=headers, auth=auth, json=payload, data=data)
return res.json()
def document_rename(auth, payload=None, *, headers=HEADERS, data=None):
res = requests.post(url=f"{HOST_ADDRESS}{DOCUMENT_APP_URL}/rename", headers=headers, auth=auth, json=payload, data=data)
return res.json()
def document_set_meta(auth, payload=None, *, headers=HEADERS, data=None):
res = requests.post(url=f"{HOST_ADDRESS}{DOCUMENT_APP_URL}/set_meta", headers=headers, auth=auth, json=payload, data=data)
return res.json()
def bulk_upload_documents(auth, kb_id, num, tmp_path):
fps = []
for i in range(num):
@ -208,8 +454,33 @@ def batch_add_chunks(auth, doc_id, num):
# DIALOG APP
def create_dialog(auth, payload=None, *, headers=HEADERS, data=None):
res = requests.post(url=f"{HOST_ADDRESS}{DIALOG_APP_URL}/set", headers=headers, auth=auth, json=payload, data=data)
return res.json()
if payload is None:
payload = {}
url = f"{HOST_ADDRESS}{DIALOG_APP_URL}/set"
req_id = str(uuid.uuid4())
req_headers = dict(headers)
req_headers["X-Request-ID"] = req_id
start = time.monotonic()
res = requests.post(url=url, headers=req_headers, auth=auth, json=payload, data=data)
elapsed_ms = (time.monotonic() - start) * 1000
resp_json = None
json_error = None
try:
resp_json = res.json()
except ValueError as exc:
json_error = exc
_log_http_debug("POST", url, req_id, payload, res.status_code, res.text, resp_json, elapsed_ms)
if _http_debug_enabled():
if not res.ok or (resp_json is not None and resp_json.get("code") != 0):
payload_summary = _redact_payload(payload)
raise AssertionError(
"HTTP helper failure: "
f"req_id={req_id} url={url} status={res.status_code} "
f"payload={payload_summary} response={res.text}"
)
if json_error:
raise json_error
return resp_json
def update_dialog(auth, payload=None, *, headers=HEADERS, data=None):
@ -238,11 +509,21 @@ def batch_create_dialogs(auth, num, kb_ids=None):
dialog_ids = []
for i in range(num):
if kb_ids:
prompt_config = {
"system": "You are a helpful assistant. Use the following knowledge to answer questions: {knowledge}",
"parameters": [{"key": "knowledge", "optional": False}],
}
else:
prompt_config = {
"system": "You are a helpful assistant.",
"parameters": [],
}
payload = {
"name": f"dialog_{i}",
"description": f"Test dialog {i}",
"kb_ids": kb_ids,
"prompt_config": {"system": "You are a helpful assistant. Use the following knowledge to answer questions: {knowledge}", "parameters": [{"key": "knowledge", "optional": False}]},
"prompt_config": prompt_config,
"top_n": 6,
"top_k": 1024,
"similarity_threshold": 0.1,
@ -250,6 +531,12 @@ def batch_create_dialogs(auth, num, kb_ids=None):
"llm_setting": {"model": "gpt-3.5-turbo", "temperature": 0.7},
}
res = create_dialog(auth, payload)
if res is None or res.get("code") != 0:
uses_knowledge = "{knowledge}" in payload["prompt_config"]["system"]
raise AssertionError(
"batch_create_dialogs failed: "
f"res={res} kb_ids_len={len(kb_ids)} uses_knowledge={uses_knowledge}"
)
if res["code"] == 0:
dialog_ids.append(res["data"]["id"])
return dialog_ids