From 4cdba63b9bde6f1d948df707776eb827486d49c2 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 21 Oct 2026 21:32:08 +0200 Subject: [PATCH] Read tools for the checker's verdicts, a pull request's report and the kept verdicts (issue 1686) --- .../cmd/mesh-delivery-checker-tools/main.go | 71 +++++++ .../mesh-delivery-checker-tools/main_test.go | 27 +++ .../internal/check/read.go | 193 ++++++++++++++++++ .../internal/check/read_test.go | 104 ++++++++++ .../mesh-delivery-checker/manifest_test.go | 31 ++- modules/mesh-delivery-checker/module.json | 16 ++ 6 files changed, 433 insertions(+), 9 deletions(-) create mode 100644 modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go create mode 100644 modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main_test.go create mode 100644 modules/mesh-delivery-checker/internal/check/read.go create mode 100644 modules/mesh-delivery-checker/internal/check/read_test.go diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go new file mode 100644 index 0000000..2fef5a6 --- /dev/null +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go @@ -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..: 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": "/"}, + "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..: 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) + } +} diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main_test.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main_test.go new file mode 100644 index 0000000..5fec12b --- /dev/null +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main_test.go @@ -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) + } + } +} diff --git a/modules/mesh-delivery-checker/internal/check/read.go b/modules/mesh-delivery-checker/internal/check/read.go new file mode 100644 index 0000000..874e99b --- /dev/null +++ b/modules/mesh-delivery-checker/internal/check/read.go @@ -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 +} diff --git a/modules/mesh-delivery-checker/internal/check/read_test.go b/modules/mesh-delivery-checker/internal/check/read_test.go new file mode 100644 index 0000000..1d1c8fb --- /dev/null +++ b/modules/mesh-delivery-checker/internal/check/read_test.go @@ -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") + } +} diff --git a/modules/mesh-delivery-checker/manifest_test.go b/modules/mesh-delivery-checker/manifest_test.go index 08d68ba..9d7eebb 100644 --- a/modules/mesh-delivery-checker/manifest_test.go +++ b/modules/mesh-delivery-checker/manifest_test.go @@ -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" { diff --git a/modules/mesh-delivery-checker/module.json b/modules/mesh-delivery-checker/module.json index bb08186..15e2cc7 100644 --- a/modules/mesh-delivery-checker/module.json +++ b/modules/mesh-delivery-checker/module.json @@ -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",