const fs = require('node:fs/promises'), path = require('node:path'), assert = require('node:assert/strict') if (!process.versions.electron) { ;(async () => { const directory = await fs.mkdtemp(path.resolve('.tmp/custom-model-settings-')) for (const name of ['data', 'home']) await fs.mkdir(path.join(directory, name)) await fs.cp(path.resolve('out'), path.join(directory, 'build/out'), { recursive: true }) await fs.cp(path.resolve('resources/model-metadata'), path.join(directory, 'build/resources/model-metadata'), { recursive: true }) const env = { ...process.env, YCODE_QA_DIRECTORY: directory, YCODE_USER_DATA_DIR: path.join(directory, 'data'), YCODE_OMP_HOME: path.join(directory, 'home') } delete env.ELECTRON_RUN_AS_NODE; delete env.ELECTRON_RENDERER_URL const child = require('node:child_process').spawn(require('electron'), ['--disable-gpu-sandbox', __filename], { env, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }) let output = ''; child.stdout.on('data', data => { output += data; if (data.toString().includes('CUSTOM_MODEL_')) process.stdout.write(data) }); child.stderr.on('data', data => output += data) const timer = setTimeout(() => child.kill(), 180000) child.on('exit', async code => { clearTimeout(timer); await fs.writeFile(path.join(directory, 'process.log'), output); console.log(JSON.stringify({ directory, exitCode: code })); if (code) console.error(output.slice(-5000)); process.exitCode = code ? 1 : 0 }) })().catch(error => { console.error(error); process.exitCode = 1 }) } else { const { app, BrowserWindow } = require('electron'), { pathToFileURL } = require('node:url'), { createServer } = require('node:http') const directory = process.env.YCODE_QA_DIRECTORY app.setPath('home', path.join(directory, 'home')); app.setPath('userData', path.join(directory, 'data')); app.setPath('sessionData', path.join(directory, 'data')) let main, settings, phase = 'boot' const checks = [], servers = [], requests = [], rendererErrors = [] const pause = ms => new Promise(resolve => setTimeout(resolve, ms)) const read = (source, win = settings) => win.webContents.executeJavaScript(source) const check = (label, actual, expected = true) => checks.push({ label, actual, expected, pass: JSON.stringify(actual) === JSON.stringify(expected) }) async function wait(source, label, win = settings) { for (let i = 0; i < 500; i++) { const value = await read(source, win); if (value) return value; await pause(20) }; throw Error('Timeout: ' + label) } async function step(label) { phase = label; await fs.writeFile(path.join(directory, 'phase.txt'), label) } const click = selector => read(`(()=>{const node=document.querySelector(${JSON.stringify(selector)});if(!node)throw Error('Missing control: '+${JSON.stringify(selector)});node.click();return true})()`) async function fill(selector, value) { await read(`(()=>{const node=document.querySelector(${JSON.stringify(selector)});if(!node)throw Error('Missing input: '+${JSON.stringify(selector)});Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set.call(node,${JSON.stringify(value)});node.dispatchEvent(new Event('input',{bubbles:true}));return true})()`) } async function option(value) { const source = `(()=>{const nodes=[...document.querySelectorAll('[role="option"]')];const node=nodes.find(n=>n.textContent.trim()===${JSON.stringify(value)}&&n.getClientRects().length);if(!node||getComputedStyle(node.closest('.wb-select__panel')).pointerEvents==='none'||getComputedStyle(node.closest('.wb-popover')).visibility==='hidden'||Number(getComputedStyle(node.closest('.wb-popover')).opacity)<.99)return false;node.click();return true})()` await wait(source, 'select option ' + value) await wait(`![...document.querySelectorAll('[role="option"]')].some(node=>node.textContent.trim()===${JSON.stringify(value)}&&node.getClientRects().length)`, 'option committed ' + value) } async function shot(name) { await read(`new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(()=>resolve(true))))`) await read(`(async()=>{const menu=document.querySelector('.custom-model-options');if(menu)await Promise.allSettled(menu.getAnimations({subtree:true}).filter(animation=>animation.effect.getComputedTiming().iterations!==Infinity).map(animation=>animation.finished));await new Promise(resolve=>requestAnimationFrame(()=>resolve(true)))})()`) await fs.writeFile(path.join(directory, name + '.png'), (await settings.webContents.capturePage()).toPNG()) await fs.writeFile(path.join(directory, name + '.layout.json'), JSON.stringify(await read(`(()=>{const body=document.querySelector('.models-settings-panel__editor-body'),menu=document.querySelector('.custom-model-options');return {theme:document.documentElement.dataset.theme,body:body?{top:body.scrollTop,height:body.clientHeight,scrollHeight:body.scrollHeight}:null,menu:menu?{x:menu.getBoundingClientRect().x,y:menu.getBoundingClientRect().y,width:menu.getBoundingClientRect().width,opacity:getComputedStyle(menu).opacity}:null}})()`), null, 2)) } function anthropicStream(model) { return [ { type: 'message_start', message: { id: 'msg_ui', type: 'message', role: 'assistant', model, content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 20, output_tokens: 0 } } }, { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } }, { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: 'MODEL_PROTOCOL_OK' } }, { type: 'content_block_stop', index: 0 }, { type: 'message_delta', delta: { stop_reason: 'end_turn', stop_sequence: null }, usage: { output_tokens: 4 } }, { type: 'message_stop' } ].map(row => `event: ${row.type}\ndata: ${JSON.stringify(row)}\n\n`).join('') } async function upstream(protocol, model, options = {}) { const server = createServer(async (req, res) => { const url = new URL(req.url, 'http://local') const validAuth = options.noKey ? !req.headers['x-api-key'] && !req.headers['x-goog-api-key'] && !req.headers.authorization : protocol === 'anthropic' ? req.headers['x-api-key'] === 'controlled-model-key' && !req.headers.authorization : protocol === 'gemini' ? req.headers['x-goog-api-key'] === 'controlled-model-key' : req.headers.authorization === 'Bearer controlled-model-key' const listPath = protocol === 'gemini' ? '/v1beta/models' : '/v1/models' const generationPath = protocol === 'anthropic' ? '/v1/messages' : protocol === 'gemini' ? `/v1beta/models/${model}:streamGenerateContent` : protocol === 'responses' ? '/v1/responses' : '/v1/chat/completions' if (req.method === 'GET') { if (url.pathname !== listPath || !validAuth || options.noList) { res.writeHead(404); res.end('{}'); return } options.listStarted = true if (options.listDelay) await pause(options.listDelay) res.setHeader('content-type', 'application/json'); res.end(JSON.stringify(protocol === 'gemini' ? { models: options.emptyList ? [] : [{ name: 'models/' + model, displayName: model, supportedGenerationMethods: ['generateContent'] }] } : { data: options.emptyList ? [] : [{ id: model }, ...(options.additionalModels ?? []).map(id => ({ id }))] })); options.listEnded = true; return } let bytes = ''; for await (const chunk of req) bytes += chunk const body = JSON.parse(bytes || '{}'), nonstreamGemini = protocol === 'gemini' && url.pathname === `/v1beta/models/${model}:generateContent` requests.push({ protocol, path: req.url, stream: !!body.stream || url.searchParams.get('alt') === 'sse', body, authorized: validAuth, authShape: { names: Object.keys(req.headers), bearerMatches: req.headers.authorization === 'Bearer controlled-model-key', xKeyMatches: req.headers['x-api-key'] === 'controlled-model-key', xKeyLength: String(req.headers['x-api-key'] ?? '').length } }) if (!validAuth) { res.writeHead(401); res.end('{}'); return } if (url.pathname !== generationPath && !nonstreamGemini) { res.writeHead(404); res.end('{}'); return } const streaming = body.stream || url.searchParams.get('alt') === 'sse' if (!streaming) { options.probeStarted = true if (options.probeDelay) await pause(options.probeDelay) res.setHeader('content-type', 'application/json') res.end(JSON.stringify(protocol === 'anthropic' ? { type: 'message', role: 'assistant', content: [{ type: 'text', text: 'OK' }] } : protocol === 'gemini' ? { candidates: [{ content: { role: 'model', parts: [{ text: 'OK' }] } }] } : protocol === 'responses' ? { id: 'resp_test', object: 'response', status: 'completed', output: [{ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'OK' }] }] } : { choices: [{ message: { role: 'assistant', content: 'OK' } }] })); options.probeEnded = true; return } res.writeHead(200, { 'content-type': 'text/event-stream' }) if (protocol === 'anthropic') res.end(anthropicStream(model)) else if (protocol === 'gemini') res.end('data: ' + JSON.stringify({ candidates: [{ index: 0, content: { role: 'model', parts: [{ text: 'MODEL_PROTOCOL_OK' }] }, finishReason: 'STOP' }], usageMetadata: { promptTokenCount: 20, candidatesTokenCount: 4, totalTokenCount: 24 } }) + '\n\n') else if (protocol === 'responses') { const message = { id: 'msg_resp', type: 'message', role: 'assistant', status: 'completed', content: [{ type: 'output_text', text: 'MODEL_PROTOCOL_OK' }] } res.end([{ type: 'response.output_item.added', output_index: 0, item: { ...message, content: [] } }, { type: 'response.output_text.delta', output_index: 0, content_index: 0, item_id: message.id, delta: 'MODEL_PROTOCOL_OK' }, { type: 'response.completed', response: { id: 'resp_actual', status: 'completed', output: [message], usage: { input_tokens: 20, output_tokens: 4, total_tokens: 24 } } }].map(row => `event: ${row.type}\ndata: ${JSON.stringify(row)}\n\n`).join('')) } else res.end([{ choices: [{ index: 0, delta: { role: 'assistant', content: 'MODEL_PROTOCOL_OK' }, finish_reason: null }] }, { choices: [{ index: 0, delta: {}, finish_reason: 'stop' }], usage: { prompt_tokens: 20, completion_tokens: 4, total_tokens: 24 } }].map(row => 'data: ' + JSON.stringify(row) + '\n\n').join('') + 'data: [DONE]\n\n') }) await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); servers.push(server) return `127.0.0.1:${server.address().port}` } ;(async () => { await import(pathToFileURL(path.join(directory, 'build/out/main/index.mjs')).href); await app.whenReady() for (let i = 0; i < 300; i++) { main = BrowserWindow.getAllWindows().find(win => win.getTitle() === 'YCode'); if (main && !main.webContents.isLoading()) break; await pause(20) } await wait(`!!window.ycode&&!!document.querySelector('.chat-pane')`, 'main app', main) await wait(`!!document.querySelector('.cr-model-selector__trigger')`, 'composer model selector', main) await read(`document.querySelector('.cr-model-selector__trigger').click()`, main) await wait(`document.querySelector('.cr-model-selector__menu > .cr-menu-item')?.textContent.trim()==='配置自定义模型'`, 'configure custom models action', main) await read(`document.querySelector('.cr-model-selector__menu > .cr-menu-item').click()`, main) settings = BrowserWindow.getAllWindows().find(win => win !== main) settings.webContents.on('console-message', event => { if (event.level === 'error') rendererErrors.push(event.message) }) await wait(`!!document.querySelector('.models-settings-panel')`, 'model list page') check('configure custom models opens the list without an add dialog', await read(`!document.querySelector('.models-settings-panel__editor-overlay')`)) check('model page has no metadata update button', await read(`![...document.querySelectorAll('.models-settings-panel button')].some(node=>node.textContent.includes('更新元数据'))`)) await click('.models-settings-panel__add-button') await wait(`!!document.querySelector('.models-settings-panel__editor-overlay')`, 'model editor') check('keyless switch and explanatory text are removed', await read(`!document.querySelector('.models-settings-panel__editor-body').textContent.includes('无需')`)) await step('custom-provider-only') await click('.models-settings-panel__provider-select'); await option('自定义') check('only custom provider appears in the editor', await read(`document.querySelector('.models-settings-panel__provider-select').textContent.trim()`), '自定义') const source = await read(`window.ycode.models.metadataStatus()`) const manifest = JSON.parse(await fs.readFile(path.join(directory, 'build/resources/model-metadata/manifest.json'), 'utf8')) check('bundled catalog matches the fetched snapshot', [source.sha256, source.providers, source.models], [manifest.sha256, manifest.providers, manifest.models]) await shot('custom-editor-empty-light') const cases = [{ protocol: 'openai', model: 'gpt-5.4' }, { protocol: 'anthropic', model: 'claude-sonnet-4-6' }, { protocol: 'gemini', model: 'gemini-2.5-flash' }, { protocol: 'responses', model: 'gpt-5.4-pro' }, { protocol: 'responses', model: 'gpt-6.1-sol', extended: true }] const profiles = [] for (let index = 0; index < cases.length; index++) { const item = cases[index]; await step('form-' + item.protocol) if (index) { await read(`window.ycode.app.openSettings('model','new')`, main); await wait(`!!document.querySelector('.models-settings-panel__editor-overlay')`, 'next model editor') } const domain = await upstream(item.protocol, item.model, index === 0 ? { additionalModels: [item.model + '-alt'] } : {}) await fill('input[placeholder*="api.example.com"]', domain); await fill('input[type="password"]', 'controlled-model-key') await read(`[...document.querySelectorAll('.custom-model-id-row button')].find(node=>node.textContent.includes('获取模型')).click()`) await option(item.model) await wait(`document.querySelector('.custom-model-id-combobox input').value===${JSON.stringify(item.model)}&&!document.querySelector('.models-settings-panel__editor-save').disabled`, 'metadata applied') const metadata = await read(`window.ycode.models.metadata(${JSON.stringify(item.model)},'auto')`) check(item.protocol + ' metadata found', !!metadata) check(item.protocol + ' metadata descriptions and total-context field are removed', await read(`!document.querySelector('.custom-model-metadata')&&!document.querySelector('[aria-label="总上下文窗口"]')&&!document.querySelector('.models-settings-panel__editor-body').textContent.includes('参考价格')`)) if (index === 0) { await fill('.custom-model-id-combobox input', '5.4-alt') await wait(`document.querySelectorAll('.custom-model-options [role="option"]').length===1`, 'inline model search') check('one combobox searches the list directly without a second search field', await read(`document.querySelector('.custom-model-id-combobox input').getAttribute('role')==='combobox'&&!document.querySelector('.custom-model-options input')`)) await read(`document.querySelector('.custom-model-id-combobox input').dispatchEvent(new KeyboardEvent('keydown',{key:'ArrowDown',bubbles:true}))`) await wait(`!!document.querySelector('.custom-model-id-combobox input').getAttribute('aria-activedescendant')`, 'active keyboard option') await read(`document.querySelector('.custom-model-id-combobox input').dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',bubbles:true}))`) await wait(`document.querySelector('.custom-model-id-combobox input').value==='gpt-5.4-alt'`, 'keyboard selection') await fill('.custom-model-id-combobox input', 'vendor/manual-model') await read(`document.querySelector('.custom-model-id-combobox input').dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',bubbles:true}))`) check('an unmatched manual model stays editable and is not replaced', await read(`document.querySelector('.custom-model-id-combobox input').value`), 'vendor/manual-model') await fill('.custom-model-id-combobox input', item.model) await read(`document.querySelector('.custom-model-id-combobox input').dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}))`) check('Escape closes only the model list', await read(`!document.querySelector('.custom-model-options')&&!!document.querySelector('.models-settings-panel__editor-overlay')`)) await wait(`!document.querySelector('.models-settings-panel__editor-save').disabled`, 'original model metadata restored') } await shot('custom-editor-' + item.protocol + '-light') await read(`document.querySelector('.models-settings-panel__editor-body').scrollTop=10000`) if (item.extended) { await read(`(()=>{const fields=document.querySelectorAll('.models-settings-panel__number-grid .models-settings-panel__field');fields[0].querySelectorAll('button').forEach(button=>{if(button.textContent==='500K')button.click()});fields[1].querySelectorAll('button').forEach(button=>{if(button.textContent==='128K')button.click()})})()`) check('500K and 128K shortcuts use exact catalog-compatible token counts', await read(`[...document.querySelectorAll('.models-settings-panel__number-grid input')].map(node=>node.value)`), ['500000', '128000']) check('both large presets show their selected state', await read(`[...document.querySelectorAll('.models-settings-panel__quick-value.wb-button--secondary')].map(node=>node.textContent)`), ['500K', '128K']) check('fields configure total context and output', await read(`[...document.querySelectorAll('.models-settings-panel__number-grid input')].map(node=>node.getAttribute('aria-label'))`), ['上下文','输出']) check('live budgets show 372K input and compression at 500K total', await read(`document.querySelector('[data-context-budget]').textContent.includes('372,000 tokens · 自动压缩阈值:372,000')`)) } await shot('custom-editor-' + item.protocol + '-metadata-light') if (item.extended) await shot('custom-editor-500k-128k-light') await click('.models-settings-panel__editor-save') await wait(`!document.querySelector('.models-settings-panel__editor-overlay')`, 'save with protocol verification') const registry = await read(`window.ycode.models.list()`), profile = registry.models.find(model => model.id === item.model) assert(profile, 'saved profile ' + item.model); profiles.push(profile) check(item.protocol + ' stores only custom provider', profile.providerId, 'custom') check(item.protocol + ' automatically negotiated generation API', profile.endpoint.api, item.protocol === 'anthropic' ? 'anthropic-messages' : item.protocol === 'gemini' ? 'google-generative-ai' : item.protocol === 'responses' ? 'openai-responses' : 'openai-completions') check(item.protocol + ' saves actual capabilities', [profile.supportsImages, profile.supportsReasoning, profile.supportsToolCall], [metadata.supportsImages, metadata.supportsReasoning, metadata.supportsToolCall]) check(item.protocol + ' saves metadata thinking defaults', [profile.onlyReasoning, profile.canDisableThinking, profile.defaultEffort, profile.supportedEfforts], [metadata.onlyReasoning, metadata.canDisableThinking, metadata.defaultEffort, metadata.supportedEfforts]) check(item.protocol + ' saves total context and derived input budget', [profile.maxInputTokens, profile.maxOutputTokens, profile.contextWindowTokens], item.extended ? [372000, 128000, 500000] : [metadata.maxInputTokens, metadata.maxOutputTokens, metadata.contextWindowTokens]) check(item.protocol + ' saves the matching tier reference', profile.price, item.extended ? metadata.priceTiers.find(tier=>tier.minContext===272000) : metadata.price) check(item.protocol + ' retains no plaintext key in renderer view', !JSON.stringify(profile).includes('controlled-model-key')) await step('real-chat-' + item.protocol) const session = await read(`window.ycode.session.create('Protocol ${item.protocol}',{modelId:${JSON.stringify(item.model)}})`, main) const before = requests.length await read(`window.ycode.chat.send({sessionId:${JSON.stringify(session.id)},text:'Reply briefly with MODEL_PROTOCOL_OK.'})`, main) await wait(`window.ycode.session.messages(${JSON.stringify(session.id)}).then(rows=>rows.some(row=>row.role==='assistant'&&['done','error'].includes(row.status)))`, 'real native turn completes', main) const messages = await read(`window.ycode.session.messages(${JSON.stringify(session.id)})`, main), assistant = messages.filter(row => row.role === 'assistant').at(-1) check(item.protocol + ' production chat completes through actual OMP', [assistant.status, assistant.content.includes('MODEL_PROTOCOL_OK')], ['done', true]) check(item.protocol + ' actual chat reaches configured native server', requests.slice(before).some(request => request.protocol === item.protocol && request.stream && request.authorized)) const actualRequest = requests.slice(before).find(request => request.protocol === item.protocol && request.stream)?.body if (item.extended) { check('large output budget reaches the actual Responses provider request', actualRequest.max_output_tokens, 128000) check('context accounting uses the configured 500K total', assistant.contextBreakdown?.contextWindow, 500000) const managedRoot = path.join(directory, 'home', '.omp', 'profiles') const yamls = await Promise.all((await fs.readdir(managedRoot)).map(async name => ({name,text:await fs.readFile(path.join(managedRoot, name, 'agent', 'models.yml'), 'utf8').catch(()=>'')}))) const managed = yamls.find(item=>item.text.includes('gpt-6.1-sol')) check('real OMP profile uses 500K total and 128K output', !!managed && /contextWindow: 500000/.test(managed.text) && /maxTokens: 128000/.test(managed.text)) const budgetConfig = await fs.readFile(path.join(managedRoot, managed.name, 'agent', 'config.yml'), 'utf8') check('OMP runtime config reserves output and sets the dynamic compression threshold', /reserveTokens: 128000/.test(budgetConfig) && /thresholdTokens: 372000/.test(budgetConfig)) const executable = process.env.YCODE_OMP_PATH || path.join(process.env.LOCALAPPDATA, 'Ygent', 'omp.exe') const parsed = JSON.parse(require('node:child_process').execFileSync(executable, ['config','get','compaction.thresholdTokens','--json'], {encoding:'utf8',windowsHide:true,timeout:15000,env:{...process.env,OMP_PROFILE:managed.name,USERPROFILE:path.join(directory,'home'),HOME:path.join(directory,'home')}})) check('installed OMP loads the generated compression threshold', parsed.value ?? parsed, 372000) } check(item.protocol + ' actual request uses the automatically selected thinking strength', item.protocol === 'gemini' ? actualRequest.generationConfig?.thinkingConfig?.thinkingBudget === 1024 : (item.protocol === 'anthropic' ? actualRequest.output_config?.effort : item.protocol === 'responses' ? actualRequest.reasoning?.effort : actualRequest.reasoning_effort) === metadata.defaultEffort) check(item.protocol + ' native request retains context breakdown observations', !!assistant.contextBreakdown && assistant.contextBreakdown.scope === 'provider-request' && assistant.contextBreakdown.counts.system > 0) } async function edit(profile) { await read(`(()=>{const row=[...document.querySelectorAll('.models-settings-panel__row')].find(row=>row.querySelector('.models-settings-panel__row-name').textContent===${JSON.stringify(profile.name)});row.querySelector('button').click()})()`) await wait(`!!document.querySelector('.models-settings-panel__editor-overlay')&&!document.querySelector('.models-settings-panel__editor-save').disabled`, 'edit saved model') } const largeProfile = profiles.find(profile=>profile.id==='gpt-6.1-sol') await edit(largeProfile) check('reopening keeps the 500K and 128K budgets', await read(`[...document.querySelectorAll('.models-settings-panel__number-grid input')].map(node=>node.value)`), ['500000','128000']) await fill('.models-settings-panel__number-grid .models-settings-panel__field:nth-child(2) input', '64000') check('changing output dynamically recalculates input and keeps 15 percent compression headroom', await read(`document.querySelector('[data-context-budget]').textContent.includes('436,000 tokens · 自动压缩阈值:425,000')`)) await fill('.models-settings-panel__number-grid .models-settings-panel__field:nth-child(2) input', '131072') await click('.models-settings-panel__editor-save') await wait(`document.querySelector('.model-ui-error')?.textContent.includes('128,000')`, 'catalog output ceiling validation') check('invalid output budget keeps the editor open', await read(`!!document.querySelector('.models-settings-panel__editor-overlay')`)) check('invalid output budget preserves the saved model', (await read(`window.ycode.models.list()`)).models.find(model=>model.id===largeProfile.id).maxOutputTokens, 128000) await click('.models-settings-panel__editor-cancel') await step('existing-secret-list-does-not-downgrade-responses') const responsesProfile = profiles.find(profile => profile.id === 'gpt-5.4-pro') await edit(responsesProfile) check('editing never reveals saved key', await read(`document.querySelector('input[type="password"]').value`), '') await read(`[...document.querySelectorAll('.custom-model-id-row button')].find(node=>node.textContent.includes('获取模型')).click()`) await option(responsesProfile.model) await click('.models-settings-panel__editor-save') await wait(`!document.querySelector('.models-settings-panel__editor-overlay')`, 'unchanged model saved') check('fetching with saved key preserves verified Responses routing', (await read(`window.ycode.models.list()`)).models.find(model => model.id === responsesProfile.id).endpoint.api, 'openai-responses') await step('manual-overrides-survive-reopen') const profile = profiles.find(profile => profile.id === 'gpt-5.4') await edit(profile) await fill('.models-settings-panel__number-grid input', '32768') await fill('.models-settings-panel__number-grid .models-settings-panel__field:nth-child(2) input', '4096') await read(`[...document.querySelectorAll('.models-settings-panel__checkbox-item')].find(node=>node.textContent.trim()==='图片输入').querySelector('input').click()`) await click('.models-settings-panel__editor-save') await wait(`!document.querySelector('.models-settings-panel__editor-overlay')`, 'manual overrides saved') const overridden = (await read(`window.ycode.models.list()`)).models.find(model => model.id === profile.id) check('manual total/output and image override are persisted with derived input', [overridden.maxInputTokens, overridden.maxOutputTokens, overridden.contextWindowTokens, overridden.supportsImages], [28672, 4096, 32768, false]) await edit(overridden) check('reopening retains manual input/output limits', await read(`[...document.querySelectorAll('.models-settings-panel__number-grid input')].map(node=>node.value)`), ['32768', '4096']) await fill('.custom-model-id-combobox input', 'manually-entered-model') await wait(`!document.querySelector('.models-settings-panel__editor-save').disabled`, 'unknown metadata settled') check('model input remains freely editable', await read(`document.querySelector('.custom-model-id-combobox input').value`), 'manually-entered-model') check('unknown model keeps manual capability controls without metadata copy', await read(`!!document.querySelector('.models-settings-panel__advanced')&&!document.querySelector('.custom-model-metadata')`)) await click('.models-settings-panel__editor-cancel') await step('manual-model-without-models-api-or-key') const manualDomain = await upstream('openai', 'manual-unlisted', { noList: true, noKey: true }) await read(`window.ycode.app.openSettings('model','new')`, main) await wait(`!!document.querySelector('.models-settings-panel__editor-overlay')`, 'edit saved model') await fill('input[placeholder*="api.example.com"]', manualDomain) await read(`[...document.querySelectorAll('.custom-model-id-row button')].find(node=>node.textContent.includes('获取模型')).click()`) await wait(`!!document.querySelector('.model-ui-error')`, 'missing models API feedback') await fill('.custom-model-id-combobox input', 'manual-unlisted') await wait(`!document.querySelector('.models-settings-panel__editor-save').disabled`, 'manual model ready') await click('.models-settings-panel__editor-save') await wait(`!document.querySelector('.models-settings-panel__editor-overlay')`, 'manual keyless save') const manual = (await read(`window.ycode.models.list()`)).models.find(model => model.id === 'manual-unlisted') check('manual model without models API saves a verified keyless route', [manual.authMode, manual.hasApiKey, manual.endpoint.api, manual.endpoint.verifiedBy], ['none', false, 'openai-completions', 'generation']) await step('keyless-gemini-production-fallback') const keylessId = 'gemini-keyless-controlled' await read(`window.ycode.app.openSettings('model','new')`, main) await wait(`!!document.querySelector('.models-settings-panel__editor-overlay')`, 'keyless Gemini form') await fill('input[placeholder*="api.example.com"]', await upstream('gemini', keylessId, { noKey: true, noList: true })) await fill('.custom-model-id-combobox input', keylessId) await wait(`!document.querySelector('.models-settings-panel__editor-save').disabled`, 'keyless Gemini ready') await click('.models-settings-panel__editor-save') await wait(`!document.querySelector('.models-settings-panel__editor-overlay')`, 'keyless Gemini saved') const keylessSession = await read(`window.ycode.session.create('Keyless Gemini',{modelId:${JSON.stringify(keylessId)}})`, main) await read(`window.ycode.chat.send({sessionId:${JSON.stringify(keylessSession.id)},text:'Reply briefly.'})`, main) await wait(`window.ycode.session.messages(${JSON.stringify(keylessSession.id)}).then(rows=>rows.some(row=>row.role==='assistant'&&['done','error'].includes(row.status)))`, 'keyless Gemini turn', main) const keylessReply = (await read(`window.ycode.session.messages(${JSON.stringify(keylessSession.id)})`, main)).filter(row => row.role === 'assistant').at(-1) check('keyless Gemini completes through production native HTTP gateway', [keylessReply.status, keylessReply.content.includes('MODEL_PROTOCOL_OK')], ['done', true]) await step('clear-saved-key-without-an-auth-switch') const clearable = {}, clearId = 'clearable-controlled' await read(`window.ycode.app.openSettings('model','new')`, main) await wait(`!!document.querySelector('.models-settings-panel__editor-overlay')`, 'clearable model form') await fill('input[placeholder*="api.example.com"]', await upstream('openai', clearId, clearable)) await fill('input[type="password"]', 'controlled-model-key') await fill('.custom-model-id-combobox input', clearId) await wait(`!document.querySelector('.models-settings-panel__editor-save').disabled`, 'clearable model ready') await click('.models-settings-panel__editor-save') await wait(`!document.querySelector('.models-settings-panel__editor-overlay')`, 'clearable model saved') const clearProfile = (await read(`window.ycode.models.list()`)).models.find(model => model.id === clearId) await edit(clearProfile) clearable.noKey = true await read(`document.querySelector('input[type="password"]').dispatchEvent(new KeyboardEvent('keydown',{key:'Delete',bubbles:true}))`) await fill('.custom-model-id-combobox input', clearId + '-renamed') await wait(`!document.querySelector('.models-settings-panel__editor-save').disabled`, 'clear and rename ready') await click('.models-settings-panel__editor-save') await wait(`!document.querySelector('.models-settings-panel__editor-overlay')`, 'cleared key persisted') const cleared = (await read(`window.ycode.models.list()`)).models.find(model => model.id === clearId + '-renamed') check('clearing the saved key remains keyless after changing the model ID', [cleared.authMode, cleared.hasApiKey], ['none', false]) await step('stale-model-fetch-and-connection-test') await read(`window.ycode.app.openSettings('model','new')`, main) await wait(`!!document.querySelector('.models-settings-panel__editor-overlay')`, 'race test form') const slowList = { listDelay: 300 }, slowDomain = await upstream('openai', 'old-list', slowList) await fill('input[placeholder*="api.example.com"]', slowDomain); await fill('input[type="password"]', 'controlled-model-key') await read(`[...document.querySelectorAll('.custom-model-id-row button')].find(node=>node.textContent.includes('获取模型')).click()`) while (!slowList.listStarted) await pause(10) const slowProbe = { probeDelay: 300 }, probeDomain = await upstream('openai', 'old-probe', slowProbe) await fill('input[placeholder*="api.example.com"]', probeDomain) while (!slowList.listEnded) await pause(10) check('a late old-domain model list cannot refill the new-domain picker', await read(`document.querySelector('.custom-model-id-toggle').disabled`)) await fill('.custom-model-id-combobox input', 'old-probe') await wait(`!document.querySelector('.models-settings-panel__editor-save').disabled`, 'initial probe model ready') await click('.models-settings-panel__editor-test-connection') while (!slowProbe.probeStarted) await pause(10) await fill('.custom-model-id-combobox input', 'new-probe') while (!slowProbe.probeEnded) await pause(10) await wait(`!document.querySelector('.models-settings-panel__editor-save').disabled`, 'late probe settled') const beforeProbeSave = requests.length await click('.models-settings-panel__editor-save') await wait(`!document.querySelector('.models-settings-panel__editor-overlay')`, 'new model verified and saved') check('a late test result cannot mark a changed model verified', requests.slice(beforeProbeSave).some(request => request.body.model === 'new-probe' && !request.stream)) await step('light-dark-form-and-menu') await read(`window.ycode.app.openSettings('appearance')`, main) await wait(`!!document.querySelector('.appearance-card--dark')`, 'appearance page') await click('.appearance-card--dark') await wait(`document.documentElement.dataset.theme==='dark'`, 'dark theme persisted') await read(`window.ycode.app.openSettings('model','new')`, main) await wait(`!!document.querySelector('.models-settings-panel__editor-overlay')`, 'dark model form') check('supplier label no longer claims OpenAI only', await read(`!document.querySelector('.models-settings-panel__editor-body').textContent.includes('仅支持 OpenAI')`)) await shot('custom-editor-empty-dark') await fill('input[placeholder*="api.example.com"]', await upstream('openai', 'gpt-5.4', { emptyList: true })) await fill('input[type="password"]', 'controlled-model-key') await read(`[...document.querySelectorAll('.custom-model-id-row button')].find(node=>node.textContent.includes('获取模型')).click()`) await wait(`!!document.querySelector('.model-ui-toast')?.textContent.includes('空列表')`, 'empty model list feedback') check('empty lists allow manual input', await read(`!document.querySelector('.custom-model-id-combobox input').disabled`)) await fill('input[placeholder*="api.example.com"]', await upstream('gemini', 'gemini-2.5-flash')) await read(`[...document.querySelectorAll('.custom-model-id-row button')].find(node=>node.textContent.includes('获取模型')).click()`) await wait(`!!document.querySelector('[role="option"]')`, 'dark model dropdown') await shot('custom-editor-model-list-dark') check('the dropdown aligns with the full input width', await read(`(()=>{const input=document.querySelector('.custom-model-id-combobox').getBoundingClientRect(),list=document.querySelector('.custom-model-options').getBoundingClientRect();return Math.abs(input.x-list.x)<1&&Math.abs(input.width-list.width)<1})()`)) check('the settled model dropdown is visible and opaque', await read(`(()=>{const style=getComputedStyle(document.querySelector('.custom-model-options'));return style.visibility==='visible'&&Number(style.opacity)>.99})()`)) await option('gemini-2.5-flash') await fill('.custom-model-id-combobox input', 'gemini-2.5-flash') await wait(`!document.querySelector('.models-settings-panel__editor-save').disabled`, 'dark metadata ready') await read(`document.querySelector('.models-settings-panel__editor-body').scrollTop=10000`) await shot('custom-editor-metadata-dark') await click('.models-settings-panel__editor-cancel') await step('metadata-update-in-about') await read(`window.ycode.app.openSettings('helpFeedback')`, main) await wait(`!!document.querySelector('#settings-about-model-metadata button')`, 'metadata update in About') check('About contains an enabled metadata update action', await read(`!document.querySelector('#settings-about-model-metadata button').disabled`)) await shot('about-model-metadata-dark') const originalFetch = globalThis.fetch let refreshRequests = 0 globalThis.fetch = async (...args) => { if (String(args[0]) !== 'https://models.dev/api.json') return originalFetch(...args) refreshRequests++ return new Response(await fs.readFile(path.join(directory, 'build/resources/model-metadata/models.dev.json')), { headers: { 'content-type': 'application/json' } }) } try { await click('#settings-about-model-metadata button') await wait(`document.querySelector('.help-feedback-panel__metadata-feedback')?.textContent==='模型元数据已更新'`, 'metadata refreshed through About') check('About update calls the real metadata service once', refreshRequests, 1) } finally { globalThis.fetch = originalFetch } check('no renderer errors', rendererErrors, []) const report = { result: checks.every(item => item.pass) ? 'PASS' : 'FAIL', scope: 'Real isolated built UI/preload/IPC, bundled metadata, encrypted storage and actual native OMP against loopback HTTP; no external model calls.', source, profiles, requests, checks, rendererErrors } await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify(report, null, 2)) console.log('CUSTOM_MODEL_UI_' + report.result + ' ' + directory) for (const failure of checks.filter(item => !item.pass)) console.log('CUSTOM_MODEL_UI_FAILURE ' + JSON.stringify(failure)) for (const server of servers) server.closeAllConnections() app.exit(report.result === 'PASS' ? 0 : 1) })().catch(async error => { await fs.writeFile(path.join(directory, 'failure.json'), JSON.stringify({ phase, error: String(error), stack: error.stack, checks, requests, rendererErrors }, null, 2)); console.error(error) if (settings && !settings.isDestroyed()) await shot('failure').catch(() => undefined) for (const server of servers) server.closeAllConnections() app.exit(1) }) }