Files
ycode/scripts/verify-personalization-settings.cjs

391 lines
32 KiB
JavaScript
Raw Permalink 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 13 验收:设置 → 个性化页的 1:1 结构/几何 + 身份编辑/回复风格/开关/长文本弹窗 + 持久化与主题。
// 真实隔离 YCode(真实 preload/IPC/渲染进程/文件系统)+ 原生设置窗口,视口 900×680(与原版一致)。
//
// 原版依据(REA ev_af920…/ev_88af07…/ev_d6e0a2…/ev_523e07…/ev_4f4f76…):
// 面板 [232,40,638,1160];身份卡 638×100(两行 [244,48,614,34]、胶囊 172×34);
// 身份编辑输入 [607,49,177,32]、编辑行 [607,48,251,34];图标为 REA svg_nodes 原路径。
// 回复偏好卡 638×204(回复风格 select 120×32 @734,174;开关 34×20 @820,248/316);
// 指令/人设/长期记忆三张长卡 638×264(y=376/656/936),预览 598×160;编辑弹窗 [130,80,640,520]。
const fs = require('node:fs/promises')
const path = require('node:path')
const assert = require('node:assert/strict')
const TONE_LABELS = ['默认', '专业严谨', '亲和友善', '直言不讳', '天马行空', '高效务实', '毒舌吐槽', '启发引导']
const TONE_HINTS = ['不设定特定风格。', '清晰、准确、值得信赖。', '温暖、平易近人、鼓励支持。', '简明扼要、不废话、直击要点。',
'富有想象力、善用比喻类比。', '最少文字、最大信息量。', '犀利吐槽、但绝不伤人。', '用提问引导思考、授人以渔。']
const CLOSE_ICON_PATH = 'M3.46967 3.46967C3.76256 3.17678 4.23744 3.17678 4.53033 3.46967L8 6.93934L11.4697 3.46967C11.7626 3.17678 12.2374 3.17678 12.5303 3.46967C12.8232 3.76256 12.8232 4.23744 12.5303 4.53033L9.06066 8L12.5303 11.4697C12.8232 11.7626 12.8232 12.2374 12.5303 12.5303C12.2374 12.8232 11.7626 12.8232 11.4697 12.5303L8 9.06066L4.53033 12.5303C4.23744 12.8232 3.76256 12.8232 3.46967 12.5303C3.17678 12.2374 3.17678 11.7626 3.46967 11.4697L6.93934 8L3.46967 4.53033C3.17678 4.23744 3.17678 3.76256 3.46967 3.46967Z'
const CHECK_ICON_PATH = 'M5.1314 6.7172L11.4243 0.4243L10.5757 -0.4243L4.2828 5.8686L4.2732 5.8783Q4.0908 6.0607 4 6.1423Q3.9092 6.0607 3.7268 5.8783L3.7172 5.8686L0.4243 2.5757L-0.4243 3.4243L2.8686 6.7172L2.8783 6.7268Q3.1924 7.041 3.3327 7.1476Q3.6556 7.3929 4 7.3929Q4.3444 7.3929 4.6673 7.1476Q4.8076 7.041 5.1217 6.7268L5.1314 6.7172Z'
if (!process.versions.electron) {
;(async () => {
const directory = await fs.mkdtemp(path.resolve('.tmp/personalization-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_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('PERSONALIZATION_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 = 0.75) => 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 setInput = (selector, value) => `(()=>{const input=document.querySelector(${JSON.stringify(selector)})
const proto=input instanceof HTMLTextAreaElement?HTMLTextAreaElement.prototype:HTMLInputElement.prototype
Object.getOwnPropertyDescriptor(proto,'value').set.call(input,${JSON.stringify(value)})
input.dispatchEvent(new Event('input',{bubbles:true}));return true})()`
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('.personalization-panel')
return {
panel: rect(panel),
identityCard: rect(document.querySelector('.local-memory-identity-card')),
rows: [...document.querySelectorAll('.local-memory-identity-row')].map(row => ({
id: row.id,
label: row.querySelector('.local-memory-identity-row__label')?.textContent?.trim(),
rect: rect(row),
pill: (() => { const node = row.querySelector('.local-memory-pill')
return node ? { rect: rect(node), text: node.textContent.trim(), placeholder: !!node.querySelector('.local-memory-pill__placeholder') } : null })(),
input: (() => { const node = row.querySelector('.local-memory-inline__input')
return node ? { rect: rect(node), value: node.value } : null })(),
actions: row.querySelectorAll('.local-memory-inline__icon-btn').length,
confirmDisabled: row.querySelector('.local-memory-inline__icon-btn--confirm')?.disabled ?? null
})),
preference: rect(document.querySelector('.general-settings-panel__group-card')),
sections: [...document.querySelectorAll('.general-settings-panel__group-card > .general-settings-panel__section')].map(section => ({
id: section.id,
title: section.querySelector('.general-settings-panel__section-title')?.textContent?.trim(),
desc: section.querySelector('.general-settings-panel__section-desc')?.textContent?.trim(),
rect: rect(section),
select: rect(section.querySelector('.wb-select')),
switch: rect(section.querySelector('.wb-switch__track')),
switchChecked: section.querySelector('.wb-switch__input')?.checked ?? null,
switchDisabled: section.querySelector('.wb-switch__input')?.disabled ?? null,
unavailable: section.querySelector('[data-settings-unavailable=true]')?.getAttribute('title')
})),
longCards: [...document.querySelectorAll('.local-memory-long-card')].map(card => ({
id: card.id,
rect: rect(card),
title: card.querySelector('.general-settings-panel__section-title')?.textContent?.trim(),
desc: card.querySelector('.general-settings-panel__section-desc')?.textContent?.trim(),
edit: rect(card.querySelector('.general-settings-panel__section-action .wb-button')),
preview: rect(card.querySelector('.local-memory-long__preview')),
text: card.querySelector('.local-memory-long__value,.local-memory-long__placeholder')?.textContent?.trim(),
placeholder: !!card.querySelector('.local-memory-long__placeholder')
})),
toneLabel: document.querySelector('.personalization-panel__tone-trigger .wb-select__value')?.textContent?.trim(),
toneTrigger: (() => { const node = document.querySelector('.personalization-panel__tone-trigger')
return node ? { role: node.getAttribute('role'), valueClass: node.querySelector('.wb-select__value')?.firstElementChild?.className,
nameClass: node.querySelector('.personalization-panel__option-name')?.className } : null })(),
popover: rect(document.querySelector('.personalization-panel__tone-popover')),
options: [...document.querySelectorAll('.personalization-panel__tone-popover [role=option]')].map(option => ({
name: option.querySelector('.personalization-panel__option-name')?.textContent?.trim(),
hint: option.querySelector('.personalization-panel__option-hint')?.textContent?.trim(),
selected: option.getAttribute('aria-selected')
})),
modal: rect(document.querySelector('.local-memory-edit-modal')),
modalTitle: document.querySelector('.local-memory-edit-modal .wb-modal__title')?.textContent?.trim(),
editorValue: document.querySelector('.local-memory-editor')?.value ?? null,
editorPlaceholder: document.querySelector('.local-memory-editor')?.getAttribute('placeholder') ?? null,
modalButtons: [...document.querySelectorAll('.local-memory-edit-modal .wb-modal__footer .wb-button')].map(button => ({ text: button.textContent.trim(), rect: rect(button) })),
rowBackground: getComputedStyle(document.querySelector('.local-memory-identity-card')).backgroundColor,
rowColor: getComputedStyle(document.querySelector('.local-memory-identity-row__label')).color,
loadingTips: localStorage.getItem('ycode:loading-tips'),
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('open-personalization-settings')
await openSettings('personalization'); observe(settings)
await wait('!!document.querySelector(".personalization-panel")', 'personalization panel', settings)
const nav = await read(`(()=>{const item=document.querySelector('[data-settings-tab="personalization"]')
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, 'personalization'])
const light = await read(layoutSource, settings)
snapshots.light = light
await fs.writeFile(path.join(directory, 'personalization-light.png'), (await settings.webContents.capturePage()).toPNG())
check('panel keeps the recovered 638x1160 box at 232,40',
[near(light.panel.x, 232), near(light.panel.y, 40), near(light.panel.width, 638), near(light.panel.height, 1160)], [true, true, true, true])
check('identity card keeps 638x100 with two 614x34 rows',
[near(light.identityCard.width, 638), near(light.identityCard.height, 100),
near(light.rows[0].rect.y, 48), near(light.rows[0].rect.width, 614), near(light.rows[0].rect.height, 34),
near(light.rows[1].rect.y, 98)], [true, true, true, true, true, true])
check('identity rows keep the recovered labels and empty pills',
[light.rows.map(row => row.label), light.rows.every(row => row.pill?.text === '暂无内容,点击编辑添加' && row.pill?.placeholder),
light.rows.every(row => row.pill && near(row.pill.rect.x, 686) && near(row.pill.rect.width, 172) && near(row.pill.rect.height, 34))],
[['YCode 对你的称呼', 'YCode 的名字'], true, true])
check('reply preference card keeps 638x204 with three recovered rows',
[near(light.preference.y, 156), near(light.preference.width, 638), near(light.preference.height, 204),
light.sections.map(section => section.title)], [true, true, true, ['回复风格', '欢迎语', '显示文件修改过程']])
check('reply style select keeps 120x32 and the default tone',
[near(light.sections[0].select.x, 734), near(light.sections[0].select.y, 174), near(light.sections[0].select.width, 120),
near(light.sections[0].select.height, 32), light.toneLabel], [true, true, true, true, '默认'])
check('switches keep 34x20 with 欢迎语 on and 显示文件修改过程 disabled',
[[near(light.sections[1].switch.width, 34), near(light.sections[1].switch.height, 20), light.sections[1].switchChecked].every(Boolean),
light.sections[2].switchChecked === false && light.sections[2].switchDisabled === true && light.sections[2].unavailable === '暂未接入'], [true, true])
check('three long cards keep 638x264 at y=376/656/936',
light.longCards.map((card, index) => [near(card.rect.y, [376, 656, 936][index]), near(card.rect.width, 638), near(card.rect.height, 264)].every(Boolean)),
[true, true, true])
check('long cards keep the recovered titles, descriptions and 598x160 previews',
[light.longCards.map(card => card.title), light.longCards.map(card => card.desc),
light.longCards.every(card => near(card.preview.width, 598) && near(card.preview.height, 160)),
light.longCards.every(card => card.edit && near(card.edit.width, 68) && near(card.edit.height, 32))], [
['给 YCode 的指令', '人设设定', '长期记忆'],
['写下你希望 YCode 一直遵守的要求,之后每次对话和任务都会默认生效。', '设定 YCode 的身份、性格与表达风格,对所有回复生效。', '跨会话保存的重要信息与结论,可随时查看和修改。'],
true, true])
check('long previews keep their original content and placeholders',
[light.longCards[0].placeholder, light.longCards[0].text, light.longCards[1].text?.startsWith("You're not a chatbot."), light.longCards[2].placeholder],
[true, '例如:回答尽量简洁,先给结论', true, true])
await step('personalization-tone')
check('回复风格 trigger keeps the recovered combobox value structure',
[light.toneTrigger?.role, light.toneTrigger?.valueClass, light.toneTrigger?.nameClass],
['combobox', 'personalization-panel__option-text', 'personalization-panel__option-name'])
await read(`document.querySelector('.personalization-panel__tone-trigger').click()`, settings)
const popover = await wait(`(()=>{const node=document.querySelector('.personalization-panel__tone-popover')
return node?{rect:(()=>{const r=node.getBoundingClientRect();return {x:r.x,width:r.width}})(),options:[...node.querySelectorAll('[role=option]')].length}:null})()`, 'tone popover', settings)
check('回复风格 popover keeps the recovered 240px width and 8 options', [near(popover.rect.width, 240), popover.options], [true, 8])
const options = await read(`[...document.querySelectorAll('.personalization-panel__tone-popover [role=option]')].map(option => [
option.querySelector('.personalization-panel__option-name')?.textContent?.trim(),
option.querySelector('.personalization-panel__option-hint')?.textContent?.trim()])`, settings)
check('tone options keep every recovered label and hint', [options.map(item => item[0]), options.map(item => item[1])], [TONE_LABELS, TONE_HINTS])
await read(`[...document.querySelectorAll('.personalization-panel__tone-popover [role=option]')][1].click()`, settings)
await wait(`document.querySelector('.personalization-panel__tone-trigger .wb-select__value')?.textContent?.trim()==='专业严谨'`, 'tone applied', settings)
const toneStored = await read(`window.ycode.personalization.get().then(view => view.tone)`, settings)
check('choosing 专业严谨 persists the tone', toneStored, 'professional')
await step('personalization-switches')
await read(`(()=>{window.__loadingTipsStorageEvents=0
window.__loadingTipsStorageProbe=event=>{if(event.key==='ycode:loading-tips')window.__loadingTipsStorageEvents++}
window.addEventListener('storage',window.__loadingTipsStorageProbe)
return true})()`, main)
await read(`document.querySelector('#settings-loading-tips .wb-switch__input').click()`, settings)
await wait(`localStorage.getItem('ycode:loading-tips')==='0'`, 'loading tips mirror', settings)
await wait(`localStorage.getItem('ycode:loading-tips')==='0'&&window.__loadingTipsStorageEvents>0`, 'loading tips cross-window mirror', main)
check('欢迎语 switches off, persists and mirrors to the renderer',
[await read(`window.ycode.personalization.get().then(view => view.loadingTips)`, settings), await read(`localStorage.getItem('ycode:loading-tips')`, settings)], [false, '0'])
check('欢迎语关闭在另一个窗口触发 storage 同步', [await read(`localStorage.getItem('ycode:loading-tips')`, main),
await read(`window.__loadingTipsStorageEvents`, main)], ['0', 1])
await read(`document.querySelector('#settings-loading-tips .wb-switch__input').click()`, settings)
await wait(`localStorage.getItem('ycode:loading-tips')==='1'`, 'loading tips restored', settings)
await wait(`localStorage.getItem('ycode:loading-tips')==='1'&&window.__loadingTipsStorageEvents>1`, 'loading tips cross-window restored', main)
check('欢迎语 switches back on', await read(`window.ycode.personalization.get().then(view => view.loadingTips)`, settings), true)
check('欢迎语开启同样跨窗口同步', [await read(`localStorage.getItem('ycode:loading-tips')`, main),
await read(`window.__loadingTipsStorageEvents`, main)], ['1', 2])
await read(`document.querySelector('#settings-file-changes .wb-switch__input').click()`, settings)
check('显示文件修改过程 stays disabled and keeps the stored default', await read(`window.ycode.personalization.get().then(view => view.showFileChanges)`, settings), false)
await step('personalization-identity')
await read(`document.querySelector('#settings-local-memory-user-call-name .local-memory-pill').click()`, settings)
const editing = await wait(`(()=>{const row=document.querySelector('#settings-local-memory-user-call-name')
const input=row.querySelector('.local-memory-inline__input')
if(!input)return null
const r=input.getBoundingClientRect()
const edit=row.querySelector('.local-memory-identity-row__edit')
const er=edit.getBoundingClientRect()
return { width:r.width, height:r.height, editWidth:er.width, editHeight:er.height,
inputClass:input.className, inputRadius:getComputedStyle(input).borderRadius,
actions:row.querySelectorAll('.local-memory-inline__icon-btn').length,
confirmDisabled:row.querySelector('.local-memory-inline__icon-btn--confirm')?.disabled,
buttons:[...row.querySelectorAll('.local-memory-inline__icon-btn')].map(button=>({
className:button.className, width:button.getBoundingClientRect().width, height:button.getBoundingClientRect().height,
iconClass:button.querySelector('.wb-button__icon')?.className,
iconFill:button.querySelector('svg')?.getAttribute('fill'),
path:button.querySelector('path')?.getAttribute('d'),
transform:button.querySelector('path')?.getAttribute('transform') ?? null
})) }})()`, 'identity editing', settings)
check('clicking the pill opens the recovered 177x32 input inside the 251x34 inline editor',
[near(editing.width, 177), near(editing.height, 32), near(editing.editWidth, 251), near(editing.editHeight, 34),
editing.inputClass.split(/\s+/).includes('wb-input') && editing.inputClass.split(/\s+/).includes('wb-input--medium'),
editing.inputRadius, editing.actions, editing.confirmDisabled], [true, true, true, true, true, '8px', 2, true])
await fs.writeFile(path.join(directory, 'identity-edit.png'), (await settings.webContents.capturePage()).toPNG())
check('inline editor action buttons keep the recovered 34x34 ghost geometry and exact icon wrappers',
[editing.buttons.every(button => near(button.width, 34) && near(button.height, 34)),
editing.buttons.every(button => button.className.includes('wb-button--ghost') && button.className.includes('wb-button--small') && button.className.includes('wb-button--icon-only')),
editing.buttons.map(button => button.iconClass),
editing.buttons.map(button => button.iconFill),
editing.buttons.map(button => button.path),
editing.buttons[1].transform],
[true, true,
['wb-button__icon wb-button__icon--left', 'wb-button__icon wb-button__icon--left'],
['currentColor', 'none'], [CLOSE_ICON_PATH, CHECK_ICON_PATH], 'matrix(1 0 0 1 3 3.99854)'])
await read(setInput('#settings-local-memory-user-call-name .local-memory-inline__input', '小明'), settings)
await pause(120)
await read(`document.querySelector('#settings-local-memory-user-call-name .local-memory-inline__icon-btn--confirm').click()`, settings)
const savedIdentity = await wait(`(()=>{const pill=document.querySelector('#settings-local-memory-user-call-name .local-memory-pill')
return pill?pill.textContent.trim():null})()`, 'identity saved', settings)
check('confirming a non-empty identity persists and renders it',
[savedIdentity, await read(`window.ycode.personalization.get().then(view => view.userCallName)`, settings)], ['小明', '小明'])
await read(`document.querySelector('#settings-local-memory-ai-name .local-memory-pill').click()`, settings)
await wait(`!!document.querySelector('#settings-local-memory-ai-name .local-memory-inline__input')`, 'ai name editor', settings)
await read(setInput('#settings-local-memory-ai-name .local-memory-inline__input', '临时草稿'), settings)
// 真实焦点离开本行(点不可聚焦的 div 不会触发 blur,这里把焦点交给回复风格按钮)。
await read(`document.querySelector('.personalization-panel__tone-trigger').focus();true`, settings)
const afterBlur = await wait(`(()=>{const row=document.querySelector('#settings-local-memory-ai-name')
return row.querySelector('.local-memory-inline__input')?null:row.querySelector('.local-memory-pill')?.textContent?.trim()})()`, 'blur discards draft', settings)
check('blurring outside the row discards the identity draft like the original',
[afterBlur, await read(`window.ycode.personalization.get().then(view => view.aiName)`, settings)], ['暂无内容,点击编辑添加', ''])
await step('personalization-editors')
await read(`document.querySelector('#settings-local-memory-ai-name .local-memory-pill').click()`, settings)
await wait(`!!document.querySelector('#settings-local-memory-ai-name .local-memory-inline__input')`, 'ai name editor again', settings)
await read(setInput('#settings-local-memory-ai-name .local-memory-inline__input', '小助手'), settings)
await pause(100)
await read(`document.querySelector('#settings-local-memory-ai-name .local-memory-inline__icon-btn--confirm').click()`, settings)
await wait(`document.querySelector('#settings-local-memory-ai-name .local-memory-pill')?.textContent?.trim()==='小助手'`, 'ai name saved', settings)
await read(`document.querySelector('#settings-custom-prompt .local-memory-long__preview').click()`, settings)
const modal = await wait(`(()=>{const node=document.querySelector('.local-memory-edit-modal')
if(!node)return null
const r=node.getBoundingClientRect()
return { x:r.x, y:r.y, width:r.width, height:r.height,
title:node.querySelector('.wb-modal__title')?.textContent?.trim(),
placeholder:node.querySelector('.local-memory-editor')?.getAttribute('placeholder'),
close:(()=>{const button=node.querySelector('.wb-modal__close'), br=button?.getBoundingClientRect()
return button?{className:button.className,width:br.width,height:br.height,
iconWidth:button.querySelector('svg')?.getBoundingClientRect().width,
iconHeight:button.querySelector('svg')?.getBoundingClientRect().height,
iconFill:button.querySelector('svg')?.getAttribute('fill'),
pathFillRule:button.querySelector('path')?.getAttribute('fill-rule')}:null})(),
buttons:[...node.querySelectorAll('.wb-modal__footer .wb-button')].map(button=>({text:button.textContent.trim(),width:button.getBoundingClientRect().width,height:button.getBoundingClientRect().height})) }})()`, 'prompt editor modal', settings)
snapshots.modal = modal
await fs.writeFile(path.join(directory, 'personalization-editor.png'), (await settings.webContents.capturePage()).toPNG())
check('指令 editor keeps the recovered 640x520 modal with 保存/取消 68x32 buttons',
[[near(modal.x, 130), near(modal.y, 80), near(modal.width, 640), near(modal.height, 520)].every(Boolean),
modal.title, modal.buttons.map(button => button.text), modal.buttons.every(button => near(button.width, 68) && near(button.height, 32))],
[true, '给 YCode 的指令', ['取消', '保存'], true])
check('empty 指令 editor keeps the recovered placeholder', [modal.placeholder], ['当前内容为空,点击开始输入'])
check('指令 modal close control keeps the recovered 24x24 shell and exact filled X icon',
[near(modal.close.width, 24), near(modal.close.height, 24), modal.close.iconWidth,
modal.close.iconHeight, modal.close.iconFill, modal.close.pathFillRule,
modal.close.className.includes('wb-button--circle')],
[true, true, 16, 16, 'currentColor', 'evenodd', true])
await read(setInput('.local-memory-editor', '先给结论,再给依据。'), settings)
await pause(120)
await read(`[...document.querySelectorAll('.local-memory-edit-modal .wb-modal__footer .wb-button')].find(button=>button.textContent.trim()==='保存').click()`, settings)
await wait(`!document.querySelector('.local-memory-edit-modal')`, 'modal closed after save', settings)
const promptCard = await read(`(()=>{const card=document.querySelector('#settings-custom-prompt')
return { text:card.querySelector('.local-memory-long__value')?.textContent?.trim(), placeholder:!!card.querySelector('.local-memory-long__placeholder') }})()`, settings)
check('saving the 指令 updates the preview and persists',
[promptCard.text, promptCard.placeholder, await read(`window.ycode.personalization.get().then(view => view.customPrompt)`, settings)],
['先给结论,再给依据。', false, '先给结论,再给依据。'])
await read(`document.querySelector('#settings-local-memory-soul .local-memory-long__preview').click()`, settings)
const personaOpen = await wait(`(()=>{const node=document.querySelector('.local-memory-edit-modal')
return node?{title:node.querySelector('.wb-modal__title')?.textContent?.trim(), value:node.querySelector('.local-memory-editor')?.value}:null})()`, 'persona editor', settings)
check('人设 editor opens with the recovered default text',
[personaOpen.title, personaOpen.value?.startsWith("You're not a chatbot.")], ['人设设定', true])
await read(`[...document.querySelectorAll('.local-memory-edit-modal .wb-modal__footer .wb-button')].find(button=>button.textContent.trim()==='取消').click()`, settings)
await wait(`!document.querySelector('.local-memory-edit-modal')`, 'persona modal cancelled', settings)
check('cancelling the persona editor keeps the stored default', await read(`window.ycode.personalization.get().then(view => view.persona.startsWith("You're not a chatbot."))`, settings), true)
await step('personalization-persistence')
const config = JSON.parse(await fs.readFile(path.join(directory, 'data', 'config.json'), 'utf8'))
snapshots.config = config.personalization
check('config.json keeps the identity, tone and prompt values',
[config.personalization?.userCallName, config.personalization?.aiName, config.personalization?.tone, config.personalization?.customPrompt,
config.personalization?.loadingTips], ['小明', '小助手', 'professional', '先给结论,再给依据。', true])
settings.close()
await pause(500)
await openSettings('personalization')
const reopened = await read(layoutSource, settings)
check('reopened settings restores the persisted personalization',
[reopened.rows[0].pill?.text, reopened.rows[1].pill?.text, reopened.toneLabel, reopened.longCards[0].text],
['小明', '小助手', '专业严谨', '先给结论,再给依据。'])
await step('personalization-dark-theme')
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('personalization')
await wait(`document.documentElement.dataset.theme==='dark'&&!!document.querySelector('.personalization-panel')`, 'dark personalization panel', settings)
const dark = await read(layoutSource, settings)
snapshots.dark = dark
await fs.writeFile(path.join(directory, 'personalization-dark.png'), (await settings.webContents.capturePage()).toPNG())
check('dark theme keeps the recovered card surface and text',
[dark.rowBackground, dark.rowColor], ['rgb(31, 31, 31)', 'rgb(242, 243, 245)'])
check('dark theme keeps every recovered geometry',
[near(dark.panel.width, 638), near(dark.panel.height, 1160), near(dark.preference.height, 204),
dark.longCards.every(card => near(card.preview.width, 598) && near(card.preview.height, 160))], [true, true, true, true])
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('personalization-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('PERSONALIZATION_SETTINGS_' + result + ' ' + directory)
for (const failure of checks.filter(item => !item.pass)) console.log('PERSONALIZATION_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)
})
}