-
Notifications
You must be signed in to change notification settings - Fork 1k
[CCOR-13383]fix:task definition form missing fields #1586
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
najeebkp
wants to merge
4
commits into
conductor-oss:main
Choose a base branch
from
najeebkp:CCOR-13383-task-definition-form-missing-fields
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 3 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
40f3c68
[CCOR-13383]fix:Add form controls for task fields that were JSON-only
najeebkp 322257d
fix(bad-ux):Stop blocking task definition save on an empty description
najeebkp cf83bf2
revert tooltip text changes
najeebkp d0b7c03
test: assert an empty description does not block save
najeebkp File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
150 changes: 150 additions & 0 deletions
150
ui-next/src/pages/definition/task/form/TaskDefinitionForm.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| /** | ||
| * The Task tab must expose every field the Code tab's JSON carries, otherwise | ||
| * a setting can only be reached by hand-editing raw JSON. These tests cover | ||
| * the four that had no control: maxRetryDelaySeconds, backoffJitterMs, | ||
| * totalTimeoutSeconds and taskStatusListenerEnabled. | ||
| * | ||
| * They assert against the form machine's context, which is what the Code tab | ||
| * renders — so a control wired to a misspelled field name fails here. The | ||
| * compiler cannot catch that: modifiedTaskDefinition reaches the form through | ||
| * a loosely typed xstate ActorRef and is effectively `any`. | ||
| */ | ||
| import "@testing-library/jest-dom"; | ||
| import { fireEvent, render, screen } from "@testing-library/react"; | ||
| import { interpret } from "xstate"; | ||
| import { Provider as ThemeProvider } from "theme/material/provider"; | ||
| import TaskDefinitionForm from "pages/definition/task/form/TaskDefinitionForm"; | ||
| import { taskDefinitionFormMachine } from "pages/definition/task/form/state/machine"; | ||
| import { TaskRetryLogic, TaskTimeoutPolicy } from "pages/definition/task/state"; | ||
| import { TaskDefinitionDto } from "types/TaskDefinition"; | ||
|
|
||
| vi.mock("utils/query", async (importOriginal) => ({ | ||
| ...(await importOriginal<typeof import("utils/query")>()), | ||
| useFetch: () => ({ data: [], refetch: vi.fn() }), | ||
| })); | ||
|
|
||
| const baseTaskDefinition = { | ||
| name: "my_task", | ||
| description: "", | ||
| retryCount: 3, | ||
| retryDelaySeconds: 60, | ||
| retryLogic: TaskRetryLogic.FIXED, | ||
| backoffScaleFactor: 1, | ||
| timeoutSeconds: 3600, | ||
| timeoutPolicy: TaskTimeoutPolicy.TIME_OUT_WF, | ||
| responseTimeoutSeconds: 600, | ||
| pollTimeoutSeconds: 3600, | ||
| rateLimitPerFrequency: 0, | ||
| rateLimitFrequencyInSeconds: 1, | ||
| concurrentExecLimit: 0, | ||
| inputKeys: [], | ||
| outputKeys: [], | ||
| inputTemplate: {}, | ||
| } as unknown as TaskDefinitionDto; | ||
|
|
||
| const renderForm = (overrides: Partial<TaskDefinitionDto> = {}) => { | ||
| const taskDefinition = { | ||
| ...baseTaskDefinition, | ||
| ...overrides, | ||
| } as TaskDefinitionDto; | ||
| const service = interpret( | ||
| taskDefinitionFormMachine.withContext({ | ||
| modifiedTaskDefinition: taskDefinition, | ||
| originTaskDefinition: taskDefinition, | ||
| }), | ||
| ).start(); | ||
|
|
||
| render( | ||
| <ThemeProvider> | ||
| <TaskDefinitionForm formActor={service as never} /> | ||
| </ThemeProvider>, | ||
| ); | ||
|
|
||
| return { | ||
| /** What the Code tab would show. */ | ||
| definition: () => | ||
| service.getSnapshot().context.modifiedTaskDefinition as Record< | ||
| string, | ||
| unknown | ||
| >, | ||
| json: () => | ||
| ( | ||
| service.getSnapshot().context as unknown as { | ||
| modifiedTaskDefinitionString?: string; | ||
| } | ||
| ).modifiedTaskDefinitionString ?? "", | ||
| }; | ||
| }; | ||
|
|
||
| const field = (label: string) => | ||
| screen.getByLabelText(label) as HTMLInputElement; | ||
|
|
||
| const setNumber = (label: string, value: string) => | ||
| fireEvent.change(field(label), { target: { value } }); | ||
|
|
||
| describe("TaskDefinitionForm — fields that were JSON-only", () => { | ||
| it("shows the stored values for all four fields", () => { | ||
| renderForm({ | ||
| maxRetryDelaySeconds: 120, | ||
| backoffJitterMs: 250, | ||
| totalTimeoutSeconds: 7200, | ||
| taskStatusListenerEnabled: false, | ||
| }); | ||
|
|
||
| expect(field("Max retry delay seconds")).toHaveValue("120"); | ||
| expect(field("Backoff jitter ms")).toHaveValue("250"); | ||
| expect(field("Total Timeout Seconds")).toHaveValue("7200"); | ||
| expect( | ||
| screen.getByLabelText("Enable task status listener"), | ||
| ).not.toBeChecked(); | ||
| }); | ||
|
|
||
| it("writes maxRetryDelaySeconds back into the definition", () => { | ||
| const { definition, json } = renderForm({ maxRetryDelaySeconds: 0 }); | ||
|
|
||
| setNumber("Max retry delay seconds", "45"); | ||
|
|
||
| expect(definition().maxRetryDelaySeconds).toBe(45); | ||
| expect(json()).toContain('"maxRetryDelaySeconds": 45'); | ||
| }); | ||
|
|
||
| it("writes backoffJitterMs back into the definition", () => { | ||
| const { definition } = renderForm({ backoffJitterMs: 0 }); | ||
|
|
||
| setNumber("Backoff jitter ms", "500"); | ||
|
|
||
| expect(definition().backoffJitterMs).toBe(500); | ||
| }); | ||
|
|
||
| it("writes totalTimeoutSeconds back into the definition", () => { | ||
| const { definition } = renderForm({ totalTimeoutSeconds: 0 }); | ||
|
|
||
| setNumber("Total Timeout Seconds", "900"); | ||
|
|
||
| expect(definition().totalTimeoutSeconds).toBe(900); | ||
| }); | ||
|
|
||
| it("toggles taskStatusListenerEnabled", () => { | ||
| const { definition } = renderForm({ taskStatusListenerEnabled: true }); | ||
|
|
||
| fireEvent.click(screen.getByLabelText("Enable task status listener")); | ||
|
|
||
| expect(definition().taskStatusListenerEnabled).toBe(false); | ||
| }); | ||
|
|
||
| it("reads an absent taskStatusListenerEnabled as on, matching the server default", () => { | ||
| renderForm(); | ||
|
|
||
| expect(screen.getByLabelText("Enable task status listener")).toBeChecked(); | ||
| }); | ||
|
|
||
| it("keeps the retry delay cap and jitter editable under a FIXED retry policy", () => { | ||
| // Both are applied by the server for every retry policy, unlike the | ||
| // backoff scale factor, which only applies to the backoff policies. | ||
| renderForm({ retryLogic: TaskRetryLogic.FIXED }); | ||
|
|
||
| expect(field("Max retry delay seconds")).toBeEnabled(); | ||
| expect(field("Backoff jitter ms")).toBeEnabled(); | ||
| expect(field("Backoff scale factor")).toBeDisabled(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| /** | ||
| * Save used to be disabled whenever the description was empty, with nothing on | ||
| * screen saying so — and the rule was not even applied consistently (new task | ||
| * definitions bypassed it, so a definition could be created without a | ||
| * description and then never edited again). The API marks description optional; | ||
| * only ownerEmail is required. These tests pin what may and may not block Save. | ||
| */ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { isSaveDisabled } from "./helpers"; | ||
|
|
||
| const editingExisting = { | ||
| noChanges: false, | ||
| isNewTaskDef: false, | ||
| isTrialExpired: false, | ||
| }; | ||
|
|
||
| describe("isSaveDisabled", () => { | ||
| it("allows saving an edited definition", () => { | ||
| expect(isSaveDisabled(editingExisting)).toBe(false); | ||
| }); | ||
|
|
||
| it("blocks saving an unchanged existing definition", () => { | ||
| expect(isSaveDisabled({ ...editingExisting, noChanges: true })).toBe(true); | ||
| }); | ||
|
|
||
| it("allows saving a brand new definition that has no changes yet", () => { | ||
| expect( | ||
| isSaveDisabled({ | ||
| ...editingExisting, | ||
| isNewTaskDef: true, | ||
| noChanges: true, | ||
| }), | ||
| ).toBe(false); | ||
| }); | ||
|
|
||
| it("blocks saving when the JSON cannot be parsed", () => { | ||
| expect(isSaveDisabled({ ...editingExisting, jsonInvalid: true })).toBe( | ||
| true, | ||
| ); | ||
| }); | ||
|
|
||
| it("blocks saving on an expired trial", () => { | ||
| expect(isSaveDisabled({ ...editingExisting, isTrialExpired: true })).toBe( | ||
| true, | ||
| ); | ||
| }); | ||
|
|
||
| it("treats an undefined isNewTaskDef as an existing definition", () => { | ||
| // The form machine's context does not carry the flag. | ||
| expect( | ||
| isSaveDisabled({ | ||
| noChanges: true, | ||
| isTrialExpired: false, | ||
| isNewTaskDef: undefined, | ||
| }), | ||
| ).toBe(true); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.