返回 DeepSeek-Reasonix
windows_update_handoff_test.go
根目录 / desktop / windows_update_handoff_test.go
1 package main
2
3 import (
4 "os"
5 "strings"
6 "testing"
7 )
8
9 func TestInstallerCommandLineUsesVisibleUpdateModeAndKeepsDFlagLast(t *testing.T) {
10 got := installerCommandLine(`C:\Temp\Reasonix Installer.exe`, `D:\Tools\Reasonix App`)
11 want := `"C:\Temp\Reasonix Installer.exe" /REASONIXUPDATE=1 /REASONIXSTAGE=1 /D=D:\Tools\Reasonix App`
12 if got != want {
13 t.Fatalf("installerCommandLine = %q, want %q", got, want)
14 }
15 if strings.Contains(got, " /S") {
16 t.Fatalf("auto-update must expose progress instead of using silent mode, got %q", got)
17 }
18 if !strings.HasSuffix(got, `/D=D:\Tools\Reasonix App`) {
19 t.Fatalf("/D= must be the final unquoted NSIS token, got %q", got)
20 }
21 }
22
23 func TestWindowsUpdateHandoffArgsCarryParentInstallAndRelaunch(t *testing.T) {
24 got := windowsUpdateHandoffArgs(
25 4242,
26 `C:\Users\Jane Doe\AppData\Local\Reasonix\updates\Reasonix-windows-amd64-installer.exe`,
27 strings.Repeat("a", 64),
28 `D:\Tools\Reasonix App`,
29 `D:\Tools\Reasonix App\reasonix-desktop.exe`,
30 "v1.6.0",
31 "2026-07-29T00:00:00Z",
32 "transaction-1",
33 )
34 want := []string{
35 "--parent-pid", "4242",
36 "--installer", `C:\Users\Jane Doe\AppData\Local\Reasonix\updates\Reasonix-windows-amd64-installer.exe`,
37 "--installer-sha256", strings.Repeat("a", 64),
38 "--to-version", "v1.6.0",
39 "--created-at", "2026-07-29T00:00:00Z",
40 "--transaction-id", "transaction-1",
41 "--install-dir", `D:\Tools\Reasonix App`,
42 "--relaunch", `D:\Tools\Reasonix App\reasonix-desktop.exe`,
43 }
44 if strings.Join(got, "\x00") != strings.Join(want, "\x00") {
45 t.Fatalf("args = %#v, want %#v", got, want)
46 }
47 }
48
49 func TestWindowsVersionedUpdateHandoffArgsDoNotRequireLegacyPendingIdentity(t *testing.T) {
50 got := windowsVersionedUpdateHandoffArgs(
51 4242,
52 `C:\Temp\Reasonix-installer.exe`,
53 strings.Repeat("b", 64),
54 `D:\Tools\Reasonix`,
55 `D:\Tools\Reasonix\reasonix-launcher.exe`,
56 "v1.20.0",
57 )
58 want := []string{
59 "--parent-pid", "4242",
60 "--installer", `C:\Temp\Reasonix-installer.exe`,
61 "--installer-sha256", strings.Repeat("b", 64),
62 "--to-version", "v1.20.0",
63 "--install-layout", "versioned-v1",
64 "--install-dir", `D:\Tools\Reasonix`,
65 "--relaunch", `D:\Tools\Reasonix\reasonix-launcher.exe`,
66 }
67 if strings.Join(got, "\x00") != strings.Join(want, "\x00") {
68 t.Fatalf("args = %#v, want %#v", got, want)
69 }
70 for _, legacy := range []string{"--created-at", "--transaction-id"} {
71 if strings.Contains(strings.Join(got, " "), legacy) {
72 t.Fatalf("versioned handoff must not carry legacy field %s", legacy)
73 }
74 }
75 }
76
77 func TestWindowsInstallerScriptWaitsBeforeCopyingExecutable(t *testing.T) {
78 data, err := os.ReadFile("build/windows/installer/project.nsi")
79 if err != nil {
80 t.Fatal(err)
81 }
82 script := string(data)
83 for _, want := range []string{
84 `!define REASONIX_LEGACY_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\Reasonix"`,
85 `!define REASONIX_LEGACY_PRODUCT_KEY "Software\reasonix\Reasonix"`,
86 `!define REASONIX_UPDATE_HELPER "reasonix-update-helper.exe"`,
87 `!define REASONIX_GUARD "reasonix-guard.exe"`,
88 `!define REASONIX_LAUNCHER "reasonix-launcher.exe"`,
89 `!define REASONIX_CLI "reasonix-cli.exe"`,
90 `!define REASONIX_PORTABLE_ENTRY "Reasonix.exe"`,
91 `!define REASONIX_LAYOUT_INSTALLER "reasonix-layout-installer.exe"`,
92 `!define REASONIX_PAYLOAD_MANIFEST "reasonix-payload.json"`,
93 `!define REASONIX_PAYLOAD_SIGNATURE "reasonix-payload.json.minisig"`,
94 "Var ReasonixUpdateMode",
95 "Var ReasonixStageMode",
96 `${GetOptions} $R0 "/REASONIXUPDATE=" $R1`,
97 `${GetOptions} $R0 "/REASONIXSTAGE=" $R2`,
98 "Function reasonix.skipSetupPageForUpdate",
99 "Function reasonix.showUpdateProgress",
100 `!define MUI_PAGE_CUSTOMFUNCTION_PRE reasonix.skipFinishPageForUpdate`,
101 "Function reasonix.skipFinishPageForUpdate",
102 `StrCmp $ReasonixUpdateMode "1" 0 reasonix_show_finish_page`,
103 "SetAutoClose true",
104 "BringToFront",
105 `LangString reasonixUpdateTitle ${LANG_ENGLISH} "Updating Reasonix"`,
106 `LangString reasonixUpdateTitle ${LANG_SIMPCHINESE} "正在更新 Reasonix"`,
107 `LangString reasonixUpdateTitle ${LANG_TRADCHINESE} "正在更新 Reasonix"`,
108 `LangString reasonixUpdateSubtitle ${LANG_ENGLISH} "Installing the verified update. Reasonix will restart automatically."`,
109 `LangString reasonixUpdateSubtitle ${LANG_SIMPCHINESE} "正在安装已验证的更新,完成后 Reasonix 将自动重启。"`,
110 `LangString reasonixUpdateSubtitle ${LANG_TRADCHINESE} "正在安裝已驗證的更新,完成後 Reasonix 將自動重新啟動。"`,
111 "Function reasonix.waitForExecutableUnlock",
112 `FileOpen $1 "$INSTDIR\${PRODUCT_EXECUTABLE}" a`,
113 `FileOpen $1 "$INSTDIR\versions\v${INFO_PRODUCTVERSION}\${PRODUCT_EXECUTABLE}" a`,
114 `FileOpen $1 "$INSTDIR\${REASONIX_GUARD}" a`,
115 `FileOpen $1 "$INSTDIR\${REASONIX_LAUNCHER}" a`,
116 `FileOpen $1 "$INSTDIR\${REASONIX_CLI}" a`,
117 `FileOpen $1 "$INSTDIR\${REASONIX_PORTABLE_ENTRY}" a`,
118 "SetErrorLevel 1618",
119 "Call reasonix.waitForExecutableUnlock",
120 `File "/oname=${REASONIX_UPDATE_HELPER}" "${REASONIX_UPDATE_HELPER}"`,
121 `File "/oname=${REASONIX_CLI}" "${REASONIX_CLI}"`,
122 `File "/oname=${REASONIX_LAYOUT_INSTALLER}" "${REASONIX_GUARD}"`,
123 `nsExec::ExecToLog /OEM`,
124 `Reasonix layout activator output:`,
125 `--activate-staging "$R9" --no-relaunch`,
126 `File "/oname=${REASONIX_PAYLOAD_MANIFEST}" "${REASONIX_PAYLOAD_MANIFEST}"`,
127 `File "/oname=${REASONIX_PAYLOAD_SIGNATURE}" "${REASONIX_PAYLOAD_SIGNATURE}"`,
128 `Delete "$INSTDIR\${REASONIX_UPDATE_HELPER}"`,
129 `Delete "$INSTDIR\${REASONIX_CLI}"`,
130 `DeleteRegValue HKCU "${REASONIX_LEGACY_PRODUCT_KEY}" ""`,
131 `!insertmacro reasonix.deleteLegacyInstallerStateIfOwned`,
132 } {
133 if !strings.Contains(script, want) {
134 t.Fatalf("project.nsi missing %q", want)
135 }
136 }
137 finishPageHook := strings.Index(script, "!define MUI_PAGE_CUSTOMFUNCTION_PRE reasonix.skipFinishPageForUpdate")
138 finishPage := strings.Index(script, "!insertmacro MUI_PAGE_FINISH")
139 if finishPageHook < 0 || finishPage < 0 || finishPageHook > finishPage {
140 t.Fatalf("update-only finish page hook must be attached to MUI_PAGE_FINISH (hook=%d page=%d)", finishPageHook, finishPage)
141 }
142 wait := strings.Index(script, "Call reasonix.waitForExecutableUnlock")
143 copyFiles := strings.Index(script, "!insertmacro wails.files")
144 if wait < 0 || copyFiles < 0 || wait > copyFiles {
145 t.Fatalf("installer must wait for the running exe to unlock before wails.files (wait=%d copy=%d)", wait, copyFiles)
146 }
147 stageBranch := strings.Index(script, "StrCmp $ReasonixStageMode \"1\" reasonix_stage_payload")
148 if stageBranch < 0 || stageBranch > copyFiles {
149 t.Fatalf("staging mode must bypass live executable unlock before payload extraction (branch=%d copy=%d)", stageBranch, copyFiles)
150 }
151 if !strings.Contains(script, "Goto reasonix_section_done") {
152 t.Fatal("staging mode must skip registry, shortcuts, associations, and uninstaller")
153 }
154 if strings.Contains(script, `FileOpen $0 "$INSTDIR\current.json" w`) {
155 t.Fatal("normal installer must delegate the current.json commit to the atomic Go activator")
156 }
157 writeCurrent := strings.Index(script, `!insertmacro reasonix.writeUninstaller`)
158 deleteLegacy := strings.Index(script, `!insertmacro reasonix.deleteLegacyInstallerStateIfOwned`)
159 if writeCurrent < 0 || deleteLegacy < 0 || writeCurrent > deleteLegacy {
160 t.Fatalf("installer must write the current uninstall entry before reconciling owned legacy state (write=%d delete=%d)", writeCurrent, deleteLegacy)
161 }
162 legacyMacro := script[strings.Index(script, `!macro reasonix.deleteLegacyInstallerStateIfOwned`):strings.Index(script, `!macro reasonix.deleteUninstaller`)]
163 deleteLegacyLocation := strings.Index(legacyMacro, `DeleteRegValue HKCU "${REASONIX_LEGACY_PRODUCT_KEY}" ""`)
164 deleteLegacyAlias := strings.Index(legacyMacro, `DeleteRegKey HKCU "${REASONIX_LEGACY_UNINST_KEY}"`)
165 if deleteLegacyLocation < 0 || deleteLegacyAlias < 0 || deleteLegacyLocation > deleteLegacyAlias {
166 t.Fatalf("installer must clear the same-root Tauri install-location breadcrumb before deleting its uninstall alias (location=%d alias=%d)", deleteLegacyLocation, deleteLegacyAlias)
167 }
168 metadataBranch := strings.Index(script, `reasonix_stage_payload:`)
169 metadataFile := strings.Index(script, `File "/oname=${REASONIX_PAYLOAD_MANIFEST}"`)
170 normalInstall := strings.Index(script, `reasonix_normal_install:`)
171 if metadataBranch < 0 || metadataFile < 0 || normalInstall < 0 || metadataBranch > metadataFile || metadataFile > normalInstall {
172 t.Fatalf("payload manifest must be extracted only in staging mode (branch=%d file=%d)", metadataBranch, metadataFile)
173 }
174 }
175
176 func TestDesktopBuildScriptCompilesAndPackagesWindowsUpdateHelper(t *testing.T) {
177 data, err := os.ReadFile("../scripts/desktop-build.sh")
178 if err != nil {
179 t.Fatal(err)
180 }
181 script := string(data)
182 for _, want := range []string{
183 `UPDATE_HELPER="reasonix-update-helper.exe"`,
184 `go build -trimpath -o "$windows_resource_tool" ./cmd/windows-resource`,
185 `GOOS=windows GOARCH="$arch" go build`,
186 `./cmd/update-helper`,
187 `build/windows/installer/$UPDATE_HELPER`,
188 `stamp_windows_executable "build/windows/installer/$UPDATE_HELPER"`,
189 `cp "build/windows/installer/$UPDATE_HELPER" "$payload_dir/$UPDATE_HELPER"`,
190 } {
191 if !strings.Contains(script, want) {
192 t.Fatalf("desktop-build.sh missing %q", want)
193 }
194 }
195
196 packageData, err := os.ReadFile("../scripts/package-windows-desktop.sh")
197 if err != nil {
198 t.Fatal(err)
199 }
200 packager := string(packageData)
201 for _, want := range []string{
202 `cp "$PAYLOAD/$UPDATE_HELPER" "$INSTALLER_DIR/$UPDATE_HELPER"`,
203 `cp "$PAYLOAD/$UPDATE_HELPER" "$portable_staging/versions/$version_label/$UPDATE_HELPER"`,
204 `"$ROOT/scripts/verify-windows-portable.sh" "$portable_staging"`,
205 } {
206 if !strings.Contains(packager, want) {
207 t.Fatalf("package-windows-desktop.sh missing %q", want)
208 }
209 }
210 }
211
212 func TestWindowsUpdateRequiresObservedHelperHandoff(t *testing.T) {
213 data, err := os.ReadFile("updater_windows.go")
214 if err != nil {
215 t.Fatal(err)
216 }
217 source := string(data)
218 if !strings.Contains(source, "return startWindowsUpdateHelper(") {
219 t.Fatal("Windows update handoff does not require the observed helper path")
220 }
221 if strings.Contains(source, "return installerCommand(installerPath, installDir).Start()") {
222 t.Fatal("Windows update silently falls back to an unobserved installer")
223 }
224 if !strings.Contains(source, "cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}") {
225 t.Fatal("Windows handoff helper should stay hidden while NSIS shows update progress")
226 }
227 helperData, err := os.ReadFile("cmd/update-helper/main_windows.go")
228 if err != nil {
229 t.Fatal(err)
230 }
231 helperSource := string(helperData)
232 if !strings.Contains(helperSource, "reconcileWindowsUninstallRegistrationFn(installDir, toVersion)") {
233 t.Fatal("versioned Windows activation must refresh its managed uninstall registration")
234 }
235 if strings.Contains(helperSource, "installerCommandLine(installer, installDir), HideWindow: true") {
236 t.Fatal("update helper still hides the NSIS progress window")
237 }
238 }
239
239 lines GO