F-tools: the runner's views consumer is proven on the real bus; the heartbeat is asked of the bus only when a bundle wants it, with no race; a seat record says when the bus wrote it; the beat test counts ticks, not sleeps
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
@@ -72,8 +72,13 @@ type Deps struct {
|
||||
// Start asks the four first views, each until answered, and follows the owners' views families on the runner's own
|
||||
// consumers. It returns at once: the picture fills as the owners answer.
|
||||
func Start(parent context.Context, conn *bus.Conn, logf func(string, ...any)) (*Views, func()) {
|
||||
return StartWith(parent, DepsOf(conn), logf)
|
||||
}
|
||||
|
||||
// DepsOf is the views' reach through the runner's own connection: its tool calls, its consumer and its findings state.
|
||||
func DepsOf(conn *bus.Conn) Deps {
|
||||
me := conn.Module()
|
||||
return StartWith(parent, Deps{Module: me,
|
||||
return Deps{Module: me,
|
||||
Ask: func(address string, body any) (json.RawMessage, error) {
|
||||
got, err := conn.Ask(address, body, "")
|
||||
return got.Result, err
|
||||
@@ -84,7 +89,7 @@ func Start(parent context.Context, conn *bus.Conn, logf func(string, ...any)) (*
|
||||
return err
|
||||
},
|
||||
DeleteFinding: func(key string) error { return conn.StateDelete(me, FindingsState, key) },
|
||||
}, logf)
|
||||
}
|
||||
}
|
||||
|
||||
// StartWith is Start over deps.
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
//go:build integration
|
||||
|
||||
package firstview
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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
|
||||
// consumer carries the manifest's four `consumes` to the owners' views events: 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 consumer is the mesh's, made from the manifest's `consumes` as the mesh makes it: one filter per family.
|
||||
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)
|
||||
}
|
||||
var filters []string
|
||||
for _, c := range manifest.Consumes {
|
||||
emitter, event, _ := strings.Cut(c, ".views.")
|
||||
filters = append(filters, "mesh.mod."+emitter+".event.views."+event)
|
||||
}
|
||||
if len(filters) != len(Specs) {
|
||||
t.Fatalf("the manifest consumes %d families, the runner holds %d first views", len(filters), len(Specs))
|
||||
}
|
||||
mesh.Consumer(t, "anchor", "node-tools", filters, 2*time.Second)
|
||||
|
||||
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)
|
||||
|
||||
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, "mesh.mod."+s.Owner+".event.views."+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" }
|
||||
Reference in New Issue
Block a user