mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
36ce0950e4
Remove linking, multicast, durable delivery, and host push machinery from the v1 hosting core. Keep those scenarios in a proposed follow-up ADR and update channel packages, samples, docs, tests, and workspace metadata around the smaller host/channel contract. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
145 lines
5.8 KiB
Python
145 lines
5.8 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
"""Tests for the OpenAI Responses request-body parser."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from agent_framework_hosting_responses import (
|
|
messages_from_responses_input,
|
|
parse_responses_identity,
|
|
parse_responses_request,
|
|
)
|
|
|
|
|
|
class TestMessagesFromResponsesInput:
|
|
def test_string_input_becomes_single_user_message(self) -> None:
|
|
msgs = messages_from_responses_input("hello")
|
|
assert len(msgs) == 1
|
|
assert msgs[0].role == "user"
|
|
assert msgs[0].text == "hello"
|
|
|
|
def test_input_text_items_collapse_into_one_user_message(self) -> None:
|
|
msgs = messages_from_responses_input([{"type": "input_text", "text": "a"}, {"type": "input_text", "text": "b"}])
|
|
assert len(msgs) == 1
|
|
assert msgs[0].role == "user"
|
|
assert msgs[0].text == "a b"
|
|
|
|
def test_message_envelope_with_string_content(self) -> None:
|
|
msgs = messages_from_responses_input([
|
|
{"type": "message", "role": "system", "content": "be brief"},
|
|
{"type": "message", "role": "user", "content": "hi"},
|
|
])
|
|
assert [m.role for m in msgs] == ["system", "user"]
|
|
assert msgs[0].text == "be brief"
|
|
|
|
def test_message_envelope_with_content_parts(self) -> None:
|
|
msgs = messages_from_responses_input([
|
|
{
|
|
"type": "message",
|
|
"role": "user",
|
|
"content": [{"type": "input_text", "text": "describe this"}],
|
|
}
|
|
])
|
|
assert msgs[0].text == "describe this"
|
|
|
|
def test_pending_text_flushes_before_message_envelope(self) -> None:
|
|
msgs = messages_from_responses_input([
|
|
{"type": "input_text", "text": "first"},
|
|
{"type": "message", "role": "user", "content": "second"},
|
|
])
|
|
assert len(msgs) == 2
|
|
assert msgs[0].text == "first"
|
|
assert msgs[1].text == "second"
|
|
|
|
def test_image_url_via_string(self) -> None:
|
|
msgs = messages_from_responses_input([{"type": "input_image", "image_url": "https://example.com/cat.png"}])
|
|
assert len(msgs) == 1
|
|
# Image content present.
|
|
assert any(getattr(c, "uri", None) == "https://example.com/cat.png" for c in msgs[0].contents)
|
|
|
|
def test_image_url_via_object(self) -> None:
|
|
msgs = messages_from_responses_input([
|
|
{"type": "input_image", "image_url": {"url": "https://example.com/cat.png"}}
|
|
])
|
|
assert any(getattr(c, "uri", None) == "https://example.com/cat.png" for c in msgs[0].contents)
|
|
|
|
def test_unknown_input_type_raises(self) -> None:
|
|
with pytest.raises(ValueError, match="Unsupported"):
|
|
messages_from_responses_input([{"type": "weird"}])
|
|
|
|
def test_empty_list_raises(self) -> None:
|
|
with pytest.raises(ValueError, match="non-empty"):
|
|
messages_from_responses_input([])
|
|
|
|
def test_non_string_non_list_raises(self) -> None:
|
|
with pytest.raises(ValueError):
|
|
messages_from_responses_input(42) # type: ignore[arg-type]
|
|
|
|
def test_image_url_missing_raises(self) -> None:
|
|
with pytest.raises(ValueError, match="image_url"):
|
|
messages_from_responses_input([{"type": "input_image"}])
|
|
|
|
|
|
class TestParseResponsesRequest:
|
|
def test_instructions_are_forwarded_as_chat_options(self) -> None:
|
|
msgs, opts, sess = parse_responses_request({"input": "hi", "instructions": "be brief"})
|
|
assert len(msgs) == 1
|
|
assert msgs[0].role == "user"
|
|
assert msgs[0].text == "hi"
|
|
assert opts["instructions"] == "be brief"
|
|
assert sess is None
|
|
|
|
def test_options_passthrough(self) -> None:
|
|
_, opts, _ = parse_responses_request({"input": "x", "temperature": 0.4, "top_p": 0.9, "tool_choice": "auto"})
|
|
assert opts["temperature"] == 0.4
|
|
assert opts["top_p"] == 0.9
|
|
assert opts["tool_choice"] == "auto"
|
|
|
|
def test_options_remap(self) -> None:
|
|
_, opts, _ = parse_responses_request({"input": "x", "max_output_tokens": 256, "parallel_tool_calls": False})
|
|
assert opts == {"max_tokens": 256, "allow_multiple_tool_calls": False}
|
|
|
|
def test_transport_keys_not_forwarded(self) -> None:
|
|
_, opts, _ = parse_responses_request({
|
|
"input": "x",
|
|
"model": "gpt-x",
|
|
"stream": True,
|
|
"previous_response_id": "r",
|
|
})
|
|
for key in ("input", "model", "stream", "previous_response_id"):
|
|
assert key not in opts
|
|
|
|
def test_unknown_keys_silently_dropped(self) -> None:
|
|
_, opts, _ = parse_responses_request({"input": "x", "truncation": "auto", "reasoning": {"effort": "low"}})
|
|
assert opts == {}
|
|
|
|
def test_none_values_dropped(self) -> None:
|
|
_, opts, _ = parse_responses_request({"input": "x", "temperature": None})
|
|
assert "temperature" not in opts
|
|
|
|
def test_previous_response_id_becomes_session(self) -> None:
|
|
_, _, sess = parse_responses_request({"input": "x", "previous_response_id": "resp_42"})
|
|
assert sess is not None
|
|
assert sess.isolation_key == "resp_42"
|
|
|
|
|
|
class TestParseResponsesIdentity:
|
|
def test_safety_identifier_preferred(self) -> None:
|
|
ident = parse_responses_identity({"safety_identifier": "abc", "user": "legacy"}, "responses")
|
|
assert ident is not None
|
|
assert ident.native_id == "abc"
|
|
assert ident.channel == "responses"
|
|
|
|
def test_fallback_to_user(self) -> None:
|
|
ident = parse_responses_identity({"user": "legacy"}, "responses")
|
|
assert ident is not None
|
|
assert ident.native_id == "legacy"
|
|
|
|
def test_returns_none_when_absent(self) -> None:
|
|
assert parse_responses_identity({}, "responses") is None
|
|
|
|
def test_returns_none_for_non_string(self) -> None:
|
|
assert parse_responses_identity({"safety_identifier": 42}, "responses") is None
|