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 @@ -33,6 +33,7 @@ import {
fetchChallenge,
fetchProfile,
fetchProjectBillingAccount,
fetchWorkflows,
patchChallenge,
fetchResourceRoles,
fetchResources,
Expand Down Expand Up @@ -71,6 +72,7 @@ jest.mock('../../../../lib/services', () => ({
fetchProjectBillingAccount: jest.fn(),
fetchResourceRoles: jest.fn(),
fetchResources: jest.fn(),
fetchWorkflows: jest.fn(),
patchChallenge: jest.fn(),
}))
jest.mock('../../../../lib/utils', () => ({
Expand Down Expand Up @@ -569,6 +571,7 @@ const mockedCreateResource = createResource as jest.Mock
const mockedCreateChallenge = createChallenge as jest.Mock
const mockedDeleteResource = deleteResource as jest.Mock
const mockedFetchChallenge = fetchChallenge as jest.Mock
const mockedFetchWorkflows = fetchWorkflows as jest.Mock
const mockedFetchProfile = fetchProfile as jest.Mock
const mockedFetchProjectBillingAccountService = fetchProjectBillingAccount as jest.Mock
const mockedPatchChallenge = patchChallenge as jest.Mock
Expand Down Expand Up @@ -677,6 +680,7 @@ describe('ChallengeEditorForm', () => {
mockedUseFetchTimelineTemplates.mockReturnValue({
timelineTemplates: [],
})
mockedFetchWorkflows.mockResolvedValue([])
mockedFetchProjectBillingAccountService.mockResolvedValue({
billingAccount: undefined,
})
Expand Down Expand Up @@ -2434,6 +2438,61 @@ describe('ChallengeEditorForm', () => {
.not.toHaveBeenCalledWith('Failed to save challenge')
})

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

mockedFetchWorkflows.mockResolvedValue([{
disabled: true,
id: 'workflow-disabled',
name: 'Disabled workflow',
}])

render(
<MemoryRouter>
<ChallengeEditorForm
challenge={{
...validDraftChallenge,
reviewers: [{
aiWorkflowId: 'workflow-disabled',
isMemberReview: false,
phaseId: 'review-phase-id',
}],
}}
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)
.toEqual(expect.objectContaining({
message: 'One or more saved AI workflows were disabled. '
+ 'Update the AI workflow configuration before saving or launching this challenge.',
}))
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.',
)
})

it('does not render the attachments section while editing a draft', () => {
render(
<MemoryRouter>
Expand Down Expand Up @@ -2473,6 +2532,46 @@ describe('ChallengeEditorForm', () => {
})
})

it('blocks saving when an assigned AI workflow has been disabled', async () => {
const user = userEvent.setup()

mockedFetchWorkflows.mockResolvedValue([{
disabled: true,
id: 'workflow-disabled',
name: 'Disabled workflow',
}])

render(
<MemoryRouter>
<ChallengeEditorForm
challenge={{
...validDraftChallenge,
reviewers: [{
aiWorkflowId: 'workflow-disabled',
isMemberReview: false,
phaseId: 'review-phase-id',
}],
}}
/>
</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(
'One or more saved AI workflows were disabled. '
+ 'Update the AI workflow configuration before saving or launching this challenge.',
)
expect(mockedShowErrorToast)
.not.toHaveBeenCalledWith('Failed to save challenge')
})

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 @@ -58,6 +58,7 @@ import {
fetchProjectBillingAccount,
fetchResourceRoles,
fetchResources,
fetchWorkflows,
patchChallenge,
} from '../../../../lib/services'
import {
Expand Down Expand Up @@ -254,6 +255,9 @@ const SAVE_VALIDATION_ERROR_MESSAGE = 'Please fix validation errors before savin
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.`
Comment thread
vas3a marked this conversation as resolved.
const CHALLENGE_TYPE_CHALLENGE_ABBREVIATION = 'CH'
const CHALLENGE_TYPE_CHALLENGE_NAME = 'CHALLENGE'
const CHALLENGE_TYPE_FIRST_2_FINISH_ABBREVIATION = 'F2F'
Expand Down Expand Up @@ -1116,6 +1120,37 @@ function getReviewerValidationError(
return getMissingRequiredPhaseCoverageError(reviewers, requiredPhases)
}

async function getDisabledAiWorkflowForActionError(
formData: ChallengeEditorFormData,
): Promise<string | undefined> {
const selectedAiWorkflowIds = Array.from(new Set((Array.isArray(formData.reviewers)
? formData.reviewers
: [])
.map(reviewer => normalizeTextValue(reviewer?.aiWorkflowId))
.filter(Boolean)))

if (!selectedAiWorkflowIds.length) {
return undefined
}

const workflows = await fetchWorkflows()
const workflowMapById = new Map(
workflows.map(workflow => [
normalizeTextValue(workflow.id),
workflow,
] as const),
)
const hasDisabledWorkflow = selectedAiWorkflowIds.some(workflowId => {
const matchedWorkflow = workflowMapById.get(workflowId)

return matchedWorkflow?.disabled === true
})

return hasDisabledWorkflow
? DISABLED_AI_WORKFLOW_FOR_CHALLENGE_ACTION_MESSAGE
: undefined
}

function getStatusText(
saveStatus: 'error' | 'idle' | 'saved' | 'saving',
): string {
Expand Down Expand Up @@ -2599,6 +2634,23 @@ export const ChallengeEditorForm: FC<ChallengeEditorFormProps> = (
throw createHandledLaunchBlockError(taskLaunchValidationError)
}

const disabledAiWorkflowError = await getDisabledAiWorkflowForActionError(formData)

if (disabledAiWorkflowError) {
setSaveStatus('idle')
setError('reviewers', {
message: disabledAiWorkflowError,
type: 'manual',
})
setSaveValidationError(disabledAiWorkflowError)

if (!options.isAutosave) {
showErrorToast(disabledAiWorkflowError)
}

throw createHandledLaunchBlockError(disabledAiWorkflowError)
}

if (!options.isAutosave) {
setIsSaving(true)
setSaveStatus('saving')
Expand Down Expand Up @@ -2826,9 +2878,17 @@ export const ChallengeEditorForm: FC<ChallengeEditorFormProps> = (
}

clearErrors('reviewers')
await saveChallenge(formData, {
redirectToViewOnSuccess: true,
})
try {
await saveChallenge(formData, {
redirectToViewOnSuccess: true,
})
} catch (error) {
if (isHandledLaunchBlockError(error)) {
return
}

throw error
}
},
[
clearErrors,
Expand Down
Loading