diff --git a/contracts/go/clients/meshdeliverycheckerclient/client.go b/contracts/go/clients/meshdeliverycheckerclient/client.go new file mode 100644 index 00000000..c97e6ac3 --- /dev/null +++ b/contracts/go/clients/meshdeliverycheckerclient/client.go @@ -0,0 +1,222 @@ +// Package meshdeliverycheckerclient holds the logic the holders and callers of the seat mesh-delivery-checker share over its +// contract (novox/hq ADR 0625 §4, ADR 0634 §2): the rules a verdict and a kept verdict must keep to be written or read. The +// checker writes them, and the view, the gate and the indexer read them; the contract's decoding is strict by schema only, so +// these rules are written once here and no caller copies them. +package meshdeliverycheckerclient + +import ( + "fmt" + "strings" + + checker "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" + "git.novox.be/novox/mesh-delivery/sdk/kept" +) + +// The states of a verdict, the levels of a check and the compositions' states. +const ( + VerdictRunning = "running" + VerdictPass = "pass" + VerdictFail = "fail" + + LevelUnit = "unit" + LevelIntegration = "integration" + + CompositionNotRun = "not-run" + CompositionPass = "pass" + CompositionFail = "fail" +) + +// MaxFailures is the most failures a verdict names; it names the count of the rest beside them. +const MaxFailures = 50 + +func checkLevel(level string) error { + if level != LevelUnit && level != LevelIntegration { + return fmt.Errorf("meshdeliverycheckerclient: the level %q is not %s or %s", level, LevelUnit, LevelIntegration) + } + return nil +} + +func isHex12(s string) bool { return len(s) == 12 && strings.Trim(s, "0123456789abcdef") == "" } + +// CheckRepository refuses a repository that is not / with exactly one slash, each part a key part. +func CheckRepository(repository string) error { + owner, name, ok := strings.Cut(repository, "/") + if !ok || strings.Contains(name, "/") { + return fmt.Errorf("meshdeliverycheckerclient: the repository %q is not /, with exactly one slash", repository) + } + if err := kept.CheckName("repository owner", owner); err != nil { + return fmt.Errorf("meshdeliverycheckerclient: the repository %q: %w", repository, err) + } + if err := kept.CheckName("repository name", name); err != nil { + return fmt.Errorf("meshdeliverycheckerclient: the repository %q: %w", repository, err) + } + return nil +} + +func checkComposition(c *checker.VerdictsByGroupHeadsValueComposition) error { + if c == nil { + return nil + } + switch c.State { + case CompositionNotRun: + if c.Sdk != "" || len(c.Machines) > 0 { + return fmt.Errorf("meshdeliverycheckerclient: the verdict's composition is not run and names no mesh-sdk commit or machines") + } + case CompositionPass: + if c.Sdk == "" { + return fmt.Errorf("meshdeliverycheckerclient: the verdict's composition passed and names no mesh-sdk commit") + } + case CompositionFail: + if c.Words == "" { + return fmt.Errorf("meshdeliverycheckerclient: the verdict's composition failed and gives no words") + } + default: + return fmt.Errorf("meshdeliverycheckerclient: the verdict's composition state %q is not %s, %s or %s", c.State, CompositionNotRun, CompositionPass, CompositionFail) + } + return nil +} + +// CheckVerdict refuses a verdict a reader could not place: no group, heads that are not 12 lowercase hex digits, a repository +// both merged and read beside, a carried-from that carries nothing, a series change from a series to itself or named twice, a +// level that is neither unit nor integration or counts a negative number, a time below zero, a pass that says nothing of a +// level, a fail that names neither a test nor a machine, and failures on a verdict that did not fail. +func CheckVerdict(v checker.VerdictsByGroupHeadsValue) error { + const who = "meshdeliverycheckerclient: " + if v.Group == "" { + return fmt.Errorf(who + "a verdict names no group") + } + if !isHex12(v.Heads) { + return fmt.Errorf(who+"the verdict's heads hash %q is not 12 lowercase hex digits", v.Heads) + } + for repository, head := range v.Trunk { + if head == "" { + return fmt.Errorf(who+"the verdict's trunk head of %s is empty", repository) + } + } + for repository, commit := range v.Beside { + if commit == "" { + return fmt.Errorf(who+"the verdict's sibling commit of %s is empty", repository) + } + if _, ok := v.Trunk[repository]; ok { + return fmt.Errorf(who+"the verdict names %s as a trunk it merged and as a sibling it read", repository) + } + } + if len(v.CarriedFrom) > 0 { + if v.State == VerdictRunning { + return fmt.Errorf(who + "a running verdict is carried from nothing: carried-from is written with its end") + } + moved := false + for repository, commit := range v.CarriedFrom { + now, inTrunk := v.Trunk[repository] + if !inTrunk { + now = v.Beside[repository] + } + if commit == "" || now == "" { + return fmt.Errorf(who+"the verdict's carried-from names %s, which it judged on no commit of", repository) + } + moved = moved || commit != now + } + if !moved { + return fmt.Errorf(who + "the verdict's carried-from names the commits it is judged on: nothing moved, so nothing was carried") + } + } + changed := map[string]bool{} + for _, c := range v.Series { + if c.Module == "" || c.Artifact == "" || c.From == "" || c.To == "" { + return fmt.Errorf(who+"the verdict's series change %+v names no module, artifact, from or to", c) + } + if c.From == c.To { + return fmt.Errorf(who+"the verdict's series change of %s/%s is from %s to the same series", c.Module, c.Artifact, c.From) + } + if changed[c.Module+"/"+c.Artifact] { + return fmt.Errorf(who+"the verdict names the series change of %s/%s twice", c.Module, c.Artifact) + } + changed[c.Module+"/"+c.Artifact] = true + } + for level, r := range v.Levels { + if err := checkLevel(level); err != nil { + return err + } + if r.Run < 0 || r.Kept < 0 || r.Skipped < 0 { + return fmt.Errorf(who+"the verdict's %s level counts a negative number of tests", level) + } + } + if err := checkComposition(v.Composition); err != nil { + return err + } + seen := map[string]bool{} + for _, level := range v.OverBound { + if err := checkLevel(level); err != nil { + return fmt.Errorf(who+"the verdict's over-bound names: %w", err) + } + if seen[level] { + return fmt.Errorf(who+"the verdict's over-bound names the level %s twice", level) + } + seen[level] = true + } + if t := v.Took; t != nil { + for part, ms := range map[string]int64{"queue": t.Queue, "clone": t.Clone, "unit": t.Unit, "lint": t.Lint, + "integration-setup": t.IntegrationSetup, "integration-run": t.IntegrationRun, "composition": t.Composition} { + if ms < 0 { + return fmt.Errorf(who+"the verdict's time of the part %s is negative", part) + } + } + } + named := v.Failed != nil && (v.Failed.Test != "" || v.Failed.Machine != "") + if len(v.Failures) > 0 && v.State != VerdictFail { + return fmt.Errorf(who+"a %s verdict names no failures", v.State) + } + if len(v.Failures) > MaxFailures+1 { + return fmt.Errorf(who+"the verdict names %d failures, more than %d and the count of the rest", len(v.Failures), MaxFailures) + } + switch v.State { + case VerdictRunning: + if v.Failed != nil && *v.Failed != (checker.VerdictsByGroupHeadsValueFailed{}) { + return fmt.Errorf(who + "a running verdict names no failure") + } + case VerdictPass: + if v.Failed != nil && *v.Failed != (checker.VerdictsByGroupHeadsValueFailed{}) { + return fmt.Errorf(who + "a pass names no failure") + } + for _, level := range []string{LevelUnit, LevelIntegration} { + if _, ok := v.Levels[level]; !ok { + return fmt.Errorf(who+"a pass says nothing of the %s level", level) + } + } + case VerdictFail: + if !named { + return fmt.Errorf(who + "a fail names neither a test nor a machine") + } + default: + return fmt.Errorf(who+"the verdict state %q is not running, pass or fail", v.State) + } + return nil +} + +// CheckKept refuses a kept verdict that is not a pass of a repository's package at a level, with its toolchain and its time: +// a failure is never kept. +func CheckKept(k checker.KeptByLevelDigestValue) error { + const who = "meshdeliverycheckerclient: " + if k.State != VerdictPass { + return fmt.Errorf(who+"a kept verdict is a pass, never %q: a failure is never kept", k.State) + } + if err := CheckRepository(k.Repository); err != nil { + return err + } + if k.Package == "" { + return fmt.Errorf(who + "a kept verdict names no package") + } + if err := checkLevel(k.Level); err != nil { + return err + } + if k.Tests < 0 || k.Took < 0 { + return fmt.Errorf(who + "a kept verdict counts a negative number of tests or a negative time") + } + if k.Toolchain == "" { + return fmt.Errorf(who + "a kept verdict names no toolchain version") + } + if k.At == "" { + return fmt.Errorf(who + "a kept verdict says no time") + } + return nil +} diff --git a/contracts/go/clients/meshdeliverycheckerclient/client_test.go b/contracts/go/clients/meshdeliverycheckerclient/client_test.go new file mode 100644 index 00000000..062efc25 --- /dev/null +++ b/contracts/go/clients/meshdeliverycheckerclient/client_test.go @@ -0,0 +1,81 @@ +package meshdeliverycheckerclient + +import ( + "strings" + "testing" + + checker "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" +) + +type verdict = checker.VerdictsByGroupHeadsValue + +func passing() verdict { + return verdict{Group: "g", Heads: "0123456789ab", State: VerdictPass, Trunk: map[string]string{"novox/a": "c1"}, + Levels: map[string]checker.VerdictsByGroupHeadsValueLevelsValue{LevelUnit: {Run: 3}, LevelIntegration: {Run: 1}}} +} + +// The rules a verdict keeps to be written or read: each case edits a passing verdict and names what the refusal says. +func TestAVerdictIsRefusedForWhatAReaderCouldNotPlace(t *testing.T) { + if err := CheckVerdict(passing()); err != nil { + t.Fatalf("a passing verdict is refused: %v", err) + } + for name, c := range map[string]struct { + edit func(*verdict) + says string + }{ + "no group": {func(v *verdict) { v.Group = "" }, "no group"}, + "heads of 11 digits": {func(v *verdict) { v.Heads = "0123456789a" }, "12 lowercase hex"}, + "an empty trunk head": {func(v *verdict) { v.Trunk["novox/a"] = "" }, "trunk head"}, + "merged and read": {func(v *verdict) { v.Beside = map[string]string{"novox/a": "c2"} }, "trunk it merged and as a sibling"}, + "nothing carried": {func(v *verdict) { v.CarriedFrom = map[string]string{"novox/a": "c1"} }, "nothing moved"}, + "a running carry": {func(v *verdict) { v.State, v.CarriedFrom = VerdictRunning, map[string]string{"novox/a": "c0"} }, "carried from nothing"}, + "a series to itself": {func(v *verdict) { + v.Series = []checker.VerdictsByGroupHeadsValueSeriesItem{{Module: "m", Artifact: "a", From: "0.1", To: "0.1"}} + }, "same series"}, + "an unknown level": {func(v *verdict) { v.Levels["smoke"] = checker.VerdictsByGroupHeadsValueLevelsValue{} }, "smoke"}, + "a negative count": {func(v *verdict) { v.Levels[LevelUnit] = checker.VerdictsByGroupHeadsValueLevelsValue{Run: -1} }, "negative"}, + "a negative time": {func(v *verdict) { v.Took = &checker.VerdictsByGroupHeadsValueTook{Unit: -1} }, "negative"}, + "a pass without a level": {func(v *verdict) { delete(v.Levels, LevelIntegration) }, "integration level"}, + "a pass that failed": {func(v *verdict) { v.Failed = &checker.VerdictsByGroupHeadsValueFailed{Test: "T"} }, "names no failure"}, + "failures of a pass": {func(v *verdict) { v.Failures = []checker.VerdictsByGroupHeadsValueFailuresItem{{Test: "T"}} }, "names no failures"}, + "a fail naming nothing": {func(v *verdict) { v.State = VerdictFail }, "neither a test nor a machine"}, + "an unknown state": {func(v *verdict) { v.State = "maybe" }, "maybe"}, + "a composition passed without a commit": {func(v *verdict) { + v.Composition = &checker.VerdictsByGroupHeadsValueComposition{State: CompositionPass} + }, "mesh-sdk commit"}, + "the same level over bound twice": {func(v *verdict) { v.OverBound = []string{LevelUnit, LevelUnit} }, "twice"}, + } { + v := passing() + c.edit(&v) + if err := CheckVerdict(v); err == nil || !strings.Contains(err.Error(), c.says) { + t.Errorf("%s: %v, want a refusal saying %q", name, err, c.says) + } + } + failed := passing() + failed.State, failed.Failed = VerdictFail, &checker.VerdictsByGroupHeadsValueFailed{Machine: "ace"} + if err := CheckVerdict(failed); err != nil { + t.Errorf("a fail naming a machine is refused: %v", err) + } +} + +func TestAKeptVerdictIsAPassOfAPackageAtALevel(t *testing.T) { + 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) + } + for name, edit := range map[string]func(*checker.KeptByLevelDigestValue){ + "a fail": func(k *checker.KeptByLevelDigestValue) { k.State = VerdictFail }, + "no owner": func(k *checker.KeptByLevelDigestValue) { k.Repository = "mesh-sdk" }, + "no package": func(k *checker.KeptByLevelDigestValue) { k.Package = "" }, + "an unknown level": func(k *checker.KeptByLevelDigestValue) { k.Level = "smoke" }, + "negative tests": func(k *checker.KeptByLevelDigestValue) { k.Tests = -1 }, + "no toolchain": func(k *checker.KeptByLevelDigestValue) { k.Toolchain = "" }, + "no time": func(k *checker.KeptByLevelDigestValue) { k.At = "" }, + } { + k := ok + edit(&k) + if CheckKept(k) == nil { + t.Errorf("%s was taken", name) + } + } +} diff --git a/contracts/go/clients/meshdeliverycheckerclient/go.mod b/contracts/go/clients/meshdeliverycheckerclient/go.mod new file mode 100644 index 00000000..27d39949 --- /dev/null +++ b/contracts/go/clients/meshdeliverycheckerclient/go.mod @@ -0,0 +1,9 @@ +module git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient + +go 1.26.0 + +require ( + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef + git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker v0.0.0 + git.novox.be/novox/mesh-delivery/sdk v0.0.0 +) diff --git a/contracts/module.json b/contracts/module.json index 113c2fc6..4e537386 100644 --- a/contracts/module.json +++ b/contracts/module.json @@ -112,6 +112,15 @@ ], "series": "0.1" }, + { + "name": "go-meshdeliverycheckerclient", + "kind": "package", + "language": "go", + "paths": [ + "go/clients/meshdeliverycheckerclient" + ], + "series": "0.1" + }, { "name": "go-gopackageregistryclient", "kind": "package",