Files
goodbuddy/tests/support/supervisor-layout-driver.mjs
mesalogo d80d42b032 feat(storage): move desktop SQLite ownership into a storage utility process
Main no longer opens desktop business SQLite. A single utility-process
storage owner holds the Assistant, Knowledge, attachment, document-result,
remote-binding and native model-ledger databases and runs whole domain
transactions; Main and Runtime callers use explicit async domain operations.

- Remove the Main readWithFallback path and the supervision worker's
  separate business write connection; manifests are scanned read-only and
  committed by the sole writer.
- Remote events are ACKed only after commit; process loss reports
  unconfirmed writes instead of replaying them.
- Use native utilityProcess messaging with one in-flight bound (32) and
  ordered waiting instead of capacity rejection and retries.
- Start the storage process before the window so startup overlaps.
- Keep short tool output in memory and store full output once in owned
  storage; paging, branching, restart and deletion reuse that copy.
- Put required runtime launch files under goodbuddy-prefixed owned temp
  directories and reclaim confirmed stale ones; stop writing duplicate
  parsed.md.
- Add lint guards for SQLite ownership and architecture, concurrency
  acceptance fixtures, and maintenance/architecture documentation.

Validation: typecheck and lint pass. Alternating baseline (edf019aa) vs
candidate runs show no regression on single-session, C01, C02, C04 and a
5-minute C06; Main p99 drops from ~21 ms to ~16.7 ms under concurrency.
Fresh portable copy (1.75M events) upgrade content unchanged; real-model
output paging passes with 4 external requests. Full npm test runs had
1-9 different timing-sensitive failures per run that pass when rerun.
Not covered: C03 review publication, C07 source body, app-level C05.
2026-10-06 15:40:18 +08:00

1615 lines
112 KiB
JavaScript

import { app, BrowserWindow } from 'electron'
import assert from 'node:assert/strict'
import { mkdir, writeFile as writeArtifact } from 'node:fs/promises'
import { join } from 'node:path'
import process from 'node:process'
import console from 'node:console'
import { setTimeout } from 'node:timers/promises'
import { validateSupervisorSelection } from './supervisor-selection-driver.mjs'
import { validateStoryGraphFocus } from './story-graph-focus-driver.mjs'
const directory = process.env.GOODBUDDY_SUPERVISOR_DIRECTORY
const artifacts = process.env.GOODBUDDY_SUPERVISOR_ARTIFACTS || directory
const writeFile = (path, data) => process.env.GOODBUDDY_SUPERVISOR_NO_SCREENSHOT && path.endsWith('.png') ? Promise.resolve() : writeArtifact(path, data)
app.setPath('userData', join(directory, 'profile'))
app.commandLine.appendSwitch('force-device-scale-factor', '1')
app.commandLine.appendSwitch('disable-features', 'CalculateNativeWinOcclusion')
app
.whenReady()
.then(async () => {
await mkdir(artifacts, { recursive: true })
const win = new BrowserWindow({
show: false,
width: 1440,
height: 1100,
useContentSize: true,
webPreferences: {
sandbox: true,
contextIsolation: true,
nodeIntegration: false,
backgroundThrottling: false
}
})
const errors = []
win.webContents.on('console-message', (event) => {
if (event.level === 'error') errors.push(event.message)
})
const js = async (code) => {
try {
return await win.webContents.executeJavaScript(code)
} catch (error) {
throw new Error(`${code}\n${errors.join('\n')}`, { cause: error })
}
}
const settle = async () => {
await js(
'new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))'
)
await setTimeout(250)
}
const wait = async (code) => {
for (let i = 0; i < 600; i++) {
if (await js(code)) return
await setTimeout(25)
}
throw new Error(`Timeout: ${code}\n${errors.join('\n')}`)
}
const open = async (dense) => {
await win.loadURL(
process.env.GOODBUDDY_SUPERVISOR_URL + (dense ? '?dense=1' : '')
)
await wait('!!document.querySelector("#supervisor-tab-graph")')
await js('document.querySelector("#supervisor-tab-graph").click()')
await wait('!!document.querySelector(".supervisor-workspace__map")')
await js('document.fonts.ready')
await settle()
}
const reports = []
if (process.env.GOODBUDDY_STORY_GRAPH_FOCUS) {
await validateStoryGraphFocus(win, js, wait, process.env.GOODBUDDY_SUPERVISOR_URL)
assert.deepEqual(errors, [])
win.destroy(); app.quit(); return
}
if (process.env.GOODBUDDY_SUPERVISOR_WINDOWING) {
await validateSupervisorSelection(win, js, wait, process.env.GOODBUDDY_SUPERVISOR_URL)
assert.deepEqual(errors, [])
console.log('Supervisor source Electron selection, history, source and edit interactions passed')
win.destroy(); app.quit(); return
}
const selectHistory = async (id) => {
await js('document.querySelector(".supervisor-workspace__result-navigation button").click()')
await wait('!!document.querySelector(".supervisor-workspace__history-menu")')
await js(`[...document.querySelectorAll('.supervisor-workspace__history-menu button')].find(item => item.value === ${JSON.stringify(id)}).click()`)
}
if (process.env.GOODBUDDY_SUPERVISOR_HISTORY) {
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL + '?recap=1')
await wait('document.querySelector(".supervisor-workspace")?.getAttribute("aria-busy") === "false"')
for (const width of [1440, 390]) for (const theme of ['light', 'dark']) {
win.setContentSize(width, 800)
await wait(`innerWidth === ${width}`)
await js(`document.documentElement.dataset.theme = '${theme}'`)
await js('document.querySelector(".supervisor-workspace__result-navigation button").click()')
await wait('!!document.querySelector(".supervisor-workspace__history-menu")')
await settle()
const report = await js(`(() => {
const menu = document.querySelector('.supervisor-workspace__history-menu');
const context = document.querySelector('.supervisor-workspace__review-context');
const r = menu.getBoundingClientRect();
return { left:r.left, right:r.right, bottom:r.bottom, top:r.top,
overflow:menu.scrollWidth > menu.clientWidth + 1 || context.scrollWidth > context.clientWidth + 1,
options:[...menu.querySelectorAll('[role=menuitemradio]')].map(e => e.textContent),
duplicate:!!context.querySelector('.supervisor-workspace__review-meta') };
})()`)
assert(report.left >= 16 && report.right <= width - 16 && report.top >= 16 && report.bottom <= 784)
assert(!report.overflow && !report.duplicate)
assert.equal(report.options.length, 2)
assert(report.options.every(text => text.includes('生成于') && text.includes('全局') && text.includes('时间范围')))
await js('document.activeElement.dispatchEvent(new KeyboardEvent("keydown", {key:"Escape",bubbles:true}))')
reports.push({ width, theme, ...report })
}
await selectHistory('older-result')
await wait('document.querySelector(".supervisor-workspace")?.getAttribute("aria-busy") === "false"')
await js('document.querySelector("#supervisor-tab-graph").click()')
assert.equal(await js('document.querySelector(".supervisor-workspace__result-navigation button").value'), 'older-result')
assert.deepEqual(errors, [])
console.log(JSON.stringify({ history: 'passed', reports }))
win.destroy(); app.quit(); return
}
const checkSpiral = async () => {
const report = await js(`(() => {
const heading = document.querySelector('.story-graph-3d__toolbar');
const viewport = document.querySelector('.story-graph-3d__viewport');
const picker = document.querySelector('.story-graph-3d__picker');
const info = heading.querySelector('button[title="图例"]');
const h = heading.getBoundingClientRect(), v = viewport.getBoundingClientRect(), p = picker.getBoundingClientRect();
return {scene:JSON.parse(document.documentElement.dataset.helix),
levels:heading.parentElement.children.length, headingBottom:h.bottom, canvasTop:v.top,
pickerInHeading:heading.contains(picker), pickerTop:p.top, headingTop:h.top,
rootFont:getComputedStyle(heading.querySelector('strong')).fontSize,
iconOnly:info.textContent === '', infoLabel:info.getAttribute('aria-label'),
presets:document.querySelectorAll('.story-graph-3d__views,[aria-label="视角"]').length,
caption:document.querySelectorAll('.story-graph-3d__scale').length};
})()`)
assert.equal(report.levels, 2, 'Only heading controls and canvas')
assert.equal(report.canvasTop, report.headingBottom, 'No inner toolbar row between title and canvas')
assert(report.pickerInHeading && report.pickerTop >= report.headingTop)
assert.equal(report.rootFont, '13px')
assert(report.iconOnly && report.infoLabel === '图例')
assert.equal(report.presets, 0); assert.equal(report.caption, 0)
const { timeline, marks, staves } = report.scene
for (const mark of marks) assert(mark.storyId ? mark.color !== timeline.colors[0] : mark.color === timeline.colors[0], 'Assigned and unassigned marks keep their color semantics')
assert.equal(timeline.groups.reduce((total, group) => total + group.count, 0), timeline.indices, 'Every face belongs to the single helix')
assert(timeline.groups.some(group => group.materialIndex === 0), 'Sparse events leave neutral gaps')
const assignedIntervals = timeline.intervals.filter(interval => interval.event?.storyId)
if (assignedIntervals.length) assert(timeline.groups.some(group => group.materialIndex > 0), 'Saved intervals produce colored tube faces')
for (const interval of assignedIntervals) {
const mark = marks.find(mark => mark.ids.includes(interval.event.id))
assert(mark && mark.storyId === interval.event.storyId)
assert(timeline.colors.includes(mark.color), 'Event mark and interval share story palette')
assert(interval.from >= interval.event.t && interval.to <= interval.event.end)
}
for (const group of timeline.groups.filter(group => group.materialIndex > 0)) {
const covered = assignedIntervals.filter(interval => interval.from < group.to && interval.to > group.from)
const duration = covered.reduce((sum, interval) => sum + Math.max(0, Math.min(interval.to, group.to) - Math.max(interval.from, group.from)), 0)
assert(Math.abs(duration - (group.to - group.from)) < 200, 'Actual colored vertices never cover a gap (Float32 geometry tolerance)')
for (const interval of covered.filter(interval => Math.min(interval.to, group.to) - Math.max(interval.from, group.from) > 200)) {
assert.equal(timeline.colors[group.materialIndex], marks.find(mark => mark.ids.includes(interval.event.id))?.color)
}
}
for (const stave of staves) for (const mark of marks.filter(mark => mark.storyId === stave.id)) assert.equal(stave.color, mark.color)
await js(`document.querySelector('.story-graph-3d__toolbar button[title="图例"]').focus(); document.activeElement.click()`)
await wait('!!document.querySelector(".inline-help__content")')
assert(await js('document.activeElement.getAttribute("aria-expanded") === "true"'))
await js('document.activeElement.dispatchEvent(new KeyboardEvent("keydown", {key:"Escape",bubbles:true}))')
await wait('!document.querySelector(".inline-help__content")')
assert(await js('document.activeElement.getAttribute("title") === "图例"'), 'Info Escape retains trigger focus')
return { coloredIntervals: assignedIntervals.length, coloredGroups: timeline.groups.filter(group => group.materialIndex > 0).length,
marks: marks.length, assignedEvents: marks.filter(mark => mark.storyId).flatMap(mark => mark.ids).length,
unassignedEvents: marks.filter(mark => !mark.storyId).flatMap(mark => mark.ids).length }
}
const capturePage = async () => process.env.GOODBUDDY_SUPERVISOR_NO_SCREENSHOT
? { toPNG: () => new Uint8Array() }
: win.webContents.capturePage()
if (process.env.GOODBUDDY_SUPERVISOR_BACKUP) {
const resultId = process.env.GOODBUDDY_SUPERVISOR_RESULT
assert(resultId, 'Specify the historical result to measure')
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL + '?spiral=1&portable=1')
await wait('document.querySelector(".supervisor-workspace")?.getAttribute("aria-busy") === "false"')
await js('document.querySelector("#supervisor-tab-graph").click()')
await selectHistory(resultId)
await wait('document.querySelector(".supervisor-workspace")?.getAttribute("aria-busy") === "false" && document.querySelectorAll(".supervisor-workspace__list-panel button").length === 21')
await js('[...document.querySelectorAll("button")].find(b => b.textContent === "时间螺旋").click()')
await wait('JSON.parse(document.documentElement.dataset.helix || "null")?.dots.length === 21')
await js('document.fonts.ready')
await settle()
const geometry = await js('JSON.parse(document.documentElement.dataset.helix)')
const initialCamera = await js('JSON.parse(document.documentElement.dataset.camera)')
assert(Math.abs(initialCamera[1] / 900 - Math.sin(0.38)) < 1e-6, 'Default oblique camera')
const expected = await js(`fetch('/portable-review.json').then(r=>r.json()).then(data=>data.graphs[${JSON.stringify(resultId)}].events.map(e=>e.id))`)
assert.deepEqual([...geometry.events].sort(), [...expected].sort())
assert.deepEqual([...geometry.dots].sort(), [...expected].sort(), 'Real event clusters on the helix, not just empty staves')
assert.equal(geometry.points.length, 2801, 'Actual rings of one continuous tube for point-only history')
assert(geometry.clusters > 1 && geometry.clusters <= 21, 'Events occupy multiple real time positions after layout')
const radii = geometry.points.map(point => point[0])
assert(Math.max(...radii) - Math.min(...radii) > 40)
for (const [width, theme] of [[1440, 'light'], [1440, 'dark'], [1024, 'light'], [390, 'light'], [390, 'dark']]) {
win.setContentSize(width, 1000)
await wait(`innerWidth === ${width} && innerHeight === 1000`)
await js(`document.documentElement.dataset.theme = '${theme}'`)
await settle()
const boxes = await js(`(() => {
const box = e => { const r=e.getBoundingClientRect(); return {left:r.left,right:r.right,top:r.top,bottom:r.bottom,width:r.width,height:r.height}; };
const column=document.querySelector('.supervisor-workspace__graph-canvas');
const selector=document.querySelector('.supervisor-workspace__result-navigation button');
const toolbar=document.querySelector('.story-graph-3d__toolbar');
const views=toolbar.querySelector('button[title="图例"]');
const segment=toolbar.querySelector('.segmented-control');
const buttons=[...segment.querySelectorAll('button')].map(box);
return {width:innerWidth,pageWidth:document.documentElement.scrollWidth,column:box(column),selector:box(selector),
context:box(document.querySelector('.supervisor-workspace__review-context')),toolbar:box(toolbar),views:box(views),
canvas:box(document.querySelector('.story-graph-3d__gl')),gaps:buttons.slice(1).map((b,i)=>b.left-buttons[i].right),
segmentPadding:getComputedStyle(segment).paddingTop,
events:document.querySelectorAll('.supervisor-workspace__list-panel button').length,
bounds:JSON.parse(document.documentElement.dataset.sceneBounds),
scene:JSON.parse(document.documentElement.dataset.helix).dots.length,
clusters:JSON.parse(document.documentElement.dataset.helix).clusters};
})()`)
assert(boxes.selector.top >= boxes.context.top && boxes.selector.bottom <= boxes.context.bottom, 'History inside shared context')
assert(boxes.context.bottom <= boxes.column.top, 'Review context above graph columns')
assert(boxes.gaps.every(gap => gap >= 2), 'Shared camera segment gaps')
assert.equal(boxes.segmentPadding, '3px')
assert(boxes.views.right <= boxes.column.right - 16 && boxes.pageWidth <= width, `Controls fit without overflow: ${JSON.stringify(boxes)}`)
assert(boxes.canvas.width > 0 && boxes.canvas.height > 0)
assert.equal(boxes.events, 21); assert.equal(boxes.scene, 21)
assert(Object.values(boxes.bounds).every(value => Math.abs(value) < 1), 'All root scene vertices fit within the camera viewport')
reports.push({ theme, ...boxes, colors: await checkSpiral() })
if (width === 1440 && !process.env.GOODBUDDY_SUPERVISOR_NO_SCREENSHOT) await writeFile(join(artifacts, 'portable-spiral.png'), (await win.webContents.capturePage()).toPNG())
}
await js('document.querySelector(".supervisor-workspace__list-panel button").click()')
await wait('document.querySelector(".supervisor-workspace__list-panel button")?.getAttribute("aria-pressed") === "true"')
// The pre-assignment case uses the same real events with an explicitly empty membership response.
await js(`window.goodbuddy.supervision.stories = async () => ({stories:[],experiences:[],unassigned:21,canUndo:false}); document.querySelector('.supervisor-workspace__review-context > .secondary-button').click()`)
await wait('JSON.parse(document.documentElement.dataset.helix || "null")?.dots.length === 21 && document.querySelector(".supervisor-workspace")?.getAttribute("aria-busy") === "false"')
await js('document.querySelector(".story-graph-3d__picker").click()')
await wait('document.querySelectorAll(".story-graph-3d__menu [role=menuitem]").length === 1')
await js('document.querySelector(".story-graph-3d__menu [role=menuitem]").click()')
await wait('document.querySelector(".story-graph-3d__picker")?.textContent.includes("未归属事件")')
assert.equal(await js('JSON.parse(document.documentElement.dataset.helix).dots.length'), 21)
assert(await js('JSON.parse(document.documentElement.dataset.helix).marks.every(mark => !mark.storyId)'), 'Empty membership keeps neutral event marks')
assert.deepEqual(errors, [])
await writeFile(join(artifacts, 'portable-measurements.json'), JSON.stringify({ resultId, reports, clusters: geometry.clusters, radiusRange: [Math.min(...radii), Math.max(...radii)], errors }, null, 2))
console.log(JSON.stringify({ resultId, reports, clusters: geometry.clusters, radiusRange: [Math.min(...radii), Math.max(...radii)], emptyMembershipEvents: 21, errors }))
win.destroy(); app.quit(); return
}
if (process.env.GOODBUDDY_SUPERVISOR_SPIRAL) {
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL + '?spiral=1&recap=1')
await wait('document.querySelector(".supervisor-workspace")?.getAttribute("aria-busy") === "false"')
await js('document.querySelector("#supervisor-tab-graph").click()')
await wait('[...document.querySelectorAll("button")].some(b => b.textContent === "时间螺旋")')
await js('[...document.querySelectorAll("button")].find(b => b.textContent === "时间螺旋").click()')
await wait('JSON.parse(document.documentElement.dataset.helix || "null")?.events.length === 8')
const geometry = await js('JSON.parse(document.documentElement.dataset.helix)')
const palettes = new Map()
assert(new Set(geometry.timeline.groups.filter(group => group.materialIndex > 0).map(group => group.materialIndex)).size >= 2, 'Different primary stories paint distinct interval materials in this fixture')
assert(geometry.points.length >= 2801, 'One continuous helix with exact interval boundaries')
const radii = geometry.points.map(p => p[0])
assert(Math.max(...radii) - Math.min(...radii) > 40, 'Actual rendered geometry reflects uneven attention')
geometry.points.forEach((point, i) => { if (i) assert(point[1] > geometry.points[i - 1][1], 'Actual tube advances monotonically in time') })
assert(!geometry.events.includes('outside-review'), 'Aggregate story events outside this review are not plotted')
for (const [width, theme] of [[1440, 'light'], [1440, 'dark'], [1024, 'light'], [390, 'light'], [390, 'dark']]) {
win.setContentSize(width, 1000)
await wait(`innerWidth === ${width} && innerHeight === 1000`)
await js(`document.documentElement.dataset.theme = '${theme}'`)
await settle()
const boxes = await js(`(() => {
const selector = document.querySelector('.supervisor-workspace__result-navigation button');
const r = selector.getBoundingClientRect(), gl = document.querySelector('.story-graph-3d__gl').getBoundingClientRect();
const toolbar = document.querySelector('.story-graph-3d__toolbar').getBoundingClientRect();
const breadcrumb = document.querySelector('.story-graph-3d__breadcrumb');
const context = document.querySelector('.supervisor-workspace__review-context').getBoundingClientRect();
return { value: selector.value, left: r.left, right: r.right, top: r.top, bottom: r.bottom,
toolbar: { left: toolbar.left, top: toolbar.top, bottom: toolbar.bottom },
atRoot: breadcrumb.textContent === '全部', context: {top:context.top,bottom:context.bottom},
pickers: document.querySelectorAll('.story-graph-3d__picker').length,
headingSelectors: document.querySelectorAll('.supervisor-workspace__canvas-heading select').length,
selectors: document.querySelectorAll('.supervisor-workspace__result-navigation button').length,
width: innerWidth, canvasWidth: gl.width, canvasHeight: gl.height, pageWidth: document.documentElement.scrollWidth };
})()`)
assert.equal(boxes.value, 'fixture-result')
assert.equal(boxes.selectors, 1)
assert.equal(boxes.headingSelectors, 0)
assert(boxes.atRoot, 'Root All breadcrumb stays independent of review history')
assert.equal(boxes.pickers, 1, 'Only the hierarchy drill picker remains')
assert(boxes.top >= boxes.context.top && boxes.bottom <= boxes.context.bottom, 'History is inside shared review context')
assert(boxes.context.bottom <= boxes.toolbar.top, 'Context precedes graph tools')
assert(boxes.left >= 0 && boxes.right <= boxes.width && boxes.pageWidth <= boxes.width, 'History selector fits without horizontal overflow')
assert(boxes.canvasWidth > 0 && boxes.canvasHeight > 0, 'WebGL canvas is laid out')
assert((await checkSpiral()).coloredGroups > 0, 'Interval fixture paints the actual tube in both themes and all sizes')
palettes.set(theme, await js('JSON.parse(document.documentElement.dataset.helix).timeline.colors'))
await js('[...document.querySelectorAll(".supervisor-workspace__graph-mode button")].find(b => b.getAttribute("aria-pressed") === "false").click()')
await wait('!!document.querySelector(".supervisor-workspace__map")')
assert.equal(await js('getComputedStyle(document.querySelector(".supervisor-workspace__canvas-heading > strong")).fontSize'), '13px', 'Flat and spiral All use identical typography')
await js('document.querySelector(".supervisor-workspace__canvas-heading button[title=图例]").click()')
await wait('!!document.querySelector(".inline-help__content")')
await js('window.dispatchEvent(new KeyboardEvent("keydown", {key:"Escape",bubbles:true}))')
await wait('!document.querySelector(".inline-help__content")')
await js('[...document.querySelectorAll(".supervisor-workspace__graph-mode button")].find(b => b.textContent === "时间螺旋").click()')
await wait('!!document.querySelector(".story-graph-3d__gl")')
await js('document.querySelector("#supervisor-tab-overview").click(); document.querySelector(".page-shell").scrollTop = 0')
await settle()
const overview = await js(`(() => { const r = document.querySelector('.supervisor-workspace__review-context').getBoundingClientRect();
return {top:r.top,bottom:r.bottom,forms:document.querySelectorAll('.supervisor-workspace__toolbar').length}; })()`)
assert.equal(overview.top, boxes.context.top, 'Review bar keeps its top across tabs')
assert.equal(overview.bottom, boxes.context.bottom, 'Review bar keeps its height across tabs')
assert.equal(overview.forms, 0, 'New review inputs collapsed by default')
await js('document.querySelector("#supervisor-tab-graph").click()')
await wait('!!document.querySelector(".story-graph-3d__gl")')
}
assert.notDeepEqual(palettes.get('light'), palettes.get('dark'), 'WebGL palette follows theme tokens')
await js('[...document.querySelectorAll(".supervisor-workspace__graph-mode button")].find(b => b.textContent === "时间螺旋").click()')
await wait('!!document.querySelector(".story-graph-3d__gl")')
const cameraBefore = await js('document.documentElement.dataset.camera')
await js('document.querySelector(".story-graph-3d__viewport canvas[tabindex]").dispatchEvent(new KeyboardEvent("keydown", {key:"ArrowLeft",bubbles:true}))')
await wait(`document.documentElement.dataset.camera !== ${JSON.stringify(cameraBefore)}`)
win.show(); win.focus(); win.webContents.focus()
await wait('document.hasFocus()')
await js('document.querySelector(".story-graph-3d__toolbar button[title=图例]").focus()')
win.webContents.sendInputEvent({type:'keyDown', keyCode:'Space'})
win.webContents.sendInputEvent({type:'keyUp', keyCode:'Space'})
await wait('document.activeElement.getAttribute("aria-expanded") === "true"')
win.webContents.sendInputEvent({type:'keyDown', keyCode:'Escape'})
win.webContents.sendInputEvent({type:'keyUp', keyCode:'Escape'})
await wait('!document.querySelector(".inline-help__content")')
const dragAt = await js(`(() => { const c = document.querySelector('.story-graph-3d__viewport canvas[tabindex]'); c.scrollIntoView();
const r = c.getBoundingClientRect(); return {x:Math.round(r.left+r.width/2), y:Math.round(r.top+r.height/2)}; })()`)
const beforeDrag = await js('document.documentElement.dataset.camera')
win.webContents.sendInputEvent({type:'mouseDown',button:'left',clickCount:1,...dragAt})
win.webContents.sendInputEvent({type:'mouseMove',button:'left',modifiers:['leftButtonDown'],x:dragAt.x+30,y:dragAt.y+12})
win.webContents.sendInputEvent({type:'mouseUp',button:'left',clickCount:1,x:dragAt.x+30,y:dragAt.y+12})
await wait(`document.documentElement.dataset.camera !== ${JSON.stringify(beforeDrag)}`)
win.setContentSize(1024, 900)
await settle()
assert(await js('!document.querySelector(".supervisor-workspace__detail").getClientRects().length'), 'Medium details closed by default')
await js('document.querySelector(".supervisor-workspace__list-panel button").click()')
await wait('document.activeElement === document.querySelector(".supervisor-workspace__detail")')
assert(await js(`(() => { const panel = document.querySelector('.supervisor-workspace__detail').getBoundingClientRect();
const graph = document.querySelector('.supervisor-workspace__graph-layout').getBoundingClientRect();
return panel.top >= graph.top && panel.bottom <= graph.bottom + 1; })()`), 'Medium details stay inside graph row')
await js('document.activeElement.dispatchEvent(new KeyboardEvent("keydown", {key:"Escape",bubbles:true}))')
await wait('!document.querySelector(".supervisor-workspace__detail").getClientRects().length')
assert(await js('document.activeElement.matches(".supervisor-workspace__detail-toggle")'), 'Details restore toggle focus')
win.setContentSize(1440, 700)
await settle()
assert(await js('document.querySelector(".supervisor-workspace__graph-layout").getBoundingClientRect().height < 660'), 'Short spiral does not inherit flat graph minimum')
for (const name of ['Fixture project', 'Fixture story']) {
await js('document.querySelector(".story-graph-3d__picker").click()')
await wait('!!document.querySelector(".story-graph-3d__menu [role=menuitem]")')
await js('document.activeElement.dispatchEvent(new KeyboardEvent("keydown", {key:"End",bubbles:true}))')
assert(await js('document.activeElement === [...document.querySelectorAll(".story-graph-3d__menu [role=menuitem]")].at(-1)'), 'Hierarchy menu keyboard End')
await js(`([...document.querySelectorAll('.story-graph-3d__menu [role=menuitem]')].find(e => e.textContent.includes('${name}'))).click()`)
await wait(`document.querySelector('.story-graph-3d__breadcrumb strong')?.textContent === '${name}'`)
if (name === 'Fixture project') await checkSpiral()
assert.equal(await js('document.querySelectorAll(".story-graph-3d__breadcrumb select").length'), 0)
}
await js('[...document.querySelectorAll(".story-graph-3d__breadcrumb button")].find(b => b.textContent === "Fixture project").click()')
await wait('document.querySelector(".story-graph-3d__breadcrumb strong")?.textContent === "Fixture project"')
await js('document.querySelector(".story-graph-3d__breadcrumb button").click()')
await wait('document.querySelector(".story-graph-3d__breadcrumb strong")?.textContent === "全部"')
assert.equal(await js('document.querySelectorAll(".story-graph-3d__breadcrumb > span").length'), 1)
await js('[...document.querySelectorAll(".supervisor-workspace__graph-mode button")].find(b => b.getAttribute("aria-pressed") === "false").click()')
await wait('!!document.querySelector(".supervisor-workspace__map")')
assert.equal(await js('document.querySelector(".supervisor-workspace__result-navigation button").value'), 'fixture-result')
assert.equal(await js('document.querySelectorAll(".supervisor-workspace__result-navigation button").length'), 1)
await js('[...document.querySelectorAll(".supervisor-workspace__graph-mode button")].find(b => b.getAttribute("aria-pressed") === "false").click()')
await wait('!!document.querySelector(".story-graph-3d__gl")')
await selectHistory('older-result')
await wait('document.querySelector(".supervisor-workspace")?.getAttribute("aria-busy") === "false" && JSON.parse(document.documentElement.dataset.helix || "null")?.events.length === 1')
await js('document.querySelector("#supervisor-tab-overview").click()')
await wait('document.querySelector(".supervisor-workspace__result-navigation button")?.value === "older-result"')
assert.deepEqual(errors, [])
console.log(JSON.stringify({ spiral: 'passed', vertices: geometry.points.length, radiusRange: [Math.min(...radii), Math.max(...radii)], widths: [1440, 390], errors }))
win.destroy(); app.quit(); return
}
const checkDiscussion = async () => {
const report = await js(`(() => {
const editor = document.querySelector('.supervision-discussion-editor');
const box = e => { const r = e.getBoundingClientRect(); return {left:r.left, right:r.right, top:r.top, bottom:r.bottom}; };
const target = editor.querySelector('.supervision-discussion-editor__target');
const field = editor.querySelector('.field'), label = field.querySelector('span'), input = field.querySelector('textarea');
const footer = editor.querySelector('footer');
return { target:box(target), field:box(field), label:box(label), input:box(input), footer:box(footer),
buttons:[...footer.querySelectorAll('button')].map(box), align:getComputedStyle(footer).justifyContent,
clientWidth:editor.clientWidth, scrollWidth:editor.scrollWidth, value:input.value,
border:getComputedStyle(target).borderBottomWidth, disclosures:editor.querySelectorAll('details').length };
})()`)
assert(report.value.includes('本周已核对交付清单') && report.value.includes('原始依据:模拟会议记录'), 'Actual reference content is editable')
assert.equal(report.field.top - report.target.bottom, 16, 'Target to message spacing')
assert.equal(report.input.top - report.label.bottom, 8, 'Label to input spacing')
assert.equal(report.footer.top - report.field.bottom, 16, 'Message to footer spacing')
assert.equal(report.border, '1px', 'Destination is separated')
assert.equal(report.align, 'flex-end', 'Shared right aligned footer')
assert(Math.abs(report.buttons[1].right - report.footer.right) < 1, 'Send at right edge')
assert(report.buttons[0].right < report.buttons[1].left && report.buttons[0].top === report.buttons[1].top, 'Cancel then send')
assert(report.scrollWidth <= report.clientWidth && report.disclosures === 0, 'No overflow or redundant disclosure')
return report
}
if (process.env.GOODBUDDY_SUPERVISOR_SETTINGS) {
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL)
await wait('!!document.querySelector("#supervisor-tab-settings")')
await js('document.querySelector("#supervisor-tab-settings").click()')
await wait('!!document.querySelector(".supervisor-settings")')
await js('document.fonts.ready')
for (const theme of ['light', 'dark']) {
await js(`document.documentElement.dataset.theme = '${theme}'`)
for (const [width, height] of [[1440, 1100], [1024, 768], [390, 480]]) {
win.setContentSize(width, height)
for (const tab of ['model', 'review', 'stories', 'suggestions']) {
await js(`document.querySelector('#supervisor-settings-tab-${tab}').click(); document.querySelector('.page-shell').scrollTop = 0`)
await wait(`document.querySelector('#supervisor-settings-tab-${tab}').getAttribute('aria-selected') === 'true' && innerWidth === ${width}`)
await settle()
const report = await js(`(() => {
const root = document.querySelector('.supervisor-settings');
const nav = root.querySelector('.page-tabs');
const panel = root.querySelector('[role=tabpanel]:not([hidden])');
const form = panel.querySelector('form');
const box = e => { const r = e.getBoundingClientRect(); return {left:r.left, right:r.right, top:r.top, bottom:r.bottom, height:r.height}; };
return { root:box(root), nav:box(nav), form:box(form), heading:form.querySelector('h2').textContent,
primaryTabs:document.querySelectorAll('.heartbeat-center > .page-tabs [role=tab]').length,
settingsTabs:nav.querySelectorAll('[role=tab]').length,
visiblePanels:root.querySelectorAll('[role=tabpanel]:not([hidden])').length,
navFlex:getComputedStyle(nav).flexShrink, rootFlex:getComputedStyle(root).flexShrink,
formBorder:getComputedStyle(form).borderTopWidth, formBackground:getComputedStyle(form).backgroundColor,
pageWidth:document.documentElement.scrollWidth, rootWidth:root.clientWidth, rootScrollWidth:root.scrollWidth,
controls:[...form.querySelectorAll('input,select,button')].map(box) };
})()`)
assert.equal(report.primaryTabs, 5)
assert.equal(report.settingsTabs, 4)
assert.equal(report.visiblePanels, 1)
assert.equal(report.nav.top - report.root.top, 17, 'Tabs inset from the card border')
assert.equal(report.form.top - report.nav.bottom, 24, 'Tabs separated from section content')
assert.equal(report.form.left, report.nav.left, 'Section and tabs share the content edge')
assert.equal(report.navFlex, '0')
assert.equal(report.rootFlex, '0')
assert(report.nav.height >= 36, 'Short windows do not compress tabs')
assert.equal(report.formBorder, '0px', 'No nested form border')
assert.equal(report.formBackground, 'rgba(0, 0, 0, 0)', 'One settings surface')
assert(report.pageWidth <= width && report.rootScrollWidth <= report.rootWidth, 'No horizontal page or card overflow')
assert(report.controls.every(control => control.left >= report.form.left && control.right <= report.form.right + 1), 'Fields fit the section')
if (tab === 'model') assert.equal(report.heading, '模型与运行限制')
reports.push({ theme, width, height, tab, ...report })
await writeFile(join(artifacts, `settings-${theme}-${width}-${tab}.png`), (await win.webContents.capturePage()).toPNG())
}
}
}
assert.deepEqual(errors, [])
await writeFile(join(artifacts, 'settings-measurements.json'), JSON.stringify(reports, null, 2))
console.log(JSON.stringify({ settingsCases: reports.length, tabInset: 17, contentGap: 24, errors }))
win.destroy()
app.quit()
return
}
if (process.env.GOODBUDDY_SUPERVISOR_CONTENT_LAYOUT) {
win.show()
const controlsOnly = process.env.GOODBUDDY_SUPERVISOR_CONTENT_LAYOUT === 'controls'
for (const view of controlsOnly ? ['activity', 'pending'] : ['recap', 'empty', 'activity', 'pending', 'failure']) {
console.log(`Checking content layout: ${view}`)
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL + (view === 'activity' ? '?activity=1' : '?recap=1&long-summary=1' + (view === 'failure' ? '&fail-run=1' : view === 'empty' ? '&state=empty' : '')))
await wait('document.querySelector(".supervisor-workspace")?.getAttribute("aria-busy") === "false"')
if (view === 'activity') {
await js('document.querySelector("#supervisor-tab-activity").click()')
await wait('document.querySelectorAll(".supervisor-activity__item").length === 3')
} else if (view === 'pending' || view === 'failure') {
await js('document.querySelector(".supervisor-workspace__review-context .primary-button").click()')
await wait('!!document.querySelector(".supervisor-workspace__toolbar .primary-button")')
await js('document.querySelector(".supervisor-workspace__toolbar .primary-button").click()')
await wait(`document.documentElement.dataset.reviewNoticeTone === '${view === 'pending' ? 'info' : 'error'}'`)
}
await js('document.fonts.ready')
for (const theme of ['light', 'dark']) {
await js(`document.documentElement.dataset.theme = '${theme}'`)
for (const width of controlsOnly ? [1440, 1024, 390] : [2000, 1440, 1024, 390]) {
win.setContentSize(width, 1100)
await js('document.querySelector(".page-shell").scrollTop = 0')
await settle()
const report = await js(`(() => {
const box = selector => { const r = document.querySelector(selector)?.getBoundingClientRect(); return r && { left:r.left, right:r.right, width:r.width, height:r.height, top:r.top, bottom:r.bottom }; };
return { width:innerWidth, pageWidth:document.documentElement.scrollWidth,
panel:box('.heartbeat-center > [role=tabpanel]:not([hidden])'), toolbar:box('.supervisor-workspace__review-context'), recap:box('.supervisor-workspace__recap'),
prose:box('.supervisor-workspace__prose'), activity:box('.supervisor-activity'),
readingColumns:document.querySelector('.supervisor-workspace__recap') && getComputedStyle(document.querySelector('.supervisor-workspace__recap')).gridTemplateColumns.split(' ').map(parseFloat),
sidebar:box('.supervisor-workspace__recap-sidebar'),
steps:box('.supervisor-activity__steps'),
empty:box('.supervisor-workspace > .empty-state'), emptyTitle:box('.supervisor-workspace > .empty-state strong'),
stageConnectors:[...document.querySelectorAll('.supervisor-activity__steps li:not(:last-child)')].map(e=>getComputedStyle(e,'::after').borderTopWidth),
summaryLength:[...document.querySelectorAll('.supervisor-workspace__summary')].map(e=>e.textContent).join('').length,
tail:document.querySelector('.supervisor-workspace__prose')?.textContent.includes('长摘要末尾'),
controls:[...document.querySelectorAll('.supervisor-activity > .supervisor-workspace__action-bar > *')].map(e=>{const r=e.getBoundingClientRect();return {left:r.left,right:r.right,top:r.top};}),
toolbarControls:[...document.querySelectorAll('.supervisor-workspace__toolbar select, .supervisor-workspace__toolbar button')].map(e=>({disabled:e.disabled,text:e.textContent,top:e.getBoundingClientRect().top,bottom:e.getBoundingClientRect().bottom})),
notice:box('.supervisor-workspace__run-status'),
cancel:box('.supervisor-activity__item .danger-ghost') };
})()`)
assert(report.pageWidth <= width, 'No page overflow')
if (view === 'empty') {
assert(Math.abs(report.empty.width - report.panel.width) < 1, 'Empty state fills panel');
assert(Math.abs((report.emptyTitle.left + report.emptyTitle.right) / 2 - (report.panel.left + report.panel.right) / 2) < 1, 'Empty state title centered');
assert(report.empty.top >= report.toolbar.bottom, 'Empty state below toolbar');
} else if (view !== 'activity') {
assert(report.summaryLength > 2000 && report.tail, 'Long summary retained')
assert(Math.abs(report.recap.width - report.panel.width) < 1, 'Recap fills the page panel')
if (width >= 1024) assert(Math.abs(report.readingColumns[0] / report.readingColumns[1] - 2) < 0.02, 'Overview uses a 2:1 reading layout')
else assert(report.sidebar.top >= report.prose.bottom, 'Narrow overview puts supporting content after summary')
for (const control of [report.toolbar]) {
assert(Math.abs(control.left - report.recap.left) < 1 && Math.abs(control.right - report.recap.right) < 1, 'Toolbar, history and result share edges')
}
assert(report.prose.left - report.recap.left <= 25, 'No separately centered inner body')
if (view === 'pending') {
assert(report.toolbarControls.every(control => !control.disabled), 'Long review keeps toolbar enabled')
assert.equal(report.toolbarControls[2].text, '回顾', 'Start name remains stable')
assert.equal(report.notice, undefined, 'Operation feedback does not occupy workspace layout')
if (width >= 1024) assert(Math.max(...report.toolbarControls.map(control => control.top)) - Math.min(...report.toolbarControls.map(control => control.top)) <= 4, 'Toolbar stays on one aligned row')
}
} else {
assert(Math.abs(report.activity.width - report.panel.width) < 1, 'Activity fills the page panel')
assert(report.cancel.width >= 28 && report.cancel.right <= report.panel.right, 'Cancel fits activity panel')
if (width >= 1024) {
assert(report.steps.height < 40, 'Compact stage row')
assert(report.stageConnectors.every(value => value === '1px'), 'Connected stages')
assert(report.controls[2].left - report.controls[1].right <= 24, 'Refresh grouped with filter')
}
}
reports.push({ view, theme, ...report })
await writeFile(join(artifacts, `${view}-${theme}-${width}.png`), (await capturePage()).toPNG())
if (view === 'recap') {
await js('document.querySelector(".supervisor-workspace__prose").lastElementChild.scrollIntoView({block:"end"})')
await settle()
await writeFile(join(artifacts, `${view}-tail-${theme}-${width}.png`), (await capturePage()).toPNG())
}
}
}
}
assert.deepEqual(errors, [])
await writeFile(join(artifacts, 'content-layout-measurements.json'), JSON.stringify(reports, null, 2))
console.log(JSON.stringify({ artifacts, cases: reports.length, errors }))
win.destroy()
app.quit()
return
}
if (process.env.GOODBUDDY_SUPERVISOR_ACTIVITY) {
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL + '?activity=1')
await wait('!!document.querySelector("#supervisor-tab-activity")')
await js('document.querySelector("#supervisor-tab-activity").click()')
await wait('document.querySelectorAll(".supervisor-activity__item").length === 3')
await js('document.fonts.ready')
win.show()
win.focus()
for (const theme of ['light', 'dark']) {
await js(`document.documentElement.dataset.theme = '${theme}'`)
for (const width of [1440, 390]) {
win.setContentSize(width, 1000)
await js('document.querySelectorAll(".supervisor-activity__item details").forEach(e => e.open = false); document.querySelector(".page-shell").scrollTop = 0')
await settle()
const report = await js(`(() => {
const root = document.querySelector('.supervisor-activity');
const item = root.querySelector('.supervisor-activity__item');
return { width: innerWidth, pageWidth: document.documentElement.scrollWidth, clientWidth: root.clientWidth, scrollWidth: root.scrollWidth,
itemHeight: item.getBoundingClientRect().height,
states: [...item.querySelectorAll('[data-phase]')].map(e => e.dataset.state),
diagnosticOpen: item.querySelector('.supervisor-activity__diagnostics').open,
advancedOpen: item.querySelector('.supervisor-activity__advanced').open };
})()`)
assert(report.pageWidth <= width && report.scrollWidth <= report.clientWidth, 'Activity overflow')
assert.deepEqual(report.states, ['completed', 'completed', 'failed', 'pending'])
assert(!report.diagnosticOpen && !report.advancedOpen, 'Details collapsed by default')
assert(report.itemHeight < (width === 390 ? 650 : 440), 'Compact run summary')
reports.push({ theme, ...report })
await writeFile(join(artifacts, `review-activity-${theme}-${width}.png`), (await win.webContents.capturePage()).toPNG())
await js('document.querySelector(".supervisor-activity__diagnostics summary").focus()')
win.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Enter' })
win.webContents.sendInputEvent({ type: 'char', keyCode: '\r' })
win.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Enter' })
await settle()
assert(await js('document.querySelector(".supervisor-activity__diagnostics").open'), 'Native keyboard disclosure')
const error = await js(`(() => { const e = document.querySelector('.supervisor-activity__diagnostics pre'); return { height: e.clientHeight, scrollHeight: e.scrollHeight, selectable: getComputedStyle(e).userSelect, text: e.textContent }; })()`)
assert(error.height <= 240 && error.scrollHeight > error.height && error.selectable === 'text' && error.text.includes('relations'), 'Full bounded selectable error')
await js('document.querySelector(".supervisor-activity__diagnostics").scrollIntoView({block:"center"})')
await settle()
await writeFile(join(artifacts, `review-error-${theme}-${width}.png`), (await win.webContents.capturePage()).toPNG())
await js('document.querySelector(".supervisor-activity__diagnostics").open = false; document.querySelector(".supervisor-activity__item button[aria-expanded=false]")?.click()')
await wait('!!document.querySelector(".supervisor-activity__batch")')
await js('document.querySelectorAll(".supervisor-activity__project, .supervisor-activity__conversation").forEach(e => e.open = true); document.querySelector(".supervisor-activity__tree").scrollIntoView({block:"start"})')
await settle()
assert(await js('document.querySelectorAll(".supervisor-activity__batch").length === 6'), 'All six saved leaves')
assert(await js('document.documentElement.scrollWidth <= innerWidth'), 'Hierarchy overflow')
await writeFile(join(artifacts, `review-tree-${theme}-${width}.png`), (await win.webContents.capturePage()).toPNG())
await js('document.querySelector(".supervisor-activity__item button[aria-expanded=true]").click()')
}
}
await js('document.querySelectorAll(".supervisor-activity__item")[1].querySelector("button[aria-expanded]").click()')
await wait('document.querySelectorAll(".supervisor-activity__batch").length === 10')
await js('[...document.querySelectorAll(".supervisor-activity__tree button")].find(e => e.textContent === "下一页").click()')
await wait('document.querySelectorAll(".supervisor-activity__batch").length === 2')
assert(await js('[...document.querySelectorAll(".supervisor-activity__tree button")].find(e => e.textContent === "下一页").disabled'), 'Paging ends at persisted batch count')
assert.deepEqual(errors, [])
await writeFile(join(artifacts, 'review-activity-measurements.json'), JSON.stringify(reports, null, 2))
console.log(JSON.stringify({ artifacts, cases: reports.length, errors }))
win.destroy()
app.quit()
return
}
if (process.env.GOODBUDDY_SUPERVISOR_RECAP) {
for (const scenario of ['populated', 'automatic-empty', 'empty']) {
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL + '?recap=1' + (scenario === 'populated' ? '&menu=1' : scenario === 'empty' ? '&state=empty' : ''))
await wait('document.querySelector(".supervisor-workspace")?.getAttribute("aria-busy") === "false"')
await js('document.fonts.ready')
for (const width of [1440, 390]) {
win.setContentSize(width, 1100)
for (const tab of ['overview', 'plans']) {
await js(`document.querySelector('#supervisor-tab-${tab}').click(); document.querySelector('.page-shell').scrollTop = 0`)
await settle()
const report = await js(`(() => {
const panel = document.querySelector('#supervisor-panel-${tab}');
const recap = panel.querySelector('.supervisor-workspace__recap');
return { width: innerWidth, pageWidth: document.documentElement.scrollWidth,
clientWidth: panel.clientWidth, scrollWidth: panel.scrollWidth,
reports: !!panel.querySelector('#heartbeat-reports-title'),
suggestions: !!panel.querySelector('#supervision-suggestions-title'),
refresh: [...panel.querySelectorAll('button')].filter(b => b.getClientRects().length && b.textContent.includes('刷新')).length,
recap: !!recap && !!recap.getClientRects().length,
paragraphs: recap?.querySelectorAll('.supervisor-workspace__summary').length ?? 0,
proseWidth: recap?.querySelector('.supervisor-workspace__prose').getBoundingClientRect().width,
text: panel.innerText };
})()`)
assert(report.pageWidth <= width && report.scrollWidth <= report.clientWidth, 'Recap/automatic overflow')
assert.equal(report.refresh, 1, 'One refresh owner per panel')
assert.equal(report.reports, tab === 'plans' && scenario === 'populated')
assert.equal(report.suggestions, tab === 'plans' && scenario === 'populated')
assert.equal(report.recap, tab === 'overview' && scenario !== 'empty')
if (tab === 'overview') {
assert(!report.text.includes('暂无历史心跳报告') && !report.text.includes('待确认记忆') && !report.text.includes('监督建议'))
if (scenario !== 'empty') {
assert.equal(report.paragraphs, 4)
assert(report.text.includes('外部评审时间') && report.text.includes('未解决事项'))
assert(report.proseWidth > report.clientWidth * (width > 1000 ? 0.55 : 0.8), 'Prose uses the primary reading column')
} else assert(report.text.includes('还没有成功回顾'))
} else {
assert(!report.text.includes('暂无历史心跳报告'))
assert.equal(report.text.includes('历史心跳报告'), scenario === 'populated')
}
reports.push({ scenario, tab, ...report })
const prefix = `recap-${scenario}-${tab}-${width}`
await writeFile(join(artifacts, `${prefix}.png`), (await win.webContents.capturePage()).toPNG())
if (tab === 'plans' && scenario === 'populated') {
for (const anchor of ['supervision-suggestions-title', 'heartbeat-reports-title']) {
await js(`document.getElementById('${anchor}').scrollIntoView({block:'start'})`)
if (anchor === 'heartbeat-reports-title' && scenario === 'populated') await js(`document.querySelector('#heartbeat-panel-history button[aria-expanded=false]')?.click()`)
await settle()
await writeFile(join(artifacts, `${prefix}-${anchor}.png`), (await win.webContents.capturePage()).toPNG())
}
}
}
}
if (scenario === 'empty') {
// Without a graph, the empty state fills the visible height and its message sits in the middle.
for (const [width, height] of [[1440, 900], [390, 800]]) {
win.setContentSize(width, height)
await js(`document.querySelector('#supervisor-tab-graph').click(); document.querySelector('.page-shell').scrollTop = 0`)
await wait('!!document.querySelector(".supervisor-workspace__graph-empty .empty-state strong")')
await settle()
const empty = await js(`(() => {
const box = document.querySelector('.supervisor-workspace__graph-empty');
const shell = document.querySelector('.page-shell');
const title = box.querySelector('.empty-state strong').getBoundingClientRect();
const r = box.getBoundingClientRect(); const s = shell.getBoundingClientRect();
return { boxCenterX: r.left + r.width / 2, titleCenterX: title.left + title.width / 2,
boxTop: r.top, boxBottom: r.bottom, titleTop: title.top, shellBottom: s.bottom,
pageScroll: shell.scrollHeight > shell.clientHeight + 1,
buttons: [...box.querySelectorAll('button')].map(b => b.textContent),
outsideRefresh: [...document.querySelectorAll('#supervisor-panel-graph button')].filter(b => !box.contains(b) && b.textContent.includes('刷新')).length };
})()`)
assert(Math.abs(empty.boxCenterX - empty.titleCenterX) <= 2, 'Graph empty state is horizontally centred')
const middle = (empty.boxTop + empty.boxBottom) / 2
assert(empty.titleTop > empty.boxTop + 60 && Math.abs(empty.titleTop - middle) < (empty.boxBottom - empty.boxTop) / 4, 'Graph empty state is vertically centred')
assert(empty.shellBottom - empty.boxBottom <= 48, 'Graph empty state reaches the bottom margin')
assert.equal(empty.pageScroll, false, 'Graph empty state does not scroll the page')
assert.deepEqual(empty.buttons, ['工作回顾'])
assert.equal(empty.outsideRefresh, 1)
reports.push({ scenario: 'graph-empty', width, height, ...empty })
await writeFile(join(artifacts, `graph-empty-${width}x${height}.png`), (await win.webContents.capturePage()).toPNG())
}
}
if (scenario === 'populated') {
await js('document.querySelector("#supervisor-tab-overview").click()')
await js('document.querySelector(".supervisor-workspace__review-context .primary-button").click()')
// Toolbar: incremental review button, refresh, and the more menu holding re-analysis.
const toolbar = await js(`(() => {
const bar = document.querySelector('.supervisor-workspace__toolbar');
const rect = bar.getBoundingClientRect();
const more = bar.querySelector('[aria-haspopup=menu]');
// Rows are groups of items whose vertical ranges overlap.
const items = [...bar.children].filter(el => el.getClientRects().length).map(el => el.getBoundingClientRect()).sort((a, b) => a.top - b.top);
const tops = items.reduce((rows, rect) => { const last = rows.at(-1); if (last && rect.top < last.bottom) last.bottom = Math.max(last.bottom, rect.bottom); else rows.push({ top: Math.round(rect.top), bottom: rect.bottom }); return rows }, []).map(row => row.top);
return { primary: bar.querySelector('.primary-button').textContent, more: more?.getAttribute('aria-label'),
overflow: bar.scrollWidth > rect.width + 1, rows: new Set(tops).size };
})()`)
assert.equal(toolbar.primary, '回顾')
assert.equal(toolbar.more, '更多回顾操作')
assert.equal(toolbar.overflow, false, 'Recap toolbar overflows')
assert(toolbar.rows <= 2, `Recap toolbar wraps into ${toolbar.rows} rows`)
await js('document.querySelector(".supervisor-workspace__toolbar [aria-haspopup=menu]").click()')
await wait('!!document.querySelector("[role=menu] [role=menuitem]")')
assert.equal(await js('document.querySelector("[role=menu] [role=menuitem]").textContent'), '重新整理…')
await js('document.querySelector("[role=menu] [role=menuitem]").click()')
await wait('!!document.querySelector(".supervisor-workspace__confirm")')
const confirm = await js(`(() => { const el = document.querySelector('.supervisor-workspace__confirm'); const r = el.getBoundingClientRect();
return { width: r.width, overflow: el.scrollWidth > el.clientWidth + 1, text: el.textContent, buttons: [...el.querySelectorAll('button')].map(b => b.textContent) } })()`)
assert.equal(confirm.overflow, false, 'Re-analysis confirmation overflows')
assert.deepEqual(confirm.buttons, ['取消', '重新整理'])
assert(confirm.text.includes('可能产生较多模型用量'))
await settle()
await writeFile(join(artifacts, `recap-reanalyze-confirm-${win.getContentSize()[0]}.png`), (await win.webContents.capturePage()).toPNG())
await js('document.querySelector(".supervisor-workspace__confirm .secondary-button").click()')
await wait('!document.querySelector(".supervisor-workspace__confirm")')
await selectHistory('older-result')
await wait('document.querySelector(".supervisor-workspace")?.getAttribute("aria-busy") === "false"')
await js('document.querySelector(".supervisor-workspace__toolbar .primary-button").click(); document.querySelector(".page-shell").scrollTop = 0')
await settle()
assert(await js('document.querySelector(".supervisor-workspace__recap").textContent.includes("上期结果")'))
assert(await js('!document.querySelector(".supervisor-workspace__run-status")'))
await writeFile(join(artifacts, 'recap-running-history-390.png'), (await win.webContents.capturePage()).toPNG())
await js('document.querySelector("#supervisor-tab-activity").click()')
await wait('document.querySelector("#supervisor-tab-activity").getAttribute("aria-selected") === "true"')
}
}
assert.deepEqual(errors, [])
await writeFile(join(artifacts, 'recap-measurements.json'), JSON.stringify(reports, null, 2))
console.log(JSON.stringify({ artifacts, cases: reports.length, errors }))
win.destroy()
app.quit()
return
}
if (process.env.GOODBUDDY_SUPERVISOR_AUTOMATIC_OVERVIEW || process.env.GOODBUDDY_SUPERVISOR_DISCUSSION) {
for (const scenario of process.env.GOODBUDDY_SUPERVISOR_DISCUSSION ? [] : ['populated', 'empty', 'plan-only']) {
const empty = scenario !== 'populated'
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL + (scenario === 'populated' ? '?menu=1' : scenario === 'plan-only' ? '?plan-only=1' : ''))
await wait('!!document.querySelector("#supervisor-tab-plans")')
await js('document.fonts.ready')
await js('document.documentElement.dataset.theme = "light"')
for (const width of [1440, 390]) {
win.setContentSize(width, 1100)
for (const tab of ['plans', 'activity']) {
await js(`document.querySelector('#supervisor-tab-${tab}').click(); document.querySelector('.page-shell').scrollTop = 0`)
if (tab === 'activity') await wait('document.querySelector(".supervisor-activity")?.getAttribute("aria-busy") === "false"')
await settle()
const report = await js(`(() => {
const panel = document.querySelector('#supervisor-panel-${tab}');
return { width: innerWidth, pageWidth: document.documentElement.scrollWidth,
panelWidth: panel.clientWidth, scrollWidth: panel.scrollWidth,
overview: !!panel.querySelector('#heartbeat-panel-overview'),
metrics: !!panel.querySelector('.heartbeat-center__metrics'),
trend: !!panel.querySelector('#heartbeat-trend-title'),
audit: !!panel.querySelector('#heartbeat-runs-title'),
reports: !!panel.querySelector('#heartbeat-reports-title'),
suggestions: !!panel.querySelector('#supervision-suggestions-title'),
suggestionItems: panel.querySelectorAll('.supervision-suggestions__list > li').length,
emptyReports: panel.textContent.includes('暂无历史心跳报告'),
plans: !!panel.querySelector('#heartbeat-panel-plans'),
create: [...panel.querySelectorAll('button')].filter(b => b.textContent === '创建心跳计划').length,
emptyStates: panel.querySelectorAll('#heartbeat-panel-overview .empty-state').length,
activity: panel.querySelectorAll('.supervisor-activity__item').length };
})()`)
assert(report.pageWidth <= width && report.scrollWidth <= report.panelWidth, 'Automatic panel overflow')
for (const key of ['overview', 'plans']) assert.equal(report[key], tab === 'plans', key)
// Statistics, the trend and the run audit are gone; earlier reports stay readable.
for (const key of ['metrics', 'trend', 'audit']) assert.equal(report[key], false, key)
for (const key of ['reports', 'suggestions']) assert.equal(report[key], tab === 'plans' && !empty, key)
assert.equal(report.suggestionItems, tab === 'plans' && !empty ? 2 : 0)
assert.equal(report.emptyReports, false)
assert.equal(report.create, tab === 'plans' ? 1 : 0)
assert.equal(report.emptyStates, 0)
if (tab === 'activity') assert.equal(report.activity, 3)
reports.push({ scenario, empty, tab, ...report })
const prefix = `automatic-${scenario}-${tab}-light-${width}`
await writeFile(join(artifacts, `${prefix}.png`), (await win.webContents.capturePage()).toPNG())
if (tab === 'plans') {
for (const anchor of empty ? [] : ['supervision-suggestions-title', 'heartbeat-reports-title']) {
await js(`document.getElementById('${anchor}').scrollIntoView({block:'start'})`)
await settle()
await writeFile(join(artifacts, `${prefix}-${anchor}.png`), (await win.webContents.capturePage()).toPNG())
}
await js('document.querySelector(".heartbeat-settings__intro button").click()')
await wait('!!document.querySelector("[role=dialog]")')
assert(await js('document.querySelector("[role=dialog] > header button").getAttribute("aria-label") === "关闭心跳计划"'))
assert.equal(await js('document.querySelector("[role=dialog] > footer").textContent'), '取消保存并启用计划')
const intervention = await js(`(() => {
const dialog = document.querySelector('[role=dialog]');
const group = [...dialog.querySelectorAll('fieldset')].find(f => f.textContent.includes('介入方式'));
const buttons = group ? [...group.querySelectorAll('button')].filter(b => b.textContent === '生成建议' || b.textContent === '仅更新记忆') : [];
const body = dialog.querySelector('.custom-task-dialog__body, [class*=body]') || dialog;
return { present: !!group, options: buttons.map(b => b.textContent), pressed: buttons.find(b => b.getAttribute('aria-pressed') === 'true' || b.getAttribute('aria-checked') === 'true')?.textContent,
overflow: body.scrollWidth > body.clientWidth + 1 };
})()`)
assert.equal(intervention.present, true, 'Intervention field missing')
assert.deepEqual(intervention.options, ['生成建议', '仅更新记忆'])
assert.equal(intervention.overflow, false, 'Plan modal overflows horizontally')
if (width === 390) await writeFile(join(artifacts, `plan-modal-intervention-${scenario}-390.png`), (await win.webContents.capturePage()).toPNG())
await js('document.querySelector("[role=dialog] > header button").click()')
await wait('!document.querySelector("[role=dialog]")')
}
}
}
if (!empty) {
await js('document.querySelector("#supervisor-tab-plans").click()')
await js('[...document.querySelectorAll(".heartbeat-settings__actions button")].find(b => b.textContent === "执行记录").click()')
await wait('document.querySelector(".supervisor-activity select")?.value === "plan" && document.querySelectorAll(".supervisor-activity__item").length === 2')
assert(await js('!document.querySelector("#heartbeat-panel-overview, #heartbeat-runs-title")'))
await js('[...document.querySelectorAll(".supervisor-activity button")].find(b => b.textContent === "清除筛选").click()')
await wait('document.querySelectorAll(".supervisor-activity__item").length === 3')
}
}
assert.deepEqual(errors, [])
await writeFile(join(artifacts, 'automatic-overview-measurements.json'), JSON.stringify(reports, null, 2))
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL + '?preview=1')
await wait('document.querySelector(".supervisor-workspace")?.getAttribute("aria-busy") === "false"')
await js('document.querySelector("#supervisor-tab-graph").click()')
await wait('!!document.querySelector(".supervisor-workspace__map")')
await js(`document.querySelector('.supervisor-workspace__map [aria-label="观察工作过程"]').dispatchEvent(new MouseEvent('click', {bubbles:true}))`)
await settle()
await js('document.querySelector(".supervisor-workspace__detail > .link-button").click()')
await wait('!!document.querySelector(".supervisor-discussion > button")')
await js('document.querySelector(".supervisor-discussion > button").click()')
await wait('!!document.querySelector(".supervisor-discussion textarea")')
await js('document.fonts.ready')
const previews = []
for (const theme of ['light', 'dark']) {
await js(`document.documentElement.dataset.theme = '${theme}'`)
for (const width of [1440, 390]) {
win.setContentSize(width, 1100)
await js(`document.querySelector('.supervisor-discussion').scrollIntoView({block:'center'})`)
await settle()
const report = await js(`(() => {
const preview = document.querySelector('.supervisor-discussion');
const box = e => { const r = e.getBoundingClientRect(); return {left:r.left, right:r.right, top:r.top, bottom:r.bottom}; };
return { width:innerWidth, pageWidth:document.documentElement.scrollWidth,
clientWidth:preview.clientWidth, scrollWidth:preview.scrollWidth,
question:preview.querySelector('textarea').value, text:preview.textContent,
controls:[...preview.querySelectorAll('textarea, button')].map(box) };
})()`)
assert(report.pageWidth <= width && report.scrollWidth <= report.clientWidth, 'Discussion preview overflow')
assert(report.question.trim() && report.text.includes('模拟会议记录'), 'Discussion question and destination visible')
assert(report.controls.every(box => box.left >= 0 && box.right <= width && box.top >= 0 && box.bottom <= 1100), 'Preview controls fit viewport')
assert(await js('!document.querySelector(".supervisor-discussion [role=alert]")'), 'Preview loads without error')
assert(await js('!document.querySelector(".supervisor-discussion details")'), 'No redundant context disclosure')
previews.push({ theme, ...report, editor: await checkDiscussion() })
await writeFile(join(artifacts, `discussion-preview-${theme}-${width}.png`), (await win.webContents.capturePage()).toPNG())
}
}
await js('document.querySelector(".supervisor-discussion .secondary-button").click()')
await wait('!document.querySelector(".supervisor-discussion textarea")')
assert.deepEqual(errors, [])
await writeFile(join(artifacts, 'discussion-preview-measurements.json'), JSON.stringify(previews, null, 2))
console.log(JSON.stringify({ artifacts, cases: reports.length, errors }))
win.destroy()
app.quit()
return
}
if (process.env.GOODBUDDY_SUPERVISOR_SIDEBAR) {
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL)
await wait('!!document.querySelector(".supervision-card")')
await js('document.fonts.ready')
win.show()
win.focus()
await wait('[...document.querySelectorAll(".supervision-card button")].some(b => b.textContent === "继续讨论" && !b.disabled)')
await js('[...document.querySelectorAll(".supervision-card button")].find(b => b.textContent === "继续讨论").click()')
await wait('!!document.querySelector(".supervision-discussion-editor textarea")')
for (const theme of ['light', 'dark']) {
await js(`document.documentElement.dataset.theme = '${theme}'`)
for (const width of [480, 300, 200]) {
win.setContentSize(width, 640)
await js('document.querySelector(".supervision-card__header button").focus()')
win.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Tab' })
win.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Tab' })
await settle()
const report = await js(`(() => {
const card = document.querySelector('.supervision-card');
const header = card.querySelector('.supervision-card__header');
const buttons = [...header.querySelectorAll('button')];
return {
width: innerWidth, scrollWidth: document.documentElement.scrollWidth,
cardWidth: card.clientWidth, cardScrollWidth: card.scrollWidth,
headerHeight: header.getBoundingClientRect().height,
buttons: buttons.map(b => ({ width: b.getBoundingClientRect().width,
height: b.getBoundingClientRect().height, background: getComputedStyle(b).backgroundColor })),
focus: getComputedStyle(document.activeElement).outlineStyle,
text: card.textContent
};
})()`)
assert(report.scrollWidth <= report.width, `Page overflow: ${JSON.stringify(report)}`)
assert(report.cardScrollWidth <= report.cardWidth, 'Card overflow')
assert.equal(report.headerHeight, 34)
assert(report.buttons.every(b => b.width === 34 && b.height === 34), 'Compact shared buttons')
assert.equal(report.focus, 'solid')
assert(!report.text.includes('simulated-conversation-uuid'), 'Raw UUID visible')
reports.push({ theme, ...report, editor: await checkDiscussion() })
await js('document.querySelector(".supervision-discussion-editor footer").scrollIntoView({block:"end"})')
await settle()
assert(await js(`([...document.querySelectorAll('.supervision-discussion-editor footer button')].every(e => {
const r = e.getBoundingClientRect();
return r.top >= 0 && r.bottom <= innerHeight && e.contains(document.elementFromPoint(r.x+r.width/2, r.y+r.height/2));
}))`), 'Sidebar footer reachable and hittable after scrolling')
await writeFile(join(artifacts, `sidebar-${theme}-${width}.png`), (await win.webContents.capturePage()).toPNG())
}
}
assert.deepEqual(errors, [])
await writeFile(join(artifacts, 'sidebar-measurements.json'), JSON.stringify(reports, null, 2))
console.log(JSON.stringify({ artifacts, cases: reports.length, errors }))
win.destroy()
app.quit()
return
}
if (process.env.GOODBUDDY_SUPERVISOR_PLAN_MODAL) {
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL + '?menu=1')
await wait('!!document.querySelector("#supervisor-tab-plans")')
await js('document.fonts.ready')
await js('document.querySelector("#supervisor-tab-plans").click()')
assert.equal(await js('document.querySelectorAll("[role=tab]").length'), 5)
win.show()
win.focus()
for (const mode of ['create', 'edit']) {
const opener = mode === 'create' ? '.heartbeat-settings__intro button' : '.heartbeat-settings__actions button[aria-label^="编辑"]'
await js(`document.querySelector('${opener}').focus(); document.querySelector('${opener}').click()`)
await wait('!!document.querySelector("[role=dialog]")')
assert(await js('document.activeElement === document.querySelector("[role=dialog] input")'), 'Name initial focus')
await js(`(() => {
const select = document.querySelector('[aria-label="监督频率"]');
select.value = 'weekly'; select.dispatchEvent(new Event('change', { bubbles: true }));
})()`)
await wait('!!document.querySelector("[aria-label=监督星期]")')
for (const theme of ['light', 'dark']) {
await js(`document.documentElement.dataset.theme = '${theme}'`)
for (const [width, height] of [[1440, 1100], [390, 1100], [390, 480]]) {
win.setContentSize(width, height)
await settle()
const measure = () => js(`(() => {
const dialog = document.querySelector('[role=dialog]');
const header = dialog.querySelector(':scope > header'), footer = dialog.querySelector(':scope > footer');
const body = dialog.querySelector(':scope > .custom-task-dialog__content');
const box = e => { const r = e.getBoundingClientRect(); return { left:r.left, right:r.right, top:r.top, bottom:r.bottom, width:r.width, height:r.height }; };
const close = header.querySelector('button'), buttons = [...footer.querySelectorAll('button')];
const hit = e => { const r = e.getBoundingClientRect(); return e.contains(document.elementFromPoint(r.x+r.width/2, r.y+r.height/2)); };
return { width:innerWidth, height:innerHeight, pageWidth:document.documentElement.scrollWidth,
dialog:box(dialog), header:box(header), title:box(header.querySelector('h2')), close:box(close),
footer:box(footer), buttons:buttons.map(box), hit:[close,...buttons].every(hit),
headerButtons:header.querySelectorAll('button').length, closeLabel:close.getAttribute('aria-label'),
tooltip:close.title, bodyWidth:body.clientWidth, bodyScrollWidth:body.scrollWidth,
bodyHeight:body.clientHeight, bodyScrollHeight:body.scrollHeight, bodyTop:body.scrollTop,
footerAlign:getComputedStyle(footer).justifyContent, text:buttons.map(e=>e.textContent) };
})()`)
const before = await measure()
assert(before.pageWidth <= width && before.bodyScrollWidth <= before.bodyWidth, 'No horizontal overflow')
assert(before.dialog.top >= 0 && before.dialog.bottom <= height, 'Dialog fits viewport')
assert(before.headerButtons === 1 && before.closeLabel === '关闭心跳计划' && before.tooltip === before.closeLabel, 'Accessible header X only')
assert(before.title.right <= before.close.left && before.close.right > (before.dialog.left + before.dialog.right) / 2, 'Title left, close right')
assert.equal(before.footerAlign, 'flex-end')
assert.equal(before.text[0], '取消')
assert(before.buttons[0].right <= before.buttons[1].left && before.buttons[0].top === before.buttons[1].top, 'Cancel then save on same row')
assert(before.buttons[1].right > (before.dialog.left + before.dialog.right) / 2 && before.hit, 'Footer right and controls hittable')
await js('document.querySelector("[role=dialog] > .custom-task-dialog__content").scrollTop = 10000')
await settle()
const after = await measure()
assert.deepEqual(after.header, before.header, 'Header stays visible when body scrolls')
assert.deepEqual(after.footer, before.footer, 'Footer stays visible when body scrolls')
assert(after.hit, 'Scrolled controls hittable')
if (height === 480) assert(after.bodyTop > 0 && after.bodyScrollHeight > after.bodyHeight, 'Short body scrolls')
reports.push({ mode, theme, before, after })
await writeFile(join(artifacts, `plan-${mode}-${theme}-${width}x${height}.png`), (await win.webContents.capturePage()).toPNG())
}
}
win.focus()
win.webContents.focus()
await js('document.querySelector("[role=dialog] > header button").focus()')
const key = async (keyCode, modifiers = []) => {
win.webContents.sendInputEvent({ type: 'keyDown', keyCode, modifiers })
if (keyCode === 'Enter') win.webContents.sendInputEvent({ type: 'char', keyCode: '\r' })
win.webContents.sendInputEvent({ type: 'keyUp', keyCode, modifiers })
await settle()
}
await key('Tab', ['shift'])
assert(await js('document.activeElement.matches("footer .primary-button")'), 'Shift Tab wraps to save')
await key('Tab', ['shift'])
assert(await js('document.activeElement.matches("footer .secondary-button")'), 'Footer cancel keyboard reachable')
const focus = await js('({ focused: document.hasFocus(), visible: document.activeElement.matches(":focus-visible"), outline: getComputedStyle(document.activeElement).outlineStyle })')
assert(focus.visible && focus.outline === 'solid', `Visible keyboard focus: ${JSON.stringify(focus)}`)
await key('Enter')
await wait('document.querySelector("[role=dialog]").textContent.includes("放弃未保存")')
assert(await js('document.activeElement.textContent === "继续编辑"'), 'Confirmation initial focus')
await key('Escape')
assert(await js('document.activeElement === document.querySelector("[role=dialog] input")'), 'Escape returns to editing')
await js('document.querySelector("[role=dialog] > header button").click()')
await wait('!!document.querySelector("[role=dialog] .danger-solid")')
await js('document.querySelector("[role=dialog] .danger-solid").click()')
await wait('!document.querySelector("[role=dialog]")')
assert(await js(`document.activeElement === document.querySelector('${opener}')`), 'Opener focus restored')
}
assert.deepEqual(errors, [])
await writeFile(join(artifacts, 'plan-modal-measurements.json'), JSON.stringify(reports, null, 2))
console.log(JSON.stringify({ artifacts, cases: reports.length, errors }))
win.destroy()
app.quit()
return
}
await validateSupervisorSelection(win, js, wait, process.env.GOODBUDDY_SUPERVISOR_URL)
await open(false)
await js(
'document.querySelector(".supervisor-workspace__canvas-tools .link-button").click()'
)
await wait(
'!document.querySelector(".supervisor-workspace__map.has-selection")'
)
assert(await js(`(() => {
const ring = document.querySelector('.supervisor-workspace__timeline-ring');
const start = ring.getPointAtLength(0), next = ring.getPointAtLength(20), end = ring.getPointAtLength(ring.getTotalLength());
return next.x > start.x && next.y < start.y && end.x < start.x && end.y > 490;
})()`), 'Time must run counter-clockwise and leave a bottom gap')
for (const theme of ['light', 'dark']) {
await js(`document.documentElement.dataset.theme = '${theme}'`)
for (const width of [1440, 1024, 390]) {
win.setContentSize(width, 1100)
await settle()
const report = await js(`(() => {
const q = s => document.querySelector('.supervisor-workspace' + s);
const box = e => { const r = e.getBoundingClientRect(); return { width: r.width, height: r.height, top: r.top, bottom: r.bottom, clientWidth: e.clientWidth, scrollWidth: e.scrollWidth }; };
const svg = q('__map'), scroller = q('__map-scroll');
const texts = [...svg.querySelectorAll('text')].map(e => {
const b = e.getBBox(), c = getComputedStyle(e), r = e.getBoundingClientRect();
return { text: e.textContent, width: b.width, height: b.height, fontSize: c.fontSize,
screenFontSize: parseFloat(c.fontSize) * e.getScreenCTM().a, fill: c.fill,
inSvg: b.x >= 0 && b.y >= 0 && b.x + b.width <= 760 && b.y + b.height <= 620,
painted: c.display !== 'none' && c.visibility === 'visible' && c.fill !== 'none' && Number(c.opacity) > 0 && r.width > 0,
x: b.x, y: b.y };
});
const tokens = Object.fromEntries(${process.env.GOODBUDDY_SUPERVISOR_TOKENS}.map(t => [t, getComputedStyle(q('')).getPropertyValue(t).trim()]));
return { viewport: innerWidth, page: box(document.documentElement), shell: box(document.querySelector('.page-shell')),
container: box(q('')), grid: getComputedStyle(q('__graph-layout')).gridTemplateColumns,
list: box(q('__graph-list')), canvas: box(q('__graph-canvas')), detail: box(q('__detail')), svg: box(svg), scroller: box(scroller), texts, tokens };
})()`)
reports.push({ theme, ...report })
await writeFile(
join(artifacts, `${theme}-${width}.png`),
(await win.webContents.capturePage()).toPNG()
)
assert(report.page.scrollWidth <= width, JSON.stringify(report))
if (width >= 1024) {
assert(Math.abs(report.list.top - report.canvas.top) < 1, 'Sidebar top alignment')
assert(Math.abs(report.list.bottom - report.canvas.bottom) < 1,
`Sidebar must fill canvas height: ${JSON.stringify({ list: report.list, canvas: report.canvas })}`)
if (width === 1440)
assert(Math.abs(report.list.bottom - report.detail.bottom) < 1, 'Sidebar/detail height alignment')
}
assert(
report.shell.scrollWidth <= report.shell.clientWidth,
'Shell overflow'
)
assert(
report.container.scrollWidth <= report.container.clientWidth,
'Container overflow'
)
assert(
report.texts.every(
(text) => text.inSvg && text.painted && (width < 1024 || text.screenFontSize >= 11)
),
`Clipped or invisible SVG text at ${width}: ${JSON.stringify(report.texts.filter(text => !(text.inSvg && text.painted && text.screenFontSize >= 11)).slice(0, 3))} svg=${JSON.stringify(report.svg)}`
)
for (const [index, a] of report.texts.entries()) {
for (const b of report.texts.slice(index + 1)) {
assert(
!(
a.x < b.x + b.width &&
a.x + a.width > b.x &&
a.y < b.y + b.height &&
a.y + a.height > b.y
),
`Overlapping labels: ${a.text} / ${b.text}`
)
}
}
assert(
Object.values(report.tokens).every(Boolean),
'Unresolved CSS variable'
)
assert(
report.texts.some((text) => text.text === '工作过程'),
'Missing real entity label'
)
if (width === 1440)
assert(
report.canvas.width > report.list.width + report.detail.width,
'Canvas must take priority'
)
if (width === 1440) {
await js('document.querySelector(".supervisor-workspace__graph-layout").scrollIntoView({block:"end"})')
await settle()
await writeFile(join(artifacts, `${theme}-1440-aligned-bottom.png`), (await win.webContents.capturePage()).toPNG())
await js('document.querySelector(".page-shell").scrollTop = 0')
}
if (width >= 1024)
assert.equal(
report.scroller.scrollWidth,
report.scroller.clientWidth,
'Desktop canvas must fit'
)
if (width === 390) {
assert(
report.scroller.scrollWidth === report.scroller.clientWidth,
'Mobile canvas fits without internal scrolling; full labels remain in the list'
)
await js(
'document.querySelector(".supervisor-workspace__map-scroll").scrollLeft = 600'
)
assert(
await js(
'document.querySelector(".supervisor-workspace__map-scroll").scrollLeft === 0'
)
)
await writeFile(
join(artifacts, `${theme}-${width}-scrolled.png`),
(await win.webContents.capturePage()).toPNG()
)
}
}
}
// The viewport stays wide while the actual workspace is constrained.
win.setContentSize(1440, 1100)
await js(
`document.documentElement.dataset.theme = 'light'; document.querySelector('.supervisor-workspace__map [aria-label="观察工作过程"]').dispatchEvent(new MouseEvent('click', {bubbles:true}))`
)
await settle()
await js(
'document.querySelector(".supervisor-workspace__detail > .link-button").click()'
)
await wait('!!document.querySelector(".supervisor-workspace__source")')
assert(
await js(
'document.querySelector(".supervisor-workspace__source").textContent.includes("不是真实用户数据")'
)
)
await settle()
await writeFile(
join(artifacts, 'selected-source-1440.png'),
(await win.webContents.capturePage()).toPNG()
)
win.setContentSize(390, 1100)
await js(
'document.querySelector(".supervisor-workspace__detail").scrollIntoView({block:"start"})'
)
await settle()
await writeFile(
join(artifacts, 'selected-source-390.png'),
(await win.webContents.capturePage()).toPNG()
)
assert(
await js('document.documentElement.scrollWidth <= innerWidth'),
'Mobile source overflow'
)
win.setContentSize(1440, 1100)
for (const width of [800, 860, 861, 980, 1000, 1010, 1110, 1190, 1191]) {
await js(
`document.querySelector('.supervisor-workspace').style.width = '${width}px'`
)
await settle()
const result = await js(
`(() => { const e = document.querySelector('.supervisor-workspace'); return { width: e.clientWidth, scrollWidth: e.scrollWidth, columns: getComputedStyle(e.querySelector('.supervisor-workspace__graph-layout')).gridTemplateColumns }; })()`
)
assert.equal(result.width, result.scrollWidth, 'Breakpoint overflow')
reports.push({ constrainedContainer: result })
}
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL + '?short=1')
await wait('!!document.querySelector("#supervisor-tab-graph")')
await js('document.querySelector("#supervisor-tab-graph").click()')
await wait('!!document.querySelector(".supervisor-workspace__map")')
for (const width of [1440, 390]) {
win.setContentSize(width, 1100)
await settle()
const short = await js(`(() => {
const list = document.querySelector('.supervisor-workspace__graph-list');
const r = list.getBoundingClientRect();
return { width: innerWidth, height: r.height, clientHeight: list.clientHeight, scrollHeight: list.scrollHeight,
canvasHeight: document.querySelector('.supervisor-workspace__graph-canvas').getBoundingClientRect().height,
buttonHeights: [...list.querySelectorAll('button')].map(e => e.getBoundingClientRect().height) };
})()`)
assert(short.buttonHeights.every(height => height < 80), 'Short rows must not stretch to fill space')
assert.equal(short.scrollHeight, short.clientHeight, 'Short list must not acquire artificial overflow')
if (width === 1440) assert(Math.abs(short.height - short.canvasHeight) < 1, 'Short sidebar must still fill its row')
else assert(short.height <= 320, 'Short mobile list must use its natural height')
reports.push({ shortList: short })
await js('document.querySelector(".supervisor-workspace__graph-list").scrollIntoView({block:"end"})')
await settle()
await writeFile(join(artifacts, `short-${width}.png`), (await win.webContents.capturePage()).toPNG())
}
win.setContentSize(1440, 1100)
await open(true)
for (const width of [1440, 1024, 390]) {
win.setContentSize(width, 1100)
await settle()
await js(`(() => {const list=document.querySelector('.supervisor-workspace__list-panel'); list.scrollIntoView({block:'end'}); list.scrollTop=list.scrollHeight;list.dispatchEvent(new Event('scroll'))})()`)
await wait('!!document.querySelector("[data-list-window-row=event-79] button")')
await settle()
const scrolling = await js(`(() => {
const sidebar = document.querySelector('.supervisor-workspace__graph-list');
const list = sidebar.querySelector('[role=tabpanel]');
const canvas = document.querySelector('.supervisor-workspace__graph-canvas');
const shell = document.querySelector('.page-shell');
list.scrollIntoView({block:'end'});
const pageHeight = shell.scrollHeight, pageTop = shell.scrollTop;
list.scrollTop = list.scrollHeight;
const last = list.lastElementChild.getBoundingClientRect();
const button = list.querySelector('[data-list-window-row="event-79"] button');
const b = button.getBoundingClientRect(), r = list.getBoundingClientRect();
return { width: innerWidth, height: sidebar.getBoundingClientRect().height, canvasHeight: canvas.getBoundingClientRect().height,
clientHeight: list.clientHeight, scrollHeight: list.scrollHeight, scrollTop: list.scrollTop,
bottomGap: r.bottom - last.bottom,
paddingBottom: parseFloat(getComputedStyle(list).paddingBottom),
bottomVisible: last.bottom <= r.bottom && last.top >= r.top,
lastButtonHit: button.contains(document.elementFromPoint(b.x + b.width / 2, b.y + b.height / 2)),
pageStable: pageHeight === shell.scrollHeight && pageTop === shell.scrollTop,
pageHeight };
})()`)
assert(scrolling.scrollHeight > scrolling.clientHeight, 'Dense list must scroll locally')
assert(Math.abs(scrolling.scrollTop + scrolling.clientHeight - scrolling.scrollHeight) <= 1, 'List bottom unreachable')
assert(scrolling.bottomVisible && scrolling.lastButtonHit, 'Last relation and trailing copy must be visible')
assert(Math.abs(scrolling.bottomGap - scrolling.paddingBottom) <= 1,
`List must use the full scroll area, leaving only its padding: ${JSON.stringify(scrolling)}`)
assert(scrolling.pageStable, 'List scrolling must not move or grow the page')
if (width >= 1024)
assert(Math.abs(scrolling.height - scrolling.canvasHeight) < 1, 'Dense list must follow canvas height')
else
assert(scrolling.height <= 320, 'Single-column list must remain bounded')
reports.push({ denseScrolling: scrolling })
await settle()
await writeFile(join(artifacts, `dense-bottom-${width}.png`), (await win.webContents.capturePage()).toPNG())
}
// The graph row fills the window down to a steady bottom margin; each column scrolls on its own.
for (const [width, height] of [[1440, 1200], [1440, 900], [1440, 700], [1024, 1100]]) {
win.setContentSize(width, height)
await settle()
const fill = await js(`(() => {
const shell = document.querySelector('.page-shell');
shell.scrollTop = 0;
const layout = document.querySelector('.supervisor-workspace__graph-layout');
const shellBox = shell.getBoundingClientRect(), box = layout.getBoundingClientRect();
const columns = [...layout.children].filter(column => column.getClientRects().length).map(column => {
const scroller = column.matches('.supervisor-workspace__graph-list') ? column.querySelector('[role=tabpanel]') : column;
const r = column.getBoundingClientRect();
return { name: column.className.split(' ')[0], canvas: column.matches('.supervisor-workspace__graph-canvas'), top: r.top, bottom: r.bottom,
scrolls: getComputedStyle(scroller).overflowY, clientHeight: scroller.clientHeight, scrollHeight: scroller.scrollHeight };
});
const canvas = layout.querySelector('.supervisor-workspace__graph-canvas'), map = layout.querySelector('.supervisor-workspace__map').getBoundingClientRect();
const playback = layout.querySelector('.supervisor-workspace__playback')?.getBoundingClientRect();
const graphArea = { map: { top: map.top, bottom: map.bottom, height: map.height }, playbackBottom: playback?.bottom, canvasBottom: canvas.getBoundingClientRect().bottom };
// Bottom of the first grid row (the side-by-side columns), measured from the window bottom.
const rowBottom = Math.max(...columns.filter(column => Math.abs(column.top - columns[0].top) < 1).map(column => column.bottom));
return { bottomGap: shellBox.bottom - rowBottom, wrapped: columns.length - columns.filter(column => Math.abs(column.top - columns[0].top) < 1).length,
height: box.height, pageScroll: shell.scrollHeight - shell.clientHeight, columns, graphArea };
})()`)
reports.push({ fill: { width, height, ...fill } })
await writeFile(join(artifacts, `fill-${width}x${height}.json`), JSON.stringify({ width, height, bottomGap: Math.round(fill.bottomGap), wrapped: fill.wrapped, pageScroll: fill.pageScroll, rowHeight: Math.round(fill.columns[0].bottom - fill.columns[0].top), mapHeight: Math.round(fill.graphArea.map.height) }))
// At the 660px minimum (labels stay ≥ 11px) a short window scrolls the page; otherwise the row ends at the bottom margin.
const atMinimum = Math.abs(fill.columns[0].bottom - fill.columns[0].top - 660) < 1
if (!atMinimum) {
assert(fill.bottomGap >= 20 && fill.bottomGap <= 40, `Graph must stop above the window bottom: ${JSON.stringify(fill)}`)
// Three columns fit the window with no page scroll; at two columns only the wrapped detail extends the page.
if (!fill.wrapped) assert(fill.pageScroll <= 1, `Page must not scroll at three columns: ${JSON.stringify(fill)}`)
}
const sideBySide = fill.columns.filter(column => Math.abs(column.top - fill.columns[0].top) < 1)
for (const column of sideBySide) {
assert(Math.abs(column.bottom - sideBySide[0].bottom) < 1, `Columns must share the row height: ${JSON.stringify(fill)}`)
// The graph column has no scroll bar: the graph takes the height left after the heading and event bar.
if (column.canvas) assert(column.scrollHeight <= column.clientHeight + 1 && column.scrolls === 'hidden', `Graph column must not scroll: ${JSON.stringify(column)}`)
else assert(['auto', 'scroll'].includes(column.scrolls), `Column must scroll on its own: ${JSON.stringify(column)}`)
}
if (width === 1440) {
const heading = await js(`(() => {
const context = document.querySelector('.supervisor-workspace__review-context');
return { meta: context.querySelector('.sr-only').textContent,
above: context.getBoundingClientRect().bottom <= document.querySelector('.supervisor-workspace__graph-layout').getBoundingClientRect().top,
heading: document.querySelector('.supervisor-workspace__canvas-heading').textContent };
})()`)
assert(heading.above && heading.meta.includes('全局') && !heading.heading.includes('事件与知识'), `Review context shared above graph: ${JSON.stringify(heading)}`)
reports.push({ heading })
}
assert(Math.abs((fill.graphArea.playbackBottom ?? fill.graphArea.map.bottom) - fill.graphArea.canvasBottom) <= 1,
`Graph area must reach the column bottom: ${JSON.stringify(fill.graphArea)}`)
await writeFile(join(artifacts, `fill-${width}x${height}.png`), (await win.webContents.capturePage()).toPNG())
}
win.setContentSize(1440, 1100)
await js(
'document.querySelectorAll(".supervisor-workspace__graph-list [role=tab]")[2].click()'
)
await settle()
await js('document.querySelector(".supervisor-workspace__list-panel button:last-of-type").click()')
await settle()
assert(
await js('document.querySelectorAll("[data-relation]").length === 1'),
'Selected relation endpoints must remain visible'
)
await js('document.querySelectorAll(".supervisor-workspace__graph-list [role=tab]")[0].click()')
await js(`document.querySelector('.supervisor-workspace__list-panel').dispatchEvent(new KeyboardEvent('keydown',{key:'End',bubbles:true}))`)
await settle()
const target = await js(
`(() => { const e = document.querySelector('[data-list-window-row="event-79"] button'); e.scrollIntoView({block:'center'}); const 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)) }; })()`
)
assert(target.hit, 'Last event is not reachable')
win.webContents.sendInputEvent({
type: 'mouseDown',
x: target.x,
y: target.y,
button: 'left',
clickCount: 1
})
win.webContents.sendInputEvent({
type: 'mouseUp',
x: target.x,
y: target.y,
button: 'left',
clickCount: 1
})
await settle()
assert(
await js(
`!!document.querySelector('.supervisor-workspace__map [aria-label="模拟事件 80"][aria-pressed=true]')`
)
)
assert(await js('document.querySelectorAll(".supervisor-workspace__list-panel button").length < 80'), 'Dense list is windowed without losing the last event')
const positions = await js(
'[...document.querySelectorAll(".supervisor-workspace__node:not(.supervisor-workspace__entity) circle")].map(e => [e.getAttribute("cx"), e.getAttribute("cy")].join())'
)
assert.equal(new Set(positions).size, 8)
assert(await js(`(() => {
return [...document.querySelectorAll('.supervisor-workspace__event-index')].every(e => {
const range = document.createRange(); range.selectNodeContents(e);
const tops = [...range.getClientRects()].map(r => r.top);
return Math.max(...tops) - Math.min(...tops) < 1;
});
})()`), 'Dense event numbers must not wrap')
await js(
'document.querySelector(".supervisor-workspace__graph-canvas").scrollIntoView({block:"start"})'
)
await settle()
await writeFile(
join(artifacts, 'dense-selected.png'),
(await win.webContents.capturePage()).toPNG()
)
await js('document.querySelector("#supervisor-tab-settings").click()')
await settle()
assert.equal(
await js('document.querySelectorAll("h1").length'),
1,
'Duplicate settings h1'
)
await writeFile(
join(artifacts, 'settings.png'),
(await win.webContents.capturePage()).toPNG()
)
// Settings are grouped into one row of section tabs, with one visible form at a time.
assert.deepEqual(await js('[...document.querySelectorAll("#supervisor-panel-settings [role=tablist] [role=tab]")].map(t => t.textContent)'),
['模型', '回顾整理', '故事与经验', '建议'])
assert.equal(await js('document.querySelectorAll("[role=tablist]").length'), 2, 'Page tabs plus one settings section row')
for (const section of ['model', 'review', 'stories', 'suggestions']) {
await js(`document.querySelector('#supervisor-settings-tab-${section}').click()`)
for (const width of [1440, 1024, 390]) {
win.setContentSize(width, 1100)
await js('document.querySelector(".page-shell").scrollTop = 0')
await settle()
const sizes = await js(`(() => {
const shell = document.querySelector('.page-shell');
const forms = [...document.querySelectorAll('#supervisor-panel-settings form')].filter(f => f.getClientRects().length);
return { viewport: innerWidth, page: document.documentElement.scrollWidth,
clientWidth: shell.clientWidth, scrollWidth: shell.scrollWidth, headings: document.querySelectorAll('h1').length,
visibleForms: forms.length, title: forms[0]?.querySelector('h2')?.textContent };
})()`)
assert(
sizes.page <= width && sizes.scrollWidth <= sizes.clientWidth,
`Settings overflow: ${section}/${width}`
)
assert.equal(sizes.headings, 1)
assert.equal(sizes.visibleForms, 1, `One settings form per section: ${section}`)
reports.push({ settings: section, ...sizes })
await writeFile(
join(artifacts, `settings-${section}-${width}.png`),
(await win.webContents.capturePage()).toPNG()
)
}
}
for (const query of [
'story=1',
'story=2',
'long=1',
'state=loading',
'state=error',
'state=unavailable',
'state=empty'
]) {
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL + '?' + query)
await wait('!!document.querySelector("#supervisor-tab-graph")')
await js('document.querySelector("#supervisor-tab-graph").click()')
await wait(
query === 'state=loading'
? '!!document.querySelector(".empty-state")'
: '!document.querySelector(".supervisor-workspace[aria-busy=true]")'
)
for (const width of [1440, 1024, 390]) {
win.setContentSize(width, 1100)
await settle()
assert(
await js('document.documentElement.scrollWidth <= innerWidth'),
`Overflow: ${query}/${width}`
)
if (!query.startsWith('state=')) {
const labels = await js(
'[...document.querySelectorAll(".supervisor-workspace__map text")].map(e => { const b = e.getBBox(); return { text:e.textContent, x:b.x, y:b.y, width:b.width, height:b.height } })'
)
for (const [index, a] of labels.entries()) {
for (const b of labels.slice(index + 1)) {
assert(
!(
a.x < b.x + b.width &&
a.x + a.width > b.x &&
a.y < b.y + b.height &&
a.y + a.height > b.y
),
`Overlapping ${query}: ${a.text} / ${b.text}`
)
}
}
reports.push({ scenario: query, viewport: width, labels })
}
await writeFile(
join(artifacts, `${query.replace('=', '-')}-${width}.png`),
(await win.webContents.capturePage()).toPNG()
)
}
}
await js('document.querySelector("#supervisor-tab-overview").click()')
await settle()
await js('document.querySelector(".supervisor-workspace__review-context .primary-button").click()')
await wait('!!document.querySelector(".supervisor-workspace__toolbar .primary-button")')
await js(
'document.querySelector(".supervisor-workspace__toolbar .primary-button").click()'
)
await wait(
'document.documentElement.dataset.reviewNoticeTone === "error"'
)
assert(
await js(
'document.documentElement.dataset.reviewNoticeMessage.includes("回顾未能完成")'
)
)
await writeFile(
join(artifacts, 'manual-review-error-390.png'),
(await win.webContents.capturePage()).toPNG()
)
await js('document.querySelector("#supervisor-tab-settings").click()')
await wait('!!document.querySelector(".heartbeat-settings")')
assert(await js('!document.querySelector("#supervisor-panel-settings .scope-badge")'))
assert.equal(await js('document.querySelectorAll("#supervisor-panel-settings [role=tablist]").length'), 1, 'Only the settings section tabs')
assert(await js('!document.querySelector("#heartbeat-panel-plans")'), 'Plans must not live in settings')
await js('document.querySelector("#supervisor-tab-plans").click()')
await js('document.querySelector(".heartbeat-settings__intro button").focus(); document.querySelector(".heartbeat-settings__intro button").click()')
await wait('!!document.querySelector("[role=dialog]")')
assert(await js('document.activeElement === document.querySelector("[role=dialog] input")'), 'Plan initial focus')
await js(`(() => {
const select = document.querySelector('[aria-label="监督频率"]');
select.value = 'weekly'; select.dispatchEvent(new Event('change', { bubbles: true }));
})()`)
await wait('!!document.querySelector("[aria-label=监督星期]")')
for (const theme of ['light', 'dark']) {
await js(`document.documentElement.dataset.theme = '${theme}'`)
for (const width of [1440, 1024, 390]) {
win.setContentSize(width, 1100)
await settle()
const settings = await js(`(() => {
const root = document.querySelector('[role=dialog]');
return { width: innerWidth, pageWidth: document.documentElement.scrollWidth,
clientWidth: root.clientWidth, scrollWidth: root.scrollWidth,
fields: [...root.querySelectorAll('input, select')].filter(e => e.getClientRects().length).map(e => {
const r = e.getBoundingClientRect();
return { left: r.left, right: r.right, height: r.height };
}), text: root.textContent };
})()`)
assert(settings.pageWidth <= width && settings.scrollWidth <= settings.clientWidth, 'Settings overflow')
assert(settings.fields.every(r => r.left >= 0 && r.right <= width && r.height >= 28), 'Clipped settings fields')
reports.push({ scenario: 'automatic-supervision-modal', theme, ...settings })
await writeFile(join(artifacts, `plan-modal-${theme}-${width}.png`), (await win.webContents.capturePage()).toPNG())
if (width === 390) {
win.setContentSize(390, 720)
await js('document.querySelector("[role=dialog] .heartbeat-settings__submit").scrollIntoView({block:"end"})')
await settle()
assert(await js(`(() => {
const button = document.querySelector('[role=dialog] .heartbeat-settings__submit');
const r = button.getBoundingClientRect();
return r.left >= 16 && r.right <= innerWidth - 16 && r.bottom <= innerHeight && button.contains(document.elementFromPoint(r.x + r.width / 2, r.y + r.height / 2));
})()`), 'Short modal save button must be reachable')
await writeFile(join(artifacts, `plan-modal-${theme}-390-short.png`), (await win.webContents.capturePage()).toPNG())
}
}
}
await js('document.querySelector("[role=dialog]").dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }))')
await wait('document.querySelector("[role=dialog]").textContent.includes("放弃未保存")')
await js('document.querySelector("[role=dialog] .danger-solid").click()')
await wait('!document.querySelector("[role=dialog]")')
assert(await js('document.activeElement === document.querySelector(".heartbeat-settings__intro button")'), 'Plan focus restored')
await js('document.querySelector("#supervisor-tab-activity").click()')
await wait('document.querySelectorAll(".supervisor-activity__item").length === 3')
for (const theme of ['light', 'dark']) {
await js(`document.documentElement.dataset.theme = '${theme}'`)
for (const width of [1440, 1024, 390]) {
win.setContentSize(width, width === 390 ? 720 : 1100)
await settle()
const activity = await js(`(() => {
const root = document.querySelector('.supervisor-activity');
const tabs = document.querySelector('#supervisor-tab-activity').closest('[role=tablist]');
return { width: innerWidth, pageWidth: document.documentElement.scrollWidth,
clientWidth: root.clientWidth, scrollWidth: root.scrollWidth,
tabsHeight: tabs.getBoundingClientRect().height,
fields: [...root.querySelectorAll('dd, button')].map(e => {
const r = e.getBoundingClientRect(); return { left: r.left, right: r.right };
}), text: root.textContent };
})()`)
assert(activity.pageWidth <= width && activity.scrollWidth <= activity.clientWidth, 'Activity overflow')
assert(activity.tabsHeight >= 32, 'Activity tabs must not collapse')
assert(activity.fields.every(r => r.left >= 0 && r.right <= width), 'Clipped activity metadata or actions')
assert(activity.text.includes('运行中') && activity.text.includes('失败') && activity.text.includes('已完成'), 'Missing execution states')
reports.push({ scenario: 'supervisor-activity', theme, ...activity })
await writeFile(join(artifacts, `activity-${theme}-${width}.png`), (await win.webContents.capturePage()).toPNG())
}
}
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL + '?menu=1')
await wait('!!document.querySelector(".supervisor-workspace__recap")')
win.show()
win.focus()
for (const theme of ['light', 'dark']) {
await js(`document.documentElement.dataset.theme = '${theme}'`)
for (const width of [1440, 390]) {
win.setContentSize(width, width === 390 ? 720 : 1100)
for (const tab of ['overview', 'graph', 'plans', 'settings', 'activity']) {
await js(`document.querySelector('#supervisor-tab-${tab}').click(); document.querySelector('.page-shell').scrollTop = 0`)
await settle()
const menuLayout = await js(`(() => {
const panel = document.querySelector('#supervisor-panel-${tab}');
return { width: innerWidth, pageWidth: document.documentElement.scrollWidth,
panelWidth: panel.clientWidth, scrollWidth: panel.scrollWidth,
tabs: [...document.querySelectorAll('[role=tab][id^="supervisor-tab-"]')].map(e => e.textContent),
nestedMenus: panel.querySelectorAll('[role=tablist]').length,
reports: !!panel.querySelector('#heartbeat-reports-title'),
reportWidth: panel.querySelector('#heartbeat-panel-history > section')?.getBoundingClientRect().width,
suggestions: !!panel.querySelector('#supervision-suggestions-title'),
suggestionItems: panel.querySelectorAll('.supervision-suggestions__list > li').length,
audit: !!panel.querySelector('#heartbeat-runs-title'),
plans: !!panel.querySelector('#heartbeat-panel-plans'),
settingsBadge: !!panel.querySelector('.scope-badge'),
headings: [...panel.querySelectorAll('h2')].map(e => e.textContent),
refresh: [...panel.querySelectorAll('button')].some(e => e.textContent.includes('刷新')) };
})()`)
assert.deepEqual(menuLayout.tabs, ['工作回顾', '故事线图谱', '智能心跳', '活动记录', '设置'])
assert.equal(menuLayout.nestedMenus, tab === 'graph' || tab === 'settings' ? 1 : 0)
assert(menuLayout.pageWidth <= width && menuLayout.scrollWidth <= menuLayout.panelWidth, 'Menu panel overflow')
assert.equal(menuLayout.reports, tab === 'plans')
if (tab === 'plans') assert(Math.abs(menuLayout.reportWidth - menuLayout.panelWidth) <= 1, 'Reports must use full reading width')
assert.equal(menuLayout.suggestions, tab === 'plans')
assert.equal(menuLayout.suggestionItems, tab === 'plans' ? 2 : 0)
assert.equal(menuLayout.audit, false, 'Run audit belongs to Activity, not Smart heartbeat')
assert.equal(menuLayout.plans, tab === 'plans')
if (tab === 'settings') {
assert(!menuLayout.settingsBadge && !menuLayout.refresh, 'Redundant settings badge/refresh')
assert(menuLayout.headings.includes('回顾算法') && menuLayout.headings.includes('模型与运行限制'))
}
reports.push({ scenario: 'menu-placement', theme, tab, ...menuLayout })
await writeFile(join(artifacts, `menu-${tab}-${theme}-${width}.png`), (await win.webContents.capturePage()).toPNG())
const anchors = tab === 'settings' ? ['.supervisor-settings [role=tabpanel]:not([hidden]) form']
: tab === 'plans' ? ['#heartbeat-panel-overview', '.supervision-suggestions', '#heartbeat-panel-history'] : []
for (const [index, anchor] of anchors.entries()) {
await js(`document.querySelector('${anchor}').scrollIntoView({block:'start'})`)
await settle()
await writeFile(join(artifacts, `menu-${tab}-${theme}-${width}-section-${index}.png`), (await win.webContents.capturePage()).toPNG())
}
}
}
}
await js('document.querySelector("#supervisor-tab-plans").click()')
await js('[...document.querySelectorAll(".heartbeat-settings__actions button")].find(b => b.textContent === "执行记录").click()')
await wait('document.querySelector(".supervisor-activity select")?.value === "plan" && document.querySelectorAll(".supervisor-activity__item").length === 2')
await js('document.querySelector(".page-shell").scrollTop = 0')
await settle()
await writeFile(join(artifacts, 'activity-exact-plan.png'), (await win.webContents.capturePage()).toPNG())
await js('document.querySelector("#supervisor-tab-settings").focus()')
win.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Left' })
win.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Left' })
await settle()
assert(await js('document.activeElement.id === "supervisor-tab-activity" && document.activeElement.getAttribute("aria-selected") === "true"'), 'Native keyboard tab navigation')
assert.deepEqual(errors, [])
await writeFile(
join(artifacts, 'measurements.json'),
JSON.stringify(reports, null, 2)
)
console.log(
JSON.stringify(
{
artifacts,
cases: reports.map(({ texts, tokens, ...report }) => ({
...report,
svgTexts: texts?.length,
resolvedTokens: tokens && Object.keys(tokens).length
})),
errors
},
null,
2
)
)
win.destroy()
app.quit()
})
.catch((error) => {
console.error(error)
app.exit(1)
})