fix(api): report checkpoint deletion failures and classify SSE errors

Delete checkpoints before removing business fields. Suppress only an
explicit CheckpointNotFoundError; propagate storage failures and
cancellation so the endpoint cannot report false success. Prefer async
checkpoint deletion and fail clearly when cleanup is unsupported.

Classify SSE error events as 401 for authentication, 429 for rate limits,
and 500 for other failures, using the same fixed messages as non-stream
responses. Errors terminate the stream without a success done event or
provider exception text in the response.

Validation: 286 offline tests passed (19 added), including failed-delete
HTTP retry with preserved business/checkpoint state, explicit missing
threads, cancellation, missing cleanup capability, and HTTP SSE errors
before and after text output.
This commit is contained in:
Eric Wang
2026-10-23 19:48:29 +08:00
parent 24e2cfbe10
commit 3caf17494f
9 changed files with 260 additions and 15 deletions
+40
View File
@@ -104,3 +104,43 @@ def test_assignment6_acceptance_flow(client):
"text": "新对话", "clientMode": "direct",
})
assert response.json()["code"] == "200"
def test_delete_checkpoint_failure_returns_error_and_retry_cleans_session(client, monkeypatch):
from unittest.mock import AsyncMock
from src.core import fastgpt_client as wiring
session = "delete-retry"
payload = {"sessionId": session, "timeStamp": TS}
assert client.post("/set_info", json={
**payload, "key": "hphm1", "value": "test-plate",
}).json()["code"] == "200"
assert client.post("/chat", json={
**payload, "text": "新对话",
}).json()["nextStageCode"] == "1001"
saver = wiring._langgraph_graph.checkpointer
config = {"configurable": {"thread_id": session}}
assert saver.get_tuple(config) is not None
original_delete = saver.adelete_thread
failing_delete = AsyncMock(side_effect=ConnectionError("private storage detail"))
monkeypatch.setattr(saver, "adelete_thread", failing_delete)
response = client.request("DELETE", "/delete_session", json=payload)
assert response.json()["code"] == "500"
assert response.json()["msg"] == "删除会话失败"
assert "private storage detail" not in response.text
failing_delete.assert_awaited_once_with(session)
assert saver.get_tuple(config) is not None
assert json.loads(client.post("/get_info", json={
**payload, "key": "hphm1",
}).json()["value"]) == "test-plate"
monkeypatch.setattr(saver, "adelete_thread", original_delete)
assert client.request("DELETE", "/delete_session", json=payload).json()["code"] == "200"
assert saver.get_tuple(config) is None
assert json.loads(client.post("/get_info", json={
**payload, "key": "hphm1",
}).json()["value"]) == ""
assert client.post("/chat", json={
**payload, "text": "新对话",
}).json()["nextStageCode"] == "1001"