返回 DeepSeek-Reasonix
windows_upgrade_fixture_test.go
根目录 / desktop / windows_upgrade_fixture_test.go
1 package main
2
3 import (
4 "context"
5 "encoding/json"
6 "os"
7 "path/filepath"
8 "strings"
9 "sync"
10 "testing"
11 "time"
12
13 "reasonix/desktop/internal/upgradefixture"
14 "reasonix/internal/agent"
15 "reasonix/internal/config"
16 )
17
18 func TestWindowsUpgradeFixtureImportsLegacyOnlyWhenPrepared(t *testing.T) {
19 isolateDesktopUserDirs(t)
20 home := filepath.Join(t.TempDir(), "home # %20 中文")
21 t.Setenv("REASONIX_HOME", home)
22 t.Setenv("REASONIX_STATE_HOME", home)
23 t.Setenv("REASONIX_CACHE_HOME", filepath.Join(home, "cache"))
24 reportPath := filepath.Join(t.TempDir(), "fixture.json")
25 if err := upgradefixture.Run("create", home, reportPath, ""); err != nil {
26 t.Fatal(err)
27 }
28 // Reports and runtime manifests may spell the same file differently.
29 // Windows runtime paths fold drive case; dot segments exercise the same
30 // identity requirement on every platform without rewriting real history.
31 var report map[string]json.RawMessage
32 body, err := os.ReadFile(reportPath)
33 if err != nil {
34 t.Fatal(err)
35 }
36 if err := json.Unmarshal(body, &report); err != nil {
37 t.Fatal(err)
38 }
39 var legacyPath string
40 if err := json.Unmarshal(report["legacyPath"], &legacyPath); err != nil {
41 t.Fatal(err)
42 }
43 legacyPath = agent.CanonicalSessionPath(legacyPath)
44 report["legacyPath"], err = json.Marshal(filepath.Dir(legacyPath) + string(filepath.Separator) + "." + string(filepath.Separator) + filepath.Base(legacyPath))
45 if err != nil {
46 t.Fatal(err)
47 }
48 body, err = json.Marshal(report)
49 if err != nil {
50 t.Fatal(err)
51 }
52 if err := os.WriteFile(reportPath, body, 0600); err != nil {
53 t.Fatal(err)
54 }
55 for _, path := range []string{config.SessionStoreDir(), config.DesktopSessionStoreDir()} {
56 if _, err := os.Stat(path); !os.IsNotExist(err) {
57 t.Fatalf("fixture must not pre-create canonical history: %s: %v", path, err)
58 }
59 }
60 for _, phase := range []string{"first", "restart"} {
61 app := NewApp()
62 app.ctx = t.Context()
63 installNoopRuntimeEvents(app)
64 app.initializeDesktopSessionRoot()
65 closeApp := sync.OnceFunc(func() {
66 app.shutdown(context.Background())
67 app.stopHistoricalImports()
68 app.closeSessionServices()
69 if err := app.draftStore().Close(); err != nil {
70 t.Errorf("close fixture draft store: %v", err)
71 }
72 })
73 t.Cleanup(closeApp)
74 if app.NeedsOnboarding() {
75 t.Fatal("legacy fixture must restore the conversation instead of opening first-run provider settings")
76 }
77 app.startDesktopSessionMigration(t.Context())
78 if !app.waitForDesktopMigration(t.Context()) {
79 t.Fatal("startup discovery did not finish")
80 }
81 if phase == "first" {
82 file, _ := app.reconcileSavedTabs(t.Context(), loadTabsFile())
83 if len(file.Tabs) != 1 || file.Tabs[0].historicalSource == nil {
84 t.Fatalf("saved historical tab must be pending, not corrupt: %+v", file.Tabs)
85 }
86 tab := &WorkspaceTab{}
87 if !prepareRestoredTabIdentity(tab, file.Tabs[0]) || tab.Ctrl != nil || tab.StartupErr != "" || tab.SessionPath != legacyPath {
88 t.Fatalf("native identity was not admitted independently of discovery: %+v", tab)
89 }
90 state, err := app.workspaceRegistry().Load(t.Context())
91 if err != nil || len(state.SourceMappings) != 0 || len(state.PendingOperations) != 0 {
92 t.Fatalf("startup converted historical content: %+v, %v", state, err)
93 }
94 // A normal workspace metadata write upgrades the v1 registry without
95 // preparing or importing the legacy transcript.
96 if err := app.workspaceRegistry().SetWorkspaceVisible(t.Context(), "global", true); err != nil {
97 t.Fatal(err)
98 }
99 }
100 if phase == "restart" {
101 // Real startup also snapshots the current registry before recovery.
102 if err := app.backupDesktopUpgradeMetadata(t.Context()); err != nil {
103 t.Fatal(err)
104 }
105 if err := upgradefixture.Run("verify", home, reportPath, "restart"); err == nil {
106 t.Fatal("prepared verification passed before the legacy source was imported")
107 }
108 prepareRestoredUpgradeTab(t, app)
109 if err := upgradefixture.Run("verify", home, reportPath, "first"); err == nil || !strings.Contains(err.Error(), "startup imported or recreated the legacy session") {
110 t.Fatalf("no-import verification must refuse the prepared state: %v", err)
111 }
112 }
113 closeApp()
114 if err := upgradefixture.Run("verify", home, reportPath, phase); err != nil {
115 t.Fatalf("%s: %v", phase, err)
116 }
117 var evidence struct {
118 LegacyPath string `json:"legacyPath"`
119 History string `json:"history"`
120 }
121 body, err := os.ReadFile(filepath.Join(filepath.Dir(reportPath), "verification-"+phase+".json"))
122 if err != nil {
123 t.Fatal(err)
124 }
125 if err := json.Unmarshal(body, &evidence); err != nil {
126 t.Fatal(err)
127 }
128 if agent.CanonicalSessionPath(evidence.LegacyPath) != legacyPath {
129 t.Fatalf("%s changed the restored legacy path: %q", phase, evidence.LegacyPath)
130 }
131 if evidence.History == "" {
132 t.Fatalf("%s lost the saved history", phase)
133 }
134 }
135 }
136
137 // prepareRestoredUpgradeTab drives what the restored tab's import banner does:
138 // PrepareSession on the tab's historical source, then open the ready session.
139 func prepareRestoredUpgradeTab(t *testing.T, app *App) {
140 t.Helper()
141 app.tabsRestored = make(chan struct{})
142 app.restoreOrBuildTabs()
143 tab := waitForTabReady(t, app, "upgrade-tab")
144 source := app.metaForTab(tab.ID).HistoricalSource
145 if source == nil {
146 t.Fatal("restored legacy tab does not offer its historical source for preparation")
147 }
148 view, err := app.PrepareSession(SessionSelector{Source: source})
149 if err != nil {
150 t.Fatal(err)
151 }
152 deadline := time.Now().Add(20 * time.Second)
153 for view.Status != "ready" && view.Status != "failed" && view.Status != "blocked" && time.Now().Before(deadline) {
154 time.Sleep(20 * time.Millisecond)
155 if view, err = app.GetSessionPreparation(view.OperationID); err != nil {
156 t.Fatal(err)
157 }
158 }
159 if view.Status != "ready" || view.Target == nil {
160 t.Fatalf("legacy source was not prepared: %+v", view)
161 }
162 if _, err := app.OpenSession(*view.Target); err != nil {
163 t.Fatal(err)
164 }
165 }
166
166 lines GO