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 @@ -31,6 +31,7 @@ import {
createChallenge,
deleteResource,
fetchAiReviewConfigByChallenge,
fetchAiReviewTemplates,
fetchChallenge,
fetchProfile,
fetchProjectBillingAccount,
Expand Down Expand Up @@ -69,6 +70,7 @@ jest.mock('../../../../lib/services', () => ({
createResource: jest.fn(),
deleteResource: jest.fn(),
fetchAiReviewConfigByChallenge: jest.fn(),
fetchAiReviewTemplates: jest.fn(),
fetchChallenge: jest.fn(),
fetchProfile: jest.fn(),
fetchProjectBillingAccount: jest.fn(),
Expand Down Expand Up @@ -573,6 +575,7 @@ 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 mockedFetchAiReviewTemplates = fetchAiReviewTemplates 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 @@ -684,6 +687,7 @@ describe('ChallengeEditorForm', () => {
timelineTemplates: [],
})
mockedFetchAiReviewConfigByChallenge.mockResolvedValue(undefined)
mockedFetchAiReviewTemplates.mockResolvedValue([])
mockedFetchWorkflows.mockResolvedValue([])
mockedFetchProjectBillingAccountService.mockResolvedValue({
billingAccount: undefined,
Expand Down Expand Up @@ -2491,6 +2495,63 @@ describe('ChallengeEditorForm', () => {
.toHaveBeenCalledWith(expect.stringContaining('One or more saved AI workflows were disabled.'))
})

it('blocks launching when the saved AI template has been disabled', async () => {
let launchAction: (() => Promise<void>) | undefined
let launchError: Error | undefined

mockedFetchAiReviewConfigByChallenge.mockResolvedValue({
challengeId: '12345',
id: 'config-1',
minPassingThreshold: 75,
mode: 'AI_GATING',
templateId: 'template-disabled',
workflows: [],
})
mockedFetchAiReviewTemplates.mockResolvedValue([{
autoFinalize: false,
challengeTrack: 'DESIGN',
challengeType: 'First2Finish',
description: 'Disabled template',
disabled: true,
id: 'template-disabled',
minPassingThreshold: 75,
mode: 'AI_GATING',
title: 'Disabled template',
workflows: [],
}])

render(
<MemoryRouter>
<ChallengeEditorForm
challenge={validDraftChallenge}
onRegisterLaunchAction={action => {
launchAction = action
}}
/>
</MemoryRouter>,
)

await waitFor(() => {
expect(launchAction)
.toEqual(expect.any(Function))
})

await act(async () => {
try {
await (launchAction as () => Promise<void>)()
} catch (error) {
launchError = error as Error
}
})

expect(launchError?.message)
.toContain('The saved AI review template was disabled.')
expect(mockedPatchChallenge)
.not.toHaveBeenCalled()
expect(mockedShowErrorToast)
.toHaveBeenCalledWith(expect.stringContaining('The saved AI review template was disabled.'))
})

it('does not render the attachments section while editing a draft', () => {
render(
<MemoryRouter>
Expand Down Expand Up @@ -2612,6 +2673,47 @@ describe('ChallengeEditorForm', () => {
.toHaveBeenCalledWith(expect.stringContaining('One or more saved AI workflows were disabled.'))
})

it('blocks saving when the saved AI template has been disabled', async () => {
const user = userEvent.setup()

mockedFetchAiReviewConfigByChallenge.mockResolvedValue({
challengeId: '12345',
id: 'config-1',
minPassingThreshold: 75,
mode: 'AI_GATING',
templateId: 'template-disabled',
workflows: [],
})
mockedFetchAiReviewTemplates.mockResolvedValue([{
autoFinalize: false,
challengeTrack: 'DESIGN',
challengeType: 'First2Finish',
description: 'Disabled template',
disabled: true,
id: 'template-disabled',
minPassingThreshold: 75,
mode: 'AI_GATING',
title: 'Disabled template',
workflows: [],
}])

render(
<MemoryRouter>
<ChallengeEditorForm challenge={validDraftChallenge} />
</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(mockedShowErrorToast)
.toHaveBeenCalledWith(expect.stringContaining('The saved AI review template was disabled.'))
})

it('refreshes phase data when the fetched challenge updates for the same id', async () => {
const initialChallenge = {
...validDraftChallenge,
Expand Down
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
createResource,
deleteResource,
fetchAiReviewConfigByChallenge,
fetchAiReviewTemplates,
fetchChallenge,
fetchProfile,
fetchProjectBillingAccount,
Expand Down Expand Up @@ -259,6 +260,9 @@ const TASK_ASSIGNED_MEMBER_REQUIRED_FOR_LAUNCH_MESSAGE
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.'
const DISABLED_AI_TEMPLATE_FOR_CHALLENGE_ACTION_MESSAGE
= 'The saved AI review template was disabled. '
+ 'Update the AI template selection 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 @@ -1122,8 +1126,10 @@ function getReviewerValidationError(
}

async function getDisabledAiWorkflowForActionError(
challengeId: string | undefined,
formData: ChallengeEditorFormData,
challengeId: string | undefined,
challengeTrack?: string,
challengeType?: string,
): Promise<string | undefined> {
const selectedAiWorkflowIds = (Array.isArray(formData.reviewers)
? formData.reviewers
Expand All @@ -1142,6 +1148,29 @@ async function getDisabledAiWorkflowForActionError(
...selectedAiWorkflowIds,
...persistedWorkflowIds,
]))
const selectedTemplateId = normalizeTextValue(persistedAiConfig?.templateId)

if (selectedTemplateId) {
const templates = await fetchAiReviewTemplates({
challengeTrack,
challengeType,
})
let selectedTemplate = templates.find(template => (
normalizeTextValue(template.id) === selectedTemplateId
))

if (!selectedTemplate && (challengeTrack || challengeType)) {
const allTemplates = await fetchAiReviewTemplates()

selectedTemplate = allTemplates.find(template => (
normalizeTextValue(template.id) === selectedTemplateId
))
}

if (selectedTemplate?.disabled === true) {
return DISABLED_AI_TEMPLATE_FOR_CHALLENGE_ACTION_MESSAGE
}
}

if (!configuredAiWorkflowIds.length) {
return undefined
Expand Down Expand Up @@ -2649,8 +2678,10 @@ export const ChallengeEditorForm: FC<ChallengeEditorFormProps> = (
}

const disabledAiWorkflowError = await getDisabledAiWorkflowForActionError(
currentChallengeId,
formData,
currentChallengeId,
selectedChallengeTrack?.track || selectedChallengeTrack?.name,
selectedChallengeType?.name,
)

if (disabledAiWorkflowError) {
Expand Down Expand Up @@ -2783,6 +2814,8 @@ export const ChallengeEditorForm: FC<ChallengeEditorFormProps> = (
onChallengeStatusChange,
reset,
resolveProjectBillingAccount,
selectedChallengeTrack,
selectedChallengeType,
setError,
syncDraftSingleAssignments,
usesManualReviewers,
Expand Down
Loading