Files
mesh-delivery/modules/mesh-delivery-builder/internal/build/retakeask_test.go
T
jschoubben b3f7054345 Builder: the operator's ask uses mesh-sdk's operatorask asker
The builder's own lookup of the seat holder and read of the router's record (routerBucket, recordVia in
cmd/mesh-delivery-builder/retake.go) are deleted: the ask is published and the router's decided heard
through operatorask.Asker, and each warrant is held to its ask and acted on once by the operator-channel
client's Take.

Builds: design 47 G A-101 (one operator-channel asker in mesh-sdk), §700, A-74
Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 01:05:42 +01:00

157 lines
6.9 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-sdk/go/operatorask"
"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
}
// Design 47 G A-101: the operator's answer comes through mesh-sdk's asker (its `decided` subject for the builder), is held to
// the ask it answers and acted on once.
func TestTheOperatorsAnswerIsHeardThroughTheSdkAskerAndActedOnOnce(t *testing.T) {
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"})
var hear func(stdio.Envelope) error
var subscribed string
var published []oc.Ask
state := &askedState{}
asker := &operatorask.Asker[oc.Warrant]{Module: "mesh-delivery-builder",
Subscribe: func(pattern string, h func(stdio.Envelope) error) error { subscribed, hear = pattern, h; return nil },
Publish: func(subject string, body any, _ string) error {
published = append(published, body.(oc.Ask))
return nil
},
AskSubject: oc.AcceptSubjectAsk, DecidedSubject: oc.SubjectDecided}
o := &OperatorAsk{Builder: r.b, Acted: &actedStore{}, Ctx: context.Background(), Asker: asker,
Now: func() time.Time { return time.Date(2026, 11, 11, 12, 0, 0, 0, time.UTC) },
Client: &occ.Client{Module: "mesh-delivery-builder", State: state, Publish: func(_ string, body any, id string) error { return asker.Ask(id, body) }}}
if err := o.Listen(); err != nil || subscribed != oc.SubjectDecided("mesh-delivery-builder") {
t.Fatalf("Listen = %v, subscribed to %q", err, subscribed)
}
key := r.key
if err := o.Ask(context.Background(), key, builds.BuilderBuild{State: builds.BuildFailed, EventBuilds: 3, Error: "transport fault"}, 7); err != nil || len(published) != 1 {
t.Fatalf("Ask = %v, published %d", err, len(published))
}
a := published[0]
w, _ := json.Marshal(oc.Warrant{Ask: a.ID, AskDigest: occ.Digest(a), Asker: "mesh-delivery-builder", Outcome: occ.OutcomeChosen, Option: OptionLeaveIt, Level: levelOf(a, OptionLeaveIt),
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"}})
for i := 0; i < 2; i++ { // given twice, acted on once
if err := hear(stdio.Envelope{Key: subscribed, Body: w}); err != nil {
t.Fatal(err)
}
}
var k occ.Kept
e, _ := state.Get("ask." + a.ID)
if err := json.Unmarshal(e.Value, &k); err != nil || !k.Acted || k.Answer == nil || k.Answer.Option != OptionLeaveIt {
t.Errorf("the ask as kept = %+v, %v; want acted on with the answer", k, err)
}
}