|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +from typing import Any |
| 4 | +from unittest.mock import MagicMock |
| 5 | + |
| 6 | +import pytest |
| 7 | + |
| 8 | +from openai.lib import validate_response_input, get_response_input_items |
| 9 | + |
| 10 | + |
| 11 | +def _make_reasoning_item(item_id: str = "rs_001") -> Any: |
| 12 | + item = MagicMock() |
| 13 | + item.type = "reasoning" |
| 14 | + item.id = item_id |
| 15 | + item.model_dump.return_value = {"type": "reasoning", "id": item_id, "summary": []} |
| 16 | + return item |
| 17 | + |
| 18 | + |
| 19 | +def _make_message_item(item_id: str = "msg_001") -> Any: |
| 20 | + item = MagicMock() |
| 21 | + item.type = "message" |
| 22 | + item.role = "assistant" |
| 23 | + item.id = item_id |
| 24 | + item.model_dump.return_value = {"type": "message", "role": "assistant", "id": item_id, "content": []} |
| 25 | + return item |
| 26 | + |
| 27 | + |
| 28 | +def _make_response(output: list[Any]) -> Any: |
| 29 | + response = MagicMock() |
| 30 | + response.output = output |
| 31 | + return response |
| 32 | + |
| 33 | + |
| 34 | +# --------------------------------------------------------------------------- |
| 35 | +# get_response_input_items |
| 36 | +# --------------------------------------------------------------------------- |
| 37 | + |
| 38 | + |
| 39 | +def test_get_response_input_items_reasoning_and_message() -> None: |
| 40 | + """Returns both reasoning and message items in order.""" |
| 41 | + reasoning = _make_reasoning_item("rs_1") |
| 42 | + message = _make_message_item("msg_1") |
| 43 | + response = _make_response([reasoning, message]) |
| 44 | + |
| 45 | + result = get_response_input_items(response) |
| 46 | + |
| 47 | + assert len(result) == 2 |
| 48 | + assert result[0] == {"type": "reasoning", "id": "rs_1", "summary": []} |
| 49 | + assert result[1] == {"type": "message", "role": "assistant", "id": "msg_1", "content": []} |
| 50 | + |
| 51 | + |
| 52 | +def test_get_response_input_items_message_only() -> None: |
| 53 | + """Returns message items when there are no reasoning items.""" |
| 54 | + message = _make_message_item("msg_2") |
| 55 | + response = _make_response([message]) |
| 56 | + |
| 57 | + result = get_response_input_items(response) |
| 58 | + |
| 59 | + assert len(result) == 1 |
| 60 | + assert result[0] == {"type": "message", "role": "assistant", "id": "msg_2", "content": []} |
| 61 | + |
| 62 | + |
| 63 | +def test_get_response_input_items_empty() -> None: |
| 64 | + """Returns empty list for empty output.""" |
| 65 | + response = _make_response([]) |
| 66 | + result = get_response_input_items(response) |
| 67 | + assert result == [] |
| 68 | + |
| 69 | + |
| 70 | +# --------------------------------------------------------------------------- |
| 71 | +# validate_response_input |
| 72 | +# --------------------------------------------------------------------------- |
| 73 | + |
| 74 | + |
| 75 | +def test_validate_passes_for_consecutive_pair_dicts() -> None: |
| 76 | + """No error when reasoning immediately precedes assistant message (dict form).""" |
| 77 | + items = [ |
| 78 | + {"type": "reasoning", "id": "rs_1", "summary": []}, |
| 79 | + {"type": "message", "role": "assistant", "id": "msg_1", "content": []}, |
| 80 | + ] |
| 81 | + validate_response_input(items) # should not raise |
| 82 | + |
| 83 | + |
| 84 | +def test_validate_raises_for_orphaned_message_dicts() -> None: |
| 85 | + """ValueError raised when assistant message is not preceded by reasoning (dict form).""" |
| 86 | + items = [ |
| 87 | + {"type": "message", "role": "user", "content": "hello"}, |
| 88 | + {"type": "reasoning", "id": "rs_1", "summary": []}, |
| 89 | + {"type": "message", "role": "user", "content": "follow-up"}, |
| 90 | + {"type": "message", "role": "assistant", "id": "msg_orphan", "content": []}, |
| 91 | + ] |
| 92 | + with pytest.raises(ValueError, match="msg_orphan"): |
| 93 | + validate_response_input(items) |
| 94 | + |
| 95 | + |
| 96 | +def test_validate_raises_error_describes_constraint() -> None: |
| 97 | + """Error message explains the reasoning+message pairing constraint.""" |
| 98 | + items = [ |
| 99 | + {"type": "reasoning", "id": "rs_1", "summary": []}, |
| 100 | + {"type": "message", "role": "user", "content": "hi"}, |
| 101 | + {"type": "message", "role": "assistant", "id": "msg_bad", "content": []}, |
| 102 | + ] |
| 103 | + with pytest.raises(ValueError, match="consecutive pair"): |
| 104 | + validate_response_input(items) |
| 105 | + |
| 106 | + |
| 107 | +def test_validate_passes_for_user_only_messages() -> None: |
| 108 | + """No error when there are only user messages and no reasoning items.""" |
| 109 | + items = [ |
| 110 | + {"type": "message", "role": "user", "content": "hello"}, |
| 111 | + {"type": "message", "role": "user", "content": "how are you"}, |
| 112 | + ] |
| 113 | + validate_response_input(items) # should not raise |
| 114 | + |
| 115 | + |
| 116 | +def test_validate_passes_for_empty_input() -> None: |
| 117 | + """No error for empty input list.""" |
| 118 | + validate_response_input([]) # should not raise |
| 119 | + |
| 120 | + |
| 121 | +def test_validate_passes_with_object_form() -> None: |
| 122 | + """Works with object-form items (not dicts), consecutive pair.""" |
| 123 | + reasoning = MagicMock() |
| 124 | + reasoning.type = "reasoning" |
| 125 | + reasoning.id = "rs_obj" |
| 126 | + |
| 127 | + message = MagicMock() |
| 128 | + message.type = "message" |
| 129 | + message.role = "assistant" |
| 130 | + message.id = "msg_obj" |
| 131 | + |
| 132 | + validate_response_input([reasoning, message]) # should not raise |
| 133 | + |
| 134 | + |
| 135 | +def test_validate_raises_with_object_form_orphaned() -> None: |
| 136 | + """Raises ValueError with object-form items when message is orphaned.""" |
| 137 | + reasoning = MagicMock() |
| 138 | + reasoning.type = "reasoning" |
| 139 | + reasoning.id = "rs_obj" |
| 140 | + |
| 141 | + user_msg = MagicMock() |
| 142 | + user_msg.type = "message" |
| 143 | + user_msg.role = "user" |
| 144 | + |
| 145 | + asst_msg = MagicMock() |
| 146 | + asst_msg.type = "message" |
| 147 | + asst_msg.role = "assistant" |
| 148 | + asst_msg.id = "msg_orphan_obj" |
| 149 | + |
| 150 | + with pytest.raises(ValueError, match="msg_orphan_obj"): |
| 151 | + validate_response_input([reasoning, user_msg, asst_msg]) |
0 commit comments