Ctrl K
ComunidadGuías…

server/browser-relay.test.ts

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

Archivo original ↗Preguntar a KiBot
import { readdirSync } from "node:fs";
import { createServer } from "node:http";
import { tmpdir } from "node:os";
import { afterEach, describe, expect, it } from "vitest";

import { RelayPorts, RelayTimeout, captureFrame, relay, relayTarget, typeText } from "./browser-relay.ts";
import { FakeRelay } from "./testing/fake-relay.ts";

const relays: FakeRelay[] = [];
async function fake(session = "bot-test") {
  const value = await new FakeRelay(session).start();
  relays.push(value);
  return value;
}
afterEach(async () => { await Promise.all(relays.splice(0).map((value) => value.close())); });

describe("the engine's loopback command relay", () => {
  it("posts one same-origin command, reports a launch, and refuses redirects", async () => {
    const daemon = await fake();
    daemon.launchNext = true;
    const answer = await relay(daemon.port, { action: "console" }, { timeoutMs: 1_000 });
    expect(answer).toMatchObject({ success: true, launched: true, data: { messages: [] } });
    // The fake answers 403 to a request without the same-origin header, as the engine does.
    expect(daemon.commands).toEqual([{ action: "console" }]);
    const redirecting = createServer((req, res) => {
      if (req.url === "/api/command") { res.writeHead(307, { location: "/elsewhere" }); res.end(); return; }
      res.writeHead(200, { "content-type": "application/json" });
      res.end(JSON.stringify({ success: true, data: { messages: [] } }));
    });
    await new Promise<void>((resolve) => redirecting.listen(0, "127.0.0.1", resolve));
    try {
      await expect(relay((redirecting.address() as { port: number }).port, { action: "console" }, { timeoutMs: 1_000 })).rejects.toThrow();
    } finally { await new Promise((resolve) => redirecting.close(resolve)); }
  });

  it("caps an answer's size and times out a daemon that does not answer", async () => {
    const daemon = await fake();
    daemon.console = Array.from({ length: 2_000 }, (_, index) => ({ type: "log", text: `line ${index} ${"x".repeat(100)}` }));
    await expect(relay(daemon.port, { action: "console" }, { timeoutMs: 1_000, maxBytes: 10_000 })).rejects.toThrow(/too large/);
    daemon.hang.add("errors");
    await expect(relay(daemon.port, { action: "errors" }, { timeoutMs: 100 })).rejects.toBeInstanceOf(RelayTimeout);
  });

  it("finds the port in the daemon's .stream file, and trusts it only for a daemon that names this session", async () => {
    const daemon = await fake("bot-a");
    const ports = new RelayPorts();
    expect(await ports.port(daemon.target)).toBe(daemon.port);
    expect(daemon.actions()).toEqual(["session_info"]);
    expect(await ports.port(daemon.target)).toBe(daemon.port);
    expect(daemon.actions()).toEqual(["session_info"]);
    const other = await fake("bot-b");
    other.writeStreamFile();
    const stranger = new RelayPorts();
    other.sessionName = "bot-someone-else";
    expect(await stranger.port(other.target)).toBeNull();
    daemon.removeStreamFile();
    const before = daemon.commands.length;
    expect(await ports.port(daemon.target)).toBeNull();
    expect(daemon.commands.length).toBe(before);
  });

  it("asks again once the daemon it confirmed has died, so a port another bot's daemon took over is never typed into or pictured", async () => {
    const daemon = await fake("bot-a");
    const alive = new Set([daemon.daemonPid]);
    const ports = new RelayPorts({ alive: (pid) => alive.has(pid) });
    expect(await ports.port(daemon.target)).toBe(daemon.port);
    // bot-a's daemon crashed and left its .stream file; bot-b's daemon now listens on that port.
    alive.delete(daemon.daemonPid);
    daemon.sessionName = "bot-b";
    daemon.daemonPid = 5151;
    alive.add(5151);
    expect(await ports.port(daemon.target)).toBeNull();
    await expect(captureFrame(ports, daemon.target)).rejects.toThrow(/no open page/);
    await expect(typeText(ports, daemon.target, "fake-S3cret!")).rejects.toThrow();
    expect(daemon.typed).toEqual([]);
    expect(daemon.actions().filter((action) => action === "screenshot" || action === "keyboard")).toEqual([]);
  });

  it("takes a daemon too busy to confirm its session as unknown, not absent", async () => {
    const daemon = await fake();
    daemon.hang.add("session_info");
    const ports = new RelayPorts();
    expect(await ports.probe(daemon.target)).toBeNull();
    expect(ports.isTripped(daemon.session)).toBe(true);
  });

  it("probes whether Chrome is connected, and leaves a session alone after a timeout until a probe answers", async () => {
    const daemon = await fake();
    const ports = new RelayPorts();
    expect(await ports.probe(daemon.target)).toEqual({ port: daemon.port, connected: true });
    daemon.connected = false;
    expect(await ports.probe(daemon.target)).toEqual({ port: daemon.port, connected: false });
    daemon.hang.add("stream_status");
    expect(await ports.probe(daemon.target)).toBeNull();
    expect(ports.isTripped(daemon.session)).toBe(true);
    daemon.hang.clear();
    daemon.connected = true;
    expect(await ports.probe(daemon.target)).toEqual({ port: daemon.port, connected: true });
    expect(ports.isTripped(daemon.session)).toBe(false);
  });
});

describe("preview frames over the relay", () => {
  it("returns the picture the daemon wrote, and leaves no file behind", async () => {
    const daemon = await fake();
    const before = readdirSync(tmpdir()).filter((name) => name.startsWith("openmausbot-browser-")).length;
    const frame = await captureFrame(new RelayPorts(), daemon.target);
    expect(frame.format).toBe("jpeg");
    expect(Buffer.from(frame.png, "base64")).toEqual(daemon.picture);
    expect(daemon.commands.find((command) => command.action === "screenshot")).toMatchObject({ format: "jpeg", quality: 80 });
    expect(readdirSync(tmpdir()).filter((name) => name.startsWith("openmausbot-browser-")).length).toBe(before);
  });

  it("never starts a browser: no daemon, or a Chrome that is not connected, gives no frame and sends no capture", async () => {
    const daemon = await fake();
    daemon.connected = false;
    await expect(captureFrame(new RelayPorts(), daemon.target)).rejects.toThrow(/no open page/);
    daemon.removeStreamFile();
    await expect(captureFrame(new RelayPorts(), daemon.target)).rejects.toThrow(/no open page/);
    expect(daemon.actions().every((action) => action === "stream_status" || action === "session_info")).toBe(true);
  });

  it("fails rather than inventing a picture the daemon did not take", async () => {
    const daemon = await fake();
    daemon.picture = null;
    await expect(captureFrame(new RelayPorts(), daemon.target)).rejects.toThrow(/could not be pictured/);
  });

  it("notes a Chrome that died between the probe and the picture and was started again, for the bot's next call", async () => {
    const daemon = await fake();
    const ports = new RelayPorts();
    await captureFrame(ports, daemon.target);
    expect(ports.takeStarted(daemon.session)).toBe(false);
    daemon.launchNext = true; // the screenshot found Chrome gone and the engine started it
    await captureFrame(ports, daemon.target);
    expect(ports.takeStarted(daemon.session)).toBe(true);
    expect(ports.takeStarted(daemon.session)).toBe(false);
    ports.noteStarted(daemon.session);
    ports.forget(daemon.session); // a close or restart: nothing of the old browser carries over
    expect(ports.takeStarted(daemon.session)).toBe(false);
  });
});

describe("a session's relay address", () => {
  it("names the session, keeps its own socket folder, and carries none of the server's credentials", () => {
    process.env.ANTHROPIC_API_KEY_FOR_RELAY_TEST = "fake-provider-key";
    try {
      const target = relayTarget({ command: "/bin/agent-browser", env: { AGENT_BROWSER_SOCKET_DIR: "/tmp/sockets" } }, "bot-a");
      expect(target).toMatchObject({ session: "bot-a", binaryPath: "/bin/agent-browser", env: { AGENT_BROWSER_SESSION: "bot-a", AGENT_BROWSER_SOCKET_DIR: "/tmp/sockets" } });
      expect(JSON.stringify(target)).not.toContain("fake-provider-key");
    } finally { delete process.env.ANTHROPIC_API_KEY_FOR_RELAY_TEST; }
  });
});

describe("typing over the relay", () => {
  it("types the text into the focused field through the session's own relay", async () => {
    const daemon = await fake();
    await typeText(new RelayPorts(), daemon.target, "fake-S3cret!");
    expect(daemon.typed).toEqual(["fake-S3cret!"]);
    expect(daemon.commands).toEqual([{ action: "session_info" }, { action: "keyboard", subaction: "insertText", text: "fake-S3cret!" }]);
  });
});