返回 DeepSeek-Reasonix
historical_reconciliation_restart_test.go
根目录 / desktop / historical_reconciliation_restart_test.go
1 package main
2
3 import (
4 "os"
5 "path/filepath"
6 "testing"
7
8 "reasonix/desktop/internal/workspacestate"
9 "reasonix/internal/session"
10 )
11
12 func TestHistoricalDiscoveryResumesPublishedRepair(t *testing.T) {
13 for _, replay := range []string{"discovery", "journal"} {
14 t.Run(replay, func(t *testing.T) { checkHistoricalPublishedRepair(t, replay) })
15 }
16 }
17
18 func checkHistoricalPublishedRepair(t *testing.T, replay string) {
19 t.Helper()
20 app, selector, _ := historicalArchiveFixture(t)
21 result, err := app.ImportHistoricalSession(selector.Source.SourceKey)
22 if err != nil {
23 t.Fatal(err)
24 }
25 title, pinned := "Preserve on restart", true
26 if err := app.workspaceRegistry().UpdatePresentation(t.Context(), []string{result.Session.SessionID}, &title, &pinned); err != nil {
27 t.Fatal(err)
28 }
29 app.stopHistoricalImports()
30 app.closeSessionServices()
31 if err := os.Rename(filepath.Join(app.desktopSessions.root, result.Session.SessionID), filepath.Join(t.TempDir(), "removed")); err != nil {
32 t.Fatal(err)
33 }
34 app = newHistoricalLifecycleApp(t)
35 app.lifecycleCheckpointHook = func(phase string) {
36 if phase == "historical-repair-content-published" {
37 panic("simulated exit")
38 }
39 }
40 func() {
41 defer func() {
42 if recover() == nil {
43 t.Error("missing interruption")
44 }
45 }()
46 _, _ = app.ListHistoricalSessions()
47 }()
48 app.stopHistoricalImports()
49 app.closeSessionServices()
50 app = newHistoricalLifecycleApp(t)
51 if replay == "journal" {
52 if err := app.recoverDesktopSessionOperations(t.Context()); err != nil {
53 t.Fatal(err)
54 }
55 }
56 if _, err := app.ListHistoricalSessions(); err != nil {
57 t.Fatal(err)
58 }
59 state, err := app.workspaceRegistry().Load(t.Context())
60 if err != nil {
61 t.Fatal(err)
62 }
63 if pendingHistoricalOperation(state, selector.Source.SourceKey) != nil {
64 t.Fatal("repair journal was not completed")
65 }
66 if p := state.Presentation[result.Session.SessionID]; p.Title != title || !p.Pinned {
67 t.Fatalf("lost presentation: %+v", p)
68 }
69 if err := app.ArchiveCanonicalSession(result.Session); err != nil {
70 t.Fatal(err)
71 }
72 if err := app.PurgeCanonicalSession(result.Session); err != nil {
73 t.Fatal(err)
74 }
75 }
76
77 func TestHistoricalDiscoveryRepairsMissingLegacyDestination(t *testing.T) {
78 for _, damage := range []string{"mapping", "target", "source-changed"} {
79 t.Run(damage, func(t *testing.T) { checkHistoricalLegacyRepair(t, damage) })
80 }
81 }
82
83 func checkHistoricalLegacyRepair(t *testing.T, damage string) {
84 t.Helper()
85 isolateDesktopUserDirs(t)
86 path, _, head := migrationSingleDAGFixture(t)
87 app := newHistoricalLifecycleApp(t)
88 workspace, err := app.ensureDesktopWorkspace(t.Context(), "global", "")
89 if err != nil {
90 t.Fatal(err)
91 }
92 if err := app.migrateLegacySession(t.Context(), path, desktopMigrationSource{scope: "global", headID: head}, workspace); err != nil {
93 t.Fatal(err)
94 }
95 state, err := app.workspaceRegistry().Load(t.Context())
96 if err != nil {
97 t.Fatal(err)
98 }
99 mapping := state.SourceMappings[desktopSourceKey(path, head)]
100 app.stopHistoricalImports()
101 app.closeSessionServices()
102 if damage != "mapping" {
103 if err := os.Rename(filepath.Join(app.desktopSessions.root, mapping.SessionID), filepath.Join(t.TempDir(), "removed")); err != nil {
104 t.Fatal(err)
105 }
106 } else {
107 rewriteHistoricalRegistry(t, app, func(state *workspacestate.State) {
108 state.SourceMappings = map[string]workspacestate.SourceMapping{}
109 state.PendingOperations = map[string]workspacestate.Operation{}
110 })
111 }
112 app = newHistoricalLifecycleApp(t)
113 if damage == "source-changed" {
114 app.lifecycleCheckpointHook = func(phase string) {
115 if phase != "historical-repair-source-exported" {
116 return
117 }
118 body, err := os.ReadFile(path)
119 if err != nil {
120 t.Fatal(err)
121 }
122 if err := os.WriteFile(path, append(body, '\n'), 0600); err != nil {
123 t.Fatal(err)
124 }
125 }
126 }
127 if damage == "mapping" {
128 // The historical startup pass can precede first catalog discovery.
129 // Its completion must recover the receipt without a management action.
130 installSessionCatalogForTest(t, app, filepath.Dir(path), "global", "")
131 app.historicalImports.mu.Lock()
132 app.historicalImports.initialize(t.Context())
133 app.historicalImports.catalogEnabled = true
134 app.historicalImports.mu.Unlock()
135 app.requestHistoricalLegacyReconciliation(t.Context())
136 app.historicalImports.workers.Wait()
137 } else if _, err := app.ListHistoricalSessions(); err != nil {
138 t.Fatal(err)
139 }
140 if damage == "source-changed" {
141 if _, err := os.Stat(filepath.Join(app.desktopSessions.root, mapping.SessionID)); !os.IsNotExist(err) {
142 t.Fatalf("published stale source content: %v", err)
143 }
144 if _, err := os.Stat(path); err != nil {
145 t.Fatal("source was not retained", err)
146 }
147 return
148 }
149 state, err = app.workspaceRegistry().Load(t.Context())
150 if err != nil {
151 t.Fatal(err)
152 }
153 if restored, found, err := state.ResolveSource(desktopSourceKey(path, head)); err != nil || !found || restored.SessionID != mapping.SessionID {
154 t.Fatalf("legacy receipt not repaired: %+v %v", restored, err)
155 }
156 ref := session.SessionRef{HostID: localDesktopHostID, SessionID: mapping.SessionID}
157 if _, err := app.desktopSessionService("").Query().Snapshot(t.Context(), ref); err != nil {
158 t.Fatal(err)
159 }
160 if err := app.ArchiveCanonicalSession(ref); err != nil {
161 t.Fatal(err)
162 }
163 if err := app.PurgeCanonicalSession(ref); err != nil {
164 t.Fatal(err)
165 }
166 state, err = app.workspaceRegistry().Load(t.Context())
167 if err != nil || state.SessionStates[ref.SessionID].Lifecycle != workspacestate.Deleted {
168 t.Fatalf("legacy destination not deleted: %v", err)
169 }
170 }
171
171 lines GO