carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "os"
8 "path/filepath"
9 "strings"
10 "sync/atomic"
11 "testing"
12 "time"
13
14 "github.com/carlosframework/platform/internal/registry"
15 "github.com/carlosframework/platform/internal/release"
16)
17
18func noRefusal(string) (*release.Refusal, error) { return nil, nil }
19
20func TestTheRelayAsksAgainUntilTheHeadIsInstalled(t *testing.T) {
21 rows := map[string]*registry.Route{"a": {Host: "a", Version: "v1"}, "b": {Host: "b", Version: "v1"}}
22 touches := 0
23 r := &stageRelay{pending: map[string]pendingHead{},
24 get: func(h string) (*registry.Route, error) { return rows[h], nil },
25 refused: noRefusal,
26 touch: func() { touches++ }}
27 if r.tick() || touches != 0 {
28 t.Fatal("touched with nothing outstanding")
29 }
30 r.note("a", "v2")
31 r.note("b", "v2")
32 if !r.tick() || touches != 1 {
33 t.Fatal("one touch per tick while heads are outstanding")
34 }
35 rows["a"].Version = "v2"
36 delete(rows, "b")
37 if r.tick() || touches != 1 {
38 t.Fatal("kept asking after root installed a's head and b's route went")
39 }
40 r.note("a", "v3")
41 r.get = func(string) (*registry.Route, error) { return nil, errors.New("database is locked") }
42 if !r.tick() {
43 t.Fatal("an unreadable row is not proof the head is installed")
44 }
45}
46
47// Root's refusal record is an outcome too: once root has refused a
48// (host, version), asking again only reruns the refusal.
49func TestTheRelayStopsAtRootsRefusal(t *testing.T) {
50 rows := map[string]*registry.Route{"a": {Host: "a", Version: "v1"}, "b": {Host: "b", Version: "v1"}}
51 refusals := map[string]*release.Refusal{"a": {Host: "a", Version: "v2", Kind: release.RefusalWrongProgram}}
52 touches := 0
53 r := &stageRelay{pending: map[string]pendingHead{},
54 get: func(h string) (*registry.Route, error) { return rows[h], nil },
55 refused: func(h string) (*release.Refusal, error) { return refusals[h], nil },
56 touch: func() { touches++ }}
57 r.note("a", "v2")
58 if r.tick() || touches != 0 {
59 t.Fatal("touched for a head root has refused")
60 }
61 r.note("b", "v3")
62 refusals["b"] = &release.Refusal{Host: "b", Version: "v2"} // an older refusal names another version
63 if !r.tick() || touches != 1 {
64 t.Fatal("a refusal of another version is no outcome for this head")
65 }
66 r.refused = func(string) (*release.Refusal, error) { return nil, errors.New("permission denied") }
67 if !r.tick() {
68 t.Fatal("an unreadable refusal record is not an outcome")
69 }
70}
71
72// A newer head replaces the older one for the same host, so the relay asks
73// for the newest only and stops when that one is installed.
74func TestTheRelayFollowsTheNewestHead(t *testing.T) {
75 rows := map[string]*registry.Route{"a": {Host: "a", Version: "v1"}}
76 touches := 0
77 r := &stageRelay{pending: map[string]pendingHead{},
78 get: func(h string) (*registry.Route, error) { return rows[h], nil },
79 refused: noRefusal,
80 touch: func() { touches++ }}
81 r.note("a", "v2")
82 r.note("a", "v3")
83 if !r.tick() || touches != 1 {
84 t.Fatal("the newest head is outstanding")
85 }
86 rows["a"].Version = "v3"
87 if r.tick() || touches != 1 {
88 t.Fatal("kept asking after the newest head was installed")
89 }
90}
91
92// A head the channel has moved past is dropped. Root installed v3 straight
93// from the channel, so v2 will never match the row, and no blocked pass
94// notes it again; asking for it every 15 s would start a root pass each time
95// until the route went.
96func TestTheRelayDropsAHeadTheHostMovedPast(t *testing.T) {
97 rows := map[string]*registry.Route{"a": {Host: "a", Version: "v1"}}
98 touches := 0
99 r := &stageRelay{pending: map[string]pendingHead{},
100 get: func(h string) (*registry.Route, error) { return rows[h], nil },
101 refused: noRefusal,
102 touch: func() { touches++ }}
103 r.note("a", "v2")
104 if !r.tick() || touches != 1 {
105 t.Fatal("v2 is outstanding while the row is still at v1")
106 }
107 rows["a"].Version = "v3"
108 if r.tick() || touches != 1 {
109 t.Fatal("kept touching for v2 after root installed v3")
110 }
111 if len(r.pending) != 0 {
112 t.Fatalf("the overtaken head stayed pending: %v", r.pending)
113 }
114}
115
116// The baseline is the Version at note time. Noting the same head again
117// refreshes it when it changed, so a head noted after root moved the host is
118// judged against the new Version and stays pending.
119func TestTheRelayRenoteRefreshesTheBaseline(t *testing.T) {
120 rows := map[string]*registry.Route{"a": {Host: "a", Version: "v1"}}
121 touches := 0
122 r := &stageRelay{pending: map[string]pendingHead{},
123 get: func(h string) (*registry.Route, error) { return rows[h], nil },
124 refused: noRefusal,
125 touch: func() { touches++ }}
126 r.note("a", "v4")
127 rows["a"].Version = "v3" // root installed something else; a blocked pass notes v4 again
128 r.note("a", "v4")
129 if !r.tick() || touches != 1 {
130 t.Fatal("a re-noted head must be judged against the Version at the re-note")
131 }
132 r.get = func(string) (*registry.Route, error) { return nil, errors.New("database is locked") }
133 r.note("a", "v4") // an unreadable row must not clobber the baseline
134 r.get = func(h string) (*registry.Route, error) { return rows[h], nil }
135 if !r.tick() {
136 t.Fatal("the baseline was lost to an unreadable row")
137 }
138}
139
140// A row unreadable at note time gets its baseline from the first readable
141// tick, and the head stays pending until then.
142func TestTheRelayBaselinesAtTheFirstReadableTick(t *testing.T) {
143 rows := map[string]*registry.Route{"a": {Host: "a", Version: "v1"}}
144 readable := false
145 touches := 0
146 r := &stageRelay{pending: map[string]pendingHead{},
147 get: func(h string) (*registry.Route, error) {
148 if !readable {
149 return nil, errors.New("database is locked")
150 }
151 return rows[h], nil
152 },
153 refused: noRefusal,
154 touch: func() { touches++ }}
155 r.note("a", "v2")
156 if !r.tick() {
157 t.Fatal("an unreadable row is not an outcome")
158 }
159 readable = true
160 if !r.tick() {
161 t.Fatal("v2 is still outstanding at the first readable tick")
162 }
163 rows["a"].Version = "v3"
164 if r.tick() {
165 t.Fatal("kept asking after the host moved off its baseline")
166 }
167}
168
169// The loop runs on its own goroutine, touches on its own interval and stops
170// when the agent's context is cancelled.
171func TestTheRelayLoopTicksAndStopsWithTheContext(t *testing.T) {
172 t.Parallel()
173 var touches atomic.Int32
174 r := &stageRelay{pending: map[string]pendingHead{},
175 get: func(h string) (*registry.Route, error) { return &registry.Route{Host: h, Version: "v1"}, nil },
176 refused: noRefusal,
177 touch: func() { touches.Add(1) }}
178 r.note("a", "v2")
179 ctx, cancel := context.WithCancel(context.Background())
180 done := make(chan struct{})
181 go func() { r.run(ctx, 5*time.Millisecond); close(done) }()
182 deadline := time.Now().Add(5 * time.Second)
183 for touches.Load() < 2 && time.Now().Before(deadline) {
184 time.Sleep(time.Millisecond)
185 }
186 if touches.Load() < 2 {
187 t.Fatal("the loop did not retransmit")
188 }
189 cancel()
190 select {
191 case <-done:
192 case <-time.After(5 * time.Second):
193 t.Fatal("the loop outlived its context")
194 }
195 n := touches.Load()
196 time.Sleep(30 * time.Millisecond)
197 if touches.Load() != n {
198 t.Fatal("touched after the context was cancelled")
199 }
200}
201
202// stageRequester feeds the armed relay; mintRequester shares its marker and
203// throttle but a proof-mint request is not a stage-blocked head, so it must
204// not. With no relay armed (the flag off) stageRequester is as before.
205func TestOnlyStageRequestsFeedTheRelay(t *testing.T) {
206 dd := t.TempDir()
207 stageRequester(dd)("a", "v2", nil) // unarmed: no panic, marker as before
208 if _, err := os.Stat(filepath.Join(dd, stageRequestMarker)); err != nil {
209 t.Fatalf("marker not written with no relay armed: %v", err)
210 }
211 r := &stageRelay{pending: map[string]pendingHead{}, get: func(string) (*registry.Route, error) { return nil, nil }, refused: noRefusal, touch: func() {}}
212 armedStageRelay.Store(r)
213 t.Cleanup(func() { armedStageRelay.Store(nil) })
214 mintRequester(dd)("m", "v9", nil)
215 if len(r.pending) != 0 {
216 t.Fatalf("a mint request reached the relay: %v", r.pending)
217 }
218 stageRequester(dd)("a", "v2", nil)
219 if r.pending["a"].ver != "v2" {
220 t.Fatalf("a stage-blocked head did not reach the relay: %v", r.pending)
221 }
222}
223
224// armStageRelay reads root's real records: the registry row and the refusal
225// file in the data dir.
226func TestArmStageRelayReadsRootsRecords(t *testing.T) {
227 reg, dd := testRegistry(t), t.TempDir()
228 if err := reg.Upsert(registry.Route{Host: "a.example.com", Version: "v1"}); err != nil {
229 t.Fatalf("seed a route: %v", err)
230 }
231 r := newStageRelay(reg, dd)
232 r.note("a.example.com", "v2")
233 if !r.tick() {
234 t.Fatal("an uninstalled, unrefused head must be outstanding")
235 }
236 if _, err := os.Stat(filepath.Join(dd, stageRequestMarker)); err != nil {
237 t.Fatalf("tick did not touch the marker: %v", err)
238 }
239 if err := release.WriteRefusal(dd, release.Refusal{Schema: 1, Host: "a.example.com", Version: "v2", Kind: release.RefusalNotProven}); err != nil {
240 t.Fatal(err)
241 }
242 if r.tick() {
243 t.Fatal("kept asking after root wrote a refusal for the head")
244 }
245}
246
247// The moves worker's requester (movespass.go) asks root for a pass like
248// stageRequester but never feeds the relay: its version is a move's held
249// version, and nothing would ever tell the relay it was answered.
250func TestTheMovesRequesterDoesNotFeedTheRelay(t *testing.T) {
251 dd := t.TempDir()
252 r := &stageRelay{pending: map[string]pendingHead{}, get: func(string) (*registry.Route, error) { return nil, nil }, refused: noRefusal, touch: func() {}}
253 armedStageRelay.Store(r)
254 t.Cleanup(func() { armedStageRelay.Store(nil) })
255 stageMarkerRequester(dd)("a", "v2", nil)
256 if _, err := os.Stat(filepath.Join(dd, stageRequestMarker)); err != nil {
257 t.Fatalf("the moves requester did not touch the marker: %v", err)
258 }
259 if len(r.pending) != 0 {
260 t.Fatalf("a moves-path request entered the relay: %v", r.pending)
261 }
262}
263
264// The flag-off guarantee lives on armedStageRelay staying nil: runAgent arms
265// the relay through armRolloutAndRelay, and with CARLOS_SWAP unset it must
266// leave nothing armed.
267func TestTheFlagOffLeavesTheStageRelayUnarmed(t *testing.T) {
268 armedStageRelay.Store(nil)
269 svc, reg, dd := rolloutFixture(t, false)
270 if m := armRolloutAndRelay(context.Background(), noEnv, svc, reg, dd, nil, unusedStore); m != nil {
271 t.Fatal("CARLOS_SWAP unset armed the scheduler")
272 }
273 if armedStageRelay.Load() != nil {
274 t.Fatal("CARLOS_SWAP unset armed the stage relay")
275 }
276}
277
278// Astra R1 T15: root can fail to answer for good, and the relay must not
279// touch the marker for as long as the head stays on the channel. After
280// relayGiveUp with neither an install nor a refusal it drops the head with a
281// log line and stops touching; the next blocked pass notes it again, which
282// asks for another relayGiveUp.
283func TestTheRelayGivesUpOnAHeadRootNeverAnswersAndNotesItAgain(t *testing.T) {
284 rows := map[string]*registry.Route{"a": {Host: "a", Version: "v1"}}
285 now := time.Date(2026, 10, 12, 9, 0, 0, 0, time.UTC)
286 touches := 0
287 var lines []string
288 r := &stageRelay{pending: map[string]pendingHead{},
289 get: func(h string) (*registry.Route, error) { return rows[h], nil },
290 refused: noRefusal,
291 touch: func() { touches++ },
292 now: func() time.Time { return now },
293 logf: func(f string, a ...any) { lines = append(lines, fmt.Sprintf(f, a...)) }}
294 r.note("a", "v2")
295 now = now.Add(relayGiveUp - time.Second)
296 if !r.tick() || touches != 1 {
297 t.Fatalf("just under the give-up: outstanding or touches wrong (touches %d)", touches)
298 }
299 if len(lines) != 0 {
300 t.Fatalf("logged before the give-up: %q", lines)
301 }
302 now = now.Add(time.Second)
303 if r.tick() || touches != 1 {
304 t.Fatalf("at the give-up the relay still asked (touches %d)", touches)
305 }
306 if len(lines) != 1 || !strings.Contains(lines[0], "v2") || !strings.Contains(lines[0], "a") {
307 t.Fatalf("give-up log = %q, want one line naming the head and the host", lines)
308 }
309 if len(r.pending) != 0 {
310 t.Fatalf("the dropped head is still pending: %v", r.pending)
311 }
312 if r.tick() || touches != 1 || len(lines) != 1 {
313 t.Fatalf("a tick after the drop touched or logged again (touches %d, lines %q)", touches, lines)
314 }
315 r.note("a", "v2") // the next blocked pass
316 if !r.tick() || touches != 2 {
317 t.Fatalf("the head noted again was not asked for (touches %d)", touches)
318 }
319 now = now.Add(relayGiveUp - time.Second)
320 if !r.tick() {
321 t.Fatal("the fresh hour ended early: a re-noted head must count from the new note")
322 }
323}
324
325// The hour counts from the first note of a head, not from each note: a
326// blocked pass every few seconds re-notes the same head, and that must not
327// keep it alive for ever. A newer head is a different request and starts its
328// own hour.
329func TestTheRelayCountsTheHourFromTheFirstNoteOfAHead(t *testing.T) {
330 now := time.Date(2026, 10, 12, 9, 0, 0, 0, time.UTC)
331 touches := 0
332 r := &stageRelay{pending: map[string]pendingHead{},
333 get: func(h string) (*registry.Route, error) { return &registry.Route{Host: h, Version: "v1"}, nil },
334 refused: noRefusal,
335 touch: func() { touches++ },
336 now: func() time.Time { return now },
337 logf: func(string, ...any) {}}
338 r.note("a", "v2")
339 for range 4 {
340 now = now.Add(15 * time.Minute)
341 r.note("a", "v2")
342 }
343 if r.tick() {
344 t.Fatal("re-noting the same head kept it past the hour")
345 }
346 r.note("a", "v3")
347 now = now.Add(relayGiveUp - time.Minute)
348 if !r.tick() {
349 t.Fatal("a newer head did not get its own hour")
350 }
351}
352
353// Root's outcome still wins over the clock: a head installed or refused
354// inside the hour is dropped quietly, with no give-up line.
355func TestTheRelayLogsNoGiveUpForAnAnsweredHead(t *testing.T) {
356 rows := map[string]*registry.Route{"a": {Host: "a", Version: "v1"}}
357 now := time.Date(2026, 10, 12, 9, 0, 0, 0, time.UTC)
358 var lines []string
359 r := &stageRelay{pending: map[string]pendingHead{},
360 get: func(h string) (*registry.Route, error) { return rows[h], nil },
361 refused: noRefusal,
362 touch: func() {},
363 now: func() time.Time { return now },
364 logf: func(f string, a ...any) { lines = append(lines, fmt.Sprintf(f, a...)) }}
365 r.note("a", "v2")
366 now = now.Add(time.Minute)
367 rows["a"].Version = "v2"
368 if r.tick() || len(lines) != 0 {
369 t.Fatalf("an installed head: outstanding or logged %q", lines)
370 }
371}
372