返回 DeepSeek-Reasonix
guard_packaging_test.go
根目录 / desktop / guard_packaging_test.go
1 package main
2
3 import (
4 "bytes"
5 "os"
6 "os/exec"
7 "path/filepath"
8 "strings"
9 "testing"
10 )
11
12 func writePortableFixture(t *testing.T, dir, name, content string) {
13 t.Helper()
14 if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o755); err != nil {
15 t.Fatal(err)
16 }
17 }
18
19 func TestVerifyWindowsPortableVersionedLayout(t *testing.T) {
20 verify := filepath.Join("..", "scripts", "verify-windows-portable.sh")
21 good := t.TempDir()
22 // versioned-v1 root entries
23 writePortableFixture(t, good, "reasonix-launcher.exe", "launcher")
24 writePortableFixture(t, good, "Reasonix.exe", "launcher")
25 writePortableFixture(t, good, "reasonix-cli.exe", "cli")
26 ver := filepath.Join(good, "versions", "v1.20.0")
27 if err := os.MkdirAll(ver, 0o755); err != nil {
28 t.Fatal(err)
29 }
30 for _, name := range []string{"reasonix-desktop.exe", "reasonix-cli.exe", "reasonix-update-helper.exe"} {
31 writePortableFixture(t, ver, name, name)
32 }
33 if err := os.WriteFile(filepath.Join(good, "current.json"), []byte(`{
34 "schemaVersion": 1,
35 "activeVersion": "v1.20.0",
36 "activeDir": "versions/v1.20.0"
37 }
38 `), 0o644); err != nil {
39 t.Fatal(err)
40 }
41 if out, err := exec.Command("bash", verify, good).CombinedOutput(); err != nil {
42 t.Fatalf("valid versioned portable failed: %v\n%s", err, out)
43 }
44
45 // Flat Guard layout must be rejected.
46 flat := t.TempDir()
47 for _, name := range []string{
48 "reasonix-desktop.exe",
49 "reasonix-guard.exe",
50 "reasonix-update-helper.exe",
51 "reasonix-launcher.exe",
52 "Reasonix.exe",
53 "reasonix-cli.exe",
54 } {
55 writePortableFixture(t, flat, name, name)
56 }
57 if out, err := exec.Command("bash", verify, flat).CombinedOutput(); err == nil {
58 t.Fatalf("flat portable with guard should fail, output=%s", out)
59 }
60 }
61
62 func TestDesktopPackagesPreserveNativePlatformLaunchers(t *testing.T) {
63 buildData, err := os.ReadFile("../scripts/desktop-build.sh")
64 if err != nil {
65 t.Fatal(err)
66 }
67 build := string(buildData)
68 for _, want := range []string{
69 `CLINAME="reasonix"`,
70 `WINDOWS_CLINAME="reasonix-cli"`,
71 `./cmd/reasonix`,
72 `./cmd/reasonix-legacy-migrator`,
73 `./cmd/reasonix-launcher`,
74 `cp "$cli_out" "$app/Contents/MacOS/$CLINAME"`,
75 `macOS bundle must not include $GUARDNAME`,
76 `[ "$bundle_executable" = "$BINNAME" ]`,
77 `Print :CFBundleIconFile`,
78 `darwin_icon="$ROOT/desktop/build/darwin/icon.icns"`,
79 `cp "$darwin_icon" "$app/Contents/Resources/$bundle_icon"`,
80 `cmp -s "$darwin_icon" "$app/Contents/Resources/$bundle_icon"`,
81 `Contents/Resources/$bundle_icon`,
82 `-H windowsgui`,
83 `stamp_windows_executable "$guard_out" "Reasonix Legacy Migrator"`,
84 `stamp_windows_executable "$launcher_out" "Reasonix Launcher"`,
85 `stamp_windows_executable "build/windows/installer/$UPDATE_HELPER" "Reasonix Update Helper"`,
86 `payload_dir="$ROOT/desktop/build/windows/signing-payload"`,
87 `cp "build/bin/$BINNAME.exe" "$payload_dir/$BINNAME.exe"`,
88 `cp "$launcher_out" "$payload_dir/$LAUNCHERNAME.exe"`,
89 `cp "$guard_out" "$payload_dir/$GUARDNAME.exe"`,
90 `cp "build/windows/installer/$WINDOWS_CLINAME.exe" "$payload_dir/$WINDOWS_CLINAME.exe"`,
91 `cp "build/windows/installer/reasonix-uninstall.exe" "$payload_dir/reasonix-uninstall.exe"`,
92 `"$ROOT/scripts/package-windows-desktop.sh" "$arch" "$payload_dir"`,
93 `"$BINNAME" "$LAUNCHERNAME" "$GUARDNAME" "$CLINAME"`,
94 `Exec=reasonix-launcher`,
95 } {
96 if !strings.Contains(build, want) {
97 t.Errorf("desktop-build.sh missing packaging contract %q", want)
98 }
99 }
100 if strings.Contains(build, `Set :CFBundleExecutable $GUARDNAME`) {
101 t.Fatal("macOS package must not replace the native Wails bundle executable with Guard")
102 }
103 launcherStamp := strings.Index(build, `stamp_windows_executable "$launcher_out" "Reasonix Launcher"`)
104 payloadCopy := strings.Index(build, `cp "$launcher_out" "$payload_dir/$LAUNCHERNAME.exe"`)
105 if launcherStamp < 0 || payloadCopy < 0 || launcherStamp > payloadCopy {
106 t.Fatalf("Windows payload must copy the already-stamped launcher (stamp=%d copy=%d)", launcherStamp, payloadCopy)
107 }
108 if strings.Contains(build, `"$staging/$CLINAME.exe"`) {
109 t.Fatal("Windows package must not collide reasonix.exe with the Reasonix.exe launcher")
110 }
111 darwinIconCopy := strings.Index(build, `cp "$darwin_icon" "$app/Contents/Resources/$bundle_icon"`)
112 developerIDSign := strings.Index(build, `codesign --force --deep --timestamp --options runtime`)
113 if darwinIconCopy < 0 || developerIDSign < 0 || darwinIconCopy > developerIDSign {
114 t.Fatalf("macOS icon must be replaced before signing (icon=%d sign=%d)", darwinIconCopy, developerIDSign)
115 }
116
117 for _, want := range []string{
118 `dpkg-deb --field "$deb_path" Package | grep -x 'reasonix-desktop'`,
119 `usr/lib/reasonix/reasonix-update-helper`,
120 `usr/share/polkit-1/actions/io.reasonix.desktop.update.policy`,
121 } {
122 if !strings.Contains(build, want) {
123 t.Errorf("desktop-build.sh missing Linux deb helper contract %q", want)
124 }
125 }
126 for _, unsafe := range []string{
127 `dpkg-deb --field "$deb_path" Package | grep -qx`,
128 `dpkg-deb --field "$deb_path" Version | grep -qx`,
129 `dpkg-deb --field "$deb_path" Depends | grep -Fq`,
130 `dpkg-deb --contents "$deb_path" | grep -Eq`,
131 } {
132 if strings.Contains(build, unsafe) {
133 t.Errorf("desktop-build.sh uses early-exit grep under pipefail: %q", unsafe)
134 }
135 }
136
137 desktopEntry, err := os.ReadFile("build/linux/reasonix.desktop")
138 if err != nil {
139 t.Fatal(err)
140 }
141 if !strings.Contains(string(desktopEntry), "Exec=reasonix-launcher") || strings.Contains(string(desktopEntry), "reasonix-guard") {
142 t.Fatal("Linux desktop entry must launch the permanent launcher without Guard")
143 }
144 nfpmData, err := os.ReadFile("build/linux/nfpm.yaml")
145 if err != nil {
146 t.Fatal(err)
147 }
148 nfpm := string(nfpmData)
149 if !strings.Contains(nfpm, "dst: /usr/bin/reasonix-launcher") || strings.Contains(nfpm, "dst: /usr/bin/reasonix-guard") {
150 t.Fatal("Linux deb must install the permanent launcher and must not persist Guard")
151 }
152 if !strings.Contains(nfpm, "postinstall: ./build/linux/postinstall.sh") {
153 t.Fatal("Linux deb must refresh native desktop icon caches after install and upgrade")
154 }
155 if !strings.Contains(nfpm, "dst: /usr/share/applications/reasonix.desktop") {
156 t.Fatal("Linux deb must install the Reasonix desktop entry")
157 }
158 postInstall, err := os.ReadFile("build/linux/postinstall.sh")
159 if err != nil {
160 t.Fatal(err)
161 }
162 for _, want := range []string{"gtk-update-icon-cache", "update-desktop-database"} {
163 if !strings.Contains(string(postInstall), want) {
164 t.Errorf("Linux post-install icon repair missing %q", want)
165 }
166 }
167
168 windowsData, err := os.ReadFile("build/windows/installer/project.nsi")
169 if err != nil {
170 t.Fatal(err)
171 }
172 if !bytes.HasPrefix(windowsData, []byte{0xef, 0xbb, 0xbf}) {
173 t.Fatal("Windows installer script must have a UTF-8 BOM so native makensis accepts localized strings")
174 }
175 windows := string(windowsData)
176 for _, want := range []string{
177 `File "/oname=${REASONIX_CLI}" "${REASONIX_CLI}"`,
178 `!uninstfinalize 'cmd.exe /C copy /Y "%1" "reasonix-uninstall.exe" >NUL'`,
179 `File "/oname=uninstall.exe" "${ARG_REASONIX_SIGNED_UNINSTALLER}"`,
180 `StrCpy $R9 "$INSTDIR\versions\.installer-v${INFO_PRODUCTVERSION}-$R8"`,
181 `File "/oname=${REASONIX_LAYOUT_INSTALLER}" "${REASONIX_GUARD}"`,
182 `nsExec::ExecToLog /OEM`,
183 `Reasonix layout activator output:`,
184 `--activate-staging "$R9" --no-relaunch`,
185 `CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${REASONIX_LAUNCHER}" "" "$INSTDIR\${REASONIX_LAUNCHER}" 0`,
186 `CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${REASONIX_LAUNCHER}" "" "$INSTDIR\${REASONIX_LAUNCHER}" 0`,
187 `StrCmp $ReasonixStageMode "1" reasonix_stage_payload`,
188 `File "/oname=${REASONIX_GUARD}" "${REASONIX_GUARD}"`,
189 } {
190 if !strings.Contains(windows, want) {
191 t.Errorf("Windows installer missing versioned-layout contract %q", want)
192 }
193 }
194 if strings.Contains(windows, `FileOpen $0 "$INSTDIR\current.json" w`) ||
195 strings.Contains(windows, `SetOutPath "$INSTDIR\versions\v${INFO_PRODUCTVERSION}"`) {
196 t.Fatal("normal Windows installer must not write the live version or current.json in place")
197 }
198 if strings.Contains(windows, `ExecWait '"$PLUGINSDIR\${REASONIX_LAYOUT_INSTALLER}"`) {
199 t.Fatal("Windows installer must not discard layout activator stdout/stderr")
200 }
201 if strings.Contains(windows, `CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${REASONIX_LAUNCHER}" "" "$INSTDIR\versions\v${INFO_PRODUCTVERSION}\${PRODUCT_EXECUTABLE}" 0`) ||
202 strings.Contains(windows, `CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${REASONIX_LAUNCHER}" "" "$INSTDIR\versions\v${INFO_PRODUCTVERSION}\${PRODUCT_EXECUTABLE}" 0`) {
203 t.Fatal("Windows shortcut icon must not point into a version directory that retention removes")
204 }
205 }
206
206 lines GO