@@ -681,6 +681,195 @@ async def json_stream():
681681 assert "list[SimpleModel]" in result .telemetry .schema_name
682682
683683
684+ class TestStreamFactoryRetryBehavior :
685+ """Test that factory functions are called fresh on each retry attempt.
686+
687+ This tests the fix for the "stream already consumed" bug where factory
688+ functions were not being called on each retry, causing streams to be
689+ reused and fail with "ReadableStream is locked" or similar errors.
690+ """
691+
692+ @pytest .mark .asyncio
693+ async def test_factory_called_fresh_on_each_retry_structured (self ):
694+ """Test that factory is called fresh on each retry in structured()."""
695+ factory_call_count = 0
696+
697+ def stream_factory ():
698+ nonlocal factory_call_count
699+ factory_call_count += 1
700+
701+ async def gen ():
702+ if factory_call_count == 1 :
703+ # First attempt: invalid JSON that will fail validation
704+ yield Event (type = EventType .TOKEN , text = '{"wrong": "field"}' )
705+ else :
706+ # Subsequent attempts: valid JSON
707+ yield Event (type = EventType .TOKEN , text = '{"value": "success"}' )
708+ yield Event (type = EventType .COMPLETE )
709+
710+ return gen ()
711+
712+ result = await structured (
713+ schema = SimpleModel ,
714+ stream = stream_factory ,
715+ retry = Retry (attempts = 3 ),
716+ )
717+
718+ assert result .data .value == "success"
719+ assert factory_call_count == 2 # Called twice: first failed, second succeeded
720+
721+ @pytest .mark .asyncio
722+ async def test_factory_called_fresh_on_each_retry_structured_array (self ):
723+ """Test that factory is called fresh on each retry in structured_array()."""
724+ factory_call_count = 0
725+
726+ def stream_factory ():
727+ nonlocal factory_call_count
728+ factory_call_count += 1
729+
730+ async def gen ():
731+ if factory_call_count == 1 :
732+ # First attempt: invalid JSON that will fail validation
733+ yield Event (type = EventType .TOKEN , text = '[{"wrong": "field"}]' )
734+ else :
735+ # Subsequent attempts: valid JSON
736+ yield Event (type = EventType .TOKEN , text = '[{"value": "success"}]' )
737+ yield Event (type = EventType .COMPLETE )
738+
739+ return gen ()
740+
741+ result = await structured_array (
742+ item_schema = SimpleModel ,
743+ stream = stream_factory ,
744+ retry = Retry (attempts = 3 ),
745+ )
746+
747+ assert len (result .data ) == 1
748+ assert result .data [0 ].value == "success"
749+ assert factory_call_count == 2 # Called twice: first failed, second succeeded
750+
751+ @pytest .mark .asyncio
752+ async def test_factory_exhausts_all_retries_on_persistent_failure (self ):
753+ """Test that all retry attempts are used when validation keeps failing."""
754+ factory_call_count = 0
755+
756+ def stream_factory ():
757+ nonlocal factory_call_count
758+ factory_call_count += 1
759+
760+ async def gen ():
761+ # Always return invalid JSON
762+ yield Event (type = EventType .TOKEN , text = '{"wrong": "field"}' )
763+ yield Event (type = EventType .COMPLETE )
764+
765+ return gen ()
766+
767+ with pytest .raises (ValueError , match = "Schema validation failed" ):
768+ await structured (
769+ schema = SimpleModel ,
770+ stream = stream_factory ,
771+ retry = Retry (attempts = 3 ),
772+ )
773+
774+ # Factory should have been called 3 times (once per retry attempt)
775+ assert factory_call_count == 3
776+
777+ @pytest .mark .asyncio
778+ async def test_factory_not_called_after_success (self ):
779+ """Test that factory is not called again after successful validation."""
780+ factory_call_count = 0
781+
782+ def stream_factory ():
783+ nonlocal factory_call_count
784+ factory_call_count += 1
785+
786+ async def gen ():
787+ # Always return valid JSON
788+ yield Event (type = EventType .TOKEN , text = '{"value": "test"}' )
789+ yield Event (type = EventType .COMPLETE )
790+
791+ return gen ()
792+
793+ result = await structured (
794+ schema = SimpleModel ,
795+ stream = stream_factory ,
796+ retry = Retry (attempts = 5 ),
797+ )
798+
799+ assert result .data .value == "test"
800+ assert factory_call_count == 1 # Only called once since first attempt succeeded
801+
802+ @pytest .mark .asyncio
803+ async def test_fallback_factory_called_fresh_on_retry (self ):
804+ """Test that fallback factory functions are also called fresh on retry."""
805+ main_call_count = 0
806+ fallback_call_count = 0
807+
808+ def main_factory ():
809+ nonlocal main_call_count
810+ main_call_count += 1
811+
812+ async def gen ():
813+ # Main always fails
814+ yield Event (type = EventType .TOKEN , text = '{"wrong": "field"}' )
815+ yield Event (type = EventType .COMPLETE )
816+
817+ return gen ()
818+
819+ def fallback_factory ():
820+ nonlocal fallback_call_count
821+ fallback_call_count += 1
822+
823+ async def gen ():
824+ if fallback_call_count == 1 :
825+ # First fallback attempt fails
826+ yield Event (type = EventType .TOKEN , text = '{"also_wrong": "field"}' )
827+ else :
828+ # Second fallback attempt succeeds
829+ yield Event (type = EventType .TOKEN , text = '{"value": "from_fallback"}' )
830+ yield Event (type = EventType .COMPLETE )
831+
832+ return gen ()
833+
834+ result = await structured (
835+ schema = SimpleModel ,
836+ stream = main_factory ,
837+ fallbacks = [fallback_factory ],
838+ retry = Retry (attempts = 2 ),
839+ )
840+
841+ assert result .data .value == "from_fallback"
842+ assert main_call_count == 2 # Main tried twice
843+ assert fallback_call_count == 2 # Fallback tried twice, second succeeded
844+
845+ @pytest .mark .asyncio
846+ async def test_async_factory_called_fresh_on_retry (self ):
847+ """Test that async factory functions are called fresh on each retry."""
848+ factory_call_count = 0
849+
850+ async def async_stream_factory ():
851+ nonlocal factory_call_count
852+ factory_call_count += 1
853+
854+ async def gen ():
855+ if factory_call_count == 1 :
856+ yield Event (type = EventType .TOKEN , text = '{"wrong": "field"}' )
857+ else :
858+ yield Event (type = EventType .TOKEN , text = '{"value": "async_success"}' )
859+ yield Event (type = EventType .COMPLETE )
860+
861+ return gen ()
862+
863+ result = await structured (
864+ schema = SimpleModel ,
865+ stream = async_stream_factory ,
866+ retry = Retry (attempts = 3 ),
867+ )
868+
869+ assert result .data .value == "async_success"
870+ assert factory_call_count == 2
871+
872+
684873class TestStructuredStrictMode :
685874 """Test strict_mode parameter for rejecting extra fields."""
686875
0 commit comments