- Replaced direct FastGPT client usage with a backend-neutral chat interface, allowing for improved flexibility and maintainability. - Introduced a new `ChatBackend` protocol and implemented `FastGPTBackend` to handle chat operations. - Updated the chat endpoint to utilize the new backend structure, enhancing the handling of chat requests and responses. - Added comprehensive tests to ensure compatibility and functionality of the new backend integration.
196 lines
5.4 KiB
Python
196 lines
5.4 KiB
Python
"""Characterization tests for the public HTTP API schemas.
|
|
|
|
These tests freeze the current contract before the FastGPT backend is replaced.
|
|
They should change only when the teams integrating with this service agree to a
|
|
contract change.
|
|
"""
|
|
|
|
from collections.abc import Callable
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from pydantic import BaseModel, ValidationError
|
|
|
|
from src.schemas.models import (
|
|
ProcessRequest_chat,
|
|
ProcessRequest_get,
|
|
ProcessRequest_set,
|
|
ProcessResponse_chat,
|
|
ProcessResponse_get,
|
|
ProcessResponse_set,
|
|
)
|
|
|
|
|
|
SESSION_ID = "session-001"
|
|
TIMESTAMP = "20260725120000"
|
|
|
|
|
|
def chat_request_payload() -> dict[str, Any]:
|
|
return {
|
|
"sessionId": SESSION_ID,
|
|
"timeStamp": TIMESTAMP,
|
|
"text": "发生了交通事故",
|
|
}
|
|
|
|
|
|
def chat_response_payload() -> dict[str, Any]:
|
|
return {
|
|
"sessionId": SESSION_ID,
|
|
"timeStamp": TIMESTAMP,
|
|
"outputText": "请描述事故经过。",
|
|
"nextStage": "通话中",
|
|
"nextStageCode": "1002",
|
|
"code": "200",
|
|
}
|
|
|
|
|
|
def get_request_payload() -> dict[str, Any]:
|
|
return {
|
|
"sessionId": SESSION_ID,
|
|
"timeStamp": TIMESTAMP,
|
|
"key": "acdinfo",
|
|
}
|
|
|
|
|
|
def get_response_payload() -> dict[str, Any]:
|
|
return {
|
|
"sessionId": SESSION_ID,
|
|
"timeStamp": TIMESTAMP,
|
|
"value": '{"jdcsl": "2"}',
|
|
"code": "200",
|
|
}
|
|
|
|
|
|
def set_request_payload() -> dict[str, Any]:
|
|
return {
|
|
"sessionId": SESSION_ID,
|
|
"timeStamp": TIMESTAMP,
|
|
"key": "hphm1",
|
|
"value": "沪A12345",
|
|
}
|
|
|
|
|
|
def set_response_payload() -> dict[str, Any]:
|
|
return {
|
|
"sessionId": SESSION_ID,
|
|
"timeStamp": TIMESTAMP,
|
|
"code": "200",
|
|
}
|
|
|
|
|
|
def test_chat_request_defaults_are_backward_compatible() -> None:
|
|
request = ProcessRequest_chat(**chat_request_payload())
|
|
|
|
assert request.model_dump() == {
|
|
**chat_request_payload(),
|
|
"needFormUpdate": False,
|
|
"useTextChunk": False,
|
|
}
|
|
|
|
|
|
def test_chat_response_shape_is_backward_compatible() -> None:
|
|
response = ProcessResponse_chat(**chat_response_payload())
|
|
|
|
assert response.model_dump() == {
|
|
**chat_response_payload(),
|
|
"formUpdate": {},
|
|
"msg": None,
|
|
}
|
|
|
|
|
|
def test_get_info_shapes_are_backward_compatible() -> None:
|
|
request = ProcessRequest_get(**get_request_payload())
|
|
response = ProcessResponse_get(**get_response_payload())
|
|
|
|
assert request.model_dump() == {
|
|
**get_request_payload(),
|
|
"includeInputInfo": False,
|
|
}
|
|
assert response.model_dump() == {
|
|
**get_response_payload(),
|
|
"msg": None,
|
|
}
|
|
|
|
|
|
def test_set_info_shapes_are_backward_compatible() -> None:
|
|
request = ProcessRequest_set(**set_request_payload())
|
|
response = ProcessResponse_set(**set_response_payload())
|
|
|
|
assert request.model_dump() == {
|
|
**set_request_payload(),
|
|
"includeInputInfo": False,
|
|
}
|
|
assert response.model_dump() == {
|
|
**set_response_payload(),
|
|
"msg": None,
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("model", "payload_factory", "required_field"),
|
|
[
|
|
(ProcessRequest_chat, chat_request_payload, "sessionId"),
|
|
(ProcessRequest_chat, chat_request_payload, "timeStamp"),
|
|
(ProcessRequest_chat, chat_request_payload, "text"),
|
|
(ProcessRequest_get, get_request_payload, "key"),
|
|
(ProcessRequest_set, set_request_payload, "key"),
|
|
(ProcessRequest_set, set_request_payload, "value"),
|
|
(ProcessResponse_chat, chat_response_payload, "nextStageCode"),
|
|
(ProcessResponse_get, get_response_payload, "value"),
|
|
],
|
|
)
|
|
def test_required_fields_remain_required(
|
|
model: type[BaseModel],
|
|
payload_factory: Callable[[], dict[str, Any]],
|
|
required_field: str,
|
|
) -> None:
|
|
payload = payload_factory()
|
|
payload.pop(required_field)
|
|
|
|
with pytest.raises(ValidationError):
|
|
model(**payload)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("model", "payload_factory", "field_name", "invalid_value"),
|
|
[
|
|
(ProcessRequest_chat, chat_request_payload, "sessionId", "s" * 65),
|
|
(ProcessRequest_chat, chat_request_payload, "timeStamp", "t" * 33),
|
|
(ProcessRequest_get, get_request_payload, "sessionId", "s" * 65),
|
|
(ProcessRequest_set, set_request_payload, "timeStamp", "t" * 33),
|
|
(ProcessResponse_chat, chat_response_payload, "nextStage", "n" * 33),
|
|
(ProcessResponse_chat, chat_response_payload, "nextStageCode", "10020"),
|
|
(ProcessResponse_chat, chat_response_payload, "code", "10000"),
|
|
(ProcessResponse_get, get_response_payload, "code", "10000"),
|
|
(ProcessResponse_set, set_response_payload, "code", "10000"),
|
|
],
|
|
)
|
|
def test_public_length_limits_are_enforced(
|
|
model: type[BaseModel],
|
|
payload_factory: Callable[[], dict[str, Any]],
|
|
field_name: str,
|
|
invalid_value: str,
|
|
) -> None:
|
|
payload = payload_factory()
|
|
payload[field_name] = invalid_value
|
|
|
|
with pytest.raises(ValidationError):
|
|
model(**payload)
|
|
|
|
|
|
def test_get_info_value_remains_a_json_encoded_string() -> None:
|
|
response = ProcessResponse_get(**get_response_payload())
|
|
|
|
assert isinstance(response.value, str)
|
|
assert response.value == '{"jdcsl": "2"}'
|
|
|
|
|
|
def test_chat_form_update_remains_unstructured_for_compatibility() -> None:
|
|
form_update = {"jdcsl": 2, "ywrysw": False}
|
|
response = ProcessResponse_chat(
|
|
**chat_response_payload(),
|
|
formUpdate=form_update,
|
|
)
|
|
|
|
assert response.formUpdate == form_update
|