Files
mesh-delivery/modules/mesh-delivery-builder/internal/build/retakeask_test.go
T
jschoubben 50cca4309f BL17c: the operator is asked once a key's event builds are spent; Build once more builds it as a person's build, Leave it nothing
The builder's asker on the seat operator-channel (level approve, about the key, state asked), the teller's three bus
adapters (publish, the router's record through the seat's holder on the seat list, decided heard through the hub's
events handler); the manifest uses the seat and consumes decided.

Builds: design 47 G §568, A-74
Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-11 23:25:00 +01:00

114 lines
4.5 KiB
Go

package build
import (
"context"
"encoding/json"
"testing"
"time"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
)
// askedState is the client's state `asked`.
type askedState struct{ vals map[string][]byte }
func (s *askedState) Get(key string) (*stdio.StateEntry, error) {
if v, ok := s.vals[key]; ok {
return &stdio.StateEntry{Key: key, Value: v, Revision: 1}, nil
}
return nil, nil
}
func (s *askedState) Create(key string, value any) (uint64, error) {
if _, ok := s.vals[key]; ok {
return 0, stdio.ErrChanged
}
return s.Update(key, value, 0)
}
func (s *askedState) Update(key string, value any, _ uint64) (uint64, error) {
if s.vals == nil {
s.vals = map[string][]byte{}
}
s.vals[key], _ = json.Marshal(value)
return 1, nil
}
// Design 47 G §568: after three event builds the operator is asked, through the seat operator-channel's ask (level approve,
// *Build once more* and *Leave it*, about the key), and *Build once more* builds the key as a person's build, *Leave it* does not.
func TestTheOperatorIsAskedAndAnswers(t *testing.T) {
for _, option := range []string{OptionLeaveIt, OptionBuildOnceMore} {
t.Run(option, func(t *testing.T) { theOperatorAnswers(t, option) })
}
}
func theOperatorAnswers(t *testing.T, option string) {
r := newRig(t, goBundle)
r.consider(t, "modules/m")
seed(t, r.st, builds.BuilderBuild{State: builds.BuildFailed, By: "novox.builder", Manifest: mdigest('a'), EventBuilds: 3,
Error: "transport fault: connection, node-build-go ace, dial tcp: connection refused"})
var published []oc.Ask
acted := &actedStore{}
asker := &OperatorAsk{Builder: r.b, Acted: acted, Ctx: context.Background(), Now: func() time.Time { return time.Date(2026, 11, 11, 12, 0, 0, 0, time.UTC) },
Client: &occ.Client{Module: "mesh-delivery-builder", State: &askedState{},
Publish: func(subject string, body any, id string) error {
if subject != oc.AcceptSubjectAsk("mesh-delivery-builder") {
t.Errorf("published on %s", subject)
}
published = append(published, body.(oc.Ask))
return nil
}}}
rt := &Retaker{Builder: r.b, Acted: acted, Ask: asker.Ask,
Entry: nil}
data, _ := json.Marshal(map[string]string{"node": "ace", "why": "returned"})
for seq := uint64(1); seq <= 2; seq++ { // the same key and revision asked at two events is one ask
if _, err := rt.Heard(context.Background(), "mesh.seat.node-engine.event.manifest-reads-changed.ace", data, seq); err != nil {
t.Fatal(err)
}
r.b.Wait()
}
if len(published) != 1 {
t.Fatalf("asked %d times, want once", len(published))
}
a := published[0]
if a.About != r.key || len(a.Options) != 2 || a.Options[0].Level != occ.Approve || a.Options[0].Label != "Build once more" || a.Options[1].Label != "Leave it" {
t.Fatalf("the ask = %+v, want about the key with the options Build once more (approve) and Leave it", a)
}
if err := occ.Check(a, time.Date(2026, 11, 11, 12, 0, 0, 0, time.UTC)); err != nil {
t.Fatalf("the ask does not hold to the seat's checks: %v", err)
}
if err := occ.Glance(a.Explanation); err != nil {
t.Fatal(err)
}
warrant := func(option string) oc.Warrant {
return oc.Warrant{Ask: a.ID, AskDigest: occ.Digest(a), Asker: "mesh-delivery-builder", Outcome: occ.OutcomeChosen, Option: option, Level: levelOf(a, option),
At: occ.FormatTime(time.Date(2026, 11, 11, 12, 5, 0, 0, time.UTC)), By: &oc.Person{Who: occ.Operator, Kind: "telegram", Identity: "op", Verified: "user"}}
}
h, err := asker.Client.Take(warrant(option), asker.act)
if err != nil || h != occ.Acted {
t.Fatalf("%s: %v, %v", option, h, err)
}
for i := 0; i < 50 && option == OptionBuildOnceMore; i++ { // the build the answer starts runs on its own goroutine
r.b.Wait()
if valueOf(t, r.st, r.key).State == builds.BuildDone {
break
}
time.Sleep(time.Millisecond)
}
r.b.Wait()
v := valueOf(t, r.st, r.key)
if option == OptionLeaveIt && (v.State != builds.BuildFailed || v.EventBuilds != 3) {
t.Fatalf("after Leave it the key = %+v, want failed as it was", v)
}
if option == OptionBuildOnceMore && (v.State != builds.BuildDone || v.EventBuilds != 0) {
t.Fatalf("after Build once more the key = %+v, want built as a person's build (event-builds 0)", v)
}
}
func levelOf(a oc.Ask, option string) string {
o, _ := occ.OptionOf(a, option)
return o.Level
}