Files
ycode/scripts/verify-conversation-states.cjs
T

464 lines
33 KiB
JavaScript

// Verify YCode conversation state transitions in an isolated Electron renderer.
if (!process.versions.electron) {
const env = { ...process.env }
delete env.ELECTRON_RUN_AS_NODE
const { spawn, spawnSync } = require('node:child_process')
const path = require('node:path')
const server = spawn(process.execPath, [path.join(__dirname, 'verify-ui.mjs'), '0'], {
env, windowsHide: true, stdio: ['ignore', 'pipe', 'inherit']
})
let started = false
const timer = setTimeout(() => { if (!started) { server.kill(); process.exitCode = 1 } }, 10000)
server.stdout.on('data', bytes => {
const port = bytes.toString().match(/127\.0\.0\.1:(\d+)/)?.[1]
if (!port || started) return
started = true
clearTimeout(timer)
try {
const result = spawnSync(require('electron'), [__filename], {
env: { ...env, YCODE_STATE_QA_PORT: port }, windowsHide: true, stdio: 'inherit', timeout: 60000
})
process.exitCode = result.status ?? 1
} finally { server.kill() }
})
server.on('error', error => { clearTimeout(timer); console.error(error); process.exitCode = 1 })
} else {
const { app, BrowserWindow } = require('electron')
const fs = require('node:fs/promises')
const path = require('node:path')
const assert = require('node:assert/strict')
app.setPath('userData', path.resolve('.tmp/conversation-states-qa-user-data'))
app.disableHardwareAcceleration()
let win
const errors = []
const read = code => win.webContents.executeJavaScript(code)
const pause = ms => new Promise(resolve => setTimeout(resolve, ms))
function maxDurationSeconds(value) {
return Math.max(...value.split(',').map(part => {
const duration = Number.parseFloat(part)
return part.trim().endsWith('ms') ? duration / 1000 : duration
}))
}
async function waitFor(code, label = code) {
for (let attempt = 0; attempt < 250; attempt++) {
const value = await read(code)
if (value) return value
await pause(40)
}
throw new Error('Condition failed: ' + label)
}
async function settle() {
await read('new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))')
}
async function waitStableHeight(id, open) {
const selector = JSON.stringify(toolSelector(id) + ' .cr-tool-exp__content-shell')
let previous
let stable = 0
for (let attempt = 0; attempt < 100; attempt++) {
const height = await read('document.querySelector(' + selector + ')?.getBoundingClientRect().height')
if (height !== undefined && (open ? height > 1 : height <= 1) && previous !== undefined && Math.abs(height - previous) < 0.5) stable += 1
else stable = 0
if (stable >= 3) return height
previous = height
await pause(40)
}
throw new Error('Height did not settle for ' + id + ' open=' + open)
}
async function screenshot(name, selector) {
await read('document.querySelector(' + JSON.stringify(selector) + ').scrollIntoView({block:"center",behavior:"instant"})')
await read('document.fonts.ready')
await settle()
await new Promise(resolve => { win.webContents.once('paint', resolve); win.webContents.invalidate() })
await fs.writeFile(path.resolve('.tmp/' + name), (await win.webContents.capturePage()).toPNG())
}
async function send(text) {
await read('(()=>{const input=document.querySelector(".wb-composer__input");const set=Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype,"value").set;set.call(input,' + JSON.stringify(text) + ');input.dispatchEvent(new Event("input",{bubbles:true}))})()')
await waitFor('!document.querySelector(".cr-send-button:not(.cr-send-button--stop)")?.disabled', 'composer accepts text')
await read('document.querySelector(".cr-send-button:not(.cr-send-button--stop)").click()')
await waitFor('!!window.__YCODE_CONVERSATION_STATE_TEST__?.lastRunId', 'fixture chat.send created a run')
return await read('window.__YCODE_CONVERSATION_STATE_TEST__.lastRunId')
}
async function emit(runId, event) {
await read('window.__YCODE_CONVERSATION_STATE_TEST__.emit(' + JSON.stringify({ ...event, runId }) + ')')
}
function toolSelector(id) { return '[data-tool-call-id="' + id + '"]' }
async function toolState(id) {
return read('(()=>{const root=document.querySelector(' + JSON.stringify(toolSelector(id)) + ');if(!root)return null;const head=root.querySelector(".cr-tool-head__main"),shell=root.querySelector(".cr-tool-exp__content-shell");return{status:root.dataset.toolStatus,loading:!!root.querySelector(".cr-tool-head__primary--loading"),expanded:head?.getAttribute("aria-expanded")??null,hidden:shell?.getAttribute("aria-hidden")??null,height:shell?.getBoundingClientRect().height??null,styleHeight:shell?.style.height??null,transition:shell?getComputedStyle(shell).transition:null,transitionDuration:shell?getComputedStyle(shell).transitionDuration:null,text:root.textContent}})()')
}
async function waitTool(id, status, expanded) {
const selector = JSON.stringify(toolSelector(id))
let condition = '(()=>{const root=document.querySelector(' + selector + ');return !!root&&root.dataset.toolStatus===' + JSON.stringify(status)
if (expanded !== undefined) condition += '&&root.querySelector(".cr-tool-head__main")?.getAttribute("aria-expanded")===' + JSON.stringify(expanded ? 'true' : 'false')
condition += '})()'
await waitFor(condition, id + ' status=' + status + (expanded === undefined ? '' : ' expanded=' + expanded))
}
async function clickTool(id) {
await read('document.querySelector(' + JSON.stringify(toolSelector(id) + ' .cr-tool-head__main') + ').click()')
}
async function stopRun() {
await read('document.querySelector(".cr-send-button--stop").click()')
}
function toolCall(id, name, status, args, extra = {}) {
return { id, name, args, argsRaw: JSON.stringify(args), status, ...extra }
}
async function emitTool(runId, type, call) {
await emit(runId, { type, call })
}
function interactionEvent(runId, id, toolCallId, status = 'pending') {
return { type: 'interaction', interaction: { id, requestId: id, runId, toolCallId, kind: 'approval', title: 'Fixture approval', status,
options: [{ optionId: 'allow-once', name: 'Once', kind: 'allow_once' }, { optionId: 'allow-always', name: 'Always', kind: 'allow_always' },
{ optionId: 'reject-once', name: 'Reject', kind: 'reject_once' }] } }
}
function approvalSelector(id) { return '.conversation-input-area__custom-floating .permission-floating[data-pending-id="' + id + '"]' }
async function waitInteraction(runId, interactionId, status) {
const run = 'window.__YCODE_CONVERSATION_STATE_TEST__.runs[' + JSON.stringify(runId) + ']'
const condition = run + '?.events.some(event=>event.type==="interaction"&&event.interaction.id===' + JSON.stringify(interactionId) + '&&event.interaction.status===' + JSON.stringify(status) + ')'
await waitFor(condition, interactionId + ' interaction event status=' + status)
}
async function approvalState(toolId, interactionId) {
return read('(()=>{const root=document.querySelector(' + JSON.stringify(toolSelector(toolId)) + '),floating=document.querySelector(' + JSON.stringify(approvalSelector(interactionId)) + '),composer=document.querySelector(".conversation-input-area__input-stack"),buttons=floating?[...floating.querySelectorAll(".conversation-high-credit-approval__option")]:[],inlineButtons=root?[...root.querySelectorAll(".cr-tool-btn[data-tool-decision]")]:[];const floatingRect=floating?.getBoundingClientRect(),composerRect=composer?.getBoundingClientRect();return{toolStatus:root?.dataset.toolStatus??null,inlineButtons:inlineButtons.map(button=>({decision:button.dataset.toolDecision,disabled:button.disabled})),floatingCount:document.querySelectorAll(".conversation-input-area__custom-floating .permission-floating").length,floatingForInteraction:!!floating,floatingStatus:floating?.className??null,inConversation:!!document.querySelector(".chat-thread .conversation-high-credit-approval"),aboveComposer:!!floatingRect&&!!composerRect&&floatingRect.bottom<=composerRect.top,floatingGap:floatingRect&&composerRect?composerRect.top-floatingRect.bottom:null,options:buttons.map(button=>({optionId:button.dataset.optionId??null,label:button.textContent,reject:button.classList.contains("conversation-high-credit-approval__option--reject"),disabled:button.disabled}))}})()')
}
async function approvalButtonStyles(interactionId) {
return read('(()=>{const root=document.querySelector(' + JSON.stringify(approvalSelector(interactionId)) + ');return[...(root?.querySelectorAll(".conversation-high-credit-approval__option")??[])].map(button=>{const style=getComputedStyle(button);return{optionId:button.dataset.optionId??null,reject:button.classList.contains("conversation-high-credit-approval__option--reject"),className:button.className,height:style.height,minHeight:style.minHeight,fontSize:style.fontSize,padding:style.padding,color:style.color,backgroundColor:style.backgroundColor}})})()')
}
async function clickApproval(interactionId, optionId, twice = false) {
const selector = optionId.startsWith('reject')
? approvalSelector(interactionId) + ' .conversation-high-credit-approval__option--reject'
: approvalSelector(interactionId) + ' .conversation-high-credit-approval__option[data-option-id="' + optionId + '"]'
await read('(()=>{const button=document.querySelector(' + JSON.stringify(selector) + ');if(!button)throw new Error("Approval action is missing");button.click()' + (twice ? ';button.click()' : '') + '})()')
}
async function responseRecords(interactionId) {
return read('window.__YCODE_CONVERSATION_STATE_TEST__.responses.filter(item=>item.interactionId===' + JSON.stringify(interactionId) + ')')
}
async function finishToolRun(runId, call, status) {
const finalCall = { ...call, status, output: status === 'ok' ? '' : 'fixture permission result', result: status === 'ok' ? { result: { stdout: '', exitCode: 0 } } : { result: { error: 'fixture permission result' } } }
await emitTool(runId, 'tool_result', finalCall)
await emit(runId, { type: 'done', content: '', tools: [finalCall] })
await waitFor('!document.querySelector(".cr-send-button--stop")', 'approved tool run finished')
}
async function scrollMetrics() {
return read('(()=>{const e=document.querySelector(".chat-scroll");return{top:e.scrollTop,height:e.scrollHeight,client:e.clientHeight,gap:e.scrollHeight-e.scrollTop-e.clientHeight,rect:e.getBoundingClientRect().toJSON()}})()')
}
async function wheel(deltaY) {
const rect = (await scrollMetrics()).rect
await win.webContents.debugger.sendCommand('Input.dispatchMouseEvent', {
type: 'mouseWheel', x: rect.x + rect.width / 2, y: rect.y + rect.height / 2, deltaX: 0, deltaY
})
await pause(80)
}
async function atBottom() {
return waitFor('(()=>{const e=document.querySelector(".chat-scroll");return e&&e.scrollHeight-e.scrollTop-e.clientHeight<3})()', 'conversation timeline reached bottom')
}
app.whenReady().then(async () => {
await fs.mkdir(path.resolve('.tmp'), { recursive: true })
win = new BrowserWindow({ show: false, useContentSize: true, width: 1336, height: 901,
webPreferences: { offscreen: true, contextIsolation: true } })
win.webContents.on('console-message', (...args) => {
const event = args[0]
const level = event?.level ?? args[1]
const message = event?.message ?? args[2]
if (level === 'error' || level === 3) errors.push(String(message))
})
await win.loadURL('http://127.0.0.1:' + (process.env.YCODE_STATE_QA_PORT || '8792') + '/?fixture=conversation-states')
await waitFor('!!document.querySelector(".chat-scroll")&&document.querySelectorAll(".msg--assistant").length===14', 'fixture history rendered')
await win.webContents.debugger.attach('1.3')
await win.webContents.debugger.sendCommand('Emulation.setEmulatedMedia', {
media: 'screen', features: [{ name: 'prefers-reduced-motion', value: 'no-preference' }]
})
const normalMotionPreference = await read('matchMedia("(prefers-reduced-motion: reduce)").matches')
assert.equal(normalMotionPreference, false)
await atBottom()
assert.ok((await scrollMetrics()).height > (await scrollMetrics()).client + 500, 'fixture history creates a real scroll range')
const scrollRun = await send('scroll-follow')
const firstText = Array.from({ length: 7 }, (_, index) => 'Streaming paragraph ' + index + ' grows the assistant response while the user changes timeline position. '.repeat(4)).join('\n\n')
const secondText = Array.from({ length: 5 }, (_, index) => 'Scrolled update ' + index + ' adds new rows below the viewport. '.repeat(5)).join('\n\n')
const thirdText = Array.from({ length: 4 }, (_, index) => 'Bottom follow update ' + index + ' remains attached to the newest content. '.repeat(5)).join('\n\n')
await emit(scrollRun, { type: 'text', delta: firstText })
await atBottom()
const beforeScroll = await scrollMetrics()
for (let attempt = 0; attempt < 8 && (await scrollMetrics()).gap <= 80; attempt++) await wheel(-650)
const userScrolledUp = await scrollMetrics()
assert.ok(userScrolledUp.gap > 80, 'wheel input moved the conversation away from the bottom')
await emit(scrollRun, { type: 'text', delta: '\n\n' + secondText })
await waitFor('document.querySelector(".chat-scroll").scrollHeight>' + Math.floor(userScrolledUp.height), 'new stream content expanded the timeline')
await settle()
const stayedScrolledUp = await scrollMetrics()
assert.ok(Math.abs(stayedScrolledUp.top - userScrolledUp.top) < 8, 'stream updates do not pull a user back down after scrolling up')
assert.ok(stayedScrolledUp.gap > 80)
for (let attempt = 0; attempt < 8 && (await scrollMetrics()).gap >= 3; attempt++) await wheel(900)
await atBottom()
await emit(scrollRun, { type: 'text', delta: '\n\n' + thirdText })
await atBottom()
const scrollContent = firstText + '\n\n' + secondText + '\n\n' + thirdText
await emit(scrollRun, { type: 'done', content: scrollContent, tools: [] })
await waitFor('!document.querySelector(".cr-send-button--stop")', 'scroll run finished')
const scrollFollow = { beforeScroll: beforeScroll.top, userScrolledUp: userScrolledUp.top, afterUpdate: stayedScrolledUp.top,
returnedToBottom: true, followedAfterReturn: (await scrollMetrics()).gap < 3 }
assert.equal(scrollFollow.followedAfterReturn, true)
const autoRun = await send('write-auto-collapse')
const autoContent = 'first line\nsecond line\nthird line'
const autoRunning = toolCall('write-auto', 'write_to_file', 'running', { path: 'src/auto.ts' }, { result: { result: { newContent: 'first line\nsecond line' } } })
await emitTool(autoRun, 'tool_call', autoRunning)
await waitTool('write-auto', 'running', true)
const autoOpen = await toolState('write-auto')
await emitTool(autoRun, 'tool_result', { ...autoRunning, status: 'ok', result: { result: { newContent: autoContent } }, output: 'written' })
await waitTool('write-auto', 'success', false)
const autoClosed = await toolState('write-auto')
assert.equal(autoOpen.expanded, 'true')
assert.equal(autoClosed.hidden, 'true')
await emit(autoRun, { type: 'done', content: '', tools: [{ ...autoRunning, status: 'ok', result: { result: { newContent: autoContent } }, output: 'written' }] })
await waitFor('!document.querySelector(".cr-send-button--stop")', 'automatic-collapse write run finished')
const manualRun = await send('write-manual-collapse')
const manualFirst = toolCall('write-manual', 'write_to_file', 'running', { path: 'src/manual.ts' }, { result: { result: { newContent: 'partial content' } } })
await emitTool(manualRun, 'tool_call', manualFirst)
await waitTool('write-manual', 'running', true)
const manualOpen = await toolState('write-manual')
assert.ok(manualOpen.height > 0, 'small write expands to a measurable height')
const normalTransitionSeconds = maxDurationSeconds(manualOpen.transitionDuration)
assert.ok(Math.abs(normalTransitionSeconds - 0.28) < 0.01, 'normal small writes retain the source 0.28s height transition')
assert.ok(manualOpen.transition.includes('cubic-bezier(0.33, 1, 0.68, 1)'), 'normal small writes retain the source height easing curve')
await clickTool('write-manual')
await waitTool('write-manual', 'running', false)
await waitStableHeight('write-manual', false)
const manualClosed = await toolState('write-manual')
assert.ok(manualClosed.height <= 1, 'small write collapses to zero height')
const manualUpdate = { ...manualFirst, result: { result: { newContent: 'partial content\nmore streamed content' } }, output: 'partial content' }
await emitTool(manualRun, 'tool_call', manualUpdate)
await waitFor('document.querySelector(' + JSON.stringify(toolSelector('write-manual')) + ').textContent.includes("more streamed content")', 'manual write received another streaming tool update')
const manualAfterUpdate = await toolState('write-manual')
assert.equal(manualAfterUpdate.expanded, 'false', 'a same-position stream update must not reopen a manually collapsed write view')
await clickTool('write-manual')
await waitTool('write-manual', 'running', true)
await waitStableHeight('write-manual', true)
const manualReopened = await toolState('write-manual')
assert.ok(manualReopened.height > 0, 'small write can be expanded again after a stream update')
await stopRun()
await waitTool('write-manual', 'cancelled', false)
const commandRun = await send('command-states')
const commandRunning = toolCall('command-running', 'Bash', 'running', { command: 'npm run build' })
await emitTool(commandRun, 'tool_call', commandRunning)
await waitTool('command-running', 'running', false)
await clickTool('command-running')
await waitFor('document.querySelector(' + JSON.stringify(toolSelector('command-running') + ' .cr-tool-head__primary--loading') + ')', 'running command loading state rendered')
const commandRunningState = await toolState('command-running')
assert.equal(commandRunningState.loading, true)
await emitTool(commandRun, 'tool_result', { ...commandRunning, status: 'error', output: 'fixture command failure', result: { result: { stderr: 'fixture stderr', exitCode: 2, error: 'fixture command failure' } } })
await waitTool('command-running', 'error')
await waitFor('document.querySelector(' + JSON.stringify(toolSelector('command-running') + ' .cr-tool-exec__output') + ')', 'error command output rendered')
const commandErrorState = await toolState('command-running')
const emptyCommand = toolCall('command-empty-success', 'execute_command', 'running', { command: 'true' })
await emitTool(commandRun, 'tool_call', emptyCommand)
await waitTool('command-empty-success', 'running', false)
await emitTool(commandRun, 'tool_result', { ...emptyCommand, status: 'ok', output: '', result: { result: { stdout: '', stderr: '', exitCode: 0 } } })
await waitTool('command-empty-success', 'success')
await clickTool('command-empty-success')
await waitFor('document.querySelector(' + JSON.stringify(toolSelector('command-empty-success') + ' .cr-tool-exec__ok') + ')', 'empty successful command state rendered')
const commandEmptySuccessState = await toolState('command-empty-success')
const cancelledCommand = toolCall('command-cancelled', 'run_command', 'running', { command: 'sleep 60' })
await emitTool(commandRun, 'tool_call', cancelledCommand)
await waitTool('command-cancelled', 'running', false)
await stopRun()
await waitTool('command-cancelled', 'cancelled')
await waitFor('document.querySelector(".chat-thread")?.textContent.includes("用户已取消")', 'cancel terminal message rendered')
const commandCancelledState = await toolState('command-cancelled')
assert.equal(commandRunningState.status, 'running')
assert.equal(commandErrorState.status, 'error')
assert.equal(commandEmptySuccessState.status, 'success')
assert.equal(commandCancelledState.status, 'cancelled')
const fileApprovalRun = await send('approval-file-accept')
const fileApprovalTool = toolCall('approval-file-tool', 'write_to_file', 'running', { path: 'src/approval.ts' }, { output: '', result: { result: { newContent: 'pending file body' } } })
await emitTool(fileApprovalRun, 'tool_call', fileApprovalTool)
await waitTool('approval-file-tool', 'running', true)
await emit(fileApprovalRun, interactionEvent(fileApprovalRun, 'approval-file-request', 'approval-file-tool'))
await waitTool('approval-file-tool', 'pending')
await waitFor('document.querySelector(' + JSON.stringify(approvalSelector('approval-file-request')) + ')', 'file approval floats above the composer')
const filePending = await approvalState('approval-file-tool', 'approval-file-request')
assert.equal(filePending.toolStatus, 'pending')
assert.equal(filePending.inlineButtons.length, 0, 'file approval actions are not rendered inside the tool row')
assert.equal(filePending.floatingForInteraction, true)
assert.equal(filePending.floatingCount, 1, 'one active approval is shown at a time')
assert.equal(filePending.inConversation, false, 'approval cards are not rendered in the conversation stream')
assert.equal(filePending.aboveComposer, true, 'approval card sits above the composer')
assert.ok(filePending.floatingGap >= 0 && filePending.floatingGap <= 12, 'approval card stays visually attached to the composer')
assert.deepEqual(filePending.options.map(option => option.reject ? 'reject' : option.optionId), ['allow-once', 'allow-always', 'reject'])
const fileButtonStyles = await approvalButtonStyles('approval-file-request')
for (const button of fileButtonStyles) {
assert.equal(button.height, '44px')
assert.equal(button.fontSize, '14px')
assert.equal(button.padding, '8px 12px')
}
const fileAcceptStyle = fileButtonStyles.find(button => button.optionId === 'allow-once')
const fileRejectStyle = fileButtonStyles.find(button => button.reject)
assert.notEqual(fileAcceptStyle.color, fileRejectStyle.color)
await screenshot('conversation-file-approval-floating-2026-10-20.png', '.conversation-input-area__custom-floating')
await read('window.__YCODE_CONVERSATION_STATE_TEST__.queueResponse("failure")')
await clickApproval('approval-file-request', 'allow-once')
await waitFor('document.querySelector(' + JSON.stringify(approvalSelector('approval-file-request') + ' .conversation-high-credit-approval__error') + ')', 'failed floating approval response surfaced')
const fileRetry = await approvalState('approval-file-tool', 'approval-file-request')
assert.ok(fileRetry.options.some(option => option.optionId === 'allow-once' && !option.disabled), 'failed approval can be retried')
const fileFirstAttempt = await responseRecords('approval-file-request')
assert.equal(fileFirstAttempt.length, 1)
assert.equal(fileFirstAttempt[0].response.optionId, 'allow-once', 'file accept selects once before always')
await read('window.__YCODE_CONVERSATION_STATE_TEST__.queueResponse("defer")')
await clickApproval('approval-file-request', 'allow-once', true)
await waitFor('(()=>{const buttons=[...document.querySelectorAll(' + JSON.stringify(approvalSelector('approval-file-request') + ' .conversation-high-credit-approval__option') + ')];return buttons.length>0&&buttons.every(button=>button.disabled)})()', 'responding disables all floating approval options')
const fileDeferredResponses = await responseRecords('approval-file-request')
assert.equal(fileDeferredResponses.length, 2, 'double click submits only one request while the first response is deferred')
await read('window.__YCODE_CONVERSATION_STATE_TEST__.releaseDeferredResponse()')
await waitInteraction(fileApprovalRun, 'approval-file-request', 'answered')
const fileAcceptedResponses = await responseRecords('approval-file-request')
assert.deepEqual(fileAcceptedResponses.map(item => item.response.optionId), ['allow-once', 'allow-once'])
await waitFor('!document.querySelector(' + JSON.stringify(approvalSelector('approval-file-request')) + ')', 'accepted file approval floating card is removed')
await stopRun()
await waitFor('!document.querySelector(".cr-send-button--stop")', 'approval fixture run cancelled')
const commandApprovalResults = []
let commandButtonStyles
for (const scenario of [
{ decision: 'allow', optionId: 'allow-once', status: 'answered', toolStatus: 'ok', arrival: 'early' },
{ decision: 'skip', optionId: 'reject-once', status: 'rejected', toolStatus: 'error', arrival: 'delayed' },
{ decision: 'reject', optionId: 'reject-once', status: 'rejected', toolStatus: 'error', arrival: 'delayed' }
]) {
const toolId = 'approval-command-' + scenario.decision + '-tool'
const interactionId = 'approval-command-' + scenario.decision + '-request'
const call = toolCall(toolId, 'Bash', 'running', { command: 'fixture command' }, { output: '' })
if (scenario.arrival === 'early') {
await read('window.__YCODE_CONVERSATION_STATE_TEST__.queueEarlyBatch(' + JSON.stringify([
{ type: 'tool_call', call }, interactionEvent('', interactionId, toolId)
]) + ')')
}
const runId = await send('approval-command-' + scenario.decision)
if (scenario.arrival === 'delayed') {
await emitTool(runId, 'tool_call', call)
await waitTool(toolId, 'running', false)
await emit(runId, interactionEvent(runId, interactionId, toolId))
await waitTool(toolId, 'pending')
} else {
await waitTool(toolId, 'pending')
}
const commandPending = await toolState(toolId)
await waitFor('document.querySelector(' + JSON.stringify(approvalSelector(interactionId)) + ')', 'command approval floats above the composer')
if (scenario.arrival === 'early') commandButtonStyles = await approvalButtonStyles(interactionId)
const commandApproval = await approvalState(toolId, interactionId)
assert.equal(commandApproval.inlineButtons.length, 0, 'command approval actions are not rendered inside the tool row')
assert.equal(commandApproval.inConversation, false, 'command approval cards are not rendered in the conversation stream')
assert.equal(commandApproval.aboveComposer, true)
if (scenario.arrival === 'early') await screenshot('conversation-command-approval-floating-2026-10-20.png', '.conversation-input-area__custom-floating')
await clickApproval(interactionId, scenario.optionId)
await waitInteraction(runId, interactionId, scenario.status)
await waitFor('!document.querySelector(' + JSON.stringify(approvalSelector(interactionId)) + ')', 'command approval floating card is removed')
const responses = await responseRecords(interactionId)
assert.equal(responses.length, 1)
assert.equal(responses[0].response.optionId, scenario.optionId)
const interaction = await read('window.__YCODE_CONVERSATION_STATE_TEST__.runs[' + JSON.stringify(runId) + '].interactions[' + JSON.stringify(interactionId) + ']')
assert.equal(interaction.selectedOptionId, scenario.optionId)
await finishToolRun(runId, call, scenario.toolStatus)
commandApprovalResults.push({ decision: scenario.decision, arrival: scenario.arrival,
initialExpanded: commandPending.expanded, optionId: responses[0].response.optionId, status: interaction.status })
}
for (const button of commandButtonStyles) {
assert.equal(button.height, '44px')
assert.equal(button.fontSize, '14px')
assert.equal(button.padding, '8px 12px')
}
const commandRunStyle = commandButtonStyles.find(button => button.optionId === 'allow-once')
const commandRejectStyle = commandButtonStyles.find(button => button.reject)
assert.ok(commandRunStyle)
assert.ok(commandRejectStyle)
assert.notEqual(commandRunStyle.color, commandRejectStyle.color)
const largeRun = await send('write-large-motion')
const largeText = Array.from({ length: 180 }, (_, index) => 'export const value' + index + ' = ' + index + ';').join('\n')
const largeCall = toolCall('write-large', 'write_to_file', 'running', { path: 'src/large.ts' }, { result: { result: { newContent: largeText } } })
await emitTool(largeRun, 'tool_call', largeCall)
await waitTool('write-large', 'running', true)
await settle()
const largeOpen = await toolState('write-large')
assert.ok(largeOpen.height > 0, 'large write shell has a real open height')
assert.equal(largeOpen.styleHeight, 'auto', 'large write uses a static open height')
assert.equal(largeOpen.transition, 'none', 'large write disables its transition')
await clickTool('write-large')
await waitTool('write-large', 'running', false)
const largeClosed = await toolState('write-large')
assert.ok(largeClosed.height <= 1, 'large write shell reaches zero height when collapsed')
assert.equal(largeClosed.styleHeight, '0px')
await clickTool('write-large')
await waitTool('write-large', 'running', true)
const largeReopened = await toolState('write-large')
assert.ok(largeReopened.height > 0)
assert.equal(largeReopened.styleHeight, 'auto')
await stopRun()
await waitTool('write-large', 'cancelled')
await win.webContents.debugger.sendCommand('Emulation.setEmulatedMedia', {
media: 'screen', features: [{ name: 'prefers-reduced-motion', value: 'reduce' }]
})
assert.equal(await read('matchMedia("(prefers-reduced-motion: reduce)").matches'), true)
const reducedRun = await send('write-reduced-motion')
const reducedCall = toolCall('write-reduced', 'write_to_file', 'running', { path: 'src/reduced.ts' }, { result: { result: { newContent: 'small file\nnext line' } } })
await emitTool(reducedRun, 'tool_call', reducedCall)
await waitTool('write-reduced', 'running', true)
await settle()
const reducedOpen = await toolState('write-reduced')
assert.ok(reducedOpen.height > 0)
assert.equal(reducedOpen.transition, 'none', 'reduced motion disables the write shell transition')
await clickTool('write-reduced')
await waitTool('write-reduced', 'running', false)
const reducedClosed = await toolState('write-reduced')
assert.ok(reducedClosed.height <= 1, 'reduced-motion shell reaches zero height when collapsed')
await stopRun()
await waitTool('write-reduced', 'cancelled')
const report = {
scope: 'YCode offline fixture state regression only; WorkBuddy parity is not assessed',
normalMotionPreferenceReduced: normalMotionPreference,
scrollFollow,
writeFile: { autoOpen: autoOpen.expanded, successClosed: autoClosed.expanded, manualAfterStream: manualAfterUpdate.expanded,
manualMotion: { openHeight: manualOpen.height, transition: manualOpen.transition, transitionDuration: manualOpen.transitionDuration,
maxTransitionSeconds: normalTransitionSeconds, closedHeight: manualClosed.height, reopenedHeight: manualReopened.height },
largeOpen: { height: largeOpen.height, styleHeight: largeOpen.styleHeight, transition: largeOpen.transition },
largeClosed: { height: largeClosed.height, styleHeight: largeClosed.styleHeight },
largeReopened: { height: largeReopened.height, styleHeight: largeReopened.styleHeight },
reducedOpen: { height: reducedOpen.height, transition: reducedOpen.transition, transitionDuration: reducedOpen.transitionDuration },
reducedClosed: { height: reducedClosed.height, transition: reducedClosed.transition } },
commands: { running: commandRunningState.status, error: commandErrorState.status,
emptySuccess: commandEmptySuccessState.status, cancelled: commandCancelledState.status },
approvals: { filePending: { toolStatus: filePending.toolStatus, inlineDecisionCount: filePending.inlineButtons.length,
floatingCount: filePending.floatingCount, aboveComposer: filePending.aboveComposer, floatingGap: filePending.floatingGap,
inConversation: filePending.inConversation },
fileButtonStyles, commandButtonStyles,
fileRetryOptionIds: fileAcceptedResponses.map(item => item.response.optionId), deferredDoubleClickRequestCount: fileDeferredResponses.length,
commandDecisions: commandApprovalResults,
commandSkipRejectTransportLimitation: { skip: 'reject-once', reject: 'reject-once', distinctKernelResponse: false } },
consoleErrors: errors
}
assert.deepEqual(errors, [], 'renderer should produce zero console errors')
await fs.writeFile(path.resolve('.tmp/conversation-states-ui-report.json'), JSON.stringify(report, null, 2))
console.log(JSON.stringify(report))
win.destroy()
app.quit()
}).catch(async error => {
console.error(error)
console.error(JSON.stringify(errors))
if (win && !win.isDestroyed()) {
try {
const diagnostics = await read('({body:document.body.innerText.slice(-12000),state:window.__YCODE_CONVERSATION_STATE_TEST__?.runs})')
await fs.writeFile(path.resolve('.tmp/conversation-states-ui-failure.json'), JSON.stringify({ error: String(error), diagnostics, consoleErrors: errors }, null, 2))
} catch { /* Keep the original failure if the renderer has already exited. */ }
win.destroy()
}
app.exit(1)
})
}