Files
gnome-jarvis/test/groq-agent.test.js
T
snxraven ac5f18f79d
Rolling release / release (push) Failing after 2m11s
Updates
2026-09-14 10:19:13 -04:00

292 lines
12 KiB
JavaScript

import { mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
process.env.XDG_CONFIG_HOME = mkdtempSync(path.join(tmpdir(), 'jarvis-groq-config-'));
process.env.XDG_DATA_HOME ||= mkdtempSync(path.join(tmpdir(), 'jarvis-groq-data-'));
import test, { afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
import { mkdtemp, writeFile, rm } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { Agent, acquireQvac, releaseQvac, closeQvac, applyAgentInference, qvacStatus } from '../daemon/qvac-master.js';
import { voiceSettings, SETTINGS_FIELDS } from '../daemon/voice-settings.js';
import { JarvisDaemon } from '../daemon/index.js';
import { normalizeSettings, settingVisible, publicSettings } from '../apps/gnome-extension/[email protected]/settings-values.js';
const require = createRequire(import.meta.url);
const groq = require('../vendor/agent-harness/lib/groq.js');
const webProcess = require('../vendor/agent-harness/lib/web-process.js');
const engine = Agent.engine;
function field(key) {
return SETTINGS_FIELDS.find((item) => item.key === key);
}
function resetRemote() {
groq.setHttpsRequest(null);
engine.configureRemote({ provider: 'local', apiKey: '', model: 'openai/gpt-oss-20b' });
}
afterEach(resetRemote);
function mockRequest(handler) {
return (options, cb) => {
const req = new EventEmitter();
req.chunks = [];
req.write = (chunk) => { req.chunks.push(Buffer.from(chunk)); };
req.end = () => handler(options, req, cb);
req.destroy = () => { req.destroyed = true; };
req.setTimeout = () => {};
return req;
};
}
test('Groq settings default to local inference and hide cloud fields', () => {
const settings = voiceSettings({});
assert.equal(settings.agentInference, 'local');
assert.equal(settings.groqApiKey, '');
assert.equal(settings.groqModel, 'openai/gpt-oss-20b');
assert.equal(settingVisible(field('groqApiKey'), settings), false);
assert.equal(settingVisible(field('groqModel'), settings), false);
assert.equal(settingVisible(field('modelProfile'), settings), true);
const groqSettings = { ...settings, agentInference: 'groq' };
assert.equal(settingVisible(field('groqApiKey'), groqSettings), true);
assert.equal(settingVisible(field('modelProfile'), groqSettings), true);
assert.equal(settingVisible(field('groqModel'), groqSettings), true);
const groqModel = field('groqModel');
assert.equal(groqModel.type, 'choice');
assert.deepEqual(groqModel.options.map((option) => option.value), [
'openai/gpt-oss-20b',
'openai/gpt-oss-120b',
'llama-3.1-8b-instant',
'llama-3.3-70b-versatile',
'qwen/qwen3.6-27b',
'qwen/qwen3.8-27b',
'minimaxai/minimax-m2.7',
'openai/gpt-oss-safeguard-20b',
]);
assert.equal(groqModel.options.some((option) => /compound|whisper|orpheus/i.test(option.value)), false);
assert.equal(publicSettings({ groqApiKey: 'gsk_live_secret' }, SETTINGS_FIELDS).groqApiKey, 'set');
assert.equal(normalizeSettings({ groqApiKey: ' gsk_abc ' }, SETTINGS_FIELDS).groqApiKey, 'gsk_abc');
});
test('Groq web processing follows a browser crawl and ignores retired search tools', () => {
assert.equal(webProcess.needsGroqWebProcess([{ role: 'user', content: 'weather' }]), false);
assert.equal(webProcess.needsGroqWebProcess([
{ role: 'user', content: 'weather' },
{ role: 'assistant', content: '', tool_calls: [{ name: 'read_file', arguments: {} }] },
]), false);
assert.equal(webProcess.needsGroqWebProcess([
{ role: 'user', content: 'weather' },
{ role: 'tool', name: 'browser', content: '{"text":"rain"}', tool_call_id: 'call_1' },
]), true);
const rewritten = webProcess.asBrowserCall('web_search', { query: 'weather' });
assert.equal(rewritten.name, 'browser');
assert.equal(rewritten.arguments.action, 'navigate');
assert.equal(rewritten.arguments.url, 'https://duckduckgo.com/?q=weather');
assert.deepEqual(webProcess.browserToolsOnly([
{ name: 'browser' },
{ name: 'read_file' },
]).map((tool) => tool.name), ['browser']);
});
test('Groq maps harness history and tools to OpenAI chat completions', () => {
const tools = groq.toOpenAiTools([
{ name: 'web_search', description: 'Search', parameters: { type: 'object', properties: { query: { type: 'string' } } } },
]);
assert.equal(tools[0].type, 'function');
assert.equal(tools[0].function.name, 'web_search');
const messages = groq.toOpenAiMessages([
{ role: 'system', content: 'You are Jarvis.' },
{ role: 'user', content: 'status', attachments: [{ path: '/tmp/still.jpg' }] },
{ role: 'assistant', content: '', tool_calls: [{ id: 'call_1', name: 'jarvis_status', arguments: {} }] },
{ role: 'tool', name: 'jarvis_status', tool_call_id: 'call_1', content: '{"ok":true}' },
], { vision: false });
assert.equal(messages[1].content, 'status');
assert.equal(messages[2].tool_calls[0].function.name, 'jarvis_status');
assert.equal(messages[3].role, 'tool');
assert.equal(messages[3].tool_call_id, 'call_1');
assert.equal(messages[3].name, 'jarvis_status');
assert.ok(!JSON.stringify(messages).includes('/tmp/still.jpg'));
});
test('Groq vision models attach JPEG stills and text-only models drop them', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'jarvis-groq-vision-'));
const still = path.join(dir, 'still.jpg');
await writeFile(still, Buffer.from('ffd8ffe000104a464946', 'hex'));
try {
const withVision = groq.toOpenAiMessages(
[{ role: 'user', content: 'What do you see?', attachments: [{ path: still }] }],
{ vision: true },
);
assert.equal(withVision[0].content[0].type, 'text');
assert.equal(withVision[0].content[1].type, 'image_url');
assert.match(withVision[0].content[1].image_url.url, /^data:image\/jpeg;base64,/);
assert.equal(groq.visionModel('qwen/qwen3.6-27b'), true);
assert.equal(groq.visionModel('openai/gpt-oss-20b'), false);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
test('Groq requests pin api.groq.com and redact API keys from errors', async () => {
process.env.GROQ_BASE_URL = 'https://evil.example/openai/v1';
let hostname = '';
let auth = '';
groq.setHttpsRequest(mockRequest((options, req, cb) => {
hostname = options.hostname;
auth = options.headers.Authorization;
const res = new EventEmitter();
res.statusCode = 401;
cb(res);
res.emit('data', Buffer.from(JSON.stringify({ error: { message: 'invalid ' + auth } })));
res.emit('end');
}));
try {
await assert.rejects(
groq.complete({
apiKey: 'gsk_live_secret_value',
model: 'openai/gpt-oss-20b',
history: [{ role: 'user', content: 'hi' }],
}),
(error) => {
assert.equal(hostname, 'api.groq.com');
assert.equal(groq.groqHost(), 'api.groq.com');
assert.match(error.message, /Groq HTTP 401/);
assert.doesNotMatch(error.message, /gsk_live_secret_value/);
assert.doesNotMatch(error.message, /Bearer gsk_live_secret_value/);
return true;
},
);
} finally {
delete process.env.GROQ_BASE_URL;
}
});
test('Groq complete streams content and native tool calls', async () => {
groq.setHttpsRequest(mockRequest((options, req, cb) => {
assert.equal(options.hostname, 'api.groq.com');
assert.equal(options.path, '/openai/v1/chat/completions');
const body = JSON.parse(Buffer.concat(req.chunks).toString('utf8'));
assert.equal(body.model, 'openai/gpt-oss-20b');
assert.equal(body.stream, true);
assert.equal(body.parallel_tool_calls, false);
assert.equal(body.reasoning_effort, 'low');
assert.equal(body.tools[0].function.name, 'web_search');
const res = new EventEmitter();
res.statusCode = 200;
cb(res);
const frames = [
'data: {"choices":[{"delta":{"content":"Looking"}}]}\n',
'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_9","function":{"name":"web_search","arguments":"{\\"query\\":\\"news\\"}"}}]}}]}\n',
'data: [DONE]\n',
];
for (const frame of frames) res.emit('data', Buffer.from(frame));
res.emit('end');
}));
const deltas = [];
const result = await groq.complete({
apiKey: 'gsk_test',
model: 'openai/gpt-oss-20b',
history: [{ role: 'user', content: 'news' }],
tools: [{ name: 'web_search', description: 'Search', parameters: { type: 'object', properties: { query: { type: 'string' } } } }],
}, (ev) => deltas.push(ev));
assert.equal(result.text, 'Looking');
assert.equal(result.toolCalls[0].name, 'web_search');
assert.equal(result.toolCalls[0].arguments.query, 'news');
assert.equal(deltas[0].type, 'contentDelta');
});
test('Groq refuses compound models so tools stay on this computer', async () => {
await assert.rejects(
groq.complete({ apiKey: 'gsk_test', model: 'groq/compound', history: [{ role: 'user', content: 'hi' }] }),
/tools stay on this computer/,
);
assert.equal(groq.reasoningEffort('llama-3.3-70b-versatile'), null);
assert.equal(groq.reasoningEffort('qwen/qwen3.6-27b'), 'none');
});
test('Groq thinks only about a browser result and keeps the local chat model', async () => {
const original = {
resources: Agent.engine.resources,
load: Agent.engine.load,
close: Agent.engine.close,
ensureInit: Agent.engine.ensureInit,
};
let loads = 0;
Agent.engine.ensureInit = async () => ({});
Agent.engine.resources = async () => ({ gpus: [{}] });
Agent.engine.load = async () => { loads += 1; return { device: 'gpu', modelId: 'local' }; };
Agent.engine.close = async () => {};
try {
engine.configureRemote({ provider: 'groq', apiKey: '', model: 'openai/gpt-oss-20b' });
await assert.rejects(engine.complete({ history: [{ role: 'user', content: 'hi' }] }), /no model loaded/);
await assert.rejects(
engine.complete({ webProcess: true, history: [{ role: 'tool', name: 'browser', content: 'page' }] }),
/Groq API key/,
);
await acquireQvac();
assert.equal(loads, 1);
assert.equal(qvacStatus().agentInference, 'groq');
assert.equal(engine.getLoaded().device, 'groq');
releaseQvac();
engine.configureRemote({ provider: 'groq', apiKey: 'gsk_test', model: 'openai/gpt-oss-20b' });
groq.setHttpsRequest(mockRequest((_options, req, cb) => {
const body = JSON.parse(Buffer.concat(req.chunks).toString('utf8'));
assert.equal(body.tools.length, 1);
assert.equal(body.tools[0].function.name, 'browser');
assert.match(body.messages.map((m) => m.content).join('\n'), /web-page processing only/);
const res = new EventEmitter();
res.statusCode = 200;
cb(res);
res.emit('data', Buffer.from('data: {"choices":[{"delta":{"content":"ok"}}]}\ndata: [DONE]\n'));
res.emit('end');
}));
const result = await engine.complete({
webProcess: true,
history: [{ role: 'tool', name: 'browser', content: '{"text":"page"}', tool_call_id: 'call_1' }],
tools: [
{ name: 'browser', description: 'Browse', parameters: { type: 'object', properties: {} } },
{ name: 'read_file', description: 'Read', parameters: { type: 'object', properties: {} } },
],
});
assert.equal(result.text, 'ok');
assert.equal(loads, 1);
} finally {
resetRemote();
await closeQvac();
Object.assign(Agent.engine, original);
}
});
test('applyAgentInference honors env keys and runtimeStatus redacts them', async () => {
const previous = process.env.GROQ_API_KEY;
process.env.GROQ_API_KEY = 'gsk_env_secret';
try {
let unloaded = 0;
const previousUnload = Agent.engine.unload;
Agent.engine.unload = async () => { unloaded += 1; };
const applied = applyAgentInference({ agentInference: 'groq', groqApiKey: 'gsk_file', groqModel: 'llama-3.1-8b-instant' });
try {
assert.equal(applied.provider, 'groq');
assert.equal(applied.model, 'llama-3.1-8b-instant');
assert.equal(applied.keySet, true);
await applied.release;
assert.equal(unloaded, 0);
} finally { Agent.engine.unload = previousUnload; }
const daemon = new JarvisDaemon();
daemon.settings = { ...daemon.settings, agentInference: 'groq', groqApiKey: 'gsk_file_secret' };
const status = JSON.parse(daemon.runtimeStatus());
assert.equal(status.agentInference, 'groq');
assert.equal(status.local, false);
assert.equal(status.settings.groqApiKey, 'set');
assert.doesNotMatch(daemon.runtimeStatus(), /gsk_file_secret/);
assert.doesNotMatch(daemon.runtimeStatus(), /gsk_env_secret/);
await daemon.close();
} finally {
if (previous === undefined) delete process.env.GROQ_API_KEY;
else process.env.GROQ_API_KEY = previous;
resetRemote();
}
});