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

+97
View File
@@ -0,0 +1,97 @@
import assert from 'node:assert/strict'
import {
diffSidesFromCurrentFile,
diffSidesFromUnifiedDiff,
parseDiffContent,
resolveCachedDiffSides
} from '../src/renderer/src/workbuddy/artifacts/diff-content'
const diff = [
'diff --git a/file.txt b/file.txt',
'index 1234567..abcdef0 100644',
'--- a/file.txt',
'+++ b/file.txt',
'@@ -1,3 +1,3 @@',
' before',
'-old value',
'+new value',
' after'
].join('\n')
assert.deepEqual(parseDiffContent(diff), {
original: 'before\nold value\nafter',
modified: 'before\nnew value\nafter'
})
assert.deepEqual(parseDiffContent(undefined), { original: '', modified: '' })
assert.equal(resolveCachedDiffSides(undefined, diff), undefined)
assert.deepEqual(diffSidesFromCurrentFile('current file', 'added'), { original: '', modified: 'current file' })
assert.deepEqual(diffSidesFromCurrentFile('removed file', 'deleted'), { original: 'removed file', modified: '' })
assert.deepEqual(resolveCachedDiffSides('current file', undefined, 'added'), { original: '', modified: 'current file' })
assert.deepEqual(resolveCachedDiffSides('removed file', undefined, 'deleted'), { original: 'removed file', modified: '' })
const cached = diffSidesFromUnifiedDiff('before\nnew value\nafter', diff)
assert.deepEqual(cached, {
original: 'before\nold value\nafter',
modified: 'before\nnew value\nafter',
exactDiff: diff
})
assert.deepEqual(resolveCachedDiffSides('before\nnew value\nafter', diff), cached)
const bareHunk = '@@ -1 +1 @@\n-old\n+new'
assert.deepEqual(diffSidesFromUnifiedDiff('new', bareHunk), {
original: 'old',
modified: 'new',
exactDiff: bareHunk
})
const shiftedHunk = '@@ -1 +1 @@\n-old\n+new'
assert.deepEqual(diffSidesFromUnifiedDiff('prefix\nnew\nsuffix', shiftedHunk), {
original: 'prefix\nold\nsuffix',
modified: 'prefix\nnew\nsuffix',
exactDiff: shiftedHunk
})
const multipleHunks = [
'--- a/file.txt',
'+++ b/file.txt',
'@@ -2,3 +2,3 @@',
' b',
'-c',
'+C',
' d',
'@@ -6,2 +6,2 @@',
' f',
'-g',
'+G'
].join('\n')
assert.equal(
diffSidesFromUnifiedDiff('a\nb\nC\nd\ne\nf\nG', multipleHunks).original,
'a\nb\nc\nd\ne\nf\ng'
)
assert.equal(
diffSidesFromUnifiedDiff('before\r\nnew value\r\nafter\r\n', diff).original,
'before\r\nold value\r\nafter\r\n'
)
const noFinalNewline = [
'--- a/file.txt',
'+++ b/file.txt',
'@@ -1 +1 @@',
'-old',
'\\ No newline at end of file',
'+new',
'\\ No newline at end of file'
].join('\n')
assert.deepEqual(diffSidesFromUnifiedDiff('new', noFinalNewline), {
original: 'old',
modified: 'new',
exactDiff: noFinalNewline
})
const mismatchedCache = diffSidesFromUnifiedDiff('unrelated cached content', diff)
assert.deepEqual(mismatchedCache, {
original: 'before\nold value\nafter',
modified: 'unrelated cached content'
})
console.log('Artifact diff recovery regression checks passed')
+86
View File
@@ -0,0 +1,86 @@
// Own isolated renderer QA. Original observations come only from the pinned REA archive.
const fs=require('node:fs/promises'),path=require('node:path'),crypto=require('node:crypto'),assert=require('node:assert/strict')
const output=path.resolve('.tmp/artifact-link-ui-regression')
const reference=path.resolve('.tmp/rea-assessment-initial/layout.json')
const referenceId='ev_d6e6920ff4e47f73a182fac5312c5f02393366a035445d18c8a6733a8c8a11ed'
const sha=bytes=>crypto.createHash('sha256').update(bytes).digest('hex')
if(!process.versions.electron){
;(async()=>{
await fs.mkdir(output,{recursive:true})
const entry=`import React,{useState} from 'react';import {createRoot} from 'react-dom/client';
import SourceArtifactLink from './src/renderer/src/components/SourceArtifactLink';
import './src/renderer/src/workbuddy/typography/tokens.css';import './src/renderer/src/styles/tokens.css';import './src/renderer/src/styles/app.css';
import './src/renderer/src/workbuddy/conversation/source-styles.css';import './src/renderer/src/workbuddy/conversation/source-adapters.css';
window.qa={clicks:0};function Fixture(){const [count,setCount]=useState(13);window.qa.setCount=setCount;return <div className="chat-pane cr-theme" data-cr-theme="light" style={{position:'absolute',inset:0,display:'block',overflow:'visible',fontSize:13,lineHeight:'19px'}}>
<div className="conversation-finished-footer" style={{position:'absolute',left:324.28125,top:602.578125,width:436.703125}}><SourceArtifactLink count={count} onClick={()=>window.qa.clicks++}/></div>
</div>}createRoot(document.getElementById('root')).render(<Fixture/>);`
await require('esbuild').build({stdin:{contents:entry,loader:'tsx',resolveDir:process.cwd()},outfile:path.join(output,'fixture.js'),bundle:true,platform:'browser',format:'iife',jsx:'automatic',logLevel:'silent'})
await fs.writeFile(path.join(output,'index.html'),'<!doctype html><html data-theme="light"><meta charset="utf-8"><link rel="stylesheet" href="fixture.css"><style>html,body,#root{width:1336px;height:901px;margin:0;background:white}</style><div id="root"></div><script src="fixture.js"></script></html>')
const env={...process.env};delete env.ELECTRON_RUN_AS_NODE
const result=require('node:child_process').spawnSync(require('electron'),[__filename],{env,windowsHide:true,stdio:'inherit',timeout:30000})
process.exitCode=result.status??1
})().catch(error=>{console.error(error);process.exitCode=1})
}else{
const {app,BrowserWindow,nativeImage}=require('electron')
app.setPath('userData',path.join(output,'user-data'))
app.disableHardwareAcceleration()
app.commandLine.appendSwitch('use-angle','swiftshader')
app.commandLine.appendSwitch('disable-gpu-compositing')
const delay=ms=>new Promise(resolve=>setTimeout(resolve,ms))
app.whenReady().then(async()=>{
const source=JSON.parse(await fs.readFile(reference,'utf8'))
assert.equal(source.evidenceId,referenceId)
const sourceNodes=source.layout.documents[0].nodes,sourceSvg=source.layout.documents[0].svg_nodes
const errors=[],checks=[],win=new BrowserWindow({x:0,y:0,width:1336,height:901,useContentSize:true,frame:false,show:false,webPreferences:{contextIsolation:true,nodeIntegration:false,backgroundThrottling:false}})
win.webContents.on('console-message',event=>{if(event.level==='error')errors.push(event.message)})
const check=(label,actual,expected)=>checks.push({label,pass:JSON.stringify(actual)===JSON.stringify(expected),actual,expected})
const read=expression=>win.webContents.executeJavaScript(expression)
await win.loadFile(path.join(output,'index.html'));await delay(250)
win.showInactive();win.webContents.sendInputEvent({type:'mouseMove',x:0,y:0});await delay(200)
const metrics=await read(`(()=>{const selectors={actions:'.artifact-slot-panel__actions',button:'.artifact-slot-panel__action-btn',chevron:'.artifact-slot-panel__action-chevron',svg:'.artifact-slot-panel__action-chevron svg'};const properties=${JSON.stringify(source.layout.properties)};return {nodes:Object.fromEntries(Object.entries(selectors).map(([key,selector])=>{const e=document.querySelector(selector),r=e.getBoundingClientRect(),c=getComputedStyle(e);return[key,{bounds:[r.x,r.y,r.width,r.height],styles:Object.fromEntries(properties.map(name=>[name,c.getPropertyValue(name)])),attributes:Object.fromEntries([...e.attributes].map(a=>[a.name,a.value]))}]})),slotDisplay:getComputedStyle(document.querySelector('[data-chat-search-skip]')).display,text:document.querySelector('.artifact-slot-panel__action-btn').textContent}})()`)
for(const [name,index] of [['actions',1644],['button',1645],['chevron',1647],['svg',1648]]){
const expected=sourceNodes.find(node=>node.node_index===index)
check(name+' bounds',metrics.nodes[name].bounds,expected.bounds)
for(const [property,value] of Object.entries(metrics.nodes[name].styles))check(name+' '+property,value,expected.styles[property])
}
check('display-contents slot',metrics.slotDisplay,'contents')
check('exact count label',metrics.text,'查看所有产物 (13)')
check('single direct label text node',await read("[...document.querySelector('.artifact-slot-panel__action-btn').childNodes].filter(n=>n.nodeType===Node.TEXT_NODE).map(n=>n.textContent)"),['查看所有产物 (13)'])
check('SVG attributes',Object.fromEntries(['width','height','viewBox','fill'].map(key=>[key,metrics.nodes.svg.attributes[key]])),sourceSvg.find(node=>node.node_index===1648).attributes)
check('SVG path',await read("document.querySelector('.artifact-slot-panel__action-chevron path').getAttribute('d')"),sourceSvg.find(node=>node.node_index===1649).attributes.d)
win.webContents.debugger.attach('1.3')
const capture=await Promise.race([win.webContents.debugger.sendCommand('Page.captureScreenshot',{format:'png',fromSurface:true,captureBeyondViewport:false}),delay(5000).then(()=>{throw Error('Own artifact-link screenshot timeout')})])
const screenshot=Buffer.from(capture.data,'base64');await fs.writeFile(path.join(output,'screen.png'),screenshot)
const originalBytes=await fs.readFile('.tmp/rea-assessment-initial/workbuddy.png')
assert.equal(sha(originalBytes),source.artifact.sha256,'reference PNG must match retained REA artifact')
const original=nativeImage.createFromBuffer(originalBytes),candidate=nativeImage.createFromBuffer(screenshot),roi={x:324,y:614,width:139,height:26}
const a=original.crop(roi),b=candidate.crop(roi),before=a.toBitmap(),after=b.toBitmap()
await fs.writeFile(path.join(output,'reference-roi.png'),a.toPNG());await fs.writeFile(path.join(output,'candidate-roi.png'),b.toPNG())
let changed=0,max=0,sum=0,changedInSvg=0;const deltas=[]
for(let i=0;i<before.length;i+=4){let different=false;for(let k=0;k<3;k++){const delta=Math.abs(before[i+k]-after[i+k]);sum+=delta;max=Math.max(max,delta);if(delta)different=true}if(different){changed++;const x=roi.x+(i/4)%roi.width,y=roi.y+Math.floor((i/4)/roi.width);if(x>=450&&x<459&&y>=621&&y<633)changedInSvg++;if(deltas.length<12)deltas.push({x,y,reference:[...before.subarray(i,i+3)],candidate:[...after.subarray(i,i+3)]})}}
const pixel={roi,pixels:roi.width*roi.height,changed,changedInSvg,max,meanRgbDelta:sum/(roi.width*roi.height*3),fullPixelParity:changed===0,deltas,referenceSha256:sha(a.toPNG()),candidateSha256:sha(b.toPNG())}
const [x,y,w,h]=metrics.nodes.button.bounds,point={x:Math.floor(x+w/2),y:Math.floor(y+h/2)}
win.webContents.sendInputEvent({type:'mouseMove',...point});await delay(200)
const hover=await read("(()=>{const b=document.querySelector('.artifact-slot-panel__action-btn'),c=document.querySelector('.artifact-slot-panel__action-chevron');return{buttonColor:getComputedStyle(b).color,chevronColor:getComputedStyle(c).color,bounds:b.getBoundingClientRect().toJSON()}})()")
check('own hover button color',hover.buttonColor,'rgb(0, 0, 0)');check('own hover chevron color',hover.chevronColor,'rgb(0, 0, 0)')
win.webContents.sendInputEvent({type:'mouseDown',button:'left',clickCount:1,...point});win.webContents.sendInputEvent({type:'mouseUp',button:'left',clickCount:1,...point});await delay(50)
check('native click callback once',await read('window.qa.clicks'),1)
win.focus();win.webContents.focus();await delay(100)
const keyboard={windowFocused:win.isFocused(),webContentsFocused:win.webContents.isFocused(),buttonFocused:await read("document.activeElement?.matches('.artifact-slot-panel__action-btn')===true"),tested:false}
if(keyboard.windowFocused&&keyboard.webContentsFocused&&keyboard.buttonFocused){
win.webContents.sendInputEvent({type:'keyDown',keyCode:'Return'});win.webContents.sendInputEvent({type:'char',keyCode:'\r'});win.webContents.sendInputEvent({type:'keyUp',keyCode:'Return'});await delay(50)
keyboard.tested=true
check('native Enter callback once',await read('window.qa.clicks'),2)
}
await read('window.qa.setCount(0)');await delay(50)
check('empty count omits entry',await read("document.querySelector('.artifact-slot-panel__action-btn')===null"),true)
check('renderer errors',errors,[])
const integration=JSON.parse(await fs.readFile('.tmp/panel-integration-Capd62/report.json','utf8'))
const gpuFeatureStatus=app.getGPUFeatureStatus(),gpu=await app.getGPUInfo('complete')
check('archived GPU feature status',gpuFeatureStatus,source.environment_gpu.feature_status)
const report={result:checks.every(check=>check.pass)?'PASS geometry/styles/own interactions':'FAIL geometry/styles/own interactions',checks,counts:{total:checks.length,pass:checks.filter(check=>check.pass).length,fail:checks.filter(check=>!check.pass).length},metrics,pixel,hover,keyboard,reference:{evidenceId:referenceId,archive:source.archive,screenshotSha256:sha(originalBytes),version:'WorkBuddy 5.8.2'},gpuFeatureStatus,sourceGpuFeatureStatus:source.environment_gpu.feature_status,gpu,productionHashes:{component:sha(await fs.readFile('src/renderer/src/components/SourceArtifactLink.tsx')),css:sha(await fs.readFile('src/renderer/src/components/source-artifact-link.css'))},integration:{result:integration.result,total:integration.checks.length,pass:integration.checks.filter(check=>check.pass).length,scope:integration.scope},limitations:['Original hover state is not archived; own hover and activation tests do not establish original hover pixel parity.','The callback is an isolated counter; actual overview/tab retention is covered by the separate production integration report.',...(!keyboard.tested?['Native Enter was not tested because a real focused window/webContents/button precondition was unavailable.']:[]),'No production GPU flags or UI source changed; no WorkBuddy runtime actions or new original evidence were collected.']}
await fs.writeFile(path.join(output,'report.json'),JSON.stringify(report,null,2))
console.log(JSON.stringify({result:report.result,checks:report.counts,button:metrics.nodes.button.bounds,svg:metrics.nodes.svg.bounds,pixel,integration:report.integration.total}))
win.destroy();app.exit(report.counts.fail?1:0)
}).catch(error=>{console.error(error);app.exit(1)})
}
+83
View File
@@ -0,0 +1,83 @@
import assert from 'node:assert/strict'
import { renderToStaticMarkup } from 'react-dom/server'
import { ArtifactContentRenderer } from '../src/renderer/src/workbuddy/artifacts/ArtifactContentRenderer'
import { loadCustomArtifactContent, loadDiffContent } from '../src/renderer/src/workbuddy/artifacts/content-loader'
import { selectArtifactView, isHtmlPreviewSizeOversized, HTML_PREVIEW_SIZE_LIMIT } from '../src/renderer/src/workbuddy/artifacts/routing'
import { createWriteArtifact } from '../src/renderer/src/workbuddy/artifacts/tool-artifact'
import { ToolCallRenderer } from '../src/renderer/src/workbuddy/conversation/ToolCallRenderer'
import { createSourceWriteFileOpenRequest } from '../src/renderer/src/workbuddy/conversation/SourceWriteFileView'
import { artifactImageSource } from '../src/renderer/src/workbuddy/artifacts/image-path'
async function main(): Promise<void> {
assert.equal(artifactImageSource('../image.png', 'file:///C:/work/with%20space/notes/readme.md', 'C:/other'), 'file:///C:/work/with%20space/image.png')
assert.equal(artifactImageSource('image.png', 'C:\\work\\notes\\readme.md', 'C:/other'), 'C:/work/notes/image.png')
assert.equal(artifactImageSource('image.png', 'notes/readme.md', 'C:/work'), 'C:/work/notes/image.png')
assert.equal(artifactImageSource('../image.png', 'https://example.com/notes/readme.md', 'C:/work'), 'https://example.com/image.png')
assert.equal(artifactImageSource('data:image/png;base64,AA==', 'notes/readme.md', 'C:/work'), 'data:image/png;base64,AA==')
const view = (type: string, extra = {}) => selectArtifactView({ id: 'a', type, ...extra })
assert.equal(view('implementation-plan'), 'plan')
assert.equal(view('overview', { contentType: 'code', content: 'x' }), 'overview')
assert.equal(view('task', { contentType: 'markdown', content: 'x' }), 'empty')
assert.equal(view('custom-artifact', { category: 'agents', customType: 'wb/app' }), 'agent-member')
assert.equal(view('custom-artifact', { customType: 'mcp/app', content: 'x' }), 'extension')
assert.equal(view('custom-artifact', { customType: 'other', contentType: 'code' }), 'custom')
assert.equal(view('media-artifact', { category: 'artifact', url: 'agent://files/page.HTM', contentType: 'code' }), 'html')
assert.equal(view('media-artifact', { category: 'other', url: 'page.html', contentType: 'code' }), 'media')
assert.equal(view('file-changes', { fileChangeInfo: { uri: 'a.ts' } }), 'file-diff')
assert.equal(view('other', { contentType: 'diff', originalContent: '', modifiedContent: 'new' }), 'empty', 'retain the captured direct-diff truthiness gate')
assert.equal(isHtmlPreviewSizeOversized(HTML_PREVIEW_SIZE_LIMIT - 1), false)
assert.equal(isHtmlPreviewSizeOversized(HTML_PREVIEW_SIZE_LIMIT), true)
assert.equal(isHtmlPreviewSizeOversized(NaN), false)
let reads = 0
const read = async (uri: string) => { reads++; assert.equal(uri, 'a.md'); return '# live' }
assert.equal(await loadCustomArtifactContent('a.md', '# cached', read), '# cached')
assert.equal(reads, 0)
assert.equal(await loadCustomArtifactContent('a.md', '# cached', read, true), '# live')
assert.equal(reads, 1)
assert.equal(await loadCustomArtifactContent('a.md', '', async () => ''), '')
await assert.rejects(loadCustomArtifactContent(undefined, '# cached', read), /No file URI/)
await assert.rejects(loadCustomArtifactContent('a.md'), /No readFile/)
const diff = '@@ -1 +1 @@\n-old\n+new'
const cached = await loadDiffContent({ uri: 'a.md', diff, cachedContent: 'new' }, read)
assert.equal(cached.original, 'old')
assert.equal(cached.exactDiff, diff)
assert.equal(reads, 1, 'cached full file must bypass host reads')
const live = await loadDiffContent({ uri: 'a.md', diff }, async () => 'new')
assert.deepEqual(live, cached)
const fallback = await loadDiffContent({ uri: 'missing', diff }, async () => { throw Error('missing') })
assert.deepEqual(fallback, { original: 'old', modified: 'new', fallback: true })
for (const [before, after] of [['old', 'new'], ['', 'created\n'], ['removed\n', ''], ['a\r\nb\r\n', 'a\r\nB\r\n'], ['', ''], ['a\n\n', 'b\n\n']]) {
const artifact = createWriteArtifact('write', 'a.txt', { oldContent: before, newContent: after })!
const sides = await loadDiffContent(artifact.fileChangeInfo!)
assert.equal(sides.original, before, 'complete pre-write content should survive the adapter')
assert.equal(sides.modified, after, 'complete post-write content should survive the adapter')
}
assert.equal(createWriteArtifact('write', 'a.txt', { oldText: 'fragment', newText: 'fragment' }), undefined)
const plan = renderToStaticMarkup(<ArtifactContentRenderer artifact={{ id: 'plan', type: 'implementation-plan', content: '# plan' }} renderMarkdown={(content, _artifact, header) => <span data-header={header}>{content}</span>} />)
assert.match(plan, /artifact-content/)
assert.match(plan, /plan-content/)
assert.match(plan, /data-header="true"/)
const code = renderToStaticMarkup(<ArtifactContentRenderer artifact={{ id: 'code', type: 'other', contentType: 'code', content: 'code' }} renderMarkdown={() => null} renderCode={(content, language) => <span data-language={language}>{content}</span>} />)
assert.match(code, /data-language="typescript"/)
const written = renderToStaticMarkup(<ToolCallRenderer defaultExpanded onOpenFile={() => undefined} tool={{ id: 'write', name: 'edit_file', status: 'success', args: { path: 'actual.ts', oldText: 'fragment', newText: 'replacement' }, result: { result: { path: 'actual.ts', oldContent: 'full before', newContent: 'full after' } } }} />)
assert.match(written, /actual.ts/)
assert.match(written, /role="link"/)
assert.match(written, /full before/)
assert.match(written, /full after/)
const writeRequest = createSourceWriteFileOpenRequest({
id: 'write', name: 'edit_file', status: 'success',
args: { path: 'actual.ts', oldText: 'fragment', newText: 'replacement' },
result: { result: { path: 'actual.ts', oldContent: 'full before', newContent: 'full after' } }
})
assert.equal(writeRequest?.content, 'full after')
assert.ok(writeRequest?.artifact?.fileChangeInfo)
const openedWriteSides = await loadDiffContent(writeRequest.artifact.fileChangeInfo)
assert.equal(openedWriteSides.original, 'full before')
assert.equal(openedWriteSides.modified, 'full after')
console.log('Artifact routing, content loading, full write projection and renderer regressions passed')
}
void main().catch((error) => { console.error(error); process.exitCode = 1 })
+42
View File
@@ -0,0 +1,42 @@
// Assessment captures of the user-owned YCode app; WorkBuddy is observed separately through REA.
import fs from 'node:fs/promises'
const targets = await (await fetch('http://127.0.0.1:9237/json/list')).json()
const socket = new WebSocket(targets.find(target => target.type === 'page').webSocketDebuggerUrl)
await new Promise(resolve => { socket.onopen = resolve })
let seq = 0
const pending = new Map()
socket.onmessage = event => {
const response = JSON.parse(event.data)
const pair = pending.get(response.id)
if (pair) { pending.delete(response.id); response.error ? pair.reject(response.error) : pair.resolve(response.result) }
}
const send = (method, params = {}) => new Promise((resolve, reject) => {
const id = ++seq; pending.set(id, { resolve, reject }); socket.send(JSON.stringify({ id, method, params }))
})
const evaluate = async expression => (await send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true })).result.value
await fs.mkdir('.tmp/assessment-ycode', { recursive: true })
try {
await send('Emulation.setDeviceMetricsOverride', { width: 1336, height: 901, deviceScaleFactor: 1, mobile: false })
const wasOpen = await evaluate('!!document.querySelector(".right-sidebar")')
for (const open of [false, true]) {
await evaluate(`(()=>{const button=document.querySelector('.chat-head button[title="${open ? '展开右侧栏' : '收起右侧栏'}"]');button?.click()})()`)
await evaluate('new Promise(resolve=>setTimeout(resolve,400))')
const state = await evaluate(`(()=>{
const selectors=['.app-shell','.sidebar','.chat-pane','.chat-head','.chat-messages','.right-sidebar','.right-panel__top','.wb-composer','.cr-agent__content','.cr-text-block','.msg__status','.cr-collapse--completed'];
return{viewport:{width:innerWidth,height:innerHeight},panelsOpen:!!document.querySelector('.right-sidebar'),
title:document.querySelector('.chat-head__title')?.textContent,
nodes:selectors.map(selector=>({selector,items:[...document.querySelectorAll(selector)].map(node=>{const rect=node.getBoundingClientRect(),style=getComputedStyle(node);return{bounds:[rect.x,rect.y,rect.width,rect.height],fontSize:style.fontSize,lineHeight:style.lineHeight,padding:style.padding,gap:style.gap,text:selector==='.msg__status'?node.textContent:undefined}})})),
toolCount:document.querySelectorAll('[data-tool-call-id]').length,
renderedMessages:{users:document.querySelectorAll('.msg--user').length,assistants:document.querySelectorAll('.msg--assistant').length}}
})()`)
const name = open ? 'panel-open' : 'panel-closed'
const screenshot = await send('Page.captureScreenshot', { format: 'png', fromSurface: true })
await fs.writeFile(`.tmp/assessment-ycode/${name}.png`, Buffer.from(screenshot.data, 'base64'))
await fs.writeFile(`.tmp/assessment-ycode/${name}.json`, JSON.stringify(state, null, 2))
console.log(JSON.stringify({ name, viewport: state.viewport, title: state.title, toolCount: state.toolCount }))
}
await evaluate(`document.querySelector('.chat-head button[title="${wasOpen ? '展开右侧栏' : '收起右侧栏'}"]')?.click()`)
} finally {
await send('Emulation.clearDeviceMetricsOverride')
socket.close()
}
@@ -0,0 +1,59 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { renderToStaticMarkup } from 'react-dom/server'
import { freezeAssistantModelMetadata } from '../src/shared/assistant-model'
import { AssistantMessageDetails, formatMessageTime } from '../src/renderer/src/components/AssistantMessageDetails'
import { getModelIconByName } from '../src/renderer/src/workbuddy/composer/model-selector/source.mjs'
import type { ChatMessage } from '../src/shared/types'
const renderTime = new Date()
renderTime.setHours(12, 0, 0, 0)
const profile = { id: 'profile-1', name: 'Frozen Model' }
const metadata = freezeAssistantModelMetadata(profile)
profile.name = 'Renamed Model'
assert.deepEqual(metadata, { modelId: 'profile-1', modelName: 'Frozen Model' })
const newAssistantMessage: ChatMessage = {
id: 'assistant-1', role: 'assistant', content: 'answer', createdAt: renderTime.getTime(), status: 'done', ...metadata
}
const newFooter = renderToStaticMarkup(<AssistantMessageDetails message={newAssistantMessage} />)
assert.match(newFooter, /cb-credit-usage-text cb-credit-usage-text--feedback/)
assert.match(newFooter, /title="profile-1"/)
assert.match(newFooter, /class="cb-model-tag-icon"[^>]*aria-hidden="true"/)
assert.match(newFooter, /Frozen Model/)
assert.doesNotMatch(newFooter, /Renamed Model/)
assert.match(newFooter, /_message_time_tip_1p6pj_1 cb-message-time-tip/)
assert.match(newFooter, />12:00<\/span>/)
const historicalMessage: ChatMessage = {
id: 'assistant-old', role: 'assistant', content: 'legacy answer', createdAt: renderTime.getTime(), status: 'done'
}
const historicalFooter = renderToStaticMarkup(<AssistantMessageDetails message={historicalMessage} />)
assert.doesNotMatch(historicalFooter, /cb-credit-usage-text/)
assert.doesNotMatch(historicalFooter, /Frozen Model|Renamed Model/)
const footerCss = readFileSync(resolve('src/renderer/src/components/assistant-message-details.css'), 'utf8')
assert.doesNotMatch(footerCss, /\.cb-credit-usage-text--feedback\s*\{[^}]*opacity:\s*0/s)
assert.doesNotMatch(footerCss, /top:4px/, 'metadata shares a flex centerline instead of a timestamp offset')
assert.match(footerCss, /cb-assistant-message-details\{[^}]*font-family:[^}]*sans-serif[^}]*font-size:15px[^}]*line-height:24px/s)
const brandMarkup = (name: string): string => renderToStaticMarkup(<AssistantMessageDetails message={{ ...newAssistantMessage, modelName: name }} />)
const gptGlyph = renderToStaticMarkup(<>{getModelIconByName('gpt-6-luna')}</>)
const deepseekGlyph = renderToStaticMarkup(<>{getModelIconByName('deepseek-flash')}</>)
assert.notEqual(gptGlyph, deepseekGlyph, 'GPT and DeepSeek must use different brand logos')
assert.ok(brandMarkup('gpt-6-luna').includes(gptGlyph))
assert.ok(brandMarkup('deepseek-flash').includes(deepseekGlyph))
assert.match(footerCss, /\.cb-assistant-message:hover \.cb-credit-usage-text--feedback,\.cb-assistant-message:hover \.cb-message-time-tip\{opacity:1\}/)
const now = new Date(2025, 3, 10, 12, 0)
const today = new Date(2025, 3, 10, 8, 4).getTime()
const yesterday = new Date(2025, 3, 9, 9, 5).getTime()
const earlierThisYear = new Date(2025, 3, 7, 9, 5).getTime()
const earlierYear = new Date(2024, 11, 31, 9, 5).getTime()
assert.equal(formatMessageTime(today, now), '08:04')
assert.equal(formatMessageTime(yesterday, now), '4月9日 09:05')
assert.equal(formatMessageTime(earlierThisYear, now), '4月7日 09:05')
assert.equal(formatMessageTime(earlierYear, now), '2024年12月31日 09:05')
assert.equal(formatMessageTime(Number.NaN, now), undefined)
console.log('assistant-message-footer-regression: frozen model metadata, legacy history, and date formatting passed')
+34
View File
@@ -0,0 +1,34 @@
// Inspect archived REA results only; this script has no access to the original runtime.
import fs from 'node:fs/promises'
import path from 'node:path'
import crypto from 'node:crypto'
const states = [
['专家市场', '.tmp/rea-expert-entry-static-20261016'],
['我的专家空态', '.tmp/rea-my-experts-static-20261016'],
['技能市场', '.tmp/rea-market-skills-20261016', 0],
['已安装技能', '.tmp/rea-installed-skills-static-20261016'],
['添加技能菜单', '.tmp/rea-add-skill-options-20261016', 0],
['连接器市场', '.tmp/rea-market-connectors-static-20261016'],
['QQ邮箱未连接详情', '.tmp/rea-connector-detail-20261016', 0],
['详情收到Escape后仍打开', '.tmp/rea-connector-detail-20261016', 1],
['自定义MCP管理', '.tmp/rea-custom-connectors-static-20261016'],
['取证结束已恢复首页', '.tmp/rea-capability-restored-home-20261016']
]
const results = []
for (const [name, directory, frame] of states) {
const layout = JSON.parse(await fs.readFile(path.join(directory, 'layout.json'), 'utf8'))
const artifact = frame === undefined ? layout.artifact : layout.probe.frames[frame].artifact
const file = path.join(directory, frame === undefined ? 'workbuddy.png' : `frame-${String(frame).padStart(3, '0')}.png`)
const png = await fs.readFile(file)
const sha256 = crypto.createHash('sha256').update(png).digest('hex')
if (sha256 !== artifact.sha256) throw Error(`REA PNG mismatch: ${name}`)
results.push({ name, evidenceId: layout.evidenceId, file, sha256,
observation: frame === undefined ? 'static baseline layout and image' : 'post-action image; baseline layout describes the earlier state',
...(frame === undefined ? {} : { step: layout.probe.frames[frame].step_id, probeStatus: layout.probe.status,
steps: layout.probe.steps.map(({ step_id, status, error }) => ({ step_id, status, error })), restoration: layout.probe.restoration }) })
}
const output = '.tmp/capability-assessment-20261016/runtime-index.json'
await fs.mkdir(path.dirname(output), { recursive: true })
await fs.writeFile(output, JSON.stringify({ originalVersion: '5.8.2', scope: 'REA archived UI observations; not an implementation or pixel-parity verdict',
actionsExcluded: ['installation', 'enabling/disabling', 'account authorization', 'configuration save', 'model messages'], results }, null, 2))
console.log(JSON.stringify({ output, imagesVerified: results.length, partialFramesRetained: results.filter(result => result.probeStatus === 'failed').length }))
+92
View File
@@ -0,0 +1,92 @@
// Static assessment of retained REA exports only. Never reads the original installation.
const fs = require('node:fs/promises')
const path = require('node:path')
const crypto = require('node:crypto')
const assert = require('node:assert/strict')
const archive = path.resolve('.tmp/rea-runtime-scripts.json')
const output = path.resolve('.tmp/connector-assessment-20261016')
const evidence = 'ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb'
const sha = text => crypto.createHash('sha256').update(text, 'utf8').digest('hex')
;(async () => {
const source = JSON.parse(await fs.readFile(archive, 'utf8'))
assert.equal(source.evidence_id, evidence)
const files = source.items.map(item => {
const artifact = item.source?.artifact, text = artifact?.text
const actualSha256 = typeof text === 'string' ? sha(text) : null
return { name: path.win32.basename(item.file_path), declaredLength: item.length, textLength: text?.length ?? null,
sha256: artifact?.sha256 ?? null, actualSha256, verified: actualSha256 !== null && actualSha256 === artifact?.sha256 }
})
const failures = files.filter(file => !file.verified)
assert.equal(failures.length, 0, 'Every retained script body must match its REA artifact SHA before assessment')
const args = process.argv.slice(2)
if (args[0] === '--find') {
const file = source.items.find(item => path.win32.basename(item.file_path) === args[1])
assert.ok(file, 'Select an exact retained REA filename')
const text = file.source.artifact.text
console.log(JSON.stringify({ file: args[1], sha256: file.source.artifact.sha256, matches: args.slice(2).map(term => {
const offsets = []; let offset = -1
while ((offset = text.indexOf(term, offset + 1)) !== -1) offsets.push(offset)
return { term, offsets }
}) }))
return
}
if (args[0] === '--excerpt') {
const file = source.items.find(item => path.win32.basename(item.file_path) === args[1])
assert.ok(file, 'Select an exact retained REA filename')
const start = Number(args[2]), length = Number(args[3] || 4000)
assert.ok(Number.isInteger(start) && start >= 0 && Number.isInteger(length) && length > 0 && length <= 16000)
console.log(JSON.stringify({ evidenceId: evidence, file: args[1], sha256: file.source.artifact.sha256,
offsetUnit: 'zero-based JavaScript UTF-16 code units', start, end: start + length, text: file.source.artifact.text.slice(start, start + length) }))
return
}
const selectedNames = new Set(['store-BKmTj0KI.js', 'contexts-BMeWSUHk.js', 'common-DRZ6FwkK.js', 'adapters-SPAem2E3.js',
'skills-panel-CvMlV3m8.js', 'wb-input-add-B045GnIg.js', 'runtime-B7PLES2V.js', 'workbuddy-provider-DkH2YFYM.js',
'desktop-sdk-setup-C2AUiJmx.js', 'main-content-core-Dlaq72qt.js', 'services-Dz7-cD7Y.js', 'zh-cn-Dfzn8myY.js', 'wb-CocRQn4C.js'])
const selected = source.items.filter(item => /connector|mcp-server-loader|mcp-token-config|oauth-contract/.test(path.win32.basename(item.file_path)) || selectedNames.has(path.win32.basename(item.file_path))).map(item => {
const name = path.win32.basename(item.file_path), text = item.source.artifact.text
return { ...files.find(file => file.name === name), functions: [...text.matchAll(/function ([\w$]+)\(/g)]
.filter(match => /connector|mcp|oauth|tokenConfig|catalog|category/i.test(match[1]))
.map(match => ({ name: match[1], offset: match.index })) }
})
const supplementaryPath = path.resolve('.tmp/rea-expert-runtime-scripts-20261016.json')
let supplementary
try {
const newer = JSON.parse(await fs.readFile(supplementaryPath, 'utf8'))
assert.equal(newer.evidence_id, 'ev_e91787b1d4dea651582c6cf12827775b9068b0ee8b80c68ba08d4c1d87d94e90')
const verified = newer.items.map(item => ({ name: path.win32.basename(item.file_path), sha256: item.source?.artifact?.sha256,
actualSha256: sha(item.source.artifact.text) }))
assert.ok(verified.every(file => file.sha256 === file.actualSha256), 'Supplementary REA body SHA mismatch')
supplementary = { archive: supplementaryPath, evidenceId: newer.evidence_id, total: verified.length, verified: verified.length,
changedFromBaseline: verified.filter(file => files.some(before => before.name === file.name && before.sha256 !== file.sha256)),
newNames: verified.filter(file => !files.some(before => before.name === file.name)).map(file => file.name) }
} catch (error) { if (error.code !== 'ENOENT') throw error }
const cssManifest = JSON.parse(await fs.readFile('.tmp/rea-css-recovered/manifest.json', 'utf8'))
const css = []
for (const file of cssManifest.files.filter(file => /^(connector-(?:device-code|token-config)-modal|skills-panel)-/.test(file.file))) {
const bytes = await fs.readFile(path.join('.tmp/rea-css-recovered', file.file))
const actualSha256 = crypto.createHash('sha256').update(bytes).digest('hex')
assert.equal(actualSha256, file.sha256, 'Retained connector CSS SHA mismatch')
css.push({ file: file.file, sha256: file.sha256, bytes: bytes.length, rules: file.rules, verified: true })
}
const runtimeBaselines = []
for (const directory of ['rea-market-connectors-static-20261016', 'rea-custom-connectors-static-20261016', 'rea-connector-detail-20261016']) {
const capture = path.join('.tmp', directory), layout = JSON.parse(await fs.readFile(path.join(capture, 'layout.json'), 'utf8'))
const bytes = await fs.readFile(path.join(capture, 'workbuddy.png'))
assert.equal(crypto.createHash('sha256').update(bytes).digest('hex'), layout.artifact.sha256)
const retained = JSON.parse(await fs.readFile(layout.archive, 'utf8')), probe = retained.normalized_result.visuals.probe
const frames = []
for (const [index, frame] of (probe?.frames || []).entries()) {
const bytes = await fs.readFile(path.join(capture, `frame-${String(index).padStart(3, '0')}.png`))
assert.equal(crypto.createHash('sha256').update(bytes).digest('hex'), frame.artifact.sha256)
frames.push({ index, stepId: frame.step_id, sha256: frame.artifact.sha256 })
}
runtimeBaselines.push({ directory, evidenceId: layout.evidenceId, screenshotSha256: layout.artifact.sha256,
probe: probe && { status: probe.status, steps: probe.steps, restoration: probe.restoration, frames } })
}
await fs.mkdir(output, { recursive: true })
const report = { result: 'PASS retained-script integrity', archive, evidenceId: evidence, version: 'WorkBuddy 5.8.2',
offsetUnit: 'zero-based JavaScript UTF-16 code units', filesTotal: files.length, verifiedCount: files.filter(file => file.verified).length,
files, selected, supplementary, css: { evidenceId: cssManifest.sourceEvidenceId, files: css }, runtimeBaselines, limitations: ['Retained REA exports only; this audit performs no original runtime actions or authentication.', 'Function presence does not establish backend behavior or current catalog contents.'] }
await fs.writeFile(path.join(output, 'integrity-and-anchors.json'), JSON.stringify(report, null, 2))
console.log(JSON.stringify({ result: report.result, total: files.length, verified: report.verifiedCount, selected: selected.length, report: path.join(output, 'integrity-and-anchors.json') }))
})().catch(error => { console.error(error); process.exitCode = 1 })
+91
View File
@@ -0,0 +1,91 @@
/** Read-only audit of already REA-exported WorkBuddy source; never reads the installed app. */
import fs from 'node:fs'
import crypto from 'node:crypto'
import path from 'node:path'
import { parse } from '@babel/parser'
const input = process.env.YCODE_EXPERT_AUDIT_INPUT || '.tmp/rea-runtime-scripts.json'
const bundle = JSON.parse(fs.readFileSync(input, 'utf8'))
const sha = text => crypto.createHash('sha256').update(text).digest('hex')
const modules = bundle.items.filter(item => typeof item.source?.artifact?.text === 'string')
for (const item of modules) {
if (sha(item.source.artifact.text) !== item.source.artifact.sha256) throw new Error(`REA text SHA mismatch: ${item.file_path}`)
}
const selected = modules.filter(item => /(?:contexts-|services-|runtime-|provider-|desktop-sdk-|main-content-core-|expert-|wb-input-add-|wb-Coc|adapters-)/.test(item.file_path))
const filter = process.argv[2]
const needle = process.argv[3]
const manifest = { source: input, evidenceId: bundle.evidence_id, version: 'WorkBuddy 5.8.2',
rootSha256: '035254bc76310ea46317433e4840602094543ef323d2d5c5aff7c131729de918',
verifiedModules: modules.length, offsetUnit: 'UTF-16 code units, end exclusive', modules: [], supplementary: [] }
for (const item of selected) {
const text = item.source.artifact.text
const basename = path.win32.basename(item.file_path)
if (filter && !basename.includes(filter)) continue
if (needle) {
const matches = [...text.matchAll(new RegExp(needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'))].slice(0, 16)
console.log(JSON.stringify({ basename, sha256: sha(text), matches: matches.map(match => ({ offset: match.index,
snippet: text.slice(Math.max(0, match.index - 180), match.index + 1300) })) }, null, 2))
continue
}
const ast = parse(text, { sourceType: 'module' })
const definitions = []
const visit = node => {
if (!node || typeof node !== 'object') return
let name
if (node.type === 'FunctionDeclaration' || node.type === 'ClassDeclaration') name = node.id?.name
if (node.type === 'VariableDeclarator') name = node.id?.name
if (node.type === 'AssignmentExpression') name = node.left?.name
if (node.type === 'ClassMethod' || node.type === 'ObjectMethod') name = node.key?.name
if (name && /expert/i.test(name)) definitions.push({ name, type: node.type, start: node.start, end: node.end,
sha256: sha(text.slice(node.start, node.end)) })
for (const [key, child] of Object.entries(node)) {
if (key === 'loc' || key === 'tokens' || key === 'comments') continue
if (Array.isArray(child)) child.forEach(visit)
else if (child && typeof child === 'object') visit(child)
}
}
visit(ast.program)
manifest.modules.push({ basename, filePath: item.file_path, sha256: sha(text), length: text.length,
imports: ast.program.body.filter(node => node.type === 'ImportDeclaration').map(node => ({
source: node.source.value, start: node.start, end: node.end,
bindings: node.specifiers.map(specifier => ({ local: specifier.local.name,
imported: specifier.imported?.name ?? specifier.type }))
})), definitions })
}
if (!needle) {
for (const file of ['.tmp/rea-experts-main-evidence.json', '.tmp/rea-experts-host-evidence.json']) {
if (!fs.existsSync(file)) continue
const evidence = JSON.parse(fs.readFileSync(file, 'utf8'))
if (evidence.normalized_result?.root_sha256 !== manifest.rootSha256) throw new Error(`Root pin mismatch: ${file}`)
const files = evidence.normalized_result.files.map(item => {
if (sha(item.text) !== item.sha256) throw new Error(`REA supplementary SHA mismatch: ${item.path}`)
return { path: item.path, sha256: item.sha256, length: item.text.length }
})
manifest.supplementary.push({ file, evidenceId: evidence.evidence_id, files })
}
const currentPath = '.tmp/rea-expert-runtime-scripts-20261016.json'
if (fs.existsSync(currentPath)) {
const current = JSON.parse(fs.readFileSync(currentPath, 'utf8'))
for (const item of current.items) {
if (sha(item.source.artifact.text) !== item.source.artifact.sha256) throw new Error(`Current REA SHA mismatch: ${item.file_path}`)
}
manifest.supplementary.push({ file: currentPath, evidenceId: current.evidence_id, verifiedModules: current.items.length })
}
const cssPath = '.tmp/rea-css-recovered/contexts-BkjwYSZT.css'
const cssManifest = JSON.parse(fs.readFileSync('.tmp/rea-css-recovered/manifest.json', 'utf8'))
const css = cssManifest.files.find(file => file.file === path.basename(cssPath))
if (!css || sha(fs.readFileSync(cssPath)) !== css.sha256) throw new Error('REA expert CSS SHA mismatch')
manifest.supplementary.push({ file: cssPath, evidenceId: cssManifest.sourceEvidenceId, sha256: css.sha256 })
const layoutPath = '.tmp/rea-expert-entry-static-20261016/layout.json'
if (fs.existsSync(layoutPath)) {
const layout = JSON.parse(fs.readFileSync(layoutPath, 'utf8'))
const imagePath = path.join(path.dirname(layoutPath), 'workbuddy.png')
if (sha(fs.readFileSync(imagePath)) !== layout.artifact.sha256) throw new Error('REA expert screenshot SHA mismatch')
manifest.supplementary.push({ file: layoutPath, evidenceId: layout.evidenceId, viewport: layout.viewport,
imageSha256: layout.artifact.sha256 })
}
fs.writeFileSync(process.env.YCODE_EXPERT_AUDIT_OUTPUT || '.tmp/experts-source-audit.json', JSON.stringify(manifest, null, 2))
console.log(JSON.stringify({ evidenceId: manifest.evidenceId, verifiedModules: modules.length,
indexedExpertDefinitions: manifest.modules.reduce((total, module) => total + module.definitions.length, 0),
astModules: manifest.modules.length, supplementaryRecords: manifest.supplementary.length }))
}
+45
View File
@@ -0,0 +1,45 @@
// Inspect only YCode's already-built, isolated homepage fixture. No WorkBuddy connection.
const fs = require('node:fs')
const path = require('node:path')
const { app, BrowserWindow } = require('electron')
app.disableHardwareAcceleration()
const root = path.resolve(__dirname, '..')
const output = path.join(root, '.tmp', 'home-visual-audit-20261016')
fs.mkdirSync(output, { recursive: true })
app.setPath('userData', path.join(output, 'user-data'))
const selectors = [
'.wb-home-header', '.wb-home-header__title', '.wb-scene-tabs',
'.wb-segmented', '.wb-segmented-item', '.wb-home-composer__chips',
'.quick-actions__item', '.wb-home-composer__input-slot', '.cr-input-container',
'.composer__toolbar', '.composer__toolbar-left', '.composer__toolbar-right',
'.wb-home-route__growth-buddy', '.source-home-robot img',
'.wb-home-composer__below-input',
]
const properties = ['font-family', 'font-size', 'font-weight', 'line-height', 'letter-spacing',
'color', 'background-color', 'border-radius', 'border-top-width', 'border-top-color',
'padding-top', 'padding-right', 'padding-bottom', 'padding-left', 'gap', 'box-shadow',
'text-rendering', '-webkit-font-smoothing', 'transform', 'opacity']
app.whenReady().then(async () => {
const win = new BrowserWindow({ width: 1336, height: 901, useContentSize: true,
frame: false, show: false, webPreferences: { contextIsolation: true, nodeIntegration: false } })
const errors = []
win.webContents.on('console-message', (_event, level, message) => { if (level === 3) errors.push(message) })
const fixture = path.join(root, '.tmp', 'home-native-ui', 'index.html')
await win.loadFile(fixture)
await new Promise(resolve => setTimeout(resolve, 1350))
const nodes = await win.webContents.executeJavaScript(`(${function (selectors, properties) {
return selectors.flatMap(selector => Array.from(document.querySelectorAll(selector), (el, index) => {
const r = el.getBoundingClientRect(), style = getComputedStyle(el)
return { selector, index, text: el.textContent, bounds: [r.x, r.y, r.width, r.height],
styles: Object.fromEntries(properties.map(key => [key, style.getPropertyValue(key)])) }
}))
}.toString()})(${JSON.stringify(selectors)},${JSON.stringify(properties)})`)
fs.writeFileSync(path.join(output, 'native-layout.json'), JSON.stringify({
capturedAt: new Date().toISOString(), fixture, viewport: { width: 1336, height: 901 }, nodes, errors,
scope: 'Frozen YCode fixture bundle, not the production app and not the original WorkBuddy runtime',
}, null, 2))
fs.writeFileSync(path.join(output, 'native.png'), (await win.webContents.capturePage()).toPNG())
win.destroy()
console.log(JSON.stringify({ output, nodes: nodes.length, errors }))
app.quit()
}).catch(error => { console.error(error); app.exit(1) })
+153
View File
@@ -0,0 +1,153 @@
"""Audit saved REA pixels and a frozen YCode fixture; never access the original app.
Run with a Python runtime providing Pillow. The exported buddy image is explicitly
only the screenshot-visible fragment, not a recovered original source asset.
"""
from collections import deque
from hashlib import sha256
from pathlib import Path
import json
from PIL import Image, ImageChops
ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / ".tmp/rea-home-static-20261016"
OUTPUT = ROOT / ".tmp/home-visual-audit-20261016"
ASSETS = ROOT / "src/renderer/src/components/source-home-assets"
EXPECTED_SHA = "360b076811ef19381f0670c314b24b06252310bf24fdc245f529f4894cfd7241"
def digest(file):
return sha256(file.read_bytes()).hexdigest()
def write_json(file, value):
file.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def main():
OUTPUT.mkdir(parents=True, exist_ok=True)
source_path = SOURCE / "workbuddy.png"
assert digest(source_path) == EXPECTED_SHA, "REA evidence screenshot changed"
layout = json.loads((SOURCE / "layout.json").read_text(encoding="utf-8"))
nodes = layout["layout"]["documents"][0]["nodes"]
by_index = {node["node_index"]: node for node in nodes}
assert by_index[919]["bounds"] == [1085.125, 286.5, 120, 120]
assert by_index[922]["bounds"] == [1065.125, 276.5, 140, 140]
source = Image.open(source_path).convert("RGB")
assert source.size == (1336, 901)
# Source composer begins y364.5. Row364 may contain antialias/overlap.
# Exclude that entire row; never invent the occluded body below it.
box = (1085, 286, 1205, 364)
crop = source.crop(box)
crop.save(OUTPUT / "buddy-visible-raw-rgb.png")
width, height = crop.size
pixels = crop.load()
outside = set()
queue = deque((x, y) for x in range(width) for y in (0, height - 1))
queue.extend((x, y) for y in range(height) for x in (0, width - 1))
while queue:
x, y = queue.popleft()
if (x, y) in outside or pixels[x, y] != (255, 255, 255):
continue
outside.add((x, y))
for xx, yy in ((x-1, y), (x+1, y), (x, y-1), (x, y+1)):
if 0 <= xx < width and 0 <= yy < height and (xx, yy) not in outside:
queue.append((xx, yy))
reference = crop.convert("RGBA")
alpha = Image.new("L", crop.size, 255)
for point in outside:
alpha.putpixel(point, 0)
reference.putalpha(alpha)
asset = ASSETS / "workbuddy-home-buddy-visible-reference.png"
reference.save(asset)
alpha.save(OUTPUT / "buddy-visible-derived-alpha.png")
# Exact equality on the observed white backdrop is the only alpha claim.
recomposed = Image.new("RGBA", crop.size, (255, 255, 255, 255))
recomposed.alpha_composite(reference)
diff = ImageChops.difference(recomposed.convert("RGB"), crop)
assert diff.getbbox() is None
foreground_box = alpha.getbbox()
manifest = {
"kind": "screenshot-visible-static-reference-fragment",
"evidenceId": layout["evidenceId"], "capturedAt": layout["captured_at"],
"originalProduct": "WorkBuddy", "observedVersion": "5.8.2 (screenshot sidebar)",
"sourceScreenshot": str(source_path.relative_to(ROOT)).replace("\\", "/"),
"sourceScreenshotSha256": EXPECTED_SHA, "viewport": [1336, 901], "deviceScale": 1,
"sourceGrowthWrapperNode": 919, "sourceGrowthWrapperBounds": by_index[919]["bounds"],
"sourceImageNode": 922, "sourceImageBounds": by_index[922]["bounds"],
"cropPixelBoxExclusiveRightBottom": list(box), "size": [width, height],
"cropOffsetFromGrowthWrapper": [-0.125, -0.5],
"foregroundBoxWithinCropExclusiveRightBottom": list(foreground_box),
"assetSha256": digest(asset),
"alphaDerivation": "Binary alpha: remove only exact-white pixels connected to crop edges. Preserve every other observed RGB pixel and enclosed white highlight.",
"transparentPixelCount": len(outside), "whiteBackdropCompositeChangedPixels": 0,
"knownBackdrop": [255, 255, 255],
"limitations": [
"Not the original thumbnail file, not a full robot, and not a recovered original alpha channel.",
"Only observed pixels y286..363 are included; composer overlap row364 and all occluded content are excluded.",
"Antialiased edge pixels retain their white matte; dark-background appearance is unverified.",
"No animation or hover frame is included. Original video URL and thumbnail URL have not been observed.",
"Use as a 120x78 static fragment at cropOffsetFromGrowthWrapper; applying the original 140x140 image style rescales and misplaces it.",
"Pixel placement is tied to this viewport and raster phase. Retest after viewport/sidebar/DPR changes.",
],
"sourceSelectionEvidence": {
"runtimeScriptEvidenceId": "ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb",
"script": "contexts-BMeWSUHk.js",
"scriptSha256": "5f35b7489b107e21ef452c8ee88a95265756f6fc444841eef869228bbc44768b",
"growthBuddyApproxOffset": 7027087, "fallbackWwnApproxOffset": 7018348,
"getGrowthBuddyApproxOffset": 4295749,
"finding": "GrowthBuddy selects adapter buddy.thumbnail_url unless absent/image-error; fallback is Wwn default-miao. Exact rendered URL is omitted by existing REA layout capture.",
"fallbackEvidenceId": "ev_60a03f3376f0e0c04e16d12c7798e25401d1e697f33b39e4b5f7027794b09223",
"fallbackAssetSha256": "d164ac0e6e54438ea2a26a26e8ef622de3d911a674f55086d465ce10b011641d",
},
}
write_json(ASSETS / "workbuddy-home-buddy-visible-reference.json", manifest)
# Static-region comparison is diagnostic only: integer x-translation leaves
# 0.125px raster-phase error. No full-page equality percentage is reported.
own_path = OUTPUT / "native.png"
region_metrics = []
if own_path.exists():
own = Image.open(own_path).convert("RGB")
assert own.size == source.size
regions = {"title": (660, 158, 950, 201), "scene-tabs": (692, 216, 918, 253),
"first-chip": (405, 316, 487, 349), "composer-top-border": (435, 364, 1055, 376)}
for name, region in regions.items():
x1, y1, x2, y2 = region
a, b = source.crop(region), own.crop((x1 - 2, y1, x2 - 2, y2))
distances = list(ImageChops.difference(a, b).getdata())
region_metrics.append({"name": name, "sourceBox": list(region), "ownTranslation": [-2, 0],
"residualRasterPhaseX": 0.125, "pixels": len(distances),
"changedPixels": sum(any(p) for p in distances),
"meanAbsoluteChannelDifference": sum(sum(p) for p in distances)/(len(distances)*3),
"maxChannelDifference": max(max(p) for p in distances)})
native_report = OUTPUT / "native-layout.json"
geometry = []
if native_report.exists():
own_nodes = json.loads(native_report.read_text(encoding="utf-8"))["nodes"]
for own_node in own_nodes:
selector = own_node["selector"]
if selector == ".source-home-robot img":
source_node = by_index[922]
else:
matches = [n for n in nodes if selector[1:] in n.get("attributes", {}).get("class", "").split()]
if len(matches) <= own_node["index"]:
continue
source_node = matches[own_node["index"]]
geometry.append({"selector": selector, "index": own_node["index"],
"sourceNodeIndex": source_node["node_index"], "source": source_node["bounds"],
"own": own_node["bounds"],
"delta": [b-a for a,b in zip(source_node["bounds"], own_node["bounds"])],
"styleDifferences": {key: {"source": source_node["styles"].get(key), "own": value}
for key, value in own_node["styles"].items()
if source_node["styles"].get(key) != value}})
write_json(OUTPUT / "audit.json", {"sourceEvidenceId": layout["evidenceId"],
"sourceScreenshotSha256": EXPECTED_SHA, "ownScreenshotSha256": digest(own_path) if own_path.exists() else None,
"geometry": geometry, "diagnosticRegionMetrics": region_metrics, "buddyReference": manifest,
"limitations": ["Frozen fixture data differs from original. Not a production/UI parity pass.",
"Source capture does not report actual platform fonts. Matching declared fonts does not prove glyph identity."]})
print(json.dumps({"asset": str(asset), "size": [width, height], "whiteCompositeChangedPixels": 0,
"foregroundBox": list(foreground_box), "geometrySamples": len(geometry)}, ensure_ascii=False))
if __name__ == "__main__":
main()
+72
View File
@@ -0,0 +1,72 @@
// Static assessment of already-exported REA artifacts. This script never changes production files.
import fs from 'node:fs/promises'
import { createHash } from 'node:crypto'
import { parse } from '@babel/parser'
import generateModule from '@babel/generator'
import postcss from 'postcss'
const generate = generateModule.default ?? generateModule
const output = '.tmp/model-select-audit'
await fs.mkdir(output, { recursive: true })
const runtime = JSON.parse(await fs.readFile('.tmp/rea-runtime-scripts.json', 'utf8'))
const panel = JSON.parse(await fs.readFile('.tmp/rea-assessment-model-source.json', 'utf8'))
const shared = runtime.items.find(item => item.file_path.endsWith('src-DPDj7_qD.js')).source.artifact
const settings = panel.normalized_result.files.find(file => file.path.endsWith('ModelsSettingsPanel-Dn7mp8BK.js'))
for (const [text, hash] of [[shared.text, '747e41d215375e1214c366213aea7e077d85421923029c61cbb6e9562be58de0'],
[settings.text, '739ab43ee521d8ef6e5b5b7731bb4e71b51454b2865f44a1228d7fcc2700149c']]) {
if (createHash('sha256').update(text).digest('hex') !== hash) throw Error('Pinned REA source changed')
}
function definitions(text, names, plugins = []) {
const found = []
function visit(node) {
if (!node || typeof node !== 'object') return
const name = node.type === 'FunctionDeclaration' ? node.id?.name : node.type === 'AssignmentExpression' ? node.left.name : undefined
if (names.includes(name)) found.push({ name, offset: node.start, length: node.end - node.start, text: text.slice(node.start, node.end), formatted: generate(node).code })
for (const value of Object.values(node)) if (Array.isArray(value)) value.forEach(visit); else if (value && typeof value === 'object') visit(value)
}
visit(parse(text, { sourceType: 'module', plugins }))
return found
}
const originals = definitions(shared.text, ['isOptionGroup', 'flattenOptions', 'defaultFilter', 'OptionLabel', 'OptionStringLabel', 'Select',
'Popover', 'PopoverLayer', 'usePopoverActivation', 'useExitTransition', 'buildTriggerAria', 'mergeRefs'])
const panelFunctions = definitions(settings.text, ['getProviderIcon', 'getLocalizedSearchText', 'formatProviderLabel'])
const own = definitions(await fs.readFile('src/renderer/src/components/ModelSettingsPanel.tsx', 'utf8'), ['Select'], ['typescript', 'jsx'])
for (const required of ['Select', 'Popover', 'PopoverLayer']) if (!originals.some(item => item.name === required)) throw Error(`Missing original ${required}`)
await fs.writeFile(`${output}/shared-select-readable.js`, originals.map(item => `// ${item.name} offset=${item.offset} length=${item.length}\n${item.formatted}`).join('\n\n'))
await fs.writeFile(`${output}/models-provider-readable.js`, panelFunctions.map(item => item.formatted).join('\n\n'))
await fs.writeFile(`${output}/own-select-readable.tsx`, own.map(item => item.formatted).join('\n\n'))
const css = []
for (const file of ['src-Bh3YZ35H.css', 'ModelsSettingsPanel-FfiX-Du7.css']) {
const text = file.startsWith('Models') ? panel.normalized_result.files.find(item => item.path.endsWith(file)).text : await fs.readFile(`.tmp/rea-css-recovered/${file}`, 'utf8')
const groups = { select: [], popover: [], input: [], provider: [] }
postcss.parse(text).walkRules(rule => {
const kind = /models-settings-panel__provider-(?:option|icon-image)/.test(rule.selector) ? 'provider'
: /\.wb-select/.test(rule.selector) ? 'select' : /\.wb-popover/.test(rule.selector) ? 'popover'
: /\.wb-input/.test(rule.selector) ? 'input' : undefined
if (kind) {
let copy = rule.clone()
for (let parent = rule.parent; parent?.type === 'atrule'; parent = parent.parent) { const wrapped = parent.clone({ nodes: [] }); wrapped.append(copy); copy = wrapped }
groups[kind].push(copy.toString())
}
})
css.push({ file, sha256: createHash('sha256').update(text).digest('hex'), rules: Object.fromEntries(Object.entries(groups).map(([key, value]) => [key, value.length])) })
await fs.writeFile(`${output}/${file}`, Object.values(groups).flat().join('\n'))
}
const states = []
for (const directory of ['rea-model-select-provider-static-20261016', 'rea-model-select-model-static-20261016']) {
const capture = JSON.parse(await fs.readFile(`.tmp/${directory}/layout.json`, 'utf8'))
const nodes = capture.layout.documents[0].nodes
const matches = selector => nodes.filter(node => node.attributes.class?.split(' ').includes(selector.slice(1)))
const selectors = ['.models-settings-panel__provider-select', '.models-settings-panel__model-select', '.wb-select-popover', '.wb-select__panel',
'.wb-select__search', '.wb-select__search-input', '.wb-select__list', '.wb-select__group-label', '.wb-select__option', '.models-settings-panel__editor-body']
states.push({ directory, evidenceId: capture.evidenceId, viewport: capture.viewport,
nodes: selectors.flatMap(selector => matches(selector).map((node, index) => ({ selector, index, nodeIndex: node.node_index,
bounds: node.bounds, styles: node.styles }))) })
}
const report = { runtimeEvidenceId: runtime.evidence_id, modelsPanelEvidenceId: panel.evidence_id,
sourceHashes: { shared: shared.sha256, panel: settings.sha256 },
originalDefinitions: originals.map(({ text, formatted, ...item }) => item), ownDefinitions: own.map(({ text, formatted, ...item }) => item), css, states,
scope: 'Read-only assessment of fixed REA source plus current own adapter. No production Select edits.',
limits: ['REA probe schema currently has no arbitrary text action. Search input/query/clear behavior is source-proven but query typing was not replayed on original.',
'Provider/model native screenshots cover default Tencent provider only; exact provider switches, filtered/no-result and viewport flip states remain to capture.'] }
await fs.writeFile(`${output}/report.json`, JSON.stringify(report, null, 2))
console.log(JSON.stringify({ originals: report.originalDefinitions, css, states: states.map(({ directory, evidenceId, viewport }) => ({ directory, evidenceId, viewport })) }))
+89
View File
@@ -0,0 +1,89 @@
// Read only saved, digest-verified REA exports; never run original code or touch its runtime.
import fs from 'node:fs/promises'
import path from 'node:path'
import { createHash } from 'node:crypto'
import { parse } from '@babel/parser'
import generator from '@babel/generator'
const generate = generator.default ?? generator
const runtime = JSON.parse(await fs.readFile('.tmp/rea-runtime-scripts.json', 'utf8'))
const output = '.tmp/skills-audit-20261016'
await fs.mkdir(output, { recursive: true })
const digest = text => createHash('sha256').update(text).digest('hex')
const sources = runtime.items.filter(item => item.source?.artifact?.text).map(item => {
const artifact = item.source.artifact
if (digest(artifact.text) !== artifact.sha256) throw Error('REA export changed: ' + item.file_path)
return { file: path.basename(item.file_path.replaceAll('\\', '/')), ...artifact }
})
const verifiedRuntimeScripts=sources.length
const selected = JSON.parse(await fs.readFile('.tmp/rea-skills-market-selected-20261016.json','utf8'))
if (selected.normalized_result.root_sha256 !== '035254bc76310ea46317433e4840602094543ef323d2d5c5aff7c131729de918') throw Error('Selected source root changed')
for (const file of selected.normalized_result.files) {
if (digest(file.text) !== file.sha256) throw Error('Selected source digest changed')
if (file.path.endsWith('.js')) sources.push({file:path.basename(file.path),text:file.text,sha256:file.sha256,selectedEvidenceId:selected.evidence_id})
}
const pinned = { 'skills-panel-CvMlV3m8.js': 'b30c43c11ec476db639d6be58065d927411845487d34b0ea8e839d1c6eb7a238',
'skills-market-Cwf-NvXV.js':'f1434cadabf5ed3762f00fc2a356bc7418b0b4aab27db62dc9fbbe5add6f6dfb',
'standalone-skill-detail-modal-Bi5X8CZ4.js': 'ee7de2a7123f94110aa01258d3d6b3fcc3ce48f2ec5fd8f91cb3367c817a0f01' }
for (const [file, sha] of Object.entries(pinned)) if (sources.find(source => source.file === file)?.sha256 !== sha) throw Error('Pinned source changed: ' + file)
const report = { version: 'WorkBuddy 5.8.2', evidenceId: runtime.evidence_id, selectedEvidenceId:selected.evidence_id, verifiedRuntimeScripts,
rootSha256: '035254bc76310ea46317433e4840602094543ef323d2d5c5aff7c131729de918', modules: [], references: [], css: [] }
for (const source of sources) {
if (/^(?:editor\.|clipboard-|remark-|file-path-|local-file-path-|branch-switch-|load-i18n-|space-library-)/.test(source.file)) continue
const skillModule = /skill|entitlement/i.test(source.file), helperModule = !skillModule && /function\s+\w*(?:Skill|Slash|Command|Entitle|PaidSkill)/.test(source.text)
if (!skillModule && !helperModule) continue
const ast = parse(source.text, { sourceType: 'module' }), imports = [], exports = [], definitions = [], classes = [], hostMethods = [], calls = []
function visit(node, parent) {
if (!node || typeof node !== 'object') return
if (node.type === 'ImportDeclaration') imports.push({ file: node.source.value, bindings: node.specifiers.map(item => ({ local: item.local.name, imported: item.imported?.name })) })
if (node.type === 'ExportNamedDeclaration') exports.push(...node.specifiers.map(item => ({ local: item.local.name, exported: item.exported.name })))
const name = node.type === 'FunctionDeclaration' ? node.id?.name : node.type === 'AssignmentExpression' && ['FunctionExpression', 'ArrowFunctionExpression'].includes(node.right.type) ? node.left.name : node.type === 'VariableDeclarator' && ['FunctionExpression', 'ArrowFunctionExpression'].includes(node.init?.type) ? node.id.name : undefined
if (name && (skillModule || /skill|slash|command|entitle|scan/i.test(name))) definitions.push({ name, offset: node.start, length: node.end-node.start, text: source.text.slice(node.start, node.end), formatted: generate(node).code })
if (['ClassDeclaration','ClassExpression'].includes(node.type)) {
const name=node.id?.name??(parent?.type==='AssignmentExpression'?parent.left.name:parent?.type==='VariableDeclarator'?parent.id.name:undefined)
if(name&&/Skill|Market|Enterprise|Plugin|SearchService|UIService|AsyncSecurityScan|DetailService|KnotService/.test(name)) classes.push({name,offset:node.start,length:node.end-node.start,methods:node.body.body.filter(item=>item.key?.name).map(item=>({name:item.key.name,offset:item.start,length:item.end-item.start})),formatted:generate(node).code})
}
if(source.file.startsWith('workbuddy-provider-')&&node.type==='ClassMethod'&&/Skill|Command/.test(node.key?.name??''))hostMethods.push({name:node.key.name,offset:node.start,length:node.end-node.start,formatted:generate(node).code})
if (['CallExpression', 'OptionalCallExpression'].includes(node.type) && ['MemberExpression', 'OptionalMemberExpression'].includes(node.callee.type)) {
const method = node.callee.property.name ?? node.callee.property.value
if (typeof method === 'string' && /skill|scan|purchase|entitle|install|market|payment|subscription|import|upload|update|openUrl|openExternal|reveal|redeem|checkout|select.*file|folder|directory/i.test(method)) calls.push({ method, offset: node.start, expression: source.text.slice(node.start, node.end).slice(0,550) })
}
for (const [key, value] of Object.entries(node)) if (key !== 'loc') {
if (Array.isArray(value)) value.forEach(child => visit(child,node)); else if (value && typeof value === 'object') visit(value,node)
}
}
visit(ast)
if (skillModule) await fs.writeFile(`${output}/${source.file}`, generate(ast).code)
else await fs.writeFile(`${output}/${source.file}-skill-helpers.js`, definitions.map(item => `// ${item.name} offset=${item.offset} length=${item.length}\n${item.formatted}`).join('\n\n'))
if(classes.length) await fs.writeFile(`${output}/${source.file}-skill-classes.js`,classes.map(item=>`// ${item.name} offset=${item.offset} length=${item.length}\n${item.formatted}`).join('\n\n'))
if(hostMethods.length)await fs.writeFile(`${output}/${source.file}-skill-host-methods.js`,hostMethods.map(item=>`// ${item.name} offset=${item.offset} length=${item.length}\n${item.formatted}`).join('\n\n'))
report.modules.push({ file: source.file, evidenceId:source.selectedEvidenceId??runtime.evidence_id, sha256: source.sha256, bytes: Buffer.byteLength(source.text), skillModule, imports, exports,
definitions: definitions.map(({ text, formatted, ...item }) => item),classes:classes.map(({formatted,...item})=>item),hostMethods:hostMethods.map(({formatted,...item})=>item),calls })
for (const item of definitions) if (!skillModule) report.references.push({file:source.file,sha256:source.sha256,name:item.name,offset:item.offset,length:item.length})
}
for(const file of selected.normalized_result.files.filter(file=>file.path.endsWith('.css'))) {
report.css.push({file:path.basename(file.path),sha256:file.sha256,evidenceId:selected.evidence_id,sizeBytes:file.size_bytes})
await fs.writeFile(`${output}/${path.basename(file.path)}`,file.text)
}
const cssManifest = JSON.parse(await fs.readFile('.tmp/rea-css-recovered/manifest.json','utf8'))
for (const file of cssManifest.files.filter(file => /skill/.test(file.file))) {
const text = await fs.readFile('.tmp/rea-css-recovered/' + file.file, 'utf8')
if (digest(text) !== file.sha256) throw Error('REA CSS changed: ' + file.file)
report.css.push(file)
}
try {
const fresh=JSON.parse(await fs.readFile('.tmp/rea-expert-runtime-scripts-20261016.json','utf8'))
const items=fresh.items??fresh.normalized_result?.items??[]
for(const item of items){const a=item.source?.artifact;if(a?.text&&digest(a.text)!==a.sha256)throw Error('Fresh runtime digest changed')}
const market=items.find(item=>item.file_path?.endsWith('skills-market-Cwf-NvXV.js'))
report.liveLoadedMarket={evidenceId:fresh.evidence_id,verifiedScriptCount:items.length,sha256:market?.source?.artifact?.sha256}
}catch(error){if(error.code!=='ENOENT')throw error}
report.captures=[]
for(const [directory,imageFile,step]of [['rea-market-skills-20261016','frame-000.png','skills-ready'],['rea-installed-skills-static-20261016','workbuddy.png',null],['rea-add-skill-options-20261016','frame-000.png','add-skill-options-ready']]){
const capture=JSON.parse(await fs.readFile(`.tmp/${directory}/layout.json`,'utf8'))
const record=step?capture.probe.frames.find(frame=>frame.step_id===step):capture
const bytes=await fs.readFile(`.tmp/${directory}/${imageFile}`)
if(digest(bytes)!==record.artifact.sha256)throw Error('REA screenshot SHA changed: '+directory)
report.captures.push({directory,imageFile,evidenceId:capture.evidenceId,sha256:record.artifact.sha256,step,probeStatus:capture.probe?.status??'not-requested',steps:capture.probe?.steps??[],restoration:capture.probe?.restoration??[]})
}
await fs.writeFile(`${output}/report.json`,JSON.stringify(report,null,2))
console.log(JSON.stringify({verified:report.verifiedRuntimeScripts,modules:report.modules.map(module=>({file:module.file,definitions:module.definitions.length,calls:module.calls.length})),css:report.css.map(file=>file.file)}))
+102
View File
@@ -0,0 +1,102 @@
// Exercise the actual YCode WebContentsView host against a loopback-only HTTP server.
if (!process.versions.electron) {
const path = require('node:path')
require('esbuild').buildSync({ entryPoints: ['src/main/browser-host.ts'], platform: 'node', format: 'cjs', bundle: true,
external: ['electron'], outfile: '.tmp/browser-host-test-module.cjs', logLevel: 'silent' })
require('esbuild').buildSync({ entryPoints: ['src/shared/browser-host.ts'], platform: 'node', format: 'cjs', bundle: true,
outfile: '.tmp/browser-host-contract.cjs', logLevel: 'silent' })
const env = { ...process.env }
delete env.ELECTRON_RUN_AS_NODE
const result = require('node:child_process').spawnSync(require('electron'), [__filename], { env, windowsHide: true, stdio: 'inherit', timeout: 60000 })
process.exitCode = result.status ?? 1
} else {
const { app, BrowserWindow, WebContentsView } = require('electron')
const assert = require('node:assert/strict')
const path = require('node:path')
const http = require('node:http')
app.setPath('userData', path.resolve('.tmp/browser-host-regression-data'))
app.disableHardwareAcceleration()
const server = http.createServer((req, res) => {
const page = req.url === '/b' ? 'B' : 'A'
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
res.end(`<html><head><title>Local ${page}</title></head><body>Local ${page}<a href="/b">Next</a></body></html>`)
})
let owner, other
async function waitFor(check, label) {
for (let attempt = 0; attempt < 200; attempt++) { if (await check()) return; await new Promise(resolve => setTimeout(resolve, 25)) }
throw Error('Browser condition timed out: ' + label)
}
app.whenReady().then(async () => {
const host = require(path.resolve('.tmp/browser-host-test-module.cjs'))
const contract = require(path.resolve('.tmp/browser-host-contract.cjs'))
assert.equal(contract.normalizeBrowserAddressInput('127.0.0.1:1234/a'), 'http://127.0.0.1:1234/a')
assert.equal(contract.normalizeBrowserAddressInput('example.com'), 'https://example.com/')
assert.equal(contract.normalizeBrowserAddressInput('hello world'), 'https://www.sogou.com/web?query=hello%20world')
assert.throws(() => contract.normalizeBrowserHostUrl('https://user:secret@example.com'), /仅支持/)
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
const base = `http://127.0.0.1:${server.address().port}`
owner = new BrowserWindow({ width: 800, height: 600, show: false, webPreferences: { sandbox: true, nodeIntegration: false, contextIsolation: true } })
other = new BrowserWindow({ width: 800, height: 600, show: false })
const first = host.createBrowser(owner, 'task-a')
const second = host.createBrowser(owner, 'task-b')
const result = await host.navigateBrowser(owner, first.id, base + '/a', 'task-a')
assert.equal(result.title, 'Local A')
assert.equal(result.error, undefined)
assert.throws(() => host.getBrowserState(owner, first.id, 'task-b'), /不属于/)
assert.throws(() => host.getBrowserState(other, first.id, 'task-a'), /不属于/)
assert.throws(() => host.getBrowserState(owner, first.id), /不属于/)
await assert.rejects(() => host.navigateBrowser(owner, first.id, 'file:///C:/Windows/win.ini', 'task-a'), /仅支持/)
await assert.rejects(() => host.navigateBrowser(owner, first.id, 'javascript:alert(1)', 'task-a'), /仅支持/)
assert.equal(host.getBrowserState(owner, first.id, 'task-a').url, base + '/a')
const views = owner.contentView.children.filter(view => view instanceof WebContentsView)
assert.equal(views.length, 2)
const primary = views.find(view => view.webContents.getURL() === base + '/a')
const secondary = views.find(view => view !== primary)
const secondaryContents = secondary.webContents
const prefs = primary.webContents.getLastWebPreferences()
assert.equal(prefs.sandbox, true); assert.equal(prefs.nodeIntegration, false); assert.equal(prefs.contextIsolation, true)
assert.ok(!prefs.preload)
assert.equal(await primary.webContents.executeJavaScript('typeof require'), 'undefined')
assert.equal(await primary.webContents.executeJavaScript('typeof process'), 'undefined')
assert.equal(await primary.webContents.executeJavaScript('Notification.requestPermission()'), 'denied')
await primary.webContents.session.cookies.set({ url: base, name: 'isolation', value: 'private-to-first' })
assert.equal((await secondary.webContents.session.cookies.get({ url: base, name: 'isolation' })).length, 0)
host.setBrowserBounds(owner, first.id, { x: -20, y: 50, width: 2000, height: 2000 }, 'task-a')
const [width, height] = owner.getContentSize()
assert.deepEqual(primary.getBounds(), { x: 0, y: 50, width, height: height - 50 })
assert.equal(host.getBrowserState(owner, first.id, 'task-a').visible, true)
assert.throws(() => host.setBrowserBounds(owner, first.id, { x: .5, y: 0, width: 10, height: 10 }, 'task-a'), /整数/)
host.hideBrowser(owner, first.id, 'task-a')
assert.equal(host.getBrowserState(owner, first.id, 'task-a').visible, false)
await host.navigateBrowser(owner, first.id, base + '/b', 'task-a')
assert.equal(host.getBrowserState(owner, first.id, 'task-a').canGoBack, true)
host.backBrowser(owner, first.id, 'task-a')
await waitFor(() => host.getBrowserState(owner, first.id, 'task-a').title === 'Local A' && !host.getBrowserState(owner, first.id, 'task-a').loading, 'back to A')
assert.equal(host.getBrowserState(owner, first.id, 'task-a').canGoForward, true)
host.forwardBrowser(owner, first.id, 'task-a')
await waitFor(() => host.getBrowserState(owner, first.id, 'task-a').title === 'Local B' && !host.getBrowserState(owner, first.id, 'task-a').loading, 'forward to B')
host.reloadBrowser(owner, first.id, 'task-a')
await waitFor(() => !host.getBrowserState(owner, first.id, 'task-a').loading, 'reload')
await primary.webContents.executeJavaScript(`window.open(${JSON.stringify(base + '/a')}, '_blank'); undefined`)
await waitFor(() => host.getBrowserState(owner, first.id, 'task-a').title === 'Local A', 'popup stays in owned browser')
assert.equal(BrowserWindow.getAllWindows().length, 2)
host.closeBrowser(owner, first.id, 'task-a')
assert.throws(() => host.getBrowserState(owner, first.id, 'task-a'), /不存在/)
const draft = host.createBrowser(owner)
assert.throws(() => host.rebindBrowser(other, draft.id, undefined, 'created-session'), /不属于/)
const rebound = host.rebindBrowser(owner, draft.id, undefined, 'created-session')
assert.equal(rebound.sessionId, 'created-session')
assert.equal(rebound.id, draft.id)
assert.throws(() => host.getBrowserState(owner, draft.id), /不属于/)
assert.equal(host.getBrowserState(owner, draft.id, 'created-session').sessionId, 'created-session')
host.closeBrowser(owner, draft.id, 'created-session')
owner.destroy()
await waitFor(() => secondaryContents.isDestroyed(), 'owner closes child browser')
assert.throws(() => host.getBrowserState(owner, second.id, 'task-b'), /不存在/)
console.log('Actual Electron browser host: loopback navigation/back/forward/reload, sandbox, cookie/window/session isolation, bounds, popup routing and lifecycle passed')
}).catch(error => { console.error(error); process.exitCode = 1 }).finally(() => {
if (owner && !owner.isDestroyed()) owner.destroy()
if (other && !other.isDestroyed()) other.destroy()
server.close(() => app.exit(process.exitCode || 0))
})
}
+19
View File
@@ -0,0 +1,19 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { renderToStaticMarkup } from 'react-dom/server'
import { NativeBrowserPane } from '../src/renderer/src/workbuddy/workbench/NativeBrowserPane'
;(globalThis as any).window = { ycode: { browser: {} } }
const markup = renderToStaticMarkup(<NativeBrowserPane id="browser-fixture" sessionId="fixture" />)
assert.match(markup, /data-browser-host-id="browser-fixture"/)
assert.match(markup, /_browserToolbar_x8iy7_18 _nativeToolbar_rh4x8_319/)
assert.match(markup, /搜索或输入网址/)
assert.match(markup, /输入网址,或打开对话中的链接/)
assert.match(markup, /aria-label="后退" disabled/)
assert.match(markup, /M5\.2806 11\.9404/)
assert.match(markup, /M7\.4702 11\.9404/)
assert.match(markup, /M14\.83 7\.415/)
const css = readFileSync('src/renderer/src/workbuddy/workbench/browser-source.css', 'utf8')
assert.match(css, /\._browserToolbar_x8iy7_18\{[^}]*padding:8px 16px/)
assert.match(css, /\._browserUrl_rh4x8_10\{[^}]*height:28px/)
console.log('Native browser toolbar, disabled state, source locale/SVG and CSS contracts passed')
+31
View File
@@ -0,0 +1,31 @@
// YCode-only live DOM/screenshot inspection. Original WorkBuddy must use REA instead.
import fs from 'node:fs/promises'
const targets = await (await fetch('http://127.0.0.1:9237/json/list')).json()
const setting = process.argv.includes('--settings')
const target = targets.find(item => item.type === 'page' && item.url.includes('window=settings') === setting)
if (!target) throw new Error('Requested YCode window not found')
const socket = new WebSocket(target.webSocketDebuggerUrl)
await new Promise(resolve => { socket.onopen = resolve })
let sequence = 0
const pending = new Map()
socket.onmessage = event => { const message = JSON.parse(event.data); const task = pending.get(message.id); if (task) { pending.delete(message.id); message.error ? task.reject(message.error) : task.resolve(message.result) } }
const send = (method, params = {}) => new Promise((resolve, reject) => { const id = ++sequence; pending.set(id, { resolve, reject }); socket.send(JSON.stringify({ id, method, params })) })
const evaluate = async expression => { const response = await send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true }); if (response.exceptionDetails) throw new Error(response.exceptionDetails.text); return response.result.value }
const out = '.tmp/ycode-live-' + (setting ? 'settings' : 'main')
try {
await fs.mkdir(out, { recursive: true })
const viewportArgument = process.argv.find(value => value.startsWith('--viewport='))
if (viewportArgument) {
const match = /^--viewport=(\d+)x(\d+)$/.exec(viewportArgument)
if (!match || Number(match[1]) < 160 || Number(match[2]) < 160) throw new Error('Invalid viewport')
await send('Emulation.setDeviceMetricsOverride', { width: Number(match[1]), height: Number(match[2]), deviceScaleFactor: 1, mobile: false })
}
if (process.argv.includes('--home')) await evaluate(`Array.from(document.querySelectorAll('.sidebar__tab')).find(button=>button.textContent.includes('新建任务'))?.click()`)
await evaluate('document.fonts.ready.then(()=>true)')
await evaluate('new Promise(resolve=>setTimeout(resolve,1200))')
const layout = await evaluate(`(()=>{const selectors=['.sidebar','.chat-pane','.source-home','.wb-home-page','.wb-home-page__main-content','.wb-home-header','.wb-scene-tabs','.wb-home-composer','.wb-home-composer__chips','.wb-home-composer__input-slot','.wb-home-composer__below-input','.wb-composer','.cr-input-container','.wb-composer__workspace-footer','.cr-input-toolbar','.cr-permission-setting','.settings-modal','.settings-modal__nav','.settings-modal__content','.models-settings-panel','.models-settings-panel__editor','.sidebar-next'];return{viewport:{width:innerWidth,height:innerHeight},nodes:selectors.flatMap(selector=>Array.from(document.querySelectorAll(selector)).map(node=>{const box=node.getBoundingClientRect(),style=getComputedStyle(node);return{selector,bounds:[box.x,box.y,box.width,box.height],font:style.fontFamily,fontSize:style.fontSize,lineHeight:style.lineHeight,padding:style.padding,margin:style.margin,gap:style.gap,background:style.backgroundColor,display:style.display}}))}})()`)
await fs.writeFile(out + '/layout.json', JSON.stringify(layout, null, 2))
const screenshot = await send('Page.captureScreenshot', { format: 'png', fromSurface: true })
await fs.writeFile(out + '/screen.png', Buffer.from(screenshot.data, 'base64'))
console.log(JSON.stringify({ targetId: target.id, output: out, viewport: layout.viewport }))
} finally { socket.close() }
+324
View File
@@ -0,0 +1,324 @@
import assert from 'node:assert/strict'
import { testTerminalHostPlugin } from './test-terminal-host-plugin'
import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { build } from 'esbuild'
import type { ChatMessage } from '../src/shared/types'
interface PersistenceTestState {
userData: string
handlers: Map<string, (...args: unknown[]) => unknown>
promptCalls: number
abortCalls: number
cancelOutputEmitted: boolean
useOmp: boolean
fallbackHistory: ChatMessage[] | null
}
const TEST_STATE_KEY = '__YCODE_CHAT_PERSISTENCE_TEST__'
function wait(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
}
async function waitForMessages(
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>,
sessionId: string,
done: (messages: ChatMessage[]) => boolean
): Promise<ChatMessage[]> {
for (let attempt = 0; attempt < 100; attempt++) {
const messages = (await invoke('session:messages', sessionId)) as ChatMessage[]
if (done(messages)) return messages
await wait(10)
}
const last = (await invoke('session:messages', sessionId)) as ChatMessage[]
assert.fail(`Timed out waiting for persisted messages: ${JSON.stringify(last)}`)
}
async function readDiskMessages(userData: string, sessionId: string): Promise<ChatMessage[]> {
const stored = JSON.parse(await readFile(join(userData, 'sessions.json'), 'utf8')) as {
sessions: Array<{ id: string; messages: ChatMessage[] }>
}
return stored.sessions.find((item) => item.id === sessionId)?.messages ?? []
}
async function waitForDiskAssistant(
userData: string,
sessionId: string,
matches: (message: ChatMessage) => boolean
): Promise<ChatMessage> {
for (let attempt = 0; attempt < 100; attempt++) {
const message = (await readDiskMessages(userData, sessionId)).findLast((entry) => entry.role === 'assistant')
if (message && matches(message)) return message
await wait(10)
}
const messages = await readDiskMessages(userData, sessionId)
assert.fail(`Timed out waiting for assistant checkpoint: ${JSON.stringify(messages.at(-1))}`)
}
function assistant(messages: ChatMessage[]): ChatMessage {
const result = messages.findLast((message) => message.role === 'assistant')
assert.ok(result, 'session should contain an assistant message')
return result
}
async function main(): Promise<void> {
const tempRoot = await mkdtemp(join(tmpdir(), 'ycode-chat-persistence-'))
const state: PersistenceTestState = {
userData: join(tempRoot, 'userData'),
handlers: new Map(),
promptCalls: 0,
abortCalls: 0,
cancelOutputEmitted: false,
useOmp: true,
fallbackHistory: null
}
await mkdir(state.userData, { recursive: true })
const globals = globalThis as typeof globalThis & Record<string, unknown>
const previousState = globals[TEST_STATE_KEY]
globals[TEST_STATE_KEY] = state
try {
const ipcEntry = join(process.cwd(), 'src', 'main', 'ipc.ts')
const sessionsEntry = join(process.cwd(), 'src', 'main', 'sessions.ts')
const outfile = join(tempRoot, 'ipc-chat-persistence.mjs')
await build({
stdin: {
contents: `export { registerIpc } from ${JSON.stringify(ipcEntry)};\nexport * as sessions from ${JSON.stringify(sessionsEntry)};`,
resolveDir: process.cwd(),
sourcefile: 'chat-persistence-regression-entry.ts',
loader: 'ts'
},
outfile,
bundle: true,
external: ['node-pty'],
platform: 'node',
format: 'esm',
banner: { js: "import { createRequire as createIpcTestRequire } from 'node:module'; const require = createIpcTestRequire(import.meta.url);" },
logLevel: 'silent',
plugins: [
testTerminalHostPlugin,
{
name: 'chat-persistence-mocks',
setup(pluginBuild) {
pluginBuild.onResolve({ filter: /^electron$/ }, () => ({ path: 'electron', namespace: 'mock-electron' }))
pluginBuild.onLoad({ filter: /.*/, namespace: 'mock-electron' }, () => ({
loader: 'js',
contents: `
const state = globalThis.${TEST_STATE_KEY};
export const app = {
getPath: (name) => name === 'userData' ? state.userData : state.userData,
getName: () => 'YCode',
getVersion: () => '0.1.0'
};
export const safeStorage = {
isEncryptionAvailable: () => false,
encryptString: (value) => Buffer.from(value),
decryptString: (value) => value.toString()
};
export const ipcMain = {
handle: (name, handler) => state.handlers.set(name, handler)
};
export const dialog = { showOpenDialog: async () => ({ canceled: true, filePaths: [] }) };
export const shell = { openPath: async () => '' };
export class BrowserWindow {}
export class WebContentsView {}
`
}))
pluginBuild.onResolve({ filter: /^\.\/omp$/ }, () => ({ path: 'omp', namespace: 'mock-omp' }))
pluginBuild.onLoad({ filter: /.*/, namespace: 'mock-omp' }, () => ({
loader: 'js',
contents: `
import { EventEmitter } from 'node:events';
const state = globalThis.${TEST_STATE_KEY};
const toolId = 'persisted-tool';
const fullOutput = '${'long-output-'.repeat(60)}PERSISTED-OUTPUT-SENTINEL';
const emit = (kernel, frame) => kernel.emit('event', frame);
export class OmpKernel extends EventEmitter {
constructor() { super(); }
setHostTools() { return Promise.resolve({ type: 'response', success: true, data: { toolNames: [] } }); }
send() { return Promise.resolve({ type: 'response', success: true, data: { messages: [] } }); }
prompt(message) {
state.promptCalls++;
if (message.split('\\n', 1)[0] === 'persist-cancel') {
setImmediate(() => {
emit(this, { type: 'message_update', assistantMessageEvent: { type: 'text_delta', delta: 'cancel-before' } });
emit(this, { type: 'tool_execution_start', toolCall: { id: toolId, name: 'shell', args: { command: 'long-running' } } });
emit(this, { type: 'tool_execution_update', toolCall: { id: toolId, partialResult: 'cancelled tool output retained' } });
state.cancelOutputEmitted = true;
});
} else {
setImmediate(() => {
emit(this, { type: 'message_update', assistantMessageEvent: { type: 'text_delta', delta: 'before' } });
emit(this, { type: 'tool_execution_start', toolCall: { id: toolId, name: 'read_file', args: { path: 'large.txt' } } });
emit(this, { type: 'tool_execution_update', toolCall: { id: toolId, partialResult: fullOutput } });
emit(this, { type: 'message_update', assistantMessageEvent: { type: 'text_delta', delta: 'after' } });
emit(this, { type: 'tool_execution_end', toolCall: { id: toolId, name: 'read_file', result: { content: [{ type: 'text', text: fullOutput }], isError: false }, success: true } });
emit(this, { type: 'agent_end', yielded: true });
});
}
return Promise.resolve({ type: 'response', success: true });
}
abort() {
state.abortCalls++;
setImmediate(() => emit(this, { type: 'agent_end', yielded: true }));
return Promise.resolve({ type: 'response', success: true });
}
dispose() {}
}
export const detectOmpPath = () => state.useOmp ? 'mock-omp' : null;
export const getOmpInfo = () => ({ available: true, exePath: 'mock-omp', profile: 'ycode', model: 'ycode/test' });
`
}))
pluginBuild.onResolve({ filter: /^\.\/agent\/loop$/ }, () => ({ path: 'agent-loop', namespace: 'mock-loop' }))
pluginBuild.onLoad({ filter: /.*/, namespace: 'mock-loop' }, () => ({
loader: 'js',
contents: `
const state = globalThis.${TEST_STATE_KEY};
export async function runTurn(options) {
state.fallbackHistory = options.history;
const content = 'fallback response';
options.emit({ runId: options.runId, type: 'text', delta: content });
options.emit({ runId: options.runId, type: 'done', content, tools: [] });
return { content, tools: [] };
}
`
}))
pluginBuild.onResolve({ filter: /^\.\/gateway$/ }, () => ({ path: 'gateway', namespace: 'mock-gateway' }))
pluginBuild.onLoad({ filter: /.*/, namespace: 'mock-gateway' }, () => ({
loader: 'js',
contents: 'export class Gateway { constructor() {} }'
}))
}
}
]
})
const ipc = await import(pathToFileURL(outfile).href)
;(ipc.registerIpc as (getWindow: () => unknown) => void)(() => ({
webContents: { send: () => undefined }
}))
const invoke = (channel: string, ...args: unknown[]): Promise<unknown> => {
const handler = state.handlers.get(channel)
if (!handler) throw new Error(`Missing IPC handler: ${channel}`)
return Promise.resolve(handler({}, ...args))
}
const normalSession = (await invoke('session:create', 'ordered persistence')) as { id: string }
await invoke('chat:send', { sessionId: normalSession.id, text: 'persist-order' })
const normalMessages = await waitForMessages(invoke, normalSession.id, (messages) => assistant(messages).status === 'done')
const renamedSession = ((await invoke('session:list')) as Array<{ id: string; title: string }>)
.find((entry) => entry.id === normalSession.id)
assert.equal(renamedSession?.title, 'persist-order', 'the first send should set the session title')
const normalAssistant = assistant(normalMessages)
const expectedParts = [
{ type: 'text', text: 'before' },
{ type: 'tool', toolId: 'persisted-tool' },
{ type: 'text', text: 'after' }
]
assert.deepEqual(normalAssistant.parts, expectedParts, 'session:messages should retain text/tool/text order')
assert.equal(normalAssistant.status, 'done')
assert.equal(normalAssistant.tools?.length, 1)
const normalTool = normalAssistant.tools?.[0]
assert.ok(normalTool, 'completed tool should be present')
assert.ok((normalTool.output?.length ?? 0) > 600, 'full tool output should exceed the summary limit')
assert.ok(normalTool.output?.endsWith('PERSISTED-OUTPUT-SENTINEL'), 'full tool output sentinel should be retained')
assert.ok((normalTool.summary?.length ?? Infinity) < (normalTool.output?.length ?? 0),
'the summary may be truncated while full output remains intact')
const diskNormalAssistant = assistant(await readDiskMessages(state.userData, normalSession.id))
assert.deepEqual(diskNormalAssistant.parts, expectedParts, 'disk JSON should retain text/tool/text order')
assert.ok((diskNormalAssistant.tools?.[0].output?.length ?? 0) > 600,
'disk JSON should retain output beyond the summary limit')
assert.equal(diskNormalAssistant.tools?.[0].output, normalTool.output,
'disk JSON should retain the complete tool output')
assert.ok(diskNormalAssistant.tools?.[0].output?.endsWith('PERSISTED-OUTPUT-SENTINEL'))
const fallbackSession = (await invoke('session:create', 'fallback history')) as { id: string }
const sessions = ipc.sessions as {
appendMessage(sessionId: string, message: ChatMessage): Promise<void>
}
await sessions.appendMessage(fallbackSession.id, {
id: 'fallback-user',
role: 'user',
content: 'old request',
createdAt: Date.now(),
status: 'done'
})
await sessions.appendMessage(fallbackSession.id, {
id: 'fallback-assistant',
role: 'assistant',
content: 'old answer',
parts: [{ type: 'text', text: 'old answer' }],
createdAt: Date.now(),
status: 'done'
})
state.useOmp = false
await invoke('chat:send', { sessionId: fallbackSession.id, text: 'fallback-history' })
const fallbackMessages = await waitForMessages(
invoke,
fallbackSession.id,
(messages) => assistant(messages).content === 'fallback response' && assistant(messages).status === 'done'
)
assert.equal(assistant(fallbackMessages).content, 'fallback response')
assert.deepEqual(state.fallbackHistory?.map((message) => message.content), ['old request', 'old answer'],
'the fallback loop should receive previously persisted user and assistant messages')
state.useOmp = true
const cancelSession = (await invoke('session:create', 'cancelled persistence')) as { id: string }
const cancelledSend = (await invoke('chat:send', {
sessionId: cancelSession.id,
text: 'persist-cancel'
})) as { runId: string }
for (let attempt = 0; attempt < 100 && !state.cancelOutputEmitted; attempt++) await wait(10)
assert.equal(state.cancelOutputEmitted, true, 'fake OMP should emit a running tool and partial output')
const streamingCheckpoint = await waitForDiskAssistant(state.userData, cancelSession.id, (message) =>
message.status === 'streaming' &&
message.content === 'cancel-before' &&
message.tools?.[0].status === 'running' &&
message.tools?.[0].output === 'cancelled tool output retained'
)
assert.deepEqual(streamingCheckpoint.parts, [
{ type: 'text', text: 'cancel-before' },
{ type: 'tool', toolId: 'persisted-tool' }
], 'the streaming checkpoint should persist text/tool order before cancellation')
const cancelResult = (await invoke('chat:cancel', cancelledSend.runId)) as { cancelled: boolean }
assert.equal(cancelResult.cancelled, true)
const cancelledMessages = await waitForMessages(
invoke,
cancelSession.id,
(messages) => assistant(messages).status === 'cancelled'
)
const cancelledAssistant = assistant(cancelledMessages)
assert.deepEqual(cancelledAssistant.parts, [
{ type: 'text', text: 'cancel-before' },
{ type: 'tool', toolId: 'persisted-tool' }
])
assert.equal(cancelledAssistant.status, 'cancelled')
assert.equal(cancelledAssistant.tools?.[0].status, 'cancelled')
assert.equal(cancelledAssistant.tools?.[0].output, 'cancelled tool output retained')
const diskCancelledAssistant = assistant(await readDiskMessages(state.userData, cancelSession.id))
assert.equal(diskCancelledAssistant.status, 'cancelled', 'disk JSON should persist the assistant cancellation')
assert.equal(diskCancelledAssistant.tools?.[0].status, 'cancelled', 'disk JSON should persist terminal tool status')
assert.equal(diskCancelledAssistant.tools?.[0].output, 'cancelled tool output retained',
'disk JSON should preserve output from a running tool cancelled mid-turn')
assert.equal(state.promptCalls, 2)
assert.equal(state.abortCalls, 1)
console.log('chat-persistence-regression: all checks passed')
} finally {
globals[TEST_STATE_KEY] = previousState
await rm(tempRoot, { recursive: true, force: true })
}
}
void main().catch((error: unknown) => {
console.error('chat-persistence-regression failed:', error)
process.exitCode = 1
})
+365
View File
@@ -0,0 +1,365 @@
import assert from 'node:assert/strict'
import { useChatStore } from '../src/renderer/src/stores/chat'
import type { SendResult } from '../src/shared/ipc'
import type { ChatEvent, ChatMessage, SessionMeta, ToolCallRecord } from '../src/shared/types'
const session: SessionMeta = { id: 'session-1', title: 'Regression', createdAt: 1, updatedAt: 1 }
function tool(overrides: Partial<ToolCallRecord> = {}): ToolCallRecord {
return {
id: 'tool-1',
name: 'read_file',
args: { path: 'notes.txt' },
argsRaw: '{"path":"notes.txt"}',
status: 'running',
...overrides
}
}
function sendResult(runId: string): SendResult {
return {
runId,
assistantMessageId: `${runId}-assistant`,
modelId: 'model-profile-1',
modelName: 'Frozen Model',
userMessage: {
id: `${runId}-user`,
role: 'user',
content: 'question',
createdAt: 10
}
}
}
function resetStore(): void {
useChatStore.setState({
sessions: [session],
currentSessionId: session.id,
messages: [],
sessionMessages: {},
runs: {},
sendingSessions: [],
running: null,
sending: false,
loading: false,
error: null
})
}
function installFakeWindow(send: (payload: { sessionId: string; text: string }) => Promise<SendResult>): void {
const api = {
session: {
list: async () => [session],
create: async () => session,
messages: async () => [],
rename: async () => undefined,
remove: async () => undefined
},
chat: {
send,
cancel: async () => ({ cancelled: true }),
onEvent: () => () => undefined
}
}
Object.defineProperty(globalThis, 'window', {
configurable: true,
value: { ycode: api }
})
}
function assistantMessage(): ChatMessage {
const message = useChatStore.getState().messages.find((entry) => entry.role === 'assistant')
assert.ok(message, 'send result should create an assistant placeholder')
return message
}
function deferred<T>(): { promise: Promise<T>; resolve(value: T): void } {
let resolve!: (value: T) => void
const promise = new Promise<T>((done) => {
resolve = done
})
return { promise, resolve }
}
async function testEarlyOrderedEventsAndDuplicateGuard(): Promise<void> {
resetStore()
const result = sendResult('run-order')
const response = deferred<SendResult>()
let sendCalls = 0
const events: ChatEvent[] = [
{ runId: 'other-run', type: 'text', delta: 'must be ignored' },
{ runId: result.runId, type: 'text', delta: 'before' },
{ runId: result.runId, type: 'tool_call', call: tool() },
{ runId: result.runId, type: 'text', delta: 'after' },
{
runId: result.runId,
type: 'tool_result',
call: tool({ status: 'ok', output: 'file contents', summary: 'file contents', durationMs: 12 })
},
{
runId: result.runId,
type: 'done',
content: 'beforeafter',
tools: [tool({ status: 'ok', output: 'file contents', summary: 'file contents', durationMs: 12 })]
}
]
installFakeWindow(async () => {
sendCalls++
for (const event of events) useChatStore.getState().applyEvent(event)
return response.promise
})
const firstSend = useChatStore.getState().send('question')
assert.equal(useChatStore.getState().sending, true, 'sending should cover the pending IPC request')
assert.equal(useChatStore.getState().messages.length, 0, 'early events wait for the send result placeholder')
await useChatStore.getState().send('duplicate')
assert.equal(sendCalls, 1, 'a second send must be ignored while the first send is pending')
response.resolve(result)
await firstSend
const message = assistantMessage()
assert.equal(message.turnStartedAt, undefined, 'terminal events clear the live timer timestamp')
assert.equal(message.content, 'beforeafter')
assert.deepEqual(message.parts, [
{ type: 'text', text: 'before' },
{ type: 'tool', toolId: 'tool-1' },
{ type: 'text', text: 'after' }
])
assert.equal(message.status, 'done')
assert.equal(message.tools?.length, 1)
assert.equal(message.tools?.[0].output, 'file contents')
assert.equal(useChatStore.getState().running, null, 'done should clear the running turn')
assert.equal(useChatStore.getState().sending, false, 'the send request should leave the sending state')
assert.equal(message.content.includes('must be ignored'), false, 'events from another run must be ignored')
useChatStore.getState().applyEvent({ runId: result.runId, type: 'text', delta: 'late' })
assert.equal(assistantMessage().content, 'beforeafter', 'events after a terminal event must not change the message')
}
async function testLiveTurnStartTimestamp(): Promise<void> {
resetStore()
const result = sendResult('run-live-timer')
installFakeWindow(async () => result)
await useChatStore.getState().send('question')
const message = assistantMessage()
assert.equal(message.status, 'streaming')
assert.equal(message.modelId, 'model-profile-1', 'assistant placeholder keeps the model identity from the send result')
assert.equal(message.modelName, 'Frozen Model', 'assistant placeholder can render its model label before reloading history')
assert.equal(message.turnStartedAt, result.userMessage.createdAt,
'the live timer starts from the same user-submit timestamp used by terminal duration')
useChatStore.getState().applyEvent({ runId: result.runId, type: 'text', delta: 'answer' })
assert.equal(assistantMessage().turnStartedAt, result.userMessage.createdAt,
'ordinary stream updates keep the timer origin stable')
useChatStore.getState().applyEvent({ runId: result.runId, type: 'done', content: 'answer', tools: [], turnDurationMs: 1200 })
assert.equal(assistantMessage().turnDurationMs, 1200)
assert.equal(assistantMessage().turnStartedAt, undefined)
}
async function testEarlyTerminalEvents(): Promise<void> {
const cases: Array<{
name: string
terminal: ChatEvent
messageStatus: 'cancelled' | 'error'
toolStatus: 'cancelled' | 'error'
}> = [
{
name: 'cancelled',
terminal: { runId: 'run-cancel', type: 'cancelled' },
messageStatus: 'cancelled',
toolStatus: 'cancelled'
},
{
name: 'error',
terminal: { runId: 'run-error', type: 'error', message: 'provider failed' },
messageStatus: 'error',
toolStatus: 'error'
}
]
for (const scenario of cases) {
resetStore()
const result = sendResult(scenario.terminal.runId)
const response = deferred<SendResult>()
installFakeWindow(async () => {
useChatStore.getState().applyEvent({ runId: result.runId, type: 'text', delta: 'prefix' })
useChatStore.getState().applyEvent({ runId: result.runId, type: 'tool_call', call: tool({ output: 'partial output' }) })
useChatStore.getState().applyEvent({ runId: result.runId, type: 'text', delta: 'suffix' })
useChatStore.getState().applyEvent(scenario.terminal)
return response.promise
})
const sending = useChatStore.getState().send('question')
response.resolve(result)
await sending
const message = assistantMessage()
assert.equal(message.content, 'prefixsuffix', `${scenario.name}: text received before the result must be retained`)
assert.deepEqual(message.parts, [
{ type: 'text', text: 'prefix' },
{ type: 'tool', toolId: 'tool-1' },
{ type: 'text', text: 'suffix' }
], `${scenario.name}: ordered content should survive a terminal event`)
assert.equal(message.status, scenario.messageStatus)
assert.equal(message.tools?.[0].status, scenario.toolStatus)
assert.equal(message.tools?.[0].output, 'partial output', `${scenario.name}: terminal state must preserve tool output`)
assert.equal(useChatStore.getState().running, null)
assert.equal(useChatStore.getState().sending, false)
if (scenario.name === 'error') assert.equal(useChatStore.getState().error, 'provider failed')
}
}
async function testFailedSendCanRetry(): Promise<void> {
resetStore()
let sendCalls = 0
const retryResult = sendResult('run-retry')
installFakeWindow(async () => {
sendCalls++
if (sendCalls === 1) {
useChatStore.getState().applyEvent({ runId: 'run-failed', type: 'text', delta: 'discarded failed run' })
throw new Error('send failed')
}
useChatStore.getState().applyEvent({
runId: retryResult.runId,
type: 'done',
content: 'retry complete',
tools: []
})
return retryResult
})
await assert.rejects(useChatStore.getState().send('question'), /send failed/)
assert.equal(useChatStore.getState().sending, false, 'a rejected send must reset sending')
assert.equal(useChatStore.getState().error, 'send failed')
assert.equal(useChatStore.getState().messages.length, 0, 'a failed send must not install a placeholder')
await useChatStore.getState().send('retry')
assert.equal(sendCalls, 2, 'a new send should be accepted after a failed request')
const message = assistantMessage()
assert.equal(message.content, 'retry complete', 'failed-run buffered events must not leak into a retry')
assert.equal(message.status, 'done')
assert.equal(useChatStore.getState().sending, false)
assert.equal(useChatStore.getState().running, null)
}
async function testCancelFlushesPendingAndIgnoresLateEvents(): Promise<void> {
resetStore()
const result = sendResult('run-user-cancel')
installFakeWindow(async () => result)
let cancelCalls = 0
window.ycode.chat.cancel = async (runId) => {
cancelCalls++
assert.equal(runId, result.runId)
return { cancelled: true }
}
await useChatStore.getState().send('question')
useChatStore.getState().applyEvent({ runId: result.runId, type: 'text', delta: 'already rendered' })
useChatStore.getState().applyEvent({ runId: result.runId, type: 'text', delta: ' pending text' })
await useChatStore.getState().cancel()
assert.equal(cancelCalls, 1)
assert.equal(assistantMessage().content, 'already rendered pending text', 'cancel must flush pending text before requesting cancellation')
useChatStore.getState().applyEvent({ runId: result.runId, type: 'text', delta: 'late before terminal' })
assert.equal(assistantMessage().content, 'already rendered pending text', 'text after a cancel request must be ignored')
useChatStore.getState().applyEvent({ runId: result.runId, type: 'cancelled' })
useChatStore.getState().applyEvent({ runId: result.runId, type: 'text', delta: 'late after terminal' })
assert.equal(assistantMessage().content, 'already rendered pending text')
assert.equal(assistantMessage().status, 'cancelled')
}
async function testSessionSwitchFlushesPendingIntoItsOwner(): Promise<void> {
resetStore()
const result = sendResult('run-session-switch')
installFakeWindow(async () => result)
await useChatStore.getState().send('question')
useChatStore.getState().applyEvent({ runId: result.runId, type: 'text', delta: 'session one' })
useChatStore.getState().applyEvent({ runId: result.runId, type: 'text', delta: ' pending' })
await useChatStore.getState().selectSession('session-2')
assert.equal(useChatStore.getState().messages.length, 0, 'switching sessions must not show the outgoing assistant message')
assert.equal(useChatStore.getState().sessionMessages[session.id]?.find((message) => message.id === result.assistantMessageId)?.content,
'session one pending', 'switching must flush buffered text into the session that owns the run')
await useChatStore.getState().selectSession(session.id)
assert.equal(assistantMessage().content, 'session one pending', 'restoring the session must preserve the flushed stream order')
useChatStore.getState().applyEvent({ runId: result.runId, type: 'done', content: 'session one pending', tools: [] })
}
async function testParallelSessionsAndStaleLoads(): Promise<void> {
resetStore()
const a = deferred<SendResult>()
const b = deferred<SendResult>()
installFakeWindow(async (payload) => payload.sessionId === session.id ? a.promise : b.promise)
const first = useChatStore.getState().send('A')
await useChatStore.getState().selectSession('session-2')
assert.equal(useChatStore.getState().sending, false)
const second = useChatStore.getState().send('B')
useChatStore.getState().applyEvent({ sessionId: session.id, runId: 'a', type: 'text', delta: 'early A' })
useChatStore.getState().applyEvent({ sessionId: 'session-2', runId: 'b', type: 'text', delta: 'early B' })
b.resolve(sendResult('b'))
await second
a.resolve(sendResult('a'))
await first
assert.equal(assistantMessage().content, 'early B', 'a background send cannot replace the foreground')
assert.equal(useChatStore.getState().running?.runId, 'b')
useChatStore.getState().applyEvent({ sessionId: session.id, runId: 'a', type: 'done', content: 'finished A', tools: [] })
assert.equal(useChatStore.getState().running?.runId, 'b', 'background terminal cannot stop foreground run')
await useChatStore.getState().selectSession(session.id)
assert.equal(assistantMessage().content, 'finished A', 'switch reads current in-memory data')
assert.equal(useChatStore.getState().running, null)
await useChatStore.getState().selectSession('session-2')
assert.equal(useChatStore.getState().running?.runId, 'b')
useChatStore.getState().applyEvent({ runId: 'b', type: 'cancelled' })
const slow = deferred<ChatMessage[]>()
window.ycode.session.messages = async (id) => id === 'slow' ? slow.promise : [{ id: 'fast-user', role: 'user', content: 'fast', createdAt: 1 }]
const oldSelection = useChatStore.getState().selectSession('slow')
await useChatStore.getState().selectSession('fast')
slow.resolve([{ id: 'old', role: 'user', content: 'slow', createdAt: 1 }])
await oldSelection
assert.equal(useChatStore.getState().currentSessionId, 'fast')
assert.equal(useChatStore.getState().messages[0].content, 'fast', 'stale session fetch cannot replace a newer selection')
}
async function main(): Promise<void> {
await testEarlyOrderedEventsAndDuplicateGuard()
await testLiveTurnStartTimestamp()
await testEarlyTerminalEvents()
await testFailedSendCanRetry()
await testCancelFlushesPendingAndIgnoresLateEvents()
await testSessionSwitchFlushesPendingIntoItsOwner()
await testParallelSessionsAndStaleLoads()
resetStore()
installFakeWindow(async () => sendResult('unused'))
const creation = deferred<SessionMeta>()
window.ycode.session.create = async () => creation.promise
const creating = useChatStore.getState().newSession()
await useChatStore.getState().selectSession('chosen-later')
creation.resolve({ ...session, id: 'slow-created' })
await creating
assert.equal(useChatStore.getState().currentSessionId, 'chosen-later', 'late session creation cannot override a later selection')
resetStore()
useChatStore.setState({ currentSessionId: null })
let sentTo = ''
installFakeWindow(async (payload) => { sentTo = payload.sessionId; return sendResult('owned-create') })
const ownedCreation = deferred<SessionMeta>()
window.ycode.session.create = async () => ownedCreation.promise
const ownedSend = useChatStore.getState().send('original draft')
await useChatStore.getState().selectSession('other-session')
ownedCreation.resolve({ ...session, id: 'owned-session' })
await ownedSend
assert.equal(sentTo, 'owned-session', 'pending creation keeps the send bound to its own session')
assert.equal(useChatStore.getState().currentSessionId, 'other-session')
resetStore()
installFakeWindow(async () => sendResult('unused'))
window.ycode.session.list = async () => { throw new Error('corrupt session store') }
await useChatStore.getState().init()
assert.equal(useChatStore.getState().error, 'corrupt session store', 'load failure is visible instead of silently creating an empty session')
console.log('chat-store-regression: all checks passed')
}
void main()
+124
View File
@@ -0,0 +1,124 @@
import assert from 'node:assert/strict'
import { ChatStreamCommitter, type ChatStreamKey, type ChatStreamScheduler } from '../src/renderer/src/stores/chat-stream-committer'
import { applyAssistantEvent } from '../src/shared/message-parts'
import type { ChatEvent, ChatMessage, ToolCallRecord } from '../src/shared/types'
class FakeScheduler implements ChatStreamScheduler {
private currentTime = 0
private nextId = 1
private readonly frames = new Map<number, () => void>()
private readonly timers = new Map<number, { at: number; callback: () => void }>()
now(): number { return this.currentTime }
requestFrame(callback: () => void): number {
const id = this.nextId++
this.frames.set(id, callback)
return id
}
cancelFrame(handle: unknown): void { this.frames.delete(handle as number) }
setTimeout(callback: () => void, delayMs: number): number {
const id = this.nextId++
this.timers.set(id, { at: this.currentTime + delayMs, callback })
return id
}
clearTimeout(handle: unknown): void { this.timers.delete(handle as number) }
runFrame(at: number): void {
this.currentTime = at
const callbacks = [...this.frames.values()]
this.frames.clear()
callbacks.forEach((callback) => callback())
}
advanceTo(at: number): void {
while (true) {
const next = [...this.timers.entries()].sort((a, b) => a[1].at - b[1].at)[0]
if (!next || next[1].at > at) break
this.currentTime = next[1].at
this.timers.delete(next[0])
next[1].callback()
}
this.currentTime = at
}
}
const key: ChatStreamKey = { sessionId: 'session-1', assistantId: 'assistant-1' }
const tool: ToolCallRecord = { id: 'tool-1', name: 'read_file', args: {}, argsRaw: '{}', status: 'running' }
function eventTypes(events: ChatEvent[]): string[] { return events.map((event) => event.type) }
function testSameFrameTextDeltasAreOneStoreCommit(): void {
const scheduler = new FakeScheduler()
const commits: ChatEvent[][] = []
const committer = new ChatStreamCommitter((_key, events) => commits.push(events), scheduler)
let message: ChatMessage = { id: key.assistantId, role: 'assistant', content: '', createdAt: 1, parts: [], status: 'streaming' }
const apply = (events: ChatEvent[]) => { message = events.reduce((current, event) => applyAssistantEvent(current, event), message) }
committer.enqueue(key, { runId: 'run-1', type: 'text', delta: 'start' })
assert.equal(commits.length, 1, 'the first text block must render immediately')
for (const delta of [' ', 'batched', ' ', 'text']) committer.enqueue(key, { runId: 'run-1', type: 'text', delta })
assert.equal(commits.length, 1, 'same-frame deltas must not submit the store repeatedly')
scheduler.runFrame(16)
scheduler.advanceTo(149)
assert.equal(commits.length, 1, 'the first batch must respect the 150ms minimum interval')
scheduler.advanceTo(150)
assert.equal(commits.length, 2, 'all same-frame deltas must share one store submission')
assert.deepEqual(commits[1].map((event) => event.type), ['text'])
apply(commits.flat())
assert.equal(message.content, 'start batched text', 'coalescing must preserve exact text concatenation')
}
function testStructuralEventFlushesTextInOrder(): void {
const scheduler = new FakeScheduler()
const commits: ChatEvent[][] = []
const committer = new ChatStreamCommitter((_key, events) => commits.push(events), scheduler)
committer.enqueue(key, { runId: 'run-2', type: 'text', delta: 'before ' })
committer.enqueue(key, { runId: 'run-2', type: 'text', delta: 'tool' })
committer.enqueue(key, { runId: 'run-2', type: 'tool_call', call: tool })
assert.equal(commits.length, 2, 'the tool event must force the queued text out immediately')
assert.deepEqual(eventTypes(commits[1]), ['text', 'tool_call'], 'the pending text must precede its structural event')
let message: ChatMessage = { id: key.assistantId, role: 'assistant', content: '', createdAt: 1, parts: [] }
message = commits.flat().reduce((current, event) => applyAssistantEvent(current, event), message)
assert.equal(message.content, 'before tool')
assert.deepEqual(message.parts, [{ type: 'text', text: 'before tool' }, { type: 'tool', toolId: 'tool-1' }])
}
function testNewReasoningBlockIsImmediateAndUpdatesAreThrottled(): void {
const scheduler = new FakeScheduler()
const commits: ChatEvent[][] = []
const committer = new ChatStreamCommitter((_key, events) => commits.push(events), scheduler)
const reasoning = (text: string): ChatEvent => ({ runId: 'run-3', type: 'part', part: { type: 'reasoning', id: 'reason-1', text } })
committer.enqueue(key, reasoning('first'))
assert.equal(commits.length, 1, 'a new reasoning block must appear immediately')
committer.enqueue(key, reasoning('second'))
scheduler.runFrame(16)
scheduler.advanceTo(149)
assert.equal(commits.length, 1)
scheduler.advanceTo(150)
assert.equal(commits.length, 2)
assert.deepEqual(commits[1], [reasoning('second')], 'repeated updates to the same reasoning block can be compacted')
}
function testTerminalFlushCancelsPendingSchedule(): void {
const scheduler = new FakeScheduler()
const commits: ChatEvent[][] = []
const committer = new ChatStreamCommitter((_key, events) => commits.push(events), scheduler)
committer.enqueue(key, { runId: 'run-4', type: 'text', delta: 'kept' })
committer.enqueue(key, { runId: 'run-4', type: 'text', delta: ' before cancel' })
committer.enqueue(key, { runId: 'run-4', type: 'cancelled' })
assert.equal(commits.length, 2, 'cancellation must flush queued text before applying its terminal state')
assert.deepEqual(eventTypes(commits[1]), ['text', 'cancelled'])
scheduler.runFrame(16)
scheduler.advanceTo(1000)
assert.equal(commits.length, 2, 'terminal cleanup must cancel the scheduled frame and timer')
}
testSameFrameTextDeltasAreOneStoreCommit()
testStructuralEventFlushesTextInOrder()
testNewReasoningBlockIsImmediateAndUpdatesAreThrottled()
testTerminalFlushCancelsPendingSchedule()
console.log('chat-stream-committer-regression: all checks passed')
+80
View File
@@ -0,0 +1,80 @@
/**
* 交互验收:点击指定选择器后截图。
* 用法:node scripts/click-shot.mjs <port> <selector> <outPng>
*/
import { writeFile } from 'node:fs/promises'
const port = Number(process.argv[2] ?? 9335)
const selector = process.argv[3] ?? 'button'
const outPng = process.argv[4] ?? 'shot.png'
async function main() {
const list = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json()
const page = list.filter((t) => t.type === 'page')[0]
if (!page) throw new Error('未找到页面')
const ws = new WebSocket(page.webSocketDebuggerUrl)
await new Promise((r, j) => {
const t = setTimeout(() => j(new Error('连接超时')), 8000)
ws.addEventListener('open', () => {
clearTimeout(t)
r()
})
ws.addEventListener('error', () => {
clearTimeout(t)
j(new Error('连接失败'))
})
})
const send = (method, params = {}) =>
new Promise((res, rej) => {
const id = Math.floor(Math.random() * 1e9)
const timer = setTimeout(() => rej(new Error(`超时 ${method}`)), 15000)
const h = (ev) => {
const m = JSON.parse(ev.data)
if (m.id === id) {
clearTimeout(timer)
ws.removeEventListener('message', h)
res(m.result)
}
}
ws.addEventListener('message', h)
ws.send(JSON.stringify({ id, method, params }))
})
// 用 title 属性选中目标(比 :nth-child 稳)
const clickExpr = `(() => {
const el = document.querySelector(${JSON.stringify(selector)});
if (!el) return 'NOT_FOUND';
el.click();
return 'CLICKED: ' + (el.getAttribute('title') || el.className || '');
})()`
const r = await send('Runtime.evaluate', { expression: clickExpr, returnByValue: true })
console.log('点击结果:', r.result.value)
await new Promise((res) => setTimeout(res, 700))
const probe = await send('Runtime.evaluate', {
expression: `JSON.stringify({
notif: !!document.querySelector('.notif-panel'),
notifItems: document.querySelectorAll('.notif-item').length,
settings: !!document.querySelector('.dialog--settings'),
settingsNav: document.querySelectorAll('.settings__nav-item').length
})`,
returnByValue: true
})
console.log('面板状态:', probe.result.value)
const shot = await send('Page.captureScreenshot', { format: 'png' })
if (shot?.data) {
await writeFile(outPng, Buffer.from(shot.data, 'base64'))
console.log('截图:', outPng)
}
ws.close()
}
main().catch((e) => {
console.error('失败:', e.message)
process.exit(1)
})
+124
View File
@@ -0,0 +1,124 @@
import assert from 'node:assert/strict'
import { isValidElement, type ReactElement, type ReactNode } from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { CopyButton, RecoveredCodeBlock, type RecoveredCodeBlockProps } from '../src/renderer/src/workbuddy/components/CodeBlock'
function hasClassToken(markup: string, token: string): boolean {
return [...markup.matchAll(/class="([^"]*)"/g)]
.some((match) => match[1].split(/\s+/).includes(token))
}
function assertHasClass(markup: string, token: string): void {
assert.ok(hasClassToken(markup, token), `expected class token ${token}`)
}
function elementHasClass(element: ReactElement, token: string): boolean {
const className = (element.props as { className?: string }).className ?? ''
return className.split(/\s+/).includes(token)
}
function findElement(node: ReactNode, predicate: (element: ReactElement) => boolean): ReactElement | null {
if (!isValidElement(node)) return null
if (predicate(node)) return node
const children = (node.props as { children?: ReactNode }).children
for (const child of Array.isArray(children) ? children : [children]) {
const match = findElement(child, predicate)
if (match) return match
}
return null
}
function testFileMetadataAndRenderedChildren(): void {
const rawCode = 'const answer = 42\n'
const renderedChild = (
<code className="language-typescript">
<span className="token keyword">const</span> answer = <span className="token number">42</span>
</code>
)
const opened: Array<[string, number, number]> = []
const props: RecoveredCodeBlockProps = {
code: rawCode,
language: 'typescript',
fileMeta: {
startLine: 12,
endLine: 18,
filePath: 'src/example.ts',
fileName: 'example.ts',
metaLanguage: 'typescript'
},
children: renderedChild,
isRendering: false,
onOpenFile: (path, start, end) => opened.push([path, start, end])
}
const tree = RecoveredCodeBlock(props)
const html = renderToStaticMarkup(tree)
assertHasClass(html, 'cr-code-like-box')
assertHasClass(html, 'cr-code-like-box__header')
assertHasClass(html, 'cr-code-like-box__content')
assertHasClass(html, 'cr-code-block__file-title')
assertHasClass(html, 'cr-code-block__file-title--clickable')
assertHasClass(html, 'cr-code-block__file-name')
assertHasClass(html, 'cr-code-block__line-range')
assert.match(html, /title="12:18:src\/example\.ts"/)
assert.match(html, /example\.ts/)
assert.match(html, /L12-L18/)
assertHasClass(html, 'cr-code-block__pre')
assert.match(html, /<pre\b[^>]*><code class="language-typescript">/)
assertHasClass(html, 'token')
assert.match(html, /class="token keyword"/)
assert.doesNotMatch(html, /const answer = 42/)
const copy = findElement(tree, (element) => element.type === CopyButton)
assert.ok(copy, 'code block should render the shared copy button')
const copyProps = copy.props as { text: string; label?: string }
assert.equal(copyProps.text, rawCode, 'copy should receive raw source text, not rendered children')
assert.equal(copyProps.label, '复制')
const fileButton = findElement(tree, (element) =>
element.type === 'button' &&
elementHasClass(element, 'cr-code-block__file-title--clickable')
)
assert.ok(fileButton)
;(fileButton.props as { onClick(): void }).onClick()
assert.deepEqual(opened, [['src/example.ts', 12, 18]])
}
function testLanguageFallbackAndNonClickableFile(): void {
const fallbackHtml = renderToStaticMarkup(
<RecoveredCodeBlock code="plain source" language="" />
)
assertHasClass(fallbackHtml, 'cr-code-block__language')
assert.match(fallbackHtml, />plaintext<\/span>/)
assertHasClass(fallbackHtml, 'cr-code-block__pre')
assert.match(fallbackHtml, /<pre\b[^>]*>plain source<\/pre>/)
const metadata: NonNullable<RecoveredCodeBlockProps['fileMeta']> = {
startLine: 3,
endLine: 4,
filePath: 'README.md',
fileName: 'README.md'
}
const tree = RecoveredCodeBlock({ code: 'source', language: 'markdown', fileMeta: metadata })
const html = renderToStaticMarkup(tree)
assertHasClass(html, 'cr-code-block__file-title')
assert.match(html, /title="3:4:README\.md"/)
assert.equal(hasClassToken(html, 'cr-code-block__file-title--clickable'), false)
assert.equal(findElement(tree, (element) => element.type === 'button' &&
elementHasClass(element, 'cr-code-block__file-title')), null,
'file metadata without an open callback should not be a fake clickable button')
}
function testCopyButtonMarkup(): void {
const html = renderToStaticMarkup(<CopyButton text="tool output" label="复制工具输出" className="tool-copy" />)
assertHasClass(html, 'cr-code-block__copy-tooltip')
assertHasClass(html, 'cr-code-block__copy-button')
assertHasClass(html, 'tool-copy')
assert.match(html, /aria-label="复制工具输出"/)
assert.match(html, /title="复制工具输出"/)
}
testFileMetadataAndRenderedChildren()
testLanguageFallbackAndNonClickableFile()
testCopyButtonMarkup()
console.log('code-block-regression: all checks passed')
+54
View File
@@ -0,0 +1,54 @@
"""Compare fixed REA captures with the one calibrated YCode header fixture mode."""
from pathlib import Path
from hashlib import sha256
import json
from PIL import Image, ImageChops
ROOT = Path(__file__).resolve().parents[1]
OUTPUT = ROOT / '.tmp/chat-header-native-swiftshader-20261016'
report = json.loads((OUTPUT / 'report.json').read_text(encoding='utf-8'))
rendering = report['rendering']
assert rendering['mode'] == 'visible-swiftshader' and rendering['visible']
assert rendering['gpu']['auxAttributes']['displayType'] == 'ANGLE_SWIFTSHADER'
assert rendering['gpu']['auxAttributes']['glImplementationParts'] == '(gl=egl-angle,angle=swiftshader)'
assert rendering['gpuFeatures']['gpu_compositing'] == 'disabled_software'
regions = [
('title', 'rea-chat-header-final-20261016/workbuddy.png', 'collapsed.png', (308, 50, 388, 74), []),
('search-button', 'rea-chat-header-final-20261016/workbuddy.png', 'collapsed.png', (1215, 46, 1248, 78), []),
('share-button', 'rea-chat-header-final-20261016/workbuddy.png', 'collapsed.png', (1251, 46, 1284, 78), []),
('panel-button', 'rea-chat-header-final-20261016/workbuddy.png', 'collapsed.png', (1287, 46, 1320, 78), []),
('header', 'rea-chat-header-final-20261016/workbuddy.png', 'collapsed.png', (285, 30, 1336, 94), []),
# Empty query: no dynamic counter. Exclude only the 3px caret column at the input start.
('search', 'rea-chat-header-title-20261016/workbuddy.png', 'search.png', (947, 46, 1248, 78), [(977, 50, 980, 74)]),
('edit-selected-title', 'rea-chat-header-title-20261016/frame-001.png', 'edit.png', (308, 50, 512, 74), []),
]
results = []
for name, reference, candidate, box, masks in regions:
source_path, own_path = ROOT / '.tmp' / reference, OUTPUT / candidate
a, b = Image.open(source_path).convert('RGB'), Image.open(own_path).convert('RGB')
assert a.size == b.size == (1336, 901)
diff = ImageChops.difference(a.crop(box), b.crop(box))
compared, changed, total, maximum = 0, 0, 0, 0
differences = []
for y in range(box[1], box[3]):
for x in range(box[0], box[2]):
if any(l <= x < r and t <= y < bt for l, t, r, bt in masks):
continue
delta = diff.getpixel((x - box[0], y - box[1]))
compared += 1
if any(delta):
changed += 1
differences.append({'x': x, 'y': y, 'source': a.getpixel((x, y)), 'candidate': b.getpixel((x, y))})
total += sum(delta)
maximum = max(maximum, *delta)
diff.save(OUTPUT / (name + '-diff.png'))
results.append({'name': name, 'source': reference, 'candidate': candidate, 'box': box, 'masks': masks,
'sourceSha256': sha256(source_path.read_bytes()).hexdigest(), 'candidateSha256': sha256(own_path.read_bytes()).hexdigest(),
'comparedPixels': compared, 'differentPixels': changed, 'maxChannelDifference': maximum,
'meanAbsoluteChannelDifference': total / (compared * 3), 'differentPixelSamples': differences[:60]})
summary = {'mode': rendering['mode'], 'displayType': rendering['gpu']['auxAttributes']['displayType'],
'gpuCompositing': rendering['gpuFeatures']['gpu_compositing'], 'channelTolerance': 0,
'regions': results, 'limitations': ['Fixed Windows/DPR1/1336x901 component fixture only. Production integration, tooltips, prompt-list states, compact/Mac/dark are outside these pixel results.',
'Remaining different pixels are preserved and reported; no tolerance or reference-pixel patch was applied.']}
(OUTPUT / 'pixels.json').write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding='utf-8')
print(json.dumps([{'name': r['name'], 'pixels': r['comparedPixels'], 'different': r['differentPixels'], 'max': r['maxChannelDifference']} for r in results]))
+33
View File
@@ -0,0 +1,33 @@
"""Compare exact same-state native crops. Original PNGs are exclusively saved REA frames."""
import json
from pathlib import Path
from PIL import Image, ImageChops
root = Path(__file__).resolve().parents[1]
output = root / '.tmp/source-select-native-20261016'
results = []
for query, own in [('english', 'provider-custom-query'), ('empty', 'provider-empty-query')]:
source = root / f'.tmp/rea-select-search-{query}-20261016'
layout = json.loads((source / 'layout.json').read_text(encoding='utf-8'))
frame = layout['probe']['frames'][0]
assert frame['step_id'] == {'english': 'query-custom-result', 'empty': 'query-empty-result'}[query]
region = next(region for region in frame['regions'] if region['selector'] == '.wb-select-popover')
x, y, width, height = region['bounds']
assert [x, y, width, height] == [154, 176, 592, 120 if query == 'english' else 96]
box = tuple(round(value) for value in (x, y, x + width, y + height))
original = Image.open(source / 'frame-000.png').convert('RGB').crop(box)
restored = Image.open(output / (own + '.png')).convert('RGB').crop(box)
delta = ImageChops.difference(original, restored)
pixels = list(delta.get_flattened_data())
result = {'state': query, 'sourceEvidenceId': layout['evidenceId'], 'box': box,
'pixels': len(pixels), 'different': sum(any(pixel) for pixel in pixels),
'maxChannel': max(max(pixel) for pixel in pixels),
'meanChannel': sum(sum(pixel) for pixel in pixels) / (len(pixels) * 3),
'mask': None, 'tolerance': 0}
results.append(result)
delta.point(lambda value: min(255, value * 8)).save(output / (query + '-diff.png'))
original.save(output / (query + '-source-crop.png'))
restored.save(output / (query + '-own-crop.png'))
(output / 'pixels.json').write_text(json.dumps(results, indent=2), encoding='utf-8')
print(json.dumps(results, ensure_ascii=False))
assert all(result['different'] == 0 for result in results), 'Exact crop parity changed; inspect differences without increasing tolerance'
+345
View File
@@ -0,0 +1,345 @@
import assert from 'node:assert/strict'
import { renderToStaticMarkup } from 'react-dom/server'
import { CompletedFold } from '../src/renderer/src/workbuddy/conversation/CompletedFold'
import { computeSourceFoldSummary } from '../src/renderer/src/workbuddy/conversation/recovered-fold-summary/index.mjs'
import { sourceConversationLabel, type SourceConversationLabelKey } from '../src/renderer/src/workbuddy/conversation/source-labels'
import MessageBubble from '../src/renderer/src/components/MessageBubble'
import { formatStreamingTurnDuration } from '../src/renderer/src/workbuddy/conversation/StreamingTurnDuration'
import {
buildCompletedFoldModel,
classifyCompletedFoldPart,
formatCompletedFoldDuration,
sourceCompletedFoldLabelsZh,
type CompletedFoldPart
} from '../src/renderer/src/workbuddy/conversation/completed-fold'
const orderedParts: CompletedFoldPart[] = [
{ type: 'reasoning', id: 'reasoning-prefix', text: 'thinking' },
{ type: 'tool', toolId: 'tool-prefix' },
{ type: 'text', text: 'This is the longest body text anchor.' },
{ type: 'reasoning', id: 'reasoning-process', text: 'more thinking' },
{ type: 'tool', toolId: 'tool-process' },
{ type: 'text', text: 'tail' },
{ type: 'tool', toolId: 'tool-trailing' },
{ type: 'plan', plan: { id: 'plan-1', title: 'Plan', steps: [] } }
]
const model = buildCompletedFoldModel(orderedParts, { turnFinished: true, turnDurationMs: 65_000 })
assert.ok(model)
assert.deepEqual(model.anchors.map(unit => unit.index), [2, 5])
assert.deepEqual(model.completedUnits.map(unit => unit.index), [0, 1, 6])
assert.deepEqual(model.processGroups.map(group => group.units.map(unit => unit.index)), [[3, 4]])
assert.deepEqual(model.summaryGroups.map(group => ({
indices: group.units.map(unit => unit.index),
shouldFold: group.shouldFold,
adjacentText: group.adjacentText,
toolCallCount: group.toolCallCount
})), [
{ indices: [0, 1], shouldFold: true, adjacentText: 'This is the longest body text anchor.', toolCallCount: 1 },
{ indices: [3, 4], shouldFold: true, adjacentText: 'tail', toolCallCount: 1 }
])
assert.equal(model.title, '已完成 1m5s')
assert.deepEqual(model.segments.map(segment => segment.kind), ['completed', 'unit', 'process', 'unit', 'unit'])
const onlyText = [{ type: 'text', text: 'plain answer' }] satisfies CompletedFoldPart[]
const emptyDurationFold = buildCompletedFoldModel(onlyText, { turnFinished: true, turnDurationMs: 1 })
assert.ok(emptyDurationFold)
assert.equal(emptyDurationFold.completedUnits.length, 0)
assert.equal(emptyDurationFold.title, '已完成 <1s')
assert.equal(buildCompletedFoldModel(onlyText, { turnFinished: true }), null)
assert.equal(buildCompletedFoldModel(onlyText, { turnFinished: false, turnDurationMs: 5000 }), null)
assert.equal(buildCompletedFoldModel(onlyText, { turnFinished: true, turnDurationMs: 5000, suppressCompletedFold: true }), null)
const cancelled = buildCompletedFoldModel(onlyText, { turnFinished: true, turnDurationMs: 2500, turnCancelled: true })
assert.equal(cancelled?.title, '已取消 2s')
assert.equal(cancelled?.status, 'cancelled')
assert.equal(buildCompletedFoldModel(onlyText, { turnFinished: true, turnCancelled: true }), null)
assert.equal(formatCompletedFoldDuration(Number.NaN), '0s')
assert.equal(formatCompletedFoldDuration(60_000), '1m')
assert.equal(formatCompletedFoldDuration(3_661_000), '1h1m')
assert.equal(formatStreamingTurnDuration(0), '1s')
assert.equal(formatStreamingTurnDuration(999), '1s')
assert.equal(formatStreamingTurnDuration(60_000), '1m')
assert.equal(formatStreamingTurnDuration(3_661_000), '1h1m')
const sourceSummary = computeSourceFoldSummary([{ type: 'tool-call', tool: {
name: 'Read', status: 'executed', args: { file_path: 'C:/work/readme.md' }
} }], {
t: (key, params) => sourceConversationLabel(key as SourceConversationLabelKey, params, 'zh-CN')
})
assert.deepEqual(sourceSummary, { text: '读取 readme.md', status: 'success', isFallback: false })
const safeVisibleClassification = {
isBodyText: false, keepVisible: true, isCompletedAnchor: false, foldable: false,
isWidget: false, isShowWidget: false, preservePosition: false, hasRootBlock: true
}
assert.deepEqual(classifyCompletedFoldPart({ type: 'plan', plan: { id: 'p', title: 'Plan', steps: [] } }, 0), safeVisibleClassification)
assert.deepEqual(classifyCompletedFoldPart({ type: 'task', task: { id: 't', title: 'Task', status: 'pending' } }, 0), safeVisibleClassification)
const futurePart = { type: 'future-widget', label: 'keep outside fold' } as CompletedFoldPart
assert.equal(classifyCompletedFoldPart(futurePart, 0).keepVisible, true)
assert.deepEqual(classifyCompletedFoldPart({ type: 'tool', toolId: 'visible-tool' }, 0, () => ({ role: 'show-widget' })), {
isBodyText: false, keepVisible: true, isCompletedAnchor: false, foldable: false,
isWidget: true, isShowWidget: true, preservePosition: false, role: 'show-widget', hasRootBlock: true
})
assert.deepEqual(classifyCompletedFoldPart({ type: 'tool', toolId: 'inline-question' }, 0, () => ({ role: 'inline-question' })), {
isBodyText: false, keepVisible: false, isCompletedAnchor: false, foldable: false,
isWidget: false, isShowWidget: false, preservePosition: false, role: 'inline-question', hasRootBlock: true
})
const activePrefixSummary = buildCompletedFoldModel([
{ type: 'tool', toolId: 'prefix-tool' },
{ type: 'reasoning', id: 'prefix-thinking', text: 'thinking' },
{ type: 'text', text: 'answer' },
{ type: 'tool', toolId: 'trailing-tool' },
{ type: 'reasoning', id: 'trailing-thinking', text: 'thinking' }
], { turnFinished: false })
assert.ok(activePrefixSummary)
assert.deepEqual(activePrefixSummary.summaryGroups.map(group => [group.startIndex, group.endIndex, group.shouldFold]), [
[0, 1, true], [3, 4, true]
])
assert.equal(activePrefixSummary.summaryGroups[0].isRunning, true)
assert.equal(activePrefixSummary.segments.filter(segment => segment.kind === 'unit' && segment.unit.kind === 'summary').length, 2)
const initialInspectionMarkup = renderToStaticMarkup(<MessageBubble message={{
id: 'streaming-inspection', role: 'assistant', status: 'streaming', createdAt: 1,
parts: [
{ type: 'reasoning', id: 'inspection-reasoning', text: '**Listing project, reading handoff**' },
{ type: 'tool', toolId: 'inspect-root' },
{ type: 'tool', toolId: 'read-handoff' }
],
tools: [
{ id: 'inspect-root', name: 'list_dir', status: 'ok', args: { path: '.' } },
{ id: 'read-handoff', name: 'read_file', status: 'ok', args: { path: 'docs/接手状态-2026-10-15.md' } }
]
}} />)
const streamingInspectionMarkup = renderToStaticMarkup(<MessageBubble message={{
id: 'streaming-inspection', role: 'assistant', status: 'streaming', createdAt: 1, turnStartedAt: Date.now(),
parts: [
{ type: 'reasoning', id: 'inspection-reasoning', text: '**Listing project, reading handoff**' },
{ type: 'tool', toolId: 'inspect-root' },
{ type: 'tool', toolId: 'read-handoff' },
{ type: 'reasoning', id: 'inspection-next-step', text: '**Inspecting the source tree**' },
{ type: 'tool', toolId: 'inspect-src' }
],
tools: [
{ id: 'inspect-root', name: 'list_dir', status: 'ok', args: { path: '.' } },
{ id: 'read-handoff', name: 'read_file', status: 'ok', args: { path: 'docs/接手状态-2026-10-15.md' } },
{ id: 'inspect-src', name: 'list_dir', status: 'stream_executing', args: { path: 'src' } }
]
}} />)
assert.equal((initialInspectionMarkup.match(/cr-streaming-activity/g) ?? []).length, 1)
assert.match(initialInspectionMarkup, /读取 接手状态-2026-10-15/)
assert.equal((streamingInspectionMarkup.match(/cr-streaming-activity/g) ?? []).length, 1,
'new tool calls replace the current activity row instead of nesting under the first action')
assert.match(streamingInspectionMarkup, /正在查看文件列表 src/)
assert.match(streamingInspectionMarkup, /data-activity-tool-id="inspect-src"/)
assert.match(streamingInspectionMarkup, /已处理 1s/,
'the elapsed duration starts at one second and is visible while the assistant turn is streaming')
assert.match(streamingInspectionMarkup, /cr-collapse--process cr-collapse--collapsed cr-live-event-feed/,
'the current action is an expandable event feed before the turn finishes')
assert.match(streamingInspectionMarkup, /aria-label="展开事件消息列表"/)
assert.match(streamingInspectionMarkup, /aria-controls="live-events-streaming-inspection-content"/)
assert.match(streamingInspectionMarkup, /data-event-type="reasoning"[^]*data-event-type="tool"/,
'reasoning and tool calls appear as ordered event messages')
assert.doesNotMatch(streamingInspectionMarkup, /过程消息/)
assert.match(streamingInspectionMarkup, /data-tool-name="list_dir"[^]*data-tool-name="read_file"/,
'all supported live tool details are mounted inside the independently expandable process section')
assert.doesNotMatch(streamingInspectionMarkup, /cr-collapse--summary/,
'streaming actions have no expandable parent summary')
assert.match(streamingInspectionMarkup, /<strong>Listing project, reading h/,
'deep-thinking details remain available and render Markdown instead of exposing raw markers')
const completedInspectionMarkup = renderToStaticMarkup(<MessageBubble message={{
id: 'completed-inspection', role: 'assistant', status: 'done', createdAt: 1, content: 'Done.',
parts: [
{ type: 'reasoning', id: 'completed-inspection-reasoning', text: '**Listing project, reading handoff**' },
{ type: 'tool', toolId: 'completed-inspect-root' },
{ type: 'tool', toolId: 'completed-read-handoff' },
{ type: 'text', text: 'Done.' }
],
tools: [
{ id: 'completed-inspect-root', name: 'list_dir', status: 'ok', args: { path: '.' } },
{ id: 'completed-read-handoff', name: 'read_file', status: 'ok', args: { path: 'docs/接手状态-2026-10-15.md' } }
]
}} />)
assert.doesNotMatch(completedInspectionMarkup, /cr-collapse--summary/)
assert.match(completedInspectionMarkup, /data-tool-name="list_dir"/)
assert.match(completedInspectionMarkup, /data-tool-name="read_file"/)
const oneFoldablePart = buildCompletedFoldModel([
{ type: 'text', text: 'answer' },
{ type: 'reasoning', id: 'single-thinking', text: 'thinking' }
], { turnFinished: true })
assert.equal(oneFoldablePart?.summaryGroups.length, 0)
const inlineQuestionParts: CompletedFoldPart[] = [
{ type: 'tool', toolId: 'before-question' },
{ type: 'tool', toolId: 'question' },
{ type: 'tool', toolId: 'after-question' },
{ type: 'text', text: 'answer' }
]
const inlineQuestionBreak = buildCompletedFoldModel(inlineQuestionParts, {
turnFinished: true,
classifyToolCall: part => part.toolId === 'question' ? { role: 'inline-question' } : undefined
})
assert.equal(inlineQuestionBreak?.summaryGroups.length, 0)
const inlineQuestionWithoutBlock = buildCompletedFoldModel(inlineQuestionParts, {
turnFinished: true,
classifyToolCall: part => part.toolId === 'question' ? { role: 'inline-question', hasRootBlock: false } : undefined
})
assert.deepEqual(inlineQuestionWithoutBlock?.summaryGroups.map(group => group.units.map(unit => unit.index)), [[0, 2]])
const widgetReorderParts: CompletedFoldPart[] = [
{ type: 'tool', toolId: 'movable-before-body' },
{ type: 'text', text: 'This is the longest body anchor.' },
{ type: 'tool', toolId: 'preserved-before-guidance' },
{ type: 'tool', toolId: 'linked-show-widget' },
{ type: 'queue-guidance', id: 'queue-boundary' },
{ type: 'tool', toolId: 'movable-after-guidance' },
{ type: 'tool', toolId: 'process-work' },
{ type: 'text', text: 'tail' },
{ type: 'tool', toolId: 'trailing-work' }
]
const widgetReorder = buildCompletedFoldModel(widgetReorderParts, {
turnFinished: true,
classifyToolCall: part => {
if (part.toolId === 'movable-before-body' || part.toolId === 'movable-after-guidance') return { role: 'widget' }
if (part.toolId === 'preserved-before-guidance') return { role: 'widget', preservePosition: true }
if (part.toolId === 'linked-show-widget') return { role: 'show-widget' }
return undefined
}
})
assert.ok(widgetReorder)
const renderedIndices = widgetReorder.segments.flatMap(segment => {
if (segment.kind === 'completed') return [`completed:${segment.units.flatMap(unit => unit.kind === 'part' ? [unit.unit.index] : unit.group.units.map(member => member.index)).join(',')}`]
if (segment.kind === 'process') return [`process:${segment.group.units.map(unit => unit.index).join(',')}`]
return [segment.unit.kind === 'part' ? String(segment.unit.unit.index) : `summary:${segment.unit.group.startIndex}-${segment.unit.group.endIndex}`]
})
assert.deepEqual(renderedIndices, ['completed:8', '1', '0', '2', '3', '4', 'process:6', '7', '5'])
assert.deepEqual(widgetReorder.anchors.map(unit => unit.index), [1, 4, 7])
const presentationClassification = widgetReorder.segments.flatMap(segment => {
if (segment.kind === 'unit' && segment.unit.kind === 'part') return [segment.unit]
if (segment.kind === 'completed') return segment.units.filter(unit => unit.kind === 'part')
if (segment.kind === 'process') return segment.group.children.filter(unit => unit.kind === 'part')
return []
})
const linkedBody = presentationClassification.find(unit => unit.unit.index === 1)?.unit.classification?.presentationGroupId
const linkedWidget = presentationClassification.find(unit => unit.unit.index === 3)?.unit.classification?.presentationGroupId
assert.ok(linkedBody)
assert.equal(linkedBody, linkedWidget)
const markup = renderToStaticMarkup(
<CompletedFold
parts={orderedParts}
turnFinished
turnDurationMs={65_000}
foldId="message-1"
renderGroup={(group, children) => <section className="source-summary-group" data-summary-start={group.startIndex}>{children}</section>}
renderUnit={(part, index) => part.type === 'tool'
? <section className="cr-collapse cr-collapse--summary" data-unit={part.toolId}>{part.toolId}</section>
: <span data-unit={`${part.type}-${index}`}>{part.type === 'text' ? part.text : part.type}</span>}
/>
)
assert.match(markup, /<section class="cr-collapse cr-collapse--completed cr-collapse--collapsed">/)
assert.match(markup, /<header class="cr-collapse__header" role="button" tabindex="0" aria-expanded="false"/)
assert.match(markup, /已完成 1m5s/)
assert.match(markup, /cr-collapse__content-inner cr-collapse__content-inner--completed/)
assert.match(markup, /aria-hidden="true" inert=""/)
assert.equal(markup.split('class="source-summary-group"').length - 1, 2)
assert.match(markup, /data-summary-start="0"/)
assert.match(markup, /data-summary-start="3"/)
const completedStart = markup.indexOf('cr-collapse--completed')
const prefixSummary = markup.indexOf('cr-collapse--summary" data-unit="tool-prefix')
const trailingSummary = markup.indexOf('cr-collapse--summary" data-unit="tool-trailing')
const firstAnchor = markup.indexOf('This is the longest body text anchor.')
const processFold = markup.indexOf('cr-collapse--process')
const processSummary = markup.indexOf('cr-collapse--summary" data-unit="tool-process')
const lastAnchor = markup.indexOf('tail')
const visiblePlan = markup.indexOf('data-unit="plan-7"')
assert.ok(completedStart >= 0 && completedStart < prefixSummary && prefixSummary < trailingSummary && trailingSummary < firstAnchor)
assert.ok(firstAnchor < processFold && processFold < processSummary && processSummary < lastAnchor)
assert.ok(lastAnchor < visiblePlan)
assert.match(markup, /tool-prefix/)
assert.match(markup, /tool-trailing/)
assert.match(markup, /cr-collapse__content-inner cr-collapse__content-inner--completed[^]*cr-collapse--summary/)
const expandedOnCompletionMarkup = renderToStaticMarkup(
<CompletedFold
parts={orderedParts}
turnFinished
turnDurationMs={65_000}
expandFinishedEvents
foldId="message-expanded-live-process"
renderUnit={(part, index) => <span data-unit={part.type === 'tool' ? part.toolId : `${part.type}-${index}`} />}
/>
)
assert.match(expandedOnCompletionMarkup, /aria-expanded="true"/,
'a live process section the user opened stays expanded when its turn completes')
const noFoldMarkup = renderToStaticMarkup(
<CompletedFold parts={onlyText} turnFinished renderUnit={part => <span>{part.type === 'text' ? part.text : part.type}</span>} />
)
assert.doesNotMatch(noFoldMarkup, /cr-collapse/)
assert.match(noFoldMarkup, /plain answer/)
const processWithoutDuration = buildCompletedFoldModel([
{ type: 'text', text: 'longest body anchor' },
{ type: 'tool', toolId: 'between-anchors' },
{ type: 'text', text: 'last' }
], { turnFinished: true })
assert.ok(processWithoutDuration)
assert.equal(processWithoutDuration.segments.some(segment => segment.kind === 'completed'), false)
assert.equal(processWithoutDuration.processGroups.length, 1)
assert.equal(sourceCompletedFoldLabelsZh.cancelled, '已取消')
const widgetBoundaryMarkup = renderToStaticMarkup(<MessageBubble message={{
id: 'widget-boundary', role: 'assistant', content: 'Answer', status: 'done', createdAt: 1,
parts: [
{ type: 'tool', toolId: 'before' }, { type: 'tool', toolId: 'widget' },
{ type: 'tool', toolId: 'after' }, { type: 'text', text: 'Answer' }
],
tools: [
{ id: 'before', name: 'before_widget', status: 'ok' },
{ id: 'widget', name: 'show_widget', status: 'ok' },
{ id: 'after', name: 'after_widget', status: 'ok' }
]
}} />)
for (const name of ['before_widget', 'show_widget', 'after_widget']) {
assert.equal(widgetBoundaryMarkup.split(`data-tool-name="${name}"`).length - 1, 1)
}
assert.doesNotMatch(widgetBoundaryMarkup, /3 个工具调用/)
const streamingToolIntentMarkup = renderToStaticMarkup(<MessageBubble message={{
id: 'streaming-tool-intent', role: 'assistant', status: 'streaming', createdAt: 1,
parts: [{ type: 'tool', toolId: 'list-src' }],
tools: [{ id: 'list-src', name: 'list_dir', status: 'running', intent: 'Inspecting source layout', args: { path: 'src' }, argsRaw: '{"path":"src"}' }]
}} />)
assert.match(streamingToolIntentMarkup, /正在查看文件列表 src/,
'the one live activity row uses the localized summary for the current tool call')
const liveIntentStatusRow = streamingToolIntentMarkup.match(/<div class="cr-captured-tool cr-streaming-activity"[^]*?<\/div><\/div>/)?.[0] ?? ''
assert.ok(liveIntentStatusRow)
assert.doesNotMatch(liveIntentStatusRow, /Inspecting source layout/,
'provider English intent stays out of the operation status line')
for (const status of ['cancelled', 'error'] as const) {
const terminalMarkup = renderToStaticMarkup(<MessageBubble message={{
id: `terminal-${status}`, role: 'assistant', status, createdAt: 1, turnDurationMs: 65_000,
error: status === 'error' ? '429 rate limit' : undefined, content: 'Answer',
parts: [{ type: 'tool', toolId: 'finished-tool' }, { type: 'text', text: 'Answer' }],
tools: [{ id: 'finished-tool', name: 'Read', status: 'ok', args: {}, argsRaw: '{}' }]
}} />)
assert.match(terminalMarkup, /cr-collapse--completed cr-collapse--collapsed/)
assert.match(terminalMarkup, status === 'cancelled' ? /已取消 1m5s/ : /已完成 1m5s/)
if (status === 'cancelled') {
assert.match(terminalMarkup, /用户已取消/)
assert.doesNotMatch(terminalMarkup, /msg__status--error/)
} else assert.match(terminalMarkup, /429 rate limit/)
}
const historicalQuestion = renderToStaticMarkup(<MessageBubble message={{
id: 'historical-question', role: 'assistant', status: 'done', createdAt: 1, content: '',
parts: [{ type: 'tool', toolId: 'question' }], tools: [{ id: 'question', name: 'AskUserQuestion',
status: 'ok', args: { questions: [{ question: 'Choose?' }] }, argsRaw: '{}',
result: { content: ' · Choose? → Desktop' }, metadata: { importedHistory: true } }]
}} />)
assert.match(historicalQuestion, /ask-user-question--completed/)
assert.match(historicalQuestion, /向用户提问/)
assert.match(historicalQuestion, /aria-expanded="false"/)
assert.doesNotMatch(historicalQuestion, /data-tool-name="AskUserQuestion"/)
console.log('Completed-fold model and SSR regression checks passed')
+152
View File
@@ -0,0 +1,152 @@
import assert from 'node:assert/strict'
import { renderToStaticMarkup } from 'react-dom/server'
import { ComposerControls } from '../src/renderer/src/workbuddy/composer/ComposerControls'
import { encodePcmWav, downsamplePcm, VOICE_MAX_DURATION_MS } from '../src/renderer/src/workbuddy/composer/VoiceInputControl'
import { buildComposerSendPayload, MAX_ATTACHMENT_BYTES, oversizedAttachmentNames, parseResourcePath, readReferenceUris } from '../src/renderer/src/workbuddy/composer/ComposerControls'
import { MessageActionToolbar } from '../src/renderer/src/workbuddy/message-actions/MessageActionToolbar'
import { buildCopyMessagePayload, buildUserMessageClipboard, getSortedRequestMessages, projectSafeMessage, safeStringify, USER_CONTENT_BLOCKS_MIME } from '../src/renderer/src/workbuddy/message-actions/project-safe-message'
import type { ChatMessage } from '../src/shared/types'
const protectedMessage = {
id: 'assistant-sensitive',
messageType: 'assistant',
content: [
{ type: 'text', text: 'before secret after', _meta: { sensitiveProtection: { version: 2, items: [{ start: 7, end: 13 }] } } },
{ type: 'tool-call', tool: { id: 'secret-tool', name: 'readFile', status: 'ok', args: { path: '/private' }, metaData: { sensitiveProtection: { version: 1, items: [{ ciphertext: 'ciphertext-1' }] } }, result: { private: true } } }
]
}
const originalSerialized = JSON.stringify(protectedMessage)
const projected = projectSafeMessage(protectedMessage)
assert.equal(JSON.stringify(protectedMessage), originalSerialized, 'projection must preserve the caller object')
const projectedText = (projected.content?.[0] as { text: string }).text
assert.equal(projectedText.startsWith('before ') && projectedText.endsWith(' after'), true)
assert.equal(projectedText.includes('secret'), false, 'copy projection must redact protected text')
const unchangedUser = { messageType: 'user', content: [{ type: 'text', text: 'user secret', _meta: { sensitiveProtection: { version: 2, items: [{ start: 5, end: 11 }] } } }] }
assert.equal(projectSafeMessage(unchangedUser), unchangedUser, 'assistant output hooks leave user messages unchanged')
assert.equal((projected.content?.[1] as { tool: { args: { sensitiveContent: string } } }).tool.args.sensitiveContent, 'ciphertext-1')
const quickQa = projectSafeMessage({ messageType: 'assistant', extra: { quickQa: true }, content: [
{ type: 'text', text: 'answer' },
{ type: 'tool-call', tool: { id: 'hidden', name: 'search', status: 'ok' } }
] })
assert.equal(quickQa.content?.length, 1, 'Quick QA copy projection omits tool-call blocks')
const legacy: ChatMessage = {
id: 'legacy-assistant',
role: 'assistant',
content: 'legacy text',
tools: [{ id: 'legacy-tool', name: 'readFile', args: { path: '/tmp/a' }, argsRaw: '{}', output: 'full output', status: 'ok' }],
createdAt: 1
}
const userMessage: ChatMessage = { id: 'user-copy', role: 'user', content: 'plain user text', createdAt: 1 }
const userClipboard = buildUserMessageClipboard({ ...userMessage, content: '<plain>&\u200B' })
assert.equal(userClipboard.plainText, '<plain>&', 'user text/plain omits the source zero-width marker')
assert.equal(userClipboard.html, '&lt;plain&gt;&amp;\u200B')
assert.equal(userClipboard.customMimeType, USER_CONTENT_BLOCKS_MIME)
assert.deepEqual(JSON.parse(userClipboard.customData), { version: '1.0', blocks: [{ type: 'text', text: '<plain>&\u200B' }] })
const richClipboard = buildUserMessageClipboard({
...userMessage,
content: 'see <img>&\u200B',
attachments: [
{
id: 'image-1', name: 'preview.png', mimeType: 'image/png', size: 3,
previewUrl: 'data:image/png;base64,AQID', path: 'C:\\work\\preview.png'
},
{
id: 'file-1', name: 'notes.pdf', mimeType: 'application/pdf', size: 4,
path: 'C:\\work\\notes.pdf'
}
],
resources: ['C:\\work\\src.ts', 'C:\\work\\src.ts']
})
const richPayload = JSON.parse(richClipboard.customData) as {
version: string
blocks: Array<{ type: string; text?: string; data?: string; mimeType?: string; name?: string; uri?: string; _meta?: Record<string, unknown> }>
}
assert.equal(richPayload.version, '1.0')
assert.deepEqual(richPayload.blocks.map((block) => block.type), ['text', 'image', 'resource_link', 'resource_link', 'resource_link'],
'custom MIME should preserve the text, image, file, and both resource blocks')
assert.equal(richClipboard.customMimeType, 'application/x-cb-chat-contentblocks')
assert.equal(richClipboard.plainText, 'Context: C:\\work\\src.ts\n\nsee <img>&preview.pngnotes.pdf',
'plain text should summarize context links once and append attachment phrases')
assert.ok(richClipboard.html.includes('see &lt;img&gt;&amp;\u200B'), 'HTML should escape text while retaining source zero-width markers')
assert.equal((richClipboard.html.match(/data-contentblock=/g) ?? []).length, 2,
'phrase attachment blocks should be carried in the HTML data-contentblock spans')
assert.ok(!richClipboard.html.includes('C:\\work\\src.ts'), 'context links are represented in plain text and custom MIME, not HTML')
assert.equal(richPayload.blocks[1].data, 'AQID')
assert.equal(richPayload.blocks[1].mimeType, 'image/png')
assert.equal(richPayload.blocks[1]._meta?.filename, 'preview.png')
assert.equal(richPayload.blocks[2].uri, 'C:\\work\\notes.pdf')
assert.equal(richPayload.blocks[2]._meta?.filePath, 'C:\\work\\notes.pdf')
assert.equal(richPayload.blocks[3]._meta?.displayAsContext, true)
const payload = buildCopyMessagePayload('request-1', [legacy])
assert.equal(payload.requestId, 'request-1')
assert.equal(payload.messageCount, 1)
assert.equal(payload.toolCallCount, 1)
assert.equal(payload.summary[0].contentBlockCount, 2, 'legacy content and tools both appear in fallback copy')
assert.equal((payload.messages[0].content?.[1] as { tool: { result: string } }).tool.result, 'full output')
assert.equal(JSON.parse(safeStringify(payload)).summary[0].toolCalls?.[0].id, 'legacy-tool')
const circular: { self?: unknown } = {}
circular.self = circular
assert.equal(JSON.parse(safeStringify(circular)).self, '[Circular]')
const requestPayload = buildCopyMessagePayload('request-2', [
{ id: 'user', role: 'user', content: 'question', createdAt: 1 } as ChatMessage,
{ id: 'a1', messageType: 'assistant', content: [{ type: 'text', text: 'one' }] },
{ id: 'a2', messageType: 'assistant', content: [{ type: 'text', text: 'two' }] }
])
assert.equal(requestPayload.messageCount, 2, 'request copy envelope includes assistant messages only')
const requestOrder = getSortedRequestMessages([
{ id: 'late', role: 'assistant', requestId: 'ordered', content: 'later', createdAt: 20 },
{ id: 'other', role: 'assistant', requestId: 'different', content: 'other', createdAt: 5 },
{ id: 'early', role: 'assistant', requestId: 'ordered', content: 'earlier', createdAt: 10 }
], 'ordered')
assert.deepEqual(requestOrder.map((item) => item.id), ['early', 'late'])
const samples = Float32Array.from([-1, -0.5, 0, 0.5, 1, 0])
assert.equal(downsamplePcm(samples, 32_000).length, 3)
const wav = encodePcmWav(samples)
assert.equal(new TextDecoder().decode(wav.slice(0, 4)), 'RIFF')
assert.equal(new DataView(wav.buffer).getUint32(24, true), 16_000)
assert.equal(new DataView(wav.buffer).getUint32(40, true), samples.length * 2)
assert.equal(VOICE_MAX_DURATION_MS, 295_000)
assert.deepEqual(oversizedAttachmentNames([
{ name: 'limit.bin', size: MAX_ATTACHMENT_BYTES },
{ name: 'over.bin', size: MAX_ATTACHMENT_BYTES + 1 }
]), ['over.bin'])
assert.deepEqual(buildComposerSendPayload(' prompt ', [], ['C:/work/file.ts'], 'full-access'), {
text: 'prompt', attachments: [], resources: ['C:/work/file.ts'], mode: 'full-access'
})
assert.equal(parseResourcePath('file:///C:/work/file.ts'), 'C:\\work\\file.ts')
assert.equal(parseResourcePath('C:\\work\\file.ts'), 'C:\\work\\file.ts')
assert.deepEqual(readReferenceUris({ getData: (type) => type === 'application/vnd.code.uri-list' ? 'file:///C:/a.ts\r\nfile:///C:/b.ts' : '' }), ['file:///C:/a.ts', 'file:///C:/b.ts'])
assert.deepEqual(readReferenceUris({ getData: (type) => type === 'resourceurls' ? '["file:///C:/a.ts"]' : '' }), ['file:///C:/a.ts'])
const assistant: ChatMessage = { id: 'toolbar-assistant', role: 'assistant', content: 'done', createdAt: 1, requestId: 'request-3' }
const assistantMarkup = renderToStaticMarkup(<MessageActionToolbar message={assistant} canRegenerate isSessionActive isLastAssistant onRegenerate={() => undefined} />)
assert.match(assistantMarkup, /复制/)
assert.match(assistantMarkup, /重新生成/)
assert.doesNotMatch(assistantMarkup, /编辑/)
const userMarkup = renderToStaticMarkup(<MessageActionToolbar message={{ id: 'user', role: 'user', content: 'hi', createdAt: 1 }} onEdit={() => undefined} onRevert={() => undefined} onResend={() => undefined} />)
assert.match(userMarkup, /编辑/)
assert.match(userMarkup, /重新发送/)
assert.doesNotMatch(userMarkup, /重新生成/)
assert.doesNotMatch(userMarkup, /赞同|不赞同/, 'feedback actions are assistant-only')
const composerMarkup = renderToStaticMarkup(
<ComposerControls value="draft" onChange={() => undefined} onSend={() => undefined} onModeChange={() => undefined} modelSlot={<span>Model slot</span>} workspaceFooter={<span>Workspace slot</span>} sending />
)
assert.match(composerMarkup, /语音转写服务不可用/, 'without an ASR callback, voice input reports unavailable')
assert.match(composerMarkup, /默认沙箱/)
assert.match(composerMarkup, /添加/)
assert.match(composerMarkup, /Model slot/)
assert.match(composerMarkup, /Workspace slot/)
assert.equal(composerMarkup.indexOf('Model slot') < composerMarkup.indexOf('语音转写服务不可用'), true)
assert.ok(composerMarkup.indexOf('cr-input-toolbar__left') < composerMarkup.indexOf('默认沙箱'))
assert.ok(composerMarkup.indexOf('默认沙箱') < composerMarkup.indexOf('cr-input-toolbar__right'), 'permission control belongs in the left toolbar')
assert.ok(composerMarkup.indexOf('cr-input-toolbar__right') < composerMarkup.indexOf('wb-composer__workspace-footer'), 'workspace footer remains outside the input toolbar')
const activeRunMarkup = renderToStaticMarkup(<ComposerControls value="next draft" onChange={() => undefined} onSend={() => undefined} onStop={() => undefined} />)
assert.match(activeRunMarkup, /aria-label="停止"/)
assert.doesNotMatch(activeRunMarkup, /<textarea[^>]*disabled/)
console.log('Composer/message action regression cases passed')
+706
View File
@@ -0,0 +1,706 @@
import assert from 'node:assert/strict'
import { testTerminalHostPlugin } from './test-terminal-host-plugin'
import { createServer } from 'node:http'
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { build } from 'esbuild'
import type { ChatMessage, ToolCallRecord } from '../src/shared/types'
interface TimelineEntry {
kind: 'branch' | 'prompt'
sessionId: string
entryId?: string
text?: string
}
interface TestState {
userData: string
home: string
handlers: Map<string, (...args: unknown[]) => unknown>
dialogPaths: string[]
nativeMessages: Map<string, Array<{ entryId: string; text: string }>>
failBranchIds: Set<string>
timeline: TimelineEntry[]
pendingPrompts: Array<{ sessionId: string; complete: () => void }>
holdSessions: Set<string>
activePrompts: number
maxActivePrompts: number
nextNativeId: number
speechStatus: number
speechRequests: Array<{ path: string; authorization: string | undefined; contentType: string | undefined; body: string }>
events: unknown[]
}
interface TestBundle {
registerIpc(getWindow: () => unknown): void
disposeOmpKernels(): Promise<void>
sessions: {
appendMessage(sessionId: string, message: ChatMessage): Promise<void>
getMessages(sessionId: string): Promise<ChatMessage[]>
getPendingNativeBranch(sessionId: string): Promise<string | undefined>
setPendingNativeBranch(sessionId: string, entryId: string | undefined): Promise<void>
}
attachments: {
getAttachment(id: string): Promise<{ path?: string; name: string }>
}
}
const TEST_STATE_KEY = '__YCODE_CONVERSATION_IPC_REGRESSION__'
function wait(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
}
async function waitUntil(predicate: () => boolean | Promise<boolean>, message: string): Promise<void> {
for (let attempt = 0; attempt < 250; attempt++) {
if (await predicate()) return
await wait(10)
}
assert.fail(`Timed out: ${message}`)
}
async function waitForAssistant(
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>,
sessionId: string,
predicate: (message: ChatMessage) => boolean = (message) => message.status === 'done'
): Promise<ChatMessage[]> {
for (let attempt = 0; attempt < 250; attempt++) {
const messages = await invoke('session:messages', sessionId) as ChatMessage[]
const last = messages.findLast((message) => message.role === 'assistant')
if (last && predicate(last)) return messages
await wait(10)
}
const messages = await invoke('session:messages', sessionId) as ChatMessage[]
assert.fail(`Timed out waiting for assistant state: ${JSON.stringify(messages.at(-1))}`)
}
function assistant(messages: ChatMessage[]): ChatMessage {
const found = messages.findLast((message) => message.role === 'assistant')
assert.ok(found, 'expected an assistant message')
return found
}
function invokeFor(state: TestState) {
return (channel: string, ...args: unknown[]): Promise<unknown> => {
const handler = state.handlers.get(channel)
if (!handler) throw new Error(`Missing IPC handler: ${channel}`)
return Promise.resolve().then(() => handler({}, ...args))
}
}
function message(id: string, role: 'user' | 'assistant', content: string, extras: Partial<ChatMessage> = {}): ChatMessage {
return { id, role, content, createdAt: Date.now(), status: 'done', ...extras }
}
function seedNativeHistory(state: TestState, sessionId: string, entries: Array<[string, string]>): void {
state.nativeMessages.set(sessionId, entries.map(([entryId, text]) => ({ entryId, text })))
}
function nativePath(userData: string, sessionId: string): string {
return join(userData, 'omp-sessions', `session-${encodeURIComponent(sessionId)}.jsonl`)
}
async function buildBundle(tempRoot: string, state: TestState, name: string): Promise<TestBundle> {
const ipcEntry = join(process.cwd(), 'src', 'main', 'ipc.ts')
const sessionsEntry = join(process.cwd(), 'src', 'main', 'sessions.ts')
const attachmentsEntry = join(process.cwd(), 'src', 'main', 'attachments.ts')
const outfile = join(tempRoot, `${name}.mjs`)
const stateKey = JSON.stringify(TEST_STATE_KEY)
await build({
stdin: {
contents: [
`export { registerIpc, disposeOmpKernels } from ${JSON.stringify(ipcEntry)};`,
`export * as sessions from ${JSON.stringify(sessionsEntry)};`,
`export * as attachments from ${JSON.stringify(attachmentsEntry)};`
].join('\n'),
resolveDir: process.cwd(),
sourcefile: `${name}.ts`,
loader: 'ts'
},
outfile,
bundle: true,
external: ['node-pty'],
platform: 'node',
format: 'esm',
banner: { js: `import { createRequire as createTestRequire } from 'node:module'; const require = createTestRequire(import.meta.url);` },
logLevel: 'silent',
plugins: [
testTerminalHostPlugin,
{
name: 'conversation-ipc-electron-and-omp-mocks',
setup(pluginBuild) {
pluginBuild.onResolve({ filter: /^electron$/ }, () => ({ path: 'electron', namespace: 'mock-electron' }))
pluginBuild.onLoad({ filter: /.*/, namespace: 'mock-electron' }, () => ({
loader: 'js',
contents: `
const state = globalThis[${stateKey}];
export const app = {
getPath: (name) => name === 'userData' ? state.userData : name === 'home' ? state.home : state.userData,
getName: () => 'YCode', getVersion: () => '0.1.0'
};
export const safeStorage = {
isEncryptionAvailable: () => false,
encryptString: (value) => Buffer.from(value),
decryptString: (value) => value.toString()
};
export const ipcMain = { handle: (name, handler) => state.handlers.set(name, handler) };
export const dialog = {
showOpenDialog: async (...args) => ({ canceled: false, filePaths: state.dialogPaths.splice(0) }),
showSaveDialog: async () => ({ canceled: true, filePath: undefined })
};
export const shell = { openPath: async () => '' };
export class BrowserWindow {}
export class WebContentsView {}
`
}))
pluginBuild.onResolve({ filter: /^\.\/omp$/ }, () => ({ path: 'omp', namespace: 'mock-omp' }))
pluginBuild.onLoad({ filter: /.*/, namespace: 'mock-omp' }, () => ({
loader: 'js',
contents: `
import { EventEmitter } from 'node:events';
import { basename } from 'node:path';
const state = globalThis[${stateKey}];
export class OmpKernel extends EventEmitter {
constructor(_exe, _workspace, _profile, sessionPath) {
super();
this.sessionId = decodeURIComponent(basename(sessionPath).replace(/^session-/, '').replace(/\\.jsonl$/, ''));
if (!state.nativeMessages.has(this.sessionId)) state.nativeMessages.set(this.sessionId, []);
}
setHostTools() { return Promise.resolve({ type: 'response', success: true, data: { toolNames: [] } }); }
send(command) {
if (command.type === 'get_branch_messages') {
return Promise.resolve({ type: 'response', success: true, data: { messages: [...(state.nativeMessages.get(this.sessionId) || [])] } });
}
if (command.type === 'branch') {
const entryId = String(command.entryId || '');
state.timeline.push({ kind: 'branch', sessionId: this.sessionId, entryId });
if (state.failBranchIds.has(entryId)) return Promise.resolve({ type: 'response', success: false, error: 'mock branch failure' });
const entries = state.nativeMessages.get(this.sessionId) || [];
const index = entries.findIndex((entry) => entry.entryId === entryId);
if (index < 0) return Promise.resolve({ type: 'response', success: false, error: 'Invalid entry ID for branching' });
state.nativeMessages.set(this.sessionId, entries.slice(0, index));
}
return Promise.resolve({ type: 'response', success: true, data: {} });
}
branch(entryId) { return this.send({ type: 'branch', entryId }); }
prompt(text) {
state.timeline.push({ kind: 'prompt', sessionId: this.sessionId, text });
const entryId = 'native-prompt-' + (++state.nextNativeId);
const entries = state.nativeMessages.get(this.sessionId) || [];
entries.push({ entryId, text });
state.nativeMessages.set(this.sessionId, entries);
state.activePrompts++;
state.maxActivePrompts = Math.max(state.maxActivePrompts, state.activePrompts);
const complete = () => {
state.activePrompts--;
this.emit('event', { type: 'message_update', assistantMessageEvent: { type: 'text_delta', delta: 'reply:' + text } });
this.emit('event', { type: 'agent_end', yielded: true });
};
if (state.holdSessions.has(this.sessionId)) state.pendingPrompts.push({ sessionId: this.sessionId, complete });
else setImmediate(complete);
return Promise.resolve({ type: 'response', success: true });
}
abort() {
setImmediate(() => this.emit('event', { type: 'prompt_result', status: 'aborted' }));
return Promise.resolve({ type: 'response', success: true });
}
respondToHostTool() { return Promise.resolve(); }
dispose() { return Promise.resolve(); }
}
export const detectOmpPath = () => 'mock-omp';
export const getOmpInfo = () => ({ available: true, exePath: 'mock-omp', profile: 'ycode', model: 'ycode/test' });
`
}))
pluginBuild.onResolve({ filter: /^\.\/agent\/loop$/ }, () => ({ path: 'agent-loop', namespace: 'mock-agent-loop' }))
pluginBuild.onLoad({ filter: /.*/, namespace: 'mock-agent-loop' }, () => ({
loader: 'js',
contents: `export async function runTurn() { throw new Error('unexpected non-OMP fallback'); }`
}))
pluginBuild.onResolve({ filter: /^\.\/gateway$/ }, () => ({ path: 'gateway', namespace: 'mock-gateway' }))
pluginBuild.onLoad({ filter: /.*/, namespace: 'mock-gateway' }, () => ({
loader: 'js', contents: 'export class Gateway { constructor() {} }'
}))
}
}
]
})
return await import(pathToFileURL(outfile).href) as TestBundle
}
function register(bundle: TestBundle, state: TestState): (channel: string, ...args: unknown[]) => Promise<unknown> {
state.handlers.clear()
bundle.registerIpc(() => ({
isDestroyed: () => false,
webContents: { isDestroyed: () => false, send: (_channel: string, event: unknown) => state.events.push(event) }
}))
return invokeFor(state)
}
async function testConcurrency(bundle: TestBundle, state: TestState, invoke: ReturnType<typeof invokeFor>): Promise<void> {
const first = await invoke('session:create', 'concurrent one') as { id: string }
const second = await invoke('session:create', 'concurrent two') as { id: string }
state.holdSessions.add(first.id)
state.holdSessions.add(second.id)
const firstSend = await invoke('chat:send', { sessionId: first.id, text: 'concurrent-first' }) as {
modelId?: string
modelName?: string
}
await assert.rejects(invoke('chat:send', { sessionId: first.id, text: 'must-not-append' }), /正在运行/)
await invoke('chat:send', { sessionId: second.id, text: 'concurrent-second' })
await waitUntil(() => state.pendingPrompts.filter((item) => item.sessionId === first.id || item.sessionId === second.id).length === 2,
'both sessions should reach OMP concurrently')
const firstMessages = await invoke('session:messages', first.id) as ChatMessage[]
assert.equal(firstSend.modelId, assistant(firstMessages).modelId,
'chat:send returns the frozen model ID used by the persisted assistant message')
assert.equal(firstSend.modelName, assistant(firstMessages).modelName,
'chat:send returns the frozen model name used by the persisted assistant message')
assert.deepEqual(firstMessages.filter((item) => item.role === 'user').map((item) => item.content), ['concurrent-first'],
'a duplicate same-session send must not append a second user message')
assert.ok(state.maxActivePrompts >= 2, 'different sessions should be able to run in parallel')
const held = state.pendingPrompts.splice(0)
state.holdSessions.clear()
held.forEach((item) => item.complete())
const doneFirst = await waitForAssistant(invoke, first.id)
const doneSecond = await waitForAssistant(invoke, second.id)
assert.equal(assistant(doneFirst).status, 'done')
assert.equal(assistant(doneSecond).status, 'done')
const metadata = (await invoke('session:list')) as Array<{ id: string; title: string }>
assert.equal(metadata.find((item) => item.id === first.id)?.title, 'concurrent-first', 'first send should set its title')
}
async function seedActionHistory(
bundle: TestBundle,
state: TestState,
action: 'edit' | 'resend' | 'regenerate'
): Promise<{ sessionId: string; targetUserId: string; targetAssistantId: string }> {
const invoke = invokeFor(state)
const session = await invoke('session:create', `action-${action}`) as { id: string }
const targetUserId = `${action}-user-target`
const targetAssistantId = `${action}-assistant-target`
const history = [
message(`${action}-user-before`, 'user', 'first prompt', { nativeEntryId: `${action}-native-before` }),
message(`${action}-assistant-before`, 'assistant', 'first reply'),
message(targetUserId, 'user', 'old prompt', { nativeEntryId: `${action}-native-target` }),
message(targetAssistantId, 'assistant', 'old reply')
]
for (const item of history) await bundle.sessions.appendMessage(session.id, item)
seedNativeHistory(state, session.id, [
[`${action}-native-before`, 'first prompt'],
[`${action}-native-target`, 'old prompt']
])
return { sessionId: session.id, targetUserId, targetAssistantId }
}
async function testActionBranch(
bundle: TestBundle,
state: TestState,
action: 'edit' | 'resend' | 'regenerate'
): Promise<void> {
const invoke = invokeFor(state)
const seeded = await seedActionHistory(bundle, state, action)
const timelineStart = state.timeline.length
const fromMessageId = action === 'regenerate' ? seeded.targetAssistantId : seeded.targetUserId
await invoke('chat:send', {
sessionId: seeded.sessionId,
text: `${action}-replacement`,
action,
fromMessageId
})
const messages = await waitForAssistant(invoke, seeded.sessionId)
const activity = state.timeline.slice(timelineStart).filter((item) => item.sessionId === seeded.sessionId)
const branchIndex = activity.findIndex((item) => item.kind === 'branch' && item.entryId === `${action}-native-target`)
const promptIndex = activity.findIndex((item) => item.kind === 'prompt' && item.text?.split(/\r?\n/, 1)[0] === `${action}-replacement`)
assert.ok(branchIndex >= 0 && promptIndex > branchIndex, `${action} must branch by native entryId before prompting`)
assert.equal(activity[promptIndex].text, [`${action}-replacement`, messages[2].capabilityContext].filter(Boolean).join('\n\n'),
'native prompt must include the exact persisted turn context without changing the user text')
assert.match(activity[promptIndex].text ?? '', /<ycode-agent-mode mode="craft">/, 'native prompt declares the frozen craft mode')
assert.equal(messages.length, 4, `${action} should keep the old prefix and replace the selected suffix`)
assert.deepEqual(messages.slice(0, 2).map((item) => item.content), ['first prompt', 'first reply'])
assert.equal(messages[2].content, `${action}-replacement`)
assert.ok(messages[2].nativeEntryId?.startsWith('native-prompt-'), 'new user message should receive the returned native entryId')
assert.equal(await bundle.sessions.getPendingNativeBranch(seeded.sessionId), undefined,
'successful native entry lookup should clear the pending branch marker')
const archiveDirectory = join(state.userData, 'session-branches', encodeURIComponent(seeded.sessionId))
const archiveFiles = await readdir(archiveDirectory)
assert.equal(archiveFiles.length, 1, `${action} should archive the replaced branch`)
const archive = JSON.parse(await readFile(join(archiveDirectory, archiveFiles[0]), 'utf8')) as {
reason: string
session: { messages: ChatMessage[] }
}
assert.equal(archive.reason, action)
assert.equal(archive.session.messages.length, 4, 'archive should preserve the full pre-action conversation')
}
async function testActionBranchFailure(bundle: TestBundle, state: TestState): Promise<void> {
const invoke = invokeFor(state)
const seeded = await seedActionHistory(bundle, state, 'edit')
const before = await bundle.sessions.getMessages(seeded.sessionId)
state.failBranchIds.add('edit-native-target')
await assert.rejects(invoke('chat:send', {
sessionId: seeded.sessionId,
text: 'must-not-append-on-branch-failure',
action: 'edit',
fromMessageId: seeded.targetUserId
}), /branch failure/)
state.failBranchIds.delete('edit-native-target')
assert.deepEqual(await bundle.sessions.getMessages(seeded.sessionId), before,
'failed native branching must not archive or append a replacement user message')
assert.equal(await bundle.sessions.getPendingNativeBranch(seeded.sessionId), undefined,
'failed action branch should restore the prior marker')
}
async function testInterruptedResend(bundle: TestBundle, state: TestState): Promise<void> {
const invoke = invokeFor(state)
for (const nativeAccepted of [false, true]) {
const session = await invoke('session:create', 'interrupted-resend') as { id: string }
await bundle.sessions.appendMessage(session.id, message('prefix-user', 'user', 'earlier', { nativeEntryId: 'prefix-native' }))
await bundle.sessions.appendMessage(session.id, message('prefix-assistant', 'assistant', 'earlier reply'))
await bundle.sessions.appendMessage(session.id, message('interrupted-user', 'user', 'interrupted prompt'))
await bundle.sessions.appendMessage(session.id, message('interrupted-assistant', 'assistant', 'partial', { status: 'cancelled', interrupted: true }))
seedNativeHistory(state, session.id, [['prefix-native', 'earlier'], ...(nativeAccepted ? [['unbound-native', 'interrupted prompt'] as [string, string]] : [])])
const start = state.timeline.length
await invoke('chat:send', { sessionId: session.id, text: 'interrupted prompt', action: 'resend', fromMessageId: 'interrupted-user' })
const result = await waitForAssistant(invoke, session.id)
assert.equal(result.length, 4)
assert.equal(result[2].content, 'interrupted prompt')
const branches = state.timeline.slice(start).filter((entry) => entry.kind === 'branch')
assert.equal(branches.length, nativeAccepted ? 1 : 0)
if (nativeAccepted) assert.equal(branches[0].entryId, 'unbound-native', 'recover the accepted native user without truncating its preceding context')
assert.ok(result[2].nativeEntryId?.startsWith('native-prompt-'))
}
}
function writeTool(id: string, path: string, oldContent: string | null, newContent: string): ToolCallRecord {
return {
id,
name: 'write_file',
args: { path },
argsRaw: JSON.stringify({ path }),
status: 'ok',
output: 'write complete',
result: { path, oldContent, newContent }
}
}
async function seedWriteHistory(
bundle: TestBundle,
state: TestState,
title: string,
path: string,
targetEntryId: string,
currentFile: string
): Promise<string> {
await writeFile(path, currentFile, 'utf8')
const session = await invokeFor(state)('session:create', title) as { id: string }
await bundle.sessions.appendMessage(session.id, message(`${title}-user`, 'user', 'write prompt', { nativeEntryId: targetEntryId }))
await bundle.sessions.appendMessage(session.id, message(`${title}-assistant`, 'assistant', 'write answer', {
tools: [writeTool(`${title}-write`, path, 'before', 'after')]
}))
seedNativeHistory(state, session.id, [[targetEntryId, 'write prompt']])
return session.id
}
async function testFeedbackAndRevert(bundle: TestBundle, state: TestState, workspace: string): Promise<{
markerSessionId: string
markerEntryId: string
}> {
const invoke = invokeFor(state)
const feedbackSession = await invoke('session:create', 'feedback') as { id: string }
const feedbackUser = message('feedback-user', 'user', 'feedback request')
const feedbackAssistant = message('feedback-assistant', 'assistant', 'feedback response')
await bundle.sessions.appendMessage(feedbackSession.id, feedbackUser)
await bundle.sessions.appendMessage(feedbackSession.id, feedbackAssistant)
await invoke('chat:feedback', feedbackSession.id, feedbackAssistant.id, 'like')
assert.equal((await bundle.sessions.getMessages(feedbackSession.id))[1].feedback, 'like')
await assert.rejects(invoke('chat:feedback', feedbackSession.id, feedbackUser.id, 'dislike'), /助手消息/)
await assert.rejects(invoke('chat:feedback', feedbackSession.id, 'missing', 'like'), /助手消息/)
await assert.rejects(invoke('chat:feedback', feedbackSession.id, feedbackAssistant.id, 'bad'), /反馈值无效/)
const conflictPath = join(workspace, 'conflict.txt')
const conflictId = await seedWriteHistory(bundle, state, 'conflict-revert', conflictPath, 'native-conflict-target', 'external edit')
const conflictBefore = await bundle.sessions.getMessages(conflictId)
await assert.rejects(invoke('chat:revert', conflictId, 'conflict-revert-user'), /外部修改/)
assert.equal(await readFile(conflictPath, 'utf8'), 'external edit', 'revert conflict must preserve the externally edited file')
assert.deepEqual(await bundle.sessions.getMessages(conflictId), conflictBefore, 'conflict should leave history intact')
assert.equal(await bundle.sessions.getPendingNativeBranch(conflictId), undefined)
const rejectedPath = join(workspace, 'branch-failure.txt')
const rejectedId = await seedWriteHistory(bundle, state, 'failed-revert', rejectedPath, 'native-failed-revert', 'after')
state.failBranchIds.add('native-failed-revert')
await assert.rejects(invoke('chat:revert', rejectedId, 'failed-revert-user'), /恢复失败/)
state.failBranchIds.delete('native-failed-revert')
assert.equal(await readFile(rejectedPath, 'utf8'), 'after', 'failed native branch must undo the file restore')
assert.equal((await bundle.sessions.getMessages(rejectedId)).length, 2, 'failed native branch must preserve history')
assert.equal(await bundle.sessions.getPendingNativeBranch(rejectedId), undefined)
const readOnlyId = await invoke('session:create', 'read-only-revert') as { id: string }
await bundle.sessions.appendMessage(readOnlyId.id, message('read-only-user', 'user', 'read only'))
await bundle.sessions.appendMessage(readOnlyId.id, message('read-only-assistant', 'assistant', 'read', {
tools: [{ id: 'read-tool', name: 'read_file', args: {}, argsRaw: '{}', status: 'ok' }]
}))
await assert.rejects(invoke('chat:revert', readOnlyId.id, 'read-only-user'), /没有可恢复/)
const unknownPath = join(workspace, 'unknown-write.txt')
await writeFile(unknownPath, 'after', 'utf8')
const unknownId = await invoke('session:create', 'unknown-write-revert') as { id: string }
await bundle.sessions.appendMessage(unknownId.id, message('unknown-write-user', 'user', 'unknown write', {
nativeEntryId: 'native-unknown-write'
}))
await bundle.sessions.appendMessage(unknownId.id, message('unknown-write-assistant', 'assistant', 'unknown', {
tools: [{
id: 'unknown-write-tool', name: 'delete_file', args: {}, argsRaw: '{}', status: 'ok',
result: { path: unknownPath, oldContent: 'before', newContent: 'after' }
}]
}))
await assert.rejects(invoke('chat:revert', unknownId.id, 'unknown-write-user'), /无法完整回滚/)
assert.equal(await readFile(unknownPath, 'utf8'), 'after', 'unrecognized writes must not be treated as reversible')
const markerPath = join(workspace, 'persist-revert.txt')
const markerSessionId = await seedWriteHistory(bundle, state, 'pending-revert', markerPath, 'native-revert-target', 'after')
await invoke('chat:revert', markerSessionId, 'pending-revert-user')
assert.equal(await readFile(markerPath, 'utf8'), 'before', 'known complete host write should be restored')
assert.deepEqual(await bundle.sessions.getMessages(markerSessionId), [], 'successful revert should truncate the active history prefix')
const markerEntryId = await bundle.sessions.getPendingNativeBranch(markerSessionId)
assert.equal(markerEntryId, 'native-revert-target', 'successful revert should persist a replay marker')
const revertArchiveDirectory = join(state.userData, 'session-branches', encodeURIComponent(markerSessionId))
const revertArchiveFiles = await readdir(revertArchiveDirectory)
assert.equal(revertArchiveFiles.length, 1, 'revert should archive its abandoned branch')
const revertArchive = JSON.parse(await readFile(join(revertArchiveDirectory, revertArchiveFiles[0]), 'utf8')) as {
reason: string
session: { messages: ChatMessage[] }
}
assert.equal(revertArchive.reason, 'revert')
assert.equal(revertArchive.session.messages.length, 2, 'revert archive should preserve the messages removed from the active history')
const stored = JSON.parse(await readFile(join(state.userData, 'sessions.json'), 'utf8')) as {
sessions: Array<{ id: string; pendingNativeBranchId?: string }>
}
assert.equal(stored.sessions.find((item) => item.id === markerSessionId)?.pendingNativeBranchId, markerEntryId,
'pending native branch marker should be present on disk')
return { markerSessionId, markerEntryId: markerEntryId! }
}
async function testAttachments(bundle: TestBundle, state: TestState, tempRoot: string): Promise<void> {
const invoke = invokeFor(state)
const pickedPath = join(tempRoot, 'picked.txt')
await writeFile(pickedPath, 'picked file content', 'utf8')
state.dialogPaths.push(pickedPath)
const picked = await invoke('attachments:pick') as Array<{ id: string; name: string; path?: string; size: number }>
assert.equal(picked.length, 1)
assert.equal(picked[0].name, 'picked.txt')
assert.equal(picked[0].size, Buffer.byteLength('picked file content'))
assert.ok(picked[0].path && (await readFile(picked[0].path, 'utf8')) === 'picked file content',
'picker should stage a real copy in user data')
const staged = await invoke('attachments:stage', {
name: '../safe-note.txt', mimeType: 'text/plain', dataBase64: Buffer.from('attachment body').toString('base64')
}) as { id: string; name: string; path?: string; size: number }
assert.equal(staged.name, 'safe-note.txt', 'staging should discard path components from the supplied filename')
assert.equal(staged.size, Buffer.byteLength('attachment body'))
assert.equal((await bundle.attachments.getAttachment(staged.id)).name, 'safe-note.txt')
assert.equal(await readFile(staged.path!, 'utf8'), 'attachment body')
await assert.rejects(invoke('attachments:stage', {
name: 'metadata.json', mimeType: 'text/plain', dataBase64: Buffer.from('no').toString('base64')
}), /文件名无效/)
const tamperedMetadata = join(state.userData, 'attachments', staged.id, 'metadata.json')
await writeFile(tamperedMetadata, JSON.stringify({ ...staged, path: join(tempRoot, 'outside.txt') }), 'utf8')
const pathChecked = await bundle.attachments.getAttachment(staged.id)
assert.equal(pathChecked.path, staged.path, 'attachment metadata path must be recomputed inside the staged directory')
assert.notEqual(pathChecked.path, join(tempRoot, 'outside.txt'))
await writeFile(tamperedMetadata, JSON.stringify({ ...staged, name: '../outside.txt', path: join(tempRoot, 'outside.txt') }), 'utf8')
await assert.rejects(bundle.attachments.getAttachment(staged.id), /元数据无效/)
const sendSession = await invoke('session:create', 'invalid-attachment') as { id: string }
await assert.rejects(invoke('chat:send', {
sessionId: sendSession.id, text: 'must not send', attachments: [{ ...staged, id: staged.id }]
}), /元数据无效/)
assert.deepEqual(await invoke('session:messages', sendSession.id), [], 'invalid attachment metadata must reject before appending')
const sendWithAttachment = await invoke('session:create', 'attachment-send') as { id: string }
await invoke('chat:send', { sessionId: sendWithAttachment.id, text: 'read attachment', attachments: [picked[0]] })
await waitForAssistant(invoke, sendWithAttachment.id)
const prompt = state.timeline.findLast((item) => item.kind === 'prompt' && item.sessionId === sendWithAttachment.id)
assert.ok(prompt?.text?.includes('附件 picked.txt:\npicked file content'), 'OMP prompt should receive staged file contents')
await assert.rejects(invoke('attachments:remove', picked[0].id), /已用于会话/)
assert.ok(await readFile(join(state.userData, 'attachments', picked[0].id, 'metadata.json'), 'utf8'),
'removing a committed attachment should leave the session source intact')
await invoke('attachments:remove', staged.id)
await assert.rejects(bundle.attachments.getAttachment(staged.id))
}
async function testSpeech(bundle: TestBundle, state: TestState): Promise<void> {
const server = createServer((request, response) => {
const chunks: Buffer[] = []
request.on('data', (chunk: Buffer) => chunks.push(Buffer.from(chunk)))
request.on('end', () => {
state.speechRequests.push({
path: request.url || '',
authorization: request.headers.authorization,
contentType: request.headers['content-type'],
body: Buffer.concat(chunks).toString('utf8')
})
response.writeHead(state.speechStatus, { 'content-type': 'application/json' })
response.end(state.speechStatus === 200 ? JSON.stringify({ text: 'recognized local audio' }) : JSON.stringify({ error: 'mock failure' }))
})
})
await new Promise<void>((resolve, reject) => {
server.once('error', reject)
server.listen(0, '127.0.0.1', resolve)
})
try {
const address = server.address()
assert.ok(address && typeof address === 'object')
const speechBaseUrl = `http://127.0.0.1:${address.port}`
const invoke = invokeFor(state)
await invoke('config:update', { speechBaseUrl, speechModel: 'local-test-model', apiKey: 'regression-only-key' })
const audioBase64 = Buffer.from('local fake wav bytes').toString('base64')
const result = await invoke('speech:transcribe', { audioBase64, mimeType: 'audio/wav' }) as { text: string }
assert.deepEqual(result, { text: 'recognized local audio' })
const request = state.speechRequests.at(-1)
assert.ok(request)
assert.equal(request.path, '/audio/transcriptions')
assert.equal(request.authorization, 'Bearer regression-only-key')
assert.ok(request.contentType?.includes('multipart/form-data; boundary='))
assert.ok(request.body.includes('name="model"') && request.body.includes('local-test-model'))
assert.ok(request.body.includes('filename="recording.wav"') && request.body.includes('local fake wav bytes'))
state.speechStatus = 503
await assert.rejects(invoke('speech:transcribe', { audioBase64, mimeType: 'audio/wav' }), /转写失败(503)/)
assert.equal(state.speechRequests.length, 2, 'both transcription requests should stay on the local server')
assert.ok(state.speechRequests.every((item) => item.path === '/audio/transcriptions'))
} finally {
await new Promise<void>((resolve) => server.close(() => resolve()))
}
}
async function testPendingReplay(
tempRoot: string,
bundle: TestBundle,
state: TestState,
markerSessionId: string,
markerEntryId: string
): Promise<TestBundle> {
// A real OMP cold start loads the old file leaf; branch alone was not persisted.
seedNativeHistory(state, markerSessionId, [[markerEntryId, 'write prompt']])
const restarted = await buildBundle(tempRoot, state, 'conversation-ipc-restarted')
const invoke = register(restarted, state)
const before = await restarted.sessions.getMessages(markerSessionId)
assert.deepEqual(before, [], 'restarted session should load the archived active prefix')
state.failBranchIds.add(markerEntryId)
await assert.rejects(invoke('chat:send', { sessionId: markerSessionId, text: 'blocked until branch succeeds' }), /恢复已保存的原生会话分支/)
state.failBranchIds.delete(markerEntryId)
assert.deepEqual(await restarted.sessions.getMessages(markerSessionId), before,
'failed pending branch replay must not append a user message')
assert.equal(await restarted.sessions.getPendingNativeBranch(markerSessionId), markerEntryId,
'failed pending branch replay must retain its marker for a retry')
const start = state.timeline.length
await invoke('chat:send', { sessionId: markerSessionId, text: 'replayed branch prompt' })
const messages = await waitForAssistant(invoke, markerSessionId)
const activity = state.timeline.slice(start).filter((item) => item.sessionId === markerSessionId)
const branch = activity.findIndex((item) => item.kind === 'branch' && item.entryId === markerEntryId)
const prompt = activity.findIndex((item) => item.kind === 'prompt' && item.text?.split(/\r?\n/, 1)[0] === 'replayed branch prompt')
assert.ok(branch >= 0 && prompt > branch, 'fresh OMP kernel must replay the saved branch marker before prompt')
assert.equal(activity[prompt].text, ['replayed branch prompt', messages[0].capabilityContext].filter(Boolean).join('\n\n'),
'restart prompt retains the exact current turn context')
assert.match(activity[prompt].text ?? '', /<ycode-agent-mode mode="craft">/, 'restart prompt declares current craft mode')
assert.equal(messages[0].content, 'replayed branch prompt', 'persisted user content stays exact across restart')
assert.ok(messages[0].nativeEntryId?.startsWith('native-prompt-'), 'restarted send should map its returned native entryId')
assert.equal(await restarted.sessions.getPendingNativeBranch(markerSessionId), undefined,
'new native user entry should clear the marker after successful lookup')
return restarted
}
async function testColdRecovery(tempRoot: string, state: TestState, bundle: TestBundle): Promise<void> {
const invoke = invokeFor(state)
const session = await invoke('session:create', 'cold streaming recovery') as { id: string }
await bundle.sessions.appendMessage(session.id, message('cold-user', 'user', 'incomplete request', { status: 'done' }))
await bundle.sessions.appendMessage(session.id, message('cold-assistant', 'assistant', 'partial', {
requestId: 'cold-run',
status: 'streaming',
parts: [{ type: 'text', text: 'partial' }, { type: 'tool', toolId: 'cold-tool' }],
tools: [{ id: 'cold-tool', name: 'shell', args: {}, argsRaw: '{}', status: 'running', output: 'partial tool output' }]
}))
const cold = await buildBundle(tempRoot, state, 'conversation-ipc-cold-recovery')
const messages = await cold.sessions.getMessages(session.id)
const recovered = assistant(messages)
assert.equal(recovered.status, 'cancelled')
assert.equal(recovered.interrupted, true)
assert.equal(recovered.tools?.[0].status, 'cancelled')
assert.equal(recovered.tools?.[0].output, 'partial tool output', 'cold recovery should retain running tool output')
const disk = JSON.parse(await readFile(join(state.userData, 'sessions.json'), 'utf8')) as {
sessions: Array<{ id: string; messages: ChatMessage[] }>
}
const diskRecovered = disk.sessions.find((item) => item.id === session.id)?.messages.at(-1)
assert.equal(diskRecovered?.status, 'cancelled', 'cold recovery should persist its terminal status')
assert.equal(diskRecovered?.tools?.[0].status, 'cancelled')
assert.equal(diskRecovered?.tools?.[0].output, 'partial tool output')
}
async function main(): Promise<void> {
const tempRoot = await mkdtemp(join(tmpdir(), 'ycode-conversation-ipc-'))
const state: TestState = {
userData: join(tempRoot, 'userData'),
home: join(tempRoot, 'home'),
handlers: new Map(),
dialogPaths: [],
nativeMessages: new Map(),
failBranchIds: new Set(),
timeline: [],
pendingPrompts: [],
holdSessions: new Set(),
activePrompts: 0,
maxActivePrompts: 0,
nextNativeId: 0,
speechStatus: 200,
speechRequests: [],
events: []
}
await mkdir(state.userData, { recursive: true })
await mkdir(state.home, { recursive: true })
const globals = globalThis as typeof globalThis & Record<string, unknown>
const previousState = globals[TEST_STATE_KEY]
globals[TEST_STATE_KEY] = state
const bundles: TestBundle[] = []
try {
const bundle = await buildBundle(tempRoot, state, 'conversation-ipc-primary')
bundles.push(bundle)
const invoke = register(bundle, state)
const workspace = join(tempRoot, 'workspace')
await mkdir(workspace, { recursive: true })
state.dialogPaths.push(workspace)
assert.deepEqual(await invoke('workspace:choose'), { root: workspace, changed: true })
await testConcurrency(bundle, state, invoke)
for (const action of ['edit', 'resend', 'regenerate'] as const) await testActionBranch(bundle, state, action)
await testActionBranchFailure(bundle, state)
await testInterruptedResend(bundle, state)
await testAttachments(bundle, state, tempRoot)
await testSpeech(bundle, state)
const { markerSessionId, markerEntryId } = await testFeedbackAndRevert(bundle, state, workspace)
const restarted = await testPendingReplay(tempRoot, bundle, state, markerSessionId, markerEntryId)
bundles.push(restarted)
await testColdRecovery(tempRoot, state, restarted)
console.log('conversation-ipc-regression: all checks passed')
} finally {
await Promise.all(bundles.map((bundle) => bundle.disposeOmpKernels().catch(() => undefined)))
globals[TEST_STATE_KEY] = previousState
await rm(tempRoot, { recursive: true, force: true })
}
}
void main().catch((error: unknown) => {
console.error('conversation-ipc-regression failed:', error)
process.exitCode = 1
})
@@ -0,0 +1,693 @@
import assert from 'node:assert/strict'
import { renderToStaticMarkup } from 'react-dom/server'
import {
AskUserQuestion,
PlanCreateBlock,
PlanUpdateBlock,
PermissionConfirm,
QuestionFloating,
QuestionAnswerTranscript,
TaskBlock,
ThinkingBlock,
ToolCallCard,
ToolCallRenderer,
ToolGroup,
buildQuestionAnswerPayload,
computeToolGroupVisibleStart,
getPermissionChoices,
getCapturedToolKind,
selectPlanTasks
} from '../src/renderer/src/workbuddy/conversation'
import { convertReadFile, createListFileOpenRequest, createReadFileOpenRequest } from '../src/renderer/src/workbuddy/conversation/ToolCallRenderer'
import { SourceToolHeader } from '../src/renderer/src/workbuddy/conversation/SourceToolHeader'
import { resolveSourceToolIcon } from '../src/renderer/src/workbuddy/conversation/SourceToolIcons'
import { SourceToolDiff } from '../src/renderer/src/workbuddy/conversation/SourceToolDiff'
const toolIconMarkup = (name: string) => renderToStaticMarkup(resolveSourceToolIcon(name) ?? null)
assert.match(toolIconMarkup('Read'), /M7\.7407 13\.66/)
assert.match(toolIconMarkup('read_file'), /M7\.7407 13\.66/)
assert.match(toolIconMarkup('PowerShell'), /M0\.1942 3\.7746/)
assert.match(toolIconMarkup('execute_command'), /M0\.1942 3\.7746/)
assert.match(toolIconMarkup('list_dir'), /M0\.1942 3\.7746/)
assert.equal(toolIconMarkup('Inspect'), toolIconMarkup('execute_command'))
assert.match(toolIconMarkup('Grep'), /M1\.2982 9\.8292/)
assert.match(toolIconMarkup('web_search'), /M1\.37 6\.6667/)
assert.match(toolIconMarkup('mcp_call_tool'), /M2\.9742 -0\.1497/)
assert.match(toolIconMarkup('mcp__browser__tabs_list'), /M6\.3745 2\.6414/)
assert.match(toolIconMarkup('mcp__computer-use__open_app'), /M6\.5 11\.0857/)
assert.match(renderToStaticMarkup(resolveSourceToolIcon('mcp__connector-proxy__pick_location', 'captured-icon') ?? null), /class="captured-icon"/)
assert.match(toolIconMarkup('mcp__workbuddy__skill_management'), /M4\.6063 4\.6987/)
const pluginToolAliases = [
'search_plugins', 'suggest_plugin_install', 'list_installed_plugins', 'uninstall_plugin', 'reconnect_plugin',
'mcp__workbuddy__search_plugins', 'mcp__workbuddy__suggest_plugin_install', 'mcp__workbuddy__list_installed_plugins', 'mcp__workbuddy__uninstall_plugin', 'mcp__workbuddy__reconnect_plugin',
'mcp__connector-proxy__search_plugins', 'mcp__connector-proxy__suggest_plugin_install', 'mcp__connector-proxy__list_installed_plugins', 'mcp__connector-proxy__uninstall_plugin', 'mcp__connector-proxy__reconnect_plugin'
]
for (const alias of pluginToolAliases) assert.match(toolIconMarkup(alias), /M5\.7784 2\.3425/)
assert.match(renderToStaticMarkup(resolveSourceToolIcon('mcp__connector-proxy__reconnect_plugin', 'plugin-icon') ?? null), /class="plugin-icon"/)
assert.match(toolIconMarkup('unregistered_tool'), /M2\.9742 -0\.1497/)
assert.match(toolIconMarkup('mcp__unknown__tool'), /M2\.9742 -0\.1497/)
const tool = {
id: 'tool-1',
name: 'execute_command',
status: 'cancelled' as const,
args: { command: 'echo test' },
result: { output: 'first line\nFINAL OUTPUT SENTINEL' }
}
const presentedFiles = renderToStaticMarkup(<ToolCallRenderer tool={{ id: 'present-files', name: 'present_files', status: 'success',
result: { result: '{"files":["result.html"]}' } }} />)
assert.match(presentedFiles, /cr-tool-compact-misc__pre/)
assert.match(presentedFiles, /result.html/)
assert.match(presentedFiles, /aria-expanded="false"/)
assert.doesNotMatch(presentedFiles, /cr-tool-call__data/)
const failedFiles = renderToStaticMarkup(<ToolCallRenderer tool={{ id: 'present-files', name: 'present_files', status: 'error',
result: { result: 'failed' } }} />)
assert.doesNotMatch(failedFiles, /aria-expanded=/)
const toolMarkup = renderToStaticMarkup(<ToolCallCard tool={tool} defaultExpanded />)
assert.match(toolMarkup, /execute_command/)
assert.match(toolMarkup, /Cancelled/)
assert.match(toolMarkup, /echo test/)
assert.match(toolMarkup, /FINAL OUTPUT SENTINEL/)
assert.match(renderToStaticMarkup(<ToolCallCard tool={{ ...tool, status: 'failed' }} />), /Failed/)
const completeOutput = 'Complete output that exceeds the summary. LAST OUTPUT SENTINEL'
const outputPreferred = renderToStaticMarkup(
<ToolCallCard
tool={{ id: 'output-1', name: 'custom', status: 'success', output: completeOutput, result: { output: 'short summary only' } }}
defaultExpanded
/>
)
assert.match(outputPreferred, /Complete output that exceeds the summary/)
assert.match(outputPreferred, /LAST OUTPUT SENTINEL/)
assert.doesNotMatch(outputPreferred, /short summary only/)
const collapsedGroup = renderToStaticMarkup(
<ToolGroup stats={{ toolCallCount: 2, messageCount: 1, errorCount: 1 }} summary={{ text: 'Running command' }}>
<span>group child</span>
</ToolGroup>
)
assert.match(collapsedGroup, /aria-expanded="false"/)
assert.match(collapsedGroup, /Running command/)
assert.match(collapsedGroup, /group child/, 'source collapse retains mounted children for height/opacity transitions')
assert.match(collapsedGroup, /aria-hidden="true" inert=""/)
const expandedGroup = renderToStaticMarkup(
<ToolGroup stats={{ toolCallCount: 2, messageCount: 1 }} defaultExpanded>
<span>group child</span>
</ToolGroup>
)
assert.match(expandedGroup, /Tools/)
assert.match(expandedGroup, /group child/)
assert.equal(computeToolGroupVisibleStart(20, undefined, 1), 5)
assert.equal(computeToolGroupVisibleStart(20, undefined, 2), 0)
const pagedGroup = renderToStaticMarkup(
<ToolGroup stats={{ toolCallCount: 20, messageCount: 0 }} defaultExpanded>
{Array.from({ length: 20 }, (_, index) => <span key={index}>{`item-${index}`}</span>)}
</ToolGroup>
)
assert.match(pagedGroup, /Show 5 earlier/)
assert.doesNotMatch(pagedGroup, /item-0</)
assert.match(pagedGroup, /item-19</)
const sourceSummaryGroup = renderToStaticMarkup(
<ToolGroup variant="summary" stats={{ toolCallCount: 20, messageCount: 20 }} summary={{ text: '运行命令' }} defaultExpanded>
{Array.from({ length: 20 }, (_, index) => <span key={index}>{`summary-item-${index}`}</span>)}
</ToolGroup>
)
assert.match(sourceSummaryGroup, /cr-collapse--summary/)
assert.match(sourceSummaryGroup, /content-inner--summary/)
assert.match(sourceSummaryGroup, /summary-item-0</)
assert.match(sourceSummaryGroup, /summary-item-19</)
assert.doesNotMatch(sourceSummaryGroup, /Show \d+ earlier/)
const permissionOptions = [
{ optionId: 'once', kind: 'allow_once', name: 'Allow once' },
{ optionId: 'always', kind: 'allow_always', name: 'Always allow' },
{ optionId: 'reject', kind: 'reject_once', name: 'Reject' }
]
assert.equal(getPermissionChoices(permissionOptions).primary?.optionId, 'once')
assert.equal(getPermissionChoices(permissionOptions).secondary?.optionId, 'always')
assert.equal(getPermissionChoices(permissionOptions, 'session').primary?.optionId, 'always')
const pendingPermission = renderToStaticMarkup(
<PermissionConfirm
pendingId="permission-1"
toolName="execute_command"
toolKind="computerUse"
options={permissionOptions}
resolve={() => undefined}
reject={() => undefined}
/>
)
assert.match(pendingPermission, /role="dialog"/)
assert.match(pendingPermission, /data-pending-id="permission-1"/)
assert.match(pendingPermission, /data-option-id="once"/)
assert.match(pendingPermission, /Always allow/)
assert.match(pendingPermission, /Reject/)
const answeredPermission = renderToStaticMarkup(
<PermissionConfirm
pendingId="permission-2"
toolName="image_gen"
options={permissionOptions}
status="answered"
resolve={() => undefined}
reject={() => undefined}
/>
)
assert.match(answeredPermission, /Approved/)
assert.doesNotMatch(answeredPermission, /data-option-id=/)
const respondingPermission = renderToStaticMarkup(
<PermissionConfirm
pendingId="permission-responding"
toolName="image_gen"
options={permissionOptions}
status="responding"
resolve={() => undefined}
reject={() => undefined}
/>
)
assert.match(respondingPermission, /Sending decision/)
assert.match(respondingPermission, /disabled=""/)
const failedPermission = renderToStaticMarkup(
<PermissionConfirm
pendingId="permission-error"
toolName="image_gen"
options={permissionOptions}
status="error"
error="Broker unavailable"
resolve={() => undefined}
reject={() => undefined}
/>
)
assert.match(failedPermission, /Broker unavailable/)
assert.match(failedPermission, /data-option-id="once"/)
const rejectedPermission = renderToStaticMarkup(
<PermissionConfirm
pendingId="permission-rejected"
toolName="image_gen"
options={permissionOptions}
status="rejected"
resolve={() => undefined}
reject={() => undefined}
/>
)
assert.match(rejectedPermission, /Rejected/)
const cancelledPermission = renderToStaticMarkup(
<PermissionConfirm
pendingId="permission-3"
toolName="image_gen"
options={permissionOptions}
status="cancelled"
resolve={() => undefined}
reject={() => undefined}
/>
)
assert.match(cancelledPermission, /Cancelled/)
const questionConfig = {
title: 'Choose a runtime',
questions: [
{
id: 'runtime',
question: 'Which runtime?',
options: [
{ id: 'node', question: 'Node.js', description: 'JavaScript runtime' },
{ id: 'deno', question: 'Deno' }
]
},
{
id: 'features',
question: 'Select features',
multiSelect: true,
options: ['Tests', 'Lint']
}
]
}
const questionMarkup = renderToStaticMarkup(<QuestionFloating config={questionConfig} />)
assert.match(questionMarkup, /Which runtime\?/)
assert.match(questionMarkup, /Node\.js/)
assert.match(questionMarkup, /JavaScript runtime/)
assert.match(questionMarkup, /1\/2/)
assert.match(questionMarkup, /Add your own answer/)
assert.match(questionMarkup, /role="button" aria-pressed="true"/)
const answer = buildQuestionAnswerPayload(
questionConfig.questions,
{ runtime: ['node'], features: ['Tests'] },
{ runtime: '', features: 'Custom feature' },
{ features: true },
questionConfig.title
)
assert.deepEqual(answer, {
title: 'Choose a runtime',
questions: [
{ id: 'runtime', question: 'Which runtime?', answers: ['node'] },
{ id: 'features', question: 'Select features', answers: ['Tests', 'Custom feature'] }
]
})
const pendingQuestion = renderToStaticMarkup(
<AskUserQuestion tool={{ name: 'AskUserQuestion', status: 'pending' }} />
)
assert.match(pendingQuestion, /Waiting for your answer/)
const completedQuestion = renderToStaticMarkup(
<AskUserQuestion
tool={{
name: 'AskUserQuestion',
status: 'executed',
metaData: { questionAnswer: { questions: [{ id: 'q1', question: 'Where?', answers: ['Here'] }] } }
}}
contentSettled={false}
/>
)
assert.match(completedQuestion, /Question for you/)
assert.match(completedQuestion, /Where\?/)
assert.match(completedQuestion, /Here/)
const cancelledQuestion = renderToStaticMarkup(
<AskUserQuestion tool={{ name: 'AskUserQuestion', status: 'cancelled' }} />
)
assert.match(cancelledQuestion, /Question cancelled/)
const failedQuestion = renderToStaticMarkup(
<AskUserQuestion tool={{ name: 'AskUserQuestion', status: 'failed' }} />
)
assert.match(failedQuestion, /Question failed/)
const skippedQuestion = renderToStaticMarkup(
<AskUserQuestion tool={{ name: 'AskUserQuestion', status: 'executed', metadata: { questionSkipped: true } }} />
)
assert.match(skippedQuestion, /Question skipped/)
const transcript = renderToStaticMarkup(
<QuestionAnswerTranscript questionAnswer={{ questions: [{ id: 'q2', question: 'Skipped?', answers: [] }] }} />
)
assert.match(transcript, /\[Skipped\]/)
const collapsedThinking = renderToStaticMarkup(<ThinkingBlock text="Hidden reasoning" isFinished />)
// The source collapse keeps its content mounted for CSS transitions; it must be inaccessible while closed.
assert.match(collapsedThinking, /cr-collapse--collapsed/)
assert.match(collapsedThinking, /class="cr-collapse__content" aria-hidden="true" inert=""/)
const expandedThinking = renderToStaticMarkup(<ThinkingBlock text="Visible reasoning" isFinished isLastContent renderMarkdown={text => <strong>{text}</strong>} />)
assert.match(expandedThinking, /Visible reasoning/)
assert.match(expandedThinking, /<strong>Visible reasoning<\/strong>/)
assert.doesNotMatch(expandedThinking, /cr-collapse--collapsed/)
const planTasks = Array.from({ length: 8 }, (_, index) => ({
id: `task-${index}`,
content: `Plan task ${index}`,
status: index < 5 ? 'completed' : index === 5 ? 'in_progress' : 'pending'
}))
assert.deepEqual(selectPlanTasks(planTasks, 3), {
visible: planTasks.slice(4, 7), hiddenCount: 5, currentIndex: 5, startIndex: 4
})
const successfulPlan = renderToStaticMarkup(
<PlanCreateBlock data={{ id: 'plan-1', name: 'Release plan', overview: 'Ship the release', todolist: planTasks }} status="success" maxDisplayTasks={3} onViewPlan={() => undefined} onDownloadPlan={() => undefined} />
)
assert.match(successfulPlan, /Release plan/)
assert.match(successfulPlan, /Plan task 5/)
assert.doesNotMatch(successfulPlan, /Current/, 'the captured plan list has no added Current label')
assert.match(successfulPlan, /\+ 5 更多/)
assert.match(successfulPlan, /Download plan/)
assert.doesNotMatch(successfulPlan, /Plan task 0/)
const failedPlan = renderToStaticMarkup(
<PlanCreateBlock data={{ id: 'plan-2', name: 'Broken plan' }} status="error" />
)
assert.match(failedPlan, /Failed/)
const activePlanUpdate = renderToStaticMarkup(
<PlanUpdateBlock data={{ name: 'Release plan', status: 'building' }} status="running" />
)
assert.match(activePlanUpdate, /Release plan confirmed/)
assert.match(activePlanUpdate, /cb-plan-update-spinner/)
const cancelledTask = renderToStaticMarkup(
<TaskBlock data={{ description: 'Run checks', subagentName: 'builder', finalResult: 'Stopped output', toolInfo: [{ name: 'execute_command', needApprove: true, toolCallId: 'inner-tool' }] }} status="cancelled" />
)
assert.match(cancelledTask, /任务 builder 子代理: Run checks/)
assert.match(cancelledTask, /任务执行已取消!/)
assert.match(cancelledTask, /data-status="cancelled"/)
assert.equal(getCapturedToolKind('Bash'), 'command')
assert.equal(getCapturedToolKind('bash'), 'command')
assert.equal(getCapturedToolKind('glob'), 'file-list')
assert.equal(getCapturedToolKind('grep'), 'file-list')
assert.equal(getCapturedToolKind('read'), 'read-file')
assert.equal(getCapturedToolKind('NotebookEdit'), 'write-file')
assert.equal(getCapturedToolKind('Grep'), 'file-list')
assert.equal(getCapturedToolKind('WebSearch'), 'web-search')
assert.equal(getCapturedToolKind('todo_write'), 'todo-write')
assert.equal(getCapturedToolKind('TodoWrite'), 'todo-write')
assert.equal(getCapturedToolKind('mcp__ardot__create_design'), 'mcp-display')
assert.equal(getCapturedToolKind('mcp__browser__tab_goto'), 'mcp-display')
assert.equal(getCapturedToolKind('mcp__custom__unknown'), undefined)
assert.equal(getCapturedToolKind('custom_tool'), undefined)
const commandRenderer = renderToStaticMarkup(
<ToolCallRenderer tool={{ id: 'cmd-1', name: 'Bash', status: 'success', args: { command: 'npm test' }, result: { result: { stdout: 'all checks passed', exitCode: 0 } } }} defaultExpanded />
)
assert.match(commandRenderer, /npm test/)
assert.match(commandRenderer, /all checks passed/)
assert.match(commandRenderer, /cr-tool-exec__output/)
assert.match(commandRenderer, /cr-tool-head__primary/)
assert.doesNotMatch(commandRenderer, /Exit code 0/)
assert.doesNotMatch(commandRenderer, /Full output/)
assert.doesNotMatch(commandRenderer, /Arguments/)
assert.equal(getCapturedToolKind('powershell'), 'command')
const runningCommandRenderer = renderToStaticMarkup(
<ToolCallRenderer tool={{ id: 'cmd-running', name: 'bash', status: 'running', args: {
command: 'ls src', description: 'List contents of src directory'
}, output: 'first output snapshot\nsecond output snapshot' }} defaultExpanded />
)
assert.match(runningCommandRenderer, /List contents of src directory/)
assert.match(runningCommandRenderer, /aria-expanded="false"/,
'a running command stays on its one-line summary even when the parent requests expanded details')
assert.match(runningCommandRenderer, /cr-tool-exp__content-shell" aria-hidden="true" inert=""/)
assert.equal((runningCommandRenderer.match(/class="cr-tool-exec__output"/g) ?? []).length, 1,
'the command output remains one replaceable details region')
assert.match(runningCommandRenderer, /first output snapshot\nsecond output snapshot/)
const runningGlobRenderer = renderToStaticMarkup(
<ToolCallRenderer tool={{ id: 'glob-running', name: 'glob', status: 'running', intent: 'Inspecting source layout', args: { path: 'src' }, output: 'src/index.ts\nsrc/main.ts' }} />
)
assert.match(runningGlobRenderer, /cr-tool-list-files__header/)
assert.match(runningGlobRenderer, /搜索中/)
assert.match(runningGlobRenderer, /title="src">Inspecting source layout<\/span>/)
assert.match(runningGlobRenderer, /cr-tool-head__secondary">src<\/span>/)
assert.match(runningGlobRenderer, /aria-expanded="false"/)
assert.match(runningGlobRenderer, /src\/index\.ts\nsrc\/main\.ts/)
const inferredRunningListRenderer = renderToStaticMarkup(
<ToolCallRenderer tool={{ id: 'list-no-intent', name: 'list_dir', status: 'running', args: { path: 'src' }, output: 'index.ts\nmain.ts' }} />
)
assert.match(inferredRunningListRenderer, /List contents of src directory/,
'a list_dir call without provider intent still gets a descriptive one-line action title')
assert.doesNotMatch(inferredRunningListRenderer, /cr-tool-head__secondary/,
'the inferred action title does not repeat the directory as secondary text')
assert.match(inferredRunningListRenderer, /aria-expanded="false"/,
'a running directory listing remains one collapsed, replaceable row')
const inferredRootListRenderer = renderToStaticMarkup(
<ToolCallRenderer tool={{ id: 'list-root-no-path', name: 'list_dir', status: 'success', args: {}, result: { files: [] } }} />
)
assert.match(inferredRootListRenderer, /List contents of the project directory/,
'an omitted list_dir path resolves to the documented workspace root')
const runningCommandWithIntent = renderToStaticMarkup(
<ToolCallRenderer tool={{ id: 'cmd-intent', name: 'bash', status: 'running', intent: 'List contents of src directory', args: { command: 'ls src' }, output: 'file-a.ts\nfile-b.ts' }} />
)
assert.match(runningCommandWithIntent, /List contents of src directory/)
assert.match(runningCommandWithIntent, /aria-expanded="false"/)
const emptySuccessfulCommand = renderToStaticMarkup(
<ToolCallRenderer tool={{ id: 'cmd-empty', name: 'Bash', status: 'success', args: { command: 'true' }, result: { stdout: '(empty)', stderr: '(empty)', exitCode: 0, message: 'Operation completed' } }} defaultExpanded />
)
assert.doesNotMatch(emptySuccessfulCommand, /cr-tool-exec__output/)
assert.doesNotMatch(emptySuccessfulCommand, /Command failed/)
assert.match(emptySuccessfulCommand, /cr-tool-exec__ok/)
assert.match(emptySuccessfulCommand, /运行成功/)
const commandOutputFallback = renderToStaticMarkup(
<ToolCallRenderer tool={{ id: 'cmd-output', name: 'Bash', status: 'success', args: { command: 'run task' }, output: completeOutput, result: { success: true, exitCode: 0 } }} defaultExpanded />
)
assert.match(commandOutputFallback, /LAST OUTPUT SENTINEL/)
assert.doesNotMatch(commandOutputFallback, /"success": true/)
assert.doesNotMatch(commandOutputFallback, /Full output/)
const failedCommandRenderer = renderToStaticMarkup(
<ToolCallRenderer tool={{
id: 'cmd-failed', name: 'Bash', status: 'failed', args: { command: 'run failing task' },
result: { result: { stdout: 'started', stderr: 'operation rejected', exitCode: 7 } }
}} />
)
assert.match(failedCommandRenderer, /started/)
assert.match(failedCommandRenderer, /operation rejected/)
assert.match(failedCommandRenderer, /执行失败 \(退出码: 7\)/)
assert.doesNotMatch(failedCommandRenderer, /Exit code 7/)
const readRenderer = renderToStaticMarkup(
<ToolCallRenderer tool={{ id: 'read-1', name: 'Read', status: 'executed', args: { file_path: 'src/main.ts' }, result: { content: 'line three\nline four' } }} defaultExpanded />
)
assert.match(readRenderer, /src\/main\.ts/)
assert.match(readRenderer, /class="cr-tool-head__status">已读取<\/span>/)
assert.match(readRenderer, /cr-tool-head__icon[^>]*><svg/)
assert.doesNotMatch(readRenderer, /line three/, 'source read card is header only; content remains in the file-open adapter')
const readIntentRenderer = renderToStaticMarkup(
<ToolCallRenderer tool={{ id: 'read-intent', name: 'read_file', status: 'running', intent: 'Reading project handover status', args: { path: 'docs/handover.md' } }} />
)
assert.match(readIntentRenderer, /Reading project handover status/)
assert.match(readIntentRenderer, /handover\.md/)
const inferredReadRenderer = renderToStaticMarkup(
<ToolCallRenderer tool={{ id: 'read-no-intent', name: 'read_file', status: 'success', args: { path: 'docs/README.md', offset: 10, limit: 5 }, result: { content: 'contents' } }} />
)
assert.match(inferredReadRenderer, /Read file docs\/README\.md \(lines 10-14\)/,
'a read_file call without provider intent still gets a descriptive action title')
const readOutputFallback = renderToStaticMarkup(
<ToolCallRenderer tool={{ id: 'read-output', name: 'Read', status: 'executed', args: { file_path: 'src/result.ts' }, result: { success: true }, output: 'complete file output sentinel' }} defaultExpanded />
)
assert.doesNotMatch(readOutputFallback, /complete file output sentinel/)
const openedReadTool = {
id: 'read-open', name: 'Read', status: 'executed' as const,
args: { file_path: 'src/open.ts' },
result: [{ type: 'text', text: 'line four\nline five' }]
}
assert.deepEqual(createReadFileOpenRequest(openedReadTool), {
toolCallId: 'read-open', toolName: 'Read', filePath: 'src/open.ts', startLine: undefined, endLine: undefined, content: 'line four\nline five'
})
const numberedReadTool = {
id: 'read-numbered-open', name: 'Read', status: 'executed' as const,
args: { file_path: 'src/numbered.ts' }, result: [{ type: 'text', text: '1→line one\n2→line two\n' }]
}
assert.equal(createReadFileOpenRequest(numberedReadTool).content, 'line one\nline two')
const partialReadTool = {
id: 'read-partial-open', name: 'Read', status: 'executed' as const,
args: { file_path: 'src/partial.ts', startLine: 10, endLine: 12 }, result: [{ type: 'text', text: 'partial chunk' }]
}
assert.equal(convertReadFile(partialReadTool).content, undefined)
assert.deepEqual(createReadFileOpenRequest(partialReadTool), {
toolCallId: 'read-partial-open', toolName: 'Read', filePath: 'src/partial.ts', startLine: 10, endLine: 12, content: undefined
})
const missingReadPathRenderer = renderToStaticMarkup(
<ToolCallRenderer tool={{ id: 'read-missing-path', name: 'Read', status: 'executed', args: {}, result: [{ type: 'text', text: 'read output without a path' }] }} />
)
assert.match(missingReadPathRenderer, /已读取/)
assert.doesNotMatch(missingReadPathRenderer, /read output without a path/)
const failedReadRenderer = renderToStaticMarkup(
<ToolCallRenderer tool={{ id: 'read-failed', name: 'Read', status: 'failed', args: { file_path: 'missing.ts' }, result: { result: { error: 'ENOENT: missing.ts' } } }} />
)
assert.match(failedReadRenderer, /cr-tool-head__icon--error/)
assert.doesNotMatch(failedReadRenderer, /ENOENT: missing\.ts/)
const numberedReadRenderer = renderToStaticMarkup(
<ToolCallRenderer tool={{ id: 'read-2', name: 'read_file', status: 'executed', args: {}, result: [{ type: 'text', text: 'line ' }, { type: 'text', text: 'joined' }] }} defaultExpanded />
)
assert.doesNotMatch(numberedReadRenderer, /line joined/)
assert.doesNotMatch(numberedReadRenderer, /line \njoined/)
const partialReadRenderer = renderToStaticMarkup(
<ToolCallRenderer tool={{ id: 'read-3', name: 'Read', status: 'executed', args: { offset: 10 }, result: [{ type: 'text', text: 'partial chunk' }] }} defaultExpanded />
)
assert.doesNotMatch(partialReadRenderer, /partial chunk/)
const todoWriteRenderer = renderToStaticMarkup(
<ToolCallRenderer
tool={{
id: 'todo-write-1', name: 'todo_write', status: 'executed', isLastContent: true,
args: { _rawInput: { merge: true, totalCount: 3, todos: [
{ id: 'done', content: 'Completed task', status: 'completed' },
{ id: 'active', content: 'Original active task', activeForm: 'Writing the active task', status: 'in_progress' },
{ id: 'pending', content: 'Pending task', status: 'pending' }
] } }
}}
/>
)
assert.match(todoWriteRenderer, /进度更新 1\/3/)
assert.match(todoWriteRenderer, /Completed task/)
assert.match(todoWriteRenderer, /Writing the active task/)
assert.match(todoWriteRenderer, /Pending task/)
assert.match(todoWriteRenderer, /cb-plan-task--completed/)
assert.match(todoWriteRenderer, /cb-plan-task--in_progress/)
assert.match(todoWriteRenderer, /cb-plan-task--pending/)
const todoStepsRenderer = renderToStaticMarkup(
<ToolCallRenderer tool={{ id: 'todo-write-steps', name: 'TodoWrite', status: 'running', args: { steps: [{ title: 'Compatibility step', status: 'doing' }] } }} defaultExpanded />
)
assert.match(todoStepsRenderer, /Compatibility step/)
assert.match(todoStepsRenderer, /cb-plan-task--in_progress/)
const fileListRenderer = renderToStaticMarkup(
<ToolCallRenderer tool={{ id: 'files-1', name: 'Glob', status: 'success', args: { pattern: 'src/**/*.ts' }, result: { result: { results: [{ path: 'src/main.ts' }, { path: 'src/util.ts' }] } } }} defaultExpanded />
)
assert.match(fileListRenderer, /src\/main\.ts/)
assert.match(fileListRenderer, /src\/util\.ts/)
assert.match(fileListRenderer, /搜索文件/)
assert.match(fileListRenderer, /M1\.2982 9\.8292/)
const listFileRequest = createListFileOpenRequest(
{ id: 'files-open', name: 'Grep', status: 'success' },
{ filePath: 'src/main.ts', fileName: 'main.ts', isDirectory: false, startLine: 12, endLine: 13, content: 'matched text' }
)
assert.deepEqual(listFileRequest, { toolCallId: 'files-open', toolName: 'Grep', filePath: 'src/main.ts', startLine: 12, endLine: 13 })
const markdownWebSearchRenderer = renderToStaticMarkup(
<ToolCallRenderer tool={{
id: 'web-1', name: 'WebSearch', status: 'success', args: {},
result: '# Search Results for "captured query"\n\n## 1. [First result](https://example.com/one)\n\n## 2. [Second result](https://example.com/two)'
}} defaultExpanded />
)
assert.match(markdownWebSearchRenderer, /captured query/)
assert.match(markdownWebSearchRenderer, /First result/)
assert.match(markdownWebSearchRenderer, /Second result/)
const structuredWebSearchRenderer = renderToStaticMarkup(
<ToolCallRenderer tool={{
id: 'web-2', name: 'web_search', status: 'success', args: { query: 'fallback query' }, result: { results: [{ title: 'Structured result', url: 'https://example.org' }] },
metaData: { _meta: { 'codebuddy.ai': { rawResponse: { type: 'web_search_tool_result', query: 'structured query', results: [{ title: 'Raw response result', url: 'https://example.net' }] } } } }
}} defaultExpanded />
)
assert.match(structuredWebSearchRenderer, /structured query/)
assert.match(structuredWebSearchRenderer, /Raw response result/)
assert.match(structuredWebSearchRenderer, /网页搜索/)
assert.match(structuredWebSearchRenderer, /M1\.37 6\.6667/)
assert.doesNotMatch(structuredWebSearchRenderer, /Structured result/)
const webFetchRenderer = renderToStaticMarkup(
<ToolCallRenderer tool={{
id: 'web-fetch-icon', name: 'web_fetch', status: 'success', args: { url: 'https://example.org/page' },
result: { result: { title: 'Example page', content: 'Fetched body' } }
}} />
)
assert.match(webFetchRenderer, /网页获取/)
assert.match(webFetchRenderer, /M1\.37 6\.6667/)
const mcpCallRenderer = renderToStaticMarkup(
<ToolCallRenderer tool={{
id: 'mcp-call-1', name: 'mcp_call_tool', status: 'success',
args: { serverName: 'browser', toolName: 'navigate', arguments: { url: 'https://example.com' } },
result: { result: { data: [{ type: 'text', text: 'navigation completed' }] } }
}} />
)
assert.match(mcpCallRenderer, /browser \(navigate\)/)
assert.match(mcpCallRenderer, /cr-tool-mcp__body/)
assert.match(mcpCallRenderer, /https:\/\/example\.com/)
assert.match(mcpCallRenderer, /navigation completed/)
assert.match(mcpCallRenderer, /运行成功/)
assert.match(mcpCallRenderer, /M2\.9742 -0\.1497/)
const mcpErrorRenderer = renderToStaticMarkup(
<ToolCallRenderer tool={{ id: 'mcp-call-error', name: 'mcp_call_tool', status: 'failed', result: { result: { error: 'server unavailable', isError: true } } }} />
)
assert.match(mcpErrorRenderer, /server unavailable/)
assert.match(mcpErrorRenderer, /cr-tool-head__icon--error/)
const mcpDisplayRenderer = renderToStaticMarkup(
<ToolCallRenderer tool={{
id: 'mcp-display-1', name: 'mcp__ardot__create_design', status: 'success', args: { name: 'Landing page' },
result: { result: { data: [{ type: 'text', text: 'design created' }] } }
}} />
)
assert.match(mcpDisplayRenderer, /cr-tool-display-list/)
assert.match(mcpDisplayRenderer, /create design/)
assert.match(mcpDisplayRenderer, /Landing page/)
assert.match(mcpDisplayRenderer, /design created/)
assert.match(mcpDisplayRenderer, /M2\.9742 -0\.1497/)
const editRenderer = renderToStaticMarkup(
<ToolCallRenderer tool={{ id: 'edit-1', name: 'Edit', status: 'success', args: { filePath: 'src/main.ts', edits: [{ old_str: 'before', new_str: 'after' }] } }} defaultExpanded onOpenFile={() => undefined} />
)
assert.match(editRenderer, /before/)
assert.match(editRenderer, /after/)
assert.match(editRenderer, /class="cr-tool-head__status">编辑<\/span>/)
assert.match(editRenderer, /1 处修改/)
assert.match(editRenderer, /role="link"/)
const replacementRenderer = renderToStaticMarkup(
<ToolCallRenderer tool={{ id: 'replace-1', name: 'replace_in_file', status: 'success', args: { filePath: 'src/main.ts', old_str: 'before', new_str: 'after' } }} defaultExpanded />
)
assert.match(replacementRenderer, /\+1/)
assert.match(replacementRenderer, /-1/)
const streamedWriteContent = Array.from({ length: 502 }, (_, index) => `line-${index}`).join('\n')
const streamedWriteRenderer = renderToStaticMarkup(
<ToolCallRenderer tool={{ id: 'write-stream-1', name: 'replace_in_file', status: 'running', isLastContent: true, args: { filePath: 'src/new.ts', content: streamedWriteContent } }} />
)
assert.match(streamedWriteRenderer, /修改中/)
assert.match(streamedWriteRenderer, /line-499/)
assert.match(streamedWriteRenderer, /… 省略 2 行/)
assert.doesNotMatch(streamedWriteRenderer, /line-501/)
assert.doesNotMatch(streamedWriteRenderer, /cr-tool-write__content--add/)
const creatingRenderer = renderToStaticMarkup(
<ToolCallRenderer tool={{ id: 'write-create-1', name: 'write_file', status: 'running', args: { filePath: 'src/created.ts', content: 'new content' } }} />
)
assert.match(creatingRenderer, /生成中/)
assert.doesNotMatch(creatingRenderer, /cr-tool-write__content--add/)
const createdRenderer = renderToStaticMarkup(
<ToolCallRenderer tool={{ id: 'write-create-done', name: 'write_file', status: 'success', args: { filePath: 'src/created.ts', content: 'new content' } }} defaultExpanded />
)
assert.match(createdRenderer, /cr-tool-write__content--add/)
const successfulWriteWithOutput = renderToStaticMarkup(
<ToolCallRenderer tool={{
id: 'write-success-output', name: 'write_file', status: 'success',
args: { filePath: 'src/success.ts', content: 'saved content' },
result: { result: { newContent: 'saved content' } }, output: '{"success":true}'
}} defaultExpanded />
)
assert.doesNotMatch(successfulWriteWithOutput, /role="alert"/)
assert.doesNotMatch(successfulWriteWithOutput, /\{"success":true\}/)
const failedWriteWithOutput = renderToStaticMarkup(
<ToolCallRenderer tool={{ id: 'write-failure-output', name: 'write_file', status: 'failed', output: 'write failed' }} />
)
assert.doesNotMatch(failedWriteWithOutput, /role="alert"/)
assert.doesNotMatch(failedWriteWithOutput, /write failed/)
assert.match(failedWriteWithOutput, /cr-tool-head__icon--error/)
const virtualWriteDiff = renderToStaticMarkup(
<SourceToolDiff oldCode="" newCode={Array.from({ length: 100 }, (_, index) => `line-${index}`).join('\n')} fileName="src/new.ts" />
)
assert.match(virtualWriteDiff, /cr-tool-diff--virtual/)
assert.ok((virtualWriteDiff.match(/cr-tool-diff__virtual-line/g) ?? []).length < 100)
const planRenderer = renderToStaticMarkup(
<ToolCallRenderer
tool={{
id: 'plan-3',
name: 'plan_create',
status: 'success',
result: {
result: {
data: JSON.stringify({
id: 'plan-3',
name: 'Created by tool',
status: 'prepare',
overview: 'Plan overview',
todolist: [{ id: 1, content: 'First task', status: 'completed' }]
})
}
}
}}
/>
)
assert.match(planRenderer, /Created by tool/)
assert.match(planRenderer, /First task/)
assert.doesNotMatch(planRenderer, /查看计划/)
const actionablePlan = renderToStaticMarkup(
<ToolCallRenderer
tool={{ id: 'plan-actions', name: 'plan_create', status: 'success', result: { result: { data: { id: 'plan-actions', name: 'Action plan', status: 'building', todolist: [{ id: 'todo-1', content: 'Clickable task', status: 'in_progress' }] } } } }}
onPlanTaskClick={() => undefined}
onPlanView={() => undefined}
onPlanBuild={() => undefined}
/>
)
assert.match(actionablePlan, /role="button"/)
assert.match(actionablePlan, /查看计划/)
assert.match(actionablePlan, />执行</)
const exitPlanRenderer = renderToStaticMarkup(
<ToolCallRenderer
tool={{
id: 'exit-plan-1',
name: 'exit_plan_mode',
status: 'pending',
isFinished: false,
metaData: { acp_request_permission: { planInfo: { plan: '# Release plan\nShip safely', planFilePath: 'plan.md' } } }
}}
onOpenFile={() => undefined}
/>
)
assert.match(exitPlanRenderer, /role="button"/)
assert.match(exitPlanRenderer, /Release plan/)
assert.match(exitPlanRenderer, /Ship safely/)
const fallbackRenderer = renderToStaticMarkup(
<ToolCallRenderer tool={{ id: 'custom-1', name: 'unregistered_tool', status: 'failed', args: { value: 'kept' } }} defaultExpanded />
)
assert.match(fallbackRenderer, /unregistered_tool/)
assert.match(fallbackRenderer, /kept/)
const sharedToolHeader = renderToStaticMarkup(
<SourceToolHeader primaryContent="Shared header" statusText="Running" isExecuting collapsible expanded onToggle={() => undefined} />
)
assert.match(sharedToolHeader, /class="cr-tool-head cr-tool-head--collapsible cr-tool-head--expanded"/)
assert.match(sharedToolHeader, /cr-tool-head__status--loading/)
assert.match(sharedToolHeader, /aria-expanded="true"/)
console.log('Conversation specialized renderer SSR regression checks passed')
+165
View File
@@ -0,0 +1,165 @@
import assert from 'node:assert/strict'
import { EventEmitter } from 'node:events'
import { mkdtemp, mkdir, readFile, writeFile, rm, symlink } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { createInteractionBroker, respondToInteraction } from '../src/main/interaction-broker'
import { executeConversationTool } from '../src/main/agent/conversation-tools'
import { runOmpTurn } from '../src/main/agent/omp-turn'
import { restoreWorkspaceFiles } from '../src/main/workspace-revert'
import { getRevertChanges } from '../src/shared/revert'
import { applyAssistantEvent } from '../src/shared/message-parts'
import type { ChatEvent, ChatMessage } from '../src/shared/types'
import type { ChatInteraction } from '../src/shared/conversation'
import type { OmpKernel } from '../src/main/omp'
import { assistantWireHistory } from '../src/main/agent/history'
async function main() {
const directory = await mkdtemp(join(tmpdir(), 'ycode-conversation-runtime-'))
const workspace = join(directory, 'workspace')
await mkdir(workspace)
try {
const events: ChatEvent[] = []
const emitted = new EventEmitter()
const emit = (event: ChatEvent) => { events.push(event); emitted.emit('event', event) }
const controller = new AbortController()
const broker = createInteractionBroker('run', controller.signal, emit)
const context = { runId: 'run', toolCallId: 'write', workspaceRoot: workspace, signal: controller.signal, interactions: broker, emit }
const nextInteraction = () => new Promise<ChatInteraction>((resolve) => {
const listener = (event: ChatEvent) => { if (event.type === 'interaction' && event.interaction.status === 'pending') { emitted.off('event', listener); resolve(event.interaction) } }
emitted.on('event', listener)
})
const pendingApproval = nextInteraction()
const write = executeConversationTool('write_file', { path: 'nested/new/file.txt', content: 'first' }, context)
const approval = await pendingApproval
assert.equal(approval.kind, 'approval')
await assert.rejects(readFile(join(workspace, 'nested/new/file.txt')), { code: 'ENOENT' })
assert.equal(respondToInteraction('another-run', approval.id, { kind: 'approval', optionId: 'allow-once' }, emit), false)
assert.throws(() => respondToInteraction('run', approval.id, { kind: 'approval', optionId: 'invented' }, emit), /无效/)
assert.equal(respondToInteraction('run', approval.id, { kind: 'approval', optionId: 'allow-once' }, emit), true)
assert.equal(respondToInteraction('run', approval.id, { kind: 'approval', optionId: 'allow-once' }, emit), false)
const written = await write
assert.equal(written.ok, true)
assert.equal(await readFile(join(workspace, 'nested/new/file.txt'), 'utf8'), 'first')
assert.equal((written.data as { oldContent: unknown }).oldContent, null)
const pendingEdit = nextInteraction()
const edit = executeConversationTool('edit_file', { path: 'nested/new/file.txt', oldText: 'first', newText: 'second' }, context)
const editApproval = await pendingEdit
await writeFile(join(workspace, 'nested/new/file.txt'), 'external change')
respondToInteraction('run', editApproval.id, { kind: 'approval', optionId: 'allow-once' }, emit)
assert.equal((await edit).ok, false)
assert.equal(await readFile(join(workspace, 'nested/new/file.txt'), 'utf8'), 'external change', 'stale approval cannot overwrite a newer file')
const outside = join(directory, 'outside')
await mkdir(outside)
await writeFile(join(outside, 'secret.txt'), 'private')
await symlink(outside, join(workspace, 'linked'), process.platform === 'win32' ? 'junction' : 'dir')
assert.equal((await executeConversationTool('read_file', { path: 'linked/secret.txt' }, context)).ok, false)
assert.equal((await executeConversationTool('write_file', { path: '../outside/deep/new.txt', content: 'x' }, context)).ok, false)
assert.equal((await executeConversationTool('execute_command', { command: 'echo SHOULD_NOT_RUN' }, context)).ok, false)
const fullController = new AbortController()
const childScript = join(directory, 'child.cjs')
const parentScript = join(directory, 'parent.cjs')
const pidFile = join(directory, 'child-pid.txt')
await writeFile(childScript, `require('node:fs').writeFileSync(${JSON.stringify(pidFile)},String(process.pid));setInterval(()=>{},1000)`)
await writeFile(parentScript, `require('node:child_process').spawn(process.execPath,[${JSON.stringify(childScript)}],{windowsHide:true,stdio:'ignore'});process.stdout.write('child started');setInterval(()=>{},1000)`)
const fullCommand = executeConversationTool('execute_command', { command: `"${process.execPath}" "${parentScript}"` }, { ...context, permissionMode: 'full-access', signal: fullController.signal })
let childPid = 0
for (let attempt = 0; attempt < 100; attempt++) {
childPid = Number(await readFile(pidFile, 'utf8').catch(() => '0'))
if (childPid) break
await new Promise((resolve) => setTimeout(resolve, 20))
}
fullController.abort()
const terminated = await fullCommand
assert.ok(childPid, 'command should have spawned its own child for cancellation verification')
assert.equal(terminated.ok, false)
assert.throws(() => process.kill(childPid, 0), 'cancellation must terminate the shell child tree')
const questionPending = nextInteraction()
const questionTask = executeConversationTool('ask_user_question', { questions: [
{ id: 'q1', question: 'Question one?', options: ['A', 'B'] },
{ id: 'q2', question: 'Question two?', options: ['C', 'D'], multiSelect: true }
] }, { ...context, toolCallId: 'question' })
const question = await questionPending
respondToInteraction('run', question.id, { kind: 'question', answer: { questions: [
{ id: 'q1', question: 'untrusted changed label', answers: [] },
{ id: 'q2', question: 'Question two?', answers: ['C', 'custom answer'] }
] } }, emit)
const answered = await questionTask
assert.equal(JSON.parse(answered.content).questions[0].question, 'Question one?')
assert.deepEqual((answered.metadata!.questionAnswer as { questions: unknown[] }).questions.length, 2)
const aborting = new AbortController()
const abortBroker = createInteractionBroker('abort', aborting.signal, emit)
const cancelled = abortBroker.question('cancel-question', 'Cancel?', [{ id: 'q', question: 'Cancel?', options: ['yes'] }])
const abortedAssertion = assert.rejects(cancelled, { name: 'AbortError' })
aborting.abort()
await abortedAssertion
assert.ok(events.some((event) => event.type === 'interaction' && event.interaction.status === 'cancelled'))
let message: ChatMessage = { id: 'assistant', role: 'assistant', content: '', createdAt: 0, status: 'streaming' }
const planEmit = (event: ChatEvent) => { message = applyAssistantEvent(message, event) }
const firstPlan = await executeConversationTool('todo_write', { todos: [{ id: '1', content: 'step', status: 'in_progress' }] }, { ...context, emit: planEmit })
const lastPlan = await executeConversationTool('todo_write', { todos: [{ id: '1', content: 'step', status: 'completed' }] }, { ...context, emit: planEmit })
assert.equal(message.parts, undefined, 'TodoWrite produces a specialized tool result rather than a duplicate plan part')
for (const result of [firstPlan, lastPlan]) planEmit({ runId: 'run', type: 'part', part: { type: 'plan', plan: result.data as import('../src/shared/conversation').PlanContent } })
assert.equal(message.parts?.length, 1, 'plan updates replace the same plan without duplicated cards')
assert.equal(message.parts?.[0].type === 'plan' && message.parts[0].plan.steps[0].status, 'completed')
message = applyAssistantEvent(message, { runId: 'run', type: 'done', content: 'complete', tools: [{ id: 'final-only', name: 'read_file', args: {}, argsRaw: '{}', status: 'ok' }] })
assert.deepEqual(message.parts?.map((part) => part.type), ['text', 'plan', 'tool'])
const exchanges = assistantWireHistory({ id: 'history', role: 'assistant', content: 'beforeafter', createdAt: 0,
parts: [{ type: 'text', text: 'before' }, { type: 'tool', toolId: 'read' }, { type: 'text', text: 'after' }],
tools: [{ id: 'read', name: 'read_file', args: {}, argsRaw: '{}', status: 'ok', output: 'complete tool output' }] })
assert.deepEqual(exchanges.map((entry) => entry.role), ['assistant', 'tool', 'assistant'])
assert.equal(exchanges[0].content, 'before')
assert.equal(exchanges[1].content, 'complete tool output')
assert.equal(exchanges[2].content, 'after')
await writeFile(join(workspace, 'a.txt'), 'new A')
await writeFile(join(workspace, 'b.txt'), 'new B')
const history: ChatMessage[] = [{ id: 'user', role: 'user', content: 'change', createdAt: 0 }, { id: 'assistant', role: 'assistant', content: 'done', createdAt: 1, status: 'done', tools: [
{ id: 'a', name: 'edit_file', args: {}, argsRaw: '{}', status: 'ok', result: { path: join(workspace, 'a.txt'), oldContent: 'old A', newContent: 'new A' } },
{ id: 'b', name: 'write_file', args: {}, argsRaw: '{}', status: 'ok', result: { path: join(workspace, 'b.txt'), oldContent: null, newContent: 'new B' } }
] }]
const changes = getRevertChanges(history, 'user')
await writeFile(join(workspace, 'b.txt'), 'manual B')
await assert.rejects(restoreWorkspaceFiles(workspace, changes), /外部修改/)
assert.equal(await readFile(join(workspace, 'a.txt'), 'utf8'), 'new A', 'all conflicts are checked before the first write')
await writeFile(join(workspace, 'b.txt'), 'new B')
const undo = await restoreWorkspaceFiles(workspace, changes)
assert.equal(await readFile(join(workspace, 'a.txt'), 'utf8'), 'old A')
await assert.rejects(readFile(join(workspace, 'b.txt')), { code: 'ENOENT' })
await undo()
assert.equal(await readFile(join(workspace, 'b.txt'), 'utf8'), 'new B')
class HostKernel extends EventEmitter {
definitions: unknown[] = []
async setHostTools(tools: unknown[]) { this.definitions = tools; return { success: true } }
async prompt() {
queueMicrotask(() => {
this.emit('event', { type: 'tool_execution_start', toolCallId: 'native-read', toolName: 'read_file', args: { path: 'a.txt' } })
this.emit('event', { type: 'host_tool_call', id: 'host-request', toolCallId: 'native-read', toolName: 'read_file', arguments: { path: 'a.txt' } })
})
return { success: true }
}
async respondToHostTool(id: string, content: string, isError: boolean) {
assert.equal(id, 'host-request')
assert.equal(isError, false)
assert.match(content, /new A/)
this.emit('event', { type: 'tool_execution_end', toolCallId: 'native-read', result: { content: [{ type: 'text', text: content }] } })
this.emit('event', { type: 'message_update', assistantMessageEvent: { type: 'text_delta', delta: 'Native host complete' } })
this.emit('event', { type: 'agent_end' })
}
async abort() { return { success: true } }
}
const hostKernel = new HostKernel()
const native = await runOmpTurn({ runId: 'native', kernel: hostKernel as unknown as OmpKernel, workspaceRoot: workspace, userText: 'read', signal: controller.signal, emit })
assert.equal(native.content, 'Native host complete')
assert.equal(hostKernel.definitions.length, 8)
assert.equal(native.tools[0].status, 'ok')
assert.equal((native.tools[0].result as { path: string }).path, join(workspace, 'a.txt'))
assert.equal(hostKernel.listenerCount('event'), 0)
console.log('conversation-runtime-regression: approval, questions, cancellation, real file writes/revert, path constraints, plan updates and native host adapter passed')
} finally { await rm(directory, { recursive: true, force: true }) }
}
void main().catch((error) => { console.error(error); process.exitCode = 1 })
+114
View File
@@ -0,0 +1,114 @@
/**
* 端到端验证「取消」链路:发起长回合 → 中途点停止 → 校验状态与内核行为。
*
* 用法:node scripts/e2e-cancel.mjs <port>
*/
const port = Number(process.argv[2] ?? 9335)
async function main() {
const list = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json()
const page = list.filter((t) => t.type === 'page')[0]
if (!page) throw new Error('未找到页面')
const ws = new WebSocket(page.webSocketDebuggerUrl)
await new Promise((r, j) => {
const t = setTimeout(() => j(new Error('连接超时')), 8000)
ws.addEventListener('open', () => {
clearTimeout(t)
r()
})
ws.addEventListener('error', () => {
clearTimeout(t)
j(new Error('连接失败'))
})
})
const send = (method, params = {}) =>
new Promise((res, rej) => {
const id = Math.floor(Math.random() * 1e9)
const timer = setTimeout(() => rej(new Error(`超时 ${method}`)), 25000)
const h = (ev) => {
const m = JSON.parse(ev.data)
if (m.id === id) {
clearTimeout(timer)
ws.removeEventListener('message', h)
res(m.result)
}
}
ws.addEventListener('message', h)
ws.send(JSON.stringify({ id, method, params }))
})
const evaluate = (expression) => send('Runtime.evaluate', { expression, returnByValue: true })
// 1) 发起一个长回合
const fill = `(() => {
const el = document.querySelector('.composer__input');
const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
setter.call(el, '写一篇 4000 字的软件架构设计文章,分十个章节,每章都要有完整内容。');
el.dispatchEvent(new Event('input', { bubbles: true }));
return 'OK';
})()`
await evaluate(fill)
const clicked = await evaluate(`(() => {
const btn = document.querySelector('.btn-send');
if (!btn || btn.disabled) return 'FAIL';
btn.click();
return 'SENT';
})()`)
console.log('发送:', clicked.result.value)
if (clicked.result.value !== 'SENT') process.exit(1)
// 2) 等出现停止按钮(说明回合在跑)
let runningSeen = false
for (let i = 0; i < 40; i++) {
await new Promise((r) => setTimeout(r, 500))
const probe = await evaluate(`JSON.stringify({ stop: !!document.querySelector('.btn-stop') })`)
if (JSON.parse(probe.result.value).stop) {
runningSeen = true
break
}
}
console.log('回合进行中:', runningSeen)
if (!runningSeen) {
console.log('⚠️ 未进入运行态,无法验证取消')
ws.close()
process.exit(1)
}
// 3) 让它跑一小会儿再取消
await new Promise((r) => setTimeout(r, 3000))
const cancel = await evaluate(`(() => {
const btn = document.querySelector('.btn-stop');
if (!btn) return 'NO_STOP';
btn.click();
return 'CANCELLED';
})()`)
console.log('点击停止:', cancel.result.value)
// 4) 等待终态
let final = null
for (let i = 0; i < 40; i++) {
await new Promise((r) => setTimeout(r, 700))
const probe = await evaluate(`JSON.stringify({
stopGone: !document.querySelector('.btn-stop'),
sendBack: !!document.querySelector('.btn-send'),
status: [...document.querySelectorAll('.msg--assistant .msg__status')].map((n) => n.textContent),
contentLen: [...document.querySelectorAll('.msg--assistant .msg__content')].pop()?.textContent.length ?? 0
})`)
final = JSON.parse(probe.result.value)
if (final.stopGone && final.sendBack) break
}
console.log('终态:', JSON.stringify(final))
const ok = final.sendBack && final.stopGone
console.log(ok ? '\n✅ 取消链路:停止按钮消失、发送按钮恢复' : '\n❌ 取消链路未恢复正常')
ws.close()
process.exit(ok ? 0 : 1)
}
main().catch((e) => {
console.error('失败:', e.message)
process.exit(1)
})
+105
View File
@@ -0,0 +1,105 @@
/**
* 端到端验证:通过 UI 发送一条消息,等待回复,报告结果。
*
* 用法:node scripts/e2e-send.mjs <port> <消息> [超时秒]
*/
const port = Number(process.argv[2] ?? 9335)
const message = process.argv[3] ?? '列出工作目录里有哪些文件'
const timeoutMs = Number(process.argv[4] ?? 180) * 1000
async function main() {
const list = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json()
const page = list.filter((t) => t.type === 'page')[0]
if (!page) throw new Error('未找到页面')
const ws = new WebSocket(page.webSocketDebuggerUrl)
await new Promise((r, j) => {
const t = setTimeout(() => j(new Error('连接超时')), 8000)
ws.addEventListener('open', () => {
clearTimeout(t)
r()
})
ws.addEventListener('error', () => {
clearTimeout(t)
j(new Error('连接失败'))
})
})
const send = (method, params = {}) =>
new Promise((res, rej) => {
const id = Math.floor(Math.random() * 1e9)
const timer = setTimeout(() => rej(new Error(`超时 ${method}`)), 25000)
const h = (ev) => {
const m = JSON.parse(ev.data)
if (m.id === id) {
clearTimeout(timer)
ws.removeEventListener('message', h)
res(m.result)
}
}
ws.addEventListener('message', h)
ws.send(JSON.stringify({ id, method, params }))
})
const evaluate = (expression) => send('Runtime.evaluate', { expression, returnByValue: true })
// 1) 填入消息(React 受控组件需用原生 setter)
const fill = `(() => {
const el = document.querySelector('.composer__input');
if (!el) return 'NO_INPUT';
const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
setter.call(el, ${JSON.stringify(message)});
el.dispatchEvent(new Event('input', { bubbles: true }));
return 'OK';
})()`
const filled = await evaluate(fill)
console.log('填入:', filled.result.value)
// 2) 点击发送
const click = `(() => {
const btn = document.querySelector('.btn-send');
if (!btn) return 'NO_BUTTON';
if (btn.disabled) return 'DISABLED';
btn.click();
return 'SENT';
})()`
const clicked = await evaluate(click)
console.log('发送:', clicked.result.value)
if (clicked.result.value !== 'SENT') {
ws.close()
process.exit(1)
}
// 3) 轮询等待回复完成
const started = Date.now()
let last = ''
while (Date.now() - started < timeoutMs) {
await new Promise((r) => setTimeout(r, 1200))
const probe = await evaluate(`(() => {
const msgs = [...document.querySelectorAll('.msg--assistant')];
const lastMsg = msgs[msgs.length - 1];
const status = lastMsg ? lastMsg.querySelector('.msg__status')?.textContent ?? '' : '';
return JSON.stringify({
count: msgs.length,
content: lastMsg ? lastMsg.querySelector('.msg__content')?.textContent.slice(0, 400) : '',
status
});
})()`)
const state = JSON.parse(probe.result.value)
last = state.content
if (state.status && /已完成|失败|已取消|error|done/i.test(state.status)) {
console.log('状态:', state.status)
console.log('回复:', state.content)
ws.close()
return
}
}
console.log('超时,最后内容:', last)
ws.close()
}
main().catch((e) => {
console.error('失败:', e.message)
process.exit(1)
})
+105
View File
@@ -0,0 +1,105 @@
# Enable WorkBuddy CDP and capture UI styles.
#
# Purpose: make the real WorkBuddy expose DevTools Protocol on port 9223, then
# capture element rects + computed styles for the sidebar, conversation
# area and input area, as the basis for YCode visual alignment.
#
# NOTE: WorkBuddy has a single-instance lock, so it must be fully exited and
# restarted with the env var set. Running this script therefore CLOSES the
# current WorkBuddy window (including any conversation in progress).
#
# Usage (run in a PowerShell window OUTSIDE WorkBuddy):
# powershell -NoProfile -ExecutionPolicy Bypass -File C:\Users\Administrator\code\YCode\scripts\enable-wb-cdp.ps1
#
# This file is intentionally ASCII-only: Windows PowerShell 5.1 reads .ps1 as
# ANSI, so non-ASCII text would be corrupted and break the parser.
$ErrorActionPreference = 'Stop'
$WorkBuddyExe = Join-Path $env:LOCALAPPDATA 'Programs\WorkBuddy\WorkBuddy.exe'
$ProjectRoot = 'C:\Users\Administrator\code\YCode'
$DebugPort = 9223
# Captures contain real conversation content, so they are kept in a private
# user directory instead of the repository.
$OutDir = Join-Path $env:USERPROFILE '.ycode\captures'
Write-Host '=== YCode: enable WorkBuddy CDP and capture ===' -ForegroundColor Cyan
if (-not (Test-Path $WorkBuddyExe)) {
Write-Host "[FAIL] WorkBuddy not found: $WorkBuddyExe" -ForegroundColor Red
exit 1
}
# ---------- 1. stop existing instances ----------
Write-Host '[1/5] Stopping running WorkBuddy processes...'
Get-Process -Name WorkBuddy -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 4
$left = Get-Process -Name WorkBuddy -ErrorAction SilentlyContinue
if ($left) {
Write-Host "[WARN] still alive: $($left.Id -join ',') ; waiting 4s more..." -ForegroundColor Yellow
Start-Sleep -Seconds 4
}
# ---------- 2. launch with debug port ----------
Write-Host '[2/5] Starting with WORKBUDDY_REMOTE_DEBUGGING_PORT...'
$previous = $env:WORKBUDDY_REMOTE_DEBUGGING_PORT
try {
$env:WORKBUDDY_REMOTE_DEBUGGING_PORT = "$DebugPort"
Start-Process -FilePath $WorkBuddyExe
} finally {
if ($null -eq $previous) {
Remove-Item Env:WORKBUDDY_REMOTE_DEBUGGING_PORT -ErrorAction SilentlyContinue
} else {
$env:WORKBUDDY_REMOTE_DEBUGGING_PORT = $previous
}
}
# ---------- 3. wait for the debug port ----------
Write-Host '[3/5] Waiting for debug port (up to 90s)...'
$ready = $false
for ($i = 1; $i -le 45; $i++) {
Start-Sleep -Seconds 2
try {
$null = Invoke-RestMethod "http://127.0.0.1:$DebugPort/json/version" -TimeoutSec 3
$ready = $true
break
} catch {
if (($i % 5) -eq 0) {
$waited = $i * 2
Write-Host " ...waited $waited s"
}
}
}
if (-not $ready) {
Write-Host '[FAIL] Debug port did not open. WorkBuddy is running normally, but without CDP.' -ForegroundColor Red
Write-Host 'Possible causes: the switch was removed in this version, or the port is taken.' -ForegroundColor Yellow
Write-Host 'Please send this window output back to the assistant.' -ForegroundColor Yellow
exit 2
}
Write-Host ' Port is ready.' -ForegroundColor Green
# ---------- 4. capture ----------
Write-Host '[4/5] Capturing UI styles...'
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$target = Join-Path $OutDir $stamp
New-Item -ItemType Directory -Force -Path $target | Out-Null
Push-Location $ProjectRoot
try {
node scripts\wb-capture.mjs $DebugPort $target
} finally {
Pop-Location
}
# ---------- 5. summary ----------
Write-Host '[5/5] Done.'
Get-ChildItem -LiteralPath $target | Select-Object Name, Length | Format-Table -AutoSize
Write-Host ''
Write-Host 'Output directory:' -NoNewline
Write-Host " $target" -ForegroundColor Green
Write-Host ''
Write-Host 'Next steps:' -ForegroundColor Cyan
Write-Host ' 1) Go back to WorkBuddy, reopen the conversation, and send the directory path above to the assistant.'
Write-Host ' 2) To disable CDP later: exit WorkBuddy, then start it again WITHOUT the env var.'
+115
View File
@@ -0,0 +1,115 @@
import assert from 'node:assert/strict'
import { useChatStore } from '../src/renderer/src/stores/chat'
import { clearExpertInputPrefill, expertInputUpdate } from '../src/renderer/src/capabilities/expert-input'
const events: Array<{ owner: string; prompt: string; explicit: boolean }> = []
const writes: Array<{ owner: string; expertId: string | null }> = []
const calls: string[] = []
const gates = new Map<string, ReturnType<typeof deferred<any>>>()
let failWrite = false
function deferred<T>() { let resolve!: (value: T) => void, reject!: (error: Error) => void; const promise = new Promise<T>((yes, no) => { resolve = yes; reject = no }); return { promise, resolve, reject } }
function activation(id: string) { return { id, name: id, expertType: 'agent' as const, runtimeIdentity: 'expert-v2:' + id, defaultInitPrompt: { zh: '默认提问:' + id } } }
const sessions = [
{ id: 'one', title: 'one', createdAt: 1, updatedAt: 1, workspaceId: 'space-one', cwd: 'C:/fixture', modelId: 'custom-model', expertId: 'old-expert' },
{ id: 'two', title: 'two', createdAt: 2, updatedAt: 2 }
]
Object.defineProperty(globalThis, 'CustomEvent', { configurable: true, value: class { type: string; detail: unknown; constructor(type: string, options: { detail: unknown }) { this.type = type; this.detail = options.detail } } })
Object.defineProperty(globalThis, 'window', { configurable: true, value: {
dispatchEvent(event: { type: string; detail: any }) { if (event.type === 'ycode:expert-input') events.push(event.detail); return true },
ycode: { experts: { activate: async (id: string) => { calls.push(id); return gates.get(id)?.promise ?? activation(id) } },
session: { messages: async () => [], updateContext: async (owner: string, patch: any) => { if (failWrite) throw Error('disk unavailable'); writes.push({ owner, expertId: patch.expertId }); return { ...sessions.find(session => session.id === owner), ...patch } } } }
} })
function reset(owner: string | null = null) {
useChatStore.getState().startTask({ cwd: 'C:/draft', workspaceId: 'draft-space', modelId: 'draft-model' })
useChatStore.setState({ sessions: structuredClone(sessions), currentSessionId: owner, messages: [], sessionMessages: {}, running: null, sending: false, runs: {}, sendingSessions: [] })
events.length = 0; writes.length = 0; calls.length = 0; gates.clear(); failWrite = false
}
let checks = 0
function eq(actual: unknown, expected: unknown, label: string) { assert.deepEqual(actual, expected, label); checks++ }
const empty = { text: '', attachments: [] as unknown[] }
eq(expertInputUpdate(empty, 'A').text, 'A', 'empty draft gets default')
eq(expertInputUpdate({ ...empty, text: 'user question' }, 'A').text, 'user question', 'user text survives')
eq(expertInputUpdate({ ...empty, text: 'A', autoFilledPrompt: 'A' }, 'B').text, 'B', 'unchanged expert default replaced')
eq(expertInputUpdate({ ...empty, text: 'A edited', autoFilledPrompt: 'A' }, 'B').text, 'A edited', 'edited default survives')
eq(expertInputUpdate({ ...empty, attachments: ['image'] }, 'A').text, '', 'non-text-only attachment survives without autofill')
eq(expertInputUpdate({ ...empty, resources: ['file'] }, 'A').text, '', 'non-text-only reference survives without autofill')
eq(expertInputUpdate({ ...empty, skills: ['skill'] }, 'A').text, '', 'manual skill-only input survives')
eq(expertInputUpdate({ ...empty, text: 'A', autoFilledPrompt: 'A', attachments: ['image'] }, 'B').attachments, ['image'], 'replacing default keeps attachments')
eq(expertInputUpdate({ ...empty, text: 'edit', editingId: 'old-message' }, 'A').text, 'edit', 'editing message preserved')
eq(expertInputUpdate({ ...empty, text: 'user', attachments: ['image'] }, 'explicit', true).text, 'explicit', 'explicit quick prompt adopted')
eq(expertInputUpdate({ ...empty, text: 'A', autoFilledPrompt: 'A' }, '').text, 'A', 'expert without default keeps input')
eq(expertInputUpdate({ ...empty, text: 'A', autoFilledPrompt: 'A' }, '', false, true).text, '', 'conversation expert without default clears old prefill')
eq(expertInputUpdate({ ...empty, text: 'user edited', autoFilledPrompt: 'A' }, '', false, true).text, 'user edited', 'conversation expert without default preserves user text')
eq(clearExpertInputPrefill({ ...empty, text: 'A', autoFilledPrompt: 'A' }).text, '', 'conversation clear removes untouched prefill')
eq(clearExpertInputPrefill({ ...empty, text: 'A edited', autoFilledPrompt: 'A' }).text, 'A edited', 'conversation clear preserves edited text')
reset()
const draft = useChatStore.getState().draftId
await useChatStore.getState().selectExpert('cpp')
eq(useChatStore.getState().draftId, draft, 'picker binds same unsent owner')
eq(useChatStore.getState().draftContext, { cwd: 'C:/draft', workspaceId: 'draft-space', modelId: 'draft-model', expertId: 'cpp', agentMode: 'craft' }, 'model and workspace preserved and expert resumes craft')
eq(events, [{ owner: `new-${draft}`, prompt: '默认提问:cpp', explicit: false }], 'single validated activation supplies default')
eq(writes.length, 0, 'no persisted empty conversation created')
await useChatStore.getState().selectExpert('cpp')
eq(calls.length, 2, 'explicit repeat refreshes activation like source provider')
await useChatStore.getState().selectExpert(null)
eq(useChatStore.getState().draftContext.expertId, null, 'remove pending expert')
eq(events.length, 2, 'homepage remove never rewrites input')
reset('one')
await useChatStore.getState().summonExpert('pm')
eq(writes, [{ owner: 'one', expertId: 'pm' }], 'market return binds existing conversation')
eq(useChatStore.getState().currentSessionId, 'one', 'return retains conversation')
eq(events.length, 1, 'market return offers default with user input protection in composer')
eq(useChatStore.getState().sessions[0].modelId, 'custom-model', 'conversation model unchanged')
await useChatStore.getState().selectExpert('ui')
eq(events.at(-1)?.owner, 'one', 'picker targets same conversation')
eq(events.at(-1)?.prompt, '默认提问:ui', 'picker default gated by current input in composer')
reset('one')
await useChatStore.getState().summonExpert('algo', { newTask: true })
eq(useChatStore.getState().currentSessionId, null, 'standalone market summon prepares homepage')
eq(useChatStore.getState().draftContext, { workspaceId: 'space-one', cwd: 'C:/fixture', modelId: 'custom-model', expertId: 'algo', agentMode: 'craft' }, 'new expert task inherits model and space and uses craft')
eq(writes.length, 0, 'standalone summon leaves old session unchanged')
eq(events.at(-1)?.owner, `new-${useChatStore.getState().draftId}`, 'default follows new owner immediately')
reset('one'); failWrite = true
await assert.rejects(useChatStore.getState().selectExpert('bad'), /disk unavailable/); checks++
eq(useChatStore.getState().sessions[0].expertId, 'old-expert', 'failed persist retains prior expert')
eq(events.length, 0, 'failed persist leaves draft untouched')
reset(); const bad = deferred<any>(); gates.set('bad', bad)
const rejected = useChatStore.getState().selectExpert('bad'); bad.reject(Error('missing dependency'))
await assert.rejects(rejected, /missing dependency/); checks++
eq(useChatStore.getState().draftContext.expertId, undefined, 'failed activation does not bind')
eq(events.length, 0, 'failed activation never inserts default')
reset(); const slow = deferred<any>(); gates.set('slow', slow)
const stale = useChatStore.getState().selectExpert('slow')
await useChatStore.getState().selectExpert('latest')
slow.resolve(activation('slow'))
await assert.rejects(stale, /选择已切换/); checks++
eq(useChatStore.getState().draftContext.expertId, 'latest', 'late activation cannot overwrite latest')
eq(events.map(event => event.prompt), ['默认提问:latest'], 'late activation cannot overwrite text')
reset(); const shared = deferred<any>(); gates.set('same', shared)
const first = useChatStore.getState().selectExpert('same'), second = useChatStore.getState().selectExpert('same')
const obsolete = assert.rejects(first, /选择已切换/)
shared.resolve(activation('same')); await Promise.all([obsolete, second]); checks++
eq(calls, ['same'], 'concurrent same expert shares activation')
eq(events.length, 1, 'only newest concurrent selection publishes')
reset('one'); const late = deferred<any>(); gates.set('late', late)
const departed = useChatStore.getState().selectExpert('late')
await useChatStore.getState().selectSession('two'); late.resolve(activation('late'))
await assert.rejects(departed, /任务或专家选择已切换/); checks++
eq(writes.length, 0, 'changing conversation prevents stale bind')
eq(events.length, 0, 'changing conversation prevents stale prompt')
reset('one'); useChatStore.setState({ running: { runId: 'running', assistantId: 'assistant', sessionId: 'one' } })
await assert.rejects(useChatStore.getState().selectExpert('next'), /先停止/); checks++
eq(calls.length, 0, 'running conversation switch rejected before activation')
await useChatStore.getState().summonExpert('next', { newTask: true })
eq(useChatStore.getState().draftContext.expertId, 'next', 'independent new task may be prepared during another turn')
console.log(JSON.stringify({ result: 'PASS', checks, scope: 'production Zustand expert state and composer input policy with controlled IPC; no model requests' }))
+30
View File
@@ -0,0 +1,30 @@
// Isolated YCode host fixtures, not claims about WorkBuddy runtime screenshots.
const now = Date.now()
const tool = (id, name, args, result) => ({ id, name, args, argsRaw: JSON.stringify(args), status: 'ok', result, output: JSON.stringify(result) })
const manyLines = Array.from({ length: 300 }, (_, index) => `export const value${index} = ${index}`).join('\n')
const writes = [
tool('source-edit', 'edit_file', { path: 'src/example.ts', oldText: '1', newText: '2' }, { path: 'src/example.ts', oldContent: 'export const value = 1\n', newContent: 'export const value = 2\n' }),
tool('source-create', 'write_file', { path: 'new.txt', content: 'created\n' }, { path: 'new.txt', oldContent: null, newContent: 'created\n' }),
tool('source-empty', 'edit_file', { path: 'empty.txt', oldText: 'removed', newText: '' }, { path: 'empty.txt', oldContent: 'removed\n', newContent: '' }),
tool('source-many', 'write_file', { path: 'many.ts', content: manyLines }, { path: 'many.ts', oldContent: null, newContent: manyLines })
]
const artifact = (id, name, preview) => ({ type: 'artifact', artifact: { id, name, path: preview.uri || id, preview } })
export const artifactSourceFixture = {
sessions: [{ id: 'artifact-source', title: '产物源码分支验证', createdAt: now, updatedAt: now }],
messages: { 'artifact-source': [
{ id: 'user', role: 'user', content: '验证完整 Diff、异步 Markdown、HTML 和原生下载。', status: 'done', createdAt: now },
{ id: 'assistant', role: 'assistant', content: '', status: 'done', createdAt: now + 1, tools: writes, parts: [
...writes.flatMap(item => [{ type: 'text', text: `**${item.id}**` }, { type: 'tool', toolId: item.id }]),
artifact('custom', '异步 Markdown', { id: 'custom', type: 'custom-artifact', uri: 'notes/readme.md' }),
artifact('cached', '缓存 Markdown', { id: 'cached', type: 'custom-artifact', uri: 'missing.md', content: '# 缓存内容\n\n没有读取磁盘。' }),
artifact('slow', '慢速 Markdown', { id: 'slow', type: 'custom-artifact', uri: 'slow.md' }),
artifact('html', 'HTML 预览', { id: 'html', type: 'media-artifact', category: 'artifact', url: 'preview.html', contentType: 'code' }),
artifact('missing', '读取失败', { id: 'missing', type: 'custom-artifact', uri: 'missing.md' }),
artifact('timeout', 'Diff 超时', { id: 'timeout', type: 'file-changes', title: 'large.txt', fileChangeInfo: { uri: 'large.txt', diffStatus: 'timeout' } }),
{ type: 'plan', plan: { id: 'plan', title: '计划下载', steps: [{ id: '1', text: '验证 Markdown 保存', status: 'pending' }] } }
] }
] },
files: { 'src/example.ts': 'export const value = 2\n', 'new.txt': 'created\n', 'empty.txt': '', 'many.ts': manyLines,
'notes/readme.md': '# 异步加载成功\n\n真实调用文件读取桥。', 'slow.md': '# 过期读取不能覆盖新预览', 'preview.html': '<!doctype html><html><body><h1>HTML 预览成功</h1></body></html>' },
delays: { 'slow.md': 300 }
}
+49
View File
@@ -0,0 +1,49 @@
// 仅供离线 UI 验证;不进入产品数据,不请求模型。
const output = Array.from({ length: 90 }, (_, i) => `src/example-${i + 1}.ts`).join('\n') + '\nOUTPUT_END_完整输出'
const before = '我先检查工作目录中的文件,再给出结果。\n'
const after = [
'# 检查结果',
'',
'已读取 **90 个文件**。下面是正文、表格和代码块的渲染样本。',
'这一行用于检查普通换行。',
'',
'## 状态清单',
'',
'- [x] 列出工作目录',
'- [ ] 核对变更',
'- 保留嵌套列表',
' - 第二层内容',
'',
'| 模块 | 状态 | 说明 |',
'| :--- | :---: | ---: |',
'| 对话渲染 | 已完成 | 支持 Markdown |',
'| 工具过程 | 已完成 | 保留完整输出 |',
'',
'```typescript',
'',
'const message: string = "你好,YCode"',
'console.log(message)',
'',
'```',
'',
'> 工具发生在这段正文之前,重开会话后也应保持这个顺序。',
'',
'访问 https://example.com/docs;~~旧的内容~~,以及行内 `code`。',
'',
'<script>window.__unsafeExecuted = true</script>',
'',
'未标注语言的代码:',
'',
'```',
'plain text',
'```'
].join('\n')
const now = Date.now()
const tool = { id: 'fixture-read', name: 'read', args: { path: 'src' }, argsRaw: '{"path":"src"}', status: 'ok', summary: output.slice(0, 600), output, durationMs: 123 }
export const chatParityFixture = {
sessions: [{ id: 'parity', title: '对话渲染与工具过程验证', createdAt: now, updatedAt: now }],
messages: { parity: [
{ id: 'user-fixture', role: 'user', content: '检查文件并用表格、代码块汇报。', createdAt: now, status: 'done' },
{ id: 'assistant-fixture', role: 'assistant', content: before + after, tools: [tool], parts: [{ type: 'text', text: before }, { type: 'tool', toolId: tool.id }, { type: 'text', text: after }], createdAt: now, status: 'done' }
] }
}
@@ -0,0 +1,60 @@
// Deterministic UI data. Native/OMP execution is verified separately by the runtime regressions.
const now = Date.now()
const message = (id, role, content, extra = {}) => ({ id, role, content, createdAt: now, status: 'done', ...extra })
const tool = (id, name, args, result) => ({ id, name, args, argsRaw: JSON.stringify(args), status: 'ok', result, output: typeof result === 'string' ? result : JSON.stringify(result) })
const tools = [
tool('read', 'read_file', { path: 'src/example.ts', offset: 4, limit: 2 }, { content: 'export const value = 1', startLine: 4, endLine: 5, totalLines: 8 }),
tool('command', 'Bash', { command: 'npm run build' }, { stdout: 'Build complete', stderr: '', exitCode: 0 }),
tool('web', 'web_search', { query: 'documentation' }, { results: [{ title: 'Documentation', url: 'https://example.com', snippet: 'Example documentation result.' }] }),
tool('mcp', 'mcp_call_tool', { server_name: 'local', tool_name: 'inspect', arguments: {} }, { content: 'MCP result' }),
tool('unknown', 'custom_unknown', { input: 'x' }, 'Unknown-tool fallback output')
]
tools.push({ ...tool('plan-artifact', 'exit_plan_mode', {}, 'Plan ready'), metadata: { acp_request_permission: { planInfo: { planFilePath: 'plans/not-materialized.md', plan: '# 尚未落盘的计划\n\n- [ ] 验证内存内容预览\n- [ ] 保留原始 Markdown' } } } })
export const conversationCompleteFixture = {
sessions: [
{ id: 'conversation-complete', title: '八类对话组件验证', createdAt: now, updatedAt: now },
{ id: 'parallel-session', title: '并行会话验证', createdAt: now - 1, updatedAt: now - 1 }
],
messages: {
'conversation-complete': [
message('complete-user', 'user', '对齐问答、审批、工具、计划和消息操作。', { requestId: 'complete-request' }),
message('complete-assistant', 'assistant', '已完成组件恢复。', { requestId: 'complete-request', tools, parts: [
{ type: 'reasoning', id: 'thinking', text: '先读取实现,再检查状态和宿主回调。', finished: true },
...tools.map((item) => ({ type: 'tool', toolId: item.id })),
{ type: 'plan', plan: { id: 'plan', title: '对齐计划', steps: [{ id: '1', text: '恢复组件', status: 'completed' }, { id: '2', text: '接入宿主', status: 'in_progress' }] } },
{ type: 'task', task: { id: 'task', title: '检查工具状态', description: '验证成功、失败和取消状态。', status: 'success', summary: '**检查完成**,状态可恢复。' } },
{ type: 'text', text: '输入“问答验证”或“审批验证”检查完整交互。支持在运行时切换另一个会话并保留各自草稿。' }
] })
],
'parallel-session': [message('parallel-user', 'user', '第二个独立会话。'), message('parallel-assistant', 'assistant', '此处的草稿和运行状态独立保存。')]
},
files: { 'src/example.ts': 'line 1\nline 2\nline 3\nexport const value = 1\nline 5\nline 6\nline 7\nline 8' }
}
export const conversationCompleteBridge = `<script>
(()=>{
const fixture=window.__YCODE_FIXTURE__, listeners=new Set(), pending=new Map();let seq=0;
const emit=(event)=>listeners.forEach(listener=>listener(event));
const finish=(run,text)=>{pending.delete(run.runId);emit({sessionId:run.sessionId,runId:run.runId,type:'text',delta:text});emit({sessionId:run.sessionId,runId:run.runId,type:'done',content:text,tools:[]})};
window.ycode.chat.onEvent=(listener)=>{listeners.add(listener);return ()=>listeners.delete(listener)};
window.ycode.chat.send=async(payload)=>{
const run={sessionId:payload.sessionId,runId:'fixture-run-'+(++seq)}, assistantMessageId=run.runId+'-assistant';
const userMessage={id:run.runId+'-user',requestId:run.runId,planId:payload.planId,role:'user',content:payload.text,attachments:payload.attachments,createdAt:Date.now(),status:'done'};
const row=fixture.messages[run.sessionId]??(fixture.messages[run.sessionId]=[]);
let resultMessages;
if(payload.action){let index=row.findIndex(item=>item.id===payload.fromMessageId);if(payload.action==='regenerate')index=row.slice(0,index).findLastIndex(item=>item.role==='user');row.splice(index);resultMessages=[...row,userMessage,{id:assistantMessageId,requestId:run.runId,role:'assistant',content:'',status:'streaming',createdAt:Date.now()}]}
row.push(userMessage);
setTimeout(()=>{
if(/问答/.test(payload.text)){const interaction={id:run.runId+'-question',runId:run.runId,toolCallId:'fixture-question',kind:'question',title:'选择验证方式',status:'pending',questions:[{id:'choice',question:'选择哪种验证方式?',options:['源码和状态验证','浏览器交互验证']}]};pending.set(run.runId,{...run,interaction});emit({...run,type:'interaction',interaction})}
else if(/审批/.test(payload.text)){const interaction={id:run.runId+'-approval',requestId:run.runId+'-approval',runId:run.runId,toolCallId:'fixture-write',kind:'approval',title:'写入示例文件',status:'pending',options:[{optionId:'allow-once',name:'允许一次',kind:'allow_once'},{optionId:'reject-once',name:'拒绝',kind:'reject_once'}]};pending.set(run.runId,{...run,interaction});emit({...run,type:'interaction',interaction})}
else finish(run,'验证回复:'+payload.text);
},50);
return {...run,assistantMessageId,userMessage,...(resultMessages?{messages:resultMessages}:{})}
};
window.ycode.chat.respond=async(runId,id,response)=>{const run=pending.get(runId);if(!run||run.interaction.id!==id)return {accepted:false};const interaction={...run.interaction,status:response.kind==='approval'&&response.optionId==='reject-once'?'rejected':'answered',...(response.kind==='question'?{answer:response.answer,skipped:response.skipped}:{selectedOptionId:response.optionId})};emit({...run,type:'interaction',interaction});finish(run,response.kind==='question'?'已收到回答并继续。':'审批已回传并继续。');return {accepted:true}};
window.ycode.chat.cancel=async(runId)=>{const run=pending.get(runId);if(run){pending.delete(runId);emit({...run,type:'cancelled'})}return {cancelled:!!run}};
window.ycode.chat.feedback=async()=>{};
window.ycode.attachments={pick:async()=>[{id:'fixture-attachment',name:'example.txt',mimeType:'text/plain',size:15}],stage:async(input)=>({id:'fixture-staged-'+(++seq),name:input.name,mimeType:input.mimeType,size:input.dataBase64.length}),remove:async()=>{}};
window.ycode.speech={transcribe:async()=>{throw new Error('UI fixture does not call a speech service')}};
})();
</script>`
+65
View File
@@ -0,0 +1,65 @@
// Offline YCode state samples only; these do not establish original-app parity.
const now = Date.now()
const history = Array.from({ length: 14 }, (_, index) => {
const createdAt = now + index * 2
const text = Array.from({ length: 4 }, (__, paragraph) =>
`历史消息 ${index + 1},段落 ${paragraph + 1}。用于让会话时间线形成可滚动的真实内容。`.repeat(3)
).join('\n\n')
return [
{ id: `history-user-${index}`, role: 'user', content: `历史请求 ${index + 1}`, status: 'done', createdAt },
{ id: `history-assistant-${index}`, role: 'assistant', content: text, status: 'done', createdAt: createdAt + 1 }
]
}).flat()
export const conversationStatesFixture = {
sessions: [{ id: 'conversation-states', title: '对话状态回归', createdAt: now, updatedAt: now }],
messages: { 'conversation-states': history }
}
export const conversationStatesBridge = `<script>
(()=>{
const listeners=new Set(),runs=Object.create(null);let sequence=0;
const state={lastRunId:null,runs,responses:[],responsePlans:[],deferredResponses:[],earlyBatches:[],emit(event){
const runId=event.runId||state.lastRunId,run=runs[runId];
if(!run)throw new Error('No fixture run for event '+event.type);
const payload={...event,runId,sessionId:run.sessionId};
run.events.push(payload);listeners.forEach(listener=>listener(payload));
if(payload.type==='interaction')run.interactions[payload.interaction.id]=payload.interaction;
if(['done','cancelled','error'].includes(payload.type))run.finished=true;
return payload;
},queueResponse(mode){state.responsePlans.push(mode)},queueEarlyBatch(events){state.earlyBatches.push(events)},releaseDeferredResponse(){const release=state.deferredResponses.shift();if(!release)return false;release();return true}};
const completeResponse=(runId,id,response)=>{
const run=runs[runId],interaction=run?.interactions[id];
if(!interaction)return{accepted:false};
const option=interaction.options.find(item=>item.optionId===response.optionId);
if(!option)return{accepted:false};
const status=option.kind.startsWith('reject')?'rejected':'answered';
state.emit({runId,type:'interaction',interaction:{...interaction,selectedOptionId:option.optionId,status}});
return{accepted:true};
};
window.__YCODE_CONVERSATION_STATE_TEST__=state;
window.ycode.chat.onEvent=listener=>{listeners.add(listener);return()=>listeners.delete(listener)};
window.ycode.chat.send=async payload=>{
const runId='state-run-'+(++sequence),assistantMessageId=runId+'-assistant';
const run={runId,sessionId:payload.sessionId,assistantMessageId,events:[],interactions:Object.create(null),finished:false};
runs[runId]=run;state.lastRunId=runId;
const userMessage={id:runId+'-user',requestId:runId,role:'user',content:payload.text,attachments:payload.attachments,createdAt:Date.now(),status:'done'};
const earlyEvents=state.earlyBatches.shift()||[];
earlyEvents.forEach(event=>state.emit({...event,runId,...(event.type==='interaction'?{interaction:{...event.interaction,runId}}:{})}));
return{...run,userMessage};
};
window.ycode.chat.cancel=async runId=>{
const run=runs[runId];
if(!run||run.finished)return{cancelled:false};
state.emit({runId,type:'cancelled'});
return{cancelled:true};
};
window.ycode.chat.respond=async(runId,id,response)=>{
state.responses.push({runId,interactionId:id,response});
const mode=state.responsePlans.shift()||'success';
if(mode==='failure')throw new Error('fixture respond failure');
if(mode==='defer')return await new Promise(resolve=>state.deferredResponses.push(()=>resolve(completeResponse(runId,id,response))));
return completeResponse(runId,id,response);
};
})();
</script>`
+21
View File
@@ -0,0 +1,21 @@
// Offline verification data only; the resource bridge here never reads product files.
const now = Date.now()
const svgImage = (color, label) => `data:image/svg+xml;base64,${Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" width="320" height="160"><rect width="320" height="160" rx="12" fill="${color}"/><text x="160" y="88" text-anchor="middle" fill="white" font-size="28">${label}</text></svg>`).toString('base64')}`
const content = [
'## 图表、图片与文件交互', '',
'```mermaid', 'flowchart LR', ' A[输入] --> B[处理]', ' B --> C[输出]', '```', '',
'点击文件 `src/example.ts#L4-L7` 或目录 `src` 查看内容。', '',
'```ts:4:7:src/example.ts', 'export const result = input.map(transform)', '```', '',
'![蓝色示例](images/blue.svg)', '', '![绿色示例](images/green.svg)', '',
'以下验证错误图表仍可查看源码:', '',
'```mermaid', 'flowchart LR', ' A[unclosed', '```'
].join('\n')
export const dialogueMediaFixture = {
sessions: [{ id: 'dialogue-media', title: '图表、图片与文件交互', createdAt: now, updatedAt: now }],
messages: { 'dialogue-media': [
{ id: 'media-user', role: 'user', content: '验证图表缩放和下载、图片预览、文件行号跳转。', status: 'done', createdAt: now },
{ id: 'media-assistant', role: 'assistant', content, parts: [{ type: 'text', text: content }], tools: [], status: 'done', createdAt: now }
] },
images: { 'images/blue.svg': svgImage('#3567be', 'Image A'), 'images/green.svg': svgImage('#277c64', 'Image B') },
files: { 'src/example.ts': Array.from({ length: 32 }, (_, i) => `// example line ${i + 1}`).join('\n') }
}
+21
View File
@@ -0,0 +1,21 @@
// Offline YCode renderer cases. These are not original WorkBuddy runtime samples.
const now = Date.now()
const cases = [
['changed', '修改 Diff', 'sample.ts', 'modified', 'export const value = 2\n', '--- a/sample.ts\n+++ b/sample.ts\n@@ -1 +1 @@\n-export const value = 1\n+export const value = 2\n'],
['added', '新增 Diff', 'new.txt', 'added', 'created\n', '--- /dev/null\n+++ b/new.txt\n@@ -0,0 +1 @@\n+created\n'],
['deleted', '删除 Diff', 'empty.txt', 'deleted', '', '--- a/empty.txt\n+++ /dev/null\n@@ -1 +0,0 @@\n-removed\n'],
['unchanged', '相同 Diff', 'same.txt', 'modified', 'unchanged\n', '']
]
export const pierreDiffFixture = {
sessions: [{ id: 'pierre-diff', title: 'Pierre Diff 运行验证', createdAt: now, updatedAt: now }],
messages: { 'pierre-diff': [
{ id: 'user', role: 'user', content: '验证 Diff 渲染与视图切换。', status: 'done', createdAt: now },
{ id: 'assistant', role: 'assistant', content: '离线 Diff 样本。', status: 'done', createdAt: now + 1,
parts: [{ type: 'text', text: '离线 Diff 样本。' }, ...cases.map(([id, name, uri, changeType, cachedContent, diff]) => ({
type: 'artifact', artifact: { id, name, path: uri, preview: {
id, title: uri, type: 'file-changes', fileChangeInfo: { uri, changeType, cachedContent, diff }
} }
}))] }
] },
files: Object.fromEntries(cases.map(([, , uri, , content]) => [uri, content]))
}
+37
View File
@@ -0,0 +1,37 @@
// Offline verification only. No model request or product session is created.
const now = Date.now()
const content = [
'## 对话渲染源码还原验证',
'',
'行内公式:$E=mc^2$,集合宏:$\\RR$。',
'',
'$$',
'\\int_0^1 x^2\\,dx = \\frac{1}{3}',
'$$',
'',
'带文件和行号的代码:',
'',
'```ts:12:15:src/components/Message.tsx',
'export function Message({ text }: { text: string }) {',
' return <p>{text}</p>',
'}',
'```',
'',
'| 检查项 | 行为 |',
'| --- | --- |',
'| 公式 | KaTeX 渲染 |',
'| 代码 | 原文复制、文件行号 |',
'| 表格 | 引用原始 Markdown |',
'',
'- [x] 完成内容保持原顺序',
'- [ ] 继续补齐宿主能力',
'',
'<function_calls><invoke name="hidden">protocol payload</invoke></function_calls>'
].join('\n')
export const sourceRecoveryFixture = {
sessions: [{ id: 'source-recovery', title: '对话渲染源码还原', createdAt: now, updatedAt: now }],
messages: { 'source-recovery': [
{ id: 'recovery-user', role: 'user', content: '验证数学公式、代码文件行号和表格引用。', status: 'done', createdAt: now },
{ id: 'recovery-assistant', role: 'assistant', content, parts: [{ type: 'text', text: content }], tools: [], status: 'done', createdAt: now }
] }
}
+26
View File
@@ -0,0 +1,26 @@
// Verification fixture only. Does not create real sessions or contact a model.
const now = Date.now()
const content = [
'## 字体与间距', '',
'这是一段用于核对正文的文本,包含 **加粗文字** 和 `inline code`。', '',
'下一段文字保持相同的正文规则。字号改变时,段落间距使用公共间距变量。', '',
'- 一级列表', ' - 二级列表', '- 第二项', '',
'> 引用内容,用于检查继承的字号和行高。', '',
'| 项目 | 内容 |', '| --- | --- |', '| 字体 | 统一字号 |', '| 间距 | 公共变量 |', '',
'```ts', 'const greeting = "你好,WorkBuddy"', 'console.log(greeting)', '```'
].join('\n')
export const typographyFixture = {
sessions: [{ id: 'typography', title: '字体与间距核对', createdAt: now, updatedAt: now }],
messages: { typography: [
{ id: 'typography-user', role: 'user', content: '核对正文、代码和控件在六档字号下的表现。', status: 'done', createdAt: now },
{ id: 'typography-assistant', role: 'assistant', content, parts: [{ type: 'text', text: content }], tools: [], status: 'done', createdAt: now }
] }
}
// Off-canvas measurement probes for the unified handler, not product UI.
export const typographyProbes = `<style id="typography-fixture-style">
#typography-probes { position: fixed; left: -10000px; top: 0; }
.typography-probe-static { font-size: 13px; }
.typography-probe-semantic { font-size: var(--yc-text-body-size); line-height: var(--yc-text-body-line-height); }
.typography-probe-wb { font-size: var(--wb-font-body-size); }
</style><div id="typography-probes" aria-hidden="true"><span class="typography-probe-static">static</span><span class="typography-probe-semantic">semantic</span><span class="typography-probe-wb">wb</span><div class="cb-font-size-fixed"><span class="typography-probe-static">fixed static</span><span class="typography-probe-semantic">fixed semantic</span><span class="typography-probe-wb">fixed wb</span></div></div>`
+395
View File
@@ -0,0 +1,395 @@
import assert from 'node:assert/strict'
import { getFontLevel, getFontLevelSpec, initFontSize } from '../src/renderer/src/fontSize'
import {
buildFontSizeOverrideCSS,
buildWbFontSizeTokenCSS,
createFontSizeDomAdapter,
getFontSizeShortcutAction
} from '../src/renderer/src/workbuddy/typography/font-size'
let assertions = 0
function equal<T>(actual: T, expected: T, message: string): void {
assert.equal(actual, expected, message)
assertions += 1
}
function includes(value: string, expected: string, message: string): void {
assert.ok(value.includes(expected), message)
assertions += 1
}
function excludes(value: string, forbidden: string, message: string): void {
assert.ok(!value.includes(forbidden), message)
assertions += 1
}
class FakeStyleDeclaration {
private readonly entries: Array<[string, string]>
constructor(values: Record<string, string>) {
this.entries = Object.entries(values)
}
get length(): number {
return this.entries.length
}
item(index: number): string {
return this.entries[index]?.[0] ?? ''
}
getPropertyValue(name: string): string {
return this.entries.find(([key]) => key === name)?.[1] ?? ''
}
}
function styleRule(selectorText: string, values: Record<string, string>): CSSRule {
return {
type: 1,
selectorText,
style: new FakeStyleDeclaration(values)
} as unknown as CSSRule
}
function mediaRule(conditionText: string, cssRules: CSSRule[]): CSSRule {
return {
type: 4,
conditionText,
cssRules: cssRules as unknown as CSSRuleList
} as unknown as CSSRule
}
function stylesheet(cssRules: CSSRule[], ownerId?: string): CSSStyleSheet {
return {
ownerNode: ownerId ? ({ id: ownerId } as unknown as Element) : null,
cssRules: cssRules as unknown as CSSRuleList
} as unknown as CSSStyleSheet
}
function testRuleConstruction(): void {
const rules = [
styleRule('.card, .dialog', { 'font-size': '14px' }),
styleRule('.fallback', { 'font-size': 'var(--unresolved-size, 13.5px)' }),
styleRule(':root', { '--stable-size': '18px' }),
styleRule('.stable', { 'font-size': 'var(--stable-size, 14px)' }),
styleRule(':root', { '--dynamic-size': 'calc(15px + var(--yc-font-offset) * 1px)' }),
styleRule('.dynamic', { 'font-size': 'var(--dynamic-size, 15px)' }),
styleRule('.already-dynamic', { 'font-size': 'calc(16px + var(--cb-font-size-offset))' }),
styleRule('.token', { 'font-size': 'var(--yc-text-body-size)' }),
styleRule('.relative', { 'font-size': '1rem' }),
styleRule('.repeated', { 'font-size': '11px' }),
mediaRule('(min-width: 720px)', [styleRule('.media', { 'font-size': '12px' })]),
mediaRule('(min-width: 720px)', [styleRule('.media', { 'font-size': '12px' })]),
mediaRule('(max-width: 500px)', [styleRule('.repeated', { 'font-size': '11px' })])
]
const css = buildFontSizeOverrideCSS([stylesheet(rules), stylesheet([styleRule('.repeated', { 'font-size': '11px' })])])
includes(css, '.card:not(.cb-font-size-fixed):not(.cb-font-size-fixed *)', 'adds the fixed-zone exclusion to the first selector')
includes(css, '.dialog:not(.cb-font-size-fixed):not(.cb-font-size-fixed *)', 'splits grouped selectors before adding the fixed-zone exclusion')
includes(css, 'font-size: max(8px, calc(14px + var(--cb-font-size-offset) * 1px)) !important', 'scales a literal px size with an 8px floor')
includes(css, 'calc(13.5px + var(--cb-font-size-offset) * 1px)', 'uses the numeric px fallback from var() declarations')
includes(css, 'calc(14px + var(--cb-font-size-offset) * 1px)', 'uses the declared static fallback without resolving the custom property')
includes(css, '.dynamic:not(.cb-font-size-fixed)', 'matches the reference fallback behavior for var() declarations')
includes(css, '@media (min-width: 720px)', 'keeps media rules scoped')
includes(css, '@media (max-width: 500px)', 'keeps separate media conditions distinct')
excludes(css, '.already-dynamic:not(', 'does not rescale an existing calc() declaration')
excludes(css, '.token:not(', 'does not replace a token-only font-size declaration')
excludes(css, '.relative:not(', 'does not convert relative units to px')
equal((css.match(/\.repeated:not\(/g) ?? []).length, 2, 'dedupes within a CSS context but retains a different media context')
equal((css.match(/\.media:not\(/g) ?? []).length, 2, 'uses a fresh dedupe set for each media rule')
const inaccessible = {
get cssRules(): CSSRuleList {
throw new Error('cross-origin stylesheet')
}
} as unknown as CSSStyleSheet
equal(buildFontSizeOverrideCSS([inaccessible]), '', 'skips stylesheets whose cssRules cannot be read')
}
function testTokenCSS(): void {
const css = buildWbFontSizeTokenCSS()
includes(css, '--wb-font-display-size: calc(32px + var(--wb-font-size-offset, 0) * 1px)', 'provides the display token override')
includes(css, '--wb-button-icon-size-medium: calc(16px + var(--wb-font-size-offset, 0) * 1px)', 'scales button icon tokens with the same offset')
includes(css, '--wb-button-height-large: calc(var(--wb-button-line-height-large) + 13px * 2)', 'derives the large button height from line height and padding')
includes(css, '.cb-font-size-fixed,\n.cb-font-size-fixed *', 'creates the fixed-zone token reset')
includes(css, '--wb-font-body-size: 14px', 'restores token base sizes inside fixed zones')
includes(css, '--wb-button-line-height-small: 20px', 'restores button line-height tokens inside fixed zones')
}
class FakeStyleElement {
id = ''
textContent: string | null = ''
readonly tagName = 'STYLE'
parent: FakeHead | null = null
remove(): void {
this.parent?.removeChild(this)
}
}
class FakeHead {
readonly children: FakeStyleElement[] = []
appendChild(element: FakeStyleElement): FakeStyleElement {
element.parent = this
this.children.push(element)
return element
}
removeChild(element: FakeStyleElement): void {
const index = this.children.indexOf(element)
if (index >= 0) this.children.splice(index, 1)
element.parent = null
}
}
class FakeMutationObserver {
disconnected = false
private readonly callback: MutationCallback
constructor(callback: MutationCallback) {
this.callback = callback
}
observe(_target: Node, _options?: MutationObserverInit): void {}
disconnect(): void {
this.disconnected = true
}
triggerAddedNode(node: Node): void {
const record = { addedNodes: [node] as unknown as NodeList }
this.callback([record as MutationRecord], this as unknown as MutationObserver)
}
}
function testDOMAdapter(): void {
const head = new FakeHead()
const properties = new Map<string, string>()
const styleSheets: CSSStyleSheet[] = [stylesheet([styleRule('.live', { 'font-size': '14px' })])]
const rootStyle = {
setProperty: (name: string, value: string): void => properties.set(name, value),
removeProperty: (name: string): void => {
properties.delete(name)
},
getPropertyValue: (name: string): string => properties.get(name) ?? '',
get length(): number {
return properties.size
},
item: (index: number): string => [...properties.keys()][index] ?? ''
} as unknown as CSSStyleDeclaration
const documentRef = {
documentElement: { style: rootStyle, dataset: {} },
styleSheets,
head,
body: null,
getElementById: (id: string): FakeStyleElement | null => head.children.find((child) => child.id === id) ?? null,
createElement: (name: string): FakeStyleElement => {
assert.equal(name, 'style')
return new FakeStyleElement()
}
} as unknown as Document
const observerInstances: FakeMutationObserver[] = []
const timers = new Map<number, () => void>()
let timerId = 0
const windowRef = {
MutationObserver: class extends FakeMutationObserver {
constructor(callback: MutationCallback) {
super(callback)
observerInstances.push(this)
}
},
setTimeout: (callback: () => void): number => {
timerId += 1
timers.set(timerId, callback)
return timerId
},
clearTimeout: (timer: number): void => {
timers.delete(timer)
}
} as unknown as Window & typeof globalThis
const adapter = createFontSizeDomAdapter(documentRef, windowRef)
adapter.apply(3, '18.286px')
equal(properties.get('--yc-font-offset-global'), '3', 'writes the unbounded YCode offset')
equal(properties.get('--cb-font-size-offset-global'), '3', 'writes the unbounded compatibility offset')
equal(properties.get('--wb-font-size-offset'), 'clamp(-1, var(--cb-font-size-offset-global, 0), 2)', 'clamps token consumers independently')
equal(properties.get('--yc-md-root'), '18.286px', 'keeps the YCode markdown root')
equal(properties.get('--cb-md-root'), '18.286px', 'keeps the compatibility markdown root')
includes(head.children.find((child) => child.id === 'yc-font-size-override')?.textContent ?? '', '.live:not(', 'injects scanned CSS rules at nonzero levels')
includes(head.children.find((child) => child.id === 'yc-wb-token-font-size-override')?.textContent ?? '', '--wb-font-body-size', 'injects WB token rules at nonzero levels')
equal(observerInstances.length, 1, 'starts one observer for active global sizing')
const initialOverride = documentRef.getElementById('yc-font-size-override')
adapter.apply(4, '20.571px')
equal(documentRef.getElementById('yc-font-size-override'), initialOverride, 'reuses scanned CSS between nonzero levels')
equal(head.children.filter((child) => child.id === 'yc-wb-token-font-size-override').length, 1, 'does not duplicate token overrides')
styleSheets.push(stylesheet([styleRule('.late-style', { 'font-size': '15px' })]))
observerInstances[0].triggerAddedNode({ nodeType: 1, id: 'late-style-node', tagName: 'STYLE' } as unknown as Node)
equal(timers.size, 1, 'debounces a newly inserted stylesheet')
for (const callback of timers.values()) callback()
timers.clear()
includes(head.children.find((child) => child.id === 'yc-font-size-override')?.textContent ?? '', '.late-style:not(', 'rebuilds scanned overrides after a style insertion')
adapter.apply(0, '17.143px')
equal(documentRef.getElementById('yc-font-size-override'), null, 'removes scanned CSS overrides at zero offset')
equal(documentRef.getElementById('yc-wb-token-font-size-override'), null, 'removes WB token overrides at zero offset')
adapter.dispose()
equal(observerInstances[0].disconnected, true, 'disconnects the observer during cleanup')
}
function testPublicMappingAndShortcuts(): void {
equal(getFontLevelSpec(1).offset, -1, 'level one keeps the raw -1 offset')
equal(getFontLevelSpec(2).offset, 0, 'level two is the zero-offset default')
equal(getFontLevelSpec(5).offset, 3, 'level five retains raw +3 before consumer clamping')
equal(getFontLevelSpec(6).offset, 4, 'level six retains raw +4 before consumer clamping')
equal(getFontSizeShortcutAction({ key: '-', code: 'Minus', shiftKey: false }), 'decrease', 'maps the minus key')
equal(getFontSizeShortcutAction({ key: 'Unidentified', code: 'Equal', shiftKey: true }), 'increase', 'maps the Equal code fallback')
equal(getFontSizeShortcutAction({ key: 'Unidentified', code: 'Digit0', shiftKey: false }), 'reset', 'maps the Digit0 code fallback')
equal(getFontSizeShortcutAction({ key: '-', code: 'Minus', shiftKey: true }), null, 'does not treat shifted minus as decrease')
equal(getFontSizeShortcutAction({ key: '0', code: 'Digit0', shiftKey: true }), null, 'does not treat shifted zero as reset')
}
function testKeyboardIntegration(): void {
const descriptors = new Map<string, PropertyDescriptor | undefined>()
const originalGlobal = globalThis as unknown as Record<string, unknown>
const styleValues = new Map<string, string>()
const head = new FakeHead()
const listeners = new Map<string, { listener: EventListener; capture: boolean }>()
const eventNames: string[] = []
const timeoutCallbacks = new Map<number, () => void>()
let nextTimerId = 0
const documentRef = {
documentElement: {
style: {
setProperty: (name: string, value: string): void => styleValues.set(name, value),
removeProperty: (name: string): void => {
styleValues.delete(name)
},
getPropertyValue: (name: string): string => styleValues.get(name) ?? '',
get length(): number {
return styleValues.size
},
item: (index: number): string => [...styleValues.keys()][index] ?? ''
},
dataset: {}
},
styleSheets: [],
head,
body: null,
getElementById: (id: string): FakeStyleElement | null => head.children.find((child) => child.id === id) ?? null,
createElement: (): FakeStyleElement => new FakeStyleElement(),
addEventListener: (type: string, listener: EventListener, options?: boolean | AddEventListenerOptions): void => {
listeners.set(type, { listener, capture: options === true || (typeof options === 'object' && options.capture === true) })
},
removeEventListener: (type: string): void => {
listeners.delete(type)
}
} as unknown as Document
class TestCustomEvent {
readonly type: string
readonly detail: unknown
constructor(type: string, init?: { detail?: unknown }) {
this.type = type
this.detail = init?.detail
}
}
const windowRef = {
MutationObserver: FakeMutationObserver,
setTimeout: (callback: () => void): number => {
nextTimerId += 1
timeoutCallbacks.set(nextTimerId, callback)
return nextTimerId
},
clearTimeout: (id: number): void => {
timeoutCallbacks.delete(id)
},
dispatchEvent: (event: Event): boolean => {
eventNames.push((event as unknown as TestCustomEvent).type)
return true
}
} as unknown as Window & typeof globalThis
const storage = {
value: '2',
getItem(_key: string): string | null {
return this.value
},
setItem(_key: string, value: string): void {
this.value = value
}
}
const globals: Record<string, unknown> = {
document: documentRef,
window: windowRef,
localStorage: storage,
CustomEvent: TestCustomEvent
}
for (const [key, value] of Object.entries(globals)) {
descriptors.set(key, Object.getOwnPropertyDescriptor(globalThis, key))
Object.defineProperty(originalGlobal, key, { value, configurable: true, writable: true })
}
try {
const cleanup = initFontSize()
equal(listeners.get('keydown')?.capture, true, 'registers shortcuts in capture phase')
const listener = listeners.get('keydown')?.listener
assert.ok(listener, 'registers a keydown listener')
assertions += 1
const sendKey = (key: string, code: string, options: { ctrl?: boolean; meta?: boolean; alt?: boolean; shift?: boolean; target?: EventTarget | null } = {}): { prevented: boolean } => {
let prevented = false
listener!({
key,
code,
ctrlKey: options.ctrl ?? true,
metaKey: options.meta ?? false,
altKey: options.alt ?? false,
shiftKey: options.shift ?? false,
target: options.target ?? null,
preventDefault: () => {
prevented = true
}
} as unknown as KeyboardEvent)
return { prevented }
}
const focusedComposer = { tagName: 'TEXTAREA' } as unknown as EventTarget
equal(sendKey('+', 'Equal', { target: focusedComposer }).prevented, true, 'keeps shortcuts available while a composer input is focused')
equal(getFontLevel(), 3, 'increases the current level from the key handler')
equal(storage.value, '3', 'persists the selected YCode level')
equal(sendKey('-', 'Minus', { alt: true }).prevented, false, 'ignores Alt-modified shortcut combinations')
equal(getFontLevel(), 3, 'Alt-modified input leaves the current level unchanged')
equal(sendKey('Unidentified', 'Minus').prevented, true, 'uses the physical Minus code when key is unavailable')
equal(getFontLevel(), 2, 'decreases the current level from the code fallback')
equal(eventNames.filter((name) => name === 'ycode:font-level-change').length, 2, 'preserves the YCode public event')
for (const callback of timeoutCallbacks.values()) callback()
timeoutCallbacks.clear()
equal(eventNames.filter((name) => name === 'cb-font-size-change').length, 1, 'dispatches the compatible offset event after coalescing changes')
equal(eventNames.filter((name) => name === 'cb-font-size-level-change').length, 1, 'dispatches the compatible level event after coalescing changes')
cleanup()
equal(listeners.has('keydown'), false, 'removes the keydown listener during cleanup')
} finally {
for (const [key, descriptor] of descriptors) {
if (descriptor) Object.defineProperty(originalGlobal, key, descriptor)
else delete originalGlobal[key]
}
}
}
testRuleConstruction()
testTokenCSS()
testDOMAdapter()
testPublicMappingAndShortcuts()
testKeyboardIntegration()
console.log(`Font-size regression passed (${assertions} assertions).`)
+152
View File
@@ -0,0 +1,152 @@
import assert from 'node:assert/strict'
import { createServer, type ServerResponse } from 'node:http'
import { once } from 'node:events'
import { mkdtemp, writeFile } from 'node:fs/promises'
import { resolve, join, relative, isAbsolute } from 'node:path'
import { Gateway, GatewayError, type GatewayDelta, type WireMessage } from '../src/main/gateway'
import { runTurn } from '../src/main/agent/loop'
import { responsesInput } from '../src/main/gateway/responses'
import type { GatewayConfig, ChatEvent } from '../src/shared/types'
const directory = await mkdtemp(resolve('.tmp/gateway-responses-'))
const rel = relative(resolve('.tmp'), directory)
assert.ok(!isAbsolute(rel) && !rel.startsWith('..') && rel.startsWith('gateway-responses-'))
await writeFile(join(directory, 'proof.txt'), 'only controlled local data')
const secret = 'controlled-secret-must-not-appear-in-errors'
const requests: Array<{ path: string; body: any; authorizationPresent: boolean }> = []
const checks: string[] = []
const check = (name: string, actual: unknown, expected: unknown): void => { assert.deepEqual(actual, expected, name); checks.push(name) }
const payload = (type: string, values: Record<string, unknown> = {}): Record<string, unknown> => ({ type, ...values })
const frame = (data: unknown, event?: string): string => `${event ? 'event: ' + event + '\r\n' : ''}data: ${JSON.stringify(data)}\r\n\r\n`
const complete = (output: unknown[] = []): Record<string, unknown> => payload('response.completed', { response: { id: 'response-controlled', status: 'completed', output } })
const functionItem = { type: 'function_call', id: 'fc_internal', call_id: 'call_public', name: 'list_dir', arguments: '{"path":"."}', status: 'completed' }
const messageItem = (text: string, id = 'msg_1'): Record<string, unknown> => ({ type: 'message', id, role: 'assistant', status: 'completed', content: [{ type: 'output_text', text, annotations: [] }] })
const reasoningItem = { type: 'reasoning', id: 'rs_1', summary: [{ type: 'summary_text', text: '先看目录。' }] }
async function send(res: ServerResponse, events: unknown[], tail = ''): Promise<void> {
res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache' })
const data = Buffer.from(': keepalive\r\n\r\ndata: invalid-json-heartbeat\r\n\r\n' + events.map(item => frame(item)).join('') + tail)
// Actual HTTP chunks split UTF-8 glyphs, CRLF boundaries and JSON tokens.
for (let start = 0; start < data.length; start += 13) {
if (res.destroyed) return
if (!res.write(data.subarray(start, start + 13))) await once(res, 'drain')
if (start % 143 === 0) await new Promise<void>(resolve => setImmediate(resolve))
}
res.end()
}
const server = createServer((req, res) => {
void (async () => {
let raw = ''; for await (const chunk of req) raw += chunk
const body = JSON.parse(raw), url = new URL(req.url ?? '', 'http://127.0.0.1'), mode = url.searchParams.get('case')
requests.push({ path: req.url ?? '', body, authorizationPresent: !!req.headers.authorization })
if (url.pathname.endsWith('/chat/completions')) {
await send(res, [{ choices: [{ delta: { content: '旧 Chat 保留' }, finish_reason: 'stop' }] }], 'data: [DONE]\n\n'); return
}
assert.equal(url.pathname, '/v1/responses')
if (mode === 'http-error') { res.writeHead(429, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: { message: secret } })); return }
if (mode === 'stream-error') { await send(res, [payload('error', { message: secret, code: 'provider_failure' })]); return }
if (mode === 'failed') { await send(res, [payload('response.failed', { response: { status: 'failed', error: { message: secret } } })]); return }
if (mode === 'incomplete') { await send(res, [payload('response.output_item.added', { output_index: 0, item: { ...functionItem, status: 'in_progress' } }), payload('response.incomplete', { response: { status: 'incomplete', incomplete_details: { reason: 'max_output_tokens' } } })]); return }
if (mode === 'wrong-status') { await send(res, [payload('response.completed', { response: { status: 'failed', error: { message: secret } } })]); return }
if (mode === 'missing-status' || mode === 'missing-output') {
await send(res, [payload('response.output_item.done', { output_index: 0, item: functionItem }),
payload('response.completed', { response: mode === 'missing-status' ? { output: [functionItem] } : { status: 'completed' } })]); return
}
if (mode === 'truncated') { await send(res, [payload('response.output_text.delta', { item_id: 'msg_1', output_index: 0, content_index: 0, delta: '部分文本' })], 'data: [DONE]\n\n'); return }
if (mode === 'cancel') {
res.writeHead(200, { 'content-type': 'text/event-stream' })
res.write(frame(payload('response.output_text.delta', { item_id: 'msg_1', output_index: 0, content_index: 0, delta: '取消前文本' })))
return
}
if (mode === 'tool') {
if (body.input.some((item: any) => item.type === 'function_call_output')) {
await send(res, [payload('response.output_text.delta', { item_id: 'msg_final', output_index: 0, content_index: 0, delta: '完成读取。' }), complete([messageItem('完成读取。', 'msg_final')])]); return
}
await send(res, [
payload('response.created', { response: { status: 'in_progress' } }),
payload('response.output_item.added', { output_index: 0, item: { type: 'reasoning', id: 'rs_1', summary: [] } }),
payload('response.reasoning_summary_text.delta', { item_id: 'rs_1', output_index: 0, summary_index: 0, delta: '先看' }),
payload('response.reasoning_summary_text.delta', { item_id: 'rs_1', output_index: 0, summary_index: 0, delta: '目录。' }),
payload('response.reasoning_summary_text.done', { item_id: 'rs_1', output_index: 0, summary_index: 0, text: '先看目录。' }),
payload('response.output_item.done', { output_index: 0, item: reasoningItem }),
payload('response.output_text.delta', { item_id: 'msg_1', output_index: 1, content_index: 0, delta: '查看' }),
payload('response.output_text.done', { item_id: 'msg_1', output_index: 1, content_index: 0, text: '查看目录。' }),
payload('response.output_item.added', { output_index: 2, item: { ...functionItem, arguments: '', status: 'in_progress' } }),
payload('response.function_call_arguments.delta', { item_id: 'fc_internal', output_index: 2, delta: '{"pa' }),
payload('response.function_call_arguments.delta', { item_id: 'fc_internal', output_index: 2, delta: 'th":"."}' }),
payload('response.function_call_arguments.done', { item_id: 'fc_internal', output_index: 2, arguments: '{"path":"."}' }),
payload('response.output_item.done', { output_index: 2, item: functionItem }),
complete([reasoningItem, messageItem('查看目录。'), functionItem])]); return
}
if (mode === 'snapshot') {
await send(res, [complete([reasoningItem, messageItem('仅完成快照。')])]); return
}
if (mode === 'parallel') {
await send(res, [payload('response.function_call_arguments.delta', { output_index: 1, item_id: 'fc_second', delta: '{"b":' }),
payload('response.output_item.added', { output_index: 0, item: { type: 'function_call', id: 'fc_first', call_id: 'call_first', name: 'one', arguments: '', status: 'in_progress' } }),
payload('response.function_call_arguments.done', { output_index: 0, item_id: 'fc_first', arguments: '{"a":1}' }),
payload('response.output_item.done', { output_index: 0, item: { type: 'function_call', id: 'fc_first', call_id: 'call_first', name: 'one', arguments: '{"a":1}' } }),
payload('response.output_item.done', { output_index: 1, item: { type: 'function_call', id: 'fc_second', call_id: 'call_second', name: 'two', arguments: '{"b":2}' } }), complete()]); return
}
if (mode === 'invalid-call') { await send(res, [complete([{ ...functionItem, arguments: '{' }])]); return }
if (mode === 'missing-id') { await send(res, [complete([{ ...functionItem, call_id: '' }])]); return }
await send(res, [payload('response.output_text.delta', { output_index: 0, content_index: 0, delta: '你好' }),
payload('response.output_text.done', { item_id: 'msg_1', output_index: 0, content_index: 0, text: '你好,Responses。' }),
payload('response.content_part.done', { item_id: 'msg_1', output_index: 0, content_index: 0, part: { type: 'output_text', text: '你好,Responses。' } }),
complete([messageItem('你好,Responses。')])])
})().catch(() => { res.destroy() })
})
server.listen(0, '127.0.0.1'); await once(server, 'listening')
const address = server.address(); assert.ok(address && typeof address === 'object')
const base = `http://127.0.0.1:${address.port}`
const config = (mode: string, suffix = '/v1/responses'): GatewayConfig => ({ baseUrl: `${base}${suffix}?case=${mode}`, model: 'gpt-6-luna', apiKey: secret, temperature: 0.3, maxToolRounds: 4, maxOutputTokens: 1024 })
const request = { messages: [{ role: 'user' as const, content: '受控测试' }] }
async function collect(gateway: Gateway, input = request): Promise<GatewayDelta[]> { const result: GatewayDelta[] = []; for await (const delta of gateway.stream(input)) result.push(delta); return result }
const joined = (deltas: GatewayDelta[], type: 'text' | 'reasoning'): string => deltas.filter(delta => delta.type === type).map(delta => 'text' in delta ? delta.text : '').join('')
try {
const events: ChatEvent[] = []
const result = await runTurn({ runId: 'controlled-responses', history: [], userText: '列出本地受控文件', signal: new AbortController().signal,
emit: event => events.push(event), gateway: new Gateway(config('tool')), model: 'gpt-6-luna', maxToolRounds: 4, workspaceRoot: directory })
check('real fallback loop executes function and completes next response', [result.content, result.tools.length, result.tools[0].status], ['查看目录。完成读取。', 1, 'ok'])
const rounds = requests.filter(req => req.path.includes('case=tool'))
check('complete endpoint has exactly two rounds and no repeated protocol path', rounds.map(req => req.path), ['/v1/responses?case=tool', '/v1/responses?case=tool'])
check('function tools flatten and keep optional parameter semantics', [rounds[0].body.tools[0].type, rounds[0].body.tools[0].name, rounds[0].body.tools[0].strict, rounds[0].body.tools[0].function], ['function', 'list_dir', false, undefined])
check('Responses token/temperature/model fields', [rounds[0].body.max_output_tokens, rounds[0].body.max_tokens, rounds[0].body.temperature, rounds[0].body.model, rounds[0].body.store], [1024, undefined, 0.3, 'gpt-6-luna', false])
const call = rounds[1].body.input.find((item: any) => item.type === 'function_call'), output = rounds[1].body.input.find((item: any) => item.type === 'function_call_output')
check('history replays public call_id, exact JSON and matching output', [call.call_id, call.arguments, output.call_id, output.output.includes('proof.txt')], ['call_public', '{"path":"."}', 'call_public', true])
check('reasoning summary deltas do not duplicate snapshots', events.filter(event => event.type === 'part' && event.part.type === 'reasoning').at(-1)?.type === 'part' ? (events.filter(event => event.type === 'part' && event.part.type === 'reasoning').at(-1) as any).part.text : '', '先看目录。')
check('fallback emits done exactly once and no error', [events.filter(event => event.type === 'done').length, events.filter(event => event.type === 'error').length], [1, 0])
const multimodal = await collect(new Gateway(config('text')), { messages: [{ role: 'user', content: [{ type: 'text', text: '看图' }, { type: 'image_url', image_url: { url: 'data:image/png;base64,Y29udHJvbGxlZA==' } }] }] })
const multimodalRequest = requests.at(-1)!
check('image/text convert to Responses input content', multimodalRequest.body.input[0].content, [{ type: 'input_text', text: '看图' }, { type: 'input_image', image_url: 'data:image/png;base64,Y29udHJvbGxlZA==', detail: 'auto' }])
check('no tools means no tools/tool_choice keys', [multimodalRequest.body.tools, multimodalRequest.body.tool_choice], [undefined, undefined])
check('text delta/done/part/item/completed snapshots deduplicate even late item id', [joined(multimodal, 'text'), multimodal.at(-1)], ['你好,Responses。', { type: 'finish', reason: 'stop' }])
const snapshots = await collect(new Gateway(config('snapshot')))
check('snapshot-only response can deliver text/reasoning', [joined(snapshots, 'text'), joined(snapshots, 'reasoning')], ['仅完成快照。', '先看目录。'])
const parallel = await collect(new Gateway(config('parallel'))), calls = parallel.find(delta => delta.type === 'tool_calls')
check('parallel calls reconcile by item/index and order by output_index', calls?.type === 'tool_calls' ? calls.calls.map(call => [call.id, call.function.name, call.function.arguments]) : [], [['call_first', 'one', '{"a":1}'], ['call_second', 'two', '{"b":2}']])
check('tool finish is emitted once', parallel.filter(delta => delta.type === 'finish'), [{ type: 'finish', reason: 'tool_calls' }])
for (const mode of ['http-error', 'stream-error', 'failed', 'wrong-status', 'missing-status', 'missing-output', 'incomplete', 'truncated', 'invalid-call', 'missing-id']) {
const deltas: GatewayDelta[] = []
let error: unknown
try { for await (const delta of new Gateway(config(mode)).stream(request)) deltas.push(delta) } catch (reason) { error = reason }
check(`${mode}: throws GatewayError without successful finish or executable calls`, [error instanceof GatewayError, deltas.some(delta => delta.type === 'finish' || delta.type === 'tool_calls'), String(error).includes(secret), (error as GatewayError).body], [true, false, false, undefined])
if (mode === 'http-error') check('HTTP failure retains safe status', (error as GatewayError).status, 429)
}
const failedEvents: ChatEvent[] = []
await assert.rejects(runTurn({ runId: 'incomplete-turn', history: [], userText: '受控未完成', signal: new AbortController().signal,
emit: event => failedEvents.push(event), gateway: new Gateway(config('incomplete')), model: 'gpt-6-luna', maxToolRounds: 2, workspaceRoot: directory }), GatewayError)
check('incomplete fallback loop emits error rather than done/tool execution', [failedEvents.some(event => event.type === 'error'), failedEvents.some(event => event.type === 'done' || event.type === 'tool_call')], [true, false])
const abort = new AbortController(), cancelled: GatewayDelta[] = []
await assert.rejects(async () => { for await (const delta of new Gateway(config('cancel')).stream({ ...request, signal: abort.signal })) { cancelled.push(delta); abort.abort() } }, { name: 'AbortError' })
check('abort preserves partial text and never finishes', [joined(cancelled, 'text'), cancelled.some(delta => delta.type === 'finish')], ['取消前文本', false])
for (const suffix of ['/v1', '/v1/chat/completions/']) {
const chat = await collect(new Gateway(config('chat', suffix)))
check('legacy Chat endpoint and stream preserved: ' + suffix, [requests.at(-1)!.path, joined(chat, 'text'), requests.at(-1)!.body.max_tokens, requests.at(-1)!.body.max_output_tokens, requests.at(-1)!.body.messages], ['/v1/chat/completions?case=chat', '旧 Chat 保留', 1024, undefined, request.messages])
}
const history: WireMessage[] = [{ role: 'system', content: 'system' }, { role: 'assistant', content: null, tool_calls: [{ id: 'public', type: 'function', function: { name: 'f', arguments: '{}' } }] }, { role: 'tool', tool_call_id: 'public', content: 'ok' }, { role: 'assistant', content: 'reply' }]
check('system/assistant/tool history has no null assistant tool-only message', responsesInput(history), [{ role: 'system', content: 'system' }, { type: 'function_call', call_id: 'public', name: 'f', arguments: '{}' }, { type: 'function_call_output', call_id: 'public', output: 'ok' }, { role: 'assistant', content: 'reply' }])
assert.throws(() => responsesInput([{ role: 'tool', content: 'missing' }]), /调用 ID/); checks.push('missing historical tool call id fails locally')
const report = { result: 'PASS', scope: 'Controlled loopback HTTP + actual Gateway/fallback runTurn/local list_dir; no real user endpoint or OMP', checks, requestCount: requests.length }
await writeFile(join(directory, 'report.json'), JSON.stringify(report, null, 2))
console.log(JSON.stringify({ ...report, directory }))
} finally { server.closeAllConnections(); await new Promise<void>(resolve => server.close(() => resolve())) }
+196
View File
@@ -0,0 +1,196 @@
/**
* 从 icons.json 生成 src/renderer/src/components/icons.tsx
*
* 用法:node scripts/gen-icons.mjs <iconsJson> <outTsx>
*/
import { readFile, writeFile } from 'node:fs/promises'
const src = process.argv[2] ?? 'C:/Users/Administrator/.ycode/captures/icons.json'
const out = process.argv[3] ?? 'src/renderer/src/components/icons.tsx'
/** 第二轮采集的导航/账号图标(可选) */
const navSource = process.argv[4] ?? 'C:/Users/Administrator/.ycode/captures/nav-icons.json'
/** 第三轮:标题栏左上操作区图标(可选) */
const topbarSource = process.argv[5] ?? 'C:/Users/Administrator/.ycode/captures/topbar-icons.json'
/** 图标名 → [target, svgIndex, 覆盖的渲染尺寸] */
const MAP = {
IconNewTask: ['newTask', 0, 16],
IconChevronDown: ['templates', 1, 14],
IconFilter: ['topbarFilter', 0, 16],
IconCompact: ['sectionCompact', 0, 16],
IconSectionAdd: ['sectionAdd', 0, 16],
IconPlus: ['inputToolbarLeft', 0, 16],
IconShield: ['inputToolbarLeft', 1, 16],
IconChevronSmall: ['inputToolbarRight', 2, 12],
IconMic: ['inputToolbarRight', 3, 16],
IconStop: ['sendButton', 0, 32],
IconTab: ['tabIcon', 0, 13],
IconClose: ['tabClose', 0, 14],
IconChevronRight: ['artifactActions', 0, 8],
IconPanelCollapse: ['panelActions', 0, 16],
IconPanelRight: ['panelActions', 1, 16]
}
/**
* 第二轮:从 nav-icons.json 采集的导航与账号区图标。
* 键为采集时的标签文案 / 选择器。
*/
const NAV_MAP = {
IconNavAssistant: '助理Beta',
IconNavExperts: '专家·技能·连接器',
IconNavScheduled: '定时任务',
IconNavLibrary: '资料库',
IconNavMore: '更多',
IconBell: '.msg-center-bell-btn',
IconMiniprogram: '.user-menu-trigger-miniprogram',
IconDiscover: '.industry-template-switcher__host button'
}
const NAV_SIZES = {
IconNavAssistant: 16,
IconNavExperts: 16,
IconNavScheduled: 16,
IconNavLibrary: 16,
IconNavMore: 16,
IconBell: 16,
IconMiniprogram: 16,
IconDiscover: 14
}
/** HTML 连字符属性 → JSX 驼峰属性 */
const JSX_ATTRS = {
'fill-rule': 'fillRule',
'clip-rule': 'clipRule',
'stroke-width': 'strokeWidth',
'stroke-linecap': 'strokeLinecap',
'stroke-linejoin': 'strokeLinejoin',
'stroke-dasharray': 'strokeDasharray',
'stroke-dashoffset': 'strokeDashoffset',
'stroke-miterlimit': 'strokeMiterlimit',
'clip-path': 'clipPath',
'fill-opacity': 'fillOpacity',
'stroke-opacity': 'strokeOpacity'
}
/** 只保留几何与绘制属性,去掉 class / aria 等宿主细节 */
function sanitize(svg, size) {
let out = svg
out = out.replace(/\sclass="[^"]*"/g, '')
out = out.replace(/\sstyle="[^"]*"/g, '')
out = out.replace(/\saria-hidden="[^"]*"/g, '')
out = out.replace(/\sxmlns="[^"]*"/g, '')
// 统一由外层控制尺寸
out = out.replace(/\swidth="[^"]*"/, '')
out = out.replace(/\sheight="[^"]*"/, '')
for (const [html, jsx] of Object.entries(JSX_ATTRS)) {
out = out.replace(new RegExp(`\\s${html}=`, 'g'), ` ${jsx}=`)
}
out = out.replace(/<svg /, `<svg width="__SIZE__" height="__SIZE__" `)
if (size) out = out.replace(/__SIZE__/g, String(size))
return out
}
const data = JSON.parse(await readFile(src, 'utf8'))
const parts = [
'/**',
' * 图标组件。',
' *',
' * ⚠️ 来源说明:本文件中的 SVG 路径取自本机安装的 WorkBuddy 运行时界面(CDP 采集,',
' * 见 scripts/wb-icons.mjs + scripts/gen-icons.mjs),用于当前阶段的 1:1 视觉还原。',
' * 这些矢量路径属于第三方产品的界面资产,**仅作临时占位**;',
' * 正式发布前必须替换为自有图标或开源图标集(如 Lucide / VS Code codicons,后者为 MIT)。',
' *',
' * 重新生成:node scripts/gen-icons.mjs <icons.json> <输出路径>',
' */',
'',
"import type { SVGProps } from 'react'",
'',
'type IconProps = SVGProps<SVGSVGElement>',
''
]
for (const [name, [target, index, size]] of Object.entries(MAP)) {
const entry = data[target]
const svg = entry?.svgs?.[index]
if (!svg) {
parts.push(`// ${name}: 采集数据缺失(${target}[${index}])`)
parts.push('')
continue
}
const cleaned = sanitize(svg, size)
.replace(/\sfill="currentColor"/g, ' fill="currentColor"')
.replace(/^<svg /, `<svg {...props} `)
parts.push(`export function ${name}(props: IconProps) {`)
parts.push(' return (')
parts.push(' ' + cleaned)
parts.push(' )')
parts.push('}')
parts.push('')
}
/* ---- 第二轮:导航与账号区图标 ---- */
let navData = null
if (navSource) {
try {
navData = JSON.parse(await readFile(navSource, 'utf8'))
} catch {
navData = null
}
}
for (const [name, key] of Object.entries(NAV_MAP)) {
const svg = navData?.[key]
if (!svg) {
parts.push(`// ${name}: 导航采集数据缺失(${key})`)
parts.push('')
continue
}
const cleaned = sanitize(svg, NAV_SIZES[name])
.replace(/\sfill="currentColor"/g, ' fill="currentColor"')
.replace(/^<svg /, `<svg {...props} `)
parts.push(`export function ${name}(props: IconProps) {`)
parts.push(' return (')
parts.push(' ' + cleaned)
parts.push(' )')
parts.push('}')
parts.push('')
}
/* ---- 第三轮:标题栏左上操作区(收起侧边栏 / 搜索 / 筛选)---- */
const TOPBAR_MAP = {
IconCollapseSidebar: '收起侧边栏',
IconSearchTopbar: '搜索'
}
let topbarData = null
if (topbarSource) {
try {
topbarData = JSON.parse(await readFile(topbarSource, 'utf8'))
} catch {
topbarData = null
}
}
for (const [name, title] of Object.entries(TOPBAR_MAP)) {
const entry = Array.isArray(topbarData) ? topbarData.find((n) => n.title === title) : null
if (!entry?.svg) {
parts.push(`// ${name}: 标题栏采集数据缺失(${title})`)
parts.push('')
continue
}
const cleaned = sanitize(entry.svg, 16)
.replace(/\sfill="currentColor"/g, ' fill="currentColor"')
.replace(/^<svg /, `<svg {...props} `)
parts.push(`export function ${name}(props: IconProps) {`)
parts.push(' return (')
parts.push(' ' + cleaned)
parts.push(' )')
parts.push('}')
parts.push('')
}
await writeFile(out, parts.join('\n'), 'utf8')
console.log('已生成', out)
console.log('图标数:', Object.keys(MAP).length)
+154
View File
@@ -0,0 +1,154 @@
import assert from 'node:assert/strict'
import { renderToStaticMarkup } from 'react-dom/server'
import { downloadImageSafely, ImagePreview } from '../src/renderer/src/workbuddy/images/ImagePreview'
import { MarkdownImage } from '../src/renderer/src/workbuddy/images/MarkdownImage'
import { PreviewIcon } from '../src/renderer/src/workbuddy/images/PreviewIcon'
import {
clampPreviewIndex,
isSafeImageUrl,
makeImagePreviewRequest,
resolveImageUrl,
requiresHostImageResolution,
stepPreviewIndex
} from '../src/renderer/src/workbuddy/images/image-url'
async function testSafeImageSchemesAndResolution(): Promise<void> {
for (const url of [
'data:image/png;base64,AA==',
'https://example.test/image.png',
'HTTP://example.test/image.png',
'blob:https://example.test/id',
'file:///C:/images/photo.png',
'local-file://image/photo.png'
]) assert.equal(isSafeImageUrl(url), true, `${url} should be safe`)
for (const url of [
'javascript:alert(1)',
'data:text/html,unsafe',
'../images/photo.png',
'not a URL'
]) assert.equal(isSafeImageUrl(url), false, `${url} should be rejected`)
let resolverCalls = 0
assert.equal(await resolveImageUrl('https://example.test/image.png', async () => {
resolverCalls++
return 'data:image/png;base64,AA=='
}), 'https://example.test/image.png')
assert.equal(resolverCalls, 0, 'safe sources should bypass host resolution')
assert.equal(await resolveImageUrl('C:\\workspace\\photo.png', async (source) => {
assert.equal(source, 'C:\\workspace\\photo.png')
return 'data:image/png;base64,BB=='
}), 'data:image/png;base64,BB==')
assert.equal(requiresHostImageResolution('file:///C:/workspace/photo.png'), true)
assert.equal(requiresHostImageResolution('local-file://workspace/photo.png'), true)
assert.equal(requiresHostImageResolution('https://example.test/photo.png'), false)
let hostCalls = 0
assert.equal(await resolveImageUrl('file:///C:/workspace/photo.png', async () => {
hostCalls++
return 'data:image/png;base64,CC=='
}, true), 'data:image/png;base64,CC==')
assert.equal(hostCalls, 1, 'preferred host resolution should run for local file schemes')
assert.equal(await resolveImageUrl('https://example.test/photo.png', async () => {
hostCalls++
return 'data:image/png;base64,DD=='
}, true), 'https://example.test/photo.png')
assert.equal(hostCalls, 1, 'preferred host resolution should not intercept remote images')
assert.equal(await resolveImageUrl('/unsafe/path.png', async () => 'javascript:alert(1)'), undefined)
assert.equal(await resolveImageUrl('/no-resolver.png'), undefined)
await assert.rejects(resolveImageUrl('/reject.png', async () => { throw new Error('resolver failed') }), /resolver failed/)
}
function testSafePreviewFilteringAndIndex(): void {
assert.deepEqual(makeImagePreviewRequest([
'https://example.test/one.png',
'../unsafe.png',
'data:image/png;base64,AA==',
'javascript:alert(1)'
], 'data:image/png;base64,AA=='), {
urlList: ['https://example.test/one.png', 'data:image/png;base64,AA=='],
initialIndex: 1
})
assert.deepEqual(makeImagePreviewRequest(['../one.png'], 'data:image/png;base64,AA=='), {
urlList: [],
initialIndex: 0
})
assert.equal(clampPreviewIndex(-3, 4), 0)
assert.equal(clampPreviewIndex(9, 4), 3)
assert.equal(clampPreviewIndex(1, 0), 0)
assert.equal(stepPreviewIndex(1, 4, -1), 0)
assert.equal(stepPreviewIndex(2, 4, 1), 3)
}
async function testRejectedImageDownloadIsReported(): Promise<void> {
let message = ''
await downloadImageSafely(async () => {
throw new Error('disk unavailable')
}, 'https://example.test/image.png', (value) => { message = value })
assert.equal(message, '下载失败:disk unavailable')
}
function testMarkdownImageServerFallback(): void {
const html = renderToStaticMarkup(
<MarkdownImage
src="C:\\workspace\\photo.png"
alt="source diagram"
urlList={['https://example.test/photo.png']}
imageUrlResolver={async () => 'data:image/png;base64,AA=='}
/>
)
assert.match(html, /class="[^"]*cr-image-block-fallback[^"]*"/)
assert.match(html, /aria-busy="true"/)
assert.match(html, /role="img"/)
assert.match(html, />source diagram<\/span>/)
const source = 'C:\\workspace\\source-only.png'
const sourceFallback = renderToStaticMarkup(<MarkdownImage src={source} urlList={[]} />)
assert.ok(sourceFallback.includes(source))
}
function testPreviewDialogServerStructure(): void {
const html = renderToStaticMarkup(
<ImagePreview
urlList={['https://example.test/one.png', 'https://example.test/two.png']}
initialIndex={1}
onClose={() => undefined}
onDownload={() => undefined}
/>
)
assert.match(html, /role="dialog"/)
assert.match(html, /aria-modal="true"/)
assert.match(html, /src="https:\/\/example\.test\/two\.png"/)
assert.match(html, /crossorigin="use-credentials"/)
assert.match(html, /aria-label="上一张"/)
assert.match(html, /aria-label="下一张"/)
assert.match(html, /aria-label="下载图片"/)
assert.match(html, /2\/2/)
}
function testRecoveredPreviewIconVectors(): void {
const actualSize = renderToStaticMarkup(<PreviewIcon name="actual-size" />)
assert.match(actualSize, /d="M2\.5 0L1\.25 0L1\.25 1L0 1L0 2\.25/)
const zoomIn = renderToStaticMarkup(<PreviewIcon name="zoom-in" />)
assert.match(zoomIn, /viewBox="0 0 16 16"/)
assert.match(zoomIn, /transform="matrix\(1 0 0 1 0\.882352 0\.882294\)"/)
const close = renderToStaticMarkup(<PreviewIcon name="close" size={20} />)
assert.match(close, /viewBox="0 0 12 12"/)
assert.match(close, /d="M9\.375 2\.625L2\.625 9\.375"/)
}
async function main(): Promise<void> {
await testSafeImageSchemesAndResolution()
testSafePreviewFilteringAndIndex()
await testRejectedImageDownloadIsReported()
testMarkdownImageServerFallback()
testPreviewDialogServerStructure()
testRecoveredPreviewIconVectors()
console.log('image-renderer-regression: all checks passed')
}
void main().catch((error: unknown) => {
console.error('image-renderer-regression failed:', error)
process.exitCode = 1
})
+77
View File
@@ -0,0 +1,77 @@
/** Install a SHA-verified REA recovery into YCode's managed local expert catalog. */
import fs from 'node:fs/promises'
import path from 'node:path'
import { createHash, randomUUID } from 'node:crypto'
import { pathToFileURL } from 'node:url'
import { build } from 'esbuild'
import { parse as parseYaml } from 'yaml'
const option = name => { const index = process.argv.indexOf(name); if (index < 0 || !process.argv[index + 1]) throw Error('Missing ' + name); return process.argv[index + 1] }
const userData = path.resolve(option('--user-data')), source = path.resolve(option('--source'))
if (path.basename(userData).toLowerCase() !== 'ycode' || /[\\/]\.workbuddy(?:[\\/]|$)/i.test(userData)) throw Error('Explicit YCode userData directory required')
const reportDirectory = await fs.mkdtemp(path.resolve('.tmp/rea-expert-install-'))
const sha = bytes => createHash('sha256').update(bytes).digest('hex')
function relative(value) { if (typeof value !== 'string' || !value || value.includes('\\') || /[\x00-\x1f<>:"|?*]/.test(value) || value.split('/').some(part => !part || part === '.' || part === '..' || /[. ]$/.test(part) || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))) throw Error('Invalid recovery path'); return value }
async function directory(target, create = false) {
let current = path.parse(path.resolve(target)).root
for (const part of path.relative(current, path.resolve(target)).split(path.sep)) {
current = path.join(current, part)
try { const stat = await fs.lstat(current); if (!stat.isDirectory() || stat.isSymbolicLink()) throw Error('Ordinary directories required') }
catch (e) { if (e.code !== 'ENOENT' || !create) throw e; await fs.mkdir(current); const stat = await fs.lstat(current); if (!stat.isDirectory() || stat.isSymbolicLink()) throw Error('Directory changed') }
}
}
async function readOrdinary(file) { await directory(path.dirname(file)); const stat = await fs.lstat(file); if (!stat.isFile() || stat.isSymbolicLink()) throw Error('Ordinary file required'); return fs.readFile(file) }
const evidence = JSON.parse(await readOrdinary(path.join(source, 'evidence-manifest.json')))
if (!/^[a-f0-9]{64}$/.test(evidence.rootSha256) || !Array.isArray(evidence.files) || !evidence.files.length || evidence.files.length > 256) throw Error('Invalid REA source manifest')
let total = 0; const files = new Map(), texts = Object.create(null), folded = new Set()
for (const item of evidence.files) {
const name = relative(item.path), key = name.normalize('NFKC').toLowerCase()
if (folded.has(key)) throw Error('Duplicate recovery file'); folded.add(key)
if (!/^ev_[a-f0-9]{64}$/.test(item.evidenceId)) throw Error('Missing file Evidence identity')
const bytes = await readOrdinary(path.join(source, name)); total += bytes.length
if (bytes.length !== item.bytes || sha(bytes) !== item.sha256 || total > 16 * 1024 * 1024) throw Error('Recovery SHA/byte budget mismatch: ' + name)
files.set(name, bytes)
if (/\.(?:md|json|txt|yaml|yml|py|js|ts|csv)$/i.test(name)) texts[name] = new TextDecoder('utf-8', { fatal: true }).decode(bytes)
}
const manifest = JSON.parse(texts['.codebuddy-plugin/plugin.json'] ?? texts['.workbuddy-plugin/plugin.json'])
const id = manifest.name
if (!/^[a-z0-9][a-z0-9-]{0,126}[a-z0-9]$/.test(id)) throw Error('Invalid expert identifier')
if (manifest.avatar) { const avatar = files.get(relative(manifest.avatar.replace(/^\.\//, ''))); if (!avatar || avatar.length > 500 * 1024) throw Error('Recovered avatar missing/oversized') }
await build({ stdin: { contents: "export * from './src/main/capabilities/experts'", resolveDir: process.cwd(), loader: 'ts' }, outfile: path.join(reportDirectory, 'expert-service.mjs'), bundle: true, platform: 'node', format: 'esm', packages: 'external', logLevel: 'warning', plugins: [{ name: 'installation-path', setup(builder) { builder.onResolve({ filter: /^electron$/ }, () => ({ path: 'electron', namespace: 'installation' })); builder.onLoad({ filter: /.*/, namespace: 'installation' }, () => ({ contents: `export const app={getPath:()=>${JSON.stringify(userData)}};export const safeStorage={isEncryptionAvailable:()=>false}` })) } }] })
const service = await import(pathToFileURL(path.join(reportDirectory, 'expert-service.mjs')).href)
const validation = service.validateExpertFiles(texts, id)
if (validation.errors.length) throw Error(validation.errors.join('\n'))
const root = path.join(userData, 'plugins', 'marketplaces', 'my-experts'), packages = path.join(root, 'plugins'), target = path.join(packages, id), index = path.join(root, '.codebuddy-plugin', 'marketplace.json')
const result = { expertId: id, sourceRootSha256: evidence.rootSha256, files: evidence.files.map(({ path, bytes, sha256, evidenceId }) => ({ path, bytes, sha256, evidenceId })), bytes: total, warnings: validation.warnings, installedPath: target }
if (process.argv.includes('--validate-only')) { await fs.writeFile(path.join(reportDirectory, 'report.json'), JSON.stringify({ ...result, result: 'VALIDATED' }, null, 2)); console.log(JSON.stringify({ result: 'VALIDATED', expertId: id, files: files.size, bytes: total, directory: reportDirectory })); process.exit(0) }
await directory(packages, true); await directory(path.dirname(index), true)
let before = null
try { before = await readOrdinary(index) } catch (e) { if (e.code !== 'ENOENT') throw e }
const catalog = before ? JSON.parse(before) : { name: 'my-experts', description: 'my-experts marketplace (auto-generated)', plugins: [] }
if (!Array.isArray(catalog.plugins)) throw Error('Existing expert catalog is invalid')
if (await fs.lstat(target).then(() => true, e => { if (e.code === 'ENOENT') return false; throw e })) throw Error('Expert already exists; refusing to overwrite user files')
if (catalog.plugins.some(entry => entry.name === id || entry.source === `./plugins/${id}`)) throw Error('Existing catalog identity conflict')
if (before) await fs.writeFile(path.join(reportDirectory, 'marketplace.before.json'), before, { flag: 'wx' })
const stage = path.join(packages, '.install-' + randomUUID()), temporaryIndex = index + '.' + randomUUID() + '.tmp'
let published = false, committed = false
await fs.mkdir(stage)
try {
for (const [name, bytes] of files) { const destination = path.join(stage, name); await directory(path.dirname(destination), true); await fs.writeFile(destination, bytes, { flag: 'wx' }); if (sha(await fs.readFile(destination)) !== sha(bytes)) throw Error('Staged file mismatch') }
let current = null; try { current = await readOrdinary(index) } catch (e) { if (e.code !== 'ENOENT') throw e }
if ((current?.toString('base64') ?? null) !== (before?.toString('base64') ?? null)) throw Error('Catalog changed during import; retry after current operation completes')
await fs.rename(stage, target); published = true
const after = { ...catalog, plugins: [...catalog.plugins, { name: id, source: `./plugins/${id}`, description: manifest.description }] }
await fs.writeFile(temporaryIndex, JSON.stringify(after, null, 2), { flag: 'wx' }); await fs.rename(temporaryIndex, index); committed = true
} catch (e) { if (published && !committed) await fs.rm(target, { recursive: true, force: true }); throw e }
finally { await fs.rm(stage, { recursive: true, force: true }); await fs.rm(temporaryIndex, { force: true }) }
const document = await service.readExpert(id), binding = await service.activateExpert(id), turn = await service.expertTurnContext(id)
const frontmatter = text => parseYaml(text?.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1] ?? '') ?? {}
const registeredSkills = (manifest.skills ?? []).map(entry => { const resource = entry.replace(/^\.\//, '') + '/SKILL.md'; return { name: frontmatter(texts[resource]).name, content: texts[resource] } })
const declaredSkills = manifest.expertType === 'agent' ? frontmatter(texts[`agents/${manifest.agentName ?? id}.md`]).skills ?? [] : registeredSkills.map(skill => skill.name)
const skillPreloaded = declaredSkills.every(name => { const matches = registeredSkills.filter(skill => skill.name === name); return matches.length === 1 && turn.context.includes(matches[0].content) })
if (document.expert.error || !document.prompt || !turn.context.includes(document.prompt) || !binding.avatarUrl?.startsWith('data:image/') || !skillPreloaded) throw Error('Installed expert did not activate correctly')
for (const [name, bytes] of files) if (sha(await readOrdinary(path.join(target, name))) !== sha(bytes)) throw Error('Installed SHA mismatch')
const provenance = path.join(userData, 'capabilities', 'expert-imports'); await directory(provenance, true)
await fs.writeFile(path.join(provenance, id + '.json'), JSON.stringify({ ...result, installedAt: new Date().toISOString() }, null, 2), { flag: 'wx' })
await fs.writeFile(path.join(reportDirectory, 'report.json'), JSON.stringify({ ...result, result: 'PASS', registered: true, activated: true, assistantName: binding.profession || binding.name, skillPreloaded, installedFileShaMatched: files.size }, null, 2))
console.log(JSON.stringify({ result: 'PASS', expertId: id, files: files.size, bytes: total, directory: reportDirectory }))
+87
View File
@@ -0,0 +1,87 @@
// Import an inert conversation from REA Evidence. Never reads WorkBuddy storage directly.
import fs from 'node:fs/promises'
import path from 'node:path'
import { createHash, randomUUID } from 'node:crypto'
import { pathToFileURL } from 'node:url'
import { build } from 'esbuild'
import { execFileSync } from 'node:child_process'
const args = process.argv.slice(2)
const evidencePath = args[0]
const destination = args[1]
const title = args[2] || '开始新会话'
if (!evidencePath || !destination || !path.isAbsolute(destination)) {
throw new Error('Usage: node scripts/import-workbuddy-session.mjs <REA export> <absolute YCode sessions.json> [title] [--apply]')
}
const evidence = JSON.parse((await fs.readFile(evidencePath, 'utf8')).replace(/^\uFEFF/, ''))
if (evidence.operation !== 'export_selected_conversation_text' || evidence.provider?.id !== 'rea-selected-conversation-text' ||
!/^ev_[a-f0-9]{64}$/.test(evidence.evidence_id)) throw new Error('Expected a REA selected transcript Evidence')
const source = evidence.normalized_result.file
const sha = data => createHash('sha256').update(data).digest('hex')
if (sha(Buffer.from(source.text)) !== source.sha256 || Buffer.byteLength(source.text) !== source.bytes) {
throw new Error('REA transcript digest/byte count does not match')
}
await build({ entryPoints: ['src/shared/workbuddy-import.ts'], bundle: true, platform: 'node', format: 'esm',
outfile: '.tmp/workbuddy-import-converter.mjs', logLevel: 'silent' })
const { convertWorkBuddyJsonl } = await import(pathToFileURL(path.resolve('.tmp/workbuddy-import-converter.mjs')).href)
const originalBytes = await fs.readFile(destination)
const existing = JSON.parse(originalBytes.toString('utf8').replace(/^\uFEFF/, ''))
if (!Array.isArray(existing.sessions)) throw new Error('Invalid YCode session store')
if (existing.sessions.some(session => session.messages?.some(message => message.status === 'streaming'))) {
throw new Error('YCode has unfinished persisted messages; finish the active run before offline import')
}
const sourceId = evidence.normalized_result.session_id
const duplicate = existing.sessions.find(session => session.importProvenance?.sourceSessionId === sourceId)
if (duplicate) {
console.log(JSON.stringify({ status: 'already-imported', sessionId: duplicate.id, title: duplicate.title }))
process.exit(0)
}
const importedAt = Date.now()
const result = convertWorkBuddyJsonl(source.text, { sessionId: randomUUID(), title,
sourceSessionId: sourceId, evidenceId: evidence.evidence_id, sourceSha256: source.sha256,
importedAt, branchMode: 'visible' })
const exportedImages = new Map((evidence.normalized_result.attachments ?? []).map(image => {
const bytes = Buffer.from(image.data_base64, 'base64')
if (sha(bytes) !== image.sha256 || image.sha256 !== image.blob_id || bytes.length !== image.bytes ||
!['image/png', 'image/jpeg', 'image/webp', 'image/gif'].includes(image.mime)) throw new Error('Invalid REA image artifact')
return [image.blob_id, image]
}))
let copiedImages = 0
for (const reference of result.provenance.referencedAttachments ?? []) {
const attachment = result.messages.find(message => message.id === reference.messageId)?.attachments?.find(item => item.id === reference.attachmentId)
const image = exportedImages.get(reference.sourceBlobId)
if (!attachment || !image) throw new Error('A referenced image is unavailable in the REA export; no import performed')
attachment.previewUrl = `data:${image.mime};base64,${image.data_base64}`
copiedImages++
}
if (copiedImages === result.provenance.referencedAttachments.length) {
result.warnings = result.warnings.filter(warning => !warning.startsWith('Image attachment references were retained'))
}
const session = { ...result.meta, updatedAt: importedAt, messages: result.messages, importProvenance: result.provenance }
await fs.writeFile('.tmp/workbuddy-copy-session.json', JSON.stringify(session, null, 2))
const report = { applied: false, title, sessionId: session.id, sourceSessionId: sourceId,
evidenceId: evidence.evidence_id, sourceSha256: source.sha256, stats: { ...result.stats, copiedImages }, warnings: result.warnings,
existingSessionCount: existing.sessions.length, destination }
if (args.includes('--apply')) {
if (process.platform === 'win32') {
const executable = path.resolve('node_modules/electron/dist/electron.exe').replaceAll("'", "''")
const processIds = execFileSync('powershell.exe', ['-NoProfile', '-Command',
`Get-CimInstance Win32_Process -Filter "Name='electron.exe'" | Where-Object { $_.ExecutablePath -eq '${executable}' -and $_.CommandLine -notmatch '--type=' -and ($_.CommandLine -match '\\s\\.\\s*$' -or $_.CommandLine -match 'out[\\\\/]main') } | Select-Object -ExpandProperty ProcessId`],
{ encoding: 'utf8', windowsHide: true }).trim()
if (processIds) throw new Error('Close the YCode app before applying an offline import so its main-process cache cannot overwrite the copy')
}
const currentBytes = await fs.readFile(destination)
if (sha(currentBytes) !== sha(originalBytes)) throw new Error('YCode store changed during import; no writes performed')
const backup = `${destination}.before-workbuddy-${importedAt}.bak`
await fs.copyFile(destination, backup, 1)
const expected = { ...existing, sessions: [...existing.sessions, session] }
const temp = `${destination}.import-${randomUUID()}.tmp`
await fs.writeFile(temp, JSON.stringify(expected, null, 2))
await fs.rename(temp, destination)
const restored = JSON.parse(await fs.readFile(destination, 'utf8'))
if (JSON.stringify(restored) !== JSON.stringify(expected)) throw new Error(`Import readback failed; backup: ${backup}`)
report.applied = true
report.backup = backup
}
await fs.writeFile('.tmp/workbuddy-copy-report.json', JSON.stringify(report, null, 2))
console.log(JSON.stringify(report))
@@ -0,0 +1,53 @@
import assert from 'node:assert/strict'
import { createMarkdownParser } from '../src/renderer/src/workbuddy/markdown/parser'
import { computeIncrementalCache, findSafeBoundary, groupTopLevelTokens, offsetOfLineStart, tryIncrementalParse, type IncrementalCache } from '../src/renderer/src/workbuddy/markdown/incremental'
const md = createMarkdownParser({ loading: true })
const sequences = [
'A settled paragraph.\n\nAnother paragraph grows: **bold** 中文字符。',
'Intro.\n\n- one\n- two\n\nFollowing paragraph.',
'Intro.\n\n```ts\nconst x = 1\n\nconsole.log(x)\n```\n\nDone.',
'Intro.\n\n| a | b |\n| - | - |\n| 1 | 2 |\n\nDone.',
'Intro.\n\n> quote\n>\n> second\n\nDone.',
'Intro.\n\n[link][ref]\n\n[ref]: https://example.com',
'Intro.\n\n$$x^2$$\n\nDone.',
'Intro.\n\n![image](https://example.com/a.png) trailing text',
'Intro.\n\n- [x] done\n- [ ] waiting\n\nConclusion.'
]
let comparisons = 0
let hits = 0
for (const final of sequences) {
let cache: IncrementalCache | null = null
for (let length = 1; length <= final.length; length++) {
const input = final.slice(0, length)
const cachedBefore = cache ? JSON.stringify(cache.prefixTokens) : null
const incremental = tryIncrementalParse(cache, md, input)
const env = {}
const full = md.parse(input, env)
if (incremental) {
assert.equal(md.renderer.render(incremental, md.options, {}), md.renderer.render(full, md.options, env), `HTML mismatch at ${JSON.stringify(input)}`)
assert.deepEqual(incremental.map((token) => token.map), full.map((token) => token.map), `Line map mismatch at ${JSON.stringify(input)}`)
hits++
}
if (cache) assert.equal(JSON.stringify(cache.prefixTokens), cachedBefore, 'tail parsing must not mutate cached prefix')
cache = computeIncrementalCache(md, input, incremental ?? full, env)
comparisons++
}
}
assert.ok(hits > 30, `expected useful cache reuse; got ${hits}`)
const plain = 'Prefix.\n\nTail'
const cache = computeIncrementalCache(md, plain, md.parse(plain, {}), {})
assert.ok(cache)
assert.equal(tryIncrementalParse(cache, md, 'Changed.\n\nTail grows'), null)
assert.equal(tryIncrementalParse(cache, createMarkdownParser({ loading: true }), plain + ' more'), null)
for (const input of ['Prefix.\n\n[ref]: https://example.com', 'Prefix.\n\n$$x$$']) {
assert.equal(computeIncrementalCache(md, input, md.parse(input, {}), {}), null)
}
assert.equal(offsetOfLineStart('a\nb\nc', 0), 0)
assert.equal(offsetOfLineStart('a\nb\nc', 2), 4)
assert.equal(offsetOfLineStart('a\nb\nc', 9), 5)
assert.ok('failReason' in findSafeBoundary('Only one paragraph', md.parse('Only one paragraph', {})))
const groups = groupTopLevelTokens(md.parse('Heading\n===\n\n- first\n- second\n\nLast', {}))
assert.equal(groups.length, 3)
assert.deepEqual(groups.map((group) => group.reduce((sum, token) => sum + token.nesting, 0)), [0, 0, 0])
console.log(`incremental-markdown-regression: ${comparisons} stream prefixes compared, ${hits} cache hits; all checks passed`)
+134
View File
@@ -0,0 +1,134 @@
import React from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import MarkdownContent, {
getCodeBlockLanguage,
getCodeBlockText,
getMappedSource,
parseCodeBlockMeta,
resolveCodeBlockFenceInfo
} from '../src/renderer/src/components/MarkdownContent'
import MarkdownIt from 'markdown-it'
import { getPrismLanguage, prismLanguageLoaders } from '../src/renderer/src/workbuddy/markdown/languages'
let failures = 0
function check(label: string, condition: boolean, detail = ''): void {
if (condition) console.log(` ✓ ${label}`)
else {
failures++
console.error(` ✗ ${label}${detail ? ` — ${detail}` : ''}`)
}
}
function render(content: string, options: { streaming?: boolean; onQuote?: (markdown: string) => void } = {}): string {
// Original OpacityText retains plain span wrappers after completion; compare textual structure without those wrappers.
return renderToStaticMarkup(<MarkdownContent content={content} {...options} />).replace(/<span>([^<]*)<\/span>/g, '$1')
}
async function main(): Promise<void> {
await prismLanguageLoaders.typescript()
await prismLanguageLoaders.json()
console.log('\n[1] Markdown structure and raw HTML fallback')
const structure = render(
'# Heading\n\nParagraph with **bold**, _italic_, ~~strike~~, and `inline`.\nLine two.\n\n> quoted\n\n---\n\n#### Small heading'
)
check('renders headings, paragraphs, emphasis, strike, inline code, quote, break, and rule',
structure.includes('<h1>') && structure.includes('<p>') && structure.includes('<strong>bold</strong>') &&
structure.includes('<em>italic</em>') && structure.includes('<s>strike</s>') &&
structure.includes('markdown-content__inline-code') && structure.includes('<blockquote') &&
structure.includes('<br') && structure.includes('<hr') && structure.includes('<h4>'))
const ordinaryHtml = render('<b onclick="alert(1)">bold text</b>')
check('ordinary raw HTML tags are stripped while their text remains', ordinaryHtml.includes('bold text') && !ordinaryHtml.includes('<b'))
const dangerousHtml = render('before<script>alert(1)</script>after <style>hidden</style><iframe>frame text</iframe>')
check('dangerous raw HTML containers suppress their contents', dangerousHtml.includes('before') && dangerousHtml.includes('after') &&
!dangerousHtml.includes('alert(1)') && !dangerousHtml.includes('hidden') && !dangerousHtml.includes('frame text'))
const unclosedDangerousHtml = render('before<svg><text>hidden to end')
check('unclosed dangerous raw HTML suppresses content through the end', unclosedDangerousHtml.includes('before') && !unclosedDangerousHtml.includes('hidden to end'))
const fallbackTags = render('line<br>next <u>underlined</u>')
check('br becomes a hard break and u becomes underline markup', fallbackTags.includes('<br') && fallbackTags.includes('<u>underlined</u>'))
const htmlInCode = render('`<b>inline</b>`\n\n```html\n<b>fenced</b>\n```')
check('HTML inside inline and fenced code remains literal code', htmlInCode.includes('&lt;b&gt;inline&lt;/b&gt;') &&
htmlInCode.includes('&lt;') && htmlInCode.includes('&lt;/') && htmlInCode.includes('fenced'), htmlInCode)
console.log('\n[2] Lists, tasks, and tables')
const lists = render('- [x] complete\n- [ ] pending\n - nested item')
check('task checkboxes are disabled and preserve checked state',
lists.includes('type="checkbox"') && lists.includes('disabled=""') && lists.includes('checked=""'))
check('nested lists render as nested list elements', (lists.match(/<ul(?:\s|>)/g) ?? []).length === 2 && lists.includes('<li>nested item</li>'))
const tableSource = '| Name | Value |\n| --- | ---: |\n| alpha | 2 |'
const table = render(tableSource, { onQuote: () => undefined })
check('GFM table renders with a horizontal-scroll wrapper', table.includes('<table ') && table.includes('markdown-content__table-scroll'))
check('table quote action only appears when a callback is supplied', table.includes('添加到对话') && !render(tableSource).includes('添加到对话'))
check('table cells retain alignment', table.includes('text-align:right'))
check('source-map slice normalizes CRLF and omits trailing newline',
getMappedSource('a\r\nb\r\nc\r\n', [1, 2]) === 'b' && getMappedSource('a\r\nb\r\nc\r\n', [0, 3]) === 'a\nb\nc')
console.log('\n[3] Code blocks and copy source')
const rawCode = ' const value = 1\n\n'
const parser = new MarkdownIt()
const codeToken = parser.parse(`\`\`\`js\n${rawCode}\`\`\``, {}).find((token) => token.type === 'fence')
check('visible language labels retain js, ts, py, and txt while verified aliases normalize',
getCodeBlockLanguage('js title=demo') === 'js' && getCodeBlockLanguage('ts') === 'ts' &&
getCodeBlockLanguage('py') === 'py' && getCodeBlockLanguage('txt') === 'txt' &&
getCodeBlockLanguage('c++') === 'cpp' && getCodeBlockLanguage('c#') === 'csharp' &&
getCodeBlockLanguage('echarts-data') === 'json' && getCodeBlockLanguage('shell') === 'bash' &&
getCodeBlockLanguage('html') === 'markup' && getCodeBlockLanguage('svg') === 'markup' &&
getCodeBlockLanguage('') === 'plaintext' && getCodeBlockLanguage('custom') === 'custom')
check('short visible labels still select their Prism grammars',
!!getPrismLanguage('js') && !!getPrismLanguage('ts'))
check('copy text preserves leading spaces and trailing blank lines', !!codeToken && getCodeBlockText(codeToken) === rawCode)
const fenced = render(`\`\`\`ts\n${rawCode}\`\`\``)
check('fenced code preserves visible language label and uses Prism tokens',
fenced.includes('>ts</span>') && fenced.includes('language-ts') && fenced.includes('token') && fenced.includes('aria-label="复制"'))
const metadata = parseCodeBlockMeta('ts:4:7:C:\\project\\src\\App.ts')
check('language metadata parses Windows paths and file basenames',
metadata?.metaLanguage === 'ts' && metadata.filePath === 'C:\\project\\src\\App.ts' && metadata.fileName === 'App.ts')
const metadataFence = render('```ts:4:7:C:\\project\\src\\App.ts\nconst value: number = 1\n```')
check('metadata fence shows filename and line range with the integrated host click handler',
metadataFence.includes('markdown-content__code-file-title') && metadataFence.includes('title="4:7:C:\\project\\src\\App.ts"') &&
metadataFence.includes('App.ts') && metadataFence.includes('L4-L7') && metadataFence.includes('cr-code-block__file-title--clickable'))
const inferredFenceInfo = resolveCodeBlockFenceInfo('1:0:C:\\project\\data\\payload.json')
const inferredFence = render('```1:0:C:\\project\\data\\payload.json\n{"ok": true}\n```')
check('metadata without a language infers Prism grammar from the file extension and clamps lines',
inferredFenceInfo.language === 'json' &&
inferredFence.includes('title="1:1:C:\\project\\data\\payload.json"') && inferredFence.includes('language-json') && inferredFence.includes('token property'))
const rangeMetadataFence = render('```echarts-data:2-4:charts/series.json\n{"series": []}\n```')
check('language-range metadata format renders file context and normalized json language',
rangeMetadataFence.includes('title="2:4:charts/series.json"') && rangeMetadataFence.includes('series.json') && rangeMetadataFence.includes('language-json'))
const indented = render(' print("hello")')
check('indented code uses the same plaintext code block renderer', indented.includes('markdown-content__code-block') && indented.includes('plaintext'))
const streaming = render('```ts\nconst pending: number = 1', { streaming: true })
check('an incomplete streaming fence remains renderable', streaming.includes('data-streaming="true"') && streaming.includes('markdown-content__code-block'))
check('streaming cues use six UTF-16 units before converting to characters, outside fenced code',
!streaming.includes('markdown-content__loading-char') &&
(render('Prefix abcde🙂', { streaming: true }).match(/class="markdown-content__loading-char"/g) ?? []).length === 5)
console.log('\n[4] Safe links and linkify settings')
const links = render('[https](https://example.com) [mail](mailto:user@example.com) [bad](javascript:alert(1)) www.example.com user@example.com')
check('HTTP(S) and mailto links open safely in a new tab',
(links.match(/target="_blank" rel="noreferrer"/g) ?? []).length === 2)
check('unsafe schemes never receive a runnable href', !/href="(?:javascript|data|file):/i.test(links))
const fuzzy = render('www.example.com user@example.com')
check('fuzzy bare domains and email addresses stay unlinked', !fuzzy.includes('<a') && !fuzzy.includes('href='), fuzzy)
const cjkHostname = render('https://example.com中文/path')
check('autolink host repair leaves the CJK suffix and path outside the link',
cjkHostname.includes('href="https://example.com/"') && cjkHostname.includes('>https://example.com</a>中文/path'), cjkHostname)
const cjkPath = render('https://example.com/path中文')
check('autolink host repair does not split non-ASCII path text',
cjkPath.includes('href="https://example.com/path%E4%B8%AD%E6%96%87"') && cjkPath.includes('>https://example.com/path中文</a>'))
const cjkEmphasis = render('**中文:**继续中文 and **😀文字**继续')
check('CJK-friendly emphasis handles punctuation and astral characters',
cjkEmphasis.includes('<strong>中文:</strong>继续中文') && cjkEmphasis.includes('<strong>😀文字</strong>继续'), cjkEmphasis)
const localImage = render('![local](file:///private/image.png)')
const remoteImage = render('![remote](https://example.com/image.png)')
check('images initially show the source-derived loading fallback and do not expose local file URLs',
remoteImage.includes('aria-busy="true"') && remoteImage.includes('remote') && !localImage.includes('src="file:'))
console.log(failures === 0 ? '\nMarkdown regression checks passed.\n' : `\n${failures} Markdown regression check(s) failed.\n`)
process.exitCode = failures === 0 ? 0 : 1
}
void main().catch((error) => { console.error(error); process.exitCode = 1 })
+44
View File
@@ -0,0 +1,44 @@
import assert from 'node:assert/strict'
import { renderToStaticMarkup } from 'react-dom/server'
import MarkdownContent from '../src/renderer/src/components/MarkdownContent'
import { loadMathModules, renderLatexFence } from '../src/renderer/src/workbuddy/markdown/math'
async function main(): Promise<void> {
assert.ok(await loadMathModules(), 'math renderer must load')
const render = (content: string, streaming = false): string => renderToStaticMarkup(<MarkdownContent content={content} streaming={streaming} />)
const inline = render('Inline $x^2$ equation.')
assert.match(inline, /class="cr-math-inline"/)
assert.match(inline, /class="katex"/)
assert.doesNotMatch(inline, /katex-error/)
const block = render('$$\nx^2 + y^2 = z^2\n$$')
assert.match(block, /cr-math-block__body/)
assert.match(block, /katex-display/)
assert.match(render('\\[ x^2 \\]'), /cr-math-block/)
assert.match(render('\\(x^2\\)'), /cr-math-inline/)
for (const macro of ['RR', 'NN', 'ZZ', 'QQ', 'CC']) {
const html = render(`$\\${macro}$`)
assert.match(html, /class="katex"/)
assert.doesNotMatch(html, /katex-error/)
}
assert.match(render('```latex\n\\frac{1}{2}\n```'), /cr-math-block/)
assert.doesNotMatch(render('```latex\n\\frac{1}{2}\n```'), /cr-code-like-box/)
assert.equal(renderLatexFence('\\documentclass{article}\n\\begin{document}hi\\end{document}'), null)
assert.equal(renderLatexFence('\\notARealCommand{'), null)
assert.match(render('```latex\n\\documentclass{article}\n```'), /cr-code-like-box/)
assert.match(render('```latex\n\\notARealCommand{\n```'), /cr-code-like-box/)
const unsafe = render('$\\href{javascript:alert(1)}{click}$')
assert.doesNotMatch(unsafe, /href="javascript:/)
assert.doesNotMatch(render('before<script>window.bad = true</script>after'), /<script|window\.bad/)
assert.match(render('Waiting $x +', true), /markdown-content__loading-char/)
const table = '| a | b |\n| - | - |\n| 1 | 2 |'
const tableWithAction = (streaming: boolean): string => renderToStaticMarkup(<MarkdownContent content={table} streaming={streaming} onQuote={() => undefined} />)
assert.match(tableWithAction(true), /class="markdown-content__table-quote" disabled=""/)
assert.doesNotMatch(tableWithAction(false), /class="markdown-content__table-quote" disabled/)
const opacity = render('abcdef', true)
assert.match(opacity, /style="opacity:0\.6/)
assert.match(opacity, /style="opacity:0\.1/)
const trailingFence = render('Previous paragraph.\n\n```js\nconst x = 1', true)
assert.doesNotMatch(trailingFence, /markdown-content__loading-char/)
console.log('math-renderer-regression: math, fallback, safety, table state and opacity checks passed')
}
void main().catch((error) => { console.error(error); process.exitCode = 1 })
+156
View File
@@ -0,0 +1,156 @@
import assert from 'node:assert/strict'
import { createElement } from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { MermaidSvgCache } from '../src/renderer/src/workbuddy/mermaid/cache'
import { MermaidBlock } from '../src/renderer/src/workbuddy/mermaid/MermaidBlock'
import {
createMermaidRenderer,
type MermaidApi,
type MermaidConfiguration
} from '../src/renderer/src/workbuddy/mermaid/renderer'
import {
isWidthDependentDiagram,
normalizeMermaidCode
} from '../src/renderer/src/workbuddy/mermaid/sanitize'
import { remapSvgIdReferences } from '../src/renderer/src/workbuddy/mermaid/svg'
import {
MERMAID_ACTIVATION_ROOT_MARGIN,
MERMAID_VISIBLE_ROOT_MARGIN
} from '../src/renderer/src/workbuddy/mermaid/visibility'
import {
MERMAID_WIDTH_DEBOUNCE_MS,
shouldPublishMermaidWidth
} from '../src/renderer/src/workbuddy/mermaid/width'
const flowchart = normalizeMermaidCode('flowchart LR\nA["draft (one)"] --> B[\'two\']')
assert.equal(flowchart, 'flowchart LR\nA[draft one] --> B[two]')
const graph = normalizeMermaidCode('graph TD\nA["one"] --> B[\'two\']\nclick A "open \"details\""')
assert.match(graph, /A\[one\] --> B\[two\]/)
assert.ok(graph.includes("click A \"open 'details'\""))
const xychart = normalizeMermaidCode('xychart-beta\nx-axis "Quarter (Q1)" ["One", "Two"]')
assert.match(xychart, /x-axis ".*Quarter.*Q1.*" \[/)
assert.match(xychart, /_#40#_.*_#41#_/)
assert.equal(isWidthDependentDiagram('---\ntitle: Plan\n---\n%% note\ngantt\ndateFormat YYYY-MM-DD'), true)
assert.equal(isWidthDependentDiagram('flowchart LR\nA-->B'), false)
assert.equal(MERMAID_VISIBLE_ROOT_MARGIN, '320px')
assert.equal(MERMAID_ACTIVATION_ROOT_MARGIN, '320px 0px')
assert.equal(MERMAID_WIDTH_DEBOUNCE_MS, 360)
assert.equal(shouldPublishMermaidWidth(0, 1), true)
assert.equal(shouldPublishMermaidWidth(500, 515), false)
assert.equal(shouldPublishMermaidWidth(500, 516), true)
const ids = new Map([
['a', 'cr-mermaid-1-a'],
['long-id', 'cr-mermaid-1-long-id']
])
assert.equal(
remapSvgIdReferences('fill="url(#a)" href="#long-id" url(#a-next)', ids),
'fill="url(#cr-mermaid-1-a)" href="#cr-mermaid-1-long-id" url(#a-next)'
)
assert.equal(
remapSvgIdReferences('url(#a.b) url(#axb)', new Map([['a.b', 'safe-a-b']])),
'url(#safe-a-b) url(#axb)'
)
const lru = new MermaidSvgCache(1024, 2)
lru.set('one', '<svg>one</svg>')
lru.set('two', '<svg>two</svg>')
assert.equal(lru.get('one'), '<svg>one</svg>')
lru.set('three', '<svg>three</svg>')
assert.equal(lru.get('two'), undefined)
assert.equal(lru.size, 2)
assert.ok(lru.byteSize <= 1024)
const entryBounded = new MermaidSvgCache()
for (let index = 0; index < 65; index++) entryBounded.set(`diagram-${index}`, '<svg/>')
assert.equal(entryBounded.size, 64)
assert.equal(entryBounded.get('diagram-0'), undefined)
const oversized = new MermaidSvgCache(8, 4)
oversized.set('too-large-key', '<svg/>')
assert.equal(oversized.size, 0)
let activeRenders = 0
let maxActiveRenders = 0
const renderedCodes: string[] = []
const configurations: MermaidConfiguration[] = []
const fakeMermaid: MermaidApi = {
initialize(configuration) { configurations.push(configuration) },
async render(_id, code) {
activeRenders++
maxActiveRenders = Math.max(maxActiveRenders, activeRenders)
renderedCodes.push(code)
await new Promise((resolve) => setTimeout(resolve, 10))
activeRenders--
return { svg: `<svg>${code}</svg>` }
}
}
const render = createMermaidRenderer()
const firstController = new AbortController()
const secondController = new AbortController()
const firstRender = render({
mermaid: fakeMermaid,
code: 'gantt\ntitle first',
dark: true,
width: 540,
signal: firstController.signal
})
const secondRender = render({
mermaid: fakeMermaid,
code: 'flowchart LR\nA-->B',
dark: false,
width: 540,
signal: secondController.signal
})
secondController.abort()
assert.equal(await firstRender, '<svg>gantt\ntitle first</svg>')
assert.equal(await secondRender, undefined)
assert.deepEqual(renderedCodes, ['gantt\ntitle first'])
assert.equal(maxActiveRenders, 1)
assert.equal(configurations[0].gantt.useWidth, 540)
const cacheHitController = new AbortController()
assert.equal(await render({
mermaid: fakeMermaid,
code: 'gantt\ntitle first',
dark: true,
width: 540,
signal: cacheHitController.signal
}), '<svg>gantt\ntitle first</svg>')
assert.equal(renderedCodes.length, 1)
let releaseSlowRender: ((value: string) => void) | undefined
let slowStarted: (() => void) | undefined
const slowApi: MermaidApi = {
initialize() {},
render() {
slowStarted?.()
return new Promise<string>((resolve) => { releaseSlowRender = resolve })
}
}
const staleController = new AbortController()
const staleRenderer = createMermaidRenderer()
const staleResult = staleRenderer({
mermaid: slowApi,
code: 'flowchart LR\nA-->B',
dark: false,
width: 800,
signal: staleController.signal
})
await new Promise<void>((resolve) => { slowStarted = resolve })
staleController.abort()
releaseSlowRender?.('<svg>stale</svg>')
assert.equal(await staleResult, undefined)
const blockMarkup = renderToStaticMarkup(createElement(MermaidBlock, {
code: 'flowchart LR\nA-->B',
isRendering: false
}))
assert.match(blockMarkup, /cr-mermaid-block__tab--active/)
assert.match(blockMarkup, />图表</)
assert.match(blockMarkup, /data-mermaid-render-state="pending"/)
console.log('Mermaid cache, sanitizer, queue, and abort regression checks passed')
+121
View File
@@ -0,0 +1,121 @@
import assert from 'node:assert/strict'
import { applyAssistantEvent, getMessageParts } from '../src/shared/message-parts'
import type { ChatEvent, ChatMessage, ToolCallRecord } from '../src/shared/types'
function tool(overrides: Partial<ToolCallRecord> = {}): ToolCallRecord {
return {
id: 'tool-1',
name: 'read_file',
args: { path: 'notes.txt' },
argsRaw: '{"path":"notes.txt"}',
status: 'running',
...overrides
}
}
function reduce(message: ChatMessage, event: ChatEvent): ChatMessage {
return applyAssistantEvent(message, event)
}
function testOrderedPartsAndImmutableUpdates(): void {
const initial: ChatMessage = {
id: 'assistant-1',
role: 'assistant',
content: '',
tools: [],
parts: [],
createdAt: 1,
status: 'streaming'
}
const afterFirstText = reduce(initial, { runId: 'run-1', type: 'text', delta: 'before' })
const afterToolCall = reduce(afterFirstText, { runId: 'run-1', type: 'tool_call', call: tool() })
const afterSecondText = reduce(afterToolCall, { runId: 'run-1', type: 'text', delta: 'after' })
const afterToolResult = reduce(afterSecondText, {
runId: 'run-1',
type: 'tool_result',
call: tool({ status: 'ok', output: 'file contents', summary: 'file contents', durationMs: 12 })
})
const final = reduce(afterToolResult, {
runId: 'run-1',
type: 'done',
turnDurationMs: 22000,
content: 'beforeafter',
tools: [tool({ status: 'ok', output: 'file contents', summary: 'file contents', durationMs: 12 })]
})
assert.deepEqual(final.parts, [
{ type: 'text', text: 'before' },
{ type: 'tool', toolId: 'tool-1' },
{ type: 'text', text: 'after' }
])
assert.equal(final.content, 'beforeafter')
assert.equal(final.status, 'done')
assert.equal(final.turnDurationMs, 22000, 'terminal duration survives the shared live/persistence projection')
assert.equal(final.tools?.length, 1, 'tool updates must not create duplicate tool records')
assert.equal(final.parts?.filter((part) => part.type === 'tool' && part.toolId === 'tool-1').length, 1,
'tool updates must not create duplicate ordered references')
assert.equal(final.tools?.[0].status, 'ok')
assert.equal(final.tools?.[0].output, 'file contents')
assert.equal(initial.content, '', 'reducing events must not mutate the original message')
assert.deepEqual(initial.parts, [], 'reducing events must not mutate the original parts array')
assert.deepEqual(afterFirstText.parts, [{ type: 'text', text: 'before' }],
'later text and tool updates must not mutate prior message snapshots')
assert.equal(afterFirstText.content, 'before')
assert.deepEqual(afterToolResult.parts, [
{ type: 'text', text: 'before' },
{ type: 'tool', toolId: 'tool-1' },
{ type: 'text', text: 'after' }
])
}
function testLegacyFallback(): void {
const legacy: ChatMessage = {
id: 'legacy-1',
role: 'assistant',
content: 'legacy response',
tools: [tool({ id: 'legacy-tool', status: 'ok' })],
createdAt: 2
}
assert.deepEqual(getMessageParts(legacy), [
{ type: 'text', text: 'legacy response' },
{ type: 'tool', toolId: 'legacy-tool' }
])
}
function testTerminalToolFinalization(): void {
const initial: ChatMessage = {
id: 'assistant-terminal',
role: 'assistant',
content: 'partial answer',
tools: [tool({ output: 'retained output' })],
parts: [
{ type: 'text', text: 'partial answer' },
{ type: 'tool', toolId: 'tool-1' }
],
createdAt: 3,
status: 'streaming'
}
const cancelled = reduce(initial, { runId: 'run-cancel', type: 'cancelled' })
assert.equal(cancelled.status, 'cancelled')
assert.equal(cancelled.tools?.[0].status, 'cancelled')
assert.equal(cancelled.tools?.[0].output, 'retained output')
const errored = reduce(initial, { runId: 'run-error', type: 'error', message: 'connection lost' })
assert.equal(errored.status, 'error')
assert.equal(errored.error, 'connection lost')
assert.equal(errored.tools?.[0].status, 'error')
assert.equal(errored.tools?.[0].output, 'retained output')
assert.equal(initial.status, 'streaming', 'terminal updates must leave the prior message unchanged')
assert.equal(initial.tools?.[0].status, 'running', 'terminal updates must leave prior tool state unchanged')
assert.equal(initial.tools?.[0].output, 'retained output')
}
testOrderedPartsAndImmutableUpdates()
testLegacyFallback()
testTerminalToolFinalization()
console.log('message-order-regression: all checks passed')
+258
View File
@@ -0,0 +1,258 @@
import assert from 'node:assert/strict'
import { build } from 'esbuild'
import { mkdtemp, mkdir, readFile, writeFile, readdir, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { createServer } from 'node:http'
import type { CustomModelInput } from '../src/shared/models'
import type * as Registry from '../src/main/models'
const STATE_KEY = '__YCODE_MODEL_REGISTRY_TEST__'
const globals = globalThis as typeof globalThis & Record<string, unknown>
const previous = globals[STATE_KEY]
const temp = await mkdtemp(join(tmpdir(), 'ycode-model-registry-'))
const state = { userData: join(temp, 'userData'), encryption: true }
globals[STATE_KEY] = state
const outfile = join(temp, 'registry.mjs')
let importNumber = 0
const legacyKey = 'legacy-secret-that-must-stay-private'
const legacy = { baseUrl: 'https://legacy.example/v1', model: 'legacy-model', temperature: 0.4, maxToolRounds: 9,
apiKeyPlain: legacyKey, speechBaseUrl: 'https://speech.example/v1', speechModel: 'voice-model' }
const reload = (): Promise<typeof Registry> => import(`${pathToFileURL(outfile).href}?instance=${++importNumber}`)
const input = (id: string, extra: Partial<CustomModelInput> = {}): CustomModelInput => ({
id, name: `Name ${id}`, providerId: 'custom', model: `model-${id}`, baseUrl: 'https://provider.example/v1',
apiKey: ' new-key\n\t with-space ', supportsToolCall: true, supportsImages: true, ...extra
})
let server: ReturnType<typeof createServer> | undefined
try {
await mkdir(state.userData, { recursive: true })
const configFile = join(state.userData, 'config.json')
await writeFile(configFile, JSON.stringify(legacy))
const legacyBefore = await readFile(configFile, 'utf8')
await build({
entryPoints: [join(process.cwd(), 'src/main/models.ts')], outfile, bundle: true, platform: 'node', format: 'esm', logLevel: 'silent',
plugins: [{ name: 'model-registry-electron', setup(pluginBuild) {
pluginBuild.onResolve({ filter: /^electron$/ }, () => ({ path: 'electron', namespace: 'mock' }))
pluginBuild.onLoad({ filter: /.*/, namespace: 'mock' }, () => ({ loader: 'js', contents: `
const state = globalThis.${STATE_KEY};
export const app = { getPath: () => state.userData };
export const safeStorage = {
isEncryptionAvailable: () => state.encryption,
encryptString: value => Buffer.from('encrypted:' + value),
decryptString: value => {
const decoded = value.toString();
if (!decoded.startsWith('encrypted:')) throw new Error('Invalid cipher');
return decoded.slice(10);
}
};
` }))
} }]
})
let registry = await reload()
const missingTest = await registry.testModel(input('unsaved', { baseUrl: 'file:///private', apiKey: '' }))
assert.equal(missingTest.success, false)
assert.deepEqual((await readdir(state.userData)).sort(), ['config.json'], 'testing an unsaved model must not migrate or save any data')
const initial = await Promise.all(Array.from({ length: 8 }, () => registry.listModels()))
for (const item of initial) {
assert.equal(item.selectedModelId, 'legacy-default')
assert.equal(item.models[0].model, legacy.model)
assert.equal(item.models[0].hasApiKey, true)
assert.equal(item.models[0].keyStorage, 'encrypted')
assert.equal(JSON.stringify(item).includes(legacyKey), false)
}
assert.equal(await readFile(configFile, 'utf8'), legacyBefore, 'migration must never overwrite the legacy config')
const migrated = await registry.resolveModel()
assert.equal(migrated?.config.apiKey, legacyKey)
assert.equal(migrated?.config.temperature, 0.4)
assert.equal(migrated?.config.maxToolRounds, 9)
assert.equal(migrated?.config.speechModel, 'voice-model')
const modelsFile = join(state.userData, 'models.json')
assert.equal((await readFile(modelsFile, 'utf8')).includes(legacyKey), false)
await Promise.all(Array.from({ length: 16 }, (_, index) => registry.saveModel(input(`parallel-${index}`))))
assert.equal((await registry.listModels()).models.length, 17, 'concurrent saves must not lose profiles')
await assert.rejects(() => registry.saveModel(input('parallel-0')), /已存在/)
await registry.setSelectedModel('parallel-0')
await registry.saveModel(input('renamed', { previousId: 'parallel-0', apiKey: '', model: 'updated-model',
supportsReasoning: true, supportedEfforts: ['high', 'high', 'low'], defaultEffort: 'high',
maxInputTokens: 131072, maxOutputTokens: 32768 }))
const edited = await registry.resolveModel()
assert.equal(edited?.profile.id, 'renamed')
assert.equal(edited?.config.model, 'updated-model')
assert.equal(edited?.config.apiKey, 'new-keywith-space', 'editing an empty secret keeps the stored secret')
assert.deepEqual(edited?.profile.supportedEfforts, ['high', 'low'])
assert.equal(edited?.profile.maxInputTokens, 131072)
registry = await reload()
assert.equal((await registry.resolveModel())?.profile.id, 'renamed', 'selection survives a fresh module/application load')
await registry.setSelectedModel(null)
registry = await reload()
assert.equal((await registry.listModels()).selectedModelId, null, 'an explicit empty selection survives a restart')
await registry.setSelectedModel('renamed')
await registry.saveModel(input('renamed', { previousId: 'renamed', apiKey: '', clearApiKey: true }))
assert.equal((await registry.resolveModel('renamed'))?.profile.hasApiKey, false)
assert.equal((await registry.resolveModel('renamed'))?.config.apiKey, '')
assert.equal((await registry.testModel(input('renamed', { apiKey: '' }))).success, false)
await registry.saveModel(input('no-auth', { authMode: 'none', apiKey: undefined }))
assert.equal((await registry.resolveModel('no-auth'))?.profile.keyStorage, 'none')
const beforeInvalid = await readFile(modelsFile, 'utf8')
await assert.rejects(() => registry.saveModel(input('bad', { apiKey: '中文字' })), /ASCII/)
await assert.rejects(() => registry.saveModel(input('bad', { baseUrl: 'ftp://test.example' })), /http/)
await assert.rejects(() => registry.saveModel(input('bad', { baseUrl: 'https://user:password@example.com' })), /账户密码/)
await assert.rejects(() => registry.saveModel(input('bad', { maxInputTokens: Number.NaN })), /Token/)
await assert.rejects(() => registry.setSelectedModel('missing'), /不存在/)
assert.equal(await readFile(modelsFile, 'utf8'), beforeInvalid, 'invalid operations must not change saved profiles')
state.encryption = false
const fallback = await registry.saveModel(input('plaintext'))
assert.equal(fallback.models.find(model => model.id === 'plaintext')?.keyStorage, 'plaintext')
assert.equal(JSON.stringify(fallback).includes('new-keywith-space'), false)
state.encryption = true
let responseMode = 'success'
let responsePayload: unknown = undefined
const requests: Array<{ path: string; auth?: string; data: Record<string, unknown> }> = []
server = createServer((request, response) => {
let body = ''
request.on('data', chunk => { body += chunk })
request.on('end', () => {
requests.push({ path: request.url ?? '', auth: request.headers.authorization, data: JSON.parse(body) as Record<string, unknown> })
response.setHeader('content-type', 'application/json')
if (responseMode === 'auth') { response.statusCode = 401; response.end(JSON.stringify({ error: 'private-key-echo new-keywith-space' })); return }
if (responseMode === 'not-found') { response.statusCode = 404; response.end(JSON.stringify({ error: 'private-endpoint-echo' })); return }
if (responseMode === 'malformed') { response.end('not JSON'); return }
if (responseMode === 'wrong-shape') { response.end('{}'); return }
if (responsePayload !== undefined) { response.end(JSON.stringify(responsePayload)); return }
response.end(JSON.stringify({ choices: [{ message: { role: 'assistant', content: 'OK' } }] }))
})
})
await new Promise<void>(resolve => server!.listen(0, '127.0.0.1', resolve))
const address = server.address()
assert.ok(address && typeof address === 'object')
const endpoint = `http://127.0.0.1:${address.port}/v1`
const testInput = input('unsaved-test', { baseUrl: endpoint })
const beforeTest = await readFile(modelsFile, 'utf8')
const successful = await registry.testModel(testInput)
assert.equal(successful.success, true)
assert.ok(successful.durationMs >= 0)
assert.equal(requests[0].path, '/v1/chat/completions')
assert.equal(requests[0].auth, 'Bearer new-keywith-space')
assert.equal(requests[0].data.model, 'model-unsaved-test')
assert.equal(requests[0].data.stream, false)
assert.equal(requests[0].data.max_tokens, 16)
assert.deepEqual(requests[0].data.messages, [{ role: 'user', content: 'Reply with OK.' }])
assert.equal('input' in requests[0].data, false)
assert.equal('max_output_tokens' in requests[0].data, false)
await registry.testModel(input('anonymous-test', { baseUrl: endpoint, authMode: 'none', apiKey: undefined }))
assert.equal(requests[1].auth, undefined)
assert.equal((await registry.testModel(input('complete-chat', { baseUrl: `${endpoint}/chat/completions/` }))).success, true)
assert.equal(requests.at(-1)?.path, '/v1/chat/completions', 'complete Chat endpoints must not be appended twice')
responsePayload = { object: 'response', status: 'completed', output: [
{ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'OK', annotations: [] }] }
], error: null }
const responsesInput = input('unsaved-responses', { baseUrl: `${endpoint}/responses` })
assert.equal((await registry.testModel(responsesInput)).success, true)
const responsesRequest = requests.at(-1)!
assert.equal(responsesRequest.path, '/v1/responses', 'complete Responses endpoints must not be appended twice')
assert.equal(responsesRequest.auth, 'Bearer new-keywith-space')
assert.deepEqual(responsesRequest.data, {
model: 'model-unsaved-responses', input: 'Reply with OK.', stream: false, max_output_tokens: 16
}, 'Responses probe must use its own request schema without Chat-only fields')
assert.equal((await registry.testModel(input('responses-no-auth', {
baseUrl: `${endpoint}/responses/`, authMode: 'none', apiKey: undefined
}))).success, true)
assert.equal(requests.at(-1)?.path, '/v1/responses')
assert.equal(requests.at(-1)?.auth, undefined)
assert.equal((await registry.testModel(input('parallel-1', {
baseUrl: `${endpoint}/responses`, apiKey: ''
}))).success, true, 'unsaved endpoint edits with a blank secret reuse the saved secret')
assert.equal(requests.at(-1)?.auth, 'Bearer new-keywith-space')
assert.equal((await registry.resolveModel('parallel-1'))?.config.baseUrl, 'https://provider.example/v1', 'testing does not change the saved URL')
for (const output of [[], [{ type: 'reasoning', summary: [] }], [
{ type: 'reasoning', summary: [{ type: 'summary_text', text: 'private-upstream-summary' }] }
]]) {
responsePayload = { object: 'response', status: 'incomplete', output,
incomplete_details: { reason: 'max_output_tokens' }, error: null }
const incomplete = await registry.testModel(responsesInput)
assert.equal(incomplete.success, true, 'reasoning exhausting a minimal output budget is a valid connected response')
assert.equal(incomplete.message.includes('private-upstream-summary'), false)
}
responsePayload = { object: 'response', status: 'completed', output: [
{ type: 'message', role: 'assistant', content: [{ type: 'refusal', refusal: 'private-upstream-refusal' }] }
] }
assert.equal((await registry.testModel(responsesInput)).success, true, 'a structured refusal still proves protocol connectivity')
for (const invalid of [
null, {}, { output: 'OK' }, { output: [] }, { output: [{}] },
{ output: [{ type: 'message', role: 'assistant', content: 'private-upstream-text' }] },
{ output: [{ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 42 }] }] },
{ output: [{ type: 'reasoning', summary: 'private-upstream-text' }] },
{ status: 'failed', output: [], error: { message: 'private-key-echo new-keywith-space' } },
{ status: 'completed', output: [{ type: 'reasoning', summary: [] }], error: { message: 'private-upstream-error' } },
{ status: 'in_progress', output: [] },
{ status: 'incomplete', output: [], incomplete_details: { reason: 'invalid-provider-state' } },
{ status: 'incomplete', output: [] },
{ choices: [{ message: { content: 'Chat schema at Responses URL' } }] }
]) {
responsePayload = invalid
const invalidResult = await registry.testModel(responsesInput)
assert.equal(invalidResult.success, false, 'invalid Responses output must fail rather than accepting HTTP 200 alone')
assert.equal(invalidResult.message.includes('private-'), false)
assert.equal(invalidResult.message.includes('new-keywith-space'), false)
assert.equal(invalidResult.message.includes(endpoint), false)
}
responsePayload = undefined
responseMode = 'auth'
const authFailure = await registry.testModel(testInput)
assert.equal(authFailure.success, false)
assert.match(authFailure.message, /鉴权/)
assert.equal(authFailure.message.includes('new-keywith-space'), false, 'provider error bodies must not leak secrets')
const responsesAuthFailure = await registry.testModel(responsesInput)
assert.equal(responsesAuthFailure.success, false)
assert.match(responsesAuthFailure.message, /鉴权/)
assert.equal(responsesAuthFailure.message.includes('new-keywith-space'), false)
responseMode = 'not-found'
const responsesNotFound = await registry.testModel(responsesInput)
assert.equal(responsesNotFound.success, false)
assert.match(responsesNotFound.message, /未找到模型或接口/)
assert.equal(responsesNotFound.message.includes('private-'), false)
responseMode = 'malformed'
assert.equal((await registry.testModel(testInput)).success, false)
assert.equal((await registry.testModel(responsesInput)).success, false)
responseMode = 'wrong-shape'
assert.equal((await registry.testModel(testInput)).success, false)
assert.equal(await readFile(modelsFile, 'utf8'), beforeTest, 'tests, including successes, never save')
await new Promise<void>(resolve => server!.close(() => resolve()))
server = undefined
const networkFailure = await registry.testModel(testInput)
assert.equal(networkFailure.success, false)
assert.match(networkFailure.message, /无法连接/)
const originalFetch = globalThis.fetch
globalThis.fetch = (async (_url, init) => {
// Model a request aborted by the timeout without waiting fifteen seconds.
const signal = init?.signal
if (signal) Object.defineProperty(signal, 'aborted', { value: true })
throw new Error('contains-private-credentials')
}) as typeof fetch
try {
assert.match((await registry.testModel(testInput)).message, /超时/)
} finally { globalThis.fetch = originalFetch }
await registry.setSelectedModel('renamed')
const deleted = await registry.deleteModel('renamed')
assert.equal(deleted.selectedModelId, deleted.models[0].id)
await Promise.all(deleted.models.map(model => registry.deleteModel(model.id)))
assert.deepEqual(await registry.listModels(), { models: [], selectedModelId: null })
assert.equal(await registry.resolveModel(), null)
registry = await reload()
assert.deepEqual(await registry.listModels(), { models: [], selectedModelId: null }, 'intentionally empty registries must not remigrate legacy settings')
await assert.rejects(() => registry.resolveModel('removed'), /不存在/)
assert.deepEqual((await readdir(state.userData)).sort(), ['config.json', 'models.json'], 'atomic temporary files must be cleaned up')
await writeFile(modelsFile, '{malformed')
registry = await reload()
await assert.rejects(() => registry.listModels(), /损坏/)
assert.equal(await readFile(modelsFile, 'utf8'), '{malformed', 'corrupt registries must never be silently overwritten')
console.log('PASS model registry: migration, encrypted CRUD, concurrent writes, persistence, selection, Chat/Responses HTTP schema, incomplete reasoning, blank-secret reuse and secret-safe failures')
} finally {
if (server) await new Promise<void>(resolve => server!.close(() => resolve()))
globals[STATE_KEY] = previous
await rm(temp, { recursive: true, force: true })
}
+710
View File
@@ -0,0 +1,710 @@
import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from 'node:child_process'
import { createServer, type IncomingMessage } from 'node:http'
import { createInterface } from 'node:readline'
import { existsSync } from 'node:fs'
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { homedir, tmpdir } from 'node:os'
import { join, parse, relative, resolve, sep } from 'node:path'
import { randomUUID } from 'node:crypto'
interface Frame {
type?: string
id?: string
command?: string
success?: boolean
data?: unknown
error?: { message?: string }
[key: string]: unknown
}
interface ProbeRun {
child: ChildProcessWithoutNullStreams
frames: Frame[]
rawLines: string[]
stderrLines: string[]
}
interface MockRequest {
model?: string
stream: boolean
roles: string[]
toolNames: string[]
toolProperties: Record<string, string[]>
containsImage: boolean
messageShape: Array<Record<string, unknown>>
}
const STARTUP_WAIT_MS = 12_000
const COMMAND_WAIT_MS = 20_000
const SHUTDOWN_WAIT_MS = 1_500
const PROVIDER_ID = 'loopback-probe'
const MODEL_ID = 'probe-model'
const MODEL = `${PROVIDER_ID}/${MODEL_ID}`
function delay(ms: number): Promise<void> {
return new Promise((resolveDelay) => setTimeout(resolveDelay, ms))
}
function makeIsolatedEnvironment(home: string, tempRoot: string, profile: string): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = {}
for (const name of ['PATH', 'SystemRoot', 'SystemDrive', 'ComSpec', 'WINDIR']) {
const value = process.env[name]
if (value) env[name] = value
}
const root = parse(home).root
env.USERPROFILE = home
env.HOME = home
env.HOMEDRIVE = root.replace(/[\\/]$/, '')
env.HOMEPATH = relative(root, home).replaceAll('/', '\\')
env.APPDATA = join(home, 'AppData', 'Roaming')
env.LOCALAPPDATA = join(home, 'AppData', 'Local')
env.TEMP = tempRoot
env.TMP = tempRoot
env.OMP_PROFILE = profile
return env
}
function summarizeFrame(frame: Frame): Record<string, unknown> {
const data = frame.data
const dataKeys = data && typeof data === 'object' ? Object.keys(data as object) : []
const event = frame.assistantMessageEvent
const eventKeys = event && typeof event === 'object' ? Object.keys(event as object) : []
return {
type: frame.type,
frameKeys: Object.keys(frame),
id: frame.id,
command: frame.command,
success: frame.success,
status: frame.status,
assistantEventType: event && typeof event === 'object' ? (event as Record<string, unknown>).type : undefined,
toolCallId: frame.toolCallId,
toolName: frame.toolName,
argumentKeys: frame.arguments && typeof frame.arguments === 'object' ? Object.keys(frame.arguments as object) : [],
eventKeys,
dataKeys,
error: typeof frame.error === 'string' ? frame.error : frame.error?.message
}
}
async function waitForFrame(
run: ProbeRun,
predicate: (frame: Frame) => boolean,
timeoutMs = COMMAND_WAIT_MS,
startIndex = 0
): Promise<Frame | null> {
const deadline = Date.now() + timeoutMs
while (Date.now() < deadline) {
const frame = run.frames.slice(startIndex).find(predicate)
if (frame) return frame
if (run.child.exitCode !== null || run.child.signalCode !== null) break
await delay(20)
}
return run.frames.slice(startIndex).find(predicate) ?? null
}
async function sendCommand(run: ProbeRun, id: string, command: Record<string, unknown>): Promise<Frame | null> {
run.child.stdin.write(`${JSON.stringify({ id, ...command })}\n`)
return waitForFrame(run, (frame) => frame.id === id)
}
async function sendLooseCommand(run: ProbeRun, id: string, command: Record<string, unknown>): Promise<Frame | null> {
const startIndex = run.frames.length
run.child.stdin.write(`${JSON.stringify({ id, ...command })}\n`)
return waitForFrame(run, (frame) => frame.id === id || frame.type === 'response', 3_000, startIndex)
}
async function installProbeHostTool(run: ProbeRun, suffix: string): Promise<Frame | null> {
return sendCommand(run, `probe-host-tools-${suffix}`, {
type: 'set_host_tools',
tools: [{
name: 'read',
label: 'Read local probe file',
description: 'Read a file from the isolated protocol probe directory.',
parameters: {
type: 'object',
properties: { path: { type: 'string' } },
required: ['path'],
additionalProperties: false
}
}]
})
}
async function stopRun(run: ProbeRun): Promise<void> {
try {
run.child.stdin.end()
} catch {
// The sidecar may already have exited.
}
await new Promise<void>((resolveExit) => {
if (run.child.exitCode !== null || run.child.signalCode !== null) {
resolveExit()
return
}
let settled = false
const finish = (): void => {
if (settled) return
settled = true
clearTimeout(timer)
resolveExit()
}
const timer = setTimeout(finish, SHUTDOWN_WAIT_MS)
run.child.once('exit', finish)
})
if (run.child.exitCode === null && run.child.signalCode === null) {
run.child.kill()
await new Promise<void>((resolveExit) => {
if (run.child.exitCode !== null || run.child.signalCode !== null) resolveExit()
else run.child.once('exit', () => resolveExit())
})
}
}
async function startRun(
exePath: string,
tempRoot: string,
profile: string,
home: string,
sessionDir: string,
sessionPath: string
): Promise<ProbeRun> {
const args = [
'--mode', 'rpc',
'--profile', profile,
'--cwd', tempRoot,
`--model=${MODEL}`,
'--no-tools',
`--session-dir=${sessionDir}`,
`--resume=${sessionPath}`,
'--no-ui',
'--no-title',
'--no-extensions',
'--no-skills',
'--no-rules',
'--no-lsp'
]
const child = spawn(exePath, args, {
cwd: tempRoot,
env: makeIsolatedEnvironment(home, tempRoot, profile),
windowsHide: true,
stdio: ['pipe', 'pipe', 'pipe']
})
const run: ProbeRun = { child, frames: [], rawLines: [], stderrLines: [] }
const stdout = createInterface({ input: child.stdout })
const stderr = createInterface({ input: child.stderr })
stdout.on('line', (line) => {
run.rawLines.push(line)
try {
run.frames.push(JSON.parse(line) as Frame)
} catch {
// Keep non-JSON stdout separate from RPC frames.
}
})
stderr.on('line', (line) => run.stderrLines.push(line))
const ready = await waitForFrame(run, (frame) => frame.type === 'ready', STARTUP_WAIT_MS)
if (!ready) {
const stderrText = run.stderrLines.join('\n').slice(0, 1000)
await stopRun(run)
throw new Error(`OMP did not send ready; exit=${child.exitCode}; stderr=${stderrText}`)
}
return run
}
async function readRequest(request: IncomingMessage): Promise<Record<string, unknown>> {
const chunks: Buffer[] = []
for await (const chunk of request) chunks.push(Buffer.from(chunk))
try {
return JSON.parse(Buffer.concat(chunks).toString('utf8')) as Record<string, unknown>
} catch {
return {}
}
}
function toolNamesFrom(body: Record<string, unknown>): string[] {
if (!Array.isArray(body.tools)) return []
return body.tools.flatMap((tool) => {
if (!tool || typeof tool !== 'object') return []
const item = tool as Record<string, unknown>
const fn = item.function && typeof item.function === 'object' ? item.function as Record<string, unknown> : null
const name = fn?.name ?? item.name
return typeof name === 'string' ? [name] : []
})
}
function requestHasImage(body: Record<string, unknown>): boolean {
if (!Array.isArray(body.messages)) return false
return body.messages.some((message) => {
if (!message || typeof message !== 'object') return false
const content = (message as Record<string, unknown>).content
return Array.isArray(content) && content.some((part) => {
if (!part || typeof part !== 'object') return false
const item = part as Record<string, unknown>
return item.type === 'image_url' || item.type === 'image'
})
})
}
async function startMockProvider(probeFile: string): Promise<{
server: ReturnType<typeof createServer>
url: string
requests: MockRequest[]
toolCallSent: boolean
}> {
const requests: MockRequest[] = []
let toolCallSent = false
const server = createServer(async (request, response) => {
const body = await readRequest(request)
const names = toolNamesFrom(body)
const messages = Array.isArray(body.messages) ? body.messages as Array<Record<string, unknown>> : []
const latestUser = [...messages].reverse().find((message) => message.role === 'user')
const latestUserContent = Array.isArray(latestUser?.content)
? (latestUser.content as Array<Record<string, unknown>>).map((block) => block.text).filter((value) => typeof value === 'string').join('\n')
: typeof latestUser?.content === 'string' ? latestUser.content : ''
const toolProperties: Record<string, string[]> = {}
for (const tool of Array.isArray(body.tools) ? body.tools : []) {
if (!tool || typeof tool !== 'object') continue
const item = tool as Record<string, unknown>
const fn = item.function && typeof item.function === 'object' ? item.function as Record<string, unknown> : null
const name = fn?.name ?? item.name
const parameters = fn?.parameters && typeof fn.parameters === 'object' ? fn.parameters as Record<string, unknown> : null
const properties = parameters?.properties && typeof parameters.properties === 'object'
? parameters.properties as Record<string, unknown> : null
if (typeof name === 'string') toolProperties[name] = Object.keys(properties ?? {})
}
requests.push({
model: typeof body.model === 'string' ? body.model : undefined,
stream: body.stream === true,
roles: messages.map((message) => typeof message.role === 'string' ? message.role : '?'),
toolNames: names,
toolProperties,
containsImage: requestHasImage(body),
messageShape: messages.map((message) => {
const content = message.content
const blocks = Array.isArray(content) ? content as Array<Record<string, unknown>> : []
const calls = Array.isArray(message.tool_calls) ? message.tool_calls as Array<Record<string, unknown>> : []
return {
role: message.role,
contentType: typeof content === 'string' ? 'string' : Array.isArray(content) ? 'array' : typeof content,
contentBlockTypes: blocks.map((block) => block.type),
toolCallCount: calls.length,
hasToolCallId: typeof message.tool_call_id === 'string'
}
})
})
const readToolName = names.find((name) => name === 'read' || name.endsWith('__read'))
const sendReadTool = latestUserContent.includes('TOOL_PROBE') && readToolName && !toolCallSent
const completion = sendReadTool
? {
message: {
role: 'assistant',
content: '',
tool_calls: [{
id: 'call_local_read_1',
type: 'function',
function: { name: readToolName, arguments: JSON.stringify({ path: probeFile }) }
}]
},
finish_reason: 'tool_calls'
}
: {
message: { role: 'assistant', content: 'PROBE_OK' },
finish_reason: 'stop'
}
if (sendReadTool) toolCallSent = true
if (body.stream === true) {
response.writeHead(200, {
'content-type': 'text/event-stream',
'cache-control': 'no-cache',
connection: 'keep-alive'
})
const completionId = `chatcmpl-local-${requests.length}`
const writeChunk = (delta: Record<string, unknown>, finishReason: string | null = null): void => {
response.write(`data: ${JSON.stringify({
id: completionId,
object: 'chat.completion.chunk',
created: Math.floor(Date.now() / 1000),
model: MODEL_ID,
choices: [{ index: 0, delta, finish_reason: finishReason }]
})}\n\n`)
}
writeChunk({ role: 'assistant' })
if (sendReadTool) {
writeChunk({ tool_calls: [{
index: 0,
id: 'call_local_read_1',
type: 'function',
function: { name: readToolName, arguments: JSON.stringify({ path: probeFile }) }
}] })
writeChunk({}, 'tool_calls')
} else {
writeChunk({ reasoning_content: 'LOCAL_THINKING_PROBE' })
writeChunk({ content: 'PROBE_OK' })
writeChunk({}, 'stop')
}
response.write(`data: ${JSON.stringify({
id: completionId,
object: 'chat.completion.chunk',
created: Math.floor(Date.now() / 1000),
model: MODEL_ID,
choices: [],
usage: { prompt_tokens: 17, completion_tokens: 4, total_tokens: 21 }
})}\n\n`)
response.end('data: [DONE]\n\n')
return
}
response.writeHead(200, { 'content-type': 'application/json' })
response.end(JSON.stringify({
id: `chatcmpl-local-${requests.length}`,
object: 'chat.completion',
created: Math.floor(Date.now() / 1000),
model: MODEL_ID,
choices: [{ index: 0, ...completion }],
usage: { prompt_tokens: 17, completion_tokens: 4, total_tokens: 21 }
}))
})
await new Promise<void>((resolveListen, reject) => {
server.once('error', reject)
server.listen(0, '127.0.0.1', () => {
server.off('error', reject)
resolveListen()
})
})
const address = server.address()
if (!address || typeof address === 'string') throw new Error('Loopback mock did not bind to a TCP port')
return { server, url: `http://127.0.0.1:${address.port}/v1`, requests, get toolCallSent() { return toolCallSent } }
}
function summarizeMessages(data: unknown): Record<string, unknown> {
const root = data && typeof data === 'object' ? data as Record<string, unknown> : {}
const possibleArrays = [root.messages, root.entries, root.branchMessages]
const records = possibleArrays.find(Array.isArray) as unknown[] | undefined
const items = (records ?? (Array.isArray(data) ? data : [])).map((value) => {
if (!value || typeof value !== 'object') return { valueType: typeof value }
const item = value as Record<string, unknown>
const message = item.message && typeof item.message === 'object' ? item.message as Record<string, unknown> : item
const content = Array.isArray(message.content) ? message.content as Array<Record<string, unknown>> : []
const toolCalls = Array.isArray(message.toolCalls) ? message.toolCalls as Array<Record<string, unknown>>
: Array.isArray(message.tool_calls) ? message.tool_calls as Array<Record<string, unknown>> : []
return {
itemKeys: Object.keys(item),
messageKeys: Object.keys(message),
type: item.type,
role: message.role,
entryId: item.entryId ?? item.id,
parentId: item.parentId,
textBlocks: content.filter((block) => block.type === 'text').length,
toolCalls: toolCalls.map((call) => call.name ?? (call.function as Record<string, unknown> | undefined)?.name)
}
})
return { keys: Object.keys(root), count: items.length, messages: items }
}
async function inspectTranscript(path: string): Promise<Record<string, unknown>> {
try {
const text = await readFile(path, 'utf8')
const records = text.split(/\r?\n/).filter(Boolean).map((record) => JSON.parse(record) as Record<string, unknown>)
return {
exists: true,
records: records.length,
entries: records.map((record) => ({
type: record.type,
id: record.id,
parentId: record.parentId,
messageRole: (record.message as Record<string, unknown> | undefined)?.role,
toolName: (record.toolName ?? (record.message as Record<string, unknown> | undefined)?.toolName) ?? undefined
}))
}
} catch {
return { exists: false }
}
}
function summarizeTree(value: unknown): unknown {
if (Array.isArray(value)) return value.map(summarizeTree)
if (!value || typeof value !== 'object') return value
const node = value as Record<string, unknown>
const entry = node.entry && typeof node.entry === 'object' ? node.entry as Record<string, unknown> : node
const message = entry.message && typeof entry.message === 'object' ? entry.message as Record<string, unknown> : null
return {
nodeKeys: Object.keys(node),
entryKeys: Object.keys(entry),
id: entry.id,
parentId: entry.parentId,
type: entry.type,
role: message?.role,
label: node.label,
children: Array.isArray(node.children) ? summarizeTree(node.children) : []
}
}
function findUserNode(value: unknown): { id: string; parentId: string | null } | null {
if (Array.isArray(value)) {
let latest: { id: string; parentId: string | null } | null = null
for (const child of value) {
const found = findUserNode(child)
if (found) latest = found
}
return latest
}
if (!value || typeof value !== 'object') return null
const node = value as Record<string, unknown>
const entry = node.entry && typeof node.entry === 'object' ? node.entry as Record<string, unknown> : node
const message = entry.message && typeof entry.message === 'object' ? entry.message as Record<string, unknown> : null
let latest: { id: string; parentId: string | null } | null = null
if (entry.type === 'message' && message?.role === 'user' && typeof entry.id === 'string') {
latest = { id: entry.id, parentId: typeof entry.parentId === 'string' ? entry.parentId : null }
}
const child = Array.isArray(node.children) ? findUserNode(node.children) : null
return child ?? latest
}
async function main(): Promise<void> {
const exePath = process.env.YCODE_OMP_PATH ?? 'C:\\Users\\Administrator\\AppData\\Local\\Ygent\\omp.exe'
if (!existsSync(exePath)) throw new Error(`OMP executable not found: ${exePath}`)
const tempRoot = await mkdtemp(join(tmpdir(), 'ycode-omp-protocol-'))
const home = join(tempRoot, 'isolated-home')
const sessionDir = join(tempRoot, 'sessions')
const profile = `protocol-probe-${randomUUID().replaceAll('-', '')}`
const sessionPath = join(sessionDir, 'probe.jsonl')
const probeFile = join(tempRoot, 'probe.txt')
const realProfilePath = resolve(homedir(), '.omp', 'profiles', profile)
const realProfileRoot = resolve(homedir(), '.omp', 'profiles')
const realProfileRelative = relative(realProfileRoot, realProfilePath)
if (!realProfileRelative || realProfileRelative.startsWith(`..${sep}`) || resolve(realProfileRoot, realProfileRelative) !== realProfilePath) {
throw new Error('Refusing an invalid isolated profile path')
}
if (existsSync(realProfilePath)) throw new Error('The unique probe profile unexpectedly exists')
await mkdir(home, { recursive: true })
await mkdir(sessionDir, { recursive: true })
await writeFile(probeFile, 'LOCAL_OMP_TOOL_SENTINEL\n', 'utf8')
const profileAgentDir = join(home, '.omp', 'profiles', profile, 'agent')
await mkdir(profileAgentDir, { recursive: true })
const mock = await startMockProvider(probeFile)
await writeFile(join(profileAgentDir, 'models.yml'), [
'providers:',
` ${PROVIDER_ID}:`,
` baseUrl: ${mock.url}`,
' api: openai-completions',
' auth: none',
' models:',
` - id: ${MODEL_ID}`,
' name: Local protocol probe',
' reasoning: true',
' input: [text, image]',
' contextWindow: 64000',
' maxTokens: 4096',
''
].join('\n'), 'utf8')
const runs: ProbeRun[] = []
try {
const version = spawnSync(exePath, ['--version'], {
encoding: 'utf8',
windowsHide: true,
env: makeIsolatedEnvironment(home, tempRoot, profile)
})
if (version.error) throw version.error
if (version.status !== 0) throw new Error(`OMP --version failed with exit ${version.status}`)
console.log(`OMP_VERSION: ${version.stdout.trim()}`)
console.log(`ISOLATED_PROFILE: ${profile}`)
console.log('PROVIDER_NETWORK: loopback only')
const first = await startRun(exePath, tempRoot, profile, home, sessionDir, sessionPath)
runs.push(first)
const hostToolsFirst = await installProbeHostTool(first, '1')
const stateResponse = await sendCommand(first, 'probe-state-1', { type: 'get_state' })
console.log(`FIRST_READY: true`)
console.log(`SET_HOST_TOOLS: ${JSON.stringify(hostToolsFirst ? summarizeFrame(hostToolsFirst) : null)}`)
console.log(`GET_STATE: ${JSON.stringify(stateResponse ? summarizeFrame(stateResponse) : null)}`)
const image = {
type: 'image' as const,
data: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/dXEAAAAASUVORK5CYII=',
mimeType: 'image/png'
}
first.child.stdin.write(`${JSON.stringify({
id: 'probe-prompt-1',
type: 'prompt',
message: 'Reply exactly PROBE_OK.',
images: [image]
})}\n`)
const promptAck = await waitForFrame(first, (frame) => frame.id === 'probe-prompt-1')
const turnDone = await waitForFrame(first, (frame) =>
frame.type === 'agent_end' || (frame.type === 'prompt_result' && frame.agentInvoked === false), 60_000)
const toolTurnStart = first.frames.length
first.child.stdin.write(`${JSON.stringify({
id: 'probe-prompt-tool',
type: 'prompt',
message: 'TOOL_PROBE: call the read tool for probe.txt, then reply exactly PROBE_OK.'
})}\n`)
const toolPromptAck = await waitForFrame(first, (frame) => frame.id === 'probe-prompt-tool')
const hostToolCall = await waitForFrame(first, (frame) => frame.type === 'host_tool_call', 20_000, toolTurnStart)
let hostToolResponseSent = false
if (hostToolCall && typeof hostToolCall.id === 'string') {
const args = hostToolCall.arguments && typeof hostToolCall.arguments === 'object'
? hostToolCall.arguments as Record<string, unknown> : {}
const targetPath = typeof args.path === 'string' ? args.path : probeFile
const content = await readFile(targetPath, 'utf8')
first.child.stdin.write(`${JSON.stringify({
type: 'host_tool_result',
id: hostToolCall.id,
result: { content: [{ type: 'text', text: content }] }
})}\n`)
hostToolResponseSent = true
}
const toolTurnDone = await waitForFrame(first, (frame) =>
frame.type === 'agent_end' || (frame.type === 'prompt_result' && frame.agentInvoked === false), 60_000, toolTurnStart)
const getMessages = await sendCommand(first, 'probe-messages-1', { type: 'get_messages' })
const tree = await sendCommand(first, 'probe-tree-1', { type: 'get_tree' })
const branchMessages = await sendCommand(first, 'probe-branch-messages-1', { type: 'get_branch_messages' })
const stateAfterTurn = await sendCommand(first, 'probe-state-after-turn', { type: 'get_state' })
const entries = getMessages ? summarizeMessages(getMessages.data) : null
const treeData = tree?.data && typeof tree.data === 'object' ? tree.data as Record<string, unknown> : {}
const treeShape = summarizeTree(treeData.tree)
const firstUserNode = findUserNode(treeData.tree)
const fork = firstUserNode
? await sendCommand(first, 'probe-fork-before-branch', { type: 'fork', entryId: firstUserNode.id })
: null
const stateAfterFork = await sendCommand(first, 'probe-state-after-fork', { type: 'get_state' })
const forkStateData = stateAfterFork?.data && typeof stateAfterFork.data === 'object'
? stateAfterFork.data as Record<string, unknown> : {}
const forkSessionPath = typeof forkStateData.sessionFile === 'string' ? forkStateData.sessionFile : ''
const relativeForkSession = forkSessionPath ? relative(tempRoot, resolve(forkSessionPath)) : ''
const forkSessionInProbe = !!relativeForkSession && relativeForkSession !== '..' &&
!relativeForkSession.startsWith(`..${sep}`) && !relativeForkSession.startsWith(sep)
const messagesAfterFork = await sendCommand(first, 'probe-messages-after-fork', { type: 'get_messages' })
const treeAfterFork = await sendCommand(first, 'probe-tree-after-fork', { type: 'get_tree' })
const treeAfterForkData = treeAfterFork?.data && typeof treeAfterFork.data === 'object'
? treeAfterFork.data as Record<string, unknown> : {}
const forkData = fork?.data && typeof fork.data === 'object' ? fork.data as Record<string, unknown> : {}
const stateBeforeForkData = stateAfterTurn?.data && typeof stateAfterTurn.data === 'object'
? stateAfterTurn.data as Record<string, unknown> : {}
console.log(`PROMPT_ACK: ${JSON.stringify(promptAck ? summarizeFrame(promptAck) : null)}`)
console.log(`FIRST_TURN_END_FRAME: ${JSON.stringify(turnDone ? summarizeFrame(turnDone) : null)}`)
console.log(`TOOL_PROMPT_ACK: ${JSON.stringify(toolPromptAck ? summarizeFrame(toolPromptAck) : null)}`)
console.log(`HOST_TOOL_CALL: ${JSON.stringify(hostToolCall ? summarizeFrame(hostToolCall) : null)}`)
console.log(`HOST_TOOL_RESPONSE_SENT: ${hostToolResponseSent}`)
console.log(`TOOL_TURN_END_FRAME: ${JSON.stringify(toolTurnDone ? summarizeFrame(toolTurnDone) : null)}`)
console.log(`TURN_EVENTS: ${JSON.stringify(first.frames.filter((frame) => [
'agent_start', 'message_start', 'message_update', 'message_end', 'tool_execution_start',
'tool_execution_update', 'tool_execution_end', 'host_tool_call', 'turn_start', 'turn_end', 'agent_end', 'prompt_result'
].includes(frame.type ?? '')).map(summarizeFrame))}`)
console.log(`GET_MESSAGES_RESPONSE: ${JSON.stringify(getMessages ? {
...summarizeFrame(getMessages),
dataSummary: entries
} : null)}`)
console.log(`GET_TREE_RESPONSE: ${JSON.stringify(tree ? {
...summarizeFrame(tree), leafId: treeData.leafId, tree: treeShape
} : null)}`)
console.log(`GET_BRANCH_MESSAGES_RESPONSE: ${JSON.stringify(branchMessages ? {
...summarizeFrame(branchMessages), dataSummary: summarizeMessages(branchMessages.data)
} : null)}`)
console.log(`FORK_ENTRY_ID: ${JSON.stringify(firstUserNode?.id ?? null)}`)
console.log(`FORK_RESPONSE: ${JSON.stringify(fork ? summarizeFrame(fork) : null)}`)
console.log(`FORK_CANCELLED: ${forkData.cancelled}`)
console.log(`FORK_STATE: ${JSON.stringify(stateAfterFork ? summarizeFrame(stateAfterFork) : null)}`)
console.log(`FORK_SESSION_ID_CHANGED: ${stateBeforeForkData.sessionId !== forkStateData.sessionId}`)
console.log(`FORK_SESSION_FILE_IS_PROBE_PATH: ${forkSessionInProbe}`)
console.log(`FORK_SESSION_FILE_EXISTS: ${forkSessionInProbe ? existsSync(forkSessionPath) : false}`)
console.log(`FORK_MESSAGES: ${JSON.stringify(messagesAfterFork ? summarizeMessages(messagesAfterFork.data) : null)}`)
console.log(`FORK_TREE_LEAF_ID: ${treeAfterForkData.leafId ?? null}`)
console.log(`TRANSCRIPT_AFTER_FORK: ${JSON.stringify(await inspectTranscript(sessionPath))}`)
console.log(`STATE_AFTER_TURN: ${JSON.stringify(stateAfterTurn ? summarizeFrame(stateAfterTurn) : null)}`)
console.log(`LOOPBACK_REQUESTS: ${JSON.stringify(mock.requests)}`)
console.log(`TOOL_CALL_SENT: ${mock.toolCallSent}`)
console.log(`TRANSCRIPT_AFTER_TURN: ${JSON.stringify(await inspectTranscript(sessionPath))}`)
await stopRun(first)
const second = await startRun(exePath, tempRoot, profile, home, sessionDir, sessionPath)
runs.push(second)
const hostToolsSecond = await installProbeHostTool(second, '2')
const resumedState = await sendCommand(second, 'probe-state-2', { type: 'get_state' })
const resumedMessages = await sendCommand(second, 'probe-messages-2', { type: 'get_messages' })
console.log(`RESUME_READY: true`)
console.log(`RESUME_SET_HOST_TOOLS: ${JSON.stringify(hostToolsSecond ? summarizeFrame(hostToolsSecond) : null)}`)
console.log(`RESUME_GET_STATE: ${JSON.stringify(resumedState ? summarizeFrame(resumedState) : null)}`)
console.log(`RESUME_GET_MESSAGES: ${JSON.stringify(resumedMessages ? {
...summarizeFrame(resumedMessages),
dataSummary: summarizeMessages(resumedMessages.data)
} : null)}`)
console.log(`RESUMED_TRANSCRIPT: ${JSON.stringify(await inspectTranscript(sessionPath))}`)
let currentRun: ProbeRun = second
const resumeTree = await sendCommand(second, 'probe-tree-2', { type: 'get_tree' })
const resumeTreeData = resumeTree?.data && typeof resumeTree.data === 'object' ? resumeTree.data as Record<string, unknown> : {}
const resumeUserNode = findUserNode(resumeTreeData.tree)
console.log(`RESUME_TREE: ${JSON.stringify(resumeTree ? {
...summarizeFrame(resumeTree), leafId: resumeTreeData.leafId, tree: summarizeTree(resumeTreeData.tree)
} : null)}`)
if (resumeUserNode) {
const branch = await sendCommand(second, 'probe-branch-1', { type: 'branch', entryId: resumeUserNode.id })
const branchState = await sendCommand(second, 'probe-state-after-branch', { type: 'get_state' })
const branchHistory = await sendCommand(second, 'probe-messages-after-branch', { type: 'get_messages' })
const branchTree = await sendCommand(second, 'probe-tree-after-branch', { type: 'get_tree' })
const branchTreeData = branchTree?.data && typeof branchTree.data === 'object' ? branchTree.data as Record<string, unknown> : {}
console.log(`BRANCH_ENTRY_ID: ${JSON.stringify(resumeUserNode.id)}`)
console.log(`BRANCH_RESPONSE: ${JSON.stringify(branch ? summarizeFrame(branch) : null)}`)
console.log(`BRANCH_STATE: ${JSON.stringify(branchState ? summarizeFrame(branchState) : null)}`)
console.log(`BRANCH_MESSAGES: ${JSON.stringify(branchHistory ? {
...summarizeFrame(branchHistory),
dataSummary: summarizeMessages(branchHistory.data)
} : null)}`)
console.log(`BRANCH_TREE: ${JSON.stringify(branchTree ? {
...summarizeFrame(branchTree), leafId: branchTreeData.leafId, tree: summarizeTree(branchTreeData.tree)
} : null)}`)
if (branch?.success === true) {
await stopRun(second)
const branchResume = await startRun(exePath, tempRoot, profile, home, sessionDir, sessionPath)
runs.push(branchResume)
currentRun = branchResume
const resumeHostTools = await installProbeHostTool(branchResume, 'after-branch')
const messagesAfterBranchResume = await sendCommand(branchResume, 'probe-messages-after-branch-resume', { type: 'get_messages' })
const treeAfterBranchResume = await sendCommand(branchResume, 'probe-tree-after-branch-resume', { type: 'get_tree' })
const treeAfterBranchResumeData = treeAfterBranchResume?.data && typeof treeAfterBranchResume.data === 'object'
? treeAfterBranchResume.data as Record<string, unknown> : {}
console.log(`BRANCH_RESUME_READY: true`)
console.log(`BRANCH_RESUME_SET_HOST_TOOLS: ${JSON.stringify(resumeHostTools ? summarizeFrame(resumeHostTools) : null)}`)
console.log(`BRANCH_RESUME_MESSAGES: ${JSON.stringify(messagesAfterBranchResume ? summarizeMessages(messagesAfterBranchResume.data) : null)}`)
console.log(`BRANCH_RESUME_TREE_LEAF_ID: ${treeAfterBranchResumeData.leafId ?? null}`)
console.log(`TRANSCRIPT_AFTER_BRANCH_WITHOUT_PROMPT: ${JSON.stringify(await inspectTranscript(sessionPath))}`)
}
const navigateTree = await sendLooseCommand(currentRun, 'probe-navigate-tree', {
type: 'navigate_tree', entryId: resumeUserNode.id
})
const navigateToEntry = await sendLooseCommand(currentRun, 'probe-navigate-to-entry', {
type: 'navigate_to_entry', entryId: resumeUserNode.id
})
const switchSession = await sendCommand(currentRun, 'probe-switch-session', {
type: 'switch_session', sessionPath
})
const clear = await sendLooseCommand(currentRun, 'probe-clear', { type: 'clear' })
console.log(`NAVIGATE_TREE_RESPONSE: ${JSON.stringify(navigateTree ? summarizeFrame(navigateTree) : null)}`)
console.log(`NAVIGATE_TO_ENTRY_RESPONSE: ${JSON.stringify(navigateToEntry ? summarizeFrame(navigateToEntry) : null)}`)
console.log(`SWITCH_SESSION_SAME_PATH: ${JSON.stringify(switchSession ? summarizeFrame(switchSession) : null)}`)
console.log(`CLEAR_RESPONSE: ${JSON.stringify(clear ? summarizeFrame(clear) : null)}`)
} else {
console.log('BRANCH_ENTRY_ID: unavailable from get_tree parent relation')
}
await stopRun(currentRun)
} finally {
for (const run of runs) await stopRun(run)
await new Promise<void>((resolveClose) => mock.server.close(() => resolveClose()))
await rm(tempRoot, { recursive: true, force: true })
if (existsSync(realProfilePath)) await rm(realProfilePath, { recursive: true, force: true })
}
}
main().catch((error: unknown) => {
console.error('OMP protocol probe failed:', error)
process.exitCode = 1
})
+90
View File
@@ -0,0 +1,90 @@
import assert from 'node:assert/strict'
import { EventEmitter } from 'node:events'
import { runOmpTurn } from '../src/main/agent/omp-turn'
import type { OmpKernel } from '../src/main/omp'
import type { ChatEvent } from '../src/shared/types'
class Kernel extends EventEmitter {
abortCalls = 0
disposeCalls = 0
rejectAbort = false
async prompt(): Promise<{ type: string; success: boolean }> { return { type: 'response', success: true } }
async abort(): Promise<void> { this.abortCalls++; if (this.rejectAbort) throw new Error('abort unavailable') }
async dispose(): Promise<void> { this.disposeCalls++ }
}
function begin(kernel = new Kernel(), controller = new AbortController()) {
const events: ChatEvent[] = []
const outcome = runOmpTurn({ kernel: kernel as unknown as OmpKernel, runId: 'provider-error',
userText: 'fixture', signal: controller.signal, emit: event => events.push(event) })
.then(value => ({ value, error: undefined }), (error: Error) => ({ value: undefined, error }))
return { kernel, controller, events, outcome }
}
const errorMessage = { role: 'assistant', stopReason: 'error', content: [], errorMessage: 'Unable to connect. Is the computer able to access the url?' }
const emit = (kernel: Kernel, frame: Record<string, unknown>): void => { kernel.emit('event', frame) }
const delay = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 15))
{
const test = begin()
emit(test.kernel, { type: 'message_end', message: errorMessage })
const result = await test.outcome
assert.equal(result.error?.message, errorMessage.errorMessage)
assert.equal(test.kernel.abortCalls, 1)
assert.equal(test.events.filter(event => event.type === 'error').length, 1)
emit(test.kernel, { type: 'agent_end', yielded: false, messages: [errorMessage] })
emit(test.kernel, { type: 'agent_end', yielded: true })
assert.equal(test.events.filter(event => ['done', 'cancelled'].includes(event.type)).length, 0)
assert.equal(test.kernel.listenerCount('event'), 0)
assert.equal(test.kernel.listenerCount('error'), 0)
}
{
const test = begin()
emit(test.kernel, { type: 'agent_end', yielded: false, messages: [{ role: 'user' }, errorMessage] })
assert.equal((await test.outcome).error?.message, errorMessage.errorMessage)
assert.equal(test.kernel.abortCalls, 1)
}
{
const test = begin()
emit(test.kernel, { type: 'message_end', message: { ...errorMessage, errorMessage: 'Failed https://account:secret@example.invalid/v1?key=url-secret API Key: field-secret Authorization: Bearer bearer-secret sk-plainsecret123' } })
const message = (await test.outcome).error!.message
for (const secret of ['account', 'secret@example', 'url-secret', 'field-secret', 'bearer-secret', 'plainsecret123']) assert.equal(message.includes(secret), false)
assert.match(message, /已隐藏/)
}
{
const test = begin()
test.controller.abort()
emit(test.kernel, { type: 'message_end', message: errorMessage })
assert.equal((await test.outcome).value?.status, 'cancelled')
assert.equal(test.events.some(event => event.type === 'error'), false)
assert.equal(test.events.filter(event => event.type === 'cancelled').length, 1)
assert.equal(test.kernel.abortCalls, 1)
}
{
const test = begin()
let ended = false
void test.outcome.then(() => { ended = true })
// There is no first-byte deadline: long thinking and tool-level failures remain live.
emit(test.kernel, { type: 'message_update', assistantMessageEvent: { type: 'thinking_delta', delta: 'thinking', contentIndex: 0 } })
emit(test.kernel, { type: 'message_end', message: { ...errorMessage, role: 'toolResult' } })
emit(test.kernel, { type: 'tool_execution_start', toolCallId: 'tool', toolName: 'shell', args: {} })
emit(test.kernel, { type: 'tool_execution_end', toolCallId: 'tool', toolName: 'shell', isError: true, result: { content: [{ type: 'text', text: 'command failed' }] } })
emit(test.kernel, { type: 'agent_end', yielded: false, messages: [{ role: 'assistant', stopReason: 'toolUse' }] })
await delay()
assert.equal(ended, false)
assert.equal(test.kernel.abortCalls, 0)
assert.equal(test.events.some(event => event.type === 'error'), false)
emit(test.kernel, { type: 'message_update', assistantMessageEvent: { type: 'text_delta', delta: 'recovered' } })
emit(test.kernel, { type: 'agent_end', yielded: true, messages: [errorMessage, { role: 'assistant', stopReason: 'stop' }] })
const result = (await test.outcome).value!
assert.equal(result.status, 'done')
assert.equal(result.content, 'recovered')
assert.equal(result.tools[0].status, 'error')
}
{
const test = begin()
test.kernel.rejectAbort = true
emit(test.kernel, { type: 'message_end', message: { role: 'assistant', stopReason: 'error' } })
assert.equal((await test.outcome).error?.message, 'OMP 模型服务请求失败')
await delay()
assert.equal(test.kernel.disposeCalls, 1, 'failed abort disposes the sidecar instead of leaving automatic retries running')
}
console.log('omp-provider-error-regression: provider error, retry stop, sanitization, cancellation, long thinking, and tool error isolation passed (protocol fixtures; no real upstream request)')
+436
View File
@@ -0,0 +1,436 @@
import { EventEmitter } from 'node:events'
import { testTerminalHostPlugin } from './test-terminal-host-plugin'
import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { build } from 'esbuild'
import type { OmpFrame, OmpKernel } from '../src/main/omp'
import { runOmpTurn, type OmpTurnResult } from '../src/main/agent/omp-turn'
import type { ChatEvent } from '../src/shared/types'
type Outcome<T> =
| { kind: 'resolved'; value: T }
| { kind: 'rejected'; error: unknown }
| { kind: 'timeout' }
class FakeOmpKernel extends EventEmitter {
promptCalls = 0
abortCalls = 0
ack: OmpFrame = { type: 'response', success: true }
prompt(_message: string): Promise<OmpFrame> {
this.promptCalls++
return Promise.resolve(this.ack)
}
abort(): Promise<OmpFrame> {
this.abortCalls++
return Promise.resolve({ type: 'response', success: true })
}
setHostTools(tools: Array<{ name: string }>): Promise<OmpFrame<{ toolNames: string[] }>> {
return Promise.resolve({ type: 'response', command: 'set_host_tools', success: true, data: { toolNames: tools.map((tool) => tool.name) } })
}
respondToHostTool(): Promise<void> {
return Promise.resolve()
}
}
let failures = 0
function check(label: string, condition: boolean, detail = ''): void {
if (condition) console.log(` ✓ ${label}`)
else {
failures++
console.error(` ✗ ${label}${detail ? ` — ${detail}` : ''}`)
}
}
function listenerCounts(kernel: FakeOmpKernel): Record<'event' | 'error' | 'exit', number> {
return {
event: kernel.listenerCount('event'),
error: kernel.listenerCount('error'),
exit: kernel.listenerCount('exit')
}
}
function checkListeners(label: string, kernel: FakeOmpKernel, baseline: ReturnType<typeof listenerCounts>): void {
const actual = listenerCounts(kernel)
check(
`${label}: event/error/exit listener counts return to baseline`,
JSON.stringify(actual) === JSON.stringify(baseline),
`baseline=${JSON.stringify(baseline)}, actual=${JSON.stringify(actual)}`
)
}
async function settleWithin<T>(promise: Promise<T>, timeoutMs = 250): Promise<Outcome<T>> {
let timer: NodeJS.Timeout | undefined
const timeout = new Promise<Outcome<T>>((resolve) => {
timer = setTimeout(() => resolve({ kind: 'timeout' }), timeoutMs)
})
const settled = promise.then<Outcome<T>, Outcome<T>>(
(value) => ({ kind: 'resolved', value }),
(error: unknown) => ({ kind: 'rejected', error })
)
const outcome = await Promise.race([settled, timeout])
if (timer) clearTimeout(timer)
return outcome
}
function turn(
kernel: FakeOmpKernel,
signal: AbortSignal,
events: ChatEvent[]
): Promise<OmpTurnResult> {
return runOmpTurn({
runId: 'run-regression',
kernel: kernel as unknown as OmpKernel,
userText: 'test prompt',
signal,
emit: (event) => events.push(event)
})
}
async function testTurnLifecycle(): Promise<void> {
console.log('\n[1] OMP turn lifecycle')
{
const kernel = new FakeOmpKernel()
const baseline = listenerCounts(kernel)
const events: ChatEvent[] = []
const pending = turn(kernel, new AbortController().signal, events)
kernel.emit('event', {
type: 'message_update',
assistantMessageEvent: { type: 'text_delta', delta: 'hello' }
})
kernel.emit('event', { type: 'agent_end', yielded: true })
const outcome = await settleWithin(pending)
check(
'agent_end resolves as done with accumulated text',
outcome.kind === 'resolved' && outcome.value.status === 'done' && outcome.value.content === 'hello'
)
check('normal completion emits done', events.some((event) => event.type === 'done'))
checkListeners('normal completion', kernel, baseline)
kernel.removeAllListeners()
}
{
const kernel = new FakeOmpKernel()
const baseline = listenerCounts(kernel)
const controller = new AbortController()
const events: ChatEvent[] = []
const pending = turn(kernel, controller.signal, events)
controller.abort()
kernel.emit('event', { type: 'agent_end', yielded: true })
const outcome = await settleWithin(pending)
check(
'abort followed by agent_end resolves as cancelled',
outcome.kind === 'resolved' && outcome.value.status === 'cancelled'
)
check('abort requests kernel cancellation once', kernel.abortCalls === 1, `actual ${kernel.abortCalls}`)
check('agent_end after abort emits cancelled', events.some((event) => event.type === 'cancelled'))
checkListeners('agent_end cancellation', kernel, baseline)
kernel.removeAllListeners()
}
{
const kernel = new FakeOmpKernel()
const baseline = listenerCounts(kernel)
const events: ChatEvent[] = []
const pending = turn(kernel, new AbortController().signal, events)
kernel.emit('event', { type: 'prompt_result', status: 'aborted' })
const outcome = await settleWithin(pending)
check(
'aborted prompt_result resolves as cancelled',
outcome.kind === 'resolved' && outcome.value.status === 'cancelled'
)
check('aborted prompt_result emits cancelled', events.some((event) => event.type === 'cancelled'))
checkListeners('prompt_result cancellation', kernel, baseline)
kernel.removeAllListeners()
}
{
const kernel = new FakeOmpKernel()
kernel.ack = { type: 'response', success: false, error: { message: 'ack rejected' } }
const baseline = listenerCounts(kernel)
const events: ChatEvent[] = []
const pending = turn(kernel, new AbortController().signal, events)
const outcome = await settleWithin(pending)
check(
'a rejected prompt ack rejects with Error',
outcome.kind === 'rejected' && outcome.error instanceof Error && outcome.error.message === 'ack rejected',
outcome.kind === 'rejected' ? `rejection=${String(outcome.error)}` : `outcome=${outcome.kind}`
)
check('a rejected prompt ack emits error', events.some((event) => event.type === 'error'))
checkListeners('prompt ack error', kernel, baseline)
kernel.removeAllListeners()
}
{
const kernel = new FakeOmpKernel()
const baseline = listenerCounts(kernel)
const events: ChatEvent[] = []
const pending = turn(kernel, new AbortController().signal, events)
kernel.emit('event', { type: 'prompt_result', status: 'error', error: { message: 'event rejected' } })
const outcome = await settleWithin(pending)
check(
'an error prompt_result rejects with Error',
outcome.kind === 'rejected' && outcome.error instanceof Error && outcome.error.message === 'event rejected'
)
check('an error prompt_result emits error', events.some((event) => event.type === 'error'))
checkListeners('prompt_result error', kernel, baseline)
kernel.removeAllListeners()
}
{
const kernel = new FakeOmpKernel()
const baseline = listenerCounts(kernel)
const events: ChatEvent[] = []
const pending = turn(kernel, new AbortController().signal, events)
kernel.emit('error', new Error('kernel error'))
const outcome = await settleWithin(pending)
check(
'a kernel error rejects with Error',
outcome.kind === 'rejected' && outcome.error instanceof Error && outcome.error.message === 'kernel error'
)
checkListeners('kernel error', kernel, baseline)
kernel.removeAllListeners()
}
{
const kernel = new FakeOmpKernel()
const baseline = listenerCounts(kernel)
const events: ChatEvent[] = []
const pending = turn(kernel, new AbortController().signal, events)
kernel.emit('exit', 1)
const outcome = await settleWithin(pending)
check(
'a kernel exit rejects with Error',
outcome.kind === 'rejected' && outcome.error instanceof Error && outcome.error.message.includes('OMP 进程已退出')
)
checkListeners('kernel exit', kernel, baseline)
kernel.removeAllListeners()
}
{
const kernel = new FakeOmpKernel()
const baseline = listenerCounts(kernel)
const controller = new AbortController()
controller.abort()
const events: ChatEvent[] = []
const pending = turn(kernel, controller.signal, events)
const outcome = await settleWithin(pending, 75)
check(
'a pre-aborted signal rejects with AbortError',
outcome.kind === 'rejected' && outcome.error instanceof Error && outcome.error.name === 'AbortError',
`outcome=${outcome.kind}`
)
check(
'a pre-aborted signal sends neither prompt nor abort',
kernel.promptCalls === 0 && kernel.abortCalls === 0,
`prompt=${kernel.promptCalls}, abort=${kernel.abortCalls}`
)
if (outcome.kind === 'timeout') {
kernel.emit('event', { type: 'agent_end', yielded: true })
await pending.catch(() => undefined)
}
checkListeners('pre-aborted signal', kernel, baseline)
kernel.removeAllListeners()
}
}
interface IpcTestState {
userData: string
handlers: Map<string, (...args: unknown[]) => unknown>
kernel: EventEmitter | null
promptCalls: number
abortCalls: number
rejectNextPrompt: boolean
}
async function testIpcCancellationPersistence(): Promise<void> {
console.log('\n[2] IPC cancellation persistence')
const tempRoot = await mkdtemp(join(tmpdir(), 'ycode-omp-regression-'))
const state: IpcTestState = {
userData: join(tempRoot, 'userData'),
handlers: new Map(),
kernel: null,
promptCalls: 0,
abortCalls: 0,
rejectNextPrompt: false
}
await mkdir(state.userData, { recursive: true })
const globalWithMock = globalThis as typeof globalThis & { __YCODE_OMP_TEST__?: IpcTestState }
const previous = globalWithMock.__YCODE_OMP_TEST__
globalWithMock.__YCODE_OMP_TEST__ = state
try {
const entry = join(process.cwd(), 'src', 'main', 'ipc.ts')
const outfile = join(tempRoot, 'ipc-regression.mjs')
await build({
entryPoints: [entry],
outfile,
bundle: true,
external: ['node-pty'],
platform: 'node',
format: 'esm',
banner: { js: "import { createRequire as createIpcTestRequire } from 'node:module'; const require = createIpcTestRequire(import.meta.url);" },
logLevel: 'silent',
plugins: [
testTerminalHostPlugin,
{
name: 'ipc-regression-mocks',
setup(pluginBuild) {
pluginBuild.onResolve({ filter: /^electron$/ }, () => ({ path: 'electron', namespace: 'mock-electron' }))
pluginBuild.onLoad({ filter: /.*/, namespace: 'mock-electron' }, () => ({
loader: 'js',
contents: `
const state = globalThis.__YCODE_OMP_TEST__;
export const app = {
getPath: (name) => name === 'userData' ? state.userData : state.userData,
getName: () => 'YCode',
getVersion: () => '0.1.0'
};
export const safeStorage = {
isEncryptionAvailable: () => false,
encryptString: (value) => Buffer.from(value),
decryptString: (value) => value.toString()
};
export const ipcMain = {
handle: (name, handler) => state.handlers.set(name, handler)
};
export const dialog = { showOpenDialog: async () => ({ canceled: true, filePaths: [] }) };
export const shell = { openPath: async () => '' };
export class BrowserWindow {}
export class WebContentsView {}
`
}))
pluginBuild.onResolve({ filter: /^\.\/omp$/ }, () => ({ path: 'omp', namespace: 'mock-omp' }))
pluginBuild.onLoad({ filter: /.*/, namespace: 'mock-omp' }, () => ({
loader: 'js',
contents: `
import { EventEmitter } from 'node:events';
export class OmpKernel extends EventEmitter {
constructor() { super(); globalThis.__YCODE_OMP_TEST__.kernel = this; }
prompt() {
const state = globalThis.__YCODE_OMP_TEST__;
state.promptCalls++;
if (state.rejectNextPrompt) {
state.rejectNextPrompt = false;
return Promise.resolve({ type: 'response', success: false, error: { message: 'mock OMP error' } });
}
return Promise.resolve({ type: 'response', success: true });
}
setHostTools(tools) {
return Promise.resolve({ type: 'response', command: 'set_host_tools', success: true, data: { toolNames: tools.map((tool) => tool.name) } });
}
respondToHostTool() { return Promise.resolve(); }
send() { return Promise.resolve({ type: 'response', success: true, data: { messages: [] } }); }
abort() {
const state = globalThis.__YCODE_OMP_TEST__;
state.abortCalls++;
setImmediate(() => this.emit('event', { type: 'agent_end', yielded: true }));
return Promise.resolve({ type: 'response', success: true });
}
dispose() {}
}
export const detectOmpPath = () => 'mock-omp';
export const getOmpInfo = () => ({ available: true, exePath: 'mock-omp', profile: 'ycode', model: 'ycode/test' });
`
}))
}
}
]
})
const ipc = await import(pathToFileURL(outfile).href)
;(ipc.registerIpc as (getWindow: () => unknown) => void)(() => ({
webContents: { send: () => undefined }
}))
const invoke = (channel: string, ...args: unknown[]): Promise<unknown> => {
const handler = state.handlers.get(channel)
if (!handler) throw new Error(`Missing IPC handler: ${channel}`)
return Promise.resolve(handler({}, ...args))
}
const session = (await invoke('session:create', 'cancel persistence')) as { id: string }
const sent = (await invoke('chat:send', { sessionId: session.id, text: 'cancel this turn' })) as { runId: string }
// Native kernel acquisition is asynchronous; exercise cancellation after prompt dispatch.
for (let i = 0; i < 50 && state.promptCalls === 0; i++) {
await new Promise((resolve) => setTimeout(resolve, 10))
}
const cancelled = (await invoke('chat:cancel', sent.runId)) as { cancelled: boolean }
check('chat:cancel accepts the active run', cancelled.cancelled === true)
let messages: Array<{ role: string; status?: string }> = []
for (let i = 0; i < 50; i++) {
await new Promise((resolve) => setTimeout(resolve, 10))
messages = (await invoke('session:messages', session.id)) as Array<{ role: string; status?: string }>
if (messages.at(-1)?.status !== 'streaming') break
}
check('the fake OMP kernel received one prompt and one abort', state.promptCalls === 1 && state.abortCalls === 1)
check('cancelled assistant status is persisted through IPC', messages.at(-1)?.role === 'assistant' && messages.at(-1)?.status === 'cancelled', JSON.stringify(messages))
const sessionsFile = join(state.userData, 'sessions.json')
const readStoredMessages = async (sessionId: string): Promise<Array<{ role: string; status?: string; error?: string }>> => {
try {
const stored = JSON.parse(await readFile(sessionsFile, 'utf8')) as {
sessions: Array<{ id: string; messages: Array<{ role: string; status?: string; error?: string }> }>
}
return stored.sessions.find((item) => item.id === sessionId)?.messages ?? []
} catch {
return []
}
}
let diskMessages: Array<{ role: string; status?: string; error?: string }> = []
for (let i = 0; i < 50; i++) {
diskMessages = await readStoredMessages(session.id)
if (diskMessages.at(-1)?.status === 'cancelled') break
await new Promise((resolve) => setTimeout(resolve, 10))
}
check('cancelled status is present on disk', diskMessages.at(-1)?.status === 'cancelled', JSON.stringify(diskMessages))
state.rejectNextPrompt = true
const errorSession = (await invoke('session:create', 'error persistence')) as { id: string }
await invoke('chat:send', { sessionId: errorSession.id, text: 'fail this turn' })
let errorMessages: Array<{ role: string; status?: string; error?: string }> = []
for (let i = 0; i < 50; i++) {
errorMessages = (await invoke('session:messages', errorSession.id)) as Array<{ role: string; status?: string; error?: string }>
if (errorMessages.at(-1)?.status === 'error') break
await new Promise((resolve) => setTimeout(resolve, 10))
}
check(
'OMP error status and message are preserved in session memory',
errorMessages.at(-1)?.status === 'error' && errorMessages.at(-1)?.error === 'mock OMP error',
JSON.stringify(errorMessages)
)
let errorDiskMessages: Array<{ role: string; status?: string; error?: string }> = []
for (let i = 0; i < 50; i++) {
errorDiskMessages = await readStoredMessages(errorSession.id)
if (errorDiskMessages.at(-1)?.status === 'error') break
await new Promise((resolve) => setTimeout(resolve, 10))
}
check(
'OMP error status and message are preserved on disk',
errorDiskMessages.at(-1)?.status === 'error' && errorDiskMessages.at(-1)?.error === 'mock OMP error',
JSON.stringify(errorDiskMessages)
)
} finally {
globalWithMock.__YCODE_OMP_TEST__ = previous
await rm(tempRoot, { recursive: true, force: true })
}
}
async function main(): Promise<void> {
await testTurnLifecycle()
await testIpcCancellationPersistence()
console.log(failures === 0 ? '\nOMP regression checks passed.\n' : `\n${failures} OMP regression check(s) failed.\n`)
process.exitCode = failures === 0 ? 0 : 1
}
main().catch((error: unknown) => {
console.error('OMP regression checks aborted:', error)
process.exitCode = 1
})
+293
View File
@@ -0,0 +1,293 @@
import { EventEmitter } from 'node:events'
import { mkdtemp, rm, stat } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { build } from 'esbuild'
interface FakeChildState {
exePath: string
args: string[]
cwd: string
child: EventEmitter & {
pid: number
exitCode: number | null
signalCode: string | null
killed: boolean
stdin: { write: (payload: string) => boolean; end: () => void }
kill: () => boolean
}
}
interface KernelTestState {
children: FakeChildState[]
readers: EventEmitter[]
stdinWrites: string[]
autoExitOnEnd: boolean
stdinEnds: number
readerCloses: number
killCalls: number
}
let failures = 0
function check(label: string, condition: boolean, detail = ''): void {
if (condition) console.log(` ✓ ${label}`)
else {
failures++
console.error(` ✗ ${label}${detail ? ` — ${detail}` : ''}`)
}
}
async function waitFor(predicate: () => boolean, timeoutMs = 500): Promise<boolean> {
const deadline = Date.now() + timeoutMs
while (Date.now() < deadline) {
if (predicate()) return true
await new Promise((resolve) => setTimeout(resolve, 5))
}
return predicate()
}
async function buildKernelBundle(outfile: string): Promise<void> {
await build({
entryPoints: [join(process.cwd(), 'src', 'main', 'omp.ts')],
outfile,
bundle: true,
platform: 'node',
format: 'esm',
logLevel: 'silent',
plugins: [
{
name: 'omp-session-kernel-mocks',
setup(pluginBuild) {
pluginBuild.onResolve({ filter: /^node:child_process$/ }, () => ({
path: 'mock-child-process',
namespace: 'omp-session-mock'
}))
pluginBuild.onLoad({ filter: /^mock-child-process$/, namespace: 'omp-session-mock' }, () => ({
loader: 'js',
contents: `
import { EventEmitter } from 'node:events';
const state = globalThis.__YCODE_OMP_SESSION_TEST__;
class FakeChild extends EventEmitter {
constructor(exePath, args, cwd) {
super();
this.pid = 100 + state.children.length;
this.exitCode = null;
this.signalCode = null;
this.killed = false;
this.stdin = {
write: (payload) => {
state.stdinWrites.push(payload);
let command;
try { command = JSON.parse(payload); } catch { return true; }
if (typeof command.id === 'string' && command.type !== 'host_tool_result') {
const reader = state.readers[state.readers.length - 1];
const data = command.type === 'set_host_tools'
? { toolNames: (command.tools || []).map((tool) => tool.name) }
: command.type === 'get_branch_messages'
? { messages: [{ entryId: 'user-entry-1', text: 'saved prompt' }] }
: command.type === 'get_tree' ? { tree: [], leafId: null }
: command.type === 'get_messages' ? { messages: [] } : {};
setImmediate(() => reader?.emit('line', JSON.stringify({
id: command.id, type: 'response', command: command.type, success: true, data
})));
}
return true;
},
end: () => {
state.stdinEnds++;
if (state.autoExitOnEnd) setTimeout(() => this.finish(0, null), 5);
}
};
this.stdout = {};
this.stderr = new EventEmitter();
state.children.push({ exePath, args, cwd, child: this });
}
finish(code, signal) {
if (this.exitCode !== null || this.signalCode !== null) return;
this.exitCode = code;
this.signalCode = signal;
this.emit('exit', code, signal);
this.emit('close', code, signal);
}
kill() {
this.killed = true;
state.killCalls++;
this.finish(null, 'SIGTERM');
return true;
}
}
export function spawn(exePath, args, options) {
return new FakeChild(exePath, args, options.cwd);
}
`
}))
pluginBuild.onResolve({ filter: /^node:readline$/ }, () => ({
path: 'mock-readline',
namespace: 'omp-session-mock'
}))
pluginBuild.onLoad({ filter: /^mock-readline$/, namespace: 'omp-session-mock' }, () => ({
loader: 'js',
contents: `
import { EventEmitter } from 'node:events';
const state = globalThis.__YCODE_OMP_SESSION_TEST__;
export function createInterface() {
const reader = new EventEmitter();
reader.close = () => { state.readerCloses++; };
state.readers.push(reader);
return reader;
}
`
}))
}
}
]
})
}
async function main(): Promise<void> {
const tempRoot = await mkdtemp(join(tmpdir(), 'ycode-omp-session-regression-'))
const state: KernelTestState = {
children: [],
readers: [],
stdinWrites: [],
autoExitOnEnd: true,
stdinEnds: 0,
readerCloses: 0,
killCalls: 0
}
const globalWithMock = globalThis as typeof globalThis & { __YCODE_OMP_SESSION_TEST__?: KernelTestState }
const previous = globalWithMock.__YCODE_OMP_SESSION_TEST__
globalWithMock.__YCODE_OMP_SESSION_TEST__ = state
try {
const outfile = join(tempRoot, 'omp-kernel-regression.mjs')
await buildKernelBundle(outfile)
const { OmpKernel, parseFirstProfileModel } = await import(pathToFileURL(outfile).href)
const yaml = [
'providers:',
' local-probe:',
' api: openai-completions',
' models:',
' - id: probe-model',
' name: Probe model',
' second-provider:',
' models:',
' - id: second-model'
].join('\n')
check('profile model resolution returns provider/model, not profile/model',
parseFirstProfileModel(yaml) === 'local-probe/probe-model')
check('model resolution returns null when models.yml has no configured provider model',
parseFirstProfileModel('providers:\n empty-provider:\n models: []') === null)
const sessionPath = join(tempRoot, 'native', 'session-one.jsonl')
const selectedModel = 'local-probe/probe-model'
const persistent = new OmpKernel(process.execPath, tempRoot, 'ycode', sessionPath, selectedModel)
const persistentStart = persistent.start()
check('session parent directory is created before spawn', await waitFor(() => state.children.length === 1) &&
(await stat(dirname(sessionPath))).isDirectory())
check('persistent start reaches the ready frame', state.readers.length === 1)
state.readers[0]?.emit('line', JSON.stringify({ type: 'ready' }))
await persistentStart
const persistentArgs = state.children[0]?.args ?? []
check('persistent mode passes the configured provider/model selection',
persistentArgs.includes(`--model=${selectedModel}`))
check('built-in OMP tools are disabled by default so execution stays behind the host broker',
persistentArgs.includes('--no-tools'))
check('persistent mode passes the native resume path and session directory',
persistentArgs.includes(`--resume=${sessionPath}`) && persistentArgs.includes(`--session-dir=${dirname(sessionPath)}`))
check('persistent mode omits --no-session', !persistentArgs.includes('--no-session'))
const hostToolsAck = await persistent.setHostTools([{
name: 'read',
description: 'Read workspace text',
parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] }
}])
const branchMessages = await persistent.getBranchMessages()
const branchAck = await persistent.branch('user-entry-1')
await persistent.respondToHostTool('host-request-1', 'tool result')
const image = { type: 'image' as const, data: 'c21hbGw=', mimeType: 'image/png' }
const promptAck = await persistent.prompt('inspect this image', [image])
const writtenFrames = state.stdinWrites.map((payload) => JSON.parse(payload) as Record<string, unknown>)
const promptFrame = writtenFrames.find((frame) => frame.type === 'prompt')
const hostResultFrame = writtenFrames.find((frame) => frame.type === 'host_tool_result')
check('host tools, branch history, and images use their typed RPC helpers',
hostToolsAck.success === true && branchMessages.data.messages[0]?.entryId === 'user-entry-1' &&
branchAck.success === true && promptAck.success === true &&
Array.isArray(promptFrame?.images) &&
JSON.stringify((promptFrame.images as unknown[])[0]) === JSON.stringify(image) &&
hostResultFrame?.id === 'host-request-1')
let sawExit = false
persistent.once('exit', () => { sawExit = true })
await persistent.dispose()
check('dispose ends stdin and waits until the child exits', state.stdinEnds === 1 && sawExit)
check('kernel event listeners are removed after child exit', persistent.eventNames().length === 0)
const ephemeral = new OmpKernel(process.execPath, tempRoot, 'ycode', undefined, selectedModel)
const ephemeralStart = ephemeral.start()
check('ephemeral start creates a second child', await waitFor(() => state.children.length === 2))
state.readers[1]?.emit('line', JSON.stringify({ type: 'ready' }))
await ephemeralStart
const ephemeralArgs = state.children[1]?.args ?? []
check('three-argument constructors keep --no-session behavior',
ephemeralArgs.includes('--no-session') && !ephemeralArgs.some((arg) => arg.startsWith('--resume=')) &&
ephemeralArgs.includes('--no-tools'))
await ephemeral.dispose()
const missingModel = new OmpKernel(process.execPath, tempRoot, 'ycode', undefined, null)
let missingModelError: unknown
try {
await missingModel.start()
} catch (error) {
missingModelError = error
}
check('a profile without provider/model fails before spawn',
missingModelError instanceof Error && missingModelError.message.includes('provider/model') && state.children.length === 2)
state.autoExitOnEnd = false
const forceKilled = new OmpKernel(process.execPath, tempRoot, 'ycode', join(tempRoot, 'native', 'session-three.jsonl'), selectedModel)
const forceKillStart = forceKilled.start()
check('force-kill case starts a third child', await waitFor(() => state.children.length === 3))
state.readers[2]?.emit('line', JSON.stringify({ type: 'ready' }))
await forceKillStart
check('built-in OMP tools remain disabled for every kernel session',
state.children[2]?.args.includes('--no-tools') === true)
const killStartedAt = Date.now()
const keepAlive = setTimeout(() => undefined, 1800)
await forceKilled.dispose()
clearTimeout(keepAlive)
const killDelay = Date.now() - killStartedAt
check('stalled child is killed after the 1500ms grace period',
state.killCalls === 1 && state.children[2]?.child.killed === true && killDelay >= 1400 && killDelay < 2500,
`${killDelay}ms`)
const recoverable = new OmpKernel(process.execPath, tempRoot, 'ycode', sessionPath, selectedModel)
const firstStart = recoverable.start()
check('recoverable kernel creates a fresh sidecar', await waitFor(() => state.children.length === 4))
state.readers[3]?.emit('line', JSON.stringify({ type: 'ready' }))
await firstStart
state.children[3]?.child.finish(1, null)
const recoveredStart = recoverable.start()
check('kernel can restart after its sidecar exits', await waitFor(() => state.children.length === 5))
state.readers[4]?.emit('line', JSON.stringify({ type: 'ready' }))
await recoveredStart
const recoveredArgs = state.children[4]?.args ?? []
check('restarted sidecar reuses the same resume path and provider/model',
recoveredArgs.includes(`--resume=${sessionPath}`) && recoveredArgs.includes(`--model=${selectedModel}`))
await recoverable.dispose()
console.log(failures === 0 ? '\nOMP session regression checks passed.\n' : `\n${failures} OMP session regression check(s) failed.\n`)
process.exitCode = failures === 0 ? 0 : 1
} finally {
globalWithMock.__YCODE_OMP_SESSION_TEST__ = previous
await rm(tempRoot, { recursive: true, force: true })
}
}
main().catch((error: unknown) => {
console.error('OMP session regression checks aborted:', error)
process.exitCode = 1
})
+156
View File
@@ -0,0 +1,156 @@
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { createHash } from 'node:crypto'
import { execFileSync } from 'node:child_process'
import { parse } from '@babel/parser'
import postcss, { type Rule } from 'postcss'
import {
Rw, zw, Bw, Vw, JQe, DETAIL_NARROW_WIDTH, SIDEBAR_NARROW_WIDTH, DETAIL_PANEL_MOTION, SOURCE_PANEL_LAYOUT_EVIDENCE,
normalizeInset, clampSidebarWidth, resolveSidebarDragAction, resolveChatAreaAvailableWidth, resolveChatAreaWidth,
resolveDetailDrawerWidth, resolveDetailPanelLayoutWidths, resolveRememberedDetailWidth, resolveLeadingInsetRebalanceWidth,
getNarrowState, resolveNarrowState, stickyNarrow
} from '../src/renderer/src/workbuddy/workbench/source-panel-layout'
const hash = (value: string): string => createHash('sha256').update(value).digest('hex')
assert.deepEqual(Rw, { EXPANDED_WIDTH: 280, MIN_WIDTH: 240, MAX_WIDTH: 360, COLLAPSE_THRESHOLD: 120, EXPAND_THRESHOLD: 150, COLLAPSED_WIDTH: 48 })
assert.deepEqual(zw, { MIN_WIDTH: 360, PREFERRED_WIDTH: 600 })
assert.deepEqual(Bw, { BREAKPOINT: 600, CHAT_MIN_WIDTH: 360 })
assert.deepEqual(Vw, { MIN_WIDTH: 360, DEFAULT_WIDTH: 500, OVERVIEW_ONLY_WIDTH: 318, MAX_WIDTH: Infinity })
assert.deepEqual(JQe, { WIDTH: 2 })
assert.equal(DETAIL_NARROW_WIDTH, 1000)
assert.equal(SIDEBAR_NARROW_WIDTH, 640)
assert.deepEqual(DETAIL_PANEL_MOTION, { openMs: 200, closeMs: 150, openEasing: 'cubic-bezier(0, 0, .2, 1)', closeEasing: 'cubic-bezier(.4, 0, 1, 1)' })
assert.equal(normalizeInset(Number.NaN), 0)
assert.equal(normalizeInset(Infinity), 0)
assert.equal(normalizeInset(-1), 0)
assert.equal(normalizeInset(31.9), 31)
assert.equal(clampSidebarWidth(48), 240)
assert.equal(clampSidebarWidth(280), 280)
assert.equal(clampSidebarWidth(500), 360)
assert.equal(resolveSidebarDragAction(119, false), 'collapse')
assert.equal(resolveSidebarDragAction(120, false), 'resize')
assert.equal(resolveSidebarDragAction(150, true), 'keep-collapsed')
assert.equal(resolveSidebarDragAction(151, true), 'expand')
assert.equal(resolveChatAreaAvailableWidth({ containerWidth: 1336, sidebarWidth: 280 }), 1056)
assert.equal(resolveChatAreaAvailableWidth({ containerWidth: 1336, sidebarWidth: 280, leadingInset: 100 }), 956)
assert.equal(resolveChatAreaAvailableWidth({ containerWidth: 100, sidebarWidth: 280, leadingInset: 100 }), 0)
assert.equal(resolveChatAreaWidth(1336, 100), 1236)
assert.deepEqual(resolveDetailPanelLayoutWidths({ containerWidth: 1336, sidebarWidth: 280, requestedDetailWidth: 528 }), { mainWidth: 528, detailWidth: 528 })
assert.deepEqual(resolveDetailPanelLayoutWidths({ containerWidth: 1336, sidebarWidth: 280, requestedDetailWidth: 647 }), { mainWidth: 409, detailWidth: 647 })
assert.deepEqual(resolveDetailPanelLayoutWidths({ containerWidth: 1336, sidebarWidth: 280, requestedDetailWidth: 900 }), { mainWidth: 360, detailWidth: 696 })
assert.deepEqual(resolveDetailPanelLayoutWidths({ containerWidth: 1336, sidebarWidth: 280, leadingInset: 100, requestedDetailWidth: 900 }), { mainWidth: 460, detailWidth: 596 })
assert.equal(resolveDetailPanelLayoutWidths({ containerWidth: 999, sidebarWidth: 280, requestedDetailWidth: 360 }), undefined)
assert.deepEqual(resolveDetailPanelLayoutWidths({ containerWidth: 1000, sidebarWidth: 280, requestedDetailWidth: 318 }), { mainWidth: 360, detailWidth: 360 })
assert.deepEqual(resolveDetailPanelLayoutWidths({ containerWidth: 1001, sidebarWidth: 280, requestedDetailWidth: 360.9 }), { mainWidth: 361, detailWidth: 360 })
assert.equal(resolveDetailDrawerWidth({ containerWidth: 1336, sidebarWidth: 280 }), 528)
assert.equal(resolveDetailDrawerWidth({ containerWidth: 900, sidebarWidth: 280 }), 360)
assert.equal(resolveDetailDrawerWidth({ containerWidth: 900, sidebarWidth: 280, preferredWidth: 500.9 }), 500.9)
assert.equal(resolveDetailDrawerWidth({ containerWidth: 900, sidebarWidth: 280, preferredWidth: 1200 }), 900)
assert.equal(resolveDetailDrawerWidth({ containerWidth: 599, sidebarWidth: 280, isMobile: true, preferredWidth: 360 }), 599)
assert.equal(resolveDetailDrawerWidth({ containerWidth: 300, sidebarWidth: 0 }), 300)
assert.equal(resolveDetailDrawerWidth({ containerWidth: 0, sidebarWidth: 280 }), 0)
assert.equal(resolveRememberedDetailWidth([undefined, null, 0, 360, 700.9, 800], 1056), 700)
assert.equal(resolveRememberedDetailWidth([undefined, null, 360], 1057), 528)
assert.equal(resolveRememberedDetailWidth([360.9, 700], 1056), 360, 'source checks >360 before flooring')
assert.equal(resolveRememberedDetailWidth([Infinity], 1056), Infinity, 'source behavior is preserved; caller owns finite stored-value validation')
const rebalance = { previousInset: 0, nextInset: 48, isNarrowForDetail: false, isPanelVisible: true, rememberedDetailWidth: null, chatAreaWidth: 1001 }
assert.equal(resolveLeadingInsetRebalanceWidth(rebalance), 500)
assert.equal(resolveLeadingInsetRebalanceWidth({ ...rebalance, previousInset: 48 }), undefined)
assert.equal(resolveLeadingInsetRebalanceWidth({ ...rebalance, isNarrowForDetail: true }), undefined)
assert.equal(resolveLeadingInsetRebalanceWidth({ ...rebalance, isPanelVisible: false }), undefined)
assert.equal(resolveLeadingInsetRebalanceWidth({ ...rebalance, rememberedDetailWidth: 500 }), undefined)
assert.deepEqual(getNarrowState(1000), { isMobile: false, isNarrowForDetail: false, isNarrowForSidebar: false, screenWidth: 1000 })
assert.equal(getNarrowState(999).isNarrowForDetail, true)
assert.equal(getNarrowState(1047, 48).isNarrowForDetail, true)
assert.equal(getNarrowState(1048, 48).isNarrowForDetail, false)
assert.equal(getNarrowState(639).isNarrowForSidebar, true)
assert.equal(getNarrowState(640).isNarrowForSidebar, false)
assert.equal(getNarrowState(599).isMobile, true)
assert.equal(getNarrowState(600).isMobile, false)
assert.equal(stickyNarrow(false, 1000, 1000), false)
assert.equal(stickyNarrow(true, 1023, 1000), true)
assert.equal(stickyNarrow(true, 1024, 1000), false)
assert.equal(resolveNarrowState(getNarrowState(999), 1023).isNarrowForDetail, true)
assert.equal(resolveNarrowState(getNarrowState(999), 1024).isNarrowForDetail, false)
assert.equal(resolveNarrowState(getNarrowState(639), 663).isNarrowForSidebar, true)
assert.equal(resolveNarrowState(getNarrowState(639), 664).isNarrowForSidebar, false)
assert.equal(resolveNarrowState(getNarrowState(599), 623).isMobile, true)
assert.equal(resolveNarrowState(getNarrowState(599), 624).isMobile, false)
assert.deepEqual(resolveNarrowState(undefined, 1000), getNarrowState(1000))
assert.deepEqual(resolveNarrowState(null, 1048, 48), getNarrowState(1048, 48))
let geometryCases = 0
for (let containerWidth = 240; containerWidth <= 1920; containerWidth += 17) {
for (const sidebarWidth of [0, 48, 240, 280, 360]) for (const leadingInset of [0, 1, 31, 128]) {
let previous = -Infinity
for (const requestedDetailWidth of [-100, 0, 318, 360, 500.9, 10_000]) {
const result = resolveDetailPanelLayoutWidths({ containerWidth, sidebarWidth, leadingInset, requestedDetailWidth })
if (containerWidth - sidebarWidth - leadingInset < 720) assert.equal(result, undefined)
else {
assert.ok(result)
assert.equal(result.mainWidth + result.detailWidth, containerWidth - sidebarWidth, 'space is conserved')
assert.ok(result.mainWidth - leadingInset >= 360, 'minimum chat width remains after inset')
assert.ok(result.detailWidth >= 360, 'minimum detail width remains')
assert.ok(result.detailWidth >= previous, 'increasing requested width never shrinks the panel')
assert.ok(Number.isInteger(result.detailWidth) && Number.isInteger(result.mainWidth))
previous = result.detailWidth
}
geometryCases++
}
}
}
const recoveredText = await readFile('src/renderer/src/workbuddy/workbench/source-panel-layout.ts', 'utf8')
const recoveredAst = parse(recoveredText, { sourceType: 'module', plugins: ['typescript'] })
const expectedBodies = SOURCE_PANEL_LAYOUT_EVIDENCE.functions as Record<string, { bodySha256: string }>
let bodyCount = 0, constantCount = 0
for (const statement of recoveredAst.program.body) {
if (statement.type !== 'ExportNamedDeclaration') continue
const declaration = statement.declaration
if (declaration?.type === 'FunctionDeclaration' && declaration.id && expectedBodies[declaration.id.name]) {
assert.equal(hash(recoveredText.slice(declaration.body.start!, declaration.body.end!)), expectedBodies[declaration.id.name].bodySha256, `source body preserved: ${declaration.id.name}`)
bodyCount++
}
if (declaration?.type === 'VariableDeclaration') for (const variable of declaration.declarations) {
if (variable.id.type !== 'Identifier') continue
const expected = (SOURCE_PANEL_LAYOUT_EVIDENCE.constants as Record<string, string>)[variable.id.name]
if (!expected || !variable.init) continue
const expression = variable.init.type === 'TSAsExpression' ? variable.init.expression : variable.init
assert.equal(hash(recoveredText.slice(expression.start!, expression.end!)), expected, `source constant preserved: ${variable.id.name}`)
constantCount++
}
}
assert.equal(bodyCount, 12)
assert.equal(constantCount, 7)
const sourceCssText = await readFile('.tmp/rea-css-recovered/desktop-sdk-setup-Bh0rg_xP.css', 'utf8')
assert.equal(hash(sourceCssText), SOURCE_PANEL_LAYOUT_EVIDENCE.motionSha256)
const generatedCssText = await readFile('src/renderer/src/workbuddy/workbench/panel-motion-source.css', 'utf8')
const sourceCss = postcss.parse(sourceCssText), generatedCss = postcss.parse(generatedCssText)
const selected = (selector: string): boolean => /\.detail-panel-motion-[\w-]+|\.detail-panel-container(?:[\s.:#[]|$)/.test(selector)
function describe(rule: Rule, source: boolean): unknown {
const conditions: string[] = []
let ancestor = rule.parent
while (ancestor) { if (ancestor.type === 'atrule') conditions.unshift(`@${ancestor.name} ${ancestor.params}`); ancestor = ancestor.parent }
return { selectors: rule.selectors.filter(selector => !source || selected(selector)).map(selector => source ? selector.startsWith('.detail-panel-motion-') ? `.source-detail-layout${selector}` : `.source-detail-layout ${selector}` : selector),
declarations: rule.nodes.filter(node => node.type === 'decl').map(node => ({ property: node.prop, value: node.value, important: !!node.important })), conditions }
}
const expectedRules: unknown[] = [], actualRules: unknown[] = []
sourceCss.walkRules(rule => { if (rule.selectors.some(selected)) expectedRules.push(describe(rule, true)) })
generatedCss.walkRules(rule => { if (rule.selector !== '.source-detail-layout') actualRules.push(describe(rule, false)) })
assert.equal(actualRules.length, 16)
assert.deepEqual(actualRules, expectedRules, 'source motion declarations, order, selector membership and media conditions remain intact')
const reduced = actualRules.filter(rule => (rule as { conditions: string[] }).conditions.includes('@media (prefers-reduced-motion:reduce)'))
assert.equal(reduced.length, 1, 'transition:none must remain conditional on reduced motion')
assert.ok(generatedCssText.includes('--wb-motion-duration-medium:.2s'))
assert.ok(generatedCssText.includes('--wb-motion-duration-fast:.15s'))
assert.ok(generatedCssText.includes('--wb-motion-easing-decelerate:cubic-bezier(0, 0, .2, 1)'))
assert.ok(generatedCssText.includes('--wb-motion-easing-accelerate:cubic-bezier(.4, 0, 1, 1)'))
execFileSync(process.execPath, ['scripts/rea/recover-panel-layout.mjs', '--check'], { stdio: 'pipe' })
console.log(JSON.stringify({ result: 'PASS source panel layout', geometryCases, sourceBodies: bodyCount, sourceConstants: constantCount, sourceCssRules: actualRules.length,
checks: ['source SHA/body identity', 'source CSS/media identity', 'finite geometry invariants', 'sidebar drag thresholds', 'drawer/mobile width', 'remembered widths', 'leading inset', 'narrow 24px hysteresis', 'motion token values', 'reproducible generation'] }))
+206
View File
@@ -0,0 +1,206 @@
import { createElement } from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import InlineCode, { extractInlineCodeText, isResolvedPathType } from '../src/renderer/src/workbuddy/paths/InlineCode'
import {
getCachedResolvedType,
revalidateAndOpenPath,
subscribeToPathResolution,
type PathResolutionValue,
type PathTypeResolver
} from '../src/renderer/src/workbuddy/paths/path-resolution'
import {
detectPath,
extractPurePath,
getPathDisplayText,
hasLineRange,
isAbsolutePath,
isFilename,
isRelativePath,
isSymbol,
middleEllipsis,
parseLineRange,
shouldDetectAsPath,
truncatePathDisplay,
type PathType
} from '../src/renderer/src/workbuddy/paths/path-detection'
// Expectations are derived from REA-captured path ranges 722350-724687 and inline-code ranges 722904-723374, 727268-728626.
let checks = 0
function check(label: string, condition: boolean): void {
if (!condition) throw new Error(label)
checks++
}
function detectedType(value: string): PathType | undefined {
const detection = detectPath(value)
return detection.isPath ? detection.type : undefined
}
function deferred<T>(): { promise: Promise<T>; resolve: (value: T) => void; reject: (reason?: unknown) => void } {
let resolve!: (value: T) => void
let reject!: (reason?: unknown) => void
const promise = new Promise<T>((res, rej) => {
resolve = res
reject = rej
})
return { promise, resolve, reject }
}
function nextTurn(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, 0))
}
async function main(): Promise<void> {
check('detects Windows absolute paths', isAbsolutePath('C:\\workspace\\src\\main.ts') && detectedType('C:\\workspace\\src\\main.ts') === 'file')
check('detects POSIX absolute paths', isAbsolutePath('/workspace/src/main.ts') && detectedType('/workspace/src/main.ts') === 'file')
check('detects relative paths and filename-only paths',
isRelativePath('src\\renderer\\App.tsx') && isFilename('README.md') && detectedType('README.md') === 'file')
check('classifies trailing slash paths as directories',
detectedType('/workspace/src/') === 'directory' && detectedType('C:\\workspace\\src\\') === 'directory')
const ranged = detectPath('C:\\workspace\\src\\main.ts#L12-L28')
check('parses #Lstart-Lend and extracts the pure Windows path',
ranged.isPath && ranged.type === 'file' && ranged.purePath === 'C:\\workspace\\src\\main.ts' &&
ranged.range?.start === 12 && ranged.range?.end === 28 && extractPurePath('src/main.ts#L4') === 'src/main.ts')
check('parses single-line ranges with an omitted end',
hasLineRange('/workspace/src/main.ts#L7') && parseLineRange('/workspace/src/main.ts#L7')?.start === 7 &&
parseLineRange('/workspace/src/main.ts#L7')?.end === undefined)
check('does not treat lowercase #l ranges or invalid suffixes as source ranges',
!hasLineRange('/workspace/src/main.ts#l7') && !hasLineRange('/workspace/src/main.ts#L0x'))
check('recognizes symbol identifiers and leaves unrelated strings unclassified',
isSymbol('renderMessage_2') && isSymbol('_private') && detectedType('renderMessage') === 'symbol' &&
!shouldDetectAsPath('ordinary words'))
check('symbols without a resolver result are not resolved path types',
!isResolvedPathType(undefined) && !isResolvedPathType('unknown') && isResolvedPathType('symbol'))
check('middle ellipsis uses balanced retained text and exact length',
middleEllipsis('abcdefghijklmnopqrst', 10) === 'abcd...rst')
const truncatedPath = truncatePathDisplay('C:\\very\\long\\path\\to\\file.ts', 20)
check('path truncation preserves the basename when space allows',
truncatedPath === 'C:\\very\\lo...file.ts' && truncatedPath.length === 20)
check('path display shows a file basename and keeps its line suffix',
getPathDisplayText('C:\\repo\\src\\file.ts#L12-L14', 'C:\\repo\\src\\file.ts', 'file') === 'file.ts#L12-L14')
check('path display truncates directory and symbol text',
getPathDisplayText('/workspace/a/very/long/path', '/workspace/a/very/long/path', 'directory') ===
truncatePathDisplay('/workspace/a/very/long/path') && getPathDisplayText('renderMessage', 'renderMessage', 'symbol') === 'renderMessage')
const nested = createElement('span', null, 'src/', createElement('code', null, 'App.tsx'), 3)
check('extracts code text recursively from React children', extractInlineCodeText(nested) === 'src/App.tsx3')
let nativeHandlerCalls = 0
const ssr = renderToStaticMarkup(createElement(InlineCode, {
children: 'C:\\repo\\src\\main.ts#L12-L18',
className: 'custom-code',
isFinished: true,
pathClickHandler: () => { nativeHandlerCalls++ },
resolveCode: async () => 'file' as const,
requestId: 'request-1',
paths: ['C:\\repo\\src\\main.ts']
}))
check('SSR leaves unresolved inline paths plain and performs no native action',
ssr.includes('class="cr-inline-code custom-code"') && ssr.includes('C:\\repo\\src\\main.ts#L12-L18') &&
!ssr.includes('cr-clickable-path') && nativeHandlerCalls === 0)
const streamedCode = 'C:\\repo\\src\\stream.ts'
subscribeToPathResolution('stream-request', streamedCode, [], async () => 'file' as const, () => undefined)
await nextTurn()
let streamedNativeCalls = 0
const cachedSsr = renderToStaticMarkup(createElement(InlineCode, {
children: streamedCode,
isFinished: false,
pathClickHandler: () => { streamedNativeCalls++ },
resolveCode: async () => 'unknown' as const,
requestId: 'stream-request'
}))
check('does not gate already-resolved inline paths on isFinished and SSR performs no native action',
cachedSsr.includes('cr-clickable-path cr-path-type-file') && cachedSsr.includes('title="C:\\repo\\src\\stream.ts"') &&
streamedNativeCalls === 0)
const sharedRequest = deferred<PathResolutionValue>()
const sharedCode = 'C:\\repo\\src\\shared.ts'
const sharedPaths = ['C:\\repo\\src\\shared.ts']
let resolverCalls = 0
const firstResults: PathResolutionValue[] = []
const secondResults: PathResolutionValue[] = []
const resolver: PathTypeResolver = () => {
resolverCalls++
return sharedRequest.promise
}
subscribeToPathResolution('cache-request', sharedCode, sharedPaths, resolver, (type) => firstResults.push(type))
subscribeToPathResolution('cache-request', sharedCode, [...sharedPaths], resolver, (type) => secondResults.push(type))
check('deduplicates simultaneous requests with equal request id, code, and paths', resolverCalls === 1)
sharedRequest.resolve('file')
await nextTurn()
check('notifies all active subscribers and caches the resolved path type',
firstResults[0] === 'file' && secondResults[0] === 'file' &&
getCachedResolvedType('cache-request', sharedCode, sharedPaths) === 'file')
let cacheReplay: PathResolutionValue
subscribeToPathResolution('cache-request', sharedCode, sharedPaths, () => {
resolverCalls++
return Promise.resolve('directory')
}, (type) => { cacheReplay = type })
check('reuses cached resolution without another resolver call', cacheReplay! === 'file' && resolverCalls === 1)
const revalidationEvents: string[] = []
let revalidationCalls = 0
const revalidatedType = await revalidateAndOpenPath(
'cache-request',
sharedCode,
sharedPaths,
async () => {
revalidationCalls++
return 'directory' as const
},
(type) => revalidationEvents.push(`state:${type}`),
(type) => revalidationEvents.push(`open:${type}`)
)
check('click revalidation bypasses the mount cache, updates state, then opens the refreshed type',
revalidatedType === 'directory' && revalidationCalls === 1 &&
getCachedResolvedType('cache-request', sharedCode, sharedPaths) === 'directory' &&
revalidationEvents.join(',') === 'state:directory,open:directory')
const failedRevalidationEvents: string[] = []
const failedRevalidation = await revalidateAndOpenPath(
'cache-request',
sharedCode,
sharedPaths,
async () => { throw new Error('path changed before opening') },
(type) => failedRevalidationEvents.push(`state:${type}`),
(type) => failedRevalidationEvents.push(`open:${type}`)
)
check('failed click revalidation caches unknown and does not open the stale path',
failedRevalidation === 'unknown' && getCachedResolvedType('cache-request', sharedCode, sharedPaths) === 'unknown' &&
failedRevalidationEvents.join(',') === 'state:unknown')
const cancelledRequest = deferred<PathResolutionValue>()
const cancelledResults: PathResolutionValue[] = []
const cancel = subscribeToPathResolution('cancel-request', '/repo/src/item.ts', [], () => cancelledRequest.promise, (type) => cancelledResults.push(type))
cancel()
cancelledRequest.resolve('directory')
await nextTurn()
check('cancels stale component updates without discarding the shared result',
cancelledResults.length === 0 && getCachedResolvedType('cancel-request', '/repo/src/item.ts', []) === 'directory')
let rejectedCalls = 0
const rejectedResults: PathResolutionValue[] = []
const rejectResolver: PathTypeResolver = () => {
rejectedCalls++
return Promise.reject(new Error('expected resolver failure'))
}
subscribeToPathResolution('error-request', 'symbolName', [], rejectResolver, (type) => rejectedResults.push(type))
await nextTurn()
let rejectedCacheReplay: PathResolutionValue
subscribeToPathResolution('error-request', 'symbolName', [], () => {
rejectedCalls++
return Promise.resolve('symbol')
}, (type) => { rejectedCacheReplay = type })
check('caches resolver failures as unknown and suppresses repeat requests',
rejectedResults[0] === 'unknown' && rejectedCacheReplay! === 'unknown' && rejectedCalls === 1 &&
!isResolvedPathType(rejectedCacheReplay))
console.log(`Path renderer regression checks passed (${checks}).`)
}
void main()
+130
View File
@@ -0,0 +1,130 @@
/**
* 无依赖 PNG 像素差异对比(8 位、非隔行、RGB/RGBA)。
*
* 用途:验证 CSS 重构(如 token 化)是否改变了实际渲染。
*
* 用法:node scripts/pngdiff.mjs <before.png> <after.png> [channelThreshold]
*/
import { readFileSync } from 'node:fs'
import { inflateSync } from 'node:zlib'
function decodePng(buf) {
if (buf.readUInt32BE(0) !== 0x89504e47) throw new Error('不是 PNG')
let pos = 8
let width = 0
let height = 0
let bitDepth = 0
let colorType = 0
let interlace = 0
const idat = []
while (pos < buf.length) {
const len = buf.readUInt32BE(pos)
const type = buf.toString('ascii', pos + 4, pos + 8)
const data = buf.subarray(pos + 8, pos + 8 + len)
if (type === 'IHDR') {
width = data.readUInt32BE(0)
height = data.readUInt32BE(4)
bitDepth = data[8]
colorType = data[9]
interlace = data[12]
} else if (type === 'IDAT') {
idat.push(data)
} else if (type === 'IEND') {
break
}
pos += 12 + len
}
if (bitDepth !== 8) throw new Error(`仅支持 8 位深度,实际 ${bitDepth}`)
if (interlace !== 0) throw new Error('不支持隔行 PNG')
const channels = colorType === 6 ? 4 : colorType === 2 ? 3 : 0
if (!channels) throw new Error(`不支持的颜色类型 ${colorType}`)
const raw = inflateSync(Buffer.concat(idat))
const stride = width * channels
const pixels = Buffer.alloc(height * stride)
for (let y = 0; y < height; y++) {
const filter = raw[y * (stride + 1)]
const line = raw.subarray(y * (stride + 1) + 1, y * (stride + 1) + 1 + stride)
const prev = y > 0 ? pixels.subarray((y - 1) * stride, y * stride) : null
const out = pixels.subarray(y * stride, (y + 1) * stride)
for (let i = 0; i < stride; i++) {
const a = i >= channels ? out[i - channels] : 0
const b = prev ? prev[i] : 0
const c = prev && i >= channels ? prev[i - channels] : 0
let v = line[i]
switch (filter) {
case 0:
break
case 1:
v = (v + a) & 0xff
break
case 2:
v = (v + b) & 0xff
break
case 3:
v = (v + ((a + b) >> 1)) & 0xff
break
case 4: {
const p = a + b - c
const pa = Math.abs(p - a)
const pb = Math.abs(p - b)
const pc = Math.abs(p - c)
const pr = pa <= pb && pa <= pc ? a : pb <= pc ? b : c
v = (v + pr) & 0xff
break
}
default:
throw new Error(`未知过滤器 ${filter}`)
}
out[i] = v
}
}
return { width, height, channels, pixels }
}
const [, , beforePath, afterPath, thresholdArg] = process.argv
const threshold = Number(thresholdArg ?? 0)
const a = decodePng(readFileSync(beforePath))
const b = decodePng(readFileSync(afterPath))
if (a.width !== b.width || a.height !== b.height) {
console.log(`尺寸不同:${a.width}x${a.height} vs ${b.width}x${b.height}`)
const minW = Math.min(a.width, b.width)
const minH = Math.min(a.height, b.height)
console.log(`仅比较重叠区域 ${minW}x${minH}`)
}
const w = Math.min(a.width, b.width)
const h = Math.min(a.height, b.height)
let changed = 0
let maxDelta = 0
let sumDelta = 0
const perChannel = [0, 0, 0, 0]
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
let diff = false
for (let c = 0; c < 3; c++) {
const ia = y * a.width * a.channels + x * a.channels + c
const ib = y * b.width * b.channels + x * b.channels + c
const d = Math.abs(a.pixels[ia] - b.pixels[ib])
if (d > threshold) diff = true
if (d > 0) perChannel[c] += 1
if (d > maxDelta) maxDelta = d
sumDelta += d
}
if (diff) changed++
}
}
const total = w * h
console.log(`总像素 ${total}`)
console.log(`变化像素 ${changed} (${((changed / total) * 100).toFixed(3)}%)`)
console.log(`最大通道差 ${maxDelta}`)
console.log(`平均通道差 ${(sumDelta / (total * 3)).toFixed(4)}`)
console.log(`通道变化数 R=${perChannel[0]} G=${perChannel[1]} B=${perChannel[2]}`)
+51
View File
@@ -0,0 +1,51 @@
/** 读取当前会话消息的真实状态。用法:node scripts/probe-messages.mjs <port> */
const port = Number(process.argv[2] ?? 9335)
async function main() {
const list = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json()
const page = list.filter((t) => t.type === 'page')[0]
const ws = new WebSocket(page.webSocketDebuggerUrl)
await new Promise((r) => ws.addEventListener('open', r))
const send = (method, params = {}) =>
new Promise((res) => {
const id = 1
const h = (e) => {
const d = JSON.parse(e.data)
if (d.id === id) {
ws.removeEventListener('message', h)
res(d.result)
}
}
ws.addEventListener('message', h)
ws.send(JSON.stringify({ id, method, params }))
})
const r = await send('Runtime.evaluate', {
expression: `(async () => {
const sessions = await window.ycode.session.list();
const current = sessions[0];
if (!current) return JSON.stringify({ err: 'no session' });
const msgs = await window.ycode.session.messages(current.id);
return JSON.stringify({
sessionId: current.id,
messages: msgs.slice(-3).map((m) => ({
role: m.role,
status: m.status ?? null,
error: m.error ?? null,
len: (m.content || '').length,
tools: (m.tools || []).length
}))
});
})()`,
returnByValue: true,
awaitPromise: true
})
console.log(JSON.parse(r.result.value))
ws.close()
}
main().catch((e) => {
console.error('失败:', e.message)
process.exit(1)
})
+41
View File
@@ -0,0 +1,41 @@
/** 检查工具卡与最后回复。用法:node scripts/probe-tools.mjs <port> */
const port = Number(process.argv[2] ?? 9335)
async function main() {
const list = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json()
const page = list.filter((t) => t.type === 'page')[0]
const ws = new WebSocket(page.webSocketDebuggerUrl)
await new Promise((r) => ws.addEventListener('open', r))
const send = (method, params = {}) =>
new Promise((res) => {
const id = 1
const h = (e) => {
const d = JSON.parse(e.data)
if (d.id === id) {
ws.removeEventListener('message', h)
res(d.result)
}
}
ws.addEventListener('message', h)
ws.send(JSON.stringify({ id, method, params }))
})
const r = await send('Runtime.evaluate', {
expression: `JSON.stringify({
toolCards: document.querySelectorAll('.tool-card').length,
toolNames: [...document.querySelectorAll('.tool-card__name')].map((n) => n.textContent),
toolStatus: [...document.querySelectorAll('.tool-card__dot')].map((n) => n.dataset.status),
lastContent: [...document.querySelectorAll('.msg--assistant .msg__content')].pop()?.textContent.slice(0, 400),
statuses: [...document.querySelectorAll('.msg--assistant')].map((m) => m.querySelector('.msg__status')?.textContent ?? '')
})`,
returnByValue: true
})
console.log(JSON.parse(r.result.value))
ws.close()
}
main().catch((e) => {
console.error('失败:', e.message)
process.exit(1)
})
@@ -0,0 +1 @@
{"actions":[{"kind":"click","step_id":"activate-current-algorithm-expert","selector":"[role=dialog] button[class*='_headerSummonBtn_']","frames":0},{"kind":"wait","step_id":"algorithm-package-ready","duration_ms":1800,"frames":1}]}
+14
View File
@@ -0,0 +1,14 @@
/** Read only retained REA observations. No original filesystem access. */
import fs from 'node:fs'
import crypto from 'node:crypto'
const partial=JSON.parse(fs.readFileSync('.tmp/rea-cpp-plugins-analysis-20261016.json','utf8')).details.partial_observation
const modules=partial.normalized_result.graph.nodes.flatMap(n=>n.observations??[]).filter(o=>o.evidence?.extractor?.operation==='inventory-relevant-file')
const metadata=modules.filter(o=>/^marketplaces\/experts\/plugins\/[^/]+\/\.(?:codebuddy|workbuddy)-plugin\/plugin\.json$/.test(o.properties.path)).map(o=>({path:o.properties.path,sha256:o.evidence.artifact.sha256,bytes:o.properties.bytes}))
const exported=JSON.parse(fs.readFileSync('.tmp/rea-cpp-text-ev_5cc1639489033f42be3eef3c4eea6df9f41c54201a6c86f36680473cd278f8d2.json','utf8'))
for(const f of exported.normalized_result.files)if(crypto.createHash('sha256').update(f.text).digest('hex')!==f.sha256)throw Error('Retained export SHA mismatch')
const catalog=JSON.parse(exported.normalized_result.files.find(f=>f.path==='manifest.json').text),map=JSON.parse(exported.normalized_result.files.find(f=>f.path==='expert-bundle-map.json').text)
const matches=catalog.experts.filter(e=>/高级算法|小算|algorithm engineer|algorithm-engineer/i.test(JSON.stringify([e.id,e.name,e.displayName,e.profession,e.plugin,e.agentName])))
const otherCandidates=modules.filter(o=>!o.properties.path.includes('node_modules/')&&/algorithm|小算|算法/i.test(o.properties.path)).map(o=>({path:o.properties.path,sha256:o.evidence.artifact.sha256,bytes:o.properties.bytes}))
const report={partialEvidence:partial.evidence_id,partialScope:partial.normalized_result.graph.coverage,cacheEvidence:exported.evidence_id,metadata,catalogMatches:matches,bundleMap:map,otherCandidates}
fs.writeFileSync('.tmp/rea-algorithm-locate.json',JSON.stringify(report,null,2))
console.log(JSON.stringify(report,null,2))
+19
View File
@@ -0,0 +1,19 @@
/** Current exact known metadata directories via formal REA CLI, never original FS directly. */
import fs from 'node:fs'
import path from 'node:path'
import {spawnSync} from 'node:child_process'
import crypto from 'node:crypto'
const retained=JSON.parse(fs.readFileSync('.tmp/rea-algorithm-locate.json','utf8'))
const results=[]
function cli(command,args){const r=spawnSync(process.execPath,['.tmp/rea-css-runtime/scripts/rea.mjs',command,...args],{encoding:'utf8',maxBuffer:4*1024*1024});if(r.status!==0)throw Error(r.stderr||r.stdout);return JSON.parse(r.stdout)}
for(const old of retained.metadata){
const root='C:/Users/Administrator/.workbuddy/plugins/'+path.posix.dirname(old.path)
try{
const inventory=cli('inspect-selected-directory-text-root',[root]);fs.writeFileSync(`.tmp/rea-algorithm-metadata-inventory-${inventory.evidence_id}.json`,JSON.stringify(inventory))
const evidence=cli('export-selected-directory-text',[root,inventory.normalized_result.root_sha256,JSON.stringify(['plugin.json'])])
const file=evidence.normalized_result.files[0];if(crypto.createHash('sha256').update(file.text).digest('hex')!==file.sha256)throw Error('SHA mismatch')
fs.writeFileSync(`.tmp/rea-algorithm-metadata-${evidence.evidence_id}.json`,JSON.stringify(evidence))
const p=JSON.parse(file.text);results.push({root,evidenceId:evidence.evidence_id,rootSha256:inventory.normalized_result.root_sha256,sha256:file.sha256,oldInventoryShaMatches:file.sha256===old.sha256,name:p.name,profession:p.profession,displayName:p.displayName,agentName:p.agentName,isTarget:/高级算法|小算|algorithm engineer|algorithm-engineer/i.test(JSON.stringify([p.name,p.profession,p.displayName,p.agentName]))})
}catch(error){results.push({root,error:String(error),status:'unknown'})}
}
fs.writeFileSync('.tmp/rea-algorithm-current-metadata.json',JSON.stringify({results},null,2));console.log(JSON.stringify({results},null,2))
+27
View File
@@ -0,0 +1,27 @@
/** Reproduce target recovery only through public formal REA directory CLI. */
import fs from 'node:fs'
import path from 'node:path'
import crypto from 'node:crypto'
import {spawnSync} from 'node:child_process'
const input=process.argv[2]
if(!input)throw Error('Provide retained narrow package analysis or formal directory inventory')
const analysis=JSON.parse(fs.readFileSync(input,'utf8')),source=analysis.normalized_result
const root=source?.input_path??source?.root_identity?.path,pin=source?.root_artifact_sha256??source?.root_sha256,recovered='.tmp/rea-algorithm-recovered-20261016'
if(!root||!pin)throw Error('Pinned narrow REA analysis or formal inventory required')
function cli(command,args){const r=spawnSync(process.execPath,['.tmp/rea-css-runtime/scripts/rea.mjs',command,...args],{encoding:'utf8',maxBuffer:40*1024*1024});if(r.status!==0)throw Error(r.stderr||r.stdout);return JSON.parse(r.stdout)}
const inventory=cli('inspect-selected-directory-text-root',[root]);if(inventory.normalized_result.root_sha256!==pin)throw Error('Package changed since narrow REA analysis')
fs.writeFileSync(`.tmp/rea-algorithm-inventory-${inventory.evidence_id}.json`,JSON.stringify(inventory))
if(inventory.normalized_result.coverage.status!=='complete')throw Error('Target package inventory is incomplete')
const members=inventory.normalized_result.members.filter(m=>m.kind==='file'),meta=members.find(m=>/^\.(codebuddy|workbuddy)-plugin\/plugin\.json$/.test(m.path))
if(!meta)throw Error('No exact source plugin manifest')
function exportBatch(list,binary=false){return cli(binary?'export-selected-directory-binary':'export-selected-directory-text',[root,pin,JSON.stringify(binary?list.map(m=>({path:m.path,sha256:m.sha256})):list.map(m=>m.path))])}
const identityEvidence=exportBatch([meta]),p=JSON.parse(identityEvidence.normalized_result.files[0].text)
if(!/高级算法|小算|algorithm.?engineer/i.test(JSON.stringify([p.name,p.displayName,p.profession,p.agentName])))throw Error('Source metadata does not establish the requested algorithm expert')
const manifest={sourceExpert:'高级算法工程师 | 小算',sourceRoot:root,rootSha256:pin,inputEvidenceId:analysis.evidence_id,inputEvidenceKind:source.input_path?'javascript-application-analysis':'selected-directory-inventory',...(source.input_path?{analysisEvidenceId:analysis.evidence_id}:{}),inventoryEvidenceId:inventory.evidence_id,inventoryCoverage:inventory.normalized_result.coverage,identity:{name:p.name,agentName:p.agentName,expertType:p.expertType,displayName:p.displayName,profession:p.profession,version:p.version,agents:p.agents,skills:p.skills,dependencies:p.dependencies,mcpServers:p.mcpServers},files:[],omittedMarkers:[],unknown:[]}
const text=[],binary=[]
for(const m of members){if(/^\.(downloaded_at|created-by-session|pending-preview-session)$/.test(m.path)){manifest.omittedMarkers.push(m.path);continue}if(/\.png$/i.test(m.path))binary.push(m);else if(/\.(md|txt|json|yaml|yml|py|js|css|html|mjs|cjs|ps1)$/i.test(m.path)||/(^|\/)(LICENSE|NOTICE)$/i.test(m.path))text.push(m);else manifest.unknown.push({path:m.path,reason:'Required extension not yet supported by this recovery script'})}
function restore(evidence){if(evidence.normalized_result.root_sha256!==pin)throw Error('Root mismatch');const exportPath=`.tmp/rea-algorithm-export-${evidence.evidence_id}.json`;fs.writeFileSync(exportPath,JSON.stringify(evidence));for(const f of evidence.normalized_result.files){const original=members.find(m=>m.path===f.path),bytes=f.encoding==='base64'?Buffer.from(f.data_base64,'base64'):Buffer.from(f.text,'utf8'),sha=crypto.createHash('sha256').update(bytes).digest('hex');if(sha!==f.sha256||sha!==original.sha256||bytes.length!==f.size_bytes||bytes.length!==original.size_bytes)throw Error('Source byte/SHA mismatch');if(f.path.split('/').some(p=>!p||p==='.'||p==='..')||f.path.includes('\\'))throw Error('Noncanonical relative path');const destination=path.join(recovered,...f.path.split('/'));fs.mkdirSync(path.dirname(destination),{recursive:true});fs.writeFileSync(destination,bytes);if(crypto.createHash('sha256').update(fs.readFileSync(destination)).digest('hex')!==sha)throw Error('Recovery disk SHA mismatch');manifest.files.push({path:f.path,bytes:f.size_bytes,sha256:sha,evidenceId:evidence.evidence_id,retainedExport:exportPath,...(f.encoding?{encoding:f.encoding,mediaType:f.media_type}:{})})}}
restore(identityEvidence)
for(const [list,isBinary]of [[text.filter(m=>m.path!==meta.path),false],[binary,true]]){let batch=[],bytes=0;for(const m of list){if(batch.length&&(batch.length>=12||bytes+m.size_bytes>8*1024*1024)){restore(exportBatch(batch,isBinary));batch=[];bytes=0}batch.push(m);bytes+=m.size_bytes}if(batch.length)restore(exportBatch(batch,isBinary))}
fs.writeFileSync(path.join(recovered,'evidence-manifest.json'),JSON.stringify(manifest,null,2)+'\n')
console.log(JSON.stringify({result:manifest.unknown.length?'PARTIAL':'PASS',recovered,identity:manifest.identity,files:manifest.files.length,bytes:manifest.files.reduce((n,f)=>n+f.bytes,0),unknown:manifest.unknown,omittedMarkers:manifest.omittedMarkers},null,2))
+50
View File
@@ -0,0 +1,50 @@
/** Verify retained REA exports and recovery only; never read original paths. */
import fs from 'node:fs'
import path from 'node:path'
import crypto from 'node:crypto'
import assert from 'node:assert/strict'
import {parse} from 'yaml'
const root='.tmp/rea-algorithm-recovered-20261016'
const manifest=JSON.parse(fs.readFileSync(path.join(root,'evidence-manifest.json'),'utf8'))
const inventory=JSON.parse(fs.readFileSync('.tmp/rea-algorithm-package-inventory.json','utf8'))
const members=inventory.normalized_result.members.filter(x=>x.kind==='file')
assert.equal(inventory.normalized_result.root_sha256,manifest.rootSha256)
assert.equal(inventory.normalized_result.coverage.status,'complete')
assert.deepEqual(manifest.unknown,[])
const checked=[]
for(const file of manifest.files){
assert.ok(!path.isAbsolute(file.path)&&!file.path.split('/').includes('..'))
const bytes=fs.readFileSync(path.join(root,...file.path.split('/')))
const sha=crypto.createHash('sha256').update(bytes).digest('hex')
const entry=members.find(x=>x.path===file.path)
assert.equal(sha,file.sha256);assert.equal(sha,entry.sha256)
assert.equal(bytes.length,file.bytes);assert.equal(bytes.length,entry.size_bytes)
const evidence=JSON.parse(fs.readFileSync(file.retainedExport,'utf8'))
assert.equal(evidence.evidence_id,file.evidenceId)
assert.equal(evidence.normalized_result.root_sha256,manifest.rootSha256)
const exported=evidence.normalized_result.files.find(x=>x.path===file.path)
const exportedBytes=exported.encoding==='base64'?Buffer.from(exported.data_base64,'base64'):Buffer.from(exported.text,'utf8')
assert.deepEqual(bytes,exportedBytes)
checked.push(file.path)
}
assert.deepEqual(members.map(x=>x.path).sort(),[...checked,...manifest.omittedMarkers].sort())
assert.deepEqual(manifest.omittedMarkers,['.downloaded_at'])
const read=rel=>fs.readFileSync(path.join(root,...rel.split('/')),'utf8')
const plugin=JSON.parse(read('.codebuddy-plugin/plugin.json'))
const frontmatter=rel=>parse(read(rel).match(/^---\r?\n([\s\S]*?)\r?\n---/)[1])
const agent=frontmatter(plugin.agents[0].replace(/^\.\//,''))
const skillRoot=plugin.skills[0].replace(/^\.\//,''),skill=frontmatter(`${skillRoot}/SKILL.md`)
assert.equal(agent.name,plugin.agentName)
assert.ok(agent.skills.includes(skill.name))
assert.equal(agent.maxTurns,100)
assert.ok(!Object.hasOwn(agent,'tools'))
assert.ok(!Object.hasOwn(plugin,'dependencies')&&!Object.hasOwn(plugin,'mcpServers'))
const references=[...read(`${skillRoot}/SKILL.md`).matchAll(/@((?:references|templates)\/[\w./-]+\.md)/g)].map(m=>m[1])
for(const rel of references)assert.ok(checked.includes(`${skillRoot}/${rel}`))
assert.ok(checked.includes(`${skillRoot}/scripts/verify_shortest_paths.py`))
const avatar=fs.readFileSync(path.join(root,plugin.avatar))
assert.equal(avatar.subarray(0,8).toString('hex'),'89504e470d0a1a0a')
assert.equal(avatar.readUInt32BE(16),512);assert.equal(avatar.readUInt32BE(20),512)
const report={result:'PASS',scope:'Retained REA bytes/SHA, complete inventory coverage, JSON/YAML identity and internal skill references; no original script execution or model behavior tested',rootSha256:manifest.rootSha256,files:checked.length,bytes:manifest.files.reduce((n,x)=>n+x.bytes,0),internalMarkdownReferences:references.length,avatar:{width:512,height:512,bytes:avatar.length},omittedMarkers:manifest.omittedMarkers}
fs.writeFileSync('.tmp/rea-algorithm-verification.json',JSON.stringify(report,null,2)+'\n')
console.log(JSON.stringify(report,null,2))
+74
View File
@@ -0,0 +1,74 @@
// Invoke the public repaired REA CLI. This wrapper has no target protocol access.
import fs from 'node:fs/promises'
import path from 'node:path'
import { spawn } from 'node:child_process'
import { fileURLToPath } from 'node:url'
const project = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
process.chdir(project)
const includeVisuals = process.argv.includes('--visuals')
const args = process.argv.slice(2).filter(arg => arg !== '--visuals')
function option(name) { const index=args.indexOf(name);if(index<0)return undefined;const value=args[index+1];if(!value||value.startsWith('--'))throw new Error(`Missing ${name} value`);args.splice(index,2);return value }
const probeFile=option('--probe'),outputDirectory=option('--output'),selectedTargetId=option('--target-id')
const probe=probeFile?JSON.parse(await fs.readFile(probeFile,'utf8')):undefined
if(probe&&!includeVisuals)throw new Error('--probe requires --visuals')
const endpoint = args[0] || 'http://127.0.0.1:9223'
const expectedPage = args[1] || 'C:/Users/Administrator/AppData/Local/Programs/WorkBuddy/resources/app.asar/renderer/index.html'
const launcher = path.resolve('.tmp/rea-css-runtime/scripts/rea.mjs')
await fs.access(launcher).catch(() => { throw new Error('Run npm run rea:css:prepare first') })
async function run(args) {
return new Promise((resolve, reject) => {
const child = spawn(process.execPath, [launcher, ...args, '--json'], { cwd: project, stdio: ['ignore', 'pipe', 'inherit'], windowsHide: true })
const chunks = []
child.stdout.on('data', chunk => chunks.push(chunk))
child.once('error', reject)
child.once('close', async code => {
if (code === 0) return resolve(Buffer.concat(chunks))
await fs.writeFile('.tmp/rea-capture-failure.json', Buffer.concat(chunks))
reject(new Error(`REA exited with ${code}; diagnostic saved to .tmp/rea-capture-failure.json`))
})
})
}
const normalize = value => value.replace(/\\/g, '/').toLowerCase()
const discoveryBytes = await run(['list-electron-targets', endpoint])
const discovery = JSON.parse(discoveryBytes.toString('utf8'))
if (discovery.operation !== 'list_electron_targets') throw new Error('REA did not return target discovery Evidence')
const matches = discovery.normalized_result.targets.filter(target => normalize(target.file_path) === normalize(expectedPage) && (!selectedTargetId || target.target_id === selectedTargetId))
if (matches.length !== 1) throw new Error(`Expected exactly one authorized WorkBuddy page; found ${matches.length}`)
const bytes = await run(['inspect-electron-page', endpoint, matches[0].target_id, '--include-style-sources', ...(includeVisuals ? ['--include-visuals'] : []), ...(probe?['--visual-probe',JSON.stringify(probe)]:[]), '--observation-ms', '250'])
const evidence = JSON.parse(bytes.toString('utf8'))
if (evidence.operation !== 'inspect_electron_page' || evidence.parameters?.include_style_sources !== true || !evidence.normalized_result?.styles) throw new Error('REA did not return CSS Evidence')
await fs.writeFile('.tmp/rea-css-targets.json', discoveryBytes)
await fs.writeFile('.tmp/rea-workbuddy-css-evidence.json', bytes)
await fs.mkdir('.tmp/rea-css-evidence', { recursive: true })
if (!/^ev_[a-f0-9]{64}$/.test(evidence.evidence_id) || !/^ev_[a-f0-9]{64}$/.test(discovery.evidence_id)) throw new Error('Invalid REA Evidence identity')
await fs.writeFile(path.join('.tmp/rea-css-evidence', `${discovery.evidence_id}.json`), discoveryBytes)
const archive = path.join('.tmp/rea-css-evidence', `${evidence.evidence_id}.json`)
await fs.writeFile(archive, bytes)
if (includeVisuals) {
const visuals = evidence.normalized_result.visuals
if (!visuals) throw new Error('REA did not retain visual capture')
const visualDirectory=outputDirectory?path.resolve(outputDirectory):path.resolve('.tmp/rea-visual-reference')
await fs.mkdir(visualDirectory, { recursive: true })
const { createHash } = await import('node:crypto')
const image = Buffer.from(visuals.artifact.data_base64, 'base64')
if (createHash('sha256').update(image).digest('hex') !== visuals.artifact.sha256 || image.length !== visuals.artifact.bytes) throw new Error('REA screenshot digest mismatch')
await fs.writeFile(path.join(visualDirectory,'workbuddy.png'), image)
await fs.writeFile(path.join(visualDirectory,'layout.json'), JSON.stringify({ evidenceId: evidence.evidence_id, archive: path.resolve(archive), ...visuals, artifact: { ...visuals.artifact, data_base64: undefined },probe:visuals.probe?{...visuals.probe,frames:visuals.probe.frames.map(f=>({...f,artifact:{...f.artifact,data_base64:undefined}}))}:undefined }, null, 2))
for(let index=0;index<(visuals.probe?.frames.length??0);index++){
const frame=visuals.probe.frames[index],png=Buffer.from(frame.artifact.data_base64,'base64');
if(createHash('sha256').update(png).digest('hex')!==frame.artifact.sha256)throw new Error('REA scenario frame digest mismatch');
await fs.writeFile(path.join(visualDirectory,`frame-${String(index).padStart(3,'0')}.png`),png)
}
if(visuals.font_samples){
const os=await import('node:os');
const report={evidenceId:evidence.evidence_id,archive:path.resolve(archive),evidenceSha256:createHash('sha256').update(bytes).digest('hex'),version:evidence.normalized_result.browser.user_agent.match(/WorkBuddy[ /]([\d.]+)/i)?.[1]??'unobserved',environment:{platform:os.platform(),osRelease:os.release(),browser:evidence.normalized_result.browser},source:{kind:'rea-workbuddy'},samples:visuals.font_samples,missing:visuals.font_missing??[]};
await fs.writeFile(path.join(visualDirectory,'font-platform-report.json'),JSON.stringify(report,null,2))
await fs.writeFile('.tmp/rea-font-platform-report.json',JSON.stringify(report,null,2))
}
}
const styles = evidence.normalized_result.styles
console.log(JSON.stringify({ evidenceId: evidence.evidence_id, targetId: matches[0].target_id, total: styles.total,
included: styles.items.filter(item => item.source.included).length, excluded: styles.excluded,
sourceFailures: styles.source_failures, truncated: styles.truncated,
output: path.resolve('.tmp/rea-workbuddy-css-evidence.json'), archive: path.resolve(archive) }, null, 2))
@@ -0,0 +1,37 @@
import { createRequire } from 'node:module'
import { mkdir, writeFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
import { captureYcodePlatformFonts } from './verify-font-platform-parity.mjs'
const require = createRequire(import.meta.url)
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
const outputPath = path.resolve(projectRoot, process.argv[2] ?? '.tmp/ycode-font-platform-selftest-reference.json')
const samples = [
{ category: 'assistant-name', text: 'YCode' },
{ category: 'chinese-body', text: '中文字体字形探针:你好,世界!这是用于复现的正文样本。' },
{ category: 'english-body', text: 'Typography fixture: The quick brown fox jumps over 13 lazy dogs.' },
{ category: 'user-bubble', text: '请对照这条用户消息的 glyph count,其中包含 English。' },
{ category: 'code', text: 'const greeting = "你好, YCode";\nconsole.log(greeting);' }
]
const captured = await captureYcodePlatformFonts(samples)
const report = {
schemaVersion: 1,
source: { kind: 'ycode-electron-self-test', description: 'Local YCode Electron fixture; this is not WorkBuddy evidence.' },
evidenceId: `local-ycode-font-fixture-${Date.now()}`,
version: `YCode ${require('../../package.json').version}`,
capturedAt: new Date().toISOString(),
environment: captured.environment,
samples: captured.samples,
missing: []
}
await mkdir(path.dirname(outputPath), { recursive: true })
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({
kind: report.source.kind,
evidenceId: report.evidenceId,
version: report.version,
samples: report.samples.map((sample) => ({ category: sample.category, fontFamily: sample.fontFamily, platformFonts: sample.platformFonts })),
outputPath: path.relative(projectRoot, outputPath).replaceAll('\\', '/')
}, null, 2))
File diff suppressed because it is too large. Load diff
+590
View File
@@ -0,0 +1,590 @@
import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import fs from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { createRequire } from 'node:module'
import { spawn } from 'node:child_process'
import { pathToFileURL } from 'node:url'
const require = createRequire(import.meta.url)
const electronExecutable = require('electron')
const runtime = path.resolve('.tmp/rea-css-runtime/dist')
const hash = bytes => createHash('sha256').update(bytes).digest('hex')
const evidenceIdPattern = /^ev_[a-f0-9]{64}$/
const electronHelperSource = String.raw`
const fs = require('node:fs')
const path = require('node:path')
const { app, nativeImage } = require('electron')
const jobPath = process.env.YCODE_PIXEL_JOB
const resultPath = process.env.YCODE_PIXEL_RESULT
const finish = (value, code = 0) => {
try { fs.writeFileSync(resultPath, JSON.stringify(value)) } catch {}
app.exit(code)
}
const fail = error => finish({ ok: false, error: String(error && (error.stack || error.message) || error) }, 1)
try {
const job = JSON.parse(fs.readFileSync(jobPath, 'utf8'))
const isolated = path.join(path.dirname(jobPath), 'electron-data')
for (const name of ['userData', 'sessionData', 'logs']) {
try { app.setPath(name, path.join(isolated, name)) } catch {}
}
app.disableHardwareAcceleration()
app.whenReady().then(() => {
try {
if (job.mode === 'fixtures') {
const createPng = (file, width, height, changed) => {
const bitmap = Buffer.alloc(width * height * 4)
for (let offset = 0; offset < bitmap.length; offset += 4) bitmap.set([20, 40, 60, 255], offset)
if (changed) bitmap[0] += 1
const image = nativeImage.createFromBitmap(bitmap, { width, height, scaleFactor: 1 })
if (image.isEmpty()) throw new Error('nativeImage could not encode self-test fixture')
fs.writeFileSync(file, image.toPNG())
}
createPng(job.files.reference, 2, 2, false)
createPng(job.files.changed, 2, 2, true)
createPng(job.files.wide, 3, 2, false)
finish({ ok: true })
return
}
if (job.mode !== 'compare') throw new Error('Unknown nativeImage helper mode')
const output = []
for (const pair of job.pairs) {
const reference = nativeImage.createFromBuffer(fs.readFileSync(pair.reference))
const candidate = nativeImage.createFromBuffer(fs.readFileSync(pair.candidate))
if (reference.isEmpty() || candidate.isEmpty()) throw new Error('A screenshot could not be decoded by nativeImage')
const beforeSize = reference.getSize(1)
const afterSize = candidate.getSize(1)
const base = {
name: pair.name,
before: beforeSize,
after: afterSize,
channel_tolerance: job.channelTolerance,
}
if (beforeSize.width !== afterSize.width || beforeSize.height !== afterSize.height) {
output.push({ ...base, status: 'dimension_mismatch', compared_pixels: 0, changed_pixels: null, changed_ratio: null, mean_absolute_channel_delta: null, maximum_channel_delta: null, diff_width: null, diff_height: null })
continue
}
const region = pair.region || { x: 0, y: 0, width: beforeSize.width, height: beforeSize.height }
if (!Number.isInteger(region.x) || !Number.isInteger(region.y) || !Number.isInteger(region.width) || !Number.isInteger(region.height) || region.x < 0 || region.y < 0 || region.width < 1 || region.height < 1 || region.x + region.width > beforeSize.width || region.y + region.height > beforeSize.height) {
throw new Error('Comparison region must be a positive integer rectangle inside both screenshots')
}
const beforeBitmap = reference.toBitmap({ scaleFactor: 1 })
const afterBitmap = candidate.toBitmap({ scaleFactor: 1 })
const expectedBeforeBytes = beforeSize.width * beforeSize.height * 4
const expectedAfterBytes = afterSize.width * afterSize.height * 4
if (beforeBitmap.length !== expectedBeforeBytes || afterBitmap.length !== expectedAfterBytes) throw new Error('nativeImage returned an unexpected bitmap byte count')
const diffBitmap = Buffer.alloc(region.width * region.height * 4)
const comparedPixels = region.width * region.height
let changedPixels = 0
let maximumChannelDelta = 0
let absoluteChannelDelta = 0
for (let y = 0; y < region.height; y += 1) {
for (let x = 0; x < region.width; x += 1) {
const beforeOffset = ((region.y + y) * beforeSize.width + region.x + x) * 4
const afterOffset = ((region.y + y) * afterSize.width + region.x + x) * 4
const diffOffset = (y * region.width + x) * 4
let pixelMaximum = 0
for (let channel = 0; channel < 4; channel += 1) {
const delta = Math.abs(beforeBitmap[beforeOffset + channel] - afterBitmap[afterOffset + channel])
absoluteChannelDelta += delta
pixelMaximum = Math.max(pixelMaximum, delta)
maximumChannelDelta = Math.max(maximumChannelDelta, delta)
}
if (pixelMaximum > job.channelTolerance) {
changedPixels += 1
diffBitmap.fill(255, diffOffset, diffOffset + 4)
}
}
}
const diffImage = nativeImage.createFromBitmap(diffBitmap, { width: region.width, height: region.height, scaleFactor: 1 })
if (diffImage.isEmpty()) throw new Error('nativeImage could not encode the diff bitmap')
fs.writeFileSync(pair.diffPath, diffImage.toPNG())
output.push({
...base,
region,
status: changedPixels === 0 ? 'identical' : 'different',
compared_pixels: comparedPixels,
changed_pixels: changedPixels,
changed_ratio: changedPixels / comparedPixels,
mean_absolute_channel_delta: absoluteChannelDelta / (comparedPixels * 4),
maximum_channel_delta: maximumChannelDelta,
diff_width: region.width,
diff_height: region.height,
})
}
finish({ ok: true, pairs: output })
} catch (error) {
fail(error)
}
}).catch(fail)
} catch (error) {
fail(error)
}
`
function parseManifest(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Manifest must be a JSON object')
const source = value.source ?? { kind: 'local-fixture' }
if (!source || typeof source !== 'object' || !['rea-workbuddy', 'local-fixture'].includes(source.kind)) throw new Error('source.kind must be rea-workbuddy or local-fixture')
if (!Array.isArray(value.requiredStates) || value.requiredStates.length === 0 || value.requiredStates.some(state => typeof state !== 'string' || !state.trim()) || new Set(value.requiredStates).size !== value.requiredStates.length) throw new Error('requiredStates must be a non-empty array of unique non-empty names')
if (!Array.isArray(value.pairs)) throw new Error('pairs must be an array')
const names = new Set()
const pairs = value.pairs.map((pair, index) => {
if (!pair || typeof pair !== 'object' || Array.isArray(pair)) throw new Error(`pairs[${index}] must be an object`)
if (typeof pair.name !== 'string' || !pair.name.trim() || names.has(pair.name)) throw new Error(`pairs[${index}].name must be unique and non-empty`)
names.add(pair.name)
if (typeof pair.reference !== 'string' || !pair.reference || typeof pair.candidate !== 'string' || !pair.candidate) throw new Error(`pairs[${index}] requires reference and candidate PNG paths`)
if (pair.region !== undefined) {
const { x, y, width, height } = pair.region ?? {}
if (![x, y, width, height].every(Number.isInteger) || x < 0 || y < 0 || width < 1 || height < 1) throw new Error(`pairs[${index}].region must be a positive integer rectangle`)
}
if (source.kind === 'rea-workbuddy') {
const evidence = pair.referenceEvidence
if (!evidence || typeof evidence !== 'object' || !evidenceIdPattern.test(evidence.id) || typeof evidence.archive !== 'string' || !evidence.archive) throw new Error(`pairs[${index}] requires referenceEvidence with a valid Evidence id and archive`)
}
return { ...pair }
})
const channelTolerance = value.channelTolerance ?? 0
if (!Number.isInteger(channelTolerance) || channelTolerance < 0 || channelTolerance > 255) throw new Error('channelTolerance must be an integer from 0 to 255')
return { source, requiredStates: [...value.requiredStates], pairs, channelTolerance }
}
async function authenticateEvidence(evidencePath) {
const modulePath = pathToFileURL(path.join(runtime, 'domain/evidence.js'))
let parseEvidence
try {
({ parseEvidence } = await import(modulePath))
} catch (error) {
throw new Error(`Prepared REA runtime is required to authenticate Evidence: ${error.message}`)
}
return parseEvidence(evidencePath)
}
function screenshotArtifacts(evidence) {
const visuals = evidence.normalized_result?.visuals
const artifacts = []
if (visuals?.artifact) artifacts.push({ location: 'visuals.artifact', artifact: visuals.artifact })
for (let index = 0; index < (visuals?.probe?.frames?.length ?? 0); index += 1) {
const artifact = visuals.probe.frames[index]?.artifact
if (artifact) artifacts.push({ location: `visuals.probe.frames[${index}].artifact`, artifact })
}
return artifacts
}
function checkedArtifactHash(artifact) {
if (!artifact || artifact.media_type !== 'image/png' || typeof artifact.data_base64 !== 'string' || typeof artifact.sha256 !== 'string') return undefined
const bytes = Buffer.from(artifact.data_base64, 'base64')
if (!bytes.length || bytes.toString('base64') !== artifact.data_base64 || (artifact.bytes !== undefined && artifact.bytes !== bytes.length)) return undefined
const actualHash = hash(bytes)
return actualHash === artifact.sha256 ? actualHash : undefined
}
async function verifyReferenceEvidence(pair, manifestDirectory, referenceHash) {
const archive = path.resolve(manifestDirectory, pair.referenceEvidence.archive)
let raw
try {
raw = JSON.parse(await fs.readFile(archive, 'utf8'))
} catch (error) {
throw new Error(`Cannot read reference Evidence archive for ${pair.name}: ${error.message}`)
}
if (raw.evidence_id !== pair.referenceEvidence.id) throw new Error(`Evidence id does not match its archive for ${pair.name}`)
let evidence
try {
evidence = await authenticateEvidence(raw)
} catch (error) {
throw new Error(`Reference Evidence archive failed authentication for ${pair.name}: ${error.message}`)
}
if (evidence.operation !== 'inspect_electron_page') throw new Error(`Reference Evidence is not an Electron page inspection for ${pair.name}`)
if (evidence.provider?.id !== 'rea-cdp-electron') throw new Error(`Reference Evidence provider is not rea-cdp-electron for ${pair.name}`)
const targetPath = evidence.normalized_result?.target?.file_path
if (typeof targetPath !== 'string' || !/WorkBuddy[\\/]resources[\\/]app\.asar[\\/]renderer[\\/]index\.html$/i.test(targetPath)) {
throw new Error(`Reference Evidence target is not the WorkBuddy renderer for ${pair.name}`)
}
const visualProbe = evidence.parameters?.visual_probe
if (visualProbe !== undefined && (!visualProbe || typeof visualProbe !== 'object' || Array.isArray(visualProbe) || !Array.isArray(visualProbe.actions))) {
throw new Error(`Reference Evidence visual_probe.actions is malformed for ${pair.name}`)
}
const visualProbeActions = visualProbe?.actions ?? []
const hasVisualProbeActions = visualProbeActions.length > 0
const expectedAuthority = hasVisualProbeActions ? 'controlled-replay' : 'external-service'
if (evidence.authority !== expectedAuthority) throw new Error(`Reference Evidence authority does not match visual_probe.actions for ${pair.name}`)
const userAgent = evidence.normalized_result?.browser?.user_agent
const version = typeof userAgent === 'string' ? userAgent.match(/WorkBuddy[ /]([\d.]+)/i)?.[1] ?? 'unobserved' : 'unobserved'
const matching = screenshotArtifacts(evidence).filter(({ artifact }) => checkedArtifactHash(artifact) === referenceHash)
if (!matching.length) throw new Error(`Reference PNG SHA is absent from authenticated REA baseline/probe artifacts for ${pair.name}`)
return {
id: evidence.evidence_id,
archive,
artifact_locations: matching.map(item => item.location),
sha256: referenceHash,
target_file_path: targetPath,
authority: evidence.authority,
version,
version_observed: version !== 'unobserved',
}
}
function safeDiffName(name) {
const stem = name.normalize('NFKD').replace(/[^A-Za-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 48) || 'state'
return `${stem}-${hash(Buffer.from(name, 'utf8')).slice(0, 12)}-diff.png`
}
async function runElectronHelper(job) {
const helperDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'ycode-visual-pixel-helper-'))
const helperPath = path.join(helperDirectory, 'helper.cjs')
const packagePath = path.join(helperDirectory, 'package.json')
const jobPath = path.join(helperDirectory, 'job.json')
const resultPath = path.join(helperDirectory, 'result.json')
await fs.writeFile(helperPath, electronHelperSource, 'utf8')
await fs.writeFile(packagePath, JSON.stringify({ name: 'ycode-visual-pixel-helper', version: '1.0.0', main: 'helper.cjs' }), 'utf8')
await fs.writeFile(jobPath, JSON.stringify(job), 'utf8')
const env = Object.fromEntries(Object.entries(process.env).filter(([name]) => name.toUpperCase() !== 'ELECTRON_RUN_AS_NODE'))
env.YCODE_PIXEL_JOB = jobPath
env.YCODE_PIXEL_RESULT = resultPath
try {
const completion = await new Promise((resolve, reject) => {
const child = spawn(electronExecutable, ['--no-sandbox', '--disable-gpu', '--disable-extensions', helperDirectory], {
cwd: helperDirectory,
env,
windowsHide: true,
stdio: ['ignore', 'ignore', 'pipe'],
})
let stderr = ''
let settled = false
const timer = setTimeout(() => {
child.kill()
if (!settled) {
settled = true
reject(new Error('Hidden Electron nativeImage helper timed out'))
}
}, 45000)
timer.unref()
child.stderr.on('data', chunk => { stderr += chunk.toString() })
child.once('error', error => {
clearTimeout(timer)
if (!settled) { settled = true; reject(error) }
})
child.once('close', (code, signal) => {
clearTimeout(timer)
if (settled) return
settled = true
resolve({ code, signal, stderr })
})
})
let result
try { result = JSON.parse(await fs.readFile(resultPath, 'utf8')) } catch {
throw new Error(`Hidden Electron nativeImage helper exited without a result (code ${completion.code}${completion.stderr ? `): ${completion.stderr.trim()}` : ')'}`)
}
if (!result.ok || completion.code !== 0) throw new Error(result.error || `Hidden Electron nativeImage helper failed (code ${completion.code})`)
return result
} finally {
await fs.rm(helperDirectory, { recursive: true, force: true, maxRetries: 8, retryDelay: 200 })
}
}
async function compareManifest(manifest, manifestDirectory, outputDirectory) {
const parsed = parseManifest(manifest)
const prepared = []
for (const pair of parsed.pairs) {
const referencePath = path.resolve(manifestDirectory, pair.reference)
const candidatePath = path.resolve(manifestDirectory, pair.candidate)
let referenceBytes, candidateBytes
try {
[referenceBytes, candidateBytes] = await Promise.all([fs.readFile(referencePath), fs.readFile(candidatePath)])
} catch (error) {
throw new Error(`Cannot read screenshot pair ${pair.name}: ${error.message}`)
}
const referenceSha = hash(referenceBytes)
const candidateSha = hash(candidateBytes)
const referenceEvidence = parsed.source.kind === 'rea-workbuddy'
? await verifyReferenceEvidence(pair, manifestDirectory, referenceSha)
: undefined
prepared.push({
name: pair.name,
reference: referencePath,
candidate: candidatePath,
reference_sha256: referenceSha,
candidate_sha256: candidateSha,
region: pair.region,
reference_evidence: referenceEvidence,
})
}
await fs.mkdir(outputDirectory, { recursive: true })
const helperPairs = prepared.map(pair => ({
name: pair.name,
reference: pair.reference,
candidate: pair.candidate,
region: pair.region,
diffPath: path.join(outputDirectory, safeDiffName(pair.name)),
}))
const nativeResult = await runElectronHelper({ mode: 'compare', channelTolerance: parsed.channelTolerance, pairs: helperPairs })
const nativeByName = new Map(nativeResult.pairs.map(pair => [pair.name, pair]))
const results = prepared.map(pair => {
const compared = nativeByName.get(pair.name)
if (!compared) throw new Error(`nativeImage helper omitted comparison ${pair.name}`)
const dimensions = { width: compared.before.width, height: compared.before.height }
const afterDimensions = { width: compared.after.width, height: compared.after.height }
const diffPath = compared.diff_width === null ? undefined : helperPairs.find(item => item.name === pair.name).diffPath
return {
name: pair.name,
status: compared.status,
reference: { path: pair.reference, sha256: pair.reference_sha256, dimensions },
candidate: { path: pair.candidate, sha256: pair.candidate_sha256, dimensions: afterDimensions },
...(compared.region ? { region: compared.region } : {}),
channel_tolerance: compared.channel_tolerance,
compared_pixels: compared.compared_pixels,
changed_pixels: compared.changed_pixels,
changed_ratio: compared.changed_ratio,
mean_absolute_channel_delta: compared.mean_absolute_channel_delta,
maximum_channel_delta: compared.maximum_channel_delta,
diff_png: diffPath ? { path: diffPath, sha256: undefined, dimensions: { width: compared.diff_width, height: compared.diff_height } } : null,
...(pair.reference_evidence ? { reference_evidence: pair.reference_evidence } : {}),
}
})
for (const pair of results) {
if (pair.diff_png) pair.diff_png.sha256 = hash(await fs.readFile(pair.diff_png.path))
}
const missingRequiredStates = parsed.requiredStates.filter(name => !parsed.pairs.some(pair => pair.name === name))
const status = missingRequiredStates.length
? 'incomplete'
: results.some(pair => pair.status !== 'identical') ? 'different' : 'identical'
const observedVersions = results.flatMap(pair => pair.reference_evidence ? [{
state: pair.name,
evidence_id: pair.reference_evidence.id,
version: pair.reference_evidence.version,
observed: pair.reference_evidence.version_observed,
}] : [])
let originalParity
if (parsed.source.kind !== 'rea-workbuddy') {
originalParity = { status: 'not_assessed', exactOriginalParity: false, versions: [], reasons: ['Source is local fixture data, not authenticated WorkBuddy Evidence.'] }
} else {
const reasons = []
if (missingRequiredStates.length) reasons.push('One or more required states are missing.')
if (observedVersions.length !== results.length || observedVersions.some(item => !item.observed)) reasons.push('A WorkBuddy version was not observed in browser.user_agent.')
if (reasons.length) originalParity = { status: 'incomplete', exactOriginalParity: false, versions: observedVersions, reasons }
else if (results.some(pair => pair.status !== 'identical')) originalParity = { status: 'different', exactOriginalParity: false, versions: observedVersions, reasons: ['At least one declared state differs beyond channel tolerance.'] }
else if (parsed.channelTolerance !== 0) originalParity = { status: 'not_exact', exactOriginalParity: false, versions: observedVersions, reasons: ['Non-zero channel tolerance does not establish exact pixel parity.'] }
else originalParity = { status: 'exact_for_declared_states', exactOriginalParity: true, versions: observedVersions, reasons: [] }
}
const reportStatus = parsed.source.kind === 'rea-workbuddy' && originalParity.status === 'incomplete'
? 'incomplete'
: parsed.source.kind === 'rea-workbuddy' && originalParity.status === 'not_exact'
? 'approximate'
: status
return {
status: reportStatus,
source: parsed.source,
originalParity,
scope: {
assessment: 'declared_state_set_only',
required_states: parsed.requiredStates,
full_1to1: 'not_assessed',
},
missing_required_states: missingRequiredStates,
channel_tolerance: parsed.channelTolerance,
metric_definition: 'Changed pixels have at least one of four decoded bitmap components above tolerance. Mean absolute channel delta is averaged across all four components in the compared region.',
limitations: [
'Only the manifest state set is assessed; this output is not a complete 1:1 product acceptance.',
'Images are decoded to nativeImage 8-bit bitmaps; their platform-specific byte order is compared consistently within this run. No registration, OCR, or semantic layout analysis is performed.',
'Diff PNG marks changed pixels white and unchanged pixels transparent.',
],
pairs: results,
}
}
async function runSelfTest() {
const fixture = await fs.mkdtemp(path.join(os.tmpdir(), 'ycode-visual-pixels-self-test-'))
try {
assert.throws(() => parseManifest({ source: { kind: 'local-fixture' }, requiredStates: [], pairs: [] }), /non-empty array/)
const files = {
reference: path.join(fixture, 'reference.png'),
changed: path.join(fixture, 'changed.png'),
wide: path.join(fixture, 'wide.png'),
}
await runElectronHelper({ mode: 'fixtures', files })
const manifest = {
source: { kind: 'local-fixture' },
requiredStates: ['same', 'one-pixel', 'dimension-mismatch'],
pairs: [
{ name: 'same', reference: files.reference, candidate: files.reference },
{ name: 'one-pixel', reference: files.reference, candidate: files.changed, region: { x: 0, y: 0, width: 1, height: 1 } },
{ name: 'dimension-mismatch', reference: files.reference, candidate: files.wide },
],
}
const report = await compareManifest(manifest, fixture, path.join(fixture, 'diffs'))
assert.equal(report.status, 'different')
assert.equal(report.scope.full_1to1, 'not_assessed')
assert.equal(report.originalParity.status, 'not_assessed')
assert.equal(report.originalParity.exactOriginalParity, false)
const same = report.pairs.find(pair => pair.name === 'same')
assert.equal(same.status, 'identical')
assert.equal(same.compared_pixels, 4)
assert.equal(same.changed_pixels, 0)
assert.equal(same.changed_ratio, 0)
assert.equal(same.maximum_channel_delta, 0)
assert.equal(same.mean_absolute_channel_delta, 0)
const changed = report.pairs.find(pair => pair.name === 'one-pixel')
assert.equal(changed.status, 'different')
assert.equal(changed.changed_pixels, 1)
assert.equal(changed.changed_ratio, 1)
assert.equal(changed.maximum_channel_delta, 1)
assert.equal(changed.mean_absolute_channel_delta, 0.25)
assert.deepEqual(changed.diff_png.dimensions, { width: 1, height: 1 })
assert.match(changed.diff_png.sha256, /^[a-f0-9]{64}$/)
assert.ok((await fs.stat(changed.diff_png.path)).size > 0)
const mismatch = report.pairs.find(pair => pair.name === 'dimension-mismatch')
assert.equal(mismatch.status, 'dimension_mismatch')
assert.deepEqual(mismatch.reference.dimensions, { width: 2, height: 2 })
assert.deepEqual(mismatch.candidate.dimensions, { width: 3, height: 2 })
assert.equal(mismatch.changed_pixels, null)
assert.equal(mismatch.diff_png, null)
const incomplete = await compareManifest({
source: { kind: 'local-fixture' },
requiredStates: ['present', 'missing'],
pairs: [{ name: 'present', reference: files.reference, candidate: files.reference }],
}, fixture, path.join(fixture, 'incomplete-diffs'))
assert.equal(incomplete.status, 'incomplete')
assert.deepEqual(incomplete.missing_required_states, ['missing'])
const { createEvidence } = await import(pathToFileURL(path.join(runtime, 'domain/evidence.js')))
const referenceBytes = await fs.readFile(files.reference)
const changedBytes = await fs.readFile(files.changed)
const asArtifact = bytes => ({ sha256: hash(bytes), bytes: bytes.length, media_type: 'image/png', data_base64: bytes.toString('base64') })
const baselineArtifact = asArtifact(referenceBytes)
const probeArtifact = asArtifact(changedBytes)
const workbuddyTargetPath = 'C:\\Program Files\\WorkBuddy\\resources\\app.asar\\renderer\\index.html'
const syntheticEvidence = ({ visuals, targetFilePath = workbuddyTargetPath, userAgent = 'WorkBuddy/5.7.0 Electron/37.10.3', actions = [], authority = actions.length ? 'controlled-replay' : 'external-service' }) => createEvidence(undefined, {
id: 'rea-cdp-electron',
name: 'Synthetic WorkBuddy REA fixture provider',
version: 'self-test',
}, {
predicateType: 'rea.electron-page-inspection',
operation: 'inspect_electron_page',
parameters: actions.length ? { visual_probe: { actions } } : {},
result: { browser: { user_agent: userAgent }, target: { file_path: targetFilePath }, visuals },
confidence: 'observed',
authority,
limitations: ['Synthetic self-test fixture only; not captured from WorkBuddy.'],
})
const authenticatedEvidence = syntheticEvidence({
visuals: { artifact: baselineArtifact, probe: { frames: [{ step_id: 'probe-step', at_ms: 0, artifact: probeArtifact }] } },
actions: [{ kind: 'click', step_id: 'synthetic-probe-step' }],
})
const archivePath = path.join(fixture, 'synthetic-evidence.json')
await fs.writeFile(archivePath, JSON.stringify(authenticatedEvidence), 'utf8')
const evidenced = await compareManifest({
source: { kind: 'rea-workbuddy' },
requiredStates: ['baseline', 'probe'],
pairs: [
{ name: 'baseline', reference: files.reference, candidate: files.reference, referenceEvidence: { id: authenticatedEvidence.evidence_id, archive: archivePath } },
{ name: 'probe', reference: files.changed, candidate: files.changed, referenceEvidence: { id: authenticatedEvidence.evidence_id, archive: archivePath } },
],
}, fixture, path.join(fixture, 'evidenced-diffs'))
assert.equal(evidenced.status, 'identical')
assert.equal(evidenced.originalParity.status, 'exact_for_declared_states')
assert.equal(evidenced.originalParity.exactOriginalParity, true)
assert.equal(evidenced.originalParity.versions[0].version, '5.7.0')
assert.equal(evidenced.pairs[0].reference_evidence.authority, 'controlled-replay')
assert.equal(evidenced.scope.assessment, 'declared_state_set_only')
assert.equal(evidenced.scope.full_1to1, 'not_assessed')
assert.deepEqual(evidenced.pairs[0].reference_evidence.artifact_locations, ['visuals.artifact'])
assert.deepEqual(evidenced.pairs[1].reference_evidence.artifact_locations, ['visuals.probe.frames[0].artifact'])
const localFixtureEvidence = syntheticEvidence({
visuals: { artifact: baselineArtifact },
targetFilePath: path.join(fixture, 'renderer', 'index.html'),
})
await fs.writeFile(archivePath, JSON.stringify(localFixtureEvidence), 'utf8')
const localMasqueradeManifest = {
source: { kind: 'rea-workbuddy' },
requiredStates: ['local-fixture'],
pairs: [{ name: 'local-fixture', reference: files.reference, candidate: files.reference, referenceEvidence: { id: localFixtureEvidence.evidence_id, archive: archivePath } }],
}
await assert.rejects(compareManifest(localMasqueradeManifest, fixture, path.join(fixture, 'local-masquerade-diffs')), /target is not the WorkBuddy renderer/)
const falseBindingEvidence = syntheticEvidence({ visuals: { artifact: probeArtifact } })
const workbuddyManifest = {
source: { kind: 'rea-workbuddy' },
requiredStates: ['forged'],
pairs: [{ name: 'forged', reference: files.reference, candidate: files.reference, referenceEvidence: { id: falseBindingEvidence.evidence_id, archive: archivePath } }],
}
const forgedEvidenceId = `ev_${'0'.repeat(64)}`
await fs.writeFile(archivePath, JSON.stringify({ ...falseBindingEvidence, evidence_id: forgedEvidenceId }), 'utf8')
const forgedIdManifest = {
...workbuddyManifest,
pairs: [{ ...workbuddyManifest.pairs[0], referenceEvidence: { id: forgedEvidenceId, archive: archivePath } }],
}
await assert.rejects(compareManifest(forgedIdManifest, fixture, path.join(fixture, 'forged-id-diffs')), /authentication|identifier|digest|Evidence/i)
await fs.writeFile(archivePath, JSON.stringify(falseBindingEvidence), 'utf8')
await assert.rejects(compareManifest(workbuddyManifest, fixture, path.join(fixture, 'unclaimed-reference-diffs')), /SHA is absent/)
const unversionedEvidence = syntheticEvidence({ visuals: { artifact: baselineArtifact }, userAgent: 'Chromium/123.0' })
await fs.writeFile(archivePath, JSON.stringify(unversionedEvidence), 'utf8')
const unversioned = await compareManifest({
source: { kind: 'rea-workbuddy' },
requiredStates: ['unversioned'],
pairs: [{ name: 'unversioned', reference: files.reference, candidate: files.reference, referenceEvidence: { id: unversionedEvidence.evidence_id, archive: archivePath } }],
}, fixture, path.join(fixture, 'unversioned-diffs'))
assert.equal(unversioned.status, 'incomplete')
assert.equal(unversioned.originalParity.status, 'incomplete')
assert.equal(unversioned.originalParity.exactOriginalParity, false)
assert.equal(unversioned.originalParity.versions[0].version, 'unobserved')
await fs.writeFile(archivePath, JSON.stringify(authenticatedEvidence), 'utf8')
const approximate = await compareManifest({
source: { kind: 'rea-workbuddy' },
requiredStates: ['tolerant'],
channelTolerance: 1,
pairs: [{ name: 'tolerant', reference: files.reference, candidate: files.reference, referenceEvidence: { id: authenticatedEvidence.evidence_id, archive: path.join(fixture, 'synthetic-evidence.json') } }],
}, fixture, path.join(fixture, 'tolerant-diffs'))
assert.equal(approximate.status, 'approximate')
assert.equal(approximate.originalParity.status, 'not_exact')
assert.equal(approximate.originalParity.exactOriginalParity, false)
const mismatchedAuthority = syntheticEvidence({
visuals: { artifact: baselineArtifact },
actions: [{ kind: 'click', step_id: 'synthetic-action' }],
authority: 'external-service',
})
await fs.writeFile(archivePath, JSON.stringify(mismatchedAuthority), 'utf8')
await assert.rejects(compareManifest({
source: { kind: 'rea-workbuddy' },
requiredStates: ['authority'],
pairs: [{ name: 'authority', reference: files.reference, candidate: files.reference, referenceEvidence: { id: mismatchedAuthority.evidence_id, archive: archivePath } }],
}, fixture, path.join(fixture, 'authority-diffs')), /authority does not match visual_probe.actions/)
console.log('Visual pixel self-test passed: pixel comparison, incomplete required states, authenticated synthetic baseline/probe binding, local-target masquerade rejection, observed-version and authority checks, nonzero-tolerance exactness rejection, and forged Evidence rejection')
} finally {
await fs.rm(fixture, { recursive: true, force: true })
}
}
async function main() {
const args = process.argv.slice(2)
if (args.length === 1 && args[0] === '--self-test') {
await runSelfTest()
return
}
const manifestPath = args[0]
if (!manifestPath || manifestPath.startsWith('--')) throw new Error('Usage: node scripts/rea/compare-visual-pixels.mjs <manifest.json> [--output <directory>] | --self-test')
let outputDirectory
for (let index = 1; index < args.length; index += 1) {
if (args[index] !== '--output' || !args[index + 1]) throw new Error('Expected --output followed by a directory')
outputDirectory = path.resolve(args[++index])
}
const absoluteManifest = path.resolve(manifestPath)
const raw = JSON.parse(await fs.readFile(absoluteManifest, 'utf8'))
const parsed = parseManifest(raw)
const report = await compareManifest(parsed, path.dirname(absoluteManifest), outputDirectory ?? path.join(path.dirname(absoluteManifest), 'visual-pixel-diffs'))
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`)
if (report.status !== 'identical') process.exitCode = 1
}
main().catch(error => {
process.stderr.write(`${error.stack || error.message || error}\n`)
process.exitCode = 1
})
@@ -0,0 +1,27 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import assert from 'node:assert/strict'
import crypto from 'node:crypto'
import {spawnSync} from 'node:child_process'
const root=await fs.mkdtemp(path.resolve('.tmp/rea-cpp-binary-fixture-')),target=path.join(root,'target'),checks=[]
await fs.mkdir(path.join(target,'avatars'),{recursive:true})
const png=Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a6hoAAAAASUVORK5CYII=','base64'),hash=b=>crypto.createHash('sha256').update(b).digest('hex')
await fs.writeFile(path.join(target,'avatars/expert.png'),png)
function cli(command,args){return spawnSync(process.execPath,['.tmp/rea-css-runtime/scripts/rea.mjs',command,...args],{encoding:'utf8',maxBuffer:32*1024*1024})}
function inspect(){const r=cli('inspect-selected-directory-text-root',[target]);assert.equal(r.status,0,r.stderr);return JSON.parse(r.stdout).normalized_result}
function run(pin,selection){return cli('export-selected-directory-binary',[target,pin,JSON.stringify(selection)])}
function reject(label,r,pattern){assert.notEqual(r.status,0,label);assert.match(r.stderr,pattern,label);checks.push({label,pass:true})}
let inventory=inspect(),pin=inventory.root_sha256,selected=[{path:'avatars/expert.png',sha256:hash(png)}],r=run(pin,selected)
assert.equal(r.status,0,r.stderr);const evidence=JSON.parse(r.stdout),f=evidence.normalized_result.files[0]
assert.equal(evidence.operation,'export_selected_directory_binary');assert.equal(f.media_type,'image/png');assert.equal(f.encoding,'base64');assert.equal(f.sha256,hash(png));assert.deepEqual(Buffer.from(f.data_base64,'base64'),png);assert.equal(f.size_bytes,png.length);assert.equal('text' in f,false);checks.push({label:'actual own PNG public CLI export is exact bytes/SHA/type and base64 Evidence',pass:true})
reject('wrong root SHA',run('0'.repeat(64),selected),/root SHA mismatch/)
reject('wrong selected PNG SHA',run(pin,[{...selected[0],sha256:'0'.repeat(64)}]),/selected PNG SHA mismatch/)
reject('path traversal',run(pin,[{path:'../expert.png',sha256:hash(png)}]),/canonical/)
reject('unsupported binary extension',run(pin,[{path:'avatars/expert.jpg',sha256:hash(png)}]),/canonical/)
reject('missing selected member',run(pin,[{path:'avatars/missing.png',sha256:hash(png)}]),/absent/)
reject('duplicate paths',run(pin,[...selected,...selected]),/Duplicate/)
reject('required explicit image SHA',run(pin,[{path:'avatars/expert.png'}]),/SHA required/)
await fs.writeFile(path.join(target,'avatars/fake.png'),'not an image');inventory=inspect();reject('PNG signature validated',run(inventory.root_sha256,[{path:'avatars/fake.png',sha256:hash('not an image')}]),/signature/)
const large=Buffer.alloc(8*1024*1024+1,97);png.subarray(0,8).copy(large);await fs.writeFile(path.join(target,'avatars/large.png'),large);inventory=inspect();reject('binary per-file budget',run(inventory.root_sha256,[{path:'avatars/large.png',sha256:hash(large)}]),/byte budget/)
await fs.rm(path.join(target,'avatars/large.png'));const exact=large.subarray(0,8*1024*1024);for(const name of['one','two'])await fs.writeFile(path.join(target,'avatars/'+name+'.png'),exact);inventory=inspect();reject('binary aggregate budget',run(inventory.root_sha256,[{path:'avatars/one.png',sha256:hash(exact)},{path:'avatars/two.png',sha256:hash(exact)},...selected]),/byte budget/)
await fs.writeFile(path.join(root,'report.json'),JSON.stringify({result:'PASS',checks,evidenceId:evidence.evidence_id},null,2));await fs.writeFile(path.join(root,'evidence.json'),JSON.stringify(evidence,null,2));console.log(JSON.stringify({result:'PASS',root,checks:checks.length,evidenceId:evidence.evidence_id}))
@@ -0,0 +1,73 @@
/** Formal local REA directory adapter. Uses REA inventory and stable non-following reader. */
import path from 'node:path'
import {lstat,realpath} from 'node:fs/promises'
import {createHash} from 'node:crypto'
import {DirectoryArtifactReader} from './DirectoryArtifactReader.js'
import {ArtifactResourceScope} from './ArtifactResourceScope.js'
import {scanCanonicalArtifactInventory} from './inventory/scanCanonical.js'
import {createEvidence} from '../domain/evidence.js'
const FILE=8*1024*1024,TOTAL=16*1024*1024,SCAN=64*1024*1024,ENTRIES=4096
const sha=s=>createHash('sha256').update(s).digest('hex')
const textPath=p=>/\.(js|cjs|mjs|ts|css|json|md|txt|yaml|yml|py|html|cs|csproj|slnx|xml|xsd|sh|ps1|LICENSE)$/i.test(p)||['LICENSE','.gitignore'].includes(path.posix.basename(p))
function selection(paths,binary=false){if(!Array.isArray(paths)||!paths.length||paths.length>16)throw Error('Select 1–16 exact paths');const seen=new Set;for(const p of paths){if(typeof p!=='string'||!p||p.includes('\\')||p.includes('\0')||path.posix.normalize(p)!==p||p.startsWith('/')||/^[a-z]:/i.test(p)||p.split('/').some(x=>!x||x==='.'||x==='..')||!(binary?/\.(png|ttf)$/i.test(p):textPath(p)))throw Error('Invalid canonical supported artifact path');if(seen.has(p))throw Error('Duplicate selected path');seen.add(p)}return paths}
async function rootIdentity(input){if(typeof input!=='string'||!path.isAbsolute(input))throw Error('Directory root must be explicitly absolute');const resolved=path.resolve(input);let current=path.parse(resolved).root;for(const part of path.relative(current,resolved).split(path.sep)){current=path.join(current,part);if((await lstat(current)).isSymbolicLink())throw Error('Directory root cannot contain symlink ancestors')}const canonical=await realpath(resolved),meta=await lstat(canonical,{bigint:true});if(!meta.isDirectory()||meta.isSymbolicLink())throw Error('Root is not an ordinary directory');return{path:canonical,dev:String(meta.dev),ino:String(meta.ino)}}
class BoundedReader extends DirectoryArtifactReader{async *entries(signal,filter){let bytes=0,count=0;for await(const entry of super.entries(signal,filter)){if(++count>ENTRIES)throw Error('Directory inventory entry budget exceeded');if(entry.kind==='file'){bytes+=entry.declaredSize;if(!Number.isSafeInteger(entry.declaredSize)||entry.declaredSize<0||bytes>SCAN)throw Error('Directory inventory byte budget exceeded')}yield entry}}}
async function inventory(input){const scope=new ArtifactResourceScope;try{return await scanCanonicalArtifactInventory(input,{resourceScope:scope,integrity:{mode:'fail'}},(root,format)=>{if(format!=='directory')throw Error('Directory format required');return new BoundedReader(root)})}finally{await scope.close()}}
function members(snapshot){const nodes=new Map(snapshot.nodes.map(n=>[n.artifact_id,n]));return snapshot.occurrences.filter(o=>o.logical_path!=='.').map(o=>({path:o.logical_path,kind:o.entry_kind,size_bytes:o.size_bytes??o.declared_size??null,sha256:nodes.get(o.artifact_id)?.sha256??null})).sort((a,b)=>a.path.localeCompare(b.path))}
export async function inspectSelectedDirectory(input){const identity=await rootIdentity(input),snapshot=await inventory(identity.path);return createEvidence({path:identity.path,sha256:snapshot.manifest.root_sha256,format:'directory'},{id:'rea-selected-directory-text',name:'REA bounded directory text adapter',version:'1+local'}, {predicateType:'rea.artifact.directory-inventory',operation:'inspect_selected_directory_text_root',parameters:{max_inventory_bytes:SCAN,max_inventory_entries:ENTRIES},result:{root_sha256:snapshot.manifest.root_sha256,root_identity:identity,inventory_graph_sha256:snapshot.manifest.graph_sha256,coverage:snapshot.coverage,members:members(snapshot)},confidence:'observed',authority:'shipped-artifact',limitations:['Directory content root digest uses the official REA canonical inventory. All inventoried member streams are hashed within independent inventory budgets; source text is not exported by this operation. Symlink targets are never followed.'],locations:[{kind:'artifact-path',path:identity.path}]})}
export async function exportSelectedDirectoryText(input,expectedRoot,paths){return exportSelectedDirectoryArtifact(input,expectedRoot,paths,false,{})}
export async function exportSelectedDirectoryBinary(input,expectedRoot,selected){
if(!Array.isArray(selected))throw Error('Select exact PNG/TTF paths and SHA values')
const hashes=Object.create(null)
for(const item of selected){if(!item||typeof item.path!=='string'||!/^[a-f0-9]{64}$/.test(item.sha256))throw Error('Expected selected binary SHA required');hashes[item.path]=item.sha256}
return exportSelectedDirectoryArtifact(input,expectedRoot,selected.map(item=>item.path),true,hashes)
}
async function exportSelectedDirectoryArtifact(input,expectedRoot,paths,binary,expectedHashes){
selection(paths,binary)
if(!/^[a-f0-9]{64}$/.test(expectedRoot))throw Error('Expected directory SHA must be64 lowercase hex')
const identity=await rootIdentity(input),snapshot=await inventory(identity.path)
if(snapshot.manifest.root_sha256!==expectedRoot)throw Error('Directory root SHA mismatch before selected export')
const listed=members(snapshot),reader=new BoundedReader(identity.path),entries=new Map,wanted=new Set(paths)
let total=0
try{
for await(const e of reader.entries(undefined,{descendInto:p=>paths.some(s=>s.startsWith(p+'/'))})){if(wanted.has(e.path)){if(entries.has(e.path))throw Error('Ambiguous selected member');entries.set(e.path,e)}}
const files=[]
for(const p of paths){
const entry=entries.get(p),known=listed.filter(e=>e.path===p)
if(!entry||known.length!==1||entry.kind!=='file')throw Error('Selected ordinary member absent: '+p)
if(binary&&known[0].sha256!==expectedHashes[p])throw Error('Expected selected PNG SHA mismatch')
if(entry.declaredSize>FILE||total+entry.declaredSize>TOTAL)throw Error('Selected text byte budget exceeded')
const chunks=[];let size=0
for await(const b of await reader.open(entry)){size+=b.length;if(size>FILE||size>entry.declaredSize||total+size>TOTAL)throw Error('Selected text byte budget exceeded');chunks.push(b)}
const bytes=Buffer.concat(chunks),digest=sha(bytes)
if(size!==entry.declaredSize||digest!==known[0].sha256||(binary&&digest!==expectedHashes[p]))throw Error('Selected member changed or SHA mismatch')
if(binary){
let mediaType
if(/\.png$/i.test(p)){
if(!bytes.subarray(0,8).equals(Buffer.from([137,80,78,71,13,10,26,10])))throw Error('Selected PNG signature mismatch')
mediaType='image/png'
}else{
if(bytes.length<12||![0x00010000,0x74727565].includes(bytes.readUInt32BE(0)))throw Error('Selected TTF signature mismatch')
const tables=bytes.readUInt16BE(4),directoryEnd=12+16*tables
if(!tables||tables>4096||directoryEnd>bytes.length)throw Error('Selected TTF table directory invalid')
for(let i=0;i<tables;i++){const start=12+16*i,offset=bytes.readUInt32BE(start+8),length=bytes.readUInt32BE(start+12);if(offset<directoryEnd||offset+length>bytes.length)throw Error('Selected TTF table range invalid')}
mediaType='font/ttf'
}
files.push({path:p,size_bytes:size,sha256:digest,encoding:'base64',media_type:mediaType,data_base64:bytes.toString('base64')})
}else{
let text;try{text=new TextDecoder('utf-8',{fatal:true,ignoreBOM:true}).decode(bytes)}catch{throw Error('Selected member is not strict UTF-8')}
files.push({path:p,size_bytes:size,sha256:digest,text})
}
total+=size
}
const after=await rootIdentity(input)
if(JSON.stringify(after)!==JSON.stringify(identity))throw Error('Directory physical root identity changed')
const verified=await inventory(identity.path)
if(verified.manifest.root_sha256!==expectedRoot)throw Error('Directory root SHA mismatch after selected export')
return createEvidence({path:identity.path,sha256:expectedRoot,format:'directory'},{id:binary?'rea-selected-directory-binary':'rea-selected-directory-text',name:'REA bounded directory artifact adapter',version:'1+binary-local'},
{predicateType:binary?'rea.artifact.selected-directory-binary':'rea.artifact.selected-directory-text',operation:binary?'export_selected_directory_binary':'export_selected_directory_text',
parameters:{expected_root_sha256:expectedRoot,selected_paths:paths,...(binary?{expected_file_sha256:expectedHashes}:{}),max_file_bytes:FILE,max_total_bytes:TOTAL,max_inventory_bytes_per_pass:SCAN,max_inventory_entries:ENTRIES},
result:{root_sha256:expectedRoot,root_identity:identity,inventory_graph_sha256:snapshot.manifest.graph_sha256,scope:{kind:'selected-files-only-export',selected_paths:paths,root_inventory:'Full bounded root inventory hashed before/after selected export; unselected content not exported.'},files},
confidence:'observed',authority:'shipped-artifact',limitations:['Only selected artifacts are returned. Full directory inventory bytes are hashed to validate the same REA root content digest before and after export.','No target code is executed, no HTTP resource is requested, no symlink target is followed. Mutable directories can fail pin validation; do not silently replace the expected pin.'],locations:paths.map(p=>({kind:'artifact-path',path:p}))})
}finally{await reader.close()}
}
@@ -0,0 +1,30 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import assert from 'node:assert/strict'
import crypto from 'node:crypto'
import {spawnSync} from 'node:child_process'
const root=await fs.mkdtemp(path.resolve('.tmp/rea-cpp-directory-fixture-')),target=path.join(root,'target'),checks=[]
await fs.mkdir(path.join(target,'agents'),{recursive:true})
const files={'plugin.json':'{"name":"cpp-fixture","agents":["agents/cpp.md"]}\n','agents/cpp.md':'---\nname: cpp\n---\n# 测试专家\n','schema.yml':'name: cpp\n','tool.py':'# Never execute retained text\n'}
for(const[p,text]of Object.entries(files))await fs.writeFile(path.join(target,p),text)
function cli(command,args){return spawnSync(process.execPath,['.tmp/rea-css-runtime/scripts/rea.mjs',command,...args],{encoding:'utf8',maxBuffer:32*1024*1024})}
function inspect(dir=target){const r=cli('inspect-selected-directory-text-root',[dir]);assert.equal(r.status,0,r.stderr);return JSON.parse(r.stdout)}
function exportText(pin,paths,dir=target){return cli('export-selected-directory-text',[dir,pin,JSON.stringify(paths)])}
function reject(label,r,pattern){assert.notEqual(r.status,0,label);assert.match(r.stderr,pattern,label);checks.push({label,pass:true})}
let inventory=inspect(),pin=inventory.normalized_result.root_sha256
const analysis=cli('analyze-javascript-application',[target,'--source-path','plugin.json','--json']);assert.equal(analysis.status,0,analysis.stderr);assert.equal(JSON.parse(analysis.stdout).normalized_result.root_artifact_sha256,pin);checks.push({label:'new provider pin equals public REA analyzer canonical directory SHA',pass:true})
let r=exportText(pin,Object.keys(files));assert.equal(r.status,0,r.stderr);const evidence=JSON.parse(r.stdout)
assert.deepEqual(evidence.normalized_result.files.map(f=>f.path),Object.keys(files));for(const f of evidence.normalized_result.files){assert.equal(f.text,files[f.path]);assert.equal(f.sha256,crypto.createHash('sha256').update(files[f.path]).digest('hex'))}checks.push({label:'exact selected bytes/SHA and UTF-8 source text exported through public CLI',pass:true})
reject('incorrect root pin',exportText('0'.repeat(64),['plugin.json']),/root SHA mismatch/)
reject('traversal selection',exportText(pin,['../plugin.json']),/canonical/)
reject('unsupported selection',exportText(pin,['private.db']),/canonical/)
reject('duplicate selection',exportText(pin,['plugin.json','plugin.json']),/Duplicate/)
reject('absent member',exportText(pin,['agents/absent.md']),/absent/)
await fs.writeFile(path.join(target,'bad.md'),Buffer.from([0xc3,0x28]));const bad=inspect().normalized_result.root_sha256;reject('strict UTF-8 rejects invalid selected bytes',exportText(bad,['bad.md']),/strict UTF-8/)
reject('mutable content invalidates old root pin',exportText(pin,['plugin.json']),/root SHA mismatch/)
await fs.writeFile(path.join(target,'large.md'),Buffer.alloc(8*1024*1024+1,97));const large=inspect().normalized_result.root_sha256;reject('per-file text budget',exportText(large,['large.md']),/text byte budget/)
await fs.rm(path.join(target,'large.md'));for(const p of['one.md','two.md'])await fs.writeFile(path.join(target,p),Buffer.alloc(8*1024*1024,97));await fs.writeFile(path.join(target,'extra.txt'),'x');const aggregate=inspect().normalized_result.root_sha256;reject('aggregate text budget',exportText(aggregate,['one.md','two.md','extra.txt']),/text byte budget/)
const outside=path.join(root,'outside');await fs.mkdir(outside);await fs.writeFile(path.join(outside,'secret.md'),'outside-source-not-exported')
await fs.symlink(outside,path.join(root,'root-link'),'junction');reject('symlink root rejected',cli('inspect-selected-directory-text-root',[path.join(root,'root-link')]),/symlink ancestors/)
await fs.symlink(outside,path.join(target,'link'),'junction');const linked=inspect().normalized_result.root_sha256;reject('symlink descendant never followed',exportText(linked,['link/secret.md']),/absent/)
await fs.writeFile(path.join(root,'report.json'),JSON.stringify({result:'PASS',checks,evidenceId:evidence.evidence_id,pin},null,2));await fs.writeFile(path.join(root,'export-evidence.json'),JSON.stringify(evidence,null,2));console.log(JSON.stringify({result:'PASS',root,checks:checks.length,evidenceId:evidence.evidence_id}))
+18
View File
@@ -0,0 +1,18 @@
/** Exact avatar recovery from formal public REA binary export; no original FS access here. */
import fs from 'node:fs'
import path from 'node:path'
import crypto from 'node:crypto'
import {spawnSync} from 'node:child_process'
const analysis=JSON.parse(fs.readFileSync('.tmp/rea-cpp-expert-package-analysis-20261016.json','utf8'))
const inventory=JSON.parse(fs.readFileSync('.tmp/rea-cpp-inventory-ev_d8bab73f2bffe8f99a6af2e4d50f9a820ec3ec8a4ea8cff3a4ef4bc9063a4a83.json','utf8')).normalized_result
const member=inventory.members.find(m=>m.path==='avatars/expert.png'),pin='b90023e9622bd74c55d583051bee3625eb491b552baeea17ae10ac792a352498'
if(inventory.root_sha256!==pin||analysis.normalized_result.root_artifact_sha256!==pin||member?.size_bytes!==420773)throw Error('Pinned inventory mismatch')
const r=spawnSync(process.execPath,['.tmp/rea-css-runtime/scripts/rea.mjs','export-selected-directory-binary',analysis.normalized_result.input_path,pin,JSON.stringify([{path:member.path,sha256:member.sha256}])],{encoding:'utf8',maxBuffer:8*1024*1024})
if(r.status!==0)throw Error(r.stderr||r.stdout)
const evidence=JSON.parse(r.stdout),file=evidence.normalized_result.files[0],bytes=Buffer.from(file.data_base64,'base64'),hash=b=>crypto.createHash('sha256').update(b).digest('hex')
if(evidence.normalized_result.root_sha256!==pin||file.sha256!==member.sha256||hash(bytes)!==member.sha256||bytes.length!==420773)throw Error('REA binary SHA/size mismatch')
const exportPath=`.tmp/rea-cpp-binary-${evidence.evidence_id}.json`;fs.writeFileSync(exportPath,JSON.stringify(evidence))
const recovered='.tmp/rea-cpp-expert-recovered-20261016',output=path.join(recovered,member.path);fs.mkdirSync(path.dirname(output),{recursive:true});fs.writeFileSync(output,bytes)
if(hash(fs.readFileSync(output))!==member.sha256)throw Error('Recovered binary SHA mismatch')
const manifestPath=path.join(recovered,'evidence-manifest.json'),manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));manifest.files=manifest.files.filter(f=>f.path!==member.path);manifest.files.push({path:member.path,bytes:bytes.length,sha256:file.sha256,evidenceId:evidence.evidence_id,encoding:'base64',mediaType:'image/png',retainedExport:exportPath});fs.writeFileSync(manifestPath,JSON.stringify(manifest,null,2)+'\n')
console.log(JSON.stringify({output,evidenceId:evidence.evidence_id,sha256:file.sha256,bytes:bytes.length,exportPath}))
+13
View File
@@ -0,0 +1,13 @@
/** Original access goes exclusively through formal public REA directory CLI. */
import fs from 'node:fs'
import {spawnSync} from 'node:child_process'
import crypto from 'node:crypto'
const saved=JSON.parse(fs.readFileSync(process.argv[2],'utf8')),analysis=saved.normalized_result?saved:saved.details.partial_observation
const inventory=process.argv[3]==='--inventory'
const r=spawnSync(process.execPath,['.tmp/rea-css-runtime/scripts/rea.mjs',...(inventory?['inspect-selected-directory-text-root',analysis.normalized_result.input_path]:['export-selected-directory-text',analysis.normalized_result.input_path,analysis.normalized_result.root_artifact_sha256,JSON.stringify(process.argv.slice(3))])],{encoding:'utf8',maxBuffer:32*1024*1024})
if(r.status!==0)throw Error(r.stderr||r.stdout)
const evidence=JSON.parse(r.stdout)
if(evidence.normalized_result.root_sha256!==analysis.normalized_result.root_artifact_sha256)throw Error('Inventory root pin mismatch')
for(const f of evidence.normalized_result.files??[])if(crypto.createHash('sha256').update(f.text).digest('hex')!==f.sha256)throw Error('Export SHA mismatch')
const output=`.tmp/rea-cpp-${inventory?'inventory':'text'}-${evidence.evidence_id}.json`;fs.writeFileSync(output,JSON.stringify(evidence))
console.log(JSON.stringify({output,evidenceId:evidence.evidence_id,root:evidence.normalized_result.root_sha256,files:inventory?evidence.normalized_result.members:evidence.normalized_result.files.map(({text,...m})=>m)},null,2))
+7
View File
@@ -0,0 +1,7 @@
/** Review copies only from retained REA text, never from original filesystem. */
import fs from 'node:fs'
import path from 'node:path'
import crypto from 'node:crypto'
const root='.tmp/rea-cpp-expert-recovered-20261016',pin='b90023e9622bd74c55d583051bee3625eb491b552baeea17ae10ac792a352498',manifest={rootSha256:pin,files:[]}
for(const input of process.argv.slice(2)){const evidence=JSON.parse(fs.readFileSync(input,'utf8'));if(evidence.normalized_result.root_sha256!==pin)throw Error('Root mismatch');for(const f of evidence.normalized_result.files){const sha=crypto.createHash('sha256').update(f.text).digest('hex');if(sha!==f.sha256||Buffer.byteLength(f.text)!==f.size_bytes)throw Error('Text byte mismatch');if(f.path.split('/').some(p=>!p||p==='.'||p==='..')||f.path.includes('\\'))throw Error('Canonical path required');const output=path.join(root,...f.path.split('/'));fs.mkdirSync(path.dirname(output),{recursive:true});fs.writeFileSync(output,f.text);if(crypto.createHash('sha256').update(fs.readFileSync(output)).digest('hex')!==sha)throw Error('Recovered file SHA mismatch');manifest.files.push({path:f.path,bytes:f.size_bytes,sha256:f.sha256,evidenceId:evidence.evidence_id,retainedExport:input})}}
fs.writeFileSync(path.join(root,'evidence-manifest.json'),JSON.stringify(manifest,null,2)+'\n');console.log(JSON.stringify({root,files:manifest.files.length,bytes:manifest.files.reduce((n,f)=>n+f.bytes,0)}))
@@ -0,0 +1,35 @@
/** Install formal provider/CLI in isolated REA runtime only; never reads original target. */
import fs from 'node:fs/promises'
import path from 'node:path'
import crypto from 'node:crypto'
const root=path.resolve('.tmp/rea-css-runtime'),hash=s=>crypto.createHash('sha256').update(s).digest('hex')
const source=await fs.readFile('scripts/rea/cpp-expert-directory-provider.mjs','utf8')
await fs.writeFile(path.join(root,'dist/artifacts/SelectedDirectoryText.js'),source)
let cli=await fs.readFile(path.join(root,'scripts/rea.mjs'),'utf8')
if(!cli.includes('inspect-selected-directory-text-root')){
const anchor='} else if (args[0] === "export-selected-artifact-text") {'
if(cli.split(anchor).length!==2)throw Error('REA CLI anchor mismatch')
cli=cli.replace(anchor,`} else if (args[0] === "inspect-selected-directory-text-root") {
const { inspectSelectedDirectory } = await import("../dist/artifacts/SelectedDirectoryText.js");
process.stdout.write(JSON.stringify(await inspectSelectedDirectory(args[1])) + "\\n");
} else if (args[0] === "export-selected-directory-text") {
const { exportSelectedDirectoryText } = await import("../dist/artifacts/SelectedDirectoryText.js");
process.stdout.write(JSON.stringify(await exportSelectedDirectoryText(args[1],args[2],JSON.parse(args[3]))) + "\\n");
${anchor}`)
await fs.writeFile(path.join(root,'scripts/rea.mjs'),cli)
}
if(!cli.includes('export-selected-directory-binary')){
const anchor='} else if (args[0] === "export-selected-artifact-text") {'
if(cli.split(anchor).length!==2)throw Error('REA CLI binary anchor mismatch')
cli=cli.replace(anchor,`} else if (args[0] === "export-selected-directory-binary") {
const { exportSelectedDirectoryBinary } = await import("../dist/artifacts/SelectedDirectoryText.js");
process.stdout.write(JSON.stringify(await exportSelectedDirectoryBinary(args[1],args[2],JSON.parse(args[3]))) + "\\n");
${anchor}`)
await fs.writeFile(path.join(root,'scripts/rea.mjs'),cli)
}
const manifestPath=path.join(root,'css-repair-manifest.json'),manifest=JSON.parse(await fs.readFile(manifestPath,'utf8'))
manifest.files.find(f=>f.path==='scripts/rea.mjs').modified_sha256=hash(cli)
manifest.files=manifest.files.filter(f=>f.path!=='dist/artifacts/SelectedDirectoryText.js')
manifest.files.push({path:'dist/artifacts/SelectedDirectoryText.js',original_sha256:null,modified_sha256:hash(source)})
await fs.writeFile(manifestPath,JSON.stringify(manifest,null,2)+'\n')
console.log(JSON.stringify({root,providerSha256:hash(source),cliSha256:hash(cli),files:manifest.files.length}))
+183
View File
@@ -0,0 +1,183 @@
import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import { mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
const projectRoot = resolve(fileURLToPath(new URL('../..', import.meta.url)))
const runtimeModule = join(projectRoot, '.tmp/rea-css-runtime/dist/browser/CdpElectronStyles.js')
const { captureElectronStyles, ingestElectronStyleEvent } = await import(pathToFileURL(runtimeModule))
const authorizedFrameIds = new Set(['frame-main'])
const noCompletenessWrites = { exclude() { assert.fail('CSS capture must not write into the old completeness sections') } }
const limitations = []
const styles = new Map()
const addStyle = (styleSheetId, header) => ingestElectronStyleEvent({ method: 'CSS.styleSheetAdded', params: { header: { styleSheetId, ...header } } }, styles)
const removeStyle = styleSheetId => ingestElectronStyleEvent({ method: 'CSS.styleSheetRemoved', params: { styleSheetId } }, styles)
const fixtureRoot = await mkdtemp(join(tmpdir(), 'rea-css-fixtures-'))
const cssFile = join(fixtureRoot, 'authorized.css')
await writeFile(cssFile, 'body { color: red; }\n')
const cssUrl = pathToFileURL(cssFile).href
addStyle('file-style', { frameId: 'frame-main', sourceURL: cssUrl, origin: 'regular' })
addStyle('inline-style', { frameId: 'frame-main', sourceURL: '', origin: 'regular', isInline: true })
addStyle('constructed-style', { frameId: 'frame-main', sourceURL: '', origin: 'regular', isConstructed: true })
addStyle('remote-style', { frameId: 'frame-main', sourceURL: 'https://example.invalid/site.css', origin: 'regular' })
addStyle('other-frame-style', { frameId: 'frame-other', sourceURL: cssUrl, origin: 'regular' })
addStyle('user-agent-style', { frameId: 'frame-main', sourceURL: cssUrl, origin: 'user-agent' })
addStyle('injected-style', { frameId: 'frame-main', sourceURL: cssUrl, origin: 'injected' })
addStyle('inspector-style', { frameId: 'frame-main', sourceURL: cssUrl, origin: 'inspector' })
addStyle('unknown-origin-style', { frameId: 'frame-main', sourceURL: cssUrl, origin: 'unknown' })
addStyle('unattributed-style', { frameId: 'frame-main', sourceURL: '', origin: 'regular' })
addStyle('removed-style', { frameId: 'frame-main', sourceURL: cssUrl, origin: 'regular' })
removeStyle('removed-style')
assert.equal(styles.has('removed-style'), false)
removeStyle('missing-style')
const bodyTexts = new Map([
['file-style', 'body { color: red; }\n'],
['inline-style', '.inline { display: block; }'],
['constructed-style', '.constructed { display: grid; }']
])
const calls = []
const connection = {
async send(method, params, sessionId, signal) {
calls.push({ method, params, sessionId, signal })
if (method !== 'CSS.getStyleSheetText') throw new Error(`Unexpected command: ${method}`)
return { text: bodyTexts.get(params.styleSheetId) }
}
}
const included = await captureElectronStyles({
connection,
sessionId: 'private-session-1',
signal: undefined,
styles,
frameIds: authorizedFrameIds,
request: { include_style_sources: true },
completeness: noCompletenessWrites,
limitations
})
assert.equal(included.total, 10)
assert.equal(included.excluded, 7)
assert.equal(included.source_failures, 0)
assert.equal(included.truncated, false)
assert.equal(included.items.length, 3)
assert.equal(calls.length, 3)
assert.ok(calls.every(call => call.method === 'CSS.getStyleSheetText' && call.sessionId === 'private-session-1'))
assert.deepEqual(included.limits, { max_sheets: 1024, max_sheet_bytes: 8 * 1024 * 1024, max_total_bytes: 64 * 1024 * 1024 })
const fileItem = included.items.find(item => item.style_sheet_id === 'file-style')
assert.equal(fileItem.file_path, await realpath(cssFile))
assert.equal(fileItem.source.included, true)
assert.equal(fileItem.source.artifact.media_type, 'text/css')
assert.equal(fileItem.source.artifact.text, bodyTexts.get('file-style'))
assert.equal(fileItem.source.artifact.bytes, Buffer.byteLength(bodyTexts.get('file-style'), 'utf8'))
assert.equal(fileItem.source.artifact.sha256, createHash('sha256').update(bodyTexts.get('file-style')).digest('hex'))
assert.match(fileItem.style_sheet_key, /^electron_stylesheet_[a-f0-9]{64}$/)
assert.equal(included.items.find(item => item.style_sheet_id === 'inline-style').file_path, null)
assert.equal(included.items.find(item => item.style_sheet_id === 'inline-style').is_inline, true)
assert.equal(included.items.find(item => item.style_sheet_id === 'constructed-style').is_constructed, true)
assert.doesNotMatch(JSON.stringify(included), /example\.invalid/)
const unselectedCalls = []
const unselected = await captureElectronStyles({
connection: { async send(...args) { unselectedCalls.push(args); throw new Error('CSS body must not be requested') } },
sessionId: 'private-session-1',
styles,
frameIds: authorizedFrameIds,
request: { include_style_sources: false },
completeness: noCompletenessWrites,
limitations: []
})
assert.equal(unselected.total, 10)
assert.equal(unselected.items.length, 0)
assert.equal(unselected.excluded, 7)
assert.equal(unselectedCalls.length, 0)
const failingStyles = new Map()
addStyleTo(failingStyles, 'throwing-style', { frameId: 'frame-main', sourceURL: cssUrl, origin: 'regular' })
addStyleTo(failingStyles, 'invalid-style', { frameId: 'frame-main', sourceURL: '', origin: 'regular', isInline: true })
addStyleTo(failingStyles, 'surviving-style', { frameId: 'frame-main', sourceURL: '', origin: 'regular', isConstructed: true })
const failures = await captureElectronStyles({
connection: { async send(_method, params) {
if (params.styleSheetId === 'throwing-style') throw new Error('stylesheet was removed')
if (params.styleSheetId === 'invalid-style') return { text: 3 }
return { text: '.still-captured{}' }
} },
sessionId: 'private-session-2',
styles: failingStyles,
frameIds: authorizedFrameIds,
request: { include_style_sources: true },
completeness: noCompletenessWrites,
limitations: []
})
assert.equal(failures.items.length, 3)
assert.equal(failures.source_failures, 2)
assert.equal(failures.truncated, false)
assert.equal(failures.items.find(item => item.style_sheet_id === 'throwing-style').source.reason, 'CSS.getStyleSheetText failed')
assert.equal(failures.items.find(item => item.style_sheet_id === 'invalid-style').source.reason, 'CSS.getStyleSheetText returned invalid text')
assert.equal(failures.items.find(item => item.style_sheet_id === 'surviving-style').source.included, true)
const oversizedStyles = new Map()
addStyleTo(oversizedStyles, 'oversized', { frameId: 'frame-main', sourceURL: '', origin: 'regular', isInline: true })
const oversized = await captureElectronStyles({
connection: { async send() { return { text: 'x'.repeat(8 * 1024 * 1024 + 1) } } },
sessionId: 'private-session-3',
styles: oversizedStyles,
frameIds: authorizedFrameIds,
request: { include_style_sources: true },
completeness: noCompletenessWrites,
limitations: []
})
assert.equal(oversized.source_failures, 1)
assert.equal(oversized.truncated, true)
assert.equal(oversized.items[0].source.included, false)
assert.match(oversized.items[0].source.reason, /per-stylesheet byte limit/)
const aggregateStyles = new Map()
const aggregateText = 'a'.repeat(8 * 1024 * 1024)
for (let index = 0; index < 8; index += 1) {
addStyleTo(aggregateStyles, `aggregate-${String(index).padStart(2, '0')}`, { frameId: 'frame-main', sourceURL: '', origin: 'regular', isInline: true })
}
addStyleTo(aggregateStyles, 'aggregate-overflow', { frameId: 'frame-main', sourceURL: '', origin: 'regular', isInline: true })
const aggregate = await captureElectronStyles({
connection: { async send(_method, params) { return { text: params.styleSheetId === 'aggregate-overflow' ? 'overflow' : aggregateText } } },
sessionId: 'private-session-4',
styles: aggregateStyles,
frameIds: authorizedFrameIds,
request: { include_style_sources: true },
completeness: noCompletenessWrites,
limitations: []
})
assert.equal(aggregate.source_failures, 1)
assert.equal(aggregate.truncated, true)
assert.equal(aggregate.items.filter(item => item.source.included).length, 8)
assert.equal(aggregate.items.find(item => item.style_sheet_id === 'aggregate-overflow').source.included, false)
assert.match(aggregate.items.find(item => item.style_sheet_id === 'aggregate-overflow').source.reason, /aggregate stylesheet source byte limit/)
const countLimitedStyles = new Map()
for (let index = 0; index < 1025; index += 1) {
addStyleTo(countLimitedStyles, `sheet-${String(index).padStart(4, '0')}`, { frameId: 'frame-main', sourceURL: '', origin: 'regular', isConstructed: true })
}
let countRequests = 0
const countLimited = await captureElectronStyles({
connection: { async send() { countRequests += 1; return { text: '' } } },
sessionId: 'private-session-5',
styles: countLimitedStyles,
frameIds: authorizedFrameIds,
request: { include_style_sources: true },
completeness: noCompletenessWrites,
limitations: []
})
assert.equal(countLimited.total, 1025)
assert.equal(countLimited.items.length, 1025)
assert.equal(countLimited.source_failures, 1)
assert.equal(countLimited.truncated, true)
assert.equal(countRequests, 1024)
assert.equal(countLimited.items.filter(item => item.source.included).length, 1024)
await rm(fixtureRoot, { recursive: true, force: true })
console.log('REA Electron CSS capture regression checks passed')
function addStyleTo(styleMap, styleSheetId, header) {
ingestElectronStyleEvent({ method: 'CSS.styleSheetAdded', params: { header: { styleSheetId, ...header } } }, styleMap)
}
+26
View File
@@ -0,0 +1,26 @@
// Test the public local REA MCP entry point without inspecting WorkBuddy.
import assert from 'node:assert/strict'
import path from 'node:path'
import { createRequire } from 'node:module'
import { pathToFileURL } from 'node:url'
const runtime = path.resolve('.tmp/rea-css-runtime')
const require = createRequire(path.join(runtime, 'package.json'))
const { Client } = await import(pathToFileURL(require.resolve('@modelcontextprotocol/client')).href)
const { StdioClientTransport } = await import(pathToFileURL(require.resolve('@modelcontextprotocol/client/stdio')).href)
const transport = new StdioClientTransport({ command: process.execPath, args: [path.join(runtime, 'scripts/rea.mjs'), 'mcp'], cwd: process.cwd(), stderr: 'pipe' })
const client = new Client({ name: 'ycode-local-rea-css-regression', version: '1' })
try {
await client.connect(transport)
assert.equal(client.getServerVersion()?.version, '6.5.0-visual.3')
const listing = await client.listTools()
const tool = listing.tools.find(tool => tool.name === 'inspect_electron_page')
assert.ok(tool, 'Electron inspection tool is advertised')
assert.ok(JSON.stringify(tool.inputSchema).includes('include_style_sources'), 'MCP input retains CSS option')
assert.ok(JSON.stringify(tool.inputSchema).includes('include_visuals'), 'MCP input retains viewport option')
assert.ok(JSON.stringify(tool.inputSchema).includes('visual_probe'), 'MCP input retains native visual probe option')
console.log('REA CSS public MCP passed: local version, tool listing, CSS input option')
} finally {
await client.close()
await transport.close()
}
@@ -0,0 +1,68 @@
import assert from 'node:assert/strict'
import fs from 'node:fs/promises'
import path from 'node:path'
import { tmpdir } from 'node:os'
import { pathToFileURL } from 'node:url'
const runtime = path.resolve('.tmp/rea-css-runtime')
const moduleAt = relative => import(pathToFileURL(path.join(runtime, 'dist', relative)).href)
const { inspectElectronPageInputSchema, electronPageInspectionSchema } = await moduleAt('domain/javascript/electronObservation.js')
const { inspectCdpElectronPage } = await moduleAt('browser/CdpElectronInspection.js')
const { createElectronEvidence } = await moduleAt('application/javascript/ElectronEvidence.js')
const fixture = await fs.mkdtemp(path.join(tmpdir(), 'rea-css-contract-'))
try {
const html = path.join(fixture, 'index.html'), css = path.join(fixture, 'app.css')
await fs.writeFile(html, '<!doctype html><title>REA fixture</title>')
await fs.writeFile(css, '.fixture { color: blue; }')
const pageUrl = pathToFileURL(html).href, cssUrl = pathToFileURL(css).href
const frame = { id: 'frame-main', url: pageUrl }
const input = inspectElectronPageInputSchema.parse({ cdp_endpoint: 'http://127.0.0.1:9223', target_id: 'test', observation_ms: 0, include_style_sources: true })
assert.equal(input.include_style_sources, true)
assert.equal(input.include_script_sources, false)
assert.equal(inspectElectronPageInputSchema.safeParse({ ...input, visual_probe: { include_fonts: true } }).success, false, 'a probe cannot be silently ignored without include_visuals')
assert.equal(inspectElectronPageInputSchema.parse({ cdp_endpoint: 'http://127.0.0.1:9223', target_id: 'test' }).include_style_sources, false)
assert.equal(inspectElectronPageInputSchema.safeParse({ ...input, cdp_endpoint: 'https://example.com' }).success, false)
let listener
const calls = []
const connection = {
onEvent(callback) { listener = callback; return () => { listener = undefined } },
async send(method, _params, sessionId) {
calls.push(method)
if (method === 'Page.getFrameTree') return { frameTree: { frame } }
if (method === 'Page.getResourceTree') return { frameTree: { frame, resources: [{ url: cssUrl, type: 'Stylesheet', mimeType: 'text/css' }] } }
if (method === 'DOMSnapshot.captureSnapshot') return { strings: [], documents: [] }
if (method === 'Target.getTargets') return { targetInfos: [] }
if (method === 'CSS.enable') {
listener({ sessionId, method: 'CSS.styleSheetAdded', params: { header: { styleSheetId: 'css-1', frameId: frame.id, sourceURL: cssUrl, origin: 'regular', isInline: false } } })
listener({ sessionId: 'another-session', method: 'CSS.styleSheetAdded', params: { header: { styleSheetId: 'foreign-session', frameId: frame.id, sourceURL: cssUrl, origin: 'regular' } } })
}
if (method === 'CSS.getStyleSheetText') return { text: '.fixture { color: blue; }' }
return {}
}
}
const context = {
connection, sessionId: 'session-main', input,
target: { id: 'test', type: 'page', title: 'REA fixture', attached: null },
discovery: { version: { product: 'Fixture/1', protocol_version: '1.3', revision: 'fixture', user_agent: 'fixture', js_version: 'fixture' } }
}
const result = electronPageInspectionSchema.parse(await inspectCdpElectronPage(context))
assert.equal(result.styles.total, 1, 'foreign session events cannot be attributed to this target')
assert.equal(result.styles.items[0].source.artifact.text, '.fixture { color: blue; }')
assert.equal(result.styles.items[0].file_path, await fs.realpath(css))
assert.equal(result.styles.source_failures, 0)
assert.equal(result.styles.truncated, false)
assert.ok(calls.indexOf('DOM.enable') < calls.indexOf('CSS.enable'))
assert.equal(listener, undefined, 'capture listener is removed')
const evidence = createElectronEvidence('inspect_electron_page', input, result, { id: 'rea-cdp-electron', name: 'REA Electron file-page CDP observation provider', version: '1+css-local.1' })
assert.equal(evidence.parameters.include_style_sources, true)
assert.equal(evidence.normalized_result.styles.items.length, 1)
assert.match(evidence.evidence_id, /^ev_[a-f0-9]{64}$/)
calls.length = 0
const optedOut = electronPageInspectionSchema.parse(await inspectCdpElectronPage({ ...context, input: { ...input, include_style_sources: false } }))
assert.equal(optedOut.styles.items.length, 0)
assert.equal(calls.some(method => method.startsWith('CSS.') || method === 'DOM.enable'), false)
assert.equal(calls.some(method => method === 'Runtime.evaluate'), false)
console.log('REA CSS runtime contracts passed: opt-in, session attribution, schema retention, Evidence, and existing opt-out behavior')
} finally {
await fs.rm(fixture, { recursive: true, force: true })
}
+17
View File
@@ -0,0 +1,17 @@
import assert from 'node:assert/strict'
import fs from 'node:fs/promises'
import path from 'node:path'
import crypto from 'node:crypto'
const runtime = path.resolve('.tmp/rea-css-runtime')
const manifest = JSON.parse(await fs.readFile(path.join(runtime, 'css-repair-manifest.json'), 'utf8'))
const sha = bytes => crypto.createHash('sha256').update(bytes).digest('hex')
for (const file of manifest.files) {
assert.equal(sha(await fs.readFile(path.join(runtime, file.path))), file.modified_sha256, `Local repaired module changed: ${file.path}`)
if (file.original_sha256 !== null) {
assert.equal(sha(await fs.readFile(path.join(manifest.sourceRoot, file.path))), file.original_sha256, `Global REA module changed: ${file.path}`)
assert.equal(sha(await fs.readFile(path.join(runtime, 'css-repair-baseline', file.path))), file.original_sha256)
}
}
assert.equal(await fs.realpath(path.join(runtime, 'node_modules')), await fs.realpath(path.join(manifest.sourceRoot, 'node_modules')))
console.log(`REA local repair integrity passed: ${manifest.files.length} module digests; corresponding global files unchanged`)
@@ -0,0 +1,26 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import { spawnSync } from 'node:child_process'
const output = await fs.mkdtemp(path.resolve('.tmp/rea-extra-resources-')), root = path.join(output, 'fixture'), checks = []
await fs.mkdir(root)
const hash = b => createHash('sha256').update(b).digest('hex')
const names = ['Tool.cs', 'Tool.csproj', 'Tool.slnx', 'schema.xml', 'schema.xsd', 'setup.sh', 'setup.ps1', 'viewer.html', 'font.LICENSE', 'LICENSE', '.gitignore']
for (const name of names) await fs.writeFile(path.join(root, name), 'OWN_FIXTURE_UTF8_中文_' + name)
const font = Buffer.alloc(32); font.writeUInt32BE(0x00010000); font.writeUInt16BE(1, 4); font.write('test', 12); font.writeUInt32BE(28, 20); font.writeUInt32BE(4, 24)
await fs.writeFile(path.join(root, 'valid.ttf'), font)
await fs.writeFile(path.join(root, 'invalid.ttf'), 'not an SFNT font')
const badRange = Buffer.from(font); badRange.writeUInt32BE(1000, 20); await fs.writeFile(path.join(root, 'range.ttf'), badRange)
const cli = (command, ...args) => spawnSync(process.execPath, ['.tmp/rea-css-runtime/scripts/rea.mjs', command, root, ...args], { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 })
const inventory = cli('inspect-selected-directory-text-root'); assert.equal(inventory.status, 0, inventory.stderr)
const pin = JSON.parse(inventory.stdout).normalized_result.root_sha256
const text = cli('export-selected-directory-text', pin, JSON.stringify(names)); assert.equal(text.status, 0, text.stderr)
const files = JSON.parse(text.stdout).normalized_result.files
for (const name of names) { const file = files.find(f => f.path === name); assert.equal(file.text, 'OWN_FIXTURE_UTF8_中文_' + name); assert.equal(file.sha256, hash(Buffer.from(file.text))); checks.push('exact UTF-8 export: ' + name) }
const binary = (name, bytes, sha = hash(bytes)) => cli('export-selected-directory-binary', pin, JSON.stringify([{ path: name, sha256: sha }]))
const valid = binary('valid.ttf', font); assert.equal(valid.status, 0, valid.stderr); const result = JSON.parse(valid.stdout).normalized_result.files[0]
assert.equal(result.media_type, 'font/ttf'); assert.deepEqual(Buffer.from(result.data_base64, 'base64'), font); checks.push('SFNT font exact bytes/SHA/media')
for (const [label, response, pattern] of [['invalid TTF signature', binary('invalid.ttf', Buffer.from('not an SFNT font')), /TTF signature/], ['TTF table bounds', binary('range.ttf', badRange), /table range/], ['selected SHA pin', binary('valid.ttf', font, '0'.repeat(64)), /SHA mismatch/], ['path traversal', cli('export-selected-directory-text', pin, JSON.stringify(['../Tool.cs'])), /canonical/]]) { assert.notEqual(response.status, 0, label); assert.match(response.stderr, pattern, label); checks.push(label) }
await fs.writeFile(path.join(output, 'report.json'), JSON.stringify({ result: 'PASS', checks, ownFixtureOnly: true }, null, 2))
console.log(JSON.stringify({ result: 'PASS', checks: checks.length, directory: output }))
+12
View File
@@ -0,0 +1,12 @@
/** Bounded excerpts from SHA-verified REA exports, never target filesystem. */
import fs from 'node:fs'
import crypto from 'node:crypto'
const input = process.argv[2]
const pattern = process.argv[3] || 'expertScanCustomExperts|expertActivatePlugin|expertInstallFromPath|clearSessionMarker|createdBySession|expert-manager'
const radius = Number(process.argv[4] || 550)
const d = JSON.parse(fs.readFileSync(input,'utf8'))
for (const f of d.normalized_result.files) {
if (crypto.createHash('sha256').update(f.text).digest('hex') !== f.sha256) throw new Error('SHA mismatch')
const matches = [...f.text.matchAll(new RegExp(pattern,'g'))].map(m=>({offset:m.index,text:f.text.slice(Math.max(0,m.index-radius),m.index+m[0].length+radius)}))
if(matches.length) console.log(JSON.stringify({path:f.path,sha256:f.sha256,evidenceId:d.evidence_id,matches:matches.slice(0,32),omitted:Math.max(0,matches.length-32)},null,2))
}
+25
View File
@@ -0,0 +1,25 @@
/** AST anchors and finite reviewable definitions from retained REA text. */
import fs from 'node:fs'
import crypto from 'node:crypto'
import {parse} from '@babel/parser'
const inputs=process.argv.slice(2)
const digest = s=>crypto.createHash('sha256').update(s).digest('hex')
const output={offsetUnit:'UTF-16 code units, end exclusive',modules:[]}
for(const input of inputs){
const d=JSON.parse(fs.readFileSync(input,'utf8'))
for(const f of d.normalized_result.files.filter(f=>f.path.endsWith('.js'))){
if(digest(f.text)!==f.sha256)throw new Error('SHA mismatch')
const ast=parse(f.text,{sourceType:'unambiguous'}), defs=[]
function visit(n,owner=''){
if(!n||typeof n!=='object')return
if(n.type==='ClassExpression'||n.type==='ClassDeclaration')owner=n.id?.name||owner
if(n.type==='VariableDeclarator'&&n.init?.type==='ClassExpression')owner=n.id.name
const name=n.type==='FunctionDeclaration'?n.id?.name:n.type==='ClassMethod'||n.type==='ObjectMethod'?n.key.name:undefined
if(name&&(/Expert|expert|SessionMarker|WorkbuddyMeta|resolveArgPath/.test(name)||/Expert/.test(owner)))defs.push({name,owner,start:n.start,end:n.end,sha256:digest(f.text.slice(n.start,n.end)),text:f.text.slice(n.start,n.end)})
for(const[k,v]of Object.entries(n)){if(['loc','comments','tokens'].includes(k))continue;if(Array.isArray(v))v.forEach(c=>visit(c,owner));else if(v&&typeof v==='object')visit(v,owner)}
}visit(ast.program)
output.modules.push({input,evidenceId:d.evidence_id,path:f.path,sha256:f.sha256,definitions:defs})
}
}
fs.writeFileSync('.tmp/rea-expert-package-contracts.json',JSON.stringify(output,null,2))
console.log(JSON.stringify({output:'.tmp/rea-expert-package-contracts.json',modules:output.modules.map(({definitions,...m})=>({...m,count:definitions.length}))},null,2))
+21
View File
@@ -0,0 +1,21 @@
/** Exact public REA export. Does not read ASAR or installed files itself. */
import fs from 'node:fs'
import { spawnSync } from 'node:child_process'
import crypto from 'node:crypto'
const inventory = JSON.parse(fs.readFileSync('.tmp/rea-expert-package-inventory.json', 'utf8'))
const paths = process.argv.slice(2)
if (!paths.length) throw new Error('Select exact paths')
const r = spawnSync(process.execPath, ['.tmp/rea-css-runtime/scripts/rea.mjs', 'export-selected-artifact-text',
inventory.inputPath, inventory.rootSha256, JSON.stringify(paths)], {encoding:'utf8', maxBuffer:32*1024*1024})
if (r.status !== 0) throw new Error(r.stderr || r.stdout || `REA exited ${r.status}`)
const evidence = JSON.parse(r.stdout)
if (evidence.normalized_result.root_sha256 !== inventory.rootSha256) throw new Error('REA root mismatch')
for (const file of evidence.normalized_result.files) {
const sha = crypto.createHash('sha256').update(file.text).digest('hex')
if (sha !== file.sha256) throw new Error(`Text SHA mismatch: ${file.path}`)
const expected = inventory.files.find(item => item.path === file.path)
if (expected && expected.sha256 !== sha) throw new Error(`Inventory SHA mismatch: ${file.path}`)
}
const output = `.tmp/rea-expert-package-${evidence.evidence_id}.json`
fs.writeFileSync(output,JSON.stringify(evidence))
console.log(JSON.stringify({output,evidenceId:evidence.evidence_id,files:evidence.normalized_result.files.map(({text,...metadata})=>metadata)},null,2))
+22
View File
@@ -0,0 +1,22 @@
/** Read only retained REA inventory; never opens the installed target. */
import fs from 'node:fs'
const input = '.tmp/rea-workbuddy-takeover-2026-10-15.json'
const pin = '035254bc76310ea46317433e4840602094543ef323d2d5c5aff7c131729de918'
const retained = JSON.parse(fs.readFileSync(input, 'utf8'))
const record = retained.records.find(item => item.provider.id === 'rea-javascript-application')
if (record.normalized_result.root_artifact_sha256 !== pin) throw new Error('Root pin mismatch')
const files = new Map()
for (const node of record.normalized_result.graph.nodes) for (const observation of node.observations) {
const p = observation.properties
if (observation.evidence.extractor.operation !== 'inventory-relevant-file') continue
if ((!p.path.startsWith('main/') && !/(?:expert|daemon|plugin|agent|schema)/i.test(p.path)) || p.path.startsWith('node_modules/')) continue
files.set(p.path, { path: p.path, sha256: observation.evidence.artifact.sha256, bytes: p.bytes,
fileKind: p.file_kind, unpacked: p.unpacked, textStatus: p.text_status })
}
const output = '.tmp/rea-expert-package-inventory.json'
fs.writeFileSync(output, JSON.stringify({ input, evidenceId: record.evidence_id, rootSha256: pin,
inventoryGraphSha256: record.normalized_result.inventory_graph_sha256,
inputPath: record.normalized_result.input_path, scope: 'matching paths in retained relevant-file inventory; not all archive entries',
files: [...files.values()].sort((a,b) => a.path.localeCompare(b.path)) }, null, 2))
console.log(JSON.stringify({ output, evidenceId: record.evidence_id, count: files.size,
candidates: [...files.values()].filter(file => /^main\/(?:daemon|runtime-handlers|module\.app-server|server)\./.test(file.path) || /expert-manager|marketplace\.json/.test(file.path)) },null,2))
+31
View File
@@ -0,0 +1,31 @@
/** Restore exact SHA-pinned REA skill exports; no original file access/execution. */
import fs from 'node:fs'
import path from 'node:path'
import crypto from 'node:crypto'
const prefix='resources/plugins/workbuddy-builtin/skills/expert-manager/'
const target=path.resolve('src/main/capabilities/assets/expert-manager')
const pin='035254bc76310ea46317433e4840602094543ef323d2d5c5aff7c131729de918'
const inputs=process.argv.slice(2)
if(!inputs.length)throw new Error('Select retained REA exports')
const manifest={targetVersion:'WorkBuddy 5.8.2',rootSha256:pin,scope:'exact retained expert-manager files; no original initialization or Python execution',files:[]}
const bundle={files:{}}
for(const input of inputs){
const d=JSON.parse(fs.readFileSync(input,'utf8'))
if(d.normalized_result.root_sha256!==pin)throw new Error('Root mismatch')
for(const f of d.normalized_result.files){
if(!f.path.startsWith(prefix))continue
const relative=f.path.slice(prefix.length)
if(relative.split('/').some(p=>!p||p==='.'||p==='..'))throw new Error('Noncanonical member')
const sha=crypto.createHash('sha256').update(f.text).digest('hex')
if(sha!==f.sha256||Buffer.byteLength(f.text)!==f.size_bytes)throw new Error('REA bytes mismatch')
const destination=path.join(target,...relative.split('/'))
fs.mkdirSync(path.dirname(destination),{recursive:true})
fs.writeFileSync(destination,f.text)
if(crypto.createHash('sha256').update(fs.readFileSync(destination)).digest('hex')!==sha)throw new Error('Restored SHA mismatch')
manifest.files.push({file:relative,originalMember:f.path,sha256:sha,bytes:f.size_bytes,evidenceId:d.evidence_id,retainedExport:input})
bundle.files[relative]=f.text
}
}
fs.writeFileSync(path.join(target,'rea-source-manifest.json'),JSON.stringify(manifest,null,2)+'\n')
fs.writeFileSync(path.join(path.dirname(target),'expert-manager-source.json'),JSON.stringify(bundle,null,2)+'\n')
console.log(JSON.stringify({target,files:manifest.files.length,bytes:manifest.files.reduce((n,f)=>n+f.bytes,0)}))
@@ -0,0 +1,41 @@
/** Own ASAR fixture exercises the official local REA provider/public CLI only. */
import assert from 'node:assert/strict'
import fs from 'node:fs/promises'
import path from 'node:path'
import crypto from 'node:crypto'
import { createRequire } from 'node:module'
import { pathToFileURL } from 'node:url'
import { spawnSync } from 'node:child_process'
const root = await fs.mkdtemp(path.resolve('.tmp/rea-expert-package-fixture-'))
const requireRuntime = createRequire(path.resolve('.tmp/rea-css-runtime/package.json'))
const {createPackageWithOptions} = await import(pathToFileURL(requireRuntime.resolve('@electron/asar')).href)
const digest = bytes => crypto.createHash('sha256').update(bytes).digest('hex')
const source = path.join(root,'source'), archive = path.join(root,'fixture.asar')
const files = {'expert/SKILL.md':'---\nname: local-test\n---\n# 本地专家\n', 'expert/schema.yaml':'name: local-test\n',
'expert/schema.yml':'version: 1\n','expert/README.txt':'strict UTF-8\n', 'expert/register.py':'# retained, never executed\nprint("中文")\n', 'expert/bad.md':Buffer.from([0xc3,0x28]),
'expert/large.txt':Buffer.alloc(8*1024*1024+1,97),'expert/one.md':Buffer.alloc(8*1024*1024,97),
'expert/two.md':Buffer.alloc(8*1024*1024,98),'expert/extra.txt':'x','expert/unselected.md':'not selected'}
for(const [p,content] of Object.entries(files)){await fs.mkdir(path.dirname(path.join(source,p)),{recursive:true});await fs.writeFile(path.join(source,p),content)}
await createPackageWithOptions(source,archive,{unpackDir:'expert'})
await fs.rm(path.join(archive+'.unpacked','expert/unselected.md'))
const pin=digest(await fs.readFile(archive)), checks=[]
function cli(paths,expected=pin){return spawnSync(process.execPath,['.tmp/rea-css-runtime/scripts/rea.mjs','export-selected-artifact-text',archive,expected,JSON.stringify(paths)],{encoding:'utf8',maxBuffer:32*1024*1024})}
function reject(label,paths,pattern,expected=pin){const r=cli(paths,expected);assert.notEqual(r.status,0,label);assert.match(r.stderr,pattern,label);checks.push({label,pass:true})}
const selected=Object.keys(files).slice(0,5), r=cli(selected)
assert.equal(r.status,0,r.stderr)
const evidence=JSON.parse(r.stdout)
assert.equal(evidence.normalized_result.root_sha256,pin)
assert.deepEqual(evidence.normalized_result.files.map(f=>f.path),selected)
for(const f of evidence.normalized_result.files){assert.equal(f.text,files[f.path]);assert.equal(f.sha256,digest(files[f.path]));assert.equal(f.size_bytes,Buffer.byteLength(files[f.path]))}
checks.push({label:'five new suffixes, exact bytes/SHA/root and missing unselected unpacked ignored; Python not executed',pass:true})
reject('root mismatch',selected,/changed before interpretation/,'0'.repeat(64))
reject('canonical selection',['../expert/SKILL.md'],/not canonical/)
reject('unknown suffix',['expert/private.db'],/supported UTF-8/)
reject('strict UTF-8',['expert/bad.md'],/strict UTF-8/)
reject('per-file budget',['expert/large.txt'],/per-file budget/)
reject('aggregate budget',['expert/one.md','expert/two.md','expert/extra.txt'],/aggregate budget/)
reject('absent selected member',['expert/absent.md'],/absent from/)
reject('duplicate selected member',['expert/SKILL.md','expert/SKILL.md'],/Duplicate selected path/)
await fs.writeFile(path.join(root,'evidence.json'),JSON.stringify(evidence,null,2))
await fs.writeFile(path.join(root,'report.json'),JSON.stringify({result:'PASS',rootPin:pin,evidenceId:evidence.evidence_id,checks},null,2))
console.log(JSON.stringify({result:'PASS',root,checks:checks.length,evidenceId:evidence.evidence_id}))
+64
View File
@@ -0,0 +1,64 @@
// Materialize only CSS text already returned in real REA Evidence. No target access.
import fs from 'node:fs/promises'
import path from 'node:path'
import crypto from 'node:crypto'
import postcss from 'postcss'
const input = path.resolve(process.argv[2] || '.tmp/rea-workbuddy-css-evidence.json')
const output = path.resolve(process.argv[3] || '.tmp/rea-css-recovered')
const evidence = JSON.parse(await fs.readFile(input, 'utf8'))
if (evidence.operation !== 'inspect_electron_page' || evidence.parameters?.include_style_sources !== true || !evidence.normalized_result?.styles) throw new Error('Expected actual REA CSS capture Evidence')
const selection = JSON.parse(await fs.readFile('.tmp/rea-style-selection.json', 'utf8'))
const nodes = new Map(selection.snapshot.nodes.map(node => [node.artifact_id, node]))
const inventory = new Map(selection.snapshot.occurrences.map(occurrence => [occurrence.logical_path, { occurrence, node: nodes.get(occurrence.artifact_id) }]))
const files = []
const usedNames = new Map()
const families = ['cr-tool-call', 'question-floating', 'cb-plan-task-wait-icon', 'cb-plan-tool-container', 'cb-task-tool-renderer', 'cr-reasoning', 'artifact-content', 'markdown-editor-container']
const coverage = Object.fromEntries(families.map(family => [family, []]))
await fs.mkdir(output, { recursive: true })
for (const style of evidence.normalized_result.styles.items) {
if (!style.source.included) continue
const artifact = style.source.artifact
const bytes = Buffer.from(artifact.text, 'utf8')
const sha256 = crypto.createHash('sha256').update(bytes).digest('hex')
if (sha256 !== artifact.sha256 || bytes.length !== artifact.bytes || artifact.media_type !== 'text/css') throw new Error('REA CSS artifact digest/size/type mismatch')
const logicalPath = style.file_path?.replace(/\\/g, '/').split('/app.asar/')[1]
let filename = style.is_inline || !logicalPath ? `inline-${sha256.slice(0, 16)}.css` : path.posix.basename(logicalPath)
if (!filename.endsWith('.css') || !/^[a-zA-Z0-9._-]+$/.test(filename)) throw new Error('Invalid CSS output filename')
if (usedNames.has(filename) && usedNames.get(filename) !== sha256) filename = filename.replace(/\.css$/, `-${sha256.slice(0, 16)}.css`)
usedNames.set(filename, sha256)
const known = logicalPath && !style.is_inline ? inventory.get(logicalPath) : undefined
const asarComparison = known ? {
status: known.node?.sha256 === sha256 && known.node?.size === bytes.length ? 'match' : 'mismatch',
evidenceId: selection.parentEvidenceId, artifactId: known.occurrence.artifact_id,
rootSha256: selection.snapshot.manifest.root_sha256,
expectedSha256: known.node?.sha256, expectedBytes: known.node?.size,
} : undefined
const root = postcss.parse(artifact.text, { from: filename })
let rules = 0
const counts = Object.fromEntries(families.map(family => [family, 0]))
root.walkRules(rule => {
rules++
for (const family of families) if (rule.selector.includes(family)) counts[family]++
})
const destination = path.join(output, filename)
await fs.writeFile(destination, bytes)
if (crypto.createHash('sha256').update(await fs.readFile(destination)).digest('hex') !== sha256) throw new Error('Written CSS digest mismatch')
for (const family of families) if (counts[family]) coverage[family].push({ file: filename, rules: counts[family] })
files.push({ file: filename, filePath: style.file_path, frameId: style.frame_id, styleSheetKey: style.style_sheet_key,
isInline: style.is_inline, sha256, bytes: bytes.length, rules,
...(asarComparison ? { asarComparison } : {}) })
}
files.sort((a, b) => a.file.localeCompare(b.file))
const capturedPaths = new Set(files.filter(file => file.asarComparison).map(file => file.filePath.replace(/\\/g, '/').split('/app.asar/')[1]))
const manifest = { sourceEvidenceId: evidence.evidence_id, sourceEvidencePath: input, provider: evidence.provider,
target: evidence.normalized_result.target, captureWindow: evidence.normalized_result.capture_window,
runtimeStyles: { total: evidence.normalized_result.styles.total, excluded: evidence.normalized_result.styles.excluded,
sourceFailures: evidence.normalized_result.styles.source_failures, truncated: evidence.normalized_result.styles.truncated },
files, bytes: files.reduce((total, file) => total + file.bytes, 0), matchedAsarArtifacts: files.filter(file => file.asarComparison?.status === 'match').length,
mismatchedAsarArtifacts: files.filter(file => file.asarComparison?.status === 'mismatch').map(file => file.file),
selectedInventoryNotObserved: [...inventory.keys()].filter(key => !capturedPaths.has(key)), selectorCoverage: coverage,
limitations: ['Scope is styles loaded in the selected Electron capture window, not every packaged CSS file.', 'Inline/constructed styles are runtime text and do not establish original file identity.', 'Only the prior selected ASAR inventory has been compared; other CSS remains runtime-attributed.', 'A mismatch is preserved as runtime CSS and does not establish original ASAR byte identity; its cause has not been determined.'] }
await fs.writeFile(path.join(output, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n')
console.log(JSON.stringify({ evidenceId: manifest.sourceEvidenceId, output, files: files.length, bytes: manifest.bytes,
matchedAsarArtifacts: manifest.matchedAsarArtifacts, mismatchedAsarArtifacts: manifest.mismatchedAsarArtifacts,
selectedInventoryNotObserved: manifest.selectedInventoryNotObserved, selectorCoverage: coverage }, null, 2))
+102
View File
@@ -0,0 +1,102 @@
import assert from 'node:assert/strict'
import fs from 'node:fs/promises'
import path from 'node:path'
import crypto from 'node:crypto'
import { tmpdir } from 'node:os'
import { pathToFileURL } from 'node:url'
const runtime=path.resolve('.tmp/rea-css-runtime/dist')
const load=relative=>import(pathToFileURL(path.join(runtime,relative)))
const {captureElectronMediaAssets}=await load('browser/CdpElectronMediaAssets.js')
const {electronVisualProbeInputSchema,electronMediaAssetsSchema}=await load('domain/javascript/ElectronVisualProbeSchema.js')
const {inspectElectronPageInputSchema,electronPageInspectionSchema}=await load('domain/javascript/electronObservation.js')
const {inspectCdpElectronPage}=await load('browser/CdpElectronInspection.js')
const {createElectronEvidence}=await load('application/javascript/ElectronEvidence.js')
const temporary=await fs.mkdtemp(path.join(tmpdir(),'rea-img-resource-'))
try {
const html=path.join(temporary,'index.html');await fs.writeFile(html,'<!doctype html><img class="selected">')
const page=pathToFileURL(html).href,resourceUrl=pathToFileURL(path.join(temporary,'actual.png')).href
const main={frame_id:'main',file_path:await fs.realpath(html)},frame={id:'main',url:page}
const png='iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jKXcAAAAASUVORK5CYII='
const bytes=Buffer.from(png,'base64'),digest=crypto.createHash('sha256').update(bytes).digest('hex')
const makeSnapshot=()=>({strings:[page,'main','HTML','IMG','class','selected','src','fallback.png',resourceUrl,'contenteditable','true','14px'],documents:[{
documentURL:0,frameId:1,scrollOffsetX:0,scrollOffsetY:0,
nodes:{nodeName:[2,3],parentIndex:[-1,0],backendNodeId:[10,20],attributes:[[],[4,5,6,7]],currentSourceURL:{index:[1],value:[8]}},
layout:{nodeIndex:[0,1],bounds:[[0,0,100,80],[10,10,40,50]],styles:[]}
}]})
let mode='ok',snapshot=makeSnapshot(),calls=[],listener,selectedResourceUrl=resourceUrl
const send=async(method,params={})=>{
calls.push({method,params})
assert.ok(!['Runtime.evaluate','Runtime.callFunctionOn','Runtime.getProperties','Network.getCookies','Network.loadNetworkResource','Page.navigate'].includes(method))
if(method==='Page.getFrameTree')return {frameTree:{frame:mode==='navigation-after-content'&&calls.some(c=>c.method==='Page.getResourceContent')?{...frame,id:'changed'}:frame}}
if(method==='DOM.getDocument')return {root:{nodeId:1}}
if(method==='DOM.querySelectorAll')return {nodeIds:mode==='absent'?[]:mode==='ambiguous'?[2,3]:[2]}
if(method==='DOM.describeNode')return {node:{nodeName:mode==='not-img'?'DIV':'IMG',backendNodeId:mode==='foreign-backend'?999:20}}
if(method==='Page.getLayoutMetrics')return {cssLayoutViewport:{clientWidth:100,clientHeight:80,pageX:0,pageY:0}}
if(method==='DOM.getNodeForLocation')return {backendNodeId:mode==='covered'?999:mode==='partly-covered'&&params.y>20?999:20}
if(method==='Page.getResourceTree')return {frameTree:{frame,resources:mode==='not-loaded'?[]:[{url:selectedResourceUrl,type:'Image',mimeType:mode==='svg'?'image/svg+xml':'image/png',...(mode==='declared-oversize'?{contentSize:2097153}:{})}],childFrames:[{frame:{id:'foreign',url:'https://foreign.invalid'},resources:[{url:resourceUrl,type:'Image',mimeType:'image/png'}]}]}}
if(method==='Page.getResourceContent'){
if(mode==='content-unavailable')throw Error('private-url-or-cookie-must-not-leak')
return {base64Encoded:mode!=='not-binary',content:mode==='invalid-base64'?'not-base64':mode==='signature'?'YWJj':mode==='oversize'?Buffer.alloc(2097153).toString('base64'):png}
}
if(method==='DOMSnapshot.captureSnapshot'){const copy=structuredClone(snapshot);for(const doc of copy.documents)doc.layout.styles=doc.layout.nodeIndex.map(()=>params.computedStyles.map(()=>11));return copy}
if(method==='Page.captureScreenshot')return {data:png}
if(method==='Target.getTargets')return {targetInfos:[]}
return {}
}
assert.deepEqual(electronVisualProbeInputSchema.parse({}).media_selectors,[])
assert.equal(electronVisualProbeInputSchema.safeParse({media_selectors:Array.from({length:5},(_,i)=>'#img'+i)}).success,false)
assert.equal(electronVisualProbeInputSchema.safeParse({media_selectors:['#same','#same']}).success,false)
assert.equal(await captureElectronMediaAssets(snapshot,[main],[],send),undefined)
assert.equal(calls.length,0,'default off must perform no resource reads')
const capture=async()=>{calls=[];return electronMediaAssetsSchema.parse(await captureElectronMediaAssets(snapshot,[main],['.selected'],send))}
let result=await capture()
assert.equal(result.items[0].status,'exported')
assert.equal(result.items[0].source.selection,'current_source_url')
assert.equal(result.items[0].artifact.sha256,digest)
assert.equal(result.items[0].artifact.data_base64,png)
assert.equal(result.total_bytes,bytes.length)
assert.equal(calls.find(c=>c.method==='Page.getResourceContent').params.url,resourceUrl,'currentSrc wins over the fallback src attribute')
assert.equal(result.items[0].backend_node_id,20)
mode='covered';result=await capture();assert.equal(result.items[0].status,'exported');assert.equal(result.items[0].visibility.status,'covered');assert.equal(result.items[0].selected_node.current_source_url,resourceUrl);
mode='partly-covered';assert.equal((await capture()).items[0].status,'exported','partial occlusion may still export actual loaded full image bytes')
for(const [test,reason] of [['absent','selector_absent'],['ambiguous','selector_ambiguous'],['not-img','selected_node_not_img'],['foreign-backend','snapshot_identity_unavailable'],['not-loaded','selected_source_not_loaded_in_main_frame'],['svg','unsupported_image_type'],['declared-oversize','asset_byte_budget_exceeded'],['oversize','asset_byte_budget_exceeded'],['signature','image_signature_mismatch'],['invalid-base64','invalid_base64'],['not-binary','binary_content_unavailable'],['content-unavailable','protocol_resource_unavailable'],['navigation-after-content','frame_changed']]){
mode=test;result=await capture();assert.equal(result.items[0].status,'unknown',test);assert.equal(result.items[0].reason,reason,test);assert.equal(result.total_bytes,0,test)
assert.equal(result.items[0].artifact,undefined,test)
if(['not-loaded','svg','declared-oversize'].includes(test))assert.equal(calls.some(c=>c.method==='Page.getResourceContent'),false,test+' must reject before the read')
assert.equal(JSON.stringify(result).includes('private-url-or-cookie'),false)
}
mode='ok';snapshot=makeSnapshot();snapshot.documents[0].nodes.attributes[0]=[9,10]
assert.equal((await capture()).items[0].reason,'editable_subtree_excluded')
snapshot=makeSnapshot();snapshot.documents[0].layout.bounds[1]=[200,200,40,50]
assert.equal((await capture()).items[0].visibility.reason,'outside_visible_viewport')
snapshot=makeSnapshot();snapshot.documents[0].layout.nodeIndex=[0]
assert.equal((await capture()).items[0].visibility.reason,'no_visible_layout')
snapshot=makeSnapshot();delete snapshot.documents[0].nodes.currentSourceURL;snapshot.strings[7]=resourceUrl
assert.equal((await capture()).items[0].source.selection,'src')
snapshot=makeSnapshot();selectedResourceUrl='https://account:private-password@assets.invalid/img.png?token=private-query'
snapshot.strings[8]=selectedResourceUrl+'#fragment'
result=await capture()
assert.equal(result.items[0].status,'exported')
assert.equal(result.items[0].source.url,'https://assets.invalid/img.png')
assert.equal(result.items[0].selected_node.current_source_url,'https://assets.invalid/img.png')
assert.equal(result.items[0].source.url_sha256,crypto.createHash('sha256').update(snapshot.strings[8]).digest('hex'))
assert.equal(JSON.stringify(result).includes('private-'),false,'source metadata omits URL credentials and queries')
selectedResourceUrl=resourceUrl
snapshot=makeSnapshot()
// Exercise the public inspection normalization and Evidence path, not only the helper.
const input=inspectElectronPageInputSchema.parse({cdp_endpoint:'http://127.0.0.1:9223',target_id:'fixture',observation_ms:0,include_visuals:true,visual_probe:{media_selectors:['.selected']}})
const context={connection:{send,onEvent(fn){listener=fn;return()=>{listener=undefined}}},sessionId:'selected-session',input,target:{id:'fixture',type:'page',title:'Own fixture',attached:false},discovery:{version:{product:'Fixture/1',protocol_version:'1.3',revision:'fixture',user_agent:'fixture',js_version:'fixture'}}}
const inspection=electronPageInspectionSchema.parse(await inspectCdpElectronPage(context))
assert.equal(inspection.visuals.media_assets.items[0].artifact.sha256,digest)
assert.deepEqual(inspection.visuals.layout.documents[0].nodes[1].attributes,{class:'selected'})
const evidence=createElectronEvidence('inspect_electron_page',input,inspection,{id:'rea-cdp-electron',name:'Local selected resource fixture',version:'1+visual-local.3'})
assert.deepEqual(evidence.parameters.visual_probe.media_selectors,['.selected'])
assert.equal(evidence.normalized_result.visuals.media_assets.items[0].artifact.data_base64,png)
assert.match(evidence.evidence_id,/^ev_[a-f0-9]{64}$/)
const optedOut=electronPageInspectionSchema.parse(await inspectCdpElectronPage({...context,input:{...input,visual_probe:undefined}}))
assert.equal(optedOut.visuals.media_assets,undefined)
assert.equal(JSON.stringify(optedOut.visuals).includes('fallback.png'),false)
assert.equal(listener,undefined)
console.log('REA selected IMG resource regression passed: opt-in/schema/Evidence, snapshot currentSrc/backend attribution, covered bytes with separate visibility, scope, binary budgets/signatures, URL credential redaction, unknowns and default URL omission')
}finally{await fs.rm(temporary,{recursive:true,force:true})}
@@ -0,0 +1,42 @@
import assert from 'node:assert/strict'
import path from 'node:path'
import { pathToFileURL } from 'node:url'
import { build } from 'esbuild'
const output = path.resolve('.tmp/modern-conversation-labels-regression.mjs')
await build({
entryPoints: ['src/renderer/src/workbuddy/conversation/source-labels.ts'],
bundle: true,
platform: 'node',
format: 'esm',
outfile: output,
logLevel: 'silent'
})
const labels = await import(`${pathToFileURL(output).href}?run=${Date.now()}`)
const label = labels.sourceConversationLabel
assert.equal(labels.SOURCE_CONVERSATION_DEFAULT_LOCALE, 'zh-CN')
assert.equal(label('tool.command.confirm'), '确认执行命令?')
assert.equal(label('tool.command.run'), '运行')
assert.equal(label('tool.command.skip'), '跳过')
assert.equal(label('tool.command.reject'), '拒绝')
assert.equal(label('tool.writeFile.editing'), '修改中')
assert.equal(label('tool.writeFile.create'), '创建')
assert.equal(label('tool.writeFile.accept'), '接受')
assert.equal(label('fold.completedWithDuration', { duration: '2.4 秒' }), '已完成 2.4 秒')
assert.equal(label('chatInput.modelSelector.thinkingMode'), '思考模式')
assert.equal(label('tool.command.confirm', undefined, 'en-US'), 'Run this command?')
assert.equal(label('tool.command.run', undefined, 'en-US'), 'Run')
assert.equal(label('tool.command.skip', undefined, 'en-US'), 'Skip')
assert.equal(label('tool.command.reject', undefined, 'en-US'), 'Reject')
assert.equal(label('tool.writeFile.editing', undefined, 'en-US'), 'Editing')
assert.equal(label('tool.writeFile.create', undefined, 'en-US'), 'Create')
assert.equal(label('tool.writeFile.accept', undefined, 'en-US'), 'Accept')
assert.equal(label('fold.completedWithDuration', { duration: '2.4 sec' }, 'en-US'), 'Completed 2.4 sec')
assert.equal(label('chatInput.modelSelector.thinkingMode', undefined, 'en-US'), 'Thinking mode')
assert.equal(label('fold.completedWithDuration'), '已完成 {duration}')
assert.equal(label('fold.completedWithDuration', { duration: 0 }), '已完成 0')
assert.equal(label('not.a.source.label'), 'not.a.source.label')
console.log('Modern conversation label regression passed (zh-CN/en-US, source anchors, interpolation, fallback).')
@@ -0,0 +1,98 @@
import { createHash } from 'node:crypto';
import { authorizedElectronFile } from './ElectronFileScope.js';
import { decodeCanonicalBase64 } from '../domain/webScreenshot.js';
const MAX_ASSETS=4, MAX_ASSET_BYTES=2*1024*1024, MAX_TOTAL_BYTES=8*1024*1024;
const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
const types=new Set(['image/png','image/jpeg','image/gif','image/webp']);
const mime=value=>value==='image/jpg'?'image/jpeg':value;
function sniff(bytes){
if(bytes.length>=24&&bytes.subarray(0,8).equals(Buffer.from([137,80,78,71,13,10,26,10])))return 'image/png';
if(bytes.length>=3&&bytes[0]===255&&bytes[1]===216&&bytes[2]===255)return 'image/jpeg';
if(bytes.length>=10&&/^GIF8[79]a$/.test(bytes.toString('ascii',0,6)))return 'image/gif';
if(bytes.length>=12&&bytes.toString('ascii',0,4)==='RIFF'&&bytes.toString('ascii',8,12)==='WEBP')return 'image/webp';
}
function safeSourceUrl(value){
try{const u=new URL(value);if(!['file:','http:','https:'].includes(u.protocol))return null;u.username='';u.password='';u.search='';u.hash='';const result=u.href;return result.length<=2048?result:null;}catch{return null;}
}
/** Only a caller-selected IMG and its already loaded main-frame resource are eligible. */
export async function captureElectronMediaAssets(snapshot,frames,selectors,send){
if(!selectors?.length)return undefined;
const result={items:[],total_bytes:0,limits:{max_assets:MAX_ASSETS,max_asset_bytes:MAX_ASSET_BYTES,max_total_bytes:MAX_TOTAL_BYTES},limitations:[
'Opt-in IMG resources only: exact unique selectors, authorized main file frame and snapshot backend identity. Visibility is reported separately and does not authorize new requests.',
'Only Page.getResourceTree resources already loaded by the selected frame are read with Page.getResourceContent. No HTTP fetch, cookies, headers, input values or renderer JavaScript are used.',
'PNG/JPEG/GIF/WebP only; SVG, canvas, video, CSS backgrounds and resources missing from the loaded frame tree remain unknown. Covered loaded images can be exported; bytes do not establish that this image is the currently visible layer.',
'Resource URLs in metadata omit credentials, query strings and fragments; URL SHA-256 identifies the exact selected URL. DOM and resource observations are sequential.'
]};
const main=frames[0],strings=snapshot.strings??[],str=index=>strings[index]??'';
const document=snapshot.documents?.find(value=>str(value.frameId)===main.frame_id);
const stable=async()=>{const frame=(await send('Page.getFrameTree',{})).frameTree?.frame;if(!frame||frame.id!==main.frame_id||await authorizedElectronFile(frame.url)!==main.file_path)throw Error('frame_changed');};
for(const selector of selectors.slice(0,MAX_ASSETS)){
const item={selector,frame_id:main.frame_id,status:'unknown',reason:'unavailable'};
result.items.push(item);
try{
await stable();
if(!document||await authorizedElectronFile(str(document.documentURL))!==main.file_path)throw Error('snapshot_frame_unavailable');
const root=(await send('DOM.getDocument',{depth:0,pierce:false})).root;
const ids=(await send('DOM.querySelectorAll',{nodeId:root.nodeId,selector})).nodeIds??[];
if(ids.length!==1)throw Error(ids.length?'selector_ambiguous':'selector_absent');
const node=(await send('DOM.describeNode',{nodeId:ids[0],depth:0,pierce:false})).node;
if(node?.nodeName!=='IMG')throw Error('selected_node_not_img');
const raw=document.nodes??{},index=raw.backendNodeId?.indexOf(node.backendNodeId)??-1;
if(index<0||str(raw.nodeName?.[index])!=='IMG')throw Error('snapshot_identity_unavailable');
item.node_index=index;item.backend_node_id=node.backendNodeId;
let excluded=false;
for(let n=index;n>=0;n=raw.parentIndex?.[n]??-1){const tag=str(raw.nodeName?.[n]),attrs=raw.attributes?.[n]??[];if(/^(INPUT|TEXTAREA|SELECT|SCRIPT|STYLE)$/.test(tag))excluded=true;for(let a=0;a<attrs.length;a+=2)if(str(attrs[a])==='contenteditable'&&str(attrs[a+1])!=='false')excluded=true;}
if(excluded)throw Error('editable_subtree_excluded');
const layoutIndex=document.layout?.nodeIndex?.indexOf(index)??-1,bounds=document.layout?.bounds?.[layoutIndex];
item.visibility={status:'unknown',reason:'no_visible_layout'};
if(layoutIndex>=0&&bounds&&bounds[2]>0&&bounds[3]>0)try{
const viewport=(await send('Page.getLayoutMetrics',{})).cssLayoutViewport;
const x=bounds[0]-(viewport.pageX??document.scrollOffsetX??0),y=bounds[1]-(viewport.pageY??document.scrollOffsetY??0);
const left=Math.max(0,x),top=Math.max(0,y),right=Math.min(viewport.clientWidth,x+bounds[2]),bottom=Math.min(viewport.clientHeight,y+bounds[3]);
if(right<=left||bottom<=top)item.visibility={status:'unknown',reason:'outside_visible_viewport'};
else{
item.visibility={status:'covered',reason:'sampled_points_hit_other_layers'};item.visibility_samples=[];
for(const [fx,fy] of [[.5,.05],[.5,.5],[.5,.95],[.05,.05],[.95,.05],[.05,.5],[.95,.5],[.05,.95],[.95,.95]]){
const point={x:Math.floor(left+(right-left)*fx),y:Math.floor(top+(bottom-top)*fy)};
const hit=await send('DOM.getNodeForLocation',{...point,includeUserAgentShadowDOM:false,ignorePointerEventsNone:true});
const hitIndex=raw.backendNodeId?.indexOf(hit.backendNodeId)??-1,hitAttrs=raw.attributes?.[hitIndex]??[];
let hitClass='';for(let a=0;a<hitAttrs.length;a+=2)if(str(hitAttrs[a])==='class')hitClass=str(hitAttrs[a+1]).slice(0,500);
item.visibility_samples.push({...point,...(hit.backendNodeId?{hit_backend_node_id:hit.backendNodeId}:{}),...(hitIndex>=0?{hit_node_index:hitIndex,hit_node_name:str(raw.nodeName?.[hitIndex]),hit_class:hitClass}:{})});
if(hit.backendNodeId===node.backendNodeId){item.visibility={status:'observed'};break;}
}
}
}catch{item.visibility={status:'unknown',reason:'hit_test_unavailable'};}
const current=raw.currentSourceURL,at=current?.index?.indexOf(index)??-1;
let source=at>=0?str(current.value?.[at]):'',selection='current_source_url';
if(!source){selection='src';const attrs=raw.attributes?.[index]??[];for(let a=0;a<attrs.length;a+=2)if(str(attrs[a])==='src')source=str(attrs[a+1]);}
if(!source)throw Error('source_url_unavailable');
const selectedUrl=new URL(source,str(document.documentURL)),url=new URL(selectedUrl);url.hash='';
const selectedAttributes={},nodeAttrs=raw.attributes?.[index]??[];
for(let a=0;a<nodeAttrs.length;a+=2){const name=str(nodeAttrs[a]),value=str(nodeAttrs[a+1]);if(['class','id','width','height','loading','decoding','alt'].includes(name))selectedAttributes[name]=value.slice(0,500);else if(name==='src'){try{selectedAttributes.src=safeSourceUrl(new URL(value,str(document.documentURL)).href)??'[URL omitted]';}catch{selectedAttributes.src='[URL unavailable]';}}}
item.selected_node={node_name:'IMG',attributes:selectedAttributes,current_source_url:at>=0?safeSourceUrl(str(current.value?.[at])):null};
const tree=(await send('Page.getResourceTree',{})).frameTree;
if(!tree?.frame||tree.frame.id!==main.frame_id||await authorizedElectronFile(tree.frame.url)!==main.file_path)throw Error('frame_changed');
const resource=tree.resources?.find(value=>value.url===url.href&&value.type==='Image'&&!value.failed&&!value.canceled);
if(!resource)throw Error('selected_source_not_loaded_in_main_frame');
const contentType=mime(resource.mimeType?.toLowerCase());
item.source={url:safeSourceUrl(selectedUrl.href),url_sha256:sha(selectedUrl.href),selection,mime_type:contentType??'unknown'};
if(!types.has(contentType))throw Error('unsupported_image_type');
if(resource.contentSize>MAX_ASSET_BYTES)throw Error('asset_byte_budget_exceeded');
const content=await send('Page.getResourceContent',{frameId:main.frame_id,url:resource.url});
if(typeof content.content!=='string'||!content.base64Encoded)throw Error('binary_content_unavailable');
if(content.content.length>4*Math.ceil(MAX_ASSET_BYTES/3))throw Error('asset_byte_budget_exceeded');
const bytes=decodeCanonicalBase64(content.content);
if(!bytes?.length)throw Error('invalid_base64');
if(bytes.length>MAX_ASSET_BYTES)throw Error('asset_byte_budget_exceeded');
if(result.total_bytes+bytes.length>MAX_TOTAL_BYTES)throw Error('total_byte_budget_exceeded');
if(sniff(bytes)!==contentType)throw Error('image_signature_mismatch');
await stable();
item.status='exported';delete item.reason;
item.artifact={mime_type:contentType,bytes:bytes.length,sha256:sha(bytes),data_base64:bytes.toString('base64')};
result.total_bytes+=bytes.length;
}catch(error){const allowed=new Set(['frame_changed','snapshot_frame_unavailable','selector_ambiguous','selector_absent','selected_node_not_img','snapshot_identity_unavailable','editable_subtree_excluded','no_visible_layout','outside_visible_viewport','visibility_unconfirmed_or_covered','source_url_unavailable','selected_source_not_loaded_in_main_frame','unsupported_image_type','asset_byte_budget_exceeded','binary_content_unavailable','invalid_base64','total_byte_budget_exceeded','image_signature_mismatch']);item.reason=allowed.has(error.message)?error.message:'protocol_resource_unavailable';}
}
return result;
}
@@ -0,0 +1,67 @@
// Read actual fonts used for bounded, visible message/name/code samples. No renderer evaluation.
export async function captureElectronPlatformFonts(snapshot,frames,send) {
const categories=['assistant-name','chinese-body','english-body','user-bubble','code'];
const samples=[],missing=[];
const document=snapshot.documents?.find(d=>snapshot.strings[d.frameId]===frames[0].frame_id);
if(!document)return {samples,missing:categories.map(category=>({category,reason:'Authorized main document absent'}))};
const strings=snapshot.strings,nodes=document.nodes,layout=document.layout;
const value=index=>strings[index]??'';
const className=index=>{const a=nodes.attributes[index]??[];for(let i=0;i<a.length;i+=2)if(value(a[i])==='class')return value(a[i+1]);return ''};
const hasClass=(index,name)=>className(index).split(' ').includes(name);
const under=(index,test)=>{for(let n=index;n>=0;n=nodes.parentIndex[n])if(test(n))return true;return false};
const code=index=>under(index,n=>/^(CODE|PRE)$/.test(value(nodes.nodeName[n])));
const text=index=>{let out='';for(let n=0;n<nodes.nodeName.length;n++)if(value(nodes.nodeName[n])==='#text'&&under(n,i=>i===index))out+=value(nodes.nodeValue[n]);return out};
const viewport=snapshot.reaViewport;
const positionOf=new Map(layout.nodeIndex.map((n,p)=>[n,p]));
const visible=new Set(layout.nodeIndex.filter((_,position)=>{
if(!viewport)return true;
const [x,y,w,h]=layout.bounds[position]??[];
let left=viewport.page_x,top=viewport.page_y,right=left+viewport.width,bottom=top+viewport.height;
for(let n=nodes.parentIndex[layout.nodeIndex[position]];n>=0;n=nodes.parentIndex[n]){
const parentPosition=positionOf.get(n);if(parentPosition===undefined)continue;
const overflowIndex=snapshot.reaLayoutProperties.indexOf('overflow');
const overflow=value((layout.styles[parentPosition]??[])[overflowIndex]);
if(!/auto|scroll|hidden|clip/.test(overflow))continue;
const [px,py,pw,ph]=layout.bounds[parentPosition];
left=Math.max(left,px);top=Math.max(top,py);right=Math.min(right,px+pw);bottom=Math.min(bottom,py+ph);
}
return w>0&&h>0&&x>=left&&y>=top&&x+w<=right&&y+h<=bottom;
}));
const candidates=[];
for(let n=0;n<nodes.nodeName.length;n++)if(value(nodes.nodeName[n])==='#text'){
const parent=nodes.parentIndex[n];if(parent>=0&&visible.has(parent)&&!candidates.includes(parent))candidates.push(parent);
}
await send('DOM.getDocument',{depth:0});
for(const category of categories){
let found=false,reason='No short visible sample of this category in the current document';
const selected=category==='assistant-name'?layout.nodeIndex.filter(n=>visible.has(n)&&hasClass(n,'cr-agent__name')):candidates;
for(const index of selected){
if(under(index,n=>/^(INPUT|TEXTAREA|SELECT)$/.test(value(nodes.nodeName[n]))||(nodes.attributes[n]??[]).some((a,i,attrs)=>i%2===0&&value(a)==='contenteditable'&&value(attrs[i+1])!=='false')))continue;
const sample=text(index);if(!sample.trim()||sample.length>256)continue;
const body=under(index,n=>hasClass(n,'cr-agent__content'))&&under(index,n=>hasClass(n,'cr-text-block'))&&!under(index,n=>hasClass(n,'cr-code-like-box'));
// Plain leaf prose avoids treating a composite bold/inline subtree as one default-body font sample.
const proseLeaf=!nodes.parentIndex.some((parent,n)=>parent===index&&value(nodes.nodeName[n])!=='#text'&&value(nodes.nodeName[n])!=='BR');
if((category==='chinese-body'||category==='english-body')&&!proseLeaf)continue;
const weightProperty=snapshot.reaLayoutProperties.indexOf('font-weight');
if((category==='chinese-body'||category==='english-body')&&weightProperty>=0&&value((layout.styles[positionOf.get(index)]??[])[weightProperty])!=='400')continue;
if(category==='chinese-body'&&(!body||code(index)||!/[\u3400-\u9fff]/.test(sample)))continue;
if(category==='english-body'&&(!body||code(index)||/[\u3400-\u9fff]/.test(sample)||!/[A-Za-z]{3}/.test(sample)))continue;
if(category==='user-bubble'&&!under(index,n=>hasClass(n,'cr-self-bubble')))continue;
if(category==='code'&&!code(index))continue;
const backend=nodes.backendNodeId?.[index];if(!backend){reason='Backend node identity absent';continue;}
try{
const pushed=await send('DOM.pushNodesByBackendIdsToFrontend',{backendNodeIds:[backend]});const nodeId=pushed.nodeIds?.[0];if(!nodeId)continue;
const fonts=(await send('CSS.getPlatformFontsForNode',{nodeId})).fonts;
if(!Array.isArray(fonts)||!fonts.some(f=>f.glyphCount>0)){reason='No rendered glyph fonts reported';continue;}
const position=layout.nodeIndex.indexOf(index),styleIndexes=layout.styles[position]??[];
// The order is supplied by the visuals module, not inferred from font names.
const properties=snapshot.reaLayoutProperties;
const styles=Object.fromEntries(properties.map((p,i)=>[p,value(styleIndexes[i])]));
samples.push({category,text:sample,fontFamily:styles['font-family']??'',...(category==='code'?{codeContext:under(index,n=>value(nodes.nodeName[n])==='PRE')?'block':'inline'}:{}),platformFonts:fonts.map(f=>({familyName:f.familyName,postScriptName:f.postScriptName??'',isCustomFont:f.isCustomFont,glyphCount:f.glyphCount})),bounds:layout.bounds[position],styles});
found=true;break;
}catch{reason='Platform font query failed for the visible candidate';}
}
if(!found)missing.push({category,reason});
}
return {samples,missing};
}
+175
View File
@@ -0,0 +1,175 @@
import { digestCanonicalValue } from "../domain/canonicalDigest.js";
import { createWebTextArtifact } from "../domain/webContentArtifact.js";
import { authorizedElectronFile } from "./ElectronFileScope.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
const MAX_SHEETS = 1024;
const MAX_SHEET_BYTES = 8 * 1024 * 1024;
const MAX_TOTAL_BYTES = 64 * 1024 * 1024;
/** Retain only stylesheet lifecycle metadata from the selected target session. */
export const ingestElectronStyleEvent = (event, stylesMap) => {
if (event?.method === "CSS.styleSheetRemoved") {
const styleSheetId = stringValue(recordValue(event.params)?.styleSheetId);
if (styleSheetId !== undefined)
stylesMap.delete(styleSheetId);
return;
}
if (event?.method !== "CSS.styleSheetAdded")
return;
const header = recordValue(recordValue(event.params)?.header);
const styleSheetId = stringValue(header?.styleSheetId);
if (styleSheetId === undefined || styleSheetId === "")
return;
stylesMap.set(styleSheetId, {
styleSheetId,
frameId: stringValue(header?.frameId) ?? null,
sourceURL: stringValue(header?.sourceURL) ?? null,
origin: stringValue(header?.origin) ?? null,
isInline: header?.isInline === true,
isConstructed: header?.isConstructed === true,
});
};
/** Capture CSS text only for authorized file frames and explicit source opt-in. */
export const captureElectronStyles = async (input) => {
const styles = [...input.styles.values()];
const items = [];
let excluded = 0;
let sourceFailures = 0;
let truncated = false;
let includedBytes = 0;
let sourceRequests = 0;
const limitationNotes = new Set();
void input.completeness;
limitationNotes.add("Stylesheet inventory includes only CSS.styleSheetAdded events still present at the capture snapshot.");
for (const style of styles) {
const scoped = await authorizedStyle(style, input.frameIds);
if (scoped === undefined) {
excluded += 1;
continue;
}
if (input.request?.include_style_sources !== true)
continue;
const identity = {
style_sheet_id: style.styleSheetId,
frame_id: scoped.frameId,
file_path: scoped.filePath,
origin: style.origin,
is_inline: style.isInline,
is_constructed: style.isConstructed,
};
const item = {
style_sheet_key: `electron_stylesheet_${digestCanonicalValue(identity, "CDP capture")}`,
style_sheet_id: style.styleSheetId,
frame_id: scoped.frameId,
file_path: scoped.filePath,
origin: style.origin,
is_inline: style.isInline,
is_constructed: style.isConstructed,
source: { included: false, reason: "CSS source was not requested" },
};
if (sourceRequests >= MAX_SHEETS) {
sourceFailures += 1;
truncated = true;
item.source = { included: false, reason: "stylesheet source count limit reached" };
limitationNotes.add(`CSS source capture reached the ${String(MAX_SHEETS)} stylesheet limit.`);
items.push(item);
continue;
}
sourceRequests += 1;
let sourceText;
try {
const result = await input.connection.send(
"CSS.getStyleSheetText",
{ styleSheetId: style.styleSheetId },
input.sessionId,
input.signal
);
sourceText = stringValue(recordValue(result)?.text);
}
catch (cause) {
if (input.signal?.aborted === true)
throw cause;
sourceFailures += 1;
item.source = { included: false, reason: "CSS.getStyleSheetText failed" };
limitationNotes.add("CSS text could not be retrieved for one or more authorized stylesheets.");
items.push(item);
continue;
}
if (sourceText === undefined) {
sourceFailures += 1;
item.source = { included: false, reason: "CSS.getStyleSheetText returned invalid text" };
limitationNotes.add("CSS text was invalid or absent for one or more authorized stylesheets.");
items.push(item);
continue;
}
const bytes = Buffer.byteLength(sourceText, "utf8");
if (bytes > MAX_SHEET_BYTES) {
sourceFailures += 1;
truncated = true;
item.source = { included: false, reason: "stylesheet source exceeds the per-stylesheet byte limit" };
limitationNotes.add(`CSS source capture omitted text larger than ${String(MAX_SHEET_BYTES)} bytes per stylesheet.`);
items.push(item);
continue;
}
if (includedBytes + bytes > MAX_TOTAL_BYTES) {
sourceFailures += 1;
truncated = true;
item.source = { included: false, reason: "aggregate stylesheet source byte limit reached" };
limitationNotes.add(`CSS source capture omitted text after reaching the ${String(MAX_TOTAL_BYTES)} byte aggregate limit.`);
items.push(item);
continue;
}
includedBytes += bytes;
item.source = { included: true, artifact: createWebTextArtifact(sourceText, "text/css") };
items.push(item);
}
if (input.request?.include_style_sources !== true)
limitationNotes.add("Stylesheet source capture was not selected; no CSS.getStyleSheetText requests were sent.");
if (excluded > 0)
limitationNotes.add(`${String(excluded)} stylesheet(s) were excluded because their origin or frame was outside the authorized local scope.`);
if (sourceFailures > 0)
limitationNotes.add(`${String(sourceFailures)} stylesheet source(s) failed retrieval or exceeded a source budget.`);
input.limitations.push(...limitationNotes);
return {
total: styles.length,
items: items.sort((left, right) => compareUnicodeCodePoints(left.style_sheet_key, right.style_sheet_key)),
excluded,
source_failures: sourceFailures,
truncated,
limits: {
max_sheets: MAX_SHEETS,
max_sheet_bytes: MAX_SHEET_BYTES,
max_total_bytes: MAX_TOTAL_BYTES,
},
};
};
const authorizedStyle = async (style, frameIds) => {
if (style.frameId === null || !frameIds.has(style.frameId) || style.origin !== "regular")
return undefined;
const sourceURL = style.sourceURL ?? "";
if (sourceURL === "") {
if (!style.isInline && !style.isConstructed)
return undefined;
return { frameId: style.frameId, filePath: null };
}
const filePath = await authorizedElectronFile(sourceURL);
return filePath === undefined ? undefined : { frameId: style.frameId, filePath };
};
const recordValue = (value) => typeof value === "object" && value !== null && !Array.isArray(value)
? value
: undefined;
const stringValue = (value) => typeof value === "string" ? value : undefined;
@@ -0,0 +1,122 @@
import { createWebScreenshotArtifact, decodeCanonicalBase64 } from '../domain/webScreenshot.js';
import { readPngHeaderDimensions } from '../domain/pngHeader.js';
import { authorizedElectronFile } from './ElectronFileScope.js';
export async function runElectronVisualProbe(context,frames,send,makeCleanupSend) {
const probe=context.input.visual_probe;
if(!probe||probe.actions.length===0)return undefined;
const result={status:'completed',steps:[],frames:[],restoration:[],limitations:[
'Native debugging input only; no renderer JavaScript evaluation or model request is performed by this provider.',
'Frames are sequential PNG samples with measured timestamps, not a guaranteed video frame rate.',
'Viewport and media cleanup clear the overrides; any overrides that existed before this request were not observable.',
'Click restoration is suitable for caller-selected toggles only; other application effects are not rolled back.',
'Scroll restoration applies the inverse wheel input; clipping, scroll inertia and layout changes can prevent exact position restoration.',
'Keyboard focus and other keyboard effects are not restored. Pointer cleanup moves to the viewport origin, not the prior pointer position.'
,'Text replacement is limited to visible, enabled single-line text/search inputs; it does not submit the input or restore its prior value.'
]};
const start=performance.now(),undo=[];let viewport=false,media=false,hover=false,totalBytes=0;
const main=frames[0];
const stable=async()=>{
const tree=await send('Page.getFrameTree',{});const frame=tree.frameTree?.frame;
if(!frame||frame.id!==main.frame_id||await authorizedElectronFile(frame.url)!==main.file_path)throw new Error('Selected Electron frame changed');
};
const select=async(selector,index)=>{
const document=await send('DOM.getDocument',{depth:0});
const selected=await send('DOM.querySelectorAll',{nodeId:document.root.nodeId,selector});
if(index===undefined&&selected.nodeIds.length!==1)throw new Error('Selector must match exactly one node or supply an explicit index');
const nodeId=selected.nodeIds[index??0];if(!nodeId)throw new Error('Selected node is absent');
return nodeId;
};
const point=async(nodeId)=>{
const box=(await send('DOM.getBoxModel',{nodeId})).model?.content;
if(!box||box.length!==8)throw new Error('Selected node has no visible box');
const x=(box[0]+box[2]+box[4]+box[6])/4,y=(box[1]+box[3]+box[5]+box[7])/4;
const viewport=(await send('Page.getLayoutMetrics',{})).cssLayoutViewport;
if(x<0||y<0||x>=viewport.clientWidth||y>=viewport.clientHeight)throw new Error('Selected node is outside the visible viewport');
const hit=await send('DOM.getNodeForLocation',{x:Math.floor(x),y:Math.floor(y),includeUserAgentShadowDOM:false});
const target=(await send('DOM.describeNode',{nodeId,depth:-1,pierce:false})).node;
let visited=0;const contains=node=>{
if(!node||++visited>5000)return false;
return node.backendNodeId===hit.backendNodeId||(node.children??[]).some(contains);
};
if(!hit.backendNodeId||!contains(target))throw new Error('Selected node center is clipped or covered by another element');
return {x,y};
};
const click=async(action)=>{
const at=await point(await select(action.selector,action.index));
await send('Input.dispatchMouseEvent',{type:'mouseMoved',...at});
await send('Input.dispatchMouseEvent',{type:'mousePressed',...at,button:'left',clickCount:1});
await send('Input.dispatchMouseEvent',{type:'mouseReleased',...at,button:'left',clickCount:1});
hover=true;
};
const scroll=async(action)=>{
const at=await point(await select(action.selector,action.index));
await send('Input.dispatchMouseEvent',{type:'mouseWheel',...at,deltaX:0,deltaY:action.delta_y});
};
const sample=async(step_id)=>{
await stable();
const captured=await send('Page.captureScreenshot',{format:'png',fromSurface:true,captureBeyondViewport:false});
const bytes=decodeCanonicalBase64(captured.data);
if(!bytes||!readPngHeaderDimensions(bytes))throw new Error('Invalid scenario PNG');
totalBytes+=bytes.length;if(totalBytes>32*1024*1024)throw new Error('Scenario PNG budget exceeded');
const regions=[];
for(const selector of probe.track_selectors){
let bounds=null;
try{const b=(await send('DOM.getBoxModel',{nodeId:await select(selector)})).model.border;const xs=[b[0],b[2],b[4],b[6]],ys=[b[1],b[3],b[5],b[7]];bounds=[Math.min(...xs),Math.min(...ys),Math.max(...xs)-Math.min(...xs),Math.max(...ys)-Math.min(...ys)];}catch{}
regions.push({selector,bounds});
}
result.frames.push({step_id,at_ms:performance.now()-start,artifact:createWebScreenshotArtifact(bytes),regions});
};
const pause=milliseconds=>new Promise((resolve,reject)=>{const timer=setTimeout(done,milliseconds);function done(){context.signal?.removeEventListener('abort',abort);resolve()}function abort(){clearTimeout(timer);context.signal?.removeEventListener('abort',abort);reject(new Error('Visual probe cancelled'))}context.signal?.addEventListener('abort',abort,{once:true});if(context.signal?.aborted)abort()});
try{
for(const action of probe.actions){
await stable();
try{
if(action.kind==='click'){await click(action);if(action.restore)undo.push({kind:'click',action});}
else if(action.kind==='hover'){await send('Input.dispatchMouseEvent',{type:'mouseMoved',...await point(await select(action.selector,action.index))});hover=true;}
else if(action.kind==='scroll'){await scroll(action);if(action.restore)undo.push({kind:'scroll',action:{...action,delta_y:-action.delta_y}});}
else if(action.kind==='viewport'){await send('Emulation.setDeviceMetricsOverride',{width:action.width,height:action.height,deviceScaleFactor:0,mobile:false});viewport=true;}
else if(action.kind==='media'){await send('Emulation.setEmulatedMedia',{features:[{name:'prefers-reduced-motion',value:action.reduced_motion}]});media=true;}
else if(action.kind==='wait')await pause(action.duration_ms);
else if(action.kind==='text'){
const nodeId=await select(action.selector,action.index);
await point(nodeId);
const node=(await send('DOM.describeNode',{nodeId})).node,raw=node.attributes??[];
const attrs=Object.fromEntries(Array.from({length:raw.length/2},(_,index)=>[raw[index*2].toLowerCase(),raw[index*2+1]]));
if(node.nodeName!=='INPUT'||!['text','search'].includes((attrs.type??'text').toLowerCase())||'readonly'in attrs||'disabled'in attrs)throw new Error('Text replacement requires an enabled text/search INPUT');
await send('DOM.focus',{nodeId});
const selectAll={key:'a',code:'KeyA',windowsVirtualKeyCode:65,nativeVirtualKeyCode:65,modifiers:2};
await send('Input.dispatchKeyEvent',{type:'keyDown',...selectAll});
await send('Input.dispatchKeyEvent',{type:'keyUp',...selectAll});
const remove={key:'Backspace',code:'Backspace',windowsVirtualKeyCode:8,nativeVirtualKeyCode:8,modifiers:0};
await send('Input.dispatchKeyEvent',{type:'keyDown',...remove});
await send('Input.dispatchKeyEvent',{type:'keyUp',...remove});
if(action.text)await send('Input.insertText',{text:action.text});
}
else if(action.kind==='key'){
if(action.selector){const nodeId=await select(action.selector,action.index);const node=(await send('DOM.describeNode',{nodeId})).node;const attrs=node.attributes??[];
if(['Enter',' '].includes(action.key)&&(/^(INPUT|TEXTAREA)$/i.test(node.nodeName)||attrs.includes('contenteditable')))throw new Error('Activation key on editable content is excluded');
await send('DOM.focus',{nodeId});
}else if(['Enter',' '].includes(action.key))throw new Error('Activation keys require an explicit noneditable target');
const codes={Escape:27,Enter:13,Tab:9,ArrowDown:40,ArrowUp:38,ArrowLeft:37,ArrowRight:39,Home:36,End:35,' ':32,'+':187,'-':189,'0':48};
const packet={key:action.key,windowsVirtualKeyCode:codes[action.key],nativeVirtualKeyCode:codes[action.key],modifiers:action.modifiers};
await send('Input.dispatchKeyEvent',{type:'keyDown',...packet,...(action.key==='Enter'?{text:'\r'}:action.key===' '?{text:' '}:{})});
await send('Input.dispatchKeyEvent',{type:'keyUp',...packet});
}
const actionCompleted=performance.now()-start;
for(let n=0;n<action.frames;n++){if(n)await pause(action.interval_ms);await sample(action.step_id);}
result.steps.push({step_id:action.step_id,kind:action.kind,status:'completed',error:null,action_completed_at_ms:actionCompleted});
}catch(error){result.status='failed';result.steps.push({step_id:action.step_id,kind:action.kind,status:'failed',error:String(error.message??error)});break;}
}
}finally{
if(makeCleanupSend)send=makeCleanupSend();
if(viewport){try{await send('Emulation.clearDeviceMetricsOverride',{});result.restoration.push({kind:'viewport',status:'completed',error:null});}catch(error){result.status='failed';result.restoration.push({kind:'viewport',status:'failed',error:String(error.message??error)});}}
for(const restoration of undo.reverse()){
try{await stable();if(restoration.kind==='click')await click(restoration.action);else await scroll(restoration.action);result.restoration.push({kind:restoration.kind,status:'completed',error:null});}
catch(error){result.status='failed';result.restoration.push({kind:restoration.kind,status:'failed',error:String(error.message??error)});}
}
if(media){try{await send('Emulation.setEmulatedMedia',{features:[]});result.restoration.push({kind:'media',status:'completed',error:null});}catch(error){result.status='failed';result.restoration.push({kind:'media',status:'failed',error:String(error.message??error)});}}
if(hover){try{await send('Input.dispatchMouseEvent',{type:'mouseMoved',x:0,y:0});result.restoration.push({kind:'neutral-pointer',status:'completed',error:null});}catch(error){result.status='failed';result.restoration.push({kind:'neutral-pointer',status:'failed',error:String(error.message??error)});}}
}
return result;
}
+119
View File
@@ -0,0 +1,119 @@
import { createWebScreenshotArtifact, decodeCanonicalBase64 } from '../domain/webScreenshot.js';
import { readPngHeaderDimensions } from '../domain/pngHeader.js';
import { BrowserObservationError } from '../domain/browserObservationError.js';
import { authorizedElectronFile } from './ElectronFileScope.js';
import { runElectronVisualProbe } from './CdpElectronVisualProbe.js';
import { captureElectronPlatformFonts } from './CdpElectronPlatformFonts.js';
import { captureElectronMediaAssets } from './CdpElectronMediaAssets.js';
export const ELECTRON_LAYOUT_PROPERTIES = ['display', 'position', 'font-family', 'font-size', 'font-weight', 'line-height', 'letter-spacing', 'color', 'background-color', 'border-radius', 'border-top-width', 'border-top-color', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left', 'gap', 'align-items', 'justify-content', 'box-shadow', 'overflow', 'max-width', 'min-height','transition-property','transition-duration','transition-timing-function','opacity','visibility','transform','text-rendering','-webkit-font-smoothing','font-kerning','font-synthesis','font-optical-sizing','font-feature-settings','text-shadow','zoom','-webkit-text-size-adjust'];
// Optional bounded message DOM text for reproducing identical fixture content.
// Attribute values outside the style identity allowlist and all editable/input subtrees are omitted.
export function captureElectronMessageContent(snapshot,frames) {
const strings=snapshot.strings??[],value=i=>strings[i]??'';
const document=snapshot.documents?.find(d=>value(d.frameId)===frames[0].frame_id);
if(!document)return {nodes:[],text_characters:0,truncated:false};
const raw=document.nodes,selected=[],roots=new Set(),excluded=new Set();let characters=0,truncated=false;
const siblingCounts=new Map(),ordinals=new Map();
for(let n=0;n<(raw.nodeName?.length??0);n++){const tag=value(raw.nodeName[n]);if(tag.startsWith('#'))continue;const key=raw.parentIndex[n]+'|'+tag;const count=(siblingCounts.get(key)??0)+1;siblingCounts.set(key,count);ordinals.set(n,count)}
const classOf=n=>{const attrs=raw.attributes?.[n]??[];for(let a=0;a<attrs.length;a+=2)if(value(attrs[a])==='class')return value(attrs[a+1]).split(/\s+/);return []};
const messageLists=raw.nodeName.map((_,n)=>n).filter(n=>classOf(n).includes('cr-message-list'));
const selectorFor=index=>{const parts=[];for(let n=index;n>=0;n=raw.parentIndex[n]){if(messageLists.length===1&&n===messageLists[0]){parts.unshift('.cr-message-list');break}const tag=value(raw.nodeName[n]);if(!tag.startsWith('#'))parts.unshift(tag.toLowerCase()+':nth-of-type('+ordinals.get(n)+')')}return parts.join(' > ')};
for(let n=0;n<(raw.nodeName?.length??0);n++){
const attrs=raw.attributes?.[n]??[],attributes={};
for(let a=0;a<attrs.length;a+=2)if(['class','id','role','lang','data-language'].includes(value(attrs[a])))attributes[value(attrs[a])]=value(attrs[a+1]);
const names=(attributes.class??'').split(/\s+/),tag=value(raw.nodeName[n]),parent=raw.parentIndex[n];
if(names.includes('cr-self-message')||names.includes('cr-agent'))roots.add(n);
if(/^(INPUT|TEXTAREA|SELECT|SCRIPT|STYLE)$/.test(tag)||excluded.has(parent)||attrs.some((a,i)=>i%2===0&&value(a)==='contenteditable'&&value(attrs[i+1])!=='false'))excluded.add(n);
let inside=false;for(let p=n;p>=0;p=raw.parentIndex[p])if(roots.has(p)){inside=true;break}
if(!inside||excluded.has(n))continue;
if(selected.length>=5000){truncated=true;break}
const text=tag==='#text'?value(raw.nodeValue[n]):'';
if(characters+text.length>65536){truncated=true;break}
characters+=text.length;
selected.push({node_index:n,parent_index:parent,node_name:tag,attributes,...(tag==='#text'?{text}:{}),...(names.some(name=>['cr-agent','cr-self-message','cr-collapse'].includes(name))?{selector:selectorFor(n)}:{})});
}
return {nodes:selected,text_characters:characters,truncated};
}
// Called only after inspectCdpElectronPage has authorized the selected file page.
export async function captureElectronVisuals(context, frames) {
if (!context.input.include_visuals) return undefined;
const signal = context.signal ? AbortSignal.any([context.signal, AbortSignal.timeout(30000)]) : AbortSignal.timeout(30000);
const send = async (method, params) => {
if (process.env.REA_VISUAL_TRACE === '1') process.stderr.write(`[REA visuals] ${method}\n`);
return context.connection.send(method, params, context.sessionId, signal);
};
const metrics = await send('Page.getLayoutMetrics', {});
let environmentGpu;
if (context.input.visual_probe?.include_fonts) {
try {
// Browser metadata only; no renderer evaluation or GPU benchmark.
const {gpu} = await context.connection.send('SystemInfo.getInfo', {}, undefined, signal);
const allowed = ['inProcessGpu','skiaBackendType','glImplementationParts','subpixelFontRendering','sandboxed','passthroughCmdDecoder','targetCpuBits','driverVendor','driverVersion','displayType'];
const aux = Object.fromEntries(allowed.filter(key => ['string','number','boolean'].includes(typeof gpu.auxAttributes?.[key]) || gpu.auxAttributes?.[key] === null).map(key => [key,gpu.auxAttributes[key]]));
environmentGpu = { status:'observed', devices:(gpu.devices??[]).slice(0,8).map(device=>({vendor_id:device.vendorId,device_id:device.deviceId,...(typeof device.deviceString==='string'?{device_string:device.deviceString.slice(0,256)}:{}),...(typeof device.active==='boolean'?{active:device.active}:{})})),aux_attributes:aux,feature_status:Object.fromEntries(Object.entries(gpu.featureStatus??{}).filter(([key,value])=>key.length<100&&typeof value==='string'&&value.length<100)) };
} catch { environmentGpu = { status:'unavailable' }; }
}
if (context.input.visual_probe) { await send('DOM.enable', {}); await send('CSS.enable', {}); }
let environmentMedia;
if(context.input.visual_probe){
try{const media=await send('CSS.getMediaQueries',{});const reduced=(media.medias??[]).find(m=>/^\(\s*prefers-reduced-motion\s*:\s*reduce\s*\)$/i.test(m.text??''));environmentMedia={reduced_motion:reduced?.mediaList?.[0]?.active??null};}
catch{environmentMedia={reduced_motion:null}}
}
const snapshot = await send('DOMSnapshot.captureSnapshot', { computedStyles: ELECTRON_LAYOUT_PROPERTIES, includePaintOrder: false, includeDOMRects: true });
const strings = snapshot.strings ?? [];
const str = index => typeof strings[index] === 'string' ? strings[index] : '';
const frameIds = new Set(frames.map(frame => frame.frame_id));
const documents = [];
for (const document of snapshot.documents ?? []) {
if (!frameIds.has(str(document.frameId)) || await authorizedElectronFile(str(document.documentURL)) === undefined) continue;
const nodes = document.nodes ?? {};
const layout = document.layout ?? {};
const items = [];
const svgNodes = [];
const svgTags = new Set(['svg', 'path', 'g', 'circle', 'rect', 'line', 'polygon', 'polyline', 'ellipse', 'defs', 'clippath', 'mask']);
const svgAttributes = new Set(['class', 'id', 'viewBox', 'width', 'height', 'd', 'points', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'r', 'rx', 'ry', 'fill', 'fill-rule', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'transform', 'clip-path', 'opacity']);
for (let n = 0; n < (nodes.nodeName?.length ?? 0); n++) {
const tag = str(nodes.nodeName[n]);
if (!svgTags.has(tag.toLowerCase())) continue;
const attributes = {};
const attrs = nodes.attributes?.[n] ?? [];
for (let a = 0; a < attrs.length; a += 2) if (svgAttributes.has(str(attrs[a]))) attributes[str(attrs[a])] = str(attrs[a + 1]);
svgNodes.push({ node_index: n, parent_index: nodes.parentIndex?.[n] ?? -1, node_name: tag, attributes });
}
for (let i = 0; i < (layout.nodeIndex?.length ?? 0); i++) {
const node = layout.nodeIndex[i];
const attrs = nodes.attributes?.[node] ?? [];
const attributes = {};
for (let a = 0; a < attrs.length; a += 2) {
const key = str(attrs[a]);
// Retain style identity only; never input values, URL attributes or hidden payloads.
if (['class', 'id', 'role'].includes(key)) attributes[key] = str(attrs[a + 1]);
}
const styles = {};
for (let p = 0; p < ELECTRON_LAYOUT_PROPERTIES.length; p++) styles[ELECTRON_LAYOUT_PROPERTIES[p]] = str(layout.styles?.[i]?.[p]);
const bounds = layout.bounds?.[i];
if (!Array.isArray(bounds) || bounds.length !== 4 || !bounds.every(Number.isFinite)) continue;
items.push({ node_index: node, parent_index: nodes.parentIndex?.[node] ?? -1, node_name: str(nodes.nodeName?.[node]), attributes, bounds, styles });
}
documents.push({ frame_id: str(document.frameId), file_path: await authorizedElectronFile(str(document.documentURL)), scroll_x: document.scrollOffsetX ?? 0, scroll_y: document.scrollOffsetY ?? 0, nodes: items, svg_nodes: svgNodes });
}
const captured = await send('Page.captureScreenshot', { format: 'png', fromSurface: true, captureBeyondViewport: false });
if (typeof captured.data !== 'string' || captured.data.length > 24 * 1024 * 1024) throw new BrowserObservationError('inspect_electron_page', 'protocol_error');
const bytes = decodeCanonicalBase64(captured.data);
const viewport = bytes && readPngHeaderDimensions(bytes);
if (!viewport) throw new BrowserObservationError('inspect_electron_page', 'protocol_error');
const css = metrics.cssLayoutViewport ?? metrics.layoutViewport;
if (!css || !Number.isFinite(css.clientWidth) || !Number.isFinite(css.clientHeight)) throw new BrowserObservationError('inspect_electron_page', 'protocol_error');
const fonts = context.input.visual_probe?.include_fonts ? await captureElectronPlatformFonts({...snapshot,reaLayoutProperties:ELECTRON_LAYOUT_PROPERTIES,reaViewport:{width:css.clientWidth,height:css.clientHeight,page_x:css.pageX??0,page_y:css.pageY??0}},frames,send) : undefined;
const mediaAssets = await captureElectronMediaAssets(snapshot,frames,context.input.visual_probe?.media_selectors,send);
// Baseline DOM/fonts/media acquisition must not consume the scenario's input budget.
// Both stages remain bounded and still inherit the caller's cancellation.
const probeSignal = context.signal ? AbortSignal.any([context.signal, AbortSignal.timeout(30000)]) : AbortSignal.timeout(30000);
const probeSend = (method,params)=>context.connection.send(method,params,context.sessionId,probeSignal);
const probe = await runElectronVisualProbe({...context,signal:probeSignal},frames,probeSend,()=>{const cleanupSignal=AbortSignal.timeout(5000);return (method,params)=>context.connection.send(method,params,context.sessionId,cleanupSignal)});
const environment = { ...(environmentMedia?{environment_media:environmentMedia}:{}), ...(environmentGpu?{environment_gpu:environmentGpu}:{}), ...(mediaAssets?{media_assets:mediaAssets}:{}) };
return { captured_at: new Date().toISOString(), viewport, css_viewport: { width: css.clientWidth, height: css.clientHeight, page_x: css.pageX ?? 0, page_y: css.pageY ?? 0 }, ...environment,artifact: createWebScreenshotArtifact(bytes), layout: { properties: ELECTRON_LAYOUT_PROPERTIES, documents }, ...(fonts ? {font_samples:fonts.samples,font_missing:fonts.missing} : {}), ...(context.input.visual_probe?.include_message_content?{message_content:captureElectronMessageContent(snapshot,frames)}:{}), ...(probe ? {probe} : {}), limitations: ['Baseline captures the visible viewport; optional native visual_probe actions are recorded separately. No renderer JavaScript is evaluated.', 'DOM layout and screenshot are sequential observations, not an atomic rendering snapshot.', 'Layout omits text and input values; include_fonts retains bounded visible samples; optional include_message_content retains bounded message DOM text including collapsed message descendants, excluding editable/input subtrees.'] };
}
@@ -0,0 +1,49 @@
// Native metadata diagnostic owned by the local REA provider. No target injection or termination.
import { spawn } from 'node:child_process';
import os from 'node:os';
import path from 'node:path';
import fs from 'node:fs/promises';
import { createEvidence } from '../domain/evidence.js';
export async function inspectDesktopElectronProcesses(processName) {
if (process.platform !== 'win32') throw new Error('Windows process metadata provider requires Windows');
if (!/^[A-Za-z0-9_.-]{1,80}\.exe$/i.test(processName)) throw new Error('Select one literal executable basename');
const script = `$targets=Get-CimInstance Win32_Process -Filter "Name='${processName}'"; $rows=foreach($p in $targets){$debugPort=$null;$processType='main';if($p.CommandLine -match '--remote-debugging-port[= ]+(\\d+)'){$debugPort=[int]$Matches[1]};if($p.CommandLine -match '--type[= ]+([^\\s]+)'){$processType=$Matches[1]};[pscustomobject]@{pid=$p.ProcessId;parentPid=$p.ParentProcessId;executable=$p.ExecutablePath;type=$processType;debugPort=$debugPort}};$listeners=Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue|Where-Object{$_.OwningProcess -in $targets.ProcessId}|Select-Object LocalAddress,LocalPort,OwningProcess;[pscustomobject]@{processes=@($rows);listeners=@($listeners)}|ConvertTo-Json -Depth 5 -Compress`;
const result = await new Promise((resolve,reject)=>{
const child=spawn('C:/Windows/System32/WindowsPowerShell/v1.0/powershell.exe',['-NoLogo','-NoProfile','-NonInteractive','-EncodedCommand',Buffer.from(script,'utf16le').toString('base64')],{windowsHide:true,stdio:['ignore','pipe','pipe']});
const chunks=[];let size=0;const timer=setTimeout(()=>{child.kill();reject(new Error('Windows metadata query timed out'))},15000);
child.stdout.on('data',chunk=>{size+=chunk.length;if(size>1024*1024){child.kill();reject(new Error('Windows metadata budget exceeded'))}else chunks.push(chunk)});
child.stderr.on('data',()=>{});
child.once('error',error=>{clearTimeout(timer);reject(error)});
child.once('close',code=>{clearTimeout(timer);if(code!==0)return reject(new Error('Windows metadata query failed'));try{resolve(JSON.parse(Buffer.concat(chunks).toString('utf8').replace(/^\uFEFF/,'')))}catch(error){reject(error)}});
});
return createEvidence(undefined,{id:'rea-windows-electron-processes',name:'Local REA Windows Electron metadata provider',version:'1+state-local.1'},{
predicateType:'rea.desktop-electron-processes',operation:'inspect_desktop_electron_processes',parameters:{process_name:processName},result,
confidence:'observed',authority:'external-service',environment:{id:`${os.platform()}-${os.release()}`,platform:os.platform(),architecture:os.arch(),isolation:'none'},
limitations:['Native process metadata and listening sockets only; no target injection, file extraction, restart or termination.','Command lines are reduced to process type and declared remote-debugging port; other argument values are omitted.']
});
}
// Explicit lifecycle operation. The caller must already have authorization to interrupt WorkBuddy.
export async function restartDesktopElectronDebug(executable,port,authorized) {
if(process.platform!=='win32'||authorized!==true)throw new Error('An authorized Windows restart is required');
if(!Number.isInteger(port)||port<1024||port>65535)throw new Error('Select one debugging port from 1024 to 65535');
const expected=path.join(process.env.LOCALAPPDATA,'Programs','WorkBuddy','WorkBuddy.exe');
const actual=await fs.realpath(executable),allowed=await fs.realpath(expected);
if(actual.toLowerCase()!==allowed.toLowerCase())throw new Error('Restart is limited to the installed WorkBuddy executable');
const quoted=actual.replaceAll("'","''");
const script=`$ErrorActionPreference='Stop';$exe='${quoted}';$before=@(Get-CimInstance Win32_Process -Filter "Name='WorkBuddy.exe'"|Where-Object{$_.ExecutablePath -eq $exe});$close=@();foreach($p in $before){$native=Get-Process -Id $p.ProcessId -ErrorAction SilentlyContinue;if($native -and $native.MainWindowHandle -ne 0){$close+= [pscustomobject]@{pid=$p.ProcessId;requested=$native.CloseMainWindow()}}};$deadline=[datetime]::UtcNow.AddSeconds(8);do{$remaining=@(Get-CimInstance Win32_Process -Filter "Name='WorkBuddy.exe'"|Where-Object{$_.ExecutablePath -eq $exe});if($remaining.Count -eq 0){break};Start-Sleep -Milliseconds 200}while([datetime]::UtcNow -lt $deadline);$forced=@();foreach($p in $before){$current=Get-CimInstance Win32_Process -Filter ("ProcessId="+$p.ProcessId);if($current -and $current.ExecutablePath -eq $exe -and $current.CreationDate -eq $p.CreationDate){Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue;$forced+=$p.ProcessId}};$deadline=[datetime]::UtcNow.AddSeconds(4);do{$remaining=@(Get-CimInstance Win32_Process -Filter "Name='WorkBuddy.exe'"|Where-Object{$_.ExecutablePath -eq $exe});if($remaining.Count -eq 0){break};Start-Sleep -Milliseconds 100}while([datetime]::UtcNow -lt $deadline);if($remaining.Count -ne 0){throw 'Selected WorkBuddy processes did not exit; no replacement was launched'};[pscustomobject]@{beforePids=@($before|ForEach-Object{$_.ProcessId});closeRequests=@($close);forcedPids=@($forced);remaining=0}|ConvertTo-Json -Depth 4 -Compress`;
const stopped=await new Promise((resolve,reject)=>{
const child=spawn('C:/Windows/System32/WindowsPowerShell/v1.0/powershell.exe',['-NoLogo','-NoProfile','-NonInteractive','-EncodedCommand',Buffer.from(script,'utf16le').toString('base64')],{windowsHide:true,stdio:['ignore','pipe','pipe']});
let output='',errors='';const timer=setTimeout(()=>{child.kill();reject(new Error('Authorized restart shutdown timed out'))},20000);
child.stdout.on('data',b=>{output+=b;if(output.length>65536){child.kill();reject(new Error('Restart result budget exceeded'))}});child.stderr.on('data',b=>errors+=b);
child.once('error',e=>{clearTimeout(timer);reject(e)});child.once('close',code=>{clearTimeout(timer);if(code!==0)return reject(new Error('Restart shutdown failed: '+errors.slice(0,1000)));try{resolve(JSON.parse(output.replace(/^\uFEFF/,'')))}catch(e){reject(e)}});
});
const env={...process.env};for(const key of Object.keys(env))if(key.toUpperCase()==='ELECTRON_RUN_AS_NODE')delete env[key];
const args=[`--remote-debugging-port=${port}`,'--remote-debugging-address=127.0.0.1'];
const launched=spawn(actual,args,{detached:true,windowsHide:true,stdio:'ignore',env});
await new Promise((resolve,reject)=>{launched.once('spawn',resolve);launched.once('error',reject)});launched.unref();
return createEvidence(undefined,{id:'rea-windows-electron-processes',name:'Local REA Windows Electron lifecycle provider',version:'1+state-local.2'},{
predicateType:'rea.desktop-electron-restart',operation:'restart_desktop_electron_debug',parameters:{executable:actual,port,restart_authorized:true},result:{...stopped,launchedPid:launched.pid,arguments:args},confidence:'observed',authority:'controlled-replay',environment:{id:`${os.platform()}-${os.release()}`,platform:os.platform(),architecture:os.arch(),isolation:'none'},
limitations:['Caller-authorized restart can interrupt in-flight tasks.','Graceful close is attempted first; surviving launch identities belonging to the exact selected executable are terminated.','Launch evidence confirms process creation only; inspect the debugging endpoint separately.','No original file contents or application data are extracted by this lifecycle operation.']
});
}
@@ -0,0 +1,49 @@
import { z } from 'zod';
import { webScreenshotArtifactSchema } from '../webScreenshot.js';
const sampling = { step_id:z.string().min(1).max(80), frames:z.number().int().min(0).max(24).default(1), interval_ms:z.number().int().min(16).max(500).default(50) };
const target = {selector:z.string().min(1).max(500),index:z.number().int().min(0).max(100).optional()};
export const electronVisualProbeInputSchema = z.strictObject({
include_fonts:z.boolean().default(false),
include_message_content:z.boolean().default(false),
media_selectors:z.array(z.string().trim().min(1).max(500)).max(4).default([]),
track_selectors:z.array(z.string().min(1).max(500)).max(12).default([]),
actions:z.array(z.discriminatedUnion('kind',[
z.strictObject({...sampling,...target,kind:z.literal('hover')}),
z.strictObject({...sampling,...target,kind:z.literal('click'),restore:z.boolean().default(false)}),
z.strictObject({...sampling,...target,kind:z.literal('text'),text:z.string().max(2048).refine(value=>!/[\u0000-\u001f\u007f]/u.test(value),'Single-line text only')}),
z.strictObject({...sampling,...target,kind:z.literal('scroll'),delta_y:z.number().min(-3000).max(3000),restore:z.boolean().default(false)}),
z.strictObject({...sampling,kind:z.literal('key'),selector:z.string().min(1).max(500).optional(),index:z.number().int().min(0).max(100).optional(),key:z.enum(['Escape','Enter','Tab','ArrowDown','ArrowUp','ArrowLeft','ArrowRight','Home','End',' ','+','-','0']),modifiers:z.number().int().min(0).max(15).default(0)}),
z.strictObject({...sampling,kind:z.literal('wait'),duration_ms:z.number().int().min(0).max(2000)}),
z.strictObject({...sampling,kind:z.literal('viewport'),width:z.number().int().min(160).max(3840),height:z.number().int().min(160).max(2160)})
,z.strictObject({...sampling,kind:z.literal('media'),reduced_motion:z.enum(['reduce','no-preference'])})
])).max(12).default([])
}).superRefine((value,ctx)=>{
if(value.actions.reduce((n,a)=>n+a.frames,0)>64)ctx.addIssue({code:'custom',message:'At most 64 scenario frames'});
if(value.actions.reduce((n,a)=>n+a.frames*a.interval_ms+(a.duration_ms??0),0)>15000)ctx.addIssue({code:'custom',message:'Declared sampling and waits must fit 15 seconds'});
if(new Set(value.actions.map(a=>a.step_id)).size!==value.actions.length)ctx.addIssue({code:'custom',message:'Step IDs must be unique'});
if(new Set(value.media_selectors).size!==value.media_selectors.length)ctx.addIssue({code:'custom',message:'Media selectors must be unique'});
});
const mediaIdentity = {
selector:z.string(),frame_id:z.string(),node_index:z.number().int().optional(),backend_node_id:z.number().int().optional(),
selected_node:z.object({node_name:z.literal('IMG'),attributes:z.record(z.string(),z.string()),current_source_url:z.string().nullable()}).optional(),
visibility:z.object({status:z.enum(['observed','covered','unknown']),reason:z.string().optional()}).optional(),
visibility_samples:z.array(z.object({x:z.number(),y:z.number(),hit_backend_node_id:z.number().int().optional(),hit_node_index:z.number().int().optional(),hit_node_name:z.string().optional(),hit_class:z.string().optional()})).max(9).optional(),
source:z.object({url:z.string().nullable(),url_sha256:z.string().regex(/^[a-f0-9]{64}$/),selection:z.enum(['current_source_url','src']),mime_type:z.string()}).optional()
};
export const electronMediaAssetsSchema = z.object({
items:z.array(z.discriminatedUnion('status',[
z.object({...mediaIdentity,status:z.literal('exported'),artifact:z.object({mime_type:z.enum(['image/png','image/jpeg','image/gif','image/webp']),bytes:z.number().int().positive().max(2097152),sha256:z.string().regex(/^[a-f0-9]{64}$/),data_base64:z.string().max(2796204)})}),
z.object({...mediaIdentity,status:z.literal('unknown'),reason:z.string()})
])).max(4),
total_bytes:z.number().int().min(0).max(8388608),
limits:z.object({max_assets:z.literal(4),max_asset_bytes:z.literal(2097152),max_total_bytes:z.literal(8388608)}),
limitations:z.array(z.string())
});
export const platformFontSampleSchema = z.object({category:z.string(),text:z.string().max(256),fontFamily:z.string(),codeContext:z.enum(['inline','block']).optional(),platformFonts:z.array(z.object({familyName:z.string(),postScriptName:z.string(),isCustomFont:z.boolean(),glyphCount:z.number().int().min(0)}))});
export const visualProbeOutputSchema = z.object({
status:z.enum(['completed','failed']),
steps:z.array(z.object({step_id:z.string(),kind:z.string(),status:z.enum(['completed','failed']),error:z.string().nullable(),action_completed_at_ms:z.number().optional()})),
frames:z.array(z.object({step_id:z.string(),at_ms:z.number(),artifact:webScreenshotArtifactSchema,regions:z.array(z.object({selector:z.string(),bounds:z.tuple([z.number(),z.number(),z.number(),z.number()]).nullable()}))})),
restoration:z.array(z.object({kind:z.string(),status:z.enum(['completed','failed']),error:z.string().nullable()})),
limitations:z.array(z.string())
});
+225
View File
@@ -0,0 +1,225 @@
import { createHash } from 'node:crypto'
import path from 'node:path'
import { pathToFileURL } from 'node:url'
const MAX_SELECTED_FILES = 16
const MAX_FILE_BYTES = 8 * 1024 * 1024
const MAX_TOTAL_BYTES = 16 * 1024 * 1024
const SHA256_PATTERN = /^[a-f0-9]{64}$/u
function assertCanonicalSelectedPath(value, binary = false) {
if (typeof value !== 'string' || value.length === 0) {
throw new TypeError('Selected paths must be non-empty relative strings')
}
if (
value.startsWith('/') ||
value.endsWith('/') ||
value.includes('\\') ||
value.includes('\0') ||
/^[a-zA-Z]:/u.test(value) ||
value.split('/').some((part) => part === '' || part === '.' || part === '..') ||
path.posix.normalize(value) !== value
) {
throw new TypeError(`Selected path is not canonical: ${value}`)
}
if (binary ? !/\.(?:png|jpe?g|gif|webp|svg|woff2?|ttf)$/u.test(value) : !/\.(?:js|css|json|md|txt|yaml|yml|py)$/u.test(value)) {
if (binary) throw new TypeError(`Selected binary path must name a supported image or font: ${value}`)
throw new TypeError(`Selected path must name a supported UTF-8 text file (.js/.css/.json/.md/.txt/.yaml/.yml/.py): ${value}`)
}
}
function validateSelection(expectedRootSha256, selectedPaths, binary = false) {
if (typeof expectedRootSha256 !== 'string' || !SHA256_PATTERN.test(expectedRootSha256)) {
throw new TypeError('Expected ASAR root SHA-256 must be 64 lowercase hexadecimal characters')
}
if (!Array.isArray(selectedPaths) || selectedPaths.length === 0 || selectedPaths.length > MAX_SELECTED_FILES) {
throw new RangeError(`Select between 1 and ${MAX_SELECTED_FILES} exact files`)
}
const paths = [...selectedPaths]
const seen = new Set()
for (const selectedPath of paths) {
assertCanonicalSelectedPath(selectedPath, binary)
if (seen.has(selectedPath)) throw new TypeError(`Duplicate selected path: ${selectedPath}`)
seen.add(selectedPath)
}
return paths
}
async function loadReaRuntime() {
const runtimeRoot = path.resolve(process.env.REA_RUNTIME_ROOT || '.tmp/rea-css-runtime')
const runtimeUrl = (relativePath) => pathToFileURL(path.join(runtimeRoot, relativePath)).href
const [readerModule, evidenceModule] = await Promise.all([
import(runtimeUrl('dist/artifacts/AsarArtifactReader.js')),
import(runtimeUrl('dist/domain/evidence.js')),
])
const { createRequire } = await import('node:module')
const requireFromRuntime = createRequire(path.join(runtimeRoot, 'package.json'))
const packageMetadata = requireFromRuntime('./package.json')
return {
AsarArtifactReader: readerModule.AsarArtifactReader,
createEvidence: evidenceModule.createEvidence,
version: packageMetadata.version,
}
}
async function readSelectedEntry(reader, entry, totalBytes, binary = false) {
if (entry.kind !== 'file') throw new TypeError(`Selected path is not a regular file: ${entry.path}`)
if (!Number.isSafeInteger(entry.declaredSize) || entry.declaredSize < 0) {
throw new Error(`Selected file has no valid declared size: ${entry.path}`)
}
if (entry.declaredSize > MAX_FILE_BYTES) {
throw new RangeError(`Selected file exceeds the ${MAX_FILE_BYTES}-byte per-file budget: ${entry.path}`)
}
if (typeof entry.declaredSha256 !== 'string' || !SHA256_PATTERN.test(entry.declaredSha256)) {
throw new Error(`Selected file has no valid declared SHA-256: ${entry.path}`)
}
const chunks = []
const digest = createHash('sha256')
let bytes = 0
const stream = await reader.open(entry)
for await (const value of stream) {
if (!(Buffer.isBuffer(value) || value instanceof Uint8Array)) {
throw new TypeError(`Selected file stream returned non-byte data: ${entry.path}`)
}
const chunk = Buffer.from(value)
bytes += chunk.length
if (bytes > MAX_FILE_BYTES || bytes > entry.declaredSize || totalBytes + bytes > MAX_TOTAL_BYTES) {
throw new RangeError(`Selected text exceeds its declared size or aggregate budget: ${entry.path}`)
}
digest.update(chunk)
chunks.push(chunk)
}
if (bytes !== entry.declaredSize) {
throw new Error(`Selected file size mismatch for ${entry.path}: declared ${entry.declaredSize}, read ${bytes}`)
}
const actualSha256 = digest.digest('hex')
if (actualSha256 !== entry.declaredSha256) {
throw new Error(`Selected file SHA-256 mismatch for ${entry.path}`)
}
if (binary) return { path: entry.path, size_bytes: bytes, sha256: actualSha256, unpacked: entry.unpacked,
encoding: 'base64', data_base64: Buffer.concat(chunks, bytes).toString('base64') }
let text
try {
text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(Buffer.concat(chunks, bytes))
} catch (cause) {
throw new Error(`Selected file is not strict UTF-8: ${entry.path}`, { cause })
}
return {
path: entry.path,
size_bytes: bytes,
sha256: actualSha256,
unpacked: entry.unpacked,
text,
}
}
/** Export bounded UTF-8 text from exact selected files in a root-pinned ASAR. */
export async function exportSelectedArtifactText(inputPath, expectedRootSha256, selectedPaths, binary = false) {
if (typeof inputPath !== 'string' || inputPath.length === 0) {
throw new TypeError('ASAR input path must be a non-empty string')
}
const paths = validateSelection(expectedRootSha256, selectedPaths, binary)
const { AsarArtifactReader, createEvidence, version } = await loadReaRuntime()
const reader = new AsarArtifactReader(inputPath)
let primaryFailure
let primaryFailed = false
let evidence
try {
await reader.prepareContainer(expectedRootSha256)
const wanted = new Set(paths)
const entries = new Map()
for await (const entry of reader.entries()) {
if (!wanted.has(entry.path)) continue
if (entries.has(entry.path)) throw new Error(`ASAR inventory contains a duplicate path: ${entry.path}`)
entries.set(entry.path, entry)
}
for (const selectedPath of paths) {
if (!entries.has(selectedPath)) throw new Error(`Selected path is absent from the ASAR inventory: ${selectedPath}`)
}
let declaredTotalBytes = 0
for (const selectedPath of paths) {
const entry = entries.get(selectedPath)
if (!Number.isSafeInteger(entry.declaredSize) || entry.declaredSize < 0) {
throw new Error(`Selected file has no valid declared size: ${selectedPath}`)
}
if (entry.declaredSize > MAX_FILE_BYTES) {
throw new RangeError(`Selected file exceeds the ${MAX_FILE_BYTES}-byte per-file budget: ${selectedPath}`)
}
declaredTotalBytes += entry.declaredSize
if (declaredTotalBytes > MAX_TOTAL_BYTES) {
throw new RangeError(`Selected files exceed the ${MAX_TOTAL_BYTES}-byte aggregate budget`)
}
}
const files = []
let actualTotalBytes = 0
for (const selectedPath of paths) {
const file = await readSelectedEntry(reader, entries.get(selectedPath), actualTotalBytes, binary)
actualTotalBytes += file.size_bytes
files.push(file)
}
const unselectedScope = 'Not read; unpacked payload content and availability are unknown.'
evidence = createEvidence(
{ path: inputPath, sha256: expectedRootSha256, format: 'asar' },
{
id: binary ? 'rea-selected-asar-binary' : 'rea-selected-asar-text',
name: binary ? 'REA selected ASAR binary adapter' : 'REA selected ASAR text adapter',
version,
},
{
predicateType: binary ? 'rea.artifact.selected-binary' : 'rea.artifact.selected-text',
operation: binary ? 'export_selected_asar_artifact_binary' : 'export_selected_asar_artifact_text',
parameters: {
expected_root_sha256: expectedRootSha256,
selected_paths: paths,
max_file_bytes: MAX_FILE_BYTES,
max_total_bytes: MAX_TOTAL_BYTES,
},
result: {
root_sha256: expectedRootSha256,
scope: {
kind: 'selected-files-only',
selected_paths: paths,
unselected_members: unselectedScope,
},
files,
},
confidence: 'observed',
authority: 'shipped-artifact',
limitations: [
'Only selected file streams were read and validated.',
`Unselected ASAR members were not opened; ${unselectedScope}`,
],
locations: paths.map((selectedPath) => ({ kind: 'artifact-path', path: selectedPath })),
},
)
} catch (error) {
primaryFailure = error
primaryFailed = true
}
try {
await reader.close()
} catch (cleanupFailure) {
if (primaryFailed) {
throw new AggregateError(
[primaryFailure, cleanupFailure],
'Selected ASAR text export failed and reader cleanup also failed',
{ cause: primaryFailure },
)
}
throw cleanupFailure
}
if (primaryFailed) throw primaryFailure
return evidence
}
@@ -0,0 +1,110 @@
import { createHash } from 'node:crypto'
import path from 'node:path'
import { DirectoryArtifactReader } from './DirectoryArtifactReader.js'
import { createEvidence } from '../domain/evidence.js'
const MAX_BYTES = 32 * 1024 * 1024
const MAX_ENTRIES = 20000
export async function exportSelectedConversationText(projectsRoot, sessionId, includeImages = false) {
if (!path.isAbsolute(projectsRoot) || !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(sessionId)) {
throw new TypeError('An absolute projects root and exact UUID session ID are required')
}
const reader = new DirectoryArtifactReader(projectsRoot)
const matches = []
let examined = 0
try {
for await (const entry of reader.entries()) {
if (++examined > MAX_ENTRIES) throw new RangeError('Conversation inventory entry budget exceeded')
if (entry.path.split('/').at(-1) === `${sessionId}.jsonl`) matches.push(entry)
}
if (matches.length !== 1) throw new Error(`Expected one exact session transcript, found ${matches.length}`)
const entry = matches[0]
if (entry.kind !== 'file' || !Number.isSafeInteger(entry.declaredSize) || entry.declaredSize > MAX_BYTES) {
throw new RangeError('Selected transcript is not a bounded regular file')
}
const chunks = []
let bytes = 0
for await (const chunk of await reader.open(entry)) {
const data = Buffer.from(chunk)
bytes += data.length
if (bytes > MAX_BYTES) throw new RangeError('Transcript grew beyond the byte budget')
chunks.push(data)
}
const data = Buffer.concat(chunks)
if (data.length !== entry.declaredSize) throw new Error('Transcript size changed during read')
const text = new TextDecoder('utf-8', { fatal: true }).decode(data)
const sha256 = createHash('sha256').update(data).digest('hex')
let records = 0
const imageRefs = new Map()
for (const line of text.split(/\r?\n/)) {
if (!line.trim()) continue
const record = JSON.parse(line)
if (!record || typeof record !== 'object' || Array.isArray(record)) throw new Error('JSONL record is not an object')
if (record.sessionId && record.sessionId !== sessionId) throw new Error('Transcript contains another session ID')
if (includeImages && record.type === 'message' && Array.isArray(record.content)) {
for (const block of record.content) {
const image = block?.type === 'input_image' ? block.image : undefined
if (image?.type !== 'image_blob_ref') continue
if (!/^[a-f0-9]{64}$/.test(image.blob_id) || !['image/png', 'image/jpeg', 'image/webp', 'image/gif'].includes(image.mime)) {
throw new Error('Unsupported or invalid transcript image reference')
}
imageRefs.set(image.blob_id, image)
}
}
records++
}
const attachments = includeImages ? await readImages(projectsRoot, [...imageRefs.values()]) : []
return createEvidence({ path: path.join(projectsRoot, entry.path), sha256, format: 'file' },
{ id: 'rea-selected-conversation-text', name: 'REA selected local conversation transcript', version: '1+local.1' }, {
predicateType: 'rea.conversation.selected-text', operation: 'export_selected_conversation_text',
parameters: { projects_root: projectsRoot, session_id: sessionId, include_images: includeImages, max_bytes: MAX_BYTES, max_entries: MAX_ENTRIES },
result: { session_id: sessionId, file: { path: entry.path, sha256, bytes, records, text }, attachments,
scope: 'One exact session JSONL and optionally its referenced image blobs; other file contents are not read' },
confidence: 'observed', authority: 'historical-reference',
limitations: ['Stable directory-reader identity checks do not establish an atomic snapshot of an actively written conversation.',
'Exports stored history only; non-image external files and live kernel state are not copied.'],
locations: [{ kind: 'artifact-path', path: entry.path }]
})
} finally { await reader.close() }
}
async function readImages(projectsRoot, references) {
if (!references.length) return []
if (references.length > 32) throw new RangeError('Too many image references')
const root = path.resolve(projectsRoot, '../blobs')
const selected = new Map()
for (const ref of references) {
const target = path.resolve(ref.blob_path)
const relative = path.relative(root, target).split(path.sep).join('/')
if (!new RegExp(`^${ref.blob_id.slice(0, 2)}/${ref.blob_id}\\.(png|jpg|jpeg|webp|gif)$`).test(relative)) {
throw new Error('Image reference is outside the selected content-addressed blob root')
}
selected.set(relative, ref)
}
const reader = new DirectoryArtifactReader(root)
const files = []
let total = 0
try {
for await (const entry of reader.entries(undefined, { descendInto: directory => [...selected.keys()].some(file => file.startsWith(directory + '/')) })) {
const ref = selected.get(entry.path)
if (!ref) continue
if (entry.kind !== 'file' || !Number.isSafeInteger(entry.declaredSize) || entry.declaredSize > 8 * 1024 * 1024) {
throw new Error('Referenced image is not a bounded regular file')
}
const chunks = []
for await (const chunk of await reader.open(entry)) {
total += chunk.length
if (total > 16 * 1024 * 1024) throw new RangeError('Image export byte budget exceeded')
chunks.push(Buffer.from(chunk))
}
const data = Buffer.concat(chunks)
const sha256 = createHash('sha256').update(data).digest('hex')
if (sha256 !== ref.blob_id || data.length !== ref.size || data.length !== entry.declaredSize) {
throw new Error('Image blob digest/size does not match the transcript reference')
}
files.push({ blob_id: ref.blob_id, mime: ref.mime, bytes: data.length, sha256, data_base64: data.toString('base64') })
}
if (files.length !== references.length) throw new Error('A referenced image blob is absent')
return files
} finally { await reader.close() }
}
+163
View File
@@ -0,0 +1,163 @@
// Prepare a reviewable local REA 6.5.0 repair. Never reads WorkBuddy or changes global registration.
import fs from 'node:fs/promises'
import path from 'node:path'
import crypto from 'node:crypto'
const sourceRoot = path.resolve(process.argv[2] || 'C:/Users/Administrator/scoop/apps/nvm/current/nodejs/nodejs/node_modules/rea-agents')
const output = path.resolve('.tmp/rea-css-runtime')
const patchRoot = path.resolve('scripts/rea/patches')
const sourcePackage = JSON.parse(await fs.readFile(path.join(sourceRoot, 'package.json'), 'utf8'))
if (sourcePackage.name !== 'rea-agents' || sourcePackage.version !== '6.5.0') throw new Error('Repair requires installed rea-agents 6.5.0')
if (output === sourceRoot || !output.startsWith(path.resolve('.tmp') + path.sep)) throw new Error('Invalid local repair destination')
await fs.mkdir(output, { recursive: true })
const marker = path.join(output, 'css-repair-manifest.json')
try {
const previous = JSON.parse(await fs.readFile(marker, 'utf8'))
if (previous.sourceRoot !== sourceRoot) throw new Error('Local repair belongs to a different source installation')
} catch (error) {
if (error.code !== 'ENOENT') throw error
for (const entry of await fs.readdir(sourceRoot, { withFileTypes: true })) {
if (entry.name === 'node_modules') continue
await fs.cp(path.join(sourceRoot, entry.name), path.join(output, entry.name), { recursive: true, force: false, errorOnExist: true, verbatimSymlinks: true })
}
await fs.symlink(path.join(sourceRoot, 'node_modules'), path.join(output, 'node_modules'), process.platform === 'win32' ? 'junction' : 'dir')
}
const records = []
const digest = text => crypto.createHash('sha256').update(text).digest('hex')
async function patch(relative, transform) {
const original = await fs.readFile(path.join(sourceRoot, relative), 'utf8')
const modified = transform(original)
if (original === modified) throw new Error(`No change for ${relative}`)
const baseline = path.join(output, 'css-repair-baseline', relative)
await fs.mkdir(path.dirname(baseline), { recursive: true })
await fs.writeFile(baseline, original)
await fs.writeFile(path.join(output, relative), modified)
records.push({ path: relative, original_sha256: digest(original), modified_sha256: digest(modified) })
}
function once(text, before, after) {
if (text.split(before).length !== 2) throw new Error(`Expected exactly one patch anchor: ${before.slice(0, 70)}`)
return text.replace(before, after)
}
// Use the package's shipped unbundled build, so CLI and MCP load the repaired modules.
await patch('scripts/rea.mjs', text => once(text.replaceAll('dist/mcp/main.js', 'dist/main.js')
.replaceAll('dist/cli-runtime/cli.js', 'dist/cli.js')
.replaceAll('dist/cli-runtime/cliOutput.js', 'dist/cliOutput.js')
.replaceAll('dist/cli-runtime/cliResultOutput.js', 'dist/cli/cliResultOutput.js'), 'if (!(await compiledRuntimeExists(runtimeFiles))) {', `if (args[0] === "inspect-desktop-electron-processes") {
const { inspectDesktopElectronProcesses } = await import("../dist/browser/DesktopElectronProcesses.js");
process.stdout.write(JSON.stringify(await inspectDesktopElectronProcesses(args[1] || "WorkBuddy.exe")) + "\\n");
} else if (args[0] === "restart-desktop-electron-debug") {
const { restartDesktopElectronDebug } = await import("../dist/browser/DesktopElectronProcesses.js");
process.stdout.write(JSON.stringify(await restartDesktopElectronDebug(args[1], Number(args[2]), args.includes("--restart-authorized"))) + "\\n");
} else if (args[0] === "export-selected-artifact-text") {
const { exportSelectedArtifactText } = await import("../dist/artifacts/SelectedArtifactText.js");
process.stdout.write(JSON.stringify(await exportSelectedArtifactText(args[1], args[2], JSON.parse(args[3]))) + "\\n");
} else if (args[0] === "export-selected-artifact-binary") {
const { exportSelectedArtifactText } = await import("../dist/artifacts/SelectedArtifactText.js");
process.stdout.write(JSON.stringify(await exportSelectedArtifactText(args[1], args[2], JSON.parse(args[3]), true)) + "\\n");
} else if (args[0] === "export-selected-conversation-text") {
const { exportSelectedConversationText } = await import("../dist/artifacts/SelectedConversationText.js");
process.stdout.write(JSON.stringify(await exportSelectedConversationText(args[1], args[2], args.includes("--include-images"))) + "\\n");
} else if (!(await compiledRuntimeExists(runtimeFiles))) {`))
await patch('package.json', text => {
const pkg = JSON.parse(text)
pkg.version = '6.5.0-visual.3'
return JSON.stringify(pkg, null, 2) + '\n'
})
await patch('dist/generatedPackageMetadata.js', text => once(text, 'version: "6.5.0",', 'version: "6.5.0-visual.3",'))
await patch('dist/domain/javascript/electronObservation.js', text => {
text = 'import { electronVisualProbeInputSchema, electronMediaAssetsSchema, platformFontSampleSchema, visualProbeOutputSchema } from "./ElectronVisualProbeSchema.js";\nimport { webScreenshotArtifactSchema } from "../webScreenshot.js";\n' + text
text = once(text, ' target_id: z.string().trim().min(1),', ' target_id: z.string().trim().min(1),\n include_style_sources: z.boolean().default(false).describe("Include CSS text from stylesheets attributed to authorized Electron frames"),\n include_visuals: z.boolean().default(false).describe("Include visible viewport PNG and attributed DOM layout styles"),')
text = text.replace(' include_visuals: z.boolean()', ' visual_probe: electronVisualProbeInputSchema.optional(),\n include_visuals: z.boolean()')
text = once(text, ']);\nconst electronTargetSchema', `]).superRefine((value, ctx) => {
if (value.visual_probe && !value.include_visuals) ctx.addIssue({code: "custom", message: "visual_probe requires include_visuals"});
});
const electronTargetSchema`)
text = once(text, ' workers: z', ` visuals: z.object({
captured_at: z.iso.datetime(),
viewport: z.object({width:z.number().int().positive(),height:z.number().int().positive()}),
css_viewport: z.object({width:z.number().positive(),height:z.number().positive(),page_x:z.number(),page_y:z.number()}),
environment_media:z.object({reduced_motion:z.boolean().nullable()}).optional(),
environment_gpu:z.object({status:z.enum(['observed','unavailable']),devices:z.array(z.object({vendor_id:z.number(),device_id:z.number(),device_string:z.string().optional(),active:z.boolean().optional()})).optional(),aux_attributes:z.record(z.string(),z.union([z.string(),z.number(),z.boolean(),z.null()])).optional(),feature_status:z.record(z.string(),z.string()).optional()}).optional(),
artifact: webScreenshotArtifactSchema,
layout: z.object({properties:z.array(z.string()),documents:z.array(z.object({frame_id:z.string(),file_path:z.string(),scroll_x:z.number(),scroll_y:z.number(),nodes:z.array(z.object({node_index:z.number().int(),parent_index:z.number().int(),node_name:z.string(),attributes:z.record(z.string(),z.string()),bounds:z.tuple([z.number(),z.number(),z.number(),z.number()]),styles:z.record(z.string(),z.string())})),svg_nodes:z.array(z.object({node_index:z.number().int(),parent_index:z.number().int(),node_name:z.string(),attributes:z.record(z.string(),z.string())}))}))}),
limitations:z.array(z.string()),
font_samples:z.array(platformFontSampleSchema.extend({bounds:z.tuple([z.number(),z.number(),z.number(),z.number()]).optional(),styles:z.record(z.string(),z.string()).optional()})).optional(),
font_missing:z.array(z.object({category:z.string(),reason:z.string()})).optional(),
message_content:z.object({nodes:z.array(z.object({node_index:z.number().int(),parent_index:z.number().int(),node_name:z.string(),attributes:z.record(z.string(),z.string()),text:z.string().optional(),selector:z.string().optional()})),text_characters:z.number().int().min(0),truncated:z.boolean()}).optional(),
probe:visualProbeOutputSchema.optional(),
media_assets:electronMediaAssetsSchema.optional(),
}).optional(),
workers: z`)
return once(text, ' workers: z', ` styles: z.object({
total: z.number().int().min(0),
items: z.array(z.object({
style_sheet_key: prefixedDigestSchema("electron_stylesheet"),
style_sheet_id: z.string(), frame_id: z.string().nullable(), file_path: z.string().nullable(),
origin: z.string(), is_inline: z.boolean(), is_constructed: z.boolean(), source: electronSourceSchema,
})),
excluded: z.number().int().min(0), source_failures: z.number().int().min(0), truncated: z.boolean(),
limits: z.object({ max_sheets: z.number().int().positive(), max_sheet_bytes: z.number().int().positive(), max_total_bytes: z.number().int().positive() }),
}).optional(),
workers: z`)
})
await patch('dist/browser/CdpElectronInspection.js', text => {
text = 'import { captureElectronVisuals } from "./CdpElectronVisuals.js";\nimport { ingestElectronStyleEvent, captureElectronStyles } from "./CdpElectronStyles.js";\n' + text
text = once(text,'"REA does not evaluate renderer JavaScript, invoke Electron APIs, navigate, click, or close the page."','"REA does not evaluate renderer JavaScript or invoke Electron APIs; declared visual_probe native inputs are the only optional UI actions."')
text = once(text, ' scripts: [],', ' scripts: [],\n styles: new Map(),')
text = once(text, ' ingestElectronScriptEvent({', ' if (context.input.include_style_sources) ingestElectronStyleEvent(event, state.styles);\n ingestElectronScriptEvent({')
text = once(text, ' await enableElectronCapture(context);', ` await enableElectronCapture(context);
if (context.input.include_style_sources) {
await context.connection.send("DOM.enable", {}, context.sessionId, context.signal);
await context.connection.send("CSS.enable", {}, context.sessionId, context.signal);
state.limitations.push("CSS capture is a local REA 6.5.0-css.1 extension: only currently loaded styles attributed to authorized file-page frames are eligible. No renderer JavaScript is evaluated. Styles have independent coverage and budgets; inspect the styles section.");
}`)
return once(text, ' return { dom, scripts, resources, workers };', ` const styles = await captureElectronStyles({
connection: context.connection, sessionId: context.sessionId, signal: context.signal,
styles: state.styles, frameIds, request: context.input,
completeness: state.completeness, limitations: state.limitations,
});
const visuals = await captureElectronVisuals(context, frames);
return { dom, scripts, resources, workers, styles, ...(visuals ? {visuals} : {}) };`)
})
await patch('dist/browser/CdpElectronProvider.js', text => {
text = once(text, ' return CDP_ELECTRON_PROVIDER_IDENTITY;', ' return { ...CDP_ELECTRON_PROVIDER_IDENTITY, version: "1+visual-local.3" };')
return once(text, '["Debugger", "Runtime", "Page"]', '["CSS", "DOM", "Debugger", "Runtime", "Page"]')
})
await patch('dist/application/javascript/ElectronEvidence.js', text => once(once(text, 'authority: "external-service",','authority: input.visual_probe?.actions?.length ? "controlled-replay" : "external-service",'),
' include_script_sources: input.include_script_sources,',
' include_script_sources: input.include_script_sources,\n include_style_sources: input.include_style_sources,\n include_visuals: input.include_visuals,\n ...(input.visual_probe ? { visual_probe: input.visual_probe } : {}),'))
await patch('dist/cliObservationOptions.js', text => once(text,
'export const electronPageInspectionOptions = z.object({',
'export const electronPageInspectionOptions = z.object({\n visualProbe: z.string().optional().describe("JSON for declared native UI actions, sampled PNG frames, font usage and opt-in media_selectors for loaded IMG bytes"),\n includeStyleSources: z.boolean().default(false).describe("Include attributed local or inline stylesheet CSS text"),\n includeVisuals: z.boolean().default(false).describe("Include current viewport PNG and layout styles"),'))
await patch('dist/cli/electronCommands.js', text => once(text,
' include_script_sources: options.includeScriptSources,',
' include_script_sources: options.includeScriptSources,\n include_style_sources: options.includeStyleSources,\n include_visuals: options.includeVisuals,\n ...(options.visualProbe ? {visual_probe: JSON.parse(options.visualProbe)} : {}),'))
await patch('dist/contracts/javascript/electronToolContracts.js', text => once(text,
'Returns frames, DOM structure, resource paths, and scripts without evaluating renderer JavaScript or invoking Electron APIs.',
'Returns frames, DOM structure, resource paths, scripts and optional attributed CSS sources without evaluating renderer JavaScript or invoking Electron APIs. Default inspection is passive. include_style_sources selects CSS text; include_visuals selects viewport PNG and layout. Optional visual_probe declares native hover/click/scroll/key/viewport actions, sequential PNG samples, and actual platform fonts; UI actions can change application state and restoration is limited. visual_probe.media_selectors explicitly selects at most four unique main-frame IMG nodes and their already loaded PNG/JPEG/GIF/WebP resources, with byte budgets, SHA/base64 and visibility reported separately; no HTTP fetch is performed. These are local REA extensions; inspect coverage, budgets, restoration and limitations.'))
const styleModule = await fs.readFile(path.join(patchRoot, 'CdpElectronStyles.js'), 'utf8')
await fs.writeFile(path.join(output, 'dist/browser/CdpElectronStyles.js'), styleModule)
records.push({ path: 'dist/browser/CdpElectronStyles.js', original_sha256: null, modified_sha256: digest(styleModule) })
const visualModule = await fs.readFile(path.join(patchRoot, 'CdpElectronVisuals.js'), 'utf8')
await fs.writeFile(path.join(output, 'dist/browser/CdpElectronVisuals.js'), visualModule)
records.push({ path: 'dist/browser/CdpElectronVisuals.js', original_sha256: null, modified_sha256: digest(visualModule) })
const processModule = await fs.readFile(path.join(patchRoot, 'DesktopElectronProcesses.js'), 'utf8')
await fs.writeFile(path.join(output, 'dist/browser/DesktopElectronProcesses.js'), processModule)
records.push({ path: 'dist/browser/DesktopElectronProcesses.js', original_sha256: null, modified_sha256: digest(processModule) })
for(const [source,destination] of [['ElectronVisualProbeSchema.js','domain/javascript/ElectronVisualProbeSchema.js'],['CdpElectronVisualProbe.js','browser/CdpElectronVisualProbe.js'],['CdpElectronPlatformFonts.js','browser/CdpElectronPlatformFonts.js'],['CdpElectronMediaAssets.js','browser/CdpElectronMediaAssets.js']]) {
const module=await fs.readFile(path.join(patchRoot,source),'utf8')
await fs.writeFile(path.join(output,'dist',destination),module)
records.push({path:'dist/'+destination,original_sha256:null,modified_sha256:digest(module)})
}
const selectedArtifactModule=await fs.readFile(path.join(patchRoot,'SelectedArtifactText.js'),'utf8')
await fs.writeFile(path.join(output,'dist/artifacts/SelectedArtifactText.js'),selectedArtifactModule)
records.push({path:'dist/artifacts/SelectedArtifactText.js',original_sha256:null,modified_sha256:digest(selectedArtifactModule)})
const conversationModule=await fs.readFile(path.join(patchRoot,'SelectedConversationText.js'),'utf8')
await fs.writeFile(path.join(output,'dist/artifacts/SelectedConversationText.js'),conversationModule)
records.push({path:'dist/artifacts/SelectedConversationText.js',original_sha256:null,modified_sha256:digest(conversationModule)})
const manifest = { sourceRoot, upstreamVersion: sourcePackage.version, localVersion: '6.5.0-visual.3', output, files: records,
notes: ['Local REA repair, not an upstream release.', 'Global installation and Codex registration unchanged.', 'Public scripts/rea.mjs entry point loads the shipped unbundled build.', 'Dependencies reused read-only through node_modules link.'] }
await fs.writeFile(marker, JSON.stringify(manifest, null, 2) + '\n')
console.log(JSON.stringify({ output, localVersion: manifest.localVersion, files: records.length }))
Loaded 100 of 1118 files, more files were not shown because too many files have changed in this diff. Show more