diff --git a/frontend/src/hooks/useAppUpdateManager.test.tsx b/frontend/src/hooks/useAppUpdateManager.test.tsx index e2d28834..16c71130 100644 --- a/frontend/src/hooks/useAppUpdateManager.test.tsx +++ b/frontend/src/hooks/useAppUpdateManager.test.tsx @@ -167,6 +167,63 @@ describe('useAppUpdateManager', () => { })); }); + it('uses the backend download result version for progress and cached update metadata', async () => { + backendApp.CheckForUpdates.mockResolvedValue({ + success: true, + data: { + hasUpdate: true, + channel: 'dev', + currentVersion: 'dev-current', + latestVersion: 'dev-old', + assetName: 'GoNavi-dev-old-Windows-Amd64-Portable.exe', + packageType: 'portable', + installMode: 'portable', + autoRelaunch: true, + downloaded: false, + assetSize: 4096, + }, + }); + backendApp.DownloadUpdate.mockResolvedValue({ + success: true, + data: { + info: { + hasUpdate: true, + channel: 'dev', + currentVersion: 'dev-current', + latestVersion: 'dev-new', + assetName: 'GoNavi-dev-new-Windows-Amd64-Portable.exe', + packageType: 'portable', + installMode: 'portable', + autoRelaunch: true, + }, + downloadPath: 'C:\\GoNavi\\GoNavi-dev-new-Windows-Amd64-Portable.exe', + packageType: 'portable', + installMode: 'portable', + autoRelaunch: true, + }, + }); + + renderHook(); + await act(async () => { + await hook?.checkForUpdates(false); + }); + await act(async () => { + await hook?.downloadUpdate(hook?.lastUpdateInfo!, false); + }); + + expect(hook?.lastUpdateInfo).toMatchObject({ + latestVersion: 'dev-new', + assetName: 'GoNavi-dev-new-Windows-Amd64-Portable.exe', + downloaded: true, + downloadPath: 'C:\\GoNavi\\GoNavi-dev-new-Windows-Amd64-Portable.exe', + }); + expect(hook?.updateDownloadProgress).toMatchObject({ + version: 'dev-new', + key: 'dev:dev-new:portable:gonavi-dev-new-windows-amd64-portable.exe', + status: 'done', + }); + }); + it('keeps same-version Portable and MSI downloads in separate cache identities', async () => { const portableInfo = { hasUpdate: true, diff --git a/frontend/src/hooks/useAppUpdateManager.ts b/frontend/src/hooks/useAppUpdateManager.ts index 41150849..385fd9d8 100644 --- a/frontend/src/hooks/useAppUpdateManager.ts +++ b/frontend/src/hooks/useAppUpdateManager.ts @@ -314,6 +314,7 @@ export const useAppUpdateManager = ({ const total = prev.total > 0 ? prev.total : (info.assetSize || 0); return { ...prev, + version: downloadedInfo.latestVersion, key: downloadedKey, status: 'done', percent: 100, @@ -328,6 +329,7 @@ export const useAppUpdateManager = ({ // 下载到 100% 后停留在就绪态,由用户确认当前安装方式对应的更新动作。 setUpdateDownloadProgress((prev) => ({ ...prev, + version: downloadedInfo.latestVersion, open: prev.open || !updateUserDismissedRef.current, status: 'done', percent: 100, diff --git a/internal/app/update_cleanup.go b/internal/app/update_cleanup.go index d655fa15..4ffd373c 100644 --- a/internal/app/update_cleanup.go +++ b/internal/app/update_cleanup.go @@ -138,8 +138,25 @@ func launchWindowsUpdateWithCleanup(staged *stagedUpdate, targetExe string, pid return nil } -func resolveWindowsUpdateFinalTargetPath(currentTarget string, _ string) string { - return strings.TrimSpace(currentTarget) +func resolveWindowsUpdateFinalTargetPath(currentTarget string, sourcePath string) string { + currentTarget = strings.TrimSpace(currentTarget) + if currentTarget == "" { + return currentTarget + } + currentName := filepath.Base(currentTarget) + sourceName := filepath.Base(strings.TrimSpace(sourcePath)) + if isVersionedWindowsUpdatePackageName(currentName) && isVersionedWindowsUpdatePackageName(sourceName) { + return filepath.Join(filepath.Dir(currentTarget), sourceName) + } + return currentTarget +} + +func isVersionedWindowsUpdatePackageName(name string) bool { + trimmed := strings.TrimSpace(name) + lower := strings.ToLower(trimmed) + return strings.HasPrefix(trimmed, "GoNavi-") && + strings.Contains(trimmed, "-Windows-") && + strings.HasSuffix(lower, ".exe") } func prepareWindowsStagedUpdateAsset(sourcePath string, stagedDir string) (string, error) { diff --git a/internal/app/update_cleanup_test.go b/internal/app/update_cleanup_test.go index e95b5e0a..06f17d4d 100644 --- a/internal/app/update_cleanup_test.go +++ b/internal/app/update_cleanup_test.go @@ -120,12 +120,22 @@ func TestPrepareWindowsStagedUpdateAssetMovesPackageIntoStagedDir(t *testing.T) } } -func TestResolveWindowsUpdateFinalTargetPathPreservesExecutablePath(t *testing.T) { +func TestResolveWindowsUpdateFinalTargetPathUsesDownloadedVersionedPortableName(t *testing.T) { currentTarget := filepath.Join("D:", "软件", "数据库管理工具", "GoNavi", "GoNavi-dev-f930ffe-Windows-Amd64.exe") - stagedSource := filepath.Join("C:", "Temp", "gonavi-updates", "GoNavi-0.8.5-Windows-Amd64.exe") + stagedSource := filepath.Join("C:", "Temp", "gonavi-updates", "GoNavi-dev-2d5f246-Windows-Amd64-Portable.exe") + want := filepath.Join(filepath.Dir(currentTarget), filepath.Base(stagedSource)) + + if got := resolveWindowsUpdateFinalTargetPath(currentTarget, stagedSource); got != want { + t.Fatalf("Windows update target = %q, want downloaded versioned path %q", got, want) + } +} + +func TestResolveWindowsUpdateFinalTargetPathKeepsFixedExecutablePath(t *testing.T) { + currentTarget := filepath.Join("D:", "软件", "数据库管理工具", "GoNavi", "GoNavi.exe") + stagedSource := filepath.Join("C:", "Temp", "gonavi-updates", "GoNavi-dev-2d5f246-Windows-Amd64-Portable.exe") if got := resolveWindowsUpdateFinalTargetPath(currentTarget, stagedSource); got != currentTarget { - t.Fatalf("Windows update target = %q, want current executable path %q", got, currentTarget) + t.Fatalf("Windows update target = %q, want fixed executable path %q", got, currentTarget) } } diff --git a/internal/app/windows_update_integration_windows_test.go b/internal/app/windows_update_integration_windows_test.go index 2b6d1f78..b709892f 100644 --- a/internal/app/windows_update_integration_windows_test.go +++ b/internal/app/windows_update_integration_windows_test.go @@ -88,6 +88,65 @@ func TestWindowsPowerShellUpdaterHandlesUnicodeAndShellMetacharacters(t *testing t.Logf("updated target at %s (%s)", targetPath, fmt.Sprintf("%x", wantHash[:8])) } +func TestWindowsPowerShellUpdaterRenamesVersionedPortableExecutable(t *testing.T) { + root := t.TempDir() + installDir := filepath.Join(root, "install") + stagedDir := filepath.Join(root, "stage") + for _, dir := range []string{installDir, stagedDir} { + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatalf("MkdirAll %q: %v", dir, err) + } + } + + sourcePath := filepath.Join(stagedDir, "GoNavi-dev-new-Windows-Amd64-Portable.exe") + build := exec.Command("go", "build", "-ldflags=-H=windowsgui", "-o", sourcePath, "./testdata/windows_update_helper") + if output, err := build.CombinedOutput(); err != nil { + t.Fatalf("build update helper: %v\n%s", err, output) + } + sourceData, err := os.ReadFile(sourcePath) + if err != nil { + t.Fatalf("ReadFile source: %v", err) + } + wantHash := sha256.Sum256(sourceData) + + currentTargetPath := filepath.Join(installDir, "GoNavi-dev-old-Windows-Amd64.exe") + targetPath := filepath.Join(installDir, "GoNavi-dev-new-Windows-Amd64-Portable.exe") + if err := os.WriteFile(currentTargetPath, []byte("old executable"), 0o755); err != nil { + t.Fatalf("WriteFile old target: %v", err) + } + logPath := filepath.Join(stagedDir, "gonavi-update-windows-versioned.log") + scriptPath := filepath.Join(stagedDir, "update.ps1") + if err := os.WriteFile(scriptPath, []byte(buildWindowsPowerShellScript()), 0o644); err != nil { + t.Fatalf("WriteFile updater: %v", err) + } + + cmd := buildWindowsLaunchCommand(scriptPath, windowsUpdateLaunchContext{ + SourcePath: sourcePath, + TargetPath: targetPath, + CurrentTargetPath: currentTargetPath, + StagedDir: stagedDir, + LogPath: logPath, + PID: 2147483647, + }) + if output, err := cmd.CombinedOutput(); err != nil { + logData, _ := os.ReadFile(logPath) + t.Fatalf("run versioned updater: %v\nstdout/stderr:\n%s\nlog:\n%s", err, output, logData) + } + + // The helper remains alive through the updater's launch health check. + time.Sleep(7 * time.Second) + targetData, err := os.ReadFile(targetPath) + if err != nil { + t.Fatalf("ReadFile renamed target: %v", err) + } + if gotHash := sha256.Sum256(targetData); gotHash != wantHash { + t.Fatalf("renamed target hash = %x, want %x", gotHash, wantHash) + } + if _, err := os.Stat(currentTargetPath); !os.IsNotExist(err) { + t.Fatalf("expected old versioned executable to be removed, stat err=%v", err) + } +} + func TestWindowsPowerShellUpdaterSelectsExactTargetFilenameRecursivelyFromZip(t *testing.T) { root := t.TempDir() installDir := filepath.Join(root, "install")