Ctrl K
ComunidadGuías…

scripts/testing/slack-management-ui.e2e.test.ts

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

Archivo original ↗Preguntar a KiBot
// Real settings in the disposable browser harness; synthetic Admin responses
// remain in this page. No Slack or Admin service is contacted.
import { spawn, type ChildProcess } from "node:child_process";
import { existsSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { afterAll, describe, expect, it } from "vitest";
import { resolveAgentBrowserBinary } from "../../server/browser-engine.ts";
import { waitForExit } from "../../server/testing/cleanup.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 binary = resolveAgentBrowserBinary({ dataDir: UI_TOOLS_DIR, env: process.env });
const forced = process.env.OMB_UI_E2E === "1";
const enabled = forced || Boolean(binary);
const launchTimeout = forced && !binary ? 600_000 : 180_000;
if (!enabled) console.log("skipping Slack settings UI e2e: no agent-browser; set OMB_UI_E2E=1 to install the pinned release");

describe("Slack management in agent settings", () => {
  let child: ChildProcess | undefined;
  afterAll(async () => { await waitForExit(child, { signal: "SIGINT", graceMs: 30_000 }); });

  (enabled ? it : it.skip)("shows the Slack row only when Admin can manage it, and never another agent's link", async () => {
    let output = "";
    let errors = "";
    let info: { ui: string; url: string; botId: string; dataDir: string; logPath: string } | undefined;
    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: Buffer) => { output += String(chunk); });
    child.stderr!.on("data", (chunk: Buffer) => { errors += String(chunk); });
    child.on("error", (error) => { errors += error.message; });
    await expect.poll(() => {
      if (child!.exitCode !== null || child!.signalCode !== null) throw new Error(`UI launcher exited: ${errors}`);
      try { info = JSON.parse(output); return Boolean(info?.ui); } catch { return false; }
    }, { timeout: launchTimeout, interval: 250 }).toBe(true);
    const fixture = info!;
    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 snapshot = async () => (await ui("snapshot")).snapshot as string;
    const click = (name: string) => ui("click", "--name", name);
    const link = () => evaluate("document.querySelector('[data-bot-settings-section=slack] a')?.getAttribute('href') ?? null");
    const row = () => evaluate("Boolean(document.querySelector('[data-bot-settings-section=slack]'))");
    const openProfile = async () => {
      const state = await ui("snapshot", "--interactive");
      const profile = Object.entries(state.refs as Record<string, { role: string; name: string }>)
        .find(([, entry]) => entry.role === "button" && /Open .+ profile/.test(entry.name));
      expect(profile).toBeDefined();
      await ui("click", "--ref", `@${profile![0]}`);
      await expect.poll(snapshot, { timeout: 10_000 }).toContain("Identity");
    };
    // Availability is read when settings open, so each mode reopens them.
    const reopen = async (mode: string) => {
      await click("Close settings");
      await evaluate(`window.slackFixture.mode = ${JSON.stringify(mode)}; true`);
      const before = await evaluate("window.slackFixture.requests.length") as number;
      await openProfile();
      await expect.poll(() => evaluate("window.slackFixture.requests.length"), { timeout: 10_000 }).toBeGreaterThan(before);
    };

    // The real API on this local fixture answers { available: false }: no Slack row at all.
    await openProfile();
    const real = await fetch(`${fixture.url}/api/bots/${fixture.botId}/slack-management`);
    expect(real.status).toBe(200);
    expect(await real.json()).toEqual({ available: false });
    expect(await row()).toBe(false);

    await evaluate(`(() => {
      const realFetch = window.fetch.bind(window);
      window.slackFixture = { mode: 'ready', requests: [], pending: [] };
      window.fetch = (input, init) => {
        const path = input instanceof Request ? input.url : String(input);
        const match = path.match(/\\/api\\/bots\\/([^/]+)\\/slack-management$/);
        if (!match) return realFetch(input, init);
        const state = window.slackFixture;
        const botId = match[1];
        state.requests.push(botId);
        const reply = () => Response.json({ available: true, managementUrl: 'https://admin.example.test/slack?workspace=acme&bot=' + botId });
        if (state.mode === 'pending') return new Promise(resolve => state.pending.push(() => resolve(reply())));
        if (state.mode === 'unavailable') return Promise.resolve(Response.json({ available: false }));
        if (state.mode === 'denied') return Promise.resolve(Response.json({ error: 'forbidden' }, { status: 403 }));
        if (state.mode === 'error') return Promise.resolve(Response.json({ error: 'fixture failure' }, { status: 503 }));
        return Promise.resolve(reply());
      };
      return true;
    })()`);
    await reopen("ready");
    await expect.poll(row, { timeout: 10_000 }).toBe(true);
    await click("Slack");
    await expect.poll(link, { timeout: 10_000 }).toMatch(/^https:\/\/admin\.example\.test\/slack\?workspace=acme&bot=/);
    expect(await snapshot()).toContain("Give this agent its own Slack app, with its own name and picture, so your team can message it directly.");
    expect(await snapshot()).toContain("Manage in Admin");
    expect(await evaluate("document.querySelector('[data-bot-settings-section=slack] a')?.getAttribute('rel')")).toBe("noopener noreferrer");
    expect(await evaluate("document.querySelector('[data-bot-settings-section=slack] a')?.getAttribute('target')")).toBe("_blank");
    // One link and the row's own toggle: no switches, no confirmations.
    expect(await evaluate("document.querySelectorAll('[data-bot-settings-section=slack] a').length")).toBe(1);
    expect(await evaluate("document.querySelectorAll('[data-bot-settings-section=slack] button, [data-bot-settings-section=slack] input').length")).toBe(1);

    // Anything but an available answer removes the row, even though Slack was the open section.
    for (const mode of ["unavailable", "denied", "error"]) {
      await reopen(mode);
      expect(await row(), mode).toBe(false);
      expect(await link(), mode).toBeNull();
    }

    // An old response ignores AbortSignal deliberately: it must not surface
    // after these settings close and another agent's open.
    await reopen("pending");
    await expect.poll(() => evaluate("window.slackFixture.pending.length"), { timeout: 10_000 }).toBe(1);
    expect(await row()).toBe(false);
    await click("Close settings");
    await runControlOmb(["new-bot", "--name", "Slack Juniper", "--url", fixture.url]);
    const saved = await fetch(`${fixture.url}/api/bots?messages=0`).then((response) => response.json()) as { bots: Array<{ id: string; name: string }> };
    const second = saved.bots.find((bot) => bot.name === "Slack Juniper")!;
    expect(second).toBeDefined();
    // Select the known fixture bot through the actual sidebar's accessible row.
    await expect.poll(snapshot, { timeout: 10_000 }).toContain("Slack Juniper");
    const sidebar = await ui("snapshot", "--interactive");
    const sidebarRow = Object.entries(sidebar.refs as Record<string, { role: string; name: string }>)
      .find(([, entry]) => entry.role === "button" && entry.name.includes("Slack Juniper") && !entry.name.startsWith("Open "));
    expect(sidebarRow).toBeDefined();
    await ui("click", "--ref", `@${sidebarRow![0]}`);
    await evaluate("window.slackFixture.mode = 'ready'; true");
    await openProfile();
    await expect.poll(row, { timeout: 10_000 }).toBe(true);
    // Slack is still the open section from the first agent's settings.
    await evaluate("document.querySelector('[data-bot-settings-section=slack] > button[aria-expanded=false]')?.click(); true");
    const expected = `https://admin.example.test/slack?workspace=acme&bot=${second.id}`;
    await expect.poll(link, { timeout: 10_000 }).toBe(expected);
    await evaluate("window.slackFixture.pending.forEach(resolve => resolve()); true");
    expect(await link()).toBe(expected);
    await ui("screenshot", "--out", `${fixture.logPath}.slack-settings.png`);
    console.info(JSON.stringify({ logPath: fixture.logPath, screenshotPath: `${fixture.logPath}.slack-settings.png`, managementUrl: expected }));
    await waitForExit(child, { signal: "SIGINT", graceMs: 30_000 });
    expect(child.exitCode).toBe(0);
    expect(existsSync(fixture.dataDir)).toBe(false);
  }, launchTimeout + 180_000);
});