Keep remote startup cancellable without blocking other sessions and surface local OpenCode submission failures promptly. Preserve supervisor project isolation, pause behavior and valid story destinations. Restore generated-image channel delivery, Telegram reconnection on unchanged Save and DingTalk business-error retries. Preserve virtualized message disclosures using sparse state and release stale supervisor UI requests. Add production-path regressions, Electron checks, isolated SQLite validation and audit evidence. Fix the SDK cleanup test fixture lifetime under garbage collection without weakening its assertions. Validation: 6136 tests passed, 87 skipped; typecheck, lint and diff checks passed. Seven real text-model calls, current-source Linux x64 Host scenarios and large-history Electron comparisons completed. Release note: Fix stuck runtime requests, inconsistent review results and channel delivery failures, and retain expanded details while browsing long conversations. Remote runtime fixes require an Agent build containing these changes.
129 lines
8.4 KiB
JavaScript
129 lines
8.4 KiB
JavaScript
import { app, BrowserWindow } from 'electron'
|
|
import assert from 'node:assert/strict'
|
|
import { join } from 'node:path'
|
|
import process from 'node:process'
|
|
import console from 'node:console'
|
|
|
|
app.setPath('userData', join(process.env.GB_EXPANSION_DIRECTORY, 'profile'))
|
|
app.commandLine.appendSwitch('force-device-scale-factor', '1')
|
|
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) })
|
|
const evaluate = source => win.webContents.executeJavaScript(source)
|
|
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(); })`)
|
|
const frames = () => evaluate('new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve(true))))')
|
|
const click = async selector => {
|
|
await evaluate(`document.querySelector(${JSON.stringify(selector)}).scrollIntoView({block:'center'})`)
|
|
await frames()
|
|
const point = await evaluate(`(() => { const e = document.querySelector(${JSON.stringify(selector)}), r = e.getBoundingClientRect();
|
|
const x = r.x + r.width / 2, y = r.y + r.height / 2;
|
|
if (!e.contains(document.elementFromPoint(x,y))) throw new Error('Obscured ' + ${JSON.stringify(selector)});
|
|
return {x: Math.round(x), y: Math.round(y)}; })()`)
|
|
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]) {
|
|
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)')
|
|
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)) {
|
|
window.expansionNavigate(index);
|
|
await new Promise((resolve, reject) => {
|
|
const deadline = performance.now() + 5000;
|
|
const check = () => {
|
|
const row = document.querySelector('[data-message-id=m' + index + ']');
|
|
if (row?.contains(document.activeElement) && document.activeElement.matches('article')) resolve();
|
|
else if (performance.now() > deadline) reject(new Error('Sweep navigation timeout'));
|
|
else requestAnimationFrame(check);
|
|
}; requestAnimationFrame(check);
|
|
});
|
|
const rows = [...document.querySelectorAll('.message-window-row')];
|
|
maxRows = Math.max(maxRows, rows.length); rows.forEach(row => visited.add(row.dataset.messageId));
|
|
}
|
|
return { visited: visited.size, entries: window.expansionEntryCount(), elapsedMs: performance.now() - start, maxRows };
|
|
})()`)
|
|
assert.ok(untouched.visited >= 1000, JSON.stringify(untouched))
|
|
assert.ok(untouched.maxRows <= 70, JSON.stringify(untouched))
|
|
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 = []
|
|
let maxRows = 0
|
|
let maxNodes = 0
|
|
const visit = async index => {
|
|
const elapsed = await evaluate(`new Promise((resolve, reject) => {
|
|
const start = performance.now(); window.expansionNavigate(${index});
|
|
const check = () => {
|
|
const row = document.querySelector('[data-message-id=m${index}]');
|
|
if (row?.contains(document.activeElement) && document.activeElement.matches('article')) {
|
|
requestAnimationFrame(() => requestAnimationFrame(() => resolve(performance.now() - start)));
|
|
} else if (performance.now() - start > 10000) reject(new Error('Navigation timeout ${index}'));
|
|
else requestAnimationFrame(check);
|
|
}; requestAnimationFrame(check);
|
|
})`)
|
|
times.push(elapsed)
|
|
const size = await evaluate('({rows:document.querySelectorAll(".message-window-row").length,nodes:document.querySelectorAll("*").length})')
|
|
maxRows = Math.max(maxRows, size.rows)
|
|
maxNodes = Math.max(maxNodes, size.nodes)
|
|
assert.ok(size.rows <= 70, JSON.stringify(size))
|
|
}
|
|
for (let n = 0; n < 13; n++) {
|
|
const index = n * Math.floor(count / 15)
|
|
await visit(index)
|
|
const row = `[data-message-id=m${index}]`
|
|
await click(row + ' .tool-execution > summary')
|
|
await waitFor(`document.querySelector('${row} .tool-execution').open`)
|
|
await click(row + ' .tool-execution__input > summary')
|
|
await waitFor(`document.querySelector('${row} .tool-execution__input').open`)
|
|
await click(row + ' .message-reasoning > summary')
|
|
await waitFor(`document.querySelector('${row} .message-reasoning').open`)
|
|
}
|
|
assert.equal(await evaluate('!!document.querySelector("[data-message-id=m0]")'), false, 'original row must actually unmount')
|
|
await visit(0)
|
|
const restored = await evaluate('[...document.querySelectorAll("[data-message-id=m0] details")].map(e=>e.open)')
|
|
assert.deepEqual(restored, process.env.GB_HISTORY_SOURCE ? [false, false, false] : [true, true, true])
|
|
// Native keyboard toggling still closes a restored disclosure.
|
|
const summary = '[data-message-id=m0] .message-reasoning > summary'
|
|
await evaluate(`document.querySelector('${summary}').focus()`)
|
|
win.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Space' })
|
|
win.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Space' })
|
|
await waitFor(`document.querySelector('[data-message-id=m0] .message-reasoning').open === ${Boolean(process.env.GB_HISTORY_SOURCE)}`)
|
|
assert.equal(await evaluate('document.querySelector("textarea").value'), 'Preserved draft')
|
|
if (!process.env.GB_EXPANSION_DENSE_SOURCE && !process.env.GB_HISTORY_SOURCE) await waitFor('window.expansionEntryCount() === 38')
|
|
const changedEntries = await evaluate('window.expansionEntryCount()')
|
|
await evaluate('window.expansionDeleteMessages()')
|
|
await waitFor('document.querySelectorAll(".message-window-row").length === 0')
|
|
await frames()
|
|
if (!process.env.GB_EXPANSION_DENSE_SOURCE && !process.env.GB_HISTORY_SOURCE) await waitFor('window.expansionEntryCount() === 0')
|
|
const afterDeletion = await evaluate('window.expansionEntryCount()')
|
|
if (!process.env.GB_EXPANSION_DENSE_SOURCE && !process.env.GB_HISTORY_SOURCE) assert.equal(afterDeletion, 0)
|
|
times.sort((a,b)=>a-b)
|
|
console.log('MessageExpansion ' + JSON.stringify({ version: process.env.GB_HISTORY_SOURCE ? 'baseline' : process.env.GB_EXPANSION_DENSE_SOURCE ? 'dense' : 'sparse', count,
|
|
samples: times.length, navigationTwoFramesP95Ms: times[Math.ceil(times.length * .95)-1],
|
|
navigationTwoFramesMedianMs: times[Math.floor(times.length/2)], maxRows, maxNodes, restored, untouched, changedEntries, afterDeletion,
|
|
electron: process.versions.electron, platform: process.platform, arch: process.arch }))
|
|
}
|
|
await win.loadURL(process.env.GB_EXPANSION_URL + '?supervisor=1')
|
|
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")')
|
|
await evaluate('window.finishSource()')
|
|
await frames()
|
|
const stuck = await evaluate('document.querySelector(".supervisor-workspace__detail .link-button").disabled')
|
|
assert.equal(stuck, Boolean(process.env.GB_HISTORY_SOURCE))
|
|
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, [])
|
|
win.destroy()
|
|
app.quit()
|
|
}).catch(error => { console.error(error); app.exit(1) })
|