My Voices - Top bar with search, filter toggle (active-count badge), synth-source drawer, group and table buttons; filters/sort in a collapsible labelled panel; one-line sort header with select-all/none checkbox - Groups chip list is a collapsed accordion; bottom bar is Refresh/Sync/Import plus one Actions (N) menu (Edit / Audio / Share / Danger), disabled until voices are checked; touch targets stay 44px - Copy selected works again (copyText was no longer exported), shows "N voice names copied to clipboard" and falls back to a copy dialog when the async clipboard is unavailable TTS / engines - Health probe works for /v1 card URLs and, behind a LiteLLM gateway, checks the selected model's own upstream so stopped containers are not reported as available - Unhiding a voice publishes a missing entry to the engine's voices.json (no more silent default-voice fallback) - Status-bar TTS picker also drives the My Voices and STT-test selectors; Use as TTS refreshes the status bar Includes the engine connect/disconnect, custom TTS, STT footer and magazine auth work that was already in the working tree, with their tests. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
325 lines
20 KiB
JavaScript
325 lines
20 KiB
JavaScript
const { test, expect } = require('./_guard');
|
|
const path = require('node:path');
|
|
const { readFileSync } = require('node:fs');
|
|
|
|
for (const [role, settingKey, modelKey] of [['llm', 'llm_url', 'llm_model'], ['ocr', 'vision_ocr_url', 'vision_ocr_model']]) {
|
|
test(`${role} active panel shows persisted disconnect and restores models on reconnect`, async ({ page }) => {
|
|
let discoveries = 0;
|
|
await page.route(url => url.pathname === '/', route => route.fulfill({ contentType: 'text/html', body: '<!doctype html><html><body></body></html>' }));
|
|
await page.route('**/api/**', route => {
|
|
discoveries++;
|
|
return route.fulfill({ json: { ok: true, models: ['saved-model'] } });
|
|
});
|
|
await page.goto('/');
|
|
await page.setContent(`<select id="${role}-active-url"><option value="http://engine/v1">Engine</option></select><select id="${role}-active-model"></select><span id="${role}-active-status"></span><button id="${role}-active-refresh">Refresh</button>`);
|
|
await page.evaluate(({ settingKey, modelKey }) => {
|
|
window.$ = id => document.getElementById(id);
|
|
window.escHtml = value => String(value);
|
|
window._appSettings = { [settingKey]: 'http://engine/v1', [modelKey]: 'saved-model', engine_active: { [settingKey]: 'saved-engine' }, engine_disabled: { 'saved-engine': true } };
|
|
}, { settingKey, modelKey });
|
|
await page.addScriptTag({ path: path.resolve('static/js/ai-backends.js') });
|
|
await expect(page.locator(`#${role}-active-status`)).toHaveText('Disconnected');
|
|
expect(discoveries).toBe(0);
|
|
await page.evaluate(role => {
|
|
delete window._appSettings.engine_disabled['saved-engine'];
|
|
window[role === 'llm' ? 'syncActiveLlmPanel' : 'syncActiveOcrPanel']('http://engine/v1');
|
|
}, role);
|
|
await expect(page.locator(`#${role}-active-model`)).toHaveValue('saved-model');
|
|
await expect(page.locator(`#${role}-active-status`)).toHaveText('1 model');
|
|
expect(discoveries).toBe(1);
|
|
let delayedModels;
|
|
await page.route('**/api/engine-models?**', route => { delayedModels = route; });
|
|
await page.evaluate(role => window[role === 'llm' ? 'syncActiveLlmPanel' : 'syncActiveOcrPanel']('http://engine/v1'), role);
|
|
await expect.poll(() => !!delayedModels).toBe(true);
|
|
await expect(page.locator(`#${role}-active-refresh`)).toBeDisabled();
|
|
await page.evaluate(role => {
|
|
window._appSettings.engine_disabled['saved-engine'] = true;
|
|
window[role === 'llm' ? 'syncActiveLlmPanel' : 'syncActiveOcrPanel']('http://engine/v1');
|
|
}, role);
|
|
await delayedModels.fulfill({ json: { ok: true, models: ['stale-model'] } });
|
|
await expect(page.locator(`#${role}-active-refresh`)).toBeEnabled();
|
|
await expect(page.locator(`#${role}-active-status`)).toHaveText('Disconnected');
|
|
await expect(page.locator(`#${role}-active-model option`)).toHaveText('Disconnected');
|
|
});
|
|
}
|
|
|
|
for (const [role, settingKey] of [['stt', 'faster_whisper_url'], ['tts', 'tts_url'], ['llm', 'llm_url'], ['ocr', 'vision_ocr_url']]) {
|
|
test(`${role} cards test repeatedly and activate the chosen model with a saved key`, async ({ page }) => {
|
|
await page.route(url => url.pathname === '/', route => route.fulfill({ contentType: 'text/html', body: '<!doctype html><html><body></body></html>' }));
|
|
const activations = [];
|
|
const connections = [];
|
|
let rejectDisconnect = false;
|
|
const settings = { engine_local_urls: { 'dc-custom-stt': 'http://engine/v1' }, engine_models: { 'custom-stt': 'whisper-b' }, engine_disabled: {}, engine_api_keys: { 'custom-stt': '__configured__' } };
|
|
let probes = 0;
|
|
await page.route('**/api/**', async route => {
|
|
const url = new URL(route.request().url());
|
|
if (url.pathname === '/api/probe-url') {
|
|
probes++;
|
|
expect(url.searchParams.get('name')).toBe('custom-stt');
|
|
return route.fulfill({ json: { ok: true } });
|
|
}
|
|
if (url.pathname.endsWith('/connection')) {
|
|
const { connected } = route.request().postDataJSON();
|
|
if (!connected && rejectDisconnect) return route.fulfill({ status: 500, json: { ok: false, error: 'Save failed' } });
|
|
connections.push(connected);
|
|
if (connected) delete settings.engine_disabled['custom-stt']; else settings.engine_disabled['custom-stt'] = true;
|
|
return route.fulfill({ json: { ok: true, connected } });
|
|
}
|
|
if (url.pathname === '/api/engine-models') {
|
|
expect(url.searchParams.get('type')).toBe(role);
|
|
expect(url.searchParams.get('name')).toBe('custom-stt');
|
|
return route.fulfill({ json: { ok: true, models: ['whisper-a', 'whisper-b'] } });
|
|
}
|
|
if (/\/use-as-(engine|llm)$/.test(url.pathname)) {
|
|
expect(route.request().headers()['x-provider-api-key']).toBe('test-only-key');
|
|
activations.push(route.request().postDataJSON());
|
|
return route.fulfill({ json: { ok: true, key_set: true } });
|
|
}
|
|
return route.fulfill({ json: settings });
|
|
});
|
|
await page.goto('/');
|
|
await page.setContent(`<div data-page="${role}"><div id="dc-grid-${role}"></div></div>`);
|
|
await page.evaluate(() => {
|
|
window.$ = id => document.getElementById(id);
|
|
window._appSettings = { engine_api_keys: { 'custom-stt': '__configured__' } };
|
|
window.escHtml = value => String(value).replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`);
|
|
window.toast = () => {};
|
|
window.initLlmSnippets = () => {};
|
|
window.engineApiKeyValue = () => '';
|
|
window.applyAndSaveSettings = async patch => Object.assign(window._appSettings, patch);
|
|
window.probeUrl = (url, type, apiKey, name) => fetch('/api/probe-url?' + new URLSearchParams({ url, type, name })).then(r => r.json());
|
|
});
|
|
await page.addScriptTag({ path: path.resolve('static/js/engines.js') });
|
|
await page.evaluate(role => renderLocalContainers([{ id: 'custom-stt', name: 'Display name', label: 'Test engine', role, isCustom: true, url: 'http://engine/v1' }]), role);
|
|
await expect(page.locator('.llm-local-name')).toHaveText('Test engine');
|
|
await expect(page.locator('.dc-apikey-inp')).toBeVisible();
|
|
await page.locator('.dc-connect-btn').click();
|
|
await page.locator('.dc-connect-btn').click();
|
|
expect(probes).toBe(2);
|
|
await expect(page.locator('.dc-use-btn')).not.toHaveClass(/active/);
|
|
await page.locator('.dc-models-btn').click();
|
|
await expect(page.locator('datalist option[value="whisper-b"]')).toHaveCount(1);
|
|
await page.locator('.dc-model-inp').fill('whisper-b');
|
|
await page.locator('.dc-model-inp').blur();
|
|
await page.locator('.dc-apikey-inp').fill('test-only-key');
|
|
await page.locator('.dc-use-btn').click();
|
|
await expect.poll(() => activations.length).toBe(1);
|
|
expect(activations[0]).toEqual(role === 'llm'
|
|
? { url: 'http://engine/v1', model: 'whisper-b' }
|
|
: { url: 'http://engine/v1', role, setting_key: settingKey, model: 'whisper-b', activate: true });
|
|
await expect(page.locator('.dc-disconnect-btn')).toBeVisible();
|
|
rejectDisconnect = true;
|
|
await page.locator('.dc-disconnect-btn').click();
|
|
await expect(page.locator('.dc-disconnect-btn')).toBeVisible();
|
|
expect(settings.engine_disabled).toEqual({});
|
|
rejectDisconnect = false;
|
|
await page.locator('.dc-disconnect-btn').click();
|
|
await expect(page.locator('.dc-disconnect-btn')).toBeHidden();
|
|
await expect(page.locator('.dc-use-btn')).not.toHaveClass(/active/);
|
|
expect(settings.engine_disabled).toEqual({ 'custom-stt': true });
|
|
expect(settings.engine_models['custom-stt']).toBe('whisper-b');
|
|
expect(settings.engine_api_keys['custom-stt']).toBe('__configured__');
|
|
// A stale browser flag cannot reconnect an engine disabled in saved settings.
|
|
await page.evaluate(() => localStorage.setItem('dc-con-custom-stt', '1'));
|
|
await page.evaluate(role => renderLocalContainers([{ id: 'custom-stt', name: 'Renamed display label', role, isCustom: true, url: 'http://engine/v1' }]), role);
|
|
await expect(page.locator('.dc-disconnect-btn')).toBeHidden();
|
|
await expect(page.locator('.dc-model-inp')).toHaveValue('whisper-b');
|
|
await expect(page.locator('.dc-url-inp')).toHaveValue('http://engine/v1');
|
|
await page.locator('.dc-connect-btn').click();
|
|
await expect(page.locator('.dc-disconnect-btn')).toBeVisible();
|
|
expect(settings.engine_disabled).toEqual({});
|
|
expect(connections).toEqual([true, true, false, true]);
|
|
await page.route('**/api/probe-url?**', route => route.fulfill({ json: { ok: false, error: 'Denied' } }));
|
|
await page.locator('.dc-connect-btn').click();
|
|
await expect(page.locator('.dc-connect-btn')).not.toHaveClass(/\bok\b/);
|
|
await page.route('**/api/engine-models?**', route => route.fulfill({ json: { ok: false, error: 'Denied' } }));
|
|
await page.locator('.dc-models-btn').click();
|
|
await expect(page.locator('.dc-model-inp')).toHaveValue('whisper-b');
|
|
if (role === 'stt' || role === 'tts') {
|
|
const name = role === 'stt' ? 'nvidia-parakeet-asr' : 'fish-speech-api';
|
|
const setting = role === 'stt' ? 'nvidia_router_url' : 'fishspeech_url';
|
|
await page.evaluate(({ name, role }) => renderLocalContainers([{ name, role, port: 9000 }]), { name, role });
|
|
await page.locator('.dc-apikey-inp').fill('test-only-key');
|
|
await page.locator('.dc-model-inp').fill('actual-model-id');
|
|
await page.locator('.dc-use-btn').click();
|
|
await expect.poll(() => activations.length).toBe(2);
|
|
expect(activations[1]).toEqual({ url: 'http://host.docker.internal:9000', role, setting_key: setting, model: 'actual-model-id', activate: true });
|
|
}
|
|
if (role === 'tts') {
|
|
const targets = [
|
|
['tts-faster-qwen3-tts-voiceclone', 'tts_url'],
|
|
['tts-faster-qwen3-tts-voicedesign', 'voice_design_url'],
|
|
['tts-faster-qwen3-tts-customvoice', 'customvoice_url'],
|
|
['tts-faster-qwen3-tts-streaming', 'tts_stream_url'],
|
|
['fish-speech-api', 'fishspeech_url'], ['kokoro-tts', 'kokoro_url'],
|
|
['vibevoice-msft-tts', 'vibevoice_url'], ['nvidia-zeroshot-nim', 'nvidia_zeroshot_url'],
|
|
['nvidia-flow-nim', 'nvidia_flow_url'], ['xtts-dgx-spark', 'xtts_url'],
|
|
];
|
|
await page.evaluate(targets => renderLocalContainers(targets.map(([name]) => ({ name, role: 'tts', port: 9000 }))), targets);
|
|
expect((await page.locator('.dc-use-btn').allTextContents()).map(text => text.trim())).toEqual(targets.map(() => 'Use as TTS'));
|
|
expect(await page.locator('.dc-use-btn').evaluateAll(buttons => buttons.map(button => button.dataset.dcUseKey))).toEqual(targets.map(([, key]) => key));
|
|
}
|
|
});
|
|
}
|
|
|
|
test('custom editor probes, saves credentials separately, edits inline by stable ID and keeps failed saves open', async ({ page }) => {
|
|
await page.route(url => url.pathname === '/', route => route.fulfill({ contentType: 'text/html', body: '<!doctype html><html><body></body></html>' }));
|
|
let settings = { custom_engine_cards: [], engine_models: {}, engine_api_keys: {} };
|
|
let rejectSave = false;
|
|
let delaySave = false;
|
|
let pendingSave;
|
|
const writes = [];
|
|
const checks = [];
|
|
const activations = [];
|
|
await page.route('**/api/**', async route => {
|
|
const request = route.request();
|
|
const url = new URL(request.url());
|
|
if (/\/use-as-(llm|engine)$/.test(url.pathname)) {
|
|
activations.push({ path: url.pathname, body: request.postDataJSON() });
|
|
return route.fulfill({ json: { ok: true } });
|
|
}
|
|
if (url.pathname === '/api/settings' && request.method() === 'POST') {
|
|
const body = request.postDataJSON();
|
|
writes.push(body);
|
|
if (delaySave) { pendingSave = route; return; }
|
|
if (rejectSave) return route.fulfill({ status: 500, json: { error: 'Save failed' } });
|
|
settings = { ...settings, ...body, engine_api_keys: { ...settings.engine_api_keys } };
|
|
for (const key of Object.keys(body.engine_api_keys || {})) settings.engine_api_keys[key] = '__configured__';
|
|
return route.fulfill({ json: { ok: true } });
|
|
}
|
|
if (['/api/probe-url', '/api/engine-models'].includes(url.pathname)) {
|
|
checks.push({ type: url.searchParams.get('type'), name: url.searchParams.get('name'), typed: request.headers()['x-provider-api-key'] });
|
|
return route.fulfill({ json: { ok: true, models: ['discovered-model'] } });
|
|
}
|
|
return route.fulfill({ json: settings });
|
|
});
|
|
await page.goto('/');
|
|
const section = readFileSync(path.resolve('static/sections/s-llms.html'), 'utf8');
|
|
await page.setContent(['stt', 'tts', 'llm', 'ocr'].map(role => `<div data-page="${role}"><div id="dc-grid-${role}"></div></div>`).join('') + section.slice(section.indexOf('<dialog'), section.indexOf('</dialog>') + 9));
|
|
await page.addStyleTag({ path: path.resolve('static/style.css') });
|
|
await page.evaluate(() => {
|
|
window.$ = id => document.getElementById(id);
|
|
window._appSettings = { custom_engine_cards: [], engine_models: {}, engine_api_keys: {} };
|
|
window.escHtml = value => String(value).replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`);
|
|
window.toast = () => {};
|
|
window.initLlmSnippets = () => {};
|
|
});
|
|
await page.addScriptTag({ path: path.resolve('static/js/engines.js') });
|
|
await page.evaluate(() => openAddEngineDialog('stt'));
|
|
await page.locator('#aed-name').fill('Example STT');
|
|
await page.locator('#aed-url').fill('http://engine/v1');
|
|
await page.locator('#aed-api-key').fill('test-only-key');
|
|
for (const invalid of ['[1]', '{', '{"nested":{"api_key":"test-only-key"}}', '{"headers":{"x-api-key":"test-only-key"}}', '{"llm_api_key":"test-only-key"}', '{"BearerToken":"test-only-key"}']) {
|
|
await page.locator('#aed-params').fill(invalid);
|
|
await page.locator('#aed-save').click();
|
|
await page.locator('#aed-test').click();
|
|
expect(writes).toHaveLength(0);
|
|
expect(checks).toHaveLength(0);
|
|
await expect(page.locator('#add-engine-dlg')).toBeVisible();
|
|
}
|
|
await page.locator('#aed-params').fill('{"temperature":0.7,"max_tokens":50}');
|
|
await page.locator('#aed-active').check();
|
|
await page.locator('#aed-test').click();
|
|
await page.locator('#aed-load-models').click();
|
|
await expect(page.locator('#aed-models option')).toHaveAttribute('value', 'discovered-model');
|
|
await page.locator('#aed-model').fill('manual-model');
|
|
await page.locator('#aed-save').click();
|
|
await expect(page.locator('#add-engine-dlg')).not.toBeVisible();
|
|
expect(writes).toHaveLength(1);
|
|
const id = writes[0].custom_engine_cards[0].id;
|
|
expect(writes[0].engine_api_keys).toEqual({ [id]: 'test-only-key' });
|
|
expect(writes[0].engine_models[id]).toBe('manual-model');
|
|
expect(writes[0].custom_engine_cards[0].params).toEqual({ temperature: 0.7, max_tokens: 50 });
|
|
await expect.poll(() => activations.length).toBe(1);
|
|
expect(activations[0]).toEqual({ path: `/api/local-containers/${id}/use-as-engine`, body: { url: 'http://engine/v1', model: 'manual-model', role: 'stt', activate: true, setting_key: 'faster_whisper_url' } });
|
|
expect(checks).toEqual([{ type: 'stt', name: id, typed: 'test-only-key' }, { type: 'stt', name: id, typed: 'test-only-key' }]);
|
|
expect(JSON.stringify(writes[0].custom_engine_cards)).not.toContain('test-only-key');
|
|
expect(await page.evaluate(() => localStorage.getItem('engines-custom-cards'))).not.toContain('test-only-key');
|
|
await expect(page.locator('#dc-grid-stt .dc-edit-btn')).toBeVisible();
|
|
await page.locator('#dc-grid-stt .dc-edit-btn').click();
|
|
await expect(page.locator('#add-engine-dlg')).not.toBeVisible();
|
|
await expect(page.locator('#dc-grid-stt .add-engine-form')).toBeVisible();
|
|
await page.locator('#aed-name').fill('Cancelled change');
|
|
await page.locator('#aed-api-key').fill('discarded-test-key');
|
|
await page.locator('#aed-cancel').click();
|
|
await expect(page.locator('#dc-grid-stt .llm-local-name')).toHaveText('Example STT');
|
|
await expect(page.locator('#dc-grid-stt .dc-edit-btn')).toBeFocused();
|
|
await page.locator('#dc-grid-stt .dc-url-inp').fill('http://visible-override/v1');
|
|
await page.locator('#dc-grid-stt .dc-container-name-inp').fill('visible-container');
|
|
await page.locator('#dc-grid-stt .dc-edit-btn').click();
|
|
await expect(page.locator('#aed-url')).toHaveValue('http://visible-override/v1');
|
|
await expect(page.locator('#aed-container')).toHaveValue('visible-container');
|
|
await expect(page.locator('#aed-name')).toHaveValue('Example STT');
|
|
await expect(page.locator('#aed-api-key')).toHaveValue('');
|
|
await expect(page.locator('#aed-api-key')).toHaveAttribute('placeholder', /Configured/);
|
|
await expect(page.locator('#aed-model')).toHaveValue('manual-model');
|
|
await expect(page.locator('#aed-params')).toHaveValue(/"temperature": 0.7/);
|
|
await page.locator('#aed-active').uncheck();
|
|
await page.locator('#aed-name').fill('Renamed STT');
|
|
await page.locator('#aed-url').fill('http://changed-engine/v1');
|
|
let delayedModels;
|
|
await page.route('**/api/engine-models?**', route => { delayedModels = route; });
|
|
await page.locator('#aed-load-models').click();
|
|
await expect.poll(() => !!delayedModels).toBe(true);
|
|
await page.locator('#aed-url').fill('http://new-engine/v1');
|
|
await delayedModels.fulfill({ json: { ok: true, models: ['stale-model'] } });
|
|
await expect(page.locator('#aed-load-models')).toBeEnabled();
|
|
await expect(page.locator('#aed-models option')).toHaveCount(0);
|
|
delaySave = true;
|
|
await page.locator('#aed-save').click();
|
|
await expect.poll(() => !!pendingSave).toBe(true);
|
|
await expect(page.locator('#aed-cancel')).toBeDisabled();
|
|
await page.evaluate(() => {
|
|
openAddEngineDialog('tts');
|
|
editCustomEngineCard(window._appSettings.custom_engine_cards[0]);
|
|
renderLocalContainers([]);
|
|
loadLocalContainers();
|
|
});
|
|
await expect(page.locator('#aed-name')).toHaveValue('Renamed STT');
|
|
await expect(page.locator('#dc-grid-stt .add-engine-form')).toBeVisible();
|
|
delaySave = false;
|
|
await pendingSave.fulfill({ status: 500, json: { error: 'Save failed' } });
|
|
await expect(page.locator('#aed-save')).toBeEnabled();
|
|
await expect(page.locator('#aed-cancel')).toBeEnabled();
|
|
await expect(page.locator('#dc-grid-stt .add-engine-form')).toBeVisible();
|
|
await expect(page.locator('#aed-name')).toHaveValue('Renamed STT');
|
|
await expect(page.locator('#aed-url')).toHaveValue('http://new-engine/v1');
|
|
rejectSave = false;
|
|
await page.route('**/api/settings', route => route.request().method() === 'GET' ? route.abort() : route.fallback());
|
|
await page.locator('#aed-save').click();
|
|
await expect(page.locator('#add-engine-dlg')).not.toBeVisible();
|
|
await expect(page.locator('#dc-grid-stt .llm-local-name')).toHaveText('Renamed STT');
|
|
expect(writes.at(-1).custom_engine_cards[0].id).toBe(id);
|
|
expect(writes.at(-1)).not.toHaveProperty('engine_api_keys');
|
|
expect(writes.at(-1).engine_local_urls['dc-' + id]).toBe('http://new-engine/v1');
|
|
expect(await page.evaluate(id => localStorage.getItem('dc-url-' + id), id)).toBe('http://new-engine/v1');
|
|
await page.route('**/api/local-containers/*/use-as-llm', route => {
|
|
activations.push({ path: new URL(route.request().url()).pathname, body: route.request().postDataJSON() });
|
|
return route.fulfill({ status: 400, json: { ok: false, error: 'Denied' } });
|
|
});
|
|
await page.evaluate(() => {
|
|
window.engineTestMessages = [];
|
|
window.toast = message => window.engineTestMessages.push(message);
|
|
editCustomEngineCard(window._appSettings.custom_engine_cards[0]);
|
|
});
|
|
await page.locator('#aed-role').selectOption('llm');
|
|
await page.locator('#aed-active').check();
|
|
const savesBeforeActivation = writes.length;
|
|
await page.locator('#aed-save').click();
|
|
await expect(page.locator('#add-engine-dlg')).not.toBeVisible();
|
|
await expect.poll(() => page.evaluate(() => window.engineTestMessages.some(message => message.includes('saved; activation failed')))).toBe(true);
|
|
await expect(page.locator('#dc-grid-llm .llm-local-name')).toHaveText('Renamed STT');
|
|
await expect(page.locator('#dc-grid-stt .llm-local-card')).toHaveCount(0);
|
|
for (const role of ['ocr', 'tts', 'stt']) {
|
|
await page.evaluate(() => editCustomEngineCard(window._appSettings.custom_engine_cards[0]));
|
|
await expect(page.locator('.dc-editing .add-engine-form')).toBeVisible();
|
|
expect(await page.locator('.add-engine-form').evaluate(form => form.getBoundingClientRect().right <= innerWidth)).toBe(true);
|
|
await page.locator('#aed-role').selectOption(role);
|
|
await page.locator('#aed-active').uncheck();
|
|
await page.locator('#aed-save').click();
|
|
await expect(page.locator(`#dc-grid-${role} .dc-edit-btn`)).toBeVisible();
|
|
}
|
|
expect(writes).toHaveLength(savesBeforeActivation + 4);
|
|
expect(activations.at(-1)).toEqual({ path: `/api/local-containers/${id}/use-as-llm`, body: { url: 'http://new-engine/v1', model: 'manual-model' } });
|
|
});
|