package compat import ( "context" "encoding/json" "io" "log/slog" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" ) func discardLogger() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) } func TestEmbedRetriesTransientFailures(t *testing.T) { var calls atomic.Int32 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if calls.Add(1) < 3 { http.Error(w, "temporary failure", http.StatusServiceUnavailable) return } _ = json.NewEncoder(w).Encode(map[string]any{ "data": []map[string]any{ {"embedding": []float32{1, 2, 3}}, }, }) })) defer server.Close() client := New(Config{ Name: "test", BaseURL: server.URL, APIKey: "secret", EmbeddingModel: "embed-model", MetadataModel: "meta-model", HTTPClient: server.Client(), Log: discardLogger(), Dimensions: 3, }) embedding, err := client.Embed(context.Background(), "hello") if err != nil { t.Fatalf("Embed() error = %v", err) } if len(embedding) != 3 { t.Fatalf("embedding len = %d, want 3", len(embedding)) } if got := calls.Load(); got != 3 { t.Fatalf("call count = %d, want 3", got) } } func TestExtractMetadataStripsThinkingBlocks(t *testing.T) { cases := []struct { name string content string }{ { name: "think tag with braces inside", content: "\nLet me map {this} to the schema carefully.\n\n{\"people\":[],\"action_items\":[],\"dates_mentioned\":[],\"topics\":[\"test\"],\"type\":\"idea\",\"source\":\"\"}", }, { name: "thinking tag", content: "reasoning {here}{\"people\":[],\"action_items\":[],\"dates_mentioned\":[],\"topics\":[\"test\"],\"type\":\"idea\",\"source\":\"\"}", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { content := tc.content server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(map[string]any{ "choices": []map[string]any{ {"message": map[string]any{"content": content}}, }, }) })) defer server.Close() client := New(Config{ Name: "test", BaseURL: server.URL, APIKey: "secret", MetadataModel: "meta-model", HTTPClient: server.Client(), Log: discardLogger(), }) metadata, err := client.ExtractMetadata(context.Background(), "hello") if err != nil { t.Fatalf("ExtractMetadata() error = %v", err) } if metadata.Type != "idea" { t.Fatalf("metadata type = %q, want idea", metadata.Type) } }) } } func TestExtractMetadataFallbackModel(t *testing.T) { var calls atomic.Int32 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req chatCompletionsRequest _ = json.NewDecoder(r.Body).Decode(&req) if req.Model == "primary-model" { calls.Add(1) http.Error(w, "model unavailable", http.StatusServiceUnavailable) return } _ = json.NewEncoder(w).Encode(map[string]any{ "choices": []map[string]any{ {"message": map[string]any{"content": "{\"people\":[],\"action_items\":[],\"dates_mentioned\":[],\"topics\":[\"test\"],\"type\":\"task\",\"source\":\"\"}"}}, }, }) })) defer server.Close() client := New(Config{ Name: "test", BaseURL: server.URL, APIKey: "secret", MetadataModel: "primary-model", FallbackMetadataModel: "fallback-model", HTTPClient: server.Client(), Log: discardLogger(), }) metadata, err := client.ExtractMetadata(context.Background(), "hello") if err != nil { t.Fatalf("ExtractMetadata() error = %v", err) } if metadata.Type != "task" { t.Fatalf("metadata type = %q, want task", metadata.Type) } if calls.Load() == 0 { t.Fatal("primary model was never called") } } func TestExtractMetadataParsesCodeFencedJSON(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(map[string]any{ "choices": []map[string]any{ { "message": map[string]any{ "content": "```json\n{\"people\":[\"Alice\"],\"action_items\":[],\"dates_mentioned\":[],\"topics\":[\"memory\"],\"type\":\"idea\",\"source\":\"mcp\"}\n```", }, }, }, }) })) defer server.Close() client := New(Config{ Name: "test", BaseURL: server.URL, APIKey: "secret", EmbeddingModel: "embed-model", MetadataModel: "meta-model", HTTPClient: server.Client(), Log: discardLogger(), }) metadata, err := client.ExtractMetadata(context.Background(), "hello") if err != nil { t.Fatalf("ExtractMetadata() error = %v", err) } if metadata.Type != "idea" { t.Fatalf("metadata type = %q, want idea", metadata.Type) } if len(metadata.People) != 1 || metadata.People[0] != "Alice" { t.Fatalf("metadata people = %#v, want [Alice]", metadata.People) } } func TestExtractMetadataParsesArrayContent(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(map[string]any{ "choices": []map[string]any{ { "message": map[string]any{ "content": []map[string]any{ {"type": "text", "text": "{\"people\":[],\"action_items\":[],\"dates_mentioned\":[],\"topics\":[\"auth\"],\"type\":\"reference\",\"source\":\"mcp\"}"}, }, }, }, }, }) })) defer server.Close() client := New(Config{ Name: "test", BaseURL: server.URL, APIKey: "secret", EmbeddingModel: "embed-model", MetadataModel: "meta-model", HTTPClient: server.Client(), Log: discardLogger(), }) metadata, err := client.ExtractMetadata(context.Background(), "hello") if err != nil { t.Fatalf("ExtractMetadata() error = %v", err) } if metadata.Type != "reference" { t.Fatalf("metadata type = %q, want reference", metadata.Type) } } func TestExtractMetadataUsesReasoningContentWhenContentEmpty(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(map[string]any{ "choices": []map[string]any{ { "message": map[string]any{ "content": "", "reasoning_content": "{\"people\":[\"Hein\"],\"action_items\":[],\"dates_mentioned\":[],\"topics\":[\"profile\"],\"type\":\"person_note\",\"source\":\"mcp\"}", }, }, }, }) })) defer server.Close() client := New(Config{ Name: "test", BaseURL: server.URL, APIKey: "secret", EmbeddingModel: "embed-model", MetadataModel: "meta-model", HTTPClient: server.Client(), Log: discardLogger(), }) metadata, err := client.ExtractMetadata(context.Background(), "hello") if err != nil { t.Fatalf("ExtractMetadata() error = %v", err) } if metadata.Type != "person_note" { t.Fatalf("metadata type = %q, want person_note", metadata.Type) } if len(metadata.People) != 1 || metadata.People[0] != "Hein" { t.Fatalf("metadata people = %#v, want [Hein]", metadata.People) } } func TestExtractMetadataFallsBackToHeuristicsWhenModelsFail(t *testing.T) { var calls atomic.Int32 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _ = calls.Add(1) _ = json.NewEncoder(w).Encode(map[string]any{ "choices": []map[string]any{ {"message": map[string]any{"content": "not json"}}, }, }) })) defer server.Close() client := New(Config{ Name: "test", BaseURL: server.URL, APIKey: "secret", MetadataModel: "primary", FallbackMetadataModel: "secondary", HTTPClient: server.Client(), Log: discardLogger(), }) input := "Personal profile - Hein (Warkanum):\n- Born: 23 May 1989\n- Wife: Cindy, born 16 November 1994" metadata, err := client.ExtractMetadata(context.Background(), input) if err != nil { t.Fatalf("ExtractMetadata() error = %v", err) } if calls.Load() != 2 { t.Fatalf("call count = %d, want 2", calls.Load()) } if metadata.Type != "person_note" { t.Fatalf("metadata type = %q, want person_note", metadata.Type) } if len(metadata.DatesMentioned) < 2 { t.Fatalf("metadata dates = %#v, want extracted dates", metadata.DatesMentioned) } if len(metadata.People) == 0 || !strings.EqualFold(metadata.People[0], "Cindy") { t.Fatalf("metadata people = %#v, want Cindy", metadata.People) } } func TestExtractMetadataRetriesEmptyResponse(t *testing.T) { var calls atomic.Int32 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { call := calls.Add(1) var req chatCompletionsRequest _ = json.NewDecoder(r.Body).Decode(&req) if req.Stream == nil || *req.Stream { t.Fatalf("expected stream=false, got %#v", req.Stream) } if call == 1 { _ = json.NewEncoder(w).Encode(map[string]any{ "choices": []map[string]any{ {"message": map[string]any{"content": ""}}, }, }) return } _ = json.NewEncoder(w).Encode(map[string]any{ "choices": []map[string]any{ {"message": map[string]any{"content": "{\"people\":[],\"action_items\":[],\"dates_mentioned\":[],\"topics\":[\"mcp\"],\"type\":\"observation\",\"source\":\"mcp\"}"}}, }, }) })) defer server.Close() client := New(Config{ Name: "test", BaseURL: server.URL, APIKey: "secret", MetadataModel: "meta-model", HTTPClient: server.Client(), Log: discardLogger(), }) metadata, err := client.ExtractMetadata(context.Background(), "hello") if err != nil { t.Fatalf("ExtractMetadata() error = %v", err) } if calls.Load() < 2 { t.Fatalf("call count = %d, want >= 2", calls.Load()) } if metadata.Type != "observation" { t.Fatalf("metadata type = %q, want observation", metadata.Type) } }