Files
Ncurator/scripts/test/package-app.mjs
T
huangyuan 508132adba perf(test): reuse knowledge computations and expand skill live evaluation
Share content-addressed parsing and embedding caches across isolated test runs, fingerprint relevant dependencies and model assets, and remove implicit UI warm-up. Add realistic two-round Skill delivery evaluation and cache regressions.
2026-10-23 18:50:49 +08:00

296 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { spawn, spawnSync } from 'node:child_process'
import { appendFileSync, existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
import { mkdtemp } from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { extractFile, uncache } from '@electron/asar'
import { resolveBundleInstaller } from '../release/artifact-bundle.mjs'
import {
PACKAGE_PRODUCT_NAME,
cleanupPackagedE2EInstall,
renameMacPackageE2EApp,
resolveMacPackageE2EAppPath,
resolveMacPackageE2EInstallRoot,
resolveWindowsPackageE2EInstallRoot
} from './package-e2e-isolation.mjs'
import { findInstalledWindowsShortcut } from './desktop-launch.mjs'
const ROOT = process.cwd()
const LOGS_ROOT = path.join(ROOT, 'tests', 'logs', 'package')
const createRunId = () => new Date().toISOString().replaceAll(':', '-').replaceAll('.', '-')
const findMountedMacApp = (mountPath) => {
const apps = readdirSync(mountPath, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && entry.name.endsWith('.app'))
.map((entry) => path.join(mountPath, entry.name))
if (apps.length !== 1) {
throw new Error(`DMG 中期望只有一个 .app,实际为 ${String(apps.length)}:${mountPath}`)
}
return apps[0]
}
const resolveMacExecutable = (appPath) => {
const plistPath = path.join(appPath, 'Contents', 'Info.plist')
const result = spawnSync(
'/usr/bin/plutil',
['-extract', 'CFBundleExecutable', 'raw', '-o', '-', plistPath],
{ encoding: 'utf8' }
)
if (result.status !== 0) {
throw new Error(`无法读取 macOS 应用可执行文件:${String(result.stderr || result.stdout)}`)
}
const executableName = String(result.stdout || '').trim()
const executablePath = path.join(appPath, 'Contents', 'MacOS', executableName)
if (!existsSync(executablePath)) throw new Error(`macOS 可执行文件不存在:${executablePath}`)
return executablePath
}
const assertMacAppNotRunning = (executablePath) => {
const result = spawnSync('/usr/bin/pgrep', ['-f', executablePath], { encoding: 'utf8' })
if (result.status === 0 && String(result.stdout || '').trim()) {
throw new Error(`测试安装目录中的应用仍在运行,请先关闭:${executablePath}`)
}
}
const clearQuarantine = (targetPath) => {
const inspect = spawnSync('/usr/bin/xattr', ['-p', 'com.apple.quarantine', targetPath], {
encoding: 'utf8'
})
if (inspect.status !== 0) return
const clear = spawnSync('/usr/bin/xattr', ['-r', '-d', 'com.apple.quarantine', targetPath], {
encoding: 'utf8'
})
if (clear.status !== 0) {
throw new Error(
[
`无法清除 quarantine:${targetPath}`,
'请先手动执行:',
`sudo xattr -r -d com.apple.quarantine ${JSON.stringify(targetPath)}`
].join('\n')
)
}
}
export const preparePackagedE2EApplication = async (bundlePath, { resultPath = '' } = {}) => {
if (process.platform !== 'darwin' && process.platform !== 'win32') {
throw new Error(`正式包 E2E 只支持 macOS 和 Windows,当前平台:${process.platform}`)
}
if (!bundlePath) throw new Error('正式包 E2E 必须提供 --bundle <release-bundle.json>')
const expectedPlatform = process.platform === 'win32' ? 'windows-x64' : undefined
const resolvedBundlePath = path.resolve(ROOT, bundlePath)
const { bundle, installer, installerPath } = await resolveBundleInstaller(
resolvedBundlePath,
expectedPlatform ? { platform: expectedPlatform } : {}
)
if (process.platform === 'darwin' && !bundle.platform.startsWith('macos-')) {
throw new Error(`macOS 不能测试 ${bundle.platform} 发布包`)
}
const runId = createRunId()
const logDir = path.join(LOGS_ROOT, `${runId}-${bundle.platform}`)
const processLogPath = path.join(logDir, 'build-install.log')
mkdirSync(logDir, { recursive: true })
writeFileSync(processLogPath, '', 'utf8')
const log = (message) => {
const line = `[${new Date().toISOString()}] ${message}\n`
process.stdout.write(line)
appendFileSync(processLogPath, line, 'utf8')
}
const runLogged = async (command, args, options = {}) => {
log(`运行:${[command, ...args].join(' ')}`)
await new Promise((resolve, reject) => {
const child = spawn(command, args, {
cwd: options.cwd || ROOT,
env: options.env || process.env,
shell: process.platform === 'win32',
stdio: ['inherit', 'pipe', 'pipe']
})
const forward = (target, chunk) => {
target.write(chunk)
appendFileSync(processLogPath, chunk)
}
child.stdout.on('data', (chunk) => forward(process.stdout, chunk))
child.stderr.on('data', (chunk) => forward(process.stderr, chunk))
child.once('error', reject)
child.once('exit', (code, signal) => {
if (code === 0) resolve()
else reject(new Error(`${command} 失败:code=${String(code)} signal=${String(signal)}`))
})
})
}
const artifactPath = installerPath
let installedAppPath = ''
let executablePath = ''
try {
if (process.platform === 'darwin') {
if (!artifactPath.toLowerCase().endsWith('.dmg')) {
throw new Error(`macOS release bundle installer must be a DMG: ${artifactPath}`)
}
clearQuarantine(artifactPath)
const mountPath = await mkdtemp(path.join(os.tmpdir(), 'ncurator-e2e-dmg-'))
let mounted = false
try {
await runLogged('/usr/bin/hdiutil', [
'attach',
artifactPath,
'-nobrowse',
'-readonly',
'-mountpoint',
mountPath
])
mounted = true
const sourceAppPath = findMountedMacApp(mountPath)
if (path.basename(sourceAppPath) !== `${PACKAGE_PRODUCT_NAME}.app`) {
throw new Error(
`macOS DMG 应用名须为 ${PACKAGE_PRODUCT_NAME}.app,实际为 ${path.basename(sourceAppPath)}`
)
}
const installRoot = path.join(resolveMacPackageE2EInstallRoot(), bundle.platform)
installedAppPath = installRoot
const stagedAppPath = path.join(installRoot, `${PACKAGE_PRODUCT_NAME}.app`)
const isolatedAppPath = resolveMacPackageE2EAppPath(stagedAppPath)
for (const existingAppPath of [stagedAppPath, isolatedAppPath]) {
if (!existsSync(existingAppPath)) continue
assertMacAppNotRunning(resolveMacExecutable(existingAppPath))
}
mkdirSync(installRoot, { recursive: true })
rmSync(stagedAppPath, { recursive: true, force: true })
rmSync(isolatedAppPath, { recursive: true, force: true })
await runLogged('/usr/bin/ditto', [sourceAppPath, stagedAppPath])
clearQuarantine(stagedAppPath)
installedAppPath = renameMacPackageE2EApp(stagedAppPath, { log })
executablePath = resolveMacExecutable(installedAppPath)
} finally {
if (mounted) {
await runLogged('/usr/bin/hdiutil', ['detach', mountPath])
}
rmSync(mountPath, { recursive: true, force: true })
}
} else {
if (!artifactPath.toLowerCase().endsWith('.exe')) {
throw new Error(`Windows release bundle installer must be an EXE: ${artifactPath}`)
}
const installRoot = resolveWindowsPackageE2EInstallRoot()
mkdirSync(installRoot, { recursive: true })
installedAppPath = installRoot
await runLogged(artifactPath, ['/S', `/D=${installRoot}`])
executablePath = path.join(installRoot, `${PACKAGE_PRODUCT_NAME}.exe`)
if (!existsSync(executablePath)) {
throw new Error(`Windows 安装后的应用不存在:${executablePath}`)
}
log(`Windows 正式包 E2E 已安装到:${executablePath}`)
}
} catch (error) {
if (installedAppPath) {
try {
cleanupPackagedE2EInstall({ installRoot: installedAppPath, log })
} catch (cleanupError) {
log(
`安装失败后清理 E2E 安装失败:${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
)
}
}
throw error
}
const artifactSha256 = installer.sha256
let launchEntryPath
try {
launchEntryPath =
process.platform === 'win32' ? findInstalledWindowsShortcut(executablePath) : installedAppPath
} catch (error) {
cleanupPackagedE2EInstall({ installRoot: installedAppPath, log })
throw error
}
const metadata = {
schemaVersion: 1,
runId,
createdAt: new Date().toISOString(),
platform: process.platform,
arch: process.arch,
releaseTag: bundle.tag,
releaseCommit: bundle.commit,
releasePlatform: bundle.platform,
bundlePath: resolvedBundlePath,
artifactPath,
artifactSha256,
installedAppPath,
executablePath,
launchEntryPath
}
writeFileSync(
path.join(logDir, 'artifact.json'),
`${JSON.stringify(metadata, null, 2)}\n`,
'utf8'
)
log(`正式包已安装:${installedAppPath}`)
return {
...metadata,
logDir,
processLogPath,
env: {
NCURATOR_E2E_APP_TARGET: 'package',
NCURATOR_E2E_EXECUTABLE: executablePath,
NCURATOR_E2E_LAUNCH_ENTRY: launchEntryPath,
NCURATOR_E2E_ARTIFACT_PATH: artifactPath,
NCURATOR_E2E_ARTIFACT_SHA256: artifactSha256,
NCURATOR_BETTER_SQLITE3_NATIVE_BINDING: ''
},
writeSummary: (input) => {
const summary = `${JSON.stringify(
{ ...metadata, finishedAt: new Date().toISOString(), ...input },
null,
2
)}\n`
writeFileSync(path.join(logDir, 'summary.json'), summary, 'utf8')
if (resultPath) {
const resolvedResultPath = path.resolve(ROOT, resultPath)
mkdirSync(path.dirname(resolvedResultPath), { recursive: true })
writeFileSync(resolvedResultPath, summary, 'utf8')
}
},
cleanup: () => {
log('开始清理正式包 E2E 安装')
return cleanupPackagedE2EApplication(metadata, { log })
}
}
}
export const cleanupPackagedE2EApplication = (session, { log = () => {} } = {}) => {
const installRoot = session?.installedAppPath
if (!installRoot) return { removed: false }
return cleanupPackagedE2EInstall({ installRoot, log })
}
// Cache identity must come from the installed artifact, never the current source tree.
export const readPackagedKnowledgeCacheIdentity = (
installedAppPath,
platform = process.platform
) => {
const resources =
platform === 'darwin'
? path.join(installedAppPath, 'Contents', 'Resources')
: path.join(installedAppPath, 'resources')
uncache(path.join(resources, 'app.asar'))
try {
const identity = JSON.parse(
extractFile(
path.join(resources, 'app.asar'),
'out/main/knowledge-cache-identity.json'
).toString('utf8')
)
return /^[a-f0-9]{64}$/.test(identity.split) && /^[a-f0-9]{64}$/.test(identity.embedding)
? identity
: null
} catch {
return null
}
}