import { expect, test } from "@playwright/test"; import { MOCK_THREAD_ID, mockLangGraphAPI } from "./utils/mock-api"; test.describe.configure({ mode: "serial" }); test("scheduled tasks page is reachable from sidebar", async ({ page }) => { mockLangGraphAPI(page, { threads: [], scheduledTasks: [ { id: "task-1", thread_id: "thread-1", title: "Daily summary", prompt: "Summarize thread", schedule_type: "cron", schedule_spec: { cron: "0 9 * * *" }, timezone: "UTC", status: "enabled", next_run_at: "2026-07-02T01:00:00+00:00", last_run_at: null, last_run_id: null, last_error: null, run_count: 0, created_at: "2026-07-01T00:00:00+00:00", updated_at: "2026-07-01T00:00:00+00:00", }, ], }); await page.goto("/workspace/chats/new"); await page.getByRole("link", { name: /scheduled tasks/i }).click(); await page.waitForURL("**/workspace/scheduled-tasks"); await expect(page).toHaveURL(/workspace\/scheduled-tasks/); await expect( page.getByRole("button", { name: /Daily summary/i }), ).toBeVisible(); await expect(page.getByTestId("scheduled-task-runs")).toContainText("0 runs"); }); test("thread page links to filtered scheduled tasks", async ({ page }) => { mockLangGraphAPI(page, { threads: [ { thread_id: MOCK_THREAD_ID, title: "Thread with schedules", updated_at: "2025-06-01T12:00:00Z", }, ], scheduledTasks: [ { id: "task-1", thread_id: MOCK_THREAD_ID, title: "Thread task", prompt: "Summarize thread", schedule_type: "cron", schedule_spec: { cron: "0 9 * * *" }, timezone: "UTC", status: "enabled", next_run_at: "2026-07-02T01:00:00+00:00", last_run_at: null, last_run_id: null, last_error: null, run_count: 0, created_at: "2026-07-01T00:00:00+00:00", updated_at: "2026-07-01T00:00:00+00:00", }, ], }); await page.goto(`/workspace/chats/${MOCK_THREAD_ID}`); await page .locator("header") .getByRole("link", { name: /scheduled tasks/i }) .click(); await page.waitForURL(new RegExp(`thread_id=${MOCK_THREAD_ID}`)); }); test("user can create a scheduled task from the page", async ({ page }) => { mockLangGraphAPI(page, { threads: [], scheduledTasks: [] }); await page.goto("/workspace/scheduled-tasks"); const createForm = page.getByTestId("scheduled-task-create-form"); await createForm.getByRole("button", { name: "One-time" }).click(); await createForm.getByLabel("Run at").fill("2026-07-02T09:00"); await createForm.getByPlaceholder("Task title").fill("Created from UI"); await createForm.getByPlaceholder("Prompt").fill("Summarize thread"); await createForm.getByRole("button", { name: "Create" }).click(); await expect( page.getByRole("button", { name: /Created from UI/i }), ).toBeVisible(); await expect( page.getByTestId("scheduled-task-detail").getByText("Summarize thread"), ).toBeVisible(); }); test("duplicate fills the create form without creating a task", async ({ page, }) => { let createRequests = 0; page.on("request", (request) => { if ( request.method() === "POST" && new URL(request.url()).pathname.endsWith("/api/scheduled-tasks") ) { createRequests += 1; } }); mockLangGraphAPI(page, { threads: [], scheduledTasks: [ { id: "task-copy-source", thread_id: "thread-copy-source", context_mode: "reuse_thread", title: "Daily summary", prompt: "Summarize this conversation", schedule_type: "cron", schedule_spec: { cron: "0 18 * * *" }, timezone: "UTC", status: "enabled", next_run_at: "2026-08-28T18:00:00Z", last_run_at: null, last_run_id: null, last_thread_id: null, last_error: null, run_count: 0, created_at: "2026-08-01T00:00:00Z", updated_at: "2026-08-01T00:00:00Z", }, ], }); await page.goto("/workspace/scheduled-tasks"); await page .getByTestId("scheduled-task-detail") .getByRole("button", { name: "Duplicate" }) .click(); const createForm = page.getByTestId("scheduled-task-create-form"); await expect(createForm.getByPlaceholder("Task title")).toHaveValue( "Daily summary (Copy)", ); await expect(createForm.getByPlaceholder("Prompt")).toHaveValue( "Summarize this conversation", ); await expect(createForm.getByPlaceholder("Thread ID")).toHaveValue( "thread-copy-source", ); await expect(createForm.getByTestId("schedule-preview")).toHaveText( "Every day at 18:00 (UTC)", ); await expect(createForm.getByPlaceholder("Task title")).toBeFocused(); expect(createRequests).toBe(0); }); test("reuse-thread tasks explain their context and busy-thread queue behavior", async ({ page, }) => { mockLangGraphAPI(page, { threads: [], scheduledTasks: [ { id: "task-reuse", thread_id: "thread-1", context_mode: "reuse_thread", title: "Conversation summary", prompt: "Summarize this conversation", schedule_type: "cron", schedule_spec: { cron: "0 18 * * *" }, timezone: "UTC", status: "enabled", next_run_at: "2026-07-02T18:00:00+00:00", last_run_at: null, last_run_id: null, last_error: null, run_count: 0, created_at: "2026-07-01T00:00:00+00:00", updated_at: "2026-07-01T00:00:00+00:00", }, ], }); await page.goto("/workspace/scheduled-tasks"); const detailNotice = page .getByTestId("scheduled-task-detail") .getByRole("alert"); await expect(detailNotice).toContainText( "Uses this thread's conversation history", ); await expect(detailNotice).toContainText( "queues this occurrence and starts it when the thread is available", ); const createForm = page.getByTestId("scheduled-task-create-form"); await expect(createForm.getByRole("alert")).toHaveCount(0); await createForm.getByRole("button", { name: "Reuse thread" }).click(); const createNotice = createForm.getByRole("alert"); await expect(createNotice).toContainText( "Uses this thread's conversation history", ); await expect(createNotice).toContainText( "It fails if the configured queue wait limit is exceeded", ); await createForm.getByRole("button", { name: "Fresh thread" }).click(); await expect(createForm.getByRole("alert")).toHaveCount(0); }); test("user can pause a scheduled task from the detail pane", async ({ page, }) => { mockLangGraphAPI(page, { threads: [], scheduledTasks: [ { id: "task-1", thread_id: "thread-1", title: "Pausable task", prompt: "Summarize thread", schedule_type: "cron", schedule_spec: { cron: "0 9 * * *" }, timezone: "UTC", status: "enabled", next_run_at: "2026-07-02T01:00:00+00:00", last_run_at: null, last_run_id: null, last_error: null, run_count: 0, created_at: "2026-07-01T00:00:00+00:00", updated_at: "2026-07-01T00:00:00+00:00", }, ], }); await page.goto("/workspace/scheduled-tasks"); const detail = page.getByTestId("scheduled-task-detail"); await detail.getByRole("button", { name: "Pause" }).click(); await expect(page.getByTestId("scheduled-task-item-task-1")).toBeVisible(); await expect( page.getByTestId("scheduled-task-item-task-1").getByText(/paused/i), ).toBeVisible(); }); test("trigger shows a run entry in the detail pane", async ({ page }) => { mockLangGraphAPI(page, { threads: [], scheduledTasks: [ { id: "task-1", thread_id: "thread-1", title: "Triggerable task", prompt: "Summarize thread", schedule_type: "cron", schedule_spec: { cron: "0 9 * * *" }, timezone: "UTC", status: "enabled", next_run_at: "2026-07-02T01:00:00+00:00", last_run_at: null, last_run_id: null, last_error: null, run_count: 0, created_at: "2026-07-01T00:00:00+00:00", updated_at: "2026-07-01T00:00:00+00:00", }, ], }); await page.goto("/workspace/scheduled-tasks"); await page.getByRole("button", { name: "Trigger now" }).click(); await expect(page.getByTestId("scheduled-task-runs")).toContainText("1 run"); await expect( page.getByTestId("scheduled-task-run-list").getByText(/Manual ยท Success/i), ).toBeVisible(); }); test("detail pane falls back to a visible task after filters hide the selected task", async ({ page, }) => { mockLangGraphAPI(page, { threads: [], scheduledTasks: [ { id: "task-enabled", thread_id: "thread-1", title: "Enabled task", prompt: "Enabled prompt", schedule_type: "cron", schedule_spec: { cron: "0 9 * * *" }, timezone: "UTC", status: "enabled", next_run_at: "2026-07-02T01:00:00+00:00", last_run_at: null, last_run_id: null, last_error: null, run_count: 0, created_at: "2026-07-01T00:00:00+00:00", updated_at: "2026-07-01T00:00:00+00:00", }, { id: "task-paused", thread_id: "thread-2", title: "Paused task", prompt: "Paused prompt", schedule_type: "cron", schedule_spec: { cron: "0 10 * * *" }, timezone: "UTC", status: "paused", next_run_at: "2026-07-02T02:00:00+00:00", last_run_at: null, last_run_id: null, last_error: null, run_count: 0, created_at: "2026-07-01T00:00:00+00:00", updated_at: "2026-07-01T00:00:00+00:00", }, ], }); await page.goto("/workspace/scheduled-tasks"); await page.getByTestId("scheduled-task-item-task-paused").click(); await expect( page.getByTestId("scheduled-task-detail").getByText("Paused task"), ).toBeVisible(); await page.getByRole("button", { name: "Enabled", exact: true }).click(); await expect( page.getByTestId("scheduled-task-detail").getByText("Enabled task"), ).toBeVisible(); await expect( page.getByTestId("scheduled-task-item-task-enabled"), ).toBeVisible(); await expect(page.getByTestId("scheduled-task-item-task-paused")).toHaveCount( 0, ); }); test("create posts the default lead_agent assistant_id", async ({ page }) => { let createBody: Record | null = null; page.on("request", (request) => { if ( request.method() === "POST" && new URL(request.url()).pathname.endsWith("/api/scheduled-tasks") ) { createBody = request.postDataJSON() as Record; } }); mockLangGraphAPI(page, { threads: [], scheduledTasks: [] }); await page.goto("/workspace/scheduled-tasks"); const createForm = page.getByTestId("scheduled-task-create-form"); await expect( createForm.getByTestId("scheduled-task-create-agent"), ).toContainText(/Default agent \(lead_agent\)/i); await createForm.getByRole("button", { name: "One-time" }).click(); await createForm.getByLabel("Run at").fill("2026-07-02T09:00"); await createForm.getByPlaceholder("Task title").fill("Agent pin"); await createForm.getByPlaceholder("Prompt").fill("Summarize thread"); await createForm.getByRole("button", { name: "Create" }).click(); await expect(page.getByRole("button", { name: /Agent pin/i })).toBeVisible(); expect(createBody).toMatchObject({ assistant_id: "lead_agent" }); await expect(page.getByTestId("scheduled-task-detail")).toContainText( /Default agent \(lead_agent\)/i, ); }); test("duplicate copies the source task assistant into the create form", async ({ page, }) => { mockLangGraphAPI(page, { threads: [], scheduledTasks: [ { id: "task-copy-agent", thread_id: null, context_mode: "fresh_thread_per_run", assistant_id: "research-bot", title: "Research digest", prompt: "Summarize papers", schedule_type: "cron", schedule_spec: { cron: "0 9 * * *" }, timezone: "UTC", status: "enabled", next_run_at: "2026-07-02T01:00:00+00:00", last_run_at: null, last_run_id: null, last_error: null, run_count: 0, created_at: "2026-07-01T00:00:00+00:00", updated_at: "2026-07-01T00:00:00+00:00", }, ], }); await page.goto("/workspace/scheduled-tasks"); await expect(page.getByTestId("scheduled-task-detail")).toContainText( "research-bot", ); await page .getByTestId("scheduled-task-detail") .getByRole("button", { name: "Duplicate" }) .click(); await expect(page.getByTestId("scheduled-task-create-agent")).toContainText( "research-bot", ); }); test("edit omits assistant_id when the agent is unchanged", async ({ page, }) => { let patchBody: Record | null = null; page.on("request", (request) => { if ( request.method() === "PATCH" && new URL(request.url()).pathname.includes("/api/scheduled-tasks/") ) { patchBody = request.postDataJSON() as Record; } }); mockLangGraphAPI(page, { threads: [], scheduledTasks: [ { id: "task-edit-agent", thread_id: null, context_mode: "fresh_thread_per_run", assistant_id: "research-bot", title: "Research digest", prompt: "Summarize papers", schedule_type: "cron", schedule_spec: { cron: "0 9 * * *" }, timezone: "UTC", status: "enabled", next_run_at: "2026-07-02T01:00:00+00:00", last_run_at: null, last_run_id: null, last_error: null, run_count: 0, created_at: "2026-07-01T00:00:00+00:00", updated_at: "2026-07-01T00:00:00+00:00", }, ], }); await page.goto("/workspace/scheduled-tasks"); await page .getByTestId("scheduled-task-detail") .getByRole("button", { name: "Edit" }) .click(); await page.getByPlaceholder("Edit title").fill("Renamed digest"); await page.getByRole("button", { name: "Save edit" }).click(); await expect(page.getByTestId("scheduled-task-detail")).toContainText( "Renamed digest", ); expect(patchBody).toMatchObject({ title: "Renamed digest" }); expect(patchBody).not.toHaveProperty("assistant_id"); }); for (const runAt of ["2026-11-01T06:30:00Z", "2027-06-01T12:30:45Z"]) { test(`editing title and prompt preserves one-time instant ${runAt}`, async ({ page, }) => { mockLangGraphAPI(page, { threads: [], scheduledTasks: [ { id: "task-instant", thread_id: null, context_mode: "fresh_thread_per_run", title: "Original title", prompt: "Original prompt", schedule_type: "once", schedule_spec: { run_at: runAt }, timezone: "America/New_York", status: "enabled", next_run_at: runAt, last_run_at: null, last_run_id: null, last_error: null, run_count: 0, created_at: "2026-07-01T00:00:00Z", updated_at: "2026-07-01T00:00:00Z", }, ], }); await page.goto("/workspace/scheduled-tasks"); const detail = page.getByTestId("scheduled-task-detail"); await detail.getByRole("button", { name: "Edit", exact: true }).click(); await detail.getByPlaceholder("Edit title").fill("Renamed task"); await detail.getByPlaceholder("Edit prompt").fill("Updated prompt"); const submitted = page.waitForRequest( (request) => request.method() === "PATCH" && new URL(request.url()).pathname === "/api/scheduled-tasks/task-instant", ); await detail .getByRole("button", { name: "Save edit", exact: true }) .click(); const request = await submitted; expect(request.postDataJSON()).toMatchObject({ title: "Renamed task", prompt: "Updated prompt", schedule_spec: { run_at: runAt }, timezone: "America/New_York", }); }); } test("switching tasks during editing saves only the selected task's schedule", async ({ page, }) => { const tasks = [ { id: "switch-a", title: "Task A", prompt: "Prompt A", timezone: "America/New_York", runAt: "2026-03-01T05:00:00+00:00", }, { id: "switch-b", title: "Task B", prompt: "Prompt B", timezone: "Asia/Shanghai", runAt: "2027-06-01T12:30:45+00:00", }, ]; mockLangGraphAPI(page, { threads: [], scheduledTasks: tasks.map((task) => ({ id: task.id, title: task.title, prompt: task.prompt, timezone: task.timezone, thread_id: null, context_mode: "fresh_thread_per_run", schedule_type: "once", schedule_spec: { run_at: task.runAt }, status: "enabled", next_run_at: task.runAt, last_run_at: null, last_run_id: null, last_error: null, run_count: 0, created_at: "2026-07-01T00:00:00Z", updated_at: "2026-07-01T00:00:00Z", })), }); await page.goto("/workspace/scheduled-tasks"); await page.getByTestId("scheduled-task-item-switch-a").click(); const detail = page.getByTestId("scheduled-task-detail"); await detail.getByRole("button", { name: "Edit", exact: true }).click(); await detail.getByPlaceholder("Edit title").fill("Unsaved A"); await page.getByTestId("scheduled-task-item-switch-b").click(); await expect(detail.getByPlaceholder("Edit title")).toHaveValue("Task B"); await expect(detail.getByPlaceholder("Edit prompt")).toHaveValue("Prompt B"); await expect(detail.getByLabel("Run at")).toHaveValue("2027-06-01T20:30"); await detail.getByLabel("Run at").fill("2027-06-01T21:30"); await detail.getByLabel("Run at").fill("2027-06-01T20:30"); const submitted = page.waitForRequest( (request) => request.method() === "PATCH" && new URL(request.url()).pathname === "/api/scheduled-tasks/switch-b", ); await detail.getByRole("button", { name: "Save edit", exact: true }).click(); expect((await submitted).postDataJSON()).toMatchObject({ title: "Task B", prompt: "Prompt B", timezone: "Asia/Shanghai", schedule_spec: { run_at: tasks[1]!.runAt }, }); });