diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryCommands.test.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryCommands.test.ts index a553bdcb4ee..4079e8a8f1e 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryCommands.test.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryCommands.test.ts @@ -5,6 +5,7 @@ import { useResumeRunFromRecoveryMutation, useStopRunMutation, useUpdateErrorRecoveryPolicy, + useResumeRunFromRecoveryAssumingFalsePositiveMutation, } from '@opentrons/react-api-client' import { useChainRunCommands } from '/app/resources/runs' @@ -14,13 +15,16 @@ import { RELEASE_GRIPPER_JAW, buildPickUpTips, buildIgnorePolicyRules, + isAssumeFalsePositiveResumeKind, UPDATE_ESTIMATORS_EXCEPT_PLUNGERS, HOME_GRIPPER_Z, } from '../useRecoveryCommands' -import { RECOVERY_MAP } from '../../constants' +import { RECOVERY_MAP, ERROR_KINDS } from '../../constants' +import { getErrorKind } from '/app/organisms/ErrorRecoveryFlows/utils' vi.mock('@opentrons/react-api-client') vi.mock('/app/resources/runs') +vi.mock('/app/organisms/ErrorRecoveryFlows/utils') describe('useRecoveryCommands', () => { const mockFailedCommand = { @@ -41,6 +45,9 @@ describe('useRecoveryCommands', () => { const mockResumeRunFromRecovery = vi.fn(() => Promise.resolve(mockMakeSuccessToast()) ) + const mockResumeRunFromRecoveryAssumingFalsePositive = vi.fn(() => + Promise.resolve(mockMakeSuccessToast()) + ) const mockStopRun = vi.fn() const mockChainRunCommands = vi.fn().mockResolvedValue([]) const mockReportActionSelectedResult = vi.fn() @@ -73,6 +80,11 @@ describe('useRecoveryCommands', () => { vi.mocked(useUpdateErrorRecoveryPolicy).mockReturnValue({ mutateAsync: mockUpdateErrorRecoveryPolicy, } as any) + vi.mocked( + useResumeRunFromRecoveryAssumingFalsePositiveMutation + ).mockReturnValue({ + mutateAsync: mockResumeRunFromRecoveryAssumingFalsePositive, + } as any) }) it('should call chainRunRecoveryCommands with continuePastCommandFailure set to false', async () => { @@ -317,7 +329,8 @@ describe('useRecoveryCommands', () => { const expectedPolicyRules = buildIgnorePolicyRules( 'aspirateInPlace', - 'mockErrorType' + 'mockErrorType', + 'ignoreAndContinue' ) expect(mockUpdateErrorRecoveryPolicy).toHaveBeenCalledWith( @@ -354,4 +367,54 @@ describe('useRecoveryCommands', () => { RECOVERY_MAP.ERROR_WHILE_RECOVERING.ROUTE ) }) + + describe('skipFailedCommand with false positive handling', () => { + it('should call resumeRunFromRecoveryAssumingFalsePositive for tip-related errors', async () => { + vi.mocked(getErrorKind).mockReturnValue(ERROR_KINDS.TIP_NOT_DETECTED) + + const { result } = renderHook(() => useRecoveryCommands(props)) + + await act(async () => { + await result.current.skipFailedCommand() + }) + + expect( + mockResumeRunFromRecoveryAssumingFalsePositive + ).toHaveBeenCalledWith(mockRunId) + expect(mockMakeSuccessToast).toHaveBeenCalled() + }) + + it('should call regular resumeRunFromRecovery for non-tip-related errors', async () => { + vi.mocked(getErrorKind).mockReturnValue(ERROR_KINDS.GRIPPER_ERROR) + + const { result } = renderHook(() => useRecoveryCommands(props)) + + await act(async () => { + await result.current.skipFailedCommand() + }) + + expect(mockResumeRunFromRecovery).toHaveBeenCalledWith(mockRunId) + expect(mockMakeSuccessToast).toHaveBeenCalled() + }) + }) +}) + +describe('isAssumeFalsePositiveResumeKind', () => { + it(`should return true for ${ERROR_KINDS.TIP_NOT_DETECTED} error kind`, () => { + vi.mocked(getErrorKind).mockReturnValue(ERROR_KINDS.TIP_NOT_DETECTED) + + expect(isAssumeFalsePositiveResumeKind({} as any)).toBe(true) + }) + + it(`should return true for ${ERROR_KINDS.TIP_DROP_FAILED} error kind`, () => { + vi.mocked(getErrorKind).mockReturnValue(ERROR_KINDS.TIP_DROP_FAILED) + + expect(isAssumeFalsePositiveResumeKind({} as any)).toBe(true) + }) + + it('should return false for other error kinds', () => { + vi.mocked(getErrorKind).mockReturnValue(ERROR_KINDS.GRIPPER_ERROR) + + expect(isAssumeFalsePositiveResumeKind({} as any)).toBe(false) + }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts index 69101d92fe9..7614dec4be3 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts @@ -5,10 +5,12 @@ import { useResumeRunFromRecoveryMutation, useStopRunMutation, useUpdateErrorRecoveryPolicy, + useResumeRunFromRecoveryAssumingFalsePositiveMutation, } from '@opentrons/react-api-client' import { useChainRunCommands } from '/app/resources/runs' -import { RECOVERY_MAP } from '../constants' +import { ERROR_KINDS, RECOVERY_MAP } from '../constants' +import { getErrorKind } from '/app/organisms/ErrorRecoveryFlows/utils' import type { CreateCommand, @@ -23,7 +25,9 @@ import type { } from '@opentrons/shared-data' import type { CommandData, + IfMatchType, RecoveryPolicyRulesParams, + RunAction, } from '@opentrons/api-client' import type { WellGroup } from '@opentrons/components' import type { FailedCommand, RecoveryRoute, RouteStep } from '../types' @@ -89,6 +93,9 @@ export function useRecoveryCommands({ const { mutateAsync: resumeRunFromRecovery, } = useResumeRunFromRecoveryMutation() + const { + mutateAsync: resumeRunFromRecoveryAssumingFalsePositive, + } = useResumeRunFromRecoveryAssumingFalsePositiveMutation() const { stopRun } = useStopRunMutation() const { mutateAsync: updateErrorRecoveryPolicy, @@ -198,9 +205,16 @@ export function useRecoveryCommands({ const handleIgnoringErrorKind = useCallback((): Promise => { if (ignoreErrors) { if (failedCommandByRunRecord?.error != null) { + const ifMatch: IfMatchType = isAssumeFalsePositiveResumeKind( + failedCommandByRunRecord + ) + ? 'assumeFalsePositiveAndContinue' + : 'ignoreAndContinue' + const ignorePolicyRules = buildIgnorePolicyRules( failedCommandByRunRecord.commandType, - failedCommandByRunRecord.error.errorType + failedCommandByRunRecord.error.errorType, + ifMatch ) return updateErrorRecoveryPolicy(ignorePolicyRules) @@ -247,9 +261,17 @@ export function useRecoveryCommands({ stopRun(runId) }, [runId]) + const handleResumeAction = (): Promise => { + if (isAssumeFalsePositiveResumeKind(failedCommandByRunRecord)) { + return resumeRunFromRecoveryAssumingFalsePositive(runId) + } else { + return resumeRunFromRecovery(runId) + } + } + const skipFailedCommand = useCallback((): void => { void handleIgnoringErrorKind().then(() => - resumeRunFromRecovery(runId).then(() => { + handleResumeAction().then(() => { analytics.reportActionSelectedResult( selectedRecoveryOption, 'succeeded' @@ -303,6 +325,20 @@ export function useRecoveryCommands({ } } +export function isAssumeFalsePositiveResumeKind( + failedCommandByRunRecord: UseRecoveryCommandsParams['failedCommandByRunRecord'] +): boolean { + const errorKind = getErrorKind(failedCommandByRunRecord) + + switch (errorKind) { + case ERROR_KINDS.TIP_NOT_DETECTED: + case ERROR_KINDS.TIP_DROP_FAILED: + return true + default: + return false + } +} + export const HOME_PIPETTE_Z_AXES: CreateCommand = { commandType: 'home', params: { axes: ['leftZ', 'rightZ'] }, @@ -372,13 +408,14 @@ export const buildPickUpTips = ( export const buildIgnorePolicyRules = ( commandType: FailedCommand['commandType'], - errorType: string + errorType: string, + ifMatch: IfMatchType ): RecoveryPolicyRulesParams => { return [ { commandType, errorType, - ifMatch: 'ignoreAndContinue', + ifMatch, }, ] } diff --git a/app/src/organisms/RunTimeControl/__tests__/hooks.test.tsx b/app/src/organisms/RunTimeControl/__tests__/hooks.test.tsx index 7450fb34e4e..2ba0d50ea3b 100644 --- a/app/src/organisms/RunTimeControl/__tests__/hooks.test.tsx +++ b/app/src/organisms/RunTimeControl/__tests__/hooks.test.tsx @@ -40,16 +40,19 @@ describe('useRunControls hook', () => { const mockStopRun = vi.fn() const mockCloneRun = vi.fn() const mockResumeRunFromRecovery = vi.fn() + const mockResumeRunFromRecoveryAssumingFalsePositive = vi.fn() when(useRunActionMutations).calledWith(mockPausedRun.id).thenReturn({ playRun: mockPlayRun, pauseRun: mockPauseRun, stopRun: mockStopRun, resumeRunFromRecovery: mockResumeRunFromRecovery, + resumeRunFromRecoveryAssumingFalsePositive: mockResumeRunFromRecoveryAssumingFalsePositive, isPlayRunActionLoading: false, isPauseRunActionLoading: false, isStopRunActionLoading: false, isResumeRunFromRecoveryActionLoading: false, + isResumeRunFromRecoveryAssumingFalsePositiveActionLoading: false, }) when(useCloneRun).calledWith(mockPausedRun.id, undefined, true).thenReturn({ cloneRun: mockCloneRun, diff --git a/app/src/pages/ODD/RunningProtocol/__tests__/RunningProtocol.test.tsx b/app/src/pages/ODD/RunningProtocol/__tests__/RunningProtocol.test.tsx index 2605c1bad5b..f98666d3cbd 100644 --- a/app/src/pages/ODD/RunningProtocol/__tests__/RunningProtocol.test.tsx +++ b/app/src/pages/ODD/RunningProtocol/__tests__/RunningProtocol.test.tsx @@ -76,6 +76,7 @@ const mockPlayRun = vi.fn() const mockPauseRun = vi.fn() const mockStopRun = vi.fn() const mockResumeRunFromRecovery = vi.fn() +const mockResumeRunFromRecoveryAssumingFalsePositive = vi.fn() const render = (path = '/') => { return renderWithProviders( @@ -133,10 +134,12 @@ describe('RunningProtocol', () => { pauseRun: mockPauseRun, stopRun: mockStopRun, resumeRunFromRecovery: mockResumeRunFromRecovery, + resumeRunFromRecoveryAssumingFalsePositive: mockResumeRunFromRecoveryAssumingFalsePositive, isPlayRunActionLoading: false, isPauseRunActionLoading: false, isStopRunActionLoading: false, isResumeRunFromRecoveryActionLoading: false, + isResumeRunFromRecoveryAssumingFalsePositiveActionLoading: false, }) when(vi.mocked(useMostRecentCompletedAnalysis)) .calledWith(RUN_ID) diff --git a/react-api-client/src/runs/__tests__/useResumeFromRecoveryAssumingFalsePositiveMutation.test.tsx b/react-api-client/src/runs/__tests__/useResumeFromRecoveryAssumingFalsePositiveMutation.test.tsx new file mode 100644 index 00000000000..ec5900d4a62 --- /dev/null +++ b/react-api-client/src/runs/__tests__/useResumeFromRecoveryAssumingFalsePositiveMutation.test.tsx @@ -0,0 +1,80 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { QueryClient, QueryClientProvider } from 'react-query' +import { act, renderHook, waitFor } from '@testing-library/react' + +import { createRunAction } from '@opentrons/api-client' + +import { useHost } from '../../api' +import { useResumeRunFromRecoveryAssumingFalsePositiveMutation } from '..' + +import { RUN_ID_1, mockResumeFromRecoveryAction } from '../__fixtures__' + +import type * as React from 'react' +import type { HostConfig, Response, RunAction } from '@opentrons/api-client' +import type { UseResumeRunFromRecoveryAssumingFalsePositiveMutationOptions } from '../useResumeFromRecoveryAssumingFalsePositiveMutation' + +vi.mock('@opentrons/api-client') +vi.mock('../../api/useHost') + +const HOST_CONFIG: HostConfig = { hostname: 'localhost' } + +describe('useResumeRunFromRecoveryAssumingFalsePositiveMutation hook', () => { + let wrapper: React.FunctionComponent< + { + children: React.ReactNode + } & UseResumeRunFromRecoveryAssumingFalsePositiveMutationOptions + > + + beforeEach(() => { + const queryClient = new QueryClient() + const clientProvider: React.FunctionComponent< + { + children: React.ReactNode + } & UseResumeRunFromRecoveryAssumingFalsePositiveMutationOptions + > = ({ children }) => ( + {children} + ) + wrapper = clientProvider + }) + + it('should return no data when calling resumeRunFromRecoveryAssumingFalsePositive if the request fails', async () => { + vi.mocked(useHost).mockReturnValue(HOST_CONFIG) + vi.mocked(createRunAction).mockRejectedValue('oh no') + + const { result } = renderHook( + useResumeRunFromRecoveryAssumingFalsePositiveMutation, + { + wrapper, + } + ) + + expect(result.current.data).toBeUndefined() + act(() => + result.current.resumeRunFromRecoveryAssumingFalsePositive(RUN_ID_1) + ) + await waitFor(() => { + expect(result.current.data).toBeUndefined() + }) + }) + + it('should create a resumeFromRecoveryAssumingFalsePositive run action when calling the resumeRunFromRecoveryAssumingFalsePositive callback', async () => { + vi.mocked(useHost).mockReturnValue(HOST_CONFIG) + vi.mocked(createRunAction).mockResolvedValue({ + data: mockResumeFromRecoveryAction, + } as Response) + + const { result } = renderHook( + useResumeRunFromRecoveryAssumingFalsePositiveMutation, + { + wrapper, + } + ) + act(() => + result.current.resumeRunFromRecoveryAssumingFalsePositive(RUN_ID_1) + ) + + await waitFor(() => { + expect(result.current.data).toEqual(mockResumeFromRecoveryAction) + }) + }) +}) diff --git a/react-api-client/src/runs/__tests__/useResumeRunFromRecoveryMutation.test.tsx b/react-api-client/src/runs/__tests__/useResumeRunFromRecoveryMutation.test.tsx index cf9794c0dd8..f556b105153 100644 --- a/react-api-client/src/runs/__tests__/useResumeRunFromRecoveryMutation.test.tsx +++ b/react-api-client/src/runs/__tests__/useResumeRunFromRecoveryMutation.test.tsx @@ -9,22 +9,22 @@ import { useResumeRunFromRecoveryMutation } from '..' import { RUN_ID_1, mockResumeFromRecoveryAction } from '../__fixtures__' import type { HostConfig, Response, RunAction } from '@opentrons/api-client' -import type { UsePlayRunMutationOptions } from '../usePlayRunMutation' +import type { UseResumeRunFromRecoveryMutationOptions } from '../useResumeRunFromRecoveryMutation' vi.mock('@opentrons/api-client') vi.mock('../../api/useHost') const HOST_CONFIG: HostConfig = { hostname: 'localhost' } -describe('usePlayRunMutation hook', () => { +describe('useResumeRunFromRecoveryMutation hook', () => { let wrapper: React.FunctionComponent< - { children: React.ReactNode } & UsePlayRunMutationOptions + { children: React.ReactNode } & UseResumeRunFromRecoveryMutationOptions > beforeEach(() => { const queryClient = new QueryClient() const clientProvider: React.FunctionComponent< - { children: React.ReactNode } & UsePlayRunMutationOptions + { children: React.ReactNode } & UseResumeRunFromRecoveryMutationOptions > = ({ children }) => ( {children} ) diff --git a/react-api-client/src/runs/__tests__/useRunActionMutations.test.tsx b/react-api-client/src/runs/__tests__/useRunActionMutations.test.tsx index 7b40e4fc88e..e5c5c4cf265 100644 --- a/react-api-client/src/runs/__tests__/useRunActionMutations.test.tsx +++ b/react-api-client/src/runs/__tests__/useRunActionMutations.test.tsx @@ -10,18 +10,21 @@ import { usePauseRunMutation, useStopRunMutation, useResumeRunFromRecoveryMutation, + useResumeRunFromRecoveryAssumingFalsePositiveMutation, } from '..' import type { UsePlayRunMutationResult, UsePauseRunMutationResult, UseStopRunMutationResult, UseResumeRunFromRecoveryMutationResult, + UseResumeRunFromRecoveryAssumingFalsePositiveMutationResult, } from '..' vi.mock('../usePlayRunMutation') vi.mock('../usePauseRunMutation') vi.mock('../useStopRunMutation') vi.mock('../useResumeRunFromRecoveryMutation') +vi.mock('../useResumeFromRecoveryAssumingFalsePositiveMutation') describe('useRunActionMutations hook', () => { let wrapper: React.FunctionComponent<{ children: React.ReactNode }> @@ -44,6 +47,7 @@ describe('useRunActionMutations hook', () => { const mockPauseRun = vi.fn() const mockStopRun = vi.fn() const mockResumeRunFromRecovery = vi.fn() + const mockResumeRunFromRecoveryAssumingFalsePositive = vi.fn() vi.mocked(usePlayRunMutation).mockReturnValue(({ playRun: mockPlayRun, @@ -61,6 +65,12 @@ describe('useRunActionMutations hook', () => { resumeRunFromRecovery: mockResumeRunFromRecovery, } as unknown) as UseResumeRunFromRecoveryMutationResult) + vi.mocked( + useResumeRunFromRecoveryAssumingFalsePositiveMutation + ).mockReturnValue(({ + resumeRunFromRecoveryAssumingFalsePositive: mockResumeRunFromRecoveryAssumingFalsePositive, + } as unknown) as UseResumeRunFromRecoveryAssumingFalsePositiveMutationResult) + const { result } = renderHook(() => useRunActionMutations(RUN_ID_1), { wrapper, }) @@ -77,5 +87,12 @@ describe('useRunActionMutations hook', () => { act(() => result.current.resumeRunFromRecovery()) expect(mockResumeRunFromRecovery).toHaveBeenCalledTimes(1) expect(mockResumeRunFromRecovery).toHaveBeenCalledWith(RUN_ID_1) + act(() => result.current.resumeRunFromRecoveryAssumingFalsePositive()) + expect( + mockResumeRunFromRecoveryAssumingFalsePositive + ).toHaveBeenCalledTimes(1) + expect(mockResumeRunFromRecoveryAssumingFalsePositive).toHaveBeenCalledWith( + RUN_ID_1 + ) }) }) diff --git a/react-api-client/src/runs/index.ts b/react-api-client/src/runs/index.ts index 71e3360a5f9..5e479ed5093 100644 --- a/react-api-client/src/runs/index.ts +++ b/react-api-client/src/runs/index.ts @@ -10,6 +10,7 @@ export { usePlayRunMutation } from './usePlayRunMutation' export { usePauseRunMutation } from './usePauseRunMutation' export { useStopRunMutation } from './useStopRunMutation' export { useResumeRunFromRecoveryMutation } from './useResumeRunFromRecoveryMutation' +export { useResumeRunFromRecoveryAssumingFalsePositiveMutation } from './useResumeFromRecoveryAssumingFalsePositiveMutation' export { useRunActionMutations } from './useRunActionMutations' export { useAllCommandsQuery } from './useAllCommandsQuery' export { useAllCommandsAsPreSerializedList } from './useAllCommandsAsPreSerializedList' @@ -23,3 +24,4 @@ export type { UsePlayRunMutationResult } from './usePlayRunMutation' export type { UsePauseRunMutationResult } from './usePauseRunMutation' export type { UseStopRunMutationResult } from './useStopRunMutation' export type { UseResumeRunFromRecoveryMutationResult } from './useResumeRunFromRecoveryMutation' +export type { UseResumeRunFromRecoveryAssumingFalsePositiveMutationResult } from './useResumeFromRecoveryAssumingFalsePositiveMutation' diff --git a/react-api-client/src/runs/useResumeFromRecoveryAssumingFalsePositiveMutation.ts b/react-api-client/src/runs/useResumeFromRecoveryAssumingFalsePositiveMutation.ts new file mode 100644 index 00000000000..6eb10990053 --- /dev/null +++ b/react-api-client/src/runs/useResumeFromRecoveryAssumingFalsePositiveMutation.ts @@ -0,0 +1,60 @@ +import { useMutation } from 'react-query' + +import { + RUN_ACTION_TYPE_RESUME_FROM_RECOVERY_ASSUMING_FALSE_POSITIVE, + createRunAction, +} from '@opentrons/api-client' + +import { useHost } from '../api' + +import type { AxiosError } from 'axios' +import type { + UseMutateFunction, + UseMutationOptions, + UseMutationResult, +} from 'react-query' +import type { HostConfig, RunAction } from '@opentrons/api-client' + +export type UseResumeRunFromRecoveryAssumingFalsePositiveMutationResult = UseMutationResult< + RunAction, + AxiosError, + string +> & { + resumeRunFromRecoveryAssumingFalsePositive: UseMutateFunction< + RunAction, + AxiosError, + string + > +} + +export type UseResumeRunFromRecoveryAssumingFalsePositiveMutationOptions = UseMutationOptions< + RunAction, + AxiosError, + string +> + +export const useResumeRunFromRecoveryAssumingFalsePositiveMutation = ( + options: UseResumeRunFromRecoveryAssumingFalsePositiveMutationOptions = {} +): UseResumeRunFromRecoveryAssumingFalsePositiveMutationResult => { + const host = useHost() + const mutation = useMutation( + [ + host, + 'runs', + RUN_ACTION_TYPE_RESUME_FROM_RECOVERY_ASSUMING_FALSE_POSITIVE, + ], + (runId: string) => + createRunAction(host as HostConfig, runId, { + actionType: RUN_ACTION_TYPE_RESUME_FROM_RECOVERY_ASSUMING_FALSE_POSITIVE, + }) + .then(response => response.data) + .catch(e => { + throw e + }), + options + ) + return { + ...mutation, + resumeRunFromRecoveryAssumingFalsePositive: mutation.mutate, + } +} diff --git a/react-api-client/src/runs/useRunActionMutations.ts b/react-api-client/src/runs/useRunActionMutations.ts index 8bf3a08f1cb..a64411e7209 100644 --- a/react-api-client/src/runs/useRunActionMutations.ts +++ b/react-api-client/src/runs/useRunActionMutations.ts @@ -5,6 +5,7 @@ import { usePauseRunMutation, useStopRunMutation, useResumeRunFromRecoveryMutation, + useResumeRunFromRecoveryAssumingFalsePositiveMutation, } from '..' interface UseRunActionMutations { @@ -12,10 +13,12 @@ interface UseRunActionMutations { pauseRun: () => void stopRun: () => void resumeRunFromRecovery: () => void + resumeRunFromRecoveryAssumingFalsePositive: () => void isPlayRunActionLoading: boolean isPauseRunActionLoading: boolean isStopRunActionLoading: boolean isResumeRunFromRecoveryActionLoading: boolean + isResumeRunFromRecoveryAssumingFalsePositiveActionLoading: boolean } export function useRunActionMutations(runId: string): UseRunActionMutations { @@ -43,6 +46,11 @@ export function useRunActionMutations(runId: string): UseRunActionMutations { isLoading: isResumeRunFromRecoveryActionLoading, } = useResumeRunFromRecoveryMutation() + const { + resumeRunFromRecoveryAssumingFalsePositive, + isLoading: isResumeRunFromRecoveryAssumingFalsePositiveActionLoading, + } = useResumeRunFromRecoveryAssumingFalsePositiveMutation() + return { playRun: () => { playRun(runId) @@ -56,9 +64,13 @@ export function useRunActionMutations(runId: string): UseRunActionMutations { resumeRunFromRecovery: () => { resumeRunFromRecovery(runId) }, + resumeRunFromRecoveryAssumingFalsePositive: () => { + resumeRunFromRecoveryAssumingFalsePositive(runId) + }, isPlayRunActionLoading, isPauseRunActionLoading, isStopRunActionLoading, isResumeRunFromRecoveryActionLoading, + isResumeRunFromRecoveryAssumingFalsePositiveActionLoading, } }