7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
198 lines
6.5 KiB
JavaScript
198 lines
6.5 KiB
JavaScript
/**
|
|
* Regression tests for processModelData in components/settings.js
|
|
*
|
|
* Issue #3800: LM Studio model dropdown was empty because the frontend's
|
|
* processModelData had hardcoded if-blocks for only ollama_models,
|
|
* openai_models, anthropic_models, and openai_endpoint_models. The fix
|
|
* (PR #3942) replaces the four hardcoded blocks with a generic loop
|
|
* that lifts every <provider>_models array.
|
|
*
|
|
* Strategy
|
|
* --------
|
|
* processModelData is defined inside an IIFE in settings.js and never
|
|
* exposed on window, so we cannot import settings.js and call it. Instead
|
|
* we read the file as text, locate the loop block (the chunk between
|
|
* `if (data.providers) {` and its matching `}`), and execute that exact
|
|
* source against a synthetic `data` object inside a new Function. This
|
|
* tests the literal shipped code, not a re-implementation.
|
|
*
|
|
* If the loop block can't be located (e.g. someone refactors the function
|
|
* shape later), the locator throws and every test fails loudly — that is
|
|
* the intended failure mode, not a fallback.
|
|
*/
|
|
import { readFileSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
|
|
const SETTINGS_PATH = resolve(
|
|
__dirname,
|
|
'../../../src/local_deep_research/web/static/js/components/settings.js',
|
|
);
|
|
|
|
/**
|
|
* Extract the full `if (data.providers) { ... }` statement (guard
|
|
* included) from settings.js by brace-matching. We need the guard so
|
|
* that the missing-providers test exercises the same null-safety as
|
|
* production.
|
|
*/
|
|
function extractProvidersLoop(source) {
|
|
const marker = 'if (data.providers) {';
|
|
const start = source.indexOf(marker);
|
|
if (start < 0) {
|
|
throw new Error(
|
|
'Could not find `if (data.providers) {` in settings.js — ' +
|
|
'the fix may have been refactored. Update this test.',
|
|
);
|
|
}
|
|
const openBrace = start + marker.length - 1; // index of `{`
|
|
let depth = 0;
|
|
let i = openBrace;
|
|
for (; i < source.length; i++) {
|
|
const ch = source[i];
|
|
if (ch === '{') depth++;
|
|
else if (ch === '}') {
|
|
depth--;
|
|
if (depth === 0) break;
|
|
}
|
|
}
|
|
if (depth !== 0) {
|
|
throw new Error('Brace mismatch when extracting providers loop.');
|
|
}
|
|
// Return the whole statement including the `if (...)` guard.
|
|
return source.slice(start, i + 1);
|
|
}
|
|
|
|
const SOURCE = readFileSync(SETTINGS_PATH, 'utf8');
|
|
const LOOP_BODY = extractProvidersLoop(SOURCE);
|
|
|
|
/**
|
|
* Run the extracted loop body against `data`, returning the populated
|
|
* formattedModels array. Mirrors the surrounding processModelData
|
|
* scaffolding (formattedModels declaration, SafeLogger stub).
|
|
*/
|
|
function runLoop(data) {
|
|
const formattedModels = [];
|
|
const SafeLogger = { log: () => {}, warn: () => {}, error: () => {} };
|
|
// The loop body references `data`, `formattedModels`, and `SafeLogger`.
|
|
// eslint-disable-next-line no-new-func
|
|
const fn = new Function(
|
|
'data', 'formattedModels', 'SafeLogger',
|
|
LOOP_BODY,
|
|
);
|
|
fn(data, formattedModels, SafeLogger);
|
|
return formattedModels;
|
|
}
|
|
|
|
describe('processModelData providers loop (issue #3800)', () => {
|
|
it('lifts lmstudio_models tagged as LMSTUDIO (the bug)', () => {
|
|
const data = {
|
|
providers: {
|
|
lmstudio_models: [
|
|
{ value: 'qwen-7b', label: 'qwen-7b' },
|
|
{ value: 'phi-3', label: 'Phi 3' },
|
|
],
|
|
},
|
|
};
|
|
const out = runLoop(data);
|
|
expect(out).toHaveLength(2);
|
|
expect(out[0]).toEqual({
|
|
value: 'qwen-7b',
|
|
label: 'qwen-7b',
|
|
provider: 'LMSTUDIO',
|
|
});
|
|
expect(out[1].provider).toBe('LMSTUDIO');
|
|
});
|
|
|
|
it('lifts llamacpp_models tagged as LLAMACPP', () => {
|
|
const data = {
|
|
providers: {
|
|
llamacpp_models: [
|
|
{ value: 'llama-3-8b.gguf', label: 'Llama 3 8B' },
|
|
],
|
|
},
|
|
};
|
|
const out = runLoop(data);
|
|
expect(out).toEqual([
|
|
{
|
|
value: 'llama-3-8b.gguf',
|
|
label: 'Llama 3 8B',
|
|
provider: 'LLAMACPP',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('still handles ollama_models (backwards compat)', () => {
|
|
const data = {
|
|
providers: {
|
|
ollama_models: [{ value: 'llama3', label: 'Llama 3' }],
|
|
},
|
|
};
|
|
const out = runLoop(data);
|
|
expect(out).toEqual([
|
|
{ value: 'llama3', label: 'Llama 3', provider: 'OLLAMA' },
|
|
]);
|
|
});
|
|
|
|
it('preserves OPENAI_ENDPOINT casing (underscore in tag)', () => {
|
|
const data = {
|
|
providers: {
|
|
openai_endpoint_models: [
|
|
{ value: 'gpt-x', label: 'gpt-x' },
|
|
],
|
|
},
|
|
};
|
|
const out = runLoop(data);
|
|
expect(out[0].provider).toBe('OPENAI_ENDPOINT');
|
|
});
|
|
|
|
it('lifts multiple providers in one response', () => {
|
|
const data = {
|
|
providers: {
|
|
ollama_models: [{ value: 'llama3', label: 'Llama 3' }],
|
|
lmstudio_models: [{ value: 'qwen-7b', label: 'qwen-7b' }],
|
|
openai_models: [{ value: 'gpt-4', label: 'GPT-4' }],
|
|
},
|
|
};
|
|
const out = runLoop(data);
|
|
const providers = out.map(m => m.provider).sort();
|
|
expect(providers).toEqual(['LMSTUDIO', 'OLLAMA', 'OPENAI']);
|
|
expect(out).toHaveLength(3);
|
|
});
|
|
|
|
it('handles empty providers dict without errors', () => {
|
|
const out = runLoop({ providers: {} });
|
|
expect(out).toEqual([]);
|
|
});
|
|
|
|
it('handles missing providers (undefined) without errors', () => {
|
|
const out = runLoop({});
|
|
expect(out).toEqual([]);
|
|
});
|
|
|
|
it('skips empty arrays', () => {
|
|
const out = runLoop({ providers: { lmstudio_models: [] } });
|
|
expect(out).toEqual([]);
|
|
});
|
|
|
|
it('skips non-array values (defensive)', () => {
|
|
const out = runLoop({
|
|
providers: {
|
|
lmstudio_models: 'not an array',
|
|
ollama_models: [{ value: 'a', label: 'A' }],
|
|
},
|
|
});
|
|
expect(out).toHaveLength(1);
|
|
expect(out[0].provider).toBe('OLLAMA');
|
|
});
|
|
|
|
it('ignores keys without _models suffix', () => {
|
|
const out = runLoop({
|
|
providers: {
|
|
some_other_key: [{ value: 'x', label: 'x' }],
|
|
lmstudio_models: [{ value: 'qwen', label: 'qwen' }],
|
|
},
|
|
});
|
|
expect(out).toHaveLength(1);
|
|
expect(out[0].provider).toBe('LMSTUDIO');
|
|
});
|
|
});
|