返回 DeepSeek-Reasonix
session_migration_startup_test.go
根目录 / desktop / session_migration_startup_test.go
1 package main
2
3 import (
4 "bytes"
5 "context"
6 "errors"
7 "os"
8 "path/filepath"
9 "reflect"
10 "testing"
11 "time"
12
13 "reasonix/internal/config"
14 "reasonix/internal/identitylock"
15 "reasonix/internal/session"
16 )
17
18 // Startup may discover historical metadata, but only an explicit open/import
19 // request is allowed to convert source content or replay a prepared import.
20 func TestDesktopStartupLeavesColdHistoryForExplicitImport(t *testing.T) {
21 for _, scope := range []string{"global", "project"} {
22 t.Run(scope, func(t *testing.T) {
23 isolateDesktopUserDirs(t)
24 root := config.SessionStoreDir()
25 if scope == "project" {
26 workspace := t.TempDir()
27 root = config.ProjectSessionStoreDir(workspace)
28 if err := saveProjectsFile(desktopProjectFile{Projects: []desktopProject{{Root: workspace}}}); err != nil {
29 t.Fatal(err)
30 }
31 }
32 const id = "startup-cold-history"
33 coldV4MigrationFixture(t, root, id)
34 original := startupHistorySourceBytes(t, root, id)
35 app := NewApp()
36 t.Cleanup(app.closeSessionServices)
37 startupExistingV5Fixture(t, app)
38 runHistoryDiscoveryStartup(t, app)
39 ref := session.SessionRef{HostID: localDesktopHostID, SessionID: id}
40 if _, err := app.desktopSessionService("").Query().Snapshot(t.Context(), ref); !errors.Is(err, session.ErrSessionNotFound) {
41 t.Errorf("startup must not publish historical content without an explicit import: %v", err)
42 }
43 assertStartupHistorySourceUnchanged(t, root, id, original)
44 assertStartupExistingV5Available(t, app)
45 })
46 }
47 }
48
49 func TestDesktopStartupPreservesPreparedImportWithoutWaitingForSource(t *testing.T) {
50 for _, lockName := range []string{"writer", "ownership"} {
51 t.Run(lockName, func(t *testing.T) {
52 isolateDesktopUserDirs(t)
53 root := config.SessionStoreDir()
54 const id = "startup-interrupted-import"
55 coldV4MigrationFixture(t, root, id)
56 original := startupHistorySourceBytes(t, root, id)
57 app := NewApp()
58 t.Cleanup(app.closeSessionServices)
59 startupExistingV5Fixture(t, app)
60 workspace, err := app.ensureDesktopWorkspace(t.Context(), "global", "")
61 if err != nil {
62 t.Fatal(err)
63 }
64 sourcePath := filepath.Join(root, id)
65 fingerprint, err := desktopSourceFingerprint(sourcePath)
66 if err != nil {
67 t.Fatal(err)
68 }
69 opID, err := app.prepareDesktopImport(t.Context(), desktopMigrationSource{scope: "global"}, sourcePath, fingerprint, id, workspace)
70 if err != nil {
71 t.Fatal(err)
72 }
73 before, err := app.workspaceRegistry().Load(t.Context())
74 if err != nil {
75 t.Fatal(err)
76 }
77 ledger := []byte(`{"version":1,"records":{"interrupted-source":{"sourceKey":"interrupted-source","targetSessionId":"startup-interrupted-import","status":"pending","attempts":4}},"futureField":{"preserve":true}}`)
78 if err := os.MkdirAll(filepath.Dir(desktopMigrationLedgerPath()), 0o700); err != nil {
79 t.Fatal(err)
80 }
81 if err := os.WriteFile(desktopMigrationLedgerPath(), ledger, 0o600); err != nil {
82 t.Fatal(err)
83 }
84 lockPath := filepath.Join(sourcePath, "writer.lock")
85 if lockName == "ownership" {
86 lockPath = filepath.Join(root, "."+id+".ownership.lock")
87 }
88 release, err := identitylock.Acquire(t.Context(), lockPath)
89 if err != nil {
90 t.Fatal(err)
91 }
92 defer release()
93 runHistoryDiscoveryStartup(t, app)
94 after, err := app.workspaceRegistry().Load(t.Context())
95 if err != nil {
96 t.Fatal(err)
97 }
98 if !reflect.DeepEqual(before.PendingOperations[opID], after.PendingOperations[opID]) {
99 t.Error("startup changed a prepared historical import without an explicit request")
100 }
101 if body := fileBytes(t, desktopMigrationLedgerPath()); !bytes.Equal(body, ledger) {
102 t.Error("startup modified the historical ledger, including unrecognized fields")
103 }
104 assertStartupHistorySourceUnchanged(t, root, id, original)
105 assertStartupExistingV5Available(t, app)
106 })
107 }
108 }
109
110 func runHistoryDiscoveryStartup(t *testing.T, app *App) {
111 t.Helper()
112 ctx, cancel := context.WithCancel(t.Context())
113 defer cancel()
114 app.ctx = ctx
115 app.startDesktopSessionMigration(ctx)
116 select {
117 case <-app.desktopMigrationDone:
118 case <-time.After(5 * time.Second):
119 // Source locks are deliberately retained throughout startup. This is a
120 // deadlock watchdog, not a startup performance requirement.
121 if app.runtimeRebuildMu.TryLock() {
122 app.runtimeRebuildMu.Unlock()
123 } else {
124 t.Error("historical startup wait retained the global runtime lock")
125 }
126 if app.runtimeAdmissionMu.TryRLock() {
127 app.runtimeAdmissionMu.RUnlock()
128 } else {
129 t.Error("historical startup wait blocked runtime admission")
130 }
131 cancel()
132 select {
133 case <-app.desktopMigrationDone:
134 case <-time.After(5 * time.Second):
135 t.Fatal("historical startup ignored cancellation")
136 }
137 t.Error("startup waited for a historical source lock without an import request")
138 }
139 }
140
141 func startupHistorySourceBytes(t *testing.T, root, id string) map[string][]byte {
142 t.Helper()
143 out := make(map[string][]byte)
144 for _, name := range []string{"manifest.json", "events.frames"} {
145 out[name] = fileBytes(t, filepath.Join(root, id, name))
146 }
147 return out
148 }
149
150 func assertStartupHistorySourceUnchanged(t *testing.T, root, id string, before map[string][]byte) {
151 t.Helper()
152 for name, body := range before {
153 if !bytes.Equal(body, fileBytes(t, filepath.Join(root, id, name))) {
154 t.Errorf("historical source %s was modified by startup", name)
155 }
156 }
157 }
158
159 func startupExistingV5Fixture(t *testing.T, app *App) {
160 t.Helper()
161 workspace, err := app.ensureDesktopWorkspace(t.Context(), "global", "")
162 if err != nil {
163 t.Fatal(err)
164 }
165 service := app.desktopSessionService("")
166 runtime, err := service.Create(t.Context(), session.CreateOptions{SessionID: "startup-existing-v5", CWD: globalWorkspaceRoot(), Origin: session.SessionOriginNew})
167 if err != nil {
168 t.Fatal(err)
169 }
170 if err := service.Close(t.Context(), runtime.Ref()); err != nil {
171 t.Fatal(err)
172 }
173 if err := app.workspaceRegistry().AttachSession(t.Context(), "", workspace, runtime.Ref().SessionID, ""); err != nil {
174 t.Fatal(err)
175 }
176 }
177
178 func assertStartupExistingV5Available(t *testing.T, app *App) {
179 t.Helper()
180 if _, err := app.desktopSessionService("").Query().Snapshot(t.Context(), session.SessionRef{HostID: localDesktopHostID, SessionID: "startup-existing-v5"}); err != nil {
181 t.Fatalf("existing v5 session became unavailable: %v", err)
182 }
183 if !app.runtimeRebuildMu.TryLock() {
184 t.Fatal("startup retained the runtime lock")
185 }
186 app.runtimeRebuildMu.Unlock()
187 if !app.runtimeAdmissionMu.TryRLock() {
188 t.Fatal("startup retained the runtime admission lock")
189 }
190 app.runtimeAdmissionMu.RUnlock()
191 }
192
192 lines GO