package buildstep import ( "context" "encoding/json" "errors" "sync" "testing" "time" stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/delivery" ) // configWatch is a double of stdio.WatchConfig: it hands the value now at the start, as the runtime does, and a test // hands each change after with set. type configWatch struct { mu sync.Mutex value string handle func(stdio.ProvisionChange) error asked string } func (w *configWatch) watch(key string, handle func(stdio.ProvisionChange) error) error { w.mu.Lock() w.asked, w.handle = key, handle value := w.value w.mu.Unlock() return handle(stdio.ProvisionChange{Kind: "config", Key: key, Value: json.RawMessage(`"` + value + `"`)}) } func (w *configWatch) set(t *testing.T, value string) { t.Helper() w.mu.Lock() handle, key := w.handle, w.asked w.mu.Unlock() if handle == nil { t.Fatal("the puller watches no config value") } if err := handle(stdio.ProvisionChange{Kind: "config", Key: key, Value: json.RawMessage(`"` + value + `"`)}); err != nil { t.Fatal(err) } } // 1887/AC-wake (g14 idle at capacity 2, 2026-10-25): given a puller started at capacity 0 with an ask waiting, when the // module's config value `capacity` moves to 2 for its node, then it takes the ask, with no pause, resume or step end to // wake it. func TestPullWakesWhenItsCapacityMoves(t *testing.T) { src, st := NewFakeAsks(), newSteps() _ = src.PublishAsk(testAsk(delivery.AskDelivery)) w := &configWatch{value: "shanks=3,g14=0"} p := &Puller{Source: src, Language: delivery.AskGo, Node: "g14", Put: st.put, PullWait: 10 * time.Millisecond, Capacity: func() int { return 0 }, // the settings file, not refilled yet WatchConfig: w.watch, Run: func(ctx context.Context, a delivery.StepAsk) (any, error) { return nil, nil }} ctx, cancel := context.WithCancel(context.Background()) defer cancel() go p.Pull(ctx) waitFor(t, "the watch of the capacity", func() bool { w.mu.Lock(); defer w.mu.Unlock(); return w.handle != nil }) time.Sleep(30 * time.Millisecond) if src.Waiting(delivery.AskDelivery) != 1 { t.Fatal("a node at capacity 0 took an ask") } if w.asked != CapacitySetting { t.Fatalf("the puller watches %q, want %q", w.asked, CapacitySetting) } w.set(t, "shanks=3,g14=2") waitFor(t, "the ask taken once the capacity moved to 2", func() bool { a, _, _ := src.Fates(); return len(a) == 1 }) } // 1887/AC-wake: a value the watch says is no longer answered pulls nothing; a watch that cannot start leaves the puller // on its Capacity (the settings file). func TestPullCapacityWatchRefusedOrUnstarted(t *testing.T) { src, st := NewFakeAsks(), newSteps() p := &Puller{Source: src, Language: delivery.AskGo, Node: "g14", Put: st.put, Capacity: func() int { return 1 }, WatchConfig: func(key string, handle func(stdio.ProvisionChange) error) error { return handle(stdio.ProvisionChange{Kind: "config", Key: key, Refused: "the module is no longer on g14"}) }, Run: func(ctx context.Context, a delivery.StepAsk) (any, error) { return nil, nil }} p.watchCapacity() if p.room() { t.Fatal("a refused capacity value left room") } q := &Puller{Source: src, Language: delivery.AskGo, Node: "g14", Put: st.put, Capacity: func() int { return 1 }, WatchConfig: func(string, func(stdio.ProvisionChange) error) error { return errors.New("no runtime") }, Run: func(ctx context.Context, a delivery.StepAsk) (any, error) { return nil, nil }} q.watchCapacity() if !q.room() { t.Fatal("a watch that could not start ignored the settings file's capacity of 1") } }