fix: complete supervisor review coverage and execution workflow

Replace truncated review input with paged, resumable batches and retain extracted facts and source references through navigation summaries. Expose concurrency, timeouts, batch budgets and response capacity in supervisor settings.

Separate automatic supervision from review results, activity and settings; use a standard plan modal and show execution stages with saved batch details. Fix historical entity identity mapping, navigation output validation and arbitrary summary length failures.

Include focused regressions, isolated real-data validation scripts and updated design and verification records. Overall layout acceptance remains outstanding; this commit does not claim that all visual issues are resolved.

Release note: 监督回顾支持完整分页、分批保存和暂停后继续,可配置并发、超时及响应容量;修复实体引用、汇总校验和长摘要报错,并重新组织自动监督与活动记录入口。
This commit is contained in:
mesalogo
2026-09-25 22:32:43 +08:00
parent f68584b4e2
commit db5e43d7d5
77 changed files with 6182 additions and 770 deletions
+438 -7
View File
@@ -61,6 +61,265 @@ app
await settle()
}
const reports = []
if (process.env.GOODBUDDY_SUPERVISOR_CONTENT_LAYOUT) {
win.show()
for (const view of ['recap', 'activity', 'pending', 'failure']) {
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL + (view === 'activity' ? '?activity=1' : '?recap=1&long-summary=1' + (view === 'failure' ? '&fail-run=1' : '')))
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__toolbar .primary-button").click()')
await wait(view === 'pending' ? '!!document.querySelector(".supervisor-workspace [role=status]")' : '!!document.querySelector(".supervisor-workspace [role=alert]")')
}
await js('document.fonts.ready')
for (const theme of ['light', 'dark']) {
await js(`document.documentElement.dataset.theme = '${theme}'`)
for (const width of [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 }; };
return { width:innerWidth, pageWidth:document.documentElement.scrollWidth,
toolbar:box('.supervisor-workspace__toolbar'), history:box('.supervisor-workspace__result-navigation'), recap:box('.supervisor-workspace__recap'),
prose:box('.supervisor-workspace__prose'), activity:box('.supervisor-activity'),
steps:box('.supervisor-activity__steps'),
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};}) };
})()`)
assert(report.pageWidth <= width, 'No page overflow')
if (view !== 'activity') {
assert(report.summaryLength > 2000 && report.tail, 'Long summary retained')
assert(report.recap.width <= 820, 'Whole recap uses reading frame')
for (const control of [report.toolbar, report.history]) {
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')
} else {
assert(report.activity.width <= 1040, 'Activity dashboard frame')
if (width >= 1024) {
assert(report.steps.height < 40, 'Compact stage row')
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 win.webContents.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 win.webContents.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('#heartbeat-panel-suggestions'),
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')
assert.equal(report.suggestions, tab === 'plans')
assert.equal(report.recap, tab === 'overview' && scenario !== 'empty')
if (tab === 'overview') {
assert(!report.text.includes('暂无自动监督报告') && !report.text.includes('待确认记忆'))
if (scenario !== 'empty') {
assert.equal(report.paragraphs, 4)
assert(report.text.includes('外部评审时间') && report.text.includes('未解决事项'))
if (width === 1440) assert(report.proseWidth < 820, 'Bounded prose width')
} else assert(report.text.includes('还没有成功回顾'))
} else {
assert.equal(report.text.includes('暂无自动监督报告'), scenario !== 'populated')
assert(report.text.includes('自动监督报告'))
}
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') {
for (const anchor of ['heartbeat-memory-title', 'heartbeat-reports-title', 'heartbeat-runs-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 === 'populated') {
await js('document.querySelector("#supervisor-tab-overview").click()')
await js(`(() => { const s = document.querySelector('.supervisor-workspace__result-navigation select'); s.value = 'older-result'; s.dispatchEvent(new Event('change', {bubbles:true})); })()`)
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 [role=status]").textContent.includes("新回顾正在整理")'))
await writeFile(join(artifacts, 'recap-running-history-390.png'), (await win.webContents.capturePage()).toPNG())
await js('document.querySelector(".supervisor-workspace [role=status] button").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) {
for (const empty of [false, true]) {
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL + (empty ? '' : '?menu=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'),
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', 'metrics', 'trend', 'audit', 'plans']) assert.equal(report[key], tab === 'plans', key)
assert.equal(report.create, tab === 'plans' ? 1 : 0)
assert.equal(report.emptyStates, 0)
if (tab === 'activity') assert.equal(report.activity, 3)
reports.push({ empty, tab, ...report })
const prefix = `automatic-${empty ? 'empty' : 'populated'}-${tab}-light-${width}`
await writeFile(join(artifacts, `${prefix}.png`), (await win.webContents.capturePage()).toPNG())
if (tab === 'plans') {
for (const anchor of ['heartbeat-trend-title', 'heartbeat-runs-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'), '取消保存并启用计划')
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))
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")')
@@ -106,6 +365,97 @@ app
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 open(false)
await js(
'document.querySelector(".supervisor-workspace__legend .link-button").click()'
@@ -400,7 +750,7 @@ app
join(artifacts, 'settings.png'),
(await win.webContents.capturePage()).toPNG()
)
await js('document.querySelector("#heartbeat-tab-plans").click()')
assert.equal(await js('document.querySelectorAll("[role=tablist]").length'), 1, 'No nested settings menu')
for (const width of [1440, 1024, 390]) {
win.setContentSize(width, 1100)
await js('document.querySelector(".page-shell").scrollTop = 0')
@@ -489,7 +839,12 @@ app
)
await js('document.querySelector("#supervisor-tab-settings").click()')
await wait('!!document.querySelector(".heartbeat-settings")')
assert(await js('document.querySelector("#heartbeat-tab-plans").getAttribute("aria-selected") === "true"'))
assert(await js('!document.querySelector("#supervisor-panel-settings [role=tablist], #supervisor-panel-settings .scope-badge")'))
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 }));
@@ -501,21 +856,36 @@ app
win.setContentSize(width, 1100)
await settle()
const settings = await js(`(() => {
const root = document.querySelector('.heartbeat-settings');
const root = document.querySelector('[role=dialog]');
return { width: innerWidth, pageWidth: document.documentElement.scrollWidth,
clientWidth: root.clientWidth, scrollWidth: root.scrollWidth,
fields: [...root.querySelectorAll('input, select')].map(e => {
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')
assert(settings.text.includes('尚未配置自动监督,目前仅支持手动回顾'), 'Missing unconfigured state')
reports.push({ scenario: 'automatic-supervision-settings', theme, ...settings })
await writeFile(join(artifacts, `settings-${theme}-${width}.png`), (await win.webContents.capturePage()).toPNG())
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']) {
@@ -541,6 +911,67 @@ app
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]')].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('#heartbeat-memory-title'),
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, 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.audit, tab === 'plans')
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' ? ['.heartbeat-settings__editor:last-child']
: tab === 'plans' ? ['#heartbeat-panel-overview', '#heartbeat-panel-suggestions', '#heartbeat-panel-history', '[aria-labelledby=heartbeat-runs-title]'] : []
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'),
+39 -11
View File
@@ -5,6 +5,7 @@ import { SupervisionCard } from '../../src/renderer/src/RightAssistantSidebar'
import { PageShell } from '../../src/renderer/src/WorkspacePrimitives'
import { UiLocaleProvider } from '../../src/renderer/src/i18n/UiLocaleProvider'
import type { SupervisionGraphView } from '../../src/shared/supervision-contracts'
import { applicationSettingsSchema, defaultLocalToolEnvironmentSettings } from '../../src/shared/application-settings-contracts'
import '../../src/renderer/src/styles.css'
import '@fontsource-variable/noto-sans-sc/wght.css'
import { installBundledUiFonts } from '../../src/renderer/src/fonts'
@@ -145,21 +146,43 @@ Object.defineProperty(window, 'goodbuddy', {
? {}
: {
supervision: {
activity: async () => ['running', 'failed', 'completed'].map((status, index) => ({
batches: async ({ runId, offset, limit }: { runId: string; offset: number; limit: number }) => Array.from({ length: runId === 'activity-0' ? 6 : 12 }, (_, index) => ({
id: `leaf-${index}`, projectId: index < 3 ? 'Atlas' : 'Beacon', conversationId: `conversation-${Math.floor(index / 2)}`,
evidence: [{ id: `source-${index}`, sourceType: 'conversation', sourceId: `conversation-${Math.floor(index / 2)}`,
title: ['交付计划与验收', '接口迁移讨论', '用户反馈核对'][Math.floor(index / 2) % 3],
content: '模拟来源:验收需要核对负责人、交付日期和原始讨论依据。'.repeat(20),
occurredAt: '2026-09-23T08:00:00Z', locator: { messageId: `message-${index}`, start: 0, end: 750, revision: 'saved-source-revision' } }],
output: { summary: ['确认交付前先完成验收资料核验,外部评审日期待定。', '保留接口变更依据,迁移方案仍需项目负责人确认。'][index % 2],
changeDigest: '已补充负责人和待核对项。', openItems: ['核对外部评审日期。'],
events: [{ title: '验收安排', description: '先核对资料,再确认交付日期。', sourceReferenceIds: [`source-${index}`] }], entities: [], entityChanges: [], relations: [] }
})).slice(offset, offset + limit),
resume: async () => {}, pause: async () => {},
activity: async (input?: { configId?: string }) => ['running', 'failed', 'completed'].map((status, index) => ({
id: `activity-${index}`, kind: index === 2 ? 'supervision' : 'heartbeat', trigger: index === 2 ? 'manual' : 'scheduled', status,
scope: { kind: 'global' }, startedAt: '2026-09-23T08:00:00Z', completedAt: status === 'running' ? null : '2026-09-23T08:01:00Z',
timeRange: { from: '2026-09-01T00:00:00Z', to: '2026-09-23T00:00:00Z' },
error: status === 'failed' ? 'SIMULATED: 下游监督回顾失败,已保存心跳报告。'.repeat(8) : null,
summary: 'SIMULATED: 已保存的执行摘要。', resultId: status === 'completed' ? 'fixture-result' : null,
heartbeatStatus: index === 2 ? null : 'completed', supervisionStatus: status
})),
heartbeatStatus: index === 2 ? null : 'completed', supervisionStatus: status,
...(params.has('activity') ? {
status: index === 0 ? 'failed' : index === 1 ? 'running' : 'completed',
supervisionStatus: index === 0 ? 'failed' : index === 1 ? 'running' : 'completed',
error: index === 0 ? JSON.stringify(['events', 'entities', 'entityChanges', 'relations'].map(key => ({ expected: 'array', code: 'invalid_type', path: [key], message: 'Invalid input: expected array, received undefined' })), null, 2) : null,
reviewProgress: { runId: `activity-${index}`, batches: index === 0 ? 6 : 12, characters: index === 0 ? 4515 : 9800,
sources: 32, remainingSources: index === 1 ? 8 : 0, complete: index === 2,
phase: index === 0 ? 'summarizing' : index === 1 ? 'extracting' : 'saving', navigationNodes: index === 0 ? 1 : index === 1 ? 0 : 11, inFlight: index === 1 ? 2 : 0,
settings: { pageSize: 50, batchCharacters: 8000, batchMessages: 20, executionSeconds: 300, timeoutSeconds: 240, concurrency: 2 } }
} : {})
})).filter(row => !input?.configId || (input.configId === 'plan' && row.kind === 'heartbeat')),
overview: async () => {
if (state === 'loading') return new Promise(() => {})
if (state === 'error')
throw new Error('SIMULATED: 读取监督者数据库失败,请重试。')
return state === 'empty' ? [] : [{ id: 'fixture-result', storyLineId: 'fixture', sourceId: 'source-1',
summary: '模拟回顾', changeDigest: '', createdAt: '2026-09-22T00:00:00Z',
scope: { kind: 'global' }, timeRange: { from: '2026-09-01T00:00:00Z', to: '2026-09-22T00:00:00Z' }, openItems: [] }]
summary: params.has('long-summary') ? Array.from({ length: 24 }, (_, index) => `第 ${index + 1} 项核验:本周已核对交付计划中的负责人、验收日期和原始讨论记录。团队决定先完成资料核验,再确认最终交付时间。现有记录支持这一安排,但外部评审时间仍待确认,不能将内部计划日期视为已承诺的截止日期。下一次回顾需要核对评审意见是否返回。`).join('\n\n') + '\n\n长摘要末尾:仍须确认最终验收条件。' : params.has('recap') ? '本周已核对交付计划中的负责人、验收日期和原始讨论记录。需求澄清与阶段总结能够对应到具体会议,交付清单中的两项缺失信息已补齐。\n\n团队决定先完成资料核验,再确认最终交付时间。现有记录支持这一安排,但外部评审时间仍待确认,不能将内部计划日期视为已承诺的截止日期。\n\n下一次回顾需要核对评审意见是否返回,以及负责人与验收条件是否发生变化。保留这些未完成事项,便于继续跟进。' : '模拟回顾',
changeDigest: params.has('recap') ? '交付清单补充了负责人;验收条件已与原始讨论对齐。' : '', createdAt: '2026-09-22T00:00:00Z',
scope: { kind: 'global' }, timeRange: { from: '2026-09-01T00:00:00Z', to: '2026-09-22T00:00:00Z' }, openItems: params.has('recap') ? ['确认外部评审时间,并回填交付清单。', '检查验收材料是否覆盖所有已确认需求。'] : [] },
...(params.has('recap') ? [{ id: 'older-result', storyLineId: 'fixture', sourceId: 'source-1', summary: '上期结果:交付清单尚缺负责人,需要继续核对。', changeDigest: '', createdAt: '2026-09-15T00:00:00Z', scope: { kind: 'global' }, timeRange: { from: '2026-09-01T00:00:00Z', to: '2026-09-15T00:00:00Z' }, openItems: ['核对负责人。'] }] : [])]
},
graph: async () =>
state === 'empty'
@@ -173,7 +196,8 @@ Object.defineProperty(window, 'goodbuddy', {
eventSources: []
}
: graph,
run: async () => {
run: async () => {
if (params.has('recap') && !params.has('fail-run')) return new Promise(() => {})
throw new Error('SIMULATED: 模型暂时不可用。此验证不调用模型。')
},
entityAction: async () => {},
@@ -187,6 +211,8 @@ Object.defineProperty(window, 'goodbuddy', {
}
})
const noop = async () => {}
const menu = params.has('menu')
const createdAt = '2026-09-23T08:00:00.000Z'
createRoot(document.getElementById('root')!).render(
<UiLocaleProvider initialPreference="zh-CN">
<div
@@ -223,12 +249,14 @@ createRoot(document.getElementById('root')!).render(
/>
</div> : <PageShell variant="supervisor">
<HeartbeatCenter
configs={[]}
runs={[]}
entries={[]}
memories={[]}
applicationSettings={applicationSettingsSchema.parse({ checkUpdatesOnStartup: true, updateSource: 'github', modelDownloadSource: 'modelscope', localToolEnvironment: defaultLocalToolEnvironmentSettings, conversationHtmlRenderingEnabled: true, remoteProjectsEnabled: false })}
onUpdateApplicationSettings={async () => true}
configs={menu ? [{ id: 'plan', name: '模拟每日回顾', scope: { kind: 'global' }, timezone: 'Asia/Shanghai', recurrence: { type: 'daily', localTime: '09:00' }, enabled: true, lookbackHours: 48, retentionDays: 90, nextRunAt: '2026-09-24T01:00:00.000Z', createdAt, updatedAt: createdAt }] : []}
runs={menu ? [{ id: 'run', configId: 'plan', trigger: 'scheduled', scheduledFor: createdAt, status: 'completed', attemptCount: 1, createdAt, updatedAt: createdAt }] : []}
entries={menu ? [{ id: 'report', configId: 'plan', runId: 'run', scheduledFor: createdAt, summary: '模拟自动监督报告:交付计划已更新,待核对负责人和验收日期。', highlights: ['保留原始依据,核对交付时间。'], proposedMemoryIds: ['memory'], followUpTaskIds: ['task'], createdAt }] : []}
memories={menu ? [{ id: 'memory', scope: 'global', type: 'preference', content: '模拟建议:周会总结保留负责人和下一次检查日期。', confidence: 0.9, salience: 0.8, status: 'proposed', createdAt, updatedAt: createdAt }] : []}
projects={[]}
tasks={[]}
tasks={menu ? [{ id: 'task', title: '模拟行动:核对交付清单', instructions: '核对负责人、验收日期和原始讨论依据。', origin: 'assistant', status: 'paused', createdAt }] : []}
onCreate={noop}
onUpdate={noop}
onSetPaused={noop}