返回 DeepSeek-Reasonix
project_tree_read_compatibility_test.go
根目录 / desktop / project_tree_read_compatibility_test.go
1 package main
2
3 import (
4 "bytes"
5 "os"
6 "path/filepath"
7 "reflect"
8 "testing"
9
10 "reasonix/desktop/internal/workspacestate"
11 "reasonix/internal/session"
12 )
13
14 func TestProjectTreeUnregisteredHistoricalPins(t *testing.T) {
15 isolateDesktopUserDirs(t)
16 root, other := t.TempDir(), t.TempDir()
17 if err := addProject(root, "Legacy"); err != nil {
18 t.Fatal(err)
19 }
20 app := NewApp()
21 app.ctx = t.Context()
22 t.Cleanup(app.closeSessionServices)
23 app.historicalImports.catalog = []historicalCatalogEntry{
24 {scope: "project", node: ProjectNode{Key: "source_kept", Root: root, Pinned: true, Source: &SessionSourceRef{HostID: localDesktopHostID, SourceKey: "kept"}}},
25 {scope: "project", node: ProjectNode{Key: "source_other", Root: other, Pinned: true, Source: &SessionSourceRef{HostID: localDesktopHostID, SourceKey: "other"}}},
26 }
27 for range 2 {
28 snapshot := mustProjectTreeSnapshot(t, app)
29 if len(snapshot.Projects) != 1 || len(snapshot.Projects[0].Children) != 1 || snapshot.Projects[0].Children[0].Key != "source_kept" {
30 t.Fatalf("unregistered project lost its pin or included another project's pin: %+v", snapshot.Projects)
31 }
32 if _, err := os.Stat(app.workspaceRegistry().Path()); !os.IsNotExist(err) {
33 t.Fatalf("snapshot created a registry: %v", err)
34 }
35 }
36 }
37
38 func TestProjectedShellOrganizationPreservesAdoptedAliasOrder(t *testing.T) {
39 app, root, refs := canonicalOrganizationFixture(t, "a", "b")
40 state, err := app.workspaceRegistry().LoadProjection(t.Context())
41 if err != nil {
42 t.Fatal(err)
43 }
44 workspaceID := desktopWorkspaceID("project", root)
45 workspace := state.Workspaces[workspaceID]
46 workspace.Organization = &workspacestate.Organization{Order: []string{}, Imported: map[string]bool{}}
47 state.SourceMappings["adopted"] = workspacestate.SourceMapping{WorkspaceID: workspaceID, SessionID: "b", SourceKey: "adopted", Path: filepath.Join(root, "old.jsonl")}
48 legacy := desktopProject{ManualSessionOrder: true, SessionOrder: []string{"source\x00local\x00adopted", workspacestate.SessionKey("a")}}
49 org := projectedShellOrganization(workspace, state, nil, legacy)
50 want := []string{workspacestate.SessionKey("b"), workspacestate.SessionKey("a")}
51 if !reflect.DeepEqual(org.Order, want) {
52 t.Fatalf("alias order=%v want=%v", org.Order, want)
53 }
54 if workspace.Organization.MigrationVersion != 0 || len(workspace.Organization.Imported) != 0 || len(workspace.Organization.Order) != 0 {
55 t.Fatal("projection mutated the caller's organization")
56 }
57 // The same identity must sort the canonical row, never its retired source.
58 nodes := []ProjectNode{{Session: &session.SessionRef{HostID: refs["a"].HostID, SessionID: "a"}}, {Session: &session.SessionRef{HostID: refs["b"].HostID, SessionID: "b"}}}
59 got := filterWorkspaceSessionNodes(ProjectTopicPageRequest{}, org, state, workspaceID, nodes)
60 if len(got) != 2 || got[0].SortOrder != 1 || got[1].SortOrder != 0 {
61 t.Fatalf("canonical ranks=%+v", got)
62 }
63 }
64
65 func TestProjectTreeUnmigratedManualOrderMatchesExpandedPage(t *testing.T) {
66 for _, mode := range []string{"session", "topic"} {
67 t.Run(mode, func(t *testing.T) {
68 app, root, _ := canonicalOrganizationFixture(t, "a", "b", "c")
69 if err := updateProjectsFile(func(f *desktopProjectFile) (bool, error) {
70 p := &f.Projects[projectIndexByRoot(f.Projects, root)]
71 if mode == "topic" {
72 p.Topics, p.ManualTopicOrder = []string{"c", "a", "b"}, true
73 } else {
74 p.SessionOrder = []string{workspacestate.SessionKey("c"), workspacestate.SessionKey("a"), workspacestate.SessionKey("b")}
75 p.ManualSessionOrder = true
76 }
77 return true, nil
78 }); err != nil {
79 t.Fatal(err)
80 }
81 pinned := true
82 if err := app.workspaceRegistry().UpdatePresentation(t.Context(), []string{"a", "b", "c"}, nil, &pinned); err != nil {
83 t.Fatal(err)
84 }
85 before, err := os.ReadFile(app.workspaceRegistry().Path())
86 if err != nil {
87 t.Fatal(err)
88 }
89 keys := func(nodes []ProjectNode) []string {
90 result := []string{}
91 for _, node := range nodes {
92 result = append(result, projectNodeSessionKey(node))
93 }
94 return result
95 }
96 want := []string{workspacestate.SessionKey("c"), workspacestate.SessionKey("a"), workspacestate.SessionKey("b")}
97 for range 2 {
98 snapshot := mustProjectTreeSnapshot(t, app)
99 if len(snapshot.Projects) != 1 || !reflect.DeepEqual(keys(snapshot.Projects[0].Children), want) {
100 t.Fatalf("cold snapshot order: %+v", snapshot.Projects)
101 }
102 }
103 after, err := os.ReadFile(app.workspaceRegistry().Path())
104 if err != nil || !bytes.Equal(before, after) {
105 t.Fatalf("snapshot persisted migration: %v", err)
106 }
107 page, err := app.ListProjectTopics(ProjectTopicPageRequest{Scope: "project", WorkspaceRoot: root, Limit: 50})
108 if err != nil || !reflect.DeepEqual(keys(page.Items), want) {
109 t.Fatalf("expanded page order=%v err=%v", keys(page.Items), err)
110 }
111 // Once migrated, durable user choices override legacy preferences.
112 want = []string{workspacestate.SessionKey("b"), workspacestate.SessionKey("a"), workspacestate.SessionKey("c")}
113 if err := app.ReorderSessions("project", root, want); err != nil {
114 t.Fatal(err)
115 }
116 if got := keys(mustProjectTreeSnapshot(t, app).Projects[0].Children); !reflect.DeepEqual(got, want) {
117 t.Fatalf("persisted order=%v want=%v", got, want)
118 }
119 })
120 }
121 }
122
123 func TestHistoricalProjectionUnregisteredPathAlias(t *testing.T) {
124 isolateDesktopUserDirs(t)
125 root := t.TempDir()
126 alias := filepath.Join(t.TempDir(), "alias")
127 if err := os.Symlink(root, alias); err != nil {
128 t.Skipf("symlink unavailable: %v", err)
129 }
130 app := NewApp()
131 app.ctx = t.Context()
132 app.historicalImports.catalog = []historicalCatalogEntry{{scope: "project", node: ProjectNode{Root: alias, Source: &SessionSourceRef{SourceKey: "alias"}}}}
133 state, err := app.workspaceRegistry().LoadProjection(t.Context())
134 if err != nil {
135 t.Fatal(err)
136 }
137 if got := app.historicalCanonicalTopics("project", root, state); len(got) != 1 {
138 t.Fatalf("unregistered alias lost: %+v", got)
139 }
140 }
141
142 func TestProjectTreeHistoricalManualOrderBeforeRegistration(t *testing.T) {
143 for _, scope := range []string{"project", "global"} {
144 t.Run(scope, func(t *testing.T) {
145 isolateDesktopUserDirs(t)
146 root := t.TempDir()
147 if scope == "project" {
148 if err := addProject(root, "Legacy"); err != nil {
149 t.Fatal(err)
150 }
151 }
152 app := NewApp()
153 app.ctx = t.Context()
154 t.Cleanup(app.closeSessionServices)
155 for _, id := range []string{"a", "b"} {
156 path := filepath.Join(root, id+".jsonl")
157 app.historicalImports.catalog = append(app.historicalImports.catalog, historicalCatalogEntry{scope: scope, node: ProjectNode{Root: root, Key: "source_" + id, TopicID: id, SessionPath: path, Pinned: true, Source: &SessionSourceRef{HostID: localDesktopHostID, SourceKey: id, Path: path}}})
158 }
159 want := []string{"source\x00local\x00b", "source\x00local\x00a"}
160 if err := updateProjectsFile(func(f *desktopProjectFile) (bool, error) {
161 if scope == "global" {
162 f.GlobalSessionOrder, f.GlobalManualSessionOrder = want, true
163 } else {
164 f.Projects[0].SessionOrder, f.Projects[0].ManualSessionOrder = want, true
165 }
166 return true, nil
167 }); err != nil {
168 t.Fatal(err)
169 }
170 for range 2 {
171 snapshot := mustProjectTreeSnapshot(t, app)
172 if len(snapshot.Projects) != 1 {
173 t.Fatalf("projects=%+v", snapshot.Projects)
174 }
175 got := []string{}
176 for _, node := range snapshot.Projects[0].Children {
177 got = append(got, projectNodeSessionKey(node))
178 }
179 if !reflect.DeepEqual(got, want) {
180 t.Fatalf("historical order=%v want=%v", got, want)
181 }
182 }
183 if _, err := os.Stat(app.workspaceRegistry().Path()); !os.IsNotExist(err) {
184 t.Fatalf("read created registry: %v", err)
185 }
186 })
187 }
188 }
189
189 lines GO