fix: reduce supervisor stalls and preserve task delivery

Load Supervisor graphs only when opened, preserve source details on demand, reuse indexed story digests and window selection lists without losing keyboard or focus behavior. Route heavy UI reads through the existing worker, batch scoped queries and construct each publication snapshot once.

Keep remote cancellation responsive during model-route cleanup, remove late routes and retain reported usage after response-cap cancellation. Preserve Telegram pending replies through polling conflicts, show delivery errors and honor the final rate-limit cooldown.

Schema 62 adds lookup indexes without rewriting user content; older clients cannot open upgraded data. Large review publication still blocks Main and remains documented follow-up work. Record the future storage-service direction without performing the architecture migration.

Validation: exact staged code passed isolated full tests (6212 passed, 87 skipped), typecheck and lint. Three-round source performance comparisons, SQLite migration equivalence, native Electron interaction checks and current-source Linux x64 Host validation completed; eight external text-model requests.

Release note: Supervisor opens reviews without loading the entire graph, keeps long selection lists responsive and preserves details and keyboard navigation. Remote cancellation and Telegram reply recovery are more reliable; remote fixes require an Agent build containing this change.
This commit is contained in:
mesalogo
2026-10-05 14:18:33 +08:00
parent e50ca6d149
commit b90ff50f2a
59 changed files with 2108 additions and 265 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ import { expect, it } from 'vitest'
it('preserves virtualized disclosures with native Electron input and bounded large-history DOM', async () => {
const directory = await mkdtemp(join(tmpdir(), 'goodbuddy-expansion-'))
const require = createRequire(import.meta.url)
const source = process.env.GB_HISTORY_SOURCE
const source = process.env.GB_EXPANSION_SOURCE ?? process.env.GB_HISTORY_SOURCE
const denseSource = process.env.GB_EXPANSION_DENSE_SOURCE
const server = await createServer({
configFile: false, root: resolve('.'), cacheDir: join(directory, 'vite'),
+1
View File
@@ -88,6 +88,7 @@ it(process.env.GOODBUDDY_SUPERVISOR_SIDEBAR
resolve('tests/support/supervisor-layout-driver.mjs'),
driver
)
await copyFile(resolve('tests/support/supervisor-selection-driver.mjs'), join(directory, 'supervisor-selection-driver.mjs'))
const child = spawn(createRequire(import.meta.url)('electron'), [driver], {
env,
stdio: ['ignore', 'pipe', 'pipe']
+24 -5
View File
@@ -10,11 +10,23 @@ app.whenReady().then(async () => {
const win = new BrowserWindow({ show: true, width: 1280, height: 800, useContentSize: true,
webPreferences: { sandbox: true, contextIsolation: true, nodeIntegration: false, backgroundThrottling: false } })
const errors = []
win.webContents.on('console-message', event => { if (event.level === 'error') errors.push(event.message) })
win.webContents.on('console-message', event => {
if (event.level === 'error') { errors.push(event.message); console.error('RendererError', event.message) }
})
const evaluate = source => win.webContents.executeJavaScript(source)
const started = Date.now()
const mark = phase => {
console.log('MessageExpansionPhase ' + JSON.stringify({ phase, elapsedMs: Date.now() - started }))
}
const waitFor = source => evaluate(`new Promise((resolve, reject) => {
const deadline = performance.now() + 10000;
const check = () => { if (${source}) resolve(true); else if (performance.now() > deadline) reject(new Error(${JSON.stringify(source)})); else requestAnimationFrame(check); }; check(); })`)
let frame;
const timeout = setTimeout(() => { cancelAnimationFrame(frame); reject(new Error(${JSON.stringify(source)})); }, 10000);
const check = () => {
try {
if (${source}) { clearTimeout(timeout); resolve(true); }
else frame = requestAnimationFrame(check);
} catch (error) { clearTimeout(timeout); reject(error); }
}; check(); })`)
const frames = () => evaluate('new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve(true))))')
const click = async selector => {
await evaluate(`document.querySelector(${JSON.stringify(selector)}).scrollIntoView({block:'center'})`)
@@ -26,12 +38,14 @@ app.whenReady().then(async () => {
win.webContents.sendInputEvent({ type: 'mouseDown', ...point, button: 'left', clickCount: 1 })
win.webContents.sendInputEvent({ type: 'mouseUp', ...point, button: 'left', clickCount: 1 })
}
for (const count of [2000, 10000]) {
for (const count of process.env.GB_EXPANSION_SUPERVISOR_ONLY ? [] : [2000, 10000]) {
mark(`history-${count}-load`)
await win.loadURL(process.env.GB_EXPANSION_URL + '?count=' + count)
win.focus()
win.webContents.focus()
await waitFor('document.hasFocus() && document.querySelector("[data-message-id=m0] details")')
await evaluate('document.fonts.ready.then(() => true)')
mark(`history-${count}-sweep`)
const untouched = await evaluate(`(async () => {
const start = performance.now(), visited = new Set(); let maxRows = 0;
for (let index = 0; index < ${count}; index += Math.floor(${count} / 200)) {
@@ -55,6 +69,7 @@ app.whenReady().then(async () => {
if (process.env.GB_EXPANSION_DENSE_SOURCE) assert.ok(untouched.entries >= untouched.visited, JSON.stringify(untouched))
else if (!process.env.GB_HISTORY_SOURCE) assert.equal(untouched.entries, 0)
const times = []
mark(`history-${count}-disclosures`)
let maxRows = 0
let maxNodes = 0
const visit = async index => {
@@ -110,12 +125,15 @@ app.whenReady().then(async () => {
navigationTwoFramesMedianMs: times[Math.floor(times.length/2)], maxRows, maxNodes, restored, untouched, changedEntries, afterDeletion,
electron: process.versions.electron, platform: process.platform, arch: process.arch }))
}
mark('supervisor-load')
await win.loadURL(process.env.GB_EXPANSION_URL + '?supervisor=1')
mark('supervisor-source-ready')
await waitFor('document.querySelector(".supervisor-workspace__detail .link-button")')
await click('.supervisor-workspace__detail .link-button')
await waitFor('typeof window.finishSource === "function"')
await click('#change-language')
await waitFor('document.documentElement.lang === "en-US" && document.querySelector(".supervisor-workspace__detail .link-button")')
mark('supervisor-language-refresh')
await waitFor('document.documentElement.lang === "en-US" && document.querySelector(".supervisor-workspace__detail .link-button")?.textContent.includes("Synthetic source 2")')
await evaluate('window.finishSource()')
await frames()
const stuck = await evaluate('document.querySelector(".supervisor-workspace__detail .link-button").disabled')
@@ -123,6 +141,7 @@ app.whenReady().then(async () => {
assert.equal(await evaluate('document.body.innerText.includes("Stale content")'), false)
console.log('MessageExpansion ' + JSON.stringify({ scenario: 'supervisor-invalidation', version: process.env.GB_HISTORY_SOURCE ? 'baseline' : 'fixed', stuck }))
assert.deepEqual(errors, [])
mark('complete')
win.destroy()
app.quit()
}).catch(error => { console.error(error); app.exit(1) })
+10 -4
View File
@@ -57,15 +57,21 @@ function Fixture() {
const supervisor = new URLSearchParams(location.search).has('supervisor')
if (supervisor) {
const at = '2026-10-01T00:00:00.000Z'
let graphReads = 0
Object.assign(window, { goodbuddy: { supervision: {
overview: async () => [{ id: 'result', storyLineId: 'story', sourceId: null, summary: 'Synthetic review', changeDigest: '',
createdAt: at, scope: { kind: 'global' }, timeRange: { from: at, to: at }, openItems: [] }],
graph: async () => ({ storyLine: null, events: [{ id: 'event', title: 'Event', description: '', occurred_at: at }],
entities: [], relations: [], eventEntities: [], sources: [{ id: 'source', title: 'Synthetic source', occurred_at: at }],
eventSources: [{ event_id: 'event', source_id: 'source' }] }),
graph: async () => {
const read = ++graphReads
// Keep the replacement read pending long enough to expose partial-state waits.
if (read > 1) await new Promise(resolve => setTimeout(resolve, 100))
return { storyLine: null, events: [{ id: 'event', title: 'Event', description: '', occurred_at: at }],
entities: [], relations: [], eventEntities: [], sources: [{ id: 'source', title: `Synthetic source ${read}`, occurred_at: at }],
eventSources: [{ event_id: 'event', source_id: 'source' }] }
},
source: () => new Promise(resolve => Object.assign(window, { finishSource: () => resolve({ title: 'Synthetic source', content: 'Stale content', occurredAt: at }) }))
} } })
}
createRoot(document.getElementById('root')!).render(<UiLocaleProvider>{supervisor
createRoot(document.getElementById('root')!).render(<UiLocaleProvider initialPreference="zh-CN">{supervisor
? <><button id="change-language" onClick={() => void changeUiLocale('en-US')}>Change language</button><SupervisorWorkspace tab="graph" /></>
: <Fixture />}</UiLocaleProvider>)
+20 -13
View File
@@ -5,6 +5,7 @@ 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'
const directory = process.env.GOODBUDDY_SUPERVISOR_DIRECTORY
const artifacts = process.env.GOODBUDDY_SUPERVISOR_ARTIFACTS || directory
@@ -63,6 +64,12 @@ app
await settle()
}
const reports = []
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")')
@@ -164,7 +171,7 @@ app
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 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')
@@ -196,7 +203,7 @@ app
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,
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};
@@ -212,8 +219,8 @@ app
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"')
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"')
@@ -317,7 +324,7 @@ app
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 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();
@@ -984,6 +991,7 @@ app
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()'
@@ -1176,6 +1184,9 @@ app
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]');
@@ -1185,7 +1196,7 @@ app
const pageHeight = shell.scrollHeight, pageTop = shell.scrollTop;
list.scrollTop = list.scrollHeight;
const last = list.lastElementChild.getBoundingClientRect();
const button = list.querySelector('button:last-of-type');
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,
@@ -1275,9 +1286,10 @@ app
'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.querySelectorAll('.supervisor-workspace__list-panel button')[79]; 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)) }; })()`
`(() => { 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({
@@ -1300,12 +1312,7 @@ app
`!!document.querySelector('.supervisor-workspace__map [aria-label="模拟事件 80"][aria-pressed=true]')`
)
)
assert.equal(
await js(
'document.querySelectorAll(".supervisor-workspace__list-panel button").length'
),
80
)
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())'
)
+24 -3
View File
@@ -141,12 +141,13 @@ const stories = [
const story = stories[Number(params.get('story') ?? 0)] ?? stories[0]
const titles = story.events
const labels = story.entities
const dense = params.has('dense')
const windowed = params.has('windowed')
const dense = params.has('dense') || windowed
const long = params.has('long')
const state = params.get('state')
const graph: SupervisionGraphView = {
storyLine: { id: 'fixture', scope_json: '{"kind":"global"}' },
events: Array.from({ length: dense ? 80 : 8 }, (_, index) => ({
events: Array.from({ length: windowed ? 600 : dense ? 80 : 8 }, (_, index) => ({
id: `event-${index}`,
title: dense
? `模拟事件 ${index + 1}`
@@ -158,7 +159,7 @@ const graph: SupervisionGraphView = {
? '2026-09-21T08:00:00.000Z'
: `2026-09-${String(14 + index).padStart(2, '0')}T08:00:00.000Z`
})),
entities: Array.from({ length: dense ? 40 : 6 }, (_, index) => ({
entities: Array.from({ length: windowed ? 600 : dense ? 40 : 6 }, (_, index) => ({
id: `entity-${index}`,
canonical_label: dense
? `模拟实体 ${index + 1}`
@@ -193,6 +194,12 @@ const graph: SupervisionGraphView = {
source_id: 'source-1'
}))
}
if (windowed) {
graph.relations = Array.from({ length: 600 }, (_, index) => ({ ...graph.relations[0]!, id: `relation-${index}`,
from_entity_id: `entity-${index}`, to_entity_id: `entity-${(index + 1) % 600}` }))
graph.eventEntities = graph.events.map((event, index) => ({ event_id: event.id, entity_id: `entity-${index}` }))
graph.eventSources = graph.events.map(event => ({ event_id: event.id, source_id: 'source-1' }))
}
if (params.has('short')) {
graph.events = graph.events.slice(0, 1)
graph.entities = graph.entities.slice(0, 1)
@@ -310,6 +317,20 @@ if (params.has('portable')) {
run: async () => { throw new Error('Read-only acceptance must not run reviews') }
})
}
if (windowed) {
const api = window.goodbuddy.supervision
const calls = { graph: 0, source: 0, edits: 0 }
const updateCalls = () => { document.documentElement.dataset.supervisorCalls = JSON.stringify(calls) }
const readGraph = api.graph, readSource = api.source
api.graph = async input => { calls.graph++; updateCalls(); return readGraph(input) }
api.source = async input => { calls.source++; updateCalls(); return readSource(input) }
api.entityAction = async input => {
calls.edits++; updateCalls()
const entity = graph.entities.find(item => item.id === input.entityId)
if (entity && input.action === 'revise' && input.label) entity.canonical_label = input.label
}
updateCalls()
}
const noop = async () => {}
createRoot(document.getElementById('root')!).render(
<UiLocaleProvider initialPreference="zh-CN">
@@ -0,0 +1,123 @@
import assert from 'node:assert/strict'
// Real production components, native keyboard and mouse input; only the bridge data is simulated.
export async function validateSupervisorSelection(win, js, wait, url) {
win.show(); win.focus(); win.setContentSize(1440, 1100)
await win.loadURL(url + '?windowed=1&recap=1')
await wait('document.querySelector(".supervisor-workspace")?.getAttribute("aria-busy") === "false"')
assert.equal(await js('JSON.parse(document.documentElement.dataset.supervisorCalls).graph'), 0)
const frames = () => js('new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)))')
const click = async selector => {
win.focus(); win.webContents.focus()
const point = await js(`(() => { const e=document.querySelector(${JSON.stringify(selector)}); e.scrollIntoView({block:'nearest'}); const r=e.getBoundingClientRect(); return {x:Math.round(r.left+r.width/2),y:Math.round(r.top+r.height/2)} })()`)
win.webContents.sendInputEvent({type:'mouseDown',button:'left',clickCount:1,...point})
win.webContents.sendInputEvent({type:'mouseUp',button:'left',clickCount:1,...point})
await frames()
}
const key = async (keyCode, modifiers = []) => {
win.focus(); win.webContents.focus()
await wait('document.hasFocus()')
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 frames()
}
const panel = '.supervisor-workspace__selection-list'
await click('#supervisor-tab-graph')
await wait(`!!document.querySelector('${panel}') && document.querySelector('.supervisor-workspace').getAttribute('aria-busy') === 'false'`)
for (const kind of ['event', 'entity', 'relation']) {
await js(`document.querySelector('.supervisor-workspace__category').value='${kind}';document.querySelector('.supervisor-workspace__category').dispatchEvent(new Event('change',{bubbles:true}))`)
await wait(`document.querySelector('${panel}')?.id.endsWith('-${kind}')`)
// Select a middle record so selection/focus pins cannot accidentally keep either endpoint.
await js(`(() => {const p=document.querySelector('${panel}'); p.scrollTop=(p.scrollHeight-p.clientHeight)/2;p.dispatchEvent(new Event('scroll'))})()`)
await wait(`(() => {const button=document.querySelector('[data-list-window-row="${kind}-300"] button');if(!button)return false;button.click();return true})()`)
await wait(`document.querySelector('[data-list-window-row="${kind}-300"] button')?.getAttribute('aria-pressed') === 'true'`)
await frames()
await js(`(() => {
const p=document.querySelector('${panel}');
const stops=[...document.querySelectorAll('button,input,select,textarea,a[href],[tabindex],summary')]
.filter(e=>e.tabIndex>=0 && !e.disabled && e.getClientRects().length && !e.closest('[hidden],[inert]'));
const index=stops.indexOf(p);
window.selectionEntryBefore=stops[index-1];
window.selectionEntryAfter=stops.slice(index+1).find(e=>!p.contains(e));
window.selectionEntryBefore.focus();
p.scrollTop=(p.scrollHeight-p.clientHeight)/2;p.dispatchEvent(new Event('scroll'));
})()`)
await frames()
assert(await js(`document.querySelector('${panel}').scrollTop > 1000`), 'Forward entry starts with the viewport in the middle')
await key('Tab')
assert(await js(`document.activeElement===document.querySelector('${panel}')`), 'Forward Tab reaches the panel first')
await key('Tab')
const forwardEntry = await js("document.activeElement.closest('[data-list-window-row]')?.dataset.listWindowRow")
const forwardVisible = await js(`(() => {const p=document.querySelector('${panel}').getBoundingClientRect(),r=document.activeElement.getBoundingClientRect();return r.top>=p.top-1 && r.bottom<=p.bottom+1})()`)
await key('Tab', ['shift']); await key('Tab', ['shift'])
const backwardExit = await js('document.activeElement===window.selectionEntryBefore')
await js(`(() => {window.selectionEntryAfter.focus();const p=document.querySelector('${panel}');p.scrollTop=(p.scrollHeight-p.clientHeight)/2;p.dispatchEvent(new Event('scroll'))})()`)
await frames()
assert(await js(`document.querySelector('${panel}').scrollTop > 1000`), 'Backward entry starts with the viewport in the middle')
await key('Tab', ['shift'])
const backwardEntry = await js("document.activeElement.closest('[data-list-window-row]')?.dataset.listWindowRow")
const backwardVisible = await js(`(() => {const p=document.querySelector('${panel}').getBoundingClientRect(),r=document.activeElement.getBoundingClientRect();return r.top>=p.top-1 && r.bottom<=p.bottom+1})()`)
await key('Tab')
const forwardExit = await js('document.activeElement===window.selectionEntryAfter')
assert.deepEqual({ forwardEntry, backwardEntry, backwardExit, forwardExit, forwardVisible, backwardVisible },
{ forwardEntry: `${kind}-0`, backwardEntry: `${kind}-599`, backwardExit: true, forwardExit: true, forwardVisible: true, backwardVisible: true },
'External sequential entry/exit follows the full logical list without a focus trap')
await js(`document.querySelector('${panel}').focus()`)
await key('End')
await wait(`document.activeElement?.closest('[data-list-window-row]')?.dataset.listWindowRow === '${kind}-599'`)
assert(await js(`document.querySelectorAll('${panel} button').length <= 123`), 'DOM must be bounded')
const focusedBounds = await js(`(() => {const panel=document.querySelector('${panel}'),p=panel.getBoundingClientRect(),r=document.activeElement.getBoundingClientRect();return {top:r.top,bottom:r.bottom,panelTop:p.top,panelBottom:p.bottom,scrollTop:panel.scrollTop,scrollHeight:panel.scrollHeight}})()`)
assert(focusedBounds.top >= focusedBounds.panelTop - 1 && focusedBounds.bottom <= focusedBounds.panelBottom + 1, `End must focus a visible last row: ${kind} ${JSON.stringify(focusedBounds)}`)
await key('Enter')
assert(await js(`document.querySelector('[data-list-window-row="${kind}-599"] button').getAttribute('aria-pressed')==='true'`))
await js(`document.querySelector('${panel}').focus()`)
await key('Home')
await wait(`document.activeElement?.closest('[data-list-window-row]')?.dataset.listWindowRow === '${kind}-0'`)
for (let i=0;i<65;i++) await key('Tab')
assert.equal(await js("document.activeElement.closest('[data-list-window-row]').dataset.listWindowRow"), `${kind}-65`, 'Tab traverses unmounted rows')
await key('Tab', ['shift'])
assert.equal(await js("document.activeElement.closest('[data-list-window-row]').dataset.listWindowRow"), `${kind}-64`)
// A focused row stays mounted during scrollbar jumps; the new viewport must fill too.
await js(`document.querySelector('${panel}').scrollTop=20000;document.querySelector('${panel}').dispatchEvent(new Event('scroll'))`)
await frames()
assert.equal(await js("document.activeElement.closest('[data-list-window-row]').dataset.listWindowRow"), `${kind}-64`)
assert(await js(`(() => {const p=document.querySelector('${panel}'),r=p.getBoundingClientRect();return [...p.querySelectorAll('[data-list-window-row]')].some(e=>{const b=e.getBoundingClientRect();return b.bottom>r.top && b.top<r.bottom})})()`), 'Scrolled viewport must have rows')
assert(await js(`document.querySelectorAll('${panel} button').length <= 123`))
}
// Metadata does not trigger body reads. Opening the source still shows its complete saved snapshot.
assert.equal(await js('JSON.parse(document.documentElement.dataset.supervisorCalls).source'), 0)
await js(`document.querySelector('.supervisor-workspace__category').value='event';document.querySelector('.supervisor-workspace__category').dispatchEvent(new Event('change',{bubbles:true}))`)
await frames()
await click('.supervisor-workspace__detail > button.link-button')
await wait('document.querySelector(".supervisor-workspace__source-content") || document.body.textContent.includes("视觉 fixture,不是真实用户数据。")')
assert.equal(await js('JSON.parse(document.documentElement.dataset.supervisorCalls).source'), 1)
await click('#supervisor-tab-overview')
await click('#supervisor-tab-graph')
assert.equal(await js('JSON.parse(document.documentElement.dataset.supervisorCalls).graph'), 1, 'Warm tab switch keeps the graph')
await click('.supervisor-workspace__result-navigation button')
await click('.supervisor-workspace__history-menu button[value="older-result"]')
await wait('document.querySelector(".supervisor-workspace").getAttribute("aria-busy")==="false"')
assert.equal(await js('JSON.parse(document.documentElement.dataset.supervisorCalls).graph'), 2)
assert(!await js('document.body.textContent.includes("视觉 fixture,不是真实用户数据。")'), 'History clears the old source')
await js(`document.querySelector('.supervisor-workspace__category').value='entity';document.querySelector('.supervisor-workspace__category').dispatchEvent(new Event('change',{bubbles:true}))`)
await frames()
await js(`document.querySelector('${panel}').focus()`)
await key('End'); await key('Enter')
await click('.supervisor-workspace__detail > .supervisor-workspace__actions button:nth-child(2)')
await click('.supervisor-workspace__detail form input')
await key('a', ['control'])
for (const character of 'Edited entity') win.webContents.sendInputEvent({type:'char',keyCode:character})
await click('.supervisor-workspace__detail form button')
await wait('JSON.parse(document.documentElement.dataset.supervisorCalls).edits===1 && document.querySelector(".supervisor-workspace").getAttribute("aria-busy")==="false"')
assert.equal(await js('JSON.parse(document.documentElement.dataset.supervisorCalls).graph'), 3)
assert(await js('document.querySelector("[data-list-window-row=entity-599]").textContent.includes("Edited entity")'))
// Narrow layout still fills the scroll viewport without clipping full labels.
win.setContentSize(390, 800)
await frames()
await js(`document.querySelector('${panel}').focus()`)
await key('End')
assert(await js('document.documentElement.scrollWidth <= innerWidth'))
assert(await js(`document.querySelectorAll('${panel} button').length <= 123`))
win.setContentSize(1440, 1100)
}
+28 -1
View File
@@ -42,6 +42,7 @@ void app.whenReady().then(async () => {
let pollsAborted = 0
let pollingUnavailable = false
let pollingConflict = false
let sendingForbidden = false
const originalNetFetch = net.fetch
// This is the only Telegram substitution: production driver construction and IPC stay intact.
net.fetch = async (input, init) => {
@@ -71,7 +72,10 @@ void app.whenReady().then(async () => {
result = updates.splice(0)
break
case 'sendChatAction': result = true; break
case 'sendMessage': result = { message_id: 1000 }; break
case 'sendMessage':
if (sendingForbidden) return new Response(JSON.stringify({ ok: false, error_code: 403,
description: `https://api.telegram.org/bot${token}/sendMessage` }), { status: 403 })
result = { message_id: 1000 }; break
default: throw new Error(`Unexpected Telegram method: ${method}`)
}
return new Response(JSON.stringify({ ok: true, result }), { headers: { 'content-type': 'application/json' } })
@@ -223,6 +227,28 @@ void app.whenReady().then(async () => {
await click('#channel-settings-panel-telegram input[role="switch"]')
await click('button.primary-button')
await wait(() => run(`!document.querySelector('button.primary-button').disabled && document.querySelector('.channel-settings-card .capability-card__header > span')?.textContent === 'Connected'`), 're-enabled before clearing credentials')
sendingForbidden = true
database.enqueueChannelResult({ channel: 'telegram', eventId: 'delivery-status-fixture',
conversationId: '123456:700001', recipientId: '700001', status: 'completed', output: 'Synthetic saved reply' })
pollingConflict = true
await wait(() => run(`document.querySelector('.channel-settings-card [role="alert"]')?.textContent.includes('polling conflict')`), 'conflict before saved reply retry')
pollingConflict = false
await click('button.primary-button')
await wait(() => run(`document.querySelector('.channel-settings-card .capability-card__header > span')?.textContent === 'Connected' && document.querySelector('.channel-settings-card [role="alert"]')?.textContent.includes('Telegram sending failed:')`), 'sending failure visible while polling stays connected')
assert.equal(database.listUndeliveredChannelResults('telegram').find(entry => entry.message.eventId === 'delivery-status-fixture')?.state, 'terminal')
const visibleFailure = await run<string>(`document.querySelector('.channel-settings-card [role="alert"]').textContent`)
assert(visibleFailure.includes('forbidden'))
assert(!visibleFailure.includes(token) && !visibleFailure.includes('api.telegram.org'))
await fill('input[aria-label="Telegram Bot Token"]', 'unsaved-delivery-token')
await fill('textarea[aria-label="Telegram allowed sender IDs"]', '700001\n700004')
const pollsBefore = calls.filter(call => call.method === 'getUpdates').length
await wait(() => calls.filter(call => call.method === 'getUpdates').length >= pollsBefore + 2, 'healthy polls after delivery failure')
assert.equal(await run(`document.querySelector('.channel-settings-card [role="alert"]').textContent`), visibleFailure)
assert.equal(await run(`document.querySelector('input[aria-label="Telegram Bot Token"]').value`), 'unsaved-delivery-token')
assert.equal(await run(`document.querySelector('textarea[aria-label="Telegram allowed sender IDs"]').value`), '700001\n700004')
await win.reload()
await wait(() => run(`document.querySelector('.channel-settings-card [role="alert"]')?.textContent.includes('Telegram sending failed:')`), 'delivery error restored from snapshot')
sendingForbidden = false
const projectBeforeClear = database.getProject(conversation.projectId!)
const historyBeforeClear = database.getConversation(conversation.id)
await click('#channel-settings-panel-telegram input[type="checkbox"]:not([role="switch"])')
@@ -238,6 +264,7 @@ void app.whenReady().then(async () => {
console.log(JSON.stringify({ telegramSmoke: 'passed', uiConfigSaveTest: true, productionPreloadIpc: true,
productionExecutor: true, desktopConversationPersisted: true, reloadRestored: true,
disableCancelsModel: true, liveStatusPreservesDrafts: true, unchangedSaveRecoversConflict: true, clearTokenDisablesAndPreservesHistory: true,
sendingFailureRetainsPollingStateAndDrafts: true,
modelCalls: payloads.length, telegramReplies: replies.length,
pollsAborted, externalNetworkCalls: 0 }))
} finally {