mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-17 04:12:24 +02:00
Squash Odysseus development history
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
"""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 == ""
|
||||
Reference in New Issue
Block a user