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