返回 DeepSeek-Reasonix
migration_test.go
根目录 / internal / migration / migration_test.go
1 package migration
2
3 import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8
9 "reasonix/internal/config"
10 "reasonix/internal/event"
11 )
12
13 const legacyMessageLog = `{"role":"user","content":"hello from v0.x"}
14 {"role":"assistant","content":"hi there"}
15 `
16
17 func migrationRescueHome(t *testing.T) string {
18 t.Helper()
19 home := t.TempDir()
20 t.Setenv("HOME", home)
21 t.Setenv("USERPROFILE", home)
22 t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
23 t.Setenv("AppData", filepath.Join(home, "AppData"))
24 t.Setenv("REASONIX_HOME", "")
25 t.Setenv("REASONIX_STATE_HOME", filepath.Join(home, "new-state"))
26 t.Setenv("REASONIX_CREDENTIALS_STORE", "file")
27 t.Chdir(t.TempDir())
28 return home
29 }
30
31 func isolateMigrationHome(t *testing.T) string {
32 t.Helper()
33 home := migrationRescueHome(t)
34 t.Setenv("REASONIX_HOME", filepath.Join(home, "new-reasonix"))
35 t.Setenv("REASONIX_STATE_HOME", "")
36 return home
37 }
38
39 func TestRunLegacyRescueImportsSessionsAndEmitsProgress(t *testing.T) {
40 home := migrationRescueHome(t)
41 legacyDir := filepath.Join(home, ".reasonix", "sessions")
42 if err := os.MkdirAll(legacyDir, 0o755); err != nil {
43 t.Fatal(err)
44 }
45 if err := os.WriteFile(filepath.Join(legacyDir, "old-chat.jsonl"), []byte(legacyMessageLog), 0o644); err != nil {
46 t.Fatal(err)
47 }
48
49 var notices []string
50 res := RunLegacyRescue(event.FuncSink(func(e event.Event) {
51 if e.Kind == event.Notice {
52 notices = append(notices, e.Text)
53 }
54 }))
55 if res.ConfigErr != nil {
56 t.Fatalf("config migration error: %v", res.ConfigErr)
57 }
58 if len(res.SessionErrs) != 0 {
59 t.Fatalf("session migration errors: %v", res.SessionErrs)
60 }
61 if got := totalImported(res.SessionImports); got != 1 {
62 t.Fatalf("imported sessions = %d, want 1; imports=%+v", got, res.SessionImports)
63 }
64 if _, err := os.Stat(filepath.Join(config.SessionDir(), "old-chat.jsonl")); err != nil {
65 t.Fatalf("migrated session missing: %v", err)
66 }
67 joined := strings.Join(notices, "\n")
68 for _, want := range []string{
69 "migration rescue: checking legacy config and credentials",
70 "migration rescue: scanning legacy sessions",
71 "imported 1 past session(s)",
72 "migration rescue complete: imported 1 past session(s)",
73 } {
74 if !strings.Contains(joined, want) {
75 t.Fatalf("missing notice %q in:\n%s", want, joined)
76 }
77 }
78 }
79
80 func TestRunLegacyRescueImportsMemory(t *testing.T) {
81 home := migrationRescueHome(t)
82 legacyRoot := filepath.Join(home, ".reasonix")
83 if err := os.MkdirAll(filepath.Join(legacyRoot, "memory", "global"), 0o755); err != nil {
84 t.Fatal(err)
85 }
86 if err := os.WriteFile(filepath.Join(legacyRoot, "REASONIX.md"), []byte("legacy user memory\n"), 0o644); err != nil {
87 t.Fatal(err)
88 }
89 if err := os.WriteFile(filepath.Join(legacyRoot, "memory", "global", "user.md"), []byte("---\nname: user\n---\nlegacy fact\n"), 0o644); err != nil {
90 t.Fatal(err)
91 }
92 projectMemory := filepath.Join(legacyRoot, "projects", "proj-slug", "memory")
93 if err := os.MkdirAll(projectMemory, 0o755); err != nil {
94 t.Fatal(err)
95 }
96 if err := os.WriteFile(filepath.Join(projectMemory, "project.md"), []byte("---\nname: project\n---\nproject fact\n"), 0o644); err != nil {
97 t.Fatal(err)
98 }
99
100 var notices []string
101 res := RunLegacyRescue(event.FuncSink(func(e event.Event) {
102 if e.Kind == event.Notice {
103 notices = append(notices, e.Text)
104 }
105 }))
106 if len(res.MemoryErrs) != 0 {
107 t.Fatalf("memory migration errors: %v", res.MemoryErrs)
108 }
109 if got := totalMemoryImported(res.MemoryImports); got != 3 {
110 t.Fatalf("imported memory files = %d, want 3; imports=%+v", got, res.MemoryImports)
111 }
112 for _, path := range []string{
113 filepath.Join(config.MemoryUserDir(), "REASONIX.md"),
114 filepath.Join(config.MemoryUserDir(), "memory", "global", "user.md"),
115 filepath.Join(config.MemoryUserDir(), "projects", "proj-slug", "memory", "project.md"),
116 } {
117 if _, err := os.Stat(path); err != nil {
118 t.Fatalf("migrated memory missing at %s: %v", path, err)
119 }
120 }
121 joined := strings.Join(notices, "\n")
122 for _, want := range []string{
123 "migration rescue: scanning legacy memory",
124 "imported 3 memory file(s)",
125 "migration rescue complete: imported 3 memory file(s)",
126 } {
127 if !strings.Contains(joined, want) {
128 t.Fatalf("missing notice %q in:\n%s", want, joined)
129 }
130 }
131 }
132
133 func TestRunLegacyRescueNoopStillShowsProgress(t *testing.T) {
134 migrationRescueHome(t)
135
136 var notices []string
137 res := RunLegacyRescue(event.FuncSink(func(e event.Event) {
138 if e.Kind == event.Notice {
139 notices = append(notices, e.Text)
140 }
141 }))
142 if got := totalImported(res.SessionImports); got != 0 {
143 t.Fatalf("imported sessions = %d, want 0", got)
144 }
145 joined := strings.Join(notices, "\n")
146 for _, want := range []string{
147 "migration rescue: checking legacy config and credentials",
148 "migration rescue: no legacy sessions needed migration",
149 "migration rescue complete: no legacy data needed migration",
150 } {
151 if !strings.Contains(joined, want) {
152 t.Fatalf("missing notice %q in:\n%s", want, joined)
153 }
154 }
155 }
156
157 func TestRunLegacyRescueSkipsImplicitSourcesWhenIsolated(t *testing.T) {
158 home := isolateMigrationHome(t)
159 legacyRoot := filepath.Join(home, ".reasonix")
160 if err := os.MkdirAll(filepath.Join(legacyRoot, "sessions"), 0o755); err != nil {
161 t.Fatal(err)
162 }
163 if err := os.WriteFile(filepath.Join(legacyRoot, "sessions", "old-chat.jsonl"), []byte(legacyMessageLog), 0o644); err != nil {
164 t.Fatal(err)
165 }
166 if err := os.WriteFile(filepath.Join(legacyRoot, "REASONIX.md"), []byte("legacy user memory\n"), 0o644); err != nil {
167 t.Fatal(err)
168 }
169
170 var notices []string
171 res := RunLegacyRescue(event.FuncSink(func(e event.Event) {
172 if e.Kind == event.Notice {
173 notices = append(notices, e.Text)
174 }
175 }))
176 if got := totalImported(res.SessionImports); got != 0 {
177 t.Fatalf("imported sessions = %d, want 0; imports=%+v", got, res.SessionImports)
178 }
179 if got := totalMemoryImported(res.MemoryImports); got != 0 {
180 t.Fatalf("imported memory files = %d, want 0; imports=%+v", got, res.MemoryImports)
181 }
182 if _, err := os.Stat(filepath.Join(config.SessionDir(), "old-chat.jsonl")); !os.IsNotExist(err) {
183 t.Fatalf("isolated rescue imported legacy session, stat err=%v", err)
184 }
185 if _, err := os.Stat(filepath.Join(config.MemoryUserDir(), "REASONIX.md")); !os.IsNotExist(err) {
186 t.Fatalf("isolated rescue imported legacy memory, stat err=%v", err)
187 }
188 joined := strings.Join(notices, "\n")
189 if !strings.Contains(joined, "REASONIX_HOME is set; implicit legacy migration is skipped") {
190 t.Fatalf("missing isolated skip notice in:\n%s", joined)
191 }
192 }
193
194 func TestRunLegacyRescueCommandImportsFromExplicitInstallDir(t *testing.T) {
195 home := isolateMigrationHome(t)
196 installRoot := filepath.Join(home, "Custom Reasonix")
197 legacySessions := filepath.Join(installRoot, "sessions")
198 if err := os.MkdirAll(legacySessions, 0o755); err != nil {
199 t.Fatal(err)
200 }
201 if err := os.WriteFile(filepath.Join(legacySessions, "custom-chat.jsonl"), []byte(legacyMessageLog), 0o644); err != nil {
202 t.Fatal(err)
203 }
204 currentSessions := config.SessionDir()
205 if err := os.MkdirAll(currentSessions, 0o755); err != nil {
206 t.Fatal(err)
207 }
208 for _, marker := range []string{".legacy-imported.v2-routed", ".legacy-imported.v3-jsonl"} {
209 if err := os.WriteFile(filepath.Join(currentSessions, marker), nil, 0o644); err != nil {
210 t.Fatal(err)
211 }
212 }
213
214 var notices []string
215 res := RunLegacyRescueCommand(`--from "`+installRoot+`"`, event.FuncSink(func(e event.Event) {
216 if e.Kind == event.Notice {
217 notices = append(notices, e.Text)
218 }
219 }))
220 if len(res.SessionErrs) != 0 {
221 t.Fatalf("session migration errors: %v", res.SessionErrs)
222 }
223 if got := totalImported(res.SessionImports); got != 1 {
224 t.Fatalf("imported sessions = %d, want 1; imports=%+v", got, res.SessionImports)
225 }
226 if _, err := os.Stat(filepath.Join(currentSessions, "custom-chat.jsonl")); err != nil {
227 t.Fatalf("explicit imported session missing: %v", err)
228 }
229 joined := strings.Join(notices, "\n")
230 for _, want := range []string{
231 "migration rescue: scanning explicit legacy sessions from " + installRoot,
232 "imported 1 past session(s) from " + legacySessions,
233 "migration rescue complete: imported 1 past session(s)",
234 } {
235 if !strings.Contains(joined, want) {
236 t.Fatalf("missing notice %q in:\n%s", want, joined)
237 }
238 }
239 }
240
241 func TestMigrateLegacySessionSourcesSkipsCurrentProjectTree(t *testing.T) {
242 home := t.TempDir()
243 t.Setenv("HOME", home)
244 t.Setenv("USERPROFILE", home)
245 t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
246 t.Setenv("AppData", filepath.Join(home, "AppData"))
247 t.Setenv("REASONIX_HOME", "")
248 t.Setenv("REASONIX_STATE_HOME", "")
249 if !samePath(config.MemoryUserDir(), filepath.Join(home, ".reasonix")) {
250 t.Skip("current Reasonix home is not ~/.reasonix on this platform")
251 }
252
253 projectSessions := filepath.Join(config.MemoryUserDir(), "projects", "current-project", "sessions")
254 subagents := filepath.Join(projectSessions, "subagents")
255 if err := os.MkdirAll(subagents, 0o755); err != nil {
256 t.Fatal(err)
257 }
258 if err := os.WriteFile(filepath.Join(subagents, "worker.jsonl"), []byte(legacyMessageLog), 0o644); err != nil {
259 t.Fatal(err)
260 }
261
262 imports := MigrateLegacySessionSources(event.FuncSink(func(event.Event) {}))
263 if got := totalImported(imports); got != 0 {
264 t.Fatalf("imported sessions = %d, want 0; imports=%+v", got, imports)
265 }
266 if _, err := os.Stat(filepath.Join(projectSessions, "worker.jsonl")); !os.IsNotExist(err) {
267 t.Fatalf("subagent transcript must not be copied into parent history, stat err=%v", err)
268 }
269 }
270
271 func totalImported(imports []SessionImport) int {
272 total := 0
273 for _, imp := range imports {
274 total += imp.Count
275 }
276 return total
277 }
278
279 func totalMemoryImported(imports []MemoryImport) int {
280 total := 0
281 for _, imp := range imports {
282 total += imp.Count
283 }
284 return total
285 }
286
286 lines GO