返回 DeepSeek-Reasonix
historical_version_test.go
根目录 / desktop / internal / workspacestate / historical_version_test.go
1 package workspacestate
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "os"
9 "path/filepath"
10 "reflect"
11 "testing"
12 )
13
14 func conflictingHistoricalVersionFixture(t *testing.T) (*Store, Operation) {
15 t.Helper()
16 store := NewStore(filepath.Join(t.TempDir(), "workspace-state-v1.json"))
17 if err := store.EnsureWorkspace(t.Context(), Workspace{ID: "global", Root: t.TempDir(), Visible: true}); err != nil {
18 t.Fatal(err)
19 }
20 if err := store.AttachSession(t.Context(), "", "global", "original", ""); err != nil {
21 t.Fatal(err)
22 }
23 if err := store.RecordSource(t.Context(), SourceMapping{SourceKey: "source", Path: "/old/source", Format: "canonical", Fingerprint: "old", SessionID: "original", WorkspaceID: "global"}, Presentation{}); err != nil {
24 t.Fatal(err)
25 }
26 op := Operation{ID: "import-source-new", Kind: "import", Lifecycle: Active, WorkspaceID: "global", SessionIDs: []string{"reserved"}}
27 if err := store.BeginOperation(t.Context(), op); err != nil {
28 t.Fatal(err)
29 }
30 mapping := &SourceMapping{SourceKey: "source", Path: "/old/source", Format: "canonical", Fingerprint: "new", SessionID: "reserved", WorkspaceID: "global", extra: map[string]json.RawMessage{"futureMapping": json.RawMessage(`{"keep":true}`)}}
31 if err := store.PrepareOperationContent(t.Context(), op.ID, op.SessionIDs, mapping, &Presentation{Title: "Branch", extra: map[string]json.RawMessage{"futureTitle": json.RawMessage(`42`)}}); err != nil {
32 t.Fatal(err)
33 }
34 if err := store.mutate(t.Context(), func(s *State) error {
35 v := s.PendingOperations[op.ID]
36 v.extra = map[string]json.RawMessage{"futureOperation": json.RawMessage(`"keep"`)}
37 s.PendingOperations[op.ID] = v
38 return nil
39 }); err != nil {
40 t.Fatal(err)
41 }
42 state, err := store.Load(t.Context())
43 if err != nil {
44 t.Fatal(err)
45 }
46 return store, state.PendingOperations[op.ID]
47 }
48
49 func TestCommitHistoricalVersionPreservesOriginalAndBacksUp(t *testing.T) {
50 store, op := conflictingHistoricalVersionFixture(t)
51 before, _ := os.ReadFile(store.Path())
52 state, _ := store.Load(t.Context())
53 original := state.SourceMappings["source"]
54 if err := store.CommitHistoricalVersion(t.Context(), op, "source:review:new"); err != nil {
55 t.Fatal(err)
56 }
57 reopened, err := NewStore(store.Path()).Load(t.Context())
58 if err != nil {
59 t.Fatal(err)
60 }
61 if !reflect.DeepEqual(original, reopened.SourceMappings["source"]) || reopened.PendingOperations[op.ID].Phase != "committed" || reopened.SourceMappings["source:review:new"].SessionID != "reserved" {
62 t.Fatal("version commit changed original adoption or lost the branch")
63 }
64 backups, _ := filepath.Glob(filepath.Join(filepath.Dir(store.Path()), "historical-version-backups", "*.json"))
65 if len(backups) != 1 {
66 t.Fatalf("backups = %v", backups)
67 }
68 saved, _ := os.ReadFile(backups[0])
69 if !bytes.Equal(saved, before) {
70 t.Fatal("backup changed original bytes")
71 }
72 after, _ := os.ReadFile(store.Path())
73 for _, key := range []string{"futureMapping", "futureOperation", "futureTitle"} {
74 if !bytes.Contains(after, []byte(key)) {
75 t.Fatalf("lost unknown metadata %q", key)
76 }
77 }
78 // Ordinary commit replay remains idempotent for older readers/writers.
79 if err := NewStore(store.Path()).CommitOperation(t.Context(), op.ID); err != nil {
80 t.Fatal(err)
81 }
82 again, _ := os.ReadFile(store.Path())
83 if !bytes.Equal(after, again) {
84 t.Fatal("committed replay rewrote state")
85 }
86 }
87
88 func TestCommitHistoricalVersionRejectsChangedOrAmbiguousState(t *testing.T) {
89 for _, scenario := range []string{"operation_changed", "original_archived", "target_deleted", "target_attached", "target_reserved", "version_taken", "competing_operation", "backup_blocked", "cancelled"} {
90 t.Run(scenario, func(t *testing.T) {
91 store, observed := conflictingHistoricalVersionFixture(t)
92 if err := store.mutate(t.Context(), func(s *State) error {
93 switch scenario {
94 case "operation_changed":
95 op := s.PendingOperations[observed.ID]
96 op.ExpectedGeneration++
97 s.PendingOperations[op.ID] = op
98 case "original_archived":
99 setLifecycle(s, "original", Archived)
100 case "target_deleted":
101 setLifecycle(s, "reserved", Deleted)
102 case "target_attached":
103 w := s.Workspaces["global"]
104 w.SessionIDs = append(w.SessionIDs, "reserved")
105 s.Workspaces[w.ID] = w
106 case "target_reserved":
107 s.PendingCreates["reserved"] = PendingCreate{OperationID: "other", SessionID: "reserved", WorkspaceID: "global"}
108 case "version_taken":
109 m := *observed.Mapping
110 m.SourceKey, m.SessionID = "source:review:new", "original"
111 s.SourceMappings[m.SourceKey] = m
112 case "competing_operation":
113 op := observed
114 op.ID = "another-operation"
115 s.PendingOperations[op.ID] = op
116 }
117 return nil
118 }); err != nil {
119 t.Fatal(err)
120 }
121 if scenario == "backup_blocked" {
122 if err := os.WriteFile(filepath.Join(filepath.Dir(store.Path()), "historical-version-backups"), []byte("block directory"), 0600); err != nil {
123 t.Fatal(err)
124 }
125 }
126 ctx, cancel := context.WithCancel(t.Context())
127 defer cancel()
128 if scenario == "cancelled" {
129 cancel()
130 }
131 before, _ := os.ReadFile(store.Path())
132 err := store.CommitHistoricalVersion(ctx, observed, "source:review:new")
133 if err == nil || (scenario != "backup_blocked" && scenario != "cancelled" && !errors.Is(err, ErrMutationConflict)) {
134 t.Fatalf("unexpected result: %v", err)
135 }
136 after, _ := os.ReadFile(store.Path())
137 if !bytes.Equal(before, after) {
138 t.Fatal("rejected repair changed registry")
139 }
140 })
141 }
142 }
143
143 lines GO