feat: complete WorkBuddy parity and Windows release workflow
This commit is contained in:
1 parent
143516b3a8
commit
d5512df549
434 files changed
+54977
-1854
No files matched your search
@@ -0,0 +1,494 @@
|
||||
// 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_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('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)
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user