@@ -182,23 +182,27 @@ public void HealMultiAgentGroups_Phase4_PreservesRolesInActualMultiAgentGroup()
182182 #region GetFocusSessions logic
183183
184184 [ Fact ]
185- public void GetFocusSessions_ReturnsProcessingSessions ( )
185+ public void GetFocusSessions_Bootstrap_IncludesAllNonWorkerSessions ( )
186186 {
187187 var svc = CreateService ( ) ;
188188
189- var processing = new AgentSessionInfo { Name = "active-session" , Model = "m" , IsProcessing = true } ;
189+ var active = new AgentSessionInfo { Name = "active-session" , Model = "m" , IsProcessing = true } ;
190190 var idle = new AgentSessionInfo { Name = "idle-session" , Model = "m" , IsProcessing = false } ;
191- idle . LastUpdatedAt = DateTime . Now . AddDays ( - 2 ) ; // old, not in 24h window
192191
193- InjectSession ( svc , processing ) ;
192+ InjectSession ( svc , active ) ;
194193 InjectSession ( svc , idle ) ;
195194 svc . Organization . Sessions . Add ( new SessionMeta { SessionName = "active-session" , GroupId = SessionGroup . DefaultId } ) ;
196195 svc . Organization . Sessions . Add ( new SessionMeta { SessionName = "idle-session" , GroupId = SessionGroup . DefaultId } ) ;
196+ // FocusOrder is empty — bootstraps from all sessions
197197
198198 var focus = svc . GetFocusSessions ( ) ;
199199
200+ // Both sessions appear (no filtering by processing status)
200201 Assert . Contains ( focus , s => s . Name == "active-session" ) ;
201- Assert . DoesNotContain ( focus , s => s . Name == "idle-session" ) ;
202+ Assert . Contains ( focus , s => s . Name == "idle-session" ) ;
203+ // FocusOrder was populated
204+ Assert . Contains ( svc . Organization . FocusOrder , n => n == "active-session" ) ;
205+ Assert . Contains ( svc . Organization . FocusOrder , n => n == "idle-session" ) ;
202206 }
203207
204208 [ Fact ]
@@ -207,182 +211,199 @@ public void GetFocusSessions_ReturnsSessionsWithUnreadMessages()
207211 var svc = CreateService ( ) ;
208212
209213 var unread = new AgentSessionInfo { Name = "unread-session" , Model = "m" , IsProcessing = false } ;
210- unread . LastUpdatedAt = DateTime . Now . AddDays ( - 2 ) ; // old
211214 unread . History . Add ( ChatMessage . AssistantMessage ( "Response pending your attention" ) ) ;
212215
213- var idle = new AgentSessionInfo { Name = "idle-session" , Model = "m" , IsProcessing = false } ;
214- idle . LastUpdatedAt = DateTime . Now . AddDays ( - 2 ) ;
215-
216216 InjectSession ( svc , unread ) ;
217- InjectSession ( svc , idle ) ;
218217 svc . Organization . Sessions . Add ( new SessionMeta { SessionName = "unread-session" , GroupId = SessionGroup . DefaultId } ) ;
219- svc . Organization . Sessions . Add ( new SessionMeta { SessionName = "idle-session" , GroupId = SessionGroup . DefaultId } ) ;
220218
221219 var focus = svc . GetFocusSessions ( ) ;
222220
223221 Assert . Contains ( focus , s => s . Name == "unread-session" ) ;
224- Assert . DoesNotContain ( focus , s => s . Name == "idle-session" ) ;
225222 }
226223
227224 [ Fact ]
228- public void GetFocusSessions_FocusOverrideIncluded_AlwaysShows ( )
225+ public void AddToFocus_AddsSessionToBottom ( )
229226 {
230227 var svc = CreateService ( ) ;
228+ svc . Organization . FocusOrder . AddRange ( [ "first" , "second" ] ) ;
229+ var session = new AgentSessionInfo { Name = "new-session" , Model = "m" } ;
230+ InjectSession ( svc , session ) ;
231+ svc . Organization . Sessions . Add ( new SessionMeta { SessionName = "new-session" , GroupId = SessionGroup . DefaultId } ) ;
231232
232- var session = new AgentSessionInfo { Name = "pinned-session" , Model = "m" , IsProcessing = false } ;
233- session . LastUpdatedAt = DateTime . Now . AddDays ( - 10 ) ; // very old, would not appear normally
233+ svc . AddToFocus ( "new-session" ) ;
234234
235- InjectSession ( svc , session ) ;
236- svc . Organization . Sessions . Add ( new SessionMeta
237- {
238- SessionName = "pinned-session" ,
239- GroupId = SessionGroup . DefaultId ,
240- FocusOverride = FocusOverride . Included
241- } ) ;
235+ Assert . Equal ( [ "first" , "second" , "new-session" ] , svc . Organization . FocusOrder ) ;
236+ }
242237
243- var focus = svc . GetFocusSessions ( ) ;
238+ [ Fact ]
239+ public void AddToFocus_AlreadyPresent_IsIdempotent ( )
240+ {
241+ var svc = CreateService ( ) ;
242+ svc . Organization . FocusOrder . AddRange ( [ "first" , "second" ] ) ;
243+
244+ svc . AddToFocus ( "first" ) ;
244245
245- Assert . Contains ( focus , s => s . Name == "pinned-session" ) ;
246+ Assert . Equal ( [ "first" , "second" ] , svc . Organization . FocusOrder ) ;
246247 }
247248
248249 [ Fact ]
249- public void GetFocusSessions_FocusOverrideExcluded_NeverShows ( )
250+ public void RemoveFromFocus_RemovesSession ( )
250251 {
251252 var svc = CreateService ( ) ;
253+ svc . Organization . FocusOrder . AddRange ( [ "first" , "second" , "third" ] ) ;
252254
253- // This session IS processing, but is explicitly excluded from Focus
254- var session = new AgentSessionInfo { Name = "excluded-session" , Model = "m" , IsProcessing = true } ;
255- InjectSession ( svc , session ) ;
256- svc . Organization . Sessions . Add ( new SessionMeta
257- {
258- SessionName = "excluded-session" ,
259- GroupId = SessionGroup . DefaultId ,
260- FocusOverride = FocusOverride . Excluded
261- } ) ;
255+ svc . RemoveFromFocus ( "second" ) ;
262256
263- var focus = svc . GetFocusSessions ( ) ;
257+ Assert . Equal ( [ "first" , "third" ] , svc . Organization . FocusOrder ) ;
258+ }
259+
260+ [ Fact ]
261+ public void RemoveFromFocus_NonExistent_DoesNotThrow ( )
262+ {
263+ var svc = CreateService ( ) ;
264+ svc . Organization . FocusOrder . Add ( "only" ) ;
265+
266+ svc . RemoveFromFocus ( "ghost" ) ;
264267
265- Assert . DoesNotContain ( focus , s => s . Name == "excluded-session" ) ;
268+ Assert . Equal ( [ "only" ] , svc . Organization . FocusOrder ) ;
266269 }
267270
268271 [ Fact ]
269- public void GetFocusSessions_WorkersInMultiAgentGroup_ExcludedFromFocus ( )
272+ public void PromoteFocusSession_MovesUp ( )
270273 {
271274 var svc = CreateService ( ) ;
275+ svc . Organization . FocusOrder . AddRange ( [ "a" , "b" , "c" ] ) ;
272276
273- var maGroup = new SessionGroup { Id = "ma-group" , Name = "MyTeam" , IsMultiAgent = true } ;
274- svc . Organization . Groups . Add ( maGroup ) ;
277+ svc . PromoteFocusSession ( "b" ) ;
275278
276- var worker = new AgentSessionInfo { Name = "MyTeam-worker-1" , Model = "m" , IsProcessing = true } ;
277- var orch = new AgentSessionInfo { Name = "MyTeam-orchestrator" , Model = "m" , IsProcessing = true } ;
278- InjectSession ( svc , worker ) ;
279- InjectSession ( svc , orch ) ;
279+ Assert . Equal ( [ "b" , "a" , "c" ] , svc . Organization . FocusOrder ) ;
280+ }
280281
281- svc . Organization . Sessions . Add ( new SessionMeta { SessionName = "MyTeam-worker-1" , GroupId = "ma-group" , Role = MultiAgentRole . Worker } ) ;
282- svc . Organization . Sessions . Add ( new SessionMeta { SessionName = "MyTeam-orchestrator" , GroupId = "ma-group" , Role = MultiAgentRole . Orchestrator } ) ;
282+ [ Fact ]
283+ public void PromoteFocusSession_AlreadyFirst_NoChange ( )
284+ {
285+ var svc = CreateService ( ) ;
286+ svc . Organization . FocusOrder . AddRange ( [ "a" , "b" , "c" ] ) ;
283287
284- var focus = svc . GetFocusSessions ( ) ;
288+ svc . PromoteFocusSession ( "a" ) ;
285289
286- // Workers should not appear in Focus (they're managed by orchestrator)
287- Assert . DoesNotContain ( focus , s => s . Name == "MyTeam-worker-1" ) ;
288- // Orchestrator (Role != Worker) should appear since it's processing
289- Assert . Contains ( focus , s => s . Name == "MyTeam-orchestrator" ) ;
290+ Assert . Equal ( [ "a" , "b" , "c" ] , svc . Organization . FocusOrder ) ;
290291 }
291292
292293 [ Fact ]
293- public void GetFocusSessions_ProcessingFirstInSort ( )
294+ public void DemoteFocusSession_MovesDown ( )
294295 {
295296 var svc = CreateService ( ) ;
297+ svc . Organization . FocusOrder . AddRange ( [ "a" , "b" , "c" ] ) ;
296298
297- var processing = new AgentSessionInfo { Name = "processing" , Model = "m" , IsProcessing = true } ;
298- var unread = new AgentSessionInfo { Name = "unread" , Model = "m" , IsProcessing = false } ;
299- unread . History . Add ( ChatMessage . AssistantMessage ( "Pending attention" ) ) ;
300- unread . LastUpdatedAt = DateTime . Now . AddMinutes ( - 5 ) ;
299+ svc . DemoteFocusSession ( "b" ) ;
301300
302- InjectSession ( svc , processing ) ;
303- InjectSession ( svc , unread ) ;
304- svc . Organization . Sessions . Add ( new SessionMeta { SessionName = "processing" , GroupId = SessionGroup . DefaultId } ) ;
305- svc . Organization . Sessions . Add ( new SessionMeta { SessionName = "unread" , GroupId = SessionGroup . DefaultId } ) ;
301+ Assert . Equal ( [ "a" , "c" , "b" ] , svc . Organization . FocusOrder ) ;
302+ }
303+
304+ [ Fact ]
305+ public void DemoteFocusSession_AlreadyLast_NoChange ( )
306+ {
307+ var svc = CreateService ( ) ;
308+ svc . Organization . FocusOrder . AddRange ( [ "a" , "b" , "c" ] ) ;
309+
310+ svc . DemoteFocusSession ( "c" ) ;
311+
312+ Assert . Equal ( [ "a" , "b" , "c" ] , svc . Organization . FocusOrder ) ;
313+ }
314+
315+ [ Fact ]
316+ public void GetFocusSessions_RespectsManualFocusOrder ( )
317+ {
318+ var svc = CreateService ( ) ;
319+
320+ var a = new AgentSessionInfo { Name = "a" , Model = "m" , IsProcessing = true } ;
321+ var b = new AgentSessionInfo { Name = "b" , Model = "m" , IsProcessing = false } ;
322+ InjectSession ( svc , a ) ;
323+ InjectSession ( svc , b ) ;
324+ svc . Organization . Sessions . Add ( new SessionMeta { SessionName = "a" , GroupId = SessionGroup . DefaultId } ) ;
325+ svc . Organization . Sessions . Add ( new SessionMeta { SessionName = "b" , GroupId = SessionGroup . DefaultId } ) ;
326+ // Manually put b before a — no auto-sort should override this
327+ svc . Organization . FocusOrder . AddRange ( [ "b" , "a" ] ) ;
306328
307329 var focus = svc . GetFocusSessions ( ) ;
308330 var names = focus . Select ( s => s . Name ) . ToList ( ) ;
309331
310- // Processing comes before unread
311- Assert . True ( names . IndexOf ( "processing" ) < names . IndexOf ( "unread" ) ,
312- "Processing session should sort before unread session" ) ;
332+ Assert . Equal ( [ "b" , "a" ] , names ) ;
313333 }
314334
315335 [ Fact ]
316- public void GetFocusSessions_HandledSessions_SortToBottom ( )
336+ public void GetFocusSessions_WorkersInMultiAgentGroup_ExcludedFromFocus ( )
317337 {
318338 var svc = CreateService ( ) ;
319339
320- var unhandled = new AgentSessionInfo { Name = "unhandled" , Model = "m" , IsProcessing = false } ;
321- unhandled . LastUpdatedAt = DateTime . Now . AddMinutes ( - 30 ) ;
322- unhandled . History . Add ( ChatMessage . AssistantMessage ( "Needs attention" ) ) ;
340+ var maGroup = new SessionGroup { Id = "ma-group" , Name = "MyTeam" , IsMultiAgent = true } ;
341+ svc . Organization . Groups . Add ( maGroup ) ;
323342
324- var handled = new AgentSessionInfo { Name = "handled" , Model = "m" , IsProcessing = false } ;
325- handled . LastUpdatedAt = DateTime . Now . AddMinutes ( - 5 ) ; // More recent but handled
343+ var worker = new AgentSessionInfo { Name = "MyTeam-worker-1" , Model = "m" , IsProcessing = true } ;
344+ var orch = new AgentSessionInfo { Name = "MyTeam-orchestrator" , Model = "m" , IsProcessing = true } ;
345+ InjectSession ( svc , worker ) ;
346+ InjectSession ( svc , orch ) ;
326347
327- InjectSession ( svc , unhandled ) ;
328- InjectSession ( svc , handled ) ;
329- svc . Organization . Sessions . Add ( new SessionMeta { SessionName = "unhandled" , GroupId = SessionGroup . DefaultId } ) ;
330- svc . Organization . Sessions . Add ( new SessionMeta
331- {
332- SessionName = "handled" ,
333- GroupId = SessionGroup . DefaultId ,
334- HandledAt = DateTime . Now . AddMinutes ( - 10 )
335- } ) ;
348+ svc . Organization . Sessions . Add ( new SessionMeta { SessionName = "MyTeam-worker-1" , GroupId = "ma-group" , Role = MultiAgentRole . Worker } ) ;
349+ svc . Organization . Sessions . Add ( new SessionMeta { SessionName = "MyTeam-orchestrator" , GroupId = "ma-group" , Role = MultiAgentRole . Orchestrator } ) ;
350+ // Workers must not bootstrap into FocusOrder
351+ // Orchestrator should be in FocusOrder (bootstrap adds non-workers)
336352
337353 var focus = svc . GetFocusSessions ( ) ;
338- var names = focus . Select ( s => s . Name ) . ToList ( ) ;
339354
340- // Unhandled (even with older activity) sorts before handled
341- Assert . True ( names . IndexOf ( "unhandled" ) < names . IndexOf ( "handled" ) ,
342- "Unhandled sessions should sort before handled sessions" ) ;
355+ // Workers should not appear in Focus (they're managed by orchestrator)
356+ Assert . DoesNotContain ( focus , s => s . Name == "MyTeam-worker-1" ) ;
357+ // Orchestrator (Role != Worker) should appear
358+ Assert . Contains ( focus , s => s . Name == "MyTeam-orchestrator" ) ;
343359 }
344360
345361 [ Fact ]
346- public void GetFocusSessions_OldestWaitingFirst_WithinUnhandled ( )
362+ public void GetFocusSessions_WorkerByNamePattern_ExcludedFromFocusEvenIfRoleNone ( )
347363 {
348364 var svc = CreateService ( ) ;
365+ // Worker session with Role=None (corruption — should still be excluded by name pattern)
366+ var worker = new AgentSessionInfo { Name = "AGR-worker-1" , Model = "m" , IsProcessing = true } ;
367+ InjectSession ( svc , worker ) ;
368+ svc . Organization . Sessions . Add ( new SessionMeta { SessionName = "AGR-worker-1" , GroupId = SessionGroup . DefaultId , Role = MultiAgentRole . None } ) ;
349369
350- // "newer" session updated 5 min ago (less urgent — user saw it more recently)
351- var newer = new AgentSessionInfo { Name = "newer" , Model = "m" , IsProcessing = false } ;
352- newer . LastUpdatedAt = DateTime . Now . AddMinutes ( - 5 ) ;
353- newer . History . Add ( ChatMessage . AssistantMessage ( "Recent message" ) ) ;
370+ var focus = svc . GetFocusSessions ( ) ;
354371
355- // "older" session waiting 60 min (more urgent — waiting longest)
356- var older = new AgentSessionInfo { Name = "older" , Model = "m" , IsProcessing = false } ;
357- older . LastUpdatedAt = DateTime . Now . AddMinutes ( - 60 ) ;
358- older . History . Add ( ChatMessage . AssistantMessage ( "Waiting a long time" ) ) ;
372+ Assert . DoesNotContain ( focus , s => s . Name == "AGR-worker-1" ) ;
373+ Assert . DoesNotContain ( svc . Organization . FocusOrder , n => n == "AGR-worker-1" ) ;
374+ }
359375
360- InjectSession ( svc , newer ) ;
361- InjectSession ( svc , older ) ;
362- svc . Organization . Sessions . Add ( new SessionMeta { SessionName = "newer" , GroupId = SessionGroup . DefaultId } ) ;
363- svc . Organization . Sessions . Add ( new SessionMeta { SessionName = "older" , GroupId = SessionGroup . DefaultId } ) ;
376+ [ Fact ]
377+ public void GetFocusSessions_SessionsRemovedFromFocusOrder_NotReturned ( )
378+ {
379+ var svc = CreateService ( ) ;
364380
381+ var a = new AgentSessionInfo { Name = "a" , Model = "m" } ;
382+ var b = new AgentSessionInfo { Name = "b" , Model = "m" } ;
383+ InjectSession ( svc , a ) ;
384+ InjectSession ( svc , b ) ;
385+ svc . Organization . Sessions . Add ( new SessionMeta { SessionName = "a" , GroupId = SessionGroup . DefaultId } ) ;
386+ svc . Organization . Sessions . Add ( new SessionMeta { SessionName = "b" , GroupId = SessionGroup . DefaultId } ) ;
387+ svc . Organization . FocusOrder . AddRange ( [ "a" , "b" ] ) ;
388+
389+ svc . RemoveFromFocus ( "b" ) ;
365390 var focus = svc . GetFocusSessions ( ) ;
366- var names = focus . Select ( s => s . Name ) . ToList ( ) ;
367391
368- // Oldest waiting session sorts first (triage order)
369- Assert . True ( names . IndexOf ( "older" ) < names . IndexOf ( "newer" ) ,
370- "Oldest waiting session should sort before more recently active sessions" ) ;
392+ Assert . Contains ( focus , s => s . Name == "a" ) ;
393+ Assert . DoesNotContain ( focus , s => s . Name == "b" ) ;
371394 }
372395
373396 [ Fact ]
374- public void MarkFocusHandled_SetsHandledAtOnMeta ( )
397+ public void MarkFocusHandled_DemotesSession ( )
375398 {
376399 var svc = CreateService ( ) ;
377400 svc . Organization . Sessions . Add ( new SessionMeta { SessionName = "my-session" , GroupId = SessionGroup . DefaultId } ) ;
401+ svc . Organization . FocusOrder . AddRange ( [ "my-session" , "other" ] ) ;
378402
379- var before = DateTime . Now ;
380403 svc . MarkFocusHandled ( "my-session" ) ;
381- var after = DateTime . Now ;
382404
383- var meta = svc . Organization . Sessions . First ( m => m . SessionName == "my-session" ) ;
384- Assert . NotNull ( meta . HandledAt ) ;
385- Assert . InRange ( meta . HandledAt ! . Value , before , after ) ;
405+ // MarkFocusHandled demotes (moves to bottom), does NOT remove
406+ Assert . Equal ( [ "other" , "my-session" ] , svc . Organization . FocusOrder ) ;
386407 }
387408
388409 [ Fact ]
0 commit comments