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
157 lines
6.9 KiB
Go
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)
|
|
}
|
|
}
|