Changeset 0.9.0 (#50)

This commit is contained in:
2026-02-23 01:57:28 +00:00
parent 15be26c516
commit 8264aa6016
94 changed files with 9812 additions and 8574 deletions

View File

@@ -0,0 +1,317 @@
// ==========================================
// Model Processing Tests
// ==========================================
// Tests the data transforms in fetchModels()
// that convert backend responses into the
// App.models array used throughout the UI.
//
// Run: node --test src/js/__tests__/model-processing.test.js
// ==========================================
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const { processModelsResponse } = require('./helpers');
// ── Basic model transform ────────────────────
describe('processModelsResponse — catalog models', () => {
// Mock uses BOTH canonical (provider_config_id) and alias (config_id)
// to match the actual UserModel Go struct serialization.
const apiResponse = {
models: [
{
id: 'entry-uuid',
model_id: 'gpt-4o',
display_name: 'GPT-4o',
provider_config_id: 'cfg-uuid',
config_id: 'cfg-uuid',
provider_name: 'OpenAI Production',
provider_type: 'openai',
source: 'catalog',
scope: 'global',
capabilities: { streaming: true, vision: true, tool_calling: true },
},
],
};
it('produces composite ID for catalog models', () => {
const models = processModelsResponse(apiResponse);
assert.equal(models[0].id, 'cfg-uuid:gpt-4o',
'catalog model ID must be config_id:model_id to avoid collisions');
});
it('produces composite ID with ONLY provider_config_id (no config_id alias)', () => {
// This is the critical regression test: if the config_id alias
// is removed from the Go struct, the frontend must still work.
const resp = {
models: [{
model_id: 'gpt-4o',
provider_config_id: 'cfg-uuid',
// NO config_id — simulates broken alias
provider_name: 'OpenAI',
source: 'catalog',
}],
};
const models = processModelsResponse(resp);
assert.equal(models[0].id, 'cfg-uuid:gpt-4o',
'must fall back to provider_config_id when config_id is missing');
});
it('preserves baseModelId', () => {
const models = processModelsResponse(apiResponse);
assert.equal(models[0].baseModelId, 'gpt-4o');
});
it('uses display_name for name', () => {
const models = processModelsResponse(apiResponse);
assert.equal(models[0].name, 'GPT-4o');
});
it('falls back to model_id when display_name empty', () => {
const resp = {
models: [{ model_id: 'claude-3-opus', provider_config_id: 'c1', provider_type: 'anthropic' }],
};
const models = processModelsResponse(resp);
assert.equal(models[0].name, 'claude-3-opus');
});
it('marks catalog models as NOT presets', () => {
const models = processModelsResponse(apiResponse);
assert.equal(models[0].isPreset, false);
});
it('preserves configId from config_id || provider_config_id', () => {
const models = processModelsResponse(apiResponse);
assert.equal(models[0].configId, 'cfg-uuid');
});
it('uses provider_name for display', () => {
const models = processModelsResponse(apiResponse);
assert.equal(models[0].provider, 'OpenAI Production');
});
});
// ── Preset transform ─────────────────────────
describe('processModelsResponse — presets', () => {
const apiResponse = {
models: [
{
id: 'preset-uuid',
model_id: 'gpt-4o',
display_name: 'Code Helper',
// Backend canonical fields
provider_config_id: 'cfg-uuid',
persona_id: 'preset-uuid',
scope: 'global',
avatar: '/avatars/code.png',
source: 'persona',
// Frontend alias fields
is_preset: true,
preset_id: 'preset-uuid',
preset_scope: 'global',
preset_avatar: '/avatars/code.png',
preset_team_name: null,
config_id: 'cfg-uuid',
provider_name: 'OpenAI',
},
],
};
it('uses preset_id as ID (not composite)', () => {
const models = processModelsResponse(apiResponse);
assert.equal(models[0].id, 'preset-uuid',
'preset ID must use preset_id, not config_id:model_id');
});
it('falls back to persona_id when preset_id is missing', () => {
const resp = {
models: [{
id: 'p-uuid', model_id: 'gpt-4o',
is_preset: true, persona_id: 'p-uuid',
// NO preset_id — simulates broken alias
source: 'persona',
}],
};
const models = processModelsResponse(resp);
assert.equal(models[0].id, 'p-uuid',
'must fall back to persona_id when preset_id is missing');
});
it('marks as preset', () => {
const models = processModelsResponse(apiResponse);
assert.equal(models[0].isPreset, true);
});
it('preserves preset metadata', () => {
const models = processModelsResponse(apiResponse);
assert.equal(models[0].presetScope, 'global');
assert.equal(models[0].presetAvatar, '/avatars/code.png');
assert.equal(models[0].presetId, 'preset-uuid');
});
it('baseModelId is the underlying model', () => {
const models = processModelsResponse(apiResponse);
assert.equal(models[0].baseModelId, 'gpt-4o');
});
});
// ── Same model from multiple providers ───────
describe('processModelsResponse — multi-provider dedup', () => {
const apiResponse = {
models: [
{
model_id: 'gpt-4o', display_name: 'GPT-4o',
provider_config_id: 'global-cfg', config_id: 'global-cfg',
provider_name: 'Global OpenAI',
source: 'catalog', scope: 'global',
},
{
model_id: 'gpt-4o', display_name: 'GPT-4o (BYOK)',
provider_config_id: 'personal-cfg', config_id: 'personal-cfg',
provider_name: 'My Key',
source: 'catalog', scope: 'personal',
},
],
};
it('same model_id with different config_ids produces unique IDs', () => {
const models = processModelsResponse(apiResponse);
assert.equal(models.length, 2);
assert.notEqual(models[0].id, models[1].id,
'same model from different providers MUST have unique IDs');
assert.equal(models[0].id, 'global-cfg:gpt-4o');
assert.equal(models[1].id, 'personal-cfg:gpt-4o');
});
it('works with only provider_config_id (no config_id alias)', () => {
const resp = {
models: [
{ model_id: 'gpt-4o', provider_config_id: 'cfg-a', source: 'catalog' },
{ model_id: 'gpt-4o', provider_config_id: 'cfg-b', source: 'catalog' },
],
};
const models = processModelsResponse(resp);
assert.equal(models[0].id, 'cfg-a:gpt-4o');
assert.equal(models[1].id, 'cfg-b:gpt-4o');
});
});
// ── Hidden models ────────────────────────────
describe('processModelsResponse — hidden models', () => {
const apiResponse = {
models: [
{ model_id: 'gpt-4o', provider_config_id: 'c1', config_id: 'c1', display_name: 'GPT-4o' },
{ model_id: 'claude-3', provider_config_id: 'c2', config_id: 'c2', display_name: 'Claude 3' },
],
};
it('marks models as hidden from user prefs', () => {
const hidden = new Set(['gpt-4o']);
const models = processModelsResponse(apiResponse, hidden);
assert.equal(models[0].hidden, true, 'gpt-4o should be hidden');
assert.equal(models[1].hidden, false, 'claude-3 should NOT be hidden');
});
it('presets are never hidden via model ID', () => {
const resp = {
models: [
{ model_id: 'gpt-4o', is_preset: true, preset_id: 'p1', display_name: 'Preset' },
],
};
const hidden = new Set(['gpt-4o']);
const models = processModelsResponse(resp, hidden);
assert.equal(models[0].hidden, false,
'presets must NOT be hidden by base model hidden pref');
});
});
// ── Empty / edge cases ───────────────────────
describe('processModelsResponse — edge cases', () => {
it('handles empty models array', () => {
const models = processModelsResponse({ models: [] });
assert.equal(models.length, 0);
});
it('handles missing models key', () => {
const models = processModelsResponse({});
assert.equal(models.length, 0);
});
it('handles null response gracefully', () => {
const models = processModelsResponse({ models: null });
assert.equal(models.length, 0);
});
it('model without config_id uses bare model_id as ID', () => {
const resp = {
models: [{ model_id: 'test-model', display_name: 'Test' }],
};
const models = processModelsResponse(resp);
assert.equal(models[0].id, 'test-model');
});
it('model without display_name or model_id uses id', () => {
const resp = {
models: [{ id: 'fallback-id' }],
};
const models = processModelsResponse(resp);
assert.equal(models[0].id, 'fallback-id');
assert.equal(models[0].name, 'fallback-id');
});
});
// ── Model sorting ────────────────────────────
describe('Model sorting', () => {
function sortModels(models) {
const scopeOrder = { global: 0, team: 1, personal: 2 };
return [...models].sort((a, b) => {
if (a.isPreset && !b.isPreset) return -1;
if (!a.isPreset && b.isPreset) return 1;
if (a.isPreset && b.isPreset) {
const sa = scopeOrder[a.presetScope] ?? 9;
const sb = scopeOrder[b.presetScope] ?? 9;
if (sa !== sb) return sa - sb;
}
return a.name.localeCompare(b.name);
});
}
it('presets sort before regular models', () => {
const models = [
{ name: 'GPT-4o', isPreset: false },
{ name: 'Code Helper', isPreset: true, presetScope: 'global' },
];
const sorted = sortModels(models);
assert.equal(sorted[0].name, 'Code Helper');
assert.equal(sorted[1].name, 'GPT-4o');
});
it('global presets sort before team presets', () => {
const models = [
{ name: 'Team Bot', isPreset: true, presetScope: 'team' },
{ name: 'Global Bot', isPreset: true, presetScope: 'global' },
{ name: 'My Bot', isPreset: true, presetScope: 'personal' },
];
const sorted = sortModels(models);
assert.equal(sorted[0].presetScope, 'global');
assert.equal(sorted[1].presetScope, 'team');
assert.equal(sorted[2].presetScope, 'personal');
});
it('regular models sort alphabetically', () => {
const models = [
{ name: 'Zephyr', isPreset: false },
{ name: 'Claude', isPreset: false },
{ name: 'GPT-4o', isPreset: false },
];
const sorted = sortModels(models);
assert.equal(sorted[0].name, 'Claude');
assert.equal(sorted[1].name, 'GPT-4o');
assert.equal(sorted[2].name, 'Zephyr');
});
});