Ctrl K
ComunidadGuías…

scripts/testing/cron-routines-ui.e2e.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Real renderer and routes in a disposable fake-engine workspace only.
import { spawn, type ChildProcess } from "node:child_process";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { afterAll, expect, it } from "vitest";
import { resolveAgentBrowserBinary } from "../../server/browser-engine.ts";
import { waitForExit } from "../../server/testing/cleanup.ts";
import type { Routine } from "../../src/lib/routines.ts";
import { runControlOmb } from "../control-omb.ts";
import { UI_TOOLS_DIR } from "./control-omb-ui.ts";

const ROOT = fileURLToPath(new URL("../..", import.meta.url));
const enabled = process.env.OMB_UI_E2E === "1" || Boolean(resolveAgentBrowserBinary({ dataDir: UI_TOOLS_DIR, env: process.env }));
let child: ChildProcess | undefined;
afterAll(() => waitForExit(child, { signal: "SIGINT", graceMs: 30_000 }));

(enabled ? it : it.skip)("creates monthly routines, validates cron, preserves arbitrary expressions and excludes calls", async () => {
  let output = "";
  let stderr = "";
  child = spawn(process.execPath, ["--experimental-strip-types", join(ROOT, "scripts/control-omb.ts"), "ui", "launch"], { cwd: ROOT, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
  child.stdout!.on("data", chunk => { output += String(chunk); });
  child.stderr!.on("data", chunk => { stderr += String(chunk); });
  let fixture: { ui: string; url: string; botId: string; logPath: string };
  await expect.poll(() => {
    if (child!.exitCode !== null) throw new Error(stderr);
    try { fixture = JSON.parse(output); return Boolean(fixture.ui); } catch { return false; }
  }, { timeout: 600_000 }).toBe(true);
  const ui = (verb: string, ...args: string[]) => runControlOmb(["ui", verb, "--ui", fixture.ui, ...args]) as Promise<Record<string, any>>;
  const evaluate = async (js: string) => (await ui("eval", "--js", js)).result;
  const click = async (name: string) => {
    await evaluate(`[...document.querySelectorAll('button')].find(button => button.textContent === ${JSON.stringify(name)})?.scrollIntoView({ block: 'center' })`);
    return ui("click", "--name", name);
  };
  // The shared harness lacks select/fill verbs. Drive native controls through
  // DOM input/change events; all saves and reads still use the real renderer.
  const fill = (selector: string, value: string) => evaluate(`(() => { const field = document.querySelector(${JSON.stringify(selector)}); const prototype = field instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; Object.getOwnPropertyDescriptor(prototype, 'value').set.call(field, ${JSON.stringify(value)}); field.dispatchEvent(new Event('input', { bubbles: true })); return field.value; })()`);
  const select = (label: string, value: string) => evaluate(`(() => { const field = document.querySelector('select[aria-label=${JSON.stringify(label)}]') ?? [...document.querySelectorAll('label')].find(label => label.firstElementChild?.textContent === ${JSON.stringify(label)})?.control; field.value = ${JSON.stringify(value)}; field.dispatchEvent(new Event('change', { bubbles: true })); return field.value; })()`);
  const routines = async (): Promise<Routine[]> => (await fetch(`${fixture.url}/api/routines`).then(response => response.json())).routines;
  const repeatOptions = () => evaluate("[...document.querySelector('select[aria-label=Repeat]').options].map(option => option.value)");
  const openOverlap = () => evaluate(`(() => {
    const field = document.querySelector('select[aria-label="If the previous run is still working"]');
    const details = field.closest('details');
    if (!details.open) details.querySelector('summary').click();
    field.scrollIntoView({ block: 'center' });
    return field.getBoundingClientRect().height > 0;
  })()`);

  const startupBox = `document.querySelector('input[aria-label="Also run when MausBot starts"]')`;

  const title = 'input[placeholder="What should happen?"]';
  const newButton = "[...document.querySelectorAll('main header button')].find(button => button.textContent === 'New')";
  await click("Automations");
  await expect.poll(() => evaluate(`Boolean(${newButton})`)).toBe(true);
  await evaluate(`${newButton}.click()`);
  await click("Set it up yourself"); await click("On a schedule"); await click("Continue");
  await fill(title, "Monthly close");
  await fill('textarea[placeholder="Tell the bot what to do each time"]', "Summarize this fixture only.");
  await click("More options");
  expect(await evaluate(`document.querySelector('${title}').value`)).toBe("Monthly close");
  await click("Call");
  const callOptions = await repeatOptions();
  for (const choice of ["monthly", "yearly", "cron"]) expect(callOptions).not.toContain(choice);
  await click("Routine");
  expect(await evaluate("document.body.innerText")).toContain("Bot’s current setup");
  expect(await evaluate("document.body.innerText")).toContain("Runs the whole job on the bot's cloud desktop");
  expect(await evaluate("document.body.innerText")).toContain("including a self-hosted VPS");
  expect(await repeatOptions()).toEqual(expect.arrayContaining(["monthly", "yearly", "cron"]));
  // A one-time routine has no schedule to run beside, so it isn't offered.
  expect(await evaluate(`${startupBox} === null`)).toBe(true);
  await select("Repeat", "monthly");
  expect(await openOverlap()).toBe(true);
  expect(await evaluate(`${startupBox}.checked`)).toBe(false);
  expect(await evaluate(`(() => { ${startupBox}.click(); return ${startupBox}.checked; })()`)).toBe(true);
  expect(await evaluate('document.querySelector(\'select[aria-label="If the previous run is still working"]\').value')).toBe("skip");
  await select("If the previous run is still working", "queue");
  await select("Day of month", "L");
  await fill('input[type="time"]', "09:00");
  await fill('input[list="routine-time-zones"]', "UTC");
  await expect.poll(() => evaluate('document.querySelectorAll(\'[aria-label="Next scheduled runs"] time\').length')).toBe(3);
  await evaluate('document.querySelector(\'[aria-label="Next scheduled runs"]\').scrollIntoView({ block: "center" })');
  await ui("screenshot", "--out", `${fixture!.logPath}.cron-monthly.png`);
  expect(await openOverlap()).toBe(true);
  await ui("screenshot", "--out", `${fixture!.logPath}.routine-overlap.png`);
  await select("Repeat", "cron");
  expect(await evaluate('document.querySelector(\'input[placeholder="0 9 1 * *"]\').value')).toBe("0 9 L * *");
  await fill('input[placeholder="0 9 1 * *"]', "0 9 31 2 *");
  await expect.poll(() => evaluate('document.querySelector("[role=alert]")?.textContent')).toContain("no future runs");
  expect(await evaluate("[...document.querySelectorAll('[role=dialog] button')].find(button => button.textContent === 'Save').disabled")).toBe(true);
  await select("Repeat", "monthly");
  await click("Save");
  await expect.poll(routines).toHaveLength(1);
  const monthly = (await routines())[0];
  expect(monthly).toMatchObject({ name: "Monthly close", overlap: "queue", runOnStartup: true, prompt: "Summarize this fixture only.", schedule: { type: "cron", expression: "0 9 L * *", timeZone: "UTC" } });
  await click("Monthly close");
  await click("Edit");
  expect(await evaluate("document.querySelector('select[aria-label=Repeat]').value")).toBe("monthly");
  expect(await evaluate('document.querySelector(\'select[aria-label="If the previous run is still working"]\').value')).toBe("queue");
  expect(await openOverlap()).toBe(true);
  expect(await evaluate(`${startupBox}.checked`)).toBe(true);
  await select("If the previous run is still working", "skip");
  await select("Repeat", "yearly");
  await select("Month", "2"); await select("Day of month", "29");
  await click("Save");
  await expect.poll(async () => (await routines()).find(routine => routine.id === monthly.id)?.schedule).toEqual({ type: "cron", expression: "0 9 29 2 *", timeZone: "UTC" });
  expect((await routines()).find(routine => routine.id === monthly.id)?.overlap).toBeUndefined();
  expect((await routines()).find(routine => routine.id === monthly.id)?.runOnStartup).toBe(true);

  // The API models a bot-created arbitrary schedule. Editing only its title
  // must not collapse its ranges, weekdays, or non-local timezone to a preset.
  const customSchedule = { type: "cron", expression: "15 9-17/2 * * 1-5", timeZone: "America/New_York" };
  const response = await fetch(`${fixture.url}/api/routines`, { method: "POST", headers: { "content-type": "application/json", origin: fixture.url }, body: JSON.stringify({ name: "Business-hours report", prompt: "Use only the fixture.", botId: monthly.botId, enabled: false, runOn: "maus", schedule: customSchedule }) });
  expect(response.status).toBe(201);
  const custom = (await response.json()).routine;
  await expect.poll(() => evaluate(`document.querySelector('li[data-automation="${custom.id}"]')?.dataset.automationStatus`)).toBe("paused");
  await click("Business-hours report");
  await click("Edit");
  expect(await evaluate("document.querySelector('select[aria-label=Repeat]').value")).toBe("cron");
  expect(await evaluate('document.querySelector(\'input[list="routine-time-zones"]\').value')).toBe("America/New_York");
  await fill(title, "Renamed report");
  await click("Save");
  await expect.poll(async () => (await routines()).find(routine => routine.id === custom.id)?.name).toBe("Renamed report");
  expect((await routines()).find(routine => routine.id === custom.id)?.schedule).toEqual(customSchedule);
  const browserConsole = await ui("console");
  expect((browserConsole.messages as Array<{ type: string }>).filter(message => message.type === "error")).toEqual([]);
  console.log(`Cron UI evidence: ${fixture!.logPath}.cron-monthly.png`);
}, 720_000);