@@ -10,6 +10,7 @@ import * as Deferred from "effect/Deferred";
1010import * as Effect from "effect/Effect" ;
1111import * as Fiber from "effect/Fiber" ;
1212import * as Layer from "effect/Layer" ;
13+ import * as Ref from "effect/Ref" ;
1314import * as Schema from "effect/Schema" ;
1415import * as Stream from "effect/Stream" ;
1516
@@ -175,6 +176,230 @@ it.layer(grokAdapterTestLayer)("GrokAdapterLive", (it) => {
175176 } ) ,
176177 ) ;
177178
179+ it . effect ( "reports a Grok session running only while the prompt is in flight" , ( ) =>
180+ Effect . gen ( function * ( ) {
181+ const threadId = ThreadId . make ( "grok-session-ready-after-prompt" ) ;
182+ const wrapperPath = yield * Effect . promise ( ( ) =>
183+ makeMockGrokWrapper ( {
184+ T3_ACP_EMIT_TOOL_CALLS : "1" ,
185+ } ) ,
186+ ) ;
187+ const adapter = yield * makeTestAdapter ( wrapperPath ) ;
188+ const requestOpened =
189+ yield * Deferred . make < Extract < ProviderRuntimeEvent , { type : "request.opened" } > > ( ) ;
190+ const eventsFiber = yield * Stream . runForEach ( adapter . streamEvents , ( event ) =>
191+ event . type === "request.opened"
192+ ? Deferred . succeed ( requestOpened , event ) . pipe ( Effect . ignore )
193+ : Effect . void ,
194+ ) . pipe ( Effect . forkChild ) ;
195+
196+ yield * adapter . startSession ( {
197+ threadId,
198+ provider : ProviderDriverKind . make ( "grok" ) ,
199+ cwd : process . cwd ( ) ,
200+ runtimeMode : "approval-required" ,
201+ modelSelection : { instanceId : ProviderInstanceId . make ( "grok" ) , model : "grok-build" } ,
202+ } ) ;
203+
204+ const sendTurnFiber = yield * adapter
205+ . sendTurn ( { threadId, input : "check lifecycle" , attachments : [ ] } )
206+ . pipe ( Effect . forkChild ) ;
207+ const requestOpenedEvent = yield * Deferred . await ( requestOpened ) ;
208+
209+ const runningSessions = yield * adapter . listSessions ( ) ;
210+ const runningSession = runningSessions . find ( ( session ) => session . threadId === threadId ) ;
211+ assert . equal ( runningSession ?. status , "running" ) ;
212+ assert . isDefined ( runningSession ?. activeTurnId ) ;
213+
214+ yield * adapter . respondToRequest (
215+ threadId ,
216+ ApprovalRequestId . make ( String ( requestOpenedEvent . requestId ) ) ,
217+ "accept" ,
218+ ) ;
219+ yield * Fiber . join ( sendTurnFiber ) ;
220+
221+ const readySessions = yield * adapter . listSessions ( ) ;
222+ const readySession = readySessions . find ( ( session ) => session . threadId === threadId ) ;
223+ assert . equal ( readySession ?. status , "ready" ) ;
224+ assert . isUndefined ( readySession ?. activeTurnId ) ;
225+
226+ yield * Fiber . interrupt ( eventsFiber ) ;
227+ yield * adapter . stopSession ( threadId ) ;
228+ } ) ,
229+ ) ;
230+
231+ it . effect ( "completes a Grok turn from xAI prompt completion when the prompt RPC hangs" , ( ) =>
232+ Effect . gen ( function * ( ) {
233+ const threadId = ThreadId . make ( "grok-xai-prompt-complete-fallback" ) ;
234+ const wrapperPath = yield * Effect . promise ( ( ) =>
235+ makeMockGrokWrapper ( {
236+ T3_ACP_EMIT_XAI_PROMPT_COMPLETE_THEN_HANG : "1" ,
237+ } ) ,
238+ ) ;
239+ const adapter = yield * makeTestAdapter ( wrapperPath ) ;
240+
241+ const runtimeEvents : ProviderRuntimeEvent [ ] = [ ] ;
242+ const turnCompleted = yield * Deferred . make < void > ( ) ;
243+ const runtimeEventsFiber = yield * Stream . runForEach ( adapter . streamEvents , ( event ) =>
244+ Effect . sync ( ( ) => {
245+ runtimeEvents . push ( event ) ;
246+ } ) . pipe (
247+ Effect . andThen (
248+ event . type === "turn.completed"
249+ ? Deferred . succeed ( turnCompleted , undefined )
250+ : Effect . void ,
251+ ) ,
252+ ) ,
253+ ) . pipe ( Effect . forkChild ) ;
254+
255+ yield * adapter . startSession ( {
256+ threadId,
257+ provider : ProviderDriverKind . make ( "grok" ) ,
258+ cwd : process . cwd ( ) ,
259+ runtimeMode : "full-access" ,
260+ modelSelection : { instanceId : ProviderInstanceId . make ( "grok" ) , model : "grok-build" } ,
261+ } ) ;
262+
263+ const sendTurnResult = yield * adapter . sendTurn ( {
264+ threadId,
265+ input : "exercise fallback" ,
266+ attachments : [ ] ,
267+ } ) ;
268+
269+ yield * Deferred . await ( turnCompleted ) ;
270+ const readySessions = yield * adapter . listSessions ( ) ;
271+ const readySession = readySessions . find ( ( session ) => session . threadId === threadId ) ;
272+ const turnCompletedEvent = runtimeEvents . find (
273+ ( event ) : event is Extract < ProviderRuntimeEvent , { type : "turn.completed" } > =>
274+ event . type === "turn.completed" ,
275+ ) ;
276+ const eventTypes = runtimeEvents . map ( ( event ) => event . type ) ;
277+
278+ assert . equal ( sendTurnResult . threadId , threadId ) ;
279+ assert . include ( eventTypes , "turn.completed" ) ;
280+ assert . equal ( turnCompletedEvent ?. payload . stopReason , "end_turn" ) ;
281+ assert . equal ( readySession ?. status , "ready" ) ;
282+ assert . isUndefined ( readySession ?. activeTurnId ) ;
283+
284+ yield * Fiber . interrupt ( runtimeEventsFiber ) ;
285+ yield * adapter . stopSession ( threadId ) ;
286+ } ) ,
287+ ) ;
288+
289+ it . effect ( "settles the in-flight prompt before emitting completion" , ( ) =>
290+ Effect . gen ( function * ( ) {
291+ const threadId = ThreadId . make ( "grok-completion-before-next-turn" ) ;
292+ const wrapperPath = yield * Effect . promise ( ( ) => makeMockGrokWrapper ( ) ) ;
293+ const adapter = yield * makeTestAdapter ( wrapperPath ) ;
294+ const completedCountRef = yield * Ref . make ( 0 ) ;
295+ const secondTurnCompleted = yield * Deferred . make < void > ( ) ;
296+
297+ const runtimeEventsFiber = yield * Stream . runForEach ( adapter . streamEvents , ( event ) => {
298+ if ( event . type !== "turn.completed" || String ( event . threadId ) !== String ( threadId ) ) {
299+ return Effect . void ;
300+ }
301+
302+ return Ref . modify ( completedCountRef , ( count ) => {
303+ const nextCount = count + 1 ;
304+ return [ nextCount , nextCount ] as const ;
305+ } ) . pipe (
306+ Effect . flatMap ( ( count ) => {
307+ if ( count === 1 ) {
308+ return adapter
309+ . sendTurn ( {
310+ threadId,
311+ input : "second turn after completion" ,
312+ attachments : [ ] ,
313+ } )
314+ . pipe ( Effect . forkChild , Effect . asVoid ) ;
315+ }
316+ if ( count === 2 ) {
317+ return Deferred . succeed ( secondTurnCompleted , undefined ) . pipe ( Effect . asVoid ) ;
318+ }
319+ return Effect . void ;
320+ } ) ,
321+ ) ;
322+ } ) . pipe ( Effect . forkChild ) ;
323+
324+ yield * adapter . startSession ( {
325+ threadId,
326+ provider : ProviderDriverKind . make ( "grok" ) ,
327+ cwd : process . cwd ( ) ,
328+ runtimeMode : "full-access" ,
329+ modelSelection : { instanceId : ProviderInstanceId . make ( "grok" ) , model : "grok-build" } ,
330+ } ) ;
331+
332+ yield * adapter . sendTurn ( {
333+ threadId,
334+ input : "first turn" ,
335+ attachments : [ ] ,
336+ } ) ;
337+ yield * Deferred . await ( secondTurnCompleted ) ;
338+
339+ const completedCount = yield * Ref . get ( completedCountRef ) ;
340+ const readySessions = yield * adapter . listSessions ( ) ;
341+ const readySession = readySessions . find ( ( session ) => session . threadId === threadId ) ;
342+
343+ assert . equal ( completedCount , 2 ) ;
344+ assert . equal ( readySession ?. status , "ready" ) ;
345+ assert . isUndefined ( readySession ?. activeTurnId ) ;
346+
347+ yield * Fiber . interrupt ( runtimeEventsFiber ) ;
348+ yield * adapter . stopSession ( threadId ) ;
349+ } ) ,
350+ ) ;
351+
352+ it . effect ( "restores a Grok session to ready when the prompt RPC fails" , ( ) =>
353+ Effect . gen ( function * ( ) {
354+ const threadId = ThreadId . make ( "grok-prompt-failure-ready" ) ;
355+ const wrapperPath = yield * Effect . promise ( ( ) =>
356+ makeMockGrokWrapper ( {
357+ T3_ACP_FAIL_PROMPT : "1" ,
358+ } ) ,
359+ ) ;
360+ const adapter = yield * makeTestAdapter ( wrapperPath ) ;
361+ const runtimeEvents : ProviderRuntimeEvent [ ] = [ ] ;
362+ const runtimeEventsFiber = yield * Stream . runForEach ( adapter . streamEvents , ( event ) =>
363+ Effect . sync ( ( ) => {
364+ runtimeEvents . push ( event ) ;
365+ } ) ,
366+ ) . pipe ( Effect . forkChild ) ;
367+
368+ yield * adapter . startSession ( {
369+ threadId,
370+ provider : ProviderDriverKind . make ( "grok" ) ,
371+ cwd : process . cwd ( ) ,
372+ runtimeMode : "full-access" ,
373+ modelSelection : { instanceId : ProviderInstanceId . make ( "grok" ) , model : "grok-build" } ,
374+ } ) ;
375+
376+ const error = yield * Effect . flip (
377+ adapter . sendTurn ( {
378+ threadId,
379+ input : "fail prompt" ,
380+ attachments : [ ] ,
381+ } ) ,
382+ ) ;
383+ const readySessions = yield * adapter . listSessions ( ) ;
384+ const readySession = readySessions . find ( ( session ) => session . threadId === threadId ) ;
385+ const failedTurnCompleted = runtimeEvents . find (
386+ ( event ) => event . type === "turn.completed" && event . threadId === threadId ,
387+ ) ;
388+
389+ assert . equal ( error . _tag , "ProviderAdapterRequestError" ) ;
390+ assert . equal ( readySession ?. status , "ready" ) ;
391+ assert . isUndefined ( readySession ?. activeTurnId ) ;
392+ assert . equal ( failedTurnCompleted ?. type , "turn.completed" ) ;
393+ if ( failedTurnCompleted ?. type === "turn.completed" ) {
394+ assert . equal ( failedTurnCompleted . payload . state , "failed" ) ;
395+ assert . isString ( failedTurnCompleted . payload . errorMessage ) ;
396+ }
397+
398+ yield * Fiber . interrupt ( runtimeEventsFiber ) ;
399+ yield * adapter . stopSession ( threadId ) ;
400+ } ) ,
401+ ) ;
402+
178403 it . effect ( "rejects startSession when provider mismatches" , ( ) =>
179404 Effect . gen ( function * ( ) {
180405 const wrapperPath = yield * Effect . promise ( ( ) => makeMockGrokWrapper ( ) ) ;
0 commit comments