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 "crypto/ed25519"
6 "crypto/sha256"
7 "encoding/hex"
8 "errors"
9 "os"
10 "path/filepath"
11 "strings"
12 "testing"
13 "time"
14
15 "github.com/carlosframework/platform/internal/release"
16)
17
18type legFixture struct {
19 t *testing.T
20 store *release.DirStore
21 priv ed25519.PrivateKey
22 leg *systemLeg
23 dd string
24 opt string
25 now time.Time
26}
27
28func newLegFixture(t *testing.T, channel, build string) *legFixture {
29 t.Helper()
30 pub, priv, err := ed25519.GenerateKey(nil)
31 if err != nil {
32 t.Fatalf("generate key: %v", err)
33 }
34 f := &legFixture{t: t, store: release.NewDirStore(t.TempDir()), priv: priv, dd: t.TempDir(), opt: t.TempDir(),
35 now: time.Date(2026, 9, 6, 10, 0, 0, 0, time.UTC)}
36 getenv := func(k string) string {
37 if k == systemChannelEnv {
38 return channel
39 }
40 return ""
41 }
42 f.leg = newSystemLeg(f.store, pub, getenv, f.dd, f.opt)
43 f.leg.build = build
44 f.leg.platform = "linux-arm64"
45 f.leg.now = func() time.Time { return f.now }
46 f.leg.jitter = func(d time.Duration) time.Duration { return d / 2 } // deterministic: half the spread
47 return f
48}
49
50func (f *legFixture) promote(version string, body []byte, spread time.Duration) {
51 f.t.Helper()
52 sum := sha256.Sum256(body)
53 rel := release.SystemRelease{Version: version, Artifacts: map[string]release.SystemArtifact{
54 "linux-arm64": {Key: release.SystemArtifactKey(version, "linux-arm64"), SHA256: hex.EncodeToString(sum[:])}}}
55 if _, err := f.store.Put(context.Background(), rel.Artifacts["linux-arm64"].Key, body, ""); err != nil {
56 f.t.Fatal(err)
57 }
58 if _, err := release.PromoteSystem(context.Background(), f.store, release.RootSigner(f.priv), release.SystemComponentRouter, f.leg.channel, rel,
59 release.SystemPromoteOptions{Spread: spread}); err != nil {
60 f.t.Fatal(err)
61 }
62}
63
64// replaceArtifact swaps the object under the signed sha. DirStore.Put is
65// create-only with an empty if-match, so the overwrite carries the current
66// etag — the point is a store whose bytes no longer match the pointer.
67func (f *legFixture) replaceArtifact(version string, body []byte) {
68 f.t.Helper()
69 key := release.SystemArtifactKey(version, "linux-arm64")
70 _, etag, err := f.store.Get(context.Background(), key)
71 if err != nil {
72 f.t.Fatal(err)
73 }
74 if _, err := f.store.Put(context.Background(), key, body, etag); err != nil {
75 f.t.Fatal(err)
76 }
77}
78
79func (f *legFixture) marker() (os.FileInfo, error) {
80 return os.Stat(filepath.Join(f.dd, systemRequestMarker))
81}
82
83func (f *legFixture) request() *systemRequest {
84 var r systemRequest
85 if err := readJSONFile(systemRequestPath(f.dd), &r); err != nil {
86 return nil
87 }
88 return &r
89}
90
91func TestSystemLegDoesNothingWithoutAPointer(t *testing.T) {
92 f := newLegFixture(t, "stable", "v1")
93 f.leg.observe(context.Background())
94 f.leg.tick(context.Background(), f.now.Add(time.Hour))
95 if _, err := f.marker(); err == nil {
96 t.Fatal("marker touched with no pointer")
97 }
98}
99
100func TestSystemLegFetchesVerifiesAndRequestsAfterTheSpread(t *testing.T) {
101 f := newLegFixture(t, "stable", "v1")
102 body := []byte("ELF v2")
103 f.promote("v2", body, 10*time.Minute)
104 f.leg.observe(context.Background())
105 // One observe with a pointer present arms the pending update — this is
106 // what fastConvergeLoop's observe-at-arming relies on.
107 if f.leg.pending == nil || f.leg.pending.version != "v2" {
108 t.Fatalf("observe armed pending = %+v, want v2", f.leg.pending)
109 }
110 if _, err := f.marker(); err == nil {
111 t.Fatal("requested before the spread elapsed")
112 }
113 f.leg.tick(context.Background(), f.now.Add(4*time.Minute)) // jitter = 5m
114 if _, err := f.marker(); err == nil {
115 t.Fatal("requested before the deadline")
116 }
117 f.leg.tick(context.Background(), f.now.Add(5*time.Minute+time.Second))
118 if _, err := f.marker(); err != nil {
119 t.Fatal("marker not touched after the deadline")
120 }
121 got, _ := os.ReadFile(systemCachePath(f.dd, "v2"))
122 if string(got) != "ELF v2" {
123 t.Fatalf("cache = %q", got)
124 }
125 r := f.request()
126 if r == nil || r.Version != "v2" || r.NoOp || r.PromotedAt.IsZero() {
127 t.Fatalf("request = %+v", r)
128 }
129 raw, _ := os.ReadFile(systemRequestPath(f.dd))
130 for _, never := range []string{"channel", "key", "sha", "path"} {
131 if strings.Contains(string(raw), `"`+never+`"`) {
132 t.Errorf("request carries %q — root must not take that from the sandbox", never)
133 }
134 }
135}
136
137func TestSystemLegRetouchesUntilRootAnswers(t *testing.T) {
138 t.Parallel()
139 f := newLegFixture(t, "stable", "v1")
140 f.promote("v2", []byte("x"), 0)
141 f.leg.observe(context.Background())
142 f.leg.tick(context.Background(), f.now)
143 first, _ := f.marker()
144 f.leg.tick(context.Background(), f.now.Add(30*time.Second))
145 same, _ := f.marker()
146 if !same.ModTime().Equal(first.ModTime()) {
147 t.Fatal("re-touched inside the minute throttle")
148 }
149 // The touch is a rewrite (PathChanged needs a change), so mtime moves.
150 time.Sleep(20 * time.Millisecond)
151 f.leg.tick(context.Background(), f.now.Add(61*time.Second))
152 later, _ := f.marker()
153 if !later.ModTime().After(first.ModTime()) {
154 t.Fatal("not re-touched after a minute with no result")
155 }
156 // Root answers: a terminal result for this tuple stops the re-touch and
157 // the leg deletes its own cache.
158 r := f.request()
159 os.MkdirAll(systemRootDir(f.opt), 0o755)
160 writeJSONDurable(filepath.Join(systemRootDir(f.opt), "result.json"),
161 systemResult{Channel: "stable", Version: "v2", PromotedAt: r.PromotedAt, Outcome: systemOutcomeRolledBack, Reason: "gate", At: f.now}, 0o644)
162 time.Sleep(20 * time.Millisecond)
163 f.leg.tick(context.Background(), f.now.Add(3*time.Minute))
164 final, _ := f.marker()
165 if !final.ModTime().Equal(later.ModTime()) {
166 t.Fatal("re-touched after a terminal result")
167 }
168 if _, err := os.Stat(systemCachePath(f.dd, "v2")); err == nil {
169 t.Fatal("cache kept after a terminal result")
170 }
171 // And a later observe of the SAME pointer does nothing: refuse to repeat a failure.
172 f.leg.observe(context.Background())
173 f.leg.tick(context.Background(), f.now.Add(10*time.Minute))
174 if f.leg.pending != nil {
175 t.Fatal("re-requested a pointer root already rolled back")
176 }
177}
178
179func TestSystemLegNoOpRequestFetchesNothing(t *testing.T) {
180 f := newLegFixture(t, "stable", "v2")
181 f.promote("v2", []byte("same"), 0)
182 f.leg.observe(context.Background())
183 f.leg.tick(context.Background(), f.now)
184 r := f.request()
185 if r == nil || !r.NoOp {
186 t.Fatalf("request = %+v, want a no-op", r)
187 }
188 if _, err := os.Stat(systemCachePath(f.dd, "v2")); err == nil {
189 t.Fatal("a no-op must not download the artifact")
190 }
191 // Once root has seen it (seen.json advanced to this PromotedAt), nothing more.
192 os.MkdirAll(systemRootDir(f.opt), 0o755)
193 writeJSONDurable(filepath.Join(systemRootDir(f.opt), "seen.json"), systemSeen{"stable": r.PromotedAt}, 0o644)
194 f.leg.pending = nil
195 os.Remove(filepath.Join(f.dd, systemRequestMarker))
196 f.leg.observe(context.Background())
197 f.leg.tick(context.Background(), f.now.Add(2*time.Minute))
198 if _, err := f.marker(); err == nil {
199 t.Fatal("requested again after root recorded the watermark")
200 }
201}
202
203func TestSystemLegIgnoresDevBuildEqualityAndOtherPlatforms(t *testing.T) {
204 f := newLegFixture(t, "stable", "dev")
205 f.promote("v2", []byte("x"), 0)
206 f.leg.platform = "linux-amd64" // no artifact for me
207 f.leg.observe(context.Background())
208 f.leg.tick(context.Background(), f.now)
209 if _, err := f.marker(); err == nil {
210 t.Fatal("requested with no artifact for this platform")
211 }
212}
213
214func TestSystemLegRefusesAnArtifactWhoseShaDoesNotMatch(t *testing.T) {
215 f := newLegFixture(t, "stable", "v1")
216 f.promote("v2", []byte("good"), 0)
217 // Swap the object under the signed sha.
218 f.replaceArtifact("v2", []byte("evil"))
219 f.leg.observe(context.Background())
220 f.leg.tick(context.Background(), f.now)
221 if _, err := f.marker(); err == nil {
222 t.Fatal("requested an artifact whose sha does not match")
223 }
224 if _, err := os.Stat(systemCachePath(f.dd, "v2")); err == nil {
225 t.Fatal("mismatched artifact left in the cache")
226 }
227}
228
229func TestSystemLegIgnoresAPointerOlderThanTheWatermark(t *testing.T) {
230 f := newLegFixture(t, "stable", "v1")
231 f.promote("v2", []byte("x"), 0)
232 os.MkdirAll(systemRootDir(f.opt), 0o755)
233 writeJSONDurable(filepath.Join(systemRootDir(f.opt), "seen.json"), systemSeen{"stable": time.Now().Add(time.Hour)}, 0o644)
234 f.leg.observe(context.Background())
235 f.leg.tick(context.Background(), f.now)
236 if _, err := f.marker(); err == nil {
237 t.Fatal("requested a pointer root would refuse as a replay")
238 }
239}
240
241// countFetches wraps the leg's fetch seam with a counter, leaving the real
242// download in place, and returns a pointer to the count.
243func (f *legFixture) countFetches() *int {
244 n := 0
245 real := f.leg.fetchTo
246 f.leg.fetchTo = func(ctx context.Context, key, dst, sha string) error {
247 n++
248 return real(ctx, key, dst, sha)
249 }
250 return &n
251}
252
253func TestSystemLegBacksOffAGrowingWindowAfterAFailedFetch(t *testing.T) {
254 f := newLegFixture(t, "stable", "v1")
255 f.promote("v2", []byte("good"), 0)
256 fetches := 0
257 f.leg.fetchTo = func(ctx context.Context, key, dst, sha string) error {
258 fetches++
259 return errors.New("store unreachable")
260 }
261 ctx := context.Background()
262 f.leg.observe(ctx)
263 f.leg.tick(ctx, f.now) // first attempt: hold off 2s
264 if fetches != 1 {
265 t.Fatalf("first tick fetched %d times, want 1", fetches)
266 }
267 f.leg.tick(ctx, f.now.Add(time.Second))
268 if fetches != 1 {
269 t.Fatal("re-downloaded inside the first backoff window")
270 }
271 f.leg.tick(ctx, f.now.Add(3*time.Second)) // second attempt: hold off 4s
272 if fetches != 2 {
273 t.Fatalf("after the 2s window fetched %d times, want 2", fetches)
274 }
275 f.leg.tick(ctx, f.now.Add(5*time.Second))
276 if fetches != 2 {
277 t.Fatal("backoff did not grow: re-downloaded 2s after the second failure")
278 }
279 f.leg.tick(ctx, f.now.Add(8*time.Second)) // third attempt: hold off 8s
280 if fetches != 3 {
281 t.Fatalf("after the 4s window fetched %d times, want 3", fetches)
282 }
283 if f.leg.backoff != 8*time.Second {
284 t.Fatalf("ladder = %s after three failures, want 8s", f.leg.backoff)
285 }
286 if _, err := f.marker(); err == nil {
287 t.Fatal("asked root to apply an artifact that never downloaded")
288 }
289}
290
291func TestSystemLegReusesAVerifiedCacheOnARetry(t *testing.T) {
292 f := newLegFixture(t, "stable", "v1")
293 f.promote("v2", []byte("ELF v2"), 0)
294 fetches := f.countFetches()
295 ctx := context.Background()
296 f.leg.observe(ctx)
297 f.leg.tick(ctx, f.now)
298 if *fetches != 1 {
299 t.Fatalf("first tick fetched %d times, want 1", *fetches)
300 }
301 // Root defers: it could not reach the store to verify the pointer.
302 r := f.request()
303 os.MkdirAll(systemRootDir(f.opt), 0o755)
304 writeJSONDurable(filepath.Join(systemRootDir(f.opt), "result.json"),
305 systemResult{Channel: "stable", Version: "v2", PromotedAt: r.PromotedAt, Outcome: systemOutcomeDeferred,
306 Reason: "store", At: f.now.Add(500 * time.Millisecond)}, 0o644)
307 f.leg.tick(ctx, f.now.Add(time.Second)) // sees the deferral, re-deadlines
308 f.leg.tick(ctx, f.now.Add(4*time.Second)) // retries
309 if *fetches != 1 {
310 t.Fatalf("retry re-downloaded an artifact the cache already holds (%d fetches)", *fetches)
311 }
312 if got, _ := os.ReadFile(systemCachePath(f.dd, "v2")); string(got) != "ELF v2" {
313 t.Fatalf("cache = %q after the retry", got)
314 }
315}
316
317func TestSystemLegSaysNotForThisBoxOnce(t *testing.T) {
318 f := newLegFixture(t, "stable", "v1")
319 f.promote("v2", []byte("x"), 0)
320 f.leg.platform = "linux-amd64" // no artifact for me
321 lines := 0
322 f.leg.logf = func(format string, args ...any) { lines++ }
323 for i := 0; i < 3; i++ {
324 f.leg.observe(context.Background())
325 }
326 if lines != 1 {
327 t.Fatalf("three observes of the same pointer logged %d lines, want 1", lines)
328 }
329}
330
331func TestSystemLegPrunesTheCacheOfSupersededVersions(t *testing.T) {
332 f := newLegFixture(t, "stable", "v1")
333 f.promote("v2", []byte("two"), 0)
334 ctx := context.Background()
335 f.leg.observe(ctx)
336 f.leg.tick(ctx, f.now)
337 if _, err := os.Stat(systemCachePath(f.dd, "v2")); err != nil {
338 t.Fatalf("v2 not cached: %v", err)
339 }
340 // v3 lands before root ever answered for v2.
341 f.promote("v3", []byte("three"), 0)
342 f.leg.observe(ctx)
343 if _, err := os.Stat(systemCachePath(f.dd, "v2")); err == nil {
344 t.Fatal("v2's binary kept after v3 superseded it")
345 }
346 if f.request() == nil {
347 t.Fatal("the prune took the request with it")
348 }
349}
350
351func TestFastTickRunsTheSystemLeg(t *testing.T) {
352 fx := newFastFixture(t)
353 d := fx.deps(nil)
354 observes := 0
355 d.systemLeg = &systemLeg{observeHook: func() { observes++ }}
356 ctx := context.Background()
357 fastTick(ctx, d, time.Now()) // boot seed
358 if observes != 0 {
359 t.Fatalf("boot seed observed %d times", observes)
360 }
361 release.BumpSerial(ctx, fx.store, time.Now())
362 fastTick(ctx, d, time.Now().Add(10*time.Second))
363 if observes != 1 {
364 t.Fatalf("one serial move → %d observes", observes)
365 }
366}
367