evOo-local/packages/adapters/src/browser-tools.test.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

163 lines
6.0 KiB
TypeScript

import type { AdapterContext, ComputerRef } from "@rakazo/adapter-kit";
import { describe, expect, it } from "vitest";
import {
browserActFromTool,
browserNavigateFromTool,
browserSnapshotFromTool,
parseBrowserActions,
} from "./browser-tools.js";
import { filterPageBrowserTools, PAGE_BROWSER_TOOL_NAMES } from "./executor.js";
import { FakeBrowserProvider } from "./fake-browser.js";
const context: AdapterContext = {
operationId: "1",
traceId: "1",
spaceId: "w",
userId: "u",
signal: new AbortController().signal,
};
const computer: ComputerRef = {
id: "c1",
botId: "b1",
kind: "fake",
providerRef: "c1",
};
describe("browser tools", () => {
it("parses click/fill/type actions and rejects bad batches", () => {
expect(
parseBrowserActions([
{ kind: "fill", ref: "e1", text: "hi" },
{ kind: "click", ref: "e2" },
]),
).toEqual([
{ kind: "fill", ref: "e1", text: "hi" },
{ kind: "click", ref: "e2" },
]);
expect(() => parseBrowserActions([])).toThrow(/at least one/i);
expect(() => parseBrowserActions([{ kind: "click" }])).toThrow(/ref/i);
expect(() => parseBrowserActions([{ kind: "fill", ref: "e1" }])).toThrow(/text/i);
});
it("rejects URL credentials before invoking the browser", async () => {
const browser = new FakeBrowserProvider();
browser.navigateError = new Error("must not invoke browser");
for (const scheme of ["http", "https"]) {
expect(
await browserNavigateFromTool(browser, computer, context, {
url: `${scheme}://example:fake-password@example.test`,
}),
).toMatchObject({ error: expect.stringContaining("credentials") });
}
});
it("navigates, snapshots, and acts through the tool helpers", async () => {
const browser = new FakeBrowserProvider({
pages: {
"https://example.test/x": {
title: "Demo",
html: `<!doctype html><html><head><title>Demo</title></head><body>
<input aria-label="Name" />
<button>Save</button>
</body></html>`,
},
},
});
const navigated = await browserNavigateFromTool(browser, computer, context, {
url: "https://example.test/x",
});
expect(navigated).toMatchObject({ title: "Demo" });
expect("fallback" in navigated && navigated.fallback).toBeFalsy();
const snap = await browserSnapshotFromTool(browser, computer, context, {});
expect(snap).toMatchObject({ title: "Demo" });
expect(String((snap as { tree?: string }).tree)).toMatch(/\[e\d+\]/);
const nameRef = (snap as { elements: Array<{ ref: string; name: string }> }).elements.find(
(el) => el.name.includes("Name"),
)!.ref;
const acted = await browserActFromTool(browser, computer, context, {
actions: [{ kind: "fill", ref: nameRef, text: "Ada" }],
});
expect(acted).toMatchObject({ ok: true, completed: 1 });
});
it("toggles checkboxes via click without undoing activation", async () => {
const browser = new FakeBrowserProvider({
pages: {
"https://example.test/form": {
title: "Form",
html: `<!doctype html><html><head><title>Form</title></head><body>
<label><input type="checkbox" aria-label="Agree" /> Agree</label>
</body></html>`,
},
},
});
await browserNavigateFromTool(browser, computer, context, { url: "https://example.test/form" });
const before = await browserSnapshotFromTool(browser, computer, context, {});
const agree = (
before as { elements: Array<{ ref: string; name: string; value?: string }> }
).elements.find((el) => el.name.includes("Agree"))!;
expect(agree.value).toMatch(/unchecked|false|^$/i);
const acted = await browserActFromTool(browser, computer, context, {
actions: [{ kind: "click", ref: agree.ref }],
});
expect(acted).toMatchObject({ ok: true });
const after = await browserSnapshotFromTool(browser, computer, context, {});
const toggled = (after as { elements: Array<{ name: string; value?: string }> }).elements.find(
(el) => el.name.includes("Agree"),
)!;
expect(toggled.value).toMatch(/checked|true/i);
});
it("falls back to computer_act for link navigation the in-process page cannot load", async () => {
const browser = new FakeBrowserProvider({
pages: {
"https://example.test/start": {
title: "Start",
html: `<!doctype html><html><head><title>Start</title></head><body>
<a href="https://example.test/next">Next</a>
</body></html>`,
},
},
});
await browserNavigateFromTool(browser, computer, context, {
url: "https://example.test/start",
});
const snap = await browserSnapshotFromTool(browser, computer, context, {});
const link = (snap as { elements: Array<{ ref: string; name: string }> }).elements.find((el) =>
el.name.includes("Next"),
)!;
const acted = await browserActFromTool(browser, computer, context, {
actions: [{ kind: "click", ref: link.ref }],
});
expect(acted).toMatchObject({
ok: false,
fallback: "computer_act",
error: expect.stringMatching(/cannot navigate links|computer_act/i),
});
});
it("surfaces computer_act fallback from the tool layer", async () => {
const browser = new FakeBrowserProvider();
browser.forceFallback = "cannot attach to page";
const result = await browserNavigateFromTool(browser, computer, context, {
url: "https://example.test",
});
expect(result).toMatchObject({
fallback: "computer_act",
note: expect.stringMatching(/computer_act/i),
});
});
it("hides page browser tools when the computer is not graphical", () => {
const tools = [...PAGE_BROWSER_TOOL_NAMES].map((name) => ({ name }));
expect(filterPageBrowserTools(tools, true)).toHaveLength(3);
expect(filterPageBrowserTools(tools, false)).toEqual([]);
expect(filterPageBrowserTools([{ name: "computer_act" }, ...tools], false)).toEqual([
{ name: "computer_act" },
]);
});
});