Changeset 0.22.7 (#149)

This commit is contained in:
2026-03-04 10:44:42 +00:00
parent d8e0664fa3
commit 389e47b0f9
62 changed files with 6820 additions and 1476 deletions

View File

@@ -116,71 +116,61 @@ describe('GET /models/enabled response contract', () => {
});
});
// ── /models/enabled with preset response ────
// ── /models/enabled with persona response ────
describe('GET /models/enabled preset contract', () => {
// ACTUAL backend UserModel for a persona includes both canonical and alias fields.
const presetModel = {
id: 'preset-uuid-123',
describe('GET /models/enabled persona contract', () => {
// ACTUAL backend UserModel for a persona (v0.22.8+: unified naming, no aliases).
const personaModel = {
id: 'persona-uuid-123',
model_id: 'gpt-4o',
display_name: 'Code Helper',
source: 'persona',
// Backend canonical fields
// Backend canonical fields (v0.22.8: all use persona_ prefix)
provider_config_id: 'cfg1',
persona_id: 'preset-uuid-123',
scope: 'global',
avatar: null,
// Frontend alias fields (populated by resolver)
is_preset: true,
preset_id: 'preset-uuid-123',
preset_scope: 'global',
preset_avatar: null,
is_persona: true,
persona_id: 'persona-uuid-123',
persona_scope: 'global',
persona_avatar: null,
persona_team_name: null,
config_id: 'cfg1',
provider_name: 'OpenAI',
capabilities: { streaming: true },
};
it('preset has is_preset = true', () => {
assert.equal(presetModel.is_preset, true);
it('persona has is_persona = true', () => {
assert.equal(personaModel.is_persona, true);
});
it('preset has preset_id matching persona_id', () => {
assert.ok(presetModel.preset_id);
assert.equal(presetModel.preset_id, presetModel.persona_id,
'preset_id alias must match persona_id');
it('persona has persona_id', () => {
assert.ok(personaModel.persona_id);
});
it('preset has preset_scope matching scope', () => {
assert.ok(['global', 'team', 'personal'].includes(presetModel.preset_scope));
assert.equal(presetModel.preset_scope, presetModel.scope,
'preset_scope alias must match scope');
it('persona has persona_scope', () => {
assert.ok(['global', 'team', 'personal'].includes(personaModel.persona_scope));
});
it('preset ID uses preset_id not composite', () => {
// Frontend: id = isPreset ? (m.preset_id || m.persona_id || m.id) : composite
const id = presetModel.is_preset
? (presetModel.preset_id || presetModel.persona_id || presetModel.id)
: `${presetModel.config_id || presetModel.provider_config_id}:${presetModel.model_id}`;
assert.equal(id, 'preset-uuid-123');
it('persona ID uses persona_id not composite', () => {
// Frontend: id = isPersona ? (m.persona_id || m.id) : composite
const id = personaModel.is_persona
? (personaModel.persona_id || personaModel.id)
: `${personaModel.config_id || personaModel.provider_config_id}:${personaModel.model_id}`;
assert.equal(id, 'persona-uuid-123');
});
it('frontend works with ONLY canonical fields (no aliases)', () => {
// If someone breaks the alias population in resolver.go,
// the frontend fallback chain must still produce correct IDs.
// v0.22.8: no more dual-key aliases — backend sends is_persona directly.
const rawBackend = {
id: 'preset-uuid-123',
id: 'persona-uuid-123',
model_id: 'gpt-4o',
source: 'persona',
provider_config_id: 'cfg1',
persona_id: 'preset-uuid-123',
scope: 'global',
// NO is_preset, NO preset_id, NO config_id aliases
is_persona: true,
persona_id: 'persona-uuid-123',
persona_scope: 'global',
};
// is_preset would be undefined/falsy → treated as catalog model
// This test documents the FAILURE MODE if aliases break
const isPreset = !!rawBackend.is_preset; // false!
assert.equal(isPreset, false,
'Without is_preset alias, persona is misidentified as catalog model');
const isPersona = !!rawBackend.is_persona;
assert.equal(isPersona, true,
'is_persona field must be present for correct identification');
});
});
@@ -441,8 +431,8 @@ describe('Response shape consistency', () => {
assert.ok('models' in shape, 'admin/models must use "models" key');
});
it('presets uses {presets:[]}', () => {
const shape = { presets: [] };
assert.ok('presets' in shape);
it('personas uses {personas:[]}', () => {
const shape = { personas: [] };
assert.ok('personas' in shape);
});
});

View File

@@ -116,22 +116,24 @@ function loadSource(sandbox, filename) {
*/
function loadAppModules(overrides = {}) {
const sandbox = createBrowserContext(overrides);
loadSource(sandbox, 'app-state.js');
loadSource(sandbox, 'api.js');
loadSource(sandbox, 'app.js');
return { API: sandbox.API, App: sandbox.App, sandbox };
}
/**
* Extract the model-processing transform from fetchModels.
* Extract the model-processing transform from fetchModels (app-state.js).
* This is the core mapping logic that converts API response → App.models.
* v0.22.8: unified persona naming — no more preset aliases.
*/
function processModelsResponse(data, hiddenModels = new Set()) {
return (data.models || []).map(m => {
const isPreset = !!m.is_preset;
const isPersona = !!m.is_persona;
const baseModelId = m.model_id || m.id;
const cfgId = m.config_id || m.provider_config_id;
const id = isPreset
? (m.preset_id || m.persona_id || m.id)
const id = isPersona
? (m.persona_id || m.id)
: (cfgId ? `${cfgId}:${baseModelId}` : baseModelId);
return {
id,
@@ -139,14 +141,14 @@ function processModelsResponse(data, hiddenModels = new Set()) {
name: m.display_name || baseModelId,
provider: m.provider_name || m.provider || '',
configId: cfgId || null,
isPreset,
presetId: m.preset_id || m.persona_id || null,
presetScope: m.preset_scope || (isPreset ? m.scope : null) || null,
presetAvatar: m.preset_avatar || (isPreset ? m.avatar : null) || null,
presetTeamName: m.preset_team_name || (isPreset ? m.team_name : null) || null,
source: m.source || (isPreset ? 'preset' : 'global'),
teamName: m.preset_team_name || (isPreset ? m.team_name : null) || null,
hidden: !isPreset && hiddenModels.has(baseModelId),
isPersona,
personaId: m.persona_id || null,
personaScope: m.persona_scope || (isPersona ? m.scope : null) || null,
personaAvatar: m.persona_avatar || (isPersona ? m.avatar : null) || null,
personaTeamName: m.persona_team_name || (isPersona ? m.team_name : null) || null,
source: m.source || (isPersona ? 'persona' : 'global'),
teamName: m.persona_team_name || null,
hidden: !isPersona && hiddenModels.has(baseModelId),
};
});
}

View File

@@ -75,9 +75,9 @@ describe('processModelsResponse — catalog models', () => {
assert.equal(models[0].name, 'claude-3-opus');
});
it('marks catalog models as NOT presets', () => {
it('marks catalog models as NOT personas', () => {
const models = processModelsResponse(apiResponse);
assert.equal(models[0].isPreset, false);
assert.equal(models[0].isPersona, false);
});
it('preserves configId from config_id || provider_config_id', () => {
@@ -93,7 +93,7 @@ describe('processModelsResponse — catalog models', () => {
// ── Preset transform ─────────────────────────
describe('processModelsResponse — presets', () => {
describe('processModelsResponse — personas', () => {
const apiResponse = {
models: [
{
@@ -107,47 +107,47 @@ describe('processModelsResponse — presets', () => {
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,
is_persona: true,
persona_id: 'preset-uuid',
persona_scope: 'global',
persona_avatar: '/avatars/code.png',
persona_team_name: null,
config_id: 'cfg-uuid',
provider_name: 'OpenAI',
},
],
};
it('uses preset_id as ID (not composite)', () => {
it('uses persona_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');
'persona ID must use persona_id, not config_id:model_id');
});
it('falls back to persona_id when preset_id is missing', () => {
it('falls back to persona_id when not present', () => {
const resp = {
models: [{
id: 'p-uuid', model_id: 'gpt-4o',
is_preset: true, persona_id: 'p-uuid',
is_persona: 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');
'must fall back to persona_id');
});
it('marks as preset', () => {
it('marks as persona', () => {
const models = processModelsResponse(apiResponse);
assert.equal(models[0].isPreset, true);
assert.equal(models[0].isPersona, true);
});
it('preserves preset metadata', () => {
it('preserves persona 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');
assert.equal(models[0].personaScope, 'global');
assert.equal(models[0].personaAvatar, '/avatars/code.png');
assert.equal(models[0].personaId, 'preset-uuid');
});
it('baseModelId is the underlying model', () => {
@@ -215,16 +215,16 @@ describe('processModelsResponse — hidden models', () => {
assert.equal(models[1].hidden, false, 'claude-3 should NOT be hidden');
});
it('presets are never hidden via model ID', () => {
it('personas are never hidden via model ID', () => {
const resp = {
models: [
{ model_id: 'gpt-4o', is_preset: true, preset_id: 'p1', display_name: 'Preset' },
{ model_id: 'gpt-4o', is_persona: true, persona_id: 'p1', display_name: 'Persona' },
],
};
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');
'personas must NOT be hidden by base model hidden pref');
});
});
@@ -270,44 +270,44 @@ 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 (a.isPersona && !b.isPersona) return -1;
if (!a.isPersona && b.isPersona) return 1;
if (a.isPersona && b.isPersona) {
const sa = scopeOrder[a.personaScope] ?? 9;
const sb = scopeOrder[b.personaScope] ?? 9;
if (sa !== sb) return sa - sb;
}
return a.name.localeCompare(b.name);
});
}
it('presets sort before regular models', () => {
it('personas sort before regular models', () => {
const models = [
{ name: 'GPT-4o', isPreset: false },
{ name: 'Code Helper', isPreset: true, presetScope: 'global' },
{ name: 'GPT-4o', isPersona: false },
{ name: 'Code Helper', isPersona: true, personaScope: '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', () => {
it('global personas sort before team personas', () => {
const models = [
{ name: 'Team Bot', isPreset: true, presetScope: 'team' },
{ name: 'Global Bot', isPreset: true, presetScope: 'global' },
{ name: 'My Bot', isPreset: true, presetScope: 'personal' },
{ name: 'Team Bot', isPersona: true, personaScope: 'team' },
{ name: 'Global Bot', isPersona: true, personaScope: 'global' },
{ name: 'My Bot', isPersona: true, personaScope: 'personal' },
];
const sorted = sortModels(models);
assert.equal(sorted[0].presetScope, 'global');
assert.equal(sorted[1].presetScope, 'team');
assert.equal(sorted[2].presetScope, 'personal');
assert.equal(sorted[0].personaScope, 'global');
assert.equal(sorted[1].personaScope, 'team');
assert.equal(sorted[2].personaScope, 'personal');
});
it('regular models sort alphabetically', () => {
const models = [
{ name: 'Zephyr', isPreset: false },
{ name: 'Claude', isPreset: false },
{ name: 'GPT-4o', isPreset: false },
{ name: 'Zephyr', isPersona: false },
{ name: 'Claude', isPersona: false },
{ name: 'GPT-4o', isPersona: false },
];
const sorted = sortModels(models);
assert.equal(sorted[0].name, 'Claude');

View File

@@ -65,12 +65,12 @@ describe('Policy wiring audit — source code', () => {
// ── allow_user_personas ──
it('UI has checkUserPresetsAllowed function', () => {
assert.ok(uiSrc.includes('checkUserPresetsAllowed'),
'MISSING: checkUserPresetsAllowed — preset button will show when policy is off');
it('UI has checkUserPersonasAllowed function', () => {
assert.ok(uiSrc.includes('checkUserPersonasAllowed'),
'MISSING: checkUserPersonasAllowed — persona button will show when policy is off');
});
it('checkUserPresetsAllowed reads App.policies.allow_user_personas', () => {
it('checkUserPersonasAllowed reads App.policies.allow_user_personas', () => {
assert.ok(uiSrc.includes('allow_user_personas'),
'MISSING: allow_user_personas check in UI');
});
@@ -104,10 +104,10 @@ describe('Policy wiring audit — source code', () => {
'MISSING: checkUserProvidersAllowed call after admin save');
});
it('admin save calls checkUserPresetsAllowed', () => {
it('admin save calls checkUserPersonasAllowed', () => {
const saveFunc = appSrc.slice(appSrc.indexOf('handleSaveAdminSettings'));
assert.ok(saveFunc.includes('checkUserPresetsAllowed'),
'MISSING: checkUserPresetsAllowed call after admin save');
assert.ok(saveFunc.includes('checkUserPersonasAllowed'),
'MISSING: checkUserPersonasAllowed call after admin save');
});
it('admin save calls fetchModels to refresh model list', () => {
@@ -118,9 +118,9 @@ describe('Policy wiring audit — source code', () => {
// ── Models tab calls preset check ──
it('models tab switch calls checkUserPresetsAllowed', () => {
assert.ok(uiSrc.includes('checkUserPresetsAllowed'),
'MISSING: checkUserPresetsAllowed call on models tab switch');
it('models tab switch calls checkUserPersonasAllowed', () => {
assert.ok(uiSrc.includes('checkUserPersonasAllowed'),
'MISSING: checkUserPersonasAllowed call on models tab switch');
});
});
@@ -283,9 +283,9 @@ describe('Critical HTML elements exist in server templates', () => {
const requiredElements = [
// Settings surface — provider section scaffold
'userPresetList', // User preset list container
'userAddPresetBtn', // New preset button (policy-gated)
'userAddPresetForm', // Preset form container
'userPersonaList', // User persona list container
'userAddPersonaBtn', // New persona button (policy-gated)
'userAddPersonaForm', // Persona form container
'userProvidersDisabled', // BYOK disabled notice
'providerShowAddBtn', // Add provider button (policy-gated)
// Admin settings — policy toggles

View File

@@ -31,7 +31,7 @@ function globalModel(modelId, configId, providerName) {
provider_type: 'openai',
source: 'catalog',
scope: 'global',
is_preset: false,
is_persona: false,
capabilities: { streaming: true },
hidden: false,
};
@@ -48,7 +48,7 @@ function teamModel(modelId, configId, providerName, teamName) {
provider_type: 'venice',
source: 'catalog',
scope: 'team',
is_preset: false,
is_persona: false,
capabilities: { streaming: true },
hidden: false,
team_name: teamName,
@@ -66,21 +66,21 @@ function personalModel(modelId, configId, providerName) {
provider_type: 'venice',
source: 'catalog',
scope: 'personal',
is_preset: false,
is_persona: false,
capabilities: { streaming: true },
hidden: false,
};
}
function presetModel(presetId, baseModelId, scope, teamName) {
function personaModel(personaId, baseModelId, scope, teamName) {
return {
id: presetId,
id: personaId,
model_id: baseModelId,
display_name: `Preset: ${baseModelId}`,
preset_id: presetId,
preset_scope: scope,
preset_team_name: teamName || '',
is_preset: true,
display_name: `Persona: ${baseModelId}`,
persona_id: personaId,
persona_scope: scope,
persona_team_name: teamName || '',
is_persona: true,
source: 'persona',
scope: scope,
capabilities: { streaming: true },
@@ -144,9 +144,9 @@ describe('User Journey: Platform Admin (no team, no BYOK)', () => {
}
});
it('source is catalog (not preset)', () => {
it('source is catalog (not persona)', () => {
for (const m of models) {
assert.equal(m.isPreset, false);
assert.equal(m.isPersona, false);
assert.equal(m.source, 'catalog');
}
});
@@ -322,42 +322,42 @@ describe('Edge: Hidden model preferences', () => {
assert.equal(main.hidden, false, 'gpt-4o should not be hidden');
});
it('presets are never hidden', () => {
const preset = presetModel('preset-1', 'gpt-4o', 'global');
it('personas are never hidden', () => {
const persona = personaModel('preset-1', 'gpt-4o', 'global');
const hidden = new Set(['gpt-4o']);
const models = processModelsResponse({ models: [preset] }, hidden);
assert.equal(models[0].hidden, false, 'presets must never be hidden');
const models = processModelsResponse({ models: [persona] }, hidden);
assert.equal(models[0].hidden, false, 'personas must never be hidden');
});
});
describe('Edge: Presets mixed with catalog models', () => {
describe('Edge: Personas mixed with catalog models', () => {
const apiResponse = {
models: [
...GLOBAL_MODELS,
presetModel('preset-code', 'gpt-4o', 'global'),
presetModel('preset-team', 'llama-3.3-70b', 'team', 'Engineering'),
personaModel('preset-code', 'gpt-4o', 'global'),
personaModel('preset-team', 'llama-3.3-70b', 'team', 'Engineering'),
],
};
const models = processModelsResponse(apiResponse);
it('presets use preset_id as ID, not composite', () => {
const preset = models.find(m => m.presetId === 'preset-code');
assert.ok(preset, 'preset-code should be present');
assert.equal(preset.id, 'preset-code');
assert.equal(preset.isPreset, true);
it('personas use persona_id as ID, not composite', () => {
const persona = models.find(m => m.personaId === 'preset-code');
assert.ok(persona, 'preset-code should be present');
assert.equal(persona.id, 'preset-code');
assert.equal(persona.isPersona, true);
});
it('preset and catalog model with same model_id have different IDs', () => {
const catalog = models.find(m => !m.isPreset && m.baseModelId === 'gpt-4o');
const preset = models.find(m => m.isPreset && m.baseModelId === 'gpt-4o');
it('persona and catalog model with same model_id have different IDs', () => {
const catalog = models.find(m => !m.isPersona && m.baseModelId === 'gpt-4o');
const persona = models.find(m => m.isPersona && m.baseModelId === 'gpt-4o');
assert.ok(catalog, 'catalog gpt-4o should exist');
assert.ok(preset, 'preset gpt-4o should exist');
assert.notEqual(catalog.id, preset.id);
assert.ok(persona, 'preset gpt-4o should exist');
assert.notEqual(catalog.id, persona.id);
});
it('preset team name flows through', () => {
const teamPreset = models.find(m => m.presetId === 'preset-team');
assert.equal(teamPreset.presetTeamName, 'Engineering');
it('persona team name flows through', () => {
const teamPersona = models.find(m => m.personaId === 'preset-team');
assert.equal(teamPersona.personaTeamName, 'Engineering');
});
});
@@ -412,10 +412,10 @@ describe('Full Matrix: 4 actors × backend response → frontend model count', (
`${tc.actor}: duplicate IDs: ${JSON.stringify(ids)}`);
});
it(`${tc.actor}: all models have configId or are presets`, () => {
it(`${tc.actor}: all models have configId or are personas`, () => {
const result = processModelsResponse({ models: tc.models });
for (const m of result) {
if (!m.isPreset) {
if (!m.isPersona) {
assert.ok(m.configId,
`${tc.actor}: model ${m.baseModelId} missing configId — ` +
`frontend will fail to route completions`);