返回 DeepSeek-Reasonix
historical_archive_removed_topic_test.go
根目录 / desktop / historical_archive_removed_topic_test.go
1 package main
2
3 import (
4 "encoding/json"
5 "os"
6 "path/filepath"
7 "slices"
8 "testing"
9 "time"
10
11 "reasonix/desktop/internal/workspacestate"
12 "reasonix/internal/agent"
13 "reasonix/internal/session"
14 )
15
16 // Earlier purge writers dropped the deleted session's topic from every receipt,
17 // so its retained legacy transcript lists again as a live topic. Clearing that
18 // row must survive a restart (#10735).
19 func TestAlreadyRemovedSourceStaysRemovedAfterRestart(t *testing.T) {
20 isolateDesktopUserDirs(t)
21 path, _, head := migrationSingleDAGFixture(t)
22 const topicID = "topic_20260903-142302_04afb4407ec2fa2a"
23 if err := agent.UpdateBranchMeta(path, false, func(m *agent.BranchMeta) error {
24 m.Scope, m.TopicID, m.TopicTitle = "global", topicID, "Ghost"
25 return nil
26 }); err != nil {
27 t.Fatal(err)
28 }
29 if err := saveProjectsFile(desktopProjectFile{GlobalTopics: []string{topicID}}); err != nil {
30 t.Fatal(err)
31 }
32 app := NewApp()
33 app.ctx = t.Context()
34 pinDesktopSessionRoot(t, app)
35 root := app.desktopSessions.root
36 installNoopRuntimeEvents(app)
37 t.Cleanup(app.closeSessionServices)
38 if err := app.migrateDesktopSessionsV5(t.Context()); err != nil {
39 t.Fatal(err)
40 }
41 state, err := app.workspaceRegistry().Load(t.Context())
42 if err != nil {
43 t.Fatal(err)
44 }
45 ref := session.SessionRef{HostID: localDesktopHostID, SessionID: state.SourceMappings[desktopSourceKey(path, head)].SessionID}
46 if err := app.ArchiveCanonicalSession(ref); err != nil {
47 t.Fatal(err)
48 }
49 if err := app.PurgeCanonicalSession(ref); err != nil {
50 t.Fatal(err)
51 }
52 app.closeSessionServices()
53 dropTopicEvidence(t, app.workspaceRegistry().Path(), ref.SessionID)
54
55 restart := func() *App {
56 next := NewApp()
57 next.ctx = t.Context()
58 next.desktopSessions.root = root
59 installNoopRuntimeEvents(next)
60 t.Cleanup(next.closeSessionServices)
61 installSessionCatalogForTest(t, next, filepath.Dir(path), "global", "")
62 t.Cleanup(func() { next.stopSessionCatalog(time.Second) })
63 return next
64 }
65 list := func(a *App) []ProjectNode {
66 t.Helper()
67 page, err := a.ListProjectTopics(ProjectTopicPageRequest{Scope: "global", Limit: 50})
68 if err != nil {
69 t.Fatal(err)
70 }
71 a.ReleaseReadSnapshot(page.SnapshotID)
72 return page.Items
73 }
74 app = restart()
75 rows := list(app)
76 if len(rows) != 1 || rows[0].TopicID != topicID {
77 t.Fatalf("fixture must reproduce the residual row: %+v", rows)
78 }
79 result, err := app.ArchiveSessionTarget(SessionSelector{Source: &SessionSourceRef{HostID: localDesktopHostID, Path: path, HeadID: head}})
80 if err != nil || !result.Committed || result.Outcome != "already_removed" {
81 t.Fatalf("clear residual row = %+v, %v", result, err)
82 }
83 app.stopSessionCatalog(time.Second)
84 app.closeSessionServices()
85 app = restart()
86 if rows := list(app); len(rows) != 0 {
87 t.Fatalf("cleared residual row returned after restart: %+v", rows)
88 }
89 }
90
91 func dropTopicEvidence(t *testing.T, statePath, sessionID string) {
92 t.Helper()
93 body, err := os.ReadFile(statePath)
94 if err != nil {
95 t.Fatal(err)
96 }
97 var state map[string]any
98 if err := json.Unmarshal(body, &state); err != nil {
99 t.Fatal(err)
100 }
101 ops, _ := state["pendingOperations"].(map[string]any)
102 for _, raw := range ops {
103 op, _ := raw.(map[string]any)
104 ids, _ := op["sessionIds"].([]any)
105 if slices.Contains(ids, any(sessionID)) {
106 delete(op, "presentation")
107 }
108 }
109 if body, err = json.Marshal(state); err != nil {
110 t.Fatal(err)
111 }
112 if err := os.WriteFile(statePath, body, 0o600); err != nil {
113 t.Fatal(err)
114 }
115 if _, err := workspacestate.NewStore(statePath).Load(t.Context()); err != nil {
116 t.Fatalf("rewritten state: %v", err)
117 }
118 }
119
120 func TestRemovedSourceTopicStaysWhileAnotherTranscriptCarriesIt(t *testing.T) {
121 dir := t.TempDir()
122 const topicID = "topic_shared"
123 paths := []string{filepath.Join(dir, "removed.jsonl"), filepath.Join(dir, "sibling.jsonl")}
124 for _, path := range paths {
125 if err := agent.NewSession("system").Save(path); err != nil {
126 t.Fatal(err)
127 }
128 if err := agent.UpdateBranchMeta(path, false, func(m *agent.BranchMeta) error { m.TopicID = topicID; return nil }); err != nil {
129 t.Fatal(err)
130 }
131 }
132 state := workspacestate.State{
133 SessionStates: map[string]workspacestate.SessionState{"gone": {Lifecycle: workspacestate.Deleted}},
134 SourceMappings: map[string]workspacestate.SourceMapping{"removed": {SourceKey: "removed", Path: paths[0], SessionID: "gone"}},
135 }
136 if shared, err := legacyTopicSharedBeyond(state, paths[0], topicID); err != nil || !shared {
137 t.Fatalf("unadopted sibling transcript = %v, %v; want shared", shared, err)
138 }
139 state.SourceMappings["sibling"] = workspacestate.SourceMapping{SourceKey: "sibling", Path: paths[1], SessionID: "gone"}
140 if shared, err := legacyTopicSharedBeyond(state, paths[0], topicID); err != nil || shared {
141 t.Fatalf("sibling adopted by a deleted session = %v, %v; want unshared", shared, err)
142 }
143 state.Presentation = map[string]workspacestate.Presentation{"live": {TopicID: topicID}}
144 state.SessionStates["live"] = workspacestate.SessionState{Lifecycle: workspacestate.Archived}
145 if !topicHasSurvivingOwner(state, topicID) {
146 t.Fatal("archived session no longer owns its topic")
147 }
148 }
149
149 lines GO