@@ -33,6 +33,7 @@ import (
3333 "github.com/malbeclabs/doublezero/client/doublezerod/internal/liveness"
3434 "github.com/malbeclabs/doublezero/client/doublezerod/internal/pim"
3535 "github.com/malbeclabs/doublezero/client/doublezerod/internal/runtime"
36+ "github.com/stretchr/testify/require"
3637 "golang.org/x/net/ipv4"
3738 "golang.org/x/sys/unix"
3839
@@ -1649,12 +1650,11 @@ func TestServiceNoCoExistence(t *testing.T) {
16491650 }()
16501651
16511652 errChan := make (chan error , 1 )
1652- ctx , _ := context .WithCancel (context .Background ())
16531653
16541654 sockFile := filepath .Join (rootPath , "doublezerod.sock" )
16551655 go func () {
16561656 programId := ""
1657- err := runtime .Run (ctx , sockFile , "" , false , false , programId , "" , 30 , 30 , newTestLivenessManagerConfig ())
1657+ err := runtime .Run (t . Context () , sockFile , "" , false , false , programId , "" , 30 , 30 , newTestLivenessManagerConfig ())
16581658 errChan <- err
16591659 }()
16601660
@@ -2052,6 +2052,120 @@ func TestServiceCoexistence(t *testing.T) {
20522052 })
20532053}
20542054
2055+ func TestRuntime_Run_ReturnsOnContextCancel (t * testing.T ) {
2056+ errChan := make (chan error , 1 )
2057+ ctx , cancel := context .WithCancel (t .Context ())
2058+ defer cancel ()
2059+
2060+ rootPath , err := os .MkdirTemp ("" , "doublezerod" )
2061+ require .NoError (t , err )
2062+ defer os .RemoveAll (rootPath )
2063+ t .Setenv ("XDG_STATE_HOME" , rootPath )
2064+
2065+ path := filepath .Join (rootPath , "doublezerod" )
2066+ if err := os .Mkdir (path , 0766 ); err != nil {
2067+ t .Fatalf ("error creating state dir: %v" , err )
2068+ }
2069+
2070+ sockFile := filepath .Join (rootPath , "doublezerod.sock" )
2071+ go func () {
2072+ programId := ""
2073+ err := runtime .Run (ctx , sockFile , "" , false , false , programId , "" , 30 , 30 , newTestLivenessManagerConfig ())
2074+ errChan <- err
2075+ }()
2076+
2077+ // Give the runtime a moment to start, then cancel the context to force exit.
2078+ select {
2079+ case err := <- errChan :
2080+ require .NoError (t , err )
2081+ case <- time .After (300 * time .Millisecond ):
2082+ }
2083+
2084+ cancel ()
2085+ select {
2086+ case err := <- errChan :
2087+ require .NoError (t , err )
2088+ case <- time .After (5 * time .Second ):
2089+ t .Fatalf ("timed out waiting for runtime to exit after context cancel" )
2090+ }
2091+ }
2092+
2093+ func TestRuntime_Run_PropagatesLivenessStartupError (t * testing.T ) {
2094+ errChan := make (chan error , 1 )
2095+ ctx , cancel := context .WithCancel (t .Context ())
2096+ defer cancel ()
2097+
2098+ rootPath , err := os .MkdirTemp ("" , "doublezerod" )
2099+ require .NoError (t , err )
2100+ defer os .RemoveAll (rootPath )
2101+ t .Setenv ("XDG_STATE_HOME" , rootPath )
2102+
2103+ // Invalid liveness config (port < 0) -> NewManager.Validate() error.
2104+ bad := * newTestLivenessManagerConfig ()
2105+ bad .Port = - 1
2106+
2107+ sockFile := filepath .Join (rootPath , "doublezerod.sock" )
2108+ go func () {
2109+ programId := ""
2110+ err := runtime .Run (ctx , sockFile , "" , false , false , programId , "" , 30 , 30 , & bad )
2111+ errChan <- err
2112+ }()
2113+
2114+ select {
2115+ case err := <- errChan :
2116+ require .Error (t , err )
2117+ require .Contains (t , err .Error (), "port must be greater than or equal to 0" )
2118+ case <- time .After (5 * time .Second ):
2119+ t .Fatalf ("expected startup error from runtime.Run with bad liveness config" )
2120+ }
2121+ }
2122+
2123+ func TestRuntime_Run_PropagatesLivenessError_FromUDPClosure (t * testing.T ) {
2124+ errCh := make (chan error , 1 )
2125+ ctx , cancel := context .WithCancel (context .Background ())
2126+ defer cancel ()
2127+
2128+ // Minimal state dir + socket path
2129+ rootPath , err := os .MkdirTemp ("" , "doublezerod" )
2130+ if err != nil {
2131+ t .Fatalf ("mktemp: %v" , err )
2132+ }
2133+ defer os .RemoveAll (rootPath )
2134+ t .Setenv ("XDG_STATE_HOME" , rootPath )
2135+ sockFile := filepath .Join (rootPath , "doublezerod.sock" )
2136+
2137+ // Create a real UDPService we can close to induce a receiver error.
2138+ udp , err := liveness .ListenUDP ("127.0.0.1" , 0 )
2139+ if err != nil {
2140+ t .Fatalf ("ListenUDP: %v" , err )
2141+ }
2142+
2143+ // Build a liveness config that uses our injected UDP service.
2144+ cfg := newTestLivenessManagerConfig ()
2145+ cfg .UDP = udp
2146+ cfg .PassiveMode = true
2147+
2148+ // Start the runtime.
2149+ go func () {
2150+ programID := ""
2151+ errCh <- runtime .Run (ctx , sockFile , "" , false , false , programID , "" , 30 , 30 , cfg )
2152+ }()
2153+
2154+ // Give the liveness receiver a moment to start, then close the UDP socket.
2155+ time .Sleep (200 * time .Millisecond )
2156+ _ = udp .Close ()
2157+
2158+ // The receiver should error, Manager should send on lm.Err(), and Run should return that error.
2159+ select {
2160+ case err := <- errCh :
2161+ if err == nil {
2162+ t .Fatalf ("expected non-nil error propagated from liveness manager, got nil" )
2163+ }
2164+ case <- time .After (5 * time .Second ):
2165+ t .Fatalf ("timeout waiting for runtime to return error from liveness manager" )
2166+ }
2167+ }
2168+
20552169func setupTest (t * testing.T ) (func (), error ) {
20562170 abortIfLinksAreUp (t )
20572171 rootPath , err := os .MkdirTemp ("" , "doublezerod" )
0 commit comments