package runtime import ( "encoding/json" "errors" "strings" "sync" "testing" "time" "github.com/nats-io/nats.go" "github.com/novox/mesh-tools/node-tools/internal/bus" mt "github.com/novox/mesh-tools/node-tools/internal/meshtest" ) // A bundle's seat traffic through moduleBus.Seat (novox/hq ADR 0259 ยง3), against a real bus: publish under // its own name with the id prefixed and a duplicate said; the record read without naming the bucket; a // worker and a proof subject bound once however often a restarted child asks; and nothing beyond what the // membership lists. func TestABundlesSeatTrafficGoesThroughItsModulesBus(t *testing.T) { mesh := mt.New(t) nc, err := nats.Connect(mt.URL(t)) if err != nil { t.Fatal(err) } t.Cleanup(nc.Close) js, _ := nc.JetStream() if _, err := js.AddStream(&nats.StreamConfig{Name: "SEAT_OPERATOR_CHANNEL", Retention: nats.WorkQueuePolicy, Subjects: []string{"mesh.seat.operator-channel.accept.>"}}); err != nil { t.Fatal(err) } if _, err := js.AddConsumer("SEAT_OPERATOR_CHANNEL", &nats.ConsumerConfig{Durable: "SEAT_OPERATOR_CHANNEL_worker", AckPolicy: nats.AckExplicitPolicy, FilterSubject: "mesh.seat.operator-channel.accept.>"}); err != nil { t.Fatal(err) } kv, err := js.CreateKeyValue(&nats.KeyValueConfig{Bucket: "messenger_asks"}) if err != nil { t.Fatal(err) } _, _ = kv.Put("mesh-delivery.d-1", []byte(`{"state":"open"}`)) asker := mt.MembershipOf("mesh-delivery", "anchor", false, nil) asker.SeatTraffic = &bus.SeatTraffic{Publish: []string{"mesh.seat.operator-channel.accept.ask.mesh-delivery"}, Records: []string{"$JS.API.DIRECT.GET.KV_messenger_asks.$KV.messenger_asks.mesh-delivery.>"}} router := mt.MembershipOf("messenger", "anchor", false, nil) router.SeatTraffic = &bus.SeatTraffic{Answers: []string{"mesh.seat.intake.proof.code.*"}, Workers: []bus.SeatWorker{{Stream: "SEAT_OPERATOR_CHANNEL", Consumer: "SEAT_OPERATOR_CHANNEL_worker", Filter: "mesh.seat.operator-channel.accept.>"}}} mesh.Issue(t, asker) mesh.Issue(t, router) conn := connect(t, "node-tools", "anchor") conn.Follow("mesh-delivery") conn.Follow("messenger") mt.Until(t, func() error { if conn.Membership("mesh-delivery") == nil || conn.Membership("messenger") == nil { return errors.New("not issued yet") } return nil }) all := &consumers{conn: conn, logf: t.Logf, of: map[string]*moduleEvents{}} asking := all.forModule("mesh-delivery").(*moduleBus) routing := all.forModule("messenger").(*moduleBus) publish := json.RawMessage(`{"subject":"mesh.seat.operator-channel.accept.ask.mesh-delivery","body":{"id":"d-1"},"id":"d-1"}`) first, err := asking.Seat("publish", publish, nil) if err != nil { t.Fatal(err) } again, _ := asking.Seat("publish", publish, nil) if !strings.Contains(string(first), `"duplicate":false`) || !strings.Contains(string(again), `"duplicate":true`) { t.Errorf("the same id twice: %s, then %s", first, again) } if _, err := asking.Seat("publish", json.RawMessage(`{"subject":"mesh.seat.operator-channel.accept.ask.mesh-controller","body":{}}`), nil); err == nil { t.Error("a module submitted under another's name") } got, err := asking.Seat("record", json.RawMessage(`{"key":"d-1"}`), nil) if err != nil || !strings.Contains(string(got), `"state":"open"`) { t.Errorf("the record read without its bucket: %s %v", got, err) } var mu sync.Mutex var taken []string handOn := func(method string, params any) (json.RawMessage, error) { mu.Lock() defer mu.Unlock() raw, _ := json.Marshal(params) taken = append(taken, method+" "+string(raw)) return json.RawMessage(`{}`), nil } for i := 0; i < 2; i++ { // a child that started again asks again if _, err := routing.Seat("take", json.RawMessage(`{"worker":"SEAT_OPERATOR_CHANNEL_worker"}`), handOn); err != nil { t.Fatal(err) } } deadline := time.Now().Add(10 * time.Second) for { mu.Lock() n := len(taken) mu.Unlock() if n >= 1 || time.Now().After(deadline) { break } time.Sleep(50 * time.Millisecond) } time.Sleep(300 * time.Millisecond) mu.Lock() if len(taken) != 1 || !strings.Contains(taken[0], "mesh/work") || !strings.Contains(taken[0], `"x-event-id":"mesh-delivery.d-1"`) { t.Errorf("taken %v", taken) } mu.Unlock() if _, err := asking.Seat("take", json.RawMessage(`{"worker":"SEAT_OPERATOR_CHANNEL_worker"}`), handOn); err == nil { t.Error("an asker took the router's work") } if _, err := asking.Seat("answer", json.RawMessage(`{"subject":"mesh.seat.intake.proof.code.*"}`), handOn); err == nil { t.Error("an asker answered proofs") } if _, err := asking.Seat("anything", json.RawMessage(`{}`), nil); err == nil { t.Error("an unknown verb was answered") } } // A lost compare-and-set is answered with its code, so the SDK knows it without reading words. func TestALostCompareAndSetCarriesItsCode(t *testing.T) { mesh := mt.New(t) mesh.Bucket(t, "messenger_asks") m := mt.MembershipOf("messenger", "anchor", false, nil) m.State = []bus.StateIssued{{Name: "asks", Bucket: "messenger_asks", Writes: true}} mesh.Issue(t, m) conn := connect(t, "node-tools", "anchor") conn.Follow("messenger") mt.Until(t, func() error { if conn.Membership("messenger") == nil { return errors.New("not issued yet") } return nil }) b := (&consumers{conn: conn, logf: t.Logf, of: map[string]*moduleEvents{}}).forModule("messenger") if _, err := b.State("create", json.RawMessage(`{"state":"asks","key":"a","value":{}}`)); err != nil { t.Fatal(err) } _, err := b.State("create", json.RawMessage(`{"state":"asks","key":"a","value":{}}`)) var coded interface{ ErrorCode() int } if !errors.As(err, &coded) || coded.ErrorCode() != -32010 || !errors.Is(err, bus.ErrStateChanged) { t.Errorf("a second create: %v", err) } if _, err := b.State("update", json.RawMessage(`{"state":"asks","key":"a","value":{},"revision":0}`)); err == nil { t.Error("an update of revision zero was taken") } }