返回 DeepSeek-Reasonix
project_state_collision_test.go
根目录 / desktop / project_state_collision_test.go
1 package main
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "testing"
8
9 "reasonix/internal/config"
10 "reasonix/internal/memory"
11 "reasonix/internal/sessioncatalog"
12 )
13
14 func TestNewCollidingProjectGetsOwnStateWithoutMovingExistingProject(t *testing.T) {
15 t.Setenv("REASONIX_HOME", t.TempDir())
16 root := t.TempDir()
17 first := filepath.Join(root, "front-end", "app")
18 second := filepath.Join(root, "front", "end-app")
19 plain := filepath.Join(root, "backend")
20 for _, path := range []string{first, second, plain} {
21 if err := os.MkdirAll(path, 0o700); err != nil {
22 t.Fatal(err)
23 }
24 }
25 if err := addProject(first, ""); err != nil {
26 t.Fatal(err)
27 }
28 firstDir := config.ProjectSessionDir(first)
29 if err := os.MkdirAll(firstDir, 0o700); err != nil {
30 t.Fatal(err)
31 }
32 oldSession := filepath.Join(firstDir, "existing.jsonl")
33 if err := os.WriteFile(oldSession, []byte("existing session\n"), 0o600); err != nil {
34 t.Fatal(err)
35 }
36 if err := addProject(second, ""); err != nil {
37 t.Fatal(err)
38 }
39 secondDir := config.ProjectSessionDir(second)
40 if secondDir == firstDir {
41 t.Fatalf("new project still shares %q", firstDir)
42 }
43 if got := config.ProjectSessionDir(first); got != firstDir {
44 t.Fatalf("existing project moved to %q", got)
45 }
46 if got, err := os.ReadFile(oldSession); err != nil || string(got) != "existing session\n" {
47 t.Fatalf("existing session changed: %q, %v", got, err)
48 }
49 if err := removeProject(first); err != nil {
50 t.Fatal(err)
51 }
52 if err := addProject(first, ""); err != nil {
53 t.Fatal(err)
54 }
55 if got := config.ProjectSessionDir(first); got != firstDir {
56 t.Fatalf("re-added original project moved to %q, want %q", got, firstDir)
57 }
58 if _, err := os.Stat(filepath.Join(secondDir, "existing.jsonl")); !os.IsNotExist(err) {
59 t.Fatalf("new project sees existing session: %v", err)
60 }
61 if err := addProject(plain, ""); err != nil {
62 t.Fatal(err)
63 }
64 wantPlain := filepath.Join(config.MemoryUserDir(), "projects", config.WorkspaceSlug(plain), "sessions")
65 if got := config.ProjectSessionDir(plain); got != wantPlain {
66 t.Fatalf("non-colliding project moved to %q, want %q", got, wantPlain)
67 }
68 if err := removeProject(second); err != nil {
69 t.Fatal(err)
70 }
71 if err := addProject(second, ""); err != nil {
72 t.Fatal(err)
73 }
74 if got := config.ProjectSessionDir(second); got != secondDir {
75 t.Fatalf("re-added project moved to %q, want %q", got, secondDir)
76 }
77 if got := config.ProjectSessionStoreDir(first); got == config.ProjectSessionStoreDir(second) {
78 t.Fatalf("canonical session stores overlap: %q", got)
79 }
80 if got := config.DesktopTopicStatePath(first); got == config.DesktopTopicStatePath(second) {
81 t.Fatalf("topic stores overlap: %q", got)
82 }
83 if got := memory.StoreFor(config.MemoryUserDir(), first).Dir; got == memory.StoreFor(config.MemoryUserDir(), second).Dir {
84 t.Fatalf("project memory stores overlap: %q", got)
85 }
86 if err := os.MkdirAll(secondDir, 0o700); err != nil {
87 t.Fatal(err)
88 }
89 firstSession := writeTopicSession(t, firstDir, "first.jsonl", "first-topic", "First", first)
90 secondSession := writeTopicSession(t, secondDir, "second.jsonl", "second-topic", "Second", second)
91 app := NewApp()
92 installSessionCatalogForTest(t, app, firstDir, "project", first)
93 reconcileSessionCatalogForTest(t, app, secondDir, "project", second)
94 for _, tc := range []struct{ root, dir, want, other string }{
95 {first, firstDir, firstSession, secondSession},
96 {second, secondDir, secondSession, firstSession},
97 } {
98 page, err := app.sessionCatalog.Load().ListSessions(context.Background(), sessioncatalog.SessionPageRequest{
99 Scope: "project", WorkspaceRoot: tc.root, Directory: tc.dir, Limit: 20,
100 })
101 if err != nil {
102 t.Fatal(err)
103 }
104 found := false
105 for _, item := range page.Items {
106 if item.Path == tc.other {
107 t.Fatalf("project %q lists another project's session %q", tc.root, tc.other)
108 }
109 found = found || item.Path == tc.want
110 }
111 if !found {
112 t.Fatalf("project %q did not list its session %q: %+v", tc.root, tc.want, page.Items)
113 }
114 }
115 }
116
117 func TestLegacyProjectImportsKeepExistingCollisionState(t *testing.T) {
118 for _, source := range []string{"workspace-list", "sidebar-recovery"} {
119 t.Run(source, func(t *testing.T) {
120 t.Setenv("REASONIX_HOME", t.TempDir())
121 root := t.TempDir()
122 first := filepath.Join(root, "front-end", "app")
123 second := filepath.Join(root, "front", "end-app")
124 for _, path := range []string{first, second} {
125 if err := os.MkdirAll(path, 0o700); err != nil {
126 t.Fatal(err)
127 }
128 }
129 if err := addProject(first, ""); err != nil {
130 t.Fatal(err)
131 }
132 legacyDir := config.ProjectSessionDir(second)
133 if err := os.MkdirAll(legacyDir, 0o700); err != nil {
134 t.Fatal(err)
135 }
136 oldSession := filepath.Join(legacyDir, "legacy.jsonl")
137 if err := os.WriteFile(oldSession, []byte("legacy session\n"), 0o600); err != nil {
138 t.Fatal(err)
139 }
140 switch source {
141 case "workspace-list":
142 rememberWorkspace(second)
143 migrateLegacyWorkspacesIntoProjects()
144 case "sidebar-recovery":
145 if _, err := recoverLegacyProjectSidebarRoots(desktopTabsFile{Tabs: []desktopTabEntry{{Scope: "project", WorkspaceRoot: second}}}); err != nil {
146 t.Fatal(err)
147 }
148 }
149 if got := config.ProjectSessionDir(second); got != legacyDir {
150 t.Fatalf("imported project moved to %q, want %q", got, legacyDir)
151 }
152 if got, err := os.ReadFile(oldSession); err != nil || string(got) != "legacy session\n" {
153 t.Fatalf("imported session changed: %q, %v", got, err)
154 }
155 })
156 }
157 }
158
159 func TestProjectRegistrationKeepsOpeningOnProjectListLockFailure(t *testing.T) {
160 t.Setenv("REASONIX_HOME", t.TempDir())
161 root := t.TempDir()
162 lockPath := filepath.Join(desktopConfigDir(), desktopProjectsFile+".lock")
163 if err := os.MkdirAll(lockPath, 0o700); err != nil {
164 t.Fatal(err)
165 }
166 app := NewApp()
167 if err := app.registerProjectRoot(root); err != nil {
168 t.Fatalf("project list lock failure blocked opening: %v", err)
169 }
170 }
171
172 func TestCollidingProjectRegistrationFailsBeforeUsingUnownedState(t *testing.T) {
173 t.Setenv("REASONIX_HOME", t.TempDir())
174 root := t.TempDir()
175 first := filepath.Join(root, "front-end", "app")
176 second := filepath.Join(root, "front", "end-app")
177 if err := addProject(first, ""); err != nil {
178 t.Fatal(err)
179 }
180 if err := config.AssignProjectStateCollision(config.MemoryUserDir(), second); err != nil {
181 t.Fatal(err)
182 }
183 assigned := config.ProjectStateDir(config.MemoryUserDir(), second)
184 if err := os.Remove(filepath.Join(assigned, ".workspace-root")); err != nil {
185 t.Fatal(err)
186 }
187 if err := os.WriteFile(filepath.Join(assigned, "orphan"), []byte("keep"), 0o600); err != nil {
188 t.Fatal(err)
189 }
190 if err := addProject(second, ""); err == nil {
191 t.Fatal("registration accepted an unowned, nonempty assigned directory")
192 }
193 if len(loadProjectsFile().Projects) != 1 {
194 t.Fatal("failed registration changed the saved project list")
195 }
196 }
197
198 func TestPreviouslyRecordedCollisionKeepsBothLegacyPaths(t *testing.T) {
199 t.Setenv("REASONIX_HOME", t.TempDir())
200 root := t.TempDir()
201 first := filepath.Join(root, "front-end", "app")
202 second := filepath.Join(root, "front", "end-app")
203 if err := saveProjectsFile(desktopProjectFile{Projects: []desktopProject{{Root: first}, {Root: second}}}); err != nil {
204 t.Fatal(err)
205 }
206 legacy := config.ProjectSessionDir(first)
207 for _, path := range []string{first, second} {
208 if err := addProject(path, ""); err != nil {
209 t.Fatal(err)
210 }
211 if got := config.ProjectSessionDir(path); got != legacy {
212 t.Fatalf("previously recorded project %q moved to %q", path, got)
213 }
214 }
215 }
216
216 lines GO