返回 DeepSeek-Reasonix
registration_journal_test.go
根目录 / internal / plugin / registration_journal_test.go
1 package plugin
2
3 import (
4 "context"
5 "errors"
6 "testing"
7 )
8
9 func TestRegistrationScopeRollbackIsInstanceScoped(t *testing.T) {
10 host := NewHost()
11 // Pre-existing sibling client accepted under scope A.
12 siblingA := &Client{name: "sibling"}
13 scopeA := host.BeginRegistrationScope()
14 ctxA := ContextWithRegistrationScope(context.Background(), scopeA)
15 if err := host.ReplaceServerBackend(ctxA, "sibling", siblingA, 1); err != nil {
16 t.Fatal(err)
17 }
18 refsA := scopeA.Snapshot()
19 if len(refsA) != 1 || refsA[0].Name != "sibling" || refsA[0].ID == 0 {
20 t.Fatalf("scope A = %+v", refsA)
21 }
22 idA := refsA[0].ID
23
24 // Stale build journals a replacement for the same name plus a new server.
25 staleSibling := &Client{name: "sibling"}
26 staleNew := &Client{name: "stale-new"}
27 scopeStale := host.BeginRegistrationScope()
28 ctxStale := ContextWithRegistrationScope(context.Background(), scopeStale)
29 if err := host.ReplaceServerBackend(ctxStale, "sibling", staleSibling, 2); err != nil {
30 t.Fatal(err)
31 }
32 if err := host.ReplaceServerBackend(ctxStale, "stale-new", staleNew, 2); err != nil {
33 t.Fatal(err)
34 }
35 refsStale := scopeStale.Snapshot()
36 if len(refsStale) != 2 {
37 t.Fatalf("stale scope len = %d, want 2: %+v", len(refsStale), refsStale)
38 }
39 var staleSiblingID uint64
40 for _, ref := range refsStale {
41 if ref.Name == "sibling" {
42 staleSiblingID = ref.ID
43 }
44 }
45 if staleSiblingID == 0 || staleSiblingID == idA {
46 t.Fatalf("stale sibling id = %d, original = %d", staleSiblingID, idA)
47 }
48
49 // Sibling/new-generation resource created AFTER the stale scope must survive
50 // rollback (name-diff would delete it incorrectly). No scope token.
51 siblingNewGen := &Client{name: "sibling-new-gen"}
52 if err := host.ReplaceServerBackend(context.Background(), "sibling-new-gen", siblingNewGen, 3); err != nil {
53 t.Fatal(err)
54 }
55 // Newer generation also replaces the shared name after the stale scope ends.
56 newerSibling := &Client{name: "sibling"}
57 if err := host.ReplaceServerBackend(context.Background(), "sibling", newerSibling, 4); err != nil {
58 t.Fatal(err)
59 }
60 newerID := newerSibling.instanceID
61 if newerID == 0 || newerID == staleSiblingID {
62 t.Fatalf("newer sibling id = %d, stale = %d", newerID, staleSiblingID)
63 }
64
65 scopeStale.AbortAndRollback()
66
67 names := map[string]bool{}
68 for _, name := range host.ServerNames() {
69 names[name] = true
70 }
71 if names["stale-new"] {
72 t.Fatal("stale-new survived instance-scoped rollback")
73 }
74 if !names["sibling-new-gen"] {
75 t.Fatal("sibling-new-gen was incorrectly removed by stale scope rollback")
76 }
77 if !names["sibling"] {
78 t.Fatal("newer-generation sibling instance was incorrectly removed")
79 }
80 if host.lookupClient("sibling") == nil || host.lookupClient("sibling").instanceID != newerID {
81 t.Fatalf("sibling live instance = %+v, want id %d", host.lookupClient("sibling"), newerID)
82 }
83 if host.RemoveIfInstance("sibling", idA) {
84 t.Fatal("RemoveIfInstance removed a non-matching/missing instance")
85 }
86 if host.RemoveIfInstance("sibling", staleSiblingID) {
87 t.Fatal("RemoveIfInstance removed the newer instance via stale id")
88 }
89 }
90
91 // TestRegistrationScopeIgnoresUnrelatedHostWrites is the deterministic
92 // interleaving that Host-global regJournalActive fails: while a stale build
93 // holds an open scope, a sibling hot-add without the scope token must not be
94 // attributed to the stale build or deleted by its rollback.
95 func TestRegistrationScopeIgnoresUnrelatedHostWrites(t *testing.T) {
96 host := NewHost()
97 scopeStale := host.BeginRegistrationScope()
98 ctxStale := ContextWithRegistrationScope(context.Background(), scopeStale)
99
100 // Sibling hot-add during the open scope WITHOUT the token.
101 sibling := &Client{name: "sibling-hot-add"}
102 if err := host.ReplaceServerBackend(context.Background(), "sibling-hot-add", sibling, 1); err != nil {
103 t.Fatal(err)
104 }
105 // Stale build also registers its own server under the scope.
106 staleOnly := &Client{name: "stale-only"}
107 if err := host.ReplaceServerBackend(ctxStale, "stale-only", staleOnly, 1); err != nil {
108 t.Fatal(err)
109 }
110
111 refs := scopeStale.Snapshot()
112 for _, ref := range refs {
113 if ref.Name == "sibling-hot-add" {
114 t.Fatalf("journal captured unrelated Host write: %+v", refs)
115 }
116 }
117 if len(refs) != 1 || refs[0].Name != "stale-only" {
118 t.Fatalf("stale scope refs = %+v, want only stale-only", refs)
119 }
120
121 scopeStale.AbortAndRollback()
122
123 names := map[string]bool{}
124 for _, name := range host.ServerNames() {
125 names[name] = true
126 }
127 if names["stale-only"] {
128 t.Fatal("stale-only survived AbortAndRollback")
129 }
130 if !names["sibling-hot-add"] {
131 t.Fatal("sibling-hot-add was deleted by stale scope rollback")
132 }
133 }
134
135 func TestRegistrationScopeRejectsLateRegistrationAfterAbort(t *testing.T) {
136 host := NewHost()
137 scope := host.BeginRegistrationScope()
138 ctx := ContextWithRegistrationScope(context.Background(), scope)
139 scope.AbortAndRollback()
140
141 late := &Client{name: "late"}
142 err := host.ReplaceServerBackend(ctx, "late", late, 1)
143 if !errors.Is(err, ErrRegistrationScopeAborted) {
144 t.Fatalf("late registration err = %v, want ErrRegistrationScopeAborted", err)
145 }
146 for _, name := range host.ServerNames() {
147 if name == "late" {
148 t.Fatal("aborted scope accepted a late client")
149 }
150 }
151 }
152
153 func TestRegistrationScopeCommitPreservesClientReusedByNewerBuild(t *testing.T) {
154 host := NewHost()
155 older := host.BeginRegistrationScope()
156 olderCtx := ContextWithRegistrationScope(context.Background(), older)
157 client := &Client{name: "shared", toolCatalog: toolCatalogSnapshot{listed: true}}
158 if err := host.ReplaceServerBackend(olderCtx, "shared", client, 1); err != nil {
159 t.Fatal(err)
160 }
161
162 newer := host.BeginRegistrationScope()
163 newerCtx := ContextWithRegistrationScope(context.Background(), newer)
164 if _, err := host.ToolsFor(newerCtx, "shared"); err != nil {
165 t.Fatalf("newer build failed to reuse shared client: %v", err)
166 }
167 if !newer.Commit() {
168 t.Fatal("newer build could not commit its shared-client claim")
169 }
170
171 older.AbortAndRollback()
172 if got := host.lookupClient("shared"); got != client {
173 t.Fatalf("older rollback removed client committed by newer build: got=%p want=%p", got, client)
174 }
175 }
176
177 func TestRegistrationScopeActiveClaimDefersCreatorRollback(t *testing.T) {
178 host := NewHost()
179 creator := host.BeginRegistrationScope()
180 creatorCtx := ContextWithRegistrationScope(context.Background(), creator)
181 client := &Client{name: "shared", toolCatalog: toolCatalogSnapshot{listed: true}}
182 if err := host.ReplaceServerBackend(creatorCtx, "shared", client, 1); err != nil {
183 t.Fatal(err)
184 }
185
186 consumer := host.BeginRegistrationScope()
187 consumerCtx := ContextWithRegistrationScope(context.Background(), consumer)
188 if _, err := host.ToolsFor(consumerCtx, "shared"); err != nil {
189 t.Fatal(err)
190 }
191 creator.AbortAndRollback()
192 if !host.HasClient("shared") {
193 t.Fatal("creator rollback removed a client claimed by an active consumer")
194 }
195
196 consumer.AbortAndRollback()
197 if host.HasClient("shared") {
198 t.Fatal("client survived after every uncommitted scope aborted")
199 }
200 if got := host.lookupClient("shared"); got != nil {
201 t.Fatalf("proxy still exposed rolled-back client: %+v", got)
202 }
203 }
204
205 func TestCommittedScopeAcceptsLateLazyRegistration(t *testing.T) {
206 host := NewHost()
207 scope := host.BeginRegistrationScope()
208 ctx := ContextWithRegistrationScope(context.Background(), scope)
209 if !scope.Commit() {
210 t.Fatal("commit failed")
211 }
212
213 late := &Client{name: "late"}
214 if err := host.ReplaceServerBackend(ctx, "late", late, 1); err != nil {
215 t.Fatalf("published scope rejected a late lazy registration: %v", err)
216 }
217 scope.AbortAndRollback()
218 if got := host.lookupClient("late"); got != late {
219 t.Fatal("committed scope rollback removed its late lazy registration")
220 }
221 }
222
223 func TestRegistrationScopesRemainIndependentWhileBothActive(t *testing.T) {
224 host := NewHost()
225 first := host.BeginRegistrationScope()
226 second := host.BeginRegistrationScope()
227 firstCtx := ContextWithRegistrationScope(context.Background(), first)
228 secondCtx := ContextWithRegistrationScope(context.Background(), second)
229 if err := host.ReplaceServerBackend(firstCtx, "a", &Client{name: "a"}, 1); err != nil {
230 t.Fatal(err)
231 }
232 if err := host.ReplaceServerBackend(secondCtx, "b", &Client{name: "b"}, 1); err != nil {
233 t.Fatal(err)
234 }
235 if len(first.Snapshot()) != 1 || len(second.Snapshot()) != 1 {
236 t.Fatalf("active scope claims leaked: first=%+v second=%+v", first.Snapshot(), second.Snapshot())
237 }
238 }
239
239 lines GO