Files
ycode/scripts/verify-live-imported-experts.mjs

48 lines
7.4 KiB
JavaScript

/** Own YCode only: verify actual registered content, visible cards and native summon actions. No messages sent. */
import fs from 'node:fs/promises'
import path from 'node:path'
import assert from 'node:assert/strict'
const sources = ['.tmp/rea-cpp-expert-recovered-20261016', '.tmp/rea-project-manager-recovered-20261016', '.tmp/rea-ui-designer-recovered-20261016', '.tmp/rea-algorithm-recovered-20261016']
const expected = []
for (const root of sources) { const manifest = JSON.parse(await fs.readFile(path.join(root, 'evidence-manifest.json'), 'utf8')), plugin = JSON.parse(await fs.readFile(path.join(root, '.codebuddy-plugin/plugin.json'), 'utf8')); expected.push({ id: plugin.name, profession: plugin.profession.zh, defaultInitPrompt: plugin.defaultInitPrompt.zh, files: manifest.files.filter(f => /\.(?:md|json|txt|yaml|yml|py|js|cjs|mjs|ts|csv|css|html|svg|cs|csproj|slnx|xml|xsd|sh|ps1|LICENSE)$/i.test(f.path) || ['LICENSE', '.gitignore'].includes(path.posix.basename(f.path))).map(({ path, sha256 }) => ({ path, sha256 })), avatarSha: manifest.files.find(f => f.path === plugin.avatar).sha256 }) }
const output = await fs.mkdtemp(path.resolve('.tmp/live-imported-experts-'))
const targets = await (await fetch('http://127.0.0.1:9237/json/list')).json(), target = targets.find(t => t.type === 'page' && !t.url.includes('window=settings'))
assert.ok(target?.url.toLowerCase().includes('/ycode/out/renderer/'))
const socket = new WebSocket(target.webSocketDebuggerUrl), pending = new Map(), errors = []; let next = 0
await new Promise((resolve, reject) => { socket.onopen = resolve; socket.onerror = reject })
socket.onmessage = event => { const message = JSON.parse(event.data); if (message.method === 'Runtime.exceptionThrown') errors.push('runtime exception'); const request = pending.get(message.id); if (request) { pending.delete(message.id); clearTimeout(request.timer); message.error ? request.reject(Error(message.error.message)) : request.resolve(message.result) } }
function send(method, params = {}) { return new Promise((resolve, reject) => { const id = ++next, timer = setTimeout(() => { pending.delete(id); reject(Error('YCode request timeout: ' + method)) }, 15000); pending.set(id, { resolve, reject, timer }); socket.send(JSON.stringify({ id, method, params })) }) }
async function evaluate(expression) { const result = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true }); if (result.exceptionDetails) throw Error('Own YCode UI evaluation failed'); return result.result.value }
const pause = ms => new Promise(resolve => setTimeout(resolve, ms))
async function wait(expression) { for (let n = 0; n < 100; n++) { const value = await evaluate(expression); if (value) return value; await pause(60) } throw Error('Own YCode UI did not reach expected state') }
await send('Runtime.enable')
const previous = await evaluate(`(()=>{if(document.querySelector('.cr-send-button--stop')||document.querySelector('.wb-composer__input')?.value?.trim()||document.querySelector('.wb-composer__attachments,.wb-composer__resources'))throw Error('User has active work');return document.querySelector('[data-session-id][data-selected="true"]')?.getAttribute('data-session-id')??null})()`)
const checks = []
try {
const records = await evaluate(`(async()=>{const expected=${JSON.stringify(expected)},hash=async text=>Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256',new TextEncoder().encode(text))),b=>b.toString(16).padStart(2,'0')).join(''),list=await window.ycode.experts.list(),results=[];for(const item of expected){const document=await window.ycode.experts.read(item.id),binding=await window.ycode.experts.activate(item.id);let matched=0;for(const file of item.files){if(typeof document.files[file.path]!=='string'||await hash(document.files[file.path])!==file.sha256)throw Error('Live expert text SHA mismatch: '+file.path);matched++}const base64=document.expert.avatar.split(',')[1],bytes=Uint8Array.from(atob(base64),c=>c.charCodeAt(0)),avatar=Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256',bytes)),b=>b.toString(16).padStart(2,'0')).join('');if(avatar!==item.avatarSha||binding.profession!==item.profession||!list.some(e=>e.id===item.id))throw Error('Live expert identity/avatar mismatch');results.push({id:item.id,profession:binding.profession,matchedTextFiles:matched,avatarMatched:true,activated:true})}return results})()`)
checks.push({ label: 'actual live preload/IPC expert bytes, avatars and activation', records })
const open = `window.dispatchEvent(new Event('ycode:experts-changed'));window.dispatchEvent(new CustomEvent('ycode:open-capabilities',{detail:{tab:'experts',page:'mine'}}))`
await evaluate(open)
await wait(`document.querySelector('.ec-my-grid')?.children.length===${expected.length}`)
await evaluate(`Promise.all([...document.getAnimations()].map(animation=>animation.finished.catch(()=>{}))).then(()=>document.fonts.ready).then(()=>true)`)
const shot = await send('Page.captureScreenshot', { format: 'png' }); await fs.writeFile(path.join(output, 'four-expert-cards.png'), Buffer.from(shot.data, 'base64'))
for (const expert of expected) {
const location = await evaluate(`(()=>{const card=[...document.querySelectorAll('.ec-my-grid>article')].find(e=>e.textContent.includes(${JSON.stringify(expert.profession)}));if(!card)throw Error('Expert card absent');const b=card.getBoundingClientRect();return{x:b.x+b.width/2,y:b.y+b.height/2}})()`)
await send('Input.dispatchMouseEvent', { type: 'mouseMoved', ...location }); await pause(120)
const button = await evaluate(`(()=>{const card=[...document.querySelectorAll('.ec-my-grid>article')].find(e=>e.textContent.includes(${JSON.stringify(expert.profession)})),button=[...card.querySelectorAll('button')].find(e=>e.textContent.trim()==='召唤'),b=button?.getBoundingClientRect();if(!b?.width||getComputedStyle(button).visibility==='hidden')throw Error('Summon not visible');return{x:b.x+b.width/2,y:b.y+b.height/2}})()`)
await send('Input.dispatchMouseEvent', { type: 'mouseMoved', ...button }); await send('Input.dispatchMouseEvent', { type: 'mousePressed', button: 'left', buttons: 1, clickCount: 1, ...button }); await send('Input.dispatchMouseEvent', { type: 'mouseReleased', button: 'left', buttons: 0, clickCount: 1, ...button })
await wait(`!!document.querySelector('[data-active-expert="${expert.id}"]')&&document.querySelector('.wb-composer__input')?.value===${JSON.stringify(expert.defaultInitPrompt)}`)
const image = await send('Page.captureScreenshot', { format: 'png' }); await fs.writeFile(path.join(output, expert.id + '-summoned.png'), Buffer.from(image.data, 'base64'))
checks.push({ label: 'native pointer summon binds expert and exact default draft', expertId: expert.id, pass: true })
await evaluate(open); await wait(`document.querySelector('.ec-my-grid')?.children.length===${expected.length}`)
}
assert.equal(errors.length, 0); checks.push({ label: 'no renderer exceptions', pass: true })
await fs.writeFile(path.join(output, 'report.json'), JSON.stringify({ result: 'PASS', checks, messagesSent: 0 }, null, 2))
console.log(JSON.stringify({ result: 'PASS', experts: records, messagesSent: 0, output }))
} finally {
if (previous) await evaluate(`(()=>{const row=[...document.querySelectorAll('[data-session-id]')].find(e=>e.getAttribute('data-session-id')===${JSON.stringify(previous)});if(row)row.click()})()`)
await pause(150)
await evaluate(`window.dispatchEvent(new CustomEvent('ycode:open-capabilities',{detail:{tab:'experts',page:'mine'}}))`)
socket.close()
}