moadim 3.1.0

Loop engine for AI agents — routines over REST, MCP, and a built-in web UI
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import type { RoutineResponse } from "../../api/hooks";
import { RoutineRow } from "./RoutineRow";

function routine(overrides: Partial<RoutineResponse> = {}): RoutineResponse {
  return {
    id: "r1",
    title: "Nightly backup",
    agent: "claude",
    model: null,
    schedule: "0 * * * *",
    prompt: "",
    repositories: [],
    machines: ["m1"],
    enabled: true,
    source: "",
    created_at: 0,
    updated_at: 0,
    last_manual_trigger_at: null,
    last_scheduled_trigger_at: null,
    snoozed_until: null,
    skip_runs: null,
    power_saving: false,
    ttl_secs: null,
    tags: [],
    agent_registered: true,
    agent_command_available: true,
    agent_setup_available: true,
    is_running: false,
    file_path: "",
    folder: null,
    slug: "routine",
    rel_path: "routine",
    schedule_description: null,
    goal: null,
    flag_count: 0,
    env_keys: [],
    ...overrides,
  };
}

function renderRow(r: RoutineResponse) {
  render(
    <table>
      <tbody>
        <RoutineRow
          routine={r}
          now={new Date(2026, 0, 1, 12, 0, 0)}
          runs={[]}
          selected={false}
          onSelect={vi.fn()}
          onEdit={vi.fn()}
          onClone={vi.fn()}
          onDelete={vi.fn()}
          onMove={vi.fn()}
          onToggle={vi.fn()}
          onTrigger={vi.fn()}
          onLogs={vi.fn()}
          onHistory={vi.fn()}
          onFlags={vi.fn()}
        />
      </tbody>
    </table>,
  );
}

describe("RoutineRow — failure circuit-breaker (issue #521)", () => {
  it("shows an AUTO-DISABLED badge with the daemon's reason as the tooltip", () => {
    renderRow(routine({ enabled: false, auto_disabled_reason: "5 consecutive failures" }));
    const badge = screen.getByText("AUTO-DISABLED");
    expect(badge).toHaveAttribute("title", "5 consecutive failures");
  });

  it("a manually-disabled routine still shows the plain DISABLED badge", () => {
    renderRow(routine({ enabled: false, auto_disabled_reason: null }));
    expect(screen.getByText("DISABLED")).toBeInTheDocument();
    expect(screen.queryByText("AUTO-DISABLED")).not.toBeInTheDocument();
  });

  it("shows no failure chip when there are no consecutive failures", () => {
    renderRow(routine({ consecutive_failures: 0, failure_threshold: 5 }));
    expect(screen.queryByText("0/5")).not.toBeInTheDocument();
  });

  it("shows a warning failure chip while short of the threshold", () => {
    renderRow(routine({ consecutive_failures: 2, failure_threshold: 5 }));
    const chip = screen.getByText("2/5");
    expect(chip).toHaveClass("failure-chip", "warning");
  });

  it("shows a critical failure chip when the next failure trips the breaker", () => {
    renderRow(routine({ consecutive_failures: 4, failure_threshold: 5 }));
    const chip = screen.getByText("4/5");
    expect(chip).toHaveClass("failure-chip", "critical");
  });
});