返回 DeepSeek-Reasonix
store_test.go
根目录 / desktop / internal / legacycleanup / store_test.go
1 package legacycleanup
2
3 import (
4 "context"
5 "errors"
6 "os"
7 "path/filepath"
8 "testing"
9
10 "reasonix/internal/filelock"
11 )
12
13 func TestStoreFreezesInitialCandidateSetAndRoundTripsUpdates(t *testing.T) {
14 path := filepath.Join(t.TempDir(), "cleanup.json")
15 store := New(path)
16 initial := State{BatchID: "batch", Items: map[string]Candidate{
17 "session:a": {ID: "session:a", Kind: "session", SessionID: "a", OperationID: "cleanup-a", Phase: "registered"},
18 }}
19 state, created, err := store.Initialize(context.Background(), initial)
20 if err != nil || !created || len(state.Items) != 1 {
21 t.Fatalf("Initialize = (%+v, %v, %v)", state, created, err)
22 }
23 second := State{BatchID: "other", Items: map[string]Candidate{
24 "session:b": {ID: "session:b", Kind: "session", SessionID: "b", OperationID: "cleanup-b", Phase: "registered"},
25 }}
26 state, created, err = store.Initialize(context.Background(), second)
27 if err != nil || created || state.BatchID != "batch" || state.Items["session:b"].ID != "" {
28 t.Fatalf("second Initialize changed frozen set: (%+v, %v, %v)", state, created, err)
29 }
30 state, err = store.Update(context.Background(), func(next *State) error {
31 item := next.Items["session:a"]
32 item.Phase, item.Classification = "archived", "empty"
33 next.Items[item.ID] = item
34 return nil
35 })
36 if err != nil || state.Items["session:a"].Phase != "archived" {
37 t.Fatalf("Update = (%+v, %v)", state, err)
38 }
39 }
40
41 func TestStoreRejectsFutureAndCorruptStateWithoutReplacingIt(t *testing.T) {
42 for _, fixture := range []string{`{"version":2,"batchId":"future"}`, `{not-json`} {
43 path := filepath.Join(t.TempDir(), "cleanup.json")
44 if err := os.WriteFile(path, []byte(fixture), 0o600); err != nil {
45 t.Fatal(err)
46 }
47 store := New(path)
48 _, _, err := store.Initialize(context.Background(), State{BatchID: "new", Items: map[string]Candidate{}})
49 if !errors.Is(err, ErrUnsupportedVersion) && !errors.Is(err, ErrCorruptState) {
50 t.Fatalf("Initialize error = %v", err)
51 }
52 body, readErr := os.ReadFile(path)
53 if readErr != nil || string(body) != fixture {
54 t.Fatalf("unknown state was modified: %q, %v", body, readErr)
55 }
56 }
57 }
58
59 func TestStoreLoadFromFreshParentRemainsUninitialized(t *testing.T) {
60 path := filepath.Join(t.TempDir(), "missing", "desktop", "cleanup.json")
61 store := New(path)
62 if _, err := store.Load(context.Background()); !errors.Is(err, ErrNotInitialized) {
63 t.Fatalf("Load error = %v, want ErrNotInitialized", err)
64 }
65 if _, err := os.Stat(path); !os.IsNotExist(err) {
66 t.Fatalf("fresh read created state file: %v", err)
67 }
68 }
69
70 func TestStoreWorkerLockIsExclusiveAcrossInstances(t *testing.T) {
71 path := filepath.Join(t.TempDir(), "cleanup.json")
72 first := New(path)
73 if _, _, err := first.Initialize(context.Background(), State{BatchID: "batch", Items: map[string]Candidate{}}); err != nil {
74 t.Fatal(err)
75 }
76 release, err := first.TryAcquireWorker()
77 if err != nil {
78 t.Fatal(err)
79 }
80 defer release()
81 if _, err := New(path).TryAcquireWorker(); !errors.Is(err, filelock.ErrHeld) {
82 t.Fatalf("second worker lock error = %v, want ErrHeld", err)
83 }
84 }
85
86 func TestStoreTransitionSerializesOtherInstances(t *testing.T) {
87 path := filepath.Join(t.TempDir(), "cleanup.json")
88 first, second := New(path), New(path)
89 if _, _, err := first.Initialize(t.Context(), State{BatchID: "batch", Items: map[string]Candidate{
90 "topic:a": {ID: "topic:a", Kind: "topic", TopicID: "a", OperationID: "cleanup-a", Phase: "registered"},
91 }}); err != nil {
92 t.Fatal(err)
93 }
94 effectStarted := make(chan struct{})
95 releaseEffect := make(chan struct{})
96 transitionDone := make(chan error, 1)
97 go func() {
98 _, err := first.Transition(context.Background(), func(state *State) error {
99 item := state.Items["topic:a"]
100 item.Phase = "archive_pending"
101 state.Items[item.ID] = item
102 return nil
103 }, func() error {
104 close(effectStarted)
105 <-releaseEffect
106 return nil
107 }, func(state *State, _ error) error {
108 item := state.Items["topic:a"]
109 item.Phase = "archived"
110 state.Items[item.ID] = item
111 return nil
112 })
113 transitionDone <- err
114 }()
115 <-effectStarted
116 if release, err := filelock.TryAcquire(path + ".lock"); !errors.Is(err, filelock.ErrHeld) {
117 if release != nil {
118 release()
119 }
120 t.Fatalf("transition did not hold cross-process state lock: %v", err)
121 }
122 close(releaseEffect)
123 if err := <-transitionDone; err != nil {
124 t.Fatal(err)
125 }
126 if _, err := second.Update(t.Context(), func(state *State) error {
127 item := state.Items["topic:a"]
128 item.Restored = true
129 state.Items[item.ID] = item
130 return nil
131 }); err != nil {
132 t.Fatal(err)
133 }
134 state, err := first.Load(t.Context())
135 if err != nil {
136 t.Fatal(err)
137 }
138 if state.Items["topic:a"].Phase != "archived" || !state.Items["topic:a"].Restored {
139 t.Fatalf("serialized state = %+v", state.Items["topic:a"])
140 }
141 }
142
142 lines GO