From b79b94b19de121a2502aec1a3fe9c276f1abf77f Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 16:42:19 +0100 Subject: [PATCH] =?UTF-8?q?Clients:=20the=20kept=20and=20preview=20keys=20?= =?UTF-8?q?of=20the=20checker's=20states=20and=20the=20candidates=20key=20?= =?UTF-8?q?of=20the=20catalog's=20(ADR=200634=20=C2=A72;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../go/clients/meshcatalogclient/candidate.go | 24 +++++++ .../clients/meshcatalogclient/catalog_test.go | 16 +++++ .../meshdeliverycheckerclient/client_test.go | 29 +++++++++ .../meshdeliverycheckerclient/codec.go | 63 +++++++++++++++++++ 4 files changed, 132 insertions(+) diff --git a/contracts/go/clients/meshcatalogclient/candidate.go b/contracts/go/clients/meshcatalogclient/candidate.go index 3c0f181c..31a5db92 100644 --- a/contracts/go/clients/meshcatalogclient/candidate.go +++ b/contracts/go/clients/meshcatalogclient/candidate.go @@ -3,10 +3,12 @@ package meshcatalogclient import ( "crypto/sha256" "encoding/hex" + "fmt" "sort" "strings" seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-sdk/go/keys" ) // FingerprintSet is the `fpset` of a candidate's pull requests: the first 12 hex digits of the sha256 over the lines @@ -36,3 +38,25 @@ func FingerprintSet(pulls []seat.CandidatesByGroupValuePullsItem) string { sum := sha256.Sum256([]byte(strings.Join(all, "\n"))) return hex.EncodeToString(sum[:])[:12] } + +// CandidatesKey is the key, in the state candidates, of a group: `` escaped (keys.EscapeName), the branch name the +// group's pull requests share (a slash is allowed); a lone pull request is a group of one under its branch name. +func CandidatesKey(group string) (string, error) { + if group == "" { + return "", fmt.Errorf("meshcatalogclient: the group name is empty") + } + return seat.CandidatesByGroupKey(keys.EscapeName(group)), nil +} + +// ParseCandidatesKey is the inverse of CandidatesKey. +func ParseCandidatesKey(key string) (string, error) { + part, ok := seat.ParseCandidatesByGroupKey(key) + if !ok { + return "", fmt.Errorf("meshcatalogclient: %q is not a candidates key", key) + } + group, err := keys.UnescapeName(part) + if err != nil { + return "", fmt.Errorf("meshcatalogclient: %q is not a candidates key: %w", key, err) + } + return group, nil +} diff --git a/contracts/go/clients/meshcatalogclient/catalog_test.go b/contracts/go/clients/meshcatalogclient/catalog_test.go index 91a4e3ef..d8a60d74 100644 --- a/contracts/go/clients/meshcatalogclient/catalog_test.go +++ b/contracts/go/clients/meshcatalogclient/catalog_test.go @@ -156,3 +156,19 @@ func TestVerdictsAreTakenAsTheReadersTookThem(t *testing.T) { t.Error("ModulesKey accepts a dotted name") } } + +func TestCandidatesKeyEscapesTheGroupAndReadsBack(t *testing.T) { + key, err := CandidatesKey("fix/1.2") + if err != nil { + t.Fatal(err) + } + if key == "fix/1.2" { + t.Errorf("the group is not escaped: %q", key) + } + if group, err := ParseCandidatesKey(key); err != nil || group != "fix/1.2" { + t.Errorf("read back = %q, %v", group, err) + } + if _, err := CandidatesKey(""); err == nil { + t.Error("an empty group keys") + } +} diff --git a/contracts/go/clients/meshdeliverycheckerclient/client_test.go b/contracts/go/clients/meshdeliverycheckerclient/client_test.go index 1a8334c6..d1e1a99f 100644 --- a/contracts/go/clients/meshdeliverycheckerclient/client_test.go +++ b/contracts/go/clients/meshdeliverycheckerclient/client_test.go @@ -192,3 +192,32 @@ func TestTheCodecsWriteAtTheVersionAndReadBackThroughTheRules(t *testing.T) { t.Error("a preview with no merge commit and no conflict is written") } } + +// The kept and preview keys: a key reads back to its parts, and a part that does not key is refused. +func TestKeptAndPreviewKeysReadBack(t *testing.T) { + digest := strings.Repeat("ab", 32) + key, err := KeptKey(LevelUnit, digest) + if err != nil || key != "unit."+digest { + t.Fatalf("kept key = %q, %v", key, err) + } + if level, d, err := ParseKeptKey(key); err != nil || level != LevelUnit || d != digest { + t.Errorf("kept key read back = %q %q, %v", level, d, err) + } + for _, bad := range []string{"fast." + digest, "unit.XY", "unit"} { + if _, _, err := ParseKeptKey(bad); err == nil { + t.Errorf("kept key %q is read", bad) + } + } + key, err = PreviewKey("novox/mesh-sdk", "h1", "t1") + if err != nil { + t.Fatal(err) + } + if repo, h, tr, err := ParsePreviewKey(key); err != nil || repo != "novox/mesh-sdk" || h != "h1" || tr != "t1" { + t.Errorf("preview key %q read back = %q %q %q, %v", key, repo, h, tr, err) + } + for _, bad := range []string{"novox.h1.t1", "a/b.h1", "a/b.h1.t1.x"} { + if _, _, _, err := ParsePreviewKey(bad); err == nil { + t.Errorf("preview key %q is read", bad) + } + } +} diff --git a/contracts/go/clients/meshdeliverycheckerclient/codec.go b/contracts/go/clients/meshdeliverycheckerclient/codec.go index 2b6b2b0c..c684e73d 100644 --- a/contracts/go/clients/meshdeliverycheckerclient/codec.go +++ b/contracts/go/clients/meshdeliverycheckerclient/codec.go @@ -3,6 +3,7 @@ package meshdeliverycheckerclient import ( "encoding/json" "fmt" + "strings" checker "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" "git.novox.be/novox/mesh-sdk/go/keys" @@ -85,3 +86,65 @@ func decode(data []byte, into any) error { } return nil } + +// KeptKey is the key, in the state kept, of a kept verdict: `.`, the level unit or integration and the digest, +// lowercase hex, of everything that package's tests read at that level as the language's build module names it. +func KeptKey(level, digest string) (string, error) { + if err := checkLevel(level); err != nil { + return "", err + } + if digest == "" || strings.Trim(digest, "0123456789abcdef") != "" { + return "", fmt.Errorf("meshdeliverycheckerclient: %q is not a digest, lowercase hex", digest) + } + return checker.KeptByLevelDigestKey(level, digest), nil +} + +// ParseKeptKey is the inverse of KeptKey. +func ParseKeptKey(key string) (level, digest string, err error) { + level, digest, ok := checker.ParseKeptByLevelDigestKey(key) + if !ok { + return "", "", fmt.Errorf("meshdeliverycheckerclient: %q is not a kept key, .", key) + } + if _, err = KeptKey(level, digest); err != nil { + return "", "", fmt.Errorf("meshdeliverycheckerclient: %q is not a kept key: %w", key, err) + } + return level, digest, nil +} + +// PreviewKey is the key, in the state previews, of a head merged onto a trunk commit: `..`, the +// repository's owner and name each escaped (keys.EscapeName). +func PreviewKey(repository, head, trunk string) (string, error) { + if err := CheckRepository(repository); err != nil { + return "", err + } + if err := keys.CheckName("head", head); err != nil { + return "", err + } + if err := keys.CheckName("trunk commit", trunk); err != nil { + return "", err + } + owner, name, _ := strings.Cut(repository, "/") + return checker.PreviewsByRepositoryHeadTrunkKey(keys.EscapeName(owner)+"/"+keys.EscapeName(name), head, trunk), nil +} + +// ParsePreviewKey is the inverse of PreviewKey. +func ParsePreviewKey(key string) (repository, head, trunk string, err error) { + repo, head, trunk, ok := checker.ParsePreviewsByRepositoryHeadTrunkKey(key) + if !ok || strings.Contains(trunk, ".") { + return "", "", "", fmt.Errorf("meshdeliverycheckerclient: %q is not a preview key, ..", key) + } + owner, name, found := strings.Cut(repo, "/") + if !found { + return "", "", "", fmt.Errorf("meshdeliverycheckerclient: %q is not a preview key: the repository has no slash", key) + } + owner, err1 := keys.UnescapeName(owner) + name, err2 := keys.UnescapeName(name) + if err1 != nil || err2 != nil { + return "", "", "", fmt.Errorf("meshdeliverycheckerclient: %q is not a preview key: the repository does not unescape", key) + } + repository = owner + "/" + name + if _, err := PreviewKey(repository, head, trunk); err != nil { + return "", "", "", fmt.Errorf("meshdeliverycheckerclient: %q is not a preview key: %w", key, err) + } + return repository, head, trunk, nil +}