Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ import type { AiReviewDecision, ChallengeDetailContextModel } from '../../models

import AiReviewsTable from './AiReviewsTable'

const mockRolePermissions = {
hasCopilotRole: false,
hasSubmitterRole: false,
isAdmin: false,
isProjectManager: false,
}

jest.mock('../../contexts', () => {
const React: typeof import('react') = jest.requireActual('react')

Expand Down Expand Up @@ -40,10 +47,14 @@ jest.mock('~/libs/shared', () => ({
}), { virtual: true })

jest.mock('~/libs/ui', () => {
const Icon = (): JSX.Element => <span />
const icons: Record<string, () => JSX.Element> = {}
const getIcon = (name: string): (() => JSX.Element) => {
icons[name] ??= () => <span data-testid={`icon-${name}`} />
return icons[name]
}

return {
IconOutline: new Proxy({}, { get: () => Icon }),
IconOutline: new Proxy({}, { get: (target, name) => getIcon(String(name)) }),
Tooltip: (props: PropsWithChildren<unknown>) => <>{props.children}</>,
}
}, { virtual: true })
Expand All @@ -59,12 +70,7 @@ jest.mock('swr', () => ({
jest.mock('../../hooks', () => ({
...jest.requireActual('../../hooks/useFetchAiWorkflowRuns'),
useFetchAiWorkflowsRuns: jest.fn(),
useRolePermissions: () => ({
hasCopilotRole: false,
hasSubmitterRole: false,
isAdmin: false,
isProjectManager: false,
}),
useRolePermissions: () => mockRolePermissions,
}))

jest.mock('../../assets/icons', () => ({
Expand All @@ -80,7 +86,12 @@ jest.mock('../../services/aiReview.service', () => ({
}))

jest.mock('./AiWorkflowRunStatus', () => ({
AiWorkflowRunStatus: (props: { status?: string }) => <span>{`status:${props.status}`}</span>,
AiWorkflowRunStatus: (props: { action?: JSX.Element, status?: string }) => (
<span>
{`status:${props.status}`}
{props.action}
</span>
),
}))

const mockedUseFetchAiWorkflowsRuns = useFetchAiWorkflowsRuns as jest.Mock
Expand Down Expand Up @@ -164,6 +175,24 @@ const renderWorkflowRow = (run: AiWorkflowRun): HTMLElement => {
describe('AiReviewsTable', () => {
afterEach(() => {
mockedUseFetchAiWorkflowsRuns.mockReset()
mockRolePermissions.hasCopilotRole = false
mockRolePermissions.isAdmin = false
})

it('hides the re-run action from users that are neither admin nor copilot', () => {
const row = within(renderWorkflowRow(buildRun({})))

expect(row.queryByTestId('icon-RefreshIcon'))
.toBeNull()
})

it('lets the challenge copilot re-run a workflow', () => {
mockRolePermissions.hasCopilotRole = true

const row = within(renderWorkflowRow(buildRun({})))

expect(row.getByTestId('icon-RefreshIcon'))
.toBeTruthy()
})

it('shows the edited score of a finished run', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,11 @@ const AiReviewsTable: FC<AiReviewsTableProps> = props => {
*/
const canSeeAuthor = isAdmin || hasCopilotRole || isProjectManager

/**
* Admins and the challenge copilot can manually queue / re-run the AI workflows.
*/
const canTriggerWorkflows = isAdmin || hasCopilotRole

const handleRerun = useCallback(async (runId?: string): Promise<void> => {
if (!runId || runId === '-1') return

Expand Down Expand Up @@ -391,11 +396,6 @@ const AiReviewsTable: FC<AiReviewsTableProps> = props => {
}
}, [mutate, props.submission.id])

/**
* Admin only action rendered next to the run result:
* - a workflow with no run at all can be queued manually
* - an existing (finished) run can be re-run
*/
const handleRebuildDecision = useCallback(async (): Promise<void> => {
if (!isAdmin || !props.submission.id) {
return
Expand All @@ -418,8 +418,13 @@ const AiReviewsTable: FC<AiReviewsTableProps> = props => {
}
}, [aiReviewConfig?.id, isAdmin, mutate, props.submission.id])

/**
* Admin & copilot action rendered next to the run result:
* - a workflow with no run at all can be queued manually
* - an existing (finished) run can be re-run
*/
const buildRowAction = useCallback((row: AiReviewerRow): ReactNode => {
if (!isAdmin || row.run?.id === '-1') {
if (!canTriggerWorkflows || row.run?.id === '-1') {
return undefined
}

Expand Down Expand Up @@ -448,7 +453,7 @@ const AiReviewsTable: FC<AiReviewsTableProps> = props => {
/>
</Tooltip>
)
}, [handleQueueMissingRuns, handleRerun, isAdmin, props.submission.virusScan, queueingRuns])
}, [canTriggerWorkflows, handleQueueMissingRuns, handleRerun, props.submission.virusScan, queueingRuns])

const failedGatingReviewers = useMemo(
() => reviewerRows
Expand Down
Loading