Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

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');
});
});