Menu
popagent
publicLatest change 2c7138e7ddf8cb6b5ac0dc1abbe6d76743366c19 - Checkpoint workspace and secret management work by AkurAI Build
import { afterAll, describe, expect, test } from "bun:test";
import { EventEmitter } from "node:events";
import type { ManagedBrowser } from "./browser-sessions";
import { browserRuntime } from "./browser-settings";
import { startServer } from "./server";
const server = startServer(0);
const base = `http://127.0.0.1:${server.port}`;
const threadId = `browser-api-${crypto.randomUUID()}`;
const sessionId = `interactive:${threadId}`;
const mouseEvents: unknown[] = [];
const keyboardEvents: unknown[] = [];
const closedThreads: string[] = [];
const browser: ManagedBrowser = {
getState: async () => ({ currentUrl: "https://example.com", tabs: [{ id: "0", url: "https://example.com", title: "Example" }], activeTabIndex: 0 }),
closeThreadSession: async (id) => { closedThreads.push(id); },
startScreencast: async () => {
const emitter = new EventEmitter() as EventEmitter & { stop: () => Promise<void>; isActive: () => boolean; reconnect: () => Promise<void>; emitUrl: (url: string) => void };
emitter.stop = async () => { emitter.emit("stop", "manual"); };
emitter.isActive = () => true;
emitter.reconnect = async () => undefined;
emitter.emitUrl = (url) => { emitter.emit("url", url); };
setTimeout(() => {
emitter.emit("frame", { data: "ZmFrZQ==", viewport: { width: 100, height: 50 } });
emitter.emit("stop", "manual");
}, 0);
return emitter;
},
injectMouseEvent: async (event) => { mouseEvents.push(event); },
injectKeyboardEvent: async (event) => { keyboardEvents.push(event); },
};
afterAll(async () => {
await browserRuntime.sessions.close(sessionId);
server.stop(true);
});
describe("browser session API", () => {
test("reports local provider health without exposing secrets", async () => {
const response = await fetch(`${base}/api/browser/health`);
expect(response.status).toBe(200);
expect(await response.json()).toEqual(expect.objectContaining({ provider: "AgentBrowser", headless: true, healthy: true }));
});
test("runs an end-to-end provider diagnostic", async () => {
const response = await fetch(`${base}/api/browser/test`, { method: "POST" });
expect(response.status).toBe(200);
expect(await response.json()).toEqual(expect.objectContaining({ url: "https://example.com/", elementCount: expect.any(Number) }));
}, 30_000);
test("lists browser sessions for a chat thread", async () => {
await browserRuntime.sessions.touch(threadId, "interactive", browser);
const response = await fetch(`${base}/api/browser/sessions?threadId=${encodeURIComponent(threadId)}`);
expect(response.status).toBe(200);
expect(await response.json()).toEqual({ sessions: [expect.objectContaining({ id: sessionId, currentUrl: "https://example.com" })] });
});
test("requires takeover before accepting bounded input events", async () => {
const encoded = encodeURIComponent(sessionId);
expect((await fetch(`${base}/api/browser/sessions/${encoded}/input`, {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ kind: "mouse", event: { type: "mouseMoved", x: 10, y: 20 } }),
})).status).toBe(409);
expect((await fetch(`${base}/api/browser/sessions/${encoded}/takeover`, {
method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ enabled: true }),
})).status).toBe(200);
expect((await fetch(`${base}/api/browser/sessions/${encoded}/input`, {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ kind: "mouse", event: { type: "mouseMoved", x: 10, y: 20 } }),
})).status).toBe(204);
expect(mouseEvents).toHaveLength(1);
expect((await fetch(`${base}/api/browser/sessions/${encoded}/input`, {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ kind: "keyboard", event: { type: "keyDown", key: "Enter", code: "Enter" } }),
})).status).toBe(204);
expect(keyboardEvents).toHaveLength(1);
});
test("streams actual screencast frames as server-sent events", async () => {
const response = await fetch(`${base}/api/browser/sessions/${encodeURIComponent(sessionId)}/stream`);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain("text/event-stream");
expect(await response.text()).toContain('"data":"ZmFrZQ=="');
});
test("closes a browser session", async () => {
const response = await fetch(`${base}/api/browser/sessions/${encodeURIComponent(sessionId)}/close`, { method: "POST" });
expect(response.status).toBe(204);
expect(await browserRuntime.sessions.list(threadId)).toEqual([]);
});
test("manages encrypted browser authentication profiles without returning state", async () => {
const name = `Browser profile ${crypto.randomUUID()}`;
const create = await fetch(`${base}/api/browser/profiles`, {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ name, enabled: false, state: { cookies: [], origins: [] } }),
});
expect(create.status).toBe(201);
const profile = await create.json() as { id: string; name: string };
try {
const listed = await (await fetch(`${base}/api/browser/profiles`)).json() as { profiles: unknown[] };
expect(listed.profiles).toContainEqual(expect.objectContaining({ id: profile.id, name }));
expect(JSON.stringify(listed)).not.toContain("cookies");
expect((await fetch(`${base}/api/browser/profiles`, {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ name: "bad", enabled: false, state: { cookies: "invalid" } }),
})).status).toBe(400);
} finally {
expect((await fetch(`${base}/api/browser/profiles/${profile.id}`, { method: "DELETE" })).status).toBe(204);
}
});
test("exposes the contained recording catalog and rejects invalid names", async () => {
const list = await fetch(`${base}/api/browser/recordings`);
expect(list.status).toBe(200);
expect(await list.json()).toEqual({ recordings: expect.any(Array) });
expect((await fetch(`${base}/api/browser/recordings/${encodeURIComponent("../secret.avi")}`)).status).toBe(404);
});
test("manages origin-bound secrets without returning values", async () => {
const name = `browser/password-${crypto.randomUUID()}`;
const value = `credential-${crypto.randomUUID()}`;
const encoded = encodeURIComponent(name);
const create = await fetch(`${base}/api/secrets`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ name, value, allowedOrigin: "https://example.com" }),
});
expect(create.status).toBe(201);
try {
expect(await create.json()).toEqual(expect.objectContaining({
name,
allowedOrigin: "https://example.com",
}));
const listed = await (await fetch(`${base}/api/secrets`)).json() as { secrets: unknown[] };
expect(listed.secrets).toContainEqual(expect.objectContaining({ name, allowedOrigin: "https://example.com" }));
expect(JSON.stringify(listed)).not.toContain(value);
const update = await fetch(`${base}/api/secrets/${encoded}`, {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ allowedOrigin: "https://example.org:443" }),
});
expect(update.status).toBe(200);
expect(await update.json()).toEqual(expect.objectContaining({ allowedOrigin: "https://example.org" }));
expect((await fetch(`${base}/api/secrets`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
name: `${name}-invalid`,
value,
allowedOrigin: "https://example.com/login",
}),
})).status).toBe(400);
} finally {
expect((await fetch(`${base}/api/secrets/${encoded}`, { method: "DELETE" })).status).toBe(204);
}
});
test("closes thread-scoped browsers when a chat is permanently deleted", async () => {
const create = await fetch(`${base}/api/sessions`, {
method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ workspaceId: "default" }),
});
expect(create.status).toBe(201);
const session = await create.json() as { id: string };
await browserRuntime.sessions.touch(session.id, "interactive", browser);
expect((await browserRuntime.sessions.list(session.id))).toHaveLength(1);
expect((await fetch(`${base}/api/sessions/${session.id}`, { method: "DELETE" })).status).toBe(204);
expect(await browserRuntime.sessions.list(session.id)).toEqual([]);
expect(closedThreads).toContain(session.id);
});
});