Files
ycode/scripts/verify-task-window-stream.cjs
T

1009 lines
42 KiB
JavaScript

// A01 acceptance: a real task window must own the complete renderer send/stream
// path, including completion, user cancellation, persistence after the sending
// window closes, reopening an active run through the main task menu, and
// dual-window rendering/stop synchronization on the same selected task.
//
// This harness runs the production YCode main/preload/renderer bundle against
// a real OMP executable and a controlled OpenAI-compatible SSE server bound to
// 127.0.0.1. It never calls window.ycode.chat.send. Every turn starts through
// the visible task-window composer.
const fs = require('node:fs/promises')
const path = require('node:path')
const assert = require('node:assert/strict')
const TASK_ID = 'task-window-stream-session'
const MODEL_ID = 'local-task-window-stream'
const DONE_PROMPT = 'TASK WINDOW DONE REQUEST'
const DONE_PARTIAL = 'TASKWINDOWDONEPARTIAL'
const DONE_FINAL = 'TASKWINDOWDONEFINAL'
const CANCEL_PROMPT = 'TASK WINDOW CANCEL REQUEST'
const CANCEL_PARTIAL = 'TASKWINDOWCANCELPARTIAL'
const CLOSE_PROMPT = 'TASK WINDOW CLOSE REQUEST'
const CLOSE_PARTIAL = 'TASKWINDOWCLOSEPARTIAL'
const CLOSE_FINAL = 'TASKWINDOWCLOSEFINAL'
const REOPEN_PROMPT = 'TASK WINDOW REOPEN REQUEST'
const REOPEN_PARTIAL = 'TASKWINDOWREOPENPARTIAL'
const REOPEN_FINAL = 'TASKWINDOWREOPENFINAL'
const DUAL_PROMPT = 'TASK WINDOW DUAL REQUEST'
const DUAL_PARTIAL = 'TASKWINDOWDUALPARTIAL'
if (!process.versions.electron) {
;(async () => {
const temporaryRoot = path.resolve('.tmp')
await fs.mkdir(temporaryRoot, { recursive: true })
const directory = await fs.mkdtemp(path.join(temporaryRoot, 'task-window-stream-'))
const data = path.join(directory, 'data')
const home = path.join(directory, 'home')
const workspace = path.join(directory, 'workspace')
await Promise.all([
fs.mkdir(data, { recursive: true }),
fs.mkdir(home, { recursive: true }),
fs.mkdir(workspace, { recursive: true })
])
const ompPath = process.env.YCODE_OMP_PATH ||
path.join(process.env.LOCALAPPDATA || '', 'Ygent', 'omp.exe')
await fs.access(ompPath, require('node:fs').constants.X_OK)
const now = Date.now()
const session = {
id: TASK_ID,
title: 'Task Window Stream Acceptance',
cwd: workspace,
modelId: MODEL_ID,
createdAt: now - 2,
updatedAt: now - 1,
messages: [
{
id: 'task-window-fixture-user',
role: 'user',
content: 'Fixture history',
requestId: 'task-window-fixture-run',
createdAt: now - 2,
status: 'done'
},
{
id: 'task-window-fixture-assistant',
role: 'assistant',
content: 'Fixture response',
requestId: 'task-window-fixture-run',
createdAt: now - 1,
status: 'done',
turnDurationMs: 1,
tools: [],
parts: []
}
]
}
await fs.writeFile(path.join(data, 'sessions.json'), JSON.stringify({ sessions: [session] }, null, 2))
const env = {
...process.env,
YCODE_QA_DIRECTORY: directory,
YCODE_USER_DATA_DIR: data,
YCODE_OMP_HOME: home,
YCODE_QA_REAL_OMP: ompPath,
YCODE_DISABLE_GPU: '1',
YCODE_QA_OFFSCREEN: '1'
}
delete env.ELECTRON_RUN_AS_NODE
delete env.ELECTRON_RENDERER_URL
const child = require('node:child_process').spawn(
require('electron'),
['--no-sandbox', '--disable-gpu-sandbox', __filename],
{ env, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }
)
let output = ''
child.stdout.on('data', (bytes) => {
output += bytes
if (bytes.toString().includes('TASK_WINDOW_STREAM_')) process.stdout.write(bytes)
})
child.stderr.on('data', (bytes) => { output += bytes })
const timer = setTimeout(() => child.kill(), 360000)
child.once('exit', async (code) => {
clearTimeout(timer)
await fs.writeFile(path.join(directory, 'process.log'), output)
console.log(JSON.stringify({ directory, exitCode: code }))
if (code) console.error(output.slice(-12000))
process.exitCode = code ?? 1
})
})().catch((error) => {
console.error(error)
process.exitCode = 1
})
} else {
const { app, BrowserWindow } = require('electron')
const { createServer } = require('node:http')
const { pathToFileURL } = require('node:url')
const directory = process.env.YCODE_QA_DIRECTORY
const relative = path.relative(path.resolve('.tmp'), directory || '')
assert.ok(
relative.startsWith('task-window-stream-') &&
!relative.startsWith('..') &&
!path.isAbsolute(relative),
'isolated QA directory must stay under .tmp/task-window-stream-*'
)
process.env.YCODE_OMP_PATH = process.env.YCODE_QA_REAL_OMP
app.setPath('userData', path.join(directory, 'data'))
app.setPath('sessionData', path.join(directory, 'data'))
app.setPath('home', path.join(directory, 'home'))
const checks = []
const snapshots = {}
const rendererErrors = []
const requestLog = []
const pendingTimers = new Set()
let main
let taskWindow
let server
let phase = 'boot'
let requestCount = 0
let doneTerminalSentAt = 0
let cancelStreamClosed = false
let closeStreamStarted = false
let closeTerminalSentAt = 0
let taskWindowClosedAt = 0
let reopenStreamStarted = false
let reopenTerminalSentAt = 0
let reopenFinalRelease
const reopenFinalGate = new Promise((resolve) => { reopenFinalRelease = resolve })
let dualStreamClosed = false
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
async function read(win, source) {
assert.ok(win && !win.isDestroyed(), 'renderer window must be alive while reading')
let timer
try {
return await Promise.race([
win.webContents.executeJavaScript(source),
new Promise((_, reject) => {
timer = setTimeout(() => reject(Error('Renderer timeout: ' + source.slice(0, 160))), 20000)
})
])
} finally {
clearTimeout(timer)
}
}
async function wait(value, label, win, attempts = 750) {
for (let index = 0; index < attempts; index++) {
const result = await read(win, value)
if (result) return result
await pause(40)
}
throw Error('Timeout: ' + label)
}
async function waitValue(readValue, label, attempts = 1000) {
for (let index = 0; index < attempts; index++) {
const result = await readValue()
if (result) return result
await pause(40)
}
throw Error('Timeout: ' + label)
}
async function step(label) {
phase = label
await fs.writeFile(path.join(directory, 'phase.txt'), label)
}
function check(label, actual, expected) {
const pass = typeof expected === 'function'
? expected(actual)
: JSON.stringify(actual) === JSON.stringify(expected)
checks.push({
label,
pass,
actual,
expected: typeof expected === 'function' ? expected.toString() : expected
})
}
function captureRendererErrors(win) {
win.webContents.on('console-message', (eventOrLevel, maybeLevel, maybeMessage) => {
const level = typeof eventOrLevel === 'object' ? eventOrLevel.level : maybeLevel
const message = typeof eventOrLevel === 'object' ? eventOrLevel.message : maybeMessage
if (level === 'error' || Number(level) >= 3) rendererErrors.push({ windowId: win.id, message })
})
}
async function findTaskWindow() {
for (let index = 0; index < 400; index++) {
const candidate = BrowserWindow.getAllWindows().find((win) => {
if (win === main || win.isDestroyed()) return false
try {
return new URL(win.webContents.getURL()).searchParams.get('window') === 'task'
} catch {
return false
}
})
if (candidate) return candidate
await pause(40)
}
return undefined
}
async function ensureTaskSelected(win, label) {
await wait(
`!!document.querySelector('[data-session-id="${TASK_ID}"]')`,
`${label} task row`,
win
)
const selected = await read(win, `document.querySelector('[data-session-id="${TASK_ID}"]')?.classList.contains('selected')`)
if (!selected) {
await read(win, `(() => {
const row = document.querySelector('[data-session-id="${TASK_ID}"]')
if (!row) return false
row.click()
return true
})()`)
}
await wait(
`document.querySelector('[data-session-id="${TASK_ID}"]')?.classList.contains('selected')`,
`${label} selected task`,
win
)
await wait('!!document.querySelector(".wb-composer__input")', `${label} task composer`, win)
}
async function openTaskWindowFromTaskMenu() {
await ensureTaskSelected(main, 'main window')
await wait(
`!!document.querySelector('[data-session-id="${TASK_ID}"] .agent-card-more-button')`,
'task row menu button',
main
)
await read(main, `(() => {
const row = document.querySelector('[data-session-id="${TASK_ID}"]')
const button = row?.querySelector('.agent-card-more-button')
if (!row || !button) return false
button.click()
return true
})()`)
await wait('!!document.querySelector("[data-task-menu-id=openWindow]")', 'open-window task menu item', main)
const menuLabel = await read(
main,
'document.querySelector("[data-task-menu-id=openWindow] .cb-context-menu-item-label")?.textContent'
)
await read(main, `(() => {
const item = document.querySelector('[data-task-menu-id="openWindow"]')
if (!item) return false
item.click()
return true
})()`)
const opened = await findTaskWindow()
assert.ok(opened, 'independent task window')
captureRendererErrors(opened)
opened.once('closed', () => { taskWindowClosedAt = Date.now() })
await wait('!!window.ycode && !!document.querySelector(".wb-composer__input")', 'task-window renderer', opened)
await wait(
`new URLSearchParams(location.search).get('taskId') === ${JSON.stringify(TASK_ID)}`,
'task-window query identity',
opened
)
await ensureTaskSelected(opened, 'task window')
return { window: opened, menuLabel }
}
async function installChatEventProbe(win) {
const installed = await read(win, `(() => {
window.__a01ChatEventProbe?.dispose?.()
const events = []
const stopStarted = window.ycode.chat.onStarted((run) => {
if (run.sessionId === ${JSON.stringify(TASK_ID)}) events.push({
runId: run.runId, type: 'run-started', sequence: run.sequence,
messageId: run.assistantMessageId
})
})
const stopEvents = window.ycode.chat.onEvent((event) => {
if (event.sessionId !== ${JSON.stringify(TASK_ID)}) return
events.push({
runId: event.runId,
type: event.type,
sequence: event.sequence,
messageId: event.messageId,
delta: event.delta,
content: event.content,
message: event.message
})
})
window.__a01ChatEventProbe = { events, dispose: () => { stopStarted(); stopEvents() } }
return true
})()`)
assert.equal(installed, true, 'chat event probe must install through the production preload API')
}
async function readChatEventProbe(win) {
return read(win, `window.__a01ChatEventProbe?.events ?? []`)
}
function eventRunDiagnostics(events) {
const runStartedTypes = new Set(['started', 'run-started'])
const runs = new Map()
for (const event of events) {
if (!runs.has(event.runId)) runs.set(event.runId, [])
runs.get(event.runId).push(event)
}
return [...runs.entries()].map(([runId, runEvents]) => {
const sequences = runEvents.map((event) => event.sequence)
return {
runId,
types: runEvents.map((event) => event.type),
sequences,
allNonNegativeIntegers: sequences.every((value) => Number.isInteger(value) && value >= 0),
strictlyIncreasing: sequences.every((value, index) => index === 0 || value > sequences[index - 1]),
unique: new Set(sequences).size === sequences.length,
startsWithRunStarted: runStartedTypes.has(runEvents[0]?.type)
}
})
}
async function markerCount(win, marker) {
return read(win, `(() => {
const text = document.querySelector('.chat-scroll')?.innerText || document.body.innerText || ''
return text.split(${JSON.stringify(marker)}).length - 1
})()`)
}
function rememberTimer(timer) {
pendingTimers.add(timer)
return timer
}
function clearRememberedTimer(timer) {
clearTimeout(timer)
pendingTimers.delete(timer)
}
function sseChunk(body, payload) {
return `data: ${JSON.stringify({
id: 'local-task-window-stream',
object: 'chat.completion.chunk',
created: 1,
model: body.model,
...payload
})}\n\n`
}
function finishSse(res, body, marker) {
if (res.destroyed || res.writableEnded) return
res.write(sseChunk(body, {
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
usage: { prompt_tokens: 16, completion_tokens: 8, total_tokens: 24 }
}))
res.write('data: [DONE]\n\n')
res.end()
return marker
}
async function readPersistedTurn(marker) {
let parsed
try {
parsed = JSON.parse(await fs.readFile(path.join(directory, 'data', 'sessions.json'), 'utf8'))
} catch (error) {
if (error && error.code === 'ENOENT') return null
throw error
}
const session = parsed.sessions?.find((item) => item.id === TASK_ID)
if (!session) return null
const index = session.messages?.findIndex((message) => message.role === 'user' && message.content === marker) ?? -1
if (index < 0) return null
return {
user: session.messages[index],
assistant: session.messages.slice(index + 1).find((message) => message.role === 'assistant') ?? null
}
}
async function waitPersistedStatus(marker, status) {
return waitValue(async () => {
const turn = await readPersistedTurn(marker)
return turn?.assistant?.status === status ? turn : null
}, `persisted ${status}: ${marker}`)
}
async function fillComposer(win, text) {
const filled = await read(win, `(() => {
const input = document.querySelector('.wb-composer__input')
if (!(input instanceof HTMLTextAreaElement)) return false
const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value').set
setter.call(input, ${JSON.stringify(text)})
input.dispatchEvent(new Event('input', { bubbles: true }))
input.focus()
return true
})()`)
assert.equal(filled, true, 'task-window composer input must exist')
await wait(
`document.querySelector('.wb-composer__input')?.value === ${JSON.stringify(text)}`,
'composer value',
win
)
await wait(
`(() => {
const button = document.querySelector('.cr-send-button:not(.cr-send-button--stop)')
return button && !button.disabled
})()`,
'enabled task-window send button',
win
)
}
async function clickComposerSend(win) {
const clicked = await read(win, `(() => {
const button = document.querySelector('.cr-send-button:not(.cr-send-button--stop)')
if (!button || button.disabled) return false
button.click()
return true
})()`)
assert.equal(clicked, true, 'task-window send button must be clickable')
await wait('!!document.querySelector(".cr-send-button--stop")', 'task-window running stop button', win)
}
async function visibleState(win) {
return read(win, `(() => {
const body = document.body.innerText || ''
const latestAssistant = [...document.querySelectorAll('.msg--assistant')].at(-1)
const latestAssistantText = latestAssistant?.innerText || ''
const stopButton = document.querySelector('.cr-send-button--stop')
return {
body,
donePartial: latestAssistantText.includes(${JSON.stringify(DONE_PARTIAL)}),
doneFinal: latestAssistantText.includes(${JSON.stringify(DONE_FINAL)}),
cancelPartial: latestAssistantText.includes(${JSON.stringify(CANCEL_PARTIAL)}),
closePartial: latestAssistantText.includes(${JSON.stringify(CLOSE_PARTIAL)}),
reopenPartial: latestAssistantText.includes(${JSON.stringify(REOPEN_PARTIAL)}),
reopenFinal: latestAssistantText.includes(${JSON.stringify(REOPEN_FINAL)}),
dualPartial: latestAssistantText.includes(${JSON.stringify(DUAL_PARTIAL)}),
completed: latestAssistantText.includes('已完成'),
cancelled: latestAssistantText.includes('已取消'),
stop: !!stopButton,
stopEnabled: !!stopButton && !stopButton.disabled,
send: !!document.querySelector('.cr-send-button:not(.cr-send-button--stop)')
}
})()`)
}
;(async () => {
server = createServer(async (req, res) => {
requestCount += 1
if (req.method === 'OPTIONS') {
res.writeHead(204, {
'access-control-allow-origin': '*',
'access-control-allow-headers': '*',
'access-control-allow-methods': '*'
})
res.end()
return
}
let raw = ''
for await (const part of req) raw += part
let body
try {
body = JSON.parse(raw || '{}')
} catch {
res.writeHead(400, { 'content-type': 'application/json' })
res.end(JSON.stringify({ error: { message: 'QA upstream expected JSON' } }))
return
}
// Later requests carry prior turns in messages[]. Select the marker with
// the greatest source offset so history cannot reclassify the new turn.
const marker = [
['done', raw.lastIndexOf(DONE_PROMPT)],
['cancel', raw.lastIndexOf(CANCEL_PROMPT)],
['close', raw.lastIndexOf(CLOSE_PROMPT)],
['reopen', raw.lastIndexOf(REOPEN_PROMPT)],
['dual', raw.lastIndexOf(DUAL_PROMPT)]
]
.sort((left, right) => right[1] - left[1])
.find((entry) => entry[1] >= 0)?.[0] ?? 'unknown'
requestLog.push({
marker,
method: req.method,
url: req.url,
stream: body.stream === true,
model: body.model,
bodyBytes: Buffer.byteLength(raw)
})
if (!body.stream || marker === 'unknown') {
res.writeHead(400, { 'content-type': 'application/json' })
res.end(JSON.stringify({ error: { message: `Unexpected QA request: ${marker}` } }))
return
}
res.writeHead(200, {
'content-type': 'text/event-stream',
'cache-control': 'no-cache',
connection: 'keep-alive'
})
if (marker === 'done') {
res.write(sseChunk(body, {
choices: [{ index: 0, delta: { role: 'assistant', content: DONE_PARTIAL }, finish_reason: null }]
}))
const timer = rememberTimer(setTimeout(() => {
pendingTimers.delete(timer)
if (res.destroyed || res.writableEnded) return
res.write(sseChunk(body, {
choices: [{ index: 0, delta: { content: DONE_FINAL }, finish_reason: null }]
}))
finishSse(res, body, 'done')
doneTerminalSentAt = Date.now()
}, 900))
res.on('close', () => clearRememberedTimer(timer))
return
}
if (marker === 'cancel') {
res.write(sseChunk(body, {
choices: [{ index: 0, delta: { role: 'assistant', content: CANCEL_PARTIAL }, finish_reason: null }]
}))
const keepalive = setInterval(() => {
if (!res.destroyed) res.write(': keepalive\n\n')
}, 500)
res.on('close', () => {
clearInterval(keepalive)
cancelStreamClosed = true
})
return
}
if (marker === 'reopen') {
res.write(sseChunk(body, {
choices: [{ index: 0, delta: { role: 'assistant', content: REOPEN_PARTIAL }, finish_reason: null }]
}))
reopenStreamStarted = true
res.on('close', () => { reopenFinalRelease() })
await reopenFinalGate
if (res.destroyed || res.writableEnded) return
res.write(sseChunk(body, {
choices: [{ index: 0, delta: { content: REOPEN_FINAL }, finish_reason: null }]
}))
finishSse(res, body, 'reopen')
reopenTerminalSentAt = Date.now()
return
}
if (marker === 'dual') {
res.write(sseChunk(body, {
choices: [{ index: 0, delta: { role: 'assistant', content: DUAL_PARTIAL }, finish_reason: null }]
}))
const keepalive = setInterval(() => {
if (!res.destroyed) res.write(': keepalive\n\n')
}, 500)
res.on('close', () => {
clearInterval(keepalive)
dualStreamClosed = true
})
return
}
res.write(sseChunk(body, {
choices: [{ index: 0, delta: { role: 'assistant', content: CLOSE_PARTIAL }, finish_reason: null }]
}))
closeStreamStarted = true
const timer = rememberTimer(setTimeout(() => {
pendingTimers.delete(timer)
if (res.destroyed || res.writableEnded) return
res.write(sseChunk(body, {
choices: [{ index: 0, delta: { content: CLOSE_FINAL }, finish_reason: null }]
}))
finishSse(res, body, 'close')
closeTerminalSentAt = Date.now()
}, 5000))
res.on('close', () => clearRememberedTimer(timer))
})
// Import the main bundle before Electron has another chance to reach ready,
// because the bundle applies YCODE_DISABLE_GPU synchronously at module load.
server.listen(0, '127.0.0.1')
await import(pathToFileURL(path.resolve('out/main/index.mjs')).href)
await app.whenReady()
await new Promise((resolve) => server.listening ? resolve() : server.once('listening', resolve))
const baseUrl = `http://127.0.0.1:${server.address().port}/v1`
assert.equal(new URL(baseUrl).hostname, '127.0.0.1')
for (let index = 0; index < 500; index++) {
main = BrowserWindow.getAllWindows().find((win) => win.getTitle() === 'YCode')
if (main && !main.webContents.isLoading()) break
await pause(40)
}
assert.ok(main, 'main YCode window')
captureRendererErrors(main)
main.setSize(1336, 902)
main.showInactive()
await wait('!!window.ycode && !!document.querySelector(".chat-pane")', 'main renderer', main)
await installChatEventProbe(main)
await step('configure-real-omp-local-upstream')
const appInfo = await read(main, 'window.ycode.app.info()')
check('production app detects the requested real OMP executable', appInfo.omp.available, true)
check('production app resolves OMP to the isolated fixture executable',
path.resolve(appInfo.omp.exePath || ''), path.resolve(process.env.YCODE_QA_REAL_OMP))
const savedModel = await read(main, `window.ycode.models.save(${JSON.stringify({
id: MODEL_ID,
name: 'Local Task Window Stream',
providerId: 'custom',
model: 'task-window-stream-model',
baseUrl,
apiKey: 'LOCAL_TEST_KEY',
supportsToolCall: true,
maxInputTokens: 64000,
maxOutputTokens: 4096
})})`)
check(
'model is saved through the real model IPC',
savedModel?.models?.some((model) => model.id === MODEL_ID),
true
)
await read(main, `window.ycode.models.select(${JSON.stringify(MODEL_ID)})`)
check(
'controlled upstream is localhost-only',
new URL(baseUrl).hostname === '127.0.0.1' && /^http:\/\/127\.0\.0\.1:\d+\/v1$/.test(baseUrl),
true
)
await step('open-task-window-from-task-menu')
const openedTask = await openTaskWindowFromTaskMenu()
taskWindow = openedTask.window
const menuLabel = openedTask.menuLabel
check('task menu exposes the independent-window action', menuLabel, '在独立窗口中打开')
await wait('document.body.innerText.includes("Fixture response")', 'fixture task history', taskWindow)
await installChatEventProbe(taskWindow)
snapshots.taskWindow = await read(taskWindow, `({
url: location.href,
taskId: new URLSearchParams(location.search).get('taskId'),
title: document.querySelector('.workbuddy-topbar-title')?.textContent,
hasFixtureHistory: document.body.innerText.includes('Fixture response')
})`)
check('task window carries the real task id', snapshots.taskWindow.taskId, TASK_ID)
check('task window selected the requested task', snapshots.taskWindow.hasFixtureHistory, true)
check('task window exposes the real composer', await read(taskWindow, `!!document.querySelector('.wb-composer__input')`), true)
await step('task-window-stream-to-completion')
await fillComposer(taskWindow, DONE_PROMPT)
await clickComposerSend(taskWindow)
await wait(
`[...document.querySelectorAll('.msg--assistant')].at(-1)?.innerText.includes(${JSON.stringify(DONE_PARTIAL)})`,
'streaming partial body',
taskWindow
)
const streamingComplete = await visibleState(taskWindow)
snapshots.streamingComplete = {
donePartial: streamingComplete.donePartial,
doneFinal: streamingComplete.doneFinal,
stop: streamingComplete.stop,
send: streamingComplete.send
}
check('task window receives the first SSE chunk while turn is running', streamingComplete.donePartial, true)
check('final chunk is still absent during the held stream', streamingComplete.doneFinal, false)
check('task window exposes stop while streaming', streamingComplete.stop, true)
await wait(
`[...document.querySelectorAll('.msg--assistant')].at(-1)?.innerText.includes("TASKWINDOWDONEFINAL") && !document.querySelector(".cr-send-button--stop")`,
'completed stream and restored send control',
taskWindow
)
await waitValue(() => doneTerminalSentAt > 0, 'first controlled terminal chunk')
const completedState = await visibleState(taskWindow)
snapshots.completedState = {
doneFinal: completedState.doneFinal,
completed: completedState.completed,
stop: completedState.stop,
send: completedState.send
}
check('task window renders the completed stream body', completedState.doneFinal, true)
check('task window replaces stop with send after completion', [completedState.stop, completedState.send], [false, true])
check('task window reports the completed terminal state', completedState.completed, true)
const doneDisk = await waitPersistedStatus(DONE_PROMPT, 'done')
check('completed turn is persisted with assistant status done', doneDisk.assistant.status, 'done')
check('persisted completed assistant contains the terminal text', doneDisk.assistant.content.includes(DONE_FINAL), true)
await step('stop-task-window-stream-and-persist-cancelled')
await fillComposer(taskWindow, CANCEL_PROMPT)
await clickComposerSend(taskWindow)
await wait(
`[...document.querySelectorAll('.msg--assistant')].at(-1)?.innerText.includes(${JSON.stringify(CANCEL_PARTIAL)})`,
'cancellable streaming body',
taskWindow
)
const streamingCancel = await visibleState(taskWindow)
check('second task-window stream renders before cancellation', streamingCancel.cancelPartial, true)
check('second task-window stream exposes stop', streamingCancel.stop, true)
const stopped = await read(taskWindow, `(() => {
const button = document.querySelector('.cr-send-button--stop')
if (!button) return false
button.click()
return true
})()`)
check('stop is triggered from the task-window composer', stopped, true)
await wait(
`!document.querySelector(".cr-send-button--stop") && [...document.querySelectorAll('.msg--assistant')].at(-1)?.innerText.includes("已取消")`,
'cancelled task-window state',
taskWindow
)
await waitValue(() => cancelStreamClosed, 'cancelled upstream connection close')
const cancelledState = await visibleState(taskWindow)
snapshots.cancelledState = {
cancelled: cancelledState.cancelled,
stop: cancelledState.stop,
send: cancelledState.send
}
check('task window reports the cancelled terminal state', cancelledState.cancelled, true)
check('task window replaces stop with send after cancellation', [cancelledState.stop, cancelledState.send], [false, true])
const cancelledDisk = await waitPersistedStatus(CANCEL_PROMPT, 'cancelled')
check('cancelled turn is persisted with assistant status cancelled', cancelledDisk.assistant.status, 'cancelled')
check('cancelled persistence retains the received partial text',
cancelledDisk.assistant.content.includes(CANCEL_PARTIAL), true)
check('cancelling stops the controlled upstream stream', cancelStreamClosed, true)
await step('close-sending-window-and-persist-background-terminal')
await fillComposer(taskWindow, CLOSE_PROMPT)
await clickComposerSend(taskWindow)
await wait(
`[...document.querySelectorAll('.msg--assistant')].at(-1)?.innerText.includes(${JSON.stringify(CLOSE_PARTIAL)})`,
'close-window streaming body',
taskWindow
)
const beforeCloseDisk = await readPersistedTurn(CLOSE_PROMPT)
snapshots.beforeWindowClose = beforeCloseDisk && {
assistantId: beforeCloseDisk.assistant?.id,
status: beforeCloseDisk.assistant?.status,
content: beforeCloseDisk.assistant?.content
}
check('third turn is persisted as streaming before the window closes',
beforeCloseDisk?.assistant?.status, 'streaming')
check('third upstream stream started before the window closes', closeStreamStarted, true)
check('third upstream stream is still running before window close', closeTerminalSentAt, 0)
taskWindow.close()
await waitValue(() => taskWindow.isDestroyed(), 'task-window destruction')
check('main application remains alive after the sending task window closes', main.isDestroyed(), false)
await waitValue(() => closeTerminalSentAt > taskWindowClosedAt, 'terminal SSE after task-window close')
const closeDisk = await waitPersistedStatus(CLOSE_PROMPT, 'done')
check('background turn terminates after its sending window closes', closeTerminalSentAt > taskWindowClosedAt, true)
check('background terminal is persisted with assistant status done', closeDisk.assistant.status, 'done')
check('background terminal persistence includes the final streamed text',
closeDisk.assistant.content.includes(CLOSE_FINAL), true)
await step('reopen-running-task-before-terminal')
taskWindow = undefined
const reopenSender = await openTaskWindowFromTaskMenu()
taskWindow = reopenSender.window
await wait('document.body.innerText.includes("Fixture response")', 'reopen sender history', taskWindow)
await fillComposer(taskWindow, REOPEN_PROMPT)
await clickComposerSend(taskWindow)
await wait(
`[...document.querySelectorAll('.msg--assistant')].at(-1)?.innerText.includes(${JSON.stringify(REOPEN_PARTIAL)})`,
'reopen sender partial',
taskWindow
)
await wait(
`[...document.querySelectorAll('.msg--assistant')].at(-1)?.innerText.includes(${JSON.stringify(REOPEN_PARTIAL)})&&!!document.querySelector(".cr-send-button--stop")`,
'main window before reopen sender closes',
main
)
const reopenBeforeClose = await waitPersistedStatus(REOPEN_PROMPT, 'streaming')
check('reopened-run sender is streaming before its window closes',
reopenBeforeClose?.assistant?.status, 'streaming')
const reopenSenderClosedAt = Date.now()
taskWindow.close()
await waitValue(() => taskWindow.isDestroyed(), 'reopen sender destruction')
check('reopened-run sender closed before any terminal event',
[reopenStreamStarted, reopenTerminalSentAt], [true, 0])
taskWindow = undefined
const reopenRequestedAt = Date.now()
const reopenedTask = await openTaskWindowFromTaskMenu()
taskWindow = reopenedTask.window
check('running task can be reopened through the main task menu', reopenedTask.menuLabel, '在独立窗口中打开')
await installChatEventProbe(taskWindow)
await wait('document.body.innerText.includes("Fixture response")', 'reopened task history', taskWindow)
await wait(
`[...document.querySelectorAll('.msg--assistant')].at(-1)?.innerText.includes(${JSON.stringify(REOPEN_PARTIAL)})`,
'reopened window active-run partial',
taskWindow
)
const reopenVisibleAt = Date.now()
await wait(
`[...document.querySelectorAll('.msg--assistant')].at(-1)?.innerText.includes(${JSON.stringify(REOPEN_PARTIAL)})`,
'main-window active-run partial',
main
)
const reopenedState = await visibleState(taskWindow)
const mainReopenState = await visibleState(main)
snapshots.reopenBeforeTerminal = {
menuLabel: reopenedTask.menuLabel,
mainPartialCount: await markerCount(main, REOPEN_PARTIAL),
taskPartialCount: await markerCount(taskWindow, REOPEN_PARTIAL),
mainFinalCount: await markerCount(main, REOPEN_FINAL),
taskFinalCount: await markerCount(taskWindow, REOPEN_FINAL),
mainStop: mainReopenState.stop,
mainStopEnabled: mainReopenState.stopEnabled,
taskStop: reopenedState.stop,
taskStopEnabled: reopenedState.stopEnabled,
reopenTerminalSentAt,
senderClosedToReopenRequestedMs: reopenRequestedAt - reopenSenderClosedAt,
reopenedAfterMs: reopenVisibleAt - reopenRequestedAt
}
check('reopened window shows the live partial before terminal',
[reopenedState.reopenPartial, reopenedState.reopenFinal], [true, false])
check('reopened window immediately exposes a usable stop control', reopenedState.stopEnabled, true)
check('reopened window obtained the run without waiting for the destroyed sender',
[reopenStreamStarted, reopenTerminalSentAt], [true, 0])
check('main window also shows the running partial before terminal',
[mainReopenState.reopenPartial, mainReopenState.reopenFinal, mainReopenState.stopEnabled], [true, false, true])
check('main and reopened task render the partial exactly once',
[snapshots.reopenBeforeTerminal.mainPartialCount, snapshots.reopenBeforeTerminal.taskPartialCount], [1, 1])
check('terminal text is absent while the reopened run is still gated',
[snapshots.reopenBeforeTerminal.mainFinalCount, snapshots.reopenBeforeTerminal.taskFinalCount], [0, 0])
reopenFinalRelease()
await wait(
`[...document.querySelectorAll('.msg--assistant')].at(-1)?.innerText.includes(${JSON.stringify(REOPEN_FINAL)})&&!document.querySelector(".cr-send-button--stop")`,
'reopened window terminal',
taskWindow
)
await wait(
`[...document.querySelectorAll('.msg--assistant')].at(-1)?.innerText.includes(${JSON.stringify(REOPEN_FINAL)})&&!document.querySelector(".cr-send-button--stop")`,
'main window terminal for reopened run',
main
)
const reopenedDoneState = await visibleState(taskWindow)
const mainReopenDoneState = await visibleState(main)
check('reopened window updates from partial to completed terminal',
[reopenedDoneState.reopenFinal, reopenedDoneState.completed, reopenedDoneState.stop], [true, true, false])
check('main window receives the same completed terminal exactly once',
[mainReopenDoneState.reopenFinal, mainReopenDoneState.completed, mainReopenDoneState.stop], [true, true, false])
check('main and reopened task render the final text exactly once',
[await markerCount(main, REOPEN_FINAL), await markerCount(taskWindow, REOPEN_FINAL)], [1, 1])
check('main and reopened task still render the partial exactly once after completion',
[await markerCount(main, REOPEN_PARTIAL), await markerCount(taskWindow, REOPEN_PARTIAL)], [1, 1])
const reopenedDisk = await waitPersistedStatus(REOPEN_PROMPT, 'done')
check('reopened run persists the terminal text', reopenedDisk.assistant.content.includes(REOPEN_FINAL), true)
await step('dual-window-send-and-stop-sync')
await ensureTaskSelected(main, 'dual-window main')
await fillComposer(taskWindow, DUAL_PROMPT)
await clickComposerSend(taskWindow)
await wait(
`[...document.querySelectorAll('.msg--assistant')].at(-1)?.innerText.includes(${JSON.stringify(DUAL_PARTIAL)})`,
'task-window dual partial',
taskWindow
)
await wait(
`[...document.querySelectorAll('.msg--assistant')].at(-1)?.innerText.includes(${JSON.stringify(DUAL_PARTIAL)})&&!!document.querySelector(".cr-send-button--stop")`,
'main-window dual partial and stop',
main
)
const dualTask = await visibleState(taskWindow)
const dualMain = await visibleState(main)
snapshots.dualStreaming = {
mainPartialCount: await markerCount(main, DUAL_PARTIAL),
taskPartialCount: await markerCount(taskWindow, DUAL_PARTIAL),
mainStop: dualMain.stop,
mainStopEnabled: dualMain.stopEnabled,
taskStop: dualTask.stop,
taskStopEnabled: dualTask.stopEnabled
}
check('both selected windows receive the independent-window send',
[dualMain.dualPartial, dualTask.dualPartial], [true, true])
check('both selected windows render the streamed partial exactly once',
[snapshots.dualStreaming.mainPartialCount, snapshots.dualStreaming.taskPartialCount], [1, 1])
check('both selected windows expose stop while the shared run is active',
[dualMain.stopEnabled, dualTask.stopEnabled], [true, true])
const dualStopped = await read(taskWindow, `(() => {
const button = document.querySelector('.cr-send-button--stop')
if (!button) return false
button.click()
return true
})()`)
check('shared run stop is triggered from the sending task window', dualStopped, true)
await wait(
`!document.querySelector(".cr-send-button--stop")&&[...document.querySelectorAll('.msg--assistant')].at(-1)?.innerText.includes("已取消")`,
'task-window dual cancellation',
taskWindow
)
await wait(
`!document.querySelector(".cr-send-button--stop")&&[...document.querySelectorAll('.msg--assistant')].at(-1)?.innerText.includes("已取消")`,
'main-window dual cancellation',
main
)
await waitValue(() => dualStreamClosed, 'dual upstream cancellation')
const dualTaskCancelled = await visibleState(taskWindow)
const dualMainCancelled = await visibleState(main)
check('both selected windows synchronize the stopped terminal state',
[dualMainCancelled.cancelled, dualTaskCancelled.cancelled], [true, true])
check('both selected windows restore send after the shared stop',
[dualMainCancelled.stop, dualMainCancelled.send, dualTaskCancelled.stop, dualTaskCancelled.send],
[false, true, false, true])
check('dual-window cancellation still renders the partial exactly once',
[await markerCount(main, DUAL_PARTIAL), await markerCount(taskWindow, DUAL_PARTIAL)], [1, 1])
const dualDisk = await waitPersistedStatus(DUAL_PROMPT, 'cancelled')
check('dual-window cancellation persists the cancelled status',
dualDisk.assistant.status, 'cancelled')
const mainRuns = eventRunDiagnostics(await readChatEventProbe(main))
const reopenedRuns = eventRunDiagnostics(await readChatEventProbe(taskWindow))
snapshots.chatEventSequences = { mainRuns, reopenedRuns }
check('main window receives run-started for every accepted turn',
mainRuns.length, 5)
check('main window run-started precedes each run event stream',
mainRuns.every((run) => run.startsWithRunStarted), true)
check('main window chat events carry non-negative integer sequences',
mainRuns.every((run) => run.allNonNegativeIntegers), true)
check('main window chat event sequences are strict and duplicate-free',
mainRuns.every((run) => run.strictlyIncreasing && run.unique), true)
check('reopened task window observes sequenced live events after hydration',
reopenedRuns.length >= 1 && reopenedRuns.every((run) => run.allNonNegativeIntegers && run.strictlyIncreasing && run.unique),
true)
check('all five turns reached the controlled local upstream',
requestLog.filter((entry) => entry.marker !== 'unknown').length, 5)
check('all upstream requests were streaming requests',
requestLog.filter((entry) => entry.marker !== 'unknown').every((entry) => entry.stream), true)
check('all upstream requests resolve to the isolated localhost fixture',
requestLog.filter((entry) => entry.marker !== 'unknown').every((entry) => entry.url?.startsWith('/v1/')), true)
check('no renderer error logs', rendererErrors, [])
const report = {
result: checks.every((item) => item.pass) ? 'PASS' : 'FAIL',
scope: 'Real isolated YCode App/preload/IPC/renderer, real OMP executable, main and independent task windows selected on the same task, a controlled localhost OpenAI-compatible SSE upstream, active-run reopen hydration, and sequenced broadcast event observation. Every send began via the visible task-window composer; window.ycode.chat.send was not invoked.',
isolation: {
directory,
userData: path.join(directory, 'data'),
ompHome: path.join(directory, 'home'),
workspace: path.join(directory, 'workspace'),
upstream: baseUrl
},
requestLog,
snapshots,
checks,
rendererErrors,
limitations: [
'The reopened-window scenario holds the SSE terminal behind a test gate, so the partial and stop control are asserted before any terminal event is sent.',
'Sequence/run-started checks use passive window.ycode.chat.onStarted and onEvent observers; production subscribers remain installed and no IPC or store listener is replaced.',
'The close-window terminal check observes the persisted sessions.json result, which is the product persistence boundary; the destroyed renderer cannot be queried for a late UI update.',
'The script requires a real OMP executable at YCODE_OMP_PATH or %LOCALAPPDATA%/Ygent/omp.exe and intentionally has no fallback to a remote or fake kernel.'
]
}
await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify(report, null, 2))
console.log('TASK_WINDOW_STREAM_' + report.result + ' ' + directory)
for (const failure of checks.filter((item) => !item.pass)) {
console.log('TASK_WINDOW_STREAM_FAILURE ' + JSON.stringify(failure))
}
for (const timer of pendingTimers) clearTimeout(timer)
server.closeAllConnections?.()
server.close()
app.exit(report.result === 'PASS' ? 0 : 1)
})().catch(async (error) => {
reopenFinalRelease?.()
for (const timer of pendingTimers) clearTimeout(timer)
server?.closeAllConnections?.()
server?.close()
await fs.writeFile(path.join(directory, 'failure.json'), JSON.stringify({
phase,
error: String(error),
stack: error?.stack,
requestCount,
requestLog,
checks,
snapshots,
rendererErrors,
taskWindowClosedAt,
doneTerminalSentAt,
cancelStreamClosed,
closeTerminalSentAt,
reopenStreamStarted,
reopenTerminalSentAt,
dualStreamClosed
}, null, 2)).catch(() => undefined)
console.error(error)
app.exit(1)
})
}