package moves import ( "context" "testing" "time" "github.com/carlosframework/platform/internal/release" ) const tLogPrefix = "moves/bab/app/a.go.example.com/mv_abcdefghijklmnopqrstuvwxyz/" func logEvent(identity, event string, at time.Time, actor string) LogEvent { return LogEvent{V: 1, Op: "mv_abcdefghijklmnopqrstuvwxyz", Host: "a.go.example.com", Event: event, Identity: identity, Actor: actor, At: at} } // Plan 6 Task 3 (preflight L10, L27): a log object's key is the transition's // identity, never a counter. A key is written create-only: the first writer // wins and creates it; a second writer of the same transition, or a replay, // meets the existing object, which is success with nothing created. func TestLogKeysAreTransitionIdentities(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) t0 := time.Date(2026, 10, 11, 9, 0, 0, 0, time.UTC) // Two writers of landing-probing (two boxes, or a crash and a replay), // with different bodies: one object, the first writer's. created, err := WriteLog(ctx, s, tLogPrefix, logEvent("landing-probing", "probing", t0, "box-uk")) if err != nil || !created { t.Fatalf("first writer: created %v, %v", created, err) } created, err = WriteLog(ctx, s, tLogPrefix, logEvent("landing-probing", "probing", t0.Add(time.Second), "box-ie")) if err != nil || created { t.Fatalf("second writer: created %v, %v; want no object and no error", created, err) } // Three probe attempts: three objects. for n, id := range []string{"probe_attempt-1", "probe_attempt-2", "probe_attempt-3"} { if created, err := WriteLog(ctx, s, tLogPrefix, logEvent(id, "probe_attempt", t0.Add(time.Duration(n+2)*time.Second), "box-uk")); err != nil || !created { t.Fatalf("%s: created %v, %v", id, created, err) } } // A replayed cutting-stopped writes nothing new. if created, err := WriteLog(ctx, s, tLogPrefix, logEvent("cutting-stopped", "stopped", t0.Add(-time.Minute), "box-ie")); err != nil || !created { t.Fatalf("cutting-stopped: %v %v", created, err) } if created, err := WriteLog(ctx, s, tLogPrefix, logEvent("cutting-stopped", "stopped", t0.Add(-time.Minute), "box-ie")); err != nil || created { t.Fatalf("replayed cutting-stopped: created %v, %v", created, err) } keys, err := s.List(ctx, tLogPrefix+"log/") if err != nil { t.Fatal(err) } if len(keys) != 5 { t.Fatalf("keys %v, want five objects", keys) } for _, id := range []string{"landing-probing", "probe_attempt-1", "probe_attempt-2", "probe_attempt-3", "cutting-stopped"} { if _, _, err := s.Get(ctx, LogKeyFor(tLogPrefix, id)); err != nil { t.Fatalf("%s: %v", id, err) } } // ReadLog orders by At, never by key. got, err := ReadLog(ctx, s, tLogPrefix) if err != nil { t.Fatal(err) } var order []string for _, ev := range got { order = append(order, ev.Identity) } want := "cutting-stopped landing-probing probe_attempt-1 probe_attempt-2 probe_attempt-3" if j := joinSpace(order); j != want { t.Fatalf("ReadLog order %q, want %q", j, want) } if got[1].Actor != "box-uk" { t.Fatalf("landing-probing is %+v, want the first writer's", got[1]) } // An identity is one key segment. for _, bad := range []string{"", "a/b", "..", "."} { if _, err := WriteLog(ctx, s, tLogPrefix, logEvent(bad, "x", t0, "box")); err == nil { t.Errorf("identity %q accepted", bad) } } } func joinSpace(xs []string) string { out := "" for i, x := range xs { if i > 0 { out += " " } out += x } return out }