Skip to content

Commit ac03e38

Browse files
committed
add tests for coverage
1 parent 4b7f0c1 commit ac03e38

File tree

3 files changed

+107
-4
lines changed

3 files changed

+107
-4
lines changed

pydantic_ai_slim/pydantic_ai/_parts_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ def _handle_text_delta_with_thinking_tags(
408408
self._isolated_start_tags[new_part_index] = start_tag
409409
elif segment_type == 'thinking':
410410
yield from self.handle_thinking_delta(vendor_part_id=vendor_part_id, content=segment_content)
411-
elif segment_type == 'end_tag':
411+
elif segment_type == 'end_tag': # pragma: no cover
412412
self._vendor_id_to_part_index.pop(vendor_part_id)
413413

414414
if new_buffer:

tests/test_parts_manager_split_tags.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,3 +347,101 @@ def test_cross_path_bare_end_tag():
347347
assert len(parts) == 1
348348
assert isinstance(parts[0], ThinkingPart)
349349
assert parts[0].content == 'donex'
350+
351+
352+
def test_invalid_partial_tag_prefix():
353+
"""Test content starting with '<' but not matching tag prefix (branch 109->113)."""
354+
events, parts = stream_text_deltas(['<xyz'])
355+
356+
assert len(parts) == 1
357+
assert isinstance(parts[0], TextPart)
358+
assert parts[0].content == '<xyz'
359+
assert len(events) == 1
360+
assert isinstance(events[0], PartStartEvent)
361+
362+
363+
def test_bare_start_tag_simple_path():
364+
"""Test isolated start tag through simple path (branch 319->321)."""
365+
manager = ModelResponsePartsManager()
366+
thinking_tags = ('<think>', '</think>')
367+
368+
events = list(manager.handle_text_delta(vendor_part_id=None, content='<think>', thinking_tags=thinking_tags))
369+
370+
assert len(events) == 0
371+
372+
final_events = list(manager.finalize())
373+
assert len(final_events) == 1
374+
assert isinstance(final_events[0], PartStartEvent)
375+
assert isinstance(final_events[0].part, TextPart)
376+
assert final_events[0].part.content == '<think>'
377+
378+
379+
def test_complete_thinking_block_with_trailing_text_single_chunk():
380+
"""Test complete thinking block and text in one chunk (branch 411->386)."""
381+
events, parts = stream_text_deltas(['<think>reasoning</think>final text'])
382+
383+
assert len(parts) == 2
384+
assert isinstance(parts[0], ThinkingPart)
385+
assert parts[0].content == 'reasoning'
386+
assert isinstance(parts[1], TextPart)
387+
assert parts[1].content == 'final text'
388+
assert len(events) == 2
389+
390+
391+
def test_thinking_delta_after_tool_call():
392+
"""Test creating ThinkingPart when latest part is a ToolCallPart (branch 515->528)."""
393+
manager = ModelResponsePartsManager()
394+
395+
manager.handle_tool_call_part(
396+
vendor_part_id='tool1', tool_name='test_tool', args={'key': 'value'}, tool_call_id='call_123'
397+
)
398+
399+
events = list(manager.handle_thinking_delta(vendor_part_id=None, content='some thinking'))
400+
401+
assert len(events) == 1
402+
assert isinstance(events[0], PartStartEvent)
403+
assert isinstance(events[0].part, ThinkingPart)
404+
405+
parts = manager.get_parts()
406+
assert len(parts) == 2
407+
assert isinstance(parts[1], ThinkingPart)
408+
assert parts[1].content == 'some thinking'
409+
410+
411+
def test_text_part_update_via_handle_part_then_emit():
412+
"""Test updating a TextPart created via handle_part (lines 471-472)."""
413+
manager = ModelResponsePartsManager()
414+
415+
manager.handle_part(vendor_part_id='text1', part=TextPart(content='initial'))
416+
417+
events = list(manager.handle_text_delta(vendor_part_id='text1', content=' more', thinking_tags=None))
418+
419+
assert len(events) == 1
420+
assert isinstance(events[0], PartStartEvent)
421+
assert isinstance(events[0].part, TextPart)
422+
assert events[0].part.content == 'initial more'
423+
424+
parts = manager.get_parts()
425+
assert len(parts) == 1
426+
assert isinstance(parts[0], TextPart)
427+
assert parts[0].content == 'initial more'
428+
429+
430+
def test_bare_end_tag_chunk():
431+
"""Test chunk containing only the closing tag (branch 411->386)."""
432+
events, parts = stream_text_deltas(['<think>', 'content', '</think>'])
433+
434+
assert len(parts) == 1
435+
assert isinstance(parts[0], ThinkingPart)
436+
assert parts[0].content == 'content'
437+
assert len(events) == 1
438+
439+
440+
def test_stream_without_finalize():
441+
"""Test streaming without finalization (branch 49->53)."""
442+
events, parts = stream_text_deltas(['<thi'], finalize=False)
443+
444+
# Incomplete tag should be buffered, not emitted
445+
assert len(events) == 0
446+
# No parts yet since not finalized
447+
assert len(parts) == 0

tests/test_streaming.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,12 @@
4242
)
4343
from pydantic_ai.agent import AgentRun
4444
from pydantic_ai.exceptions import ApprovalRequired, CallDeferred
45-
from pydantic_ai.models.function import AgentInfo, DeltaToolCall, DeltaToolCalls, FunctionModel
45+
from pydantic_ai.models.function import (
46+
AgentInfo,
47+
DeltaToolCall,
48+
DeltaToolCalls,
49+
FunctionModel,
50+
)
4651
from pydantic_ai.models.test import TestModel
4752
from pydantic_ai.output import PromptedOutput, TextOutput
4853
from pydantic_ai.result import AgentStream, FinalResult, RunUsage
@@ -2131,8 +2136,8 @@ async def event_stream_handler(_ctx: RunContext[None], stream: AsyncIterable[Age
21312136
assert len(part_end_events) == 1
21322137
assert isinstance(part_end_events[0].part, TextPart)
21332138
assert part_end_events[0].part.content == '<thi'
2134-
2135-
2139+
2140+
21362141
def test_structured_response_sync_validation():
21372142
async def text_stream(_messages: list[ModelMessage], agent_info: AgentInfo) -> AsyncIterator[DeltaToolCalls]:
21382143
assert agent_info.output_tools is not None

0 commit comments

Comments
 (0)