feat: configure agent availability per provider and add Qwen Max 0902
This commit is contained in:
@@ -6,27 +6,34 @@ const ts = require('typescript');
|
||||
const React = require('react');
|
||||
|
||||
const source = ts.createSourceFile('SettingsPage.tsx', fs.readFileSync(require.resolve('../src/pages/settings/SettingsPage.tsx'), 'utf8'), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
||||
const constants = source.statements.filter(node => ts.isVariableStatement(node) && ['AI_MODEL_DEFAULTS', 'AI_MODEL_OPTIONS', 'CUSTOM_AI_MODEL', 'AI_PROVIDER_LABELS'].includes(node.declarationList.declarations[0].name.getText(source)));
|
||||
const constants = source.statements.filter(node => ts.isVariableStatement(node) && ['AI_MODEL_DEFAULTS', 'AI_MODEL_OPTIONS', 'CUSTOM_AI_MODEL', 'AI_PROVIDER_LABELS', 'DEFAULT_QWEN_ENDPOINT'].includes(node.declarationList.declarations[0].name.getText(source)));
|
||||
function compile(code, globals = {}) {
|
||||
const exports = {};
|
||||
vm.runInNewContext(ts.transpileModule(code, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, jsx: ts.JsxEmit.ReactJSX } }).outputText, { exports, require, ...globals });
|
||||
return exports;
|
||||
}
|
||||
const catalogs = constants.map(node => `export ${node.getText(source)}`).join('\n');
|
||||
const { AI_MODEL_OPTIONS, AI_MODEL_DEFAULTS, CUSTOM_AI_MODEL } = compile(catalogs);
|
||||
const helpers = source.statements.filter(node => ts.isFunctionDeclaration(node) && ['aiSettingDraft', 'aiSettingDrafts'].includes(node.name?.text));
|
||||
const catalogs = [...constants, ...helpers].map(node => `export ${node.getText(source)}`).join('\n');
|
||||
const { AI_MODEL_OPTIONS, AI_MODEL_DEFAULTS, CUSTOM_AI_MODEL, aiSettingDrafts } = compile(catalogs);
|
||||
function find(node, predicate) {
|
||||
if (predicate(node)) return node;
|
||||
return ts.forEachChild(node, child => find(child, predicate));
|
||||
}
|
||||
const section = find(source, node => ts.isJsxElement(node) && node.openingElement.tagName.getText(source) === 'section' && node.openingElement.attributes.getText(source).includes('border-t border-[#e2e8f0] pt-5') && node.getText(source).includes('AI 공급자'));
|
||||
function controls(provider, model, custom = false, configured = true) {
|
||||
const state = { aiProvider: provider, aiModel: model, aiCustomModel: custom };
|
||||
const state = { aiProvider: provider, drafts: aiSettingDrafts() };
|
||||
Object.assign(state.drafts[provider], { model, customModel: custom });
|
||||
for (const [name, field] of [['aiModel', 'model'], ['aiCustomModel', 'customModel'], ['aiApiKey', 'apiKey'], ['aiEndpointUrl', 'endpointUrl']]) {
|
||||
Object.defineProperty(state, name, { enumerable: true, get: () => state.drafts[state.aiProvider][field] });
|
||||
}
|
||||
const context = {
|
||||
aiSettingLoading: false, saving: false, aiEndpointUrl: 'https://example.test', aiApiKey: '', aiHasApiKey: false, aiConfiguredProvider: null,
|
||||
aiCopilotConfigured: configured,
|
||||
DEFAULT_QWEN_ENDPOINT: 'https://example.test', KeyRound: () => null,
|
||||
setAiProvider(value) { state.aiProvider = value; }, setAiModel(value) { state.aiModel = value; },
|
||||
setAiCustomModel(value) { state.aiCustomModel = value; }, setAiApiKey() {}, setAiEndpointUrl() {},
|
||||
setAiProvider(value) { state.aiProvider = value; }, setAiModel(value) { state.drafts[state.aiProvider].model = value; },
|
||||
setAiCustomModel(value) { state.drafts[state.aiProvider].customModel = value; },
|
||||
setAiApiKey(value) { state.drafts[state.aiProvider].apiKey = value; },
|
||||
setAiEndpointUrl(value) { state.drafts[state.aiProvider].endpointUrl = value; }, setError() {},
|
||||
};
|
||||
const render = () => compile(`${catalogs}\nexport const tree = (${section.getText(source)});`, { ...context, ...state }).tree;
|
||||
return { state, render };
|
||||
@@ -65,9 +72,8 @@ test('provider defaults select the highest-tier current models at the top of eac
|
||||
for (const [provider, model] of Object.entries(expected)) {
|
||||
assert.equal(AI_MODEL_DEFAULTS[provider], model);
|
||||
assert.equal(AI_MODEL_OPTIONS[provider][0].value, model);
|
||||
const h = controls('QWEN', 'qwen3.7-plus');
|
||||
if (provider === 'QWEN') h.state.aiProvider = 'OPENAI';
|
||||
elements(h.render(), node => node.props.role === 'radio' && node.props.children === ({ OPENAI: 'OpenAI', ANTHROPIC: 'Claude', GEMINI: 'Gemini', QWEN: 'Qwen' })[provider])[0].props.onClick();
|
||||
const h = controls('A_COPILOT', 'a-copilot');
|
||||
elements(h.render(), node => node.props.role === 'radio' && node.props['aria-label'] === ({ OPENAI: 'OpenAI', ANTHROPIC: 'Claude', GEMINI: 'Gemini', QWEN: 'Qwen' })[provider])[0].props.onClick();
|
||||
assert.equal(h.state.aiModel, model);
|
||||
}
|
||||
});
|
||||
@@ -81,13 +87,29 @@ test('saved unknown model IDs remain visible in custom mode', () => {
|
||||
assert.equal(AI_MODEL_OPTIONS.QWEN.some(option => option.value === model), false);
|
||||
});
|
||||
|
||||
test('Qwen Max 0902 is selectable and saved selections restore without custom mode', () => {
|
||||
const model = 'qwen3.8-max-0902';
|
||||
const h = controls('QWEN', AI_MODEL_DEFAULTS.QWEN);
|
||||
const select = elements(h.render(), node => node.type === 'select')[0];
|
||||
const option = elements(select, node => node.type === 'option' && node.props.value === model);
|
||||
assert.equal(option.length, 1);
|
||||
assert.equal(option[0].props.children, 'Qwen 3.8 Max (0902)');
|
||||
select.props.onChange({ target: { value: model } });
|
||||
assert.equal(h.state.aiModel, model);
|
||||
assert.equal(h.state.aiCustomModel, false);
|
||||
const restored = aiSettingDrafts([{ provider: 'QWEN', enabled: true, model, hasApiKey: true }]);
|
||||
assert.equal(restored.QWEN.model, model);
|
||||
assert.equal(restored.QWEN.customModel, false);
|
||||
assert.equal(AI_MODEL_DEFAULTS.QWEN, 'qwen3.8-max');
|
||||
});
|
||||
|
||||
test('the selected provider button preserves a custom draft', () => {
|
||||
const h = controls('OPENAI', 'custom-gpt', true);
|
||||
const providers = elements(h.render(), node => node.props.role === 'radio');
|
||||
providers.find(node => node.props.children === 'OpenAI').props.onClick();
|
||||
providers.find(node => node.props['aria-label'] === 'OpenAI').props.onClick();
|
||||
assert.equal(h.state.aiModel, 'custom-gpt');
|
||||
assert.equal(h.state.aiCustomModel, true);
|
||||
providers.find(node => node.props.children === 'Claude').props.onClick();
|
||||
providers.find(node => node.props['aria-label'] === 'Claude').props.onClick();
|
||||
assert.equal(h.state.aiProvider, 'ANTHROPIC');
|
||||
assert.equal(h.state.aiModel, AI_MODEL_DEFAULTS.ANTHROPIC);
|
||||
assert.equal(h.state.aiCustomModel, false);
|
||||
@@ -95,7 +117,7 @@ test('the selected provider button preserves a custom draft', () => {
|
||||
|
||||
test('A-Copilot is first and exposes only fixed read-only fields without a model picker', () => {
|
||||
const tree = controls('A_COPILOT', 'a-copilot').render();
|
||||
assert.equal(elements(tree, node => node.props.role === 'radio')[0].props.children, 'A-Copilot');
|
||||
assert.equal(elements(tree, node => node.props.role === 'radio')[0].props['aria-label'], 'A-Copilot');
|
||||
assert.equal(elements(tree, node => node.type === 'select').length, 0);
|
||||
const inputs = elements(tree, node => node.type === 'input');
|
||||
assert.deepEqual(inputs.map(node => node.props.value), ['A-Copilot', '서버에서 관리']);
|
||||
@@ -112,3 +134,31 @@ test('official current API IDs are present', () => {
|
||||
QWEN: ['qwen3.8-max', 'qwen3.8-flash'],
|
||||
})) for (const id of ids) assert.ok(AI_MODEL_OPTIONS[provider].some(option => option.value === id), id);
|
||||
});
|
||||
|
||||
test('all new providers default to disabled while saved settings stay intact', () => {
|
||||
assert.ok(Object.values(aiSettingDrafts()).every(draft => !draft.enabled && !draft.hasApiKey && draft.apiKey === ''));
|
||||
const drafts = aiSettingDrafts([{ provider: 'QWEN', enabled: true, model: 'custom-qwen', hasApiKey: true, endpointUrl: 'https://qwen.test/v1' }]);
|
||||
assert.equal(drafts.QWEN.enabled, true);
|
||||
assert.equal(drafts.QWEN.model, 'custom-qwen');
|
||||
assert.equal(drafts.QWEN.customModel, true);
|
||||
assert.equal(drafts.OPENAI.enabled, false);
|
||||
assert.equal(drafts.OPENAI.hasApiKey, false);
|
||||
});
|
||||
|
||||
test('switching providers preserves separate unsaved models, keys and availability', () => {
|
||||
const h = controls('OPENAI', 'private-model', true);
|
||||
Object.assign(h.state.drafts.OPENAI, { enabled: true, apiKey: 'openai-draft' });
|
||||
const provider = name => elements(h.render(), node => node.props.role === 'radio' && node.props['aria-label'] === name)[0];
|
||||
provider('Qwen').props.onClick();
|
||||
assert.equal(h.state.aiApiKey, '');
|
||||
Object.assign(h.state.drafts.QWEN, { model: 'qwen3.8-flash', enabled: false, apiKey: 'qwen-draft' });
|
||||
provider('OpenAI').props.onClick();
|
||||
assert.equal(h.state.aiModel, 'private-model');
|
||||
assert.equal(h.state.aiCustomModel, true);
|
||||
assert.equal(h.state.aiApiKey, 'openai-draft');
|
||||
assert.equal(h.state.drafts.OPENAI.enabled, true);
|
||||
provider('Qwen').props.onClick();
|
||||
assert.equal(h.state.aiModel, 'qwen3.8-flash');
|
||||
assert.equal(h.state.aiApiKey, 'qwen-draft');
|
||||
assert.equal(h.state.drafts.QWEN.enabled, false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user