feat(services): assignment 6 skeleton - app service, field registry, in-memory repository

- fields_registry: 24 business keys in acd/human1/human2 groups with
  value_type, writable and SensitiveLevel metadata; strict bool/int
  parsing (no bool('0') trap); INTERNAL_FIELD_KEYS protection list
- repository: SessionRepository protocol + InMemorySessionRepository
  (idempotent delete; PostgreSQL swap-in is assignment 7)
- AccidentAppService: chat/set_info/get_info/delete_session share one
  business state - chat injects latest repository fields into the graph
  and writes back validated field diffs; set/get/delete make zero LLM
  calls and zero helper conversations (includeInputInfo accepted but
  unused); unknown get keys keep the '' JSON-empty-string contract,
  unknown set keys are rejected by whitelist
- ChatInput.injected_fields neutral contract extension; LangGraphBackend
  merges injected fields into graph input (set_info visible next chat)
- endpoints: thin layer - langgraph mode routes business state through
  the app service (fastgpt mode keeps the legacy path); delete cleans
  repository + graph checkpoint, idempotent
- tests: registry completeness (sfzmwh/sjwh/csbw included), app service
  combos with LLM-call spy, HTTP acceptance flow (set -> get -> chat ->
  groups -> delete -> no leak, idempotent re-delete)
- docs/assignment6-compat.md: behavior diff list vs FastGPT status quo
  and the audit backlog (arbitrary set_info key usage, stream formUpdate
  write-back, sensitive-field display boundary)

Verified: 200 unit tests (172 existing + 28 new)
This commit is contained in:
Eric Wang
2026-10-22 20:50:10 +08:00
parent 6c1fbd0295
commit 8717d9066c
12 changed files with 882 additions and 16 deletions
+106
View File
@@ -0,0 +1,106 @@
"""作业 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"