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") } }