Updates
Rolling release / release (push) Successful in 11m4s

This commit is contained in:
2026-09-14 12:24:05 -04:00
parent 79b65ae851
commit 3c9257ce88
4 changed files with 112 additions and 21 deletions
+70
View File
@@ -0,0 +1,70 @@
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);
});
}
}
+19 -3
View File
@@ -164,15 +164,14 @@ test('harness bridge recovers streamed text when final envelope is empty after a
assert.equal(reply.text, 'Your computer is ready.');
});
test('ask with no spoken text after tools returns to listening without a Reply', async () => {
test('ask with no spoken text reports failure and returns to listening', async () => {
const daemon = new JarvisDaemon();
daemon.harness = { ask: async () => ({ ok: true, text: '', reason: 'stop' }), cancel() {}, close: async () => {} };
daemon.voiceLoop = null;
const replies = [];
daemon.on('Reply', (text) => replies.push(text));
try {
const result = await daemon.ask('status');
assert.equal(result, '');
await assert.rejects(daemon.ask('status'), /no spoken answer/);
assert.deepEqual(replies, []);
assert.equal(daemon.state, 'LISTENING');
} finally {
@@ -345,3 +344,20 @@ test('mute ignores arm and does not return to listening after speech', async ()
assert.equal(daemon.state, 'ARMED');
} finally { await daemon.close(); }
});
for (const empty of [false, true]) {
test(`failed inference keeps conversation listening and reports the failure (empty=${empty})`, async () => {
const daemon = new JarvisDaemon();
daemon.settings = { ...daemon.settings, listeningMode: 'conversation' };
daemon.listenEnabled = true;
daemon.harness.ask = async () => { if (empty) return { text: '' }; throw new Error('inference unavailable'); };
const errors = [];
daemon.on('Error', (code, message) => errors.push([code, message]));
try {
await assert.rejects(daemon.ask('Read the page.'), empty ? /no spoken answer/ : /inference unavailable/);
assert.equal(daemon.state, 'LISTENING');
assert.equal(daemon.voice.state, 'LISTENING');
assert.ok(errors.some(([code]) => code === 'QVAC'));
} finally { await daemon.close(); }
});
}