moadim 1.4.1

Loop engine for AI agents — routines over REST, MCP, and a built-in web UI
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { RoutineForm } from "./RoutineForm";

function renderForm(props: Partial<React.ComponentProps<typeof RoutineForm>> = {}) {
  const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
  const onSave = vi.fn();
  const onCancel = vi.fn();
  render(
    <QueryClientProvider client={queryClient}>
      <RoutineForm mode="create" saving={false} onCancel={onCancel} onSave={onSave} {...props} />
    </QueryClientProvider>,
  );
  return { onSave, onCancel };
}

describe("RoutineForm validation", () => {
  it("disables save until title, schedule, agent, and prompt are all non-blank", () => {
    renderForm();
    const save = screen.getByRole("button", { name: "CREATE ROUTINE" });
    expect(save).toBeDisabled();

    fireEvent.change(screen.getByPlaceholderText("nightly triage"), { target: { value: "My routine" } });
    expect(save).toBeDisabled();

    fireEvent.change(screen.getByPlaceholderText("sec min hour dom month dow year"), {
      target: { value: "@daily" },
    });
    // Agent already defaults to "claude" — still missing prompt.
    expect(save).toBeDisabled();

    fireEvent.change(screen.getByPlaceholderText("Review open PRs and summarize…"), {
      target: { value: "Do the thing" },
    });
    expect(save).not.toBeDisabled();
  });

  it("whitespace-only fields do not count as filled", () => {
    renderForm();
    fireEvent.change(screen.getByPlaceholderText("nightly triage"), { target: { value: "   " } });
    fireEvent.change(screen.getByPlaceholderText("sec min hour dom month dow year"), {
      target: { value: "@daily" },
    });
    fireEvent.change(screen.getByPlaceholderText("Review open PRs and summarize…"), {
      target: { value: "Do the thing" },
    });
    expect(screen.getByRole("button", { name: "CREATE ROUTINE" })).toBeDisabled();
  });

  it("submits the parsed draft on save", async () => {
    const { onSave } = renderForm();
    fireEvent.change(screen.getByPlaceholderText("nightly triage"), { target: { value: "My routine" } });
    fireEvent.change(screen.getByPlaceholderText("sec min hour dom month dow year"), {
      target: { value: "@daily" },
    });
    fireEvent.change(screen.getByPlaceholderText("Review open PRs and summarize…"), {
      target: { value: "Do the thing" },
    });
    fireEvent.click(screen.getByRole("button", { name: "CREATE ROUTINE" }));
    await waitFor(() =>
      expect(onSave).toHaveBeenCalledWith(
        expect.objectContaining({ title: "My routine", schedule: "@daily", prompt: "Do the thing", agent: "claude" }),
      ),
    );
  });

  it("a cron preset button fills the schedule field", () => {
    renderForm();
    fireEvent.click(screen.getByRole("button", { name: "every hour" }));
    expect(screen.getByPlaceholderText("sec min hour dom month dow year")).toHaveValue("0 0 * * * * *");
  });

  it("edit mode renders the modal chrome with SAVE CHANGES", () => {
    renderForm({ mode: "edit" });
    expect(screen.getByText("EDIT ROUTINE")).toBeInTheDocument();
    expect(screen.getByRole("button", { name: "SAVE CHANGES" })).toBeInTheDocument();
  });
});