fix(cli): gate silent-abort on signal handler firing; test exit codes · Entire

fix(cli): gate silent-abort on signal handler firing; test exit codes

54a30ec→main·toothbrush·2w ago·2 files·+51 added/-6 removed

Addresses two trail findings:

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

Changes

2

88 unmodified lines

89
90
91
92
93
94
95
96
97
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108

88 unmodified lines

var silent *cli.SilentError

switch {
    case errors.Is(err, context.Canceled):
        // Aborted (a signal cancelled the root context). Don't dump the
        // raw transport/keyring cancellation string ("...: context
        // canceled", "read access token: signal: interrupt") as if it
        // were a failure — die quietly by re-raising the signal that
        // triggered it (see dieFromSignal) so an enclosing
    case errors.Is(err, context.Canceled) && caughtSignal.Load() != nil:
        // A signal cancelled the root context (our handler fired). Don't
        // dump the raw transport/keyring cancellation string ("...:
        // context canceled", "read access token: signal: interrupt") as
        // if it were a failure — die quietly by re-raising the signal
        // that triggered it (see dieFromSignal) so an enclosing
        // `while ...; do entire; done` loop actually breaks on a single
        // Ctrl-C, and a SIGTERM shutdown still exits 143.
        //
        // We gate on the handler having fired rather than on the error
        // type alone: a context.Canceled that arose without a signal
        // (e.g. an internally-cancelled sub-context) is a genuine error
        // and must fall through to normal reporting, not masquerade as a
        // user abort (which would also wrongly break an enclosing loop).
        cancel()
        dieFromSignal(terminatingSignal())
    case errors.As(err, &silent):

Mcmd/entire/main.go+12/-6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

package main

import (
    "os"
    "syscall"
    "testing"
)

// nonNumericSignal is an os.Signal that isn't a syscall.Signal, exercising
// exitCodeForSignal's fallback branch.
type nonNumericSignal struct{}

func (nonNumericSignal) String() string { return "non-numeric" }
func (nonNumericSignal) Signal() {}

// TestExitCodeForSignal locks the conventional 128+signum mapping the
// Ctrl-C/SIGTERM fix relies on, so a future "simplification" back to a
// hardcoded 130 can't silently regress SIGTERM's 143.
func TestExitCodeForSignal(t *testing.T) {
    t.Parallel()

tests := []struct {
    name string
    sig  os.Signal
    want int
}{
    {"SIGINT", os.Interrupt, 130},
    {"SIGTERM", syscall.SIGTERM, 143},
    {"non-numeric signal falls back to 130", nonNumericSignal{}, 130},
}
for _, tc := range tests {
    t.Run(tc.name, func(t *testing.T) {
        t.Parallel()
        if got := exitCodeForSignal(tc.sig); got != tc.want {
            t.Errorf("exitCodeForSignal(%v) = %d, want %d", tc.sig, got, tc.want)
        }
    })
}
}

Acmd/entire/main_test.go+39