mesh/merge-gate pass: builds mesh-sdk, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript), go/merge-check.sh (go)
mesh/delivery delivered
A settings proposal reached the phone with its values masked, so the operator could not tell what they were approving. An ask now carries Whole, shown in place of Explanation on a kind that proves its sender and carries the ask's answers, and Details, what the Details answer shows. Both enter the digest only when given, so an ask without them digests as before and a router and an asker of different builds still agree on it.
282 lines
12 KiB
Go
282 lines
12 KiB
Go
package asks
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
var now = time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC)
|
|
|
|
func held() Ask {
|
|
return Ask{ID: "d-1", Headline: "openrazer delivery held", Explanation: "Needs you: release it, or stop it.",
|
|
Who: Operator, Expires: now.Add(time.Hour), OnExpiry: "the delivery stays held", About: "delivery.openrazer",
|
|
Options: []Option{
|
|
{ID: "release", Label: "Release", Does: "the delivery goes on", Level: Approve, Binds: bind("release", "openrazer")},
|
|
{ID: "stop", Label: "Stop", Does: "the delivery ends", Level: Approve, Binds: bind("stop", "openrazer")},
|
|
{ID: "silence", Label: "Silence for a week", Does: "nothing more is said for a week", Level: Acknowledge},
|
|
}}
|
|
}
|
|
|
|
func bind(verb, what string) string {
|
|
d, err := ActDigest(Act{"verb": verb, "delivery": what})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return d
|
|
}
|
|
|
|
func TestASoundAskPasses(t *testing.T) {
|
|
if err := held().Check(now); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestAnAskIsRefusedInWordsForEachFault(t *testing.T) {
|
|
for want, change := range map[string]func(*Ask){
|
|
"is not letters": func(a *Ask) { a.ID = "a.b" },
|
|
"headline is empty": func(a *Ask) { a.Headline = "" },
|
|
"explains nothing": func(a *Ask) { a.Explanation = " " },
|
|
"only the operator answers": func(a *Ask) { a.Who = "agent" },
|
|
"offers 0 options": func(a *Ask) { a.Options = nil },
|
|
"offers 5 options": func(a *Ask) { a.Options = append(a.Options, a.Options[0], a.Options[1]) },
|
|
"is offered twice": func(a *Ask) { a.Options[1].ID = "release" },
|
|
"does not say what it does": func(a *Ask) { a.Options[0].Does = "" },
|
|
"none of acknowledge": func(a *Ask) { a.Options[0].Level = "admin" },
|
|
"expires before it is asked": func(a *Ask) { a.Expires = now },
|
|
"approves lasts at most": func(a *Ask) { a.Expires = now.Add(25 * time.Hour) },
|
|
"destroys lasts at most": func(a *Ask) { a.Options[0].Level = Destroy },
|
|
"never defaults": func(a *Ask) { a.OnExpiry = "" },
|
|
"is a key, without spaces": func(a *Ask) { a.About = "a b" },
|
|
"does not bind what it": func(a *Ask) { a.Options[0].Binds = "" },
|
|
} {
|
|
a := held()
|
|
change(&a)
|
|
if err := a.Check(now); err == nil || !strings.Contains(err.Error(), want) {
|
|
t.Errorf("want a refusal saying %q, got %v", want, err)
|
|
}
|
|
}
|
|
ack := held()
|
|
ack.Options = ack.Options[2:]
|
|
ack.OnExpiry = ""
|
|
ack.Expires = now.Add(7 * 24 * time.Hour)
|
|
if err := ack.Check(now); err != nil {
|
|
t.Errorf("an ask that only acknowledges needs no expiry words and may last a week: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAnAskerActsOnlyOnAWarrantForItsOwnAskAndOption(t *testing.T) {
|
|
w := Warrant{Ask: "d-1", Asker: "mesh-delivery", Outcome: OutcomeChosen, Option: "release", Label: "Release",
|
|
Level: Approve, By: &Person{Who: Operator, Kind: "telegram", Identity: "42", Verified: "user id verified"},
|
|
AskDigest: held().Digest(), At: now.Add(time.Minute)}
|
|
if o, err := w.For("mesh-delivery", held()); err != nil || o.ID != "release" {
|
|
t.Fatalf("a sound warrant was refused: %v", err)
|
|
}
|
|
if got := w.Says(); got != "the operator, via telegram (user id verified), chose Release" {
|
|
t.Errorf("the warrant says %q", got)
|
|
}
|
|
for why, change := range map[string]func(*Warrant){
|
|
"another asker": func(w *Warrant) { w.Asker = "mesh-controller" },
|
|
"another ask": func(w *Warrant) { w.Ask = "d-2" },
|
|
"no choice": func(w *Warrant) { w.Outcome = OutcomeExpired },
|
|
"nobody": func(w *Warrant) { w.By = nil },
|
|
"not offered": func(w *Warrant) { w.Option = "delete" },
|
|
"a lower level": func(w *Warrant) { w.Level = Acknowledge },
|
|
"another role": func(w *Warrant) { w.By = &Person{Who: "agent"} },
|
|
"no digest": func(w *Warrant) { w.AskDigest = "" },
|
|
"no time given": func(w *Warrant) { w.At = time.Time{} },
|
|
"after expiry": func(w *Warrant) { w.At = now.Add(2 * time.Hour) },
|
|
"another ask's digest": func(w *Warrant) {
|
|
other := held()
|
|
other.Options[0].Binds = bind("release", "another")
|
|
w.AskDigest = other.Digest()
|
|
},
|
|
} {
|
|
c := w
|
|
by := *w.By
|
|
c.By = &by
|
|
change(&c)
|
|
if _, err := c.For("mesh-delivery", held()); err == nil {
|
|
t.Errorf("a warrant with %s was acted on", why)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheSubjectsNameTheirPublisher(t *testing.T) {
|
|
for got, want := range map[string]string{
|
|
AskSubject("mesh-delivery"): "mesh.seat.operator-channel.accept.ask.mesh-delivery",
|
|
DecidedSubject("mesh-delivery"): "mesh.seat.operator-channel.event.decided.mesh-delivery",
|
|
ChannelSubject(Show, "telegram"): "mesh.seat.channel.accept.show.telegram",
|
|
IntakeSubject(Chosen, "desktop"): "mesh.seat.intake.event.choice.desktop",
|
|
ProofSubject(CodeProof, "telegram"): "mesh.seat.intake.proof.code.telegram",
|
|
} {
|
|
if got != want {
|
|
t.Errorf("%s, want %s", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A warrant answers one ask as it was shown, and authorises the act its option bound and no other
|
|
// (novox/hq ADR 0259 §6: the warrant is bound to the exact ask and its arguments).
|
|
func TestAWarrantAuthorisesOnlyTheActItsOptionBound(t *testing.T) {
|
|
a := held()
|
|
// The digest survives the trip through the bus: what the router decodes and digests is what was sent.
|
|
raw, err := json.Marshal(a)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var decoded Ask
|
|
if err := json.Unmarshal(raw, &decoded); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if decoded.Digest() != a.Digest() {
|
|
t.Fatalf("the ask's digest changed on the way: %s, %s", decoded.Digest(), a.Digest())
|
|
}
|
|
local := a
|
|
local.Expires = a.Expires.In(time.FixedZone("CEST", 2*3600))
|
|
if local.Digest() != a.Digest() {
|
|
t.Error("the same instant in another zone digests differently")
|
|
}
|
|
// Changing anything the person was shown, or what an option binds, changes the digest.
|
|
for what, change := range map[string]func(*Ask){
|
|
"the headline": func(a *Ask) { a.Headline += "!" },
|
|
"the explanation": func(a *Ask) { a.Explanation += " " },
|
|
"a label": func(a *Ask) { a.Options[0].Label = "Go" },
|
|
"what it binds": func(a *Ask) { a.Options[0].Binds = bind("release", "another") },
|
|
"the expiry": func(a *Ask) { a.Expires = a.Expires.Add(time.Second) },
|
|
"what it is about": func(a *Ask) { a.About = "delivery.other" },
|
|
} {
|
|
c := held()
|
|
change(&c)
|
|
if c.Digest() == a.Digest() {
|
|
t.Errorf("changing %s leaves the digest", what)
|
|
}
|
|
}
|
|
o, _ := a.Option("release")
|
|
if err := o.Performs(Act{"verb": "release", "delivery": "openrazer"}); err != nil {
|
|
t.Errorf("the bound act was refused: %v", err)
|
|
}
|
|
if err := o.Performs(Act{"verb": "release", "delivery": "another"}); err == nil {
|
|
t.Error("an act the option did not bind was let through")
|
|
}
|
|
ack, _ := a.Option("silence")
|
|
if err := ack.Performs(Act{"anything": "x"}); err != nil {
|
|
t.Errorf("an acknowledgement that bound nothing: %v", err)
|
|
}
|
|
}
|
|
|
|
// The digests are over named fields in a canonical encoding (the review of 2026-10-09): not over Go's
|
|
// encoding of a struct, so a field added to Ask or Option, a JSON tag renamed, or a map built in another
|
|
// order changes nothing it should not — and every field that is shown or bound is in it.
|
|
func TestDigestsAreCanonicalOverNamedFields(t *testing.T) {
|
|
a1, _ := ActDigest(Act{"verb": "release", "arg.delivery": "hq"})
|
|
b := Act{}
|
|
b["arg.delivery"] = "hq"
|
|
b["verb"] = "release"
|
|
a2, _ := ActDigest(b)
|
|
if a1 != a2 {
|
|
t.Error("the order an act was built in changed its digest")
|
|
}
|
|
// A value cannot be read as two: "a"+"b=c" is not "a=b"+"c".
|
|
x, _ := ActDigest(Act{"a": "b\n1:c"})
|
|
y, _ := ActDigest(Act{"a": "b", "c": ""})
|
|
if x == y {
|
|
t.Error("two different acts encode the same")
|
|
}
|
|
if _, err := ActDigest(Act{}); err == nil {
|
|
t.Error("an empty act was digested")
|
|
}
|
|
// The encoding, by hand: any language reproduces it.
|
|
sum := sha256.Sum256([]byte("novox.act.v1\n4:verb\n7:release\n"))
|
|
if got, _ := ActDigest(Act{"verb": "release"}); got != "sha256:"+hex.EncodeToString(sum[:]) {
|
|
t.Errorf("the act's encoding is not the one written down: %s", got)
|
|
}
|
|
|
|
// Every exported field of Ask and Option is in the digest: changing any one changes it.
|
|
base := held()
|
|
mutations := map[string]func(*Ask){
|
|
"ID": func(a *Ask) { a.ID = "other" },
|
|
"Headline": func(a *Ask) { a.Headline += "!" },
|
|
"Explanation": func(a *Ask) { a.Explanation += "!" },
|
|
"Who": func(a *Ask) { a.Who = "someone" },
|
|
"Expires": func(a *Ask) { a.Expires = a.Expires.Add(time.Nanosecond) },
|
|
"OnExpiry": func(a *Ask) { a.OnExpiry += "!" },
|
|
"About": func(a *Ask) { a.About += "!" },
|
|
"Urgent": func(a *Ask) { a.Urgent = !a.Urgent },
|
|
"Options": func(a *Ask) { a.Options = a.Options[:1] },
|
|
"Option.ID": func(a *Ask) { a.Options = append([]Option(nil), a.Options...); a.Options[0].ID = "x" },
|
|
"Option.Label": func(a *Ask) {
|
|
a.Options = append([]Option(nil), a.Options...)
|
|
a.Options[0].Label += "!"
|
|
},
|
|
"Option.Does": func(a *Ask) { a.Options = append([]Option(nil), a.Options...); a.Options[0].Does += "!" },
|
|
"Option.Level": func(a *Ask) {
|
|
a.Options = append([]Option(nil), a.Options...)
|
|
a.Options[0].Level = Destroy
|
|
},
|
|
"Option.Binds": func(a *Ask) {
|
|
a.Options = append([]Option(nil), a.Options...)
|
|
a.Options[0].Binds = "sha256:00"
|
|
},
|
|
}
|
|
for name, mutate := range mutations {
|
|
c := base
|
|
mutate(&c)
|
|
if c.Digest() == base.Digest() {
|
|
t.Errorf("changing %s left the digest the same", name)
|
|
}
|
|
}
|
|
// Whole and Details (issue 383) are in the digest once given, and change it; an ask without either digests
|
|
// exactly as before they existed — the encoding by hand, so no build can drift from it unseen.
|
|
plain := Ask{ID: "p", Headline: "h", Explanation: "e", Who: Operator, Expires: now, OnExpiry: "o", About: "a",
|
|
Options: []Option{{ID: "ok", Label: "OK", Does: "d", Level: Acknowledge}}}
|
|
byHand := sha256.Sum256([]byte("novox.ask.v1\n1:p\n1:h\n1:e\n8:operator\n20:2026-10-08T14:00:00Z\n1:o\n1:a\n5:false\n1:1\n" +
|
|
"2:ok\n2:OK\n1:d\n11:acknowledge\n0:\n"))
|
|
if plain.Digest() != "sha256:"+hex.EncodeToString(byHand[:]) {
|
|
t.Errorf("an ask without Whole and Details does not digest as it did before them: %s", plain.Digest())
|
|
}
|
|
withWhole, withDetails := plain, plain
|
|
withWhole.Whole = "e, whole"
|
|
withDetails.Details = "fingerprint 1234"
|
|
if withWhole.Digest() == plain.Digest() || withDetails.Digest() == plain.Digest() || withWhole.Digest() == withDetails.Digest() {
|
|
t.Error("Whole or Details left the digest the same")
|
|
}
|
|
both := withWhole
|
|
both.Details = "fingerprint 1234"
|
|
changed := both
|
|
changed.Whole += "!"
|
|
if changed.Digest() == both.Digest() {
|
|
t.Error("changing Whole beside Details left the digest the same")
|
|
}
|
|
// A field added to Ask or Option without being added to Digest fails here.
|
|
if n := reflect.TypeOf(Ask{}).NumField(); n != 11 {
|
|
t.Errorf("Ask has %d fields, and Digest covers 11: add the new one to Digest and to this test", n)
|
|
}
|
|
if n := reflect.TypeOf(Option{}).NumField(); n != 5 {
|
|
t.Errorf("Option has %d fields, and Digest covers 5: add the new one to Digest and to this test", n)
|
|
}
|
|
// The same instant in another zone is the same ask.
|
|
z := base
|
|
z.Expires = z.Expires.In(time.FixedZone("x", 3600))
|
|
if z.Digest() != base.Digest() {
|
|
t.Error("the expiry's zone changed the digest")
|
|
}
|
|
}
|
|
|
|
// An ask that says no expiry is answered by no warrant (the confirmation review of 2026-10-09): nothing could
|
|
// show the answer was in time.
|
|
func TestAWarrantForAnAskWithNoExpiryIsRefused(t *testing.T) {
|
|
a := held()
|
|
a.Expires = time.Time{}
|
|
w := Warrant{Ask: "d-1", Asker: "mesh-delivery", Outcome: OutcomeChosen, Option: "release", Label: "Release",
|
|
Level: Approve, By: &Person{Who: Operator, Kind: "telegram", Identity: "42"}, AskDigest: a.Digest(),
|
|
At: now.Add(time.Minute)}
|
|
if _, err := w.For("mesh-delivery", a); err == nil {
|
|
t.Error("a warrant for an ask with no expiry was taken")
|
|
}
|
|
}
|