返回 DeepSeek-Reasonix
topic_removal_review_test.go
根目录 / desktop / topic_removal_review_test.go
1 package main
2
3 import (
4 "encoding/json"
5 "os"
6 "path/filepath"
7 "testing"
8
9 "reasonix/desktop/internal/workspacestate"
10 )
11
12 func TestTopicRemovalReadsSupportedV1Organization(t *testing.T) {
13 a, topic, _ := topicRemovalFixture(t, "global", "")
14 file, err := readTopicRemovalProjects()
15 if err != nil {
16 t.Fatal(err)
17 }
18 file.GlobalGroups = []desktopGroup{{ID: "saved", Title: "Saved", TopicIDs: []string{topic.ID}}}
19 if err := saveProjectsFile(file); err != nil {
20 t.Fatal(err)
21 }
22 path := filepath.Join(desktopConfigDir(), desktopProjectOrganizationFile)
23 body, err := os.ReadFile(path)
24 if err != nil {
25 t.Fatal(err)
26 }
27 var organization map[string]any
28 if err := json.Unmarshal(body, &organization); err != nil {
29 t.Fatal(err)
30 }
31 organization["version"] = 1
32 body, err = json.Marshal(organization)
33 if err != nil {
34 t.Fatal(err)
35 }
36 if err := os.WriteFile(path, body, 0600); err != nil {
37 t.Fatal(err)
38 }
39 view, err := a.InspectTopicRemoval(TopicRemovalTarget{TopicID: topic.ID})
40 if err != nil || !view.Allowed || view.Disposition != "archive_placeholder" {
41 t.Fatalf("supported legacy organization must remain recoverable: %+v %v", view, err)
42 }
43 }
44
45 func TestTopicRemovalRecoveryRejectsReusedIDInAnotherWorkspace(t *testing.T) {
46 for _, action := range []string{"retry", "restore"} {
47 t.Run(action, func(t *testing.T) {
48 a, topic, _ := topicRemovalFixture(t, "global", "Original")
49 request := inspectedTopicRemoval(t, a, topic.ID)
50 if action == "retry" {
51 interruptTopicRemoval(t, a, "topic-removal-before-metadata", func() { _, _ = a.RemoveTopic(request) })
52 } else if result, err := a.RemoveTopic(request); err != nil || !result.Committed {
53 t.Fatalf("remove: %+v %v", result, err)
54 }
55 root := t.TempDir()
56 if err := addProject(root, "Other owner"); err != nil {
57 t.Fatal(err)
58 }
59 if err := updateProjectsFile(func(file *desktopProjectFile) (bool, error) {
60 i := projectIndexByRoot(file.Projects, root)
61 file.Projects[i].Topics = append(file.Projects[i].Topics, topic.ID)
62 return true, nil
63 }); err != nil {
64 t.Fatal(err)
65 }
66 if err := setTopicTitle(root, topic.ID, "New owner"); err != nil {
67 t.Fatal(err)
68 }
69 if action == "retry" {
70 result, err := a.RemoveTopic(request)
71 if err != nil || result.Committed || result.ErrorCode != "state_conflict" {
72 t.Fatalf("retry changed a new owner: %+v %v", result, err)
73 }
74 } else if err := a.restoreRemovedTopic(request.OperationID, request.Target.WorkspaceID); err == nil {
75 t.Fatal("restored over a different owner")
76 }
77 if !topicIndexedInRegistry("project", root, topic.ID) || loadTopicTitle(root, topic.ID) != "New owner" {
78 t.Fatal("new owner's data changed")
79 }
80 })
81 }
82 }
83
84 func TestTopicRemovalCanonicalConfirmationFencedAtArchiveAdmission(t *testing.T) {
85 for _, phase := range []string{"topic-sessions-before-archive", "before-archive-commit"} {
86 t.Run(phase, func(t *testing.T) { testTopicRemovalCanonicalConfirmation(t, phase) })
87 }
88 }
89
90 func testTopicRemovalCanonicalConfirmation(t *testing.T, checkpoint string) {
91 a, _, _ := topicRemovalFixture(t, "global", "")
92 tab, err := a.EnsureBlankTab("global", "")
93 if err != nil {
94 t.Fatal(err)
95 }
96 t.Cleanup(func() {
97 if err := a.CloseTab(tab.ID); err != nil {
98 t.Error(err)
99 }
100 })
101 request := inspectedTopicRemoval(t, a, tab.TopicID)
102 a.lifecycleCheckpointHook = func(phase string) {
103 if phase != checkpoint {
104 return
105 }
106 title := "Edited after confirmation"
107 if err := a.workspaceRegistry().UpdatePresentation(t.Context(), []string{tab.SessionID}, &title, nil); err != nil {
108 t.Fatal(err)
109 }
110 }
111 result, err := a.RemoveTopic(request)
112 a.lifecycleCheckpointHook = nil
113 if err != nil || result.Committed || result.ErrorCode != "state_conflict" {
114 t.Fatalf("archived changed confirmation: %+v %v", result, err)
115 }
116 state, err := a.workspaceRegistry().Load(t.Context())
117 if err != nil || state.SessionStates[tab.SessionID].Lifecycle != workspacestate.Active {
118 t.Fatalf("changed session not active: %v", err)
119 }
120 // The archive child is durable by this point. Generic startup replay must
121 // enforce the same confirmation fence instead of bypassing the topic RPC.
122 if err := a.recoverDesktopOperations(t.Context(), false); err == nil {
123 t.Fatal("replay accepted stale confirmation")
124 }
125 state, err = a.workspaceRegistry().Load(t.Context())
126 if err != nil || state.SessionStates[tab.SessionID].Lifecycle != workspacestate.Active {
127 t.Fatalf("replay archived the changed session: %v", err)
128 }
129 }
130
131 func TestTopicRemovalInterruptedRestoreSurvivesUnrelatedEdit(t *testing.T) {
132 a, topic, _ := topicRemovalFixture(t, "global", "Restore this")
133 request := inspectedTopicRemoval(t, a, topic.ID)
134 if result, err := a.RemoveTopic(request); err != nil || !result.Committed {
135 t.Fatalf("remove: %+v %v", result, err)
136 }
137 interruptTopicRemoval(t, a, "topic-restore-before-metadata", func() {
138 _ = a.restoreRemovedTopic(request.OperationID, request.Target.WorkspaceID)
139 })
140 other, err := a.CreateTopic("global", "", "Unrelated new topic")
141 if err != nil {
142 t.Fatal(err)
143 }
144 if err := a.restoreRemovedTopic(request.OperationID, request.Target.WorkspaceID); err != nil {
145 t.Fatalf("unrelated edit permanently blocks interrupted restore: %v", err)
146 }
147 if loadTopicTitle("", topic.ID) != topic.Title || loadTopicTitle("", other.ID) != other.Title {
148 t.Fatal("restore changed unrelated data")
149 }
150 }
151
151 lines GO