mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
fix(frontend): preserve schedule when switching to custom cron (#6749)
Co-authored-by: haotianqi666 <250384991+haotianqi666@users.noreply.github.com>
This commit is contained in:
parent
65fa321ec1
commit
338f8819a4
@ -2796,6 +2796,8 @@ Current MVP limits:
|
||||
|
||||
Enable background polling with `config.yaml -> scheduler.enabled`. Manual trigger uses the same scheduled-task resource and execution path.
|
||||
|
||||
In the task form, switching an hourly, daily, weekly, or monthly schedule to **Custom cron** starts from its current timing, including selected weekdays or day of the month. Switching back to Custom cron after preset edits uses those edits rather than an older expression.
|
||||
|
||||
### Lifecycle, safety caps and stop conditions
|
||||
|
||||
- The tasks page and the REST API (`POST` / `PATCH /api/scheduled-tasks`) accept the same per-run goal (`goal_objective`), safety cap (`max_runs`, `end_at`) and stop condition (`stop_condition`) as a conversation. Sending `null` in a PATCH clears any of these four; an `end_at` without a UTC offset is wall-clock time in the task's timezone.
|
||||
|
||||
@ -140,6 +140,12 @@ matching in `core/scheduled-tasks/search.ts`; clearing search retains other filt
|
||||
|
||||
Single-run schedule edits retain the mounted task's original `run_at` while its wall time and timezone match. The parent echoes edits through `initial`; retain a stable snapshot and reset the parent draft during render before remounting with a task key when switching tasks. Use the resolved timezone consistently for the snapshot and displayed wall time. Component and scheduled-task E2E tests cover DST folds and timestamp precision.
|
||||
|
||||
The scheduled-task schedule input initializes Custom cron from the current
|
||||
preset and its current fields on every transition into the custom editor.
|
||||
Do not substitute a daily schedule or restore stale `parts.raw` after preset
|
||||
edits. Component tests cover emitted specs; browser tests pin unchanged
|
||||
schedules through the task form's PATCH payload on desktop and mobile.
|
||||
|
||||
## Environment
|
||||
|
||||
Scheduled-task interval forms preserve the initial `every_seconds` on mount,
|
||||
|
||||
@ -223,8 +223,9 @@ export function ScheduledTaskScheduleInput({
|
||||
if (next === "monthly" && merged.dayOfMonth == null) {
|
||||
merged.dayOfMonth = 1;
|
||||
}
|
||||
if (next === "custom" && !merged.raw) {
|
||||
merged.raw = serializeCron("daily", prev);
|
||||
if (next === "custom" && preset !== "custom") {
|
||||
// Open the current schedule, not a daily fallback or an older draft.
|
||||
merged.raw = serializeCron(preset, prev);
|
||||
}
|
||||
return merged;
|
||||
});
|
||||
|
||||
61
frontend/tests/e2e/scheduled-task-cron-preset.spec.ts
Normal file
61
frontend/tests/e2e/scheduled-task-cron-preset.spec.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { mockLangGraphAPI } from "./utils/mock-api";
|
||||
|
||||
for (const width of [1280, 375]) {
|
||||
for (const cron of ["30 14 * * 1,3,5", "30 14 21 * *"]) {
|
||||
test(`custom cron preserves ${cron} through saving at width ${width}`, async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
const patches: Record<string, unknown>[] = [];
|
||||
page.on("request", (request) => {
|
||||
if (
|
||||
request.method() === "PATCH" &&
|
||||
new URL(request.url()).pathname === "/api/scheduled-tasks/cron-task"
|
||||
) {
|
||||
patches.push(request.postDataJSON() as Record<string, unknown>);
|
||||
}
|
||||
});
|
||||
mockLangGraphAPI(page, {
|
||||
threads: [],
|
||||
scheduledTasks: [
|
||||
{
|
||||
id: "cron-task",
|
||||
thread_id: null,
|
||||
title: "Scheduled report",
|
||||
prompt: "Prepare the report",
|
||||
schedule_type: "cron",
|
||||
schedule_spec: { cron },
|
||||
timezone: "UTC",
|
||||
status: "enabled",
|
||||
next_run_at: "2099-01-21T14:30:00Z",
|
||||
last_run_at: null,
|
||||
last_run_id: null,
|
||||
last_error: null,
|
||||
run_count: 0,
|
||||
created_at: "2026-10-01T00:00:00Z",
|
||||
updated_at: "2026-10-01T00:00:00Z",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await page.goto("/workspace/scheduled-tasks");
|
||||
await page
|
||||
.getByTestId("scheduled-task-detail")
|
||||
.getByRole("button", { name: "Edit", exact: true })
|
||||
.click();
|
||||
const form = page.getByTestId("scheduled-task-form");
|
||||
await form.getByTestId("schedule-preset").click();
|
||||
await page.getByRole("option", { name: "Custom cron" }).click();
|
||||
await expect(form.getByLabel("Cron expression")).toHaveValue(cron);
|
||||
|
||||
await form.getByLabel("Title", { exact: true }).fill("Renamed report");
|
||||
await form.getByRole("button", { name: "Save changes" }).click();
|
||||
|
||||
await expect.poll(() => patches.length).toBe(1);
|
||||
expect(patches[0]).toEqual({ title: "Renamed report" });
|
||||
await expect(form).toHaveCount(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,70 @@ import { I18nProvider } from "@/core/i18n/context";
|
||||
afterEach(cleanup);
|
||||
|
||||
describe("ScheduledTaskScheduleInput", () => {
|
||||
async function selectPreset(name: string) {
|
||||
fireEvent.keyDown(screen.getByTestId("schedule-preset"), {
|
||||
key: "ArrowDown",
|
||||
});
|
||||
fireEvent.click(await screen.findByRole("option", { name }));
|
||||
}
|
||||
|
||||
function renderCron(cron: string) {
|
||||
const emitted: ScheduleValue[] = [];
|
||||
render(
|
||||
<I18nProvider initialLocale="en-US">
|
||||
<ScheduledTaskScheduleInput
|
||||
initial={{
|
||||
schedule_type: "cron",
|
||||
schedule_spec: { cron },
|
||||
timezone: "UTC",
|
||||
}}
|
||||
onChange={(value) => emitted.push(value)}
|
||||
/>
|
||||
</I18nProvider>,
|
||||
);
|
||||
return emitted;
|
||||
}
|
||||
|
||||
test.each(["15 * * * *", "30 14 * * *", "30 14 * * 1,3,5", "30 14 21 * *"])(
|
||||
"keeps %s when opening the custom cron editor",
|
||||
async (cron) => {
|
||||
const emitted = renderCron(cron);
|
||||
|
||||
await selectPreset("Custom cron");
|
||||
|
||||
expect(
|
||||
screen.getByRole("textbox", { name: "Cron expression" }),
|
||||
).toHaveProperty("value", cron);
|
||||
expect(emitted.at(-1)?.schedule_spec).toEqual({ cron });
|
||||
},
|
||||
);
|
||||
|
||||
test("reopening custom cron uses the current preset instead of a stale expression", async () => {
|
||||
const emitted = renderCron("*/10 * * * *");
|
||||
|
||||
await selectPreset("Weekly");
|
||||
fireEvent.change(screen.getByLabelText("Time"), {
|
||||
target: { value: "16:45" },
|
||||
});
|
||||
expect(emitted.at(-1)?.schedule_spec).toEqual({ cron: "45 16 * * 1" });
|
||||
|
||||
await selectPreset("Custom cron");
|
||||
|
||||
expect(
|
||||
screen.getByRole("textbox", { name: "Cron expression" }),
|
||||
).toHaveProperty("value", "45 16 * * 1");
|
||||
expect(emitted.at(-1)?.schedule_spec).toEqual({ cron: "45 16 * * 1" });
|
||||
|
||||
fireEvent.change(screen.getByRole("textbox", { name: "Cron expression" }), {
|
||||
target: { value: "*/10 * * * *" },
|
||||
});
|
||||
expect(emitted.at(-1)).toEqual({
|
||||
schedule_type: "cron",
|
||||
schedule_spec: { cron: "*/10 * * * *" },
|
||||
timezone: "UTC",
|
||||
});
|
||||
});
|
||||
|
||||
test.each([1, 30, 59])(
|
||||
"preserves an existing %s-second interval until explicitly edited",
|
||||
(everySeconds) => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user