@@ -106,9 +106,9 @@ func TestBrokerHTTPClient_DialAndRewrite(t *testing.T) {
106106 defer func () { _ = syscall .Close (childFD ) }()
107107 defer stop ()
108108
109- client := newBrokerHTTPClient (childFD )
110- if client = = nil {
111- t .Fatal ("newBrokerHTTPClient returned nil" )
109+ client , err := newBrokerHTTPClient (childFD )
110+ if err ! = nil {
111+ t .Fatalf ("newBrokerHTTPClient: %v" , err )
112112 }
113113 defer client .CloseIdleConnections () // release dispatched keep-alive conns
114114 // The CLI's base URL is an http placeholder; broker rewrites host.
@@ -208,6 +208,41 @@ func TestDefaultNewClient_RejectsStdioFD(t *testing.T) {
208208 }
209209}
210210
211+ // TestDefaultNewClient_CredFDNotInherited covers a caller that drops the
212+ // inherited control fd before exec (Python's subprocess closes fds >= 3 by
213+ // default): the fd number is then closed, or reused by an unrelated file.
214+ // Either way defaultNewClient must fail up front with an error that names the
215+ // fd and the fix, instead of a handshake errno at the first request.
216+ func TestDefaultNewClient_CredFDNotInherited (t * testing.T ) {
217+ t .Setenv ("HOME" , t .TempDir ())
218+ t .Setenv ("FLASHDUTY_APP_KEY" , "" )
219+
220+ f , err := os .CreateTemp (t .TempDir (), "not-a-socket" )
221+ if err != nil {
222+ t .Fatal (err )
223+ }
224+ defer func () { _ = f .Close () }()
225+ pair , err := syscall .Socketpair (syscall .AF_UNIX , controlSockType , 0 )
226+ if err != nil {
227+ t .Fatalf ("socketpair: %v" , err )
228+ }
229+ closedFD := pair [0 ]
230+ _ = syscall .Close (pair [0 ])
231+ _ = syscall .Close (pair [1 ])
232+
233+ for name , fd := range map [string ]int {"closed fd" : closedFD , "regular file" : int (f .Fd ())} {
234+ t .Setenv ("FLASHDUTY_CRED_FD" , strconv .Itoa (fd ))
235+ _ , err := defaultNewClient ()
236+ if err == nil {
237+ t .Fatalf ("%s: defaultNewClient must fail" , name )
238+ }
239+ want := "FLASHDUTY_CRED_FD=" + strconv .Itoa (fd ) + " is not an open socket"
240+ if msg := err .Error (); ! strings .HasPrefix (msg , want ) || ! strings .Contains (msg , "pass_fds=(" + strconv .Itoa (fd )+ ",)" ) {
241+ t .Fatalf ("%s: error must start with %q and name pass_fds, got: %v" , name , want , msg )
242+ }
243+ }
244+ }
245+
211246// TestBrokerHTTPClient_RefusedReturnsError verifies the dialer surfaces the
212247// broker's 0xFF refusal (e.g. the runner failed to mint a connection) as a real
213248// error instead of hanging or wrapping a nil conn.
@@ -232,7 +267,10 @@ func TestBrokerHTTPClient_RefusedReturnsError(t *testing.T) {
232267 }
233268 }()
234269
235- client := newBrokerHTTPClient (childFD )
270+ client , err := newBrokerHTTPClient (childFD )
271+ if err != nil {
272+ t .Fatalf ("newBrokerHTTPClient: %v" , err )
273+ }
236274 req , _ := http .NewRequestWithContext (context .Background (), "GET" ,
237275 "http://flashduty.broker.local/x?app_key=SENTINEL" , nil )
238276 if _ , err := client .Do (req ); err == nil {
0 commit comments