fix(lifecycle): preserve mixed shutdown error identity

This commit is contained in:
end committed 2026-10-27 12:04:13 -07:00
1 parent b9a5fa8bd8
commit 64c7e6b65d
3 files changed
+91 -28

No files matched your search

+70 -2
View File
@@ -541,7 +541,75 @@ func TestMixedShutdownErrorPreservesCause(t *testing.T) {
if !errors.As(err, &pathErr) || pathErr != independent || !errors.Is(err, io.ErrUnexpectedEOF) {
t.Fatalf("independent cause lost: %v", err)
}
if errors.Is(err, context.Canceled) {
t.Fatalf("expected cancellation retained: %v", err)
if !errors.Is(err, context.Canceled) {
t.Fatalf("mixed wrapper was reconstructed: %v", err)
}
}
// matchingError exercises wrapper-owned matching independently of its child.
type matchingError struct {
child error
match error
}
func (e *matchingError) Error() string { return "custom: " + e.child.Error() }
func (e *matchingError) Unwrap() error { return e.child }
func (e *matchingError) Is(target error) bool { return target == e.match }
func TestMixedShutdownErrorPreservesWrapper(t *testing.T) {
for _, kind := range []string{"path", "custom"} {
t.Run(kind, func(t *testing.T) {
signals := make(chan os.Signal, 2)
a := newApp(t, signals, time.Second)
ready := make(chan struct{})
match := errors.New("custom match")
var original error
add(t, a, Module{Name: "mixed", Run: func(ctx context.Context) error {
close(ready)
<-ctx.Done()
joined := errors.Join(ctx.Err(), io.ErrUnexpectedEOF)
if kind == "path" {
original = &os.PathError{Op: "read", Path: "queue", Err: joined}
} else {
original = &matchingError{child: joined, match: match}
}
return original
}})
done := run(a, context.Background())
wait(t, ready)
signals <- syscall.SIGTERM
err := receive(t, done)
if !errors.Is(err, original) || !errors.Is(err, io.ErrUnexpectedEOF) {
t.Fatalf("wrapper identity or inner failure lost: %v", err)
}
if kind == "path" {
var pathErr *os.PathError
if !errors.As(err, &pathErr) || pathErr != original {
t.Fatalf("typed wrapper lost: %v", err)
}
} else {
var custom *matchingError
if !errors.As(err, &custom) || custom != original || !errors.Is(err, match) {
t.Fatalf("custom matching lost: %v", err)
}
}
})
}
}
func TestCancellationOnly(t *testing.T) {
for _, err := range []error{
context.Canceled,
fmt.Errorf("wrapped: %w", context.Canceled),
&os.PathError{Op: "read", Path: "queue", Err: errors.Join(context.Canceled, fmt.Errorf("wrapped: %w", context.Canceled))},
} {
if !cancellationOnly(err, context.Canceled) {
t.Errorf("expected cancellation not recognized: %v", err)
}
}
for _, err := range []error{nil, io.ErrUnexpectedEOF, context.DeadlineExceeded, errors.Join(context.Canceled, io.ErrUnexpectedEOF)} {
if cancellationOnly(err, context.Canceled) {
t.Errorf("independent error classified as cancellation: %v", err)
}
}
}