Files
nvfp4_gguf_quant/llama.cpp/tools/ui/tests/client/models-manager-utils.svelte.test.ts
T

185 lines
6.0 KiB
TypeScript

// The manager table is built from the store's option list, so these run in the
// browser project: how the quants fold into rows, what context a row reports and
// how the tracked downloads become rows of their own.
import {
downloadGroups,
groupModelQuants,
modelContextLength,
splitHiddenQuants
} from '$lib/components/app/models/ModelsManager/utils';
import { LOCAL_BACKEND_ID } from '$lib/constants';
import { ModelDraftSidecar, ModelGroupKind, ServerModelStatus } from '$lib/enums';
import { modelsStore } from '$lib/stores/models/index.svelte';
import type { ApiModelDataEntry, ModelOption } from '$lib/types';
import { beforeEach, describe, expect, it } from 'vitest';
function option(model: string, id = model): ModelOption {
return { capabilities: [], id, model, name: model };
}
function routerEntry(id: string, status: ServerModelStatus): ApiModelDataEntry {
return {
created: 0,
id,
in_cache: true,
object: 'model',
owned_by: 'llamacpp',
path: `/models/${id}`,
status: { value: status }
};
}
beforeEach(() => {
modelsStore.routerModels = [];
});
describe('groupModelQuants', () => {
it('folds the quants of one repo into a single entry', () => {
const groups = groupModelQuants([
option('org/Qwen3-8B:Q4_K_M'),
option('org/Qwen3-8B:Q8_0'),
option('org/Llama-3-8B:Q4_K_M')
]);
expect(groups.map((group) => [group.key, group.kind, group.quants.length])).toEqual([
['org/Qwen3-8B', ModelGroupKind.QUANTS, 2],
['org/Llama-3-8B', ModelGroupKind.QUANTS, 1]
]);
});
it('keeps the first quant of a repo as the row it renders', () => {
const groups = groupModelQuants([option('org/Qwen3-8B:Q4_K_M'), option('org/Qwen3-8B:Q8_0')]);
expect(groups[0].base.model).toBe('org/Qwen3-8B:Q4_K_M');
});
it('keeps rows of the same id apart as variants', () => {
const groups = groupModelQuants([
option('org/Qwen3-8B:Q4_K_M', 'first'),
option('org/Qwen3-8B:Q4_K_M', 'second')
]);
expect(groups.map((group) => [group.key, group.kind])).toEqual([
['org/Qwen3-8B::first', ModelGroupKind.VARIANTS],
['org/Qwen3-8B::second', ModelGroupKind.VARIANTS]
]);
});
});
describe('modelContextLength', () => {
it('reports the context of the listing', () => {
expect(modelContextLength({ ...option('org/Qwen3-8B:Q4_K_M'), contextLength: 8192 })).toBe(
8192
);
});
it('reports nothing while no source knows the context', () => {
expect(modelContextLength(option('org/Qwen3-8B:Q4_K_M'))).toBeNull();
});
});
describe('downloadGroups', () => {
it('reuses the row of a download the router already lists', () => {
const listed = option('org/Qwen3-8B:Q4_K_M');
const groups = downloadGroups(
[{ isPaused: false, progress: null, repoWithTag: 'org/Qwen3-8B:Q4_K_M' }],
[listed],
() => []
);
expect(groups[0].key).toBe('download::org/Qwen3-8B:Q4_K_M');
expect(groups[0].base).toMatchObject(listed);
});
it('stands the download in for a row of its own when nothing is listed', () => {
const groups = downloadGroups(
[{ isPaused: true, progress: null, repoWithTag: 'org/Qwen3-8B:Q4_K_M' }],
[],
(repo) =>
repo === 'org/Qwen3-8B' ? [{ kind: ModelDraftSidecar.MTP, quant: 'Q8_0', repo }] : []
);
// the sub-lines carry the sidecars, so the inline badges stay empty
expect(groups[0].base).toMatchObject({
backendId: LOCAL_BACKEND_ID,
draftSidecars: [],
model: 'org/Qwen3-8B:Q4_K_M'
});
expect(groups[0].sidecarBadges).toEqual([
{ kind: ModelDraftSidecar.MTP, quant: 'Q8_0', repo: 'org/Qwen3-8B' }
]);
});
it('keeps one row per tracked download, sidecars included', () => {
const main = { isPaused: false, progress: null, repoWithTag: 'org/Qwen3-8B:Q4_K_M' };
const other = { isPaused: true, progress: null, repoWithTag: 'org/Qwen3-8B:Q8_0' };
const groups = downloadGroups([main, other], [], () => []);
expect(groups).toHaveLength(2);
expect(groups.map((group) => group.key)).toEqual([
'download::org/Qwen3-8B:Q4_K_M',
'download::org/Qwen3-8B:Q8_0'
]);
});
});
describe('modelsStore.isModelRunning', () => {
it('counts a loaded or sleeping model as running', () => {
modelsStore.routerModels = [
routerEntry('org/loaded', ServerModelStatus.LOADED),
routerEntry('org/sleeping', ServerModelStatus.SLEEPING)
];
expect(modelsStore.isModelRunning('org/loaded')).toBe(true);
expect(modelsStore.isModelRunning('org/sleeping')).toBe(true);
});
it('does not count a loading, failed or unknown model as running', () => {
modelsStore.routerModels = [
routerEntry('org/loading', ServerModelStatus.LOADING),
routerEntry('org/failed', ServerModelStatus.FAILED)
];
expect(modelsStore.isModelRunning('org/loading')).toBe(false);
expect(modelsStore.isModelRunning('org/failed')).toBe(false);
expect(modelsStore.isModelRunning('org/unknown')).toBe(false);
});
});
describe('splitHiddenQuants', () => {
const entry = (repo: string, quants: string[]) => ({
base: option(quants[0]),
key: repo,
kind: ModelGroupKind.QUANTS,
quants: quants.map((model) => option(model))
});
it('keeps the visible quants of a partly hidden repo in the local block', () => {
const repo = entry('org/Qwen3-8B', ['org/Qwen3-8B:Q4_K_M', 'org/Qwen3-8B:Q8_0']);
const { hidden, local } = splitHiddenQuants([repo], (picked) => picked.model.endsWith('Q8_0'));
expect(hidden.map((group) => [group.key, group.quants.length])).toEqual([
['org/Qwen3-8B::hidden', 1]
]);
expect(local.map((group) => [group.key, group.quants.length])).toEqual([['org/Qwen3-8B', 1]]);
expect(local[0].base.model).toBe('org/Qwen3-8B:Q4_K_M');
});
it('moves a repo whose quants are all hidden', () => {
const repo = entry('org/Qwen3-8B', ['org/Qwen3-8B:Q4_K_M', 'org/Qwen3-8B:Q8_0']);
const { hidden, local } = splitHiddenQuants([repo], () => true);
expect(hidden[0].quants).toHaveLength(2);
expect(local).toEqual([]);
});
it('leaves a repo with no hidden quant alone', () => {
const repo = entry('org/Qwen3-8B', ['org/Qwen3-8B:Q4_K_M']);
const { hidden, local } = splitHiddenQuants([repo], () => false);
expect(hidden).toEqual([]);
expect(local[0].key).toBe('org/Qwen3-8B');
});
});