Files
ycode/scripts/verify-agents-settings.cjs
T

188 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// TODO 15 验收:智能体团队管理布局、真实持久化和深浅主题。
// 2026-10-18 用户要求移除经济模式与浏览器操控,页面仅保留 638x90 团队管理卡。
// 真实隔离 YCode(preload/IPC/渲染进程/文件系统)+ 原生设置窗口,视口 900x680。
const fs = require('node:fs/promises')
const path = require('node:path')
const assert = require('node:assert/strict')
if (!process.versions.electron) {
;(async () => {
const directory = await fs.mkdtemp(path.resolve('.tmp/agents-settings-'))
const data = path.join(directory, 'data'), home = path.join(directory, 'home')
await fs.mkdir(data, { recursive: true })
await fs.mkdir(home, { recursive: true })
const env = { ...process.env, YCODE_QA_DIRECTORY: directory, YCODE_USER_DATA_DIR: data, YCODE_OMP_HOME: home,
YCODE_DISABLE_GPU: '1', YCODE_DISABLE_SYSTEM_INTEGRATION: '1' }
delete env.ELECTRON_RUN_AS_NODE; delete env.ELECTRON_RENDERER_URL
const child = require('node:child_process').spawn(require('electron'),
['--no-sandbox', '--disable-gpu-sandbox', __filename], { env, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
let output = ''
child.stdout.on('data', bytes => { output += bytes; if (bytes.toString().includes('AGENTS_SETTINGS_')) process.stdout.write(bytes) })
child.stderr.on('data', bytes => { output += bytes })
const timer = setTimeout(() => child.kill(), 300000)
child.once('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(-4000))
process.exitCode = code ? 1 : 0
})
})().catch(error => { console.error(error); process.exitCode = 1 })
} else {
const { app, BrowserWindow } = require('electron')
const { pathToFileURL } = require('node:url')
const directory = process.env.YCODE_QA_DIRECTORY
app.setPath('home', path.join(directory, 'home'))
const checks = [], snapshots = {}, rendererErrors = []
let main, settings, phase = 'boot'
const pause = ms => new Promise(resolve => setTimeout(resolve, ms))
async function read(source, win = main) {
let timer
try {
return await Promise.race([win.webContents.executeJavaScript(source), new Promise((_, reject) => {
timer = setTimeout(() => reject(Error('Renderer timeout: ' + source.slice(0, 120))), 15000)
})])
} finally { clearTimeout(timer) }
}
async function wait(source, label, win = main) {
for (let index = 0; index < 300; index++) { const value = await read(source, win); if (value) return value; await pause(40) }
throw Error('Timeout: ' + label)
}
async function step(label) { phase = label; await fs.writeFile(path.join(directory, 'phase.txt'), label) }
function check(label, actual, expected) {
const pass = typeof expected === 'function' ? expected(actual) : JSON.stringify(actual) === JSON.stringify(expected)
checks.push({ label, pass, actual, expected: typeof expected === 'function' ? expected.toString() : expected })
}
const near = (value, target, tolerance = 1) => Math.abs(value - target) <= tolerance
function observe(win) {
win.webContents.on('console-message', event => { if (event.level === 'error' || event.level >= 3) rendererErrors.push({ windowId: win.id, message: event.message }) })
}
async function openSettings(section) {
await read(`window.ycode.app.openSettings(${JSON.stringify(section)})`)
for (let index = 0; index < 200; index++) {
settings = BrowserWindow.getAllWindows().find(win => win !== main)
if (settings && !settings.webContents.isLoading()) break
await pause(25)
}
assert.ok(settings, 'settings window')
await wait('!!document.querySelector(".settings-window-root")', 'settings root', settings)
await pause(400)
return settings
}
const layoutSource = `(()=>{
const round = value => Math.round(value * 1000) / 1000
const rect = node => { if (!node) return null; const r = node.getBoundingClientRect()
return { x: round(r.x), y: round(r.y), width: round(r.width), height: round(r.height) } }
const panel = document.querySelector('.extensions-settings-panel')
const cards = [...panel.querySelectorAll(':scope > .general-settings-panel__group-card')]
const sections = [...panel.querySelectorAll('.general-settings-panel__group-card > .general-settings-panel__section')]
const select = panel.querySelector('.extensions-settings-panel__team-dropdown .wb-select')
return {
panel: rect(panel), cards: cards.map(card => rect(card)),
sections: sections.map(section => ({ rect: rect(section),
title: section.querySelector('.general-settings-panel__section-title')?.textContent?.trim(),
desc: section.querySelector('.general-settings-panel__section-desc')?.textContent?.trim() })),
select: select ? { rect: rect(select), value: select.querySelector('.wb-select__value')?.textContent?.trim(), title: select.getAttribute('title') } : null,
obsoleteControls: panel.querySelectorAll('.wb-switch, .extensions-settings-panel__browser-group, .extensions-settings-panel__action-button').length,
obsoleteText: ['经济模式', '浏览器操控', '扩展程序', '前往安装'].filter(text => panel.textContent.includes(text)),
cardBackground: cards[0] ? getComputedStyle(cards[0]).backgroundColor : null,
titleColor: (() => { const node = panel.querySelector('.general-settings-panel__section-title'); return node ? getComputedStyle(node).color : null })(),
descColor: (() => { const node = panel.querySelector('.general-settings-panel__section-desc'); return node ? getComputedStyle(node).color : null })(),
selectBackground: select ? getComputedStyle(select).backgroundColor : null,
selectBorder: select ? getComputedStyle(select).borderTopColor : null,
theme: document.documentElement.dataset.theme ?? 'light'
}})()`
;(async () => {
await import(pathToFileURL(path.resolve('out/main/index.mjs')).href); await app.whenReady()
for (let index = 0; index < 200; index++) {
main = BrowserWindow.getAllWindows().find(win => win.getTitle() === 'YCode')
if (main && !main.webContents.isLoading()) break
await pause(25)
}
assert.ok(main, 'main window'); observe(main)
await wait('!!window.ycode', 'preload api')
await step('agents-settings')
await openSettings('extensions'); observe(settings)
await wait('!!document.querySelector(".extensions-settings-panel")', 'agents panel', settings)
const nav = await read(`(()=>{const item=document.querySelector('[data-settings-tab="extensions"]')
return { exists: !!item, disabled: item?.getAttribute('aria-disabled'), active: document.querySelector('.settings-navigation__item--active')?.dataset.settingsTab }})()`, settings)
check('智能体 nav entry is enabled and active', [nav.exists, nav.disabled !== 'true', nav.active], [true, true, 'extensions'])
const light = await read(layoutSource, settings)
snapshots.light = light
await fs.writeFile(path.join(directory, 'ycode-agents-light.png'), (await settings.webContents.capturePage()).toPNG())
check('agents panel only occupies the 638x90 team-management card at 232,40',
[near(light.panel.x, 232), near(light.panel.y, 40), near(light.panel.width, 638), near(light.panel.height, 90)], [true, true, true, true])
check('team-management keeps one 638x90 card with its 606x66 section',
[light.cards.length, near(light.cards[0].width, 638), near(light.cards[0].height, 90),
near(light.sections[0].rect.width, 606), near(light.sections[0].rect.height, 66), near(light.cards[0].y, 40)],
[1, true, true, true, true, true])
check('team-management reflects the active scheduler rules with a 120x32 select',
[light.sections[0].title, light.sections[0].desc,
near(light.select.rect.x, 734), near(light.select.rect.y, 69), near(light.select.rect.width, 120), near(light.select.rect.height, 32),
light.select.value, light.select.title],
['智能体团队管理', '开启后,复杂任务可由多个智能体协作完成。关闭后取消子任务,主对话继续。',
true, true, true, true, '开启', null])
check('economy mode and browser controls are fully removed',
[light.sections.map(section => section.title), light.obsoleteControls, light.obsoleteText],
[['智能体团队管理'], 0, []])
await step('agents-team-toggle')
await read(`document.querySelector('.extensions-settings-panel__team-dropdown .wb-select').click()`, settings)
const popover = await wait(`(()=>{const node=document.querySelector('.extensions-settings-panel__team-popover')
return node?{options:[...node.querySelectorAll('[role=option]')].map(option=>option.textContent.trim())}:null})()`, 'team popover', settings)
check('team-management dropdown offers exactly 开启 / 禁用', popover.options.map(text => text.replace(/\s+/g, '')), ['开启', '禁用'])
await read(`[...document.querySelectorAll('.extensions-settings-panel__team-popover [role=option]')].find(option=>option.textContent.trim()==='禁用').click()`, settings)
await wait(`document.querySelector('.extensions-settings-panel__team-dropdown .wb-select__value')?.textContent?.trim()==='禁用'`, 'team management disabled', settings)
const stored = await read(`window.ycode.agents.get().then(view => view.teamManagement)`, settings)
const config = JSON.parse(await fs.readFile(path.join(directory, 'data', 'config.json'), 'utf8'))
snapshots.config = config.agentSettings
check('selecting 禁用 persists the team-management preference', [stored, config.agentSettings?.teamManagement], [false, false])
await step('agents-persistence')
settings.close()
await pause(500)
await openSettings('extensions')
const reopened = await read(layoutSource, settings)
check('reopened settings restores the 禁用 preference', reopened.select.value, '禁用')
await step('agents-dark')
await openSettings('appearance')
await wait('!!document.querySelector(".appearance-card")', 'appearance panel', settings)
await read(`document.querySelector('[aria-label="深色"].appearance-card').click()`, settings)
await wait(`document.documentElement.dataset.theme==='dark'`, 'dark theme', settings)
await openSettings('extensions')
await wait(`document.documentElement.dataset.theme==='dark'&&!!document.querySelector('.extensions-settings-panel')`, 'dark agents panel', settings)
const dark = await read(layoutSource, settings)
snapshots.dark = dark
await fs.writeFile(path.join(directory, 'ycode-agents-dark.png'), (await settings.webContents.capturePage()).toPNG())
check('dark theme keeps the recovered card, text and select surfaces',
[dark.cardBackground, dark.titleColor, dark.descColor, dark.selectBackground, dark.selectBorder],
['rgb(31, 31, 31)', 'rgb(242, 243, 245)', 'rgba(255, 255, 255, 0.5)', 'rgb(31, 31, 31)', 'rgb(61, 61, 61)'])
check('dark theme only occupies the team-management card with removed controls absent',
[near(dark.panel.width, 638), near(dark.panel.height, 90), near(dark.cards[0].height, 90),
dark.sections.map(section => section.title), dark.obsoleteControls, dark.obsoleteText],
[true, true, true, ['智能体团队管理'], 0, []])
await openSettings('appearance')
await wait('!!document.querySelector(".appearance-card")', 'appearance panel again', settings)
await read(`document.querySelector('[aria-label="浅色"].appearance-card').click()`, settings)
await wait(`document.documentElement.dataset.theme==='light'`, 'light restored', settings)
await step('agents-errors')
check('no renderer error logs', rendererErrors, [])
const result = checks.every(item => item.pass) ? 'PASS' : 'FAIL'
await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify({ result, phase, checks, snapshots }, null, 2))
console.log('AGENTS_SETTINGS_' + result + ' ' + directory)
for (const failure of checks.filter(item => !item.pass)) console.log('AGENTS_SETTINGS_FAILURE ' + JSON.stringify(failure))
app.exit(result === 'PASS' ? 0 : 1)
})().catch(async error => {
console.error(error)
try { await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify({ result: 'ERROR', phase, checks, snapshots, error: String(error?.stack ?? error) }, null, 2)) } catch { /* ignore */ }
app.exit(1)
})
}