Skip to content

Commit 563b9e6

Browse files
committed
feat: restore OOTB workflow reverse-matching on edit with Radix Select guard
Re-introduce resolveWorkflowState to match stored workflow fields against OOTB workflows when editing a scheduled session. Uses a workflowResolved state guard to keep the Skeleton visible until resolution completes, ensuring Radix Select never sees a post-mount value change. Adds e2e tests for OOTB workflow edit (verifies name shown), no-workflow edit (verifies "General chat"), and updates the custom workflow edit test to use non-OOTB fields.
1 parent f86351f commit 563b9e6

2 files changed

Lines changed: 107 additions & 13 deletions

File tree

components/frontend/src/app/projects/[name]/scheduled-sessions/_components/scheduled-session-form.tsx

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,29 @@ function resolveSchedulePreset(schedule: string): { preset: string; customCron:
101101
return { preset: "custom", customCron: schedule };
102102
}
103103

104+
function resolveWorkflowState(
105+
activeWorkflow: WorkflowSelection | undefined,
106+
ootbWorkflows: { id: string; gitUrl: string; branch: string; path?: string }[]
107+
): { selectedWorkflow: string; customGitUrl: string; customBranch: string; customPath: string } {
108+
if (!activeWorkflow) {
109+
return { selectedWorkflow: "none", customGitUrl: "", customBranch: "main", customPath: "" };
110+
}
111+
const match = ootbWorkflows.find(
112+
(w) => w.gitUrl === activeWorkflow.gitUrl
113+
&& w.branch === activeWorkflow.branch
114+
&& (w.path ?? "") === (activeWorkflow.path ?? "")
115+
);
116+
if (match) {
117+
return { selectedWorkflow: match.id, customGitUrl: "", customBranch: "main", customPath: "" };
118+
}
119+
return {
120+
selectedWorkflow: "custom",
121+
customGitUrl: activeWorkflow.gitUrl,
122+
customBranch: activeWorkflow.branch || "main",
123+
customPath: activeWorkflow.path ?? "",
124+
};
125+
}
126+
104127
export function ScheduledSessionForm({ projectName, mode, initialData }: ScheduledSessionFormProps) {
105128
const router = useRouter();
106129
const isEdit = mode === "edit";
@@ -122,6 +145,9 @@ export function ScheduledSessionForm({ projectName, mode, initialData }: Schedul
122145
const [customGitUrl, setCustomGitUrl] = useState(initialWorkflow.customGitUrl);
123146
const [customBranch, setCustomBranch] = useState(initialWorkflow.customBranch);
124147
const [customPath, setCustomPath] = useState(initialWorkflow.customPath);
148+
const [workflowResolved, setWorkflowResolved] = useState(
149+
!isEdit || !initialData?.sessionTemplate.activeWorkflow
150+
);
125151
const [repos, setRepos] = useState<SessionRepo[]>(
126152
isEdit && initialData?.sessionTemplate.repos ? [...initialData.sessionTemplate.repos] : []
127153
);
@@ -184,8 +210,24 @@ export function ScheduledSessionForm({ projectName, mode, initialData }: Schedul
184210
}
185211
}, [modelsData?.defaultModel, form, isEdit, initialData]);
186212

187-
// Workflow state is initialized from initialData in useState above.
188-
// No useEffect needed — avoids timing issues with Radix Select.
213+
// Resolve workflow state once OOTB workflows finish loading. The Skeleton
214+
// guard on the Select (workflowsLoading || !workflowResolved) ensures Radix
215+
// never sees a value change after mount — the Select only mounts after this
216+
// effect has set the final selectedWorkflow value.
217+
useEffect(() => {
218+
if (workflowResolved) return;
219+
if (workflowsLoading) return;
220+
221+
const resolved = resolveWorkflowState(
222+
initialData!.sessionTemplate.activeWorkflow,
223+
ootbWorkflows
224+
);
225+
setSelectedWorkflow(resolved.selectedWorkflow);
226+
setCustomGitUrl(resolved.customGitUrl);
227+
setCustomBranch(resolved.customBranch);
228+
setCustomPath(resolved.customPath);
229+
setWorkflowResolved(true);
230+
}, [workflowResolved, workflowsLoading, ootbWorkflows, initialData]);
189231

190232
const effectiveCron = schedulePreset === "custom" ? (customCron ?? "") : schedulePreset;
191233
const nextRuns = useMemo(() => getNextRuns(effectiveCron, 3), [effectiveCron]);
@@ -444,7 +486,7 @@ export function ScheduledSessionForm({ projectName, mode, initialData }: Schedul
444486

445487
<div className="space-y-2">
446488
<FormLabel>Workflow</FormLabel>
447-
{workflowsLoading ? (
489+
{(workflowsLoading || !workflowResolved) ? (
448490
<Skeleton className="h-10 w-full" />
449491
) : (
450492
<Select
@@ -469,7 +511,7 @@ export function ScheduledSessionForm({ projectName, mode, initialData }: Schedul
469511
</SelectContent>
470512
</Select>
471513
)}
472-
{selectedWorkflow === "custom" && (
514+
{selectedWorkflow === "custom" && workflowResolved && (
473515
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 pt-1">
474516
<div className="sm:col-span-2 space-y-1">
475517
<FormLabel className="text-xs">Git Repository URL *</FormLabel>

e2e/cypress/e2e/scheduled-sessions.cy.ts

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ describe('Scheduled Sessions', () => {
310310

311311
it('should edit the custom workflow on an existing scheduled session', () => {
312312
// Create a scheduled session with a custom workflow via API
313+
// Use fields that do NOT match any OOTB workflow so it stays as "Custom workflow..."
313314
cy.request({
314315
method: 'POST',
315316
url: `/api/projects/${workspaceSlug}/scheduled-sessions`,
@@ -323,9 +324,9 @@ describe('Scheduled Sessions', () => {
323324
llmSettings: { model: 'claude-sonnet-4-20250514', temperature: 0.7, maxTokens: 4000 },
324325
timeout: 300,
325326
activeWorkflow: {
326-
gitUrl: 'https://github.com/ambient-code/workflows.git',
327+
gitUrl: 'https://github.com/my-org/my-custom-workflows.git',
327328
branch: 'main',
328-
path: 'workflows/bugfix',
329+
path: 'workflows/custom-one',
329330
},
330331
},
331332
},
@@ -336,22 +337,21 @@ describe('Scheduled Sessions', () => {
336337
// Navigate to edit page
337338
cy.visit(`/projects/${workspaceSlug}/scheduled-sessions/${scheduleName}/edit`)
338339

339-
// Wait for form to load — workflow fields should be pre-populated
340+
// Wait for form to load — should show "Custom workflow..." (no OOTB match)
340341
cy.get('[data-testid="workflow-select"]', { timeout: 10000 })
341342
.should('contain.text', 'Custom workflow...')
342343

343344
// Verify pre-populated custom workflow fields
344345
cy.get('[data-testid="workflow-git-url"]')
345-
.should('have.value', 'https://github.com/ambient-code/workflows.git')
346+
.should('have.value', 'https://github.com/my-org/my-custom-workflows.git')
346347
cy.get('[data-testid="workflow-branch"]')
347348
.should('have.value', 'main')
348349
cy.get('[data-testid="workflow-path"]')
349-
.should('have.value', 'workflows/bugfix')
350+
.should('have.value', 'workflows/custom-one')
350351

351352
// Update the workflow fields
352-
cy.get('[data-testid="workflow-git-url"]').clear().type('https://github.com/ambient-code/workflows.git')
353353
cy.get('[data-testid="workflow-branch"]').clear().type('develop')
354-
cy.get('[data-testid="workflow-path"]').clear().type('workflows/triage')
354+
cy.get('[data-testid="workflow-path"]').clear().type('workflows/custom-two')
355355

356356
// Submit
357357
cy.get('[data-testid="scheduled-session-submit"]').click()
@@ -367,12 +367,64 @@ describe('Scheduled Sessions', () => {
367367
}).then((getResp) => {
368368
expect(getResp.status).to.eq(200)
369369
const workflow = getResp.body.sessionTemplate.activeWorkflow
370-
expect(workflow.gitUrl).to.eq('https://github.com/ambient-code/workflows.git')
370+
expect(workflow.gitUrl).to.eq('https://github.com/my-org/my-custom-workflows.git')
371371
expect(workflow.branch).to.eq('develop')
372-
expect(workflow.path).to.eq('workflows/triage')
372+
expect(workflow.path).to.eq('workflows/custom-two')
373373
})
374374
})
375375
})
376+
377+
it('should show OOTB workflow name when editing a session created with an OOTB workflow', () => {
378+
// Create a session with fields that match the "bugfix" OOTB workflow
379+
cy.request({
380+
method: 'POST',
381+
url: `/api/projects/${workspaceSlug}/scheduled-sessions`,
382+
headers: apiHeaders(),
383+
body: {
384+
displayName: 'OOTB Workflow Edit Test',
385+
schedule: '0 * * * *',
386+
sessionTemplate: {
387+
initialPrompt: 'run bugfix workflow',
388+
runnerType: 'claude-code',
389+
llmSettings: { model: 'claude-sonnet-4-20250514', temperature: 0.7, maxTokens: 4000 },
390+
timeout: 300,
391+
activeWorkflow: {
392+
gitUrl: 'https://github.com/ambient-code/workflows.git',
393+
branch: 'main',
394+
path: 'workflows/bugfix',
395+
},
396+
},
397+
},
398+
}).then((resp) => {
399+
expect(resp.status).to.be.oneOf([200, 201])
400+
const scheduleName = resp.body.name
401+
402+
// Navigate to edit page
403+
cy.visit(`/projects/${workspaceSlug}/scheduled-sessions/${scheduleName}/edit`)
404+
405+
// The workflow select should show the OOTB workflow name, not "Custom workflow..."
406+
cy.get('[data-testid="workflow-select"]', { timeout: 10000 })
407+
.should('contain.text', 'Fix a bug')
408+
409+
// Custom workflow fields should NOT be visible (OOTB selected, not custom)
410+
cy.get('[data-testid="workflow-git-url"]').should('not.exist')
411+
})
412+
})
413+
414+
it('should show General chat when editing a session with no workflow', () => {
415+
// Create a session without any workflow
416+
createScheduledSessionViaApi('0 * * * *', 'No Workflow Edit Test').then((scheduleName) => {
417+
// Navigate to edit page
418+
cy.visit(`/projects/${workspaceSlug}/scheduled-sessions/${scheduleName}/edit`)
419+
420+
// The workflow select should show "General chat"
421+
cy.get('[data-testid="workflow-select"]', { timeout: 10000 })
422+
.should('contain.text', 'General chat')
423+
424+
// Custom workflow fields should NOT be visible
425+
cy.get('[data-testid="workflow-git-url"]').should('not.exist')
426+
})
427+
})
376428
})
377429

378430
// ─── Schedule Deletion ────────────────────────────────────────

0 commit comments

Comments
 (0)