package aiintake import ( "strings" "testing" ) func TestExtractInputValidate(t *testing.T) { valid := strings.Repeat("a", minTextLen) tooShort := strings.Repeat("a", minTextLen-1) tooLong := strings.Repeat("a", maxTextLen+1) tests := []struct { name string text string wantErr bool }{ {"valid minimum length", valid, false}, {"empty text", "", true}, {"below minimum length", tooShort, true}, {"at maximum length", strings.Repeat("a", maxTextLen), false}, {"above maximum length", tooLong, true}, {"typical customer text", "ноутбук ASUS не включается, клиент Иван, телефон +79991234567", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { b := extractInput{Text: tt.text} got := b.validate() if (got != "") != tt.wantErr { t.Errorf("validate() = %q, wantErr %v", got, tt.wantErr) } }) } } func TestBuildGeminiRequestEmbedsTextBetweenMarkers(t *testing.T) { text := "клиент сказал что принтер полосит" req := buildGeminiRequest(text, "gemini-2.0-flash") contents, ok := req["contents"].([]map[string]any) if !ok || len(contents) != 1 { t.Fatalf("expected exactly one content entry, got %#v", req["contents"]) } parts, ok := contents[0]["parts"].([]map[string]any) if !ok || len(parts) != 1 { t.Fatalf("expected exactly one part, got %#v", contents[0]["parts"]) } promptText, ok := parts[0]["text"].(string) if !ok { t.Fatalf("expected part text to be a string, got %#v", parts[0]["text"]) } if !strings.Contains(promptText, text) { t.Errorf("prompt does not contain the customer text: %q", promptText) } if !strings.Contains(promptText, "BEGIN CUSTOMER TEXT") || !strings.Contains(promptText, "END CUSTOMER TEXT") { t.Error("prompt is missing the delimiter markers meant to fence off untrusted customer text from instructions") } genConfig, ok := req["generationConfig"].(map[string]any) if !ok { t.Fatalf("expected generationConfig to be present, got %#v", req["generationConfig"]) } responseFormat, ok := genConfig["responseFormat"].(map[string]any) if !ok { t.Fatalf("expected responseFormat to be present, got %#v", genConfig["responseFormat"]) } textFormat, ok := responseFormat["text"].(map[string]any) if !ok { t.Fatalf("expected responseFormat.text to be present, got %#v", responseFormat["text"]) } if textFormat["mimeType"] != "application/json" { t.Errorf("expected mimeType application/json, got %#v", textFormat["mimeType"]) } if textFormat["schema"] == nil { t.Error("expected a non-nil response schema") } } func TestBuildGeminiRequestDoesNotMutateSchemaBetweenCalls(t *testing.T) { // extractionSchema is a shared package-level map — building two requests // must not let one call's mutation bleed into the other's. req1 := buildGeminiRequest("first text", "gemini-2.0-flash") req2 := buildGeminiRequest("second text", "gemini-2.0-flash") schema1 := req1["generationConfig"].(map[string]any)["responseFormat"].(map[string]any)["text"].(map[string]any)["schema"] schema2 := req2["generationConfig"].(map[string]any)["responseFormat"].(map[string]any)["text"].(map[string]any)["schema"] if schema1 == nil || schema2 == nil { t.Fatal("expected non-nil schemas") } }