//go:build integration package firstview import ( "context" "encoding/json" "os" "strings" "sync" "testing" "time" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/process" "git.novox.be/novox/mesh-sdk/go/views" "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" ) // issue 1966 TLS-5 [i] (ADR 0615 §2, §4, §6): against the real bus, with the owners down and then up, the runner's own // consumers on the owners' own views streams carry the manifest's four `consumes`: the first views are asked until answered, // the unanswered ones are findings that the answer clears, and each owner's next event reaches its picture. func TestTheRunnersConsumerCarriesTheOwnersViewsFamiliesWhenTheOwnersAreDownThenUp(t *testing.T) { mesh := mt.New(t) url := mt.URL(t) // The owners publish their views on their own streams (views.Emit: `mesh.stream..event.views.`, on // assigner-views and indexer-views), never on EVENTS. The consumers on them are the mesh's, one filter per family the // manifest's `consumes` names, and the runner's membership names them as the mesh issues it (ADR 0363 §8). raw, err := os.ReadFile("../../module.json") if err != nil { t.Fatal(err) } var manifest struct { Consumes []string `json:"consumes"` } if err := json.Unmarshal(raw, &manifest); err != nil { t.Fatal(err) } streamOf := map[string]string{Assigner: catalogue.StreamBusName("assigner-views"), Indexer: catalogue.StreamBusName("indexer-views")} filters := map[string][]string{} families := 0 for _, c := range manifest.Consumes { emitter, event, _ := strings.Cut(c, ".views.") stream := streamOf[emitter] if stream == "" { t.Fatalf("the manifest consumes %s, whose views stream the test does not know", c) } filters[stream] = append(filters[stream], process.ModuleStreamSubject(emitter, views.EventPrefix+event)) families++ } if families != len(Specs) { t.Fatalf("the manifest consumes %d families, the runner holds %d first views", families, len(Specs)) } runner := mt.MembershipOf("node-tools", "anchor", false, nil) for _, owner := range []string{Assigner, Indexer} { stream := streamOf[owner] mesh.ModuleStream(t, owner, stream) mesh.ConsumerOn(t, stream, "anchor", "node-tools", filters[stream], 2*time.Second) runner.Hears = append(runner.Hears, bus.HeardIssued{Stream: stream, Consumer: "anchor_node-tools"}) } mesh.Consumer(t, "anchor", "node-tools", []string{"mesh.mod.mesh-knowledge.event.reviewed"}, 2*time.Second) // its `waits`, on EVENTS mesh.Issue(t, runner) conn, err := bus.Connect(bus.Credential{URL: url, Module: "node-tools", Node: "anchor"}) if err != nil { t.Fatal(err) } conn.Logf = func(string, ...any) {} t.Cleanup(conn.Close) conn.Follow("node-tools") mt.Until(t, func() error { if conn.Membership("node-tools") == nil { return errFindings(-1) } return nil }) var mu sync.Mutex findings := map[string]bool{} deps := DepsOf(conn) deps.Bound, deps.Wait = 100*time.Millisecond, 20*time.Millisecond deps.PutFinding = func(key string, _ json.RawMessage) error { mu.Lock(); findings[key] = true; mu.Unlock(); return nil } deps.DeleteFinding = func(key string) error { mu.Lock(); delete(findings, key); mu.Unlock(); return nil } v, stop := StartWith(context.Background(), deps, func(string, ...any) {}) t.Cleanup(stop) // The owners are down: no tool answers, nothing is claimed, and past the bound each view is a finding. if _, err := v.State(Assigner + ".placement"); err == nil { t.Fatal("a state its owner has not answered is claimed") } mt.Until(t, func() error { mu.Lock() defer mu.Unlock() if len(findings) != len(Specs) { return errFindings(len(findings)) } return nil }) // The owners come up and answer their first views. nc, err := nats.Connect(url) if err != nil { t.Fatal(err) } t.Cleanup(nc.Close) for _, s := range Specs { s := s if _, err := nc.Subscribe("mesh.mod."+s.Owner+".tool."+s.Tool, func(m *nats.Msg) { answer, _ := json.Marshal(views.Answer{V: 1, Owner: s.Owner, State: s.State, Revision: 4, Created: time.Unix(1000, 0).UTC(), Values: map[string]json.RawMessage{"first": json.RawMessage(`{"n":1}`)}}) body, _ := json.Marshal(map[string]json.RawMessage{"result": answer}) _ = m.Respond(body) }); err != nil { t.Fatal(err) } } _ = nc.Flush() for _, s := range Specs { name := s.Name() mt.Until(t, func() error { _, err := v.Entry(name, "first"); return err }) } mt.Until(t, func() error { mu.Lock() defer mu.Unlock() if len(findings) != 0 { return errFindings(len(findings)) } return nil }) // Each owner's next event arrives on the runner's consumer: all four families are bound. for _, s := range Specs { ev, _ := json.Marshal(views.Event{V: 1, Owner: s.Owner, State: s.State, Key: "next", Revision: 5, Created: time.Unix(1000, 0).UTC(), Value: json.RawMessage(`{"n":2}`)}) mesh.Emit(t, process.ModuleStreamSubject(s.Owner, views.EventPrefix+s.State), json.RawMessage(ev)) } for _, s := range Specs { name := s.Name() mt.Until(t, func() error { got, err := v.Entry(name, "next") if err != nil || string(got) != `{"n":2}` { return errFindings(-1) } return nil }) } } type errFindings int func (e errFindings) Error() string { return "not yet" }