返回 DeepSeek-Reasonix
session_source_rows_cache_test.go
根目录 / desktop / session_source_rows_cache_test.go
1 package main
2
3 import (
4 "os"
5 "path/filepath"
6 "testing"
7 "time"
8
9 "reasonix/desktop/internal/workspacestate"
10 "reasonix/internal/agent"
11 "reasonix/internal/session"
12 "reasonix/internal/sessioncatalog"
13 "reasonix/internal/store"
14 )
15
16 func TestArchivedSourceRecoversAfterHeadIndexPublication(t *testing.T) {
17 for _, metadataOnly := range []bool{false, true} {
18 name := "materialized"
19 if metadataOnly {
20 name = "lazy"
21 }
22 t.Run(name, func(t *testing.T) {
23 isolateDesktopUserDirs(t)
24 path, _, head := migrationSingleDAGFixture(t)
25 app := newHistoricalLifecycleApp(t)
26 if err := app.migrateDesktopSessionsV5(t.Context()); err != nil {
27 t.Fatal(err)
28 }
29 catalog, err := sessioncatalog.Open(t.Context(), sessioncatalog.Options{
30 Path: filepath.Join(t.TempDir(), "catalog.sqlite"), MetadataOnly: metadataOnly, StartPaused: true, DisableRepair: true,
31 })
32 if err != nil {
33 t.Fatal(err)
34 }
35 app.sessionCatalog.Store(catalog)
36 t.Cleanup(func() { app.desktopSessions.readSnapshots.close(); app.stopSessionCatalog(time.Second) })
37 if err := catalog.ReconcileDirectory(t.Context(), sessioncatalog.DirectoryTarget{Path: filepath.Dir(path), Scope: "global"}); err != nil {
38 t.Fatal(err)
39 }
40 state, err := app.workspaceRegistry().Load(t.Context())
41 if err != nil {
42 t.Fatal(err)
43 }
44 mapping := state.SourceMappings[desktopSourceKey(path, head)]
45 ref := session.SessionRef{HostID: localDesktopHostID, SessionID: mapping.SessionID}
46 if err := app.ArchiveCanonicalSession(ref); err != nil {
47 t.Fatal(err)
48 }
49 indexPath := store.SessionEventIndex(path)
50 index, err := os.ReadFile(indexPath)
51 if err != nil {
52 t.Fatal(err)
53 }
54 // Force a sidebar read between invalidation and publication of the
55 // head index. The transcript checkpoint does not change during repair.
56 if err := os.Remove(indexPath); err != nil {
57 t.Fatal(err)
58 }
59 sourceHeadRows.Delete(path)
60 if sourceMappingHasPathAlias(mapping) {
61 t.Fatal("missing head index unexpectedly proved single-head ownership")
62 }
63 if err := os.WriteFile(indexPath, index, 0600); err != nil {
64 t.Fatal(err)
65 }
66 for _, stage := range []string{"archived", "purged"} {
67 if stage == "purged" {
68 if !metadataOnly {
69 if err := app.PurgeCanonicalSession(ref); err != nil {
70 t.Fatal(err)
71 }
72 } else {
73 trash, err := app.ListTrashEntries("", "", 50)
74 if err != nil || len(trash.Items) != 1 {
75 t.Fatalf("trash before clear: %+v %v", trash, err)
76 }
77 request := SessionLifecycleRequest{OperationID: "clear-cached-source", Action: "purge", ExpectedGeneration: trash.Generation}
78 for _, row := range trash.Items {
79 request.Targets = append(request.Targets, SessionLifecycleTarget{Ref: row.Ref, WorkspaceID: row.WorkspaceID})
80 }
81 if result, err := app.ApplySessionLifecycle(request); err != nil || !result.Committed {
82 t.Fatalf("clear trash: %+v %v", result, err)
83 }
84 }
85 if state, err := app.workspaceRegistry().Load(t.Context()); err != nil || state.SessionStates[ref.SessionID].Lifecycle != workspacestate.Deleted {
86 t.Fatalf("purge did not commit: %+v %v", state.SessionStates[ref.SessionID], err)
87 }
88 }
89 page, err := app.ListProjectTopics(ProjectTopicPageRequest{Scope: "global", Limit: 50})
90 if err != nil {
91 t.Fatal(err)
92 }
93 app.ReleaseReadSnapshot(page.SnapshotID)
94 if len(page.Items) != 0 {
95 t.Errorf("%s source reappeared after index repair: %+v", stage, page.Items)
96 }
97 if rows := app.listSessionsFromDir(filepath.Dir(path), ""); len(rows) != 0 {
98 t.Errorf("%s source reappeared in history after index repair: %+v", stage, rows)
99 }
100 }
101 })
102 }
103 }
104
105 func TestSourceHeadsRejectCachedIndexAfterLogChanges(t *testing.T) {
106 isolateDesktopUserDirs(t)
107 path, _, _ := migrationSingleDAGFixture(t)
108 heads, err := sessionSourceHeads(path)
109 if err != nil || len(heads) != 1 {
110 t.Fatalf("initial heads: %+v %v", heads, err)
111 }
112 logPath := store.SessionEventLog(path)
113 log, err := os.ReadFile(logPath)
114 if err != nil {
115 t.Fatal(err)
116 }
117 // Simulate a writer committing log bytes before its index publication.
118 if err := os.WriteFile(logPath, append(log, '\n'), 0600); err != nil {
119 t.Fatal(err)
120 }
121 if heads, err := sessionSourceHeads(path); err != nil || len(heads) != 0 {
122 t.Fatalf("stale index remained authoritative: %+v %v", heads, err)
123 }
124 if err := os.WriteFile(logPath, log, 0600); err != nil {
125 t.Fatal(err)
126 }
127 if heads, err := sessionSourceHeads(path); err != nil || len(heads) != 1 {
128 t.Fatalf("valid heads did not recover: %+v %v", heads, err)
129 }
130 }
131
132 func TestPurgedSourceDoesNotFallBackToUnidentifiedPath(t *testing.T) {
133 for _, metadataOnly := range []bool{false, true} {
134 t.Run(map[bool]string{false: "materialized", true: "lazy"}[metadataOnly], func(t *testing.T) {
135 isolateDesktopUserDirs(t)
136 path, legacy, head := migrationSingleDAGFixture(t)
137 app := newHistoricalLifecycleApp(t)
138 if err := app.migrateDesktopSessionsV5(t.Context()); err != nil {
139 t.Fatal(err)
140 }
141 state, err := app.workspaceRegistry().Load(t.Context())
142 if err != nil {
143 t.Fatal(err)
144 }
145 ref := session.SessionRef{HostID: localDesktopHostID, SessionID: state.SourceMappings[desktopSourceKey(path, head)].SessionID}
146 if err := app.ArchiveCanonicalSession(ref); err != nil {
147 t.Fatal(err)
148 }
149 if err := app.PurgeCanonicalSession(ref); err != nil {
150 t.Fatal(err)
151 }
152 catalog, err := sessioncatalog.Open(t.Context(), sessioncatalog.Options{
153 Path: filepath.Join(t.TempDir(), "catalog.sqlite"), MetadataOnly: metadataOnly, StartPaused: true, DisableRepair: true,
154 })
155 if err != nil {
156 t.Fatal(err)
157 }
158 app.sessionCatalog.Store(catalog)
159 t.Cleanup(func() { app.desktopSessions.readSnapshots.close(); app.stopSessionCatalog(time.Second) })
160 if err := catalog.ReconcileDirectory(t.Context(), sessioncatalog.DirectoryTarget{Path: filepath.Dir(path), Scope: "global"}); err != nil {
161 t.Fatal(err)
162 }
163 assertRows := func(want int) {
164 t.Helper()
165 page, err := app.ListProjectTopics(ProjectTopicPageRequest{Scope: "global", Limit: 50})
166 if err != nil {
167 t.Fatal(err)
168 }
169 app.ReleaseReadSnapshot(page.SnapshotID)
170 if len(page.Items) != want {
171 t.Errorf("sidebar rows = %+v; want %d", page.Items, want)
172 }
173 if rows := app.listSessionsFromDir(filepath.Dir(path), ""); len(rows) != want {
174 t.Errorf("history rows = %+v; want %d", rows, want)
175 }
176 }
177 indexPath := store.SessionEventIndex(path)
178 index, err := os.ReadFile(indexPath)
179 if err != nil {
180 t.Fatal(err)
181 }
182 if err := os.Remove(indexPath); err != nil {
183 t.Fatal(err)
184 }
185 assertRows(0)
186 if err := os.WriteFile(indexPath, []byte("incomplete index"), 0600); err != nil {
187 t.Fatal(err)
188 }
189 assertRows(0)
190 if err := os.WriteFile(indexPath, index, 0600); err != nil {
191 t.Fatal(err)
192 }
193 assertRows(0)
194 // A sibling can be published before catalog reconciliation sees
195 // its head count. A flat path row cannot express that identity.
196 if _, err := legacy.ForkHead(path, legacy.Snapshot()[1].ID, agent.HeadKindFork, "surviving sibling"); err != nil {
197 t.Fatal(err)
198 }
199 page, err := app.ListProjectTopics(ProjectTopicPageRequest{Scope: "global", Limit: 50})
200 if err != nil {
201 t.Fatal(err)
202 }
203 app.ReleaseReadSnapshot(page.SnapshotID)
204 if len(page.Items) != 1 || page.Items[0].Source == nil || page.Items[0].Source.HeadID == "" || page.Items[0].Source.HeadID == head {
205 t.Fatalf("sibling lost its independent identity: %+v", page.Items)
206 }
207 assertRows(1)
208 })
209 }
210 }
211
212 func TestSourceHeadsObserveForkWithoutCheckpointChange(t *testing.T) {
213 isolateDesktopUserDirs(t)
214 path, legacy, _ := migrationSingleDAGFixture(t)
215 checkpoint, err := os.Stat(path)
216 if err != nil {
217 t.Fatal(err)
218 }
219 if heads, err := sessionSourceHeads(path); err != nil || len(heads) != 1 {
220 t.Fatalf("initial heads: %+v %v", heads, err)
221 }
222 if _, err := legacy.ForkHead(path, legacy.Snapshot()[1].ID, agent.HeadKindFork, "Independent sibling"); err != nil {
223 t.Fatal(err)
224 }
225 // Checkpoint publication can be separate from the authoritative DAG and
226 // head-index writes; restoring its timestamp isolates that boundary.
227 if err := os.Chtimes(path, checkpoint.ModTime(), checkpoint.ModTime()); err != nil {
228 t.Fatal(err)
229 }
230 if heads, err := sessionSourceHeads(path); err != nil || len(heads) != 2 {
231 t.Fatalf("new sibling hidden behind cached heads: %+v %v", heads, err)
232 }
233 }
234
234 lines GO