返回 DeepSeek-Reasonix
main_test.go
1 package main
2
3 import (
4 "os"
5 "path/filepath"
6 "runtime"
7 "testing"
8
9 "reasonix/internal/installlayout"
10 "reasonix/internal/testenv"
11 )
12
13 func TestMain(m *testing.M) {
14 testenv.RunWithIsolatedUserState(m)
15 }
16
17 func installerVersionNames() []string {
18 names := []string{installlayout.DesktopBinaryName(), installlayout.CLIBinaryName()}
19 if runtime.GOOS == "windows" {
20 names = append(names, installlayout.UpdateHelperBinaryName())
21 }
22 return names
23 }
24
25 // writeFlatUnit lays out a pre-migration release root the way the portable
26 // archives ship it: the CLI carries its flat name, not the versioned one.
27 func writeFlatUnit(t *testing.T, root, label string) {
28 t.Helper()
29 names := []string{installlayout.DesktopBinaryName(), installlayout.FlatCLIBinaryName()}
30 if runtime.GOOS == "windows" {
31 names = append(names, installlayout.UpdateHelperBinaryName())
32 }
33 for _, name := range names {
34 if err := os.WriteFile(filepath.Join(root, name), []byte(label+"-"+name), 0o755); err != nil {
35 t.Fatal(err)
36 }
37 }
38 }
39
40 func writeInstallerStaging(t *testing.T, root, label string, includeLauncher bool) string {
41 t.Helper()
42 staging := filepath.Join(root, "versions", ".installer-"+label)
43 if err := os.MkdirAll(staging, 0o755); err != nil {
44 t.Fatal(err)
45 }
46 for _, name := range installerVersionNames() {
47 if err := os.WriteFile(filepath.Join(staging, name), []byte(label+"-"+name), 0o755); err != nil {
48 t.Fatal(err)
49 }
50 }
51 for _, name := range installlayout.ShellRequiredNames(runtime.GOOS) {
52 p := filepath.Join(staging, filepath.FromSlash(name))
53 if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
54 t.Fatal(err)
55 }
56 if err := os.WriteFile(p, []byte(label+"-"+name), 0o755); err != nil {
57 t.Fatal(err)
58 }
59 }
60 if includeLauncher {
61 name := installlayout.LauncherBinaryName()
62 if err := os.WriteFile(filepath.Join(staging, name), []byte(label+"-"+name), 0o755); err != nil {
63 t.Fatal(err)
64 }
65 }
66 return staging
67 }
68
69 func TestMigrateFlatInstallToVersioned(t *testing.T) {
70 root := t.TempDir()
71 writeFlatUnit(t, root, "flat")
72 // Thin launcher entry must already exist (packaging places it). Use a
73 // non-executable marker so startLauncher fails closed without hanging.
74 _ = os.WriteFile(filepath.Join(root, "reasonix-launcher"), []byte("launcher"), 0o644)
75 _ = os.WriteFile(filepath.Join(root, "reasonix-launcher.exe"), []byte("launcher"), 0o644)
76 _ = os.WriteFile(filepath.Join(root, "Reasonix.exe"), []byte("launcher"), 0o644)
77 // Marker files that must be archived, not deleted silently without record.
78 _ = os.WriteFile(filepath.Join(root, "pending-update.json"), []byte(`{"pending":true}`), 0o644)
79 _ = os.WriteFile(filepath.Join(root, "startup-state.json"), []byte(`{}`), 0o644)
80
81 if err := migrateWithRelaunch(root, "v1.20.0", true); err != nil {
82 t.Fatal(err)
83 }
84 if !installlayout.HasCurrent(root) {
85 t.Fatal("current.json missing after migration")
86 }
87 ptr, err := installlayout.ReadCurrent(root)
88 if err != nil || ptr.ActiveVersion != "v1.20.0" {
89 t.Fatalf("pointer=%+v err=%v", ptr, err)
90 }
91 // Flat desktop and CLI must be cleaned up after successful activation.
92 for _, name := range []string{installlayout.DesktopBinaryName(), installlayout.FlatCLIBinaryName()} {
93 if _, err := os.Stat(filepath.Join(root, name)); !os.IsNotExist(err) {
94 t.Fatalf("flat %s should be removed after migration", name)
95 }
96 }
97 if _, err := os.Stat(filepath.Join(root, "versions", "v1.20.0", installlayout.CLIBinaryName())); err != nil {
98 t.Fatalf("versioned CLI missing after migration: %v", err)
99 }
100 // Active desktop lives under versions/.
101 if _, err := installlayout.ActiveDesktopPath(root); err != nil {
102 t.Fatal(err)
103 }
104 // Second run is idempotent cleanup-only.
105 if err := migrateWithRelaunch(root, "v1.20.0", true); err != nil {
106 t.Fatalf("idempotent re-run: %v", err)
107 }
108 ptr2, err := installlayout.ReadCurrent(root)
109 if err != nil || ptr2.ActiveVersion != "v1.20.0" {
110 t.Fatalf("re-run overwrote active version: %+v", ptr2)
111 }
112 }
113
114 func TestMigrateRefusesWithoutFlatUnit(t *testing.T) {
115 root := t.TempDir()
116 if err := migrateWithRelaunch(root, "v1.20.0", true); err == nil {
117 t.Fatal("expected failure without flat desktop")
118 }
119 }
120
121 func TestMigrateRefusesCorruptCurrentPointerWithoutOverwritingIt(t *testing.T) {
122 root := t.TempDir()
123 corrupt := []byte(`{"schemaVersion":99}`)
124 current := filepath.Join(root, installlayout.CurrentFileName)
125 if err := os.WriteFile(current, corrupt, 0o644); err != nil {
126 t.Fatal(err)
127 }
128 writeFlatUnit(t, root, "stale")
129 if err := migrateWithRelaunch(root, "v1.20.0", true); err == nil {
130 t.Fatal("corrupt current.json was treated as an absent pointer")
131 }
132 got, err := os.ReadFile(current)
133 if err != nil {
134 t.Fatal(err)
135 }
136 if string(got) != string(corrupt) {
137 t.Fatalf("corrupt pointer was overwritten: %q", got)
138 }
139 }
140
141 func TestActivateInstallerStagingPublishesVersionAndRootEntries(t *testing.T) {
142 root := t.TempDir()
143 staging := writeInstallerStaging(t, root, "new", true)
144 if err := activateInstallerStaging(root, "v1.20.0", staging); err != nil {
145 t.Fatal(err)
146 }
147 ptr, err := installlayout.ReadCurrent(root)
148 if err != nil || ptr.ActiveVersion != "v1.20.0" {
149 t.Fatalf("pointer=%+v err=%v", ptr, err)
150 }
151 activeDesktop, err := installlayout.ActiveDesktopPath(root)
152 if err != nil {
153 t.Fatal(err)
154 }
155 if got, err := os.ReadFile(activeDesktop); err != nil || string(got) != "new-"+installlayout.DesktopBinaryName() {
156 t.Fatalf("active desktop=%q err=%v", got, err)
157 }
158 for _, name := range []string{installlayout.CanonicalLauncherBinaryName(), installlayout.CLIBinaryName()} {
159 if info, err := os.Lstat(filepath.Join(root, name)); err != nil || !info.Mode().IsRegular() {
160 t.Fatalf("root entry %s missing or invalid: %v", name, err)
161 }
162 }
163 if alias := installlayout.PortableAliasName(); alias != "" {
164 if got, err := os.ReadFile(filepath.Join(root, alias)); err != nil || string(got) != "new-"+installlayout.LauncherBinaryName() {
165 t.Fatalf("portable alias=%q err=%v", got, err)
166 }
167 if _, err := os.Lstat(filepath.Join(root, installlayout.LauncherBinaryName())); !os.IsNotExist(err) {
168 t.Fatalf("fresh installation created a legacy entry: %v", err)
169 }
170 }
171 }
172
173 func TestActivateInstallerStagingPrefersThinCLIEntryAndRejectsInvalidEntry(t *testing.T) {
174 root := t.TempDir()
175 staging := writeInstallerStaging(t, root, "new", true)
176 entry := filepath.Join(staging, "app", "resources", "bin", "reasonix-cli-launcher.exe")
177 if err := os.MkdirAll(filepath.Dir(entry), 0o755); err != nil {
178 t.Fatal(err)
179 }
180 if err := os.WriteFile(entry, []byte("thin-entry"), 0o755); err != nil {
181 t.Fatal(err)
182 }
183 if err := activateInstallerStaging(root, "v1.39.0", staging); err != nil {
184 t.Fatal(err)
185 }
186 rootCLI, err := os.ReadFile(filepath.Join(root, installlayout.CLIBinaryName()))
187 if err != nil || string(rootCLI) != "thin-entry" {
188 t.Fatalf("root CLI=%q err=%v, want thin entry", rootCLI, err)
189 }
190 activeCLI, err := installlayout.ActiveCLIPath(root)
191 if err != nil {
192 t.Fatal(err)
193 }
194 fullCLI, err := os.ReadFile(activeCLI)
195 if err != nil || string(fullCLI) != "new-"+installlayout.CLIBinaryName() {
196 t.Fatalf("active CLI=%q err=%v, want full CLI", fullCLI, err)
197 }
198
199 badRoot := t.TempDir()
200 badStaging := writeInstallerStaging(t, badRoot, "bad", true)
201 badEntry := filepath.Join(badStaging, "app", "resources", "bin", "reasonix-cli-launcher.exe")
202 if err := os.MkdirAll(filepath.Dir(badEntry), 0o755); err != nil {
203 t.Fatal(err)
204 }
205 if err := os.Symlink(filepath.Join(badStaging, installlayout.CLIBinaryName()), badEntry); err != nil {
206 t.Fatal(err)
207 }
208 if err := activateInstallerStaging(badRoot, "v1.39.0", badStaging); err == nil {
209 t.Fatal("installer accepted a symlink CLI entry")
210 }
211 if installlayout.HasCurrent(badRoot) {
212 t.Fatal("invalid CLI entry committed current.json")
213 }
214 }
215
216 func TestActivateInstallerStagingKeepsExistingLauncher(t *testing.T) {
217 root := t.TempDir()
218 legacy := filepath.Join(root, installlayout.LauncherBinaryName())
219 if err := os.WriteFile(legacy, []byte("old"), 0o755); err != nil {
220 t.Fatal(err)
221 }
222 staging := writeInstallerStaging(t, root, "new", true)
223 if err := activateInstallerStaging(root, "v1.39.0", staging); err != nil {
224 t.Fatal(err)
225 }
226 for _, name := range []string{installlayout.LauncherBinaryName(), installlayout.CanonicalLauncherBinaryName()} {
227 body, err := os.ReadFile(filepath.Join(root, name))
228 if err != nil || string(body) != "new-"+installlayout.LauncherBinaryName() {
229 t.Fatalf("%s=%q (%v)", name, body, err)
230 }
231 }
232 }
233
234 func TestActivateInstallerStagingFailureKeepsPreviousPointer(t *testing.T) {
235 root := t.TempDir()
236 oldStaging := writeInstallerStaging(t, root, "old", true)
237 if err := activateInstallerStaging(root, "v1.19.1", oldStaging); err != nil {
238 t.Fatal(err)
239 }
240 broken := writeInstallerStaging(t, root, "broken", false)
241 if err := activateInstallerStaging(root, "v1.20.0", broken); err == nil {
242 t.Fatal("staging without a launcher was activated")
243 }
244 ptr, err := installlayout.ReadCurrent(root)
245 if err != nil || ptr.ActiveVersion != "v1.19.1" {
246 t.Fatalf("previous pointer changed after failed activation: %+v err=%v", ptr, err)
247 }
248 if err := activateInstallerStaging(root, "v1.20.0", t.TempDir()); err == nil {
249 t.Fatal("staging outside the install root was accepted")
250 }
251 }
252
252 lines GO