Просмотр исходного кода

精简 Windows 客户端资源并使用真实用户目录验收升级

Samien 1 месяц назад
Родитель
Сommit
83f47b2c28

+ 15 - 7
desktop/windows-upgrade.test.mjs

@@ -3,8 +3,8 @@ import assert from 'node:assert/strict'
 import { createServer } from 'node:http'
 import { execFile } from 'node:child_process'
 import { promisify } from 'node:util'
-import { mkdtemp, readFile, writeFile, mkdir, rm, readdir, realpath, copyFile } from 'node:fs/promises'
-import { tmpdir } from 'node:os'
+import { mkdtemp, readFile, writeFile, mkdir, rm, readdir, realpath, copyFile, access } from 'node:fs/promises'
+import { tmpdir, homedir } from 'node:os'
 import { join, resolve, basename } from 'node:path'
 import { randomUUID } from 'node:crypto'
 import { _electron as electron, expect } from '@playwright/test'
@@ -16,12 +16,18 @@ test('installed NSIS client rejects a corrupt update, upgrades and preserves set
 }, async () => {
   const root = resolve(import.meta.dirname, '..'), output = join(root, 'desktop-release/windows-x64')
   const evidence = join(root, 'test-results/windows-client'), sandbox = await realpath(await mkdtemp(join(tmpdir(), 'yaoyao-upgrade-')))
-  const home = join(sandbox, '中文配置'), installed = join(sandbox, 'app'), feed = join(sandbox, 'feed')
+  // NSIS relaunches through Explorer, which correctly uses the user's normal
+  // environment instead of inheriting Playwright's fixture-only variables.
+  // This test may touch the normal profile only on a fresh disposable CI user.
+  assert.equal(process.env.GITHUB_ACTIONS, 'true', '安装升级验收只允许在一次性 GitHub Actions 用户下执行')
+  assert.ok(!process.env.YAOYAO_HOME && !process.env.HERMES_YAOYAO_HOME && !process.env.HERMES_YAOYAO_DESKTOP_TEST_HOME)
+  const home = join(homedir(), '.yaoyao'), installed = join(sandbox, 'app'), feed = join(sandbox, 'feed')
+  assert.equal(await access(home).then(() => true).catch(() => false), false, '拒绝覆盖已有用户数据')
   const executablePath = join(installed, 'Yaoyao.exe'), cacheName = 'yaoyao-upgrade-' + randomUUID()
   const originalManifest = await readFile(join(root, '.desktop-build/release.json'), 'utf8')
   const originalPackage = await readFile(join(root, '.desktop-build/shell/package.json'), 'utf8')
   const version = JSON.parse(originalPackage).version, nextVersion = version.replace(/\d+$/, value => String(Number(value) + 1))
-  let application, logs, corrupt = true, requireCookie = false, restored = 0
+  let application, logs, userData, corrupt = true, requireCookie = false, restored = 0
   const server = createServer(async (req, res) => {
     try {
       const path = new URL(req.url, 'http://localhost').pathname
@@ -67,10 +73,10 @@ test('installed NSIS client rejects a corrupt update, upgrades and preserves set
     await writeFile(join(home, 'desktop-preferences.json'), JSON.stringify(preferences))
     // This installed test copy alone uses the isolated HTTP update source.
     await writeFile(join(installed, 'resources/app-update.yml'), JSON.stringify({ provider: 'generic', url: origin + '/feed/', updaterCacheDirName: cacheName }))
-    const launch = () => electron.launch({ executablePath, env: { ...process.env,
-      HERMES_YAOYAO_DESKTOP_TEST_HOME: home, HERMES_YAOYAO_DESKTOP_TEST_AUTO_UPDATE: '1' } })
+    const launch = () => electron.launch({ executablePath, env: { ...process.env } })
     application = await launch(); let page = await application.firstWindow(); await page.waitForURL(origin + '/**')
     logs = await application.evaluate(({ app }) => app.getPath('logs'))
+    userData = await application.evaluate(({ app }) => app.getPath('userData'))
     assert.equal(await application.evaluate(({ app }) => app.getVersion()), version)
     await application.evaluate(async ({ session, safeStorage }, { origin, home }) => {
       await session.defaultSession.cookies.set({ url: origin, name: 'session', value: 'upgrade-fixture', expirationDate: Date.now() / 1000 + 3600 })
@@ -119,9 +125,11 @@ test('installed NSIS client rejects a corrupt update, upgrades and preserves set
     throw error
   } finally {
     await application?.close().catch(() => {}); await stopInstalled()
-    if (logs) await copyFile(join(logs, 'verification/server.log'), join(evidence, 'upgrade-main.log')).catch(() => {})
+    if (logs) await copyFile(join(logs, 'service/server.log'), join(evidence, 'upgrade-main.log')).catch(() => {})
     server.closeAllConnections(); await new Promise(done => server.close(done))
     await rm(sandbox, { recursive: true, force: true, maxRetries: 10, retryDelay: 500 })
+    await rm(home, { recursive: true, force: true })
+    if (userData) await rm(userData, { recursive: true, force: true, maxRetries: 10, retryDelay: 500 })
     await rm(join(process.env.LOCALAPPDATA, cacheName), { recursive: true, force: true }).catch(() => {})
   }
 })

+ 8 - 1
docs/windows-client.md

@@ -25,13 +25,20 @@ npm run desktop:test:win
 
 GitHub Actions 工作流 `Windows client` 支持手动触发、相关 PR 和 `codex/windows-client*` 分支。CI 只上传 artifacts,不发布正式 Release。下载 `yaoyao-windows-x64-unsigned`,其中包含 EXE、blockmap、`latest.yml`、`SHA256SUMS-win-x64.txt` 和构建身份。生成目录为 `desktop-release/windows-x64`。
 
+安装前在下载目录用 PowerShell 核对校验值,EXE 的 SHA-256 应与清单中同名文件一致:
+
+```powershell
+Get-ChildItem *.exe | Get-FileHash -Algorithm SHA256
+Get-Content .\SHA256SUMS-win-x64.txt
+```
+
 默认构建为未签名测试包。正式签名构建设置 `YAOYAO_WINDOWS_SIGNED=1`、`CSC_LINK`、适用的 `CSC_KEY_PASSWORD` 和证书对应的 `YAOYAO_WINDOWS_PUBLISHER`;凭据仅通过 CI secret 或本机环境注入。正式构建强制签名和更新签名校验。
 
 Windows 更新使用 NSIS,只有用户点击“重启更新”才安装,先清理设备连接和自有操作。Windows 使用 `latest.yml`,Mac 继续使用 `latest-mac.yml`。更新附件必须来自同一次构建,并在清单引用的安装包和差分文件齐备后发布;测试包不写入正式更新源。
 
 ## 验证记录与人工验收
 
-自动测试覆盖平台模式、路径边界、凭据提供器、更新状态、设备权限与来源路由。Windows CI 另外编译并自检原生助手,测试真实 PowerShell,以及打包应用的登录、DPAPI 配置保存、会话恢复、菜单与托盘行为。测试证据位于 `test-results/windows-client`。
+自动测试覆盖平台模式、路径边界、凭据提供器、更新状态、设备权限与来源路由。Windows CI 另外编译并自检原生助手,测试真实 PowerShell 及子进程清理,以及打包应用的登录、DPAPI 配置保存、会话恢复、菜单与托盘行为。交互会话可用时,测试原生截图、点击、中文/emoji 输入、组合键、显示变化、授权撤销及独立浏览器。升级测试临时构建下一个版本,实际安装、拒绝损坏包、下载、重启替换,再检查设置、Cookie、DPAPI 数据保留与卸载。临时升级版本不会上传或发布。测试证据位于 `test-results/windows-client`,实际运行结果另见验证记录。
 
 以下项目需要真实 Windows 10 和 Windows 11 交互环境,不以 CI 编译或模拟响应代替:
 

+ 3 - 0
electron-builder.yml

@@ -37,6 +37,9 @@ extraResources:
 asar: true
 npmRebuild: false
 win:
+  files:
+    - '**/*'
+    - '!**/node_modules/**'
   executableName: Yaoyao
   icon: .desktop-build/branding/icon.ico
   requestedExecutionLevel: asInvoker

+ 2 - 0
scripts/windows-desktop-release.mjs

@@ -42,6 +42,8 @@ export async function packageWindowsDesktop() {
   await build({ projectDir: root, targets: Platform.WINDOWS.createTarget(['nsis'], Arch.x64), publish: 'never',
     config: { ...signing, directories: { output: directory }, electronUpdaterCompatibility: '>=2.16' } })
   await verifyWindowsArtifacts(directory, version)
+  const { listPackage } = await import('@electron/asar')
+  if (listPackage(join(directory, 'win-unpacked/resources/app.asar')).some(name => /[\\/]node_modules[\\/]/.test(name))) throw new Error('Windows 客户端意外包含服务器或开发依赖')
   const feed = load(await readFile(join(directory, 'win-unpacked/resources/app-update.yml'), 'utf8'))
   if (feed.provider !== 'github' || feed.owner !== 'Lsamien' || feed.repo !== 'yaoyao') throw new Error('Windows 更新源配置无效')
   if (process.env.YAOYAO_WINDOWS_SIGNED === '1' && ![feed.publisherName].flat().includes(process.env.YAOYAO_WINDOWS_PUBLISHER.trim())) throw new Error('正式 Windows 更新配置缺少发布者签名校验')