返回 DeepSeek-Reasonix
removal_pagination_test.go
根目录 / internal / sessioncatalog / removal_pagination_test.go
1 package sessioncatalog
2
3 import (
4 "context"
5 "fmt"
6 "path/filepath"
7 "testing"
8 "time"
9 )
10
11 func TestTombstonesDoNotTruncateSessionOrTopicPagination(t *testing.T) {
12 t.Parallel()
13 ctx := context.Background()
14 catalog, err := Open(ctx, Options{
15 Path: filepath.Join(t.TempDir(), "catalog.sqlite"), DisableRepair: true,
16 })
17 if err != nil {
18 t.Fatal(err)
19 }
20 t.Cleanup(func() { _ = catalog.Close(context.Background()) })
21 catalog.pathIdentity = func(path string) string { return "identity:" + filepath.Clean(path) }
22
23 paths := make([]string, 5)
24 for i := range paths {
25 paths[i] = filepath.Join("/sessions", fmt.Sprintf("page-%d.jsonl", i))
26 if err := catalog.UpsertSession(ctx, SessionRecord{
27 Path: paths[i], Directory: "/sessions", Scope: "global",
28 TopicID: fmt.Sprintf("topic-%d", i), TopicTitle: fmt.Sprintf("Topic %d", i),
29 LastActivityAt: int64(5 - i), TurnsState: TurnsValid, Health: HealthOK,
30 }); err != nil {
31 t.Fatal(err)
32 }
33 }
34 // Keep the two newest SQL rows behind the read-visible overlay, as happens
35 // while an authoritative archive waits for the durable DELETE worker.
36 catalog.removedPaths.Store(catalog.pathKey(paths[0]), uint64(1))
37 catalog.removedPaths.Store(catalog.pathKey(paths[1]), uint64(1))
38
39 sessions, err := catalog.ListSessions(ctx, SessionPageRequest{Scope: "all", Limit: 2})
40 if err != nil {
41 t.Fatal(err)
42 }
43 if len(sessions.Items) != 2 || sessions.Items[0].Path != paths[2] || sessions.Items[1].Path != paths[3] || sessions.NextCursor == "" {
44 t.Fatalf("first visible session page = %+v", sessions)
45 }
46 sessionTail, err := catalog.ListSessions(ctx, SessionPageRequest{Scope: "all", Limit: 2, Cursor: sessions.NextCursor})
47 if err != nil {
48 t.Fatal(err)
49 }
50 if len(sessionTail.Items) != 1 || sessionTail.Items[0].Path != paths[4] {
51 t.Fatalf("session tail = %+v", sessionTail)
52 }
53
54 topics, err := catalog.ListTopics(ctx, TopicPageRequest{Scope: "global", Limit: 2})
55 if err != nil {
56 t.Fatal(err)
57 }
58 if len(topics.Items) != 2 || topics.Items[0].TopicID != "topic-2" || topics.Items[1].TopicID != "topic-3" || topics.NextCursor == "" {
59 t.Fatalf("first visible topic page = %+v", topics)
60 }
61 topicTail, err := catalog.ListTopics(ctx, TopicPageRequest{Scope: "global", Limit: 2, Cursor: topics.NextCursor})
62 if err != nil {
63 t.Fatal(err)
64 }
65 if len(topicTail.Items) != 1 || topicTail.Items[0].TopicID != "topic-4" {
66 t.Fatalf("topic tail = %+v", topicTail)
67 }
68 }
69
70 func TestTopicVisibilityScansPastTombstonedPayloadWindow(t *testing.T) {
71 t.Parallel()
72 ctx := context.Background()
73 catalog, err := Open(ctx, Options{
74 Path: filepath.Join(t.TempDir(), "catalog.sqlite"), DisableRepair: true,
75 })
76 if err != nil {
77 t.Fatal(err)
78 }
79 t.Cleanup(func() { _ = catalog.Close(context.Background()) })
80 catalog.pathIdentity = func(path string) string { return "identity:" + filepath.Clean(path) }
81
82 paths := make([]string, MaxLimit+1)
83 for i := range paths {
84 paths[i] = filepath.Join("/sessions", fmt.Sprintf("deep-%03d.jsonl", i))
85 if err := catalog.UpsertSession(ctx, SessionRecord{
86 Path: paths[i], Directory: "/sessions", Scope: "global", TopicID: "deep",
87 LastActivityAt: int64(len(paths) - i), TurnsState: TurnsValid, Health: HealthOK,
88 }); err != nil {
89 t.Fatal(err)
90 }
91 }
92 for _, path := range paths[:MaxLimit] {
93 catalog.removedPaths.Store(catalog.pathKey(path), uint64(1))
94 }
95 topic, ok, err := catalog.GetTopic(ctx, TopicKey{Scope: "global", TopicID: "deep"})
96 if err != nil {
97 t.Fatal(err)
98 }
99 if !ok || len(topic.Sessions) != 1 || topic.Sessions[0].Path != paths[MaxLimit] {
100 t.Fatalf("topic after payload-window tombstones: ok=%v topic=%+v", ok, topic)
101 }
102 }
103
104 func TestTombstoneOverlayPublishesRefreshableEventBeforeDurableDelete(t *testing.T) {
105 t.Parallel()
106 type revisionEvent struct {
107 revision uint64
108 roots []string
109 reason string
110 }
111 events := make(chan revisionEvent, 8)
112 ctx := context.Background()
113 dir := t.TempDir()
114 path := filepath.Join(dir, "session.jsonl")
115 catalog, err := Open(ctx, Options{
116 Path: filepath.Join(t.TempDir(), "catalog.sqlite"), DisableRepair: true,
117 OnRevision: func(revision uint64, roots []string, reason string) {
118 events <- revisionEvent{revision: revision, roots: roots, reason: reason}
119 },
120 })
121 if err != nil {
122 t.Fatal(err)
123 }
124 t.Cleanup(func() { _ = catalog.Close(context.Background()) })
125 if err := catalog.UpsertSession(ctx, SessionRecord{
126 Path: path, Directory: dir, Scope: "project", WorkspaceRoot: "/workspace",
127 TopicID: "topic", LastActivityAt: 1, TurnsState: TurnsValid, Health: HealthOK,
128 }); err != nil {
129 t.Fatal(err)
130 }
131 for len(events) > 0 {
132 <-events
133 }
134
135 dirLock := catalog.directoryLock(dir)
136 dirLock.Lock()
137 defer dirLock.Unlock()
138 before := catalog.revision.Load()
139 if err := catalog.RemoveSession(ctx, path, "topic_archived"); err != nil {
140 t.Fatal(err)
141 }
142 select {
143 case event := <-events:
144 if event.revision != before || event.reason != "topic_archived" || len(event.roots) != 0 {
145 t.Fatalf("overlay event = %+v, want equal revision, archive reason, and all roots", event)
146 }
147 case <-time.After(2 * time.Second):
148 t.Fatal("tombstone overlay did not publish an immediate project-tree event")
149 }
150 }
151
151 lines GO