268 lines
23 KiB
JavaScript
268 lines
23 KiB
JavaScript
// Real isolated production Electron App + real preload/IPC. CDP below controls YCode only.
|
|
// WorkBuddy expectations come from fixed REA source evidence, never this browser transport.
|
|
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/panel-integration-'))
|
|
await fs.mkdir(path.join(directory, 'data')); await fs.mkdir(path.join(directory, 'home'))
|
|
const cwd = path.join(directory, 'fixture-workspace'); await fs.mkdir(cwd)
|
|
const now = Date.now(), fixtureId = () => require('node:crypto').randomUUID()
|
|
const fixtures = ['Panel A', 'Panel B'].map((title, index) => ({ id: fixtureId(), title, cwd, createdAt: now + index, updatedAt: now + index,
|
|
messages: [{ id: fixtureId(), role: 'user', content: 'Persisted panel-layout test fixture.', createdAt: now, status: 'done' },
|
|
...(index === 0 ? [{ id: fixtureId(), role: 'assistant', content: 'A real resource is available.', createdAt: now + 1, status: 'done',
|
|
parts: [{ type: 'text', text: 'A real resource is available.' }, ...[1, 2].map(number => ({ type: 'artifact', artifact: {
|
|
id: `fixture-artifact-${number}`, name: 'panel-sentinel.txt', path: 'panel-sentinel.txt' } }))] }] : [])] }))
|
|
await fs.writeFile(path.join(directory, 'data', 'sessions.json'), JSON.stringify({ sessions: fixtures }))
|
|
await fs.writeFile(path.join(cwd, 'panel-sentinel.txt'), 'PANEL_NATIVE_RESOURCE_SENTINEL')
|
|
const env = { ...process.env, YCODE_QA_DIRECTORY: directory, YCODE_USER_DATA_DIR: path.join(directory, 'data'),
|
|
YCODE_OMP_HOME: path.join(directory, 'home'), YCODE_DISABLE_GPU: '1', YCODE_QA_OFFSCREEN: '1', YCODE_QA_PANEL_FIXTURES: JSON.stringify(fixtures) }
|
|
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('PANEL_')) 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.slice(-6000))
|
|
process.exitCode = code ?? 1
|
|
})
|
|
})().catch(error => { console.error(error); process.exitCode = 1 })
|
|
} else {
|
|
const { app, BrowserWindow } = require('electron')
|
|
app.disableHardwareAcceleration()
|
|
const { pathToFileURL } = require('node:url')
|
|
const directory = process.env.YCODE_QA_DIRECTORY
|
|
assert.ok(path.relative(path.resolve('.tmp'), directory).startsWith('panel-integration-'))
|
|
app.setPath('userData', path.join(directory, 'data')); app.setPath('sessionData', path.join(directory, 'data'))
|
|
app.setPath('home', path.join(directory, 'home'))
|
|
let main, phase = 'boot'
|
|
const checks = [], snapshots = {}, 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) {
|
|
for (let i = 0; i < 250; i++) { 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 })
|
|
}
|
|
const rectSource = `selector=>{const n=document.querySelector(selector);if(!n)return null;const b=n.getBoundingClientRect(),s=getComputedStyle(n);return {x:b.x,y:b.y,width:b.width,height:b.height,transform:s.transform,opacity:s.opacity,transition:s.transition,transitionDuration:s.transitionDuration,transitionTimingFunction:s.transitionTimingFunction,position:s.position,z:s.zIndex,classes:n.className}}`
|
|
async function snapshot(label) {
|
|
const result = await read(`(()=>{const rect=${rectSource};return {viewport:[innerWidth,innerHeight],shell:rect('.app-shell'),sidebar:rect('.app-shell>.sidebar'),main:rect('[data-view-id="main-content"]'),detail:rect('[data-view-id="detail-panel"]'),workbench:rect('.yc-workbench'),leftDrawer:rect('.app-shell__left-drawer'),backdrop:rect('.app-shell__drawer-backdrop'),motion:document.querySelector('.app-shell').className,owner:document.querySelector('.yc-workbench')?.dataset.owner,selected:document.querySelector('[data-session-id][data-selected="true"]')?.dataset.sessionId,layouts:JSON.parse(localStorage.getItem('ycode.panel-layouts.v1')||'{}')}})()`)
|
|
snapshots[label] = result; return result
|
|
}
|
|
async function click(selector) {
|
|
await read(`(()=>{const n=document.querySelector(${JSON.stringify(selector)});if(!n)throw Error('Missing clickable '+${JSON.stringify(selector)});n.click()})()`)
|
|
await pause(35)
|
|
}
|
|
async function resize(width) {
|
|
// Renderer emulation avoids native getContentSize hangs on this Windows test host.
|
|
await main.webContents.debugger.sendCommand('Emulation.setDeviceMetricsOverride', { width, height: 901, deviceScaleFactor: 1, mobile: false })
|
|
await wait(`innerWidth===${width}`, 'viewport ' + width); await pause(400)
|
|
}
|
|
async function shot(name) {
|
|
const shot = await main.webContents.debugger.sendCommand('Page.captureScreenshot', { format: 'png', captureBeyondViewport: false })
|
|
await fs.writeFile(path.join(directory, name + '.png'), Buffer.from(shot.data, 'base64'))
|
|
}
|
|
async function openPanel() {
|
|
if (!(await read('!!document.querySelector(".yc-workbench")'))) await click('.source-chat-header button[title="打开右侧栏"]')
|
|
await pause(400)
|
|
}
|
|
async function closePanel() {
|
|
if (await read('!!document.querySelector(".sidebar-next-collapse-toggle")')) await click('.sidebar-next-collapse-toggle')
|
|
await pause(400)
|
|
}
|
|
async function drag(selector, delta) {
|
|
const point = await read(`(()=>{const n=document.querySelector(${JSON.stringify(selector)});if(!n)throw Error('Missing separator');const b=n.getBoundingClientRect(),x=b.x+b.width/2,y=b.y+100;n.dispatchEvent(new MouseEvent('mousedown',{bubbles:true,button:0,clientX:x,clientY:y}));return {x,y}})()`)
|
|
// Match real discrete input turns so React installs its drag effect before mousemove/up.
|
|
await pause(50)
|
|
await read(`window.dispatchEvent(new MouseEvent('mousemove',{bubbles:true,buttons:1,clientX:${point.x + delta},clientY:${point.y}}))`)
|
|
await pause(40)
|
|
await read(`window.dispatchEvent(new MouseEvent('mouseup',{bubbles:true,button:0,clientX:${point.x + delta},clientY:${point.y}}))`)
|
|
await pause(250)
|
|
}
|
|
async function select(id) { await click(`[data-session-id="${id}"]`); await pause(400) }
|
|
async function createHost(kind) {
|
|
await openPanel()
|
|
if (!(await read(`!!document.querySelector('[data-entry-key="${kind}"]')`))) await click('[aria-label="新标签页"]')
|
|
await click(`[data-entry-key="${kind}"]`)
|
|
const selector = kind === 'browser' ? '[data-browser-host-id]' : '[data-terminal-id]'
|
|
const attribute = kind === 'browser' ? 'data-browser-host-id' : 'data-terminal-id'
|
|
return wait(`document.querySelector(${JSON.stringify(selector)})?.getAttribute(${JSON.stringify(attribute)})`, 'real App ' + kind + ' host mounted')
|
|
}
|
|
async function hostExists(kind, id, sessionId) {
|
|
return read(`window.ycode.${kind}.get(${JSON.stringify(id)},${JSON.stringify(sessionId)}).then(()=>true,()=>false)`)
|
|
}
|
|
;(async () => {
|
|
// Persisted conversation fixtures avoid starting an upstream turn merely to expose chat controls.
|
|
// Production session:list/messages IPC loads these unchanged; no renderer-store or IPC substitutes.
|
|
const [a, b] = JSON.parse(process.env.YCODE_QA_PANEL_FIXTURES)
|
|
const sentinel = 'PANEL_NATIVE_RESOURCE_SENTINEL'
|
|
await import(pathToFileURL(path.resolve('out/main/index.mjs')).href); await app.whenReady()
|
|
for (let i = 0; i < 200; i++) { main = BrowserWindow.getAllWindows().find(win => win.getTitle() === 'YCode'); if (main && !main.webContents.isLoading()) break; await pause(30) }
|
|
assert.ok(main); await wait('!!window.ycode && !!document.querySelector(".chat-pane")', 'real App')
|
|
main.webContents.on('console-message', event => { if (event.level === 'error' || event.level >= 3) rendererErrors.push(event.message) })
|
|
main.webContents.debugger.attach('1.3')
|
|
await main.webContents.debugger.sendCommand('Emulation.setEmulatedMedia', { features: [{ name: 'prefers-reduced-motion', value: 'no-preference' }] })
|
|
assert.equal(path.resolve((await read('window.ycode.app.info()')).configDir), path.join(directory, 'data'))
|
|
await step('native-session-and-resource-setup')
|
|
check('native session registry loads persisted conversations', (await read('window.ycode.session.list()')).map(item => item.id).sort(), [a.id, b.id].sort())
|
|
check('native resource reads from session workspace', await read(`window.ycode.resources.readText('panel-sentinel.txt',${JSON.stringify(a.id)})`), sentinel)
|
|
await wait('!!document.querySelector("[data-session-id]")', 'native sessions loaded')
|
|
await resize(1336); await select(a.id)
|
|
await step('conversation-artifact-link')
|
|
check('artifact link uses deduplicated real resource count', await read('document.querySelector(".artifact-slot-panel__action-btn")?.textContent'), '查看所有产物 (1)')
|
|
await click('.artifact-slot-panel__action-btn'); await pause(250)
|
|
check('artifact link opens real overview', await read('!!document.querySelector(".detail-new-tab-artifacts")'), true)
|
|
check('source expanded chevron rotates right arrow down', await read('getComputedStyle(document.querySelector(".detail-new-tab-artifacts__chevron")).transform'), 'matrix(0, 1, -1, 0, 0, 0)')
|
|
await click('.detail-new-tab-artifacts__title')
|
|
check('artifact group collapses', await read('!!document.querySelector(".detail-new-tab-artifacts__body")'), false)
|
|
await click('.detail-new-tab-artifacts__title')
|
|
await closePanel()
|
|
await step('default-width-drag-doubleclick')
|
|
await openPanel()
|
|
let state = await snapshot('default-open')
|
|
check('ordinary detail defaults 360', state.detail?.width, 360)
|
|
check('sidebar defaults 280', state.sidebar?.width, 280)
|
|
check('main + sidebar + detail fills viewport', Math.round(state.sidebar.width + state.main.width + state.detail.width), 1336)
|
|
check('source split motion returns idle', state.motion.includes('detail-panel-motion-idle'), true)
|
|
await drag('.app-shell__resizer', -100)
|
|
state = await snapshot('dragged-right'); check('right separator drag grows by100', state.detail?.width, 460)
|
|
await read('document.querySelector(".app-shell__resizer").dispatchEvent(new MouseEvent("dblclick",{bubbles:true}))'); await pause(250)
|
|
state = await snapshot('doubleclick-right'); check('right doubleclick restores available half', state.detail?.width, 528)
|
|
await drag('.app-shell__sidebar-resizer', 40)
|
|
state = await snapshot('dragged-sidebar'); check('sidebar separator drag width320', state.sidebar?.width, 320)
|
|
await read('document.querySelector(".app-shell__sidebar-resizer").dispatchEvent(new MouseEvent("dblclick",{bubbles:true}))'); await pause(250)
|
|
check('sidebar doubleclick restores280', (await snapshot('doubleclick-sidebar')).sidebar?.width, 280)
|
|
await select(b.id); await closePanel()
|
|
await step('resource-preview-and-session-layout')
|
|
await read('window.dispatchEvent(new CustomEvent("ycode:preview-resource",{detail:{path:"panel-sentinel.txt"}}))')
|
|
await wait(`document.querySelector('.yc-workbench')?.textContent.includes(${JSON.stringify(sentinel)})`, 'real native preview loaded')
|
|
await pause(400); state = await snapshot('native-preview')
|
|
check('first resource preview uses available half', state.detail?.width, 528)
|
|
await drag('.app-shell__resizer', -72); check('session B custom width600', (await snapshot('session-b-width')).detail?.width, 600)
|
|
check('empty-artifact session has no footer link', await read('!!document.querySelector(".artifact-slot-panel__action-btn")'), false)
|
|
await select(a.id); check('session A width retained528', (await snapshot('session-a-retained')).detail?.width, 528)
|
|
await read('window.dispatchEvent(new CustomEvent("ycode:preview-resource",{detail:{path:"panel-sentinel.txt"}}))')
|
|
await wait('!!document.querySelector(".source-code-preview[data-source-code-state=ready]")', 'real Monaco preview in session A')
|
|
const tabCount = await read('document.querySelectorAll(".artifact-tab").length')
|
|
await click('.artifact-slot-panel__action-btn'); await pause(250)
|
|
check('artifact link preserves existing resource tabs', await read('document.querySelectorAll(".artifact-tab").length'), tabCount + 1)
|
|
await click('.artifact-slot-panel__action-btn')
|
|
check('repeated artifact link reuses overview tab', await read('document.querySelectorAll(".artifact-tab").length'), tabCount + 1)
|
|
await select(b.id); check('session B width retained600', (await snapshot('session-b-retained')).detail?.width, 600)
|
|
await shot('wide-native-preview')
|
|
await step('right-drawer-threshold-mask-close-motion')
|
|
await resize(1000); state = await snapshot('threshold-1000')
|
|
check('source exact1000 remains split (strict less-than)', !!state.detail && !state.detail.classes.includes('app-shell__detail-slot--drawer'), true)
|
|
await resize(999); state = await snapshot('threshold-999')
|
|
check('999 uses right Grid drawer', state.detail?.classes.includes('_drawerItemRight_11lon_217'), true)
|
|
check('drawer backdrop covers whole shell', state.backdrop && state.backdrop.width === state.shell.width && state.backdrop.height === state.shell.height && state.backdrop.x === state.shell.x, true)
|
|
check('drawer starts at shell top', state.detail?.y, state.shell.y)
|
|
check('drawer original transition duration/easing', [state.detail?.transitionDuration, state.detail?.transitionTimingFunction], ['0.3s', 'ease-out'])
|
|
await shot('right-drawer')
|
|
await click('.sidebar-next-collapse-toggle')
|
|
state = await snapshot('right-closing'); check('closing drawer remains mounted while source300ms motion runs', !!state.detail, true)
|
|
check('closing drawer removes open class', state.detail?.classes.includes('_gridViewDrawerItemOpen_11lon_245'), false)
|
|
await pause(340); state = await snapshot('right-closed'); check('closed drawer/backdrop unmounted', [!!state.detail, !!state.backdrop], [false, false])
|
|
await openPanel(); await resize(1023); check('narrow hysteresis retained below1024', (await snapshot('hysteresis-1023')).detail?.classes.includes('--drawer'), true)
|
|
await resize(1024); check('narrow hysteresis releases at1024', (await snapshot('hysteresis-1024')).detail?.classes.includes('--drawer'), false)
|
|
await step('left-drawer-mobile-fullscreen')
|
|
await closePanel(); await resize(640)
|
|
check('source exact640 keeps sidebar (strict less-than)', !!(await snapshot('threshold-640')).sidebar, true)
|
|
await resize(639); check('639 hides persistent sidebar', (await snapshot('threshold-639')).sidebar, null)
|
|
await click('.titlebar__action[title="收起侧边栏"]'); await pause(340)
|
|
state = await snapshot('left-drawer')
|
|
check('left Grid drawer mounted and opens', state.leftDrawer?.classes.includes('_gridViewDrawerItemOpen_11lon_245'), true)
|
|
check('left drawer source width280', state.leftDrawer?.width, 280)
|
|
check('left drawer has full grid backdrop', state.backdrop?.width, 639)
|
|
await click('[aria-label="关闭抽屉"]'); await pause(340)
|
|
check('left close cleans backdrop and drawer', [!!(await snapshot('left-closed')).leftDrawer, !!snapshots['left-closed'].backdrop], [false, false])
|
|
await resize(599); await openPanel(); state = await snapshot('mobile-drawer')
|
|
check('mobile drawer full grid width599', state.detail?.width, 599)
|
|
check('mobile fullscreen source drawer class', state.detail?.classes.includes('_drawerItemFullScreen_11lon_238'), true)
|
|
await shot('mobile-drawer')
|
|
await click('[aria-label="全屏"]'); await pause(100); state = await snapshot('mobile-preview-fullscreen')
|
|
check('explicit preview fullscreen covers complete main viewport', state.workbench && state.workbench.x === 0 && state.workbench.y === state.shell.y && state.workbench.width === 599 && state.workbench.height === state.shell.height, true)
|
|
check('explicit fullscreen hides backdrop', !!state.backdrop, false)
|
|
await click('[aria-label="退出全屏"]'); await pause(150)
|
|
await resize(1336); await click('[aria-label="全屏"]'); await pause(100); state = await snapshot('wide-preview-fullscreen')
|
|
check('wide fullscreen covers sidebar/main/detail rather than detail wrapper', state.workbench && state.workbench.x === 0 && state.workbench.width === 1336 && state.workbench.height === state.shell.height, true)
|
|
await shot('wide-preview-fullscreen'); await click('[aria-label="退出全屏"]')
|
|
await step('reduced-motion-and-rapid-toggle-cleanup')
|
|
await main.webContents.debugger.sendCommand('Emulation.setEmulatedMedia', { features: [{ name: 'prefers-reduced-motion', value: 'reduce' }] })
|
|
check('actual prefers-reduced-motion active', await read('matchMedia("(prefers-reduced-motion: reduce)").matches'), true)
|
|
await closePanel(); await openPanel(); state = await snapshot('reduced-wide')
|
|
check('reduced split motion no transition', state.main?.transitionDuration, '0s')
|
|
await resize(599); state = await snapshot('reduced-mobile')
|
|
check('reduced drawer duration zero', state.detail?.transitionDuration, '0s')
|
|
await click('.sidebar-next-collapse-toggle'); await pause(10)
|
|
state = await snapshot('reduced-mobile-closed'); check('reduced drawer closes without300ms mount delay', [!!state.detail, !!state.backdrop], [false, false])
|
|
await main.webContents.debugger.sendCommand('Emulation.setEmulatedMedia', { features: [{ name: 'prefers-reduced-motion', value: 'no-preference' }] })
|
|
for (const width of [1336, 599]) {
|
|
await resize(width)
|
|
for (let i = 0; i < 10; i++) { await click('.source-chat-header button[title="打开右侧栏"]'); await click('.sidebar-next-collapse-toggle') }
|
|
await pause(450); state = await snapshot('rapid-final-' + width)
|
|
check('rapid close final no detail/backdrop/fullscreen leftovers ' + width, [!!state.detail, !!state.backdrop, !!state.workbench], [false, false, false])
|
|
check('rapid close main fills available width ' + width, Math.round(state.main.width + (state.sidebar?.width || 0)), width)
|
|
check('rapid close no resizing residue ' + width, await read('document.querySelector(".app-shell").hasAttribute("data-resizing")'), false)
|
|
}
|
|
await resize(1336); await select(a.id); await openPanel(); state = await snapshot('before-reload')
|
|
const before = state.detail.width
|
|
main.webContents.reload(); await wait('!!document.querySelector("[data-session-id]")', 'reload layout data'); await select(a.id); await pause(450)
|
|
check('real session panel width persists through renderer reload', (await snapshot('after-reload')).detail?.width, before)
|
|
await step('real-app-host-entries-and-close-tab-cleanup')
|
|
for (const kind of ['browser', 'terminal']) {
|
|
const id = await createHost(kind)
|
|
const view = await read(`window.ycode.${kind}.get(${JSON.stringify(id)},${JSON.stringify(a.id)})`)
|
|
check('App entry creates actual ' + kind + ' for current session', view.sessionId, a.id)
|
|
if (kind === 'terminal') {
|
|
check('App terminal uses exact session cwd', path.resolve(view.cwd), path.resolve(a.cwd))
|
|
check('App terminal contains live xterm', await read('!!document.querySelector(".native-terminal .xterm-helper-textarea")'), true)
|
|
await wait(`window.ycode.terminal.get(${JSON.stringify(id)},${JSON.stringify(a.id)}).then(v=>v.buffer.includes('PS '))`, 'real PowerShell prompt')
|
|
await read(`window.ycode.terminal.write(${JSON.stringify(id)},${JSON.stringify("Write-Output ('PANEL_REAL_'+'APP_PTY')\r")},${JSON.stringify(a.id)})`)
|
|
await wait(`window.ycode.terminal.get(${JSON.stringify(id)},${JSON.stringify(a.id)}).then(v=>v.buffer.includes('PANEL_REAL_APP_PTY'))`, 'actual PTY command output')
|
|
}
|
|
await click(`[data-artifact-id="${kind}:${id}"] .artifact-tab__close`)
|
|
await pause(200)
|
|
check('close App tab releases actual ' + kind + ' host', await hostExists(kind, id, a.id), false)
|
|
}
|
|
await step('real-app-session-delete-cleans-hosts')
|
|
await select(b.id); await openPanel()
|
|
const browserId = await createHost('browser'), terminalId = await createHost('terminal')
|
|
await select(a.id)
|
|
check('switching sessions keeps background browser handle', await hostExists('browser', browserId, b.id), true)
|
|
check('switching sessions keeps background PTY handle', await hostExists('terminal', terminalId, b.id), true)
|
|
await select(b.id)
|
|
check('session switch restores actual terminal tab', await read('document.querySelector("[data-terminal-id]")?.dataset.terminalId'), terminalId)
|
|
await click(`[data-session-id="${b.id}"] .sidebar__item-action`)
|
|
await wait(`!document.querySelector('[data-session-id="${b.id}"]')`, 'deleted session removed from App')
|
|
await pause(300)
|
|
check('deleting App session releases actual browser handle', await hostExists('browser', browserId, b.id), false)
|
|
check('deleting App session releases actual PTY handle', await hostExists('terminal', terminalId, b.id), false)
|
|
check('deleted session panel layout removed from persistence', await read(`!JSON.parse(localStorage.getItem('ycode.panel-layouts.v1')||'{}')[${JSON.stringify(b.id)}]`), true)
|
|
check('no renderer error logs', rendererErrors, [])
|
|
const report = { result: checks.every(item => item.pass) ? 'PASS' : 'FAIL', scope: 'Real isolated YCode App/preload/IPC/native filesystem. No upstream calls. YCode-only CDP viewport/media/screenshot control. Source fidelity from fixed REA artifacts; not complete original screenshot parity.',
|
|
sourceEvidence: ['ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb', 'ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94', 'ev_d6e6920ff4e47f73a182fac5312c5f02393366a035445d18c8a6733a8c8a11ed'], fixtures: 'Two persisted conversations; A additionally has a finished assistant with duplicate references to the real sentinel file. All runtime IPC/hosts/filesystem are production implementations.', checks, snapshots, rendererErrors }
|
|
await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify(report, null, 2))
|
|
console.log('PANEL_INTEGRATION_' + report.result + ' ' + directory)
|
|
for (const failure of checks.filter(item => !item.pass)) console.log('PANEL_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 }, null, 2))
|
|
console.error(error); if (main) await shot('failure').catch(() => undefined); app.exit(1)
|
|
})
|
|
}
|