66 lines
3.2 KiB
JavaScript
66 lines
3.2 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { Agent, acquireQvac, releaseQvac, closeQvac, qvacStatus } from '../daemon/qvac-master.js';
|
|
|
|
test('concurrent master acquisition loads once and failed acquisition does not leak owners', async () => {
|
|
const original = { resources: Agent.engine.resources, load: Agent.engine.load, close: Agent.engine.close };
|
|
let loads = 0;
|
|
Agent.engine.resources = async () => ({ gpus: [{}] });
|
|
Agent.engine.load = async () => { loads++; return { device: 'gpu' }; };
|
|
Agent.engine.close = async () => {};
|
|
try {
|
|
await Promise.all([acquireQvac(), acquireQvac()]);
|
|
assert.equal(loads, 1); assert.equal(qvacStatus().owners, 2);
|
|
releaseQvac(); releaseQvac(); await closeQvac();
|
|
Agent.engine.resources = async () => { throw new Error('discovery failed'); };
|
|
await assert.rejects(acquireQvac(), /discovery failed/);
|
|
assert.equal(qvacStatus().owners, 0);
|
|
Agent.engine.resources = async () => ({ gpus: [{}] });
|
|
await acquireQvac(); assert.equal(loads, 2); releaseQvac();
|
|
} finally { await closeQvac(); Object.assign(Agent.engine, original); }
|
|
});
|
|
|
|
test('concurrent auxiliary loads share one model and failures permit retry', async () => {
|
|
const { loadAuxiliaryModel, unloadAuxiliaryModel } = await import('../daemon/qvac-master.js');
|
|
const original = Agent.engine.ensureInit;
|
|
let loads = 0, unloads = 0, fail = false;
|
|
Agent.engine.ensureInit = async () => ({
|
|
loadModel: async () => { loads++; if (fail) throw new Error('load failed'); return `aux-${loads}`; },
|
|
unloadModel: async () => { unloads++; },
|
|
});
|
|
try {
|
|
const ids = await Promise.all([loadAuxiliaryModel('shared-asr', {}, 'whispercpp-transcription'), loadAuxiliaryModel('shared-asr', {}, 'whispercpp-transcription')]);
|
|
assert.equal(loads, 1);
|
|
assert.equal(ids[0], ids[1]);
|
|
await unloadAuxiliaryModel(ids[0]);
|
|
assert.equal(unloads, 0, 'the second user still owns the shared model');
|
|
await unloadAuxiliaryModel(ids[1]);
|
|
assert.equal(unloads, 1);
|
|
fail = true;
|
|
const failures = await Promise.allSettled([loadAuxiliaryModel('retry-asr'), loadAuxiliaryModel('retry-asr')]);
|
|
assert.ok(failures.every((result) => result.status === 'rejected'));
|
|
assert.equal(loads, 2);
|
|
fail = false;
|
|
await unloadAuxiliaryModel(await loadAuxiliaryModel('retry-asr'));
|
|
assert.equal(loads, 3);
|
|
assert.equal(qvacStatus().auxiliaryModels, 0);
|
|
} finally { Agent.engine.ensureInit = original; }
|
|
});
|
|
|
|
test('Whisper requests GPU through its own context configuration', async () => {
|
|
const { QvacVoiceAdapter } = await import('../daemon/voice-adapters.js');
|
|
const original = Agent.engine.ensureInit;
|
|
let loaded;
|
|
Agent.engine.ensureInit = async () => ({
|
|
loadModel: async (options) => { loaded = options; return 'gpu-asr'; },
|
|
unloadModel: async () => {},
|
|
});
|
|
const adapter = new QvacVoiceAdapter({ role: 'asr' });
|
|
try {
|
|
await adapter.start();
|
|
assert.equal(loaded.modelType, 'whispercpp-transcription');
|
|
assert.equal(loaded.modelConfig.contextParams.use_gpu, true);
|
|
assert.equal(loaded.modelConfig.gpu_layers, undefined);
|
|
} finally { await adapter.stop(); Agent.engine.ensureInit = original; }
|
|
});
|