moadim 3.7.0

Loop engine for AI agents — routines over REST, MCP, and a built-in web UI
import { render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { describe, expect, it } from "vitest";
import type { FleetRunSummary } from "../../api/hooks";
import { computeIncidents } from "./incidentMath";
import { FailureIncidents } from "./FailureIncidents";

function run(
  routine_id: string,
  started_at: number,
  status: FleetRunSummary["status"],
  finished_at: number | null = started_at + 60,
): FleetRunSummary {
  return {
    routine_id,
    routine_title: routine_id.toUpperCase(),
    workbench: `wb-${started_at}`,
    started_at,
    started_at_local: "",
    finished_at,
    finished_at_local: "",
    status,
    exit_code: status === "success" ? 0 : 1,
  };
}

const NOW = 100_000;

describe("computeIncidents", () => {
  it("is empty without failures", () => {
    expect(computeIncidents([run("a", 10, "success")], NOW)).toEqual({
      incidents: [],
      openCount: 0,
      mttrSecs: null,
      longestSecs: null,
    });
  });

  it("groups consecutive failures, resolves on the next success, and ignores running runs", () => {
    // Newest-first input, as the API returns it.
    const r = computeIncidents(
      [
        run("a", 5_000, "running", null),
        run("a", 4_000, "success"),
        run("a", 3_000, "failed"),
        run("a", 2_000, "failed"),
        run("a", 1_000, "success"),
      ],
      NOW,
    );
    expect(r.incidents).toEqual([
      {
        routineId: "a",
        routineTitle: "A",
        openedAt: 2_000,
        resolvedAt: 4_060,
        durationSecs: 2_060,
        failedRuns: 2,
        firstWorkbench: "wb-2000",
        truncated: false,
      },
    ]);
    expect(r.openCount).toBe(0);
    expect(r.mttrSecs).toBe(2_060);
    expect(r.longestSecs).toBe(2_060);
  });

  it("marks ongoing and sample-truncated incidents and orders ongoing first", () => {
    const r = computeIncidents(
      [
        run("b", 9_000, "failed"),
        run("a", 3_000, "success", null),
        run("a", 1_000, "failed"),
        run("a", 500, "success"),
        run("a", 100, "failed"),
      ],
      NOW,
    );
    expect(r.incidents.map((i) => [i.routineId, i.openedAt, i.resolvedAt, i.durationSecs, i.truncated])).toEqual([
      ["b", 9_000, null, NOW - 9_000, true],
      ["a", 1_000, 3_000, 2_000, false],
      ["a", 100, 560, 460, true],
    ]);
    expect(r.openCount).toBe(1);
    expect(r.mttrSecs).toBe(1_230);
    expect(r.longestSecs).toBe(2_000);
  });
});

describe("FailureIncidents", () => {
  const renderPanel = (runs: FleetRunSummary[]) =>
    render(
      <MemoryRouter>
        <FailureIncidents report={computeIncidents(runs, NOW)} />
      </MemoryRouter>,
    );

  it("renders nothing without incidents", () => {
    const { container } = renderPanel([run("a", 10, "success")]);
    expect(container).toBeEmptyDOMElement();
  });

  it("lists ongoing and resolved incidents with the MTTR summary", () => {
    renderPanel([run("b", 9_000, "failed"), run("a", 2_000, "success"), run("a", 1_000, "failed"), run("a", 1, "success")]);
    expect(screen.getByText("1 open")).toHaveClass("c-red");
    expect(screen.getByText(/2 total · MTTR 17m · longest 17m/)).toBeInTheDocument();
    expect(screen.getByText("ONGOING")).toBeInTheDocument();
    expect(screen.getByText("RESOLVED", { selector: "span" })).toBeInTheDocument();
    expect(screen.getByRole("link", { name: "A" })).toHaveAttribute("href", "/routines?history=a");
    const links = screen.getAllByTitle("Open the first failed run");
    expect(links[0]).toHaveAttribute("href", "/runs/b/wb-9000");
    expect(links[0]).toHaveTextContent(/^≥ /);
    expect(links[1]).toHaveAttribute("href", "/runs/a/wb-1000");
    expect(links[1]).not.toHaveTextContent("≥");
  });

  it("shows a dash for MTTR when nothing has recovered yet", () => {
    renderPanel([run("a", 1_000, "failed")]);
    expect(screen.getByText(/MTTR — · longest —/)).toBeInTheDocument();
  });

  it("shows no open incidents without the alert color", () => {
    renderPanel([run("a", 2_000, "success"), run("a", 1_000, "failed")]);
    expect(screen.getByText("0 open")).not.toHaveClass("c-red");
  });
});