package processing import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" ) func TestHeuristicCompleter_thinProductInventFromAttrs(t *testing.T) { t.Parallel() h := HeuristicCompleter{} system := `Retail product copywriter. Schema: {"name":"string","description":"string"}` user := "Category: Monitors\nName: UltraView 27\nDesc: UltraView 27\nAttrs: {\"brand\":\"Acme\",\"size\":\"27 inch\",\"panel\":\"IPS\"}" comp, err := h.Complete(context.Background(), system, user) if err != nil { t.Fatal(err) } var out map[string]string if err := json.Unmarshal([]byte(comp.Text), &out); err != nil { t.Fatalf("json: %v text=%q", err, comp.Text) } if out["name"] != "UltraView 27" { t.Fatalf("name=%q", out["name"]) } desc := out["description"] if desc == "" || strings.EqualFold(desc, "UltraView 27") || desc == "Product description" { t.Fatalf("desc=%q want invented non-title copy", desc) } if !strings.Contains(strings.ToLower(desc), "acme") && !strings.Contains(strings.ToLower(desc), "ips") && !strings.Contains(strings.ToLower(desc), "27") { t.Fatalf("desc=%q want attrs-derived facts", desc) } } func TestNewOpenAIClient_capsRetries(t *testing.T) { c := NewOpenAIClient("k", "https://api.openai.com/v1", "m", 0, 99) if c.MaxRetries != maxOpenAIRetries { t.Fatalf("MaxRetries=%d want %d", c.MaxRetries, maxOpenAIRetries) } c2 := NewOpenAIClient("k", "", "m", 0, 0) if c2.MaxRetries != 3 { t.Fatalf("default MaxRetries=%d want 3", c2.MaxRetries) } } func TestNewOpenAIClient_blocksPrivateDial(t *testing.T) { c := NewOpenAIClient("k", "https://api.openai.com/v1", "m", 0, 1) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://127.0.0.1:9/", nil) if err != nil { t.Fatal(err) } ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() req = req.WithContext(ctx) _, err = c.HTTPClient.Do(req) if err == nil { t.Fatal("expected dial to private/loopback blocked") } } func TestOpenAIBaseAllowsLoopback(t *testing.T) { if !openAIBaseAllowsLoopback("http://localhost:11434/v1") { t.Fatal("expected localhost allowed") } if !openAIBaseAllowsLoopback("http://127.0.0.1:11434/v1") { t.Fatal("expected 127.0.0.1 allowed") } if openAIBaseAllowsLoopback("https://api.openai.com/v1") { t.Fatal("expected public host denied for loopback flag") } if openAIBaseAllowsLoopback("https://192.168.1.1/v1") { t.Fatal("expected private IP denied") } } func TestOpenAIBaseAllowsPrivateNonProd(t *testing.T) { t.Setenv("APP_ENV", "local") if !openAIBaseAllowsPrivate("http://192.168.50.181:8767/v1") { t.Fatal("expected LAN proxy allowed in local") } if !openAIDialPolicy("http://192.168.50.181:8767/v1").AllowPrivate { t.Fatal("expected dial policy AllowPrivate") } t.Setenv("APP_ENV", "production") if openAIBaseAllowsPrivate("http://192.168.50.181:8767/v1") { t.Fatal("expected LAN proxy blocked in production") } t.Setenv("APP_ENV", "local") if openAIBaseAllowsPrivate("http://169.254.169.254/v1") { t.Fatal("expected link-local metadata blocked") } if openAIBaseAllowsPrivate("https://api.openai.com/v1") { t.Fatal("expected public host not private-allowed") } } func TestNewOpenAIClient_allowsPrivateDialNonProd(t *testing.T) { t.Setenv("APP_ENV", "development") c := NewOpenAIClient("k", "http://192.168.50.181:8767/v1", "m", 0, 1) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://192.168.50.181:9/", nil) if err != nil { t.Fatal(err) } ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() req = req.WithContext(ctx) _, err = c.HTTPClient.Do(req) if err == nil { t.Fatal("expected connection error, not success") } if strings.Contains(err.Error(), "host is not allowed") { t.Fatalf("SSRF blocked LAN OpenAI base unexpectedly: %v", err) } } func TestNewOpenAIClient_blocksPrivateDialInProduction(t *testing.T) { t.Setenv("APP_ENV", "production") c := NewOpenAIClient("k", "http://192.168.50.181:8767/v1", "m", 0, 1) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://192.168.50.181:9/", nil) if err != nil { t.Fatal(err) } ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() req = req.WithContext(ctx) _, err = c.HTTPClient.Do(req) if err == nil { t.Fatal("expected dial blocked in production") } if !strings.Contains(err.Error(), "host is not allowed") { t.Fatalf("expected host is not allowed, got: %v", err) } } func TestChoiceMessageText_prefersContentThenReasoningJSON(t *testing.T) { t.Parallel() if got := choiceMessageText(json.RawMessage(`"{\"name\":\"A\",\"description\":\"B\"}"`), "", ""); got == "" { t.Fatal("expected content string") } reasoning := `Thinking…\nDraft JSON:\n{"name":"NOSILEC W53070","description":"Stenski nosilec za TV."}\nVerify…` got := choiceMessageText(json.RawMessage(`""`), reasoning, "") if !strings.Contains(got, `"name"`) || !strings.Contains(got, "NOSILEC") { t.Fatalf("got=%q want JSON from reasoning_content", got) } if got := choiceMessageText(json.RawMessage(`null`), "no json here", ""); got != "" { t.Fatalf("expected empty without JSON, got %q", got) } } func TestOpenAIClient_doComplete_emptyContentWithReasoningJSON(t *testing.T) { t.Parallel() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(map[string]any{ "model": "code-fast", "choices": []map[string]any{{ "finish_reason": "length", "message": map[string]any{ "role": "assistant", "content": "", "reasoning_content": `steps… {"name":"TV Mount","description":"A wall mount for TVs."} more`, }, }}, "usage": map[string]int{"prompt_tokens": 10, "completion_tokens": 50, "total_tokens": 60}, }) })) defer srv.Close() c := NewOpenAIClient("test-key", srv.URL, "code-fast", 0, 1) c.HTTPClient = srv.Client() comp, err := c.CompleteWithOptions(context.Background(), "sys", "user", CompleteOptions{MaxTokens: 350, Temperature: 0.2}) if err != nil { t.Fatal(err) } if !strings.Contains(comp.Text, "TV Mount") { t.Fatalf("text=%q", comp.Text) } } func TestOpenAIClient_doComplete_emptyContentLengthRetriesWithBudget(t *testing.T) { t.Parallel() calls := 0 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls++ var req map[string]any _ = json.NewDecoder(r.Body).Decode(&req) maxTok, _ := req["max_tokens"].(float64) if calls == 1 { if maxTok != 350 { t.Errorf("first max_tokens=%v want 350", maxTok) } _ = json.NewEncoder(w).Encode(map[string]any{ "model": "code-fast", "choices": []map[string]any{{ "finish_reason": "length", "message": map[string]any{"role": "assistant", "content": "", "reasoning_content": "still thinking"}, }}, "usage": map[string]int{"prompt_tokens": 1, "completion_tokens": 350, "total_tokens": 351}, }) return } if maxTok != float64(maxTokensReasoningBudget) { t.Errorf("retry max_tokens=%v want %d", maxTok, maxTokensReasoningBudget) } _ = json.NewEncoder(w).Encode(map[string]any{ "model": "code-fast", "choices": []map[string]any{{ "finish_reason": "stop", "message": map[string]any{"role": "assistant", "content": `{"name":"X","description":"Y product description text here."}`}, }}, "usage": map[string]int{"prompt_tokens": 1, "completion_tokens": 40, "total_tokens": 41}, }) })) defer srv.Close() c := NewOpenAIClient("test-key", srv.URL, "code-fast", 0, 2) c.HTTPClient = srv.Client() comp, err := c.CompleteWithOptions(context.Background(), "sys", "user", CompleteOptions{MaxTokens: 350, Temperature: 0.2}) if err != nil { t.Fatal(err) } if calls != 2 { t.Fatalf("calls=%d want 2", calls) } if !strings.Contains(comp.Text, "\"name\"") { t.Fatalf("text=%q", comp.Text) } }