188 lines
12 KiB
JavaScript
188 lines
12 KiB
JavaScript
// 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)
|
||
})
|
||
}
|