AkurAI Build
Menu

popagent

public

Latest change 6edc7e928c57dab23c0973ed3f7ee70b3ebb72d5 - Initial popagent baseline by AkurAI Build

import { describe, expect, test } from "bun:test";
import { BrowserSessionRegistry, type ManagedBrowser } from "./browser-sessions";

function fakeBrowser() {
  const closed: string[] = [];
  const browser: ManagedBrowser = {
    getState: async () => ({
      currentUrl: "https://example.com",
      tabs: [{ id: "tab-1", url: "https://example.com", title: "Example" }],
      activeTabIndex: 0,
    }),
    closeThreadSession: async (threadId) => { closed.push(threadId); },
    startScreencast: async () => { throw new Error("not used"); },
    injectMouseEvent: async () => undefined,
    injectKeyboardEvent: async () => undefined,
  };
  return { browser, closed };
}

describe("BrowserSessionRegistry", () => {
  test("tracks thread browser state and activity", async () => {
    let now = 1_000;
    const registry = new BrowserSessionRegistry(() => now);
    const { browser } = fakeBrowser();
    registry.configure({ maxSessions: 4, idleTimeoutMs: 60_000 });
    await registry.touch("thread-1", "interactive", browser);
    now += 500;
    await registry.touch("thread-1", "interactive", browser);
    expect(await registry.list()).toEqual([
      expect.objectContaining({
        id: "interactive:thread-1",
        threadId: "thread-1",
        access: "interactive",
        status: "active",
        currentUrl: "https://example.com",
        lastActivityAt: new Date(now).toISOString(),
      }),
    ]);
  });

  test("closes the least-recent session at the configured limit", async () => {
    let now = 1_000;
    const registry = new BrowserSessionRegistry(() => now);
    registry.configure({ maxSessions: 1, idleTimeoutMs: 60_000 });
    const first = fakeBrowser();
    const second = fakeBrowser();
    await registry.touch("old", "read-only", first.browser);
    now += 1_000;
    await registry.touch("new", "interactive", second.browser);
    expect(first.closed).toEqual(["old"]);
    expect((await registry.list()).map((session) => session.id)).toEqual(["interactive:new"]);
  });

  test("cleans idle sessions and supports explicit close", async () => {
    let now = 1_000;
    const registry = new BrowserSessionRegistry(() => now);
    registry.configure({ maxSessions: 4, idleTimeoutMs: 60_000 });
    const first = fakeBrowser();
    const second = fakeBrowser();
    await registry.touch("idle", "read-only", first.browser);
    now += 61_000;
    await registry.cleanupIdle();
    expect(first.closed).toEqual(["idle"]);
    await registry.touch("manual", "interactive", second.browser);
    expect(await registry.close("interactive:manual")).toBe(true);
    expect(await registry.close("missing")).toBe(false);
    expect(second.closed).toEqual(["manual"]);
  });

  test("denies input injection unless takeover is explicitly enabled", async () => {
    const registry = new BrowserSessionRegistry();
    registry.configure({ maxSessions: 4, idleTimeoutMs: 60_000 });
    const { browser } = fakeBrowser();
    await registry.touch("thread", "interactive", browser);
    await expect(registry.injectMouse("interactive:thread", { type: "mouseMoved", x: 1, y: 2 })).rejects.toThrow("takeover");
    expect(registry.setTakeover("interactive:thread", true)).toBe(true);
    await expect(registry.injectMouse("interactive:thread", { type: "mouseMoved", x: 1, y: 2 })).resolves.toBeUndefined();
  });
});