@@ -296,6 +296,12 @@ describe("SessionService", () => {
296296 hasCodeAccess : true ,
297297 needsScopeReauth : false ,
298298 } ) ;
299+ mockTrpcAgent . onSessionEvent . subscribe . mockReturnValue ( {
300+ unsubscribe : vi . fn ( ) ,
301+ } ) ;
302+ mockTrpcAgent . onPermissionRequest . subscribe . mockReturnValue ( {
303+ unsubscribe : vi . fn ( ) ,
304+ } ) ;
299305 mockTrpcCloudTask . onUpdate . subscribe . mockReturnValue ( {
300306 unsubscribe : vi . fn ( ) ,
301307 } ) ;
@@ -1024,29 +1030,56 @@ describe("SessionService", () => {
10241030 ) ;
10251031 } ) ;
10261032
1027- it ( "sets session to error state on fatal error" , async ( ) => {
1033+ it ( "attempts automatic recovery on fatal error" , async ( ) => {
10281034 const service = getSessionService ( ) ;
1029- const mockSession = createMockSession ( ) ;
1035+ const mockSession = createMockSession ( {
1036+ logUrl : "https://logs.example.com/run-123" ,
1037+ } ) ;
10301038 mockSessionStoreSetters . getSessionByTaskId . mockReturnValue ( mockSession ) ;
10311039 mockSessionStoreSetters . getSessions . mockReturnValue ( {
10321040 "run-123" : { ...mockSession , isPromptPending : false } ,
10331041 } ) ;
1042+ mockTrpcWorkspace . verify . query . mockResolvedValue ( { exists : true } ) ;
1043+ mockTrpcLogs . readLocalLogs . query . mockResolvedValue ( "" ) ;
1044+ mockTrpcAgent . reconnect . mutate . mockResolvedValue ( {
1045+ sessionId : "run-123" ,
1046+ channel : "agent-event:run-123" ,
1047+ configOptions : [ ] ,
1048+ } ) ;
1049+
1050+ await service . connectToTask ( {
1051+ task : createMockTask ( {
1052+ latest_run : {
1053+ id : "run-123" ,
1054+ task : "task-123" ,
1055+ team : 123 ,
1056+ environment : "local" ,
1057+ status : "in_progress" ,
1058+ log_url : "https://logs.example.com/run-123" ,
1059+ error_message : null ,
1060+ output : null ,
1061+ state : { } ,
1062+ branch : null ,
1063+ created_at : "2024-01-01T00:00:00Z" ,
1064+ updated_at : "2024-01-01T00:00:00Z" ,
1065+ completed_at : null ,
1066+ } ,
1067+ } ) ,
1068+ repoPath : "/repo" ,
1069+ } ) ;
1070+
10341071 mockTrpcAgent . prompt . mutate . mockRejectedValue (
10351072 new Error ( "Internal error: process exited" ) ,
10361073 ) ;
10371074
10381075 await expect ( service . sendPrompt ( "task-123" , "Hello" ) ) . rejects . toThrow ( ) ;
1039-
1040- // Check that one of the updateSession calls set status to error
1041- const updateCalls = mockSessionStoreSetters . updateSession . mock . calls as [
1042- string ,
1043- { status ?: string } ,
1044- ] [ ] ;
1045- const errorCall = updateCalls . find (
1046- ( [ , updates ] ) => updates . status === "error" ,
1076+ expect ( mockSessionStoreSetters . updateSession ) . toHaveBeenCalledWith (
1077+ "run-123" ,
1078+ expect . objectContaining ( {
1079+ status : "disconnected" ,
1080+ errorMessage : expect . stringContaining ( "Reconnecting" ) ,
1081+ } ) ,
10471082 ) ;
1048- expect ( errorCall ) . toBeDefined ( ) ;
1049- expect ( errorCall ?. [ 0 ] ) . toBe ( "run-123" ) ;
10501083 } ) ;
10511084 } ) ;
10521085
@@ -1363,4 +1396,90 @@ describe("SessionService", () => {
13631396 ) . resolves . not . toThrow ( ) ;
13641397 } ) ;
13651398 } ) ;
1399+
1400+ describe ( "automatic local recovery" , ( ) => {
1401+ it ( "reconnects automatically after a subscription error" , async ( ) => {
1402+ vi . useFakeTimers ( ) ;
1403+ const service = getSessionService ( ) ;
1404+ const mockSession = createMockSession ( {
1405+ status : "connected" ,
1406+ logUrl : "https://logs.example.com/run-123" ,
1407+ } ) ;
1408+
1409+ mockSessionStoreSetters . getSessionByTaskId . mockReturnValue ( mockSession ) ;
1410+ mockSessionStoreSetters . getSessions . mockReturnValue ( {
1411+ "run-123" : mockSession ,
1412+ } ) ;
1413+ mockTrpcWorkspace . verify . query . mockResolvedValue ( { exists : true } ) ;
1414+ mockTrpcLogs . readLocalLogs . query . mockResolvedValue ( "" ) ;
1415+ mockTrpcAgent . reconnect . mutate . mockResolvedValue ( {
1416+ sessionId : "run-123" ,
1417+ channel : "agent-event:run-123" ,
1418+ configOptions : [ ] ,
1419+ } ) ;
1420+
1421+ await service . clearSessionError ( "task-123" , "/repo" ) ;
1422+
1423+ const onError = mockTrpcAgent . onSessionEvent . subscribe . mock . calls [ 0 ] ?. [ 1 ]
1424+ ?. onError as ( ( error : Error ) => void ) | undefined ;
1425+ expect ( onError ) . toBeDefined ( ) ;
1426+
1427+ onError ?.( new Error ( "connection dropped" ) ) ;
1428+ await vi . runAllTimersAsync ( ) ;
1429+
1430+ expect ( mockTrpcAgent . reconnect . mutate ) . toHaveBeenCalledTimes ( 2 ) ;
1431+ expect ( mockSessionStoreSetters . updateSession ) . toHaveBeenCalledWith (
1432+ "run-123" ,
1433+ expect . objectContaining ( {
1434+ status : "disconnected" ,
1435+ errorMessage : expect . stringContaining ( "Reconnecting" ) ,
1436+ } ) ,
1437+ ) ;
1438+
1439+ vi . useRealTimers ( ) ;
1440+ } ) ;
1441+
1442+ it ( "shows the error screen only after automatic reconnect attempts fail" , async ( ) => {
1443+ vi . useFakeTimers ( ) ;
1444+ const service = getSessionService ( ) ;
1445+ const mockSession = createMockSession ( {
1446+ status : "connected" ,
1447+ logUrl : "https://logs.example.com/run-123" ,
1448+ } ) ;
1449+
1450+ mockSessionStoreSetters . getSessionByTaskId . mockReturnValue ( mockSession ) ;
1451+ mockSessionStoreSetters . getSessions . mockReturnValue ( {
1452+ "run-123" : mockSession ,
1453+ } ) ;
1454+ mockTrpcWorkspace . verify . query . mockResolvedValue ( { exists : true } ) ;
1455+ mockTrpcLogs . readLocalLogs . query . mockResolvedValue ( "" ) ;
1456+ mockTrpcAgent . reconnect . mutate
1457+ . mockResolvedValueOnce ( {
1458+ sessionId : "run-123" ,
1459+ channel : "agent-event:run-123" ,
1460+ configOptions : [ ] ,
1461+ } )
1462+ . mockResolvedValue ( null ) ;
1463+
1464+ await service . clearSessionError ( "task-123" , "/repo" ) ;
1465+
1466+ const onError = mockTrpcAgent . onSessionEvent . subscribe . mock . calls [ 0 ] ?. [ 1 ]
1467+ ?. onError as ( ( error : Error ) => void ) | undefined ;
1468+ expect ( onError ) . toBeDefined ( ) ;
1469+
1470+ onError ?.( new Error ( "connection dropped" ) ) ;
1471+ await vi . runAllTimersAsync ( ) ;
1472+
1473+ expect ( mockTrpcAgent . reconnect . mutate ) . toHaveBeenCalledTimes ( 4 ) ;
1474+ expect ( mockSessionStoreSetters . setSession ) . toHaveBeenCalledWith (
1475+ expect . objectContaining ( {
1476+ status : "error" ,
1477+ errorTitle : "Connection lost" ,
1478+ errorMessage : expect . any ( String ) ,
1479+ } ) ,
1480+ ) ;
1481+
1482+ vi . useRealTimers ( ) ;
1483+ } ) ;
1484+ } ) ;
13661485} ) ;
0 commit comments