moadim 3.9.0

Loop engine for AI agents — routines over REST, MCP, and a built-in web UI
import { fireEvent, render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { FleetRunSummary } from "../../api/hooks";
import {
  computeSlo,
  DEFAULT_SLO_TARGET,
  loadSloTarget,
  saveSloTarget,
  SHORT_WINDOW_SECS,
  SLO_WINDOW_SECS,
  targetLabel,
} from "./sloMath";
import { SloBudget } from "./SloBudget";

const NOW = 10 * SLO_WINDOW_SECS;
const HOUR = 3600;

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

/** `statuses` on consecutive hours, starting `fromAgoHours` hours before NOW. */
function hourly(id: string, fromAgoHours: number, statuses: FleetRunSummary["status"][]) {
  return statuses.map((s, i) => run(id, (fromAgoHours + i) * HOUR, s));
}

const ok = (n: number) => Array<FleetRunSummary["status"]>(n).fill("success");

describe("computeSlo", () => {
  it("is empty with no runs", () => {
    expect(computeSlo([], NOW, 0.95, 300)).toEqual({ budgets: [], exhaustedCount: 0, partial: false });
  });

  it("derives budgets and sorts worst status first", () => {
    const runs = [
      // ok: 20 runs, 0 failures, all old (no 24h burn)
      ...hourly("ok", 48, ok(20)),
      // risk: 100 runs, 4 of 5 allowed failures spent, all outside 24h
      ...hourly("risk", 48, [...ok(96), "failed", "failed", "failed", "failed"]),
      // fast: 40 old successes, then 1 fail of 4 in the last 24h
      ...hourly("fast", 48, ok(40)),
      ...hourly("fast", 1, ["failed", ...ok(3)]),
      // gone: 2 of 10 failed → overspent
      ...hourly("gone", 30, ["failed", "failed", ...ok(8)]),
      // ignored: running, outside window, and in the future
      run("ok", 1, "running"),
      run("ok", SLO_WINDOW_SECS + 1, "failed"),
      run("ok", -10, "failed"),
    ];
    const r = computeSlo(runs, NOW, 0.95, 300);
    expect(r.budgets.map((b) => [b.routineId, b.status])).toEqual([
      ["gone", "exhausted"],
      ["fast", "fast-burn"],
      ["risk", "at-risk"],
      ["ok", "ok"],
    ]);
    expect(r.exhaustedCount).toBe(1);
    const [gone, fast, risk, okB] = r.budgets;
    expect(gone?.allowed).toBeCloseTo(0.5);
    expect(gone?.budgetLeft).toBeCloseTo(-3);
    expect(gone?.burn7d).toBeCloseTo(4);
    expect(fast?.burn24h).toBeCloseTo(5);
    expect(risk?.budgetLeft).toBeCloseTo(0.2);
    expect(risk?.status).toBe("at-risk");
    expect(okB).toMatchObject({ runs: 20, failures: 0, budgetLeft: 1, burn7d: 0, burn24h: null });
  });

  it("does not call a clean routine exhausted and breaks ties by title", () => {
    const runs = [...hourly("b", 2, ok(2)), ...hourly("a", 2, ok(2))];
    const r = computeSlo(runs, NOW, 0.999, 300);
    expect(r.budgets.map((b) => [b.routineId, b.status, b.burn24h])).toEqual([
      ["a", "ok", 0],
      ["b", "ok", 0],
    ]);
  });

  it("flags a capped sample that does not reach the window start", () => {
    const runs = hourly("a", 1, ok(3));
    expect(computeSlo(runs, NOW, 0.95, 3).partial).toBe(true);
    expect(computeSlo(runs, NOW, 0.95, 4).partial).toBe(false);
    expect(computeSlo([run("a", SLO_WINDOW_SECS + SHORT_WINDOW_SECS, "success"), ...runs], NOW, 0.95, 4).partial).toBe(
      false,
    );
  });
});

describe("SLO target storage", () => {
  afterEach(() => {
    localStorage.clear();
    vi.restoreAllMocks();
  });

  it("round-trips a known target and defaults otherwise", () => {
    expect(loadSloTarget()).toBe(DEFAULT_SLO_TARGET);
    saveSloTarget(0.99);
    expect(loadSloTarget()).toBe(0.99);
    localStorage.setItem("moadim.client.reliability.slo", "0.5");
    expect(loadSloTarget()).toBe(DEFAULT_SLO_TARGET);
  });

  it("survives unavailable storage", () => {
    vi.spyOn(Storage.prototype, "getItem").mockImplementation(() => {
      throw new Error("blocked");
    });
    vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => {
      throw new Error("blocked");
    });
    expect(() => saveSloTarget(0.9)).not.toThrow();
    expect(loadSloTarget()).toBe(DEFAULT_SLO_TARGET);
  });

  it("labels targets", () => {
    expect(targetLabel(0.95)).toBe("95%");
    expect(targetLabel(0.999)).toBe("99.9%");
  });
});

describe("SloBudget", () => {
  const renderPanel = (runs: FleetRunSummary[], fetchLimit = 300, onTargetChange = vi.fn()) =>
    render(
      <MemoryRouter>
        <SloBudget report={computeSlo(runs, NOW, 0.95, fetchLimit)} target={0.95} onTargetChange={onTargetChange} />
      </MemoryRouter>,
    );

  it("renders nothing without finished runs", () => {
    const { container } = renderPanel([]);
    expect(container).toBeEmptyDOMElement();
  });

  it("shows budgets, burn rates and changes the target", () => {
    const onTargetChange = vi.fn();
    renderPanel([...hourly("gone", 2, ["failed", "failed", ...ok(8)]), ...hourly("ok", 48, ok(20))], 22, onTargetChange);
    expect(screen.getByText(/SLO 95% · 7d · 1 exhausted · 1 need attention · window partial/)).toBeInTheDocument();
    expect(screen.getByText("EXHAUSTED")).toBeInTheDocument();
    expect(screen.getAllByText("OK")).toHaveLength(2);
    expect(screen.getByText("2 / 0.5 of 10 runs")).toBeInTheDocument();
    expect(screen.getByText("0 / 1.0 of 20 runs")).toBeInTheDocument();
    expect(screen.getByLabelText("GONE budget left")).toHaveAttribute("value", "0");
    expect(screen.getByText("0%")).toBeInTheDocument();
    expect(screen.queryByText(/-\d+%/)).not.toBeInTheDocument();
    expect(screen.getAllByText("4.0×")).toHaveLength(2);
    expect(screen.getAllByText("—")).toHaveLength(1);
    expect(screen.getByText(/window is incomplete/)).toBeInTheDocument();
    fireEvent.change(screen.getByLabelText(/SLO TARGET/), { target: { value: "0.99" } });
    expect(onTargetChange).toHaveBeenCalledWith(0.99);
  });

  it("formats large allowances without decimals and stays closed when all ok", () => {
    const { container } = renderPanel(ok(200).map((s, i) => run("big", (i + 1) * 1800, s)));
    expect(screen.getByText("0 / 10 of 200 runs")).toBeInTheDocument();
    expect(container.querySelector("details")).not.toHaveAttribute("open");
  });
});