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 }