/ internal / memory / service_test.go
service_test.go
  1  package memory
  2  
  3  import (
  4  	"context"
  5  	"os"
  6  	"os/exec"
  7  	"path/filepath"
  8  	"testing"
  9  	"time"
 10  )
 11  
 12  func TestService_Disabled(t *testing.T) {
 13  	s := NewService(Config{Provider: "disabled"}, nil)
 14  	if err := s.Start(context.Background()); err != nil {
 15  		t.Fatal(err)
 16  	}
 17  	if s.Status() != StatusDisabled {
 18  		t.Fatalf("status=%v want StatusDisabled", s.Status())
 19  	}
 20  	_, class, _ := s.Query(context.Background(), QueryIntent{})
 21  	if class != ClassUnavailable {
 22  		t.Fatalf("disabled service Query class=%v want ClassUnavailable", class)
 23  	}
 24  }
 25  
 26  func TestService_LocalNoTLM(t *testing.T) {
 27  	captured := []string{}
 28  	a := AuditFunc(func(ev string, _ map[string]any) { captured = append(captured, ev) })
 29  	cfg := Config{Provider: "local", TLMPath: "/definitely/not/a/real/path/for/tlm"}
 30  	s := NewService(cfg, a)
 31  	if err := s.Start(context.Background()); err != nil {
 32  		t.Fatal(err)
 33  	}
 34  	if s.Status() != StatusUnavailable {
 35  		t.Fatalf("status=%v want StatusUnavailable", s.Status())
 36  	}
 37  	found := false
 38  	for _, e := range captured {
 39  		if e == "memory_tlm_missing" {
 40  			found = true
 41  		}
 42  	}
 43  	if !found {
 44  		t.Fatalf("expected memory_tlm_missing audit, got %v", captured)
 45  	}
 46  }
 47  
 48  func TestService_CloudMissingAPIKey(t *testing.T) {
 49  	captured := []map[string]any{}
 50  	a := AuditFunc(func(ev string, fields map[string]any) {
 51  		if ev == "memory_cloud_misconfigured" {
 52  			captured = append(captured, fields)
 53  		}
 54  	})
 55  	cfg := Config{Provider: "cloud", Endpoint: "https://x", APIKey: "", TLMPath: "/bin/echo"}
 56  	s := NewService(cfg, a)
 57  	_ = s.Start(context.Background())
 58  	if s.Status() != StatusUnavailable {
 59  		t.Fatalf("status=%v want StatusUnavailable", s.Status())
 60  	}
 61  	if len(captured) == 0 {
 62  		t.Fatal("expected memory_cloud_misconfigured audit")
 63  	}
 64  	f := captured[0]
 65  	if f["endpoint_resolved"] != true {
 66  		t.Fatalf("endpoint_resolved=%v want true", f["endpoint_resolved"])
 67  	}
 68  	if f["api_key_present"] != false {
 69  		t.Fatalf("api_key_present=%v want false", f["api_key_present"])
 70  	}
 71  }
 72  
 73  func TestService_CloudMissingEndpoint(t *testing.T) {
 74  	captured := []map[string]any{}
 75  	a := AuditFunc(func(ev string, fields map[string]any) {
 76  		if ev == "memory_cloud_misconfigured" {
 77  			captured = append(captured, fields)
 78  		}
 79  	})
 80  	cfg := Config{Provider: "cloud", Endpoint: "", APIKey: "k", TLMPath: "/bin/echo"}
 81  	s := NewService(cfg, a)
 82  	_ = s.Start(context.Background())
 83  	if s.Status() != StatusUnavailable {
 84  		t.Fatalf("status=%v want StatusUnavailable", s.Status())
 85  	}
 86  	if len(captured) == 0 {
 87  		t.Fatal("expected memory_cloud_misconfigured audit")
 88  	}
 89  	f := captured[0]
 90  	if f["endpoint_resolved"] != false {
 91  		t.Fatalf("endpoint_resolved=%v want false", f["endpoint_resolved"])
 92  	}
 93  	if f["api_key_present"] != true {
 94  		t.Fatalf("api_key_present=%v want true", f["api_key_present"])
 95  	}
 96  }
 97  
 98  // writeFakeTLMScriptSvc writes a python3 script that listens on `sock` and
 99  // serves /health = ready. Sidecar tests use a similar helper in
100  // sidecar_test.go; duplicated here to keep service_test.go self-contained.
101  // Skips if python3 is unavailable.
102  func writeFakeTLMScriptSvc(t *testing.T) string {
103  	t.Helper()
104  	if _, err := exec.LookPath("python3"); err != nil {
105  		t.Skip("python3 unavailable; sidecar spawn tests require python3")
106  	}
107  	dir, err := os.MkdirTemp("", "tlmsvc")
108  	if err != nil {
109  		t.Fatal(err)
110  	}
111  	t.Cleanup(func() { os.RemoveAll(dir) })
112  	py := `import sys, os, json, http.server, socketserver
113  sock_path = sys.argv[sys.argv.index('--socket')+1]
114  try: os.unlink(sock_path)
115  except FileNotFoundError: pass
116  class H(http.server.BaseHTTPRequestHandler):
117      def do_GET(self):
118          if self.path == '/health':
119              self.send_response(200); self.send_header('Content-Type','application/json'); self.end_headers()
120              self.wfile.write(json.dumps({'ready': True, 'protocol_version': 1}).encode())
121      def log_message(self, *args, **kwargs): pass
122  class UDSServer(socketserver.UnixStreamServer):
123      allow_reuse_address = True
124  srv = UDSServer(sock_path, H)
125  srv.serve_forever()
126  `
127  	path := filepath.Join(dir, "fake_tlm.py")
128  	if err := os.WriteFile(path, []byte(py), 0o755); err != nil {
129  		t.Fatal(err)
130  	}
131  	return path
132  }
133  
134  func shortSockForSvc(t *testing.T, name string) string {
135  	t.Helper()
136  	dir, err := os.MkdirTemp("", "svc")
137  	if err != nil {
138  		t.Fatal(err)
139  	}
140  	t.Cleanup(func() { os.RemoveAll(dir) })
141  	return filepath.Join(dir, name)
142  }
143  
144  func TestService_StartReachesReady(t *testing.T) {
145  	sock := shortSockForSvc(t, "s")
146  	root := t.TempDir()
147  	script := writeFakeTLMScriptSvc(t)
148  	cfg := Config{
149  		Provider:             "local",
150  		TLMPath:              "python3",
151  		SocketPath:           sock,
152  		BundleRoot:           root,
153  		SidecarReadyTimeout:  5 * time.Second,
154  		SidecarShutdownGrace: 2 * time.Second,
155  		SidecarRestartMax:    3,
156  		ClientRequestTimeout: 5 * time.Second,
157  	}
158  	s := NewService(cfg, nil)
159  	s.testExtraSpawnArgs = []string{script}
160  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
161  	defer cancel()
162  	if err := s.Start(ctx); err != nil {
163  		t.Fatal(err)
164  	}
165  	// Poll for Ready transition.
166  	deadline := time.Now().Add(5 * time.Second)
167  	for time.Now().Before(deadline) {
168  		if s.Status() == StatusReady {
169  			break
170  		}
171  		time.Sleep(50 * time.Millisecond)
172  	}
173  	if s.Status() != StatusReady {
174  		t.Fatalf("status=%v want StatusReady", s.Status())
175  	}
176  	if err := s.Stop(); err != nil {
177  		t.Fatal(err)
178  	}
179  }
180  
181  func TestService_StatusString(t *testing.T) {
182  	cases := []struct {
183  		s    ServiceStatus
184  		want string
185  	}{
186  		{StatusDisabled, "disabled"},
187  		{StatusInitializing, "initializing"},
188  		{StatusReady, "ready"},
189  		{StatusDegraded, "degraded"},
190  		{StatusUnavailable, "unavailable"},
191  	}
192  	for _, tc := range cases {
193  		if got := tc.s.String(); got != tc.want {
194  			t.Fatalf("%v.String()=%q want %q", tc.s, got, tc.want)
195  		}
196  	}
197  }