Files
ycode/docs/协作/evidence/B01-review-01/native-probe.cjs
T

143 lines
8.8 KiB
JavaScript

const fs = require('node:fs/promises')
const path = require('node:path')
const assert = require('node:assert/strict')
const { pathToFileURL } = require('node:url')
if (!process.versions.electron) {
;(async () => {
const directory = await fs.mkdtemp(path.resolve('.tmp/b01-native-'))
const data = path.join(directory, 'data')
const workspace = path.join(directory, 'workspace')
await fs.mkdir(data); await fs.mkdir(workspace); await fs.mkdir(path.join(directory, 'home'))
const now = Date.now()
const question = { id: 'q1', question: '检查哪个本地目标? Which local target?', answers: ['本地测试目录 / Local fixture'] }
const longQuestions = Array.from({ length: 24 }, (_, i) => ({
id: `long-${i}`, question: `Question ${i}: ${'长文本与换行测试 '.repeat(12)}`,
answers: ['Long answer '.repeat(20)]
}))
const message = questions => ({
id: 'a1', role: 'assistant', content: 'Final answer after question.', createdAt: now - 9000, status: 'done',
turnStartedAt: now - 10000, turnFinishedAt: now, turnDurationMs: 10000,
tools: [
{ id: 'q-tool', name: 'AskUserQuestion', args: { questions }, argsRaw: '{}', status: 'ok', output: 'RAW_SDK_ANSWER_SENTINEL', metadata: { questionAnswer: { questions } } },
{ id: 't1', name: 'UnknownOrdinaryTool', args: {}, argsRaw: '{}', status: 'ok', output: 'ORDINARY_TOOL_OUTPUT' }
],
parts: [
{ type: 'reasoning', id: 'r1', text: 'First reasoning.', finished: true }, { type: 'tool', toolId: 't1' },
{ type: 'tool', toolId: 'q-tool' }, { type: 'text', text: 'Final answer after question.' }
]
})
const session = (id, questions) => ({
id, title: id, cwd: workspace, createdAt: now, updatedAt: now + (id === 'b01-short' ? 1 : 0),
messages: [{ id: 'u1', role: 'user', content: 'Inspect a local fixture.', status: 'done', createdAt: now - 10000 }, message(questions)]
})
await fs.writeFile(path.join(data, 'sessions.json'), JSON.stringify({ sessions: [session('b01-short', [question]), session('b01-long', longQuestions)] }))
const env = { ...process.env, YCODE_USER_DATA_DIR: data, YCODE_SKIP_KERNEL_DATA_RESET: '1', YCODE_QA_DIRECTORY: directory,
YCODE_QA_OFFSCREEN: '1', YCODE_DISABLE_GPU: '1', YCODE_DISABLE_SYSTEM_INTEGRATION: '1', B01_NATIVE_DIRECTORY: directory }
delete env.ELECTRON_RUN_AS_NODE; delete env.ELECTRON_RENDERER_URL
const child = require('node:child_process').spawn(require('electron'), [__filename], { env, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
let output = ''
child.stdout.on('data', bytes => { output += bytes })
child.stderr.on('data', bytes => { output += bytes })
const timer = setTimeout(() => child.kill(), 90000)
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 !== 0) console.error(output.slice(-6000))
process.exitCode = code === 0 ? 0 : 1
})
})().catch(error => { console.error(error); process.exitCode = 1 })
} else {
const { app, BrowserWindow } = require('electron')
const directory = process.env.B01_NATIVE_DIRECTORY
app.disableHardwareAcceleration(); app.setPath('home', path.join(directory, 'home'))
let win
const errors = []
const read = code => win.webContents.executeJavaScript(code)
const pause = ms => new Promise(resolve => setTimeout(resolve, ms))
const outer = '.cr-collapse--completed > .cr-collapse__header'
const questionHeader = '.ask-user-question__header .cr-tool-head__main'
async function wait(code) {
for (let i = 0; i < 250; i++) { if (await read(code)) return; await pause(40) }
throw new Error(`Timeout: ${code}`)
}
const click = selector => read(`document.querySelector(${JSON.stringify(selector)}).click()`)
async function setTheme(theme) {
await read(`(() => { localStorage.setItem("ycode:theme", ${JSON.stringify(theme)});
window.dispatchEvent(new StorageEvent("storage", {key:"ycode:theme",newValue:${JSON.stringify(theme)}})); })()`)
await wait(`document.documentElement.dataset.theme === ${JSON.stringify(theme)}`)
await pause(150)
}
async function shot(name) {
await read('document.fonts.ready'); await pause(400)
await fs.writeFile(path.join(directory, name + '.png'), (await win.webContents.capturePage()).toPNG())
}
async function measure() {
return read(`(() => {
const probe = selector => {
const element = document.querySelector(selector); if (!element) return null;
const style = getComputedStyle(element), box = element.getBoundingClientRect();
return {selector, fontSize:style.fontSize,fontWeight:style.fontWeight,lineHeight:style.lineHeight,
background:style.backgroundColor,padding:style.padding,borderRadius:style.borderRadius,border:style.border,
overflow:style.overflow,maxHeight:style.maxHeight,width:box.width,height:box.height,
scrollHeight:element.scrollHeight,clientHeight:element.clientHeight};
};
const section = document.querySelector(".ask-user-question--completed");
const content = section.querySelector(".ask-user-question__content");
const header = section.querySelector(".ask-user-question__header");
return { theme:document.documentElement.dataset.theme, titleOutsideContent:!content.contains(header),
cardCount:document.querySelectorAll(".ask-user-question--completed").length,
rawAnswerVisible:document.body.textContent.includes("RAW_SDK_ANSWER_SENTINEL"),
horizontalOverflow:document.documentElement.scrollWidth > innerWidth,
styles:[".ask-user-question--completed",".ask-user-question__header",".ask-user-question__content",
".question-answer-transcript__question",".question-answer-transcript__answer"].map(probe) };
})()`)
}
;(async () => {
await import(pathToFileURL(path.resolve('out/main/index.mjs')).href)
await app.whenReady(); win = BrowserWindow.getAllWindows()[0]; win.setSize(1336, 902); win.showInactive()
win.webContents.on('console-message', (_e, level, message) => { if (level >= 3) errors.push(message) })
await wait(`!!document.querySelector(${JSON.stringify(outer)})`)
await click(outer); await wait(`!!document.querySelector(${JSON.stringify(questionHeader)})`)
await click(questionHeader)
await wait(`document.querySelector(${JSON.stringify(questionHeader)}).getAttribute("aria-expanded") === "true"`)
await setTheme('light')
const light = await measure(); await shot('question-light')
assert.equal(light.titleOutsideContent, true); assert.equal(light.cardCount, 1); assert.equal(light.rawAnswerVisible, false)
const contentStyle = light.styles[2]
assert.equal(contentStyle.padding, '12px 16px'); assert.equal(contentStyle.borderRadius, '16px')
assert.equal(contentStyle.maxHeight, '300px')
for (const style of light.styles.slice(3)) {
assert.equal(style.fontSize, '14px'); assert.equal(style.fontWeight, '400'); assert.equal(style.lineHeight, '24.5px')
}
await click(outer); await click(outer)
await wait(`document.querySelector(${JSON.stringify(questionHeader)}).getAttribute("aria-expanded") === "true"`)
const preserved = await read(`document.querySelector(${JSON.stringify(questionHeader)}).getAttribute("aria-expanded")`)
await setTheme('dark')
const dark = await measure(); await shot('question-dark')
await setTheme('light')
win.setSize(820, 902); await pause(150)
const narrow = await measure(); await shot('question-narrow')
assert.equal(narrow.horizontalOverflow, false)
await click('[data-session-id="b01-long"]')
await wait('document.querySelector(".source-chat-header")?.textContent.includes("b01-long") && !!document.querySelector(".cr-collapse--completed")')
await pause(300); await click(outer)
await wait(`!!document.querySelector(${JSON.stringify(questionHeader)})`); await click(questionHeader)
await pause(350)
const long = await measure(); await shot('question-long')
assert.ok(long.styles[2].scrollHeight > long.styles[2].clientHeight)
assert.ok(long.styles[2].clientHeight <= 300)
assert.equal(long.horizontalOverflow, false)
const report = { codeSha: '51531dcae8d8ade85db9e203bc8c0f712805b2ae', scope: 'Real isolated Windows Electron main/preload/renderer; synthetic persisted messages, no model/SDK or original app access.',
light, dark, narrow, long, innerQuestionPreserved: preserved, rendererErrors: errors }
await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify(report, null, 2))
assert.equal(errors.length, 0)
app.exit(0)
})().catch(async error => {
console.error(error)
await fs.writeFile(path.join(directory, 'failure.txt'), String(error))
if (win) await shot('failure').catch(() => {})
app.exit(1)
})
}