From b67ef2bc2f95f5eaea1b1329a07267b53af5410a Mon Sep 17 00:00:00 2001 From: "DAL\\Administrator" <3452720699@qq.com> Date: Fri, 11 Sep 2026 19:49:59 +0800 Subject: [PATCH] test(desktop): validate installed and portable update modes before release --- AGENTS.md | 12 +++++++++++ .../e2e/desktop-production-boundary.spec.mjs | 9 ++++++-- .../scripts/smoke-candidate-artifacts.mjs | 9 ++++++-- apps/desktop/scripts/smoke-update-mode.mjs | 11 ++++++++++ apps/desktop/tests/smoke-update-mode.test.mjs | 21 +++++++++++++++++++ docs/WINDOWS_ELECTRON_RELEASE_ZH.md | 8 +++++++ 6 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 apps/desktop/scripts/smoke-update-mode.mjs create mode 100644 apps/desktop/tests/smoke-update-mode.test.mjs diff --git a/AGENTS.md b/AGENTS.md index 56b70b3..8967988 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -90,6 +90,18 @@ On Windows, `\\wsl.localhost\...` and `\\wsl$\...` SQLite Homes are diagnostic-o - Run `npm run architecture:check` and `npm test` for Core changes; platform skips and failures must be reported. The first command reuses workspace boundary checks and Provider I/O tests, not a second rule engine. - Behavior changes require an explicit ADR/contract/fixture update and matching user docs. Do not weaken an invariant test to bless accidental drift. README is not a substitute for the current Core guide. +## Mandatory local preflight and release flow + +Follow this order: **local preflight passes → CI cross-platform and real installation acceptance passes → publish with explicit authorization**. See [Windows release procedure](docs/WINDOWS_ELECTRON_RELEASE_ZH.md) for commands and evidence requirements. + +- Before pushing, review all affected contracts, test expectations and build modes together, then run the relevant local tests. Do not use Actions as the first check for failures reproducible locally. Report unavailable local checks explicitly and leave them to the matching CI environment. +- Desktop changes require relevant production-bundle checks, not just development-mode tests. Update changes must cover both portable/manual and authorized installed-updater modes; derive expectations from the intended channel/container, never from the application's returned value. +- Reuse an existing build for repeated local checks only when its product sources, dependencies, build configuration and version are unchanged. Test-script or documentation-only edits do not require another local package build. Record the build provenance; an old local package is not release evidence for a new SHA. +- Use isolated fixtures under D-drive temporary directories on this Windows workstation; keep desktop tests hidden or on a secondary monitor. Never test against a real Codex Home. An installed-layout fixture is not proof of real installation or upgrade; run actual installer/uninstaller tests in an isolated environment, since a custom installation directory may still affect the user's registered app. +- CI must validate the final source SHA on the required platforms and exercise real release containers, including installation/extraction, startup, SQLite loading, fixture Sync → Restore, graceful exit and applicable uninstall cleanup. All applicable gates must pass before publication; local success does not replace them. +- Diagnose failed jobs before rerunning. For a verified transient failure on unchanged code, rerun only failed jobs and reuse successful same-run/same-SHA artifacts where the release procedure permits. Do not repeatedly rebuild everything, weaken assertions or increase timeouts merely to obtain a green run. +- Publish only artifacts tied to the accepted final SHA, version and build configuration, with verified hashes. Product/build changes require rebuilding affected artifacts and rerunning affected acceptance checks. Preserve the required final-SHA gates even when reducing redundant local builds. + ## Reporting State the current Provider, whether rollout and SQLite metadata are aligned, the resolved database path, the backup created by a write operation, and whether the result was complete, partial, or blocked. Distinguish automated tests from real-machine validation and list anything not run. diff --git a/apps/desktop/e2e/desktop-production-boundary.spec.mjs b/apps/desktop/e2e/desktop-production-boundary.spec.mjs index 15dcba7..20590c8 100644 --- a/apps/desktop/e2e/desktop-production-boundary.spec.mjs +++ b/apps/desktop/e2e/desktop-production-boundary.spec.mjs @@ -10,12 +10,14 @@ import { createDesktopReadOnlyFixture } from "../../../test-support/desktop-read import { createDesktopSyncSwitchFixture } from "../../../test-support/desktop-sync-switch-fixture.mjs"; import { shouldRetryPackagedCdpActivation } from "./packaged-cdp-retry.mjs"; import { captureViewport } from "./viewport-screenshot.mjs"; +import { parseSmokeUpdateMode } from "../scripts/smoke-update-mode.mjs"; import { claimDailyUpdateCheck } from "../dist/main/daily-update-check.js"; import { OperationLogService } from "../dist/main/operation-log-service.js"; const require = createRequire(import.meta.url); const desktopRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const packagedExecutable = process.env.CPS_DESKTOP_EXECUTABLE; +const expectedUpdateMode = parseSmokeUpdateMode(process.env.CPS_EXPECTED_UPDATE_MODE); const electronExecutable = packagedExecutable || require("electron"); const PRODUCTION_SMOKE_TIMEOUT_MS = 180_000; const PRODUCTION_OPERATION_TIMEOUT_MS = 30_000; @@ -348,12 +350,15 @@ test("production desktop bundle has no test bridge and reads the real SQLite fix expect(updateStatus.installAllowed).toBe(false); expect(updateStatus.currentVersion).toBeTruthy(); if (packagedExecutable) { - expect(updateStatus.mode).toBe("manual"); + expect(updateStatus.mode).toBe(expectedUpdateMode === "manual" ? "manual" : undefined); expect(updateStatus.state).toBe("idle"); await page.getByRole("button", { name: "Settings", exact: true }).click(); await expect(page.getByRole("button", { name: "Check for updates" })).toBeVisible(); await expect(page.getByText(/Current version:/)).toBeVisible(); - await expect(page.getByText(/This portable or local build opens/)).toBeVisible(); + const manualHint = page.getByText(/This portable or local build opens/); + const installerHint = page.getByText(/Download when ready, then confirm a restart to install/); + await expect(expectedUpdateMode === "manual" ? manualHint : installerHint).toBeVisible(); + await expect(expectedUpdateMode === "manual" ? installerHint : manualHint).toBeHidden(); // No network check or download is launched by opening Settings. expect((await page.evaluate(() => window.codexProvider.updates.getStatus())).state).toBe("idle"); await page.getByRole("button", { name: "Overview", exact: true }).click(); diff --git a/apps/desktop/scripts/smoke-candidate-artifacts.mjs b/apps/desktop/scripts/smoke-candidate-artifacts.mjs index 9d7c5e4..2e686b8 100644 --- a/apps/desktop/scripts/smoke-candidate-artifacts.mjs +++ b/apps/desktop/scripts/smoke-candidate-artifacts.mjs @@ -12,6 +12,7 @@ import { } from "./release-audit.mjs"; import { assertDesktopArtifactVersion } from "./desktop-artifact-version.mjs"; +import { expectedContainerUpdateMode } from "./smoke-update-mode.mjs"; import { updateAssetNames, auditWindowsUpdateAssets, auditWindowsUpdateConfiguration } from "./windows-update-artifacts.mjs"; const LINUX_SANDBOX_HELPER = fileURLToPath(new URL("./configure-linux-sandbox.mjs", import.meta.url)); const target = process.env.CPS_CANDIDATE_TARGET; @@ -121,7 +122,7 @@ async function waitForRemoval(targetPath, timeoutMs = 15_000) { throw new Error(`Timed out waiting for uninstall cleanup: ${path.basename(targetPath)}`); } -function runProductSmoke(executable) { +function runProductSmoke(executable, expectedUpdateMode) { const npmCli = process.env.npm_execpath; if (!npmCli) throw new Error("npm_execpath is required for candidate smoke."); run(process.execPath, [ @@ -135,6 +136,7 @@ function runProductSmoke(executable) { env: { ...process.env, CPS_DESKTOP_EXECUTABLE: executable, + CPS_EXPECTED_UPDATE_MODE: expectedUpdateMode, CPS_DESKTOP_WINDOW_DISPLAY: "hidden" } }); @@ -187,7 +189,10 @@ async function inspectAndSmokeContainer({ appRoot, executable, assetName, contai assert.equal(config.signaturePolicy, staging.updateAudit.signaturePolicy); } await configureLinuxSandbox(appRoot); - runProductSmoke(executable); + runProductSmoke(executable, expectedContainerUpdateMode({ + updaterAuthorized: updateAssets.length > 0, + containerKind + })); return Object.freeze({ assetName, containerKind, diff --git a/apps/desktop/scripts/smoke-update-mode.mjs b/apps/desktop/scripts/smoke-update-mode.mjs new file mode 100644 index 0000000..6558b65 --- /dev/null +++ b/apps/desktop/scripts/smoke-update-mode.mjs @@ -0,0 +1,11 @@ +// Test expectations come from the audited container, never from the app's response. +export function expectedContainerUpdateMode({ updaterAuthorized, containerKind }) { + return updaterAuthorized && containerKind === "nsis" ? "installer" : "manual"; +} + +export function parseSmokeUpdateMode(value = "manual") { + if (value !== "manual" && value !== "installer") { + throw new Error("CPS_EXPECTED_UPDATE_MODE must be manual or installer."); + } + return value; +} diff --git a/apps/desktop/tests/smoke-update-mode.test.mjs b/apps/desktop/tests/smoke-update-mode.test.mjs new file mode 100644 index 0000000..f6a65b8 --- /dev/null +++ b/apps/desktop/tests/smoke-update-mode.test.mjs @@ -0,0 +1,21 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { expectedContainerUpdateMode, parseSmokeUpdateMode } from "../scripts/smoke-update-mode.mjs"; + +test("only an authorized NSIS container expects the installed updater", () => { + for (const updaterAuthorized of [false, true]) { + for (const containerKind of ["nsis", "zip", "dmg", "appimage", "deb"]) { + assert.equal(expectedContainerUpdateMode({ updaterAuthorized, containerKind }), + updaterAuthorized && containerKind === "nsis" ? "installer" : "manual"); + } + } +}); + +test("direct unpacked smoke defaults to manual; invalid expectations fail closed", () => { + assert.equal(parseSmokeUpdateMode(), "manual"); + assert.equal(parseSmokeUpdateMode("installer"), "installer"); + assert.equal(parseSmokeUpdateMode("manual"), "manual"); + for (const value of ["", "auto", "installed", null]) { + assert.throws(() => parseSmokeUpdateMode(value), /CPS_EXPECTED_UPDATE_MODE/); + } +}); diff --git a/docs/WINDOWS_ELECTRON_RELEASE_ZH.md b/docs/WINDOWS_ELECTRON_RELEASE_ZH.md index 495fe8d..a96ba6f 100644 --- a/docs/WINDOWS_ELECTRON_RELEASE_ZH.md +++ b/docs/WINDOWS_ELECTRON_RELEASE_ZH.md @@ -42,6 +42,14 @@ 本地测试用 D 盘独立临时目录,窗口隐藏或在副屏;不触碰真实 Codex Home。CI 使用 runner 的临时夹具环境。 +### 推送前先做本地预检 + +- 先一次性核对受影响的 IPC 合同、单元测试和生产 E2E 断言,再运行对应测试;不能只验证开发态或 unpacked 的一种模式。 +- 更新功能同时验证便携版的手动更新和授权安装版的应用内更新。容器验收按已审计的授权渠道与容器类型传入 `CPS_EXPECTED_UPDATE_MODE`:只有授权 NSIS 是 `installer`,其余为 `manual`;不能根据应用返回值反推预期。 +- 本地复用同一生产构建检查两种布局时,安装版布局只能使用 D 盘独立副本;测试仅检查状态与界面,不执行下载、安装或卸载。这不算真实 NSIS 安装或线上跨版本升级通过。 +- 不在日常用户环境运行真实 NSIS smoke;自定义安装目录仍可能影响同一应用的注册信息。真实安装、退出、卸载在隔离 Windows 环境或 CI 完成,其他平台在对应 runner 验证。 +- 仅测试脚本变更无需重复本地打包;记录使用的构建来源。发布前仍须对最终 SHA 的正式产物完成完整验收,不能拿旧本地包代替新产物证据。 + ## 4. RC 与正式版 未取得对应版本的正式版批准时,公开包使用 Windows RC。维护者已于 2026-09-08 批准未签名、手动安装的 Windows `1.0.0` 正式版;后续严格 `1.0.x` 正式版仍须逐版取得发布批准。这不替代最终 SHA 的 CI/产物验收,也不包含自动安装、签名或全部平台 stable。RC 不会被现有正式版本查更推荐;正式 `1.0.x` 可由 Electron 查更入口推荐并打开下载页。