package host import ( "context" "crypto/ed25519" "crypto/sha256" "encoding/hex" "errors" "os" "path/filepath" "strings" "testing" "time" "github.com/carlosframework/platform/internal/release" ) type legFixture struct { t *testing.T store *release.DirStore priv ed25519.PrivateKey leg *systemLeg dd string opt string now time.Time } func newLegFixture(t *testing.T, channel, build string) *legFixture { t.Helper() pub, priv, err := ed25519.GenerateKey(nil) if err != nil { t.Fatalf("generate key: %v", err) } f := &legFixture{t: t, store: release.NewDirStore(t.TempDir()), priv: priv, dd: t.TempDir(), opt: t.TempDir(), now: time.Date(2026, 9, 6, 10, 0, 0, 0, time.UTC)} getenv := func(k string) string { if k == systemChannelEnv { return channel } return "" } f.leg = newSystemLeg(f.store, pub, getenv, f.dd, f.opt) f.leg.build = build f.leg.platform = "linux-arm64" f.leg.now = func() time.Time { return f.now } f.leg.jitter = func(d time.Duration) time.Duration { return d / 2 } // deterministic: half the spread return f } func (f *legFixture) promote(version string, body []byte, spread time.Duration) { f.t.Helper() sum := sha256.Sum256(body) rel := release.SystemRelease{Version: version, Artifacts: map[string]release.SystemArtifact{ "linux-arm64": {Key: release.SystemArtifactKey(version, "linux-arm64"), SHA256: hex.EncodeToString(sum[:])}}} if _, err := f.store.Put(context.Background(), rel.Artifacts["linux-arm64"].Key, body, ""); err != nil { f.t.Fatal(err) } if _, err := release.PromoteSystem(context.Background(), f.store, release.RootSigner(f.priv), release.SystemComponentRouter, f.leg.channel, rel, release.SystemPromoteOptions{Spread: spread}); err != nil { f.t.Fatal(err) } } // replaceArtifact swaps the object under the signed sha. DirStore.Put is // create-only with an empty if-match, so the overwrite carries the current // etag — the point is a store whose bytes no longer match the pointer. func (f *legFixture) replaceArtifact(version string, body []byte) { f.t.Helper() key := release.SystemArtifactKey(version, "linux-arm64") _, etag, err := f.store.Get(context.Background(), key) if err != nil { f.t.Fatal(err) } if _, err := f.store.Put(context.Background(), key, body, etag); err != nil { f.t.Fatal(err) } } func (f *legFixture) marker() (os.FileInfo, error) { return os.Stat(filepath.Join(f.dd, systemRequestMarker)) } func (f *legFixture) request() *systemRequest { var r systemRequest if err := readJSONFile(systemRequestPath(f.dd), &r); err != nil { return nil } return &r } func TestSystemLegDoesNothingWithoutAPointer(t *testing.T) { f := newLegFixture(t, "stable", "v1") f.leg.observe(context.Background()) f.leg.tick(context.Background(), f.now.Add(time.Hour)) if _, err := f.marker(); err == nil { t.Fatal("marker touched with no pointer") } } func TestSystemLegFetchesVerifiesAndRequestsAfterTheSpread(t *testing.T) { f := newLegFixture(t, "stable", "v1") body := []byte("ELF v2") f.promote("v2", body, 10*time.Minute) f.leg.observe(context.Background()) // One observe with a pointer present arms the pending update — this is // what fastConvergeLoop's observe-at-arming relies on. if f.leg.pending == nil || f.leg.pending.version != "v2" { t.Fatalf("observe armed pending = %+v, want v2", f.leg.pending) } if _, err := f.marker(); err == nil { t.Fatal("requested before the spread elapsed") } f.leg.tick(context.Background(), f.now.Add(4*time.Minute)) // jitter = 5m if _, err := f.marker(); err == nil { t.Fatal("requested before the deadline") } f.leg.tick(context.Background(), f.now.Add(5*time.Minute+time.Second)) if _, err := f.marker(); err != nil { t.Fatal("marker not touched after the deadline") } got, _ := os.ReadFile(systemCachePath(f.dd, "v2")) if string(got) != "ELF v2" { t.Fatalf("cache = %q", got) } r := f.request() if r == nil || r.Version != "v2" || r.NoOp || r.PromotedAt.IsZero() { t.Fatalf("request = %+v", r) } raw, _ := os.ReadFile(systemRequestPath(f.dd)) for _, never := range []string{"channel", "key", "sha", "path"} { if strings.Contains(string(raw), `"`+never+`"`) { t.Errorf("request carries %q — root must not take that from the sandbox", never) } } } func TestSystemLegRetouchesUntilRootAnswers(t *testing.T) { t.Parallel() f := newLegFixture(t, "stable", "v1") f.promote("v2", []byte("x"), 0) f.leg.observe(context.Background()) f.leg.tick(context.Background(), f.now) first, _ := f.marker() f.leg.tick(context.Background(), f.now.Add(30*time.Second)) same, _ := f.marker() if !same.ModTime().Equal(first.ModTime()) { t.Fatal("re-touched inside the minute throttle") } // The touch is a rewrite (PathChanged needs a change), so mtime moves. time.Sleep(20 * time.Millisecond) f.leg.tick(context.Background(), f.now.Add(61*time.Second)) later, _ := f.marker() if !later.ModTime().After(first.ModTime()) { t.Fatal("not re-touched after a minute with no result") } // Root answers: a terminal result for this tuple stops the re-touch and // the leg deletes its own cache. r := f.request() os.MkdirAll(systemRootDir(f.opt), 0o755) writeJSONDurable(filepath.Join(systemRootDir(f.opt), "result.json"), systemResult{Channel: "stable", Version: "v2", PromotedAt: r.PromotedAt, Outcome: systemOutcomeRolledBack, Reason: "gate", At: f.now}, 0o644) time.Sleep(20 * time.Millisecond) f.leg.tick(context.Background(), f.now.Add(3*time.Minute)) final, _ := f.marker() if !final.ModTime().Equal(later.ModTime()) { t.Fatal("re-touched after a terminal result") } if _, err := os.Stat(systemCachePath(f.dd, "v2")); err == nil { t.Fatal("cache kept after a terminal result") } // And a later observe of the SAME pointer does nothing: refuse to repeat a failure. f.leg.observe(context.Background()) f.leg.tick(context.Background(), f.now.Add(10*time.Minute)) if f.leg.pending != nil { t.Fatal("re-requested a pointer root already rolled back") } } func TestSystemLegNoOpRequestFetchesNothing(t *testing.T) { f := newLegFixture(t, "stable", "v2") f.promote("v2", []byte("same"), 0) f.leg.observe(context.Background()) f.leg.tick(context.Background(), f.now) r := f.request() if r == nil || !r.NoOp { t.Fatalf("request = %+v, want a no-op", r) } if _, err := os.Stat(systemCachePath(f.dd, "v2")); err == nil { t.Fatal("a no-op must not download the artifact") } // Once root has seen it (seen.json advanced to this PromotedAt), nothing more. os.MkdirAll(systemRootDir(f.opt), 0o755) writeJSONDurable(filepath.Join(systemRootDir(f.opt), "seen.json"), systemSeen{"stable": r.PromotedAt}, 0o644) f.leg.pending = nil os.Remove(filepath.Join(f.dd, systemRequestMarker)) f.leg.observe(context.Background()) f.leg.tick(context.Background(), f.now.Add(2*time.Minute)) if _, err := f.marker(); err == nil { t.Fatal("requested again after root recorded the watermark") } } func TestSystemLegIgnoresDevBuildEqualityAndOtherPlatforms(t *testing.T) { f := newLegFixture(t, "stable", "dev") f.promote("v2", []byte("x"), 0) f.leg.platform = "linux-amd64" // no artifact for me f.leg.observe(context.Background()) f.leg.tick(context.Background(), f.now) if _, err := f.marker(); err == nil { t.Fatal("requested with no artifact for this platform") } } func TestSystemLegRefusesAnArtifactWhoseShaDoesNotMatch(t *testing.T) { f := newLegFixture(t, "stable", "v1") f.promote("v2", []byte("good"), 0) // Swap the object under the signed sha. f.replaceArtifact("v2", []byte("evil")) f.leg.observe(context.Background()) f.leg.tick(context.Background(), f.now) if _, err := f.marker(); err == nil { t.Fatal("requested an artifact whose sha does not match") } if _, err := os.Stat(systemCachePath(f.dd, "v2")); err == nil { t.Fatal("mismatched artifact left in the cache") } } func TestSystemLegIgnoresAPointerOlderThanTheWatermark(t *testing.T) { f := newLegFixture(t, "stable", "v1") f.promote("v2", []byte("x"), 0) os.MkdirAll(systemRootDir(f.opt), 0o755) writeJSONDurable(filepath.Join(systemRootDir(f.opt), "seen.json"), systemSeen{"stable": time.Now().Add(time.Hour)}, 0o644) f.leg.observe(context.Background()) f.leg.tick(context.Background(), f.now) if _, err := f.marker(); err == nil { t.Fatal("requested a pointer root would refuse as a replay") } } // countFetches wraps the leg's fetch seam with a counter, leaving the real // download in place, and returns a pointer to the count. func (f *legFixture) countFetches() *int { n := 0 real := f.leg.fetchTo f.leg.fetchTo = func(ctx context.Context, key, dst, sha string) error { n++ return real(ctx, key, dst, sha) } return &n } func TestSystemLegBacksOffAGrowingWindowAfterAFailedFetch(t *testing.T) { f := newLegFixture(t, "stable", "v1") f.promote("v2", []byte("good"), 0) fetches := 0 f.leg.fetchTo = func(ctx context.Context, key, dst, sha string) error { fetches++ return errors.New("store unreachable") } ctx := context.Background() f.leg.observe(ctx) f.leg.tick(ctx, f.now) // first attempt: hold off 2s if fetches != 1 { t.Fatalf("first tick fetched %d times, want 1", fetches) } f.leg.tick(ctx, f.now.Add(time.Second)) if fetches != 1 { t.Fatal("re-downloaded inside the first backoff window") } f.leg.tick(ctx, f.now.Add(3*time.Second)) // second attempt: hold off 4s if fetches != 2 { t.Fatalf("after the 2s window fetched %d times, want 2", fetches) } f.leg.tick(ctx, f.now.Add(5*time.Second)) if fetches != 2 { t.Fatal("backoff did not grow: re-downloaded 2s after the second failure") } f.leg.tick(ctx, f.now.Add(8*time.Second)) // third attempt: hold off 8s if fetches != 3 { t.Fatalf("after the 4s window fetched %d times, want 3", fetches) } if f.leg.backoff != 8*time.Second { t.Fatalf("ladder = %s after three failures, want 8s", f.leg.backoff) } if _, err := f.marker(); err == nil { t.Fatal("asked root to apply an artifact that never downloaded") } } func TestSystemLegReusesAVerifiedCacheOnARetry(t *testing.T) { f := newLegFixture(t, "stable", "v1") f.promote("v2", []byte("ELF v2"), 0) fetches := f.countFetches() ctx := context.Background() f.leg.observe(ctx) f.leg.tick(ctx, f.now) if *fetches != 1 { t.Fatalf("first tick fetched %d times, want 1", *fetches) } // Root defers: it could not reach the store to verify the pointer. r := f.request() os.MkdirAll(systemRootDir(f.opt), 0o755) writeJSONDurable(filepath.Join(systemRootDir(f.opt), "result.json"), systemResult{Channel: "stable", Version: "v2", PromotedAt: r.PromotedAt, Outcome: systemOutcomeDeferred, Reason: "store", At: f.now.Add(500 * time.Millisecond)}, 0o644) f.leg.tick(ctx, f.now.Add(time.Second)) // sees the deferral, re-deadlines f.leg.tick(ctx, f.now.Add(4*time.Second)) // retries if *fetches != 1 { t.Fatalf("retry re-downloaded an artifact the cache already holds (%d fetches)", *fetches) } if got, _ := os.ReadFile(systemCachePath(f.dd, "v2")); string(got) != "ELF v2" { t.Fatalf("cache = %q after the retry", got) } } func TestSystemLegSaysNotForThisBoxOnce(t *testing.T) { f := newLegFixture(t, "stable", "v1") f.promote("v2", []byte("x"), 0) f.leg.platform = "linux-amd64" // no artifact for me lines := 0 f.leg.logf = func(format string, args ...any) { lines++ } for i := 0; i < 3; i++ { f.leg.observe(context.Background()) } if lines != 1 { t.Fatalf("three observes of the same pointer logged %d lines, want 1", lines) } } func TestSystemLegPrunesTheCacheOfSupersededVersions(t *testing.T) { f := newLegFixture(t, "stable", "v1") f.promote("v2", []byte("two"), 0) ctx := context.Background() f.leg.observe(ctx) f.leg.tick(ctx, f.now) if _, err := os.Stat(systemCachePath(f.dd, "v2")); err != nil { t.Fatalf("v2 not cached: %v", err) } // v3 lands before root ever answered for v2. f.promote("v3", []byte("three"), 0) f.leg.observe(ctx) if _, err := os.Stat(systemCachePath(f.dd, "v2")); err == nil { t.Fatal("v2's binary kept after v3 superseded it") } if f.request() == nil { t.Fatal("the prune took the request with it") } } func TestFastTickRunsTheSystemLeg(t *testing.T) { fx := newFastFixture(t) d := fx.deps(nil) observes := 0 d.systemLeg = &systemLeg{observeHook: func() { observes++ }} ctx := context.Background() fastTick(ctx, d, time.Now()) // boot seed if observes != 0 { t.Fatalf("boot seed observed %d times", observes) } release.BumpSerial(ctx, fx.store, time.Now()) fastTick(ctx, d, time.Now().Add(10*time.Second)) if observes != 1 { t.Fatalf("one serial move → %d observes", observes) } }