Files
ai-video-fullstack/backend/tests/test_public_contract_snapshots.py
T
Xin Wang 2649d64b55
Quality gate / backend (push) Canceled after 0s
Quality gate / frontend (push) Canceled after 0s
feat: harden public OpenAI API contracts
2026-09-07 14:25:48 +08:00

187 lines
7.0 KiB
Python

from __future__ import annotations
import json
import unittest
from jsonschema import Draft202012Validator
from jsonschema.exceptions import ValidationError as JSONSchemaValidationError
from scripts.export_contracts import CONTRACT_DIRECTORY, contract_documents
from services.openai_realtime.bridge import OpenAIRealtimeBridge
from services.openai_realtime.events import (
RealtimeEventError,
error_event,
require_event,
)
from services.openai_realtime.schemas import (
EXTENSION_SERVER_EVENT_TYPES,
SERVER_EVENT_TYPES,
RealtimeServerEvent,
)
class PublicContractSnapshotTests(unittest.TestCase):
def test_generated_contracts_match_committed_snapshots(self):
for name, generated in contract_documents().items():
path = CONTRACT_DIRECTORY / name
self.assertTrue(path.exists(), f"Missing contract snapshot: {path}")
committed = json.loads(path.read_text(encoding="utf-8"))
self.assertEqual(committed, generated, f"Regenerate {name}")
def test_openapi_declares_every_public_http_media_type(self):
openapi = contract_documents()["openapi.json"]
paths = openapi["paths"]
self.assertIn("application/json", paths["/v1/responses"]["post"]["requestBody"]["content"])
response_content = paths["/v1/responses"]["post"]["responses"]["200"]["content"]
self.assertIn("application/json", response_content)
self.assertIn("text/event-stream", response_content)
call_content = paths["/v1/realtime/calls"]["post"]["requestBody"]["content"]
self.assertEqual(
set(call_content),
{"multipart/form-data", "application/sdp"},
)
self.assertIn(
"application/sdp",
paths["/v1/realtime/calls"]["post"]["responses"]["200"]["content"],
)
public_operations = [
operation
for path, methods in paths.items()
if path.startswith("/v1/")
for method, operation in methods.items()
if method in {"get", "post", "delete"}
]
for operation in public_operations:
self.assertIn("401", operation["responses"])
self.assertIn("application/json", operation["responses"]["401"]["content"])
self.assertIn("security", operation)
for response in operation["responses"].values():
self.assertIn("X-Request-ID", response.get("headers", {}))
class RealtimeEventContractTests(unittest.TestCase):
def test_client_schema_validates_supported_event_and_links_errors(self):
schema = contract_documents()[
"openai-realtime-client-events.schema.json"
]
Draft202012Validator.check_schema(schema)
validator = Draft202012Validator(schema)
event = {
"type": "conversation.item.create",
"event_id": "evt_123",
"item": {
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "你好"}],
},
}
validator.validate(event)
event = require_event(event)
self.assertEqual(event["event_id"], "evt_123")
invalid_event = {
"type": "input_audio_buffer.append",
"event_id": "evt_bad",
}
with self.assertRaises(JSONSchemaValidationError):
validator.validate(invalid_event)
with self.assertRaises(RealtimeEventError) as raised:
require_event(invalid_event)
wire_error = error_event(raised.exception)
self.assertEqual(wire_error["error"]["event_id"], "evt_bad")
extension_event = {
"type": "x.interactive_media.capabilities.update",
"event_id": "evt_extension",
"version": "1",
"capabilities": ["workflow_events"],
}
validator.validate(extension_event)
require_event(extension_event)
extension_without_version = {
key: value
for key, value in extension_event.items()
if key != "version"
}
with self.assertRaises(JSONSchemaValidationError):
validator.validate(extension_without_version)
with self.assertRaises(RealtimeEventError):
require_event(extension_without_version)
def test_server_schema_enumerates_every_emitted_event_name(self):
schema = contract_documents()[
"openai-realtime-server-events.schema.json"
]
Draft202012Validator.check_schema(schema)
validator = Draft202012Validator(schema)
for event_type in SERVER_EVENT_TYPES:
event = {"type": event_type, "event_id": "event_contract"}
if event_type in EXTENSION_SERVER_EVENT_TYPES:
event["version"] = "1"
validator.validate(event)
parsed = RealtimeServerEvent.model_validate(
event
)
self.assertEqual(parsed.type, event_type)
with self.assertRaises(JSONSchemaValidationError):
validator.validate(
{
"type": "x.interactive_media.workflow.event",
"event_id": "event_without_version",
}
)
def test_text_response_events_follow_realtime_order(self):
class FakeSession:
output_is_audio = False
active_response_id: str | None = None
active_output_item_id: str | None = None
response_text = ""
def begin_response(self):
self.active_response_id = self.active_response_id or "resp_1"
self.active_output_item_id = self.active_output_item_id or "item_1"
return self.active_response_id, self.active_output_item_id
def finish_response(self):
result = (
self.active_response_id,
self.active_output_item_id,
self.response_text,
)
self.active_response_id = None
self.active_output_item_id = None
self.response_text = ""
return result
bridge = OpenAIRealtimeBridge(FakeSession(), channel="websocket")
events = [
*bridge._start_assistant_output(),
*bridge._assistant_delta("你好"),
*bridge._end_assistant_output(False),
]
event_types = [event["type"] for event in events]
self.assertEqual(event_types[0:3], [
"response.created",
"response.output_item.added",
"response.content_part.added",
])
self.assertIn("response.output_text.delta", event_types)
self.assertEqual(event_types[-1], "response.done")
validator = Draft202012Validator(
contract_documents()[
"openai-realtime-server-events.schema.json"
]
)
for event in events:
validator.validate(event)
RealtimeServerEvent.model_validate(event)
if __name__ == "__main__":
unittest.main()