@@ -18,6 +18,7 @@ import { tmpdir } from 'node:os';
1818import { delimiter , join , resolve } from 'node:path' ;
1919import { afterAll , afterEach , beforeAll , describe , expect , it } from 'vitest' ;
2020import {
21+ bufferedAntigravityGeminiFirstTokenAt ,
2122 composeLiveInstructionPrompt ,
2223 resolveGrantedCodexImagegenOverride ,
2324 resolveCodexGeneratedImagesDir ,
@@ -1978,6 +1979,181 @@ process.exit(0);
19781979 ) ;
19791980 } ) ;
19801981
1982+ it ( 'parses successful Antigravity Gemini JSONL output instead of forwarding raw stdout' , async ( ) => {
1983+ await withFakeAgent (
1984+ 'agy' ,
1985+ `
1986+ const args = process.argv.slice(2);
1987+ if (args[0] === '--version') {
1988+ console.log('1.107.0-test');
1989+ process.exit(0);
1990+ }
1991+ process.stdout.write(JSON.stringify({ type: 'init', session_id: 'agy-1', model: 'gemini-3.5-flash' }) + '\\n');
1992+ process.stdout.write(JSON.stringify({ type: 'message', role: 'assistant', content: 'Hello from Antigravity.', delta: true }) + '\\n');
1993+ process.stdout.write(JSON.stringify({ type: 'result', status: 'success', stats: { input_tokens: 4, output_tokens: 5, cached: 0, duration_ms: 25 } }) + '\\n');
1994+ process.exit(0);
1995+ ` ,
1996+ async ( ) => {
1997+ const createResponse = await fetch ( `${ baseUrl } /api/runs` , {
1998+ method : 'POST' ,
1999+ headers : { 'Content-Type' : 'application/json' } ,
2000+ body : JSON . stringify ( {
2001+ agentId : 'antigravity' ,
2002+ message : 'hello' ,
2003+ } ) ,
2004+ } ) ;
2005+ expect ( createResponse . status ) . toBe ( 202 ) ;
2006+ const { runId } = await createResponse . json ( ) as { runId : string } ;
2007+
2008+ const eventsController = new AbortController ( ) ;
2009+ const eventsResponse = await fetch ( `${ baseUrl } /api/runs/${ runId } /events` , {
2010+ signal : eventsController . signal ,
2011+ } ) ;
2012+ const eventsBody = await readSseUntil ( eventsResponse , 'event: final' ) ;
2013+ eventsController . abort ( ) ;
2014+ const statusBody = await waitForRunStatus ( baseUrl , runId ) ;
2015+
2016+ expect ( eventsBody ) . toContain ( 'event: agent' ) ;
2017+ expect ( eventsBody ) . toContain ( '"type":"text_delta","delta":"Hello from Antigravity."' ) ;
2018+ expect ( eventsBody ) . toContain ( '"type":"usage"' ) ;
2019+ expect ( eventsBody ) . not . toContain ( 'event: stdout' ) ;
2020+ expect ( eventsBody ) . not . toContain ( '"role":"assistant"' ) ;
2021+ expect ( statusBody . status ) . toBe ( 'succeeded' ) ;
2022+ } ,
2023+ ) ;
2024+ } ) ;
2025+
2026+ it ( 'forwards Antigravity plain stdout JSONL when it lacks the Gemini init marker' , async ( ) => {
2027+ await withFakeAgent (
2028+ 'agy' ,
2029+ `
2030+ const args = process.argv.slice(2);
2031+ if (args[0] === '--version') {
2032+ console.log('1.107.0-test');
2033+ process.exit(0);
2034+ }
2035+ process.stdout.write(JSON.stringify({ type: 'error', message: 'requested JSONL output' }) + '\\n');
2036+ process.exit(0);
2037+ ` ,
2038+ async ( ) => {
2039+ const createResponse = await fetch ( `${ baseUrl } /api/runs` , {
2040+ method : 'POST' ,
2041+ headers : { 'Content-Type' : 'application/json' } ,
2042+ body : JSON . stringify ( {
2043+ agentId : 'antigravity' ,
2044+ message : 'return JSONL' ,
2045+ } ) ,
2046+ } ) ;
2047+ expect ( createResponse . status ) . toBe ( 202 ) ;
2048+ const { runId } = await createResponse . json ( ) as { runId : string } ;
2049+
2050+ const eventsController = new AbortController ( ) ;
2051+ const eventsResponse = await fetch ( `${ baseUrl } /api/runs/${ runId } /events` , {
2052+ signal : eventsController . signal ,
2053+ } ) ;
2054+ const eventsBody = await readSseUntil ( eventsResponse , 'event: final' ) ;
2055+ eventsController . abort ( ) ;
2056+ const statusBody = await waitForRunStatus ( baseUrl , runId ) ;
2057+
2058+ expect ( eventsBody ) . toContain ( 'event: stdout' ) ;
2059+ expect ( eventsBody ) . toContain ( 'requested JSONL output' ) ;
2060+ expect ( eventsBody ) . not . toContain ( 'event: error' ) ;
2061+ expect ( statusBody . status ) . toBe ( 'succeeded' ) ;
2062+ } ,
2063+ ) ;
2064+ } ) ;
2065+
2066+ it ( 'fails Antigravity Gemini JSONL output with no visible assistant content' , async ( ) => {
2067+ await withFakeAgent (
2068+ 'agy' ,
2069+ `
2070+ const args = process.argv.slice(2);
2071+ if (args[0] === '--version') {
2072+ console.log('1.107.0-test');
2073+ process.exit(0);
2074+ }
2075+ process.stdout.write(JSON.stringify({ type: 'init', session_id: 'agy-1', model: 'gemini-3.5-flash' }) + '\\n');
2076+ process.stdout.write(JSON.stringify({ type: 'result', status: 'success', stats: { input_tokens: 4, output_tokens: 0, cached: 0, duration_ms: 25 } }) + '\\n');
2077+ process.exit(0);
2078+ ` ,
2079+ async ( ) => {
2080+ const createResponse = await fetch ( `${ baseUrl } /api/runs` , {
2081+ method : 'POST' ,
2082+ headers : { 'Content-Type' : 'application/json' } ,
2083+ body : JSON . stringify ( {
2084+ agentId : 'antigravity' ,
2085+ message : 'hello' ,
2086+ } ) ,
2087+ } ) ;
2088+ expect ( createResponse . status ) . toBe ( 202 ) ;
2089+ const { runId } = await createResponse . json ( ) as { runId : string } ;
2090+
2091+ const eventsController = new AbortController ( ) ;
2092+ const eventsResponse = await fetch ( `${ baseUrl } /api/runs/${ runId } /events` , {
2093+ signal : eventsController . signal ,
2094+ } ) ;
2095+ const eventsBody = await readSseUntil ( eventsResponse , 'Agent completed without producing any output' ) ;
2096+ eventsController . abort ( ) ;
2097+ const statusBody = await waitForRunStatus ( baseUrl , runId ) ;
2098+
2099+ expect ( eventsBody ) . toContain ( 'event: agent' ) ;
2100+ expect ( eventsBody ) . toContain ( '"type":"usage"' ) ;
2101+ expect ( eventsBody ) . toContain ( 'event: error' ) ;
2102+ expect ( eventsBody ) . toContain ( 'AGENT_EXECUTION_FAILED' ) ;
2103+ expect ( eventsBody ) . not . toContain ( 'event: stdout' ) ;
2104+ expect ( statusBody . status ) . toBe ( 'failed' ) ;
2105+ } ,
2106+ ) ;
2107+ } ) ;
2108+
2109+ it ( 'preserves the first buffered stdout timestamp for Antigravity Gemini assistant text' , ( ) => {
2110+ const timestamp = bufferedAntigravityGeminiFirstTokenAt (
2111+ [ {
2112+ receivedAt : 1_234 ,
2113+ text : [
2114+ JSON . stringify ( { type : 'init' , session_id : 'agy-1' , model : 'gemini-3.5-flash' } ) ,
2115+ JSON . stringify ( { type : 'message' , role : 'assistant' , content : 'Hello from Antigravity.' , delta : true } ) ,
2116+ JSON . stringify ( { type : 'result' , status : 'success' , stats : { input_tokens : 4 , output_tokens : 5 } } ) ,
2117+ ] . join ( '\n' ) ,
2118+ } ] ,
2119+ ) ;
2120+
2121+ expect ( timestamp ) . toBe ( 1_234 ) ;
2122+ } ) ;
2123+
2124+ it ( 'stamps Antigravity Gemini assistant text from the chunk that completes the first assistant message' , ( ) => {
2125+ const timestamp = bufferedAntigravityGeminiFirstTokenAt ( [
2126+ {
2127+ receivedAt : 1_234 ,
2128+ text : `${ JSON . stringify ( { type : 'init' , session_id : 'agy-1' , model : 'gemini-3.5-flash' } ) } \n` ,
2129+ } ,
2130+ {
2131+ receivedAt : 5_678 ,
2132+ text : `${ JSON . stringify ( { type : 'message' , role : 'assistant' , content : 'Hello from Antigravity.' , delta : true } ) } \n` ,
2133+ } ,
2134+ {
2135+ receivedAt : 9_999 ,
2136+ text : `${ JSON . stringify ( { type : 'result' , status : 'success' , stats : { input_tokens : 4 , output_tokens : 5 } } ) } \n` ,
2137+ } ,
2138+ ] ) ;
2139+
2140+ expect ( timestamp ) . toBe ( 5_678 ) ;
2141+ } ) ;
2142+
2143+ it ( 'does not stamp a first token timestamp for Antigravity Gemini streams without assistant text' , ( ) => {
2144+ const timestamp = bufferedAntigravityGeminiFirstTokenAt (
2145+ [ {
2146+ receivedAt : 1_234 ,
2147+ text : [
2148+ JSON . stringify ( { type : 'init' , session_id : 'agy-1' , model : 'gemini-3.5-flash' } ) ,
2149+ JSON . stringify ( { type : 'result' , status : 'success' , stats : { input_tokens : 4 , output_tokens : 0 } } ) ,
2150+ ] . join ( '\n' ) ,
2151+ } ] ,
2152+ ) ;
2153+
2154+ expect ( timestamp ) . toBeNull ( ) ;
2155+ } ) ;
2156+
19812157 it ( 'surfaces Qoder assistant error records through the SSE error channel' , async ( ) => {
19822158 const qoderErrorLine = JSON . stringify ( {
19832159 type : 'assistant' ,
0 commit comments