fix(summer): exit 128+n when a delegated binary dies by a signal (WR-10, quick-261028-rqd)

This commit is contained in:
Jakub Zych
2026-10-28 20:52:57 +01:00
parent 4914cade5a
commit bb7e14bce9
4 changed files with 94 additions and 16 deletions

View File

@@ -293,7 +293,8 @@ func TestDelegateExamples(t *testing.T) {
// TestExitHelperProcess is not a real test: TestDelegateExitCodePassthrough
// re-executes the test binary with SUMMER_EXIT_HELPER set to get a child
// that prints an error on stderr and exits 2, or that is killed by SIGKILL.
// that prints an error on stderr and exits 2, or that is killed by SIGKILL
// or SIGTERM.
func TestExitHelperProcess(t *testing.T) {
switch os.Getenv("SUMMER_EXIT_HELPER") {
case "exit2":
@@ -304,6 +305,12 @@ func TestExitHelperProcess(t *testing.T) {
_ = p.Signal(syscall.SIGKILL)
time.Sleep(10 * time.Second)
os.Exit(3)
case "sigterm":
signal.Reset(syscall.SIGTERM)
p, _ := os.FindProcess(os.Getpid())
_ = p.Signal(syscall.SIGTERM)
time.Sleep(10 * time.Second)
os.Exit(3)
}
}
@@ -323,8 +330,8 @@ func exitHelperRunner(t *testing.T, mode string) {
// TestDelegateExitCodePassthrough (CR-02, WR-10): summer exits with the
// app binary's exit code and prints no error block of its own; a binary
// killed by a signal gets summer's error block, with secret flag values
// masked, and exit 1.
// killed by a signal exits 128 plus the signal number: SIGKILL gets
// summer's error block, with secret flag values masked, SIGTERM none.
func TestDelegateExitCodePassthrough(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("SIGKILL is POSIX-only")
@@ -361,8 +368,8 @@ func TestDelegateExitCodePassthrough(t *testing.T) {
t.Run("killed by a signal", func(t *testing.T) {
out, errOut, code := run(t, "sigkill", "migrate", "--bogus")
if code != bonfire.ExitFailure {
t.Errorf("exit %d, want 1", code)
if code != 137 {
t.Errorf("exit %d, want 137 (128+SIGKILL)", code)
}
if n := strings.Count(errOut, " ERROR "); n != 1 || !strings.Contains(errOut, "summer: migrate --bogus: signal: killed") {
t.Errorf("want one error block naming the signal, got %d:\nstdout %q\nstderr %q", n, out, errOut)
@@ -371,8 +378,8 @@ func TestDelegateExitCodePassthrough(t *testing.T) {
t.Run("killed by a signal masks secrets", func(t *testing.T) {
out, errOut, code := run(t, "sigkill", "migrate", "--password", "hunter2")
if code != bonfire.ExitFailure {
t.Errorf("exit %d, want 1", code)
if code != 137 {
t.Errorf("exit %d, want 137 (128+SIGKILL)", code)
}
if n := strings.Count(errOut, " ERROR "); n != 1 || !strings.Contains(errOut, "--password ***") {
t.Errorf("want one error block with the password masked, got %d:\nstdout %q\nstderr %q", n, out, errOut)
@@ -381,6 +388,56 @@ func TestDelegateExitCodePassthrough(t *testing.T) {
t.Errorf("the password reached the output:\nstdout %q\nstderr %q", out, errOut)
}
})
t.Run("terminated by SIGTERM", func(t *testing.T) {
out, errOut, code := run(t, "sigterm", "migrate", "--password", "hunter2")
if code != 143 {
t.Errorf("exit %d, want 143 (128+SIGTERM)", code)
}
if strings.Contains(out+errOut, " ERROR ") || strings.Contains(out+errOut, "hunter2") {
t.Errorf("summer printed a block for SIGTERM:\nstdout %q\nstderr %q", out, errOut)
}
})
}
// TestDelegateSignalExitCodes (WR-10 follow-up): a delegate that died by a
// signal makes summer exit 128 plus the signal number. SIGINT (130) and
// SIGTERM (143) count as already reported and print nothing; any other
// signal (SIGKILL, 137) gets summer's error block with secret flag values
// masked. The mapping is driven directly, so no signal is delivered.
func TestDelegateSignalExitCodes(t *testing.T) {
argv := []string{"migrate", "--password", "hunter2"}
cases := []struct {
sig syscall.Signal
code int
block bool
}{
{syscall.SIGINT, 130, false},
{syscall.SIGTERM, 143, false},
{syscall.SIGKILL, 137, true},
}
for _, tc := range cases {
t.Run(tc.sig.String(), func(t *testing.T) {
cmd := bonfire.Command{Name: "migrate", Passthrough: true, Run: func(context.Context, bonfire.Input, bonfire.Output) error {
return signalExit(argv, errors.New("signal: "+tc.sig.String()), tc.sig)
}}
var out, errW syncBuffer
root, err := bonfire.NewRootIO("summer", []bonfire.Command{cmd}, strings.NewReader(""), &out, &errW)
if err != nil {
t.Fatal(err)
}
if code := bonfire.Execute(context.Background(), root, argv); code != tc.code {
t.Errorf("exit %d, want %d", code, tc.code)
}
got := errW.String()
if !tc.block && got != "" {
t.Errorf("printed a block:\n%s", got)
}
if tc.block && (strings.Count(got, " ERROR ") != 1 || !strings.Contains(got, "summer: migrate --password ***") || strings.Contains(got, "hunter2")) {
t.Errorf("want one block with the argv masked:\n%s", got)
}
})
}
}
// TestRunAppCmdMasksSecrets (WR-10): the error of an app binary killed by a