返回 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 )
11
12 func installerVersionNames() []string {
13 names := []string{installlayout.DesktopBinaryName(), installlayout.CLIBinaryName()}
14 if runtime.GOOS == "windows" {
15 names = append(names, installlayout.UpdateHelperBinaryName())
16 }
17 return names
18 }
19
20 func writeInstallerStaging(t *testing.T, root, label string, includeLauncher bool) string {
21 t.Helper()
22 staging := filepath.Join(root, "versions", ".installer-"+label)
23 if err := os.MkdirAll(staging, 0o755); err != nil {
24 t.Fatal(err)
25 }
26 for _, name := range installerVersionNames() {
27 if err := os.WriteFile(filepath.Join(staging, name), []byte(label+"-"+name), 0o755); err != nil {
28 t.Fatal(err)
29 }
30 }
31 if includeLauncher {
32 name := installlayout.LauncherBinaryName()
33 if err := os.WriteFile(filepath.Join(staging, name), []byte(label+"-"+name), 0o755); err != nil {
34 t.Fatal(err)
35 }
36 }
37 return staging
38 }
39
40 func TestMigrateFlatInstallToVersioned(t *testing.T) {
41 root := t.TempDir()
42 // Flat release unit.
43 for _, name := range installlayout.AllowedVersionMembers() {
44 if err := os.WriteFile(filepath.Join(root, name), []byte("flat-"+name), 0o755); err != nil {
45 t.Fatal(err)
46 }
47 }
48 // Thin launcher entry must already exist (packaging places it). Use a
49 // non-executable marker so startLauncher fails closed without hanging.
50 _ = os.WriteFile(filepath.Join(root, "reasonix-launcher"), []byte("launcher"), 0o644)
51 _ = os.WriteFile(filepath.Join(root, "reasonix-launcher.exe"), []byte("launcher"), 0o644)
52 _ = os.WriteFile(filepath.Join(root, "Reasonix.exe"), []byte("launcher"), 0o644)
53 // Marker files that must be archived, not deleted silently without record.
54 _ = os.WriteFile(filepath.Join(root, "pending-update.json"), []byte(`{"pending":true}`), 0o644)
55 _ = os.WriteFile(filepath.Join(root, "startup-state.json"), []byte(`{}`), 0o644)
56
57 if err := migrate(root, "v1.20.0"); err != nil {
58 t.Fatal(err)
59 }
60 if !installlayout.HasCurrent(root) {
61 t.Fatal("current.json missing after migration")
62 }
63 ptr, err := installlayout.ReadCurrent(root)
64 if err != nil || ptr.ActiveVersion != "v1.20.0" {
65 t.Fatalf("pointer=%+v err=%v", ptr, err)
66 }
67 // Flat desktop must be cleaned up after successful activation.
68 if _, err := os.Stat(filepath.Join(root, installlayout.DesktopBinaryName())); !os.IsNotExist(err) {
69 t.Fatal("flat desktop should be removed after migration")
70 }
71 // Active desktop lives under versions/.
72 if _, err := installlayout.ActiveDesktopPath(root); err != nil {
73 t.Fatal(err)
74 }
75 // Second run is idempotent cleanup-only.
76 if err := migrate(root, "v1.20.0"); err != nil {
77 t.Fatalf("idempotent re-run: %v", err)
78 }
79 ptr2, err := installlayout.ReadCurrent(root)
80 if err != nil || ptr2.ActiveVersion != "v1.20.0" {
81 t.Fatalf("re-run overwrote active version: %+v", ptr2)
82 }
83 }
84
85 func TestMigrateRefusesWithoutFlatUnit(t *testing.T) {
86 root := t.TempDir()
87 if err := migrate(root, "v1.20.0"); err == nil {
88 t.Fatal("expected failure without flat desktop")
89 }
90 }
91
92 func TestMigrateRefusesCorruptCurrentPointerWithoutOverwritingIt(t *testing.T) {
93 root := t.TempDir()
94 corrupt := []byte(`{"schemaVersion":99}`)
95 current := filepath.Join(root, installlayout.CurrentFileName)
96 if err := os.WriteFile(current, corrupt, 0o644); err != nil {
97 t.Fatal(err)
98 }
99 for _, name := range installlayout.AllowedVersionMembers() {
100 if err := os.WriteFile(filepath.Join(root, name), []byte("stale-"+name), 0o755); err != nil {
101 t.Fatal(err)
102 }
103 }
104 if err := migrate(root, "v1.20.0"); err == nil {
105 t.Fatal("corrupt current.json was treated as an absent pointer")
106 }
107 got, err := os.ReadFile(current)
108 if err != nil {
109 t.Fatal(err)
110 }
111 if string(got) != string(corrupt) {
112 t.Fatalf("corrupt pointer was overwritten: %q", got)
113 }
114 }
115
116 func TestActivateInstallerStagingPublishesVersionAndRootEntries(t *testing.T) {
117 root := t.TempDir()
118 staging := writeInstallerStaging(t, root, "new", true)
119 if err := activateInstallerStaging(root, "v1.20.0", staging); err != nil {
120 t.Fatal(err)
121 }
122 ptr, err := installlayout.ReadCurrent(root)
123 if err != nil || ptr.ActiveVersion != "v1.20.0" {
124 t.Fatalf("pointer=%+v err=%v", ptr, err)
125 }
126 activeDesktop, err := installlayout.ActiveDesktopPath(root)
127 if err != nil {
128 t.Fatal(err)
129 }
130 if got, err := os.ReadFile(activeDesktop); err != nil || string(got) != "new-"+installlayout.DesktopBinaryName() {
131 t.Fatalf("active desktop=%q err=%v", got, err)
132 }
133 for _, name := range []string{installlayout.LauncherBinaryName(), installlayout.CLIBinaryName()} {
134 if info, err := os.Lstat(filepath.Join(root, name)); err != nil || !info.Mode().IsRegular() {
135 t.Fatalf("root entry %s missing or invalid: %v", name, err)
136 }
137 }
138 if alias := installlayout.PortableAliasName(); alias != "" {
139 if got, err := os.ReadFile(filepath.Join(root, alias)); err != nil || string(got) != "new-"+installlayout.LauncherBinaryName() {
140 t.Fatalf("portable alias=%q err=%v", got, err)
141 }
142 }
143 }
144
145 func TestActivateInstallerStagingFailureKeepsPreviousPointer(t *testing.T) {
146 root := t.TempDir()
147 oldStaging := writeInstallerStaging(t, root, "old", true)
148 if err := activateInstallerStaging(root, "v1.19.1", oldStaging); err != nil {
149 t.Fatal(err)
150 }
151 broken := writeInstallerStaging(t, root, "broken", false)
152 if err := activateInstallerStaging(root, "v1.20.0", broken); err == nil {
153 t.Fatal("staging without a launcher was activated")
154 }
155 ptr, err := installlayout.ReadCurrent(root)
156 if err != nil || ptr.ActiveVersion != "v1.19.1" {
157 t.Fatalf("previous pointer changed after failed activation: %+v err=%v", ptr, err)
158 }
159 if err := activateInstallerStaging(root, "v1.20.0", t.TempDir()); err == nil {
160 t.Fatal("staging outside the install root was accepted")
161 }
162 }
163
163 lines GO