chore: establish YCode replication baseline and TODO

This commit is contained in:
xiaohan.yuan committed 2026-10-17 02:43:49 +08:00
commit 6dc59f89ba
1118 files changed
+196002

No files matched your search

+78
View File
@@ -0,0 +1,78 @@
// Production renderer/preload/IPC -> actual OMP -> controlled localhost tools -> expert packages.
const fs=require('node:fs/promises'),path=require('node:path'),assert=require('node:assert/strict'),crypto=require('node:crypto')
const {pathToFileURL}=require('node:url'),pause=ms=>new Promise(resolve=>setTimeout(resolve,ms))
const ID='flow-local-expert',CREATE='帮我创建一个 XXX 专家,擅长 XXXXX。我的经验是:[请补充你的行业背景、相关经验]',EDIT=`帮我修改专家:[${ID}],增加/优化[请补充你希望新增/优化的技能或知识领域等]方面的能力`
const ROLE='EXPERT_FLOW_ROLE_V1: Analyse supplied facts and identify missing evidence.',UPDATED='EXPERT_FLOW_ROLE_V2: Analyse supplied facts, identify missing evidence, and verify conclusions.'
function sourcePackage(){const p={name:ID,version:'1.0.0',description:'Controlled source-schema expert for isolated product integration.',expertType:'agent',agentName:ID,plugin:ID,agents:[`./agents/${ID}.md`],displayName:{en:'Flow Expert',zh:'验收专家'},profession:{en:'Acceptance Analyst',zh:'验收分析师'},displayDescription:{en:'Controlled analyst who follows the registered workflow.',zh:'这位分析师依据明确输入整理事实检查证据解释限制核对输出内容并在完成任务后准确说明结果与待确认事项'},defaultInitPrompt:{en:'Analyse the supplied facts.',zh:'请分析提供的事实。'},tags:[1,2,3].map(n=>({en:'Capability '+n,zh:'能力'+n})),quickPrompts:[{en:'Analyse the supplied facts.',zh:'请分析提供的事实。'},...[2,3].map(n=>({en:'Question '+n,zh:'问题'+n}))]};return{'.codebuddy-plugin/plugin.json':JSON.stringify(p,null,2),[`agents/${ID}.md`]:`---\nname: ${ID}\ndescription: Controlled local acceptance analyst\ndisplayName:\n en: Flow Expert\n zh: 验收专家\nprofession:\n en: Acceptance Analyst\n zh: 验收分析师\nmaxTurns: 50\n---\n\n${ROLE}\n`,'README.md':'# Controlled local expert\nNo remote service or generated avatar is claimed.\n'}}
if(!process.versions.electron){
const {spawn}=require('node:child_process')
;(async()=>{
const directory=await fs.mkdtemp(path.resolve('.tmp/expert-flows-'));for(const p of['data','home','sessions','workspace'])await fs.mkdir(path.join(directory,p))
const omp=process.env.YCODE_OMP_PATH||path.join(process.env.LOCALAPPDATA,'Ygent','omp.exe');assert.ok((await fs.stat(omp)).isFile())
const env={...process.env,YCODE_EXPERT_FLOW_DIRECTORY:directory,YCODE_USER_DATA_DIR:path.join(directory,'data'),YCODE_OMP_HOME:path.join(directory,'home'),YCODE_OMP_PATH:omp,YCODE_QA_OFFSCREEN:'1',YCODE_DISABLE_GPU:'1'}
delete env.ELECTRON_RUN_AS_NODE;delete env.ELECTRON_RENDERER_URL;for(const key of Object.keys(env))if(key.startsWith('YCODE_MODEL_'))delete env[key]
assert.equal(env.ELECTRON_RUN_AS_NODE,undefined,'Must not recursively run the Node branch inside Electron')
for(const stage of['run','restart']){
const code=await new Promise(resolve=>{const child=spawn(require('electron'),['--no-sandbox','--disable-gpu-sandbox',__filename],{env:{...env,YCODE_EXPERT_FLOW_STAGE:stage},windowsHide:true,stdio:['ignore','pipe','pipe']});let output='';child.stdout.on('data',c=>{output+=c;if(c.toString().includes('EXPERT_FLOW_'))process.stdout.write(c)});child.stderr.on('data',c=>output+=c);const timer=setTimeout(()=>child.kill(),180000);child.once('exit',async code=>{clearTimeout(timer);await fs.writeFile(path.join(directory,stage+'-process.log'),output);resolve(code??1)})})
const failure=await fs.stat(path.join(directory,stage+'-failure.json')).then(()=>true,()=>false),report=await fs.readFile(path.join(directory,stage+'-report.json'),'utf8').then(JSON.parse,()=>null)
if(code||failure||report?.result!=='PASS'||!report.checks?.length){console.log(JSON.stringify({directory,stage,result:'FAIL'}));process.exitCode=1;return}
}
const reports=await Promise.all(['run','restart'].map(async stage=>({stage,...JSON.parse(await fs.readFile(path.join(directory,stage+'-report.json'),'utf8'))})))
await fs.writeFile(path.join(directory,'report.json'),JSON.stringify({result:'PASS',reports,limitations:['Controlled localhost upstream returns real OMP tool calls; no remote account or model cost.','Production UI DOM events are used, no store mutation or direct expert package IPC writes.','Whole-state pixel parity, avatars and team execution are outside this integration check.']},null,2))
console.log(JSON.stringify({directory,result:'PASS',checks:reports.map(r=>({stage:r.stage,count:r.checks.length}))}))
})().catch(e=>{console.error(e);process.exitCode=1})
}else{
const{app,BrowserWindow}=require('electron'),{createServer}=require('node:http'),directory=process.env.YCODE_EXPERT_FLOW_DIRECTORY,stage=process.env.YCODE_EXPERT_FLOW_STAGE
assert.ok(directory&&path.relative(path.resolve('.tmp'),directory).startsWith('expert-flows-'));assert.ok(['run','restart'].includes(stage))
app.disableHardwareAcceleration();app.setPath('userData',path.join(directory,'data'));app.setPath('home',path.join(directory,'home'));app.setPath('sessionData',path.join(directory,'sessions'))
let main,upstream,phase='boot';const checks=[],requests=[]
const check=(label,actual,expected=true)=>{assert.deepEqual(actual,expected,label);checks.push({label,pass:true})}
async function read(expression){const r=await main.webContents.executeJavaScript('(async()=>{try{return{ok:true,value:await ('+expression+')}}catch(e){return{ok:false,error:String(e),stack:e.stack}}})()');if(!r.ok)throw Error(r.error+'\n'+r.stack);return r.value}
async function wait(expression,label,timeout=30000){const start=Date.now();while(Date.now()-start<timeout){const value=await read(expression);if(value)return value;await pause(40)}throw Error('Timeout: '+label)}
async function click(selector){if(selector.startsWith('.cr-send-button')){const registry=await read('window.ycode.models.list()');assert.ok(registry.models.every(m=>new URL(m.baseUrl).hostname==='127.0.0.1'),'QA prohibits sending with any non-localhost model profile')}await read(`(()=>{const e=document.querySelector(${JSON.stringify(selector)});if(!e||e.disabled)throw Error('Missing control '+${JSON.stringify(selector)});e.click()})()`);await pause(70)}
async function textClick(selector,text,exact=true){await read(`(()=>{const t=${JSON.stringify(text)},e=[...document.querySelectorAll(${JSON.stringify(selector)})].find(e=>${exact?'e.textContent.trim()===t':'e.textContent.includes(t)'});if(!e||e.disabled)throw Error('Missing text '+t);e.click()})()`);await pause(70)}
async function fill(selector,value){await read(`(()=>{const e=document.querySelector(${JSON.stringify(selector)});if(!e||e.disabled)throw Error('Missing editable input');Object.getOwnPropertyDescriptor(e.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype,'value').set.call(e,${JSON.stringify(value)});e.dispatchEvent(new Event('input',{bubbles:true}))})()`);await pause(50)}
async function hoverAddMenuItem(id){const selector='.cr-add-menu__popover [data-add-menu-id="'+id+'"]';await wait('!!document.querySelector('+JSON.stringify(selector)+')','source add menu row');const point=await read('(()=>{const b=document.querySelector('+JSON.stringify(selector)+').getBoundingClientRect();return{x:Math.round(b.x+b.width/2),y:Math.round(b.y+b.height/2)}})()');const trigger=await read('(()=>{const b=document.querySelector(".wb-composer__add-button").getBoundingClientRect();return{x:Math.round(b.x+b.width/2),y:Math.round(b.y+b.height/2)}})()');main.webContents.sendInputEvent({type:'mouseMove',...trigger});await pause(25);main.webContents.sendInputEvent({type:'mouseMove',...point});await pause(220)}
async function shot(name){await read('document.fonts.ready.then(()=>true)');await pause(120);await fs.writeFile(path.join(directory,stage+'-'+name+'.png'),(await main.webContents.capturePage(undefined,{stayHidden:true,stayAwake:true})).toPNG())}
async function step(name){phase=name;await fs.writeFile(path.join(directory,stage+'-phase.txt'),name);console.log('EXPERT_FLOW_STEP '+stage+' '+name)}
async function boot(){await import(pathToFileURL(path.resolve('out/main/index.mjs')).href);await app.whenReady();for(let i=0;i<300;i++){main=BrowserWindow.getAllWindows().find(w=>w.getTitle()==='YCode');if(main&&!main.webContents.isLoading())break;await pause(30)}assert.ok(main);main.setContentSize(1336,901);await wait('!!window.ycode&&!!document.querySelector(".wb-composer__input")','production UI');const info=await read('window.ycode.app.info()');check('isolated production configDir',path.resolve(info.configDir),path.join(directory,'data'));check('actual OMP available',info.omp.available);if(stage==='run'){const registry=await read('window.ycode.models.list()');for(const model of registry.models)await read(`window.ycode.models.remove(${JSON.stringify(model.id)})`);const empty=await read('window.ycode.models.list()');check('real production models.remove clears isolated legacy migration',empty.models.length,0);check('no selected model remains after actual removal',empty.selectedModelId,null)}}
function complete(res,model,content,tool){res.writeHead(200,{'content-type':'text/event-stream'});const chunk=delta=>({id:'expert-flow-response-'+requests.length,object:'chat.completion.chunk',created:1700000000,model,choices:[{index:0,delta,finish_reason:null}]});res.write('data: '+JSON.stringify(chunk({role:'assistant',...(tool?{tool_calls:[{index:0,id:'expert-flow-'+requests.length,type:'function',function:{name:tool.name,arguments:JSON.stringify(tool.args)}}]}:{content})}))+'\n\n');const final=chunk({});final.choices[0].finish_reason=tool?'tool_calls':'stop';res.write('data: '+JSON.stringify(final)+'\n\ndata: [DONE]\n\n');res.end()}
async function startUpstream(){upstream=createServer(async(req,res)=>{try{assert.equal(req.url,'/v1/chat/completions');let raw='';for await(const c of req)raw+=c;const body=JSON.parse(raw),names=(body.tools??[]).map(t=>t.function.name),messages=body.messages??[],lastUser=messages.findLastIndex(m=>m.role==='user'),user=JSON.stringify(messages[lastUser]?.content??''),all=JSON.stringify(messages),results=messages.slice(lastUser+1).filter(m=>m.role==='tool'),mode=user.includes('帮我修改专家')?'edit':user.includes('帮我创建一个 XXX 专家')?'create':user.includes('EXPERT_FLOW')?'role':'unknown'
const observed={route:req.url,stream:body.stream,mode,toolResults:results.length,tools:names,hasOriginalManager:all.includes('WorkBuddy 专家包管理器')&&all.includes('scripts/register_expert.py'),hasHostAdapter:all.includes('ycode-expert-manager-host'),hasRoleV1:all.includes(ROLE),hasRoleV2:all.includes(UPDATED),hasActualToolResult:results.length>0};requests.push(observed);assert.ok(requests.length<=40,'Unexpected OMP request loop')
if(!body.stream){res.writeHead(200,{'content-type':'application/json'});res.end(JSON.stringify({id:'test',object:'chat.completion',choices:[{index:0,message:{role:'assistant',content:'OK'},finish_reason:'stop'}]}));return}
if(mode==='role'){complete(res,body.model,stage==='restart'?'EXPERT_FLOW_RESTART_OK':'EXPERT_FLOW_ROLE_OK');return}
assert.ok(['create','edit'].includes(mode),'Unknown controlled task');assert.ok(observed.hasOriginalManager&&observed.hasHostAdapter,'Manager source/adapter missing');assert.ok(names.includes('ycode_expert_package'),'Real package tool absent')
const steps=mode==='create'?[{name:'ycode_read_skill_resource',args:{id:'builtin:expert-manager',path:'references/plugin-json-spec.md'}},{name:'ycode_read_skill_resource',args:{id:'builtin:expert-manager',path:'references/agent-md-spec.md'}},{name:'ycode_expert_package',args:{id:ID,action:'read'}},{name:'ycode_expert_package',args:{id:ID,action:'write',files:sourcePackage()}},{name:'ycode_expert_package',args:{id:ID,action:'validate'}},{name:'ycode_expert_package',args:{id:ID,action:'register'}}]:[{name:'ycode_expert_package',args:{id:ID,action:'read'}},{name:'ycode_expert_package',args:{id:ID,action:'write',files:{[`agents/${ID}.md`]:sourcePackage()[`agents/${ID}.md`].replace(ROLE,UPDATED)}}},{name:'ycode_expert_package',args:{id:ID,action:'validate'}},{name:'ycode_expert_package',args:{id:ID,action:'register'}}]
const next=steps[results.length];observed.nextAction=next?.args.action??next?.args.path??'complete';complete(res,body.model,mode==='create'?'EXPERT_FLOW_CREATED':'EXPERT_FLOW_EDITED',next)
}catch(error){requests.push({error:String(error)});if(!res.headersSent)res.writeHead(500);res.end('Controlled upstream failure')}});await new Promise(resolve=>upstream.listen(0,'127.0.0.1',resolve))}
async function configure(retrySessionId){const profile={id:'expert-flow-model',name:'Expert Flow Fixture',providerId:'custom',model:'expert-flow-fixture',baseUrl:`http://127.0.0.1:${upstream.address().port}/v1`,apiKey:'',authMode:'none',supportsToolCall:true,supportsImages:false,maxInputTokens:64000,maxOutputTokens:4000,maxToolRounds:20};await read(`window.ycode.models.save(${JSON.stringify(profile)},${stage==='restart'})`);await read('window.ycode.models.select("expert-flow-model")');if(stage==='run')await read(`window.ycode.workspace.attach(${JSON.stringify(path.join(directory,'workspace'))})`);if(retrySessionId){await read(`window.ycode.session.updateContext(${JSON.stringify(retrySessionId)},{modelId:'expert-flow-model'})`);check('actual failed session model context configured',(await read('window.ycode.session.list()')).find(s=>s.id===retrySessionId).modelId,'expert-flow-model')}else{main.webContents.reload();await pause(400);await wait('!!document.querySelector(".wb-composer__input")','profile UI reload')}}
async function centerMine(){await textClick('.sidebar__tab','专家·技能·连接器');await wait('!!document.querySelector(".yc-capability-center")','center');if(!(await read('!!document.querySelector("[data-expert-page=mine]")'))){if(await read('[...document.querySelectorAll(".um-tab")].some(e=>e.textContent.trim()==="专家")'))await textClick('.um-tab','专家');await textClick('button','我的专家')}await wait('!!document.querySelector("[data-expert-page=mine]")','source mine page')}
async function terminal(id,expected){const start=Date.now();let approval=0;while(Date.now()-start<90000){if(await read('[...document.querySelectorAll("button")].some(e=>!e.disabled&&e.textContent.includes("允许一次"))')){await shot('approval-'+(++approval));await textClick('button','允许一次',false)}const messages=await read(`window.ycode.session.messages(${JSON.stringify(id)})`),assistant=messages.findLast(m=>m.role==='assistant');if(assistant&&assistant.status!=='streaming'){check('terminal actual OMP '+expected,assistant.status,'done');check('actual model result '+expected,assistant.content,expected);return{assistant,messages}}await pause(80)}throw Error('Timeout actual OMP '+expected)}
async function sendNew(expected){const ids=(await read('window.ycode.session.list()')).map(s=>s.id);await click('.cr-send-button:not(.cr-send-button--stop)');const created=await wait(`window.ycode.session.list().then(s=>s.find(x=>!${JSON.stringify(ids)}.includes(x.id)))`,'new UI-created session');return{session:created,...await terminal(created.id,expected)}}
async function summon(expectNew=true){await wait(`!!document.querySelector('.ec-cr-card')&&[...document.querySelectorAll('.ec-cr-card')].some(e=>e.textContent.includes('验收分析师'))`,'real source CreatedExpertsPanel card');const point=await read(`(()=>{const r=document.querySelector('.ec-cr-card').getBoundingClientRect();return{x:Math.round(r.right-40),y:Math.round(r.top+30)}})()`);main.webContents.sendInputEvent({type:'mouseMove',...point});await pause(200);await wait(`(()=>{const e=[...document.querySelectorAll('.ec-cr-card button')].find(e=>e.textContent.trim()==='召唤');if(!e||e.disabled)return false;for(let n=e;n&&n!==document.body;n=n.parentElement){const s=getComputedStyle(n);if(s.display==='none'||s.visibility==='hidden'||Number(s.opacity)===0)return false}return e.getBoundingClientRect().width>0})()`,'source summon action visible after native hover');check('source summon action revealed by native hover',true);await shot('my-experts-card');await textClick('.ec-cr-card button','召唤');await wait(`!!document.querySelector('[data-active-expert="${ID}"]')`,'expert bound to new draft');if(expectNew)check('summon selects unsent draft',await read('!document.querySelector(".sidebar__item[data-selected=true]")'));await shot('summoned-draft')}
async function cleanup(){if(upstream){upstream.closeAllConnections();await new Promise(resolve=>upstream.close(resolve))}}
;(async()=>{
await boot();await startUpstream();if(stage==='restart')await configure();const buildSha256=crypto.createHash('sha256').update(await fs.readFile('out/main/index.mjs')).digest('hex')
if(stage==='restart'){
const prior=JSON.parse(await fs.readFile(path.join(directory,'run-report.json'),'utf8'));await step('restart-history');const experts=await read('window.ycode.experts.list()');check('package discovered after real app restart',experts.some(e=>e.id===ID&&!e.error));const history=await read(`window.ycode.session.messages(${JSON.stringify(prior.roleSessionId)})`);check('prior assistant identity persists',history.findLast(m=>m.role==='assistant').assistantDisplay?.id,ID);check('prior role reply persists',history.findLast(m=>m.role==='assistant').content,'EXPERT_FLOW_ROLE_OK')
await click(`[data-session-id="${prior.roleSessionId}"]`);await wait('[...document.querySelectorAll(".cr-agent__name")].some(e=>e.textContent==="验收分析师")','restored source chat identity');await shot('history-identity')
await centerMine();await summon();await fill('.wb-composer__input','EXPERT_FLOW_RESTART_VERIFY');const role=await sendNew('EXPERT_FLOW_RESTART_OK');check('updated role reaches actual upstream after restart',requests.some(r=>r.mode==='role'&&r.hasRoleV2));check('new reply persists correct identity',role.assistant.assistantDisplay?.id,ID);await shot('new-reply-identity');await fs.writeFile(path.join(directory,stage+'-report.json'),JSON.stringify({result:'PASS',checks,requests,buildSha256,sessionId:role.session.id},null,2))
}else{
await step('source-create-entry');await centerMine();await shot('mine-empty');await textClick('button','创建专家');await wait(`document.querySelector('.wb-composer__input')?.value===${JSON.stringify(CREATE)}`,'original create prompt after owner commit');check('original create prompt',await read('document.querySelector(".wb-composer__input").value'),CREATE);check('original manager structured skill chip',await read('!!document.querySelector(\'[data-skill-uri="skill://expert-manager"]\')'));await shot('create-source-draft')
await step('actual-no-model-preflight');check('controlled model remains unconfigured before first send',(await read('window.ycode.models.list()')).models.some(m=>m.id==='expert-flow-model'),false);const beforeFailure=(await read('window.ycode.session.list()')).map(s=>s.id);await click('.cr-send-button:not(.cr-send-button--stop)');const failedSession=await wait(`window.ycode.session.list().then(s=>s.find(x=>!${JSON.stringify(beforeFailure)}.includes(x.id)))`,'real session:create before failed chat:send');await wait(`document.querySelector('[role=alert]')?.textContent.includes('请先添加并选择自定义模型')`,'real missing-model preflight rejection');check('failed session becomes actual visible owner',await read(`document.querySelector('.sidebar__item[data-selected=true]')?.dataset.sessionId`),failedSession.id);check('preflight failure retains exact create text',await read('document.querySelector(".wb-composer__input").value'),CREATE);check('preflight failure retains manager chip for new session owner',await read('!!document.querySelector(\'[data-skill-uri="skill://expert-manager"]\')'));check('preflight stores zero conversation messages',(await read(`window.ycode.session.messages(${JSON.stringify(failedSession.id)})`)).length,0);check('preflight makes no upstream model request',requests.length,0);await shot('no-model-preflight-retained')
await configure(failedSession.id);check('model setup keeps same owner draft text',await read('document.querySelector(".wb-composer__input").value'),CREATE);check('model setup keeps same owner skill chip',await read('!!document.querySelector(\'[data-skill-uri="skill://expert-manager"]\')'))
await step('actual-omp-create');await click('.cr-send-button:not(.cr-send-button--stop)');const creation={session:failedSession,...await terminal(failedSession.id,'EXPERT_FLOW_CREATED')};check('successful retry reuses actual failed session',(await read('window.ycode.session.list()')).length,beforeFailure.length+1);const creationTools=creation.assistant.tools.filter(t=>t.name==='ycode_expert_package');check('actual OMP package actions order',creationTools.map(t=>t.args.action),['read','write','validate','register']);check('create skill URI persisted',creation.messages[0].skills[0].uri,'skill://expert-manager');check('original full manager reached actual upstream',requests.some(r=>r.mode==='create'&&r.hasOriginalManager&&r.hasHostAdapter));check('actual register tool returned success',creationTools.at(-1).output.includes('"success":true'));await shot('created-chat')
const root=path.join(directory,'data','plugins','marketplaces','my-experts'),market=JSON.parse(await fs.readFile(path.join(root,'.codebuddy-plugin','marketplace.json'),'utf8'));check('actual marketplace package registered',market.plugins.some(p=>p.name===ID&&p.source===`./plugins/${ID}`));check('actual package session marker',await fs.readFile(path.join(root,'plugins',ID,'.created-by-session'),'utf8'),creation.session.id)
await step('source-card-summon');await centerMine();await summon();await fill('.wb-composer__input','EXPERT_FLOW_ROLE_VERIFY');const role=await sendNew('EXPERT_FLOW_ROLE_OK');check('real session expert binding',role.session.expertId,ID);check('expert role enters real OMP upstream',requests.some(r=>r.mode==='role'&&r.hasRoleV1));check('actual assistant identity',role.assistant.assistantDisplay?.id,ID);await wait('[...document.querySelectorAll(".cr-agent__name")].some(e=>e.textContent==="验收分析师")','source assistant header');await shot('role-chat')
await step('source-plus-menu-interactions');await fs.writeFile(path.join(directory,'workspace','reference-notes.md'),'# Controlled reference fixture');await click('.wb-composer__add-button');await wait('!!document.querySelector("[data-add-menu-id=reference-files]")','real workspace references loaded');check('source plus trigger expands actual parent',await read('document.querySelector(".wb-composer__add-button").getAttribute("aria-expanded")'),'true');check('source plus parent width',await read('Math.round(document.querySelector(".cr-add-menu__popover").getBoundingClientRect().width)'),200);check('source plus menu row height',await read('Math.round(document.querySelector(".cr-add-menu__popover [data-add-menu-id=experts]").getBoundingClientRect().height)'),32);await hoverAddMenuItem('add-files');await wait('!!document.querySelector(".cr-files-submenu")','native files submenu');check('files hover keeps parent open',await read('!!document.querySelector(".cr-add-menu__popover")'));await hoverAddMenuItem('mode');await wait('document.querySelectorAll(".cr-mode-submenu .wb-switch__input").length===2','source mode switches');await click('.cr-mode-submenu .wb-switch__input');await wait(`window.ycode.session.list().then(s=>s.find(x=>x.id===${JSON.stringify(role.session.id)})?.agentMode==='plan')`,'native mode persists plan');check('Plan clears expert binding',(await read(`window.ycode.session.list()`)).find(s=>s.id===role.session.id).expertId,null);check('mode toggle keeps parent open',await read('!!document.querySelector(".cr-add-menu__popover")'));await click('.cr-mode-submenu .wb-switch__input');await wait(`window.ycode.session.list().then(s=>s.find(x=>x.id===${JSON.stringify(role.session.id)})?.agentMode==='craft')`,'source same Plan toggle restores craft');await hoverAddMenuItem('skills');await wait('!!document.querySelector(".yc-skill-picker[data-embedded] input")','embedded skill submenu');check('skill submenu mounted inside source overlay',await read('!!document.querySelector(".cr-sub-menu__overlay .yc-skill-picker")'));await fill('.yc-skill-picker input','NO_MATCH');main.webContents.sendInputEvent({type:'keyDown',keyCode:'ESC'});main.webContents.sendInputEvent({type:'keyUp',keyCode:'ESC'});await wait('document.querySelector(".yc-skill-picker input")?.value===""','Escape clears search without dismissing parent');check('nonempty search Escape retains parent',await read('!!document.querySelector(".cr-add-menu__popover")'));await hoverAddMenuItem('connectors');await wait('!!document.querySelector(".cr-connectors-submenu")','source connector submenu');check('connector menu includes real manage action',await read('!!document.querySelector(".cr-connectors-submenu__footer")'));await shot('source-plus-connectors');await hoverAddMenuItem('reference-files');await wait('!!document.querySelector(".cr-reference-files-submenu__item")','actual reference choices');await textClick('.cr-reference-files-submenu__item','reference-notes.md');await wait('!!document.querySelector(".wb-composer__resources")','structured workspace reference');check('reference selection closes whole parent',await read('document.querySelector(".wb-composer__add-button").getAttribute("aria-expanded")'),'false');check('actual reference filename present',await read('document.querySelector(".wb-composer__resources").textContent.includes("reference-notes.md")'));await click('.wb-composer__resources button');await click('.wb-composer__add-button');await hoverAddMenuItem('experts');await click('.yc-expert-picker [data-expert-id=flow-local-expert]');await wait('!!document.querySelector("[data-active-expert]")&&!document.querySelector(".yc-expert-picker")','native source picker restores expert');check('expert restore resumes actual craft',(await read('window.ycode.session.list()')).find(s=>s.id===role.session.id).agentMode,'craft');await fill('.wb-composer__input','');
await step('expert-usage-existing-conversation');const countBeforeReturn=(await read('window.ycode.session.list()')).length;await fill('.wb-composer__input','USER_DRAFT_KEEP_AFTER_EXPERT_SWITCH');await click('.wb-composer__add-button');await hoverAddMenuItem('experts');await wait('!!document.querySelector(".yc-expert-picker [data-expert-manage]")','expert more action');await click('.yc-expert-picker [data-expert-manage]');await wait('!!document.querySelector("[data-expert-page=mine]")','return-aware expert center');await summon(false);check('expert center return retains original conversation',await read('document.querySelector(".sidebar__item[data-selected=true]")?.dataset.sessionId'),role.session.id);check('expert center return preserves typed draft',await read('document.querySelector(".wb-composer__input").value'),'USER_DRAFT_KEEP_AFTER_EXPERT_SWITCH');check('expert summon does not create empty sessions',(await read('window.ycode.session.list()')).length,countBeforeReturn);
async function removeExpertChip(){const point=await read('(()=>{const e=document.querySelector("[data-active-expert]"),b=e.getBoundingClientRect();return{x:Math.round(b.x+8),y:Math.round(b.y+b.height/2)}})()');main.webContents.sendInputEvent({type:'mouseMove',...point});await pause(160);await click('[data-active-expert] button');await wait(`window.ycode.session.list().then(s=>s.find(x=>x.id===${JSON.stringify(role.session.id)})?.expertId===null)`,'expert removed through source chip')}
async function pickExistingExpert(){await click('.wb-composer__add-button');await hoverAddMenuItem('experts');await wait('!!document.querySelector(".yc-expert-picker [data-expert-id=flow-local-expert]")','expert row');await click('.yc-expert-picker [data-expert-id=flow-local-expert]');await wait('!!document.querySelector("[data-active-expert]")&&!document.querySelector(".yc-expert-picker")','expert picker activation')}
await removeExpertChip();check('removing expert preserves user-authored text',await read('document.querySelector(".wb-composer__input").value'),'USER_DRAFT_KEEP_AFTER_EXPERT_SWITCH');await pickExistingExpert();check('picker activation preserves user-authored text',await read('document.querySelector(".wb-composer__input").value'),'USER_DRAFT_KEEP_AFTER_EXPERT_SWITCH');check('switching expert never changes prior assistant snapshot',(await read(`window.ycode.session.messages(${JSON.stringify(role.session.id)})`)).findLast(m=>m.role==='assistant').assistantDisplay.id,ID);await fill('.wb-composer__input','');await removeExpertChip();await pickExistingExpert();check('empty conversation draft receives default input',await read('document.querySelector(".wb-composer__input").value'),'请分析提供的事实。');await removeExpertChip();check('conversation clear removes untouched expert prefill',await read('document.querySelector(".wb-composer__input").value'),'');await shot('expert-usage-cleared');check('expert interaction sends no extra user message',(await read(`window.ycode.session.messages(${JSON.stringify(role.session.id)})`)).filter(m=>m.role==='user').length,1);
await step('source-edit-entry');await centerMine();await click('.ec-cr-more-btn');await wait('[...document.querySelectorAll(\'[role="menuitem"]\')].some(e=>e.textContent.includes("修改专家"))','source card edit menu');await shot('source-edit-menu');await textClick('[role="menuitem"]','修改专家');await wait(`document.querySelector('.wb-composer__input')?.value===${JSON.stringify(EDIT)}`,'original edit prompt after owner commit');check('original edit prompt',await read('document.querySelector(".wb-composer__input").value'),EDIT);check('edit manager structured skill chip',await read('!!document.querySelector(\'[data-skill-uri="skill://expert-manager"]\')'));check('editing starts manager draft without active expert',await read('!document.querySelector("[data-active-expert]")'));await shot('edit-source-draft')
await step('actual-omp-edit');const edit=await sendNew('EXPERT_FLOW_EDITED');check('actual edit package action order',edit.assistant.tools.filter(t=>t.name==='ycode_expert_package').map(t=>t.args.action),['read','write','validate','register']);check('updated Agent actually written',(await fs.readFile(path.join(root,'plugins',ID,'agents',ID+'.md'),'utf8')).includes(UPDATED));check('edit marker tied to actual edit session',await fs.readFile(path.join(root,'plugins',ID,'.created-by-session'),'utf8'),edit.session.id);check('edit skill URI persists',edit.messages[0].skills[0].uri,'skill://expert-manager');await shot('edited-chat');await centerMine();await wait('!!document.querySelector(".ec-cr-card")','edited source card');await shot('edited-source-card')
await fs.writeFile(path.join(directory,stage+'-report.json'),JSON.stringify({result:'PASS',checks,requests,buildSha256,createSessionId:creation.session.id,roleSessionId:role.session.id,editSessionId:edit.session.id},null,2))
}
console.log('EXPERT_FLOW_PASS '+stage+' '+directory);await cleanup();app.quit()
})().catch(async error=>{await fs.writeFile(path.join(directory,stage+'-failure.json'),JSON.stringify({result:'FAIL',phase,error:String(error),stack:error.stack,checks,requests},null,2)).catch(()=>{});if(main&&!main.isDestroyed())await shot('failure').catch(()=>{});console.log('EXPERT_FLOW_FAIL '+phase+' '+String(error));await cleanup().catch(()=>{});process.exitCode=1;app.quit()})
}