mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-18 14:12:19 +02:00
157 lines
5.1 KiB
Python
157 lines
5.1 KiB
Python
"""Tests for _normalize_mistral_content() — Mistral's structured content parser.
|
|
|
|
Mistral's chat completions API returns content as a typed array when reasoning
|
|
is enabled, instead of the plain string most OpenAI-compat servers use:
|
|
|
|
"content": [
|
|
{"type": "thinking", "thinking": [{"type": "text", "text": "..."}], "closed": true},
|
|
{"type": "text", "text": "..."}
|
|
]
|
|
|
|
_normalize_mistral_content() splits that into (text, thinking) plain strings.
|
|
The function is called from three sites:
|
|
- llm_call (sync, non-streaming response parser)
|
|
- llm_call_async (async, non-streaming response parser)
|
|
- stream_llm (streaming delta parser)
|
|
|
|
These tests pin the contract: string passthrough, the array shape, and the
|
|
edge cases (empty, garbage, missing fields) so a refactor doesn't silently
|
|
drop thinking content or break non-Mistral providers.
|
|
"""
|
|
from src.llm_core import _normalize_mistral_content
|
|
|
|
|
|
def test_string_passthrough_returns_text_with_empty_thinking():
|
|
"""Plain string content (the common case) passes through unchanged."""
|
|
text, thinking = _normalize_mistral_content("hello world")
|
|
assert text == "hello world"
|
|
assert thinking == ""
|
|
|
|
|
|
def test_empty_string_passthrough():
|
|
text, thinking = _normalize_mistral_content("")
|
|
assert text == ""
|
|
assert thinking == ""
|
|
|
|
|
|
def test_array_with_thinking_and_text_blocks():
|
|
"""Mistral's documented format: thinking block + text block."""
|
|
content = [
|
|
{
|
|
"type": "thinking",
|
|
"thinking": [{"type": "text", "text": "Let me work through this..."}],
|
|
"closed": True,
|
|
},
|
|
{"type": "text", "text": "The answer is 42."},
|
|
]
|
|
text, thinking = _normalize_mistral_content(content)
|
|
assert text == "The answer is 42."
|
|
assert thinking == "Let me work through this..."
|
|
|
|
|
|
def test_array_with_only_thinking_block():
|
|
"""Streaming deltas often contain only a thinking fragment (no text block yet)."""
|
|
content = [
|
|
{
|
|
"type": "thinking",
|
|
"thinking": [{"type": "text", "text": "Okay, let's"}],
|
|
"closed": True,
|
|
}
|
|
]
|
|
text, thinking = _normalize_mistral_content(content)
|
|
assert text == ""
|
|
assert thinking == "Okay, let's"
|
|
|
|
|
|
def test_array_with_only_text_block():
|
|
"""Final answer delta — only the text block, no thinking."""
|
|
content = [{"type": "text", "text": "Final answer."}]
|
|
text, thinking = _normalize_mistral_content(content)
|
|
assert text == "Final answer."
|
|
assert thinking == ""
|
|
|
|
|
|
def test_array_concatenates_multiple_text_blocks():
|
|
"""Multiple text blocks are concatenated in order."""
|
|
content = [
|
|
{"type": "text", "text": "part 1 "},
|
|
{"type": "text", "text": "part 2"},
|
|
]
|
|
text, thinking = _normalize_mistral_content(content)
|
|
assert text == "part 1 part 2"
|
|
|
|
|
|
def test_array_concatenates_multiple_thinking_fragments():
|
|
"""Multiple thinking sub-blocks are concatenated in order."""
|
|
content = [
|
|
{
|
|
"type": "thinking",
|
|
"thinking": [
|
|
{"type": "text", "text": "first "},
|
|
{"type": "text", "text": "second"},
|
|
],
|
|
"closed": True,
|
|
}
|
|
]
|
|
text, thinking = _normalize_mistral_content(content)
|
|
assert text == ""
|
|
assert thinking == "first second"
|
|
|
|
|
|
def test_empty_array_returns_empty_strings():
|
|
text, thinking = _normalize_mistral_content([])
|
|
assert text == ""
|
|
assert thinking == ""
|
|
|
|
|
|
def test_array_with_garbage_entries_skips_them():
|
|
"""Non-dict entries, missing type, missing text — all silently skipped."""
|
|
content = [
|
|
"not a dict",
|
|
None,
|
|
{"type": "unknown_type", "text": "should be ignored"},
|
|
{"type": "text"}, # missing text key
|
|
{"type": "thinking"}, # missing thinking key
|
|
{"type": "text", "text": "valid text"},
|
|
]
|
|
text, thinking = _normalize_mistral_content(content)
|
|
assert text == "valid text"
|
|
assert thinking == ""
|
|
|
|
|
|
def test_none_returns_empty_strings():
|
|
"""Defensive: None content (server bug or schema drift) doesn't crash."""
|
|
text, thinking = _normalize_mistral_content(None)
|
|
assert text == ""
|
|
assert thinking == ""
|
|
|
|
|
|
def test_int_returns_empty_strings():
|
|
"""Defensive: wrong-typed content doesn't crash."""
|
|
text, thinking = _normalize_mistral_content(42)
|
|
assert text == ""
|
|
assert thinking == ""
|
|
|
|
|
|
def test_thinking_block_with_string_inner():
|
|
"""Some Mistral API versions may use a string instead of an array for
|
|
the inner 'thinking' field. Accept both shapes."""
|
|
content = [
|
|
{"type": "thinking", "thinking": "inline string thinking"},
|
|
{"type": "text", "text": "answer"},
|
|
]
|
|
text, thinking = _normalize_mistral_content(content)
|
|
assert text == "answer"
|
|
assert thinking == "inline string thinking"
|
|
|
|
|
|
def test_thinking_block_with_empty_text_field():
|
|
"""Empty text fields don't pollute the output."""
|
|
content = [
|
|
{"type": "thinking", "thinking": [{"type": "text", "text": ""}], "closed": True},
|
|
{"type": "text", "text": ""},
|
|
]
|
|
text, thinking = _normalize_mistral_content(content)
|
|
assert text == ""
|
|
assert thinking == ""
|