fix(composer): show only approval card during authorization

This commit is contained in:
Codex committed 2026-10-21 16:04:52 +09:00
1 parent b4757dd843
commit 4a7f17961d
4 files changed
+32 -34

No files matched your search

+28 -15
View File
@@ -55,8 +55,7 @@ async function main(){
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', stopPending=false
let rejectStop!: (error: Error)=>void
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}]
@@ -65,7 +64,7 @@ async function main(){
const renderComposer=()=>act(async()=>{
const composition=composeComposer({interaction:active?{id:session,kind:'approval',status:'pending'}:null,queueCount:2,running:true})
root.render(<ComposerControls key={session} sessionId={session} value={draft} onChange={()=>{changes++}} onSend={()=>{sends++}}
attachments={attachments} resources={resources} quotes={quotes} onStop={()=>{stops++;if(stopFails)throw new Error('stop fixture failed');if(stopPending)return new Promise<void>((_yes,no)=>{rejectStop=no})}}
attachments={attachments} resources={resources} quotes={quotes} onStop={async()=>{stops++;if(stopFails)throw new Error('stop fixture failed')}}
queueSlot={composition.queue.visible?<div data-queue="true">two queued messages</div>:undefined}
approvalKey={request} approvalSlot={!composition.input.visible?<PermissionConfirm pendingId={session} toolName="Bash" options={options} resolve={pending} reject={async()=>{declined++}} onDecisionComplete={decision=>{settled=decision}}/>:undefined}/>)
})
@@ -75,6 +74,8 @@ async function main(){
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<HTMLElement>('[contenteditable]')!
check(editor?.getAttribute('contenteditable')==='false','rich editor disabled during approval')
@@ -99,18 +100,21 @@ async function main(){
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')
active=true;stopFails=true;await renderComposer();await act(async()=>click('.wb-composer__approval-stop'))
check(document.querySelector('[data-composer-approval] [role="alert"]')?.textContent?.includes('stop fixture failed'),'failed cancellation visible in approval surface')
request='request-2';stopFails=false;await renderComposer()
check(!document.querySelector('[data-composer-approval] [role="alert"]'),'next request clears previous cancellation failure')
await act(async()=>click('.wb-composer__approval-stop'))
check(!document.querySelector('[data-composer-approval] [role="alert"]'),'cancellation retry clears error')
check(stops===2,'stop remains reachable independently of normal send')
stopPending=true;await renderComposer();await act(async()=>click('.wb-composer__approval-stop'))
check(document.querySelector<HTMLButtonElement>('.wb-composer__approval-stop')?.disabled,'pending stop disabled against duplicate cancellation')
request='request-3';stopPending=false;await renderComposer();await act(async()=>rejectStop(new Error('obsolete stop failure')))
check(!document.querySelector('[data-composer-approval] [role="alert"]')&&!document.querySelector<HTMLButtonElement>('.wb-composer__approval-stop')?.disabled,'late previous-request cancellation cannot alter new request')
active=false;await renderComposer();check(!document.querySelector('[data-composer-approval]'),'cancelled interaction restores ordinary input')
// 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<typeof attachments[number]>(yes=>{stagingResolve=yes})}
@@ -129,6 +133,15 @@ async function main(){
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(<ComposerControls key="empty-session" sessionId="empty-session" value="" onChange={()=>{}} onSend={()=>{sends++}} onStop={async()=>{stops++}}
approvalKey="empty-approval" approvalSlot={approval?<PermissionConfirm pendingId="empty-approval" toolName="Bash" options={options} resolve={pending} reject={async()=>{}}/>: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)