// Real isolated YCode App + preload/IPC/filesystem verification for the archived-task UI. // No WorkBuddy runtime or original evidence is accessed by this script. const fs = require('node:fs/promises') const path = require('node:path') const assert = require('node:assert/strict') if (!process.versions.electron) { ;(async () => { const directory = await fs.mkdtemp(path.resolve('.tmp/archived-tasks-')) const data = path.join(directory, 'data') const home = path.join(directory, 'home') const independentRoot = path.join(directory, 'independent-root') const workspaceRoot = path.join(directory, 'workspace-root') await Promise.all([fs.mkdir(data), fs.mkdir(home), fs.mkdir(independentRoot), fs.mkdir(workspaceRoot)]) const now = Date.now() const crypto = require('node:crypto') const workspaceId = 'archived-space' const sessions = [ { id: 'archived-active', title: 'Archive Active', cwd: independentRoot, createdAt: now - 30_000, updatedAt: now - 30_000, messages: [ { id: crypto.randomUUID(), role: 'user', content: 'active task stays visible', createdAt: now - 30_000, status: 'done' } ] }, { id: 'archived-independent', title: 'Archive Independent', cwd: independentRoot, createdAt: now - 20_000, updatedAt: now - 20_000, archivedAt: now - 1_000, messages: [ { id: crypto.randomUUID(), role: 'user', content: 'independent message preserved', createdAt: now - 20_000, status: 'done' }, { id: crypto.randomUUID(), role: 'assistant', content: 'independent reply preserved', createdAt: now - 19_000, status: 'done', tools: [], parts: [] } ] }, { id: 'archived-workspace', title: 'Archive Workspace', workspaceId, cwd: workspaceRoot, createdAt: now - 10_000, updatedAt: now - 10_000, archivedAt: now, messages: [ { id: crypto.randomUUID(), role: 'user', content: 'workspace message preserved', createdAt: now - 10_000, status: 'done' }, { id: crypto.randomUUID(), role: 'assistant', content: 'workspace reply preserved', createdAt: now - 9_000, status: 'done', tools: [], parts: [] } ] } ] await fs.writeFile(path.join(data, 'sessions.json'), JSON.stringify({ sessions })) await fs.writeFile(path.join(data, 'workspaces.json'), JSON.stringify({ workspaces: [{ id: workspaceId, name: 'Archive Space', root: workspaceRoot, createdAt: now }], selectedWorkspaceId: workspaceId })) const env = { ...process.env, YCODE_QA_DIRECTORY: directory, YCODE_USER_DATA_DIR: data, YCODE_DISABLE_GPU: '1', YCODE_QA_OFFSCREEN: '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('ARCHIVED_TASKS_')) process.stdout.write(bytes) }) child.stderr.on('data', bytes => { output += bytes }) const timer = setTimeout(() => child.kill(), 150000) 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) process.exitCode = code ? 1 : 0 }) })() } else { const { app, BrowserWindow, ipcMain } = require('electron') app.disableHardwareAcceleration() const directory = process.env.YCODE_QA_DIRECTORY assert.ok(path.relative(path.resolve('.tmp'), directory).startsWith('archived-tasks-')) app.setPath('userData', path.join(directory, 'data')) app.setPath('sessionData', path.join(directory, 'data')) app.setPath('home', path.join(directory, 'home')) let main let phase = 'boot' const checks = [] const snapshots = {} const rendererErrors = [] const pause = ms => new Promise(resolve => setTimeout(resolve, ms)) async function read(source) { let timer try { return await Promise.race([ main.webContents.executeJavaScript(source), new Promise((_, reject) => { timer = setTimeout(() => reject(Error('Renderer timeout: ' + source.slice(0, 100))), 15000) }) ]) } catch (error) { throw Error(error.message + ' - ' + source.slice(0, 240)) } finally { clearTimeout(timer) } } async function wait(source, label, attempts = 250) { for (let index = 0; index < attempts; index++) { const value = await read(source) if (value) return value await pause(30) } 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 }) } async function shot(name) { const capture = await main.webContents.debugger.sendCommand('Page.captureScreenshot', { format: 'png', captureBeyondViewport: false }) await fs.writeFile(path.join(directory, name + '.png'), Buffer.from(capture.data, 'base64')) } const clickArchiveEntry = `(()=>{const button=document.querySelector('.conversation-section-archived-btn');if(!button)return false;button.click();return true})()` const clickRowAction = (id, selector) => `(()=>{const row=document.querySelector('[data-archived-task-id=${JSON.stringify(id)}]');const button=row?.querySelector(${JSON.stringify(selector)});if(!button)return false;button.click();button.click();return true})()` ;(async () => { const { pathToFileURL } = require('node:url') await import(pathToFileURL(path.resolve('out/main/index.mjs')).href) await app.whenReady() main = BrowserWindow.getAllWindows()[0] main.setSize(1336, 902) main.showInactive() main.webContents.debugger.attach('1.3') main.webContents.on('console-message', (_event, level, message) => { if (level >= 2) rendererErrors.push(message) }) await wait('!!document.querySelector("#root")', 'renderer root') await pause(1500) await wait('!!document.querySelector("[data-session-id=archived-active]")', 'active sidebar task') await step('entry-and-loading') check('archive entry sits in the task section header', await read(`(()=>{const header=document.querySelector('.conversation-section-label-tasks');const button=header?.querySelector('.conversation-section-archived-btn');return !!button&&button.textContent.includes('已归档')})()`), true) const initial = await read(`(async()=>{${clickArchiveEntry};await new Promise(requestAnimationFrame);return {dialog:!!document.querySelector('.archived-tasks-dialog'),loading:!!document.querySelector('.archived-tasks-dialog__loading'),count:document.querySelectorAll('.archived-tasks-dialog').length}})()`) check('archive entry opens exactly one dialog', initial.count, 1) check('dialog exposes a paint-visible loading state', initial.loading || (await read(`!!document.querySelector('.archived-tasks-dialog__loading')`)), true) await wait(`document.querySelectorAll('.archived-tasks-dialog__row').length===2`, 'two archived tasks') check('loading state is replaced by the real list', await read(`!!document.querySelector('.archived-tasks-dialog__loading')`), false) await step('list-filter-and-kind') const rows = await read(`[...document.querySelectorAll('.archived-tasks-dialog__row')].map(row=>({id:row.getAttribute('data-archived-task-id'),workspaceId:row.getAttribute('data-archived-workspace-id'),title:row.querySelector('.archived-tasks-dialog__title')?.textContent,meta:row.querySelector('.archived-tasks-dialog__meta')?.textContent,actions:[...row.querySelectorAll('.archived-tasks-dialog__actions button')].map(button=>button.textContent)}))`) snapshots.rows = rows check('only archived sessions are listed', rows.map(row => row.id).sort(), ['archived-independent', 'archived-workspace']) check('active task is excluded from the archive dialog', rows.some(row => row.id === 'archived-active'), false) check('independent task is labelled independently', rows.find(row => row.id === 'archived-independent')?.meta.includes('独立任务'), true) check('workspace task keeps its workspace identity', rows.find(row => row.id === 'archived-workspace')?.meta.includes('空间 · Archive Space'), true) check('every archived row exposes restore and view actions', rows.every(row => row.actions.join('|') === '恢复|查看'), true) check('dialog does not mention removed data management', await read(`document.querySelector('.archived-tasks-dialog').textContent.includes('数据管理')`), false) await shot('archive-list') await step('restore-independent') const rapidRestore = await read(clickRowAction('archived-independent', '.archived-tasks-dialog__restore')) check('rapid duplicate restore clicks are accepted only once', rapidRestore, true) await wait(`!document.querySelector('[data-archived-task-id=archived-independent]')`, 'independent row removed') await wait(`document.querySelectorAll('[data-session-id=archived-independent]').length===1`, 'independent restored once') check('restored independent task appears exactly once in the sidebar', await read(`document.querySelectorAll('[data-session-id=archived-independent]').length`), 1) check('restored independent task is no longer archived', await read(`window.ycode.session.listArchived().then(list=>list.filter(item=>item.archivedAt!==undefined).some(item=>item.id==='archived-independent'))`), false) await read(`(()=>{const row=document.querySelector('[data-session-id=archived-independent]');row.click();return true})()`) await wait(`document.body.textContent.includes('independent message preserved')`, 'independent history restored') check('independent task messages survive restore and open', await read(`document.body.textContent.includes('independent message preserved')&&document.body.textContent.includes('independent reply preserved')`), true) await step('view-workspace-task') check('view action accepts rapid duplicate clicks', await read(clickRowAction('archived-workspace', '.archived-tasks-dialog__view')), true) await wait(`!document.querySelector('.archived-tasks-dialog')`, 'view closes archive dialog') await wait(`document.querySelector('[data-session-id=archived-workspace].selected')`, 'workspace task selected') check('workspace task returns to its original space group', await read(`document.querySelector('[data-session-id=archived-workspace]')?.closest('[data-workspace-id]')?.getAttribute('data-workspace-id')`), 'archived-space') check('view restores the workspace task without duplicate rows', await read(`document.querySelectorAll('[data-session-id=archived-workspace]').length`), 1) await wait(`document.body.textContent.includes('workspace message preserved')`, 'workspace history restored') check('workspace task messages survive restore and view', await read(`document.body.textContent.includes('workspace message preserved')&&document.body.textContent.includes('workspace reply preserved')`), true) check('all archived tasks are absent after restore/view', await read(`window.ycode.session.listArchived().then(list=>list.filter(item=>item.archivedAt!==undefined).length)`), 0) await step('empty-state') await read(clickArchiveEntry) await wait(`!!document.querySelector('.archived-tasks-dialog__empty')`, 'archive empty state') check('empty state explains that no archived tasks exist', await read(`document.querySelector('.archived-tasks-dialog__empty').textContent.includes('暂无已归档任务')`), true) await read(`document.querySelector('.archived-tasks-dialog__close').click()`) await wait(`!document.querySelector('.archived-tasks-dialog')`, 'empty dialog close') await step('load-failure') ipcMain.removeHandler('session:list-archived') ipcMain.handle('session:list-archived', () => { throw new Error('forced archive list failure') }) await read(clickArchiveEntry) await wait(`!!document.querySelector('.archived-tasks-dialog__load-error')`, 'archive load failure') check('load failure keeps a retry action', await read(`!!document.querySelector('.archived-tasks-dialog__retry')`), true) check('load failure reports the real IPC error', await read(`document.querySelector('.archived-tasks-dialog__load-error').textContent.includes('forced archive list failure')`), true) await shot('archive-failure') ipcMain.removeHandler('session:list-archived') ipcMain.handle('session:list-archived', () => []) await read(`document.querySelector('.archived-tasks-dialog__retry').click()`) await wait(`!!document.querySelector('.archived-tasks-dialog__empty')`, 'archive retry recovers') check('retry reloads the archive and replaces the error state', await read(`!document.querySelector('.archived-tasks-dialog__load-error')&&!!document.querySelector('.archived-tasks-dialog__empty')`), true) await read(`document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true,cancelable:true}))`) await wait(`!document.querySelector('.archived-tasks-dialog')`, 'escape closes archive dialog') check('escape closes the archive dialog when idle', await read(`!document.querySelector('.archived-tasks-dialog')`), true) await step('cleanup') check('no renderer error logs', rendererErrors, []) const report = { result: checks.every(item => item.pass) ? 'PASS' : 'FAIL', scope: 'Real isolated YCode main/preload/IPC/renderer/filesystem. Archive list success and failure use IPC. Temporary data only.', checks, snapshots, rendererErrors } await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify(report, null, 2)) console.log('ARCHIVED_TASKS_' + report.result + ' ' + directory) for (const failure of checks.filter(item => !item.pass)) console.log('ARCHIVED_TASKS_FAILURE ' + JSON.stringify(failure)) app.exit(report.result === 'PASS' ? 0 : 1) })().catch(async error => { await fs.writeFile(path.join(directory, 'failure.json'), JSON.stringify({ phase, error: String(error), stack: error.stack, checks, snapshots, rendererErrors }, null, 2)) console.error(error) if (main) await shot('failure').catch(() => undefined) app.exit(1) }) }