@@ -31,6 +31,93 @@ import {
3131} from '../../../support/live-claude.js' ;
3232
3333describe ( 'live Claude lifecycle' , ( ) => {
34+ test ( 'holds queue ownership across a background Bash continuation' , async ( ) => {
35+ const serverEnvironment = await liveClaudeServerEnvironment ( ) ;
36+ await withIntegrationFixture ( 'live-claude-background-continuation' , async ( fixture ) => {
37+ const chatId = fixture . newChatId ( ) ;
38+ const launchedMarker = marker ( 'BACKGROUND_LAUNCHED' ) ;
39+ const completedMarker = marker ( 'BACKGROUND_COMPLETED' ) ;
40+ const successorMarker = marker ( 'BACKGROUND_SUCCESSOR' ) ;
41+ const prompt = [
42+ 'Use the Bash tool exactly once with run_in_background set to true.' ,
43+ `Run exactly \`sleep 20; printf done\`.` ,
44+ `As soon as Bash reports that the command started in the background, reply with exactly ${ launchedMarker } and end that model turn.` ,
45+ 'Do not call TaskOutput, poll, sleep in another tool, or wait synchronously.' ,
46+ `When the background task completion notification arrives, reply with exactly ${ completedMarker } .` ,
47+ ] . join ( ' ' ) ;
48+ const cursor = fixture . client . markEvents ( ) ;
49+ const first = await fixture . client . startChat ( liveClaudeStartRequest ( {
50+ chatId,
51+ projectPath : fixture . dirs . project ,
52+ command : prompt ,
53+ permissionMode : 'bypassPermissions' ,
54+ } ) ) ;
55+
56+ await fixture . client . waitForEvent (
57+ ( event ) : event is ChatMessagesMessage =>
58+ event . type === 'chat-messages'
59+ && event . chatId === chatId
60+ && event . messages . some ( ( entry ) =>
61+ entry . message . type === 'assistant-message'
62+ && entry . message . content . includes ( launchedMarker ) ) ,
63+ 'live Claude background launch response' ,
64+ { afterIndex : cursor , timeoutMs : TURN_TIMEOUT_MS } ,
65+ ) ;
66+ expect ( fixture . client . eventsSince ( cursor ) ) . not . toContainEqual ( expect . objectContaining ( {
67+ type : 'agent-run-finished' ,
68+ chatId,
69+ turnId : first . turnId ,
70+ } ) ) ;
71+
72+ const queueCursor = fixture . client . markEvents ( ) ;
73+ const successorPrompt = exactReplyPrompt ( successorMarker ) ;
74+ const queued = await fixture . client . enqueueNew ( chatId , successorPrompt ) ;
75+ expect ( queued . control . queue . entries . map ( ( entry ) => entry . content ) ) . toEqual ( [
76+ successorPrompt ,
77+ ] ) ;
78+
79+ await fixture . client . waitForEvent (
80+ ( event ) : event is ChatMessagesMessage =>
81+ event . type === 'chat-messages'
82+ && event . chatId === chatId
83+ && event . messages . some ( ( entry ) =>
84+ entry . message . type === 'assistant-message'
85+ && entry . message . content . includes ( completedMarker ) ) ,
86+ 'live Claude background completion response' ,
87+ { afterIndex : cursor , timeoutMs : TURN_TIMEOUT_MS } ,
88+ ) ;
89+ expectFinished ( ( await fixture . client . waitForTurnTerminal ( chatId , first . turnId , {
90+ afterIndex : cursor ,
91+ timeoutMs : TURN_TIMEOUT_MS ,
92+ } ) ) . type ) ;
93+
94+ const successor = await fixture . client . waitForEvent (
95+ ( event ) : event is PendingUserInputUpdatedMessage =>
96+ event . type === 'pending-user-input-updated'
97+ && event . input . chatId === chatId
98+ && event . input . content === successorPrompt
99+ && typeof event . input . turnId === 'string' ,
100+ 'live Claude post-background successor identity' ,
101+ { afterIndex : queueCursor , timeoutMs : TURN_TIMEOUT_MS } ,
102+ ) ;
103+ expectFinished ( ( await fixture . client . waitForTurnTerminal (
104+ chatId ,
105+ successor . input . turnId ,
106+ { afterIndex : queueCursor , timeoutMs : TURN_TIMEOUT_MS } ,
107+ ) ) . type ) ;
108+
109+ const transcript = await fixture . client . getMessages ( chatId ) ;
110+ const contents = assistantContents ( transcript . messages ) ;
111+ const launchedIndex = contents . findIndex ( ( content ) => content . includes ( launchedMarker ) ) ;
112+ const completedIndex = contents . findIndex ( ( content ) => content . includes ( completedMarker ) ) ;
113+ const successorIndex = contents . findIndex ( ( content ) => content . includes ( successorMarker ) ) ;
114+ expect ( launchedIndex ) . toBeGreaterThanOrEqual ( 0 ) ;
115+ expect ( completedIndex ) . toBeGreaterThan ( launchedIndex ) ;
116+ expect ( successorIndex ) . toBeGreaterThan ( completedIndex ) ;
117+ expect ( ( await fixture . client . getExecutionControl ( chatId ) ) . queue . entries ) . toEqual ( [ ] ) ;
118+ } , { redactSensitiveDiagnostics : true , serverEnvironment } ) ;
119+ } ) ;
120+
34121 test ( 'queues consecutive turns, forks immediately, and forks the fork' , async ( ) => {
35122 const serverEnvironment = await liveClaudeServerEnvironment ( ) ;
36123 await withIntegrationFixture ( 'live-claude-queue-and-fork' , async ( fixture ) => {
0 commit comments