@@ -18,6 +18,7 @@ import type { VanillaChatStore } from '@/store/chatStore';
1818import { useSpaceStore } from '@/store/spaceStore' ;
1919import { AgentStep , ChatTaskStatus , SessionMode } from '@/types/constants' ;
2020import {
21+ act ,
2122 cleanup ,
2223 fireEvent ,
2324 render ,
@@ -56,6 +57,12 @@ vi.mock('@/store/projectRuntimeStore', () => ({
5657 ) ,
5758} ) ) ;
5859
60+ // Brain confirms that every approval in these tests is still pending.
61+ vi . mock ( '@/service/humanInteractionApi' , async ( importOriginal ) => ( {
62+ ...( await importOriginal < typeof import ( '@/service/humanInteractionApi' ) > ( ) ) ,
63+ isHumanInteractionStillPending : vi . fn ( async ( ) => true ) ,
64+ } ) ) ;
65+
5966vi . mock ( '@/lib/projectRuntimeHydration' , ( ) => ( {
6067 ensureProjectRuntimeLoaded : navigation . ensureProjectRuntimeLoaded ,
6168} ) ) ;
@@ -586,3 +593,108 @@ describe('UserQueryGroup Space writer wait', () => {
586593 expect ( screen . queryByRole ( 'button' , { name : 'Open session' } ) ) . toBeNull ( ) ;
587594 } ) ;
588595} ) ;
596+
597+ describe ( 'UserQueryGroup restored Run requests' , ( ) => {
598+ const expiresAt = Date . now ( ) / 1000 + 86_400 ;
599+ const messages = [
600+ { id : 'user-1' , role : 'user' , content : 'Write two files' } ,
601+ {
602+ id : 'ask-two' ,
603+ role : 'agent' ,
604+ step : AgentStep . ASK ,
605+ content : 'The agent wants to write a file.' ,
606+ agent_name : 'single_agent' ,
607+ interaction : {
608+ interaction_id : 'approval:call-two' ,
609+ interaction_type : 'approval' ,
610+ approval_id : 'approval:call-two' ,
611+ run_id : 'run-1' ,
612+ version : 0 ,
613+ expires_at : expiresAt ,
614+ question : 'The agent wants to write a file.' ,
615+ title : 'Allow write_to_file?' ,
616+ agent : 'single_agent' ,
617+ action_digest : 'digest-two' ,
618+ allowed_scopes : [ 'once' ] ,
619+ target_resources : [ ] ,
620+ } ,
621+ } ,
622+ ] ;
623+ let version = 0 ;
624+ const project = ( ...eventTypes : string [ ] ) => {
625+ const ingress = new RunEventIngress ( 'session-waiting' , 'run-1' ) ;
626+ for ( const eventType of eventTypes ) {
627+ version += 1 ;
628+ ingress . ingest ( {
629+ event_id : `restored-${ version } ` ,
630+ project_id : 'session-waiting' ,
631+ run_id : 'run-1' ,
632+ run_sequence : version ,
633+ run_version : version ,
634+ event_type : eventType ,
635+ legacy_step : null ,
636+ created_at : '2026-10-07T10:00:00Z' ,
637+ payload :
638+ eventType === 'approval.requested'
639+ ? { approval_id : 'approval:call-two' , expires_at : expiresAt }
640+ : { } ,
641+ } ) ;
642+ }
643+ } ;
644+ const restored = ( durableRunStatus : string | undefined ) =>
645+ renderGroups ( messages , { type : 'replay' , durableRunStatus } ) ;
646+ const card = ( ) => screen . queryByText ( 'Allow write_to_file?' ) ;
647+
648+ afterEach ( ( ) => {
649+ cleanup ( ) ;
650+ runProjectionStore . clear ( ) ;
651+ version = 0 ;
652+ } ) ;
653+
654+ it ( 'offers a request raised after the Session was restored' , async ( ) => {
655+ project ( 'run.attempt_created' , 'run.attempt_started' ) ;
656+ restored ( 'running' ) ;
657+ expect ( card ( ) ) . toBeNull ( ) ;
658+
659+ act ( ( ) => project ( 'approval.requested' ) ) ;
660+
661+ expect ( card ( ) ) . not . toBeNull ( ) ;
662+ await waitFor ( ( ) =>
663+ expect ( screen . getByRole ( 'button' , { name : 'Approve once' } ) ) . toBeEnabled ( )
664+ ) ;
665+ } ) ;
666+
667+ it ( 'keeps the card of a Run that was already waiting when restored' , ( ) => {
668+ restored ( 'waiting_for_user' ) ;
669+
670+ expect ( card ( ) ) . not . toBeNull ( ) ;
671+ } ) ;
672+
673+ it ( 'keeps the request read-only after the Run was interrupted' , ( ) => {
674+ project ( 'run.attempt_created' , 'run.attempt_started' , 'run.interrupted' ) ;
675+ restored ( 'running' ) ;
676+
677+ expect ( card ( ) ) . toBeNull ( ) ;
678+ } ) ;
679+
680+ it ( 'never reopens a Run that has ended' , ( ) => {
681+ project ( 'run.attempt_created' , 'approval.requested' ) ;
682+ restored ( 'completed' ) ;
683+
684+ expect ( card ( ) ) . toBeNull ( ) ;
685+ } ) ;
686+
687+ it ( 'offers the request when only the projection knows the Run waits' , ( ) => {
688+ project ( 'run.attempt_created' , 'approval.requested' ) ;
689+ restored ( undefined ) ;
690+
691+ expect ( card ( ) ) . not . toBeNull ( ) ;
692+ } ) ;
693+
694+ it ( 'keeps a replay read-only until its Run waits' , ( ) => {
695+ project ( 'run.attempt_created' , 'run.attempt_started' ) ;
696+ restored ( undefined ) ;
697+
698+ expect ( card ( ) ) . toBeNull ( ) ;
699+ } ) ;
700+ } ) ;
0 commit comments