"""Tests for the parsing functionality (PyFilter)."""
import pytest
from cohere_melody import PyFilter, PyFilterOptions
class TestPyFilterWriteDecoded:
"""Tests for PyFilter.write_decoded method."""
@pytest.fixture
def cmd3_filter(self):
"""Create a fresh cmd3 filter for each test."""
return PyFilter.cmd3()
def test_plain_text(self, cmd3_filter):
"""Test write_decoded with plain text."""
result = cmd3_filter.write_decoded("Hello world")
assert result.content == "Hello world"
assert result.reasoning is None
def test_thinking_tags(self, cmd3_filter):
"""Test write_decoded with thinking tags."""
result = cmd3_filter.write_decoded("<|START_THINKING|>This is a thought")
assert result.reasoning == "This is a thought"
assert result.content is None
def test_response_tags(self, cmd3_filter):
"""Test write_decoded with response tags."""
result = cmd3_filter.write_decoded("<|START_RESPONSE|>Hello")
assert result.content == "Hello"
assert result.reasoning is None
def test_transition_thinking_to_response(self, cmd3_filter):
"""Test transitioning from thinking to response."""
result = cmd3_filter.write_decoded("<|START_THINKING|>Thinking...")
assert result.reasoning is not None
result = cmd3_filter.write_decoded("<|END_THINKING|><|START_RESPONSE|>Response")
assert result.content is not None
assert "Response" in result.content
class TestPyFilterFlushPartials:
"""Tests for PyFilter.flush_partials method."""
def test_flush_with_buffered_content(self):
"""Test flush_partials returns buffered content when chunk_size > 1."""
f = PyFilter.cmd3(chunk_size=10)
result = f.write_decoded("<|START_RESPONSE|>Hi")
assert result.content is None or result.content == ""
result = f.flush_partials()
assert result.content is not None
assert "Hi" in result.content
def test_flush_empty_filter(self):
"""Test flush_partials on fresh filter returns default result."""
f = PyFilter.cmd3()
result = f.flush_partials()
assert result.content is None
assert result.reasoning is None
def test_flush_after_complete_output(self):
"""Test flush_partials after content already emitted."""
f = PyFilter.cmd3()
f.write_decoded("<|START_RESPONSE|>Hello")
result = f.flush_partials()
assert result is not None
class TestPyFilterStreamingWorkflow:
"""Tests for complete streaming workflows."""
def test_complete_workflow(self):
"""Test a complete streaming workflow."""
f = PyFilter.cmd3()
all_text = []
tokens = ["<|START_RESPONSE|>", "Hello", " ", "world", "!"]
for token in tokens:
result = f.write_decoded(token)
if result.content is not None:
all_text.append(result.content)
result = f.flush_partials()
if result.content is not None:
all_text.append(result.content)
full_text = "".join(all_text)
assert "Hello world!" in full_text
def test_thinking_then_response_workflow(self):
"""Test workflow with thinking followed by response."""
f = PyFilter.cmd3()
result = f.write_decoded("<|START_THINKING|>This is a")
assert result.reasoning == "This is a"
assert result.content is None
result = f.write_decoded(" plan.<|END_THINKING|>")
assert result.reasoning == " plan."
result = f.write_decoded("<|START_RESPONSE|>This is the final response.")
assert result.content == "This is the final response."
assert result.reasoning is None
class TestAggregatedResult:
"""Tests for AggregatedResult attributes."""
@pytest.fixture
def sample_result(self):
"""Get a sample AggregatedResult from a filter."""
f = PyFilter.cmd3()
result = f.write_decoded("<|START_RESPONSE|>Test")
return result
def test_has_content_attribute(self, sample_result):
"""Test that AggregatedResult has content attribute."""
assert hasattr(sample_result, "content")
def test_has_reasoning_attribute(self, sample_result):
"""Test that AggregatedResult has reasoning attribute."""
assert hasattr(sample_result, "reasoning")
def test_thinking_output_has_reasoning(self):
"""Test that thinking output has reasoning set."""
f = PyFilter.cmd3()
result = f.write_decoded("<|START_THINKING|>Thought")
assert result.reasoning is not None
assert result.content is None
def test_response_output_has_content(self):
"""Test that response output has content set."""
f = PyFilter.cmd3()
result = f.write_decoded("<|START_RESPONSE|>Response")
assert result.content is not None
assert result.reasoning is None
class TestPyFilterOptions:
"""Tests for PyFilterOptions builder pattern."""
def test_cmd3_option(self):
"""Test cmd3() builder method."""
opts = PyFilterOptions().cmd3()
f = PyFilter(opts)
result = f.write_decoded("<|START_RESPONSE|>Hello")
assert result.content == "Hello"
def test_cmd4_option(self):
"""Test cmd4() builder method."""
opts = PyFilterOptions().cmd4()
f = PyFilter(opts)
result = f.write_decoded("Hello")
assert result.reasoning == "Hello"
def test_cmd5_option(self):
"""Test cmd5() builder method parses nested cofl tool calls by default."""
opts = PyFilterOptions().cmd5()
f = PyFilter(opts)
text = (
"<|START_THINKING|>I should search.<|END_THINKING|>"
''
'test'
""
)
result = f.process_full_text(text)
assert result.reasoning == "I should search."
assert len(result.tool_calls) == 1
assert result.tool_calls[0].id == "call_0"
assert result.tool_calls[0].name == "web_search"
assert result.tool_calls[0].arguments == '{"query": "test"}'
def test_cmd5_static_factory(self):
"""Test PyFilter.cmd5() factory parses nested cofl tool calls."""
f = PyFilter.cmd5()
text = (
''
'hello'
""
)
result = f.process_full_text(text)
assert len(result.tool_calls) == 1
assert result.tool_calls[0].arguments == '{"q": "hello"}'
def test_cmd4_does_not_parse_cofl_tool_calls(self):
"""cmd4 filter must not treat cofl tags as structured tool calls."""
f = PyFilter.cmd4()
text = (
''
'hello'
""
)
result = f.process_full_text(text)
assert result.tool_calls == []
def test_rag_option(self):
"""Test rag() builder method."""
opts = PyFilterOptions().rag()
f = PyFilter(opts)
result = f.write_decoded("Hello")
assert result is not None
def test_multi_hop_option(self):
"""Test multi_hop() builder method."""
opts = PyFilterOptions().multi_hop()
f = PyFilter(opts)
result = f.write_decoded("Hello")
assert result is not None
def test_search_query_option(self):
"""Test search_query() builder method."""
opts = PyFilterOptions().search_query()
f = PyFilter(opts)
result = f.write_decoded("Hello")
assert result is not None
def test_with_chunk_size(self):
"""Test with_chunk_size() builder method."""
opts = PyFilterOptions().cmd3().with_chunk_size(10)
f = PyFilter(opts)
result = f.write_decoded("<|START_RESPONSE|>Hi")
assert result.content is None or result.content == ""
result = f.flush_partials()
assert result.content is not None
assert "Hi" in result.content
def test_chained_options(self):
"""Test chaining multiple builder methods."""
opts = (
PyFilterOptions()
.cmd3()
.with_chunk_size(5)
.with_left_trimmed()
.with_right_trimmed()
)
f = PyFilter(opts)
assert f is not None
def test_with_inclusive_stops(self):
"""Test with_inclusive_stops() builder method."""
opts = PyFilterOptions().cmd3().with_inclusive_stops(["STOP"])
f = PyFilter(opts)
assert f is not None
def test_with_exclusive_stops(self):
"""Test with_exclusive_stops() builder method."""
opts = PyFilterOptions().cmd3().with_exclusive_stops(["END"])
f = PyFilter(opts)
assert f is not None
def test_stream_tool_actions(self):
"""Test stream_tool_actions() builder method."""
opts = PyFilterOptions().cmd3().stream_tool_actions()
f = PyFilter(opts)
assert f is not None
def test_stream_processed_params(self):
"""Test stream_processed_params() builder method."""
opts = PyFilterOptions().cmd3().stream_processed_params()
f = PyFilter(opts)
assert f is not None
def test_stream_non_grounded_answer(self):
"""Test stream_non_grounded_answer() builder method."""
opts = PyFilterOptions().cmd3().stream_non_grounded_answer()
f = PyFilter(opts)
assert f is not None
def test_remove_token(self):
"""Test remove_token() builder method."""
opts = PyFilterOptions().cmd3().remove_token("<|START_THINKING|>")
f = PyFilter(opts)
assert f is not None
def test_cofl_no_escape(self):
"""Test cofl_nested_xml(False) parses unescaped flat params (cmd5-no-escape)."""
opts = PyFilterOptions().cmd5().cofl_nested_xml(False)
f = PyFilter(opts)
text = (
"<|START_THINKING|>think<|END_THINKING|>"
''
''
'value with & "quotes"'
""
)
result = f.process_full_text(text)
assert len(result.tool_calls) == 1
assert result.tool_calls[0].name == "run&tool"
import json
parsed = json.loads(result.tool_calls[0].arguments)
assert parsed["str_param"] == 'value with & "quotes"'
def test_cofl_nested_xml_disabled_for_strict(self):
"""Test flat + decode for cmd5-strict tool params."""
opts = (
PyFilterOptions().cmd5().cofl_nested_xml(False).cofl_decode_xml_text(True)
)
f = PyFilter(opts)
text = (
"<|START_THINKING|>think<|END_THINKING|>"
''
'hello<world>'
""
)
result = f.process_full_text(text)
assert len(result.tool_calls) == 1
assert result.tool_calls[0].arguments == '{"q": "hello"}'
def test_options_are_immutable(self):
"""Test that builder methods return new instances."""
opts1 = PyFilterOptions()
opts2 = opts1.cmd3()
opts3 = opts2.with_chunk_size(10)
assert opts1 is not opts2
assert opts2 is not opts3
class TestWithMessageHistory:
"""End-to-end tests: parser resolves citation indices to document IDs."""
def _cmd3_input(self, citation: str) -> str:
return f"<|START_RESPONSE|>foo {citation}<|END_RESPONSE|>"
def test_without_message_history_document_ids_empty(self):
"""When no lookup is configured, document_ids stays empty."""
f = PyFilter(PyFilterOptions().cmd3())
result = f.process_full_text(self._cmd3_input("bar"))
assert len(result.citations) == 1
src = result.citations[0].sources[0]
assert src.tool_call_index == 0
assert src.tool_result_indices == [0, 1]
assert src.document_ids == []
def test_top_level_documents_resolve(self):
"""Top-level documents live in tool_call_index 0."""
opts = (
PyFilterOptions()
.cmd3()
.with_message_history(documents=[{"id": "doc-a"}, {"id": "doc-b"}])
)
f = PyFilter(opts)
result = f.process_full_text(self._cmd3_input("bar"))
src = result.citations[0].sources[0]
assert src.document_ids == ["doc-a", "doc-b"]
def test_tool_results_resolve(self):
"""Tool result documents resolve via their assigned tool_call_index."""
messages = [
{
"role": "chatbot",
"content": [],
"tool_calls": [
{"id": "call_1", "name": "search", "parameters": "{}"},
],
},
{
"role": "tool",
"tool_call_id": "call_1",
"content": [
{"type": "document", "document": {"id": "res-x"}},
{"type": "document", "document": {"id": "res-y"}},
{"type": "document", "document": {"id": "res-z"}},
],
},
]
opts = PyFilterOptions().cmd3().with_message_history(messages=messages)
f = PyFilter(opts)
result = f.process_full_text(self._cmd3_input("bar"))
src = result.citations[0].sources[0]
assert src.document_ids == ["res-x", "res-z"]
def test_docs_and_tool_calls_interleave_correctly(self):
"""Top-level docs sit at index 0; tool calls start at index 1."""
documents = [{"id": "doc-a"}]
messages = [
{
"role": "chatbot",
"content": [],
"tool_calls": [
{"id": "call_1", "name": "search", "parameters": "{}"},
],
},
{
"role": "tool",
"tool_call_id": "call_1",
"content": [
{"type": "document", "document": {"id": "res-1"}},
],
},
]
opts = (
PyFilterOptions()
.cmd3()
.with_message_history(messages=messages, documents=documents)
)
f = PyFilter(opts)
result = f.process_full_text(self._cmd3_input("bar"))
cit = result.citations[0]
assert len(cit.sources) == 2
assert cit.sources[0].document_ids == ["doc-a"]
assert cit.sources[1].document_ids == ["res-1"]
def test_out_of_bounds_indices_handled_gracefully(self):
"""Out-of-bounds tool_result_indices produce empty strings."""
opts = (
PyFilterOptions().cmd3().with_message_history(documents=[{"id": "doc-a"}])
)
f = PyFilter(opts)
result = f.process_full_text(self._cmd3_input("bar"))
src = result.citations[0].sources[0]
assert src.document_ids == ["doc-a", ""]
def test_arguments_default_to_empty(self):
"""Both messages and documents default to empty lists. With no history
to build a lookup from, the parser leaves document_ids empty (same as
never calling with_message_history at all)."""
opts = PyFilterOptions().cmd3().with_message_history()
f = PyFilter(opts)
result = f.process_full_text(self._cmd3_input("bar"))
assert result.citations[0].sources[0].document_ids == []
def test_positional_arguments_work(self):
"""messages and documents can be passed positionally, matching Rust."""
opts = PyFilterOptions().cmd3().with_message_history([], [{"id": "doc-a"}])
f = PyFilter(opts)
result = f.process_full_text(self._cmd3_input("bar"))
assert result.citations[0].sources[0].document_ids == ["doc-a"]
def test_template_shape_errors_do_not_raise(self):
"""Template-shape issues (duplicate tool_call_id here) are the
renderer's concern, not the parser's. `with_message_history` accepts
them silently and still produces a usable lookup."""
messages = [
{
"role": "chatbot",
"content": [],
"tool_calls": [{"id": "call_1", "name": "search", "parameters": "{}"}],
},
{
"role": "chatbot",
"content": [],
"tool_calls": [{"id": "call_1", "name": "search", "parameters": "{}"}],
},
{
"role": "tool",
"tool_call_id": "call_1",
"content": [{"type": "document", "document": {"id": "res-1"}}],
},
]
opts = PyFilterOptions().cmd3().with_message_history(messages=messages)
f = PyFilter(opts)
result = f.process_full_text(self._cmd3_input("bar"))
assert result.citations[0].sources[0].document_ids == ["res-1"]
def test_non_list_messages_raises(self):
"""Argument-deserialisation failures still raise ValueError."""
with pytest.raises(ValueError):
PyFilterOptions().cmd3().with_message_history(messages="not a list")
class TestIsReasoning:
def test_is_reasoning_flips_on_end_thinking(self):
f = PyFilter(PyFilterOptions().cmd4().no_tools())
assert f.is_reasoning() is True
f.write_decoded("think<|END_THINKING|>")
assert f.is_reasoning() is False