D5: checker_ask_again takes a failed verdict whose failing steps all ended on a transport fault (ADR 0664, operator answer 5, issue 1748)
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
@@ -397,7 +397,7 @@ func RecoveryTools(r check.Reader, w check.VerdictEntry) []stdio.Tool {
|
||||
return []stdio.Tool{{Name: check.AskAgainTool,
|
||||
Description: "Ask a candidate's check again by hand, whose verdict stays running while nothing runs for it (" + check.StatusReadTool + " names it " +
|
||||
"under lost): its verdict is ended as asked again, saying why, and the checker, which hears its verdicts, stops the wait that judged nothing " +
|
||||
"and asks the check anew, whole. Refused for a group with no candidate, a verdict that is not running (a pass or failure stands until a push or " +
|
||||
"and asks the check anew, whole. Refused for a group with no candidate, a verdict that is not running or a failure whose failing steps did not all end on a transport fault (a pass, or any other failure, stands until a push or " +
|
||||
"a trunk move), and while a step of its pull requests can still run on a build module (its end judges it). The checker reads a step that " +
|
||||
"ended while its watch was deaf by itself at the next step end of its build module; this is for what that does not reach.",
|
||||
Input: map[string]any{"type": "object", "required": []string{"group", "why"}, "properties": map[string]any{
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
)
|
||||
|
||||
// **A check is asked again by hand** (the lost check of 2026-10-28; tools-are-how-a-module-is-spoken-to: every recovery
|
||||
@@ -92,7 +93,7 @@ type AskedAgain struct {
|
||||
}
|
||||
|
||||
// AskAgain ends the running verdict of the group's candidate as asked again by hand, saying why. It refuses a group with
|
||||
// no candidate, a verdict that is not running (a pass or a failure stands until a push or a trunk move), one a step of
|
||||
// no candidate, a verdict that is not running (a pass stands, and a failure unless every failing step ended on a transport fault, ADR 0664; both stand until a push or a trunk move), one a step of
|
||||
// whose pull requests can still run, and one whose asks are not known or one of which no node took yet.
|
||||
func (r Reader) AskAgain(w VerdictEntry, group, why string) (AskedAgain, error) {
|
||||
why = strings.TrimSpace(why)
|
||||
@@ -121,15 +122,26 @@ func (r Reader) AskAgain(w VerdictEntry, group, why string) (AskedAgain, error)
|
||||
if err != nil {
|
||||
return AskedAgain{}, fmt.Errorf("%s holds what is not a verdict: %w", key, err)
|
||||
}
|
||||
if v.State != meshdeliverycheckerclient.VerdictRunning {
|
||||
failed := v.State == meshdeliverycheckerclient.VerdictFail
|
||||
if v.State != meshdeliverycheckerclient.VerdictRunning && !failed {
|
||||
return AskedAgain{}, fmt.Errorf("%s is %s, not running: a verdict stands until a push or a trunk move asks it again", key, v.State)
|
||||
}
|
||||
steps := r.readSteps()
|
||||
if failed {
|
||||
// ADR 0664 (operator answer 5, issue 1748): a failure stands, except one whose failing steps all ended on a transport
|
||||
// fault (a step written passing): the network failed, not the change, so it is asked again like a lost check.
|
||||
if err := steps.onlyTransportFaults(c); err != nil {
|
||||
return AskedAgain{}, fmt.Errorf("%s is fail, and a failure stands until a push or a trunk move asks it again, unless every failing step of it ended on a transport fault: %w", key, err)
|
||||
}
|
||||
}
|
||||
for _, rs := range steps.running {
|
||||
if asksOf(c, rs.What) {
|
||||
return AskedAgain{}, fmt.Errorf("a step of it runs: %s %s on %s since %s (%s); its end judges it", rs.Module, rs.ID, rs.Node, rs.Since, rs.What)
|
||||
}
|
||||
}
|
||||
if failed {
|
||||
return r.askedAgain(w, key, rev, v, why)
|
||||
}
|
||||
if r.AskState.Get == nil {
|
||||
return AskedAgain{}, fmt.Errorf("the checker's asks are not readable here, so whether its ask waits in a build seat's stream cannot be told")
|
||||
}
|
||||
@@ -151,6 +163,31 @@ func (r Reader) AskAgain(w VerdictEntry, group, why string) (AskedAgain, error)
|
||||
"the seats' backlog is in mesh-delivery-view.build_usage", id)
|
||||
}
|
||||
}
|
||||
return r.askedAgain(w, key, rev, v, why)
|
||||
}
|
||||
|
||||
// onlyTransportFaults is nil when the candidate's pull requests have a failed or refused step and every one of them ended
|
||||
// passing (buildstep.Step.Passing); else why not.
|
||||
func (s stepsRead) onlyTransportFaults(c meshcatalog.CandidatesByGroupValue) error {
|
||||
n := 0
|
||||
for _, ms := range s.all {
|
||||
st := ms.step
|
||||
if !asksOf(c, st.What) || (st.State != buildstep.StepFailed && st.State != buildstep.StepRefused) {
|
||||
continue
|
||||
}
|
||||
if !st.Passing {
|
||||
return fmt.Errorf("its step %s on %s ended %s and not on a transport fault: %s", st.ID, st.Node, st.State, st.Reason)
|
||||
}
|
||||
n++
|
||||
}
|
||||
if n == 0 {
|
||||
return errors.New("no failed step of it is read (the newest keys of each build module's steps are)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// askedAgain writes the verdict v ended as asked again by hand, why saying so.
|
||||
func (r Reader) askedAgain(w VerdictEntry, key string, rev uint64, v meshdeliverychecker.VerdictsByGroupHeadsValue, why string) (AskedAgain, error) {
|
||||
words := AskedAgainWords + why
|
||||
ended := meshdeliverychecker.VerdictsByGroupHeadsValue{Group: v.Group, Heads: v.Heads, Trunk: v.Trunk, State: meshdeliverycheckerclient.VerdictFail,
|
||||
Failed: &FailedFirst{Machine: "checker", Words: words}}
|
||||
|
||||
@@ -3,6 +3,9 @@ package check
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -347,7 +350,40 @@ func TestACheckWhoseAskJudgedNothingIsAskedAgainOrRead(t *testing.T) {
|
||||
if _, err := rd.AskAgain(asVerdicts{r.verdicts}, "feat/x", "again"); err == nil {
|
||||
t.Error("a judged check was asked again")
|
||||
}
|
||||
// ADR 0664 (issue 1748, operator answer 5): a failed verdict whose failing steps all ended on a transport fault
|
||||
// is asked again; one with a failing step that did not, or with none read, stands.
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
passing []bool
|
||||
ok bool
|
||||
}{{"every failing step on a transport fault", []bool{true, true}, true}, {"one step failed on the source", []bool{true, false}, false}, {"no failed step read", nil, false}} {
|
||||
failedKV := newMemKV()
|
||||
judgedRaw, _, _, _ := r.verdicts.Get(ctx, key)
|
||||
failed, _ := meshdeliverycheckerclient.DecodeVerdict(judgedRaw)
|
||||
failed.State, failed.Failed = meshdeliverycheckerclient.VerdictFail, &FailedFirst{Machine: "ace", Words: "a transport fault on 3 runs"}
|
||||
raw, _ := meshdeliverycheckerclient.EncodeVerdict(failed)
|
||||
failedKV.Create(ctx, key, raw)
|
||||
vals := map[string]json.RawMessage{}
|
||||
for i, p := range c.passing {
|
||||
id := fmt.Sprint("mvl0frv18d19", 80+i)
|
||||
vals["ace."+id], _ = buildstep.EncodeStep(buildstep.Step{ID: id, Node: "ace", Tool: "check ask", State: buildstep.StepFailed, Passing: p, Ended: time.Now(), What: "check " + repoA + "@" + short12(head1)})
|
||||
}
|
||||
rd := Reader{CandidateState: kvRead(cands2(t, cand)), VerdictState: kvRead(failedKV), AskState: kvRead(newMemKV()), StepStates: map[string]StateRead{buildrun.GoBuildModule: {
|
||||
Keys: func() ([]string, error) { return slices.Collect(maps.Keys(vals)), nil }, Get: func(k string) (json.RawMessage, error) { return vals[k], nil }}}}
|
||||
if got, err := rd.AskAgain(asVerdicts{failedKV}, "feat/x", "the network"); (err == nil) != c.ok || (c.ok && !strings.HasPrefix(got.Said, AskedAgainWords)) {
|
||||
t.Errorf("%s: asked again = %+v, %v, want asked again %v", c.name, got, err, c.ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// cands2 is a candidates state holding cand under feat/x.
|
||||
func cands2(t *testing.T, cand []byte) *memKV {
|
||||
t.Helper()
|
||||
kv := newMemKV()
|
||||
ckey, _ := meshcatalogclient.CandidatesKey("feat/x")
|
||||
_, _ = kv.Create(context.Background(), ckey, cand)
|
||||
return kv
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user