import test from 'node:test'; import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { createBrowserTools } from '../skills/browser-tools.js'; const require = createRequire(import.meta.url); const engine = require('../vendor/agent-harness/lib/qvac.js'); const sessions = require('../vendor/agent-harness/agent/sessions.js'); const custom = require('../vendor/agent-harness/agent/custom-tools.js'); const loop = require('../vendor/agent-harness/agent/loop.js'); for (const remote of [false, true]) { for (const outcome of ['answer', 'empty', 'error', 'cancel']) { test(`browser reading reaches continuation and recovers (${remote ? 'remote' : 'local'}, ${outcome})`, async (t) => { const root = mkdtempSync(path.join(tmpdir(), 'jarvis-browser-turn-')); const previousRoot = process.env.AGENT_HARNESS_HOME; process.env.AGENT_HARNESS_HOME = root; const original = Object.fromEntries(['complete', 'getLoaded', 'localLoaded', 'remoteActive', 'remoteVision', 'cancel'].map(k => [k, engine[k]])); t.after(() => { Object.assign(engine, original); if (previousRoot === undefined) delete process.env.AGENT_HARNESS_HOME; else process.env.AGENT_HARNESS_HOME = previousRoot; rmSync(root, { recursive: true, force: true }); }); const loaded = () => ({ modelId: 'test-model', friendlyId: 'test-model', ctxSize: 32768 }); engine.getLoaded = engine.localLoaded = loaded; engine.remoteActive = () => remote; engine.remoteVision = () => false; engine.cancel = async () => {}; const meta = sessions.create({ origin: 'jarvis-qvac', cwd: root, hostWorkspace: false, builtinTools: false, model: 'test-model' }); custom.register(meta.id, createBrowserTools({ browser: { call: async () => ({ url: 'https://example.com/article', text: 'The observatory opens at nine. Tickets cost twelve dollars.', images: [{ path: '/tmp/not-needed.jpg' }], }) } })); t.after(() => loop.finishLive(meta.id)); loop.markLive(meta.id); let completions = 0; engine.complete = async (opts) => { completions++; if (completions === 1) return { text: '', toolCalls: [{ id: 'browse1', name: 'browser', arguments: { action: 'navigate', url: 'https://example.com/article' } }] }; assert.equal(opts.webProcess, remote, 'internal answer-only reminder must not change provider'); assert.equal(opts.timeoutMs, 90000); assert.equal(opts.idleMs, 45000); assert.ok(opts.history.some(m => m.role === 'tool' && m.tool_call_id === 'browse1' && m.content.includes('page_text:\nThe observatory opens at nine. Tickets cost twelve dollars.'))); assert.deepEqual(opts.tools, [], 'answer-only budget remains respected'); if (outcome === 'error') throw new Error('inference unavailable'); if (outcome === 'cancel') { loop.cancel(meta.id); return { text: 'Do not deliver cancelled answer.', toolCalls: [] }; } if (completions === 2 || outcome === 'empty') return { text: ' ', thinking: 'Considering the page.', toolCalls: [] }; return { text: 'The observatory opens at nine; tickets are twelve dollars.', toolCalls: [] }; }; const run = loop.runTurn({ session: sessions.load(meta.id), userText: 'Find visiting hours and prices.', jobId: 'browser-test', payload: { voice: true, maxTurns: 4, maxToolRounds: 1 }, emit() {} }); if (outcome === 'empty' || outcome === 'error') { await assert.rejects(run, outcome === 'empty' ? /could not finish processing/ : /inference unavailable/); } else { const result = await run; if (outcome === 'cancel') { assert.equal(result.reason, 'cancelled'); assert.equal(result.text, ''); } else { assert.equal(result.text, 'The observatory opens at nine; tickets are twelve dollars.'); assert.equal(sessions.load(meta.id).history.at(-1).content, result.text); } } assert.equal(completions, ['error', 'cancel'].includes(outcome) ? 2 : 3); }); } }