返回 DeepSeek-Reasonix
historical_source_identity_test.go
根目录 / desktop / historical_source_identity_test.go
1 package main
2
3 import (
4 "crypto/sha256"
5 "encoding/json"
6 "fmt"
7 "os"
8 "path/filepath"
9 "reflect"
10 "slices"
11 "testing"
12
13 "reasonix/desktop/internal/workspacestate"
14 "reasonix/internal/config"
15 )
16
17 func rewriteHistoricalSourceIdentityForTest(t *testing.T, app *App, key string) string {
18 t.Helper()
19 state, err := app.workspaceRegistry().Load(t.Context())
20 if err != nil {
21 t.Fatal(err)
22 }
23 mapping, committed := state.SourceMappings[key]
24 if !committed {
25 for _, op := range state.PendingOperations {
26 if op.Mapping != nil && op.Mapping.SourceKey == key {
27 mapping = *op.Mapping
28 break
29 }
30 }
31 }
32 if mapping.Path == "" {
33 t.Fatal("fixture has no source receipt")
34 }
35 oldKey := fmt.Sprintf("%x", sha256.Sum256([]byte(mapping.Path+"\x00")))
36 if oldKey == key { // Case-sensitive CI still exercises a changed persisted identity.
37 oldKey = fmt.Sprintf("%x", sha256.Sum256([]byte("previous-spelling:"+mapping.Path+"\x00")))
38 }
39 if committed {
40 delete(state.SourceMappings, key)
41 mapping.SourceKey = oldKey
42 state.SourceMappings[oldKey] = mapping
43 }
44 for id, op := range state.PendingOperations {
45 if op.Mapping != nil && op.Mapping.SourceKey == key {
46 copy := *op.Mapping
47 copy.SourceKey = oldKey
48 op.Mapping = &copy
49 state.PendingOperations[id] = op
50 }
51 }
52 body, err := json.Marshal(state)
53 if err != nil {
54 t.Fatal(err)
55 }
56 registryPath := app.workspaceRegistry().Path()
57 if err := os.WriteFile(registryPath, body, 0600); err != nil {
58 t.Fatal(err)
59 }
60 return oldKey
61 }
62
63 func TestHistoricalSourceIdentityUpgradeKeepsOneConversation(t *testing.T) {
64 isolateDesktopUserDirs(t)
65 const id = "MixedCase-history"
66 coldV4MigrationFixture(t, config.SessionStoreDir(), id)
67 app := newHistoricalLifecycleApp(t)
68 key := historicalLifecycleID(t, app, id)
69 imported, err := app.ImportHistoricalSession(key)
70 if err != nil {
71 t.Fatal(err)
72 }
73 app.stopHistoricalImports()
74 app.closeSessionServices()
75 oldKey := rewriteHistoricalSourceIdentityForTest(t, app, key)
76 state, err := app.workspaceRegistry().Load(t.Context())
77 if err != nil {
78 t.Fatal(err)
79 }
80 mapping := state.SourceMappings[oldKey]
81 for range 2 {
82 app = newHistoricalLifecycleApp(t)
83 listed, err := app.ListHistoricalSessions()
84 if err != nil || len(listed.Items) != 1 || listed.Items[0].Session == nil || *listed.Items[0].Session != imported.Session {
85 t.Fatalf("discovery lost adoption: %+v %v", listed, err)
86 }
87 page, err := app.ListProjectTopics(ProjectTopicPageRequest{Scope: "global", Limit: 50})
88 if err != nil || len(page.Items) != 1 || page.Items[0].Session == nil || *page.Items[0].Session != imported.Session {
89 t.Fatalf("duplicate sidebar rows: %+v %v", page.Items, err)
90 }
91 if !slices.Contains(page.Items[0].IdentityAliases, "source\x00local\x00"+key) {
92 t.Fatal("current source row cannot merge with canonical identity")
93 }
94 for _, supplied := range []string{oldKey, key} {
95 target, err := app.resolveSessionTarget(SessionSelector{Source: &SessionSourceRef{HostID: localDesktopHostID, SourceKey: supplied, Path: mapping.Path}})
96 if err != nil || target.SessionRef != imported.Session {
97 t.Fatalf("source %q opened another target: %+v %v", supplied, target, err)
98 }
99 preparedKey, _, err := app.historicalSourceForSelector(SessionSelector{Source: &SessionSourceRef{SourceKey: supplied, Path: mapping.Path}})
100 if err != nil || preparedKey != oldKey {
101 t.Fatalf("preparation changed durable identity: %s %v", preparedKey, err)
102 }
103 }
104 if _, err := app.resolveSessionTarget(SessionSelector{Source: &SessionSourceRef{SourceKey: oldKey, Path: filepath.Join(mapping.Path, "different")}}); err == nil {
105 t.Fatal("old source key authorized a different path")
106 }
107 ref, adopted, err := app.legacyCanonicalRef(t.Context(), mapping.Path)
108 if err != nil || !adopted || ref != imported.Session {
109 t.Fatalf("saved path failed canonical binding: %+v %v", ref, err)
110 }
111 view, err := app.workspaceRegistry().Load(t.Context())
112 if err != nil {
113 t.Fatal(err)
114 }
115 saved := desktopTabEntry{Scope: "global", SessionPath: mapping.Path}
116 if app.savedTabHistoricalSource(saved, savedTabReconcileEvidence{registry: view}) != nil {
117 t.Fatal("restored canonical tab became a historical shell")
118 }
119 again, err := app.ImportHistoricalSession(key)
120 if err != nil || again.Session != imported.Session {
121 t.Fatalf("reimport duplicated identity: %+v %v", again, err)
122 }
123 after, err := app.workspaceRegistry().Load(t.Context())
124 if err != nil || !reflect.DeepEqual(after.SourceMappings, state.SourceMappings) || !reflect.DeepEqual(after.PendingOperations, state.PendingOperations) || !reflect.DeepEqual(after.SessionStates, state.SessionStates) {
125 t.Fatal("compatibility lookup changed adoption evidence or session lifecycles")
126 }
127 if len(after.Workspaces[workspacestate.GlobalWorkspaceID].SessionIDs) != 1 {
128 t.Fatal("reimport added a second conversation")
129 }
130 app.desktopSessions.readSnapshots.close()
131 app.stopHistoricalImports()
132 app.closeSessionServices()
133 }
134 // One lifecycle owns both source spellings, including after a cold start.
135 app = newHistoricalLifecycleApp(t)
136 assertRows := func(want int) {
137 t.Helper()
138 if _, err := app.ListHistoricalSessions(); err != nil {
139 t.Fatal(err)
140 }
141 page, err := app.ListProjectTopics(ProjectTopicPageRequest{Scope: "global", Limit: 50})
142 if err != nil || len(page.Items) != want {
143 t.Fatalf("lifecycle sidebar: %+v %v; want %d", page.Items, err, want)
144 }
145 }
146 assertRetiredAfterRestart := func(lifecycle string) {
147 t.Helper()
148 app.desktopSessions.readSnapshots.close()
149 app.stopHistoricalImports()
150 app.closeSessionServices()
151 app = newHistoricalLifecycleApp(t)
152 assertRows(0)
153 if _, err := app.ImportHistoricalSession(key); err == nil {
154 t.Fatal("retired source was imported again")
155 }
156 current, err := app.workspaceRegistry().Load(t.Context())
157 if err != nil || current.SessionStates[imported.Session.SessionID].Lifecycle != lifecycle || len(current.SourceMappings) != 1 {
158 t.Fatalf("retired identity changed: %v", err)
159 }
160 }
161 archive, err := app.ArchiveSessionTarget(SessionSelector{Source: &SessionSourceRef{Path: mapping.Path, SourceKey: key}})
162 if err != nil || !archive.Committed {
163 t.Fatalf("archive through old source: %+v %v", archive, err)
164 }
165 assertRetiredAfterRestart(workspacestate.Archived)
166 trash, err := app.ListTrashEntries("", "", 50)
167 if err != nil || len(trash.Items) != 1 || trash.Items[0].Ref == nil || *trash.Items[0].Ref != imported.Session {
168 t.Fatalf("duplicate archive entries: %+v %v", trash, err)
169 }
170 if err := app.RestoreCanonicalSession(imported.Session); err != nil {
171 t.Fatal(err)
172 }
173 assertRows(1)
174 if err := app.ArchiveCanonicalSession(imported.Session); err != nil {
175 t.Fatal(err)
176 }
177 if err := app.PurgeCanonicalSession(imported.Session); err != nil {
178 t.Fatal(err)
179 }
180 assertRetiredAfterRestart(workspacestate.Deleted)
181 app.desktopSessions.readSnapshots.close()
182 if _, err := os.Stat(mapping.Path); !os.IsNotExist(err) {
183 t.Fatalf("exclusive original survived purge: %v", err)
184 }
185 }
186
186 lines GO