返回 DeepSeek-Reasonix
session_retired_projection_test.go
根目录 / desktop / session_retired_projection_test.go
1 package main
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "testing"
8 "time"
9
10 "reasonix/internal/agent"
11 "reasonix/internal/config"
12 "reasonix/internal/provider"
13 "reasonix/internal/sessioncatalog"
14 )
15
16 func TestDeletedLegacyTopicStaysHiddenAfterCatalogRebuild(t *testing.T) {
17 for _, metadataOnly := range []bool{true, false} {
18 t.Run(map[bool]string{true: "lazy", false: "materialized"}[metadataOnly], func(t *testing.T) {
19 isolateDesktopUserDirs(t)
20 path, _, _ := migrationSingleDAGFixture(t)
21 app := newHistoricalLifecycleApp(t)
22 if err := app.DeleteTopic("deleted-topic"); err != nil {
23 t.Fatal(err)
24 }
25 catalog, err := sessioncatalog.Open(t.Context(), sessioncatalog.Options{Path: filepath.Join(t.TempDir(), "catalog.sqlite"), MetadataOnly: metadataOnly, DisableRepair: true})
26 if err != nil {
27 t.Fatal(err)
28 }
29 app.sessionCatalog.Store(catalog)
30 t.Cleanup(func() { app.desktopSessions.readSnapshots.close(); app.stopSessionCatalog(time.Second) })
31 if err := catalog.UpsertSession(t.Context(), sessioncatalog.SessionRecord{Path: path, Directory: config.SessionDir(), Scope: "global", TopicID: "deleted-topic", TopicTitle: "Deleted", OrdinaryVisible: true, Health: sessioncatalog.HealthOK}); err != nil {
32 t.Fatal(err)
33 }
34 if err := app.syncSessionCatalogMetadata(t.Context(), catalog); err != nil {
35 t.Fatal(err)
36 }
37 page, err := app.ListProjectTopics(ProjectTopicPageRequest{Scope: "global", Limit: 50})
38 if err != nil {
39 t.Fatal(err)
40 }
41 defer app.ReleaseReadSnapshot(page.SnapshotID)
42 if len(page.Items) != 0 {
43 t.Fatalf("deleted topic reappeared: %+v", page.Items)
44 }
45 })
46 }
47 }
48
49 func TestAdoptedPathAliasSuppressesExplicitLegacyHead(t *testing.T) {
50 isolateDesktopUserDirs(t)
51 path, legacy, _ := migrationSingleDAGFixture(t)
52 app := newHistoricalLifecycleApp(t)
53 catalog, err := sessioncatalog.Open(t.Context(), sessioncatalog.Options{Path: filepath.Join(t.TempDir(), "catalog.sqlite"), MetadataOnly: true})
54 if err != nil {
55 t.Fatal(err)
56 }
57 app.sessionCatalog.Store(catalog)
58 t.Cleanup(func() { app.desktopSessions.readSnapshots.close(); app.stopSessionCatalog(time.Second) })
59 if err := catalog.ReconcileDirectory(t.Context(), sessioncatalog.DirectoryTarget{Path: config.SessionDir(), Scope: "global"}); err != nil {
60 t.Fatal(err)
61 }
62 page, err := app.unadoptedLegacyTopics(ProjectTopicPageRequest{Scope: "global"}, map[string]bool{sessionRuntimeKey(path): true}, nil)
63 if err != nil {
64 t.Fatal(err)
65 }
66 if len(page.Items) != 0 {
67 t.Fatalf("adopted path reappeared under an explicit head: %+v", page.Items)
68 }
69 // A later independent fork must remain visible even when an older writer
70 // recorded the selected source only by path.
71 if _, err := legacy.ForkHead(path, legacy.Snapshot()[1].ID, agent.HeadKindFork, "child"); err != nil {
72 t.Fatal(err)
73 }
74 legacy.Add(provider.Message{ID: "child-message", Role: provider.RoleUser, Content: "independent branch"})
75 if err := legacy.Save(path); err != nil {
76 t.Fatal(err)
77 }
78 page, err = app.unadoptedLegacyTopics(ProjectTopicPageRequest{Scope: "global"}, map[string]bool{sessionRuntimeKey(path): true}, nil)
79 if err != nil || len(page.Items) != 2 {
80 t.Fatalf("path alias hid independent heads: %+v %v", page.Items, err)
81 }
82 }
83
84 func TestMissingProjectDoesNotResurrectDeletedTopic(t *testing.T) {
85 isolateDesktopUserDirs(t)
86 root := t.TempDir()
87 if err := addProject(root, "Removed project"); err != nil {
88 t.Fatal(err)
89 }
90 app := newHistoricalLifecycleApp(t)
91 if err := app.DeleteTopic("deleted"); err != nil {
92 t.Fatal(err)
93 }
94 if err := os.Remove(root); err != nil {
95 t.Fatal(err)
96 }
97 for _, metadataOnly := range []bool{true, false} {
98 catalog, err := sessioncatalog.Open(t.Context(), sessioncatalog.Options{Path: filepath.Join(t.TempDir(), "catalog.sqlite"), MetadataOnly: metadataOnly, DisableRepair: true})
99 if err != nil {
100 t.Fatal(err)
101 }
102 t.Cleanup(func() {
103 closeCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
104 defer cancel()
105 if err := catalog.Close(closeCtx); err != nil {
106 t.Errorf("close catalog fixture: %v", err)
107 }
108 })
109 app.sessionCatalog.Store(catalog)
110 for _, id := range []string{"deleted", "retained"} {
111 if err := catalog.UpsertSession(t.Context(), sessioncatalog.SessionRecord{Path: filepath.Join(desktopSessionDir(root), id+".jsonl"), Directory: desktopSessionDir(root), Scope: "project", WorkspaceRoot: root, TopicID: id, TopicTitle: id, OrdinaryVisible: true, Health: sessioncatalog.HealthOK}); err != nil {
112 t.Fatal(err)
113 }
114 }
115 page, err := app.ListProjectTopics(ProjectTopicPageRequest{Scope: "project", WorkspaceRoot: root, Limit: 1})
116 if err != nil {
117 t.Fatal(err)
118 }
119 if len(page.Items) != 1 || page.Items[0].TopicID != "retained" || page.NextCursor != "" {
120 t.Fatalf("deleted/offline distinction lost: %+v", page)
121 }
122 app.ReleaseReadSnapshot(page.SnapshotID)
123 app.stopSessionCatalog(time.Second)
124 // stopSessionCatalog has a bounded shutdown window; join the catalog's
125 // actual close before TempDir cleanup removes its SQLite file on Windows.
126 closeCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
127 err = catalog.Close(closeCtx)
128 cancel()
129 if err != nil {
130 t.Fatalf("close catalog fixture: %v", err)
131 }
132 }
133 }
134
135 func TestPurgedLegacyImportStaysHiddenAfterRestart(t *testing.T) {
136 isolateDesktopUserDirs(t)
137 path, _, _ := migrationSingleDAGFixture(t)
138 app := newHistoricalLifecycleApp(t)
139 if _, err := app.ListHistoricalSessions(); err != nil {
140 t.Fatal(err)
141 }
142 result, err := app.ImportHistoricalSession(desktopSourceKey(path, ""))
143 if err != nil {
144 t.Fatal(err)
145 }
146 if err := app.ArchiveCanonicalSession(result.Session); err != nil {
147 t.Fatal(err)
148 }
149 if err := app.PurgeCanonicalSession(result.Session); err != nil {
150 t.Fatal(err)
151 }
152 app.stopHistoricalImports()
153 app.closeSessionServices()
154 for _, metadataOnly := range []bool{true, false} {
155 restarted := newHistoricalLifecycleApp(t)
156 catalog, err := sessioncatalog.Open(t.Context(), sessioncatalog.Options{Path: filepath.Join(t.TempDir(), "catalog.sqlite"), MetadataOnly: metadataOnly, DisableRepair: true})
157 if err != nil {
158 t.Fatal(err)
159 }
160 restarted.sessionCatalog.Store(catalog)
161 t.Cleanup(func() { restarted.desktopSessions.readSnapshots.close(); restarted.stopSessionCatalog(time.Second) })
162 if err := catalog.ReconcileDirectory(t.Context(), sessioncatalog.DirectoryTarget{Path: config.SessionDir(), Scope: "global"}); err != nil {
163 t.Fatal(err)
164 }
165 page, err := restarted.ListProjectTopics(ProjectTopicPageRequest{Scope: "global", Limit: 50})
166 if err != nil {
167 t.Fatal(err)
168 }
169 if len(page.Items) != 0 {
170 t.Fatalf("purged source reappeared after restart: %+v", page.Items)
171 }
172 restarted.ReleaseReadSnapshot(page.SnapshotID)
173 if _, err := os.Stat(path); err != nil {
174 t.Fatalf("retained migration source was removed: %v", err)
175 }
176 }
177 }
178
178 lines GO