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')