package main import ( "context" "errors" "fmt" "strings" "testing" "time" "github.com/novox/mesh-controller/internal/link" ) // A wait on a clock of its own: each pause moves it on, and nothing waits in real time. func aFakeWait(bound time.Duration) (waitOut, *[]time.Duration) { clock := time.Date(2026, 10, 11, 11, 52, 2, 0, time.UTC) var paused []time.Duration return waitOut{bound: bound, first: time.Second, most: 10 * time.Second, now: func() time.Time { return clock }, sleep: func(_ context.Context, d time.Duration) error { paused = append(paused, d) clock = clock.Add(d) return nil }}, &paused } // notServed is what the bus says while node-tools re-serves, in the words seen on 2026-10-11. func notServed() error { return fmt.Errorf("cannot ask gitea.gitea_api: %w", link.ErrNothingServes) } // A registration's forge question that lands while the tool runner re-serves is asked again, and the forge's // answer, seconds later, is the registration's (novox/hq issue 486). func TestTheForgeNotServedForAMomentIsWaitedOut(t *testing.T) { w, paused := aFakeWait(60 * time.Second) asked := 0 facts, err := forgeWaitedOut(t.Context(), w, "novox/mesh-catalog", func(context.Context) (forgeFacts, error) { asked++ if asked <= 2 { return forgeFacts{}, notServed() } return forgeFacts{ID: 42, Guarded: true}, nil }) if err != nil { t.Fatalf("a forge not served for two asks failed the registration: %v", err) } if facts.ID != 42 || !facts.Guarded || asked != 3 { t.Fatalf("the forge's answer was %+v after %d asks, want its answer on the third", facts, asked) } if want := []time.Duration{time.Second, 2 * time.Second}; fmt.Sprint(*paused) != fmt.Sprint(want) { t.Fatalf("paused %v, want %v", *paused, want) } } // A tool that did not answer within its wait has held the receive loop that long already: not asked again. func TestTheForgeNotAnsweringInTimeIsNotAskedAgain(t *testing.T) { w, paused := aFakeWait(60 * time.Second) asked := 0 _, err := forgeWaitedOut(t.Context(), w, "novox/x", func(context.Context) (forgeFacts, error) { asked++ return forgeFacts{}, fmt.Errorf("gitea did not answer: %w", link.ErrNoAnswer) }) if !errors.Is(err, link.ErrNoAnswer) || asked != 1 || len(*paused) != 0 { t.Fatalf("a forge that did not answer in time was asked %d times, paused %v: %v", asked, *paused, err) } } // An ask that hangs past the bound is cut off at the bound, not at its own wait, and the failure still says it // waited: the receive loop is held no longer than the bound (novox/hq issue 486). func TestAnAskHangingPastTheBoundIsCutOffAtIt(t *testing.T) { w := waitOut{bound: 200 * time.Millisecond, first: 10 * time.Millisecond, most: 50 * time.Millisecond, now: time.Now, sleep: sleepOrDone} asked := 0 began := time.Now() _, err := forgeWaitedOut(t.Context(), w, "novox/mesh-catalog", func(ctx context.Context) (forgeFacts, error) { asked++ if asked == 1 { return forgeFacts{}, notServed() } select { // link.Ask's own wait is 30 s; the bound must end it first case <-ctx.Done(): return forgeFacts{}, fmt.Errorf("gitea did not answer: %w", link.ErrNoAnswer) case <-time.After(30 * time.Second): return forgeFacts{ID: 1, Guarded: true}, nil } }) if took := time.Since(began); took > 2*time.Second { t.Fatalf("the wait took %s, past its bound of %s", took, w.bound) } if err == nil || !strings.Contains(err.Error(), "waited") || !strings.Contains(err.Error(), "asking 2 times") || !strings.Contains(err.Error(), "issue 486") { t.Fatalf("the bound's end does not say it waited: %v", err) } } // Past the bound the registration fails, saying it waited, for how long, and what the forge last said. func TestTheForgeNotServedPastTheBoundFailsSayingItWaited(t *testing.T) { w, paused := aFakeWait(60 * time.Second) _, err := forgeWaitedOut(t.Context(), w, "novox/mesh-catalog", func(context.Context) (forgeFacts, error) { return forgeFacts{}, notServed() }) if err == nil { t.Fatal("a forge never served was read as an answer") } var waited time.Duration for _, p := range *paused { waited += p } if waited > 60*time.Second || waited < 40*time.Second { t.Fatalf("waited %s in all, want close to and within the bound of 60s", waited) } for _, want := range []string{"waited " + waited.String(), "novox/mesh-catalog", "nothing serves", "issue 486"} { if !strings.Contains(err.Error(), want) { t.Errorf("the failure does not say %q: %v", want, err) } } if !errors.Is(err, link.ErrNothingServes) { t.Errorf("the failure lost what the forge last said: %v", err) } } // A refusal is the forge's word: never asked again. func TestARefusalFromTheForgeIsNeverAskedAgain(t *testing.T) { w, paused := aFakeWait(60 * time.Second) asked := 0 _, err := forgeWaitedOut(t.Context(), w, "novox/x", func(context.Context) (forgeFacts, error) { asked++ return forgeFacts{}, errors.New("gitea.gitea_api: 404 Not Found") }) if err == nil || asked != 1 || len(*paused) != 0 { t.Fatalf("a refusal was asked %d times, paused %v: %v", asked, *paused, err) } } // A registration whose own context ends stops waiting at once. func TestAWaitForTheForgeEndsWithItsContext(t *testing.T) { w, _ := aFakeWait(60 * time.Second) ctx, cancel := context.WithCancel(t.Context()) w.sleep = func(ctx context.Context, d time.Duration) error { cancel(); return sleepOrDone(ctx, d) } asked := 0 _, err := forgeWaitedOut(ctx, w, "novox/x", func(context.Context) (forgeFacts, error) { asked++ return forgeFacts{}, notServed() }) if !errors.Is(err, context.Canceled) || asked != 1 { t.Fatalf("asked %d times after the context ended: %v", asked, err) } }