Every check is judged by the checking program built from its heads (issue 1921)

The running checker asks the checking program twice on the build seat's job run: first the
heads' seat list, which the manifest validator's requests (change and trunk) and the
node-engine composer's carry; then the verdict of the facts it gathered. The program is built
at the group's mesh-delivery head, else at mesh-delivery's trunk, and its input is kept in the
checker's new state inputs as the exact bytes of its digest, read with checker_inputs. A run
ask no build node begins within 5 minutes is put once more, then fails the check naming the
job; a program that does not build or answer fails the check naming it, and the verdict
records who judged it (JudgedBy). A second step-seen hook on a context is added to the
first, so a try still sees its steps.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-06 22:58:15 +01:00
parent 043e3fb979
commit 63bbabacbf
24 changed files with 588 additions and 72 deletions
@@ -149,8 +149,13 @@ type stepSeenKey struct{}
// WithStepSeen is ctx carrying seen, which Asker.Wait hands every value the ask's step key takes while it waits: the node
// that took it, each phase it says while running (delivery.Step.Phase), and its end. A try names its asks' progress so
// (issue 1890). seen runs in the waiting goroutine and must not block.
// (issue 1890). seen runs in the waiting goroutine and must not block. A seen ctx already carries is kept: both are handed
// every value, the earlier first (a check's begin bound watches the steps of a try that names them).
func WithStepSeen(ctx context.Context, seen func(delivery.Step)) context.Context {
if before := stepSeenOf(ctx); before != nil {
later := seen
seen = func(s delivery.Step) { before(s); later(s) }
}
return context.WithValue(ctx, stepSeenKey{}, seen)
}
@@ -26,10 +26,13 @@ func TestAnAskerPutsOnceAndReadsTheStep(t *testing.T) {
return delivery.Step{ID: a.ID, Node: "ace", State: delivery.StepPassed, Result: []byte(`{"artifacts":[]}`)}
}}
k := Asker{Publisher: f, Waiter: f}
var seen []delivery.Step // issue 1890: a waiter that asks to see each value of the step sees it
step, err := k.Ask(WithStepSeen(context.Background(), func(s delivery.Step) { seen = append(seen, s) }), goAsk("mvl0frv18d1957"))
if err != nil || step.Node != "ace" || len(f.Asks()) != 1 || len(seen) != 1 || seen[0].Node != "ace" {
t.Fatalf("step %+v, %v, asks %d, seen %+v", step, err, len(f.Asks()), seen)
var seen, also []delivery.Step // issue 1890: a waiter that asks to see each value of the step sees it
// Issue 1921: a second seen on the same context (a check's begin bound inside a try) is added, never in place of the first.
both := WithStepSeen(WithStepSeen(context.Background(), func(s delivery.Step) { seen = append(seen, s) }),
func(s delivery.Step) { also = append(also, s) })
step, err := k.Ask(both, goAsk("mvl0frv18d1957"))
if err != nil || step.Node != "ace" || len(f.Asks()) != 1 || len(seen) != 1 || seen[0].Node != "ace" || len(also) != 1 {
t.Fatalf("step %+v, %v, asks %d, seen %+v and %+v", step, err, len(f.Asks()), seen, also)
}
f.Answer = func(a delivery.StepAsk) delivery.Step {
return delivery.Step{ID: a.ID, Node: "shanks", State: delivery.StepRefused, Reason: "go 1.27 installed"}
@@ -4,16 +4,18 @@
// verdict the judging rules give them, as one JSON line on its standard output (rules.Answer).
//
// mesh-checking-program -verdict
// mesh-checking-program -seats
//
// Asked -seats, it answers the seat list of the heads laid in the directory MESH_RUN_HEADS names (rules.SeatsAnswer).
// It refuses facts of a version it does not read, naming it, with exit 2.
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"io/fs"
"os"
"slices"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-checker/rules"
@@ -25,52 +27,25 @@ func main() {
// run is the command over its arguments, environment and outputs, answering its exit.
func run(args []string, getenv func(string) string, stdout, stderr io.Writer) int {
fs := flag.NewFlagSet("mesh-checking-program", flag.ContinueOnError)
fs.SetOutput(stderr)
verdict := fs.Bool("verdict", false, "answer the verdict of the facts in the input")
seats := fs.Bool("seats", false, "answer the seat list of the heads laid for it")
if err := fs.Parse(args); err != nil {
var heads fs.FS
if dir := getenv(delivery.RunHeadsEnv); dir != "" {
heads = os.DirFS(dir)
}
input := func() ([]byte, error) {
file := getenv(delivery.RunInputEnv)
if file == "" {
return nil, fmt.Errorf("%s names no file", delivery.RunInputEnv)
}
return os.ReadFile(file)
}
line, err := rules.Program(args, input, heads)
if err != nil {
if heads == nil && slices.Contains(args, "-seats") {
err = fmt.Errorf("no heads: %s names no directory", delivery.RunHeadsEnv)
}
fmt.Fprintf(stderr, "mesh-checking-program: %v\n", err)
return 2
}
refuse := func(format string, a ...any) int {
fmt.Fprintf(stderr, "mesh-checking-program: "+format+"\n", a...)
return 2
}
if *seats {
dir := getenv(delivery.RunHeadsEnv)
if dir == "" {
return refuse("no heads: %s names no directory", delivery.RunHeadsEnv)
}
answer, err := rules.SeatsOf(os.DirFS(dir))
if err != nil {
return refuse("%v", err)
}
line, err := json.Marshal(answer)
if err != nil {
return refuse("the seat list cannot be written: %v", err)
}
fmt.Fprintf(stdout, "%s\n", line)
return 0
}
if !*verdict {
return refuse("nothing asked: give -verdict or -seats")
}
file := getenv(delivery.RunInputEnv)
if file == "" {
return refuse("no input: %s names no file", delivery.RunInputEnv)
}
data, err := os.ReadFile(file)
if err != nil {
return refuse("the input cannot be read: %v", err)
}
facts, err := rules.DecodeFacts(data)
if err != nil {
return refuse("%v", err)
}
line, err := json.Marshal(rules.Judged(facts))
if err != nil {
return refuse("the verdict cannot be written: %v", err)
}
fmt.Fprintf(stdout, "%s\n", line)
return 0
}
@@ -264,6 +264,17 @@ func Tools(r check.Reader) []stdio.Tool {
kept, cut, err := r.Kept(level, digest)
return cutAnswer(map[string]any{"kept": kept}, cut), err
}},
{Name: check.InputsReadTool,
Description: "The checking program's inputs the checker keeps, each under the sha256 of its exact bytes (inputs.<digest>, kept a day): " +
"the facts of a check its verdict is asked of, or the heads its seat list is asked of. With a digest, that input as kept; " +
"without, every digest and its size. Read-only.",
Input: map[string]any{"type": "object", "properties": map[string]any{
"digest": map[string]any{"type": "string", "description": "the input's sha256, as a run ask names it"}}},
Run: func(args map[string]any) (any, error) {
digest, _ := args["digest"].(string)
inputs, cut, err := r.Inputs(digest)
return cutAnswer(map[string]any{"inputs": inputs}, cut), err
}},
{Name: check.PreviewsReadTool,
Description: "The merge previews the checker holds, each as stored under previews.<repository>.<head>.<trunk>: the merge commit, per module directory " +
"its tree, manifest digest and lock hashes, or the files in conflict. With a repository, that repository's; with a head (7 or more characters) " +
@@ -325,9 +336,9 @@ func stepBound() time.Duration {
func main() {
r := check.Reader{VerdictState: read(stdio.State("verdicts")), KeptState: read(stdio.State("kept")), CandidateState: read(stdio.State("mesh-delivery-indexer.candidates")),
PreviewState: read(stdio.State("previews")),
StepStates: map[string]check.StateRead{"mesh-build-go": read(stdio.State("mesh-build-go.steps")), "mesh-build-node": read(stdio.State("mesh-build-node.steps"))},
HaveState: read(stdio.State("node-engine.have")), StepBound: stepBound(),
PreviewState: read(stdio.State("previews")), InputState: read(stdio.State(check.InputsStateName)),
StepStates: map[string]check.StateRead{"mesh-build-go": read(stdio.State("mesh-build-go.steps")), "mesh-build-node": read(stdio.State("mesh-build-node.steps"))},
HaveState: read(stdio.State("node-engine.have")), StepBound: stepBound(),
TurnState: read(stdio.State("mesh-delivery-gate.turns")), HearingState: read(stdio.State(buildrun.HearingState)),
AskState: read(stdio.State(check.AsksState)), BuildState: read(stdio.State("mesh-delivery-builder.builds"))}
r.Runs = runsOf{read(stdio.State("mesh-delivery-indexer.modules"))} // the node-engine at the commit the mesh runs, as the checker reads it
@@ -21,7 +21,7 @@ func TestTheToolsServedAreTheFiveReadTools(t *testing.T) {
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.KeptReadTool, check.InputsReadTool, check.PreviewsReadTool, check.StatusReadTool}
if !slices.Equal(names, want) {
t.Errorf("tools = %v, want %v", names, want)
}
@@ -183,3 +183,15 @@ func (a awaitedKV) Delete(ctx context.Context, key string) error {
}
return s.Delete(ctx, key)
}
// runInputs keeps the checking program's inputs in the checker's state inputs (check.InputsBucket), each under the
// sha256 of its exact bytes: an input already kept under its digest is the same bytes, so a second keep is no error.
type runInputs struct{ a awaitedKV }
func (r runInputs) Keep(ctx context.Context, digest string, body []byte) error {
_, err := r.a.Create(ctx, digest, body)
if errors.Is(err, check.ErrConflict) {
return nil
}
return err
}
@@ -24,6 +24,8 @@ import (
// judgeDeps are the parts the judge of a check is made of.
type judgeDeps struct {
// inputs keeps the checking program's inputs (check.InputsState).
inputs check.Inputs
asks check.Asks
tools *buildrun.ToolchainSet
trunks *check.Trunks
@@ -160,7 +162,7 @@ func newJudge(d judgeDeps) check.Judge {
nodes = d.nodes
}
ex := &check.Examiner{Nodes: nodes, Compositions: d.compositions, Asks: d.asks, Toolchains: d.tools,
Kept: check.Kept{Store: d.kept}, Previews: d.previews, Candidates: d.candidates, Modules: d.repos, Packages: d.repos, Published: d.published, Forge: d.forge, Runs: d.repos, Builds: d.builds, BuildEnds: d.ends, BuildWait: d.buildWait, Announce: d.announce, Trunks: d.trunks, Manifests: d.repos, Read: d.repos, Predefined: d.predefined, Logf: d.logf}
Kept: check.Kept{Store: d.kept}, Previews: d.previews, Candidates: d.candidates, Modules: d.repos, Packages: d.repos, Published: d.published, Forge: d.forge, Runs: d.repos, Builds: d.builds, BuildEnds: d.ends, BuildWait: d.buildWait, Announce: d.announce, Trunks: d.trunks, Manifests: d.repos, Read: d.repos, Predefined: d.predefined, Inputs: d.inputs, Logf: d.logf}
if d.nodes != nil && d.builds != nil && d.repos != nil {
ex.Serving = runningServes{have: d.nodes, builds: d.builds, read: d.repos, logf: d.logf}
}
@@ -38,7 +38,8 @@ func TestTheCheckerAnnouncesEveryStateItKeepsAndOwedReadable(t *testing.T) { //
t.Errorf("announced %v, the manifest keeps %v", announced, manifest)
}
// kept is read by the Go build module for a check ask (issue 1887 design limit 1); tries by the delivery view (build_usage).
if want := []string{"kept", "owed", "previews", "tries", "verdicts"}; !equal(readable, want) {
// inputs by the Go build module, which hands one to the checking program a run ask names (issue 1921).
if want := []string{"inputs", "kept", "owed", "previews", "tries", "verdicts"}; !equal(readable, want) {
t.Errorf("readable %v, want %v", readable, want)
}
}
@@ -72,6 +72,9 @@ var keptStates = []announce.State{
{Name: "hearing"},
// The asks each running check put (check.AsksState): read by the checker's own checker_ask_again alone.
{Name: "asks"},
// The checking program's inputs by their digest (check.InputsState): read by the Go build module, which hands one to
// the program a run ask names (issue 1921).
{Name: check.InputsStateName, Readable: true},
{Name: "kept", Readable: true},
{Name: "owed", Readable: true},
{Name: "previews", Readable: true},
@@ -153,6 +156,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
conn.SetReconnectHandler(func(*nats.Conn) { hearing.Again(); heard.Nudge("") })
waiter := buildrun.LostAskWaiter{Inner: heard, Dead: dead, Hearing: hearing}
asksKV := process.AwaitBucket(ctx, js, conn, check.AsksBucket, "checker_ask_again refuses every check, as it cannot tell whether its ask waits in a stream", logger.Printf)
inputsKV := process.AwaitBucket(ctx, js, conn, check.InputsBucket, "no checking program can be asked, so every check fails naming it", logger.Printf)
ch := &check.Checker{
Asks: awaitedKV{asksKV},
Verdicts: kvStore{buckets[delivery.CheckerVerdictsBucket]},
@@ -226,7 +230,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
asks: buildrun.Asker{Publisher: asks, Waiter: waiter, Bound: stepBound}, tools: tools, trunks: trunks,
kept: kvStore{buckets[delivery.CheckerKeptBucket]}, previews: kvStore{buckets[delivery.PreviewsBucket]}, candidates: tryCandidates{buckets[delivery.CandidatesBucket]},
repos: repos, published: published, builds: builds, ends: ends, buildWait: set.buildWait, forge: forgeReads{call: caller.Call}, logf: logger.Printf,
announce: announce, compositions: compositions,
announce: announce, compositions: compositions, inputs: runInputs{awaitedKV{inputsKV}},
nodes: openNodeNames(ctx, js, logger.Printf),
predefined: keptPredefined{process.AwaitBucket(ctx, js, conn, delivery.IndexerModule+"_"+delivery.IndexerPredefinedState,
"a change's manifests are judged with no predefined keys, each fallback as its value declares it", logger.Printf)},
@@ -1,6 +1,7 @@
package check
import (
"encoding/json"
"fmt"
"maps"
"slices"
@@ -39,6 +40,9 @@ type Case struct {
Left []string
// Queued is when the checker took the case up, for the time of its queue.
Queued time.Time
// Seats is the heads' seat list the checking program made, which the manifest validator's and the node-engine
// composer's requests carry (ADR 0636 §4); none until it is made.
Seats []json.RawMessage
// Try narrows a try's check (its level, its tests); nil for a pull request's.
Try *TryOptions
@@ -171,7 +171,7 @@ func (e EngineCompositions) inSandbox(ctx context.Context, c *Case, head, node s
}
}
req := composer.Request{V: composer.Version, Input: a.Input, Depth: composer.DepthCompose, Machines: []string{node},
Cases: []composer.Case{{Name: "change", Manifests: manifests, Verdicts: verdicts}}}
Cases: []composer.Case{{Name: "change", Manifests: manifests, Verdicts: verdicts}}, Seats: c.Seats}
raw, err := json.Marshal(req)
if err != nil {
return a, err
@@ -146,6 +146,7 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) {
t.Fatal(err)
}
host := CaseOf(c, map[string]string{repoA: trunkA, nodeEngineRepo: trunkA})
host.Seats = []json.RawMessage{json.RawMessage(`{"seat":"node-x","scope":"node"}`)} // the heads' list (ADR 0636 §4)
withInput := composed("anchor")
withInput.Input = json.RawMessage(`{"nodes":[{"name":"anchor"}]}`)
e := &fakeEngines{answers: map[string]delivery.ComposeCandidateAnswer{"anchor": withInput}}
@@ -167,7 +168,8 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) {
var req composer.Request
_ = json.Unmarshal(job.Request, &req)
if job.Fetch["repository"] != nodeEngineRepo || job.Fetch["commit"] != head2 || job.Package != composer.Package ||
string(req.Input) != string(withInput.Input) || req.Cases[0].Manifests["web"].Digest == "" {
string(req.Input) != string(withInput.Input) || req.Cases[0].Manifests["web"].Digest == "" ||
len(req.Seats) != 1 || string(req.Seats[0]) != string(host.Seats[0]) {
t.Errorf("job = %+v, request %+v", job, req)
}
// Issue 1921: the composer is built at the group's heads, also a member that publishes no package yet.
@@ -1,12 +1,15 @@
package check
import (
"cmp"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io/fs"
"mesh-delivery-checker/rules"
"os"
"path/filepath"
"reflect"
@@ -15,6 +18,7 @@ import (
"strings"
"sync"
"testing"
"testing/fstest"
"time"
"unicode/utf8"
@@ -125,6 +129,69 @@ type bench struct {
// declines is how many asks naming needs a node lacking them declines before shanks, which has every need, takes
// one (issue 890); an ask naming none runs on shanks, which then has none.
declines int
// inputs are the checking program's kept inputs; heads the heads laid for it (none when nil); run, when set, answers
// a run ask in its place (a program that does not build or answer).
inputs *memKV
heads fs.FS
run func(a delivery.StepAsk) (delivery.Step, bool)
mu sync.Mutex
runs []delivery.StepAsk // the run asks put
}
// benchAsks puts the bench's asks: a run ask is answered at once by the checking program's work (answerRun), written by
// write and kept apart (bench.runs); every other ask goes to inner (the fake bus by default), answered as it answers.
type benchAsks struct {
b *bench
inner buildrun.AskPublisher
write func(delivery.Step)
}
func (b *bench) asker(inner buildrun.AskPublisher, write func(delivery.Step)) benchAsks {
return benchAsks{b: b, inner: inner, write: write}
}
func (p benchAsks) PublishAsk(ctx context.Context, subject, id string, data []byte) error {
inner, write := p.inner, p.write
if inner == nil {
inner, write = p.b.asks, p.b.asks.Write
}
a, err := delivery.DecodeStepAsk(data)
if err != nil || a.Run == nil {
return inner.PublishAsk(ctx, subject, id, data)
}
p.b.mu.Lock()
p.b.runs = append(p.b.runs, a)
p.b.mu.Unlock()
if step := p.b.answerRun(a); step.ID != "" {
write(step)
}
return nil
}
// trunkD is mesh-delivery's trunk the bench's checking program is built at.
var trunkD = strings.Repeat("d", 40)
// answerRun answers a run ask as the build module would: the checking program's work (rules.Program) over the input the
// ask names, read from the kept inputs and checked against its digest.
func (b *bench) answerRun(a delivery.StepAsk) delivery.Step {
began := time.Date(2026, 10, 21, 19, 0, 0, 0, time.UTC)
step := delivery.Step{ID: a.ID, Tool: a.Kind + " ask", Node: "shanks", State: delivery.StepPassed, Started: began, Ended: began.Add(time.Second)}
if b.run != nil {
if s, ok := b.run(a); ok {
return s
}
}
answer := delivery.RunAnswer{Built: true, Key: "program-0123456789abcdef"}
out, err := rules.Program(a.Run.Args, func() ([]byte, error) {
body, _, _, _ := b.inputs.Get(context.Background(), a.Run.Input.Digest)
return body, a.Run.Input.Verify(body)
}, b.heads)
if err != nil {
answer.Exit, answer.Said = 2, err.Error()
}
answer.Output = out
step.Result, _ = json.Marshal(answer)
return step
}
// refsOf reads the siblings of a fetch's arguments, as they arrive in an ask (JSON) or as the checker gave them.
@@ -183,7 +250,7 @@ func (b *bench) answer(a delivery.StepAsk) delivery.Step {
// newBench is an examiner over fake build modules that merge cleanly, digest the captured fixture's packages and pass
// the captured slow fixture's result at both levels, on shanks.
func newBench(t testing.TB) *bench {
b := &bench{steps: &fakeSteps{}, kept: newMemKV(), prev: newMemKV()}
b := &bench{steps: &fakeSteps{}, kept: newMemKV(), prev: newMemKV(), inputs: newMemKV(), heads: fstest.MapFS{}}
b.fetch = captured[Fetched](t, "go-fetch-clean")
b.fetch.Modules, b.fetch.Changed, b.fetch.Trees = []string{"."}, []string{"README.md"}, nil
one := captured[Digested](t, "go-digest")
@@ -207,8 +274,8 @@ func newBench(t testing.TB) *bench {
}
now := time.Date(2026, 10, 21, 19, 0, 0, 0, time.UTC)
b.asks = &buildrun.FakeAsks{Answer: b.answer}
b.ex = &Examiner{Asks: buildrun.Asker{Publisher: b.asks, Waiter: b.asks, Bound: 5 * time.Second}, Toolchains: goEntries("shanks"), Kept: Kept{Store: b.kept}, Previews: b.prev,
Modules: langs{}, Trunks: &Trunks{}, Now: func() time.Time { now = now.Add(50 * time.Millisecond); return now }}
b.ex = &Examiner{Asks: buildrun.Asker{Publisher: b.asker(nil, nil), Waiter: b.asks, Bound: 5 * time.Second}, Toolchains: goEntries("shanks"), Kept: Kept{Store: b.kept}, Previews: b.prev,
Modules: langs{}, Trunks: &Trunks{heads: map[string]string{DeliveryRepository: trunkD}}, Inputs: memInputs{b.inputs}, Now: func() time.Time { now = now.Add(50 * time.Millisecond); return now }}
return b
}
@@ -299,6 +366,73 @@ func TestACleanMergeIsFetchedAtItsTrunkAndBothLevelsPass(t *testing.T) { // 1679
t.Errorf("the check ask carries the names %v", asks)
}
}
// Issue 1921 (B0, D4 to D6): the check's verdict is the checking program's, built at the group's mesh-delivery head,
// else at mesh-delivery's trunk, over the facts kept as its input; a program that is never begun, does not build or
// does not answer fails the check naming it, and a rule the change's program alters judges the check.
verdictOf := func(state, words string) json.RawMessage {
out, _ := json.Marshal(rules.Answer{V: rules.AnswerVersion, Verdict: delivery.Verdict{State: state, Failed: delivery.FailedFirst{Words: words}}})
return out
}
for _, row := range []struct {
name string
repo string // the member's repository: mesh-delivery unless named
run func(a delivery.StepAsk, s delivery.Step) (delivery.Step, bool)
at string // the commit the program is built at; empty when it fails
fails string
runs int // the run asks put when it fails: each put twice when never begun
}{
{name: "at the group's mesh-delivery head", at: head1},
{name: "at mesh-delivery's trunk", repo: "novox/mesh-catalog", at: trunkD},
{name: "never begun", run: func(delivery.StepAsk, delivery.Step) (delivery.Step, bool) { return delivery.Step{}, true },
fails: "no build node began the job run of the asks", runs: 4},
{name: "does not build", run: func(_ delivery.StepAsk, s delivery.Step) (delivery.Step, bool) {
s.State, s.Reason = delivery.StepFailed, "the program does not build: undefined: rules.Nothing"
return s, true
}, fails: "by the checking program: its run ask", runs: 2},
{name: "a rule the change alters", at: head1, run: func(a delivery.StepAsk, s delivery.Step) (delivery.Step, bool) {
if a.Run.Args[0] != "-verdict" {
return s, false
}
s.Result, _ = json.Marshal(delivery.RunAnswer{Key: "program-0123456789abcdef", Output: verdictOf(delivery.VerdictFail, "the change's own rule refuses it")})
return s, true
}, fails: "the change's own rule refuses it"},
} {
b := newBench(t)
b.ex.BeginBound = 20 * time.Millisecond
if row.run != nil {
b.run = func(a delivery.StepAsk) (delivery.Step, bool) {
return row.run(a, delivery.Step{ID: a.ID, Tool: "check ask", Node: "shanks", State: delivery.StepPassed})
}
}
repo := cmp.Or(row.repo, repoA)
v := b.judge(t, caseOf(t, pullOf(repo, 7, head1))).Verdict
if row.fails != "" {
if v.State != delivery.VerdictFail || !strings.Contains(v.Failed.Words, row.fails) {
t.Errorf("%s: verdict %+v, want a fail saying %q", row.name, v, row.fails)
}
if row.at == "" && (len(b.runs) != row.runs || v.JudgedBy != nil) {
t.Errorf("%s: %d run asks, judged by %+v: want %d and none judging", row.name, len(b.runs), v.JudgedBy, row.runs)
}
} else if v.State != delivery.VerdictPass {
t.Errorf("%s: verdict %+v", row.name, v)
}
if row.at == "" {
continue
}
if by := v.JudgedBy; by == nil || by.Program.Commit != row.at || by.Program.Package != CheckingProgramPackage || by.Key == "" ||
len(by.Heads) != 1 || by.Heads[0].Repository != repo || by.Heads[0].Commit != head1 {
t.Errorf("%s: judged by %+v, want the program at %s over the member's head", row.name, v.JudgedBy, row.at)
}
if len(b.runs) != 2 || b.runs[0].Run.Args[0] != "-seats" || b.runs[1].Run.Args[0] != "-verdict" {
t.Fatalf("%s: run asks %+v, want the seat list and then the verdict", row.name, b.runs)
}
in := b.runs[1].Run.Input // the facts kept as their exact bytes under their digest
if body, _, found, _ := b.inputs.Get(context.Background(), in.Digest); !found || in.Verify(body) != nil || in.State != InputsState ||
!strings.Contains(string(body), `"group":"feat/x"`) {
t.Errorf("%s: the verdict's input %+v is not kept whole: %s", row.name, in, body)
}
}
}
// namesOf is the nodes' names as a test gives them.
@@ -923,7 +1057,7 @@ func TestAnAskWhoseStepNeverEndsEndsTheCheckLoudly(t *testing.T) {
b.asks.Answer = func(a delivery.StepAsk) delivery.Step {
return delivery.Step{ID: a.ID, Tool: "check ask", Node: "shanks", State: delivery.StepRunning, Started: time.Now()}
}
b.ex.Asks = buildrun.Asker{Publisher: b.asks, Waiter: b.asks, Bound: 20 * time.Millisecond}
b.ex.Asks = buildrun.Asker{Publisher: b.asker(nil, nil), Waiter: b.asks, Bound: 20 * time.Millisecond}
out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
if f := out.Verdict.Failed; out.Verdict.State != delivery.VerdictFail || !strings.Contains(f.Words, "on shanks did not end within") {
t.Fatalf("verdict = %+v", out.Verdict)
@@ -959,7 +1093,7 @@ func TestACheckAskWaitingInItsStreamIsNotBounded(t *testing.T) {
b := newBench(t)
answer := b.asks.Answer
b.asks.Answer = nil
b.ex.Asks = buildrun.Asker{Publisher: b.asks, Waiter: b.asks, Bound: 20 * time.Millisecond}
b.ex.Asks = buildrun.Asker{Publisher: b.asker(nil, nil), Waiter: b.asks, Bound: 20 * time.Millisecond}
done := make(chan Outcome, 1)
go func() { done <- b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) }()
for deadline := time.Now().Add(10 * time.Second); len(b.asks.Asks()) == 0; time.Sleep(time.Millisecond) {
@@ -1047,8 +1181,12 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) {
// try judges the same change as a whole group try, whose manifests come from the indexer's candidate of the try's
// group (issue 1921): the same judge, the same verdicts.
try bool
// seats lays heads that add a seat: the checking program's seat list is carried by every judge ask's request (ADR
// 0636 §4, issue 1921 D4).
seats bool
}{
{name: "accepted"},
{name: "the heads' seat list is carried by the head's judge and the trunk's", seats: true, indexer: "ok", ownJudge: true},
{name: "accepted in a group try", try: true},
{name: "refused in a group try", try: true, refused: "web has no id", fails: "the indexer's judge at mesh-delivery's trunk " + strings.Repeat("d", 40) + ` refuses the manifest of web: web has no id`},
{name: "a cycle the group try makes", try: true, cycle: "provisions", fails: "the change's module graph has the cycle mail -> web -> mail"},
@@ -1069,6 +1207,9 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) {
{name: "refused", refused: "web requires \"amqp\"\nweb has no id", fails: "the indexer's judge at mesh-delivery's trunk " + strings.Repeat("d", 40) + ` refuses the manifest of web: web requires "amqp"; web has no id`},
} {
b := newBench(t)
if row.seats {
b.heads = fstest.MapFS{"novox/mesh-contracts/contracts/seats/node-x/seat.json": {Data: []byte(`{"format":1,"seat":"node-x","scope":"node"}`)}}
}
b.ex.Manifests = fixedDigests{"mail": strings.Repeat("a", 64), "web": strings.Repeat("b", 64)}
b.ex.Trunks = fixedTrunks{heads: map[string]string{DeliveryRepository: trunk}}
b.ex.Predefined = keptList(`{"font-size": {"type": "number", "description": "the size"}}`)
@@ -1174,6 +1315,13 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) {
judged[0].Package != JudgePackage || judged[0].Dir != JudgeDir {
t.Fatalf("%s: judged %+v (verdict %+v)", row.name, judged, v)
}
for _, j := range judged {
var seated composer.Request
if err := json.Unmarshal(j.Request, &seated); err != nil || (len(seated.Seats) == 1) != row.seats ||
(row.seats && !strings.Contains(string(seated.Seats[0]), `"node-x"`)) {
t.Errorf("%s: the judge at %v is handed the seats %s, want the heads' list: %v", row.name, j.Fetch["commit"], seated.Seats, err)
}
}
var req composer.Request
if err := json.Unmarshal(judged[0].Request, &req); err != nil || req.Cases[0].Manifests["web"].Digest != web ||
req.Cases[1].Name != JudgeTrunk || req.Cases[1].Manifests["web"].Digest != strings.Repeat("b", 64) ||
@@ -68,3 +68,14 @@ func (m *memKV) Delete(_ context.Context, key string) error {
delete(m.revs, key)
return nil
}
// memInputs keeps run inputs in a memKV, under their digest.
type memInputs struct{ kv *memKV }
func (m memInputs) Keep(ctx context.Context, digest string, body []byte) error {
if _, _, found, _ := m.kv.Get(ctx, digest); found {
return nil
}
_, err := m.kv.Create(ctx, digest, body)
return err
}
@@ -138,6 +138,10 @@ type Examiner struct {
// and fingerprints a group try's manifest validator and composition judge (issue 1921); nil leaves a whole try with no
// manifest or composition judged, said.
Candidates TryCandidates
// Inputs keeps the checking program's inputs (the state InputsState); BeginBound is how long a run ask may wait for a
// build node to begin it (DefaultBeginBound when zero).
Inputs Inputs
BeginBound time.Duration
Now func() time.Time
Logf func(format string, args ...any)
}
@@ -302,13 +306,17 @@ func (j *Examiner) Judge(ctx context.Context, c *Case) (Outcome, error) {
// Each failure is a fact recorded where it is met; the verdict is the judging rules' of the facts.
st.judgeAll(ctx)
st.takeTryCandidate(ctx)
// The heads' seat list, from the checking program, before the validator and the composer are asked (ADR 0636 §4).
st.seatList(ctx)
st.judgeManifests(ctx)
st.refuseCycle(ctx)
st.compose(ctx)
answer := rules.Judged(st.factsOf())
// The verdict is the checking program's of the facts (ADR 0644 §1, ADR 0656 §5).
answer, by := st.judged(ctx)
for _, n := range answer.Notes {
st.c.note(n)
}
answer.Verdict.JudgedBy = by
out := st.outcome(answer.Verdict)
out.Builds = st.j.buildNotes(ctx, st.c)
return out, nil
@@ -384,7 +392,9 @@ func (st *state) fail(machine, words string) {
// factsOf are the check's facts as met: what the checking program judges.
func (st *state) factsOf() rules.Facts {
return rules.Facts{V: rules.FactsVersion, Steps: st.facts, CloneMS: st.clone.Milliseconds(), Composition: st.composition, Notes: st.c.Notes()}
f := st.factsHead()
f.Steps, f.CloneMS, f.Composition, f.Notes = st.facts, st.clone.Milliseconds(), st.composition, st.c.Notes()
return f
}
// levels are the levels a case's asks run: both, or the one a try names.
@@ -58,7 +58,7 @@ func (l *losingAsks) PublishAsk(ctx context.Context, subject, id string, data []
func losing(b *bench, lose int) *losingAsks {
l := &losingAsks{FakeAsks: &buildrun.FakeAsks{}, answer: b.answer, dead: &buildrun.DeadLetters{}, lose: lose}
b.ex.Asks = buildrun.Asker{Publisher: l, Waiter: buildrun.LostAskWaiter{Inner: l.FakeAsks, Dead: l.dead}, Bound: 5 * time.Second}
b.ex.Asks = buildrun.Asker{Publisher: b.asker(l, l.FakeAsks.Write), Waiter: buildrun.LostAskWaiter{Inner: l.FakeAsks, Dead: l.dead}, Bound: 5 * time.Second}
return l
}
@@ -282,7 +282,7 @@ func TestACheckWhoseAskJudgedNothingIsAskedAgainOrRead(t *testing.T) {
tr = &troubled{FakeAsks: &buildrun.FakeAsks{}, answer: b.answer, restarts: row.restarts, deaf: row.deaf,
deafIDs: map[string]bool{}, steps: map[string]delivery.Step{}}
heard = &buildrun.HeardEnds{Inner: tr, Read: tr}
b.ex.Asks = buildrun.Asker{Publisher: tr, Waiter: heard, Bound: 5 * time.Second}
b.ex.Asks = buildrun.Asker{Publisher: b.asker(tr, tr.Write), Waiter: heard, Bound: 5 * time.Second}
asked = func() int { return len(tr.Asks()) }
}
r := newRig(t, b.ex)
@@ -123,6 +123,8 @@ func (st *state) judgeManifests(ctx context.Context) {
req.Predefined = list
st.c.note("the manifests are judged against the predefined keys of " + from)
}
// The heads' seat list, for the change case and the trunk case both (ADR 0636 §4).
req.Seats = st.c.Seats
cv, trunkCase, ok := st.runJudge(ctx, req, touched, commit, at, st.packagesOf(DeliveryRepository))
if !ok {
return
@@ -0,0 +1,223 @@
package check
import (
"context"
"encoding/json"
"fmt"
"strings"
"sync"
"time"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"mesh-delivery-checker/rules"
)
// The checking program (novox/hq ADR 0644 §1, ADR 0656 §5; design 47's bootstrap delivery; issue 1921): every check has
// the program built from the group's mesh-delivery member head, else from mesh-delivery's trunk, and run in the build
// seat's sandbox through the seat node-build-go's ask `run`, with every member's head laid for it. It answers the heads'
// seat list first (`-seats`), which the manifest validator's and the composer's requests carry, and then the check's
// verdict of the facts the asking gathered (`-verdict`). The running checker asks, keeps and records; it judges nothing.
const (
// InputsStateName is the checker's own state of run inputs, InputsBucket its bucket and InputsState its name as a run
// ask's input names it (delivery.RunInput.State): every input by the sha256 of its exact bytes, read by the Go build
// module, which checks the digest (delivery.RunInput.Verify).
InputsStateName = "inputs"
InputsBucket = delivery.CheckerModule + "_" + InputsStateName
InputsState = delivery.CheckerModule + "." + InputsStateName
// CheckingProgramDir and CheckingProgramPackage are where in mesh-delivery the checking program is built.
CheckingProgramDir = "modules/mesh-delivery-checker"
CheckingProgramPackage = "mesh-delivery-checker/cmd/mesh-checking-program"
// DefaultBeginBound is how long a run ask may wait for a build node to begin its step before it is put once more, and
// after that once more before the check fails naming the job: a build module that does not know the job `run` drops
// it unanswered (the lead's ruling on milestone 47/B0, 2026-11-06).
DefaultBeginBound = 5 * time.Minute
)
// Inputs keeps a run's input under its digest, exactly its bytes.
type Inputs interface {
Keep(ctx context.Context, digest string, body []byte) error
}
func (j *Examiner) beginBound() time.Duration {
if j.BeginBound > 0 {
return j.BeginBound
}
return DefaultBeginBound
}
// checkingProgram is where this check's checking program is built: the group's mesh-delivery member head, else
// mesh-delivery's trunk; false when neither is known.
func (st *state) checkingProgram() (delivery.RunProgram, string, bool) {
p := delivery.RunProgram{Repository: DeliveryRepository, Dir: CheckingProgramDir, Package: CheckingProgramPackage}
for _, pull := range st.c.Pulls {
if pull.Repository == DeliveryRepository {
p.Commit = pull.Head
return p, "the group's mesh-delivery head " + pull.Head, true
}
}
if st.j.Trunks != nil {
p.Commit = st.j.Trunks.Heads()[DeliveryRepository]
}
return p, "mesh-delivery's trunk " + p.Commit, p.Commit != ""
}
// runHeads are the members' heads, as the program reads them.
func (st *state) runHeads() []delivery.RunHead {
out := make([]delivery.RunHead, 0, len(st.c.Pulls))
for _, p := range st.c.Pulls {
out = append(out, delivery.RunHead{Repository: p.Repository, Commit: p.Head})
}
return out
}
// runProgram has the checking program built and run with args over body, and answers its output and what judged; an
// error says why it did not answer: its input over its bound, no build node beginning the job, a step that did not
// pass, or a program that exited otherwise than 0.
func (st *state) runProgram(ctx context.Context, args []string, body []byte) (json.RawMessage, *delivery.JudgedBy, error) {
prog, at, ok := st.checkingProgram()
if !ok {
return nil, nil, fmt.Errorf("it cannot be built: the trunk head of %s is not known", DeliveryRepository)
}
if st.j.Inputs == nil {
return nil, nil, fmt.Errorf("its input cannot be kept: the checker keeps no state %s", InputsState)
}
in, err := delivery.RunInputOf(InputsState, body)
if err != nil {
return nil, nil, err
}
if err := st.j.Inputs.Keep(ctx, in.Digest, body); err != nil {
return nil, nil, fmt.Errorf("its input could not be kept in %s: %w", InputsState, err)
}
job := delivery.RunJob{Program: prog, Heads: st.runHeads(), Input: in, Args: args, Reader: "the checking program at " + at}
var ids []string
for range 2 {
a := delivery.StepAsk{ID: buildstep.NewID(), Kind: delivery.AskCheck, Language: delivery.AskGo, Asker: delivery.CheckerModule,
At: st.j.now().UTC(), Packages: st.packagesOf(DeliveryRepository), Run: &job}
ids = append(ids, a.ID)
st.c.Progress.Set(fmt.Sprintf("the run ask %s of the checking program %s", a.ID, strings.Join(args, " ")), "")
st.c.Asked(a.ID)
if err := st.j.Asks.Put(ctx, a); err != nil {
return nil, nil, fmt.Errorf("its run ask could not be put: %w", err)
}
waiting := ctx
if watch := st.c.Watch; watch != nil {
id := a.ID
watch(id, nil) // a try names it at once (issue 1890)
waiting = buildrun.WithStepSeen(ctx, func(s delivery.Step) { watch(id, &s) })
}
step, begun, err := st.waitBegun(waiting, a)
if !begun {
continue
}
if err != nil {
return nil, nil, fmt.Errorf("its run ask %s: %w", a.ID, err)
}
if step.State != delivery.StepPassed {
return nil, nil, fmt.Errorf("its run ask %s ended %s on %s: %s", a.ID, step.State, step.Node, step.Reason)
}
var answer delivery.RunAnswer
if err := json.Unmarshal(step.Result, &answer); err != nil {
return nil, nil, fmt.Errorf("its run ask %s answered no run answer: %w", a.ID, err)
}
if answer.Exit != 0 {
return nil, nil, fmt.Errorf("it exited %d on %s: %s", answer.Exit, step.Node, answer.Said)
}
return answer.Output, &delivery.JudgedBy{Program: prog, Key: answer.Key, Built: answer.Built, Heads: job.Heads}, nil
}
return nil, nil, fmt.Errorf("no build node began the job run of the asks %s within %s each: a build module that does not know the job run drops it",
strings.Join(ids, " and "), st.j.beginBound())
}
// waitBegun waits for a's step, and stops waiting, begun false, when no build node began it within the begin bound: a
// bound on the wait's context, no timer of its own.
func (st *state) waitBegun(ctx context.Context, a delivery.StepAsk) (delivery.Step, bool, error) {
wait, cancel := context.WithCancel(ctx)
defer cancel()
begin, stop := context.WithTimeout(ctx, st.j.beginBound())
defer stop()
seen := make(chan struct{})
var once sync.Once
type ended struct {
step delivery.Step
err error
}
done := make(chan ended, 1)
go func() {
step, err := st.j.Asks.Wait(buildrun.WithStepSeen(wait, func(delivery.Step) { once.Do(func() { close(seen) }) }), a)
done <- ended{step, err}
}()
select {
case e := <-done:
return e.step, true, e.err
case <-seen:
case <-begin.Done():
if ctx.Err() != nil {
return delivery.Step{}, true, ctx.Err()
}
return delivery.Step{}, false, nil
}
e := <-done
return e.step, true, e.err
}
// seatList asks the checking program for the heads' seat list, which the validator's and the composer's requests carry
// (ADR 0636 §4); the list and what it left out are the check's notes. A list it cannot make fails the check, naming why.
func (st *state) seatList(ctx context.Context) {
if st.narrowTry() {
return
}
body, err := json.Marshal(st.factsHead())
if err == nil {
var out json.RawMessage
if out, _, err = st.runProgram(ctx, []string{"-seats"}, body); err == nil {
var a rules.SeatsAnswer
if err = json.Unmarshal(out, &a); err == nil && a.V != rules.SeatsVersion {
err = fmt.Errorf("it answered a seat list of version %d, not %d", a.V, rules.SeatsVersion)
}
if err == nil {
st.c.Seats = a.Seats
st.c.note(fmt.Sprintf("the heads' seat list holds %d seat definitions", len(a.Seats)))
for _, l := range a.Left {
st.c.note("the heads' seat list leaves out " + l.Path + ": " + l.Why)
}
return
}
}
}
st.fail(goModule, "the heads' seat list could not be made by the checking program: "+err.Error())
}
// factsHead are the facts that name the check, without what the asking gathered.
func (st *state) factsHead() rules.Facts {
f := rules.Facts{V: rules.FactsVersion, Group: st.c.Group, Heads: st.c.Heads, Trunk: st.c.Trunk}
for _, p := range st.c.Pulls {
f.Members = append(f.Members, rules.Member{Repository: p.Repository, Number: p.Number, Head: p.Head, Trunk: p.Trunk})
}
return f
}
// judged is the checking program's answer to the check's facts (`-verdict`), with what judged; a program that cannot be
// asked, built or run, or does not answer a verdict, fails the check naming the checking program, never the running
// checker's rules in its place: the facts gain the failure and the verdict is worded from them as a failure.
func (st *state) judged(ctx context.Context) (rules.Answer, *delivery.JudgedBy) {
body, err := json.Marshal(st.factsOf())
var by *delivery.JudgedBy
if err == nil {
var out json.RawMessage
if out, by, err = st.runProgram(ctx, []string{"-verdict"}, body); err == nil {
var a rules.Answer
if err = json.Unmarshal(out, &a); err == nil && a.V != rules.AnswerVersion {
err = fmt.Errorf("it answered a verdict of version %d, not %d", a.V, rules.AnswerVersion)
}
if err == nil {
return a, by
}
}
}
st.fail(goModule, "the check could not be judged by the checking program: "+err.Error())
return rules.Answer{V: rules.AnswerVersion, Verdict: rules.Judge(st.factsOf())}, by
}
@@ -20,6 +20,7 @@ const (
VerdictsReadTool = "checker_verdicts"
PullReadTool = "checker_pull"
KeptReadTool = "checker_kept"
InputsReadTool = "checker_inputs"
// PreviewsReadTool reads the previews state (issue 1680); StatusReadTool reads what the checker runs (issue 1685: nothing holds it since R4g).
PreviewsReadTool = "checker_previews"
StatusReadTool = "checker_status"
@@ -36,6 +37,7 @@ type Reader struct {
VerdictState, KeptState, CandidateState StateRead
PreviewState StateRead // the checker's merge previews
InputState StateRead // the checking program's inputs (InputsState)
StepStates map[string]StateRead // the build modules' steps states, by module
HaveState StateRead // the node-engine's have: whether a build module is on a node and running
StepBound time.Duration // how long the checker waits for one step; DefaultStepBound when 0
@@ -257,6 +259,42 @@ func (r Reader) Kept(level, digest string) (map[string]json.RawMessage, string,
return out, cut, nil
}
// Inputs are the checking program's kept inputs: with a digest that input as kept, else every digest with its size.
func (r Reader) Inputs(digest string) (map[string]json.RawMessage, string, error) {
if r.InputState.Keys == nil || r.InputState.Get == nil {
return nil, "", fmt.Errorf("the state %s cannot be read here", InputsState)
}
if digest != "" {
raw, err := r.InputState.Get(digest)
if err != nil {
return nil, "", err
}
if len(raw) == 0 {
return nil, "", fmt.Errorf("no input %s is kept", digest)
}
return map[string]json.RawMessage{digest: raw}, "", nil
}
keys, err := r.InputState.Keys()
if err != nil {
return nil, "", err
}
sort.Strings(keys)
keys, cut := capKeys(keys, "give a digest to read one")
sizes := make([]int, len(keys))
if err := parallel(len(keys), func(i int) error {
raw, err := r.InputState.Get(keys[i])
sizes[i] = len(raw)
return err
}); err != nil {
return nil, "", err
}
out := map[string]json.RawMessage{}
for i, k := range keys {
out[k] = json.RawMessage(fmt.Sprintf(`{"size":%d}`, sizes[i]))
}
return out, cut, nil
}
// PreviewRead is one preview as stored, with the parts of its key.
type PreviewRead struct {
Key string `json:"key"`
@@ -114,6 +114,21 @@ func TestTheKeptToolAnswersTheKeysAsStoredByLevelAndByDigest(t *testing.T) { //
if _, _, err := rd.Kept("unit", digB); err == nil {
t.Error("a digest nothing kept was answered")
}
// Issue 1921: checker_inputs answers the checking program's inputs, every digest with its size, or one as kept.
inputs := newMemKV()
body := []byte(`{"v":1,"group":"feat/x"}`)
in, _ := delivery.RunInputOf(InputsState, body)
_ = memInputs{inputs}.Keep(context.Background(), in.Digest, body)
rd.InputState = kvRead(inputs)
if all, _, err := rd.Inputs(""); err != nil || string(all[in.Digest]) != fmt.Sprintf(`{"size":%d}`, len(body)) {
t.Errorf("inputs = %s, %v", all, err)
}
if one, _, err := rd.Inputs(in.Digest); err != nil || string(one[in.Digest]) != string(body) {
t.Errorf("one input = %s, %v", one, err)
}
if _, _, err := rd.Inputs(strings.Repeat("0", 64)); err == nil {
t.Error("an input nothing kept was answered")
}
}
// 1680 live acceptance: checker_previews shows the preview key of an open pull request, as stored, narrowed by repository and head.
@@ -254,7 +254,8 @@ func TestATryIsJudgedAsAGroupsCheckAndOwesTheForgeNothing(t *testing.T) { // ADR
if len(written.running) < 3 || len(written.running[1].Asks) != 1 || written.running[1].Steps[0].State != TryAsked {
t.Errorf("the running try's writes = %+v, want its ask named while it ran", written.running)
}
if len(got.Steps) != 2 || got.Steps[0].Node != "shanks" || got.Steps[0].State != delivery.StepPassed || len(got.Asks) != 2 {
// Two check asks and the checking program's two run asks (the seat list and the verdict).
if len(got.Steps) != 4 || got.Steps[0].Node != "shanks" || got.Steps[0].State != delivery.StepPassed || len(got.Asks) != 4 {
t.Errorf("the ended try's steps = %+v, asks %v", got.Steps, got.Asks)
}
for _, a := range got.Asks { // the delivery view counts their steps as a try's (build_usage)
@@ -20,6 +20,7 @@ import (
type state struct {
Name string `json:"name"`
History int `json:"history,omitempty"`
TTL int `json:"ttl-seconds,omitempty"`
}
type artifact struct {
@@ -101,7 +102,7 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1
t.Errorf("module = %q", m.Module)
}
// delivery.CheckerKeptHistory, CheckerOwedHistory, PreviewsHistory, CheckerVerdictsHistory; the names follow the SDK's buckets.
if want := []state{{Name: "asks", History: 1}, {Name: "hearing", History: 1}, {Name: "kept", History: 1}, {Name: "owed", History: 5}, {Name: "previews", History: 5}, {Name: "tries", History: 1}, {Name: "verdicts", History: 5}}; !reflect.DeepEqual(m.State, want) {
if want := []state{{Name: "asks", History: 1}, {Name: "hearing", History: 1}, {Name: "inputs", TTL: 86400}, {Name: "kept", History: 1}, {Name: "owed", History: 5}, {Name: "previews", History: 5}, {Name: "tries", History: 1}, {Name: "verdicts", History: 5}}; !reflect.DeepEqual(m.State, want) {
t.Errorf("state = %+v, want %+v", m.State, want)
}
if want := []string{"mesh-build-go.steps", "mesh-build-go.toolchains", "mesh-build-node.steps", "mesh-build-node.toolchains",
@@ -163,7 +164,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 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).
if want := []string{"checker_ask_again", "checker_kept", "checker_previews", "checker_pull", "checker_status", "checker_tries", "checker_try", "checker_try_end", "checker_try_read", "checker_verdicts"}; !reflect.DeepEqual(sorted(m.Tools), want) {
if want := []string{"checker_ask_again", "checker_inputs", "checker_kept", "checker_previews", "checker_pull", "checker_status", "checker_tries", "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)
}
// The try is the checker's own seat's verbs (ADR 0547 §10, issue 774), served beside the tools by the same code.
@@ -23,6 +23,7 @@
"state": [
{ "name": "asks", "history": 1 },
{ "name": "hearing", "history": 1 },
{ "name": "inputs", "ttl-seconds": 86400 },
{ "name": "kept", "history": 1 },
{ "name": "owed", "history": 5 },
{ "name": "previews", "history": 5 },
@@ -52,6 +53,7 @@
],
"tools": [
"checker_ask_again",
"checker_inputs",
"checker_kept",
"checker_previews",
"checker_pull",
@@ -0,0 +1,46 @@
package rules
import (
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"io/fs"
)
// Program is the checking program's work (cmd/mesh-checking-program; novox/hq ADR 0644, ADR 0656): over its arguments,
// its input (read only when asked for the verdict) and the heads laid for it (nil when none are), the one JSON line it
// answers, or why it refuses. `-seats` answers the seat list of the heads (SeatsOf), `-verdict` the verdict of the
// facts in the input (Judged), refusing facts of a version it does not read.
func Program(args []string, input func() ([]byte, error), heads fs.FS) ([]byte, error) {
set := flag.NewFlagSet("mesh-checking-program", flag.ContinueOnError)
set.SetOutput(io.Discard)
verdict := set.Bool("verdict", false, "answer the verdict of the facts in the input")
seats := set.Bool("seats", false, "answer the seat list of the heads laid for it")
if err := set.Parse(args); err != nil {
return nil, err
}
switch {
case *seats:
if heads == nil {
return nil, errors.New("no heads are laid for it")
}
answer, err := SeatsOf(heads)
if err != nil {
return nil, err
}
return json.Marshal(answer)
case *verdict:
data, err := input()
if err != nil {
return nil, fmt.Errorf("the input cannot be read: %w", err)
}
facts, err := DecodeFacts(data)
if err != nil {
return nil, err
}
return json.Marshal(Judged(facts))
}
return nil, errors.New("nothing asked: give -verdict or -seats")
}