The holder is read from the catalog half's entries serving a verb of mesh-module-assigner, on the branch follower's change callback; the manifest's consumes names the two views by the seat (seat:mesh-module-assigner.views.*). Builds: design 47 G §616, §428, §7 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
205 lines
7.3 KiB
Go
205 lines
7.3 KiB
Go
//go:build integration
|
|
|
|
package firstview
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
condseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
|
"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"
|
|
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
|
catalogseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
|
delivery "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
|
|
|
|
"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.<owner>.event.views.<state>`, 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)
|
|
}
|
|
// the manifest names the assigner's views by the seat; the module the catalog half says claims it is the fixture's
|
|
const holder = "assigner-fixture"
|
|
streamOf := map[string]string{"seat:" + AssignerSeat: catalogue.StreamBusName("assigner-views")}
|
|
filters := map[string][]string{}
|
|
families := 0
|
|
for _, c := range manifest.Consumes {
|
|
emitter, event, isView := strings.Cut(c, ".views.")
|
|
if !isView { // the seats' events (the seat list's, the catalog half's), on EVENTS (cmd/node-tools/seatlist.go)
|
|
continue
|
|
}
|
|
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(holder, 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 _, seat := range []string{"seat:" + AssignerSeat} {
|
|
stream := streamOf[seat]
|
|
mesh.ModuleStream(t, holder, 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, t.Logf)
|
|
deps.Bound, deps.Wait = 100*time.Millisecond, 20*time.Millisecond
|
|
deps.Findings = writes{&mu, findings}
|
|
deps.StartFindings = 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(AssignmentsRead); 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) != catalogViews {
|
|
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)
|
|
respond := func(subject string, answer any) {
|
|
if _, err := nc.Subscribe(subject, func(m *nats.Msg) {
|
|
raw, _ := json.Marshal(answer)
|
|
body, _ := json.Marshal(map[string]json.RawMessage{"result": raw})
|
|
_ = m.Respond(body)
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
// the catalog half answers: the gate's source names main, whose entries say the fixture claims the assigner seat
|
|
respond("mesh.seat.mesh-delivery.tool.source", delivery.SourceAnswer{Branches: []string{"main"}})
|
|
respond("mesh.seat.mesh-catalog.tool.registrations", catalogseat.RegistrationsAnswer{Registrations: []catalogseat.Registration{}})
|
|
respond("mesh.seat.mesh-catalog.tool.revision", catalogseat.RevisionAnswer{
|
|
Revision: &catalogseat.CatalogRevision{Revision: 3, Kind: meshcatalogclient.KindTrunk, Branch: "main"},
|
|
Entries: []catalogseat.CatalogEntry{{Module: holder, Serves: []string{AssignerSeat + ".assignments"}}}})
|
|
for _, s := range Specs {
|
|
s := s
|
|
if _, err := nc.Subscribe("mesh.mod."+holder+".tool."+s.Tool, func(m *nats.Msg) {
|
|
answer, _ := json.Marshal(views.Answer{V: 1, Owner: holder, 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 { // every view answered clears its finding
|
|
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: holder, State: s.State, Key: "next", Revision: 5, Created: time.Unix(1000, 0).UTC(),
|
|
Value: json.RawMessage(`{"n":2}`)})
|
|
mesh.Emit(t, process.ModuleStreamSubject(holder, 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
|
|
})
|
|
}
|
|
}
|
|
|
|
// catalogViews is the first views the branch follower adopts before any branch is known: the registrations and the gate's source
|
|
// (design 47 G §614), each a finding while the seats' verbs go unanswered.
|
|
const catalogViews = 2
|
|
|
|
type errFindings int
|
|
|
|
func (e errFindings) Error() string { return "not yet" }
|
|
|
|
// writes is the runner's finding writer in this test: the findings written and not cleared.
|
|
type writes struct {
|
|
mu *sync.Mutex
|
|
by map[string]bool
|
|
}
|
|
|
|
func (w writes) Write(_ context.Context, f condseat.Finding) error {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
w.by[views.FindingKey(f.Scope, f.ID, f.Kind)] = true
|
|
return nil
|
|
}
|
|
|
|
func (w writes) Clear(_ context.Context, scope, id, kind, _ string) error {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
delete(w.by, views.FindingKey(scope, id, kind))
|
|
return nil
|
|
}
|