Ask a check's digest as the candidate under the runner and plan as any other when the installed module ignores it, as ADR 0535 §2 and §4 judge (issue 1887 A5a)

This commit is contained in:
2026-10-23 16:04:01 +02:00
parent ba092d543f
commit 691c97ec31
3 changed files with 64 additions and 27 deletions
+28 -15
View File
@@ -226,7 +226,10 @@ func (s AskSteps) level(ctx context.Context, job delivery.CheckJob, level string
}
return true, nil
}
todo, all := s.plan(ctx, job, level, f, runner, withRunner, la)
todo, all, keepRunner := s.plan(ctx, job, level, f, runner, withRunner, la)
if !keepRunner {
runner = "" // an installed module that ignores the runner judges the level as it judges any other (ADR 0535 §2)
}
for _, priority := range []bool{true, false} {
args := withRunner(map[string]any{"workspace": f.Workspace, "level": level})
if job.Run != "" {
@@ -269,34 +272,44 @@ func (s AskSteps) release(workspace string) {
}
}
// plan digests the level and takes the kept verdicts: the packages left to test, or all when the level runs whole. It
// runs whole, digesting nothing, for a try of some tests (a partial run proves no package) and under the candidate's own
// runner (ADR 0535 §4: the candidate decides which verdicts would be taken, so every package runs); it runs whole and
// takes nothing when the digest names no package or does not cover the siblings the fetch placed (ADR 0444, L5).
func (s AskSteps) plan(ctx context.Context, job delivery.CheckJob, level string, f fetched, runner string, withRunner func(map[string]any) map[string]any, la *delivery.LevelAnswer) ([]string, bool) {
if job.Run != "" || runner != "" {
return nil, true
// plan digests the level and takes the kept verdicts: the packages left to test, or all when the level runs whole, and
// whether the runner stays. A try of some tests runs whole, digesting nothing (a partial run proves no package). Under the
// candidate's runner the digest is asked as the candidate: when the candidate answers it, every package runs and nothing
// is digested for keeping (ADR 0535 §4); when the installed module answers it (it does not take the runner), the level is
// planned as any other and the runner is dropped. The level runs whole and takes nothing when the digest names no package
// or does not cover the siblings the fetch placed (ADR 0444, L5).
func (s AskSteps) plan(ctx context.Context, job delivery.CheckJob, level string, f fetched, runner string, withRunner func(map[string]any) map[string]any, la *delivery.LevelAnswer) ([]string, bool, bool) {
if job.Run != "" {
return nil, true, true
}
out, err := s.Digest(ctx, withRunner(map[string]any{"workspace": f.Workspace, "level": level}))
if err != nil {
if s.Logf != nil {
s.Logf("no digest of the %s level, so it runs whole: %v", level, err)
}
return nil, true
return nil, true, true
}
r, err := raw(out)
if err != nil {
return nil, true
return nil, true, true
}
var d struct {
Packages map[string]string `json:"packages"`
Beside []string `json:"beside"`
JudgedBy string `json:"judged_by"`
}
if err := json.Unmarshal(r, &d); err != nil || len(d.Packages) == 0 {
return nil, true
if err := json.Unmarshal(r, &d); err != nil {
return nil, true, true
}
if !slices.Equal(d.Beside, besideOf(job.Fetch)) {
return nil, true // a digest that does not cover the siblings keys no verdict: the level runs whole, keeping nothing
if runner != "" {
if strings.HasPrefix(d.JudgedBy, "candidate ") {
la.Judged = d.JudgedBy
return nil, true, true // the candidate decides which verdicts would be taken: all run, nothing kept
}
runner = ""
}
if len(d.Packages) == 0 || !slices.Equal(d.Beside, besideOf(job.Fetch)) {
return nil, true, runner != "" // a digest that names nothing or not the siblings keys no verdict: the level runs whole
}
la.Digest = r
pkgs := make([]string, 0, len(d.Packages))
@@ -318,7 +331,7 @@ func (s AskSteps) plan(ctx context.Context, job delivery.CheckJob, level string,
}
todo = append(todo, p)
}
return todo, false
return todo, false, false
}
// besideOf are the siblings a fetch's arguments place, as a digest echoes them: `<owner/repo> <commit>`, sorted.
+32 -11
View File
@@ -140,19 +140,40 @@ test {"level":"unit","packages":["x/pkg/b"],"workspace":"/w"}`
}
}
// ADR 0535 §4: a merge that changes the build module's own code is judged by the candidate's runner, every package run
// and nothing digested or kept; a digest that does not cover the fetch's siblings runs the level whole too.
// ADR 0535 §2, §4: a merge that changes the build module's own code is judged by the candidate's runner: its digest says
// the candidate ran, and every package runs with nothing kept; an installed module that ignores the runner is planned as
// any other, the runner dropped. A digest that does not cover the fetch's siblings runs the level whole too.
func TestRunAskRunsWholeUnderTheRunnerOrWithoutTheSiblings(t *testing.T) {
r := &recorder{fetch: map[string]any{"workspace": "/w", "changed": []string{"modules/mesh-build-go/x.go"}},
digests: map[string]string{"x/pkg/a": "d-kept"}, tests: map[string]bool{}}
if _, err := RunAsk(context.Background(), r.steps(), ask(delivery.CheckJob{Levels: []string{delivery.LevelUnit}, Kept: true,
Runner: "workspace", RunnerWhen: "modules/mesh-build-go/", Fetch: map[string]any{"repository": "novox/x", "commit": runCommit}})); err != nil {
t.Fatal(err)
for _, tc := range []struct{ judged, want string }{
{"candidate 999", `digest {"level":"unit","runner":"workspace","workspace":"/w"}
test {"level":"unit","runner":"workspace","workspace":"/w"}`},
{"installed", `digest {"level":"unit","runner":"workspace","workspace":"/w"}`},
} {
r := &recorder{fetch: map[string]any{"workspace": "/w", "changed": []string{"modules/mesh-build-go/x.go"}},
digests: map[string]string{"x/pkg/a": "d-kept"}, tests: map[string]bool{}}
steps := r.steps()
digest := steps.Digest
steps.Digest = func(ctx context.Context, args map[string]any) (any, error) {
_, _ = digest(ctx, args)
return map[string]any{"level": "unit", "packages": r.digests, "judged_by": tc.judged}, nil
}
out, err := RunAsk(context.Background(), steps, ask(delivery.CheckJob{Levels: []string{delivery.LevelUnit}, Kept: true,
Runner: "workspace", RunnerWhen: "modules/mesh-build-go/", Fetch: map[string]any{"repository": "novox/x", "commit": runCommit}}))
if err != nil {
t.Fatal(err)
}
if got := strings.Join(r.calls[1:], "\n"); got != tc.want {
t.Errorf("judged by %s: calls\n%s\nwant\n%s", tc.judged, got, tc.want)
}
l := out.(delivery.CheckAnswer).Levels[0]
if tc.judged == "installed" && (len(l.Kept) != 1 || l.Judged != "") {
t.Errorf("an installed module's level %+v, want the kept verdict taken", l)
}
if tc.judged != "installed" && (len(l.Kept) != 0 || len(l.Digest) != 0 || l.Judged != tc.judged) {
t.Errorf("the candidate's level %+v, want nothing kept and the program named", l)
}
}
if got := strings.Join(r.calls[1:], "\n"); got != `test {"level":"unit","runner":"workspace","workspace":"/w"}` {
t.Fatalf("under the runner: %s", got)
}
r = &recorder{fetch: map[string]any{"workspace": "/w"}, digests: map[string]string{"x/pkg/a": "d-kept"}, tests: map[string]bool{}}
r := &recorder{fetch: map[string]any{"workspace": "/w"}, digests: map[string]string{"x/pkg/a": "d-kept"}, tests: map[string]bool{}}
out, err := RunAsk(context.Background(), r.steps(), ask(delivery.CheckJob{Levels: []string{delivery.LevelUnit}, Kept: true,
Fetch: map[string]any{"repository": "novox/x", "commit": runCommit, "beside": []any{map[string]any{"repository": "novox/mesh-sdk", "commit": runCommit}}}}))
if err != nil {
+4 -1
View File
@@ -346,7 +346,10 @@ type CheckAnswer struct {
// LevelAnswer is one level of a check ask: the digest's answer (absent when nothing was digested: the level ran whole and
// keeps nothing), the kept verdicts taken, and one test result per test step run, in the order run.
type LevelAnswer struct {
Level string `json:"level"`
Level string `json:"level"`
// Judged is what the digest under the candidate's runner said of the program that ran it ("candidate <merge>"),
// when the candidate judged the level (ADR 0535 §4, §5); empty otherwise.
Judged string `json:"judged,omitempty"`
Digest json.RawMessage `json:"digest,omitempty"`
Kept []KeptTaken `json:"kept,omitempty"`
Tests []json.RawMessage `json:"tests,omitempty"`