client_test.go
1 package memory 2 3 import ( 4 "context" 5 "encoding/json" 6 "net" 7 "net/http" 8 "net/http/httptest" 9 "os" 10 "path/filepath" 11 "testing" 12 "time" 13 ) 14 15 func startUDSServer(t *testing.T, handler http.Handler) string { 16 t.Helper() 17 // Use a short path under os.TempDir() to stay under macOS sun_path's 18 // 104-byte limit; t.TempDir() embeds the (long) test name. 19 dir, err := os.MkdirTemp("", "tlm") 20 if err != nil { 21 t.Fatal(err) 22 } 23 sock := filepath.Join(dir, "s") 24 ln, err := net.Listen("unix", sock) 25 if err != nil { 26 t.Fatal(err) 27 } 28 srv := &httptest.Server{Listener: ln, Config: &http.Server{Handler: handler}} 29 srv.Start() 30 t.Cleanup(func() { srv.Close(); os.Remove(sock); os.RemoveAll(dir) }) 31 return sock 32 } 33 34 func TestClient_QueryHappy(t *testing.T) { 35 sock := startUDSServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 36 if r.Header.Get("X-Request-ID") == "" { 37 t.Fatal("missing X-Request-ID") 38 } 39 w.Header().Set("X-Request-ID", r.Header.Get("X-Request-ID")) 40 _ = json.NewEncoder(w).Encode(ResponseEnvelope{ 41 ProtocolVersion: 1, 42 RequestID: r.Header.Get("X-Request-ID"), 43 Reason: "ok", 44 Candidates: []QueryCandidate{{Value: "v"}}, 45 }) 46 })) 47 c := NewClient(sock, 5*time.Second) 48 ctx := WithRequestID(context.Background(), "req-test123") 49 env, class, err := c.Query(ctx, QueryIntent{Mode: ModeDirectRelation, AnchorMentions: []string{"x"}}) 50 if err != nil { 51 t.Fatal(err) 52 } 53 if class != ClassOK || env.RequestID != "req-test123" || len(env.Candidates) != 1 { 54 t.Fatalf("got %+v class=%v", env, class) 55 } 56 } 57 58 func TestClient_AutoMintsRequestID(t *testing.T) { 59 var seen string 60 sock := startUDSServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 61 seen = r.Header.Get("X-Request-ID") 62 _ = json.NewEncoder(w).Encode(ResponseEnvelope{Reason: "ok"}) 63 })) 64 c := NewClient(sock, 5*time.Second) 65 _, _, _ = c.Query(context.Background(), QueryIntent{Mode: ModeDirectRelation, AnchorMentions: []string{"x"}}) 66 if len(seen) < 5 || seen[:4] != "req-" { 67 t.Fatalf("auto-minted ID %q does not match req-<hex>", seen) 68 } 69 } 70 71 func TestClient_MalformedJSON_Unavailable(t *testing.T) { 72 sock := startUDSServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 73 _, _ = w.Write([]byte("{not json")) 74 })) 75 c := NewClient(sock, 5*time.Second) 76 _, class, err := c.Query(context.Background(), QueryIntent{Mode: ModeDirectRelation, AnchorMentions: []string{"x"}}) 77 if class != ClassUnavailable || err == nil { 78 t.Fatalf("class=%v err=%v", class, err) 79 } 80 } 81 82 func TestClient_DialCtxCancel(t *testing.T) { 83 sock := filepath.Join(t.TempDir(), "missing.sock") 84 c := NewClient(sock, 5*time.Second) 85 ctx, cancel := context.WithCancel(context.Background()) 86 cancel() 87 start := time.Now() 88 _, class, err := c.Query(ctx, QueryIntent{Mode: ModeDirectRelation, AnchorMentions: []string{"x"}}) 89 if elapsed := time.Since(start); elapsed > 500*time.Millisecond { 90 t.Fatalf("dial took %v — ctx cancellation not honored", elapsed) 91 } 92 if class != ClassUnavailable || err == nil { 93 t.Fatalf("class=%v err=%v", class, err) 94 } 95 }