diff --git a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.spec.tsx b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.spec.tsx index 640b561ea..1a45ccaf7 100644 --- a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.spec.tsx +++ b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.spec.tsx @@ -30,6 +30,7 @@ import { createResource, createChallenge, deleteResource, + fetchAiReviewConfigByChallenge, fetchChallenge, fetchProfile, fetchProjectBillingAccount, @@ -67,6 +68,7 @@ jest.mock('../../../../lib/services', () => ({ createChallenge: jest.fn(), createResource: jest.fn(), deleteResource: jest.fn(), + fetchAiReviewConfigByChallenge: jest.fn(), fetchChallenge: jest.fn(), fetchProfile: jest.fn(), fetchProjectBillingAccount: jest.fn(), @@ -570,6 +572,7 @@ const mockedUseFetchTimelineTemplates = useFetchTimelineTemplates as jest.Mock const mockedCreateResource = createResource as jest.Mock const mockedCreateChallenge = createChallenge as jest.Mock const mockedDeleteResource = deleteResource as jest.Mock +const mockedFetchAiReviewConfigByChallenge = fetchAiReviewConfigByChallenge as jest.Mock const mockedFetchChallenge = fetchChallenge as jest.Mock const mockedFetchWorkflows = fetchWorkflows as jest.Mock const mockedFetchProfile = fetchProfile as jest.Mock @@ -680,6 +683,7 @@ describe('ChallengeEditorForm', () => { mockedUseFetchTimelineTemplates.mockReturnValue({ timelineTemplates: [], }) + mockedFetchAiReviewConfigByChallenge.mockResolvedValue(undefined) mockedFetchWorkflows.mockResolvedValue([]) mockedFetchProjectBillingAccountService.mockResolvedValue({ billingAccount: undefined, @@ -2479,18 +2483,12 @@ describe('ChallengeEditorForm', () => { } }) - expect(launchError) - .toEqual(expect.objectContaining({ - message: 'One or more saved AI workflows were disabled. ' - + 'Update the AI workflow configuration before saving or launching this challenge.', - })) + expect(launchError?.message) + .toContain('One or more saved AI workflows were disabled.') expect(mockedPatchChallenge) .not.toHaveBeenCalled() expect(mockedShowErrorToast) - .toHaveBeenCalledWith( - 'One or more saved AI workflows were disabled. ' - + 'Update the AI workflow configuration before saving or launching this challenge.', - ) + .toHaveBeenCalledWith(expect.stringContaining('One or more saved AI workflows were disabled.')) }) it('does not render the attachments section while editing a draft', () => { @@ -2564,14 +2562,56 @@ describe('ChallengeEditorForm', () => { .not.toHaveBeenCalled() }) expect(mockedShowErrorToast) - .toHaveBeenCalledWith( - 'One or more saved AI workflows were disabled. ' - + 'Update the AI workflow configuration before saving or launching this challenge.', - ) + .toHaveBeenCalledWith(expect.stringContaining('One or more saved AI workflows were disabled.')) expect(mockedShowErrorToast) .not.toHaveBeenCalledWith('Failed to save challenge') }) + it('blocks saving when disabled workflow exists only in persisted AI config', async () => { + const user = userEvent.setup() + + mockedFetchAiReviewConfigByChallenge.mockResolvedValue({ + challengeId: '12345', + id: 'config-1', + minPassingThreshold: 75, + mode: 'AI_HUMAN', + workflows: [{ + id: 'config-workflow-1', + isGating: false, + weightPercent: 100, + workflowId: 'workflow-disabled', + }], + }) + mockedFetchWorkflows.mockResolvedValue([{ + disabled: true, + id: 'workflow-disabled', + name: 'Disabled workflow', + }]) + + render( + + + , + ) + + await user.type(screen.getByLabelText('Challenge Name'), ' updated') + await user.click(screen.getByRole('button', { name: 'Save Challenge' })) + + await waitFor(() => { + expect(mockedPatchChallenge) + .not.toHaveBeenCalled() + }) + expect(mockedFetchAiReviewConfigByChallenge) + .toHaveBeenCalledWith('12345') + expect(mockedShowErrorToast) + .toHaveBeenCalledWith(expect.stringContaining('One or more saved AI workflows were disabled.')) + }) + it('refreshes phase data when the fetched challenge updates for the same id', async () => { const initialChallenge = { ...validDraftChallenge, diff --git a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.tsx b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.tsx index c5e674bbc..c2e18e4ef 100644 --- a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.tsx +++ b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.tsx @@ -53,6 +53,7 @@ import { createChallenge, createResource, deleteResource, + fetchAiReviewConfigByChallenge, fetchChallenge, fetchProfile, fetchProjectBillingAccount, @@ -256,8 +257,8 @@ const DESIGN_WORK_TYPE_REQUIRED_MESSAGE = 'Select a work type' const TASK_ASSIGNED_MEMBER_REQUIRED_FOR_LAUNCH_MESSAGE = 'Assign a member before launching a task challenge.' const DISABLED_AI_WORKFLOW_FOR_CHALLENGE_ACTION_MESSAGE - = `One or more saved AI workflows were disabled. - Update the AI workflow configuration before saving or launching this challenge.` + = 'One or more saved AI workflows were disabled. ' + + 'Update the AI workflow configuration before saving or launching this challenge.' const CHALLENGE_TYPE_CHALLENGE_ABBREVIATION = 'CH' const CHALLENGE_TYPE_CHALLENGE_NAME = 'CHALLENGE' const CHALLENGE_TYPE_FIRST_2_FINISH_ABBREVIATION = 'F2F' @@ -1121,15 +1122,28 @@ function getReviewerValidationError( } async function getDisabledAiWorkflowForActionError( + challengeId: string | undefined, formData: ChallengeEditorFormData, ): Promise { - const selectedAiWorkflowIds = Array.from(new Set((Array.isArray(formData.reviewers) + const selectedAiWorkflowIds = (Array.isArray(formData.reviewers) ? formData.reviewers : []) .map(reviewer => normalizeTextValue(reviewer?.aiWorkflowId)) - .filter(Boolean))) - - if (!selectedAiWorkflowIds.length) { + .filter(Boolean) + const normalizedChallengeId = normalizeTextValue(challengeId) + const persistedAiConfig = normalizedChallengeId + ? await fetchAiReviewConfigByChallenge(normalizedChallengeId) + .catch(() => undefined) + : undefined + const persistedWorkflowIds = (persistedAiConfig?.workflows || []) + .map(workflow => normalizeTextValue(workflow.workflowId)) + .filter(Boolean) + const configuredAiWorkflowIds = Array.from(new Set([ + ...selectedAiWorkflowIds, + ...persistedWorkflowIds, + ])) + + if (!configuredAiWorkflowIds.length) { return undefined } @@ -1140,7 +1154,7 @@ async function getDisabledAiWorkflowForActionError( workflow, ] as const), ) - const hasDisabledWorkflow = selectedAiWorkflowIds.some(workflowId => { + const hasDisabledWorkflow = configuredAiWorkflowIds.some(workflowId => { const matchedWorkflow = workflowMapById.get(workflowId) return matchedWorkflow?.disabled === true @@ -2634,7 +2648,10 @@ export const ChallengeEditorForm: FC = ( throw createHandledLaunchBlockError(taskLaunchValidationError) } - const disabledAiWorkflowError = await getDisabledAiWorkflowForActionError(formData) + const disabledAiWorkflowError = await getDisabledAiWorkflowForActionError( + currentChallengeId, + formData, + ) if (disabledAiWorkflowError) { setSaveStatus('idle')