srs/internal/signal/signal_test.go
winlin fe86846e3b OpenClaw: Rename proxy environment interface
Rename the proxy environment type and generated fake to clarify that the configuration surface belongs to the proxy server.

Update proxy bootstrap, load balancers, protocol servers, debug, signal handling, tests, and codebase memory to use ProxyEnvironment.
2026-04-26 11:56:28 -04:00

171 lines
3.8 KiB
Go

// Copyright (c) 2026 Winlin
//
// SPDX-License-Identifier: MIT
package signal
import (
"context"
"os"
"strings"
"sync"
"sync/atomic"
"syscall"
"testing"
"time"
"srsx/internal/env/envfakes"
)
// swapNotify replaces signalNotify with a capturing fake and returns a getter
// for the channel registered by the code under test plus a restore func.
func swapNotify(t *testing.T) (func() chan<- os.Signal, func()) {
t.Helper()
orig := signalNotify
var (
mu sync.Mutex
ch chan<- os.Signal
)
signalNotify = func(c chan<- os.Signal, _ ...os.Signal) {
mu.Lock()
defer mu.Unlock()
ch = c
}
return func() chan<- os.Signal {
mu.Lock()
defer mu.Unlock()
return ch
}, func() {
signalNotify = orig
}
}
func swapExit(t *testing.T) (*int32, chan int, func()) {
t.Helper()
orig := osExit
var called int32
done := make(chan int, 1)
osExit = func(code int) {
atomic.StoreInt32(&called, 1)
select {
case done <- code:
default:
}
// Block to mimic os.Exit never returning; the goroutine holding us
// here is abandoned when the test ends.
select {}
}
return &called, done, func() { osExit = orig }
}
func TestInstallSignals_CancelsOnSignal(t *testing.T) {
getCh, restore := swapNotify(t)
defer restore()
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
InstallSignals(ctx, cancel)
ch := getCh()
if ch == nil {
t.Fatal("signalNotify was not called")
}
ch <- syscall.SIGINT
select {
case <-ctx.Done():
case <-time.After(time.Second):
t.Fatal("ctx was not canceled after signal")
}
}
func TestInstallSignals_HandlesRepeatedSignals(t *testing.T) {
getCh, restore := swapNotify(t)
defer restore()
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
InstallSignals(ctx, cancel)
ch := getCh()
// Multiple signals must not panic; cancel() is idempotent.
ch <- syscall.SIGINT
ch <- syscall.SIGTERM
ch <- os.Interrupt
select {
case <-ctx.Done():
case <-time.After(time.Second):
t.Fatal("ctx was not canceled")
}
}
func TestInstallForceQuit_InvalidDurationReturnsError(t *testing.T) {
fakeEnv := &envfakes.FakeProxyEnvironment{}
fakeEnv.ForceQuitTimeoutReturns("not-a-duration")
err := InstallForceQuit(t.Context(), fakeEnv)
if err == nil {
t.Fatal("want error for bad duration")
}
if !strings.Contains(err.Error(), "parse force timeout") {
t.Fatalf("err = %v", err)
}
if !strings.Contains(err.Error(), "not-a-duration") {
t.Fatalf("err missing input: %v", err)
}
}
func TestInstallForceQuit_ExitsAfterTimeout(t *testing.T) {
called, done, restore := swapExit(t)
defer restore()
fakeEnv := &envfakes.FakeProxyEnvironment{}
fakeEnv.ForceQuitTimeoutReturns("1ms")
ctx, cancel := context.WithCancel(t.Context())
if err := InstallForceQuit(ctx, fakeEnv); err != nil {
t.Fatalf("unexpected err: %v", err)
}
// Before cancel, the goroutine is blocked and exit must not fire.
if atomic.LoadInt32(called) != 0 {
t.Fatal("osExit called before ctx cancel")
}
cancel()
select {
case code := <-done:
if code != 1 {
t.Fatalf("exit code = %d, want 1", code)
}
case <-time.After(time.Second):
t.Fatal("osExit not called after cancel + timeout")
}
}
func TestInstallForceQuit_WaitsForCancelBeforeSleeping(t *testing.T) {
called, done, restore := swapExit(t)
defer restore()
fakeEnv := &envfakes.FakeProxyEnvironment{}
fakeEnv.ForceQuitTimeoutReturns("10ms")
// Intentionally use a never-canceled context and leak the goroutine:
// if we canceled at test end, the goroutine would wake and race with
// restore() writing osExit.
if err := InstallForceQuit(context.Background(), fakeEnv); err != nil {
t.Fatalf("unexpected err: %v", err)
}
select {
case <-done:
t.Fatal("osExit fired without ctx cancel")
case <-time.After(30 * time.Millisecond):
}
if atomic.LoadInt32(called) != 0 {
t.Fatal("osExit called unexpectedly")
}
}