// Production YCode UI -> real preload/IPC -> real OMP -> controlled loopback upstream. // No renderer-store writes, fake IPC, or direct API mutations of models/spaces/sessions/chat. const fs = require('node:fs/promises') const path = require('node:path') const assert = require('node:assert/strict') const tempRoot = path.resolve('.tmp') function assertContained(root, target) { const relative = path.relative(path.resolve(root), path.resolve(target)) assert.ok(relative && !relative.startsWith('..') && !path.isAbsolute(relative), `Unsafe QA path: ${target}`) } function assertQaDirectory(directory) { assertContained(tempRoot, directory) assert.match(path.basename(directory), /^product-flows-[A-Za-z0-9]+$/) } if (!process.versions.electron) { const { spawn } = require('node:child_process') ;(async () => { // Verify the intended location before the first filesystem mutation. assertContained(tempRoot, path.resolve('.tmp/product-flows-pending')) const directory = await fs.mkdtemp(path.join(tempRoot, 'product-flows-')) assertQaDirectory(directory) const data = path.join(directory, 'data'), ompHome = path.join(directory, 'home') assertContained(directory, data); assertContained(directory, ompHome) await fs.mkdir(data); await fs.mkdir(ompHome) const realOmp = process.env.YCODE_OMP_PATH || path.join(process.env.LOCALAPPDATA || '', 'Ygent', 'omp.exe') assert.ok((await fs.stat(realOmp)).isFile(), 'A real OMP executable is required') const baseEnv = { ...process.env, YCODE_USER_DATA_DIR: data, YCODE_OMP_HOME: ompHome, YCODE_QA_DIRECTORY: directory, YCODE_QA_REAL_OMP: realOmp, YCODE_DISABLE_GPU: '1', YCODE_QA_OFFSCREEN: '1' } delete baseEnv.ELECTRON_RUN_AS_NODE async function child(stage) { return new Promise(resolve => { const processChild = spawn(require('electron'), ['--no-sandbox', '--disable-gpu-sandbox', __filename], { env: { ...baseEnv, YCODE_QA_PRODUCT_STAGE: stage }, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }) let output = '' processChild.stdout.on('data', bytes => { output += bytes; if (bytes.toString().includes('PRODUCT_UI_')) process.stdout.write(bytes) }) processChild.stderr.on('data', bytes => { output += bytes }) const timer = setTimeout(() => processChild.kill(), 150000) processChild.once('exit', async code => { clearTimeout(timer) assertQaDirectory(directory) await fs.writeFile(path.join(directory, `${stage}-process.log`), output) if (code) console.error(output.slice(-7000)) resolve(code ?? 1) }) processChild.once('error', error => { console.error(error); resolve(1) }) }) } let code = await child('run') if (!code) code = await child('restart') console.log(JSON.stringify({ directory, result: code ? 'FAIL' : 'PASS', exitCode: code })) process.exitCode = code })().catch(error => { console.error(error); process.exitCode = 1 }) } else { const { app, BrowserWindow } = require('electron') // This must precede every await; enabling software rendering after ready can hang capturePage. app.disableHardwareAcceleration() const { createServer } = require('node:http') const { pathToFileURL } = require('node:url') const directory = process.env.YCODE_QA_DIRECTORY assertQaDirectory(directory) const expectedData = path.join(directory, 'data'), expectedHome = path.join(directory, 'home') assert.equal(path.resolve(process.env.YCODE_USER_DATA_DIR), path.resolve(expectedData)) assert.equal(path.resolve(process.env.YCODE_OMP_HOME), path.resolve(expectedHome)) app.setPath('userData', expectedData); app.setPath('sessionData', expectedData); app.setPath('home', expectedHome) assert.equal(path.resolve(app.getPath('userData')), path.resolve(expectedData)) process.env.YCODE_OMP_PATH = process.env.YCODE_QA_REAL_OMP const stage = process.env.YCODE_QA_PRODUCT_STAGE const requests = [], checks = [] let main, settings, server, currentPhase = 'boot' const pause = ms => new Promise(resolve => setTimeout(resolve, ms)) async function safeWrite(file, content) { assertQaDirectory(directory); assertContained(directory, file) assert.equal(path.resolve(app.getPath('userData')), path.resolve(expectedData)) await fs.writeFile(file, content) } async function phase(label) { currentPhase = label; await safeWrite(path.join(directory, `${stage}-phase.txt`), label) } async function read(expression, win = main) { let timer try { return await Promise.race([win.webContents.executeJavaScript(expression), new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('Renderer timeout: ' + expression.slice(0, 180))), 15000) })]) } catch (error) { throw new Error(`${error.message} — renderer expression: ${expression.slice(0, 240)}`) } finally { clearTimeout(timer) } } async function wait(expression, label, win = main) { for (let i = 0; i < 650; i++) { const value = await read(expression, win); if (value) return value; await pause(35) } throw new Error('Timed out: ' + label) } async function click(selector, win = main) { await read(`(()=>{const element=document.querySelector(${JSON.stringify(selector)});if(!element)throw Error('Missing clickable: '+${JSON.stringify(selector)});if(element.disabled)throw Error('Disabled clickable');element.scrollIntoView({block:'nearest'});element.click()})()`, win) await pause(40) } async function clickText(selector, text, win = main, exact = true) { await read(`(()=>{const text=${JSON.stringify(text)};const element=[...document.querySelectorAll(${JSON.stringify(selector)})].find(node=>${exact ? 'node.textContent.trim()===text' : 'node.textContent.includes(text)'});if(!element)throw Error('Missing clickable text: '+text);if(element.disabled)throw Error('Disabled clickable: '+text);element.scrollIntoView({block:'nearest'});element.click()})()`, win) await pause(40) } async function fill(selector, value, win = main) { await read(`(()=>{const input=document.querySelector(${JSON.stringify(selector)});if(!input||input.disabled)throw Error('Missing enabled input: '+${JSON.stringify(selector)});input.scrollIntoView({block:'nearest'});input.focus();const proto=input.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;Object.getOwnPropertyDescriptor(proto,'value').set.call(input,${JSON.stringify(value)});input.dispatchEvent(new Event('input',{bubbles:true}))})()`, win) await pause(30) } async function shot(name, win = main) { await read('document.fonts.ready.then(()=>true)', win) await pause(100) const image = await Promise.race([win.webContents.capturePage(undefined, { stayHidden: true, stayAwake: true }), pause(10000).then(() => { throw Error('Screenshot timeout: ' + name) })]) await safeWrite(path.join(directory, `${name}.png`), image.toPNG()) } async function openSettings() { await click('.cr-model-selector__trigger') await wait('!!document.querySelector(".cr-model-selector__menu")', 'main model picker') await clickText('.cr-model-selector__menu > .cr-menu-item', '配置自定义模型', main, false) for (let i = 0; i < 200; i++) { settings = BrowserWindow.getAllWindows().find(win => win !== main) if (settings && !settings.webContents.isLoading()) break await pause(30) } assert.ok(settings, 'UI opened a separate native settings window') await wait('!!document.querySelector(".models-settings-panel__add-button")', 'native model settings', settings) await wait('!!document.querySelector(".models-settings-panel__editor")', 'picker directly opens add model form', settings) assert.deepEqual(await read('[innerWidth,innerHeight]', settings), [900, 680]) } async function createSpace(name) { await click('[aria-label="新建工作空间"]') await wait('!!document.querySelector(".create-workspace-modal")', 'new workspace dialog') assert.equal(await read('document.querySelector(".create-workspace-modal__confirm").disabled'), true) await fill('input[aria-label="工作空间名称"]', name) await shot(`space-${name}-create`) await click('.create-workspace-modal__confirm') await wait('!document.querySelector(".create-workspace-modal")', 'workspace created') const spaces = await read('window.ycode.workspace.list()') const space = spaces.workspaces.find(item => item.name === name) assert.ok(space); assertContained(directory, space.root) assert.ok((await fs.stat(space.root)).isDirectory()) return space } async function startTask(space) { const before = (await read('window.ycode.session.list()')).length await click(`[data-workspace-id="${space.id}"] .workspace-new-task-button`) await wait('!!document.querySelector(".wb-composer--home")', 'space new-task home') const after = (await read('window.ycode.session.list()')).length assert.equal(after, before, 'new task does not persist before first send') assert.match(await read('document.querySelector(".cr-workspace-picker .cr-input-footer-item__label").textContent'), new RegExp(space.name)) return before } async function send(space, prompt, expectedCount) { await fill('.wb-composer__input', prompt) await wait('!document.querySelector(".cr-send-button:not(.cr-send-button--stop)")?.disabled', 'enabled send button') await click('.cr-send-button:not(.cr-send-button--stop)') const sessions = await wait(`window.ycode.session.list().then(s=>s.length===${expectedCount}&&s)`, 'first send creates exactly one session') const session = sessions.find(item => item.workspaceId === space.id) assert.ok(session); assert.equal(path.resolve(session.cwd), path.resolve(space.root)); assert.equal(session.modelId, 'ui-model-two') const terminal = await wait(`window.ycode.session.messages(${JSON.stringify(session.id)}).then(m=>m.length>=2&&m.at(-1)?.status!=='streaming'&&m.at(-1))`, 'real OMP terminal turn') assert.equal(terminal.status, 'done', JSON.stringify(terminal)) assert.equal(terminal.content, 'UI_REPLY_ui-model-two') await wait('document.querySelector(".chat-thread")?.textContent.includes("UI_REPLY_ui-model-two")', 'real streamed reply rendered') return session } async function selectSession(id) { await click(`[data-session-id="${id}"]`) await wait(`document.querySelector('[data-session-id="${id}"]')?.dataset.selected==='true'`, 'session selected') await pause(100) } async function openSentinel(expected) { if (await read(`!!document.querySelector('.source-chat-header button[title="打开右侧栏"]')`)) await click('.source-chat-header button[title="打开右侧栏"]') await wait('!!document.querySelector(".yc-workbench")', 'real workbench open') if (!(await read('!!document.querySelector(".source-code-preview,.yc-workbench-text")'))) { await click('[data-entry-key="fileTree"]') await wait('[...document.querySelectorAll(".yc-workbench-tree-node")].some(e=>e.textContent.includes("sentinel.txt"))', 'real workspace file tree') await clickText('.yc-workbench-tree-node', 'sentinel.txt', main, false) } await wait(`document.querySelector('.source-code-preview[data-source-code-state="ready"],.yc-workbench-text')?.textContent.includes(${JSON.stringify(expected)})`, 'session-owned sentinel preview ' + expected) const text = await read('document.querySelector(".source-code-preview,.yc-workbench-text").textContent') assert.ok(!text.includes(expected === 'SPACE_A' ? 'SPACE_B' : 'SPACE_A')) } async function boot() { await import(pathToFileURL(path.resolve('out/main/index.mjs')).href) await app.whenReady() for (let i = 0; i < 200; i++) { main = BrowserWindow.getAllWindows().find(win => win.getTitle() === 'YCode'); if (main && !main.webContents.isLoading()) break; await pause(30) } assert.ok(main) await wait('!!window.ycode && !!document.querySelector(".wb-composer__input")', 'production main UI') const info = await read('window.ycode.app.info()') assert.equal(path.resolve(info.configDir), path.resolve(expectedData)) assert.equal(info.omp.available, true, 'real OMP must be available; fallback-only success is insufficient') return info } ;(async () => { if (stage === 'restart') { await boot(); await phase('restart-persistence') const previous = JSON.parse(await fs.readFile(path.join(directory, 'report.json'), 'utf8')) const [models, spaces, sessions] = await Promise.all([read('window.ycode.models.list()'), read('window.ycode.workspace.list()'), read('window.ycode.session.list()')]) assert.equal(models.selectedModelId, 'ui-model-two') const model = models.models.find(item => item.id === 'ui-model-two') assert.ok(model?.hasApiKey); assert.equal(model.model, 'ui-model-two') assert.equal(spaces.workspaces.length, previous.spaces.length) for (const expected of previous.spaces) assert.ok(spaces.workspaces.some(space => space.id === expected.id && space.root === expected.root)) assert.equal(sessions.length, previous.sessions.length) for (const expected of previous.sessions) { const actual = sessions.find(item => item.id === expected.id) assert.equal(actual.workspaceId, expected.workspaceId); assert.equal(actual.cwd, expected.cwd) assert.equal((await read(`window.ycode.session.messages(${JSON.stringify(expected.id)})`)).at(-1).content, 'UI_REPLY_ui-model-two') } await wait('document.querySelector(".cr-model-selector__trigger")?.textContent.includes("ui-model-two")', 'persisted effective model UI') await shot('restart-main') previous.restart = { result: 'PASS', scope: 'new Electron process; models, secret availability, selected model, spaces, sessions, ownership and replies persisted' } await safeWrite(path.join(directory, 'report.json'), JSON.stringify(previous, null, 2)) console.log('PRODUCT_UI_RESTART_PASS ' + directory) app.quit(); return } server = createServer(async (request, response) => { try { let raw = ''; for await (const bytes of request) raw += bytes const body = JSON.parse(raw) const authenticated = request.headers.authorization === 'Bearer UI_LOOPBACK_KEY' requests.push({ path: request.url, model: body.model, stream: body.stream === true, authenticated }) if (!authenticated) { response.writeHead(401); response.end('{}'); return } const content = `UI_REPLY_${body.model}` if (!body.stream) { response.setHeader('content-type', 'application/json'); response.end(JSON.stringify({ choices: [{ message: { role: 'assistant', content } }] })); return } response.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache' }) response.write(`data: ${JSON.stringify({ id: 'ui-loopback', object: 'chat.completion.chunk', created: 1, model: body.model, choices: [{ index: 0, delta: { role: 'assistant', content }, finish_reason: null }] })}\n\n`) response.end(`data: ${JSON.stringify({ id: 'ui-loopback', object: 'chat.completion.chunk', created: 1, model: body.model, choices: [{ index: 0, delta: {}, finish_reason: 'stop' }], usage: { prompt_tokens: 20, completion_tokens: 5, total_tokens: 25 } })}\n\ndata: [DONE]\n\n`) } catch { response.writeHead(400); response.end('{}') } }) await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) const baseUrl = `http://127.0.0.1:${server.address().port}/v1` await boot(); await phase('model-ui-add') const initialModels = await read('window.ycode.models.list()') await openSettings() const firstSettingsId = settings.id await click('.models-settings-panel__editor-close', settings) await click('[data-settings-tab="appearance"]', settings) await openSettings() assert.equal(settings.id, firstSettingsId, 'existing native settings window is reused') await click('.models-settings-panel__editor-close', settings) await click('[data-settings-tab="appearance"]', settings) await click('[data-settings-tab="models"]', settings) assert.equal(await read('!!document.querySelector(".models-settings-panel__editor")', settings), false, 'consumed add intent does not reopen on normal navigation') checks.push('Main model picker directly opens add form in fresh/reused native settings; consumed intent stays closed on normal navigation') await click('.models-settings-panel__add-button', settings) await click('.models-settings-panel__provider-select', settings) await clickText('[role="option"]', '自定义', settings) await fill('input[aria-label="接口地址"]', baseUrl, settings) await fill('input[aria-label="API Key"]', 'UI_LOOPBACK_KEY', settings) await fill('input[aria-label="模型名称"]', 'ui-model-one', settings) const numberInputs = await read('[...document.querySelectorAll(".models-settings-panel__number-grid input")].map(e=>e.getAttribute("aria-label"))', settings) assert.equal(numberInputs.length, 2) await fill(`input[aria-label="${numberInputs[0]}"]`, '64000', settings) await fill(`input[aria-label="${numberInputs[1]}"]`, '4096', settings) await shot('model-add-complete', settings) await click('.models-settings-panel__editor-test-connection', settings) await wait('document.querySelector(".model-ui-toast")?.textContent.includes("连接成功")', 'model test success', settings) assert.equal((await read('window.ycode.models.list()')).models.length, initialModels.models.length, 'test connection must not save the model') checks.push('UI provider/endpoint/key/model/token entry and successful test without save') await click('.models-settings-panel__editor-save', settings) await wait('!document.querySelector(".models-settings-panel__editor")', 'model saved', settings) assert.equal((await read('window.ycode.models.list()')).models.find(item => item.id === 'ui-model-one').hasApiKey, true) await read(`(()=>{const row=[...document.querySelectorAll('.models-settings-panel__row')].find(e=>e.querySelector('.models-settings-panel__row-name')?.textContent==='ui-model-one');if(!row)throw Error('Missing saved row');row.querySelector('button[aria-label="编辑"]').click()})()`, settings) await wait('!!document.querySelector(".models-settings-panel__editor")', 'edit saved model', settings) assert.equal(await read(`document.querySelector('input[aria-label="API Key"]').value`, settings), '', 'edit preserves secret using a blank keep-value field') await fill('input[aria-label="模型名称"]', 'ui-model-two', settings) await click('.models-settings-panel__editor-save', settings) await wait('!document.querySelector(".models-settings-panel__editor")', 'edited model saved', settings) await shot('model-edited-list', settings) await click('.settings-window-close', settings) await wait('document.querySelector(".cr-model-selector__trigger")&&!document.querySelector(".cr-model-selector__trigger").disabled', 'main model picker available') await click('.cr-model-selector__trigger') await wait('[...document.querySelectorAll("[role=option]")].some(e=>e.textContent.includes("ui-model-two"))', 'updated custom model visible in main') await clickText('[role="option"]', 'ui-model-two', main, false) await wait('document.querySelector(".cr-model-selector__trigger")?.textContent.includes("ui-model-two")', 'edited model selected in composer') assert.equal((await read('window.ycode.models.list()')).selectedModelId, 'ui-model-two') checks.push('UI save/edit with retained key and main model selection') await phase('spaces-ui-first-send') const spaceA = await createSpace('UI空间甲') const countA = await startTask(spaceA) assertContained(directory, path.join(spaceA.root, 'sentinel.txt')) await safeWrite(path.join(spaceA.root, 'sentinel.txt'), 'SPACE_A') const sessionA = await send(spaceA, 'UI_CHECK_A: please reply with one short sentence.', countA + 1) await shot('dialogue-space-a') checks.push('UI space creation and first-send-only session persistence with real OMP reply') const spaceB = await createSpace('UI空间乙') const countB = await startTask(spaceB) assert.equal(await read('document.querySelector(".wb-composer__input").value'), '') await safeWrite(path.join(spaceB.root, 'sentinel.txt'), 'SPACE_B') const sessionB = await send(spaceB, 'UI_CHECK_B: please reply with one short sentence.', countB + 1) assert.equal(await read(`window.ycode.resources.readText('sentinel.txt',${JSON.stringify(sessionA.id)})`), 'SPACE_A') assert.equal(await read(`window.ycode.resources.readText('sentinel.txt',${JSON.stringify(sessionB.id)})`), 'SPACE_B') await phase('session-drafts-and-file-ui') await selectSession(sessionA.id); await fill('.wb-composer__input', 'UNSENT_SPACE_A') await openSentinel('SPACE_A'); await shot('file-space-a') await selectSession(sessionB.id) assert.equal(await read('document.querySelector(".wb-composer__input").value'), '') await fill('.wb-composer__input', 'UNSENT_SPACE_B') await openSentinel('SPACE_B'); await shot('file-space-b') await selectSession(sessionA.id) assert.equal(await read('document.querySelector(".wb-composer__input").value'), 'UNSENT_SPACE_A') await openSentinel('SPACE_A') await selectSession(sessionB.id) assert.equal(await read('document.querySelector(".wb-composer__input").value'), 'UNSENT_SPACE_B') checks.push('two spaces, same-name file isolation, restored per-session UI previews and independent unsent drafts') assert.ok(requests.some(request => request.model === 'ui-model-one' && !request.stream && request.authenticated)) assert.equal(requests.filter(request => request.model === 'ui-model-two' && request.stream && request.authenticated).length, 2) const profiles = await fs.readdir(path.join(expectedHome, '.omp', 'profiles')) assert.ok(profiles.length > 0) for (const profile of profiles) assert.ok(!(await fs.readFile(path.join(expectedHome, '.omp', 'profiles', profile, 'agent', 'models.yml'), 'utf8')).includes('UI_LOOPBACK_KEY')) const spaces = (await read('window.ycode.workspace.list()')).workspaces const sessions = await read('window.ycode.session.list()') const report = { result: 'PASS', scope: 'Real production YCode UI, native settings window, preload/IPC and real OMP with a loopback OpenAI-compatible service. No WorkBuddy pixel-parity or external supplier claim.', noStoreWrites: true, noFakeIpc: true, noDirectMutationApis: true, isolatedUserData: expectedData, isolatedOmpHome: expectedHome, checks, models: { tested: 'ui-model-one', savedEditedSelected: 'ui-model-two' }, spaces, sessions, requests } await safeWrite(path.join(directory, 'report.json'), JSON.stringify(report, null, 2)) console.log('PRODUCT_UI_FLOWS_PASS ' + directory) server.close(); app.quit() })().catch(async error => { console.error(error) if (main && !main.isDestroyed()) await shot('failure-main').catch(() => undefined) if (settings && !settings.isDestroyed()) await shot('failure-settings', settings).catch(() => undefined) await safeWrite(path.join(directory, `${stage}-failure.json`), JSON.stringify({ result: 'FAIL', phase: currentPhase, error: String(error), checks, requests }, null, 2)).catch(() => undefined) server?.close(); app.exit(1) }) }