None open is an answer: the checker lists open tries as [], the indexer takes null as none (issue 1921)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 21:10:42 +01:00
parent 8c67adaf91
commit 4ddf78395d
4 changed files with 37 additions and 6 deletions
@@ -795,7 +795,7 @@ func OpenTries(store TryStore) ([]tryevent.Open, error) {
}); err != nil {
return nil, err
}
var out []tryevent.Open
out := []tryevent.Open{} // none open is an empty list, never null: a reader tells it from no answer (issue 1921)
for _, o := range found {
if o != nil {
out = append(out, *o)
@@ -223,6 +223,8 @@ func TestATryIsJudgedAsAGroupsCheckAndOwesTheForgeNothing(t *testing.T) { // ADR
}
if open, err := OpenTries(memTries{kv}); err != nil || len(open) != 0 {
t.Errorf("open tries after the end = %v, %v", open, err)
} else if raw, _ := json.Marshal(open); string(raw) != "[]" { // issue 1921: none open is [] on the wire, never null
t.Errorf("none open is answered %s, want []", raw)
}
if got.State != TryPass || got.Took == "" || len(got.Members) != 2 || got.Members[0].Repository != repoA || got.Members[1].Head != head2 ||
got.Said == "" || got.Levels[delivery.LevelUnit].Run+got.Levels[delivery.LevelUnit].Kept != 4 { // the bench gives both members the same digests
@@ -306,12 +306,20 @@ func openTriesVia(call toolCaller) func(ctx context.Context) ([]tryevent.Open, e
if err != nil {
return nil, err
}
var o struct {
Tries *[]tryevent.Open `json:"tries"`
}
if err := json.Unmarshal(raw, &o); err != nil || o.Tries == nil {
// No try open is an answer: the checker's list is empty, or null in an older checker (issue 1921). An answer without the
// key is none.
var o map[string]json.RawMessage
if err := json.Unmarshal(raw, &o); err != nil {
return nil, fmt.Errorf("the answer of %s is not {tries}: %v", openTriesTool, err)
}
return *o.Tries, nil
list, ok := o["tries"]
if !ok {
return nil, fmt.Errorf("the answer of %s is not {tries}: no tries key", openTriesTool)
}
var tries []tryevent.Open
if err := json.Unmarshal(list, &tries); err != nil {
return nil, fmt.Errorf("the answer of %s is not {tries}: %v", openTriesTool, err)
}
return tries, nil
}
}
@@ -120,3 +120,24 @@ func TestReadViaReadsManifestsAndLocks(t *testing.T) {
t.Fatalf("read = %+v", got)
}
}
func TestTheOpenTriesAnswerTakesNoneOpenAsAnAnswer(t *testing.T) { // issue 1921: {"tries": null} refused the recovery for hours
for _, row := range []struct {
name, answer string
want int
refused bool
}{
{"one open", `{"tries":[{"id":"try-1","state":"running","whole":true,"members":[{"repository":"novox/a","head":"h","trunk":"t"}]}]}`, 1, false},
{"an empty list", `{"tries":[]}`, 0, false},
{"null, as an older checker answers none", `{"tries":null}`, 0, false},
{"no tries key", `{}`, 0, true},
{"not json", `nope`, 0, true},
{"tries of another shape", `{"tries":{"a":1}}`, 0, true},
} {
call := func(string, any) (json.RawMessage, error) { return json.RawMessage(row.answer), nil }
got, err := openTriesVia(call)(context.Background())
if (err != nil) != row.refused || len(got) != row.want {
t.Errorf("%s: %d tries, error %v; want %d, refused %v", row.name, len(got), err, row.want, row.refused)
}
}
}