Compare commits

...
15 changed files with 278 additions and 23 deletions
@@ -193,6 +193,24 @@ func Tools(r check.Reader) []stdio.Tool {
n, _ := args["number"].(float64) n, _ := args["number"].(float64)
return r.Pull(repository, int(n)) return r.Pull(repository, int(n))
}}, }},
{Name: check.FailureReadTool,
Description: "The full output of a pull request's failed test, which the verdict and its mesh/check status cut to their last lines " +
"(\"... (N earlier lines)\"): the test, the words the verdict keeps, the build step that holds the rest (its id, build module, " +
"machine, the check it ran for, and the call that reads it) and the first page of its build log. Read on with from = the " +
"output's next. Answers from the pull request's last failed verdict also once no candidate holds it. 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"},
"from": map[string]any{"type": "integer", "description": "the log's stream sequence to read on from: the output's next; 0 is the start"}}},
Run: func(args map[string]any) (any, error) {
repository, _ := args["repository"].(string)
n, _ := args["number"].(float64)
from, _ := args["from"].(float64)
if from < 0 {
return nil, fmt.Errorf("from is a stream sequence, 0 or more")
}
return r.Failure(repository, int(n), uint64(from))
}},
{Name: check.KeptReadTool, {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 " + 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.", "and when it was kept. With a level, that level's; with a level and a digest, that one. Read-only.",
@@ -269,7 +287,8 @@ func main() {
HaveState: read(stdio.State("node-engine.have")), StepBound: stepBound(), HaveState: read(stdio.State("node-engine.have")), StepBound: stepBound(),
TurnState: read(stdio.State("mesh-delivery-gate.turns")), HearingState: read(stdio.State(buildrun.HearingState)), TurnState: read(stdio.State("mesh-delivery-gate.turns")), HearingState: read(stdio.State(buildrun.HearingState)),
AskState: read(stdio.State(check.AsksState))} AskState: read(stdio.State(check.AsksState))}
r.Runs = runsOf{read(stdio.State("mesh-delivery-indexer.modules"))} // the node-engine at the commit the mesh runs, as the checker reads it r.Logs = func(tool string, args map[string]any) (json.RawMessage, error) { return stdio.Ask(tool, args) } // the module's invokes
r.Runs = runsOf{read(stdio.State("mesh-delivery-indexer.modules"))} // the node-engine at the commit the mesh runs, as the checker reads it
tries := stdio.State(check.TriesState) tries := stdio.State(check.TriesState)
source := stdio.State("mesh-delivery-gate.source") source := stdio.State("mesh-delivery-gate.source")
trunks := func() (map[string]string, error) { trunks := func() (map[string]string, error) {
@@ -11,7 +11,7 @@ import (
"mesh-delivery-checker/internal/check" "mesh-delivery-checker/internal/check"
) )
func TestTheToolsServedAreTheFiveReadTools(t *testing.T) { func TestTheToolsServedAreTheSixReadTools(t *testing.T) {
var names []string var names []string
for _, tl := range Tools(check.Reader{}) { for _, tl := range Tools(check.Reader{}) {
names = append(names, tl.Name) names = append(names, tl.Name)
@@ -19,7 +19,7 @@ func TestTheToolsServedAreTheFiveReadTools(t *testing.T) {
t.Errorf("%s has no description or no run", tl.Name) t.Errorf("%s has no description or no run", tl.Name)
} }
} }
want := []string{check.VerdictsReadTool, check.PullReadTool, check.KeptReadTool, check.PreviewsReadTool, check.StatusReadTool} want := []string{check.VerdictsReadTool, check.PullReadTool, check.FailureReadTool, check.KeptReadTool, check.PreviewsReadTool, check.StatusReadTool}
if !slices.Equal(names, want) { if !slices.Equal(names, want) {
t.Errorf("tools = %v, want %v", names, want) t.Errorf("tools = %v, want %v", names, want)
} }
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-delivery-checker
go 1.26.0 go 1.26.0
require ( require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771 git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030100935-c2ec8b6b2fde
github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0 github.com/nats-io/nats.go v1.54.0
) )
+4
View File
@@ -1,5 +1,9 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771 h1:Tt16HJk8JZRce1i3JzzDSNdCWOrv7dw8FQ/bckNQ5Ig= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771 h1:Tt16HJk8JZRce1i3JzzDSNdCWOrv7dw8FQ/bckNQ5Ig=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030095816-530d8d3007dc h1:Wdjl2gomooHswJ5qpkwZPq/ureZgXVw6FMCsgzPJhPo=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030095816-530d8d3007dc/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030100935-c2ec8b6b2fde h1:eKXSllHCS31GgMazfvZ7BeiLoXIUibnUmzxWVkj+Vcw=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030100935-c2ec8b6b2fde/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
@@ -432,6 +432,8 @@ func TestTheChangesOwnFailingTestRunsFirstThenTheRestAndNoLaterLevel(t *testing.
b.fetch.Changed = []string{"modules/m/internal/b/b_test.go"} b.fetch.Changed = []string{"modules/m/internal/b/b_test.go"}
b.digest[delivery.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"m/internal/a": digA, "m/internal/b": digB}} b.digest[delivery.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"m/internal/a": digA, "m/internal/b": digB}}
failing := captured[Tested](t, "go-test-failing") failing := captured[Tested](t, "go-test-failing")
// The step said the failure's whole output on its log (the operator, 2026-10-30): the verdict names where.
failing.Failed[0].Step, failing.Failed[0].Module, failing.Failed[0].Machine, failing.Failed[0].Run = "s1", "mesh-build-go", "ace", "r1"
b.test = func(c stepCall) (any, error) { b.test = func(c stepCall) (any, error) {
if got := packagesOf(c); len(got) == 1 && got[0] == "m/internal/b" { if got := packagesOf(c); len(got) == 1 && got[0] == "m/internal/b" {
return failing, nil return failing, nil
@@ -439,7 +441,8 @@ func TestTheChangesOwnFailingTestRunsFirstThenTheRestAndNoLaterLevel(t *testing.
return captured[Tested](t, "go-test-slow"), nil return captured[Tested](t, "go-test-slow"), nil
} }
out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
if out.Verdict.State != delivery.VerdictFail || !strings.Contains(out.Verdict.Failed.Test, "TestThreeIsFour") { if f := out.Verdict.Failed; out.Verdict.State != delivery.VerdictFail || !strings.Contains(f.Test, "TestThreeIsFour") ||
f.Step != "s1" || f.Module != "mesh-build-go" || f.Machine != "ace" || f.Run != "r1" || f.Pull != repoA+"#7" || out.Verdict.Failures[0].Step != "s1" {
t.Fatalf("verdict = %+v", out.Verdict) t.Fatalf("verdict = %+v", out.Verdict)
} }
tests := b.steps.tool("test") tests := b.steps.tool("test")
@@ -0,0 +1,132 @@
package check
import (
"encoding/json"
"fmt"
"strings"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// A pull request's failed test, its whole output read from the build step that said it (the operator, 2026-10-30: the
// verdict cuts a failure to its last lines, and its cause was guessed). The verdict names the step, its build module and
// machine and the check it ran for (delivery.FailedFirst); the build module's log tool reads the step's log from its own
// stream (mesh-sdk buildstep.LogTool). The verdict is found through the pull request's candidate, or, once no candidate
// holds it, as the newest verdict that names the pull request's failure.
// FailureStep is where a failure's whole output is: the step id, its build module and machine, the run it ran for, and the
// call that reads it.
type FailureStep struct {
ID string `json:"id"`
Module string `json:"module"`
Machine string `json:"machine,omitempty"`
Run string `json:"run"`
Read string `json:"read"`
}
// FailureRead is a pull request's failed test: the verdict that names it, the test and the words the verdict keeps, the
// step that holds the rest, and the first page of its log (Output, the log tool's answer), or why there is none.
type FailureRead struct {
Pull string `json:"pull"`
Verdict string `json:"verdict"`
Test string `json:"test,omitempty"`
Words string `json:"words,omitempty"`
Step *FailureStep `json:"step,omitempty"`
Output json.RawMessage `json:"output,omitempty"`
Note string `json:"note,omitempty"`
}
// logToolOf is a build module's log tool: mesh-build-go's build_go_log.
func logToolOf(module string) string {
lang := strings.TrimPrefix(module, "mesh-build-")
return module + ".build_" + lang + "_log"
}
// failureOf is the failure a verdict names of the pull request pull: the first that names it, or, in a verdict that names
// no pull request (written before), the first failure.
func failureOf(v delivery.Verdict, pull string) (delivery.FailedFirst, bool) {
for _, f := range append([]delivery.FailedFirst{v.Failed}, v.Failures...) {
if f.Pull == pull && f.Test != "" {
return f, true
}
}
if v.Failed.Pull == "" && v.Failed.Test != "" {
return v.Failed, true
}
return delivery.FailedFirst{}, false
}
// Failure answers a pull request's failed test, with the page of its log from the stream sequence from on (0 the start).
func (r Reader) Failure(repository string, number int, from uint64) (FailureRead, error) {
pull := fmt.Sprintf("%s#%d", repository, number)
out := FailureRead{Pull: pull}
var v delivery.Verdict
found := false
if read, err := r.Pull(repository, number); err == nil && len(read.Verdict) > 0 {
if v, err = delivery.DecodeVerdict(read.Verdict); err != nil {
return out, fmt.Errorf("%s holds what is not a verdict: %w", read.Key, err)
}
out.Verdict, found = read.Key, true
} else if err != nil && !strings.HasPrefix(err.Error(), "no candidate") {
return out, err
}
if _, ok := failureOf(v, pull); !found || v.State != delivery.VerdictFail || !ok {
// No candidate holds it, or its candidate's verdict did not fail of it: the newest verdict that names its failure.
key, last, err := r.lastFailure(pull)
if err != nil {
return out, err
}
if key == "" {
if found {
out.Note = "its verdict names no failed test of it, and no other verdict does"
return out, nil
}
return out, fmt.Errorf("no candidate holds %s, and no verdict names a failed test of it", pull)
}
out.Verdict, v = key, last
}
f, _ := failureOf(v, pull)
out.Test, out.Words = f.Test, f.Words
if f.Step == "" {
out.Note = "the verdict names no build step that kept the whole output (a build module or a checker from before it)"
return out, nil
}
out.Step = &FailureStep{ID: f.Step, Module: f.Module, Machine: f.Machine, Run: f.Run,
Read: fmt.Sprintf("%s {run: %s, step: %s}", logToolOf(f.Module), f.Run, f.Step)}
if r.Logs == nil {
out.Note = "the build module's log cannot be asked here"
return out, nil
}
page, err := r.Logs(logToolOf(f.Module), map[string]any{"run": f.Run, "step": f.Step, "from": from})
if err != nil {
out.Note = "its log could not be read: " + err.Error()
return out, nil
}
out.Output = page
return out, nil
}
// lastFailure is the newest verdict that names a failed test of pull, by its step id (an id begins with its time), and
// its key; "" when none does among the first MaxReads.
func (r Reader) lastFailure(pull string) (string, delivery.Verdict, error) {
rows, _, err := r.Verdicts("")
if err != nil {
return "", delivery.Verdict{}, err
}
var key, newest string
var last delivery.Verdict
for _, row := range rows {
v, err := delivery.DecodeVerdict(row.Verdict)
if err != nil || v.State != delivery.VerdictFail {
continue
}
f, ok := failureOf(v, pull)
if !ok || f.Pull != pull {
continue
}
if key == "" || len(f.Step) > len(newest) || (len(f.Step) == len(newest) && f.Step > newest) {
key, newest, last = row.Key, f.Step, v
}
}
return key, last, nil
}
@@ -318,6 +318,15 @@ func (st *state) outcome(v delivery.Verdict) Outcome {
return out return out
} }
// runOf is the run a case's asks name (delivery.StepAsk.Run), under which their steps say their logs: the check's heads
// hash, or delivery.NoRun when it is no token.
func runOf(c *Case) string {
if c == nil || c.HeadsHash == "" || strings.ContainsAny(c.HeadsHash, ".*> ") {
return delivery.NoRun
}
return c.HeadsHash
}
// fail is a verdict of failure at a machine. // fail is a verdict of failure at a machine.
func fail(machine, words string) *delivery.Verdict { func fail(machine, words string) *delivery.Verdict {
v := failure(machine, words) v := failure(machine, words)
@@ -430,7 +439,7 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l
kind = delivery.AskRecheck // a re-judge after a trunk move goes after every new head's check (ADR 0536 §4) kind = delivery.AskRecheck // a re-judge after a trunk move goes after every new head's check (ADR 0536 §4)
} }
a := delivery.StepAsk{ID: buildstep.NewID(), Kind: kind, Language: askLanguage[t.module], Asker: delivery.CheckerModule, a := delivery.StepAsk{ID: buildstep.NewID(), Kind: kind, Language: askLanguage[t.module], Asker: delivery.CheckerModule,
At: st.j.now().UTC(), Check: job, Needs: needs} At: st.j.now().UTC(), Check: job, Needs: needs, Run: runOf(st.c)}
var step delivery.Step var step delivery.Step
var firstDecline time.Time var firstDecline time.Time
for { for {
@@ -831,7 +840,12 @@ func levelFailure(t *target, level string, results ...Tested) *delivery.Verdict
if t.module == buildrun.NodeBuildModule && f.Package == "" { if t.module == buildrun.NodeBuildModule && f.Package == "" {
name = res.Module name = res.Module
} }
all = append(all, delivery.FailedFirst{Test: level + ": " + name, Words: failureWordsAfter(f, hidWords(t.module, res.By, f))}) ff := delivery.FailedFirst{Test: level + ": " + name, Words: failureWordsAfter(f, hidWords(t.module, res.By, f)),
Pull: fmt.Sprintf("%s#%d", t.pull.Repository, t.pull.Number)}
if f.Step != "" { // its build step said its whole output on the module's log: where (checker_failure reads it)
ff.Step, ff.Module, ff.Machine, ff.Run = f.Step, f.Module, f.Machine, f.Run
}
all = append(all, ff)
} }
} }
if len(all) == 0 { // no result: a level said failed with nothing to name if len(all) == 0 { // no result: a level said failed with nothing to name
@@ -848,7 +862,8 @@ func failureLines(all []delivery.FailedFirst) []delivery.FailedFirst {
n := min(len(all), delivery.MaxFailures) n := min(len(all), delivery.MaxFailures)
out := make([]delivery.FailedFirst, 0, n+1) out := make([]delivery.FailedFirst, 0, n+1)
for _, f := range all[:n] { for _, f := range all[:n] {
out = append(out, delivery.FailedFirst{Test: f.Test, Machine: f.Machine, Words: clipLine(firstLine(f.Words), MaxFailureLine)}) f.Words = clipLine(firstLine(f.Words), MaxFailureLine)
out = append(out, f)
} }
if more := len(all) - n; more > 0 { if more := len(all) - n; more > 0 {
out = append(out, delivery.FailedFirst{Test: fmt.Sprintf("and %d more", more)}) out = append(out, delivery.FailedFirst{Test: fmt.Sprintf("and %d more", more)})
@@ -312,7 +312,7 @@ func parts(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []s
// Report is the full line of a pull request's status, with no limit: what the read tools and the full report say. // Report is the full line of a pull request's status, with no limit: what the read tools and the full report say.
func Report(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []string, judged map[string]string) string { func Report(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []string, judged map[string]string) string {
return parts(v, cs, p, builds, slowest, judged).join(0) return joinSet(parts(v, cs, p, builds, slowest, judged).join(0), fullOutputWords(v, p))
} }
// Line is the words of a status for one pull request of a case (ADR 0447 clause 3, ADR 0484 clause 7): the verdict first (a // Line is the words of a status for one pull request of a case (ADR 0447 clause 3, ADR 0484 clause 7): the verdict first (a
@@ -324,26 +324,52 @@ func Report(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []
// The program that judged is never dropped: a line naming it gives the counts in their compact form, then drops them and // The program that judged is never dropped: a line naming it gives the counts in their compact form, then drops them and
// what follows, and is cut in its first part rather than lose it. // what follows, and is cut in its first part rather than lose it.
func Line(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []string, judged map[string]string) string { func Line(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []string, judged map[string]string) string {
pc := parts(v, cs, p, builds, slowest, judged) full := fullOutputWords(v, p)
if full == "" {
return lineWithin(parts(v, cs, p, builds, slowest, judged), MaxLine)
}
return lineWithin(parts(v, cs, p, builds, slowest, judged), MaxLine-utf8.RuneCountInString(full)-3) + " | " + full
}
// FullOutputTool is the read tool that answers a failed test's whole output (the checker's tools).
const FullOutputTool = "checker_failure"
// fullOutputWords end the line of a pull request whose failed test's whole output a build step said on its log: where to
// read it, "full output: checker_failure <repo>#<n>"; "" when none was said (no test failed, or an older build module).
func fullOutputWords(v delivery.Verdict, p Pull) string {
if v.State != delivery.VerdictFail {
return ""
}
pull := fmt.Sprintf("%s#%d", p.Repository, p.Number)
for _, f := range append([]delivery.FailedFirst{v.Failed}, v.Failures...) {
if f.Step != "" && (f.Pull == "" || f.Pull == pull) {
return "full output: " + FullOutputTool + " " + pull
}
}
return ""
}
// lineWithin is the line of the pieces in at most most characters (Line).
func lineWithin(pc pieces, most int) string {
for drop := 0; drop <= 4; drop++ { for drop := 0; drop <= 4; drop++ {
if s := pc.join(drop); utf8.RuneCountInString(s) <= MaxLine { if s := pc.join(drop); utf8.RuneCountInString(s) <= most {
return s return s
} }
} }
if pc.judged != "" { if pc.judged != "" {
for _, counts := range []string{pc.shortCounts, ""} { for _, counts := range []string{pc.shortCounts, ""} {
if s := joinSet(pc.head, pc.commits, counts, pc.judged); utf8.RuneCountInString(s) <= MaxLine { if s := joinSet(pc.head, pc.commits, counts, pc.judged); utf8.RuneCountInString(s) <= most {
return s return s
} }
} }
rest := " | " + joinSet(pc.commits, pc.judged) rest := " | " + joinSet(pc.commits, pc.judged)
room := MaxLine - utf8.RuneCountInString(rest) - 1 room := most - utf8.RuneCountInString(rest) - 1
if head := []rune(pc.head); room > 0 && len(head) > room { if head := []rune(pc.head); room > 0 && len(head) > room {
return string(head[:room]) + "…" + rest return string(head[:room]) + "…" + rest
} }
} }
runes := []rune(pc.join(4)) runes := []rune(pc.join(4))
return string(runes[:MaxLine-1]) + "…" return string(runes[:most-1]) + "…"
} }
// joinSet joins the parts that are not empty. // joinSet joins the parts that are not empty.
@@ -50,11 +50,18 @@ func TestAFailureNamesTheTestTheConflictOrTheRefusedStepFirst(t *testing.T) { //
"test": {delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Test: "unit: m/a.TestX", Words: "want 4"}}, "failed: unit: m/a.TestX: want 4"}, "test": {delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Test: "unit: m/a.TestX", Words: "want 4"}}, "failed: unit: m/a.TestX: want 4"},
"conflict": {delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Machine: "shanks", Words: "novox/mesh-delivery#7 does not merge into the trunk: files in conflict: go.mod"}}, "failed: shanks: novox/mesh-delivery#7 does not merge into the trunk: files in conflict: go.mod"}, "conflict": {delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Machine: "shanks", Words: "novox/mesh-delivery#7 does not merge into the trunk: files in conflict: go.mod"}}, "failed: shanks: novox/mesh-delivery#7 does not merge into the trunk: files in conflict: go.mod"},
"refused": {delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Machine: "mesh-build-go", Words: "no go build node qualifies: builders disagree"}}, "failed: mesh-build-go: no go build node qualifies"}, "refused": {delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Machine: "mesh-build-go", Words: "no go build node qualifies: builders disagree"}}, "failed: mesh-build-go: no go build node qualifies"},
// The operator, 2026-10-30: a test whose step said its whole output ends the line saying where to read it.
"whole output": {delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Test: "unit: m/a.TestX", Words: "want 4\n... " + strings.Repeat("a long line ", 40),
Step: "s1", Module: "mesh-build-go", Run: "r1", Pull: "novox/mesh-delivery#7"}}, "failed: unit: m/a.TestX: want 4"},
} { } {
line := Line(c.v, cs, p, nil, nil, nil) line := Line(c.v, cs, p, nil, nil, nil)
if !strings.HasPrefix(line, c.want) || !strings.Contains(line, "111111111111 on aaaaaaaaaaaa") { if !strings.HasPrefix(line, c.want) || !strings.Contains(line, "111111111111 on aaaaaaaaaaaa") {
t.Errorf("%s: line = %q, want it to open with %q and name the commits", name, line, c.want) t.Errorf("%s: line = %q, want it to open with %q and name the commits", name, line, c.want)
} }
full := strings.HasSuffix(line, " | full output: checker_failure novox/mesh-delivery#7")
if full != (c.v.Failed.Step != "") || utf8.RuneCountInString(line) > MaxLine {
t.Errorf("%s: line = %q: the full output named %v, want %v, within %d characters", name, line, full, !full, MaxLine)
}
} }
} }
@@ -18,7 +18,9 @@ import (
const ( const (
VerdictsReadTool = "checker_verdicts" VerdictsReadTool = "checker_verdicts"
PullReadTool = "checker_pull" PullReadTool = "checker_pull"
KeptReadTool = "checker_kept" // FailureReadTool is FullOutputTool: a pull request's failed test with its whole output (Failure).
FailureReadTool = FullOutputTool
KeptReadTool = "checker_kept"
// PreviewsReadTool reads the previews state (issue 1680); StatusReadTool reads what the checker runs (issue 1685: nothing holds it since R4g). // PreviewsReadTool reads the previews state (issue 1680); StatusReadTool reads what the checker runs (issue 1685: nothing holds it since R4g).
PreviewsReadTool = "checker_previews" PreviewsReadTool = "checker_previews"
StatusReadTool = "checker_status" StatusReadTool = "checker_status"
@@ -49,7 +51,10 @@ type Reader struct {
HearingState StateRead HearingState StateRead
// AskState is the checker's state asks: the asks each running check put (AsksState), which checker_ask_again reads. // AskState is the checker's state asks: the asks each running check put (AsksState), which checker_ask_again reads.
AskState StateRead AskState StateRead
Now func() time.Time // Logs asks a build module's log tool (`<module>.build_<lang>_log`) as the checker (stdio.Ask): a failed test's whole
// output (Failure); nil asks none.
Logs func(tool string, args map[string]any) (json.RawMessage, error)
Now func() time.Time
} }
// VerdictRead is a verdict as stored with the full report of the status of each pull request it judged (Report), when the // VerdictRead is a verdict as stored with the full report of the status of each pull request it judged (Report), when the
@@ -67,6 +67,41 @@ func TestTheVerdictToolsAnswerTheStoredVerdictWithTheFullReportOfEachPullRequest
if only, _, _ := rd.Verdicts("feat/other"); len(only) != 0 { if only, _, _ := rd.Verdicts("feat/other"); len(only) != 0 {
t.Errorf("another group's verdicts = %+v", only) t.Errorf("another group's verdicts = %+v", only)
} }
// checker_failure (the operator, 2026-10-30): a passing pull request has no failure to read; one no candidate holds any
// more is answered from its newest failed verdict, with the step that holds its whole output and that log's first page.
if got, err := rd.Failure(repoA, 7, 0); err != nil || got.Step != nil || !strings.Contains(got.Note, "names no failed test") {
t.Errorf("a passing pull request's failure: %+v, %v", got, err)
}
if _, err := rd.Failure("novox/none", 1, 0); err == nil || !strings.Contains(err.Error(), "no verdict names a failed test") {
t.Errorf("a pull request no candidate and no verdict holds: %v", err)
}
verdicts := fakeState{}
keys, _ := rd.VerdictState.Keys()
for _, k := range keys {
verdicts[k], _ = rd.VerdictState.Get(k)
}
for i, step := range []string{"mh0aaaaa000001", "mh0aaaab000002"} {
v := delivery.Verdict{V: 1, Group: "fix/y", Heads: fmt.Sprintf("%012d", i), State: delivery.VerdictFail,
Failed: delivery.FailedFirst{Test: "unit: m/a.TestX", Words: "want 4", Step: step, Module: "mesh-build-go", Machine: "ace", Run: "r1", Pull: "novox/y#9"}}
raw, err := delivery.EncodeVerdict(v)
if err != nil {
t.Fatal(err)
}
key, _ := delivery.CheckerVerdictKey(v.Group, v.Heads)
verdicts[key] = raw
}
rd.VerdictState = verdicts.read()
var asked string
rd.Logs = func(tool string, args map[string]any) (json.RawMessage, error) {
asked = fmt.Sprintf("%s %v %v %v", tool, args["run"], args["step"], args["from"])
return json.RawMessage(`{"lines":["=== m/a TestX","want 4"],"next":0}`), nil
}
got, err := rd.Failure("novox/y", 9, 0)
if err != nil || got.Step == nil || got.Step.ID != "mh0aaaab000002" || got.Step.Read != "mesh-build-go.build_go_log {run: r1, step: mh0aaaab000002}" ||
asked != "mesh-build-go.build_go_log r1 mh0aaaab000002 0" || !strings.Contains(string(got.Output), "want 4") {
t.Errorf("the failure of novox/y#9: %+v (%s), asked %q, %v", got, got.Output, asked, err)
}
} }
// 1887/A10: an hq pull request is answered as any other, its report its verdict's (ADR 0527 §2), never a note that a script // 1887/A10: an hq pull request is answered as any other, its report its verdict's (ADR 0527 §2), never a note that a script
@@ -101,10 +101,12 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1
"mesh-delivery-indexer.manifests", "mesh-delivery-indexer.modules", "mesh-delivery-watchman.presence", "node-engine.have"}; !reflect.DeepEqual(sorted(m.Reads), want) { "mesh-delivery-indexer.manifests", "mesh-delivery-indexer.modules", "mesh-delivery-watchman.presence", "node-engine.have"}; !reflect.DeepEqual(sorted(m.Reads), want) {
t.Errorf("reads = %v, want %v", m.Reads, want) t.Errorf("reads = %v, want %v", m.Reads, want)
} }
// 1887/A5b: the checker reads nothing of the controller's plans-building (ADR 0483 superseded by ADR 0525 §7), calls no // 1887/A5b: the checker reads nothing of the controller's plans-building (ADR 0483 superseded by ADR 0525 §7), runs no
// build module's tool and holds no capacity: it puts check asks on the Go and Node build seats (ADR 0525 §1, §4). // build module's step tool and holds no capacity: it puts check asks on the Go and Node build seats (ADR 0525 §1, §4).
if want := []string{"gitea.gitea_commits", "gitea.gitea_compare", "gitea.gitea_get_file", "gitea.gitea_list_branches"}; !reflect.DeepEqual(sorted(m.Invokes), want) { // Its checker_failure reads a failed step's whole output with the build modules' log tools (the operator, 2026-10-30).
t.Errorf("invokes = %v, want only the forge's reads for a mesh-sdk pin (ADR 0484 clause 9)", m.Invokes) if want := []string{"gitea.gitea_commits", "gitea.gitea_compare", "gitea.gitea_get_file", "gitea.gitea_list_branches",
"mesh-build-go.build_go_log", "mesh-build-node.build_node_log"}; !reflect.DeepEqual(sorted(m.Invokes), want) {
t.Errorf("invokes = %v, want the forge's reads for a mesh-sdk pin (ADR 0484 clause 9) and the build logs' reads", m.Invokes)
} }
if want := []string{"node-build-go", "node-build-node"}; !reflect.DeepEqual(sorted(m.Uses), want) { if want := []string{"node-build-go", "node-build-node"}; !reflect.DeepEqual(sorted(m.Uses), want) {
t.Errorf("uses = %v, want the Go and Node build seats", m.Uses) t.Errorf("uses = %v, want the Go and Node build seats", m.Uses)
@@ -126,7 +128,7 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1
// The read tools (issue 1686) are served by the tools bundle, not by the checker's process. // The read tools (issue 1686) are served by the tools bundle, not by the checker's process.
// The try's tools (ADR 0501 §5; the end by hand, issue 734) beside them: mesh-controller's try calls them. A lost check // The try's tools (ADR 0501 §5; the end by hand, issue 734) beside them: mesh-controller's try calls them. A lost check
// is asked again by hand (checker_ask_again). // is asked again by hand (checker_ask_again).
if want := []string{"checker_ask_again", "checker_kept", "checker_previews", "checker_pull", "checker_status", "checker_try", "checker_try_end", "checker_try_read", "checker_verdicts"}; !reflect.DeepEqual(sorted(m.Tools), want) { if want := []string{"checker_ask_again", "checker_failure", "checker_kept", "checker_previews", "checker_pull", "checker_status", "checker_try", "checker_try_end", "checker_try_read", "checker_verdicts"}; !reflect.DeepEqual(sorted(m.Tools), want) {
t.Errorf("tools = %v, want %v", m.Tools, want) t.Errorf("tools = %v, want %v", m.Tools, want)
} }
for _, k := range []string{"logs", "emits", "claims", "provides", "requires", "defines-seats", "seat", "seats", "bus", "config"} { for _, k := range []string{"logs", "emits", "claims", "provides", "requires", "defines-seats", "seat", "seats", "bus", "config"} {
+4 -1
View File
@@ -40,6 +40,7 @@
], ],
"tools": [ "tools": [
"checker_ask_again", "checker_ask_again",
"checker_failure",
"checker_kept", "checker_kept",
"checker_previews", "checker_previews",
"checker_pull", "checker_pull",
@@ -53,7 +54,9 @@
"gitea.gitea_commits", "gitea.gitea_commits",
"gitea.gitea_compare", "gitea.gitea_compare",
"gitea.gitea_get_file", "gitea.gitea_get_file",
"gitea.gitea_list_branches" "gitea.gitea_list_branches",
"mesh-build-go.build_go_log",
"mesh-build-node.build_node_log"
], ],
"own-secrets": { "own-secrets": {
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-grants
go 1.26.0 go 1.26.0
require ( require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771 git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030100935-c2ec8b6b2fde
github.com/nats-io/nats.go v1.54.0 github.com/nats-io/nats.go v1.54.0
) )
+4
View File
@@ -1,5 +1,9 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771 h1:Tt16HJk8JZRce1i3JzzDSNdCWOrv7dw8FQ/bckNQ5Ig= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771 h1:Tt16HJk8JZRce1i3JzzDSNdCWOrv7dw8FQ/bckNQ5Ig=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030095816-530d8d3007dc h1:Wdjl2gomooHswJ5qpkwZPq/ureZgXVw6FMCsgzPJhPo=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030095816-530d8d3007dc/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030100935-c2ec8b6b2fde h1:eKXSllHCS31GgMazfvZ7BeiLoXIUibnUmzxWVkj+Vcw=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030100935-c2ec8b6b2fde/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=