Files
floci-ui/packages/frontend/src/components/WorkflowExecutionDebugger.test.tsx
T
2026-09-30 17:43:42 -04:00

156 lines
11 KiB
TypeScript

import {QueryClient, QueryClientProvider} from '@tanstack/react-query'
import {fireEvent, render, screen, waitFor, within} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {afterEach, describe, expect, test, vi} from 'vitest'
import {WorkflowExecutionDebugger} from '@/components/WorkflowExecutionDebugger'
import {describeWorkflowExecution, getWorkflowExecutionHistory, listWorkflowExecutions, startWorkflowExecution, stopWorkflowExecution} from '@/api/cloudProxyClient'
import type {CloudResource} from '@/types/resource'
vi.mock('@/api/cloudProxyClient', () => ({
describeWorkflowExecution: vi.fn(),
getWorkflowExecutionHistory: vi.fn(),
listWorkflowExecutions: vi.fn(),
startWorkflowExecution: vi.fn(),
stopWorkflowExecution: vi.fn(),
}))
const machine: CloudResource = {id: 'arn:aws:states:us-east-1:000000000000:stateMachine:orders', name: 'orders', cloud: 'aws', service: 'workflows', type: 'state-machine', region: 'us-east-1', createdAt: null, metadata: {machineType: 'STANDARD'}}
const executionArn = 'arn:aws:states:us-east-1:000000000000:execution:orders:run-1'
function renderPanel() {
const client = new QueryClient({defaultOptions: {queries: {retry: false}}})
return render(<QueryClientProvider client={client}><WorkflowExecutionDebugger resource={machine} runtimeReachable={true}/></QueryClientProvider>)
}
afterEach(() => {
vi.clearAllMocks()
vi.useRealTimers()
})
describe('WorkflowExecutionDebugger', () => {
test('starts an execution and automatically aggregates paged history in backend order', async () => {
vi.mocked(listWorkflowExecutions).mockResolvedValue([{executionArn, name: 'run-1', status: 'SUCCEEDED', startDate: '2026-07-28T10:01:00.000Z'}])
vi.mocked(startWorkflowExecution).mockResolvedValue({executionArn, startDate: '2026-07-28T10:01:00.000Z'})
vi.mocked(describeWorkflowExecution).mockResolvedValue({executionArn, name: 'run-1', status: 'SUCCEEDED', startDate: '2026-07-28T10:01:00.000Z', stateMachineArn: machine.id, input: '{}', output: '{"ok":true}'})
vi.mocked(getWorkflowExecutionHistory).mockResolvedValueOnce({events: [{id: 1, type: 'ExecutionStarted', timestamp: '2026-07-28T10:01:00.000Z'}], nextToken: 'next'}).mockResolvedValueOnce({events: [{id: 1, type: 'ExecutionStarted', timestamp: '2026-07-28T10:01:00.000Z'}, {id: 2, type: 'ExecutionSucceeded'}]})
const user = userEvent.setup()
renderPanel()
await user.click(await screen.findByText('run-1'))
expect(await screen.findByText('ExecutionStarted')).toBeInTheDocument()
expect(await screen.findByText('ExecutionSucceeded')).toBeInTheDocument()
expect(getWorkflowExecutionHistory).toHaveBeenNthCalledWith(1, machine.id, executionArn, undefined, expect.any(AbortSignal))
expect(getWorkflowExecutionHistory).toHaveBeenNthCalledWith(2, machine.id, executionArn, 'next', expect.any(AbortSignal))
expect(screen.getAllByText('ExecutionStarted')).toHaveLength(1)
expect(screen.queryByRole('button', {name: 'Load more history'})).not.toBeInTheDocument()
fireEvent.change(screen.getByLabelText('JSON input'), {target: {value: '{"orderId":"1"}'}})
await user.click(screen.getByRole('button', {name: 'Start execution'}))
expect(screen.getByRole('alertdialog')).toHaveTextContent('{"orderId":"1"}')
await user.click(within(screen.getByRole('alertdialog')).getByRole('button', {name: 'Start execution'}))
await waitFor(() => expect(startWorkflowExecution).toHaveBeenCalledWith(machine.id, {input: '{"orderId":"1"}'}))
})
test('does not start until the inline confirmation is accepted', async () => {
vi.mocked(listWorkflowExecutions).mockResolvedValue([])
const user = userEvent.setup()
renderPanel()
await user.click(screen.getByRole('button', {name: 'Start execution'}))
expect(screen.getByRole('alertdialog')).toHaveTextContent('may invoke an unavailable Lambda')
expect(startWorkflowExecution).not.toHaveBeenCalled()
await user.click(screen.getByRole('button', {name: 'Cancel'}))
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
expect(startWorkflowExecution).not.toHaveBeenCalled()
})
test('confirms stopping only a running execution', async () => {
vi.stubGlobal('confirm', vi.fn(() => true))
vi.mocked(listWorkflowExecutions).mockResolvedValue([{executionArn, name: 'run-1', status: 'RUNNING', startDate: '2026-07-28T10:01:00.000Z'}])
vi.mocked(describeWorkflowExecution).mockResolvedValue({executionArn, name: 'run-1', status: 'RUNNING', startDate: '2026-07-28T10:01:00.000Z', stateMachineArn: machine.id})
vi.mocked(getWorkflowExecutionHistory).mockResolvedValue({events: []})
vi.mocked(stopWorkflowExecution).mockResolvedValue()
const user = userEvent.setup()
renderPanel()
await user.click(await screen.findByText('run-1'))
await user.click(await screen.findByRole('button', {name: 'Stop execution'}))
await waitFor(() => expect(stopWorkflowExecution).toHaveBeenCalledWith(machine.id, executionArn))
})
test('keeps loaded events visible when a later history page cannot load', async () => {
vi.mocked(listWorkflowExecutions).mockResolvedValue([{executionArn, name: 'run-1', status: 'SUCCEEDED', startDate: '2026-07-28T10:01:00.000Z'}])
vi.mocked(describeWorkflowExecution).mockResolvedValue({executionArn, name: 'run-1', status: 'SUCCEEDED', startDate: '2026-07-28T10:01:00.000Z', stateMachineArn: machine.id})
vi.mocked(getWorkflowExecutionHistory).mockResolvedValueOnce({events: [{id: 1, type: 'ExecutionStarted'}], nextToken: 'next'}).mockRejectedValueOnce(new Error('Unable to load the next history page'))
const user = userEvent.setup()
renderPanel()
await user.click(await screen.findByText('run-1'))
expect(await screen.findByText('Unable to load the next history page')).toBeInTheDocument()
expect(screen.getByText('ExecutionStarted')).toBeInTheDocument()
})
test('polls only the trailing history page for a running execution and appends new events', async () => {
vi.mocked(listWorkflowExecutions).mockResolvedValue([{executionArn, name: 'run-1', status: 'RUNNING', startDate: '2026-07-28T10:01:00.000Z'}])
vi.mocked(describeWorkflowExecution).mockResolvedValue({executionArn, name: 'run-1', status: 'RUNNING', startDate: '2026-07-28T10:01:00.000Z', stateMachineArn: machine.id})
vi.mocked(getWorkflowExecutionHistory)
.mockResolvedValueOnce({events: [{id: 1, type: 'ExecutionStarted'}], nextToken: 'page-2'})
.mockResolvedValueOnce({events: [{id: 2, type: 'TaskStateEntered'}]})
.mockResolvedValueOnce({events: [{id: 2, type: 'TaskStateEntered'}, {id: 3, type: 'TaskSucceeded'}]})
const user = userEvent.setup()
renderPanel()
await user.click(await screen.findByText('run-1'))
expect(await screen.findByText('TaskStateEntered')).toBeInTheDocument()
expect(await screen.findByText('TaskSucceeded', {}, {timeout: 3_000})).toBeInTheDocument()
expect(getWorkflowExecutionHistory).toHaveBeenNthCalledWith(3, machine.id, executionArn, 'page-2', expect.any(AbortSignal))
expect(getWorkflowExecutionHistory).toHaveBeenCalledTimes(3)
})
test('marks history incomplete at the explicit page ceiling while retaining loaded progress', async () => {
vi.mocked(listWorkflowExecutions).mockResolvedValue([{executionArn, name: 'run-1', status: 'SUCCEEDED', startDate: '2026-07-28T10:01:00.000Z'}])
vi.mocked(describeWorkflowExecution).mockResolvedValue({executionArn, name: 'run-1', status: 'SUCCEEDED', startDate: '2026-07-28T10:01:00.000Z', stateMachineArn: machine.id})
let page = 0
vi.mocked(getWorkflowExecutionHistory).mockImplementation(async () => {
page++
return {events: [{id: page, type: 'Event'}], nextToken: 'more'}
})
const user = userEvent.setup()
renderPanel()
await user.click(await screen.findByText('run-1'))
expect(await screen.findByText('History incomplete: reached the 10-page limit.')).toBeInTheDocument()
expect(getWorkflowExecutionHistory).toHaveBeenCalledTimes(10)
expect(screen.getAllByText('Event')).toHaveLength(10)
})
test('renders state status, selected state input, definition, and state events', async () => {
const workflow: CloudResource = {...machine, metadata: {machineType: 'STANDARD', definition: JSON.stringify({StartAt: 'ChargePayment', States: {ChargePayment: {Type: 'Task', Resource: 'arn:aws:states:::lambda:invoke', Parameters: {FunctionName: 'arn:aws:lambda:us-east-1:000000000000:function:charge-payment:$LATEST'}}, PaymentFailed: {Type: 'Task', Resource: 'arn:aws:states:::sns:publish', End: true}}})}}
vi.mocked(listWorkflowExecutions).mockResolvedValue([{executionArn, name: 'run-1', status: 'SUCCEEDED', startDate: '2026-07-28T10:01:00.000Z'}])
vi.mocked(describeWorkflowExecution).mockResolvedValue({executionArn, name: 'run-1', status: 'SUCCEEDED', startDate: '2026-07-28T10:01:00.000Z', stateMachineArn: machine.id, input: '{"orderId":"1"}'})
vi.mocked(getWorkflowExecutionHistory).mockResolvedValue({events: [
{id: 1, type: 'TaskStateEntered', timestamp: '2026-07-28T10:01:00.000Z', stateEnteredEventDetails: {name: 'ChargePayment', input: '{"orderId":"1"}'}},
{id: 2, previousEventId: 1, type: 'TaskFailed', taskFailedEventDetails: {error: 'Exception'}},
{id: 3, previousEventId: 2, type: 'TaskStateExited', stateExitedEventDetails: {name: 'ChargePayment'}},
{id: 4, type: 'TaskStateEntered', timestamp: '2026-07-28T10:01:01.000Z', stateEnteredEventDetails: {name: 'PaymentFailed', input: '{"orderId":"1"}'}},
{id: 5, previousEventId: 4, type: 'TaskSucceeded', taskSucceededEventDetails: {output: '{"messageId":"1"}'}},
{id: 6, previousEventId: 5, type: 'TaskStateExited', stateExitedEventDetails: {name: 'PaymentFailed', output: '{"messageId":"1"}'}},
]})
const client = new QueryClient({defaultOptions: {queries: {retry: false}}})
const user = userEvent.setup()
render(<QueryClientProvider client={client}><WorkflowExecutionDebugger resource={workflow} runtimeReachable={true}/></QueryClientProvider>)
await user.click(await screen.findByText('run-1'))
expect(await screen.findByText('Caught error')).toBeInTheDocument()
await user.click(screen.getAllByRole('button', {name: /ChargePayment/})[0])
expect(screen.getByRole('link', {name: 'Open logs'})).toHaveAttribute('href', '/cloud-explorer/aws/logs?resource=%2Faws%2Flambda%2Fcharge-payment')
expect(screen.getByText('{"orderId":"1"}')).toBeInTheDocument()
await user.click(screen.getByRole('tab', {name: 'definition'}))
expect(screen.getByText((_, element) => element?.tagName === 'PRE' && element.textContent?.includes('arn:aws:states:::lambda:invoke') === true)).toBeInTheDocument()
await user.click(screen.getByRole('tab', {name: 'events'}))
expect(screen.getAllByText('TaskFailed')).toHaveLength(2)
})
})