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 { describe, expect, it } from "vitest";
import type { FleetRunSummary, RoutineResponse } from "../../api/hooks";
import { CapacityForecast } from "./CapacityForecast";
import { buildForecast, DEFAULT_ESTIMATE_SECS, medianDurations, overCapWindows, projectedFires } from "./forecastMath";

function routine(overrides: Partial<RoutineResponse> = {}): RoutineResponse {
  return {
    id: "a",
    schedule: "0 * * * *",
    title: "Alpha",
    agent: "a",
    enabled: true,
    source: "",
    created_at: 0,
    updated_at: 0,
    agent_registered: false,
    agent_command_available: false,
    agent_setup_available: false,
    is_running: false,
    file_path: "",
    folder: null,
    slug: "routine",
    rel_path: "routine",
    flag_count: 0,
    env_keys: [],
    machines: ["m1"],
    ...overrides,
  };
}

function run(o: Partial<FleetRunSummary>): FleetRunSummary {
  return {
    routine_id: "a",
    routine_title: "Alpha",
    workbench: "wb",
    started_at: 0,
    started_at_local: "",
    finished_at: 600,
    finished_at_local: "",
    status: "success",
    exit_code: 0,
    ...o,
  };
}

// 2026-06-21 12:00 local, on the hour.
const NOW = Math.floor(new Date(2026, 5, 21, 12, 0, 0).getTime() / 1000);
const H = 3_600;

describe("medianDurations", () => {
  it("takes the lower median of finished runs per routine, ignoring running/unfinished", () => {
    const m = medianDurations([
      run({ finished_at: 100 }),
      run({ finished_at: 300 }),
      run({ finished_at: 200 }),
      run({ finished_at: 900 }),
      run({ finished_at: null, status: "running" }),
      run({ routine_id: "b", started_at: 50, finished_at: 20 }),
    ]);
    expect(m.get("a")).toBe(200);
    expect(m.get("b")).toBe(0);
  });
});

describe("projectedFires", () => {
  it("lists fires inside the window, merged across schedules", () => {
    const fires = projectedFires(routine({ schedules: ["0 * * * *", "0 13 * * *"] }), NOW, NOW + 3 * H);
    expect(fires).toEqual([NOW + H, NOW + 2 * H]);
  });

  it("drops fires before snoozed_until and the next skip_runs fires", () => {
    expect(projectedFires(routine({ snoozed_until: NOW + 2 * H }), NOW, NOW + 4 * H)).toEqual([NOW + 2 * H, NOW + 3 * H]);
    expect(projectedFires(routine({ skip_runs: 2 }), NOW, NOW + 4 * H)).toEqual([NOW + 3 * H]);
  });

  it("is empty for an unparseable schedule", () => {
    expect(projectedFires(routine({ schedule: "nope" }), NOW, NOW + H)).toEqual([]);
  });
});

describe("buildForecast", () => {
  it("sizes bars by median, skips disabled/dormant routines, and flags over-cap windows", () => {
    const routines = [
      routine({ schedule: "0 13 * * *" }),
      routine({ id: "b", title: "Beta", schedule: "30 13 * * *" }),
      routine({ id: "off", title: "Off", enabled: false }),
      routine({ id: "dormant", title: "Dormant", machines: [] }),
    ];
    const f = buildForecast(routines, [run({ finished_at: 2 * H })], NOW, 1);
    expect(f.lanes.map((l) => l.title)).toEqual(["Alpha", "Beta"]);
    const [alpha, beta] = f.lanes;
    expect(alpha?.estimated).toBe(false);
    expect(alpha?.bars).toEqual([{ start: NOW + H, end: NOW + 3 * H, running: false }]);
    expect(beta?.estimated).toBe(true);
    expect(beta?.estimateSecs).toBe(DEFAULT_ESTIMATE_SECS);
    expect(f.peak).toBe(2);
    expect(f.overCap).toEqual([
      { from: NOW + 1.5 * H, to: NOW + 1.5 * H + DEFAULT_ESTIMATE_SECS, peak: 2, titles: ["Alpha", "Beta"] },
    ]);
    expect(buildForecast(routines, [], NOW, 0).overCap).toEqual([]);
  });

  it("counts a currently running run until its expected finish and floors tiny medians", () => {
    const f = buildForecast(
      [routine({ schedule: "0 0 1 1 *" })],
      [run({ finished_at: 1 }), run({ started_at: NOW - 30, finished_at: null, status: "running" })],
      NOW,
      1,
    );
    expect(f.lanes[0]?.estimateSecs).toBe(60);
    expect(f.lanes[0]?.bars).toEqual([{ start: NOW, end: NOW + 60, running: true }]);
  });

  it("is empty when nothing fires", () => {
    const f = buildForecast([routine({ schedule: "0 0 1 1 *" })], [], NOW, 2);
    expect(f.lanes).toEqual([]);
    expect(f.peak).toBe(0);
  });
});

describe("overCapWindows", () => {
  it("merges adjacent over-cap spans and keeps the peak", () => {
    const spans = [
      { from: 0, to: 10, count: 3 },
      { from: 10, to: 20, count: 4 },
      { from: 20, to: 30, count: 1 },
      { from: 30, to: 40, count: 3 },
    ];
    const w = overCapWindows(spans, [], 2);
    expect(w.map(({ from, to, peak }) => [from, to, peak])).toEqual([
      [0, 20, 4],
      [30, 40, 3],
    ]);
  });
});

const ROUTINES = [
  routine({ schedule: "0 13 * * *" }),
  routine({ id: "b", title: "Beta", schedule: "30 13 * * *" }),
];
const RUNS = [
  run({ finished_at: 7_200 }),
  run({ routine_id: "b", started_at: NOW - 60, finished_at: null, status: "running" }),
];

function renderForecast(cap: number | undefined) {
  return render(<CapacityForecast forecast={buildForecast(ROUTINES, RUNS, NOW, cap ?? 0)} cap={cap} />);
}

describe("CapacityForecast", () => {
  it("renders nothing when nothing is projected", () => {
    const { container } = render(<CapacityForecast forecast={buildForecast([], [], NOW, 1)} cap={1} />);
    expect(container).toBeEmptyDOMElement();
  });

  it("draws lanes with median/default estimates and lists over-cap windows", () => {
    renderForecast(1);
    expect(screen.getByText(/projected peak 2 \/ cap 1 — 1 over-cap window$/)).toHaveClass("c-amber");
    expect(screen.getByRole("img", { name: /Alpha: fires .*, ~2h 0m median/ })).toHaveClass("runs-gantt-bar");
    expect(screen.getByRole("img", { name: /Beta: running now, ~10m \(default, no history\)/ })).toHaveClass("running");
    expect(screen.getByRole("img", { name: /Beta: fires .*default/ })).toHaveClass("est");
    expect(screen.getByRole("list", { name: "Over-cap windows" })).toHaveTextContent("13:30–13:40 · 2 projected Alpha, Beta");
    expect(document.querySelector(".runs-gantt-cap")).not.toBeNull();
    expect(document.querySelector(".runs-gantt-level.over")).not.toBeNull();
  });

  it("labels an unbounded and a loading cap without warnings", () => {
    const { unmount } = renderForecast(0);
    expect(screen.getByText(/projected peak 2 \(no cap\)$/)).not.toHaveClass("c-amber");
    expect(screen.queryByRole("list")).toBeNull();
    unmount();
    renderForecast(undefined);
    expect(screen.getByText(/^projected peak 2$/)).toBeInTheDocument();
    expect(document.querySelector(".runs-gantt-cap")).toBeNull();
  });

  it("pluralizes multiple over-cap windows", () => {
    const routines = [...ROUTINES, routine({ id: "c", title: "Gamma", schedule: "30 13,20 * * *" })];
    const runs = [run({ finished_at: 7_200 }), run({ routine_id: "b", finished_at: 36_000 })];
    render(<CapacityForecast forecast={buildForecast(routines, runs, NOW, 1)} cap={1} />);
    expect(screen.getByText(/over-cap windows$/)).toBeInTheDocument();
  });
});