- M1: a condition offering both kinds of answer is asked twice: its authorising answers about the condition, its acknowledging ones (Silence) apart, so an answer from a channel that only acknowledges never ends an approval. - L2: the asked store creates once and changes only over the revision it read, deciding again on what it reads; a stale cancel no longer writes over an act. - L3: every ask is kept before it is published, one whose publishing failed is marked unsent and asked again, and a cancel is kept before it is said. The terminal's test question is now `rehearse`, so it is not called what the glossary calls a drill; its two answers are both approve-level. - L7: two deliveries of one warrant to two controllers at once act exactly once, on a real bus. - Re-vendored onto mesh-sdk 76902998 (canonical digests): an option binds an asks.Act with each argument as arg.<name>. - The lab's bus fixture composes verified-sender only where the lab says its machine is root-free (MESH_LAB_ASKS_ROOT_FREE=true).
152 lines
5.0 KiB
Go
152 lines
5.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
"github.com/novox/mesh-controller/internal/conditions"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
"github.com/novox/mesh-controller/internal/testbus"
|
|
)
|
|
|
|
// busAsker is an asker on a real bus's `asked` bucket, counting what it performs: two of them are two
|
|
// controllers sharing one record.
|
|
type busAskerRig struct {
|
|
mu sync.Mutex
|
|
called int
|
|
acts int
|
|
open []conditions.Condition
|
|
sent [][]byte
|
|
}
|
|
|
|
func (rig *busAskerRig) asker(t *testing.T, conn *nats.Conn, now time.Time) *asker {
|
|
return &asker{
|
|
open: func(context.Context) ([]conditions.Condition, error) {
|
|
rig.mu.Lock()
|
|
defer rig.mu.Unlock()
|
|
return rig.open, nil
|
|
},
|
|
silence: func(context.Context, string, time.Duration, string, string) error { return nil },
|
|
store: busAsked{conn: conn},
|
|
publish: func(_ context.Context, subject string, body []byte, _ string) error {
|
|
rig.mu.Lock()
|
|
defer rig.mu.Unlock()
|
|
if subject == asks.AskSubject(askerName) {
|
|
rig.sent = append(rig.sent, body)
|
|
}
|
|
return nil
|
|
},
|
|
call: func(context.Context, conditions.Action, map[string]string) error {
|
|
time.Sleep(20 * time.Millisecond) // long enough for the other delivery to arrive meanwhile
|
|
rig.mu.Lock()
|
|
defer rig.mu.Unlock()
|
|
rig.called++
|
|
return nil
|
|
},
|
|
record: func(context.Context, link.HandAct) error {
|
|
rig.mu.Lock()
|
|
defer rig.mu.Unlock()
|
|
rig.acts++
|
|
return nil
|
|
},
|
|
now: func() time.Time { return now },
|
|
logf: t.Logf,
|
|
}
|
|
}
|
|
|
|
func askedBus(t *testing.T) *nats.Conn {
|
|
t.Helper()
|
|
conn, err := nats.Connect(testbus.URL(t))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(conn.Close)
|
|
js, err := jetstream.New(conn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := js.CreateKeyValue(context.Background(), jetstream.KeyValueConfig{Bucket: broker.AskedBucket}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return conn
|
|
}
|
|
|
|
// The review of 2026-10-09 (L7): two deliveries of one warrant, to two controllers at once, perform its act
|
|
// exactly once and record it once — the record's compare-and-set decides, never the warrant's message id.
|
|
func TestTwoAnswersAtOnceActOnce(t *testing.T) {
|
|
conn := askedBus(t)
|
|
now := time.Date(2026, 10, 9, 14, 0, 0, 0, time.UTC)
|
|
rig := &busAskerRig{open: []conditions.Condition{heldCondition()}}
|
|
first, second := rig.asker(t, conn, now), rig.asker(t, conn, now)
|
|
if err := first.reconcile(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(rig.sent) != 1 {
|
|
t.Fatalf("asked %d times", len(rig.sent))
|
|
}
|
|
var q asks.Ask
|
|
_ = json.Unmarshal(rig.sent[0], &q)
|
|
release, _ := q.Option("release")
|
|
w := asks.Warrant{Ask: q.ID, Asker: askerName, About: q.About, Outcome: asks.OutcomeChosen, Option: release.ID,
|
|
Label: release.Label, Level: release.Level, Channel: "telegram", Proofs: []string{"P1"}, At: now,
|
|
AskDigest: q.Digest(), By: &asks.Person{Who: asks.Operator, Kind: "telegram", Identity: "42", Verified: "user id verified"}}
|
|
body, _ := json.Marshal(w)
|
|
var wg sync.WaitGroup
|
|
for _, a := range []*asker{first, second, first, second} {
|
|
wg.Add(1)
|
|
go func(a *asker) {
|
|
defer wg.Done()
|
|
if err := a.Decided(context.Background(), body); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}(a)
|
|
}
|
|
wg.Wait()
|
|
if rig.called != 1 || rig.acts != 1 {
|
|
t.Fatalf("performed %d time(s), recorded %d time(s)", rig.called, rig.acts)
|
|
}
|
|
got, err := busAsked{conn: conn}.Get(context.Background(), q.ID)
|
|
if err != nil || got == nil || got.Acted != "done" {
|
|
t.Fatalf("kept as %+v (%v)", got, err)
|
|
}
|
|
}
|
|
|
|
// The review of 2026-10-09 (L2): a write decided on a record read earlier never lands over one made since. A
|
|
// cancel read before the answer was acted on leaves the act's record as it is.
|
|
func TestAStaleCancelDoesNotWriteOverAnAct(t *testing.T) {
|
|
conn := askedBus(t)
|
|
now := time.Date(2026, 10, 9, 14, 0, 0, 0, time.UTC)
|
|
rig := &busAskerRig{open: []conditions.Condition{heldCondition()}}
|
|
a := rig.asker(t, conn, now)
|
|
if err := a.reconcile(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var q asks.Ask
|
|
_ = json.Unmarshal(rig.sent[0], &q)
|
|
stale, _ := busAsked{conn: conn}.Get(context.Background(), q.ID)
|
|
release, _ := q.Option("release")
|
|
w := asks.Warrant{Ask: q.ID, Asker: askerName, About: q.About, Outcome: asks.OutcomeChosen, Option: release.ID,
|
|
Label: release.Label, Level: release.Level, Channel: "telegram", At: now, AskDigest: q.Digest(),
|
|
By: &asks.Person{Who: asks.Operator, Kind: "telegram", Identity: "42", Verified: "user id verified"}}
|
|
body, _ := json.Marshal(w)
|
|
if err := a.Decided(context.Background(), body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := a.cancel(context.Background(), *stale, "the condition ended"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, _ := busAsked{conn: conn}.Get(context.Background(), q.ID)
|
|
if got.State != string(asks.OutcomeChosen) || got.Acted != "done" {
|
|
t.Errorf("a stale cancel wrote over the act: %+v", got)
|
|
}
|
|
}
|