From f1fc33f4581b98db52a1542e07528c4daf9c0222 Mon Sep 17 00:00:00 2001 From: Bohan Jiang <52446949+Bohan-J@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:03:32 +0800 Subject: [PATCH] fix(autopilots): save the first schedule added from the edit dialog (MUL-5649) (#6303) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Editing a manual-only autopilot showed the schedule panel seeded with the editor's default — 09:00 every day, next runs and all — as though that were the autopilot's schedule. It was not: the autopilot had no trigger. Saving then compared that default against itself, found no change, wrote nothing, and toasted "Autopilot updated" while the detail page went on reading "No triggers configured". The dirty check is right when a schedule exists (it stops a re-picked, unchanged cron from being rewritten) and meaningless when none does, because the panel is showing a proposal rather than stored state. So the panel now says what is true — a dashed card matching the detail page's empty state, "No schedule — this autopilot only runs when triggered manually" — and asks for the schedule explicitly. Adding one writes it on save whether or not the user touched the default; leaving it alone keeps the autopilot manual, so a title-only edit can no longer put it on a daily cron by accident. The footer's auto-run promise drops out while that empty state is up. The schedule write also targets the first `schedule` trigger rather than `triggers[0]`, which on an api-triggered autopilot was a row a cron could have been patched into. Co-authored-by: Bohan-J Co-authored-by: multica-agent --- .../autopilot-dialog.schedule.test.tsx | 264 ++++++++++++++++++ .../components/autopilot-dialog.tsx | 125 ++++++--- packages/views/locales/en/autopilots.json | 2 + packages/views/locales/ja/autopilots.json | 2 + packages/views/locales/ko/autopilots.json | 2 + .../views/locales/zh-Hans/autopilots.json | 2 + 6 files changed, 365 insertions(+), 32 deletions(-) create mode 100644 packages/views/autopilots/components/autopilot-dialog.schedule.test.tsx diff --git a/packages/views/autopilots/components/autopilot-dialog.schedule.test.tsx b/packages/views/autopilots/components/autopilot-dialog.schedule.test.tsx new file mode 100644 index 0000000000..8f3acfa37b --- /dev/null +++ b/packages/views/autopilots/components/autopilot-dialog.schedule.test.tsx @@ -0,0 +1,264 @@ +import { useImperativeHandle, useRef, useState } from "react"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import type { AutopilotTrigger } from "@multica/core/types"; +import { renderWithI18n } from "../../test/i18n"; + +// Regression cover for MUL-5649: editing a manual-only autopilot (no triggers) +// showed the schedule panel seeded with the editor's default — 09:00 every day +// — as if that were the autopilot's schedule. Saving compared that default +// against itself, found no change, and wrote nothing, while the toast said the +// autopilot was updated and the detail page still read "No triggers +// configured". The panel now states what is true (no schedule) and asks for the +// schedule to be added explicitly; adding one writes it, default or not. + +const mockUpdateAutopilot = vi.hoisted(() => vi.fn()); +const mockCreateTrigger = vi.hoisted(() => vi.fn()); +const mockUpdateTrigger = vi.hoisted(() => vi.fn()); + +vi.mock("@multica/core/hooks", () => ({ useWorkspaceId: () => "ws-test" })); +vi.mock("@multica/core/paths", () => ({ useCurrentWorkspace: () => ({ name: "Acme" }) })); + +vi.mock("@multica/core/workspace/queries", () => ({ + agentListOptions: (wsId: string) => ({ + queryKey: ["agents", wsId], + queryFn: async () => [ + { + id: "agent-1", + name: "Scout", + description: "Researches things", + archived_at: null, + runtime_id: "runtime-1", + }, + ], + }), + squadListOptions: (wsId: string) => ({ + queryKey: ["squads", wsId], + queryFn: async () => [], + }), +})); + +vi.mock("@multica/core/projects/queries", () => ({ + projectListOptions: (wsId: string) => ({ + queryKey: ["projects", wsId], + queryFn: async () => [], + }), +})); + +vi.mock("@multica/core/autopilots/queries", () => ({ + cronPreviewOptions: (wsId: string, expr: string, tz: string) => ({ + queryKey: ["cron-preview", wsId, expr, tz], + queryFn: async () => ({ next_runs: ["2126-07-14T01:00:00Z"] }), + retry: false, + }), +})); + +vi.mock("@multica/core/autopilots/mutations", () => ({ + useCreateAutopilot: () => ({ mutateAsync: vi.fn() }), + useCreateAutopilotTrigger: () => ({ mutateAsync: mockCreateTrigger }), + useUpdateAutopilot: () => ({ mutateAsync: mockUpdateAutopilot }), + useUpdateAutopilotTrigger: () => ({ mutateAsync: mockUpdateTrigger }), +})); + +vi.mock("sonner", () => ({ toast: { success: vi.fn(), error: vi.fn() } })); + +vi.mock("../../editor", () => ({ + TitleEditor: ({ ref, defaultValue, placeholder, onChange, onSubmit }: any) => { + const [value, setValue] = useState(defaultValue ?? ""); + const inputRef = useRef(null); + useImperativeHandle(ref, () => ({ + getText: () => value, + focus: () => inputRef.current?.focus(), + focusAtCoords: () => inputRef.current?.focus(), + })); + return ( + { + setValue(e.target.value); + onChange?.(e.target.value); + }} + onKeyDown={(e) => { + if (e.key === "Enter") onSubmit?.(); + }} + /> + ); + }, + ContentEditor: ({ placeholder }: any) =>