@@ -613,6 +613,35 @@ async def test_full_message_stops_ticker(self) -> None:
613613 assert ctx ._buffer is None , "Full message left the buffer un-closed"
614614 assert task .done (), "coalescing-buffer ticker still running after Full (orphaned)"
615615
616+ @pytest .mark .asyncio
617+ async def test_full_is_terminal_publish_no_trailing_deltas (self ) -> None :
618+ # Leftover buffered deltas must be drained BEFORE the Full hits the
619+ # stream (deltas -> Full), never after it — a consumer treating Full as
620+ # the final message would see a trailing delta as a stale duplicate tail.
621+ ctx , svc , tm = await _make_context ("coalesced" )
622+ # First delta flushes immediately; the second stays in the coalescing
623+ # window, so it is still buffered when the Full arrives.
624+ await ctx .stream_update (_text (tm , "alpha" ))
625+ await ctx .stream_update (_text (tm , "beta" ))
626+
627+ full = StreamTaskMessageFull (
628+ parent_task_message = tm ,
629+ content = TextContent (author = "agent" , content = "alphabeta" , format = "markdown" ),
630+ type = "full" ,
631+ )
632+ await ctx .stream_update (full )
633+
634+ # Every publish (delta flushes + the Full) goes through the service mock.
635+ published = [c .args [0 ] for c in svc .stream_update .await_args_list ]
636+ assert published , "nothing was published"
637+ assert published [- 1 ] is full , (
638+ f"Full must be the terminal publish; saw trailing "
639+ f"{ type (published [- 1 ]).__name__ } after it (stale duplicate tail)"
640+ )
641+ assert any (isinstance (u , StreamTaskMessageDelta ) for u in published [:- 1 ]), (
642+ "expected the buffered deltas to be published before the Full"
643+ )
644+
616645 @pytest .mark .asyncio
617646 async def test_close_reaps_buffer_even_if_already_marked_closed (self ) -> None :
618647 # Defense-in-depth: if any path marks the context closed without closing
0 commit comments