package selfcompose import ( "context" "strings" "sync/atomic" "testing" "time" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner" nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "github.com/novox/mesh-host/internal/agent" "github.com/novox/mesh-host/internal/buckets" "github.com/novox/mesh-host/internal/buckets/bucketstest" "github.com/novox/mesh-host/internal/compositions" ) // Issue 1965 (shanks 2026-11-05 22:27: the verb compose gave no answer in 30 s while a pass waited on the bus): an ask // that comes while a pass runs is answered at once, says one more pass is queued and what the running one waits on, and // the queued pass is made when the running one ends. func TestComposeAnsweredWhileAPassRuns(t *testing.T) { b := newFakeBus() ctx, cancel := context.WithCancel(context.Background()) defer cancel() env := b.env() hold := make(chan struct{}) var passes atomic.Int64 reached := make(chan struct{}, 8) env.Running = func() []compositions.Composed { passes.Add(1) reached <- struct{}{} if passes.Load() == 1 { <-hold // the first pass waits on the bus } return nil } c := New(env) out, err := c.Watch(ctx, "n1.>") if err != nil { t.Fatal(err) } place := assignedValues(t, meshmoduleassigner.Assignments{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"}}) have, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}}) nodeWant, _ := bucketstest.NodeWant(meshwants.WantsByWantNodeNodeValue{Self: true}) b.putAssigned(t, place) b.put(t, buckets.Have, "n1.node", have) b.put(t, buckets.Wants, "want.n1.node", nodeWant) for _, bucket := range []string{buckets.Wants, buckets.Assignments, buckets.Have, buckets.Modules, ResolvedBucket, builds.BuilderBuildsBucket} { b.send(bucket, agent.Update{Caught: true}) } go func() { for range out { } }() select { case <-reached: case <-time.After(5 * time.Second): t.Fatal("no pass began") } answered := make(chan Pass, 1) go func() { p, _ := c.ComposeNow(ctx) answered <- p }() select { case p := <-answered: if !strings.Contains(p.Queued, "a pass is running") { t.Errorf("answered %+v; want it to say a pass is running and one is queued", p) } case <-time.After(2 * time.Second): t.Fatal("the verb compose gave no answer while a pass ran") } close(hold) select { case <-reached: // the queued pass case <-time.After(5 * time.Second): t.Fatal("the queued pass was not made when the running one ended") } }