import { EventEmitter } from 'node:events';
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { BrowserClient, resolveNodeBinary } from '../browser-use/client.js';
import { createBrowserTools } from '../skills/browser-tools.js';
const require = createRequire(import.meta.url);
const web = require('../vendor/agent-harness/agent/web-search.js');
const reader = require('../vendor/agent-harness/agent/web-reader.js');
test('browser-use maps Node builtins so Bare can load the sidecar client', () => {
const pkg = JSON.parse(readFileSync(new URL('../browser-use/package.json', import.meta.url), 'utf8'));
assert.equal(pkg.imports['node:child_process'].bare, 'bare-subprocess');
assert.equal(pkg.imports['node:os'].bare, 'bare-os');
assert.equal(pkg.imports['node:path'].bare, 'bare-path');
});
function mockBackend(handler) {
return { call: (action, payload, timeoutMs) => handler(action, payload, timeoutMs) };
}
function helper() {
const child = new EventEmitter();
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
child.stdin = new EventEmitter();
child.stdin.writable = true;
child.stdin.write = (chunk) => { child.written = String(chunk); return true; };
child.kill = () => { child.killed = true; };
return child;
}
test('reader extracts structured content, relative links, entities, pagination and find', () => {
const html = `
A & BGuide 🚀
${'Useful text. '.repeat(40)}
Next`;
const page = reader.extractPage(html, 'https://example.com/docs/start', { max_chars: 200, find: 'Useful' });
assert.equal(page.title, 'A & B');
assert.equal(page.headings[0].text, 'Guide 🚀');
assert.equal(page.links[0].url, 'https://example.com/next?q=one');
assert.equal(page.metadata.description, 'A guide');
assert.equal(page.next_offset, 200);
assert.equal(page.matches.length, 20);
assert.doesNotMatch(page.text, /navigation|malicious/);
});
test('web search and fetch go through the browser helper instead of HTML scrapers', async () => {
const calls = [];
web.setBrowserBackend(mockBackend(async (action, payload) => {
calls.push([action, payload.engine || payload.url]);
if (action === 'search') return [{ url: 'https://example.com/hit', title: 'Example', snippet: 'Hello', source: payload.engine }];
return { url: payload.url, status: 200, html: 'Readable article
', text: 'Readable article', via: 'browser' };
}));
try {
const hits = await web.runWebSearch('example', { timeoutMs: 200 });
assert.equal(hits[0].url, 'https://example.com/hit');
assert.equal(calls[0][0], 'search');
const page = await web.fetchPage('https://example.com/article', 200);
assert.equal(page.via, 'browser');
assert.match(page.text, /Readable article/);
const google = await web.googleSearchWithFallback('example', 200);
assert.equal(google[0].source, 'google');
} finally {
web.setBrowserBackend(null);
}
});
test('unknown engines and private URLs fail before Chromium starts', async () => {
web.setBrowserBackend(mockBackend(async () => assert.fail('backend should not run')));
try {
const unknown = await web.runWebSearch('example', { engine: 'jina' });
assert.match(String(unknown.error), /unknown engine/);
const blocked = await web.fetchPage('http://127.0.0.1/secret', 200);
assert.match(String(blocked.error), /blocked/);
} finally {
web.setBrowserBackend(null);
}
});
test('search and fetch stop at the overall budget instead of hanging', async () => {
web.setBrowserBackend(mockBackend(() => new Promise(() => {})));
try {
const started = Date.now();
const search = await web.runWebSearch('example', { timeoutMs: 80 });
assert.match(String(search.error), /timed out/i);
assert.ok(Date.now() - started < 500);
const page = await web.webFetch('https://example.com/ip', 40);
assert.match(String(page.error), /timed out/i);
assert.equal(page.url, 'https://example.com/ip');
} finally {
web.setBrowserBackend(null);
}
});
test('a remaining challenge tells the user to finish it in the Jarvis window', async () => {
web.setBrowserBackend(mockBackend(async () => ({
url: 'https://challenge.example/page',
status: 200,
html: 'Just a momentChecking your browser',
text: 'Checking your browser',
challenge: true,
next_action: 'Complete the prompt in the Jarvis browser window, then call the tool again.',
})));
try {
const page = await web.fetchPage('https://challenge.example/page', 200);
assert.equal(page.challenge, true);
assert.match(page.next_action, /Jarvis browser/);
} finally {
web.setBrowserBackend(null);
}
});
test('auto search falls back to Google when DuckDuckGo has no hits', async () => {
const engines = [];
web.setBrowserBackend(mockBackend(async (action, payload) => {
engines.push(payload.engine);
if (payload.engine === 'duckduckgo') return [];
return [{ url: 'https://example.com/g', title: 'From Google', source: 'google' }];
}));
try {
const hits = await web.runWebSearch('example', { timeoutMs: 200 });
assert.deepEqual(engines, ['duckduckgo', 'google']);
assert.equal(hits[0].source, 'google');
} finally {
web.setBrowserBackend(null);
}
});
test('code_search queries GitHub, npm, and MDN through the helper', async () => {
const engines = [];
web.setBrowserBackend(mockBackend(async (_action, payload) => {
engines.push(payload.engine);
return [{ url: 'https://example.com/' + payload.engine, title: payload.engine, source: payload.engine }];
}));
try {
const out = await web.codeSearch('playwright', 200);
assert.deepEqual(engines, ['github', 'npm', 'mdn']);
assert.equal(out.github[0].source, 'github');
assert.equal(out.npm[0].source, 'npm');
assert.equal(out.mdn[0].source, 'mdn');
} finally {
web.setBrowserBackend(null);
}
});
test('browser client maps spawn ENOENT to a Node install hint', async () => {
const child = helper();
const client = new BrowserClient({
lookupBin: false,
spawnImpl: () => {
setImmediate(() => child.emit('error', Object.assign(new Error('no such file or directory'), { code: 'ENOENT' })));
return child;
},
timeoutMs: 80,
});
const result = await client.call('navigate', { url: 'https://example.com' });
assert.match(String(result.error), /Node\.js is not available to jarvisd/);
assert.doesNotMatch(String(result.error), /^no such file or directory$/i);
});
test('browser client explains a missing helper script', async () => {
const node = resolveNodeBinary() || process.execPath;
const client = new BrowserClient({ node, command: '/no/such/jarvis-browser-helper.js' });
const result = await client.call('snapshot', {});
assert.match(String(result.error), /browser helper is missing/);
});
test('browser helper speaks JSON lines and the client times out hung actions', async () => {
const child = helper();
const client = new BrowserClient({ spawnImpl: () => child, timeoutMs: 80 });
const ready = client.ensure();
child.stdout.emit('data', '{"type":"ready","headless":true}\n');
await ready;
const pending = client.call('fetch', { url: 'https://example.com' }, 50);
await new Promise((resolve) => setImmediate(resolve));
assert.match(child.written, /"action":"fetch"/);
child.stdout.emit('data', '{"id":1,"ok":true,"result":{"url":"https://example.com","title":"Example","text":"ok"}}\n');
assert.equal((await pending).title, 'Example');
const hungStarted = Date.now();
const hung = await client.call('snapshot', {}, 40);
assert.match(String(hung.error), /timed out/i);
assert.ok(Date.now() - hungStarted >= 20);
client.close();
assert.equal(child.killed, true);
});
test('the browser gateway forwards snapshot click and type', async () => {
const calls = [];
const tools = createBrowserTools({
browser: { call: async (action, payload) => { calls.push([action, payload.ref || payload.url]); return { ok: true, action, refs: [{ ref: '1', name: 'Next' }] }; } },
});
assert.equal(tools[0].name, 'browser');
assert.equal(tools[0].permission, 'read');
assert.deepEqual(tools[0].parameters.properties.action.enum, ['navigate', 'snapshot', 'click', 'type', 'press', 'scroll', 'wait']);
assert.match(tools[0].description, /same Playwright session/);
assert.match(tools[0].description, /refs change/);
await tools[0].execute({ action: 'snapshot' });
await tools[0].execute({ action: 'click', ref: '1' });
await tools[0].execute({ action: 'navigate', url: 'https://example.com' });
assert.deepEqual(calls, [['snapshot', undefined], ['click', '1'], ['navigate', 'https://example.com']]);
});