@@ -20,6 +20,8 @@ const {
2020let buildGoogleSpeechProvider : typeof import ( "./speech-provider.js" ) . buildGoogleSpeechProvider ;
2121let testing : typeof import ( "./speech-provider.js" ) . testing ;
2222
23+ const GOOGLE_TTS_JSON_CAP_BYTES = 16 * 1024 * 1024 ;
24+
2325beforeAll ( async ( ) => {
2426 ( { buildGoogleSpeechProvider, testing } = await import ( "./speech-provider.js" ) ) ;
2527} ) ;
@@ -56,6 +58,26 @@ function installGoogleTtsRequestMock(pcm = Buffer.from([1, 0, 2, 0])) {
5658 return postJsonRequestMock ;
5759}
5860
61+ function oversizedGoogleTtsJsonResponse ( onCancel : ( ) => void ) : Response {
62+ const response = new Response (
63+ new ReadableStream < Uint8Array > ( {
64+ start ( controller ) {
65+ controller . enqueue ( new Uint8Array ( GOOGLE_TTS_JSON_CAP_BYTES + 1 ) ) ;
66+ } ,
67+ cancel ( ) {
68+ onCancel ( ) ;
69+ } ,
70+ } ) ,
71+ { headers : { "content-type" : "application/json" } , status : 200 } ,
72+ ) ;
73+ Object . defineProperty ( response , "json" , {
74+ value : async ( ) => {
75+ throw new Error ( "unbounded json reader was used" ) ;
76+ } ,
77+ } ) ;
78+ return response ;
79+ }
80+
5981function expectRecordFields ( value : unknown , expected : Record < string , unknown > ) {
6082 if ( ! value || typeof value !== "object" ) {
6183 throw new Error ( "Expected record" ) ;
@@ -149,6 +171,39 @@ describe("Google speech provider", () => {
149171 expect ( transcodeAudioBufferToOpusMock ) . not . toHaveBeenCalled ( ) ;
150172 } ) ;
151173
174+ it ( "bounds oversized Gemini TTS success JSON responses and cancels the stream" , async ( ) => {
175+ let cancelCount = 0 ;
176+ const release = vi . fn ( async ( ) => { } ) ;
177+ postJsonRequestMock
178+ . mockResolvedValueOnce ( {
179+ response : oversizedGoogleTtsJsonResponse ( ( ) => {
180+ cancelCount += 1 ;
181+ } ) ,
182+ release,
183+ } )
184+ . mockResolvedValueOnce ( {
185+ response : oversizedGoogleTtsJsonResponse ( ( ) => {
186+ cancelCount += 1 ;
187+ } ) ,
188+ release,
189+ } ) ;
190+ const provider = buildGoogleSpeechProvider ( ) ;
191+
192+ await expect (
193+ provider . synthesize ( {
194+ text : "oversized tts response" ,
195+ cfg : { } ,
196+ providerConfig : {
197+ apiKey : "google-test-key" ,
198+ } ,
199+ target : "audio-file" ,
200+ timeoutMs : 12_000 ,
201+ } ) ,
202+ ) . rejects . toThrow ( "Google TTS response: JSON response exceeds 16777216 bytes" ) ;
203+ expect ( cancelCount ) . toBe ( 2 ) ;
204+ expect ( release ) . toHaveBeenCalledTimes ( 2 ) ;
205+ } ) ;
206+
152207 it ( "transcodes Gemini PCM to Opus for voice-note targets" , async ( ) => {
153208 installGoogleTtsRequestMock ( Buffer . from ( [ 5 , 0 , 6 , 0 ] ) ) ;
154209 transcodeAudioBufferToOpusMock . mockResolvedValueOnce ( Buffer . from ( "google-opus" ) ) ;
0 commit comments