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

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)
})
}