2026-10-22 20:50:10 +08:00
|
|
|
|
"""作业 6 验收演示的 HTTP 层组合测试。
|
|
|
|
|
|
|
|
|
|
|
|
链路(对应计划作业 6):创建会话 -> /set_info 写车牌 -> /get_info 读回
|
|
|
|
|
|
-> /chat 继续核实 -> 分组读值 -> /delete_session -> 再读与再聊不泄漏。
|
|
|
|
|
|
|
|
|
|
|
|
fake LLM(LLM_PROVIDER=fake)保证本测试离线可跑、零真实模型调用。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
|
|
|
|
|
|
from src.agent.llm import workflow_fake_llm
|
|
|
|
|
|
from src.core.config import Config
|
|
|
|
|
|
from src.main import app
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
|
|
|
|
|
def client(monkeypatch):
|
|
|
|
|
|
"""langgraph 后端 + fake LLM 的测试客户端(lifespan 完整初始化)。"""
|
|
|
|
|
|
monkeypatch.setattr(Config, "AGENT_BACKEND", "langgraph")
|
2026-10-24 13:41:54 +08:00
|
|
|
|
# 通用 API 契约测试不连接用户 .env 中的真实持久化数据库。
|
|
|
|
|
|
monkeypatch.setattr(Config, "AGENT_STORAGE", "memory")
|
2026-10-22 20:50:10 +08:00
|
|
|
|
monkeypatch.setattr(Config, "LLM_PROVIDER", "fake")
|
|
|
|
|
|
|
|
|
|
|
|
import src.agent.llm as llm_module
|
|
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
|
llm_module, "build_llm_service", lambda *a, **k: workflow_fake_llm()
|
|
|
|
|
|
)
|
|
|
|
|
|
with TestClient(app) as test_client:
|
|
|
|
|
|
yield test_client
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
TS = "20261022120000"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_assignment6_acceptance_flow(client):
|
|
|
|
|
|
session = "a6-acceptance-001"
|
|
|
|
|
|
|
|
|
|
|
|
# set_info 写入车牌(零 LLM:fake 未注入任何 responder,调用即报错)
|
|
|
|
|
|
response = client.post("/set_info", json={
|
|
|
|
|
|
"sessionId": session, "timeStamp": TS,
|
|
|
|
|
|
"key": "hphm1", "value": "苏B12345",
|
|
|
|
|
|
})
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
assert response.json()["code"] == "200"
|
|
|
|
|
|
|
|
|
|
|
|
# get_info 单键读回
|
|
|
|
|
|
response = client.post("/get_info", json={
|
|
|
|
|
|
"sessionId": session, "timeStamp": TS, "key": "hphm1",
|
|
|
|
|
|
})
|
|
|
|
|
|
assert response.json()["value"] == '"苏B12345"'
|
|
|
|
|
|
|
|
|
|
|
|
# chat 使用业务状态继续(非流式,协议层解析 state 前缀)
|
|
|
|
|
|
response = client.post("/chat", json={
|
|
|
|
|
|
"sessionId": session, "timeStamp": TS,
|
|
|
|
|
|
"text": "我是刚才报警的驾驶员", "clientMode": "direct",
|
|
|
|
|
|
})
|
|
|
|
|
|
body = response.json()
|
|
|
|
|
|
assert body["code"] == "200"
|
|
|
|
|
|
assert body["nextStageCode"] in ("1001", "1002")
|
|
|
|
|
|
|
|
|
|
|
|
# 分组读值:三个分组齐全,未填字段为空串
|
|
|
|
|
|
response = client.post("/get_info", json={
|
|
|
|
|
|
"sessionId": session, "timeStamp": TS, "key": "all",
|
|
|
|
|
|
})
|
|
|
|
|
|
groups = json.loads(response.json()["value"])
|
|
|
|
|
|
assert set(groups) == {"acdinfo", "acdhuman1", "acdhuman2"}
|
|
|
|
|
|
assert groups["acdhuman1"]["hphm1"] == "苏B12345"
|
|
|
|
|
|
assert groups["acdhuman1"]["xm1"] == ""
|
|
|
|
|
|
|
|
|
|
|
|
# 未知 key 兼容:JSON 空串
|
|
|
|
|
|
response = client.post("/get_info", json={
|
|
|
|
|
|
"sessionId": session, "timeStamp": TS, "key": "no_such_key",
|
|
|
|
|
|
})
|
|
|
|
|
|
assert response.json()["value"] == '""'
|
|
|
|
|
|
|
|
|
|
|
|
# set_info 未知 key:白名单拒绝
|
|
|
|
|
|
response = client.post("/set_info", json={
|
|
|
|
|
|
"sessionId": session, "timeStamp": TS,
|
|
|
|
|
|
"key": "no_such_key", "value": "x",
|
|
|
|
|
|
})
|
|
|
|
|
|
assert response.json()["code"] == "500"
|
|
|
|
|
|
|
|
|
|
|
|
# 删除 -> 再读为空
|
|
|
|
|
|
response = client.request("DELETE", "/delete_session", json={
|
|
|
|
|
|
"sessionId": session, "timeStamp": TS,
|
|
|
|
|
|
})
|
|
|
|
|
|
assert response.json()["code"] == "200"
|
|
|
|
|
|
response = client.post("/get_info", json={
|
|
|
|
|
|
"sessionId": session, "timeStamp": TS, "key": "hphm1",
|
|
|
|
|
|
})
|
|
|
|
|
|
assert response.json()["value"] == '""'
|
|
|
|
|
|
|
|
|
|
|
|
# 重复删除幂等成功
|
|
|
|
|
|
response = client.request("DELETE", "/delete_session", json={
|
|
|
|
|
|
"sessionId": session, "timeStamp": TS,
|
|
|
|
|
|
})
|
|
|
|
|
|
assert response.json()["code"] == "200"
|
|
|
|
|
|
|
|
|
|
|
|
# 删除后再聊天:不注入旧业务数据,服务正常应答
|
|
|
|
|
|
response = client.post("/chat", json={
|
|
|
|
|
|
"sessionId": session, "timeStamp": TS,
|
|
|
|
|
|
"text": "新对话", "clientMode": "direct",
|
|
|
|
|
|
})
|
|
|
|
|
|
assert response.json()["code"] == "200"
|
2026-10-23 19:48:29 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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"
|