mesh-delivery-contracts: the client of mesh-delivery (heads hash, merge ask key, ask id, owed acts and keys, owed values) and the checker client's preview rules (D-27, ADR 0625 §4, ADR 0635 §5; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -6,6 +6,7 @@ package meshdeliverycheckerclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
pathpkg "path"
|
||||
"strings"
|
||||
|
||||
checker "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
|
||||
@@ -220,3 +221,60 @@ func CheckKept(k checker.KeptByLevelDigestValue) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckPreview refuses a preview a reader could not place (the checker writes it, the indexer reads it for the candidates'
|
||||
// fingerprints): a repository, head or trunk commit that does not key, a head in conflict that names no file or has a merge
|
||||
// commit or modules, a head that merged and names no merge commit, and a module with no directory, no tree, a manifest
|
||||
// digest that is not 64 lowercase hex digits, a lock without path or hash, or a named path that is not clean and inside the
|
||||
// module's directory or names no git object.
|
||||
func CheckPreview(p checker.PreviewsByRepositoryHeadTrunkValue) error {
|
||||
const who = "meshdeliverycheckerclient: "
|
||||
if err := CheckRepository(p.Repository); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := keys.CheckName("head", p.Head); err != nil {
|
||||
return fmt.Errorf(who+"the preview: %w", err)
|
||||
}
|
||||
if err := keys.CheckName("trunk commit", p.Trunk); err != nil {
|
||||
return fmt.Errorf(who+"the preview: %w", err)
|
||||
}
|
||||
for _, f := range p.Conflicts {
|
||||
if f == "" {
|
||||
return fmt.Errorf(who + "a preview names a conflicting file with an empty path")
|
||||
}
|
||||
}
|
||||
if len(p.Conflicts) > 0 {
|
||||
if p.Merge != "" || len(p.Modules) > 0 {
|
||||
return fmt.Errorf(who + "a preview of a head in conflict has no merge commit and no modules")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if p.Merge == "" {
|
||||
return fmt.Errorf(who + "a preview names no merge commit")
|
||||
}
|
||||
for dir, m := range p.Modules {
|
||||
if dir == "" {
|
||||
return fmt.Errorf(who + "a preview names a module with an empty directory")
|
||||
}
|
||||
if m.Tree == "" {
|
||||
return fmt.Errorf(who+"the preview's module %q has no tree", dir)
|
||||
}
|
||||
if !keys.IsHex64(m.Manifest) {
|
||||
return fmt.Errorf(who+"the preview's module %q: %q is not a manifest digest, 64 lowercase hex digits", dir, m.Manifest)
|
||||
}
|
||||
for path, hash := range m.Locks {
|
||||
if path == "" || hash == "" {
|
||||
return fmt.Errorf(who+"the lock %q of the preview's module %q has an empty path or hash", path, dir)
|
||||
}
|
||||
}
|
||||
for p, object := range m.Paths {
|
||||
if p == "" || p != pathpkg.Clean(p) || strings.HasPrefix(p, "/") || p == ".." || strings.HasPrefix(p, "../") {
|
||||
return fmt.Errorf(who+"the preview's module %q: the named path %q is not a clean path inside the module's directory", dir, p)
|
||||
}
|
||||
if object == "" {
|
||||
return fmt.Errorf(who+"the preview's module %q: the named path %q names no git object", dir, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ func TestAVerdictIsRefusedForWhatAReaderCouldNotPlace(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAKeptVerdictIsAPassOfAPackageAtALevel(t *testing.T) {
|
||||
t.Run("a preview is refused for what a reader of candidates could not place", previewIsRefused)
|
||||
ok := checker.KeptByLevelDigestValue{State: VerdictPass, Repository: "novox/mesh-sdk", Package: "./go/keys", Level: LevelUnit, Toolchain: "1.26.0", At: "2026-11-05T10:00:00Z"}
|
||||
if err := CheckKept(ok); err != nil {
|
||||
t.Fatalf("a kept pass is refused: %v", err)
|
||||
@@ -79,3 +80,48 @@ func TestAKeptVerdictIsAPassOfAPackageAtALevel(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A preview is refused for what a reader of candidates could not place.
|
||||
func previewIsRefused(t *testing.T) {
|
||||
good := func() checker.PreviewsByRepositoryHeadTrunkValue {
|
||||
return checker.PreviewsByRepositoryHeadTrunkValue{Repository: "novox/a", Head: "h1", Trunk: "t1", Merge: "m1",
|
||||
Modules: map[string]checker.PreviewsByRepositoryHeadTrunkValueModulesValue{"mod": {Tree: "tr", Manifest: strings.Repeat("a", 64),
|
||||
Locks: map[string]string{"go.sum": "x"}, Paths: map[string]string{"src": "o1"}}}}
|
||||
}
|
||||
if err := CheckPreview(good()); err != nil {
|
||||
t.Fatalf("a good preview is refused: %v", err)
|
||||
}
|
||||
for name, c := range map[string]struct {
|
||||
edit func(*checker.PreviewsByRepositoryHeadTrunkValue)
|
||||
says string
|
||||
}{
|
||||
"a repository with no owner": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) { p.Repository = "a" }, "<owner>/<name>"},
|
||||
"a head with a dot": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) { p.Head = "h.1" }, "head"},
|
||||
"a conflict with a merge": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) { p.Conflicts = []string{"f"} }, "in conflict"},
|
||||
"no merge commit": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) { p.Merge = "" }, "no merge commit"},
|
||||
"a module with no tree": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) {
|
||||
m := p.Modules["mod"]
|
||||
m.Tree = ""
|
||||
p.Modules["mod"] = m
|
||||
}, "no tree"},
|
||||
"a short manifest digest": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) {
|
||||
m := p.Modules["mod"]
|
||||
m.Manifest = "abc"
|
||||
p.Modules["mod"] = m
|
||||
}, "manifest digest"},
|
||||
"a path outside the module": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) {
|
||||
m := p.Modules["mod"]
|
||||
m.Paths = map[string]string{"../x": "o"}
|
||||
p.Modules["mod"] = m
|
||||
}, "clean path"},
|
||||
} {
|
||||
p := good()
|
||||
c.edit(&p)
|
||||
if err := CheckPreview(p); err == nil || !strings.Contains(err.Error(), c.says) {
|
||||
t.Errorf("%s: CheckPreview = %v, want it to say %q", name, err, c.says)
|
||||
}
|
||||
}
|
||||
if err := CheckPreview(checker.PreviewsByRepositoryHeadTrunkValue{Repository: "novox/a", Head: "h", Trunk: "t", Conflicts: []string{"f"}}); err != nil {
|
||||
t.Errorf("a head in conflict is refused: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
// Package meshdeliveryclient holds the logic the holders and callers of the seat mesh-delivery share over its contract
|
||||
// (novox/hq ADR 0625 §4, ADR 0634 §2, ADR 0635 §5): the keys of its states `merges`, `asks` and `owed` and the rules their
|
||||
// values keep, the id the teller gives the router for an ask, and the act an ask's options bind. The gate writes them, the
|
||||
// forge bridge, the teller and the assigner read them; the contract's decoding is strict by schema only, so these rules are
|
||||
// written once here and no caller copies them.
|
||||
package meshdeliveryclient
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
)
|
||||
|
||||
// AskKindMerge is the kind of the gate's ask for a group's merge, the first part of its key.
|
||||
const AskKindMerge = "merge"
|
||||
|
||||
// VerbMerge is the act an ask for a group's merge binds in its options, `mesh-delivery.merge` (Approve).
|
||||
const VerbMerge = "merge"
|
||||
|
||||
// Head is one pull request at one commit: the repository as `<owner>/<name>` and the full sha.
|
||||
type Head struct {
|
||||
Repository string `json:"repository"`
|
||||
Number int `json:"number"`
|
||||
Head string `json:"head"`
|
||||
}
|
||||
|
||||
// HeadsHash is the first 12 hex digits of the sha256 over the lines `<repository>#<number>=<head>`, sorted and joined by a
|
||||
// newline: the same for every order of heads, different when any head changes. An empty list is refused.
|
||||
func HeadsHash(heads []Head) (string, error) {
|
||||
if len(heads) == 0 {
|
||||
return "", fmt.Errorf("meshdeliveryclient: a heads hash needs at least one pull request")
|
||||
}
|
||||
lines := make([]string, len(heads))
|
||||
for i, h := range heads {
|
||||
lines[i] = fmt.Sprintf("%s#%d=%s", h.Repository, h.Number, h.Head)
|
||||
}
|
||||
sort.Strings(lines)
|
||||
sum := sha256.Sum256([]byte(strings.Join(lines, "\n")))
|
||||
return hex.EncodeToString(sum[:])[:12], nil
|
||||
}
|
||||
|
||||
var hashOK = regexp.MustCompile(`^[0-9a-f]{12}$`)
|
||||
|
||||
func groupKey(group string) (string, error) {
|
||||
if group == "" {
|
||||
return "", fmt.Errorf("meshdeliveryclient: the group name is empty")
|
||||
}
|
||||
return keys.EscapeName(group), nil
|
||||
}
|
||||
|
||||
// MergesKey is the key, in the state merges, of a group: `<group>`, escaped so a branch such as `fix/1.2` keys.
|
||||
func MergesKey(group string) (string, error) { return groupKey(group) }
|
||||
|
||||
// MergeAskKey is the key, in the state asks, of the ask for merging a group at these heads: `merge.<group>.<heads hash>`.
|
||||
// The teller's answer has the same key.
|
||||
func MergeAskKey(group string, heads []Head) (string, error) {
|
||||
part, err := groupKey(group)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
hash, err := HeadsHash(heads)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return AskKindMerge + "." + part + "." + hash, nil
|
||||
}
|
||||
|
||||
// ParseAskKey is the inverse of MergeAskKey: the kind, the group and the heads hash.
|
||||
func ParseAskKey(key string) (kind, group, hash string, err error) {
|
||||
parts := strings.Split(key, ".")
|
||||
if len(parts) != 3 || parts[0] != AskKindMerge {
|
||||
return "", "", "", fmt.Errorf("meshdeliveryclient: %q is not an ask key, merge.<group>.<heads hash>", key)
|
||||
}
|
||||
if parts[1] == "" {
|
||||
return "", "", "", fmt.Errorf("meshdeliveryclient: %q is not an ask key: the group name is empty", key)
|
||||
}
|
||||
if group, err = keys.UnescapeName(parts[1]); err != nil {
|
||||
return "", "", "", fmt.Errorf("meshdeliveryclient: %q is not an ask key: %w", key, err)
|
||||
}
|
||||
if !hashOK.MatchString(parts[2]) {
|
||||
return "", "", "", fmt.Errorf("meshdeliveryclient: %q is not an ask key: %q is not 12 hex digits", key, parts[2])
|
||||
}
|
||||
return parts[0], group, parts[2], nil
|
||||
}
|
||||
|
||||
// SettleGroupPrefix begins the group of a settle ask (novox/hq ADR 0556 clause 5): a person's ask to settle deliveries held
|
||||
// because their merge reached the trunk without a passing check. It is kept in the asks under MergeAskKey like a merge ask,
|
||||
// so the teller carries it unchanged. Git forbids a colon in a branch name, so no pull request's group ever begins with it.
|
||||
const SettleGroupPrefix = "settle:"
|
||||
|
||||
// SettleGroup is the group of the settle ask of these delivery ids: the prefix and twelve hex digits of the sha256 over the
|
||||
// ids, sorted and joined by a newline, so the same deliveries asked again keep one key.
|
||||
func SettleGroup(ids []string) string {
|
||||
sorted := append([]string(nil), ids...)
|
||||
sort.Strings(sorted)
|
||||
sum := sha256.Sum256([]byte(strings.Join(sorted, "\n")))
|
||||
return SettleGroupPrefix + hex.EncodeToString(sum[:])[:12]
|
||||
}
|
||||
|
||||
// AskVerb is the act the options of an ask bind, by its group: a settle ask's `mesh-delivery.settle`, any other
|
||||
// `mesh-delivery.merge`. The gate composes the act with it and the teller checks a warrant against it.
|
||||
func AskVerb(group string) string {
|
||||
if strings.HasPrefix(group, SettleGroupPrefix) {
|
||||
return seat.Seat + "." + seat.VerbSettle
|
||||
}
|
||||
return seat.Seat + "." + VerbMerge
|
||||
}
|
||||
|
||||
// AskID is the id the teller gives the router for the ask at askKey. The router's ids refuse dots, so in the readable part a
|
||||
// dot becomes `-` and any other byte outside letters, digits, `_` and `-` becomes `_`; that part is cut to fit, and every id
|
||||
// ends in `-` and the first 12 hex digits of the sha256 of the whole key, so two keys never give one id. It cannot be read
|
||||
// back: the teller keeps the key beside it.
|
||||
func AskID(askKey string) string {
|
||||
const most = 64 // the router's bound on an id
|
||||
const tail = 13 // "-" and 12 hex digits
|
||||
readable := []byte(askKey)
|
||||
for i, c := range readable {
|
||||
switch {
|
||||
case c >= 'A' && c <= 'Z', c >= 'a' && c <= 'z', c >= '0' && c <= '9':
|
||||
case c == '.':
|
||||
readable[i] = '-'
|
||||
default: // a slash, an equals sign or any other byte the router's id refuses
|
||||
readable[i] = '_'
|
||||
}
|
||||
}
|
||||
if len(readable) > 0 && (readable[0] == '-' || readable[0] == '_') {
|
||||
readable[0] = 'k' // an id starts with a letter or digit
|
||||
}
|
||||
if len(readable) > most-tail {
|
||||
readable = readable[:most-tail]
|
||||
}
|
||||
sum := sha256.Sum256([]byte(askKey))
|
||||
return string(readable) + "-" + hex.EncodeToString(sum[:])[:12]
|
||||
}
|
||||
|
||||
// AskingID is the id of the asking-th writing of the ask at askKey. Asking 0 and 1 give AskID(askKey), so the first asking's
|
||||
// id is unchanged; asking n of 2 or more gives the AskID form of askKey + "." + n, so each asking has an id of its own and the
|
||||
// router, which drops an ask under an id it keeps as a redelivery, takes each as new and an answer to an earlier asking is
|
||||
// never taken for a later one.
|
||||
func AskingID(askKey string, asking int) string {
|
||||
if asking < 2 {
|
||||
return AskID(askKey)
|
||||
}
|
||||
return AskID(askKey + "." + strconv.Itoa(asking))
|
||||
}
|
||||
|
||||
// The acts the gate owes the forge, the last part of an owed key.
|
||||
const (
|
||||
OwedStatus = "status"
|
||||
OwedView = "view"
|
||||
OwedNote = "note"
|
||||
OwedComment = "comment"
|
||||
)
|
||||
|
||||
// The status contexts the mesh owes the forge.
|
||||
const (
|
||||
StatusContextMergeGate = "mesh/merge-gate"
|
||||
StatusContextDelivery = "mesh/delivery"
|
||||
StatusContextDeliveryGroup = "mesh/delivery-group"
|
||||
StatusContextRepoCheck = "mesh/repo-check"
|
||||
StatusContextCheck = "mesh/check"
|
||||
)
|
||||
|
||||
var statusContexts = []string{StatusContextMergeGate, StatusContextDelivery, StatusContextDeliveryGroup, StatusContextRepoCheck, StatusContextCheck}
|
||||
|
||||
// StatusContexts is the contexts, in a new slice.
|
||||
func StatusContexts() []string { return append([]string(nil), statusContexts...) }
|
||||
|
||||
// The states a status the gate owes the forge may carry.
|
||||
var statusStates = []string{"pending", "success", "failure", "error", "warning"}
|
||||
|
||||
func checkStatusContext(context string) error {
|
||||
for _, c := range statusContexts {
|
||||
if context == c {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("meshdeliveryclient: the status context %q is not one of %s", context, strings.Join(statusContexts, ", "))
|
||||
}
|
||||
|
||||
// ParseOwedAct splits an owed act into its kind and, for a status, its context, for a comment its sequence as digits. The act
|
||||
// is `status=<context>`, `view`, `note` or `comment.<seq>`.
|
||||
func ParseOwedAct(act string) (kind, context string, err error) {
|
||||
kind, context, hasContext := strings.Cut(act, "=")
|
||||
switch kind {
|
||||
case OwedStatus:
|
||||
if !hasContext {
|
||||
return "", "", fmt.Errorf("meshdeliveryclient: the owed act %q is a status without its context, status=<context>", act)
|
||||
}
|
||||
if err := checkStatusContext(context); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return kind, context, nil
|
||||
case OwedView, OwedNote:
|
||||
if hasContext {
|
||||
return "", "", fmt.Errorf("meshdeliveryclient: the owed act %q takes no context", act)
|
||||
}
|
||||
return kind, "", nil
|
||||
}
|
||||
if kind, seq, ok := strings.Cut(act, "."); ok && kind == OwedComment {
|
||||
if n, err := strconv.Atoi(seq); err != nil || n < 1 || strconv.Itoa(n) != seq {
|
||||
return "", "", fmt.Errorf("meshdeliveryclient: the owed act %q is a comment without its sequence, comment.<seq> with seq of 1 or more", act)
|
||||
}
|
||||
return OwedComment, seq, nil
|
||||
}
|
||||
return "", "", fmt.Errorf("meshdeliveryclient: the owed act %q is not status=<context>, view, note or comment.<seq>", act)
|
||||
}
|
||||
|
||||
// OwedCommentAct is the act of the seq-th comment on a head: `comment.<seq>`, seq of 1 or more, so a second comment has a key
|
||||
// of its own and does not overwrite the first.
|
||||
func OwedCommentAct(seq int) (string, error) {
|
||||
if seq < 1 {
|
||||
return "", fmt.Errorf("meshdeliveryclient: a comment's sequence is 1 or more, not %d", seq)
|
||||
}
|
||||
return OwedComment + "." + strconv.Itoa(seq), nil
|
||||
}
|
||||
|
||||
func escapeRepository(repository string) (string, error) {
|
||||
owner, name, ok := strings.Cut(repository, "/")
|
||||
if !ok || owner == "" || name == "" || strings.Contains(name, "/") {
|
||||
return "", fmt.Errorf("meshdeliveryclient: the repository %q is not <owner>/<name>, with exactly one slash", repository)
|
||||
}
|
||||
return keys.EscapeName(owner) + "/" + keys.EscapeName(name), nil
|
||||
}
|
||||
|
||||
func unescapeRepository(part string) (string, error) {
|
||||
owner, name, ok := strings.Cut(part, "/")
|
||||
if !ok || owner == "" || name == "" || strings.Contains(name, "/") {
|
||||
return "", fmt.Errorf("meshdeliveryclient: the repository %q is not <owner>/<name>, with exactly one slash", part)
|
||||
}
|
||||
o, err := keys.UnescapeName(owner)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
n, err := keys.UnescapeName(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return o + "/" + n, nil
|
||||
}
|
||||
|
||||
// OwedKey is the key, in the state owed, of an act the gate owes the forge on a pull request at a head:
|
||||
// `<repository>.<number>.<head>.<act>`. The number is 0 for a commit no pull request names, and never negative.
|
||||
func OwedKey(repository string, number int, head, act string) (string, error) {
|
||||
repo, err := escapeRepository(repository)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if number < 0 {
|
||||
return "", fmt.Errorf("meshdeliveryclient: an owed key's pull request number is 0 (none) or more, not %d", number)
|
||||
}
|
||||
if err := keys.CheckName("head", head); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, _, err := ParseOwedAct(act); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return repo + "." + strconv.Itoa(number) + "." + head + "." + act, nil
|
||||
}
|
||||
|
||||
// ParseOwedKey is the inverse of OwedKey; act is whole, as `status=<context>` for a status.
|
||||
func ParseOwedKey(key string) (repository string, number int, head, act string, err error) {
|
||||
parts := strings.SplitN(key, ".", 4) // a comment's act holds a dot
|
||||
if len(parts) != 4 {
|
||||
return "", 0, "", "", fmt.Errorf("meshdeliveryclient: %q is not an owed key, <repository>.<number>.<head>.<act>", key)
|
||||
}
|
||||
number, convErr := strconv.Atoi(parts[1])
|
||||
if convErr != nil {
|
||||
return "", 0, "", "", fmt.Errorf("meshdeliveryclient: %q is not an owed key: %q is not a pull request number", key, parts[1])
|
||||
}
|
||||
if repository, err = unescapeRepository(parts[0]); err != nil {
|
||||
return "", 0, "", "", fmt.Errorf("meshdeliveryclient: %q is not an owed key: %w", key, err)
|
||||
}
|
||||
if _, err := OwedKey(repository, number, parts[2], parts[3]); err != nil {
|
||||
return "", 0, "", "", fmt.Errorf("meshdeliveryclient: %q is not an owed key: %w", key, err)
|
||||
}
|
||||
return repository, number, parts[2], parts[3], nil
|
||||
}
|
||||
|
||||
// CheckOwed refuses an owed value the forge bridge could not perform: a status needs its context (one the mesh owes), a
|
||||
// state of the list and a target; a view and a comment need text; a note needs text and a ref.
|
||||
func CheckOwed(o seat.OwedByRepositoryNumberHeadActValue) error {
|
||||
const who = "meshdeliveryclient: "
|
||||
switch o.Act {
|
||||
case OwedStatus:
|
||||
if o.Context == "" {
|
||||
return fmt.Errorf(who + "an owed status has no context")
|
||||
}
|
||||
if err := checkStatusContext(o.Context); err != nil {
|
||||
return err
|
||||
}
|
||||
if o.Target == "" {
|
||||
return fmt.Errorf(who + "an owed status has no target")
|
||||
}
|
||||
for _, s := range statusStates {
|
||||
if o.State == s {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf(who+"the owed status state %q is not %s", o.State, strings.Join(statusStates, ", "))
|
||||
case OwedView, OwedComment:
|
||||
if o.Text == "" {
|
||||
return fmt.Errorf(who+"an owed %s has no text", o.Act)
|
||||
}
|
||||
case OwedNote:
|
||||
if o.Text == "" {
|
||||
return fmt.Errorf(who + "an owed note has no text")
|
||||
}
|
||||
if o.Ref == "" {
|
||||
return fmt.Errorf(who + "an owed note has no ref")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf(who+"the owed act %q is not status, view, note or comment", o.Act)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package meshdeliveryclient
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
|
||||
)
|
||||
|
||||
// The keys, ids and acts equal what the gate and the teller have always written (the expected strings are the trunk's own
|
||||
// output for the same input), so a value written before is read after.
|
||||
func TestTheAsksAndOwedActsKeepTheirFormsAndRules(t *testing.T) {
|
||||
heads := []Head{{Repository: "novox/a", Number: 3, Head: "abc"}, {Repository: "novox/b", Number: 1, Head: "def"}}
|
||||
key, err := MergeAskKey("fix/1.2", heads)
|
||||
if err != nil || key != "merge.fix/1=2e2.d106c7950d28" {
|
||||
t.Fatalf("MergeAskKey = %q, %v", key, err)
|
||||
}
|
||||
if rev, _ := MergeAskKey("fix/1.2", []Head{heads[1], heads[0]}); rev != key {
|
||||
t.Errorf("the heads' order changes the key: %q", rev)
|
||||
}
|
||||
if kind, group, hash, err := ParseAskKey(key); err != nil || kind != "merge" || group != "fix/1.2" || hash != "d106c7950d28" {
|
||||
t.Errorf("ParseAskKey = %q %q %q %v", kind, group, hash, err)
|
||||
}
|
||||
for _, bad := range []string{"merge.g", "settle.g.d106c7950d28", "merge..d106c7950d28", "merge.g.D106C7950D28", "merge.g.d106"} {
|
||||
if _, _, _, err := ParseAskKey(bad); err == nil {
|
||||
t.Errorf("ParseAskKey(%q) reads a wrong key", bad)
|
||||
}
|
||||
}
|
||||
if _, err := MergeAskKey("", heads); err == nil {
|
||||
t.Error("an empty group keys")
|
||||
}
|
||||
if _, err := MergeAskKey("g", nil); err == nil {
|
||||
t.Error("no heads keys")
|
||||
}
|
||||
if id := AskID(key); id != "merge-fix_1_2e2-d106c7950d28-400030402c88" || AskingID(key, 1) != id || AskingID(key, 0) != id {
|
||||
t.Errorf("AskID = %q", id)
|
||||
}
|
||||
if id := AskingID(key, 3); id != "merge-fix_1_2e2-d106c7950d28-3-ea06879e48d8" || len(id) > 64 {
|
||||
t.Errorf("AskingID(3) = %q", id)
|
||||
}
|
||||
if g := SettleGroup([]string{"b", "a"}); g != "settle:7e18f737311b" || g != SettleGroup([]string{"a", "b"}) {
|
||||
t.Errorf("SettleGroup = %q", g)
|
||||
}
|
||||
if AskVerb(SettleGroup([]string{"a"})) != "mesh-delivery.settle" || AskVerb("fix/1") != "mesh-delivery.merge" {
|
||||
t.Error("AskVerb binds the wrong act")
|
||||
}
|
||||
|
||||
for act, want := range map[string]string{"status=mesh/check": "novox/a.3.abc.status=mesh/check", "view": "novox/a.3.abc.view", "comment.2": "novox/a.3.abc.comment.2"} {
|
||||
k, err := OwedKey("novox/a", 3, "abc", act)
|
||||
if err != nil || k != want {
|
||||
t.Errorf("OwedKey(%q) = %q, %v", act, k, err)
|
||||
}
|
||||
if repo, n, head, a, err := ParseOwedKey(k); err != nil || repo != "novox/a" || n != 3 || head != "abc" || a != act {
|
||||
t.Errorf("ParseOwedKey(%q) = %q %d %q %q %v", k, repo, n, head, a, err)
|
||||
}
|
||||
}
|
||||
for _, act := range []string{"status", "status=mesh/other", "view=x", "comment", "comment.0", "comment.01", "other"} {
|
||||
if _, err := OwedKey("novox/a", 3, "abc", act); err == nil {
|
||||
t.Errorf("OwedKey takes the act %q", act)
|
||||
}
|
||||
}
|
||||
if _, err := OwedKey("novox", 0, "abc", "view"); err == nil {
|
||||
t.Error("a repository with no slash keys")
|
||||
}
|
||||
if _, err := OwedKey("novox/a", -1, "abc", "view"); err == nil {
|
||||
t.Error("a negative number keys")
|
||||
}
|
||||
if act, err := OwedCommentAct(2); err != nil || act != "comment.2" {
|
||||
t.Errorf("OwedCommentAct = %q, %v", act, err)
|
||||
}
|
||||
|
||||
type owed = seat.OwedByRepositoryNumberHeadActValue
|
||||
for name, c := range map[string]struct {
|
||||
v owed
|
||||
says string
|
||||
}{
|
||||
"a good status": {owed{Act: "status", Context: StatusContextCheck, Target: "http://x", State: "success"}, ""},
|
||||
"a good note": {owed{Act: "note", Text: "t", Ref: "mesh-plan"}, ""},
|
||||
"a status no target": {owed{Act: "status", Context: StatusContextCheck, State: "success"}, "no target"},
|
||||
"a status wrong state": {owed{Act: "status", Context: StatusContextCheck, Target: "x", State: "done"}, "state"},
|
||||
"a status no context": {owed{Act: "status", Target: "x", State: "success"}, "no context"},
|
||||
"a view no text": {owed{Act: "view"}, "no text"},
|
||||
"a note no ref": {owed{Act: "note", Text: "t"}, "no ref"},
|
||||
"an unknown act": {owed{Act: "merge"}, "not status"},
|
||||
} {
|
||||
err := CheckOwed(c.v)
|
||||
if (c.says == "") != (err == nil) || (err != nil && !strings.Contains(err.Error(), c.says)) {
|
||||
t.Errorf("%s: CheckOwed = %v, want %q", name, err, c.says)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
module git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery v0.0.0
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.1 // indirect
|
||||
github.com/nats-io/nats.go v1.54.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a h1:E9ERPqR4uZXypudHGxv2/wNIOYcder9torVqJwJq/DI=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-20261105000000-000000000000 h1:BTqpa7dCCTQH9m08Jle8KVFwyIHiHPSUbwWxuqSg2yA=
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-20261105000000-000000000000/go.mod h1:pPxwhTMb3hIk7UUrBicJnGQCqfTEoBGJvNFspEK2U18=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef h1:/Jic5ho0xz32Z9DH2NSUqVxCF1KyfIOWuRTrX1aLjus=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef/go.mod h1:2wHG7ntLCG8Tpm/SCfgp524a5emUJm1gAC4KrRYsU9s=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
|
||||
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
||||
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
@@ -121,6 +121,15 @@
|
||||
],
|
||||
"series": "0.1"
|
||||
},
|
||||
{
|
||||
"name": "go-meshdeliveryclient",
|
||||
"kind": "package",
|
||||
"language": "go",
|
||||
"paths": [
|
||||
"go/clients/meshdeliveryclient"
|
||||
],
|
||||
"series": "0.1"
|
||||
},
|
||||
{
|
||||
"name": "go-meshdeliverycheckerclient",
|
||||
"kind": "package",
|
||||
|
||||
Reference in New Issue
Block a user