@@ -97,4 +97,312 @@ describe("OpenAiCodexHandler native tool calls", () => {
9797 name : "attempt_completion" ,
9898 } )
9999 } )
100+
101+ it ( "yields text when Codex emits assistant message only in response.output_item.done" , async ( ) => {
102+ vi . spyOn ( openAiCodexOAuthManager , "getAccessToken" ) . mockResolvedValue ( "test-token" )
103+ vi . spyOn ( openAiCodexOAuthManager , "getAccountId" ) . mockResolvedValue ( "acct_test" )
104+ ; ( handler as any ) . client = {
105+ responses : {
106+ create : vi . fn ( ) . mockResolvedValue ( {
107+ async * [ Symbol . asyncIterator ] ( ) {
108+ yield {
109+ type : "response.output_item.done" ,
110+ item : {
111+ type : "message" ,
112+ role : "assistant" ,
113+ content : [ { type : "output_text" , text : "hello from spark" } ] ,
114+ } ,
115+ output_index : 0 ,
116+ }
117+ yield {
118+ type : "response.completed" ,
119+ response : {
120+ id : "resp_done_only" ,
121+ status : "completed" ,
122+ output : [
123+ {
124+ type : "message" ,
125+ role : "assistant" ,
126+ content : [ { type : "output_text" , text : "hello from spark" } ] ,
127+ } ,
128+ ] ,
129+ usage : { input_tokens : 1 , output_tokens : 2 } ,
130+ } ,
131+ }
132+ } ,
133+ } ) ,
134+ } ,
135+ }
136+
137+ const stream = handler . createMessage ( "system" , [ { role : "user" , content : "test" } as any ] , {
138+ taskId : "t" ,
139+ tools : [ ] ,
140+ } )
141+
142+ const chunks : any [ ] = [ ]
143+ for await ( const chunk of stream ) {
144+ chunks . push ( chunk )
145+ }
146+
147+ const textChunks = chunks . filter ( ( c ) => c . type === "text" )
148+ expect ( textChunks . length ) . toBeGreaterThan ( 0 )
149+ expect ( textChunks . map ( ( c ) => c . text ) . join ( "" ) ) . toContain ( "hello from spark" )
150+ } )
151+
152+ it ( "yields text when Codex emits assistant message only in response.completed output" , async ( ) => {
153+ vi . spyOn ( openAiCodexOAuthManager , "getAccessToken" ) . mockResolvedValue ( "test-token" )
154+ vi . spyOn ( openAiCodexOAuthManager , "getAccountId" ) . mockResolvedValue ( "acct_test" )
155+ ; ( handler as any ) . client = {
156+ responses : {
157+ create : vi . fn ( ) . mockResolvedValue ( {
158+ async * [ Symbol . asyncIterator ] ( ) {
159+ yield {
160+ type : "response.completed" ,
161+ response : {
162+ id : "resp_completed_only" ,
163+ status : "completed" ,
164+ output : [
165+ {
166+ type : "message" ,
167+ role : "assistant" ,
168+ content : [ { type : "output_text" , text : "final payload only" } ] ,
169+ } ,
170+ ] ,
171+ usage : { input_tokens : 1 , output_tokens : 2 } ,
172+ } ,
173+ }
174+ } ,
175+ } ) ,
176+ } ,
177+ }
178+
179+ const stream = handler . createMessage ( "system" , [ { role : "user" , content : "test" } as any ] , {
180+ taskId : "t" ,
181+ tools : [ ] ,
182+ } )
183+
184+ const chunks : any [ ] = [ ]
185+ for await ( const chunk of stream ) {
186+ chunks . push ( chunk )
187+ }
188+
189+ const textChunks = chunks . filter ( ( c ) => c . type === "text" )
190+ expect ( textChunks . length ) . toBeGreaterThan ( 0 )
191+ expect ( textChunks . map ( ( c ) => c . text ) . join ( "" ) ) . toContain ( "final payload only" )
192+ } )
193+
194+ it ( "yields text when Codex emits response.output_text.done without deltas" , async ( ) => {
195+ vi . spyOn ( openAiCodexOAuthManager , "getAccessToken" ) . mockResolvedValue ( "test-token" )
196+ vi . spyOn ( openAiCodexOAuthManager , "getAccountId" ) . mockResolvedValue ( "acct_test" )
197+ ; ( handler as any ) . client = {
198+ responses : {
199+ create : vi . fn ( ) . mockResolvedValue ( {
200+ async * [ Symbol . asyncIterator ] ( ) {
201+ yield {
202+ type : "response.output_text.done" ,
203+ text : "done-event text only" ,
204+ }
205+ yield {
206+ type : "response.completed" ,
207+ response : {
208+ id : "resp_done_text_only" ,
209+ status : "completed" ,
210+ output : [ ] ,
211+ usage : { input_tokens : 1 , output_tokens : 2 } ,
212+ } ,
213+ }
214+ } ,
215+ } ) ,
216+ } ,
217+ }
218+
219+ const stream = handler . createMessage ( "system" , [ { role : "user" , content : "test" } as any ] , {
220+ taskId : "t" ,
221+ tools : [ ] ,
222+ } )
223+
224+ const chunks : any [ ] = [ ]
225+ for await ( const chunk of stream ) {
226+ chunks . push ( chunk )
227+ }
228+
229+ const textChunks = chunks . filter ( ( c ) => c . type === "text" )
230+ expect ( textChunks . length ) . toBeGreaterThan ( 0 )
231+ expect ( textChunks . map ( ( c ) => c . text ) . join ( "" ) ) . toContain ( "done-event text only" )
232+ } )
233+
234+ it ( "yields tool_call when Codex emits function_call only in response.output_item.done" , async ( ) => {
235+ vi . spyOn ( openAiCodexOAuthManager , "getAccessToken" ) . mockResolvedValue ( "test-token" )
236+ vi . spyOn ( openAiCodexOAuthManager , "getAccountId" ) . mockResolvedValue ( "acct_test" )
237+ ; ( handler as any ) . client = {
238+ responses : {
239+ create : vi . fn ( ) . mockResolvedValue ( {
240+ async * [ Symbol . asyncIterator ] ( ) {
241+ yield {
242+ type : "response.output_item.done" ,
243+ item : {
244+ type : "function_call" ,
245+ call_id : "call_done_only" ,
246+ name : "attempt_completion" ,
247+ arguments : '{"result":"ok"}' ,
248+ } ,
249+ output_index : 0 ,
250+ }
251+ yield {
252+ type : "response.completed" ,
253+ response : {
254+ id : "resp_done_tool_only" ,
255+ status : "completed" ,
256+ output : [ ] ,
257+ usage : { input_tokens : 1 , output_tokens : 2 } ,
258+ } ,
259+ }
260+ } ,
261+ } ) ,
262+ } ,
263+ }
264+
265+ const stream = handler . createMessage ( "system" , [ { role : "user" , content : "test" } as any ] , {
266+ taskId : "t" ,
267+ tools : [ ] ,
268+ } )
269+
270+ const chunks : any [ ] = [ ]
271+ for await ( const chunk of stream ) {
272+ chunks . push ( chunk )
273+ }
274+
275+ const toolCalls = chunks . filter ( ( c ) => c . type === "tool_call" )
276+ expect ( toolCalls . length ) . toBeGreaterThan ( 0 )
277+ expect ( toolCalls [ 0 ] ) . toMatchObject ( {
278+ type : "tool_call" ,
279+ id : "call_done_only" ,
280+ name : "attempt_completion" ,
281+ } )
282+ } )
283+
284+ it ( "yields text when Codex emits response.content_part.added" , async ( ) => {
285+ vi . spyOn ( openAiCodexOAuthManager , "getAccessToken" ) . mockResolvedValue ( "test-token" )
286+ vi . spyOn ( openAiCodexOAuthManager , "getAccountId" ) . mockResolvedValue ( "acct_test" )
287+ ; ( handler as any ) . client = {
288+ responses : {
289+ create : vi . fn ( ) . mockResolvedValue ( {
290+ async * [ Symbol . asyncIterator ] ( ) {
291+ yield {
292+ type : "response.content_part.added" ,
293+ part : {
294+ type : "output_text" ,
295+ text : "content part text" ,
296+ } ,
297+ output_index : 0 ,
298+ content_index : 0 ,
299+ }
300+ yield {
301+ type : "response.completed" ,
302+ response : {
303+ id : "resp_content_part" ,
304+ status : "completed" ,
305+ output : [ ] ,
306+ usage : { input_tokens : 1 , output_tokens : 2 } ,
307+ } ,
308+ }
309+ } ,
310+ } ) ,
311+ } ,
312+ }
313+
314+ const stream = handler . createMessage ( "system" , [ { role : "user" , content : "test" } as any ] , {
315+ taskId : "t" ,
316+ tools : [ ] ,
317+ } )
318+
319+ const chunks : any [ ] = [ ]
320+ for await ( const chunk of stream ) {
321+ chunks . push ( chunk )
322+ }
323+
324+ const textChunks = chunks . filter ( ( c ) => c . type === "text" )
325+ expect ( textChunks . length ) . toBeGreaterThan ( 0 )
326+ expect ( textChunks . map ( ( c ) => c . text ) . join ( "" ) ) . toContain ( "content part text" )
327+ } )
328+
329+ it ( "does not duplicate text when Codex emits delta and output_text.done" , async ( ) => {
330+ vi . spyOn ( openAiCodexOAuthManager , "getAccessToken" ) . mockResolvedValue ( "test-token" )
331+ vi . spyOn ( openAiCodexOAuthManager , "getAccountId" ) . mockResolvedValue ( "acct_test" )
332+ ; ( handler as any ) . client = {
333+ responses : {
334+ create : vi . fn ( ) . mockResolvedValue ( {
335+ async * [ Symbol . asyncIterator ] ( ) {
336+ yield { type : "response.output_text.delta" , delta : "hello " }
337+ yield { type : "response.output_text.delta" , delta : "world" }
338+ yield { type : "response.output_text.done" , text : "hello world" }
339+ yield {
340+ type : "response.completed" ,
341+ response : {
342+ id : "resp_delta_done" ,
343+ status : "completed" ,
344+ output : [ ] ,
345+ usage : { input_tokens : 1 , output_tokens : 2 } ,
346+ } ,
347+ }
348+ } ,
349+ } ) ,
350+ } ,
351+ }
352+
353+ const stream = handler . createMessage ( "system" , [ { role : "user" , content : "test" } as any ] , {
354+ taskId : "t" ,
355+ tools : [ ] ,
356+ } )
357+
358+ const chunks : any [ ] = [ ]
359+ for await ( const chunk of stream ) {
360+ chunks . push ( chunk )
361+ }
362+
363+ const textChunks = chunks . filter ( ( c ) => c . type === "text" )
364+ expect ( textChunks . map ( ( c ) => c . text ) . join ( "" ) ) . toBe ( "hello world" )
365+ } )
366+
367+ it ( "does not duplicate text when Codex emits delta and content_part.added" , async ( ) => {
368+ vi . spyOn ( openAiCodexOAuthManager , "getAccessToken" ) . mockResolvedValue ( "test-token" )
369+ vi . spyOn ( openAiCodexOAuthManager , "getAccountId" ) . mockResolvedValue ( "acct_test" )
370+ ; ( handler as any ) . client = {
371+ responses : {
372+ create : vi . fn ( ) . mockResolvedValue ( {
373+ async * [ Symbol . asyncIterator ] ( ) {
374+ yield { type : "response.output_text.delta" , delta : "hello world" }
375+ yield {
376+ type : "response.content_part.added" ,
377+ part : { type : "output_text" , text : "hello world" } ,
378+ output_index : 0 ,
379+ content_index : 0 ,
380+ }
381+ yield {
382+ type : "response.completed" ,
383+ response : {
384+ id : "resp_delta_content_part" ,
385+ status : "completed" ,
386+ output : [ ] ,
387+ usage : { input_tokens : 1 , output_tokens : 2 } ,
388+ } ,
389+ }
390+ } ,
391+ } ) ,
392+ } ,
393+ }
394+
395+ const stream = handler . createMessage ( "system" , [ { role : "user" , content : "test" } as any ] , {
396+ taskId : "t" ,
397+ tools : [ ] ,
398+ } )
399+
400+ const chunks : any [ ] = [ ]
401+ for await ( const chunk of stream ) {
402+ chunks . push ( chunk )
403+ }
404+
405+ const textChunks = chunks . filter ( ( c ) => c . type === "text" )
406+ expect ( textChunks . map ( ( c ) => c . text ) . join ( "" ) ) . toBe ( "hello world" )
407+ } )
100408} )
0 commit comments