/** jsdom behavior only; no layout, browser pixels, SDK process or model calls. */ import assert from 'node:assert/strict' import { createRequire } from 'node:module' import { readFileSync } from 'node:fs' const externalRequire=createRequire(process.cwd()+'/package.json') const {JSDOM}=externalRequire(process.env.YCODE_TEST_DOM_MODULE||'jsdom') const dom=new JSDOM('
',{url:'http://localhost/',pretendToBeVisual:true}) for(const key of ['window','document','Node','Element','HTMLElement','HTMLInputElement','HTMLTextAreaElement','MouseEvent','KeyboardEvent','Document','Text','DOMParser','MutationObserver','Range','ShadowRoot','File','FileReader','Event','getComputedStyle']) Object.defineProperty(globalThis,key,{value:dom.window[key],configurable:true}) Object.defineProperty(globalThis,'navigator',{value:dom.window.navigator,configurable:true}) Object.assign(globalThis,{IS_REACT_ACT_ENVIRONMENT:true,requestAnimationFrame:dom.window.requestAnimationFrame.bind(dom.window),cancelAnimationFrame:dom.window.cancelAnimationFrame.bind(dom.window)}) const composerStyle=document.createElement('style') composerStyle.textContent=['composer.css','composer-source.css'].map(file=>readFileSync(`src/renderer/src/workbuddy/composer/${file}`,'utf8')).join('\n') document.head.append(composerStyle) const React=require('react') as typeof import('react') const {act}=React const {createRoot}=require('react-dom/client') as typeof import('react-dom/client') const {PermissionConfirm}=require('../src/renderer/src/workbuddy/conversation/PermissionConfirm') as typeof import('../src/renderer/src/workbuddy/conversation/PermissionConfirm') const {ComposerControls}=require('../src/renderer/src/workbuddy/composer/ComposerControls') as typeof import('../src/renderer/src/workbuddy/composer/ComposerControls') const {composeComposer}=require('../src/renderer/src/workbuddy/composer/input-composition') as typeof import('../src/renderer/src/workbuddy/composer/input-composition') const root=createRoot(document.getElementById('root')!) let count=0,completed=0,declined=0, resolve!:()=>void,reject!:(e:Error)=>void let checks=0 const check=(value:unknown,message:string)=>{assert.ok(value,message);checks++} const options=[{optionId:'once',kind:'allow_once',name:'Once'},{optionId:'session',kind:'allow_always',name:'Session'},{optionId:'deny',kind:'reject_once',name:'Reject'}] const pending=()=>{count++;return new Promise((yes,no)=>{resolve=yes;reject=no})} let id='a',status:'pending'|'cancelled'='pending',defaultToNo=false const render=()=>act(async()=>root.render({declined++}} onDecisionComplete={()=>{completed++}}/>)) const key=(value:string)=>document.querySelector('section')!.dispatchEvent(new KeyboardEvent('keydown',{key:value,bubbles:true,cancelable:true})) const click=(selector:string)=>document.querySelector(selector)!.dispatchEvent(new MouseEvent('click',{bubbles:true,cancelable:true})) async function main(){ await render() check(document.activeElement?.getAttribute('data-option-id')==='once','normal prompt focuses first option') await act(async()=>{key('1');key('1')}) check(count===1,'duplicate keyboard submission synchronous guard') check(document.querySelectorAll('button[disabled]').length===3,'all choices disabled while submitting') await act(async()=>reject(new Error('fixture failed'))) check(document.querySelector('[role="alert"]')?.textContent?.includes('fixture failed'),'failure visible') await act(async()=>click('[data-option-id="once"]')) check(count===2,'failed choice retries') await act(async()=>resolve()) check(completed===1,'success completes once') id='b';defaultToNo=true;await render() check(document.activeElement?.textContent?.includes('Reject'),'default-to-no focuses reject') await act(async()=>key('1'));check(count===2,'default-to-no has no one-key approve shortcut') await act(async()=>key('Escape'));check(declined===1,'Escape rejects') id='c';defaultToNo=false;await render();await act(async()=>click('[data-option-id="once"]')) const oldResolve=resolve;const previous=completed id='d';await render();await act(async()=>oldResolve()) check(completed===previous,'late result cannot complete replacement request') check(document.querySelector('section')?.getAttribute('data-pending-id')==='d','replacement retained') await act(async()=>click('[data-option-id="once"]')) status='cancelled';await render();await act(async()=>resolve()) check(completed===previous,'cancelled request ignores stale success') check(document.querySelector('section')?.textContent?.includes('Cancelled'),'cancelled remains visible') id='e';status='pending';await render();await act(async()=>click('.conversation-intercept-card__rule-action')) check(document.querySelector('[role="status"]')?.textContent?.includes('未提供命中规则详情'),'missing rule explanation explicit') // Exercise the real composer with the real approval surface, not a skeleton. let session='session-a', active=true, sends=0, stops=0, changes=0, stopFails=false, request='request-1' let settled: 'answered'|'rejected'|undefined const draft='preserved draft' const attachments=[{id:'attachment-a',name:'a.txt',mimeType:'text/plain',size:1}] const resources=['/fixture/reference.ts'] const quotes=[{id:'quote-a',kind:'table' as const,sourceId:'source-a',markdown:'table',preview:'table'}] const renderComposer=()=>act(async()=>{ const composition=composeComposer({interaction:active?{id:session,kind:'approval',status:'pending'}:null,queueCount:2,running:true}) root.render({changes++}} onSend={()=>{sends++}} attachments={attachments} resources={resources} quotes={quotes} onStop={async()=>{stops++;if(stopFails)throw new Error('stop fixture failed')}} queueSlot={composition.queue.visible?
two queued messages
:undefined} approvalKey={request} approvalSlot={!composition.input.visible?{declined++}} onDecisionComplete={decision=>{settled=decision}}/>:undefined}/>) }) await renderComposer() const stack=document.querySelector('.conversation-input-area__input-stack')! check(stack.hidden&&stack.hasAttribute('inert'),'approval hides and inerts preserved input stack') check(getComputedStyle(stack).display==='none','composer CSS removes the normal input from layout during approval') check(document.querySelectorAll('[data-composer-approval] [role="dialog"]').length===1,'one bottom approval surface') check(!document.querySelector('.wb-composer__toolbar'),'normal toolbar and send controls removed during approval') check(!document.querySelector('button[aria-label="停止当前运行"],button[aria-label="停止"],button[aria-label="发送"],.wb-composer__approval-stop'),'approval takeover has no standalone stop or send button even with onStop supplied') check(document.querySelector('[data-composer-approval]')?.children.length===1,'bottom approval surface contains only the approval card') check(!document.querySelector('[data-queue]'),'queue hidden without deleting queue state') const editor=stack.querySelector('[contenteditable]')! check(editor?.getAttribute('contenteditable')==='false','rich editor disabled during approval') const changesBefore=changes await act(async()=>{editor.focus();editor.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',bubbles:true,cancelable:true}))}) check(!stack.contains(document.activeElement)&&sends===0&&changes===changesBefore,'hidden editor cannot focus, type or submit') await act(async()=>click('[data-option-id="once"]')) await act(async()=>reject(new Error('composer retry fixture'))) check(document.querySelector('[role="alert"]')?.textContent?.includes('composer retry fixture'),'submission failure stays in replacement surface') check(stack.hidden&&!document.querySelector('.wb-composer__toolbar'),'failure does not expose editor/send') await act(async()=>click('[data-option-id="once"]'));await act(async()=>resolve()) check(settled==='answered','allow completes through real composer') active=false;await renderComposer() check(!stack.hidden&&!stack.hasAttribute('inert'),'completion restores input stack') check(getComputedStyle(stack).display!=='none','composer CSS restores normal input layout') check(stack.textContent?.includes('a.txt')&&stack.textContent?.includes('reference.ts')&&stack.textContent?.includes('会话表格'),'attachment resource and quote survive approval') check(stack.textContent?.includes(draft),'rich draft survives approval') check(!!document.querySelector('[data-queue]')&&!!document.querySelector('.wb-composer__toolbar'),'queue and send toolbar restore') active=true;settled=undefined;await renderComposer();await act(async()=>key('Escape')) check(settled==='rejected','deny completes through real composer') active=false;await renderComposer();check(!stack.hidden,'deny restores editor after terminal interaction') active=true;await renderComposer();await act(async()=>click('[data-option-id="once"]'));const staleResolve=resolve session='session-b';active=false;settled=undefined;await renderComposer();await act(async()=>staleResolve()) check(!document.querySelector('[data-composer-approval]')&&settled===undefined,'session switch unmounts old request and ignores its late result') // Stopping remains a normal-composer/parent action, never an extra approval control. stopFails=true;await renderComposer();await act(async()=>click('.wb-composer__queue-stop')) check(document.querySelector('[role="alert"]')?.textContent?.includes('stop fixture failed'),'normal composer still reports stop failure') stopFails=false;await act(async()=>click('.wb-composer__queue-stop')) check(stops===2,'normal composer stop remains reachable and can retry independently of send') active=true;await renderComposer() const previousApproval=document.querySelector('[data-composer-approval]') check(!document.querySelector('[data-composer-approval] [role="alert"]'),'normal stop failure does not leak into approval surface') check(!document.querySelector('button[aria-label="停止当前运行"],button[aria-label="停止"],.wb-composer__approval-stop'),'approval has no standalone cancellation control') request='request-2';await renderComposer() check(document.querySelector('[data-composer-approval]')!==previousApproval,'replacement request remounts keyed approval wrapper') check(stops===2&&sends===0,'approval takeover and replacement neither stop the run nor send a message') // Parent cancellation removes the pending interaction without depending on approval stop UI. active=false;await renderComposer();check(!document.querySelector('[data-composer-approval]'),'parent-cancelled interaction restores ordinary input') check(!!document.querySelector('.wb-composer__queue-stop')&&!!document.querySelector('[data-queue]'),'normal stop and queue restore after parent cancellation') // An upload already started before takeover must finish into the same draft without stealing focus. let uploaded: typeof attachments=[], stagingResolve!: (value: typeof attachments[number])=>void, uploadActive=false const uploadHost={pick:async()=>[],stage:()=>new Promise(yes=>{stagingResolve=yes})} const renderUpload=()=>act(async()=>root.render({}} onSend={()=>{sends++}} attachments={uploaded} onAttachmentsChange={items=>{uploaded=items}} attachmentHost={uploadHost} approvalKey="upload-approval" approvalSlot={uploadActive?{}}/>:undefined}/>)) await renderUpload() const drop=new Event('drop',{bubbles:true,cancelable:true}) Object.defineProperty(drop,'dataTransfer',{value:{files:[new File(['fixture'],'upload.txt',{type:'text/plain'})],types:[],getData:()=>''}}) await act(async()=>{document.querySelector('.wb-composer')!.dispatchEvent(drop);await new Promise(yes=>setTimeout(yes,30))}) check(typeof stagingResolve==='function','attachment staging started before takeover') uploadActive=true;await renderUpload() await act(async()=>stagingResolve({id:'uploaded',name:'upload.txt',mimeType:'text/plain',size:7})) await renderUpload() check(uploaded.length===1,'in-flight attachment preserved in owning draft during approval') check(!document.querySelector('.conversation-input-area__input-stack')!.contains(document.activeElement),'upload completion does not focus hidden editor') uploadActive=false;await renderUpload() check(document.querySelector('.conversation-input-area__input-stack')?.textContent?.includes('upload.txt'),'completed attachment restored after approval') // Empty drafts restore the ordinary stop button, not a send button, after takeover. const renderEmpty=(approval:boolean)=>act(async()=>root.render({}} onSend={()=>{sends++}} onStop={async()=>{stops++}} approvalKey="empty-approval" approvalSlot={approval?{}}/>:undefined}/>)) await renderEmpty(true) check(!document.querySelector('button[aria-label="停止"],button[aria-label="发送"]'),'empty draft also has no normal stop or send during approval') await renderEmpty(false) check(!!document.querySelector('button[aria-label="停止"]')&&!document.querySelector('button[aria-label="发送"]'),'empty draft restores normal stop without send after approval') await act(async()=>click('button[aria-label="停止"]')) check(stops===3&&sends===0,'restored empty-draft stop still invokes cancellation without sending') await act(async()=>root.unmount());dom.window.close() console.log(`Permission intercept DOM: ${checks} checks passed (not pixel/SDK verification)`) process.exit(0) } main().catch(error=>{console.error(error);process.exit(1)})