The gate's and the assigner's asks carry the router's ask as its JSON, so mesh-sdk names no type of a seat defined whole; the ask's id, options, digest and an option's level are read by the gate, the assigner and the teller through operatorchannelclient. ViewAnswerFits keeps the asking's check, from the key. The channel and intake benches are not defined whole: their shapes move to go/channel (ADR 0625 §9). Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
169 lines
7.1 KiB
Go
169 lines
7.1 KiB
Go
package delivery
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// 1297/B1c: the kinds of ask, and which need proof of who answered: the merge does, hold and restore do not, an
|
|
// unknown kind is refused.
|
|
func TestAskNeedsProof(t *testing.T) {
|
|
for kind, want := range map[string]bool{AskKindMerge: true, AskKindHold: false, AskKindRestore: false} {
|
|
if got, err := AskNeedsProof(kind); err != nil || got != want {
|
|
t.Errorf("AskNeedsProof(%q) = %v, %v; want %v", kind, got, err, want)
|
|
}
|
|
}
|
|
if _, err := AskNeedsProof("approve"); err == nil {
|
|
t.Errorf("an unknown kind was read")
|
|
}
|
|
}
|
|
|
|
// 1297/B1c: each kind of ask key round-trips through ParseAnyAskKey; a key that fits no kind is refused, naming it.
|
|
func TestParseAnyAskKey(t *testing.T) {
|
|
fp := fp2of('d')
|
|
merge, _ := MergeAskKey("feat/r4g", []Head{{Repository: "novox/mesh-sdk", Number: 1, Head: strings.Repeat("1", 40)}})
|
|
hold, err := HoldAskKey("mesh-mail", fp)
|
|
if err != nil || hold != "hold.mesh-mail."+strings.Repeat("d", 64) {
|
|
t.Fatalf("HoldAskKey = %q, %v", hold, err)
|
|
}
|
|
restore, err := RestoreAskKey("ace", "mesh-mail", fp)
|
|
if err != nil || restore != "restore.ace.mesh-mail."+strings.Repeat("d", 64) {
|
|
t.Fatalf("RestoreAskKey = %q, %v", restore, err)
|
|
}
|
|
placement, err := PlacementAskKey("p-one")
|
|
if err != nil || placement != "placement.p-one" {
|
|
t.Fatalf("PlacementAskKey = %q, %v", placement, err)
|
|
}
|
|
if proof, err := AskNeedsProof(AskKindPlacement); proof || err != nil {
|
|
t.Error("the assigner cannot keep a placement ask: its kind needs proof (its yes's level does)")
|
|
}
|
|
for key, want := range map[string]AskKey{
|
|
hold: {Kind: AskKindHold, Module: "mesh-mail", FP: fp},
|
|
restore: {Kind: AskKindRestore, Node: "ace", Module: "mesh-mail", FP: fp},
|
|
placement: {Kind: AskKindPlacement, Proposal: "p-one"},
|
|
} {
|
|
if got, err := ParseAnyAskKey(key); err != nil || got != want {
|
|
t.Errorf("ParseAnyAskKey(%q) = %+v, %v", key, got, err)
|
|
}
|
|
}
|
|
if got, err := ParseAnyAskKey(merge); err != nil || got.Kind != AskKindMerge || got.Group != "feat/r4g" || len(got.Hash) != 12 {
|
|
t.Errorf("ParseAnyAskKey(%q) = %+v, %v", merge, got, err)
|
|
}
|
|
for _, bad := range []string{"want.ace.mesh-mail", "hold.mesh-mail", "restore.mesh-mail." + strings.Repeat("d", 64), "approve.x.y", "restore.a.b.c.d", "placement.P", ""} {
|
|
if _, err := ParseAnyAskKey(bad); err == nil || !strings.Contains(err.Error(), "\""+bad+"\"") {
|
|
t.Errorf("ParseAnyAskKey(%q) = %v, want a refusal naming it", bad, err)
|
|
}
|
|
}
|
|
if _, err := RestoreAskKey("a.b", "m", fp); err == nil {
|
|
t.Errorf("a restore ask on a dotted node was keyed")
|
|
}
|
|
}
|
|
|
|
// 1297/B1c: the view's answer key is the ask's key and `.view`, and round-trips; the merge's ask is refused with its
|
|
// reason (issue 953); another bucket's key is refused, naming it.
|
|
func TestViewAnswerKey(t *testing.T) {
|
|
hold, _ := HoldAskKey("mesh-mail", fp2of('e'))
|
|
key, err := ViewAnswerKey(hold)
|
|
if err != nil || key != hold+".view" {
|
|
t.Fatalf("ViewAnswerKey = %q, %v", key, err)
|
|
}
|
|
if back, err := ParseViewAnswerKey(key); err != nil || back != hold {
|
|
t.Errorf("ParseViewAnswerKey = %q, %v", back, err)
|
|
}
|
|
merge, _ := MergeAskKey("g", []Head{{Repository: "novox/hq", Number: 2, Head: strings.Repeat("2", 40)}})
|
|
if _, err := ViewAnswerKey(merge); err == nil || !strings.Contains(err.Error(), "phone") || !strings.Contains(err.Error(), "953") {
|
|
t.Errorf("the merge's ask was answerable in the view, or its reason is missing: %v", err)
|
|
}
|
|
for _, bad := range []string{hold, merge + ".view", "want.ace.mesh-mail.view", "ace.mesh-mail", ""} {
|
|
if _, err := ParseViewAnswerKey(bad); err == nil || !strings.Contains(err.Error(), "\""+bad+"\"") {
|
|
t.Errorf("ParseViewAnswerKey(%q) = %v, want a refusal naming it", bad, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1297/B1c: a view answer round-trips; one naming no asking or no option is refused.
|
|
func TestViewAnswerRoundTrip(t *testing.T) {
|
|
a := ViewAnswer{Asking: "hold-mesh-mail-aaaa-0123456789ab", Option: "release", Words: "fixed by #12"}
|
|
data, err := EncodeViewAnswer(a)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := DecodeViewAnswer(data)
|
|
a.V = Version
|
|
if err != nil || !SameViewAnswer(got, a) {
|
|
t.Errorf("view answer came back %+v, %v", got, err)
|
|
}
|
|
if _, err := EncodeViewAnswer(ViewAnswer{Asking: "x", Option: " "}); err == nil {
|
|
t.Errorf("a view answer naming no option was accepted")
|
|
}
|
|
if _, err := EncodeViewAnswer(ViewAnswer{Option: "release"}); err == nil {
|
|
t.Errorf("a view answer naming no asking was accepted")
|
|
}
|
|
}
|
|
|
|
func holdAsk(t *testing.T, asking int) (string, AssignerAsk) {
|
|
t.Helper()
|
|
key, err := HoldAskKey("mesh-mail", fp2of('f'))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, _ := json.Marshal(map[string]any{"id": AskingID(key, asking), "headline": "mesh-mail is held",
|
|
"explanation": "its health failed", "who": "operator", "expires": "2026-10-30T00:00:00Z", "on-expiry": "it stays held",
|
|
"options": []map[string]string{{"id": "release", "label": "Release"}, {"id": "keep", "label": "Keep held"}}})
|
|
return key, AssignerAsk{Asking: asking, Ask: raw}
|
|
}
|
|
|
|
// 1297/B1c: the assigner's ask round-trips at its key; an ask of a kind that needs proof, a negative asking and a value
|
|
// carrying no router's ask are refused (its id and options are the teller's to check, ADR 0627 §4).
|
|
func TestAssignerAskRoundTrip(t *testing.T) {
|
|
key, a := holdAsk(t, 2)
|
|
data, err := EncodeAssignerAsk(key, a)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.HasPrefix(string(data), `{"v":1,"ask":`) {
|
|
t.Errorf("the ask is written as %s", data)
|
|
}
|
|
got, err := DecodeAssignerAsk(key, data)
|
|
a.V = Version
|
|
if err != nil || !SameAssignerAsk(got, a) {
|
|
t.Errorf("the ask came back %+v, %v", got, err)
|
|
}
|
|
stale := a
|
|
stale.Asking = -1
|
|
if _, err := EncodeAssignerAsk(key, stale); err == nil {
|
|
t.Errorf("a negative asking was accepted")
|
|
}
|
|
merge, _ := MergeAskKey("g", []Head{{Repository: "novox/hq", Number: 2, Head: strings.Repeat("2", 40)}})
|
|
if _, err := EncodeAssignerAsk(merge, AssignerAsk{Ask: a.Ask}); err == nil {
|
|
t.Errorf("the assigner was let write a merge's ask")
|
|
}
|
|
for _, raw := range []string{"", "null", "[1]"} {
|
|
none := a
|
|
none.Ask = json.RawMessage(raw)
|
|
if _, err := EncodeAssignerAsk(key, none); err == nil || !strings.Contains(err.Error(), "no router's ask") {
|
|
t.Errorf("a value carrying %q as its ask: %v", raw, err)
|
|
}
|
|
}
|
|
spaced := a
|
|
spaced.Ask = json.RawMessage(" " + string(a.Ask) + "\n")
|
|
if !SameAssignerAsk(spaced, a) {
|
|
t.Errorf("the same ask written with spaces is another")
|
|
}
|
|
}
|
|
|
|
// 1297/B1c: a view answer fits only the asking it names: an answer to an earlier asking of the same key is refused,
|
|
// naming both. Whether the option is offered, at the level acknowledge, the teller reads (ADR 0627 §4).
|
|
func TestViewAnswerFits(t *testing.T) {
|
|
key, first := holdAsk(t, 0)
|
|
_, second := holdAsk(t, 2)
|
|
answer := ViewAnswer{Asking: AskingID(key, 0), Option: "release"}
|
|
if err := ViewAnswerFits(answer, key, first); err != nil {
|
|
t.Errorf("the answer to the first asking does not fit it: %v", err)
|
|
}
|
|
if err := ViewAnswerFits(answer, key, second); err == nil || !strings.Contains(err.Error(), AskingID(key, 0)) || !strings.Contains(err.Error(), AskingID(key, 2)) {
|
|
t.Errorf("a stale answer fits the later asking, or the refusal names neither: %v", err)
|
|
}
|
|
}
|