chore: establish YCode replication baseline and TODO
This commit is contained in:
commit
6dc59f89ba
1118 files changed
+196002
No files matched your search
@@ -0,0 +1,317 @@
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user