@@ -1810,6 +1810,162 @@ describe("runCodexAppServerAttempt", () => {
18101810 expect ( request . mock . calls . map ( ( [ method ] ) => method ) ) . not . toContain ( "app/list" ) ;
18111811 } ) ;
18121812
1813+ it ( "retires the shared Codex app-server client after one-shot cleanup turns" , async ( ) => {
1814+ const retireSpy = vi . spyOn ( sharedClientModule , "retireSharedCodexAppServerClientIfCurrent" ) ;
1815+ retireSpy . mockReturnValue ( { activeLeases : 0 , closed : true } ) ;
1816+ const events : string [ ] = [ ] ;
1817+ const closeAndWait = vi . fn ( async ( ) => {
1818+ events . push ( "closeAndWait" ) ;
1819+ return true ;
1820+ } ) ;
1821+ let startedClient : unknown ;
1822+ let notify : ( ( notification : CodexServerNotification ) => Promise < void > ) | undefined ;
1823+ setCodexAppServerClientFactoryForTest ( async ( ) => {
1824+ const client = {
1825+ request : vi . fn ( async ( method : string ) => {
1826+ events . push ( `request:${ method } ` ) ;
1827+ if ( method === "thread/start" ) {
1828+ return threadStartResult ( ) ;
1829+ }
1830+ if ( method === "turn/start" ) {
1831+ return turnStartResult ( ) ;
1832+ }
1833+ return { } ;
1834+ } ) ,
1835+ addNotificationHandler : vi . fn ( ( handler ) => {
1836+ notify = handler ;
1837+ return ( ) => undefined ;
1838+ } ) ,
1839+ addRequestHandler : vi . fn ( ( ) => ( ) => undefined ) ,
1840+ addCloseHandler : vi . fn ( ( ) => ( ) => undefined ) ,
1841+ closeAndWait,
1842+ } ;
1843+ startedClient = client ;
1844+ return client as never ;
1845+ } ) ;
1846+ const params = createParams (
1847+ path . join ( tempDir , "session.jsonl" ) ,
1848+ path . join ( tempDir , "workspace" ) ,
1849+ ) ;
1850+ params . cleanupBundleMcpOnRunEnd = true ;
1851+
1852+ const run = runCodexAppServerAttempt ( params ) ;
1853+ await vi . waitFor ( ( ) => expect ( notify ) . toBeDefined ( ) , fastWait ) ;
1854+ if ( ! notify ) {
1855+ throw new Error ( "expected turn notification handler" ) ;
1856+ }
1857+ await notify ( {
1858+ method : "turn/completed" ,
1859+ params : {
1860+ threadId : "thread-1" ,
1861+ turnId : "turn-1" ,
1862+ turn : { id : "turn-1" , status : "completed" } ,
1863+ } ,
1864+ } ) ;
1865+ await run ;
1866+
1867+ expect ( retireSpy ) . toHaveBeenCalledWith ( startedClient ) ;
1868+ expect ( closeAndWait ) . toHaveBeenCalledWith ( { exitTimeoutMs : 2_000 , forceKillDelayMs : 250 } ) ;
1869+ expect ( events . indexOf ( "request:thread/unsubscribe" ) ) . toBeGreaterThan ( - 1 ) ;
1870+ expect ( events . indexOf ( "closeAndWait" ) ) . toBeGreaterThan (
1871+ events . indexOf ( "request:thread/unsubscribe" ) ,
1872+ ) ;
1873+ } ) ;
1874+
1875+ it ( "retires the shared Codex app-server client after one-shot turn start failures" , async ( ) => {
1876+ const retireSpy = vi . spyOn ( sharedClientModule , "retireSharedCodexAppServerClientIfCurrent" ) ;
1877+ retireSpy . mockReturnValue ( { activeLeases : 0 , closed : true } ) ;
1878+ const events : string [ ] = [ ] ;
1879+ const closeAndWait = vi . fn ( async ( ) => {
1880+ events . push ( "closeAndWait" ) ;
1881+ return true ;
1882+ } ) ;
1883+ let startedClient : unknown ;
1884+ setCodexAppServerClientFactoryForTest ( async ( ) => {
1885+ const client = {
1886+ request : vi . fn ( async ( method : string ) => {
1887+ events . push ( `request:${ method } ` ) ;
1888+ if ( method === "thread/start" ) {
1889+ return threadStartResult ( ) ;
1890+ }
1891+ if ( method === "turn/start" ) {
1892+ throw new Error ( "turn start failed" ) ;
1893+ }
1894+ return { } ;
1895+ } ) ,
1896+ addNotificationHandler : vi . fn ( ( ) => ( ) => undefined ) ,
1897+ addRequestHandler : vi . fn ( ( ) => ( ) => undefined ) ,
1898+ addCloseHandler : vi . fn ( ( ) => ( ) => undefined ) ,
1899+ closeAndWait,
1900+ } ;
1901+ startedClient = client ;
1902+ return client as never ;
1903+ } ) ;
1904+ const params = createParams (
1905+ path . join ( tempDir , "session.jsonl" ) ,
1906+ path . join ( tempDir , "workspace" ) ,
1907+ ) ;
1908+ params . cleanupBundleMcpOnRunEnd = true ;
1909+
1910+ await expect ( runCodexAppServerAttempt ( params ) ) . rejects . toThrow ( "turn start failed" ) ;
1911+
1912+ expect ( retireSpy ) . toHaveBeenCalledWith ( startedClient ) ;
1913+ expect ( closeAndWait ) . toHaveBeenCalledWith ( { exitTimeoutMs : 2_000 , forceKillDelayMs : 250 } ) ;
1914+ expect ( events . indexOf ( "request:thread/unsubscribe" ) ) . toBeGreaterThan ( - 1 ) ;
1915+ expect ( events . indexOf ( "closeAndWait" ) ) . toBeGreaterThan (
1916+ events . indexOf ( "request:thread/unsubscribe" ) ,
1917+ ) ;
1918+ } ) ;
1919+
1920+ it ( "keeps the shared Codex app-server client warm without one-shot cleanup" , async ( ) => {
1921+ const retireSpy = vi . spyOn ( sharedClientModule , "retireSharedCodexAppServerClientIfCurrent" ) ;
1922+ const closeAndWait = vi . fn ( async ( ) => true ) ;
1923+ let notify : ( ( notification : CodexServerNotification ) => Promise < void > ) | undefined ;
1924+ setCodexAppServerClientFactoryForTest (
1925+ async ( ) =>
1926+ ( {
1927+ request : vi . fn ( async ( method : string ) => {
1928+ if ( method === "thread/start" ) {
1929+ return threadStartResult ( ) ;
1930+ }
1931+ if ( method === "turn/start" ) {
1932+ return turnStartResult ( ) ;
1933+ }
1934+ return { } ;
1935+ } ) ,
1936+ addNotificationHandler : vi . fn ( ( handler ) => {
1937+ notify = handler ;
1938+ return ( ) => undefined ;
1939+ } ) ,
1940+ addRequestHandler : vi . fn ( ( ) => ( ) => undefined ) ,
1941+ addCloseHandler : vi . fn ( ( ) => ( ) => undefined ) ,
1942+ closeAndWait,
1943+ } ) as never ,
1944+ ) ;
1945+ const params = createParams (
1946+ path . join ( tempDir , "session.jsonl" ) ,
1947+ path . join ( tempDir , "workspace" ) ,
1948+ ) ;
1949+
1950+ const run = runCodexAppServerAttempt ( params ) ;
1951+ await vi . waitFor ( ( ) => expect ( notify ) . toBeDefined ( ) , fastWait ) ;
1952+ if ( ! notify ) {
1953+ throw new Error ( "expected turn notification handler" ) ;
1954+ }
1955+ await notify ( {
1956+ method : "turn/completed" ,
1957+ params : {
1958+ threadId : "thread-1" ,
1959+ turnId : "turn-1" ,
1960+ turn : { id : "turn-1" , status : "completed" } ,
1961+ } ,
1962+ } ) ;
1963+ await run ;
1964+
1965+ expect ( retireSpy ) . not . toHaveBeenCalled ( ) ;
1966+ expect ( closeAndWait ) . not . toHaveBeenCalled ( ) ;
1967+ } ) ;
1968+
18131969 it ( "keeps searchable Codex dynamic tools canonical in mirrored transcript snapshots" , async ( ) => {
18141970 const params = createParams (
18151971 path . join ( tempDir , "session.jsonl" ) ,
0 commit comments