diff --git a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.spec.tsx b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.spec.tsx
index 4d61195ea..e73f88532 100644
--- a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.spec.tsx
+++ b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.spec.tsx
@@ -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')
@@ -40,10 +47,14 @@ jest.mock('~/libs/shared', () => ({
}), { virtual: true })
jest.mock('~/libs/ui', () => {
- const Icon = (): JSX.Element =>
+ const icons: Record JSX.Element> = {}
+ const getIcon = (name: string): (() => JSX.Element) => {
+ icons[name] ??= () =>
+ return icons[name]
+ }
return {
- IconOutline: new Proxy({}, { get: () => Icon }),
+ IconOutline: new Proxy({}, { get: (target, name) => getIcon(String(name)) }),
Tooltip: (props: PropsWithChildren) => <>{props.children}>,
}
}, { virtual: true })
@@ -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', () => ({
@@ -80,7 +86,12 @@ jest.mock('../../services/aiReview.service', () => ({
}))
jest.mock('./AiWorkflowRunStatus', () => ({
- AiWorkflowRunStatus: (props: { status?: string }) => {`status:${props.status}`},
+ AiWorkflowRunStatus: (props: { action?: JSX.Element, status?: string }) => (
+
+ {`status:${props.status}`}
+ {props.action}
+
+ ),
}))
const mockedUseFetchAiWorkflowsRuns = useFetchAiWorkflowsRuns as jest.Mock
@@ -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', () => {
diff --git a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx
index 413315210..deb516f69 100644
--- a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx
+++ b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx
@@ -352,6 +352,11 @@ const AiReviewsTable: FC = 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 => {
if (!runId || runId === '-1') return
@@ -391,11 +396,6 @@ const AiReviewsTable: FC = 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 => {
if (!isAdmin || !props.submission.id) {
return
@@ -418,8 +418,13 @@ const AiReviewsTable: FC = 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
}
@@ -448,7 +453,7 @@ const AiReviewsTable: FC = props => {
/>
)
- }, [handleQueueMissingRuns, handleRerun, isAdmin, props.submission.virusScan, queueingRuns])
+ }, [canTriggerWorkflows, handleQueueMissingRuns, handleRerun, props.submission.virusScan, queueingRuns])
const failedGatingReviewers = useMemo(
() => reviewerRows