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 @@ -30,6 +30,7 @@ import {
createResource,
createChallenge,
deleteResource,
fetchAiReviewConfigByChallenge,
fetchChallenge,
fetchProfile,
fetchProjectBillingAccount,
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -680,6 +683,7 @@ describe('ChallengeEditorForm', () => {
mockedUseFetchTimelineTemplates.mockReturnValue({
timelineTemplates: [],
})
mockedFetchAiReviewConfigByChallenge.mockResolvedValue(undefined)
mockedFetchWorkflows.mockResolvedValue([])
mockedFetchProjectBillingAccountService.mockResolvedValue({
billingAccount: undefined,
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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(
<MemoryRouter>
<ChallengeEditorForm
challenge={{
...validDraftChallenge,
reviewers: [],
}}
/>
</MemoryRouter>,
)

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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
createChallenge,
createResource,
deleteResource,
fetchAiReviewConfigByChallenge,
fetchChallenge,
fetchProfile,
fetchProjectBillingAccount,
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -1121,15 +1122,28 @@ function getReviewerValidationError(
}

async function getDisabledAiWorkflowForActionError(
challengeId: string | undefined,
formData: ChallengeEditorFormData,
): Promise<string | undefined> {
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
}

Expand All @@ -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
Expand Down Expand Up @@ -2634,7 +2648,10 @@ export const ChallengeEditorForm: FC<ChallengeEditorFormProps> = (
throw createHandledLaunchBlockError(taskLaunchValidationError)
}

const disabledAiWorkflowError = await getDisabledAiWorkflowForActionError(formData)
const disabledAiWorkflowError = await getDisabledAiWorkflowForActionError(
currentChallengeId,
formData,
)

if (disabledAiWorkflowError) {
setSaveStatus('idle')
Expand Down
Loading