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 & B

Guide 🚀

${'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, /duckduckgo\.com\/\?q=/); 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']]); });