feat(settings): replicate the WorkBuddy agents panel and persist team-management preference (TODO 15)
This commit is contained in:
1 parent
b3be45715e
commit
143516b3a8
33 files changed
+1071
-21
No files matched your search
@@ -19,6 +19,8 @@ try {
|
||||
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 svgBytes=Buffer.from('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><circle cx="8" cy="8" r="7"/></svg>')
|
||||
const svgContent=svgBytes.toString('base64'),svgDigest=crypto.createHash('sha256').update(svgBytes).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]}},
|
||||
@@ -34,10 +36,10 @@ try {
|
||||
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'&¶ms.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.getResourceTree')return {frameTree:{frame,resources:mode==='not-loaded'?[]:[{url:selectedResourceUrl,type:'Image',mimeType:mode==='svg'?'image/svg+xml':mode==='unsupported'?'image/bmp':'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}
|
||||
return {base64Encoded:mode!=='not-binary',content:mode==='invalid-base64'?'not-base64':mode==='signature'?'YWJj':mode==='oversize'?Buffer.alloc(2097153).toString('base64'):mode==='svg'?svgContent: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}
|
||||
@@ -58,12 +60,17 @@ try {
|
||||
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='svg';result=await capture()
|
||||
assert.equal(result.items[0].status,'exported')
|
||||
assert.equal(result.items[0].artifact.mime_type,'image/svg+xml')
|
||||
assert.equal(result.items[0].artifact.sha256,svgDigest)
|
||||
assert.equal(result.items[0].artifact.data_base64,svgContent)
|
||||
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']]){
|
||||
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'],['unsupported','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')
|
||||
if(['not-loaded','unsupported','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]
|
||||
@@ -98,5 +105,5 @@ try {
|
||||
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')
|
||||
console.log('REA selected IMG resource regression passed: opt-in/schema/Evidence, snapshot currentSrc/backend attribution, raster/SVG export, 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})}
|
||||
@@ -4,13 +4,15 @@ 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 types=new Set(['image/png','image/jpeg','image/gif','image/webp','image/svg+xml']);
|
||||
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';
|
||||
const text=bytes.toString('utf8',0,Math.min(bytes.length,4096)).replace(/^\uFEFF/,'').trimStart();
|
||||
if(/^(?:<\?xml[\s\S]*?\?>\s*)?<svg(?:\s|>)/i.test(text))return 'image/svg+xml';
|
||||
}
|
||||
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;}
|
||||
@@ -22,7 +24,7 @@ export async function captureElectronMediaAssets(snapshot,frames,selectors,send)
|
||||
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.',
|
||||
'PNG/JPEG/GIF/WebP/SVG only; 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]??'';
|
||||
|
||||
@@ -32,7 +32,7 @@ const mediaIdentity = {
|
||||
};
|
||||
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('exported'),artifact:z.object({mime_type:z.enum(['image/png','image/jpeg','image/gif','image/webp','image/svg+xml']),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),
|
||||
|
||||
@@ -135,7 +135,7 @@ await patch('dist/cli/electronCommands.js', text => once(text,
|
||||
' 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.'))
|
||||
'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/SVG 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)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"media_selectors": [
|
||||
"#settings-browser-extension-chrome img",
|
||||
"#settings-browser-extension-edge img",
|
||||
"#settings-browser-extension-qq-browser img"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "wait", "step_id": "observe-loaded-browser-extension-images", "duration_ms": 250, "frames": 1 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-navigation",
|
||||
".settings-navigation__item",
|
||||
".settings-navigation__item--active",
|
||||
".settings-modal__panel"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "scroll", "step_id": "reveal-function-group-navigation-labels", "selector": ".settings-navigation", "delta_y": 500, "frames": 1 },
|
||||
{ "kind": "wait", "step_id": "confirm-agents-navigation-label-and-order", "duration_ms": 400, "frames": 1 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-navigation__item--active",
|
||||
".settings-modal__panel",
|
||||
".settings-navigation",
|
||||
".settings-navigation__item"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "open-agents-by-observed-nav-index-7-in-dark-theme", "selector": ".settings-navigation__item", "index": 7, "frames": 1 },
|
||||
{ "kind": "wait", "step_id": "verify-agents-dark-page-stable", "duration_ms": 800, "frames": 2 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-navigation__item--active",
|
||||
".settings-modal__panel",
|
||||
".settings-navigation",
|
||||
".settings-navigation__item"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "open-agents-by-observed-nav-index-7", "selector": ".settings-navigation__item", "index": 7, "frames": 1 },
|
||||
{ "kind": "wait", "step_id": "verify-agents-page-stable", "duration_ms": 800, "frames": 4 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-modal__panel",
|
||||
".settings-navigation__item--active",
|
||||
".appearance-card--light",
|
||||
".appearance-card--dark"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "open-appearance-by-observed-nav-index-3-in-dark-theme", "selector": ".settings-navigation__item", "index": 3, "frames": 1 },
|
||||
{ "kind": "wait", "step_id": "verify-dark-appearance-before-restoring-light", "duration_ms": 500, "frames": 1 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-modal__panel",
|
||||
".settings-navigation__item--active",
|
||||
".appearance-card--light",
|
||||
".appearance-card--dark"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "open-appearance-by-observed-nav-index-3", "selector": ".settings-navigation__item", "index": 3, "frames": 1 },
|
||||
{ "kind": "wait", "step_id": "confirm-light-appearance-state", "duration_ms": 500, "frames": 1 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-modal__panel",
|
||||
".appearance-card--light",
|
||||
".appearance-card--dark"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "restore-light-theme-after-agent-settings-sample", "selector": ".appearance-card--light", "index": 0, "frames": 1 },
|
||||
{ "kind": "wait", "step_id": "wait-for-light-theme-render", "duration_ms": 800, "frames": 1 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-modal__panel",
|
||||
".general-settings-panel.extensions-settings-panel",
|
||||
"#settings-agent-teams",
|
||||
"#settings-browser-control"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "scroll", "step_id": "check-for-lower-agent-settings-content", "selector": ".settings-modal__panel", "delta_y": 700, "frames": 1 },
|
||||
{ "kind": "wait", "step_id": "confirm-panel-content-end", "duration_ms": 250, "frames": 1 },
|
||||
{ "kind": "scroll", "step_id": "return-agent-settings-to-top", "selector": ".settings-modal__panel", "delta_y": -700, "frames": 1 },
|
||||
{ "kind": "wait", "step_id": "confirm-agent-settings-top", "duration_ms": 250, "frames": 1 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-modal__panel",
|
||||
".settings-navigation__item--active",
|
||||
".appearance-card--light",
|
||||
".appearance-card--dark"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "select-dark-theme-for-agent-settings-sample", "selector": ".appearance-card--dark", "index": 0, "frames": 1 },
|
||||
{ "kind": "wait", "step_id": "wait-for-dark-theme-render", "duration_ms": 800, "frames": 1 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-modal__panel",
|
||||
".wb-select",
|
||||
".wb-select__value",
|
||||
".wb-select__option"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "open-agent-team-setting-options-without-changing-value", "selector": ".wb-select", "index": 0, "frames": 1 },
|
||||
{ "kind": "wait", "step_id": "observe-agent-team-setting-options", "duration_ms": 250, "frames": 1 },
|
||||
{ "kind": "key", "step_id": "close-agent-team-setting-options-with-escape", "key": "Escape", "frames": 1 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Recover only the three browser-extension images retained by REA Evidence.
|
||||
// This script reads the archived Evidence JSON and never accesses the WorkBuddy install.
|
||||
import { createHash } from 'node:crypto'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
const evidenceId = 'ev_9542a063514b1a980665e504a89126c7a17f00b42f0c6dff9cead085a929f5c7'
|
||||
const evidencePath = path.resolve(`.tmp/rea-css-evidence/${evidenceId}.json`)
|
||||
const outputDirectory = path.resolve('src/renderer/src/components/agents-assets')
|
||||
const expected = [
|
||||
{
|
||||
selector: '#settings-browser-extension-chrome img',
|
||||
mimeType: 'image/svg+xml',
|
||||
sha256: 'bde4af5a5d1150d3ec3108c29a5d746a00892196f3eaf1db4c4dc99aac7ba266',
|
||||
output: 'workbuddy-browser-chrome.svg'
|
||||
},
|
||||
{
|
||||
selector: '#settings-browser-extension-edge img',
|
||||
mimeType: 'image/svg+xml',
|
||||
sha256: '968cf9d827bfeaf47e4e53da55ec144bbc9565e5cb7a65cdb8dd0284fb3a5d11',
|
||||
output: 'workbuddy-browser-edge.svg'
|
||||
},
|
||||
{
|
||||
selector: '#settings-browser-extension-qq-browser img',
|
||||
mimeType: 'image/png',
|
||||
sha256: '186c82828c04b5bfe300c201ecd75cefb7984d97ea01bd491e0cfbdeb5df15a1',
|
||||
output: 'workbuddy-browser-qq.png'
|
||||
}
|
||||
]
|
||||
|
||||
const digest = (bytes) => createHash('sha256').update(bytes).digest('hex')
|
||||
const evidence = JSON.parse(await fs.readFile(evidencePath, 'utf8'))
|
||||
if (evidence.evidence_id !== evidenceId) throw new Error(`REA identity mismatch: ${evidence.evidence_id}`)
|
||||
if (evidence.completeness?.truncated_sections?.length) throw new Error('REA Evidence is truncated')
|
||||
if (!/renderer[\\/]index\.html$/i.test(evidence.normalized_result?.target?.file_path ?? '')) {
|
||||
throw new Error('REA Evidence target is not the authorized WorkBuddy renderer')
|
||||
}
|
||||
|
||||
const media = evidence.normalized_result?.visuals?.media_assets
|
||||
if (!media?.items || media.items.length !== expected.length) throw new Error('Unexpected REA media item count')
|
||||
const decoded = []
|
||||
for (const item of expected) {
|
||||
const matches = media.items.filter((candidate) => candidate.selector === item.selector)
|
||||
if (matches.length !== 1) throw new Error(`Expected one REA item for ${item.selector}`)
|
||||
const asset = matches[0]
|
||||
if (asset.status !== 'exported' || asset.visibility?.status !== 'observed') {
|
||||
throw new Error(`REA did not export a visible image for ${item.selector}: ${asset.reason}`)
|
||||
}
|
||||
if (asset.artifact?.mime_type !== item.mimeType || asset.artifact.sha256 !== item.sha256) {
|
||||
throw new Error(`REA artifact identity changed for ${item.selector}`)
|
||||
}
|
||||
const bytes = Buffer.from(asset.artifact.data_base64, 'base64')
|
||||
if (bytes.length !== asset.artifact.bytes || digest(bytes) !== item.sha256) {
|
||||
throw new Error(`REA artifact bytes changed for ${item.selector}`)
|
||||
}
|
||||
if (item.mimeType === 'image/svg+xml') {
|
||||
const source = bytes.toString('utf8')
|
||||
if (!/^<svg\s/.test(source) || !/\bviewBox=['"]/i.test(source)) throw new Error(`Invalid SVG source for ${item.output}`)
|
||||
if (/<script\b|<foreignObject\b|\b(?:href|xlink:href)=['"](?:https?:|file:|data:)/i.test(source)) {
|
||||
throw new Error(`Unsafe external or active SVG content for ${item.output}`)
|
||||
}
|
||||
} else if (item.mimeType === 'image/png') {
|
||||
if (bytes.subarray(0, 8).toString('hex') !== '89504e470d0a1a0a') throw new Error(`Invalid PNG source for ${item.output}`)
|
||||
if (bytes.readUInt32BE(16) !== 128 || bytes.readUInt32BE(20) !== 128) throw new Error('QQ browser icon is not 128x128')
|
||||
}
|
||||
decoded.push({ bytes, output: item.output, sha256: item.sha256 })
|
||||
}
|
||||
|
||||
await fs.mkdir(outputDirectory, { recursive: true })
|
||||
for (const asset of decoded) await fs.writeFile(path.join(outputDirectory, asset.output), asset.bytes)
|
||||
console.log(JSON.stringify({ evidenceId, outputDirectory, assets: decoded.map(({ output, sha256, bytes }) => ({
|
||||
output, sha256, bytes: bytes.length
|
||||
})) }, null, 2))
|
||||
@@ -0,0 +1,123 @@
|
||||
// Recover only retained REA CSS; never access the installed application here.
|
||||
// Sources: REA light/dark Evidence for WorkBuddy 5.8.3 Settings -> Agents.
|
||||
// The lazy module contains the browser-extension status rules for this page.
|
||||
import fs from 'node:fs/promises'
|
||||
import { createHash } from 'node:crypto'
|
||||
import postcss from 'postcss'
|
||||
|
||||
const sha = value => createHash('sha256').update(value).digest('hex')
|
||||
const evidenceIds = {
|
||||
light: 'ev_1cc896e04c0cc9ceab39b0a252f4982c019305609195ad44804ae9b5fb7379a8',
|
||||
dark: 'ev_3384aa03af8a1ccdffb8e025dbd4a354636d8d6b0c207abd21e12fc5eb2ae719'
|
||||
}
|
||||
const expectedModuleSha = 'cbd4b37c6061381943d96ca454176b2f083e49eb8e04c34cb5d31eef3dbcb8f5'
|
||||
const expectedTokensSha = '13f2bf82c96e5167a7d7098e36950c6c22c475b3337dbb8bb29ad2fe79cde696'
|
||||
const moduleName = 'ExtensionsSettingsPanel-BnsXh3oa.css'
|
||||
|
||||
const loadModule = async evidenceId => {
|
||||
const evidence = JSON.parse(await fs.readFile(`.tmp/rea-css-evidence/${evidenceId}.json`, 'utf8'))
|
||||
if (evidence.evidence_id !== evidenceId) throw new Error(`REA identity mismatch: ${evidenceId}`)
|
||||
const module = evidence.normalized_result.styles.items.find(item => item.file_path.endsWith(moduleName))
|
||||
if (!module) throw new Error(`Agent settings module CSS is absent from ${evidenceId}`)
|
||||
const moduleSha = sha(module.source.artifact.text)
|
||||
if (moduleSha !== expectedModuleSha || module.source.artifact.sha256 !== expectedModuleSha) {
|
||||
throw new Error(`Agent settings module CSS changed in ${evidenceId}`)
|
||||
}
|
||||
return { filePath: module.file_path, css: module.source.artifact.text, sha: moduleSha }
|
||||
}
|
||||
|
||||
const lightModule = await loadModule(evidenceIds.light)
|
||||
const darkModule = await loadModule(evidenceIds.dark)
|
||||
if (lightModule.filePath !== darkModule.filePath) throw new Error('Agent settings module path changed across captures')
|
||||
|
||||
const tokens = await fs.readFile('.tmp/rea-css-recovered/tokens-Df-i-zsO.css', 'utf8')
|
||||
const tokensSha = sha(tokens)
|
||||
if (tokensSha !== expectedTokensSha) throw new Error('Tokens CSS changed')
|
||||
|
||||
const lightTokens = new Map()
|
||||
const darkTokens = new Map()
|
||||
for (const rule of postcss.parse(tokens).nodes) {
|
||||
if (rule.type !== 'rule') continue
|
||||
const selectors = rule.selectors ?? []
|
||||
const dark = selectors.some(selector => selector.includes('[data-theme=dark]'))
|
||||
const light = selectors.some(selector => selector.trim() === ':root')
|
||||
if (!dark && !light) continue
|
||||
for (const node of rule.nodes ?? []) {
|
||||
if (node.type !== 'decl' || !node.prop.startsWith('--')) continue
|
||||
if (dark) darkTokens.set(node.prop, node.value)
|
||||
if (light) lightTokens.set(node.prop, node.value)
|
||||
}
|
||||
}
|
||||
|
||||
const relatedSelector = /\.extensions-settings-panel__browser-extension-status(?:[\w-]*)?(?![\w-])/i
|
||||
const darkScope = /^\s*(?:\[data-theme=["']?dark["']?\]|body\[data-vscode-theme-name=["']IDE Night["']\]|body\.vscode-dark|\.vscode-dark|body\.dark|\.dark)(?=\s|$)/i
|
||||
function project(node, mode = 'all') {
|
||||
if (node.type === 'rule') {
|
||||
const selectors = []
|
||||
for (const selector of node.selectors) {
|
||||
if (!relatedSelector.test(selector)) continue
|
||||
const isDark = darkScope.test(selector)
|
||||
if (mode === 'shared' && isDark) continue
|
||||
if (mode === 'dark' && !isDark) continue
|
||||
const mapped = isDark ? selector.replace(darkScope, '[data-theme=dark]') : selector
|
||||
if (!selectors.includes(mapped)) selectors.push(mapped)
|
||||
}
|
||||
return selectors.length ? node.clone({ selector: selectors.join(',') }) : null
|
||||
}
|
||||
if (node.type === 'atrule' && node.nodes) {
|
||||
const nodes = node.nodes.map(child => project(child, mode)).filter(Boolean)
|
||||
return nodes.length ? node.clone({ nodes }) : null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const sharedRoot = css => postcss.root({ nodes: postcss.parse(css).nodes.map(node => project(node, 'shared')).filter(Boolean) })
|
||||
const lightRoot = sharedRoot(lightModule.css)
|
||||
const darkSharedRoot = sharedRoot(darkModule.css)
|
||||
const darkSpecificRoot = postcss.root({ nodes: postcss.parse(darkModule.css).nodes.map(node => project(node, 'dark')).filter(Boolean) })
|
||||
const normalizeRules = root => {
|
||||
const rules = []
|
||||
root.walkRules(rule => {
|
||||
const selectors = rule.selectors.map(selector => selector.replace(/\s+/g, ' ').trim()).join(',')
|
||||
const declarations = (rule.nodes ?? [])
|
||||
.filter(node => node.type === 'decl')
|
||||
.map(node => `${node.prop}:${node.value.replace(/\b0px\b/g, '0').replace(/\s+/g, ' ').trim()}`)
|
||||
.join(';')
|
||||
rules.push(`${selectors}{${declarations}}`)
|
||||
})
|
||||
return rules.join('\n')
|
||||
}
|
||||
if (normalizeRules(lightRoot) !== normalizeRules(darkSharedRoot)) throw new Error('Light/dark agent module rules differ beyond serialization')
|
||||
|
||||
const root = postcss.root({ nodes: [...lightRoot.nodes, ...darkSpecificRoot.nodes] })
|
||||
const referenced = new Set()
|
||||
const collect = text => {
|
||||
for (const match of text.matchAll(/var\(\s*(--[\w-]+)/g)) referenced.add(match[1])
|
||||
}
|
||||
collect(root.toString())
|
||||
for (let depth = 0; depth < 8; depth++) {
|
||||
const before = referenced.size
|
||||
for (const name of [...referenced]) {
|
||||
for (const value of [lightTokens.get(name), darkTokens.get(name)]) {
|
||||
if (typeof value === 'string') collect(value)
|
||||
}
|
||||
}
|
||||
if (referenced.size === before) break
|
||||
}
|
||||
|
||||
const missingLight = [...referenced].filter(name => !lightTokens.has(name))
|
||||
if (missingLight.length) throw new Error(`Tokens missing from light root scope: ${missingLight.join(', ')}`)
|
||||
const pick = map => new Map([...referenced].map(name => [name, map.get(name) ?? lightTokens.get(name)]))
|
||||
const scope = (selector, map) => `${selector}{${[...map].map(([property, value]) => `${property}:${value}`).join(';')}}`
|
||||
const scopedTokens = `${scope('.extensions-settings-panel', pick(lightTokens))}\n${scope('[data-theme=dark] .extensions-settings-panel', pick(darkTokens))}\n`
|
||||
const evidenceList = Object.entries(evidenceIds).map(([name, evidenceId]) => `${name}=${evidenceId}`).join('; ')
|
||||
const css = `/* REA ${evidenceList}; ${moduleName} sha256=${lightModule.sha}; tokens sha256=${tokensSha}. */\n${root.toString()}\n${scopedTokens}`
|
||||
await fs.writeFile('src/renderer/src/components/source-agents-panels-recovered.css', css)
|
||||
|
||||
const rules = []
|
||||
root.walkRules(rule => rules.push(rule.selector))
|
||||
const darkRules = []
|
||||
darkSpecificRoot.walkRules(rule => darkRules.push(rule.selector))
|
||||
console.log(`Recovered ${rules.length} agent settings rules (${Buffer.byteLength(css)} bytes); ${darkRules.length} dark-specific module rules; scoped ${pick(lightTokens).size} light / ${pick(darkTokens).size} dark tokens`)
|
||||
console.log(rules.join('\n'))
|
||||
if (rules.length !== 4) throw new Error(`Expected 4 agent settings module rules, found ${rules.length}`)
|
||||
@@ -0,0 +1,243 @@
|
||||
// TODO 15 验收:设置 → 智能体的 1:1 布局 + 团队管理偏好真实持久化 + 未接入项标注 + 深浅主题。
|
||||
// 真实隔离 YCode(真实 preload/IPC/渲染进程/文件系统)+ 原生设置窗口,视口 900×680(与原版一致)。
|
||||
//
|
||||
// 原版依据(REA ev_bd0f4b…/ev_3384aa…/ev_01940b…/ev_1cc896…):
|
||||
// 页面根 [232,40,638,503];团队管理卡 638×90(section 606×66、下拉 120×32 @734,69);
|
||||
// 经济模式卡 638×90(开关 34×20 @820,181);浏览器操控标签 638×22 + Beta 39.391×20 @314,253;
|
||||
// 浏览器卡片 638×261(主 section 606×44、安装 68×32 @786,294、列表 606×193、行 68/68/56、按钮 102×32 @752)。
|
||||
const fs = require('node:fs/promises')
|
||||
const path = require('node:path')
|
||||
const assert = require('node:assert/strict')
|
||||
|
||||
if (!process.versions.electron) {
|
||||
;(async () => {
|
||||
const directory = await fs.mkdtemp(path.resolve('.tmp/agents-settings-'))
|
||||
const data = path.join(directory, 'data'), home = path.join(directory, 'home')
|
||||
await fs.mkdir(data, { recursive: true })
|
||||
await fs.mkdir(home, { recursive: true })
|
||||
const env = { ...process.env, YCODE_QA_DIRECTORY: directory, YCODE_USER_DATA_DIR: data, YCODE_OMP_HOME: home,
|
||||
YCODE_DISABLE_GPU: '1', YCODE_DISABLE_SYSTEM_INTEGRATION: '1' }
|
||||
delete env.ELECTRON_RUN_AS_NODE; delete env.ELECTRON_RENDERER_URL
|
||||
const child = require('node:child_process').spawn(require('electron'),
|
||||
['--no-sandbox', '--disable-gpu-sandbox', __filename], { env, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
|
||||
let output = ''
|
||||
child.stdout.on('data', bytes => { output += bytes; if (bytes.toString().includes('AGENTS_SETTINGS_')) process.stdout.write(bytes) })
|
||||
child.stderr.on('data', bytes => { output += bytes })
|
||||
const timer = setTimeout(() => child.kill(), 300000)
|
||||
child.once('exit', async code => {
|
||||
clearTimeout(timer)
|
||||
await fs.writeFile(path.join(directory, 'process.log'), output)
|
||||
console.log(JSON.stringify({ directory, exitCode: code }))
|
||||
if (code) console.error(output.slice(-4000))
|
||||
process.exitCode = code ? 1 : 0
|
||||
})
|
||||
})().catch(error => { console.error(error); process.exitCode = 1 })
|
||||
} else {
|
||||
const { app, BrowserWindow } = require('electron')
|
||||
const { pathToFileURL } = require('node:url')
|
||||
const directory = process.env.YCODE_QA_DIRECTORY
|
||||
app.setPath('home', path.join(directory, 'home'))
|
||||
const checks = [], snapshots = {}, rendererErrors = []
|
||||
let main, settings, phase = 'boot'
|
||||
const pause = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
async function read(source, win = main) {
|
||||
let timer
|
||||
try {
|
||||
return await Promise.race([win.webContents.executeJavaScript(source), new Promise((_, reject) => {
|
||||
timer = setTimeout(() => reject(Error('Renderer timeout: ' + source.slice(0, 120))), 15000)
|
||||
})])
|
||||
} finally { clearTimeout(timer) }
|
||||
}
|
||||
async function wait(source, label, win = main) {
|
||||
for (let index = 0; index < 300; index++) { const value = await read(source, win); if (value) return value; await pause(40) }
|
||||
throw Error('Timeout: ' + label)
|
||||
}
|
||||
async function step(label) { phase = label; await fs.writeFile(path.join(directory, 'phase.txt'), label) }
|
||||
function check(label, actual, expected) {
|
||||
const pass = typeof expected === 'function' ? expected(actual) : JSON.stringify(actual) === JSON.stringify(expected)
|
||||
checks.push({ label, pass, actual, expected: typeof expected === 'function' ? expected.toString() : expected })
|
||||
}
|
||||
const near = (value, target, tolerance = 1) => Math.abs(value - target) <= tolerance
|
||||
function observe(win) {
|
||||
win.webContents.on('console-message', event => { if (event.level === 'error' || event.level >= 3) rendererErrors.push({ windowId: win.id, message: event.message }) })
|
||||
}
|
||||
async function openSettings(section) {
|
||||
await read(`window.ycode.app.openSettings(${JSON.stringify(section)})`)
|
||||
for (let index = 0; index < 200; index++) {
|
||||
settings = BrowserWindow.getAllWindows().find(win => win !== main)
|
||||
if (settings && !settings.webContents.isLoading()) break
|
||||
await pause(25)
|
||||
}
|
||||
assert.ok(settings, 'settings window')
|
||||
await wait('!!document.querySelector(".settings-window-root")', 'settings root', settings)
|
||||
await pause(400)
|
||||
return settings
|
||||
}
|
||||
|
||||
const layoutSource = `(()=>{
|
||||
const round = value => Math.round(value * 1000) / 1000
|
||||
const rect = node => { if (!node) return null; const r = node.getBoundingClientRect()
|
||||
return { x: round(r.x), y: round(r.y), width: round(r.width), height: round(r.height) } }
|
||||
const panel = document.querySelector('.extensions-settings-panel')
|
||||
const cards = [...document.querySelectorAll('.extensions-settings-panel > .general-settings-panel__group-card')]
|
||||
const sections = [...document.querySelectorAll('.extensions-settings-panel .general-settings-panel__group-card > .general-settings-panel__section')]
|
||||
const buttons = [...document.querySelectorAll('.extensions-settings-panel__action-button')]
|
||||
const rows = [...document.querySelectorAll('.extensions-settings-panel__browser-row')]
|
||||
const select = document.querySelector('.extensions-settings-panel__team-dropdown .wb-select')
|
||||
const switchInput = document.querySelector('#agents-economy-switch, .extensions-settings-panel .wb-switch__input')
|
||||
return {
|
||||
panel: rect(panel),
|
||||
cards: cards.map(card => rect(card)),
|
||||
sections: sections.map(section => ({
|
||||
rect: rect(section),
|
||||
title: section.querySelector('.general-settings-panel__section-title')?.textContent?.trim(),
|
||||
desc: section.querySelector('.general-settings-panel__section-desc')?.textContent?.trim()
|
||||
})),
|
||||
select: select ? { rect: rect(select), value: select.querySelector('.wb-select__value')?.textContent?.trim(), title: select.getAttribute('title') } : null,
|
||||
switch: switchInput ? (() => { const track = switchInput.closest('.wb-switch')?.querySelector('.wb-switch__track')
|
||||
return { rect: rect(track), trackBackground: track ? getComputedStyle(track).backgroundColor : null,
|
||||
trackOpacity: track ? getComputedStyle(track).opacity : null, thumb: rect(track?.querySelector('.wb-switch__thumb')),
|
||||
checked: switchInput.checked, disabled: switchInput.disabled,
|
||||
unavailable: switchInput.closest('.extensions-settings-panel__unavailable-control')?.getAttribute('title') } })() : null,
|
||||
browserLabel: (() => { const node = document.querySelector('.extensions-settings-panel__browser-label'); return node ? { ...rect(node), text: node.firstChild?.textContent?.trim() } : null })(),
|
||||
beta: (() => { const node = document.querySelector('.extensions-settings-panel__beta'); return node ? { ...rect(node), text: node.textContent.trim() } : null })(),
|
||||
browserCard: rect(document.querySelector('.extensions-settings-panel__browser-card')),
|
||||
browserList: rect(document.querySelector('.extensions-settings-panel__browser-list')),
|
||||
rows: rows.map(row => ({
|
||||
rect: rect(row),
|
||||
title: row.querySelector('.extensions-settings-panel__browser-title')?.textContent?.trim(),
|
||||
desc: row.querySelector('.extensions-settings-panel__browser-desc')?.textContent?.trim(),
|
||||
icon: (() => { const node = row.querySelector('.extensions-settings-panel__browser-icon')
|
||||
return node ? { ...rect(node), tagName: node.tagName, src: node.getAttribute('src'), placeholderTitle: node.getAttribute('title'),
|
||||
complete: node.complete === true, naturalWidth: node.naturalWidth, naturalHeight: node.naturalHeight } : null })(),
|
||||
button: (() => { const node = row.querySelector('.extensions-settings-panel__action-button')
|
||||
return node ? { rect: rect(node), text: node.textContent.trim(), ariaDisabled: node.getAttribute('aria-disabled'), title: node.getAttribute('title'), arrow: !!node.querySelector('.wb-button__icon svg') } : null })()
|
||||
})),
|
||||
buttons: buttons.map(button => ({ text: button.textContent.trim(), rect: rect(button), ariaDisabled: button.getAttribute('aria-disabled'), title: button.getAttribute('title') })),
|
||||
anchors: [...document.querySelectorAll('a')].map(anchor => anchor.getAttribute('href')).filter(Boolean),
|
||||
cardBackground: cards[0] ? getComputedStyle(cards[0]).backgroundColor : null,
|
||||
titleColor: (() => { const node = document.querySelector('.extensions-settings-panel .general-settings-panel__section-title'); return node ? getComputedStyle(node).color : null })(),
|
||||
descColor: (() => { const node = document.querySelector('.extensions-settings-panel .general-settings-panel__section-desc'); return node ? getComputedStyle(node).color : null })(),
|
||||
selectBackground: select ? getComputedStyle(select).backgroundColor : null,
|
||||
selectBorder: select ? getComputedStyle(select).borderTopColor : null,
|
||||
buttonBackground: buttons[0] ? getComputedStyle(buttons[0]).backgroundColor : null,
|
||||
buttonBorder: buttons[0] ? getComputedStyle(buttons[0]).borderTopColor : null,
|
||||
betaBackground: (() => { const node = document.querySelector('.extensions-settings-panel__beta'); return node ? getComputedStyle(node).backgroundColor : null })(),
|
||||
theme: document.documentElement.dataset.theme ?? 'light'
|
||||
}})()`
|
||||
|
||||
;(async () => {
|
||||
await import(pathToFileURL(path.resolve('out/main/index.mjs')).href); await app.whenReady()
|
||||
for (let index = 0; index < 200; index++) {
|
||||
main = BrowserWindow.getAllWindows().find(win => win.getTitle() === 'YCode')
|
||||
if (main && !main.webContents.isLoading()) break
|
||||
await pause(25)
|
||||
}
|
||||
assert.ok(main, 'main window'); observe(main)
|
||||
await wait('!!window.ycode', 'preload api')
|
||||
|
||||
await step('agents-settings')
|
||||
await openSettings('extensions'); observe(settings)
|
||||
await wait('!!document.querySelector(".extensions-settings-panel")', 'agents panel', settings)
|
||||
const nav = await read(`(()=>{const item=document.querySelector('[data-settings-tab="extensions"]')
|
||||
return { exists: !!item, disabled: item?.getAttribute('aria-disabled'), active: document.querySelector('.settings-navigation__item--active')?.dataset.settingsTab }})()`, settings)
|
||||
check('智能体 nav entry is enabled and active', [nav.exists, nav.disabled !== 'true', nav.active], [true, true, 'extensions'])
|
||||
|
||||
const light = await read(layoutSource, settings)
|
||||
snapshots.light = light
|
||||
await fs.writeFile(path.join(directory, 'ycode-agents-light.png'), (await settings.webContents.capturePage()).toPNG())
|
||||
check('agents panel keeps the recovered 638x503 box at 232,40',
|
||||
[near(light.panel.x, 232), near(light.panel.y, 40), near(light.panel.width, 638), near(light.panel.height, 503)], [true, true, true, true])
|
||||
check('team-management and economy cards keep 638x90 with 606x66 sections',
|
||||
[light.cards.slice(0, 2).map(card => [near(card.width, 638), near(card.height, 90)].every(Boolean)),
|
||||
light.sections.slice(0, 2).map(section => [near(section.rect.width, 606), near(section.rect.height, 66)].every(Boolean)),
|
||||
near(light.cards[0].y, 40), near(light.cards[1].y, 146)], [[true, true], [true, true], true, true])
|
||||
check('team-management keeps the recovered title, description and 120x32 select',
|
||||
[light.sections[0].title, light.sections[0].desc,
|
||||
near(light.select.rect.x, 734), near(light.select.rect.y, 69), near(light.select.rect.width, 120), near(light.select.rect.height, 32),
|
||||
light.select.value, light.select.title],
|
||||
['智能体团队管理', '开启后,复杂任务会自动拆分多个智能体协作完成。专家团队任务不受此设置影响。',
|
||||
true, true, true, true, '开启', '仅保存偏好;多智能体协作执行尚未接入'])
|
||||
check('economy mode keeps the recovered copy, 34x20 switch and disabled annotation',
|
||||
[light.sections[1].title, light.sections[1].desc,
|
||||
near(light.switch.rect.x, 820), near(light.switch.rect.y, 181), near(light.switch.rect.width, 34), near(light.switch.rect.height, 20),
|
||||
light.switch.trackBackground, light.switch.checked, light.switch.disabled, light.switch.unavailable],
|
||||
['经济模式', '自动在合适时机触发上下文压缩,减少长对话中重复传输的上下文,降低积分消耗',
|
||||
true, true, true, true, 'rgb(0, 194, 154)', true, true, '暂未接入'])
|
||||
check('browser group keeps the recovered label 638x22 and Beta badge',
|
||||
[near(light.browserLabel.x, 232), near(light.browserLabel.y, 252), near(light.browserLabel.width, 638), near(light.browserLabel.height, 22),
|
||||
light.browserLabel.text, near(light.beta.x, 314), near(light.beta.y, 253), near(light.beta.width, 39.391), near(light.beta.height, 20), light.beta.text],
|
||||
[true, true, true, true, '浏览器操控', true, true, true, true, 'Beta'])
|
||||
check('browser card keeps 638x261 with 606x44 main section and 68x32 install button',
|
||||
[near(light.browserCard.y, 282), near(light.browserCard.width, 638), near(light.browserCard.height, 261),
|
||||
near(light.sections[2].rect.y, 294), near(light.sections[2].rect.width, 606), near(light.sections[2].rect.height, 44),
|
||||
light.sections[2].title, near(light.buttons[0].rect.x, 786), near(light.buttons[0].rect.width, 68), near(light.buttons[0].rect.height, 32),
|
||||
light.buttons[0].ariaDisabled, light.buttons[0].title],
|
||||
[true, true, true, true, true, true, '让 YCode 控制浏览器', true, true, true, 'true', '尚未配置'])
|
||||
check('browser list keeps the recovered 606x193 with 68/68/56 rows',
|
||||
[near(light.browserList.y, 338), near(light.browserList.width, 606), near(light.browserList.height, 193),
|
||||
light.rows.map(row => near(row.rect.height, row.title.includes('QQ') ? 56 : 68)),
|
||||
light.rows.every(row => row.icon && row.icon.tagName === 'IMG' && near(row.icon.width, 36) && near(row.icon.height, 36)
|
||||
&& row.icon.complete && row.icon.naturalWidth > 0 && row.icon.naturalHeight > 0 && row.icon.src && row.icon.placeholderTitle === null)],
|
||||
[true, true, true, [true, true, true], true])
|
||||
check('browser rows keep the recovered titles, descriptions and disabled 102x32 actions',
|
||||
[light.rows.map(row => row.title),
|
||||
light.rows.every(row => row.button && near(row.button.rect.width, 102) && near(row.button.rect.height, 32) && row.button.text === '前往安装' && row.button.ariaDisabled === 'true' && row.button.title === '尚未配置' && row.button.arrow)],
|
||||
[['YCode Chrome 扩展程序', 'YCode Edge 扩展程序', 'YCode QQ 浏览器扩展程序'], true])
|
||||
check('agents page contains no anchor links', light.anchors, [])
|
||||
|
||||
await step('agents-team-toggle')
|
||||
await read(`document.querySelector('.extensions-settings-panel__team-dropdown .wb-select').click()`, settings)
|
||||
const popover = await wait(`(()=>{const node=document.querySelector('.extensions-settings-panel__team-popover')
|
||||
return node?{options:[...node.querySelectorAll('[role=option]')].map(option=>option.textContent.trim())}:null})()`, 'team popover', settings)
|
||||
check('team-management dropdown offers exactly 开启 / 禁用', popover.options.map(text => text.replace(/\s+/g, '')), ['开启', '禁用'])
|
||||
await read(`[...document.querySelectorAll('.extensions-settings-panel__team-popover [role=option]')].find(option=>option.textContent.trim()==='禁用').click()`, settings)
|
||||
await wait(`document.querySelector('.extensions-settings-panel__team-dropdown .wb-select__value')?.textContent?.trim()==='禁用'`, 'team management disabled', settings)
|
||||
const stored = await read(`window.ycode.agents.get().then(view => view.teamManagement)`, settings)
|
||||
const config = JSON.parse(await fs.readFile(path.join(directory, 'data', 'config.json'), 'utf8'))
|
||||
snapshots.config = config.agentSettings
|
||||
check('selecting 禁用 persists the team-management preference', [stored, config.agentSettings?.teamManagement], [false, false])
|
||||
|
||||
await step('agents-persistence')
|
||||
settings.close()
|
||||
await pause(500)
|
||||
await openSettings('extensions')
|
||||
const reopened = await read(layoutSource, settings)
|
||||
check('reopened settings restores the 禁用 preference', reopened.select.value, '禁用')
|
||||
|
||||
await step('agents-dark')
|
||||
await openSettings('appearance')
|
||||
await wait('!!document.querySelector(".appearance-card")', 'appearance panel', settings)
|
||||
await read(`document.querySelector('[aria-label="深色"].appearance-card').click()`, settings)
|
||||
await wait(`document.documentElement.dataset.theme==='dark'`, 'dark theme', settings)
|
||||
await openSettings('extensions')
|
||||
await wait(`document.documentElement.dataset.theme==='dark'&&!!document.querySelector('.extensions-settings-panel')`, 'dark agents panel', settings)
|
||||
const dark = await read(layoutSource, settings)
|
||||
snapshots.dark = dark
|
||||
await fs.writeFile(path.join(directory, 'ycode-agents-dark.png'), (await settings.webContents.capturePage()).toPNG())
|
||||
check('dark theme keeps the recovered card, text, select and button surfaces',
|
||||
[dark.cardBackground, dark.titleColor, dark.descColor, dark.selectBackground, dark.selectBorder, dark.buttonBackground, dark.buttonBorder, dark.betaBackground],
|
||||
['rgb(31, 31, 31)', 'rgb(242, 243, 245)', 'rgba(255, 255, 255, 0.5)', 'rgb(31, 31, 31)', 'rgb(61, 61, 61)', 'rgb(38, 38, 38)', 'rgb(61, 61, 61)', 'rgb(31, 31, 31)'])
|
||||
check('dark theme keeps every recovered geometry',
|
||||
[near(dark.panel.width, 638), near(dark.panel.height, 503), near(dark.cards[0].height, 90),
|
||||
near(dark.browserCard.height, 261), near(dark.browserList.height, 193)], [true, true, true, true, true])
|
||||
check('dark economy switch keeps the recovered enabled brand track', dark.switch.trackBackground, 'rgb(34, 203, 168)')
|
||||
await openSettings('appearance')
|
||||
await wait('!!document.querySelector(".appearance-card")', 'appearance panel again', settings)
|
||||
await read(`document.querySelector('[aria-label="浅色"].appearance-card').click()`, settings)
|
||||
await wait(`document.documentElement.dataset.theme==='light'`, 'light restored', settings)
|
||||
|
||||
await step('agents-errors')
|
||||
check('no renderer error logs', rendererErrors, [])
|
||||
|
||||
const result = checks.every(item => item.pass) ? 'PASS' : 'FAIL'
|
||||
await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify({ result, phase, checks, snapshots }, null, 2))
|
||||
console.log('AGENTS_SETTINGS_' + result + ' ' + directory)
|
||||
for (const failure of checks.filter(item => !item.pass)) console.log('AGENTS_SETTINGS_FAILURE ' + JSON.stringify(failure))
|
||||
app.exit(result === 'PASS' ? 0 : 1)
|
||||
})().catch(async error => {
|
||||
console.error(error)
|
||||
try { await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify({ result: 'ERROR', phase, checks, snapshots, error: String(error?.stack ?? error) }, null, 2)) } catch { /* ignore */ }
|
||||
app.exit(1)
|
||||
})
|
||||
}
|
||||
@@ -125,10 +125,10 @@ if (!process.versions.electron) {
|
||||
// 原版 4 组;about 组按组件约定渲染为分隔线而不是标题。
|
||||
check('nav keeps the four recovered groups', [nav.groups.length, nav.dividers], [3, 1])
|
||||
// 通用(settings) 在 TODO 10 接入后同样可用。
|
||||
const expectedDisabled = EXPECTED_TABS.filter(tab => !['settings', 'models', 'appearance', 'keyboardShortcuts', 'personalization', 'helpFeedback', 'getHelp'].includes(tab))
|
||||
const expectedDisabled = EXPECTED_TABS.filter(tab => !['settings', 'models', 'appearance', 'keyboardShortcuts', 'personalization', 'extensions', 'helpFeedback', 'getHelp'].includes(tab))
|
||||
check('unwired entries stay disabled', nav.tabs.filter((tab, index) => nav.disabled[index] === 'true'), expectedDisabled)
|
||||
check('unwired entries keep the 暂未接入 hint', nav.titles.filter(title => (title ?? '').includes('暂未接入')).length, expectedDisabled.length)
|
||||
check('settings, appearance, shortcuts, personalization, models, about and help stay enabled', nav.tabs.filter((tab, index) => !nav.disabled[index]), ['settings', 'appearance', 'keyboardShortcuts', 'personalization', 'models', 'helpFeedback', 'getHelp'])
|
||||
check('settings, appearance, shortcuts, personalization, agents, models, about and help stay enabled', nav.tabs.filter((tab, index) => !nav.disabled[index]), ['settings', 'appearance', 'keyboardShortcuts', 'personalization', 'extensions', 'models', 'helpFeedback', 'getHelp'])
|
||||
|
||||
await step('settings-search-excludes-removed')
|
||||
await read(`(()=>{const input=document.querySelector('.settings-navigation input[type="search"]');
|
||||
|
||||
Reference in new issue
Block a user