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
649 lines
25 KiB
Go
649 lines
25 KiB
Go
package delivery
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func gateHeads() []Head {
|
|
return []Head{
|
|
{Repository: "novox/mesh-sdk", Number: 36, Head: "aaaa"},
|
|
{Repository: "novox/mesh-control", Number: 7, Head: "bbbb"},
|
|
}
|
|
}
|
|
|
|
func TestMergeKeys(t *testing.T) { // 1201/AC1
|
|
if k, err := MergesKey("feat/x"); err != nil || k != "feat/x" {
|
|
t.Fatalf("merges key = %q, %v", k, err)
|
|
}
|
|
if _, err := MergesKey(""); err == nil {
|
|
t.Error("an empty group accepted")
|
|
}
|
|
hh, _ := HeadsHash(gateHeads())
|
|
key, err := MergeAskKey("feat/x", gateHeads())
|
|
if err != nil || key != "merge.feat/x."+hh {
|
|
t.Fatalf("ask key = %q, %v", key, err)
|
|
}
|
|
kind, group, hash, err := ParseAskKey(key)
|
|
if err != nil || kind != AskKindMerge || group != "feat/x" || hash != hh {
|
|
t.Fatalf("parse ask key = %q %q %q %v", kind, group, hash, err)
|
|
}
|
|
if AnswerKey(key) != key {
|
|
t.Error("the answer's key is not the ask's key")
|
|
}
|
|
if _, err := MergeAskKey("feat/x", nil); err == nil {
|
|
t.Error("an ask key without heads accepted")
|
|
}
|
|
if _, err := MergeAskKey("", gateHeads()); err == nil {
|
|
t.Error("an ask key without a group accepted")
|
|
}
|
|
for _, bad := range []string{"", "merge", "merge.g", "merge.g.h.x", "other.g.0123456789ab", "merge..0123456789ab", "merge.g.nothex", "merge.a=zz.0123456789ab", "merge.a=4.0123456789ab", "merge.a=4A.0123456789ab", "merge.a+b.0123456789ab"} {
|
|
if _, _, _, err := ParseAskKey(bad); err == nil {
|
|
t.Errorf("parse ask key %q accepted", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAskID(t *testing.T) { // 1201/AC2
|
|
k1, _ := MergeAskKey("feat/x", gateHeads())
|
|
k2, _ := MergeAskKey("feat/y", gateHeads())
|
|
k3, _ := MergeAskKey("feat/x", gateHeads()[:1])
|
|
long, _ := MergeAskKey("feat/"+strings.Repeat("long-", 20), gateHeads())
|
|
long2, _ := MergeAskKey("feat/"+strings.Repeat("long-", 20)+"2", gateHeads())
|
|
slash, _ := MergeAskKey("a/b", gateHeads())
|
|
under, _ := MergeAskKey("a_b", gateHeads())
|
|
eq, _ := MergeAskKey("feat=x", gateHeads())
|
|
eqs, _ := MergeAskKey("feat_x", gateHeads())
|
|
seen := map[string]string{}
|
|
for _, k := range []string{k1, k2, k3, long, long2, slash, under, eq, eqs, "=odd.key"} {
|
|
id := AskID(k)
|
|
if strings.Contains(id, ".") {
|
|
t.Errorf("id %q holds a dot", id)
|
|
}
|
|
if !routerUsableID(id) {
|
|
t.Errorf("id %q is not usable by the router", id)
|
|
}
|
|
if other, dup := seen[id]; dup {
|
|
t.Errorf("keys %q and %q give the id %q", k, other, id)
|
|
}
|
|
seen[id] = k
|
|
}
|
|
if AskID(k1) != AskID(k1) {
|
|
t.Error("the id is not stable")
|
|
}
|
|
// 1280 (ADR 0556 clause 5): a settle ask's group is the same for the same ids in any order, keys like any group, and its
|
|
// options bind mesh-delivery.settle; every other ask binds mesh-delivery.merge.
|
|
g := SettleGroup([]string{"novox/b@222222222222", "novox/a@111111111111"})
|
|
if g != SettleGroup([]string{"novox/a@111111111111", "novox/b@222222222222"}) || !strings.HasPrefix(g, SettleGroupPrefix) || len(g) != len(SettleGroupPrefix)+12 {
|
|
t.Errorf("SettleGroup = %q", g)
|
|
}
|
|
if k, err := MergeAskKey(g, gateHeads()); err != nil || !routerUsableID(AskID(k)) {
|
|
t.Errorf("the settle ask's key %q: %v", k, err)
|
|
}
|
|
for group, want := range map[string]string{g: "mesh-delivery.settle", "feat/x": "mesh-delivery.merge",
|
|
"settle/foo": "mesh-delivery.merge", "settle": "mesh-delivery.merge"} { // a branch named settle/... is a merge's group
|
|
if got := AskVerb(group); got != want {
|
|
t.Errorf("AskVerb(%q) = %q, want %q", group, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOwedKey(t *testing.T) { // 1201/AC3
|
|
for _, act := range []string{"status=mesh/merge-gate", "view", "note", "comment.1", "comment.12"} {
|
|
key, err := OwedKey("novox/mesh-sdk", 36, "aaaa", act)
|
|
if err != nil || key != "novox/mesh-sdk.36.aaaa."+act {
|
|
t.Fatalf("owed key for %s = %q, %v", act, key, err)
|
|
}
|
|
repo, number, head, gotAct, err := ParseOwedKey(key)
|
|
if err != nil || repo != "novox/mesh-sdk" || number != 36 || head != "aaaa" || gotAct != act {
|
|
t.Fatalf("parse %q = %q %d %q %q %v", key, repo, number, head, gotAct, err)
|
|
}
|
|
}
|
|
kind, context, err := ParseOwedAct("status=mesh/merge-gate")
|
|
if err != nil || kind != OwedStatus || context != "mesh/merge-gate" {
|
|
t.Errorf("owed act = %q %q %v", kind, context, err)
|
|
}
|
|
for _, bad := range []string{"", "merge", "status", "status=", "view=x", "status=a.b", "statuses=x", "comment", "comment.", "comment.0", "comment.-1", "comment.01", "comment.x", "comment=1"} {
|
|
if _, err := OwedKey("novox/mesh-sdk", 36, "aaaa", bad); err == nil {
|
|
t.Errorf("owed act %q accepted", bad)
|
|
}
|
|
}
|
|
if _, err := OwedKey("nope", 36, "aaaa", "view"); err == nil {
|
|
t.Error("a bad repository accepted")
|
|
}
|
|
if _, err := OwedKey("novox/mesh-sdk", -1, "aaaa", "view"); err == nil {
|
|
t.Error("a negative pull request number accepted")
|
|
}
|
|
if _, err := OwedKey("novox/mesh-sdk", 36, "", "view"); err == nil {
|
|
t.Error("an empty head accepted")
|
|
}
|
|
for _, bad := range []string{"", "novox/mesh-sdk.36.aaaa", "novox/mesh-sdk.x.aaaa.view", "novox/mesh-sdk.-1.aaaa.view", "novox/mesh-sdk.36.aaaa.comment", "novox/mesh-sdk.36.aaaa.merge", "a.b.36.aaaa.view"} {
|
|
if _, _, _, _, err := ParseOwedKey(bad); err == nil {
|
|
t.Errorf("parse owed key %q accepted", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func sampleMerge() Merge {
|
|
return Merge{Group: "feat/x", Ask: "merge.feat/x.0123456789ab", Pulls: []MergePull{
|
|
{Head: gateHeads()[0], State: MergeMerged, Commit: "cccc"},
|
|
{Head: gateHeads()[1], State: MergeRefused, Words: "the forge says no"},
|
|
{Head: Head{Repository: "novox/other", Number: 1, Head: "dddd"}, State: MergeMerging},
|
|
}}
|
|
}
|
|
|
|
func sampleTurn() Turn {
|
|
at := time.Date(2026, 10, 26, 9, 0, 0, 0, time.UTC)
|
|
return Turn{Repository: "novox/mesh-sdk", Waiting: []TurnWait{
|
|
{Group: "feat/x", Heads: mustHash(gateHeads()), Ask: "merge.feat/x." + mustHash(gateHeads()), Approved: at, Since: at,
|
|
WaitsFor: "feat/y.0123456789ab", WaitsSince: at.Add(-time.Minute), Step: "the check ask of novox/mesh-sdk#40"},
|
|
{Group: "fix/1.2", Heads: "0123456789ac", Approved: at.Add(time.Minute), Since: at.Add(time.Minute)},
|
|
}}
|
|
}
|
|
|
|
func sampleAsk() Ask {
|
|
key, _ := MergeAskKey("feat/x", gateHeads())
|
|
return Ask{Group: "feat/x", Heads: gateHeads(), Ask: routerAskJSON(AskID(key), "Merge into main: group feat/x")}
|
|
}
|
|
|
|
// routerAskJSON is a router's ask as the gate carries it: the operator-channel seat's JSON, which mesh-sdk does not
|
|
// type (novox/hq ADR 0627 §4).
|
|
func routerAskJSON(id, headline string) json.RawMessage {
|
|
raw, _ := json.Marshal(map[string]any{"id": id, "headline": headline, "explanation": "The group passed its checks.",
|
|
"who": "operator", "expires": "2026-10-20T08:00:00Z", "on-expiry": "nothing is merged",
|
|
"options": []map[string]string{{"id": "approve", "label": "Approve", "does": "merges", "level": "approve", "binds": "sha256:00"}}})
|
|
return raw
|
|
}
|
|
|
|
// routerUsableID is the router's rule for an ask's id (operator-channel's client, UsableID), held here to check that
|
|
// AskID and AskingID give ids the router takes.
|
|
var routerUsableID = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$`).MatchString
|
|
|
|
func sampleOwed() []Owed {
|
|
return []Owed{
|
|
{Act: OwedStatus, Context: "mesh/merge-gate", State: "success", Target: "https://mesh.example/delivery/36"},
|
|
{Act: OwedView, Text: "the delivery reaches ace"},
|
|
{Act: OwedNote, Text: "plan: mesh-mail to ace", Ref: "mesh-plan"},
|
|
{Act: OwedComment, Text: "merged"},
|
|
}
|
|
}
|
|
|
|
func TestGateValuesRoundTrip(t *testing.T) { // 1201/AC4
|
|
hasV := func(name string, data []byte, err error) {
|
|
t.Helper()
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", name, err)
|
|
}
|
|
var probe map[string]any
|
|
if json.Unmarshal(data, &probe) != nil || probe["v"] != float64(1) {
|
|
t.Fatalf("%s: %s does not carry v:1", name, data)
|
|
}
|
|
}
|
|
m := sampleMerge()
|
|
data, err := EncodeMerge(m)
|
|
hasV("merge", data, err)
|
|
m.V = Version
|
|
if got, err := DecodeMerge(data); err != nil || !SameMerge(got, m) {
|
|
t.Errorf("merge round trip: %+v %v", got, err)
|
|
}
|
|
a := sampleAsk()
|
|
data, err = EncodeAsk(a)
|
|
hasV("ask", data, err)
|
|
a.V = Version
|
|
if got, err := DecodeAsk(data); err != nil || !SameAsk(got, a) {
|
|
t.Errorf("ask round trip: %+v %v", got, err)
|
|
}
|
|
an := Answer{Option: "approve", Warrant: "w-1", Words: "approved on telegram"}
|
|
data, err = EncodeAnswer(an)
|
|
hasV("answer", data, err)
|
|
an.V = Version
|
|
if got, err := DecodeAnswer(data); err != nil || !SameAnswer(got, an) {
|
|
t.Errorf("answer round trip: %+v %v", got, err)
|
|
}
|
|
for _, o := range sampleOwed() {
|
|
data, err = EncodeOwed(o)
|
|
hasV("owed "+o.Act, data, err)
|
|
o.V = Version
|
|
if got, err := DecodeOwed(data); err != nil || !SameOwed(got, o) {
|
|
t.Errorf("owed %s round trip: %+v %v", o.Act, got, err)
|
|
}
|
|
}
|
|
tu := sampleTurn()
|
|
data, err = EncodeTurn(tu)
|
|
hasV("turn", data, err)
|
|
tu.V = Version
|
|
if got, err := DecodeTurn(data); err != nil || !SameTurn(got, tu) {
|
|
t.Errorf("turn round trip: %+v %v", got, err)
|
|
}
|
|
for _, field := range []string{`"waits-for":"feat/y.0123456789ab"`, `"waits-since":`, `"approved":`, `"step":`} {
|
|
if !strings.Contains(string(data), field) {
|
|
t.Errorf("the turn lacks %s: %s", field, data)
|
|
}
|
|
}
|
|
// ADR 0548 §3: a turn holds off every check but those of the heads that wait in it, whoever else waits there.
|
|
if !TurnHolds(tu, "fedcba987654") || TurnHolds(tu, tu.Waiting[0].Heads) || TurnHolds(tu, tu.Waiting[1].Heads) || TurnHolds(Turn{}, "fedcba987654") {
|
|
t.Error("TurnHolds: a turn holds other heads' checks and never the check of heads waiting in it")
|
|
}
|
|
for name, dec := range map[string]func([]byte) error{
|
|
"turn": func(b []byte) error { _, err := DecodeTurn(b); return err },
|
|
"merge": func(b []byte) error { _, err := DecodeMerge(b); return err },
|
|
"ask": func(b []byte) error { _, err := DecodeAsk(b); return err },
|
|
"answer": func(b []byte) error { _, err := DecodeAnswer(b); return err },
|
|
"owed": func(b []byte) error { _, err := DecodeOwed(b); return err },
|
|
} {
|
|
for _, in := range []string{`{"group":"g"}`, `{"v":2}`} {
|
|
if err := dec([]byte(in)); !errors.Is(err, ErrUnknownVersion) {
|
|
t.Errorf("%s decode of %s = %v, want ErrUnknownVersion", name, in, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGateValuesRefused(t *testing.T) { // 1201/AC5
|
|
wantErr := func(name string, err error, words ...string) {
|
|
t.Helper()
|
|
if err == nil {
|
|
t.Errorf("%s accepted", name)
|
|
return
|
|
}
|
|
for _, w := range words {
|
|
if !strings.Contains(err.Error(), w) {
|
|
t.Errorf("%s: error %q does not name %q", name, err, w)
|
|
}
|
|
}
|
|
}
|
|
m := sampleMerge()
|
|
m.Pulls[0].Commit = ""
|
|
_, err := EncodeMerge(m)
|
|
wantErr("merged without a merge commit", err, "commit")
|
|
m = sampleMerge()
|
|
m.Pulls[1].Words = ""
|
|
_, err = EncodeMerge(m)
|
|
wantErr("refused without words", err, "words")
|
|
m = sampleMerge()
|
|
m.Pulls[2].State = "done"
|
|
_, err = EncodeMerge(m)
|
|
wantErr("unknown state", err, "done")
|
|
m = sampleMerge()
|
|
m.Pulls = nil
|
|
_, err = EncodeMerge(m)
|
|
wantErr("merge without pulls", err, "pull")
|
|
m = sampleMerge()
|
|
m.Ask = ""
|
|
_, err = EncodeMerge(m)
|
|
wantErr("merge without its ask", err, "ask")
|
|
|
|
a := sampleAsk()
|
|
for _, raw := range []string{"", "null", "[]", `"ask"`} {
|
|
a.Ask = json.RawMessage(raw)
|
|
_, err = EncodeAsk(a)
|
|
wantErr("a value with no router's ask "+raw, err, "no router's ask")
|
|
}
|
|
a = sampleAsk()
|
|
a.Heads = nil
|
|
_, err = EncodeAsk(a)
|
|
wantErr("ask without heads", err, "pull request")
|
|
|
|
for name, an := range map[string]Answer{
|
|
"option": {Warrant: "w", Words: "x"},
|
|
"warrant": {Option: "approve", Words: "x"},
|
|
"words": {Option: "approve", Warrant: "w"},
|
|
} {
|
|
_, err = EncodeAnswer(an)
|
|
wantErr("answer without "+name, err, name)
|
|
}
|
|
|
|
for name, o := range map[string]Owed{
|
|
"context": {Act: OwedStatus, State: "success", Target: "t"},
|
|
"state": {Act: OwedStatus, Context: "mesh/merge-gate", State: "green", Target: "t"},
|
|
"target": {Act: OwedStatus, Context: "mesh/merge-gate", State: "success"},
|
|
"text": {Act: OwedView},
|
|
"ref": {Act: OwedNote, Text: "line"},
|
|
"comment": {Act: OwedComment},
|
|
"act": {Act: "merge"},
|
|
} {
|
|
_, err = EncodeOwed(o)
|
|
wantErr("owed missing "+name, err)
|
|
}
|
|
_, err = EncodeOwed(Owed{Act: OwedNote, Text: "line"})
|
|
wantErr("note without ref", err, "ref")
|
|
|
|
for name, c := range map[string]struct {
|
|
edit func(*Turn)
|
|
words []string
|
|
}{
|
|
"no delivery": {func(t *Turn) { t.Waiting = nil }, []string{"no delivery", "deleted"}},
|
|
"a repository in capitals": {func(t *Turn) { t.Repository = "novox/Mesh-SDK" }, []string{"lowercase"}},
|
|
"no repository": {func(t *Turn) { t.Repository = "" }, []string{"repository"}},
|
|
"no group": {func(t *Turn) { t.Waiting[0].Group = "" }, []string{"group"}},
|
|
"a bad heads hash": {func(t *Turn) { t.Waiting[0].Heads = "xyz" }, []string{"xyz"}},
|
|
"the same heads twice": {func(t *Turn) { t.Waiting[1].Heads = t.Waiting[0].Heads }, []string{"twice"}},
|
|
"no Approve time": {func(t *Turn) { t.Waiting[1].Approved = time.Time{} }, []string{"Approve"}},
|
|
"out of the Approve's order": {func(t *Turn) { t.Waiting[1].Approved = t.Waiting[0].Approved.Add(-time.Hour) }, []string{"order"}},
|
|
"waits for no verdict key": {func(t *Turn) { t.Waiting[0].WaitsFor = "feat/y" }, []string{"verdict key"}},
|
|
"a step and no check": {func(t *Turn) { t.Waiting[1].Step = "x" }, []string{"names no check"}},
|
|
} {
|
|
tu := sampleTurn()
|
|
c.edit(&tu)
|
|
_, err = EncodeTurn(tu)
|
|
wantErr("turn with "+name, err, c.words...)
|
|
}
|
|
_, err = EncodeOwed(Owed{Act: OwedStatus, State: "success"})
|
|
wantErr("status without context", err, "context")
|
|
}
|
|
|
|
func mustHash(h []Head) string { s, _ := HeadsHash(h); return s }
|
|
|
|
func TestGateSame(t *testing.T) { // 1201/AC6
|
|
if !SameMerge(sampleMerge(), sampleMerge()) || !SameAsk(sampleAsk(), sampleAsk()) {
|
|
t.Error("equal values differ")
|
|
}
|
|
m := sampleMerge()
|
|
m.Pulls[0].Commit = "other"
|
|
if SameMerge(m, sampleMerge()) {
|
|
t.Error("different merges are the same")
|
|
}
|
|
a := sampleAsk()
|
|
a.Ask = routerAskJSON(AskID("merge.feat/x."+mustHash(gateHeads())), "other")
|
|
if SameAsk(a, sampleAsk()) {
|
|
t.Error("different asks are the same")
|
|
}
|
|
if !SameAnswer(Answer{Option: "a", Warrant: "w", Words: "x"}, Answer{Option: "a", Warrant: "w", Words: "x"}) ||
|
|
SameAnswer(Answer{Option: "a", Warrant: "w", Words: "x"}, Answer{Option: "b", Warrant: "w", Words: "x"}) {
|
|
t.Error("SameAnswer wrong")
|
|
}
|
|
if !SameOwed(sampleOwed()[0], sampleOwed()[0]) || SameOwed(sampleOwed()[0], sampleOwed()[1]) {
|
|
t.Error("SameOwed wrong")
|
|
}
|
|
}
|
|
|
|
// today's keys: names of letters, digits, - / and _ key exactly as before the escape.
|
|
func TestKeysOfPlainNamesAreUnchanged(t *testing.T) { // 1323/AC2
|
|
// And the buckets and their history are the decided layout (once TestGateBuckets; merged to pay for issue 774's
|
|
// token test).
|
|
if MergesBucket != "mesh-delivery-gate_merges" || AsksBucket != "mesh-delivery-gate_asks" ||
|
|
OwedBucket != "mesh-delivery-gate_owed" || AnswersBucket != "mesh-delivery-teller_answers" ||
|
|
MergesHistory != 10 || AsksHistory != 5 || OwedHistory != 5 || AnswersHistory != 5 ||
|
|
TurnsBucket != "mesh-delivery-gate_turns" || TurnsHistory != 5 {
|
|
t.Error("a bucket or history differs from the decided layout")
|
|
}
|
|
hh, _ := HeadsHash(gateHeads())
|
|
for _, name := range []string{"feat/x", "fix/1323-escape", "feat/delivery_source-keys", "main", "A/B_c-9"} {
|
|
if k, err := MergesKey(name); err != nil || k != name {
|
|
t.Errorf("merges key %q = %q, %v", name, k, err)
|
|
}
|
|
if k, err := CandidatesKey(name); err != nil || k != name {
|
|
t.Errorf("candidates key %q = %q, %v", name, k, err)
|
|
}
|
|
if k, err := MergeAskKey(name, gateHeads()); err != nil || k != "merge."+name+"."+hh {
|
|
t.Errorf("ask key %q = %q, %v", name, k, err)
|
|
}
|
|
}
|
|
if k, err := OwedKey("novox/mesh-sdk", 36, "aaaa", "status=mesh/merge-gate"); err != nil || k != "novox/mesh-sdk.36.aaaa.status=mesh/merge-gate" {
|
|
t.Errorf("owed key = %q, %v", k, err)
|
|
}
|
|
}
|
|
|
|
func TestKeysEscapeAndRoundTrip(t *testing.T) { // 1323/AC1
|
|
hh, _ := HeadsHash(gateHeads())
|
|
for _, name := range []string{"fix/1.2", "feat/a+b", "feat/a=b", "a.b+c=d", "fix/ü", "x y"} {
|
|
esc := EscapeName(name)
|
|
if strings.ContainsAny(esc, ".+ ") {
|
|
t.Errorf("escape of %q = %q holds a dot, plus or space", name, esc)
|
|
}
|
|
if back, err := UnescapeName(esc); err != nil || back != name {
|
|
t.Errorf("unescape(%q) = %q, %v; want %q", esc, back, err, name)
|
|
}
|
|
if k, err := MergesKey(name); err != nil || k != esc {
|
|
t.Errorf("merges key %q = %q, %v", name, k, err)
|
|
}
|
|
ck, err := CandidatesKey(name)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if g, err := ParseCandidatesKey(ck); err != nil || g != name {
|
|
t.Errorf("candidates round trip %q = %q, %v", name, g, err)
|
|
}
|
|
ak, err := MergeAskKey(name, gateHeads())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, g, h, err := ParseAskKey(ak); err != nil || g != name || h != hh {
|
|
t.Errorf("ask round trip %q = %q %q %v", name, g, h, err)
|
|
}
|
|
if AnswerKey(ak) != ak {
|
|
t.Error("answer key differs from the ask key")
|
|
}
|
|
// the ask and the answer of such a group encode and decode
|
|
ask := Ask{Group: name, Heads: gateHeads(), Ask: routerAskJSON(AskID(ak), "ok")}
|
|
if data, err := EncodeAsk(ask); err != nil {
|
|
t.Errorf("encode ask for %q: %v", name, err)
|
|
} else if _, err := DecodeAsk(data); err != nil {
|
|
t.Errorf("decode ask for %q: %v", name, err)
|
|
}
|
|
repo := "novox/" + name[strings.Index(name, "/")+1:]
|
|
if !strings.Contains(name, "/") {
|
|
repo = "novox/" + name
|
|
}
|
|
ok, err := OwedKey(repo, 3, "aaaa", "view")
|
|
if err != nil {
|
|
t.Errorf("owed key for repository %q: %v", repo, err)
|
|
continue
|
|
}
|
|
if r, n, h, a, err := ParseOwedKey(ok); err != nil || r != repo || n != 3 || h != "aaaa" || a != "view" {
|
|
t.Errorf("owed round trip %q = %q %d %q %q %v", repo, r, n, h, a, err)
|
|
}
|
|
// ADR 0548 §3: a repository's turn key is <owner>.<name>, lowercase, each part escaped.
|
|
tk, err := TurnsKey(repo)
|
|
if err != nil || strings.Count(tk, ".") != 1 {
|
|
t.Errorf("turns key for repository %q = %q, %v", repo, tk, err)
|
|
} else if r, err := ParseTurnsKey(tk); err != nil || r != strings.ToLower(repo) {
|
|
t.Errorf("turns round trip %q = %q, %v", repo, r, err)
|
|
}
|
|
}
|
|
if k, err := TurnsKey("novox/Mesh-SDK"); err != nil || k != "novox.mesh-sdk" {
|
|
t.Errorf("turns key = %q, %v; want novox.mesh-sdk", k, err)
|
|
}
|
|
for _, bad := range []string{"", "novox", "novox.", ".x", "a.b.c", "Novox.mesh-sdk", "novox.a=zz"} {
|
|
if _, err := ParseTurnsKey(bad); err == nil {
|
|
t.Errorf("parse turns key %q accepted", bad)
|
|
}
|
|
}
|
|
if EscapeName("a.b") != "a=2eb" || EscapeName("a=b") != "a=3db" || EscapeName("a+b") != "a=2bb" {
|
|
t.Errorf("escapes: %q %q %q", EscapeName("a.b"), EscapeName("a=b"), EscapeName("a+b"))
|
|
}
|
|
m := Merge{Group: "fix/1.2+x=y", Ask: "merge.x.0123456789ab", Pulls: []MergePull{{Head: gateHeads()[0], State: MergeMerging}}}
|
|
if data, err := EncodeMerge(m); err != nil {
|
|
t.Errorf("encode merge: %v", err)
|
|
} else if back, err := DecodeMerge(data); err != nil || back.Group != m.Group {
|
|
t.Errorf("decode merge: %v %v", back.Group, err)
|
|
}
|
|
}
|
|
|
|
func approvedMerge() Merge {
|
|
m := sampleMerge()
|
|
m.Approved = map[string]string{
|
|
ApprovedKey("novox/mesh-sdk", 36): "aaaa",
|
|
ApprovedKey("novox/mesh-control", 7): "bbbb",
|
|
ApprovedKey("novox/other", 1): "dddd",
|
|
}
|
|
return m
|
|
}
|
|
|
|
func TestMergeCarriesApprovedHeads(t *testing.T) { // 1321/AC1
|
|
m := approvedMerge()
|
|
m.Pulls = m.Pulls[:1] // two members approved besides the one handed so far
|
|
m.Pulls[0].State = MergeMerging
|
|
m.Pulls[0].Commit = ""
|
|
data, err := EncodeMerge(m)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
back, err := DecodeMerge(data)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m.V = Version
|
|
if !SameMerge(back, m) || len(back.Approved) != 3 || back.Approved["novox/mesh-control@7"] != "bbbb" {
|
|
t.Errorf("decoded %+v, want %+v", back, m)
|
|
}
|
|
}
|
|
|
|
func TestMergeRefusesAPullNotApproved(t *testing.T) { // 1321/AC2
|
|
m := approvedMerge()
|
|
delete(m.Approved, ApprovedKey("novox/other", 1))
|
|
if _, err := EncodeMerge(m); err == nil || !strings.Contains(err.Error(), "novox/other#1") {
|
|
t.Errorf("a handed pull missing from the approved heads: %v", err)
|
|
}
|
|
m = approvedMerge()
|
|
m.Approved[ApprovedKey("novox/other", 1)] = "eeee"
|
|
if _, err := EncodeMerge(m); err == nil {
|
|
t.Error("a handed pull at another head than the Approve's accepted")
|
|
}
|
|
m = approvedMerge()
|
|
m.Approved[ApprovedKey("novox/other", 1)] = ""
|
|
if _, err := EncodeMerge(m); err == nil {
|
|
t.Error("an empty approved head accepted")
|
|
}
|
|
data, _ := json.Marshal(approvedMerge())
|
|
var raw map[string]any
|
|
_ = json.Unmarshal(data, &raw)
|
|
raw["v"] = Version
|
|
raw["approved"] = map[string]string{"novox/mesh-sdk@36": "aaaa"}
|
|
bad, _ := json.Marshal(raw)
|
|
if _, err := DecodeMerge(bad); err == nil {
|
|
t.Error("decoding a merge whose pulls are not all approved accepted")
|
|
}
|
|
}
|
|
|
|
func TestMergeWithoutApprovedHeadsDecodes(t *testing.T) { // 1321/AC3
|
|
old := `{"v":` + strconv.Itoa(Version) + `,"group":"feat/x","ask":"merge.feat/x.0123456789ab","pulls":[{"repository":"novox/mesh-sdk","number":36,"head":"aaaa","state":"merging"}]}`
|
|
m, err := DecodeMerge([]byte(old))
|
|
if err != nil || len(m.Approved) != 0 || len(m.Pulls) != 1 {
|
|
t.Fatalf("old merge = %+v, %v", m, err)
|
|
}
|
|
data, err := EncodeMerge(sampleMerge())
|
|
if err != nil || strings.Contains(string(data), "approved") {
|
|
t.Errorf("a merge without approved heads wrote them: %s %v", data, err)
|
|
}
|
|
}
|
|
|
|
func TestOwedStatusCarriesItsTarget(t *testing.T) { // 1322/AC1
|
|
o := Owed{Act: OwedStatus, Context: "mesh/delivery", State: "success", Target: "https://mesh.example/delivery/36"}
|
|
data, err := EncodeOwed(o)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
back, err := DecodeOwed(data)
|
|
o.V = Version
|
|
if err != nil || !SameOwed(back, o) || back.Target != o.Target {
|
|
t.Errorf("decoded %+v, %v; want %+v", back, err, o)
|
|
}
|
|
if _, err := EncodeOwed(Owed{Act: OwedStatus, Context: "mesh/delivery", State: "success"}); err == nil || !strings.Contains(err.Error(), "target") {
|
|
t.Errorf("a status without a target: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestOwedKeyNumberZero(t *testing.T) { // 1322/AC2
|
|
key, err := OwedKey("novox/mesh-sdk", 0, "aaaa", "status=mesh/delivery")
|
|
if err != nil || key != "novox/mesh-sdk.0.aaaa.status=mesh/delivery" {
|
|
t.Fatalf("owed key for number 0 = %q, %v", key, err)
|
|
}
|
|
repo, number, head, act, err := ParseOwedKey(key)
|
|
if err != nil || repo != "novox/mesh-sdk" || number != 0 || head != "aaaa" || act != "status=mesh/delivery" {
|
|
t.Errorf("parse = %q %d %q %q %v", repo, number, head, act, err)
|
|
}
|
|
if _, err := OwedKey("novox/mesh-sdk", -1, "aaaa", "view"); err == nil {
|
|
t.Error("a negative number accepted by OwedKey")
|
|
}
|
|
if _, _, _, _, err := ParseOwedKey("novox/mesh-sdk.-1.aaaa.view"); err == nil {
|
|
t.Error("a negative number accepted by ParseOwedKey")
|
|
}
|
|
}
|
|
|
|
func TestOwedCommentsHaveASequence(t *testing.T) { // 1322/AC3
|
|
a1, err := OwedCommentAct(1)
|
|
a2, err2 := OwedCommentAct(2)
|
|
if err != nil || err2 != nil || a1 != "comment.1" || a2 != "comment.2" {
|
|
t.Fatalf("acts = %q %q %v %v", a1, a2, err, err2)
|
|
}
|
|
if _, err := OwedCommentAct(0); err == nil {
|
|
t.Error("comment sequence 0 accepted")
|
|
}
|
|
k1, err := OwedKey("novox/mesh-sdk", 36, "aaaa", a1)
|
|
k2, err2 := OwedKey("novox/mesh-sdk", 36, "aaaa", a2)
|
|
if err != nil || err2 != nil || k1 == k2 {
|
|
t.Fatalf("keys = %q %q %v %v", k1, k2, err, err2)
|
|
}
|
|
for key, seq := range map[string]string{k1: "1", k2: "2"} {
|
|
_, _, _, act, err := ParseOwedKey(key)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
kind, got, err := ParseOwedAct(act)
|
|
if err != nil || kind != OwedComment || got != seq {
|
|
t.Errorf("parse %q = %q %q %v; want comment %s", key, kind, got, err, seq)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAskingID(t *testing.T) { // 1370/AC1
|
|
k, _ := MergeAskKey("feat/x", gateHeads())
|
|
long, _ := MergeAskKey("feat/"+strings.Repeat("long-", 20), gateHeads())
|
|
for _, key := range []string{k, long} {
|
|
if AskingID(key, 1) != AskID(key) || AskingID(key, 0) != AskID(key) {
|
|
t.Errorf("the first asking's id is not AskID: %q %q %q", AskingID(key, 0), AskingID(key, 1), AskID(key))
|
|
}
|
|
seen := map[string]int{AskID(key): 1}
|
|
for n := 2; n <= 12; n++ {
|
|
id := AskingID(key, n)
|
|
if other, dup := seen[id]; dup {
|
|
t.Errorf("askings %d and %d give the id %q", n, other, id)
|
|
}
|
|
seen[id] = n
|
|
if len(id) > 64 || !routerUsableID(id) {
|
|
t.Errorf("id %q of asking %d is not usable by the router", id, n)
|
|
}
|
|
if id != AskID(key+"."+strconv.Itoa(n)) {
|
|
t.Errorf("id of asking %d is not the AskID form of key.%d: %q", n, n, id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAskChecksItsAsking(t *testing.T) { // 1370/AC2
|
|
key, _ := MergeAskKey("feat/x", gateHeads())
|
|
a := sampleAsk()
|
|
if _, err := EncodeAsk(a); err != nil {
|
|
t.Fatalf("asking 0: %v", err)
|
|
}
|
|
a.Asking = 1
|
|
if _, err := EncodeAsk(a); err != nil {
|
|
t.Fatalf("asking 1 under the first id: %v", err)
|
|
}
|
|
a.Asking = 3
|
|
a.Ask = routerAskJSON(AskingID(key, 3), "Merge into main: group feat/x")
|
|
data, err := EncodeAsk(a)
|
|
if err != nil {
|
|
t.Fatalf("asking 3 under its id: %v", err)
|
|
}
|
|
a.V = Version
|
|
if got, err := DecodeAsk(data); err != nil || !SameAsk(got, a) || got.Asking != 3 {
|
|
t.Errorf("asking round trip: %+v %v", got, err)
|
|
}
|
|
a.Asking = -1
|
|
if _, err := EncodeAsk(a); err == nil {
|
|
t.Error("a negative asking was accepted")
|
|
}
|
|
// the first asking is written without the field
|
|
data, _ = EncodeAsk(sampleAsk())
|
|
if strings.Contains(string(data), `"asking"`) {
|
|
t.Errorf("asking 0 is written: %s", data)
|
|
}
|
|
}
|