Files
zap/zapclient/roundtrip_test.go
zeekay 28cfc05ec5 zapclient: fix native-TCP Client→Server path (opcode routing + static dial)
Two defects made the canonical zapclient native-TCP path non-functional
(undetected — no test did a real Client→Server TCP round-trip; zapweb
works via Dispatch, inter-shard uses raw zap.Node):

1. Server.Register bound the node handler under the full opcode, but the
   Node routes inbound by Flags()>>8 (the high byte). Mismatch → handler
   never found → call hung. Register now uses op>>8; dispatch still reads
   the full Flags() to select the procedure.
2. Connect with a static Discovery set the node NoDiscovery, so it never
   learned peer addresses and could not dial. Added WithStaticPeers + a
   static Discovery, ConnectDirectID (returns the handshake-learned
   NodeID), and client-side dial-by-address (memoized) so static peers
   (placeholder NodeID + real Address — the cloud/K8s Service-DNS path)
   are reachable. mDNS path unchanged.

Adds TestNativeTCPRoundTrip as the regression guard.
2026-06-15 23:36:04 -07:00

92 lines
2.5 KiB
Go

package zapclient
import (
"context"
"fmt"
"net"
"testing"
"time"
zap "github.com/luxfi/zap"
)
func freePort(t *testing.T) int {
t.Helper()
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("freePort: %v", err)
}
defer l.Close()
return l.Addr().(*net.TCPAddr).Port
}
func textMsg(field int, s string) *zap.Message {
b := zap.NewBuilder(128)
ob := b.StartObject(64)
ob.SetText(field, s)
ob.FinishAsRoot()
m, _ := zap.Parse(b.Finish())
return m
}
// TestNativeTCPRoundTrip is the regression guard that was missing: a real
// zapclient.Client → zapclient.Server call over native TCP using static
// peers (no mDNS). It exercises BOTH fixes shipped in v0.8.6:
//
// - server opcode→handler routing: Register binds the node handler
// under op>>8 to match the inbound Flags()>>8 lookup (previously the
// full op was used, so the handler was never found and the call hung);
// - client static-peer dial: WithStaticPeers + dial-by-address via
// ConnectDirectID (previously a static-discovery node was NoDiscovery
// and could not dial the peer at all).
func TestNativeTCPRoundTrip(t *testing.T) {
port := freePort(t)
srv, err := NewServer("_lux-test._tcp", WithServerPort(port), WithNoDiscovery())
if err != nil {
t.Fatalf("NewServer: %v", err)
}
if err := srv.Register("test.echo", func(ctx context.Context, peer PeerInfo, req *zap.Message) (*zap.Message, error) {
in := req.Root().Text(0)
b := zap.NewBuilder(128)
ob := b.StartObject(64)
ob.SetText(0, "echo:"+in)
ob.FinishAsRoot()
out, _ := zap.Parse(b.Finish())
return out, nil
}); err != nil {
t.Fatalf("Register: %v", err)
}
if err := srv.Start(); err != nil {
t.Fatalf("Start: %v", err)
}
defer srv.Stop()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
c, err := Connect(ctx, "_lux-test._tcp",
WithStaticPeers(fmt.Sprintf("127.0.0.1:%d", port)),
WithCallTimeout(3*time.Second),
)
if err != nil {
t.Fatalf("Connect: %v", err)
}
defer c.Close()
resp, err := c.Call(ctx, "test.echo", textMsg(0, "hi"))
if err != nil {
t.Fatalf("Call over native TCP: %v", err)
}
if got := resp.Root().Text(0); got != "echo:hi" {
t.Fatalf("echo: got %q want %q", got, "echo:hi")
}
// Second call reuses the memoized address→NodeID mapping.
resp2, err := c.Call(ctx, "test.echo", textMsg(0, "again"))
if err != nil {
t.Fatalf("second Call: %v", err)
}
if got := resp2.Root().Text(0); got != "echo:again" {
t.Fatalf("echo2: got %q want %q", got, "echo:again")
}
}