package buildstep import ( "context" "encoding/json" "errors" "fmt" "strings" "sync" "testing" "time" reg "git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry" "git.novox.be/novox/mesh-sdk/go/delivery" ) // CAT-NR1: a build's publish on a seat says asked, waiting on the seat (on every tick of NarrateEvery while the holder // has not answered), and stored or refused, each as the ask's phase written to its running step key. func TestABuildNarratesItsPublishOnASeat(t *testing.T) { digest := "sha256:" + strings.Repeat("c", 64) for name, c := range map[string]struct { answer error ticks int want []string }{ "stored": {want: []string{ "asked go-package-registry: publish of mod 1.2.3 at " + digest, "stored on go-package-registry: publish of mod 1.2.3 at " + digest + " (0 of its 1 parts sent, the rest held)"}}, "refused": {answer: errors.New("the holder is down"), want: []string{ "asked go-package-registry: publish of mod 1.2.3 at " + digest, "refused by go-package-registry: publish of mod 1.2.3 at " + digest + ": go-package-registry's publish of mod: the holder is down"}}, "waiting twice, then stored": {ticks: 2, want: []string{ "asked go-package-registry: publish of mod 1.2.3 at " + digest, "waiting on go-package-registry: publish of mod 1.2.3 at " + digest + ", 5m0s since asked", "waiting on go-package-registry: publish of mod 1.2.3 at " + digest + ", 10m0s since asked", "stored on go-package-registry: publish of mod 1.2.3 at " + digest + " (0 of its 1 parts sent, the rest held)"}}, } { t.Run(name, func(t *testing.T) { clock := time.Date(2026, 11, 4, 16, 0, 0, 0, time.UTC) var mu sync.Mutex oldNow, oldTicks := now, narrateTicks now = func() time.Time { mu.Lock(); defer mu.Unlock(); return clock } tick := make(chan time.Time) stopped := make(chan struct{}) narrateTicks = func(every time.Duration) (<-chan time.Time, func()) { if every != 5*time.Minute { t.Errorf("the waiting lines come every %s, want 5m", every) } return tick, func() { close(stopped) } } t.Cleanup(func() { now, narrateTicks = oldNow, oldTicks }) var said []string wrote := make(chan struct{}, 16) step := delivery.Step{ID: "a1", Tool: "build", Node: "ace", State: delivery.StepRunning, Started: clock} ph := &phases{key: "ace.a1", step: step, put: func(key string, value any) (uint64, error) { var s delivery.Step _ = json.Unmarshal(value.(json.RawMessage), &s) phasesMu.Lock() said = append(said, s.Phase) phasesMu.Unlock() wrote <- struct{}{} return 1, nil }} release := make(chan struct{}) call := func(key string, body any) (json.RawMessage, error) { <-release if c.answer != nil { return nil, c.answer } return json.Marshal(reg.PublishAnswer{Held: true, Transfer: "t"}) } ctx := context.WithValue(context.Background(), phaseKey{}, ph) done := make(chan error, 1) go func() { done <- PublishOne(ctx, call, GoSeat, "mod", "package", "1.2.3", digest, "t", []byte("x")) }() line := func() { select { case <-wrote: case <-time.After(5 * time.Second): t.Fatal("the publish said no line") } } line() // asked, at the clock's start for i := 0; i < c.ticks; i++ { mu.Lock() clock = clock.Add(5 * time.Minute) mu.Unlock() tick <- clock line() // its waiting line } close(release) err := <-done if (err != nil) != (c.answer != nil) { t.Fatalf("PublishOne = %v", err) } <-stopped phasesMu.Lock() defer phasesMu.Unlock() // Each line is the step's phase, written to its key, in order. if fmt.Sprint(said) != fmt.Sprint(c.want) { t.Errorf("phases =\n%s\nwant\n%s", strings.Join(said, "\n"), strings.Join(c.want, "\n")) } }) } } var phasesMu sync.Mutex