@@ -4,6 +4,8 @@ import { PRODUCT_NAME } from "../branding.js";
44import {
55 createCodexResponsesAdapter ,
66 isResponsesStreamTerminal ,
7+ signatureForModel ,
8+ tagSignature ,
79} from "./codex-responses-adapter.js" ;
810
911const source : LastCycleSource = {
@@ -134,6 +136,158 @@ describe("createCodexResponsesAdapter usage parsing", () => {
134136 } ) ;
135137} ) ;
136138
139+ describe ( "signatureForModel" , ( ) => {
140+ const turnWithModel = ( model : string | undefined ) : ConversationTurn =>
141+ ( {
142+ role : "assistant" ,
143+ model,
144+ content : [ ] ,
145+ timestamp : 0 ,
146+ } ) as unknown as ConversationTurn ;
147+
148+ test ( "replays a signature on a turn with no persisted model" , ( ) => {
149+ const signature = tagSignature ( "codex-responses" , "cipher" ) ;
150+ const result = signatureForModel (
151+ turnWithModel ( undefined ) ,
152+ "gpt-5.1-codex" ,
153+ "codex-responses" ,
154+ signature ,
155+ ) ;
156+ expect ( result ) . toBe ( "cipher" ) ;
157+ } ) ;
158+
159+ test ( "drops a signature when the turn's model genuinely differs" , ( ) => {
160+ const signature = tagSignature ( "codex-responses" , "cipher" ) ;
161+ const result = signatureForModel (
162+ turnWithModel ( "gpt-5.0-codex" ) ,
163+ "gpt-5.1-codex" ,
164+ "codex-responses" ,
165+ signature ,
166+ ) ;
167+ expect ( result ) . toBeUndefined ( ) ;
168+ } ) ;
169+
170+ test ( "replays a signature when the model matches" , ( ) => {
171+ const signature = tagSignature ( "codex-responses" , "cipher" ) ;
172+ const result = signatureForModel (
173+ turnWithModel ( "gpt-5.1-codex" ) ,
174+ "gpt-5.1-codex" ,
175+ "codex-responses" ,
176+ signature ,
177+ ) ;
178+ expect ( result ) . toBe ( "cipher" ) ;
179+ } ) ;
180+ } ) ;
181+
182+ describe ( "createCodexResponsesAdapter orphaned function_call suppression" , ( ) => {
183+ test ( "drops a function_call whose reasoning signature could not be replayed" , ( ) => {
184+ const adapter = createCodexResponsesAdapter ( source ) ;
185+ const turns : ConversationTurn [ ] = [
186+ { role : "user" , timestamp : 0 , content : [ { type : "text" , text : "hi" } ] } ,
187+ {
188+ role : "assistant" ,
189+ model : "gpt-5.0-codex" ,
190+ timestamp : 0 ,
191+ content : [
192+ { type : "thinking" , thinking : "ponder" , signature : tagSignature ( "codex-responses" , "c" ) } ,
193+ { type : "tool_call" , id : "call_1" , name : "shell" , arguments : { } } ,
194+ ] ,
195+ } ,
196+ ] as unknown as ConversationTurn [ ] ;
197+
198+ const request = adapter . buildRequest ( turns , "gpt-5.1-codex" , { } ) ;
199+ const body = JSON . parse ( request . body ) as { input : { type : string } [ ] } ;
200+
201+ expect ( body . input . some ( ( item ) => item . type === "reasoning" ) ) . toBe ( false ) ;
202+ expect ( body . input . some ( ( item ) => item . type === "function_call" ) ) . toBe ( false ) ;
203+ } ) ;
204+
205+ test ( "keeps the function_call when its reasoning signature replays cleanly" , ( ) => {
206+ const adapter = createCodexResponsesAdapter ( source ) ;
207+ const turns : ConversationTurn [ ] = [
208+ { role : "user" , timestamp : 0 , content : [ { type : "text" , text : "hi" } ] } ,
209+ {
210+ role : "assistant" ,
211+ model : "gpt-5.1-codex" ,
212+ timestamp : 0 ,
213+ content : [
214+ { type : "thinking" , thinking : "ponder" , signature : tagSignature ( "codex-responses" , "c" ) } ,
215+ { type : "tool_call" , id : "call_1" , name : "shell" , arguments : { } } ,
216+ ] ,
217+ } ,
218+ ] as unknown as ConversationTurn [ ] ;
219+
220+ const request = adapter . buildRequest ( turns , "gpt-5.1-codex" , { } ) ;
221+ const body = JSON . parse ( request . body ) as { input : { type : string } [ ] } ;
222+
223+ expect ( body . input . some ( ( item ) => item . type === "reasoning" ) ) . toBe ( true ) ;
224+ expect ( body . input . some ( ( item ) => item . type === "function_call" ) ) . toBe ( true ) ;
225+ } ) ;
226+ } ) ;
227+
228+ describe ( "createCodexResponsesAdapter tool-name codec" , ( ) => {
229+ test ( "encodes a non-wire-safe tool name on the outgoing function tool definition" , ( ) => {
230+ const adapter = createCodexResponsesAdapter ( source ) ;
231+ const turns : ConversationTurn [ ] = [
232+ { role : "user" , timestamp : 0 , content : [ { type : "text" , text : "hi" } ] } ,
233+ ] ;
234+
235+ const request = adapter . buildRequest ( turns , "gpt-5.1-codex" , {
236+ tools : [
237+ {
238+ name : "@intx/tools-posix/sidecar-bundle:run_shell" ,
239+ description : "run a shell command" ,
240+ inputSchema : { } ,
241+ } ,
242+ ] ,
243+ } as never ) ;
244+ const body = JSON . parse ( request . body ) as { tools : { name : string } [ ] } ;
245+
246+ expect ( body . tools [ 0 ] ?. name ) . toMatch ( / ^ [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ - ] * $ / ) ;
247+ expect ( body . tools [ 0 ] ?. name ) . not . toBe ( "@intx/tools-posix/sidecar-bundle:run_shell" ) ;
248+ } ) ;
249+
250+ test ( "decodes an encoded tool_call.start name back to the internal id" , ( ) => {
251+ const adapter = createCodexResponsesAdapter ( source ) ;
252+ const encoded = "IX_-40intx-2Ftools-2Dposix-2Fsidecar-2Dbundle-3Arun_shell" ;
253+ const sseData = JSON . stringify ( {
254+ type : "response.output_item.added" ,
255+ item : { type : "function_call" , id : "item_1" , call_id : "call_1" , name : encoded } ,
256+ } ) ;
257+
258+ const events = adapter . parseResponse ( sseData ) ;
259+ const start = events . find ( ( e ) => e . type === "inference.tool_call.start" ) ;
260+
261+ expect ( ( start ?. data as { name ?: string } ) ?. name ) . not . toBe ( encoded ) ;
262+ } ) ;
263+ } ) ;
264+
265+ describe ( "createCodexResponsesAdapter block indexer reset" , ( ) => {
266+ test ( "resets block indices on a new buildRequest instead of accumulating across requests" , ( ) => {
267+ const adapter = createCodexResponsesAdapter ( source ) ;
268+ const turns : ConversationTurn [ ] = [
269+ { role : "user" , timestamp : 0 , content : [ { type : "text" , text : "hi" } ] } ,
270+ ] ;
271+
272+ adapter . buildRequest ( turns , "gpt-5.1-codex" , { } ) ;
273+ adapter . parseResponse (
274+ JSON . stringify ( { type : "response.output_text.delta" , item_id : "item_1" , delta : "a" } ) ,
275+ ) ;
276+ adapter . parseResponse (
277+ JSON . stringify ( { type : "response.output_text.delta" , item_id : "item_2" , delta : "b" } ) ,
278+ ) ;
279+
280+ // A new request (a fresh HTTP round trip) with a brand-new item id should
281+ // start indexing from 0 again, not continue accumulating from the prior
282+ // request's indexer state.
283+ adapter . buildRequest ( turns , "gpt-5.1-codex" , { } ) ;
284+ const secondRequestDelta = adapter . parseResponse (
285+ JSON . stringify ( { type : "response.output_text.delta" , item_id : "item_3" , delta : "c" } ) ,
286+ ) ;
287+ expect ( ( secondRequestDelta [ 0 ] ?. data as { index ?: number } ) ?. index ) . toBe ( 0 ) ;
288+ } ) ;
289+ } ) ;
290+
137291describe ( "isResponsesStreamTerminal" , ( ) => {
138292 test ( "is true for the Responses end-of-turn events" , ( ) => {
139293 for ( const type of [ "response.completed" , "response.incomplete" , "response.done" ] ) {
0 commit comments