Read tools for the checker's verdicts, a pull request's report and the kept verdicts (issue 1686)

This commit is contained in:
2026-10-21 21:32:08 +02:00
parent ee994f57b9
commit 4cdba63b9b
6 changed files with 433 additions and 9 deletions
@@ -0,0 +1,71 @@
// Command mesh-delivery-checker-tools serves the checker's read tools (novox/hq ADR 0484 clause 13, issue 1686): the runtime
// launches it for a call, it reads the checker's verdicts and kept states and the indexer's candidates through the checker's
// state and reads, and writes nothing. The checker is the other program: it serves no tool (ADR 0437 §4).
package main
import (
"encoding/json"
"fmt"
"os"
stdio "git.novox.be/novox/mesh-sdk/go"
"mesh-delivery-checker/internal/check"
)
func read(s stdio.KeptState) check.StateRead {
return check.StateRead{Keys: s.Keys, Get: func(key string) (json.RawMessage, error) {
e, err := s.Get(key)
if err != nil || e == nil {
return nil, err
}
return e.Value, nil
}}
}
// Tools are the read tools over the checker's states.
func Tools(r check.Reader) []stdio.Tool {
return []stdio.Tool{
{Name: check.VerdictsReadTool,
Description: "The verdicts the checker holds, each as stored under verdicts.<group>.<heads>: its state, what failed first, the tests run, kept and skipped per level, the time of each part, the trunk heads it judged against and the composition, with the full " +
"report of the status each pull request carries (the line the forge shows is cut at 140 characters, this is not). With a group, that group's only. Read-only.",
Input: map[string]any{"type": "object", "properties": map[string]any{
"group": map[string]any{"type": "string", "description": "the delivery group's branch name"}}},
Run: func(args map[string]any) (any, error) {
group, _ := args["group"].(string)
vs, err := r.Verdicts(group)
return map[string]any{"verdicts": vs}, err
}},
{Name: check.PullReadTool,
Description: "The verdict that judges one pull request and the full report of its status mesh/check; for an hq pull request, that hq is judged by " +
"merge-check.sh until issue 1296. Read-only.",
Input: map[string]any{"type": "object", "required": []string{"repository", "number"}, "properties": map[string]any{
"repository": map[string]any{"type": "string", "description": "<owner>/<name>"},
"number": map[string]any{"type": "integer", "description": "the pull request's number"}}},
Run: func(args map[string]any) (any, error) {
repository, _ := args["repository"].(string)
n, _ := args["number"].(float64)
return r.Pull(repository, int(n))
}},
{Name: check.KeptReadTool,
Description: "The kept verdicts of passing packages, each as stored under kept.<level>.<digest>: the package, its tests, the time they took, the toolchain " +
"and when it was kept. With a level, that level's; with a level and a digest, that one. Read-only.",
Input: map[string]any{"type": "object", "properties": map[string]any{
"level": map[string]any{"type": "string", "enum": []string{"unit", "integration"}, "description": "the level"},
"digest": map[string]any{"type": "string", "description": "the package's digest, with a level"}}},
Run: func(args map[string]any) (any, error) {
level, _ := args["level"].(string)
digest, _ := args["digest"].(string)
kept, err := r.Kept(level, digest)
return map[string]any{"kept": kept}, err
}},
}
}
func main() {
r := check.Reader{VerdictState: read(stdio.State("verdicts")), KeptState: read(stdio.State("kept")), CandidateState: read(stdio.State("mesh-delivery-indexer.candidates"))}
if err := stdio.Serve("", Tools(r)); err != nil {
fmt.Fprintf(os.Stderr, "[mesh-delivery-checker-tools] %v\n", err)
os.Exit(1)
}
}
@@ -0,0 +1,27 @@
package main
import (
"testing"
"mesh-delivery-checker/internal/check"
)
func TestTheToolsServedAreTheThreeReadTools(t *testing.T) {
var names []string
for _, tl := range Tools(check.Reader{}) {
names = append(names, tl.Name)
if tl.Description == "" || tl.Run == nil {
t.Errorf("%s has no description or no run", tl.Name)
}
}
want := []string{check.VerdictsReadTool, check.PullReadTool, check.KeptReadTool}
if len(names) != 3 || names[0] != want[0] || names[1] != want[1] || names[2] != want[2] {
t.Errorf("tools = %v, want %v", names, want)
}
// With nothing to read from a tool says so and changes nothing.
for _, tl := range Tools(check.Reader{}) {
if _, err := tl.Run(map[string]any{"repository": "novox/x", "number": float64(1)}); err == nil {
t.Errorf("%s answered with no state to read", tl.Name)
}
}
}
@@ -0,0 +1,193 @@
package check
import (
"encoding/json"
"fmt"
"sort"
"strings"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// The read tools (novox/hq ADR 0484 clauses 8 and 13, issue 1686): what the checker stored, as stored, with the line of the
// status it owes in full, so a check is read live without a shell. They write nothing.
// The read tools, named as the module is: checker_ and the noun.
const (
VerdictsReadTool = "checker_verdicts"
PullReadTool = "checker_pull"
KeptReadTool = "checker_kept"
)
// StateRead reads one state: its keys and a key's stored value (nil for a key with none).
type StateRead struct {
Keys func() ([]string, error)
Get func(key string) (json.RawMessage, error)
}
// Reader answers the read tools from the checker's verdicts and kept states and the indexer's candidates.
type Reader struct{ VerdictState, KeptState, CandidateState StateRead }
// VerdictRead is a verdict as stored with the full report of the status of each pull request it judged (Report), when the
// candidate still holds those heads; without it the report is that of the verdict alone.
type VerdictRead struct {
Key string `json:"key"`
Verdict json.RawMessage `json:"verdict"`
Reports map[string]string `json:"reports,omitempty"`
Note string `json:"note,omitempty"`
}
func (r Reader) candidate(group string) (delivery.Candidate, bool) {
key, err := delivery.CandidatesKey(group)
if err != nil || r.CandidateState.Get == nil {
return delivery.Candidate{}, false
}
raw, err := r.CandidateState.Get(key)
if err != nil || len(raw) == 0 {
return delivery.Candidate{}, false
}
c, err := delivery.DecodeCandidate(raw)
return c, err == nil
}
func (r Reader) read(key string, raw json.RawMessage) (VerdictRead, error) {
out := VerdictRead{Key: key, Verdict: raw}
v, err := delivery.DecodeVerdict(raw)
if err != nil {
return out, fmt.Errorf("%s holds what is not a verdict: %w", key, err)
}
c, ok := r.candidate(v.Group)
if !ok || c.HeadsHash != v.Heads {
out.Note = "the candidate no longer holds these heads"
return out, nil
}
cs := CaseOf(c, v.Trunk)
out.Reports = map[string]string{}
if cs == nil {
return out, nil
}
for _, p := range cs.Pulls {
out.Reports[fmt.Sprintf("%s#%d", p.Repository, p.Number)] = Report(v, cs, p, nil, nil)
}
for _, l := range cs.Left {
out.Reports[strings.SplitN(l, ": ", 2)[0]] = strings.SplitN(l, ": ", 2)[1]
}
return out, nil
}
// Verdicts are the verdicts the checker holds, by key; with a group only that group's.
func (r Reader) Verdicts(group string) ([]VerdictRead, error) {
if r.VerdictState.Keys == nil || r.VerdictState.Get == nil {
return nil, fmt.Errorf("the verdicts state cannot be read here")
}
keys, err := r.VerdictState.Keys()
if err != nil {
return nil, err
}
sort.Strings(keys)
var out []VerdictRead
for _, k := range keys {
if group != "" {
if g, _, err := delivery.ParseCheckerVerdictKey(k); err != nil || g != group {
continue
}
}
raw, err := r.VerdictState.Get(k)
if err != nil {
return nil, err
}
if len(raw) == 0 {
continue
}
vr, err := r.read(k, raw)
if err != nil {
return nil, err
}
out = append(out, vr)
}
return out, nil
}
// Pull answers for one pull request: the verdict of the candidate that holds it, with its report; for an hq pull request,
// that hq is judged by merge-check.sh until issue 1296 (HQNote); for one the indexer holds in no candidate, that none does.
func (r Reader) Pull(repository string, number int) (VerdictRead, error) {
if r.CandidateState.Keys == nil || r.CandidateState.Get == nil {
return VerdictRead{}, fmt.Errorf("the candidates state cannot be read here")
}
keys, err := r.CandidateState.Keys()
if err != nil {
return VerdictRead{}, err
}
sort.Strings(keys)
for _, k := range keys {
raw, err := r.CandidateState.Get(k)
if err != nil || len(raw) == 0 {
continue
}
c, err := delivery.DecodeCandidate(raw)
if err != nil {
continue
}
for _, p := range c.Pulls {
if p.Repository != repository || p.Number != number {
continue
}
if repository == HQRepository {
return VerdictRead{Key: k, Note: HQNote}, nil
}
vkey, err := delivery.CheckerVerdictKey(c.Group, c.HeadsHash)
if err != nil {
return VerdictRead{}, err
}
vraw, err := r.VerdictState.Get(vkey)
if err != nil {
return VerdictRead{}, err
}
if len(vraw) == 0 {
return VerdictRead{Key: vkey, Note: "no verdict yet for the heads of its candidate"}, nil
}
return r.read(vkey, vraw)
}
}
return VerdictRead{}, fmt.Errorf("no candidate holds %s#%d", repository, number)
}
// Kept is the kept verdicts: one key's value as stored, or with an empty key the keys of a level (or every level) and their values.
func (r Reader) Kept(level, digest string) (map[string]json.RawMessage, error) {
if r.KeptState.Keys == nil || r.KeptState.Get == nil {
return nil, fmt.Errorf("the kept state cannot be read here")
}
if digest != "" {
key, err := delivery.KeptKey(level, digest)
if err != nil {
return nil, err
}
raw, err := r.KeptState.Get(key)
if err != nil {
return nil, err
}
if len(raw) == 0 {
return nil, fmt.Errorf("no kept verdict %s", key)
}
return map[string]json.RawMessage{key: raw}, nil
}
keys, err := r.KeptState.Keys()
if err != nil {
return nil, err
}
sort.Strings(keys)
out := map[string]json.RawMessage{}
for _, k := range keys {
if l, _, err := delivery.ParseKeptKey(k); err != nil || (level != "" && l != level) {
continue
}
raw, err := r.KeptState.Get(k)
if err != nil {
return nil, err
}
if len(raw) > 0 {
out[k] = raw
}
}
return out, nil
}
@@ -0,0 +1,104 @@
package check
import (
"context"
"encoding/json"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
type fakeState map[string][]byte
func (f fakeState) read() StateRead {
return StateRead{
Keys: func() ([]string, error) {
var k []string
for key := range f {
k = append(k, key)
}
return k, nil
},
Get: func(key string) (json.RawMessage, error) {
if v, ok := f[key]; ok {
return json.RawMessage(v), nil
}
return nil, nil
},
}
}
// readerOf is a reader over a verdict the checker wrote through its own code and the candidate it judged.
func readerOf(t *testing.T) (Reader, string) {
t.Helper()
r := newRig(t, passJudge())
cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1), pullOf(HQRepository, 3, head2))
r.ch.Candidate(context.Background(), "feat%2Fx", cand)
r.ch.Wait()
ckey, _ := delivery.CandidatesKey("feat/x")
verdicts, kept := fakeState{}, fakeState{}
for k, v := range r.verdicts.vals {
verdicts[k] = v
}
if _, err := (Kept{Store: r.verdicts}).Keep(context.Background(), "unit", digA, keptValue("m/a", 3)); err != nil {
t.Fatal(err)
}
kept["unit."+digA] = r.verdicts.vals["unit."+digA]
return Reader{VerdictState: verdicts.read(), KeptState: kept.read(), CandidateState: fakeState{ckey: cand}.read()}, keyOf(t, "feat/x", cand)
}
func TestTheVerdictToolsAnswerTheStoredVerdictWithTheFullReportOfEachPullRequest(t *testing.T) { // N9/AC1, 1686
rd, key := readerOf(t)
vs, err := rd.Verdicts("")
if err != nil || len(vs) != 1 || vs[0].Key != key {
t.Fatalf("verdicts = %+v, %v", vs, err)
}
if _, err := delivery.DecodeVerdict(vs[0].Verdict); err != nil {
t.Errorf("the verdict is not as stored: %v", err)
}
if got := vs[0].Reports["novox/mesh-delivery#7"]; !strings.HasPrefix(got, "pass 111111111111 on aaaaaaaaaaaa") || !strings.Contains(got, "composition not yet run") {
t.Errorf("reports = %v", vs[0].Reports)
}
if only, _ := rd.Verdicts("feat/other"); len(only) != 0 {
t.Errorf("another group's verdicts = %+v", only)
}
}
func TestAnHqPullRequestIsSaidToBeJudgedByMergeCheckUntilIssue1296(t *testing.T) { // ADR 0484 clause 8
rd, _ := readerOf(t)
hq, err := rd.Pull(HQRepository, 3)
if err != nil || hq.Note != "hq: judged by merge-check.sh until issue 1296" || hq.Verdict != nil {
t.Errorf("hq pull = %+v, %v", hq, err)
}
p, err := rd.Pull(repoA, 7)
if err != nil || p.Note != "" || !strings.HasPrefix(p.Reports["novox/mesh-delivery#7"], "pass ") {
t.Errorf("pull = %+v, %v", p, err)
}
if _, err := rd.Pull(repoA, 99); err == nil {
t.Error("a pull request no candidate holds was answered")
}
}
func TestTheKeptToolAnswersTheKeysAsStoredByLevelAndByDigest(t *testing.T) { // N9/AC1
rd, _ := readerOf(t)
all, err := rd.Kept("", "")
if err != nil || len(all) != 1 {
t.Fatalf("kept = %v, %v", all, err)
}
one, err := rd.Kept("unit", digA)
if err != nil || len(one) != 1 {
t.Fatalf("one kept = %v, %v", one, err)
}
for _, raw := range one {
if k, err := delivery.DecodeKept(raw); err != nil || k.Package != "m/a" {
t.Errorf("kept value = %+v, %v", k, err)
}
}
if none, _ := rd.Kept("integration", ""); len(none) != 0 {
t.Errorf("integration = %v", none)
}
if _, err := rd.Kept("unit", digB); err == nil {
t.Error("a digest nothing kept was answered")
}
}
+22 -9
View File
@@ -23,12 +23,13 @@ type state struct {
}
type artifact struct {
Name string `json:"name"`
Kind string `json:"kind"`
Language string `json:"language"`
System string `json:"system"`
From string `json:"from"`
Binary string `json:"binary"`
Name string `json:"name"`
Kind string `json:"kind"`
Language string `json:"language"`
System string `json:"system"`
From string `json:"from"`
Binary string `json:"binary"`
Loads []string `json:"loads"`
}
type stream struct {
@@ -38,6 +39,7 @@ type stream struct {
type manifest struct {
Streams []stream `json:"streams"`
Tools []string `json:"tools"`
Module string `json:"module"`
Version string `json:"version"`
Slug string `json:"slug"`
@@ -115,7 +117,11 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1
if want := []stream{{Name: "checks", Events: []string{"preview.**", "owed.**"}}}; !reflect.DeepEqual(m.Streams, want) {
t.Errorf("streams = %+v, want %+v", m.Streams, want)
}
for _, k := range []string{"logs", "emits", "claims", "provides", "requires", "uses", "defines-seats", "tools", "seat", "seats", "bus"} {
// The read tools (issue 1686) are served by the tools bundle, not by the checker's process.
if want := []string{"checker_kept", "checker_pull", "checker_verdicts"}; !reflect.DeepEqual(sorted(m.Tools), want) {
t.Errorf("tools = %v, want %v", m.Tools, want)
}
for _, k := range []string{"logs", "emits", "claims", "provides", "requires", "uses", "defines-seats", "seat", "seats", "bus"} {
if _, ok := top[k]; ok {
t.Errorf("the manifest declares %q: the checker has none", k)
}
@@ -168,8 +174,15 @@ func TestManifestProcess(t *testing.T) { // 1678/AC1
if _, bus := p["bus"]; bus {
t.Errorf("the process declares a bus: it runs on its own broker credential")
}
if len(m.Build.Artifacts) != 2 || m.Build.Artifacts[1].Name != "files" || m.Build.Artifacts[1].Kind != "archive" || m.Build.Artifacts[1].From != "files" {
t.Fatalf("artifacts = %+v, want the program and the files", m.Build.Artifacts)
if len(m.Build.Artifacts) != 3 || m.Build.Artifacts[2].Name != "files" || m.Build.Artifacts[2].Kind != "archive" || m.Build.Artifacts[2].From != "files" {
t.Fatalf("artifacts = %+v, want the program, the tools and the files", m.Build.Artifacts)
}
if a := m.Build.Artifacts[1]; a.Name != "tools" || a.Kind != "bundle" || a.Language != "go" || a.Binary != "mesh-delivery-checker-tools" ||
!reflect.DeepEqual(a.Loads, []string{"mesh-delivery-checker-tools"}) {
t.Errorf("tools artifact = %+v", a)
}
if st, err := os.Stat(m.Build.Artifacts[1].From); err != nil || !st.IsDir() {
t.Errorf("the tools' source %s: %v", m.Build.Artifacts[1].From, err)
}
a := m.Build.Artifacts[0]
if st, err := os.Stat(a.From); err != nil || !st.IsDir() || a.Binary != "mesh-delivery-checker" || !strings.HasSuffix(a.From, a.Binary) || a.Kind != "bundle" || a.Language != "go" {
+16
View File
@@ -29,6 +29,11 @@
"mesh-delivery-watchman.presence",
"node-engine.have"
],
"tools": [
"checker_kept",
"checker_pull",
"checker_verdicts"
],
"invokes": [
"mesh-build-go.build_go_digest",
"mesh-build-go.build_go_fetch",
@@ -106,6 +111,17 @@
"from": "cmd/mesh-delivery-checker",
"binary": "mesh-delivery-checker"
},
{
"name": "tools",
"kind": "bundle",
"language": "go",
"system": "arch",
"from": "cmd/mesh-delivery-checker-tools",
"binary": "mesh-delivery-checker-tools",
"loads": [
"mesh-delivery-checker-tools"
]
},
{
"name": "files",
"kind": "archive",