Files
goodbuddy/tests/support/workspace-files-layout-driver.mjs
T
mesalogo 56ebd40bd8 feat: unify execution and add workspace file imports
Remove Ask/Execute modes and general product tool approvals so requests use the selected runtime, enabled capabilities, and execution account permissions. Migrate retired application metadata while preserving user content.

Retire the Results page while retaining conversation images and stored artifacts. Add local and remote workspace file imports, timestamps and sorting, improve image discovery and saving, and refine workspace navigation and supervision responsiveness. Include the accompanying web-search changes, tests, probes, and product documentation.

Remote imports require an updated Agent. Existing artifacts are not automatically copied into project directories. Validation evidence and outstanding failures remain documented in feature progress files; no full test suite or real-model calls were rerun for this commit.

Release note: Run requests without switching work modes, import files into a selected local or remote workspace folder, sort workspace files by date, and keep using conversation images after removal of the Results page.
2026-10-04 20:02:59 +08:00

163 lines
11 KiB
JavaScript

import { app, BrowserWindow } from 'electron'
import assert from 'node:assert/strict'
import { mkdir, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import process from 'node:process'
import { setTimeout } from 'node:timers/promises'
import { setTimeout as startTimeout } from 'node:timers'
const directory = process.env.GB_LAYOUT_DIRECTORY
const artifacts = process.env.GB_LAYOUT_ARTIFACTS
app.setPath('userData', join(directory, 'profile'))
app.setPath('sessionData', join(directory, 'session'))
app.setAppLogsPath(join(directory, 'logs'))
app.commandLine.appendSwitch('force-device-scale-factor', '1')
app.commandLine.appendSwitch('disable-features', 'CalculateNativeWinOcclusion')
app.whenReady().then(async () => {
const win = new BrowserWindow({ show: true, width: 1280, height: 800, useContentSize: true,
webPreferences: { sandbox: true, contextIsolation: true, nodeIntegration: false, backgroundThrottling: false } })
const observations = [], errors = []
let pending = 'startup'
const environment = { platform: process.platform, arch: process.arch, electron: process.versions.electron, locale: 'en-US' }
const report = async result => {
const text = JSON.stringify({ ...result, environment, pending, observations, errors, modelCalls: 0 }, null, 2)
await writeFile(join(directory, 'result.json'), text)
if (artifacts) await writeFile(join(artifacts, 'result.json'), text)
}
startTimeout(async () => { await report({ passed: false, error: 'Driver timeout' }); app.exit(1) }, 80000)
win.webContents.on('console-message', event => { if (event.level === 'error') errors.push(event.message) })
win.webContents.on('render-process-gone', (_event, details) => errors.push(JSON.stringify(details)))
const js = code => { pending = code; return win.webContents.executeJavaScript(code) }
const wait = async code => {
for (let i = 0; i < 240; i++) { if (await js(code)) return; await setTimeout(25) }
throw new Error(`Timeout: ${code}\n${errors.join('\n')}`)
}
const settle = () => js('new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))')
const point = selector => js(`(() => {
const e=document.querySelector(${JSON.stringify(selector)}),r=e.getBoundingClientRect();
return {x:Math.round(r.x+r.width/2),y:Math.round(r.y+r.height/2),hit:e.contains(document.elementFromPoint(r.x+r.width/2,r.y+r.height/2))};
})()`)
const click = async selector => {
win.focus(); win.webContents.focus()
await wait('document.hasFocus()')
await js(`document.querySelector(${JSON.stringify(selector)}).scrollIntoView({block:'nearest',inline:'nearest'})`)
await settle()
const p = await point(selector)
win.webContents.sendInputEvent({ type: 'mouseMove', x: p.x, y: p.y })
await settle()
assert((await point(selector)).hit, `Control occluded: ${selector}`)
win.webContents.sendInputEvent({ type: 'mouseDown', x: p.x, y: p.y, button: 'left', clickCount: 1 })
win.webContents.sendInputEvent({ type: 'mouseUp', x: p.x, y: p.y, button: 'left', clickCount: 1 })
await settle()
}
const screenshot = async name => {
if (artifacts) await writeFile(join(artifacts, `${name}.png`), (await win.webContents.capturePage()).toPNG())
}
const viewport = "document.querySelector('.workspace-files__files-view')"
const geometry = () => js(`(() => {
const rect=e=>{const r=e.getBoundingClientRect();return {x:r.x,y:r.y,width:r.width,height:r.height,bottom:r.bottom,right:r.right}};
const v=${viewport},body=document.querySelector('.assistant-sidebar__body'),nav=document.querySelector('.workspace-files__navigation');
const selectors=['.workspace-files__header','.workspace-files__actions','.workspace-files__breadcrumbs'];
const controls=[...document.querySelectorAll('.workspace-files__header button,.workspace-files__actions button,.workspace-files__breadcrumbs button')];
return {viewport:rect(v),scrollTop:v.scrollTop,scrollHeight:v.scrollHeight,clientHeight:v.clientHeight,overflow:getComputedStyle(v).overflowY,
fixed:selectors.map(s=>{const e=document.querySelector(s);return e?rect(e):null}),
controls:controls.map(e=>{const r=e.getBoundingClientRect();return {label:e.getAttribute('aria-label')||e.textContent,hit:e.contains(document.elementFromPoint(r.x+r.width/2,r.y+r.height/2)),outside:!v.contains(e),...rect(e)}}),
bodyScroll:body.scrollTop,bodyOverflow:body.scrollHeight-body.clientHeight,nav:rect(nav),
horizontalOverflow:[document.documentElement,body,nav,v].map(e=>e.scrollWidth-e.clientWidth),
innerWidth,innerHeight,scale:devicePixelRatio};
})()`)
const docked = (before, after) => {
assert.deepEqual(after.fixed, before.fixed, 'Header/actions/breadcrumbs moved with files')
assert(after.controls.every(c => c.hit && c.outside && c.height > 0), 'Docked controls must remain visible and hit-testable')
assert.equal(after.bodyScroll, 0, 'Sidebar body must not scroll with files')
assert(after.bodyOverflow <= 1, 'Files must not overflow the sidebar body')
assert(after.horizontalOverflow.slice(0, 3).every(value => value <= 1), 'Files must scroll inside their viewport without widening the sidebar')
assert(after.viewport.bottom <= after.nav.bottom + 1, 'Files viewport exceeds navigation bounds')
assert(after.viewport.y >= Math.max(...after.fixed.filter(Boolean).map(r => r.bottom)) - 1, 'Controls overlap files viewport')
}
try {
if (artifacts) await mkdir(artifacts, { recursive: true })
for (const git of [false, true]) for (const theme of ['light', 'dark']) {
for (const [width, height, sidebar] of [[1280, 800, 560], [960, 720, 420], [720, 640, 320], [640, 420, 300]]) {
const name = `${git ? 'git' : 'non-git'}-${theme}-${width}x${height}`
win.setContentSize(width, height)
await win.loadURL(`${process.env.GB_LAYOUT_URL}?git=${git}&theme=${theme}&sidebar=${sidebar}`)
// The tree is windowed (P5): only rows near the viewport are mounted.
await wait(`document.querySelectorAll('.workspace-files__row').length>0 && !!${viewport}`)
await settle()
assert(await js(`document.querySelectorAll('.workspace-files__row').length<121`), 'Long files tree must be windowed')
await js('document.fonts.ready')
await settle()
assert.equal(await js('document.querySelectorAll(".workspace-files__view-switch button").length'), git ? 2 : 1)
assert.equal(await js('document.querySelector(".workspace-files__view-switch button").textContent'), 'Files')
assert.equal(await js('document.querySelector(".workspace-files__view-switch button").getAttribute("aria-pressed")'), 'true')
const root = await geometry()
assert.equal(root.innerWidth, width); assert.equal(root.innerHeight, height)
assert(root.scrollHeight > root.clientHeight * 3 && root.clientHeight > 50, 'Long files list needs a bounded viewport')
assert.equal(root.overflow, 'auto')
docked(root, root)
const p = await point('.workspace-files__files-view')
win.webContents.sendInputEvent({ type: 'mouseWheel', x: p.x, y: p.y, deltaY: -240, deltaX: 0 })
await wait(`${viewport}.scrollTop > 0`)
// Native wheel scrolling finishes asynchronously in Chromium.
await setTimeout(350)
docked(root, await geometry())
await js(`${viewport}.scrollTop=0`)
await settle()
await click('.workspace-files__entry .workspace-files__more')
await wait('!!document.querySelector("[role=menuitem]")')
await click('[role=menuitem]:first-child')
await wait('!!document.querySelector(".workspace-files__breadcrumbs [aria-current=location]") && document.querySelectorAll(".workspace-files__row").length>0')
const before = await geometry()
assert.equal(await js('document.querySelector(".workspace-files__breadcrumbs [aria-current=location]").textContent'), 'documents')
const last = '.workspace-files__entry:last-child .workspace-files__row'
// Scrolling to the end re-windows; the measured row heights may grow the
// list, so keep scrolling until the last file is mounted at the bottom.
for (let i = 0; i < 10; i++) {
await js(`${viewport}.scrollTop=${viewport}.scrollHeight`)
await settle()
if (await js(`document.querySelector(${JSON.stringify(last)})?.title==='documents/file-120-workspace-notes.md' && Math.abs(${viewport}.scrollHeight-${viewport}.clientHeight-${viewport}.scrollTop)<=1`)) break
}
const after = await geometry()
assert(after.scrollTop > 0)
docked(before, after)
assert(await js(`(() => {const r=document.querySelector(${JSON.stringify(last)}).getBoundingClientRect(),v=${viewport}.getBoundingClientRect();return r.top>=v.top && r.bottom<=v.bottom+1})()`), 'Last file must be fully visible')
assert.equal(await js(`document.querySelector(${JSON.stringify(last)}).title`), 'documents/file-120-workspace-notes.md')
await screenshot(name)
await click(last)
await wait('!!document.querySelector("#return-to-files")')
assert.equal(await js('getComputedStyle(document.querySelector(".workspace-files__navigation")).display'), 'none')
await click('#return-to-files')
await wait('!document.querySelector("#return-to-files")')
assert.equal(await js(`${viewport}.scrollTop`), after.scrollTop, 'Preview return must retain files scroll')
assert.equal(await js('document.querySelector(".workspace-files__row[aria-current=true]").title'), 'documents/file-120-workspace-notes.md')
docked(before, await geometry())
if (git) {
await click('.workspace-files__view-switch button:last-child')
assert.equal(await js(`getComputedStyle(${viewport}).display`), 'none')
await click('.workspace-files__view-switch button:first-child')
assert.equal(await js(`${viewport}.scrollTop`), after.scrollTop, 'Git view return must retain files scroll')
docked(before, await geometry())
}
await js(`${viewport}.scrollLeft=0`)
for (const label of ['New file', 'New directory']) {
await click(`.workspace-files__actions button[aria-label="${label}"]`)
await wait('!!document.querySelector("[role=dialog]")')
win.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Escape' })
win.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Escape' })
await wait('!document.querySelector("[role=dialog]")')
}
observations.push({ name, git, theme, sidebar, root, before, after, previewRetention: true, gitRetention: git, createDialogs: 2 })
}
}
assert.deepEqual(errors, [])
await report({ passed: true })
app.exit(0)
} catch (error) {
await screenshot('failure')
await report({ passed: false, error: String(error.stack) })
app.exit(1)
}
})