diff --git a/cmd/mesh-controller/delivery.go b/cmd/mesh-controller/delivery.go index 9445a213..c24b2f43 100644 --- a/cmd/mesh-controller/delivery.go +++ b/cmd/mesh-controller/delivery.go @@ -902,16 +902,14 @@ func deliveryCommand(ctx context.Context, args []string) error { if *group == "" && len(members) == 1 { // One head, checked again as the forge's announcement would have it (a recheck): the controller's // own path for a pull request, run because the delivery's owner asked. - // Its prose goes to standard error: standard output is the answer's JSON alone, or the seat verb - // reads no answer from it (novox/hq issue 1311). - var asked string - if err := sayingOnTheBus(ctx, inv, func() (err error) { - asked, err = (following{open: open, oneShot: true}).pullUpdated(ctx, members[0].pull(), os.Stderr) - return err - }); err != nil { - return err - } - return answer(recheckAnswer(asked, members[0].ID)) + return recheckOneHead(members[0].ID, answer, func() (string, error) { + var asked string + err := sayingOnTheBus(ctx, inv, func() (err error) { + asked, err = (following{open: open, oneShot: true}).pullUpdated(ctx, members[0].pull(), os.Stderr) + return err + }) + return asked, err + }) } if *group == "" || len(members) < 2 { return errors.New("delivery check --group --members (two heads or more), or --members ") @@ -1388,3 +1386,19 @@ func overNATS(conn *nats.Conn) (link.Bus, error) { func recheckAnswer(asked, member string) map[string]any { return map[string]any{"asked": asked, "rechecked": member} } + +// recheckOneHead runs a one-head `delivery check` and answers it as data. Standard output is the answer's JSON alone, +// or the seat verb reads no answer from it (novox/hq issue 1311): while the check runs, whatever it prints to standard +// output (the controller's own path prints its prose there, as do the stores it reads) goes to standard error. +func recheckOneHead(member string, answer func(any) error, ask func() (string, error)) error { + real := os.Stdout + os.Stdout = os.Stderr + asked, err := func() (string, error) { + defer func() { os.Stdout = real }() + return ask() + }() + if err != nil { + return err + } + return answer(recheckAnswer(asked, member)) +} diff --git a/cmd/mesh-controller/delivery_check_answer_test.go b/cmd/mesh-controller/delivery_check_answer_test.go index 9fe9d395..21eee54d 100644 --- a/cmd/mesh-controller/delivery_check_answer_test.go +++ b/cmd/mesh-controller/delivery_check_answer_test.go @@ -4,6 +4,8 @@ import ( "bytes" "context" "encoding/json" + "fmt" + "os" "strings" "testing" @@ -44,3 +46,40 @@ func TestSayCheckedWritesTheSameLineToItsWriter(t *testing.T) { t.Fatalf("sayChecked said %q; want %q", w.String(), want) } } + +// Whatever the check prints to standard output while it runs (checks.go namedForTheCheck, the inventory's pointer +// repair) must not reach the answer's standard output: it is exactly one JSON document (novox/hq issue 1311). +func TestOneHeadRecheckLeavesStandardOutputTheAnswerAlone(t *testing.T) { + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + e, ew, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + realOut, realErr := os.Stdout, os.Stderr + os.Stdout, os.Stderr = w, ew + runErr := recheckOneHead("novox/x@abc", func(v any) error { return json.NewEncoder(os.Stdout).Encode(v) }, + func() (string, error) { + fmt.Println(" the earlier checks' builds could not be read, so the check builds") + fmt.Fprintln(os.Stdout, "repaired a pointer") + return "check-42", nil + }) + os.Stdout, os.Stderr = realOut, realErr + w.Close() + ew.Close() + if runErr != nil { + t.Fatal(runErr) + } + var out, errOut bytes.Buffer + _, _ = out.ReadFrom(r) + _, _ = errOut.ReadFrom(e) + var doc map[string]any + if err := json.Unmarshal(out.Bytes(), &doc); err != nil || doc["asked"] != "check-42" || doc["rechecked"] != "novox/x@abc" { + t.Fatalf("standard output %q is not the answer alone (%v)", out.String(), err) + } + if !strings.Contains(errOut.String(), "repaired a pointer") { + t.Fatalf("the prose should be on standard error: %q", errOut.String()) + } +}