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