59 lines
15 KiB
JavaScript
59 lines
15 KiB
JavaScript
const fs=require('node:fs/promises'), path=require('node:path'), assert=require('node:assert/strict');
|
|
if(!process.versions.electron){(async()=>{
|
|
const dir=await fs.mkdtemp(path.resolve('.tmp/b03-app-'));for(const n of ['data','home','workspace'])await fs.mkdir(path.join(dir,n));
|
|
const net=require('node:net').createServer();await new Promise(r=>net.listen(0,'127.0.0.1',r));const port=net.address().port;await new Promise(r=>net.close(r));
|
|
const model={id:'b03-local',name:'B03 Local Fixture',model:'claude-loopback',providerId:'custom',protocol:'anthropic',authMode:'api-key',apiKeyPlain:'local-fixture-placeholder',baseUrl:`http://127.0.0.1:${port}`,supportsToolCall:true,contextWindowTokens:200000,maxOutputTokens:4096};
|
|
await fs.writeFile(path.join(dir,'data/models.json'),JSON.stringify({version:1,models:[model],selectedModelId:model.id}));
|
|
await fs.writeFile(path.join(dir,'data/workspaces.json'),JSON.stringify({selectedWorkspaceId:'b03-space',workspaces:[{id:'b03-space',name:'QA Workspace',root:path.join(dir,'workspace'),createdAt:Date.now()}]}));
|
|
await fs.writeFile(path.join(dir,'data/config.json'),JSON.stringify({workspaceRoot:path.join(dir,'workspace'),preventSleep:false,autoLaunch:false}));
|
|
for(const phase of ['first','restart']){
|
|
const env={...process.env,YCODE_USER_DATA_DIR:path.join(dir,'data'),YCODE_QA_DIRECTORY:dir,YCODE_SKIP_KERNEL_DATA_RESET:'1',YCODE_DISABLE_GPU:'1',YCODE_DISABLE_SYSTEM_INTEGRATION:'1',HOME:path.join(dir,'home'),USERPROFILE:path.join(dir,'home'),CLAUDE_CODE_GIT_BASH_PATH:'C:/Users/Administrator/scoop/apps/git/current/bin/bash.exe',CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC:'1',DISABLE_TELEMETRY:'1',DISABLE_ERROR_REPORTING:'1',B03_APP_DIR:dir,B03_APP_PORT:String(port),B03_APP_PHASE:phase};
|
|
for(const k of Object.keys(env))if(/^(ANTHROPIC_|CLAUDE_CODE_OAUTH_TOKEN)/.test(k))delete env[k];delete env.ELECTRON_RUN_AS_NODE;delete env.ELECTRON_RENDERER_URL;
|
|
const c=require('node:child_process').spawn(require('electron'),['--no-sandbox','--disable-gpu-sandbox',__filename],{env,windowsHide:true,stdio:['ignore','pipe','pipe']});let log='';c.stdout.on('data',x=>log+=x);c.stderr.on('data',x=>log+=x);const t=setTimeout(()=>c.kill(),120000);const exit=await new Promise(r=>c.on('exit',r));clearTimeout(t);await fs.writeFile(path.join(dir,phase+'-process.txt'),log);console.log(JSON.stringify({directory:dir,phase,exit}));if(exit!==0){console.error(log.slice(-8000));process.exitCode=1;break}
|
|
}
|
|
})().catch(e=>{console.error(e);process.exitCode=1})}else{
|
|
const {app,BrowserWindow,ipcMain}=require('electron'),{pathToFileURL}=require('node:url'),{createServer}=require('node:http');
|
|
const dir=process.env.B03_APP_DIR,phase=process.env.B03_APP_PHASE,workspace=path.join(dir,'workspace');app.disableHardwareAcceleration();app.setPath('home',path.join(dir,'home'));
|
|
let rejectRespondOnce=phase==='first',rejectCancelOnce=phase==='first';const realHandle=ipcMain.handle.bind(ipcMain);ipcMain.handle=(channel,listener)=>realHandle(channel,async(...args)=>{if(channel==='chat:respond'&&rejectRespondOnce){rejectRespondOnce=false;throw Error('QA approval submission failure')}if(channel==='chat:cancel'&&rejectCancelOnce){rejectCancelOnce=false;throw Error('QA cancellation failure')}return listener(...args)});
|
|
let win,stage='start',scenario='full',issued=0,requestCount=0,held,releaseHeld=false;const checks=[],errors=[];let approvals=0;
|
|
const pause=ms=>new Promise(r=>setTimeout(r,ms)),read=s=>win.webContents.executeJavaScript(s);
|
|
async function wait(s){for(let i=0;i<350;i++){const x=await read(s);if(x)return x;await pause(30)}throw Error('Timeout '+s)}
|
|
function check(name,x){checks.push({name,pass:!!x});assert.ok(x,name)}
|
|
async function shot(n){await pause(200);await fs.writeFile(path.join(dir,phase+'-'+n+'.png'),(await win.webContents.capturePage()).toPNG())}
|
|
async function fill(text){await read('document.querySelector(".wb-composer textarea").focus()');await win.webContents.insertText(text);await wait('document.querySelector(".wb-composer textarea").value.includes('+JSON.stringify(text)+')')}
|
|
async function send(text){await fill(text);await wait('!document.querySelector(".cr-input-toolbar__send:not(.cr-send-button--stop)").disabled');await read('document.querySelector(".cr-input-toolbar__send:not(.cr-send-button--stop)").click()')}
|
|
async function settled(){await wait('!document.querySelector(".cr-send-button--stop") && !document.querySelector("[data-composer-approval]")');await pause(200)}
|
|
async function permission(full){await read('document.querySelector(".yc-source-permission-trigger > button").click()');await wait('!!document.querySelector(".cr-permission-panel__row")');await read('document.querySelector(".cr-permission-panel__row").click()');if(full){await wait('!!document.querySelector(".permission-confirm-acknowledgement")');await read('document.querySelector(".permission-confirm-acknowledgement").click()');await wait('!document.querySelector(".wb-composer__danger-button").disabled');await read('document.querySelector(".wb-composer__danger-button").click()')};await wait('!document.querySelector(".cr-permission-panel") && !document.querySelector(".permission-confirm-dialog")')}
|
|
const server=createServer((req,res)=>{if(++requestCount>35){res.writeHead(429).end();return}let raw='';req.on('data',x=>raw+=x);req.on('end',async()=>{
|
|
try{if(!req.url.includes('/messages')){res.writeHead(200).end('{}');return}const body=JSON.parse(raw);if(scenario==='default'&&!releaseHeld){await new Promise(r=>held=r)};
|
|
const steps=scenario==='full'?[{name:'Write',input:{file_path:path.join(workspace,phase+'-written.txt'),content:'verified'}},{name:'PowerShell',input:{command:`[IO.File]::WriteAllText('${phase}-command.txt','verified')`,timeout:5000}}]:scenario==='grant'?[{name:'Bash',input:{command:'git init app-grant-repo',timeout:5000}}]:[{name:'Write',input:{file_path:path.join(workspace,'default-must-not-write.txt'),content:'blocked'}}];
|
|
const step=steps[issued++];const emit=(event,data)=>res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);res.writeHead(200,{'content-type':'text/event-stream'});
|
|
emit('message_start',{type:'message_start',message:{id:'msg_'+requestCount,type:'message',role:'assistant',model:body.model,content:[],stop_reason:null,stop_sequence:null,usage:{input_tokens:10,output_tokens:1}}});emit('content_block_start',{type:'content_block_start',index:0,content_block:step?{type:'tool_use',id:'tool_'+requestCount,name:step.name,input:{}}:{type:'text',text:''}});emit('content_block_delta',{type:'content_block_delta',index:0,delta:step?{type:'input_json_delta',partial_json:JSON.stringify(step.input)}:{type:'text_delta',text:'B03 fixture complete.'}});emit('content_block_stop',{type:'content_block_stop',index:0});emit('message_delta',{type:'message_delta',delta:{stop_reason:step?'tool_use':'end_turn',stop_sequence:null},usage:{output_tokens:5}});emit('message_stop',{type:'message_stop'});res.end();}catch(e){res.destroy(e)}
|
|
})});
|
|
const timer=setTimeout(()=>{console.error('WATCHDOG',stage);app.exit(1)},100000);
|
|
;(async()=>{await new Promise(r=>server.listen(Number(process.env.B03_APP_PORT),'127.0.0.1',r));await import(pathToFileURL(path.resolve('out/main/index.mjs')).href);await app.whenReady();win=BrowserWindow.getAllWindows()[0];win.setSize(1336,902);win.showInactive();win.webContents.on('console-message',(_e,l,m)=>{if(l>=3)errors.push(m)});
|
|
await wait('!!window.ycode && !!document.querySelector(".wb-composer textarea")');await pause(500);
|
|
if(phase==='first'){
|
|
stage='first-full-selection';check('initial home permission defaults',!(await read('document.querySelector(".yc-source-permission-trigger > button").textContent')).includes('完全访问'));await permission(true);check('full selected before first send',(await read('document.querySelector(".yc-source-permission-trigger > button").textContent')).includes('完全访问'));
|
|
}else{
|
|
stage='restart-load';const sessionId=await fs.readFile(path.join(dir,'session-id.txt'),'utf8');const sessionSelector=JSON.stringify('[data-session-id="'+sessionId+'"]');await wait('!!document.querySelector('+sessionSelector+')');await read('document.querySelector('+sessionSelector+').click()');await wait('!document.querySelector(".yc-session-skeleton") && !!document.querySelector(".wb-composer textarea")');check('restart restores saved full label',(await read('document.querySelector(".yc-source-permission-trigger > button").textContent')).includes('完全访问'));
|
|
}
|
|
await read('window.__b03Observer=new MutationObserver(()=>{window.__b03Approvals=(window.__b03Approvals||0)+(document.querySelector("[data-composer-approval]")?1:0)});window.__b03Observer.observe(document.body,{subtree:true,childList:true});true');
|
|
stage='full-send';await send('B03_'+phase+'_FULL');for(let i=0;issued<3&&i<600;i++)await pause(30);assert.ok(issued>=3,'three loopback model requests completed');await wait('document.querySelector(".chat-thread")?.textContent.includes("B03 fixture complete.")');await settled();check('full write executed',(await fs.readFile(path.join(workspace,phase+'-written.txt'),'utf8'))==='verified');check('full PowerShell executed',(await fs.readFile(path.join(workspace,phase+'-command.txt'),'utf8'))==='verified');check('no approval card under full',!(await read('window.__b03Approvals||0')));
|
|
const sessions=await read('window.ycode.session.list()');const current=sessions.find(s=>s.permissionMode==='full-access');check('persisted full permission',!!current);await fs.writeFile(path.join(dir,'session-id.txt'),current.id);await shot('full-completed');
|
|
if(phase==='first'){
|
|
stage='default-selection';await permission(false);scenario='default';issued=0;await send('B03_DEFAULT_DENY');while(!held)await pause(30);await fill('PRESERVED_DRAFT');releaseHeld=true;held();
|
|
stage='approval';await wait('!!document.querySelector("[data-composer-approval]")');check('normal input hidden',await read('document.querySelector(".conversation-input-area__input-stack").hidden && getComputedStyle(document.querySelector(".conversation-input-area__input-stack")).display==="none"'));check('normal input inert',await read('document.querySelector(".conversation-input-area__input-stack").hasAttribute("inert")'));check('normal toolbar absent',await read('!document.querySelector(".wb-composer__toolbar")'));check('only one interactive permission dialog',await read('document.querySelectorAll(".conversation-intercept-card[role=dialog]").length===1'));await shot('bottom-approval');
|
|
check('approval receives native focus',await read('!!document.activeElement.closest("[data-composer-approval]")'));await read('document.querySelector(".wb-composer textarea").focus()');check('hidden native editor cannot receive focus',await read('!document.querySelector(".conversation-input-area__input-stack").contains(document.activeElement)'));win.webContents.sendInputEvent({type:'keyDown',keyCode:'X'});win.webContents.sendInputEvent({type:'char',keyCode:'x'});win.webContents.sendInputEvent({type:'keyUp',keyCode:'X'});await pause(80);check('native typing cannot change hidden draft',(await read('document.querySelector(".wb-composer textarea").value'))==='PRESERVED_DRAFT');
|
|
await read('document.querySelector(".conversation-intercept-card__option--reject").click()');await wait('document.querySelector("[data-composer-approval] [role=alert]")?.textContent.includes("QA approval submission failure")');check('failed approval stays in replacement',await read('document.querySelector(".conversation-input-area__input-stack").hidden && !document.querySelector(".wb-composer__toolbar")'));await shot('approval-failure');
|
|
await read('document.querySelector(".conversation-intercept-card__option--reject").click()');await wait('!document.querySelector("[data-composer-approval]")');await settled();check('draft restored after denial',(await read('document.querySelector(".wb-composer textarea").value'))==='PRESERVED_DRAFT');check('normal toolbar restored',await read('!!document.querySelector(".wb-composer__toolbar")'));let absent=false;try{await fs.stat(path.join(workspace,'default-must-not-write.txt'))}catch(e){absent=e.code==='ENOENT'}check('default denied actual write',absent);await shot('draft-restored');
|
|
stage='grant-first';scenario='grant';issued=0;await send('B03_GRANT_FIRST');await wait('!!document.querySelector("[data-composer-approval] [data-option-id=allow-always]")');await shot('session-grant');await read('document.querySelector("[data-option-id=allow-always]").click()');for(let i=0;issued<2&&i<500;i++)await pause(30);await settled();check('UI session grant executes command',(await fs.stat(path.join(workspace,'app-grant-repo/.git'))).isDirectory());
|
|
stage='grant-next';issued=0;await read('window.__b03Approvals=0');await send('B03_GRANT_NEXT');for(let i=0;issued<2&&i<500;i++)await pause(30);check('grant next turn reaches completion',issued>=2);await settled();check('session grant prevents repeated approval',!(await read('window.__b03Approvals||0')));stage='cancel-failure';scenario='default';issued=0;await send('B03_CANCEL');await wait('!!document.querySelector("[data-composer-approval]")');await read('document.querySelector(".wb-composer__approval-stop").click()');await wait('document.querySelector("[data-composer-approval] [role=alert]")?.textContent.includes("QA cancellation failure")');check('failed cancel error visible in replacement',await read('document.querySelector(".conversation-input-area__input-stack").hidden'));await shot('cancel-failure');await read('document.querySelector(".wb-composer__approval-stop").click()');await settled();check('cancel retry restores normal input',await read('!document.querySelector(".conversation-input-area__input-stack").hidden && !!document.querySelector(".wb-composer__toolbar")'));await permission(true);
|
|
}
|
|
if(phase==='restart'){
|
|
stage='grant-expired-after-restart';await permission(false);scenario='grant';issued=0;await send('B03_GRANT_RESTART');await wait('!!document.querySelector("[data-composer-approval] [data-option-id=allow-always]")');check('session grant expires after process restart',true);await shot('grant-reapproval-after-restart');await read('document.querySelector(".conversation-intercept-card__option--reject").click()');for(let i=0;issued<2&&i<500;i++)await pause(30);await settled();
|
|
}
|
|
check('zero renderer console errors',errors.length===0);await fs.writeFile(path.join(dir,phase+'-report.json'),JSON.stringify({codeSha:'adbbb3bd37f6043d10cbaa59d6c28ade6387a7f3',scope:'Unchanged production app + main/preload/renderer + real Windows SDK/CLI with deterministic local SSE; fresh data; actual UI permission and send',checks,requests:requestCount,errors},null,2));clearTimeout(timer);server.closeAllConnections();server.close();app.exit(0);
|
|
})().catch(async e=>{console.error(stage,e);if(win)await shot('failure').catch(()=>{});await fs.writeFile(path.join(dir,phase+'-failure.json'),JSON.stringify({stage,message:String(e),checks,errors},null,2));server.closeAllConnections();server.close();app.exit(1)})
|
|
}
|