"""作业 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") 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" 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"