@@ -646,6 +646,207 @@ describe('a Home auto-send identifies its caller before the project scope resolv
646646 expect ( mockedStreamViaDaemon ) . not . toHaveBeenCalled ( ) ;
647647 } ) ;
648648
649+ it ( 'keeps a cold Home run attached when an empty authority refresh settles after stream events' , async ( ) => {
650+ const activeStream = deferred < void > ( ) ;
651+ let runOptions : Parameters < typeof streamViaDaemon > [ 0 ] | undefined ;
652+ mockedStreamViaDaemon . mockImplementation ( ( options ) => {
653+ runOptions = options ;
654+ return activeStream . promise ;
655+ } ) ;
656+
657+ const view = renderProjectView ( ) ;
658+
659+ await waitFor ( ( ) => expect ( runOptions ) . toBeDefined ( ) ) ;
660+ await waitFor ( ( ) => {
661+ expect ( chatPaneSpy . mock . calls . at ( - 1 ) ?. [ 0 ] . messages ) . toEqual ( [
662+ expect . objectContaining ( { role : 'user' , content : SEED_PROMPT } ) ,
663+ expect . objectContaining ( { role : 'assistant' , runStatus : 'running' } ) ,
664+ ] ) ;
665+ } ) ;
666+
667+ const authorityReload = deferred < ChatMessage [ ] > ( ) ;
668+ mockedListMessages . mockReturnValueOnce ( authorityReload . promise ) ;
669+ workspaceScopeMocks . projectScope = {
670+ loading : false ,
671+ scope : {
672+ kind : 'team' ,
673+ projectId : PROJECT_ID ,
674+ workspaceId : TEAM_WORKSPACE ,
675+ visibility : 'team' ,
676+ context : {
677+ ...CALLER_CONTEXT ,
678+ role : 'admin' ,
679+ permissions : buildWorkspacePermissions ( {
680+ role : 'admin' ,
681+ lifecycleState : 'active' ,
682+ } ) ,
683+ } as WorkspaceCollabContext & { workspaceType : 'team' } ,
684+ } ,
685+ } ;
686+ await act ( async ( ) => {
687+ view . rerender ( projectViewElement ( ) ) ;
688+ } ) ;
689+
690+ await waitFor ( ( ) => expect ( mockedListMessages ) . toHaveBeenCalledTimes ( 2 ) ) ;
691+ const expectedOutput = 'The first packaged run is still live.' ;
692+ await act ( async ( ) => {
693+ runOptions ?. onRunCreated ?.( 'run-first-home' ) ;
694+ runOptions ?. onRunStatus ?.( 'running' ) ;
695+ runOptions ?. handlers . onAgentEvent ( { kind : 'text' , text : expectedOutput } ) ;
696+ runOptions ?. handlers . onAgentEvent ( {
697+ kind : 'tool_use' ,
698+ id : 'write-index' ,
699+ name : 'Write' ,
700+ input : { file_path : 'index.html' , content : '<main>ready</main>' } ,
701+ } ) ;
702+ } ) ;
703+
704+ await waitFor ( ( ) => {
705+ expect ( chatPaneSpy . mock . calls . at ( - 1 ) ?. [ 0 ] . messages ) . toEqual ( [
706+ expect . objectContaining ( { role : 'user' , content : SEED_PROMPT } ) ,
707+ expect . objectContaining ( {
708+ role : 'assistant' ,
709+ runId : 'run-first-home' ,
710+ events : expect . arrayContaining ( [
711+ expect . objectContaining ( { kind : 'text' , text : expectedOutput } ) ,
712+ ] ) ,
713+ } ) ,
714+ ] ) ;
715+ } ) ;
716+
717+ await act ( async ( ) => {
718+ authorityReload . resolve ( [ ] ) ;
719+ await authorityReload . promise ;
720+ } ) ;
721+
722+ expect ( chatPaneSpy . mock . calls . at ( - 1 ) ?. [ 0 ] . messages ) . toEqual ( [
723+ expect . objectContaining ( { role : 'user' , content : SEED_PROMPT } ) ,
724+ expect . objectContaining ( {
725+ role : 'assistant' ,
726+ runId : 'run-first-home' ,
727+ events : expect . arrayContaining ( [
728+ expect . objectContaining ( { kind : 'text' , text : expectedOutput } ) ,
729+ ] ) ,
730+ } ) ,
731+ ] ) ;
732+
733+ activeStream . resolve ( ) ;
734+ } ) ;
735+
736+ it ( 'keeps a terminal cold Home run attached when the empty refresh outlives its controller' , async ( ) => {
737+ const activeStream = deferred < void > ( ) ;
738+ let runOptions : Parameters < typeof streamViaDaemon > [ 0 ] | undefined ;
739+ mockedStreamViaDaemon . mockImplementation ( ( options ) => {
740+ runOptions = options ;
741+ return activeStream . promise ;
742+ } ) ;
743+
744+ const view = renderProjectView ( ) ;
745+ await waitFor ( ( ) => expect ( runOptions ) . toBeDefined ( ) ) ;
746+
747+ const authorityReload = deferred < ChatMessage [ ] > ( ) ;
748+ mockedListMessages . mockReturnValueOnce ( authorityReload . promise ) ;
749+ workspaceScopeMocks . projectScope = {
750+ loading : false ,
751+ scope : {
752+ kind : 'team' ,
753+ projectId : PROJECT_ID ,
754+ workspaceId : TEAM_WORKSPACE ,
755+ visibility : 'team' ,
756+ context : {
757+ ...CALLER_CONTEXT ,
758+ role : 'admin' ,
759+ permissions : buildWorkspacePermissions ( {
760+ role : 'admin' ,
761+ lifecycleState : 'active' ,
762+ } ) ,
763+ } as WorkspaceCollabContext & { workspaceType : 'team' } ,
764+ } ,
765+ } ;
766+ await act ( async ( ) => {
767+ view . rerender ( projectViewElement ( ) ) ;
768+ } ) ;
769+ await waitFor ( ( ) => expect ( mockedListMessages ) . toHaveBeenCalledTimes ( 2 ) ) ;
770+
771+ const expectedOutput = 'The terminal first run stays visible.' ;
772+ await act ( async ( ) => {
773+ runOptions ?. onRunCreated ?.( 'run-terminal-home' ) ;
774+ runOptions ?. onRunStatus ?.( 'running' ) ;
775+ runOptions ?. handlers . onAgentEvent ( { kind : 'text' , text : expectedOutput } ) ;
776+ runOptions ?. handlers . onAgentEvent ( {
777+ kind : 'tool_use' ,
778+ id : 'write-terminal-index' ,
779+ name : 'Write' ,
780+ input : { file_path : 'index.html' , content : '<main>complete</main>' } ,
781+ } ) ;
782+ runOptions ?. onRunStatus ?.( 'succeeded' ) ;
783+ } ) ;
784+
785+ await act ( async ( ) => {
786+ authorityReload . resolve ( [ ] ) ;
787+ await authorityReload . promise ;
788+ runOptions ?. handlers . onDone ( '' ) ;
789+ } ) ;
790+
791+ expect ( chatPaneSpy . mock . calls . at ( - 1 ) ?. [ 0 ] . messages ) . toEqual ( [
792+ expect . objectContaining ( { role : 'user' , content : SEED_PROMPT } ) ,
793+ expect . objectContaining ( {
794+ role : 'assistant' ,
795+ runId : 'run-terminal-home' ,
796+ endedAt : expect . any ( Number ) ,
797+ events : expect . arrayContaining ( [
798+ expect . objectContaining ( { kind : 'text' , text : expectedOutput } ) ,
799+ ] ) ,
800+ } ) ,
801+ ] ) ;
802+
803+ activeStream . resolve ( ) ;
804+ } ) ;
805+
806+ it ( 'does not restore a cold Home run after project authority is revoked' , async ( ) => {
807+ const activeStream = deferred < void > ( ) ;
808+ let runOptions : Parameters < typeof streamViaDaemon > [ 0 ] | undefined ;
809+ mockedStreamViaDaemon . mockImplementation ( ( options ) => {
810+ runOptions = options ;
811+ return activeStream . promise ;
812+ } ) ;
813+
814+ const view = renderProjectView ( ) ;
815+ await waitFor ( ( ) => expect ( runOptions ) . toBeDefined ( ) ) ;
816+
817+ workspaceScopeMocks . ambientContext = null ;
818+ workspaceScopeMocks . projectScope = {
819+ loading : false ,
820+ scope : null ,
821+ failure : 'forbidden' ,
822+ } ;
823+ await act ( async ( ) => {
824+ view . rerender ( projectViewElement ( ) ) ;
825+ } ) ;
826+
827+ await waitFor ( ( ) => expect ( mockedListMessages ) . toHaveBeenCalledTimes ( 2 ) ) ;
828+ await waitFor ( ( ) => {
829+ expect ( chatPaneSpy . mock . calls . at ( - 1 ) ?. [ 0 ] . messages ) . toEqual ( [ ] ) ;
830+ } ) ;
831+
832+ await act ( async ( ) => {
833+ runOptions ?. onRunCreated ?.( 'run-revoked-home' ) ;
834+ runOptions ?. handlers . onAgentEvent ( {
835+ kind : 'text' ,
836+ text : 'This output belongs to the revoked authority.' ,
837+ } ) ;
838+ runOptions ?. handlers . onAgentEvent ( {
839+ kind : 'tool_use' ,
840+ id : 'revoked-write' ,
841+ name : 'Write' ,
842+ input : { file_path : 'index.html' , content : '<main>private</main>' } ,
843+ } ) ;
844+ } ) ;
845+
846+ expect ( chatPaneSpy . mock . calls . at ( - 1 ) ?. [ 0 ] . messages ) . toEqual ( [ ] ) ;
847+ activeStream . resolve ( ) ;
848+ } ) ;
849+
649850 it ( 'reuses one Home handoff identity across ProjectView remounts' , async ( ) => {
650851 const firstView = renderProjectView ( ) ;
651852 await waitFor ( ( ) => expect ( mockedStreamViaDaemon ) . toHaveBeenCalledTimes ( 1 ) ) ;
0 commit comments