返回 DeepSeek-Reasonix
historical_unmapped_conversion_test.go
根目录 / desktop / historical_unmapped_conversion_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 "reasonix/internal/config"
11 "reasonix/internal/provider"
12 "reasonix/internal/session"
13 )
14
15 // An old build may adopt only the JSONL receipt, leaving its converted store
16 // unregistered. A later import must not bypass the destination tombstone.
17 func TestUnmappedHistoricalConversionDoesNotRevivePurgedLegacyTarget(t *testing.T) {
18 for _, continued := range []bool{false, true} {
19 name := "same-content"
20 if continued {
21 name = "continued-conversion"
22 }
23 t.Run(name, func(t *testing.T) { checkUnmappedPurgedConversion(t, continued) })
24 }
25 t.Run("divergent-tool-history", func(t *testing.T) {
26 checkUnmappedPurgedConversion(t, true, func(old *session.Service, ref session.SessionRef) {
27 binding, err := old.Open(t.Context(), ref)
28 if err != nil {
29 t.Fatal(err)
30 }
31 messages := []provider.Message{
32 {Role: provider.RoleUser, Content: "independent branch", RawContent: "complete original user input"},
33 {Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{ID: "call", Name: "fixture_tool", Arguments: "{}"}}},
34 {Role: provider.RoleTool, ToolCallID: "call", Content: "complete tool result"},
35 }
36 payload, err := json.Marshal(map[string]any{"messages": messages})
37 if err != nil {
38 t.Fatal(err)
39 }
40 if _, err := binding.Runtime().Session().AppendBatch(t.Context(), "divergence", []session.Event{{Kind: "history/replace", Payload: payload}}); err != nil {
41 t.Fatal(err)
42 }
43 if err := binding.Release(t.Context()); err != nil {
44 t.Fatal(err)
45 }
46 if err := old.Close(t.Context(), ref); err != nil {
47 t.Fatal(err)
48 }
49 })
50 })
51 t.Run("omitted-conversion-head", func(t *testing.T) {
52 checkUnmappedPurgedConversion(t, false, func(old *session.Service, ref session.SessionRef) {
53 path := filepath.Join(config.SessionStoreDir(), ref.SessionID, "manifest.json")
54 body, err := os.ReadFile(path)
55 if err != nil {
56 t.Fatal(err)
57 }
58 var manifest session.Manifest
59 if err := json.Unmarshal(body, &manifest); err != nil {
60 t.Fatal(err)
61 }
62 manifest.Source.LegacyHeadID = ""
63 body, err = json.Marshal(manifest)
64 if err != nil {
65 t.Fatal(err)
66 }
67 if err := os.WriteFile(path, body, 0600); err != nil {
68 t.Fatal(err)
69 }
70 })
71 })
72 }
73
74 func checkUnmappedPurgedConversion(t *testing.T, continued bool, changes ...func(*session.Service, session.SessionRef)) {
75 t.Helper()
76 isolateDesktopUserDirs(t)
77 path, _, head := migrationSingleDAGFixture(t)
78 root := config.SessionStoreDir()
79 converted, err := session.MigrateLegacyHead(t.Context(), path, root, head)
80 if err != nil {
81 t.Fatal(err)
82 }
83 if continued || len(changes) != 0 {
84 old, err := session.NewService("fixture", session.NewFilesystemPersistence(root))
85 if err != nil {
86 t.Fatal(err)
87 }
88 ref := session.SessionRef{HostID: "fixture", SessionID: converted.TargetID}
89 if len(changes) != 0 {
90 changes[0](old, ref)
91 } else {
92 appendMigrationTestMessage(t, old, ref, "independent later work")
93 }
94 if err := old.Shutdown(t.Context()); err != nil {
95 t.Fatal(err)
96 }
97 }
98 app := newHistoricalLifecycleApp(t)
99 workspace, err := app.ensureDesktopWorkspace(t.Context(), "global", "")
100 if err != nil {
101 t.Fatal(err)
102 }
103 if err := app.migrateLegacySession(t.Context(), path, desktopMigrationSource{scope: "global", headID: head}, workspace); err != nil {
104 t.Fatal(err)
105 }
106 state, err := app.workspaceRegistry().Load(t.Context())
107 if err != nil {
108 t.Fatal(err)
109 }
110 mapping := state.SourceMappings[desktopSourceKey(path, head)]
111 if mapping.SessionID != converted.TargetID {
112 t.Fatalf("fixture must share the migrated identity: %s != %s", mapping.SessionID, converted.TargetID)
113 }
114 ref := session.SessionRef{HostID: localDesktopHostID, SessionID: mapping.SessionID}
115 if err := app.ArchiveCanonicalSession(ref); err != nil {
116 t.Fatal(err)
117 }
118 if err := app.PurgeCanonicalSession(ref); err != nil {
119 t.Fatal(err)
120 }
121 app.stopHistoricalImports()
122 app.closeSessionServices()
123 app = newHistoricalLifecycleApp(t)
124 before := migrationSourceSnapshot(t, canonicalMigrationSourceFiles(root, converted.TargetID))
125 key := historicalLifecycleID(t, app, converted.TargetID)
126 selector := SessionSelector{Source: &SessionSourceRef{
127 Path: filepath.Join(root, converted.TargetID), SourceKey: key,
128 }}
129 result, err := app.ArchiveSessionTarget(selector)
130 if err != nil || !result.Committed {
131 t.Fatalf("archive: %+v, %v", result, err)
132 }
133 state, loadErr := app.workspaceRegistry().Load(t.Context())
134 if loadErr != nil || state.SessionStates[ref.SessionID].Lifecycle != workspacestate.Deleted {
135 t.Fatalf("purged adoption changed: %v", loadErr)
136 }
137 canonicalMapping, exists, err := state.ResolveSource(key)
138 if err != nil || !exists {
139 t.Fatalf("missing canonical receipt: %v", err)
140 }
141 if continued {
142 if canonicalMapping.SessionID == ref.SessionID || state.SessionStates[canonicalMapping.SessionID].Lifecycle != workspacestate.Archived || result.Outcome != "archived_copy" {
143 t.Fatalf("later work was not independently archived: %+v, %+v", canonicalMapping, result)
144 }
145 digest, err := canonicalMigrationDigest(t.Context(), app.desktopSessionService("").Query(), session.SessionRef{HostID: localDesktopHostID, SessionID: canonicalMapping.SessionID})
146 old, openErr := session.NewService("source", session.NewFilesystemPersistence(root))
147 if openErr != nil {
148 t.Fatal(openErr)
149 }
150 expected, readErr := canonicalMigrationDigest(t.Context(), old.Query(), session.SessionRef{HostID: "source", SessionID: converted.TargetID})
151 _ = old.Shutdown(t.Context())
152 if err != nil || readErr != nil || digest != expected {
153 t.Fatalf("full history changed: %v, %v", err, readErr)
154 }
155 } else if canonicalMapping.SessionID != ref.SessionID || result.Outcome != "already_removed" {
156 t.Fatalf("identical retired copy resurrected: %+v, %+v", canonicalMapping, result)
157 }
158 beforeStates := len(state.SessionStates)
159 app.stopHistoricalImports()
160 app.closeSessionServices()
161 app = newHistoricalLifecycleApp(t)
162 repeated, err := app.ArchiveSessionTarget(selector)
163 if err != nil || !repeated.Committed || repeated.TargetKey != result.TargetKey {
164 t.Fatalf("restart retry: %+v, %v", repeated, err)
165 }
166 state, err = app.workspaceRegistry().Load(t.Context())
167 if err != nil || len(state.SessionStates) != beforeStates {
168 t.Fatalf("duplicate target after retry: %v", err)
169 }
170 if continued {
171 copyRef := session.SessionRef{HostID: localDesktopHostID, SessionID: canonicalMapping.SessionID}
172 if err := app.RestoreCanonicalSession(copyRef); err != nil {
173 t.Fatal(err)
174 }
175 if err := app.ArchiveCanonicalSession(copyRef); err != nil {
176 t.Fatal(err)
177 }
178 }
179 ledger, loadErr := readDesktopMigrationLedger()
180 if loadErr != nil {
181 t.Fatal(loadErr)
182 }
183 record := ledger.Records[desktopCanonicalMigrationKey(root, converted.TargetID)]
184 adopted := ledger.Records[desktopLegacyHeadKey(path, head)]
185 if record.ContentDigest == "" || (record.ContentDigest != adopted.ContentDigest) != continued {
186 t.Fatal("fixture did not preserve the distinction between an identical copy and later work")
187 }
188 t.Logf("conversion ledger after archive: status=%s target=%s attempts=%d", record.Status, record.TargetSessionID, record.Attempts)
189 assertMigrationSourceSnapshot(t, before)
190 }
191
191 lines GO