qwen-code/packages/web-shell/client/utils/modelEncoding.test.ts
PilgrimStack 8123c6bff9
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
fix(web-shell): encode vision model picker selection & polish dispatch (#6236)
* 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>
2026-07-03 13:51:47 +00:00

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