fix: use context.Cause for interrupt detection#53
Conversation
…coverage signal.NotifyContext sets a private signalError as the cancellation cause rather than context.Canceled, so errors.Is(err, context.Canceled) never matched the error net/http surfaces on interrupt. Compare against context.Cause(ctx) instead, and add a test exercising both SIGINT and SIGTERM against an in-flight request. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
PR Summary by QodoFix interrupt detection using context.Cause and add signal tests
AI Description
Diagram
High-Level Assessment
Files changed (2)
|
Code Review by Qodo
Context used✅ Compliance rules (platform):
36 rules 1. Signal test breaks Windows
|
| // TestRunInterrupted covers both signals run() registers via | ||
| // signal.NotifyContext (os.Interrupt and syscall.SIGTERM): either one must | ||
| // cancel an in-flight request and exit quietly with code 130. | ||
| func TestRunInterrupted(t *testing.T) { | ||
| signals := []struct { | ||
| name string | ||
| sig syscall.Signal | ||
| }{ | ||
| {"SIGINT", syscall.SIGINT}, | ||
| {"SIGTERM", syscall.SIGTERM}, | ||
| } | ||
|
|
||
| oldErr := os.Stderr | ||
| for _, tt := range signals { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| started := make(chan struct{}) | ||
| done := make(chan struct{}) | ||
|
|
||
| os.Stderr = ww | ||
| defer func() { | ||
| _ = ww.Close() | ||
| srv := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { | ||
| close(started) | ||
| <-done // held open until the test releases it, regardless of client-side cancellation | ||
| })) | ||
|
|
||
| os.Stderr = oldErr | ||
| _, _ = io.Copy(io.Discard, rr) | ||
| _ = rr.Close() | ||
| }() | ||
| defer func() { | ||
| close(done) | ||
| srv.Close() | ||
| }() | ||
|
|
||
| if code := run(); code != 1 { | ||
| t.Errorf("expected exit code 1, got %d", code) | ||
| t.Setenv("MINT_PROVIDER", "openai") | ||
| t.Setenv("MINT_API_KEY", "test") | ||
| t.Setenv("MINT_BASE_URL", srv.URL) | ||
| t.Setenv("MINT_MODEL_NAME", "test-model") | ||
|
|
||
| old := os.Args | ||
|
|
||
| os.Args = []string{"mint", "--target", "en", "hello"} | ||
| defer func() { os.Args = old }() | ||
|
|
||
| flushOut := captureStdout(t) | ||
| flushErr := captureStderr(t) | ||
|
|
||
| codeCh := make(chan int, 1) | ||
|
|
||
| go func() { codeCh <- run() }() | ||
|
|
||
| select { | ||
| case <-started: | ||
| case <-time.After(5 * time.Second): | ||
| t.Fatal("request never reached the server") | ||
| } | ||
|
|
||
| if err := syscall.Kill(os.Getpid(), tt.sig); err != nil { | ||
| t.Fatalf("failed to send %s: %v", tt.name, err) | ||
| } |
There was a problem hiding this comment.
1. Signal test breaks windows 🐞 Bug ☼ Reliability
TestRunInterrupted sends SIGINT/SIGTERM to the current process via syscall.Kill, which is Unix-specific and is likely to fail on Windows (compile-time missing/unsupported symbols or runtime not-supported behavior). This is a regression because the repo’s release configuration explicitly targets Windows builds.
Agent Prompt
### Issue description
`TestRunInterrupted` uses `syscall.Signal` constants (including `SIGTERM`) and `syscall.Kill` to send real signals to the test process. This is not portable and will fail on Windows, even though the project targets Windows releases.
### Issue Context
The repository’s GoReleaser config includes `windows` in the supported `goos` list. Tests should either be portable or correctly OS-gated.
### Fix Focus Areas
- cmd/mint/main_test.go[690-764]
- .goreleaser.yaml[5-22]
### Suggested fix
Do one of:
1) Move `TestRunInterrupted` into a new file with build tags, e.g. `cmd/mint/run_interrupted_unix_test.go`:
- Add `//go:build !windows` (or `//go:build unix`) at the top.
- Keep the rest of `main_test.go` portable.
2) Add an early skip inside the test:
- `if runtime.GOOS == "windows" { t.Skip("signal delivery via syscall.Kill not supported on Windows") }`
- If you still want partial coverage on Windows, you can run only the `os.Interrupt` case and skip `SIGTERM`.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Summary
signal.NotifyContextcancels the context with a privatesignalErrorcause, notcontext.Canceled, soerrors.Is(err, context.Canceled)never matched the errornet/httpsurfaces (which wrapscontext.Cause(ctx)) — an interrupted request could be misreported as a generic error (exit 1) instead of a clean interrupt (exit 130). Compare againstcontext.Cause(ctx)instead.posixLocaleconstant for the"POSIX"locale sentinel, reused ingetSystemLanguageand its tests.captureStdoutintocaptureOutput(parameterized by**os.File) and addedcaptureStderr, witht.Cleanupas a safety net if a test fails before calling the returned flush function.TestRunInterrupted, which drivesrun()against a held-open test server and sends realSIGINT/SIGTERMsignals to confirm both cancel the in-flight request and exit with code 130 and no stderr output.Test plan
go test -race -failfast -count=1 ./...make lint