mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-10 01:29:17 +00:00
* fix(web-shell): encode vision model selection & polish picker Address Wenshao's review comments on #6209: [Critical] Encode vision model selection before persisting - handleVisionModelSelect now strips ACP (authType) suffix and stores as authType:modelId format expected by core's resolveVisionModelSelection() - Without this, picker selections silently fail to resolve when the same model ID appears on multiple providers [Suggestion] Add currentVisionModel derivation - Mirror currentVoiceModel pattern so the picker highlights the active vision model instead of falling back to the main model [Suggestion] Extract MODE_TITLE_KEY record for exhaustive dispatch - Replace duplicated 4-way ternary in App.tsx dialog title with a single Record<ModelDialogMode, string> lookup - Replace if/else if/else onSelect chain with a handlers record that would fail at compile time if a new mode is added without a handler [Suggestion] Add settings.label/description.visionModel i18n keys - Add to both EN and ZH locales so Chinese users see proper labels in the Settings dialog Files changed: - App.tsx: encoding fix, currentVisionModel, MODE_TITLE_KEY, handlers record - i18n.tsx: visionModel label + description (EN + ZH) * fix(web-shell): encode vision model selection & polish picker Address Wenshao's review comments on #6209: [Critical] Encode vision model selection before persisting - handleVisionModelSelect now strips ACP (authType) suffix and stores as authType:modelId format expected by core's resolveVisionModelSelection() - Without this, picker selections silently fail to resolve when the same model ID appears on multiple providers [Suggestion] Add currentVisionModel derivation - Mirror currentVoiceModel pattern so the picker highlights the active vision model instead of falling back to the main model [Suggestion] Extract MODE_TITLE_KEY record for exhaustive dispatch - Replace duplicated 4-way ternary in App.tsx dialog title with a single Record<ModelDialogMode, string> lookup - Replace if/else if/else onSelect chain with a handlers record that would fail at compile time if a new mode is added without a handler [Suggestion] Add settings.label/description.visionModel i18n keys - Add to both EN and ZH locales so Chinese users see proper labels in the Settings dialog Files changed: - App.tsx: encoding fix, currentVisionModel, MODE_TITLE_KEY, handlers record - i18n.tsx: visionModel label + description (EN + ZH) * fix(web-shell): address review comments for vision model picker encoding - Extract encodeVisionModelForSetting / decodeVisionModelForPicker into shared utils/modelEncoding.ts so they can be tested in isolation - Add 17 unit tests covering ACP encoding, colon-bearing IDs, empty parens passthrough, and round-trip identity - Memoize modelHandlers record with useMemo to avoid re-allocation on every model picker click - Replace dead fallback (?? 'main') — the outer modelDialogMode guard already ensures non-null, so use an explicit if-guard instead * test(web-shell): add edge-case tests for model encoding functions - Add passthrough tests for already-encoded colon format - Add malformed input tests (bare authType, unclosed paren, double-parens) - Add leadin-colon malformed input test for decode - Add empty string passthrough test - 23 encoding tests passing (up from 17), full suite: 735 passing * fix: PR #6236 follow-up — vision model encoding + fast model highlight - decodeVisionModelForPicker: strip \0baseUrl suffix before decoding to ACP - Remove dead encodeFastModelForSetting (fast picker strips ACP suffix before handler) - Add currentFastModel derivation + 'fast' branch to currentModelId ternary - Fix misleading voice handler comment (bare IDs, not ACP) - Replace unnecessary useMemo on modelHandlers with plain object Co-authored-by: atlarix-agent <agent@atlarix.dev> --------- Co-authored-by: Qwen3.6 Plus agent <agent@atlarix.dev>
147 lines
4.7 KiB
TypeScript
147 lines
4.7 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
encodeVisionModelForSetting,
|
|
extractBareModelId,
|
|
decodeVisionModelForPicker,
|
|
} from './modelEncoding';
|
|
|
|
describe('encodeVisionModelForSetting', () => {
|
|
it('encodes standard ACP format', () => {
|
|
expect(encodeVisionModelForSetting('qwen-max(qwen-oauth)')).toBe(
|
|
'qwen-oauth:qwen-max',
|
|
);
|
|
});
|
|
|
|
it('passes through non-ACP format unchanged', () => {
|
|
expect(encodeVisionModelForSetting('plain-model')).toBe('plain-model');
|
|
});
|
|
|
|
it('passes through empty parens unchanged', () => {
|
|
expect(encodeVisionModelForSetting('model()')).toBe('model()');
|
|
});
|
|
|
|
it('passes through colon-bearing IDs unchanged', () => {
|
|
expect(encodeVisionModelForSetting('openai:gpt-4o')).toBe('openai:gpt-4o');
|
|
});
|
|
|
|
it('handles nested parentheses by matching outermost ACP pattern', () => {
|
|
// The regex matches the outermost group: modelId(authType)
|
|
expect(encodeVisionModelForSetting('model(name)(extra)')).toBe(
|
|
'extra:model(name)',
|
|
);
|
|
});
|
|
|
|
it('passes through already-encoded colon format unchanged', () => {
|
|
// If someone somehow passes an already-encoded authType:modelId,
|
|
// it should be left alone — not double-encoded.
|
|
expect(encodeVisionModelForSetting('qwen-oauth:qwen-vl-max')).toBe(
|
|
'qwen-oauth:qwen-vl-max',
|
|
);
|
|
});
|
|
|
|
it('passes through malformed — bare authType only', () => {
|
|
// '(authType)' has no modelId before the parens and no text
|
|
// before the opening paren that can serve as group 1, so the
|
|
// regex won't match and we get the input back unchanged.
|
|
expect(encodeVisionModelForSetting('(authType)')).toBe('(authType)');
|
|
});
|
|
|
|
it('passes through malformed — unclosed paren', () => {
|
|
expect(encodeVisionModelForSetting('modelId(')).toBe('modelId(');
|
|
});
|
|
|
|
it('passes through malformed — double-parens inside', () => {
|
|
expect(encodeVisionModelForSetting('a((b))')).toBe('a((b))');
|
|
});
|
|
|
|
it('passes through empty string unchanged', () => {
|
|
expect(encodeVisionModelForSetting('')).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('extractBareModelId', () => {
|
|
it('extracts bare model ID from ACP format', () => {
|
|
expect(extractBareModelId('qwen-max(qwen-oauth)')).toBe('qwen-max');
|
|
});
|
|
|
|
it('passes through non-ACP format unchanged', () => {
|
|
expect(extractBareModelId('plain-model')).toBe('plain-model');
|
|
});
|
|
|
|
it('passes through empty parens unchanged', () => {
|
|
// The regex requires at least one char in each capture group,
|
|
// so '()' does not match and passes through.
|
|
expect(extractBareModelId('()')).toBe('()');
|
|
});
|
|
});
|
|
|
|
describe('decodeVisionModelForPicker', () => {
|
|
it('decodes authType:modelId back to ACP format', () => {
|
|
expect(decodeVisionModelForPicker('qwen-oauth:qwen-max')).toBe(
|
|
'qwen-max(qwen-oauth)',
|
|
);
|
|
});
|
|
|
|
it('handles colon-bearing model IDs — splits on first colon only', () => {
|
|
expect(decodeVisionModelForPicker('openai:gpt-4o:online')).toBe(
|
|
'gpt-4o:online(openai)',
|
|
);
|
|
});
|
|
|
|
it('passes through values without a colon', () => {
|
|
expect(decodeVisionModelForPicker('plain-model')).toBe('plain-model');
|
|
});
|
|
|
|
it('passes through ACP-formatted values unchanged', () => {
|
|
expect(decodeVisionModelForPicker('qwen-max(qwen-oauth)')).toBe(
|
|
'qwen-max(qwen-oauth)',
|
|
);
|
|
});
|
|
|
|
it('passes through empty string unchanged', () => {
|
|
expect(decodeVisionModelForPicker('')).toBe('');
|
|
});
|
|
|
|
it('passes through leading-colon malformed input', () => {
|
|
// colonIdx === 0, which is not > 0, so passthrough
|
|
expect(decodeVisionModelForPicker(':modelId')).toBe(':modelId');
|
|
});
|
|
|
|
it('strips \\0baseUrl suffix before decoding', () => {
|
|
expect(
|
|
decodeVisionModelForPicker(
|
|
'qwen-oauth:qwen-vl-max\0https://api.example.com',
|
|
),
|
|
).toBe('qwen-vl-max(qwen-oauth)');
|
|
});
|
|
|
|
it('handles colon-bearing ID with \\0baseUrl suffix', () => {
|
|
expect(
|
|
decodeVisionModelForPicker(
|
|
'openai:gpt-4o:online\0https://api.openai.com',
|
|
),
|
|
).toBe('gpt-4o:online(openai)');
|
|
});
|
|
|
|
it('passes through bare ID with \\0baseUrl suffix but no colon', () => {
|
|
expect(
|
|
decodeVisionModelForPicker('plain-model\0https://api.example.com'),
|
|
).toBe('plain-model');
|
|
});
|
|
});
|
|
|
|
describe('round-trip: encode then decode', () => {
|
|
it('preserves the original ACP format', () => {
|
|
const original = 'qwen-vl-max(qwen-oauth)';
|
|
const encoded = encodeVisionModelForSetting(original);
|
|
const decoded = decodeVisionModelForPicker(encoded);
|
|
expect(decoded).toBe(original);
|
|
});
|
|
|
|
it('preserves colon-bearing IDs after round-trip', () => {
|
|
const original = 'gpt-4o:new-model(openai)';
|
|
const encoded = encodeVisionModelForSetting(original);
|
|
const decoded = decodeVisionModelForPicker(encoded);
|
|
expect(decoded).toBe(original);
|
|
});
|
|
});
|