返回 DeepSeek-Reasonix
session_upgrade_metadata_test.go
根目录 / desktop / session_upgrade_metadata_test.go
1 package main
2
3 import (
4 "bytes"
5 "context"
6 "database/sql"
7 "encoding/json"
8 "os"
9 "path/filepath"
10 "strings"
11 "testing"
12 "time"
13
14 "reasonix/desktop/internal/workspacestate"
15 "reasonix/internal/config"
16 "reasonix/internal/topicstate"
17 )
18
19 func createUpgradeTopicFixture(t *testing.T, path, topicID, marker string) *sql.DB {
20 t.Helper()
21 if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
22 t.Fatal(err)
23 }
24 store, err := topicstate.Open(t.Context(), path)
25 if err != nil {
26 t.Fatal(err)
27 }
28 if _, err := store.Update(t.Context(), topicID, func(record *topicstate.Record) {
29 record.Title = "preserved " + marker
30 record.TitleSource = "manual"
31 }); err != nil {
32 _ = store.Close()
33 t.Fatal(err)
34 }
35 if err := store.Close(); err != nil {
36 t.Fatal(err)
37 }
38 db, err := sql.Open("sqlite", path)
39 if err != nil {
40 t.Fatal(err)
41 }
42 for _, statement := range []string{
43 `PRAGMA journal_mode=WAL`,
44 `PRAGMA wal_autocheckpoint=0`,
45 `ALTER TABLE topics ADD COLUMN future_column TEXT NOT NULL DEFAULT ''`,
46 `UPDATE topics SET future_column='future-` + marker + `' WHERE topic_id='` + topicID + `'`,
47 `CREATE TABLE future_data(marker TEXT NOT NULL)`,
48 `INSERT INTO future_data(marker) VALUES('` + marker + `')`,
49 } {
50 if _, err := db.ExecContext(t.Context(), statement); err != nil {
51 _ = db.Close()
52 t.Fatal(err)
53 }
54 }
55 return db
56 }
57
58 func fileBytes(t *testing.T, path string) []byte {
59 t.Helper()
60 body, err := os.ReadFile(path)
61 if err != nil {
62 t.Fatal(err)
63 }
64 return body
65 }
66
67 func TestDesktopV1UpgradeBacksUpTopicWALAndRetriesWithoutDuplicateImport(t *testing.T) {
68 isolateDesktopUserDirs(t)
69 ctx := context.Background()
70 projectRoot := filepath.Join(t.TempDir(), "project %20 # 中文")
71 if err := os.MkdirAll(projectRoot, 0o755); err != nil {
72 t.Fatal(err)
73 }
74 projects := []byte(`{"projects":[{"root":` + quotedJSON(projectRoot) + `,"topics":["project-topic"]}]}`)
75 if err := os.MkdirAll(desktopConfigDir(), 0o700); err != nil {
76 t.Fatal(err)
77 }
78 if err := os.WriteFile(filepath.Join(desktopConfigDir(), desktopProjectsFile), projects, 0o600); err != nil {
79 t.Fatal(err)
80 }
81
82 registryPath := config.DesktopWorkspaceStatePath()
83 if err := os.MkdirAll(filepath.Dir(registryPath), 0o700); err != nil {
84 t.Fatal(err)
85 }
86 originalRegistry := []byte(`{"version":1,"generation":7,"workspaceIds":["global"],"workspaces":{"global":{"id":"global","root":"/global","title":"Mine","visible":false,"sessionIds":["old"],"futureWorkspace":{"keep":42}}},"archivedSessionIds":["old"],"pendingCreates":{},"futureRoot":{"keep":true}}`)
87 if err := os.WriteFile(registryPath, originalRegistry, 0o600); err != nil {
88 t.Fatal(err)
89 }
90
91 historyDir := config.SessionDir()
92 if err := os.MkdirAll(historyDir, 0o700); err != nil {
93 t.Fatal(err)
94 }
95 historyPath := writeLegacySession(t, historyDir, "upgrade-history.jsonl", "preserve old history", time.Now())
96 historyBefore := fileBytes(t, historyPath)
97
98 globalPath := config.DesktopTopicStatePath("")
99 projectPath := config.DesktopTopicStatePath(projectRoot)
100 globalDB := createUpgradeTopicFixture(t, globalPath, "global-topic", "global")
101 defer globalDB.Close()
102 projectDB := createUpgradeTopicFixture(t, projectPath, "project-topic", "project")
103 defer projectDB.Close()
104 globalBefore := fileBytes(t, globalPath)
105 projectBefore := fileBytes(t, projectPath)
106
107 backupDir := filepath.Join(desktopConfigDir(), "desktop", "upgrade-backups")
108 if err := os.MkdirAll(filepath.Dir(backupDir), 0o700); err != nil {
109 t.Fatal(err)
110 }
111 if err := os.WriteFile(backupDir, []byte("occupied"), 0o600); err != nil {
112 t.Fatal(err)
113 }
114 store := newDesktopWorkspaceStore()
115 if err := store.RestoreSession(ctx, "old"); err == nil {
116 t.Fatal("registry upgrade succeeded despite a blocked metadata backup directory")
117 }
118 if got := fileBytes(t, registryPath); !bytes.Equal(got, originalRegistry) {
119 t.Fatal("failed upgrade published a new registry")
120 }
121 if got := fileBytes(t, globalPath); !bytes.Equal(got, globalBefore) {
122 t.Fatal("failed upgrade changed the global topic database")
123 }
124 if got := fileBytes(t, projectPath); !bytes.Equal(got, projectBefore) {
125 t.Fatal("failed upgrade changed the project topic database")
126 }
127 if got := fileBytes(t, historyPath); !bytes.Equal(got, historyBefore) {
128 t.Fatal("failed upgrade changed old session history")
129 }
130
131 if err := os.Remove(backupDir); err != nil {
132 t.Fatal(err)
133 }
134 if err := store.RestoreSession(ctx, "old"); err != nil {
135 t.Fatal(err)
136 }
137 state, err := newDesktopWorkspaceStore().Load(ctx)
138 if err != nil {
139 t.Fatal(err)
140 }
141 if state.Version != workspacestate.SchemaVersion || state.SessionStates["old"].Lifecycle != workspacestate.Active {
142 t.Fatalf("upgraded registry = %+v", state)
143 }
144 count := 0
145 for _, id := range state.Workspaces[workspacestate.GlobalWorkspaceID].SessionIDs {
146 if id == "old" {
147 count++
148 }
149 }
150 if count != 1 {
151 t.Fatalf("old session membership count = %d, want 1", count)
152 }
153 registryBody := string(fileBytes(t, registryPath))
154 for _, field := range []string{`"futureRoot"`, `"futureWorkspace"`} {
155 if !strings.Contains(registryBody, field) {
156 t.Fatalf("registry lost unknown field %s: %s", field, registryBody)
157 }
158 }
159 if got := fileBytes(t, globalPath); !bytes.Equal(got, globalBefore) {
160 t.Fatal("successful upgrade changed the global topic database")
161 }
162 if got := fileBytes(t, projectPath); !bytes.Equal(got, projectBefore) {
163 t.Fatal("successful upgrade changed the project topic database")
164 }
165 if got := fileBytes(t, historyPath); !bytes.Equal(got, historyBefore) {
166 t.Fatal("successful upgrade changed old session history")
167 }
168
169 backups, err := filepath.Glob(filepath.Join(backupDir, "topics-*.sqlite"))
170 if err != nil || len(backups) != 2 {
171 t.Fatalf("topic backups = %v, %v; want two", backups, err)
172 }
173 markers := map[string]bool{}
174 for _, backup := range backups {
175 db, err := sql.Open("sqlite", backup)
176 if err != nil {
177 t.Fatal(err)
178 }
179 var marker, title, future string
180 if err := db.QueryRowContext(ctx, `SELECT marker FROM future_data`).Scan(&marker); err != nil {
181 _ = db.Close()
182 t.Fatal(err)
183 }
184 topicID := marker + "-topic"
185 if err := db.QueryRowContext(ctx, `SELECT title,future_column FROM topics WHERE topic_id=?`, topicID).Scan(&title, &future); err != nil {
186 _ = db.Close()
187 t.Fatal(err)
188 }
189 if err := db.Close(); err != nil {
190 t.Fatal(err)
191 }
192 if title != "preserved "+marker || future != "future-"+marker {
193 t.Fatalf("backup %s lost data: marker=%q title=%q future=%q", backup, marker, title, future)
194 }
195 markers[marker] = true
196 }
197 if !markers["global"] || !markers["project"] {
198 t.Fatalf("backup markers = %v", markers)
199 }
200 }
201
202 func quotedJSON(value string) string {
203 body, _ := json.Marshal(value)
204 return string(body)
205 }
206
206 lines GO