carlos / platform Public

e2e: smoke assertions for the copy point, the clock and Serial

TestBridgeCopyFiresOncePerStagerRound stages a move and finds copy: firing
once per stage request on the stager, never on the target or a pass with
no request. TestBridgeBoxesReportThemselves now finds the snapshots stamped
at the wall clock before any Set and at the bridge clock after Advance, and
a bump made during a pass consumed by Serial's second pass. Both fail on
4456d778.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 267305d73e0ab7ca2f4979e435dcafa2598cfb62 parent faff9c7
1 file changed, +120 −16
  • internal/e2e/selfserve/bridge_test.go +120 −16
diff --git a/internal/e2e/selfserve/bridge_test.go b/internal/e2e/selfserve/bridge_test.go
index 30ac54e26..1ad42da60 100644
--- a/internal/e2e/selfserve/bridge_test.go
+++ b/internal/e2e/selfserve/bridge_test.go
@@ -9,12 +9,16 @@ package selfserve
import (
"context"
+ "encoding/json"
"path/filepath"
"slices"
"testing"
+ "time"
"github.com/carlosframework/platform/internal/console"
"github.com/carlosframework/platform/internal/host"
+ "github.com/carlosframework/platform/internal/moves"
+ "github.com/carlosframework/platform/internal/provision"
"github.com/carlosframework/platform/internal/release"
"github.com/carlosframework/platform/internal/status"
)
@@ -23,8 +27,10 @@ import (
// store a console uses, with that console beside it, one pass and one
// status tick on both boxes leave each box's status block naming its own
// box id, instance id and lane, and the source's naming the instance's host
-// facts; the serial is consumed; and the pass and status fault points fire
-// once per box.
+// facts, stamped at the wall clock; the pass and status fault points fire
+// once per box; a bump made during a pass is consumed by Serial's next
+// pass; and once the test advances the clock the status snapshot is
+// stamped at the bridge's time, not the wall's.
func TestBridgeBoxesReportThemselves(t *testing.T) {
ctx := context.Background()
store := release.NewDirStore(t.TempDir())
@@ -63,20 +69,11 @@ func TestBridgeBoxesReportThemselves(t *testing.T) {
if len(fired) != 4 {
t.Errorf("fault points fired: %v, want the pass and status points of both boxes", fired)
}
- if err := release.BumpSerial(ctx, store, e.Clock().Now()); err != nil {
- t.Fatal(err)
- }
- if err := e.Serial(ctx); err != nil {
- t.Fatal(err)
- }
-
- boxes, complete, err := status.LoadBoxesComplete(ctx, store)
- if err != nil || !complete {
- t.Fatalf("status boxes: complete %v, %v", complete, err)
- }
- byLabel := map[string]*status.BoxMetrics{}
- for _, b := range boxes {
- byLabel[b.Label] = b.Metrics
+ boxes, byLabel, generated := loadBoxes(t, store)
+ for label, at := range generated {
+ if d := time.Since(at); d < 0 || d > time.Second {
+ t.Errorf("%s's snapshot is stamped %v, %v from the wall clock before any Set", label, at, d)
+ }
}
for label, lane := range map[string]string{host.E2ESourceBox: "data-uk", host.E2ETargetBox: "data-ie"} {
h := e.Box(label)
@@ -104,4 +101,111 @@ func TestBridgeBoxesReportThemselves(t *testing.T) {
if e.Box(host.E2ESourceBox).DataBucket() != "data-ie" || e.Box(host.E2ETargetBox).DataBucket() != "data-uk" {
t.Errorf("data buckets %q and %q", e.Box(host.E2ESourceBox).DataBucket(), e.Box(host.E2ETargetBox).DataBucket())
}
+
+ // Serial: a bump made during the first pass is the next pass's to
+ // consume, and Serial runs it.
+ _, etag0, err := release.ReadSerial(ctx, store)
+ if err != nil {
+ t.Fatal(err)
+ }
+ passes, bumped := 0, false
+ e.Fault(host.FaultPass+host.E2ESourceBox, func() {
+ passes++
+ if !bumped {
+ bumped = true
+ if err := release.BumpSerial(ctx, store, time.Now()); err != nil {
+ t.Error(err)
+ }
+ }
+ })
+ if err := e.Serial(ctx); err != nil {
+ t.Fatal(err)
+ }
+ e.Fault(host.FaultPass+host.E2ESourceBox, nil)
+ if _, etag1, err := release.ReadSerial(ctx, store); err != nil || etag1 == etag0 || passes != 2 {
+ t.Errorf("Serial ran %d passes and the serial's etag is %q (was %q, %v); want the bump consumed by a second pass", passes, etag1, etag0, err)
+ }
+
+ // The clock: once the test advances it, the status snapshot is stamped
+ // at the bridge's time.
+ e.Clock().Advance(2 * time.Hour)
+ if err := e.Status(ctx); err != nil {
+ t.Fatal(err)
+ }
+ _, _, generated = loadBoxes(t, store)
+ for label, at := range generated {
+ if !at.Equal(e.Clock().Now()) {
+ t.Errorf("%s's snapshot is stamped %v after Advance, want the bridge clock's %v", label, at, e.Clock().Now())
+ }
+ }
+}
+
+// TestBridgeCopyFiresOncePerStagerRound: in the two-box world, a staged
+// move's stager runs one copy round per stage request, and copy: fires once
+// at each round's start, on the stager only, never on a pass with no new
+// request.
+func TestBridgeCopyFiresOncePerStagerRound(t *testing.T) {
+ ctx := context.Background()
+ store := release.NewDirStore(t.TempDir())
+ e := host.NewE2E(t, store)
+ copies := map[string]int{}
+ for _, label := range []string{host.E2ESourceBox, host.E2ETargetBox} {
+ p := host.FaultCopy + label
+ e.Fault(p, func() { copies[p]++ })
+ }
+ const id = "mv_stagedstagedstagedstagedab"
+ move := func(mut func(*provision.Record) error) {
+ t.Helper()
+ if _, err := provision.SetRecordMove(ctx, store, e.Account(), host.E2EApp, host.E2EHost, mut); err != nil {
+ t.Fatal(err)
+ }
+ }
+ src, dst := host.FaultCopy+host.E2ESourceBox, host.FaultCopy+host.E2ETargetBox
+ move(provision.OfflineStage(id, "", host.E2ETargetRegion, "op@example.com",
+ provision.StageTarget{Stager: host.E2ESourceBox, Bucket: e.Box(host.E2ETargetBox).DataBucket(), Region: "eu-west-2"}, e.Clock().Now(), ""))
+ for round := 1; round <= 3; round++ {
+ if round > 1 {
+ move(provision.OfflineRestage(id, e.Clock().Now().Add(time.Duration(round)*time.Second), provision.Receipt{}))
+ }
+ for range 2 { // the second pass has no new request: no round
+ if err := e.Pass(ctx); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if copies[src] != round || copies[dst] != 0 {
+ t.Fatalf("after stage request %d: copy fired %d times on the stager and %d on the target, want %d and 0", round, copies[src], copies[dst], round)
+ }
+ }
+ if !stagerReported(t, store, id) {
+ t.Fatal("the stager never reported a staged round")
+ }
+}
+
+// stagerReported is whether the stager's report for move id says staged.
+func stagerReported(t *testing.T, store release.Store, id string) bool {
+ t.Helper()
+ body, _, err := store.Get(context.Background(), moves.OfflineReportKey(host.E2EHost, id, moves.OfflineRoleStager, host.E2ESourceBox))
+ if err != nil {
+ t.Fatalf("the stager report: %v", err)
+ }
+ var r moves.OfflineReport
+ if err := json.Unmarshal(body, &r); err != nil {
+ t.Fatal(err)
+ }
+ return r.Phase == moves.OfflinePhaseStaged
+}
+
+// loadBoxes reads every box's status snapshot: the boxes, their metrics and
+// their generated-at stamps by label.
+func loadBoxes(t *testing.T, store release.Store) ([]status.Box, map[string]*status.BoxMetrics, map[string]time.Time) {
+ t.Helper()
+ boxes, complete, err := status.LoadBoxesComplete(context.Background(), store)
+ if err != nil || !complete {
+ t.Fatalf("status boxes: complete %v, %v", complete, err)
+ }
+ metrics, generated := map[string]*status.BoxMetrics{}, map[string]time.Time{}
+ for _, b := range boxes {
+ metrics[b.Label], generated[b.Label] = b.Metrics, b.GeneratedAt
+ }
+ return boxes, metrics, generated
}