494 lines
27 KiB
JavaScript
494 lines
27 KiB
JavaScript
// TODO 14 acceptance: WorkBuddy 5.8.3 memory settings, live YCode IPC, import flow and themes.
|
|
// Real isolated Electron, real preload/IPC/renderer/filesystem, viewport 900x680.
|
|
const fs = require('node:fs/promises')
|
|
const path = require('node:path')
|
|
const assert = require('node:assert/strict')
|
|
const { createHash } = require('node:crypto')
|
|
|
|
if (!process.versions.electron) {
|
|
;(async () => {
|
|
const directory = await fs.mkdtemp(path.resolve('.tmp/memory-settings-'))
|
|
const data = path.join(directory, 'data')
|
|
const 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('MEMORY_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(-5000))
|
|
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 = []
|
|
const snapshots = {}
|
|
const rendererErrors = []
|
|
let main
|
|
let settings
|
|
let phase = 'boot'
|
|
const pause = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds))
|
|
|
|
async function read(source, win = settings || 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 {
|
|
if (timer) clearTimeout(timer)
|
|
}
|
|
}
|
|
|
|
async function wait(source, label, win = settings || main) {
|
|
for (let index = 0; index < 350; 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
|
|
|
|
async function readJson(file) {
|
|
let lastError
|
|
for (let index = 0; index < 80; index++) {
|
|
try {
|
|
const text = await fs.readFile(file, 'utf8')
|
|
if (text.trim()) return JSON.parse(text)
|
|
} catch (error) {
|
|
lastError = error
|
|
}
|
|
await pause(25)
|
|
}
|
|
throw lastError ?? Error(`JSON file stayed empty: ${file}`)
|
|
}
|
|
|
|
async function waitForJson(file, predicate, label) {
|
|
let lastValue
|
|
for (let index = 0; index < 350; index++) {
|
|
try {
|
|
lastValue = await readJson(file)
|
|
if (predicate(lastValue)) return lastValue
|
|
} catch (error) {
|
|
lastValue = error
|
|
}
|
|
await pause(40)
|
|
}
|
|
throw Error(`Timeout: ${label}; last value: ${JSON.stringify(lastValue)}`)
|
|
}
|
|
|
|
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)})`, main)
|
|
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 style = node => node ? getComputedStyle(node) : null
|
|
const panel = document.querySelector('.memory-settings-panel')
|
|
const group = document.querySelector('.memory-settings-panel__generate-memory-group')
|
|
const rows = [...document.querySelectorAll('.memory-settings-panel__setting-row')]
|
|
const switches = [...document.querySelectorAll('.memory-settings-panel .wb-switch__input')].map(input => {
|
|
const label = input.closest('.wb-switch')
|
|
const track = label?.querySelector('.wb-switch__track')
|
|
const thumb = label?.querySelector('.wb-switch__thumb')
|
|
return { checked: input.checked, disabled: input.disabled, label: input.getAttribute('aria-label'),
|
|
track: rect(track), thumb: rect(thumb), trackBackground: style(track)?.backgroundColor,
|
|
thumbBackground: style(thumb)?.backgroundColor, title: label?.getAttribute('title') }
|
|
})
|
|
const manage = document.querySelector('.memory-settings-panel__manage-section')
|
|
const name = document.querySelector('.memory-settings-panel__setting-name')
|
|
const desc = document.querySelector('.memory-settings-panel__setting-desc')
|
|
const privacy = document.querySelector('.memory-settings-panel__privacy')
|
|
const link = document.querySelector('.memory-settings-panel__privacy-link')
|
|
return {
|
|
theme: document.documentElement.dataset.theme ?? 'light',
|
|
panel: rect(panel), group: rect(group), rows: rows.map(rect), switches,
|
|
manage: rect(manage),
|
|
titles: [...document.querySelectorAll('.memory-settings-panel__setting-name')].map(node => node.textContent.trim()),
|
|
descriptions: [...document.querySelectorAll('.memory-settings-panel__setting-desc')].map(node => node.textContent.trim()),
|
|
manageTitle: document.querySelector('.memory-modal__detail-name')?.textContent?.trim(),
|
|
manageDesc: document.querySelector('.memory-modal__detail-desc')?.textContent?.trim(),
|
|
importButton: (() => { const node = document.querySelector('.memory-settings-panel__manage-section .wb-button')
|
|
return node ? { text: node.textContent.trim(), rect: rect(node), background: style(node).backgroundColor, border: style(node).borderTopColor } : null })(),
|
|
privacy: rect(privacy), privacyText: privacy?.textContent?.replace(/\\s+/g, ' ').trim(),
|
|
link: link ? { href: link.getAttribute('href'), title: link.getAttribute('title'), ariaDisabled: link.getAttribute('aria-disabled') } : null,
|
|
titleStyle: name ? { fontSize: style(name).fontSize, fontWeight: style(name).fontWeight, lineHeight: style(name).lineHeight, color: style(name).color } : null,
|
|
descStyle: desc ? { fontSize: style(desc).fontSize, fontWeight: style(desc).fontWeight, lineHeight: style(desc).lineHeight, color: style(desc).color } : null,
|
|
groupBackground: style(group)?.backgroundColor,
|
|
manageBackground: style(manage)?.backgroundColor,
|
|
panelBackground: style(panel)?.backgroundColor,
|
|
privacyColor: style(privacy)?.color,
|
|
content: document.querySelector('.memory-settings-panel .memory-modal__memory-text')?.textContent ?? ''
|
|
}
|
|
})()`
|
|
|
|
const importLayoutSource = `(()=>{
|
|
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 style = node => node ? getComputedStyle(node) : null
|
|
const root = document.querySelector('[data-memory-import-dialog]')
|
|
const dialog = root?.querySelector('.wb-modal--medium')
|
|
const sections = [...(root?.querySelectorAll('.memory-modal__section') ?? [])]
|
|
const prompt = root?.querySelector('.memory-modal__prompt-text')
|
|
const textarea = root?.querySelector('textarea')
|
|
const buttons = [...(root?.querySelectorAll('.wb-modal__footer .wb-button') ?? [])]
|
|
return root ? {
|
|
dialog: rect(dialog),
|
|
title: dialog?.querySelector('.wb-modal__title')?.textContent?.trim(),
|
|
dialogBackground: style(dialog)?.backgroundColor,
|
|
dialogRadius: style(dialog)?.borderRadius,
|
|
sections: sections.map(rect),
|
|
labels: sections.map(section => section.querySelector('.memory-modal__label')?.textContent?.trim()),
|
|
prompt: prompt?.textContent ?? '',
|
|
promptHash: null,
|
|
textarea: textarea ? { rect: rect(textarea), placeholder: textarea.getAttribute('placeholder'), value: textarea.value } : null,
|
|
buttons: buttons.map(button => ({
|
|
text: button.textContent.trim(),
|
|
disabled: button.disabled,
|
|
className: button.className,
|
|
rect: rect(button)
|
|
})),
|
|
error: root.querySelector('[role=alert]')?.textContent?.trim() ?? ''
|
|
} : null
|
|
})()`
|
|
|
|
const resetLayoutSource = `(()=>{
|
|
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 style = node => node ? getComputedStyle(node) : null
|
|
const root = document.querySelector('[data-memory-reset-dialog]')
|
|
const dialog = root?.querySelector('.memory-reset-modal')
|
|
const body = dialog?.querySelector('.wb-modal__body')
|
|
const buttons = [...(dialog?.querySelectorAll('.wb-modal__footer .wb-button') ?? [])]
|
|
return root ? {
|
|
dialog: rect(dialog),
|
|
title: dialog.querySelector('.wb-modal__title')?.textContent?.trim(),
|
|
description: body?.querySelector('.memory-modal__reset-confirm-desc')?.textContent?.trim(),
|
|
hint: body?.querySelector('.memory-modal__reset-confirm-hint')?.textContent?.trim(),
|
|
background: style(dialog)?.backgroundColor,
|
|
radius: style(dialog)?.borderRadius,
|
|
closeLabel: dialog.querySelector('.wb-modal__close')?.getAttribute('aria-label'),
|
|
focused: document.activeElement?.textContent?.trim(),
|
|
buttons: buttons.map(button => ({
|
|
text: button.textContent.trim(),
|
|
disabled: button.disabled,
|
|
danger: button.classList.contains('wb-button--danger'),
|
|
background: style(button)?.backgroundColor,
|
|
border: style(button)?.borderTopColor,
|
|
rect: rect(button)
|
|
}))
|
|
} : null
|
|
})()`
|
|
|
|
;(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', main)
|
|
|
|
await step('memory-base-light')
|
|
await openSettings('memory')
|
|
observe(settings)
|
|
await wait('!!document.querySelector(".memory-settings-panel")', 'memory panel', settings)
|
|
const light = await read(layoutSource, settings)
|
|
snapshots.light = light
|
|
await fs.writeFile(path.join(directory, 'ycode-memory-light.png'), (await settings.webContents.capturePage()).toPNG())
|
|
const nav = await read(`(()=>{const item=document.querySelector('[data-settings-tab="memory"]')
|
|
return { exists: !!item, disabled: item?.getAttribute('aria-disabled'), active: document.querySelector('.settings-navigation__item--active')?.dataset.settingsTab }})()`, settings)
|
|
check('memory navigation entry is enabled and active', [nav.exists, nav.disabled !== 'true', nav.active], [true, true, 'memory'])
|
|
check('memory panel keeps the recovered 638x620 box at 232,40',
|
|
[near(light.panel.x, 232), near(light.panel.y, 40), near(light.panel.width, 638), near(light.panel.height, 620)], [true, true, true, true])
|
|
check('two setting rows keep the recovered 638x128 group and 606x46 rows',
|
|
[near(light.group.x, 232), near(light.group.y, 40), near(light.group.width, 638), near(light.group.height, 128),
|
|
light.rows.map(row => [near(row.x, 248), near(row.width, 606), near(row.height, 46)].every(Boolean)),
|
|
near(light.rows[0].y, 52), near(light.rows[1].y, 110)], [true, true, true, true, [true, true], true, true])
|
|
check('memory copy and local-skills limitation are aligned',
|
|
[light.titles, light.descriptions, light.switches[1].title],
|
|
[['对话记忆', '本地记忆与技能'],
|
|
['记住对话中的重要信息,让后续回答更贴合你。', '自动记录工作过程并优化技能,数据仅保存在你的设备上。'],
|
|
'仅保存偏好;本地工作过程自动记录执行器尚未接入'])
|
|
check('switches keep recovered geometry and source default state',
|
|
light.switches.map(item => [item.checked, item.disabled, near(item.track.width, 34), near(item.track.height, 20),
|
|
near(item.thumb.width, 16), near(item.thumb.height, 16), item.trackBackground]),
|
|
[[true, false, true, true, true, true, 'rgb(0, 194, 154)'], [false, false, true, true, true, true, 'rgb(230, 230, 230)']])
|
|
check('manage card, typography and privacy footer keep recovered geometry',
|
|
[near(light.manage.x, 232), near(light.manage.y, 184), near(light.manage.width, 638), near(light.manage.height, 78),
|
|
light.manageTitle, light.manageDesc,
|
|
light.titleStyle, light.descStyle,
|
|
near(light.privacy.x, 232), near(light.privacy.y, 638), near(light.privacy.width, 638), near(light.privacy.height, 22),
|
|
light.privacyColor],
|
|
[true, true, true, true, '管理记忆', '每晚自动整理更新,可随时查看和编辑。',
|
|
{ fontSize: '14px', fontWeight: '500', lineHeight: '22px', color: 'rgb(0, 0, 0)' },
|
|
{ fontSize: '14px', fontWeight: '400', lineHeight: '22px', color: 'rgba(0, 0, 0, 0.7)' },
|
|
true, true, true, true, 'rgba(0, 0, 0, 0.7)'])
|
|
check('privacy legal target is visibly present but has no external URL',
|
|
[light.privacyText, light.link.href, light.link.title, light.link.ariaDisabled],
|
|
['记忆内容遵循 YCode 隐私政策,仅你本人可见', null, '尚未配置', 'true'])
|
|
|
|
await step('memory-switch-persistence')
|
|
await read(`document.querySelectorAll('.memory-settings-panel .wb-switch__input')[0].click()`, settings)
|
|
await wait(`document.querySelectorAll('.memory-settings-panel .wb-switch__input')[0]?.checked===false`, 'conversation memory off', settings)
|
|
let config = await readJson(path.join(directory, 'data', 'config.json'))
|
|
check('conversation-memory preference persists without changing local memory',
|
|
[config.memoryPreferences?.conversationMemory, config.memoryPreferences?.localMemorySkills], [false, false])
|
|
settings.close()
|
|
await pause(500)
|
|
await openSettings('memory')
|
|
await wait('!!document.querySelector(".memory-settings-panel")', 'memory panel reopened', settings)
|
|
const reopened = await read(layoutSource, settings)
|
|
check('reopened settings restores both independent preferences',
|
|
[reopened.switches[0].checked, reopened.switches[1].checked], [false, false])
|
|
await read(`document.querySelectorAll('.memory-settings-panel .wb-switch__input')[0].click()`, settings)
|
|
await wait(`document.querySelectorAll('.memory-settings-panel .wb-switch__input')[0]?.checked===true`, 'conversation memory restored', settings)
|
|
await wait(`document.querySelectorAll('.memory-settings-panel .wb-switch__input')[1]?.disabled===false`, 'memory switches idle', settings)
|
|
config = await readJson(path.join(directory, 'data', 'config.json'))
|
|
check('conversation-memory round trip still leaves local memory disabled',
|
|
[config.memoryPreferences?.conversationMemory, config.memoryPreferences?.localMemorySkills], [true, false])
|
|
const enabledLocalSkills = await read(`(()=>{
|
|
const input=document.querySelectorAll('.memory-settings-panel .wb-switch__input')[1]
|
|
if(!(input instanceof HTMLInputElement)||input.disabled)return false
|
|
input.click()
|
|
return true
|
|
})()`, settings)
|
|
assert.equal(enabledLocalSkills, true, 'local memory switch must be enabled before the real click')
|
|
await wait(`(()=>{
|
|
const panel=document.querySelector('.memory-settings-panel')
|
|
const input=document.querySelectorAll('.memory-settings-panel .wb-switch__input')[1]
|
|
return panel?.getAttribute('aria-busy')==='false'&&input?.checked===true&&input?.disabled===false
|
|
})()`, 'local skills on IPC complete', settings)
|
|
config = await waitForJson(
|
|
path.join(directory, 'data', 'config.json'),
|
|
value => value.memoryPreferences?.localMemorySkills === true,
|
|
'local memory and skills preference persisted'
|
|
)
|
|
check('local memory and skills preference persists', config.memoryPreferences?.localMemorySkills, true)
|
|
const disabledLocalSkills = await read(`(()=>{
|
|
const input=document.querySelectorAll('.memory-settings-panel .wb-switch__input')[1]
|
|
if(!(input instanceof HTMLInputElement)||input.disabled)return false
|
|
input.click()
|
|
return true
|
|
})()`, settings)
|
|
assert.equal(disabledLocalSkills, true, 'local memory switch must be enabled before restoring')
|
|
await wait(`(()=>{
|
|
const panel=document.querySelector('.memory-settings-panel')
|
|
const input=document.querySelectorAll('.memory-settings-panel .wb-switch__input')[1]
|
|
return panel?.getAttribute('aria-busy')==='false'&&input?.checked===false&&input?.disabled===false
|
|
})()`, 'local skills restored to source default', settings)
|
|
await waitForJson(
|
|
path.join(directory, 'data', 'config.json'),
|
|
value => value.memoryPreferences?.localMemorySkills === false,
|
|
'local memory and skills preference restored'
|
|
)
|
|
|
|
await step('memory-import-dialog')
|
|
await read(`document.querySelector('.memory-settings-panel__manage-section .wb-button').click()`, settings)
|
|
await wait('!!document.querySelector("[data-memory-import-dialog]")', 'import dialog', settings)
|
|
const dialog = await read(importLayoutSource, settings)
|
|
snapshots.importDialog = dialog
|
|
await fs.writeFile(path.join(directory, 'ycode-memory-import.png'), (await settings.webContents.capturePage()).toPNG())
|
|
const promptHash = createHash('sha256').update(dialog.prompt).digest('hex')
|
|
check('import dialog keeps the recovered 640x520 centered shell',
|
|
[near(dialog.dialog.x, 130), near(dialog.dialog.y, 80), near(dialog.dialog.width, 640), near(dialog.dialog.height, 520),
|
|
dialog.dialogRadius], [true, true, true, true, '24px'])
|
|
check('import dialog keeps the recovered title, steps and placeholder',
|
|
[dialog.title, dialog.labels, dialog.textarea.placeholder],
|
|
['导入其他记忆', ['复制以下提示词到其他AI对话中', '将结果粘贴到下方,添加到 YCode 记忆'], '在此粘贴你的记忆'])
|
|
check('import prompt exactly matches the REA-recovered zh-CN template',
|
|
[dialog.prompt.length, Buffer.byteLength(dialog.prompt), promptHash],
|
|
[593, 1657, 'b9142225cbb0b036168e860077c7d8bdb0165f449e00772af55429b8673d632d'])
|
|
check('import footer keeps the recovered 68x32 action pair',
|
|
dialog.buttons.map(button => [button.text, near(button.rect?.width ?? 68, 68)]),
|
|
[['取消', true], ['导入', true]])
|
|
|
|
await step('memory-import-validation')
|
|
await read(`(()=>{const field=document.querySelector('[data-memory-import-dialog] textarea')
|
|
const setter=Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype,'value').set
|
|
setter.call(field,'内容过短')
|
|
field.dispatchEvent(new Event('input',{bubbles:true}))})()`, settings)
|
|
await pause(80)
|
|
await read(`document.querySelectorAll('[data-memory-import-dialog] .wb-modal__footer .wb-button')[1].click()`, settings)
|
|
await wait(`document.querySelector('[data-memory-import-dialog] [role=alert]')?.textContent?.includes('内容过短')`, 'short memory error', settings)
|
|
check('short import content is rejected with recovered copy',
|
|
await read(`document.querySelector('[data-memory-import-dialog] [role=alert]').textContent.trim()`, settings),
|
|
'内容过短,请粘贴完整的记忆内容')
|
|
|
|
await step('memory-import-write')
|
|
const imported = '**偏好**\n[2026-10-17] - 用户偏好 TypeScript 和简洁的实现。'
|
|
await read(`(()=>{const field=document.querySelector('[data-memory-import-dialog] textarea')
|
|
const setter=Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype,'value').set
|
|
setter.call(field,${JSON.stringify(imported)})
|
|
field.dispatchEvent(new Event('input',{bubbles:true}))})()`, settings)
|
|
await pause(80)
|
|
await read(`document.querySelectorAll('[data-memory-import-dialog] .wb-modal__footer .wb-button')[1].click()`, settings)
|
|
await wait(`!document.querySelector('[data-memory-import-dialog]')`, 'import dialog closes', settings)
|
|
const importedLayout = await wait(`(()=>{const node=document.querySelector('.memory-settings-panel .memory-modal__memory-text')
|
|
return node?{content:node.textContent,manage:document.querySelector('.memory-settings-panel__manage-section').getBoundingClientRect().height}:null})()`, 'imported memory visible', settings)
|
|
config = await readJson(path.join(directory, 'data', 'config.json'))
|
|
check('valid import writes to the same long-term memory used by conversations',
|
|
[importedLayout.content.includes('用户偏好 TypeScript'), config.personalization?.longTermMemory], [true, imported])
|
|
check('manage card expands after content exists while retaining the viewport footer',
|
|
[importedLayout.manage > 78,
|
|
near((await read(layoutSource, settings)).privacy.y, 638)],
|
|
[true, true])
|
|
|
|
await step('memory-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('memory')
|
|
await wait(`document.documentElement.dataset.theme==='dark'&&!!document.querySelector('.memory-settings-panel')`, 'memory dark', settings)
|
|
const dark = await read(layoutSource, settings)
|
|
snapshots.dark = dark
|
|
await fs.writeFile(path.join(directory, 'ycode-memory-dark.png'), (await settings.webContents.capturePage()).toPNG())
|
|
check('dark theme keeps recovered geometry and paints card/text with shared tokens',
|
|
[near(dark.panel.width, 638), near(dark.panel.height, 620), near(dark.group.height, 128),
|
|
dark.groupBackground, dark.titleStyle.color, dark.descStyle.color, dark.switches[0].trackBackground],
|
|
[true, true, true, 'rgb(31, 31, 31)', 'rgb(242, 243, 245)', 'rgb(242, 243, 245)', 'rgb(34, 203, 168)'])
|
|
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('memory-reset-dialog')
|
|
await openSettings('memory')
|
|
await wait(`!!document.querySelector('.memory-settings-panel .memory-modal__memory-text')`, 'memory content before reset', settings)
|
|
const openReset = `(()=>{const button=[...document.querySelectorAll('.memory-settings-panel__manage-section .wb-button')]
|
|
.find(node=>node.textContent.trim()==='重置'); button?.click(); return !!button})()`
|
|
check('reset action is present after memory content exists', await read(openReset, settings), true)
|
|
await wait(`!!document.querySelector('[data-memory-reset-dialog]')`, 'reset confirmation dialog', settings)
|
|
const resetDialog = await read(resetLayoutSource, settings)
|
|
snapshots.resetDialog = resetDialog
|
|
await fs.writeFile(path.join(directory, 'ycode-memory-reset.png'), (await settings.webContents.capturePage()).toPNG())
|
|
check('reset dialog keeps the recovered 480px WorkBuddy confirm shell',
|
|
[near(resetDialog.dialog.width, 480), resetDialog.title, resetDialog.closeLabel, resetDialog.radius],
|
|
[true, '重置记忆', '关闭重置记忆', '24px'])
|
|
check('reset dialog keeps recovered destructive and recovery copy',
|
|
[resetDialog.description, resetDialog.hint],
|
|
['此操作将清空当前全部记忆,该操作不可恢复。',
|
|
'若「生成对话记忆」处于开启状态,后续对话仍会生成新的记忆;如需彻底停用,请同时关闭记忆功能。'])
|
|
check('reset dialog exposes cancel and danger confirm actions with cancel initially focused',
|
|
[resetDialog.focused, resetDialog.buttons.map(button => [button.text, button.danger]),
|
|
resetDialog.buttons.map(button => button.text === '取消' ? near(button.rect.width, 68) : button.rect.width >= 66)],
|
|
['取消', [['取消', false], ['重置记忆', true]], [true, true]])
|
|
|
|
await read(`document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}))`, settings)
|
|
await wait(`!document.querySelector('[data-memory-reset-dialog]')`, 'reset dialog closes with Escape', settings)
|
|
await read(openReset, settings)
|
|
await wait(`!!document.querySelector('[data-memory-reset-dialog]')`, 'reset dialog reopened', settings)
|
|
await read(`document.querySelectorAll('[data-memory-reset-dialog] .wb-modal__footer .wb-button')[1].click()`, settings)
|
|
await wait(`!document.querySelector('[data-memory-reset-dialog]')&&!document.querySelector('.memory-settings-panel .memory-modal__memory-text')`, 'memory reset', settings)
|
|
config = await readJson(path.join(directory, 'data', 'config.json'))
|
|
const resetLayout = await read(layoutSource, settings)
|
|
check('reset clears the same long-term memory and restores the collapsed manage card',
|
|
[config.personalization?.longTermMemory, resetLayout.content, near(resetLayout.manage.height, 78)],
|
|
['', '', true])
|
|
|
|
await step('memory-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(`MEMORY_SETTINGS_${result} ${directory}`)
|
|
for (const failure of checks.filter(item => !item.pass)) {
|
|
console.log(`MEMORY_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 { /* Keep original failure. */ }
|
|
app.exit(1)
|
|
})
|
|
}
|