返回 DeepSeek-Reasonix
topic_archive_visibility_test.go
根目录 / desktop / topic_archive_visibility_test.go
1 package main
2
3 import (
4 "bytes"
5 "os"
6 "path/filepath"
7 "testing"
8 "time"
9
10 "reasonix/desktop/internal/workspacestate"
11 "reasonix/internal/sessioncatalog"
12 )
13
14 func TestLegacyTopicArchiveVisibility(t *testing.T) {
15 for _, scope := range []string{"global", "project"} {
16 for _, metadataOnly := range []bool{false, true} {
17 for _, topicRoute := range []bool{true, false} {
18 name := scope + "/materialized"
19 if metadataOnly {
20 name = scope + "/lazy"
21 }
22 if topicRoute {
23 name += "/topic"
24 } else {
25 name += "/session"
26 }
27 t.Run(name, func(t *testing.T) {
28 a, topic, root := topicRemovalFixture(t, scope, "Historical conversation")
29 openCatalog := func(owner *App, filename string) {
30 t.Helper()
31 catalog, err := sessioncatalog.Open(t.Context(), sessioncatalog.Options{Path: filepath.Join(t.TempDir(), filename), MetadataOnly: metadataOnly, DisableRepair: true})
32 if err != nil {
33 t.Fatal(err)
34 }
35 owner.sessionCatalog.Store(catalog)
36 // Register after this generation's TempDir cleanup and bind
37 // the owner now, so Windows sees closed SQLite handles first.
38 t.Cleanup(func() {
39 owner.desktopSessions.readSnapshots.close()
40 if !owner.stopSessionCatalog(sessionCatalogTestDeadline) {
41 t.Error("session catalog did not stop before directory cleanup")
42 }
43 })
44 }
45 if err := a.SetTopicPinned(topic.ID, true); err != nil {
46 t.Fatal(err)
47 }
48 canonicalRoot := pinDesktopSessionRoot(t, a)
49 sourceRoot := root
50 if scope == "global" {
51 sourceRoot = globalWorkspaceRoot()
52 }
53 dir := desktopSessionDir(sourceRoot)
54 if err := os.MkdirAll(dir, 0700); err != nil {
55 t.Fatal(err)
56 }
57 sourcePath := writeTopicSessionWithPrompt(t, dir, "archive-visibility.jsonl", topic.ID, topic.Title, root, "preserve this history", time.Now())
58 original, err := os.ReadFile(sourcePath)
59 if err != nil {
60 t.Fatal(err)
61 }
62 openCatalog(a, "catalog.sqlite")
63 reconcileSessionCatalogForTest(t, a, dir, scope, root)
64 assertVisible := func(want int) {
65 t.Helper()
66 page, err := a.ListProjectTopics(ProjectTopicPageRequest{Scope: scope, WorkspaceRoot: root, Limit: 50})
67 if err != nil || len(page.Items) != want {
68 t.Fatalf("sidebar = %+v, %v; want %d", page.Items, err, want)
69 }
70 }
71 assertVisible(1)
72 if topicRoute {
73 result, err := a.RemoveTopic(inspectedTopicRemoval(t, a, topic.ID))
74 if err != nil || !result.Committed {
75 t.Fatalf("archive topic = %+v, %v", result, err)
76 }
77 } else {
78 result, err := a.ArchiveSessionTarget(SessionSelector{SessionPath: sourcePath})
79 if err != nil || !result.Committed {
80 t.Fatalf("archive session = %+v, %v", result, err)
81 }
82 }
83 assertVisible(0)
84 trash, err := a.ListTrashEntries("", "", 50)
85 if err != nil || len(trash.Items) != 1 || trash.Items[0].Ref == nil {
86 t.Fatalf("trash = %+v, %v", trash, err)
87 }
88 if err := a.RestoreCanonicalSession(*trash.Items[0].Ref); err != nil {
89 t.Fatal(err)
90 }
91 assertVisible(1)
92 if err := a.ArchiveCanonicalSession(*trash.Items[0].Ref); err != nil {
93 t.Fatal(err)
94 }
95 assertVisible(0)
96 if err := a.PurgeCanonicalSession(*trash.Items[0].Ref); err != nil {
97 t.Fatal(err)
98 }
99 assertVisible(0)
100 retained, err := os.ReadFile(sourcePath)
101 if err != nil || !bytes.Equal(retained, original) {
102 t.Fatal("archive/purge changed the retained legacy source")
103 }
104 // A stale metadata-only row would take the native-confirmation
105 // route and fail exactly like the reported second archive attempt.
106 stale := inspectedTopicRemoval(t, a, topic.ID)
107 stale.OperationID += "-stale"
108 failed, err := a.RemoveTopic(stale)
109 if err != nil || failed.Committed || failed.ErrorMessage != "session not found" {
110 t.Fatalf("stale archive = %+v, %v", failed, err)
111 }
112 assertVisible(0)
113 // The retained source and metadata must not recreate a conversation
114 // after a fresh process loads the registry without a warm catalog.
115 a.closeSessionServices()
116 a.desktopSessions.readSnapshots.close()
117 if !a.stopSessionCatalog(sessionCatalogTestDeadline) {
118 t.Fatal("session catalog did not stop before the fresh process")
119 }
120 a = NewApp()
121 a.ctx = t.Context()
122 a.desktopSessions.root = canonicalRoot
123 installNoopRuntimeEvents(a)
124 t.Cleanup(a.closeSessionServices)
125 assertVisible(0)
126 state, err := a.workspaceRegistry().LoadProjection(t.Context())
127 if err != nil {
128 t.Fatal(err)
129 }
130 pins, err := a.historicalPinnedShellsFromProjection(ProjectTopicPageRequest{Scope: scope, WorkspaceRoot: root}, state, workspacestate.NewWorkspaceIndex(state), desktopProject{Root: root, Topics: []string{topic.ID}, PinnedTopics: []string{topic.ID}})
131 if err != nil || len(pins) != 0 {
132 t.Fatalf("purged topic returned in pinned shell: %+v, %v", pins, err)
133 }
134 // Independent legacy content can share the consumed topic ID.
135 // Its real source row must remain visible after rediscovery.
136 sibling := writeTopicSessionWithPrompt(t, dir, "independent-sibling.jsonl", topic.ID, topic.Title, root, "independent history", time.Now())
137 openCatalog(a, "rebuilt-catalog.sqlite")
138 reconcileSessionCatalogForTest(t, a, dir, scope, root)
139 page, err := a.ListProjectTopics(ProjectTopicPageRequest{Scope: scope, WorkspaceRoot: root, Limit: 1})
140 if err != nil || len(page.Items) != 1 || !sameDesktopPath(page.Items[0].SessionPath, sibling) || page.NextCursor != "" {
141 t.Fatalf("topic adoption hid independent history: %+v, %v", page, err)
142 }
143 if _, err := a.CreateTopic(scope, root, "Unrelated empty topic"); err != nil {
144 t.Fatal(err)
145 }
146 assertVisible(2)
147 })
148 }
149 }
150 }
151 }
152
152 lines GO