"""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