Implement environment variable resolution in configuration parsing and enhance collection management API
- Added a new test for resolving environment variables during configuration parsing, ensuring that environment variables are correctly substituted in the configuration. - Refactored the configuration handling to resolve environment variables immediately after parsing, improving the clarity and usability of the configuration structure. - Enhanced the collection management API by standardizing error responses and success responses using a custom response structure, improving consistency across endpoints. - Implemented cleanup logic for test collections to ensure proper resource management during testing.
This commit is contained in:
parent
8d35e824ae
commit
bb64eef20d
5 changed files with 981 additions and 30 deletions
125
kb/config.go
125
kb/config.go
|
|
@ -116,14 +116,31 @@ func (c *Config) createGraphStore() (types.GraphStore, error) {
|
|||
|
||||
// toVectorStoreConfig converts the vector config to VectorStoreConfig
|
||||
func (c *Config) toVectorStoreConfig() (types.VectorStoreConfig, error) {
|
||||
// Parse environment variables in config
|
||||
resolvedConfig, err := c.resolveEnvVars(c.Vector.Config)
|
||||
if err != nil {
|
||||
return types.VectorStoreConfig{}, err
|
||||
// Environment variables are already resolved during parsing
|
||||
configCopy := make(map[string]interface{})
|
||||
for k, v := range c.Vector.Config {
|
||||
configCopy[k] = v
|
||||
}
|
||||
|
||||
// Convert resolved config to types.VectorStoreConfig via JSON
|
||||
jsonData, err := json.Marshal(resolvedConfig)
|
||||
// Ensure host and port are in ExtraParams for Qdrant
|
||||
if _, exists := configCopy["extra_params"]; !exists {
|
||||
configCopy["extra_params"] = make(map[string]interface{})
|
||||
}
|
||||
|
||||
extraParams := configCopy["extra_params"].(map[string]interface{})
|
||||
|
||||
// Map host field
|
||||
if host, exists := configCopy["host"]; exists {
|
||||
extraParams["host"] = host
|
||||
}
|
||||
|
||||
// Map port field
|
||||
if port, exists := configCopy["port"]; exists {
|
||||
extraParams["port"] = port
|
||||
}
|
||||
|
||||
// Convert config to types.VectorStoreConfig via JSON
|
||||
jsonData, err := json.Marshal(configCopy)
|
||||
if err != nil {
|
||||
return types.VectorStoreConfig{}, err
|
||||
}
|
||||
|
|
@ -138,14 +155,39 @@ func (c *Config) toVectorStoreConfig() (types.VectorStoreConfig, error) {
|
|||
|
||||
// toGraphStoreConfig converts the graph config to GraphStoreConfig
|
||||
func (c *Config) toGraphStoreConfig() (types.GraphStoreConfig, error) {
|
||||
// Parse environment variables in config
|
||||
resolvedConfig, err := c.resolveEnvVars(c.Graph.Config)
|
||||
if err != nil {
|
||||
return types.GraphStoreConfig{}, err
|
||||
// Environment variables are already resolved during parsing
|
||||
configCopy := make(map[string]interface{})
|
||||
for k, v := range c.Graph.Config {
|
||||
configCopy[k] = v
|
||||
}
|
||||
|
||||
// Convert resolved config to types.GraphStoreConfig via JSON
|
||||
jsonData, err := json.Marshal(resolvedConfig)
|
||||
// Map field names to match GraphStoreConfig structure
|
||||
if url, exists := configCopy["url"]; exists {
|
||||
configCopy["database_url"] = url
|
||||
delete(configCopy, "url") // Remove the original field
|
||||
}
|
||||
|
||||
// Ensure DriverConfig exists and map username/password into it
|
||||
if _, exists := configCopy["driver_config"]; !exists {
|
||||
configCopy["driver_config"] = make(map[string]interface{})
|
||||
}
|
||||
|
||||
driverConfig := configCopy["driver_config"].(map[string]interface{})
|
||||
|
||||
// Map username field to DriverConfig
|
||||
if username, exists := configCopy["username"]; exists {
|
||||
driverConfig["username"] = username
|
||||
delete(configCopy, "username") // Remove from top level
|
||||
}
|
||||
|
||||
// Map password field to DriverConfig
|
||||
if password, exists := configCopy["password"]; exists {
|
||||
driverConfig["password"] = password
|
||||
delete(configCopy, "password") // Remove from top level
|
||||
}
|
||||
|
||||
// Convert config to types.GraphStoreConfig via JSON
|
||||
jsonData, err := json.Marshal(configCopy)
|
||||
if err != nil {
|
||||
return types.GraphStoreConfig{}, err
|
||||
}
|
||||
|
|
@ -200,6 +242,58 @@ func (c *Config) parseEnvVar(value string) string {
|
|||
})
|
||||
}
|
||||
|
||||
// resolveAllEnvVars resolves environment variables in all configuration sections
|
||||
func (c *Config) resolveAllEnvVars() error {
|
||||
// Resolve Vector config
|
||||
if c.Vector.Config != nil {
|
||||
resolved, err := c.resolveEnvVars(c.Vector.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Vector.Config = resolved
|
||||
}
|
||||
|
||||
// Resolve Graph config
|
||||
if c.Graph != nil && c.Graph.Config != nil {
|
||||
resolved, err := c.resolveEnvVars(c.Graph.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Graph.Config = resolved
|
||||
}
|
||||
|
||||
// Resolve Provider options (if they contain env vars)
|
||||
if err := c.resolveProviderEnvVars(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveProviderEnvVars resolves environment variables in provider configurations
|
||||
func (c *Config) resolveProviderEnvVars() error {
|
||||
providerLists := [][]*Provider{
|
||||
c.Chunkings, c.Embeddings, c.Converters, c.Extractors,
|
||||
c.Fetchers, c.Searchers, c.Rerankers, c.Votes, c.Weights, c.Scores,
|
||||
}
|
||||
|
||||
for _, providers := range providerLists {
|
||||
for _, provider := range providers {
|
||||
for _, option := range provider.Options {
|
||||
if option.Properties != nil {
|
||||
resolved, err := c.resolveEnvVars(option.Properties)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
option.Properties = resolved
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler interface
|
||||
func (c *Config) UnmarshalJSON(data []byte) error {
|
||||
// Use alias type to avoid infinite recursion
|
||||
|
|
@ -208,7 +302,12 @@ func (c *Config) UnmarshalJSON(data []byte) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Compute features after parsing
|
||||
// Resolve environment variables immediately after parsing
|
||||
if err := c.resolveAllEnvVars(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Compute features after parsing and resolving env vars
|
||||
c.Features = c.ComputeFeatures()
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -506,3 +506,79 @@ func TestConfig_ParseEnvVar(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_ResolveEnvVarsOnParsing(t *testing.T) {
|
||||
// Set test environment variables
|
||||
os.Setenv("TEST_VECTOR_HOST", "test-vector-host")
|
||||
os.Setenv("TEST_GRAPH_URL", "neo4j://test-graph:7687")
|
||||
os.Setenv("TEST_GRAPH_USER", "test-user")
|
||||
os.Setenv("TEST_GRAPH_PASS", "test-pass")
|
||||
defer func() {
|
||||
os.Unsetenv("TEST_VECTOR_HOST")
|
||||
os.Unsetenv("TEST_GRAPH_URL")
|
||||
os.Unsetenv("TEST_GRAPH_USER")
|
||||
os.Unsetenv("TEST_GRAPH_PASS")
|
||||
}()
|
||||
|
||||
configJSON := `{
|
||||
"vector": {
|
||||
"driver": "qdrant",
|
||||
"config": {
|
||||
"host": "$ENV.TEST_VECTOR_HOST",
|
||||
"port": 6333
|
||||
}
|
||||
},
|
||||
"graph": {
|
||||
"driver": "neo4j",
|
||||
"config": {
|
||||
"url": "$ENV.TEST_GRAPH_URL",
|
||||
"username": "$ENV.TEST_GRAPH_USER",
|
||||
"password": "$ENV.TEST_GRAPH_PASS"
|
||||
}
|
||||
},
|
||||
"chunkings": [
|
||||
{
|
||||
"id": "__yao.structured",
|
||||
"label": "Document Structure",
|
||||
"description": "Split text",
|
||||
"options": []
|
||||
}
|
||||
],
|
||||
"embeddings": [
|
||||
{
|
||||
"id": "__yao.openai",
|
||||
"label": "OpenAI",
|
||||
"description": "OpenAI embeddings",
|
||||
"options": []
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
// Parse config from JSON
|
||||
config, err := ParseConfigFromJSON([]byte(configJSON))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse config: %v", err)
|
||||
}
|
||||
|
||||
// Verify that environment variables are resolved immediately after parsing
|
||||
if config.Vector.Config["host"] != "test-vector-host" {
|
||||
t.Errorf("Expected vector host to be resolved to 'test-vector-host', got '%v'", config.Vector.Config["host"])
|
||||
}
|
||||
|
||||
if config.Graph.Config["url"] != "neo4j://test-graph:7687" {
|
||||
t.Errorf("Expected graph URL to be resolved to 'neo4j://test-graph:7687', got '%v'", config.Graph.Config["url"])
|
||||
}
|
||||
|
||||
if config.Graph.Config["username"] != "test-user" {
|
||||
t.Errorf("Expected graph username to be resolved to 'test-user', got '%v'", config.Graph.Config["username"])
|
||||
}
|
||||
|
||||
if config.Graph.Config["password"] != "test-pass" {
|
||||
t.Errorf("Expected graph password to be resolved to 'test-pass', got '%v'", config.Graph.Config["password"])
|
||||
}
|
||||
|
||||
// Verify that numeric values remain unchanged (JSON numbers are parsed as float64)
|
||||
if port, ok := config.Vector.Config["port"].(float64); !ok || port != 6333.0 {
|
||||
t.Errorf("Expected vector port to remain 6333.0, got %v (type %T)", config.Vector.Config["port"], config.Vector.Config["port"])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ package kb
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yaoapp/gou/graphrag/types"
|
||||
"github.com/yaoapp/yao/kb"
|
||||
"github.com/yaoapp/yao/openapi/response"
|
||||
)
|
||||
|
||||
// Collection Management Handlers
|
||||
|
|
@ -17,19 +17,34 @@ func CreateCollection(c *gin.Context) {
|
|||
|
||||
// Parse and bind JSON request
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format: " + err.Error()})
|
||||
// Create a custom error with the same structure but specific message
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "Invalid request format: " + err.Error(),
|
||||
}
|
||||
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate request parameters
|
||||
if err := validateCreateCollectionRequest(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
// Create a custom error with the same structure but specific message
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: err.Error(),
|
||||
}
|
||||
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if kb.Instance is available
|
||||
if kb.Instance == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Knowledge base not initialized"})
|
||||
// Create a custom error with the same structure but specific message
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Knowledge base not initialized",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -43,36 +58,150 @@ func CreateCollection(c *gin.Context) {
|
|||
// Call the actual CreateCollection method
|
||||
collectionID, err := kb.Instance.CreateCollection(c.Request.Context(), collectionConfig)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create collection: " + err.Error()})
|
||||
// Create a custom error with the same structure but specific message
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Failed to create collection: " + err.Error(),
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
successData := gin.H{
|
||||
"message": "Collection created successfully",
|
||||
"collection_id": collectionID,
|
||||
})
|
||||
}
|
||||
response.RespondWithSuccess(c, response.StatusCreated, successData)
|
||||
}
|
||||
|
||||
// RemoveCollection removes an existing collection
|
||||
func RemoveCollection(c *gin.Context) {
|
||||
// TODO: Implement remove collection logic
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Collection removed"})
|
||||
// Get collection ID from URL parameter
|
||||
collectionID := c.Param("collectionID")
|
||||
if collectionID == "" {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "Collection ID is required",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if kb.Instance is available
|
||||
if kb.Instance == nil {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Knowledge base not initialized",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Call the actual RemoveCollection method
|
||||
removed, err := kb.Instance.RemoveCollection(c.Request.Context(), collectionID)
|
||||
if err != nil {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Failed to remove collection: " + err.Error(),
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
if !removed {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "Collection not found or could not be removed",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
successData := gin.H{
|
||||
"message": "Collection removed successfully",
|
||||
"collection_id": collectionID,
|
||||
"removed": removed,
|
||||
}
|
||||
response.RespondWithSuccess(c, response.StatusOK, successData)
|
||||
}
|
||||
|
||||
// CollectionExists checks if a collection exists
|
||||
func CollectionExists(c *gin.Context) {
|
||||
// TODO: Implement collection exists check logic
|
||||
c.JSON(http.StatusOK, gin.H{"exists": false})
|
||||
// Get collection ID from URL parameter
|
||||
collectionID := c.Param("collectionID")
|
||||
if collectionID == "" {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "Collection ID is required",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if kb.Instance is available
|
||||
if kb.Instance == nil {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Knowledge base not initialized",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Call the actual CollectionExists method
|
||||
exists, err := kb.Instance.CollectionExists(c.Request.Context(), collectionID)
|
||||
if err != nil {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Failed to check collection existence: " + err.Error(),
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
successData := gin.H{
|
||||
"collection_id": collectionID,
|
||||
"exists": exists,
|
||||
}
|
||||
response.RespondWithSuccess(c, response.StatusOK, successData)
|
||||
}
|
||||
|
||||
// GetCollections retrieves collections with optional filtering
|
||||
func GetCollections(c *gin.Context) {
|
||||
collections, err := kb.Instance.GetCollections(c.Request.Context(), map[string]interface{}{})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
// Check if kb.Instance is available
|
||||
if kb.Instance == nil {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Knowledge base not initialized",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, collections)
|
||||
|
||||
// Build filter from query parameters
|
||||
filter := make(map[string]interface{})
|
||||
|
||||
// Extract all query parameters as potential filter conditions
|
||||
// This allows filtering by any metadata field, e.g.:
|
||||
// GET /collections?category=documents&status=active
|
||||
for key, values := range c.Request.URL.Query() {
|
||||
if len(values) > 0 {
|
||||
// Use the first value if multiple values are provided
|
||||
filter[key] = values[0]
|
||||
}
|
||||
}
|
||||
|
||||
collections, err := kb.Instance.GetCollections(c.Request.Context(), filter)
|
||||
if err != nil {
|
||||
// Create a custom error with the same structure but specific message
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: err.Error(),
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
response.RespondWithSuccess(c, response.StatusOK, collections)
|
||||
}
|
||||
|
||||
// CreateCollectionRequest represents the request structure for creating a collection
|
||||
|
|
|
|||
566
openapi/tests/kb/collection_test.go
Normal file
566
openapi/tests/kb/collection_test.go
Normal file
|
|
@ -0,0 +1,566 @@
|
|||
package openapi_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/yaoapp/yao/openapi"
|
||||
"github.com/yaoapp/yao/openapi/tests/testutils"
|
||||
)
|
||||
|
||||
// TestCreateCollection tests the collection creation endpoint
|
||||
func TestCreateCollection(t *testing.T) {
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
|
||||
// Get base URL from server config
|
||||
baseURL := ""
|
||||
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||
baseURL = openapi.Server.Config.BaseURL
|
||||
}
|
||||
|
||||
// Register test client and get token
|
||||
client := testutils.RegisterTestClient(t, "KB Collection Create Test Client", []string{"https://localhost/callback"})
|
||||
defer testutils.CleanupTestClient(t, client.ClientID)
|
||||
tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
|
||||
// Generate unique test collection ID
|
||||
testCollectionID := fmt.Sprintf("test_collection_%d", time.Now().UnixNano())
|
||||
|
||||
t.Run("CreateCollectionSuccess", func(t *testing.T) {
|
||||
// Register collection for cleanup
|
||||
testutils.RegisterTestCollection(testCollectionID)
|
||||
|
||||
// Prepare request body
|
||||
createData := map[string]interface{}{
|
||||
"id": testCollectionID,
|
||||
"metadata": map[string]interface{}{
|
||||
"category": "test",
|
||||
"created_by": "test_user",
|
||||
},
|
||||
"config": map[string]interface{}{
|
||||
"collection_name": testCollectionID + "_vector", // Required: collection name
|
||||
"embedding_model": "text-embedding-ada-002",
|
||||
"chunk_size": 1000,
|
||||
"chunk_overlap": 200,
|
||||
"index_type": "hnsw", // Required: valid index type
|
||||
"distance": "cosine", // Required: distance metric
|
||||
"dimension": 1536, // Required: embedding dimensions (note: singular not plural)
|
||||
},
|
||||
}
|
||||
|
||||
body, err := json.Marshal(createData)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create HTTP request
|
||||
req, err := http.NewRequest("POST", serverURL+baseURL+"/kb/collections", bytes.NewBuffer(body))
|
||||
assert.NoError(t, err)
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
// Make request
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check response
|
||||
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||
|
||||
var response map[string]interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Debug output for failed requests
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
t.Logf("Expected status 201, got %d", resp.StatusCode)
|
||||
t.Logf("Response body: %+v", response)
|
||||
}
|
||||
|
||||
assert.Equal(t, "Collection created successfully", response["message"])
|
||||
assert.Equal(t, testCollectionID, response["collection_id"])
|
||||
|
||||
t.Logf("Successfully created collection: %s", testCollectionID)
|
||||
})
|
||||
|
||||
t.Run("CreateCollectionInvalidRequest", func(t *testing.T) {
|
||||
// Test with missing required fields
|
||||
invalidData := map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"category": "test",
|
||||
},
|
||||
// Missing "id" and "config" fields
|
||||
}
|
||||
|
||||
body, err := json.Marshal(invalidData)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequest("POST", serverURL+baseURL+"/kb/collections", bytes.NewBuffer(body))
|
||||
assert.NoError(t, err)
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
|
||||
var errorResponse map[string]interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&errorResponse)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, errorResponse, "error")
|
||||
|
||||
t.Logf("Correctly rejected invalid collection creation request")
|
||||
})
|
||||
|
||||
t.Run("CreateCollectionMalformedJSON", func(t *testing.T) {
|
||||
// Test with malformed JSON
|
||||
malformedJSON := `{"id": "test", "config": malformed}`
|
||||
|
||||
req, err := http.NewRequest("POST", serverURL+baseURL+"/kb/collections", bytes.NewBufferString(malformedJSON))
|
||||
assert.NoError(t, err)
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
|
||||
t.Logf("Correctly rejected malformed JSON request")
|
||||
})
|
||||
}
|
||||
|
||||
// TestRemoveCollection tests the collection removal endpoint
|
||||
func TestRemoveCollection(t *testing.T) {
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
|
||||
baseURL := ""
|
||||
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||
baseURL = openapi.Server.Config.BaseURL
|
||||
}
|
||||
|
||||
client := testutils.RegisterTestClient(t, "KB Collection Remove Test Client", []string{"https://localhost/callback"})
|
||||
defer testutils.CleanupTestClient(t, client.ClientID)
|
||||
tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
|
||||
testCollectionID := fmt.Sprintf("test_collection_remove_%d", time.Now().UnixNano())
|
||||
|
||||
t.Run("RemoveCollectionSuccess", func(t *testing.T) {
|
||||
// Register collection for cleanup (in case removal fails)
|
||||
testutils.RegisterTestCollection(testCollectionID)
|
||||
|
||||
// First create a collection to remove
|
||||
createData := map[string]interface{}{
|
||||
"id": testCollectionID,
|
||||
"metadata": map[string]interface{}{
|
||||
"category": "test_remove",
|
||||
},
|
||||
"config": map[string]interface{}{
|
||||
"collection_name": testCollectionID + "_vector",
|
||||
"embedding_model": "text-embedding-ada-002",
|
||||
"index_type": "hnsw",
|
||||
"distance": "cosine",
|
||||
"dimension": 1536,
|
||||
},
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(createData)
|
||||
createReq, _ := http.NewRequest("POST", serverURL+baseURL+"/kb/collections", bytes.NewBuffer(body))
|
||||
createReq.Header.Set("Content-Type", "application/json")
|
||||
createReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
createResp, err := http.DefaultClient.Do(createReq)
|
||||
assert.NoError(t, err)
|
||||
defer createResp.Body.Close()
|
||||
|
||||
// Now test removal
|
||||
req, err := http.NewRequest("DELETE", serverURL+baseURL+"/kb/collections/"+testCollectionID, nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Knowledge base should be initialized for this test to be meaningful
|
||||
if resp.StatusCode == http.StatusInternalServerError {
|
||||
var errorResponse map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&errorResponse)
|
||||
if errorDescription, ok := errorResponse["error_description"].(string); ok &&
|
||||
strings.Contains(errorDescription, "Knowledge base not initialized") {
|
||||
t.Skip("Knowledge base not initialized - skipping test (this indicates environment setup issue)")
|
||||
}
|
||||
}
|
||||
|
||||
// Expect successful response for collection removal
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode, "Should successfully remove collection when KB is initialized")
|
||||
|
||||
var response map[string]interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, response, "message")
|
||||
t.Logf("Successfully removed collection: %s", testCollectionID)
|
||||
})
|
||||
|
||||
t.Run("RemoveCollectionMissingID", func(t *testing.T) {
|
||||
// Test with missing collection ID in path
|
||||
req, err := http.NewRequest("DELETE", serverURL+baseURL+"/kb/collections/", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// This should result in a 404 or similar error due to route mismatch
|
||||
assert.NotEqual(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
t.Logf("Correctly handled request with missing collection ID")
|
||||
})
|
||||
}
|
||||
|
||||
// TestCollectionExists tests the collection existence check endpoint
|
||||
func TestCollectionExists(t *testing.T) {
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
|
||||
baseURL := ""
|
||||
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||
baseURL = openapi.Server.Config.BaseURL
|
||||
}
|
||||
|
||||
client := testutils.RegisterTestClient(t, "KB Collection Exists Test Client", []string{"https://localhost/callback"})
|
||||
defer testutils.CleanupTestClient(t, client.ClientID)
|
||||
tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
|
||||
testCollectionID := fmt.Sprintf("test_collection_exists_%d", time.Now().UnixNano())
|
||||
|
||||
t.Run("CollectionExistsCheck", func(t *testing.T) {
|
||||
// Test with a collection ID
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/kb/collections/"+testCollectionID+"/exists", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Knowledge base should be initialized for this test to be meaningful
|
||||
if resp.StatusCode == http.StatusInternalServerError {
|
||||
var errorResponse map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&errorResponse)
|
||||
if errorDescription, ok := errorResponse["error_description"].(string); ok &&
|
||||
strings.Contains(errorDescription, "Knowledge base not initialized") {
|
||||
t.Skip("Knowledge base not initialized - skipping test (this indicates environment setup issue)")
|
||||
}
|
||||
}
|
||||
|
||||
// Expect successful response for collection existence check
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode, "Should successfully check collection existence when KB is initialized")
|
||||
|
||||
var response map[string]interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, response, "exists")
|
||||
assert.Contains(t, response, "collection_id")
|
||||
assert.Equal(t, testCollectionID, response["collection_id"])
|
||||
|
||||
exists, ok := response["exists"].(bool)
|
||||
assert.True(t, ok, "exists should be a boolean")
|
||||
|
||||
t.Logf("Collection %s exists: %v", testCollectionID, exists)
|
||||
})
|
||||
|
||||
t.Run("CollectionExistsMissingID", func(t *testing.T) {
|
||||
// Test with missing collection ID in path
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/kb/collections//exists", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// This should result in a bad request due to empty collection ID
|
||||
if resp.StatusCode == http.StatusBadRequest {
|
||||
var errorResponse map[string]interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&errorResponse)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, errorResponse, "error")
|
||||
t.Logf("Correctly rejected request with missing collection ID")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestGetCollections tests the collections listing endpoint
|
||||
func TestGetCollections(t *testing.T) {
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
|
||||
baseURL := ""
|
||||
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||
baseURL = openapi.Server.Config.BaseURL
|
||||
}
|
||||
|
||||
client := testutils.RegisterTestClient(t, "KB Collections List Test Client", []string{"https://localhost/callback"})
|
||||
defer testutils.CleanupTestClient(t, client.ClientID)
|
||||
tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
|
||||
t.Run("GetCollectionsSuccess", func(t *testing.T) {
|
||||
// Test listing all collections
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/kb/collections", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Knowledge base should be initialized for this test to be meaningful
|
||||
if resp.StatusCode == http.StatusInternalServerError {
|
||||
var errorResponse map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&errorResponse)
|
||||
if errorDescription, ok := errorResponse["error_description"].(string); ok &&
|
||||
strings.Contains(errorDescription, "Knowledge base not initialized") {
|
||||
t.Skip("Knowledge base not initialized - skipping test (this indicates environment setup issue)")
|
||||
}
|
||||
}
|
||||
|
||||
// Expect successful response
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode, "Should successfully retrieve collections when KB is initialized")
|
||||
|
||||
var collections []interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&collections)
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Logf("Successfully retrieved %d collections", len(collections))
|
||||
})
|
||||
|
||||
t.Run("GetCollectionsWithFilter", func(t *testing.T) {
|
||||
// Test listing collections with filter parameters
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/kb/collections?category=documents&status=active", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Knowledge base should be initialized for this test to be meaningful
|
||||
if resp.StatusCode == http.StatusInternalServerError {
|
||||
var errorResponse map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&errorResponse)
|
||||
if errorDescription, ok := errorResponse["error_description"].(string); ok &&
|
||||
strings.Contains(errorDescription, "Knowledge base not initialized") {
|
||||
t.Skip("Knowledge base not initialized - skipping test (this indicates environment setup issue)")
|
||||
}
|
||||
}
|
||||
|
||||
// Expect successful response
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode, "Should successfully retrieve collections when KB is initialized")
|
||||
|
||||
var collections []interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&collections)
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Logf("Successfully retrieved %d filtered collections", len(collections))
|
||||
})
|
||||
|
||||
t.Run("GetCollectionsWithMultipleFilters", func(t *testing.T) {
|
||||
// Test with multiple filter parameters
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/kb/collections?category=test&owner=testuser&type=public", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Knowledge base should be initialized for this test to be meaningful
|
||||
if resp.StatusCode == http.StatusInternalServerError {
|
||||
var errorResponse map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&errorResponse)
|
||||
if errorDescription, ok := errorResponse["error_description"].(string); ok &&
|
||||
strings.Contains(errorDescription, "Knowledge base not initialized") {
|
||||
t.Skip("Knowledge base not initialized - skipping test (this indicates environment setup issue)")
|
||||
}
|
||||
}
|
||||
|
||||
// Expect successful response
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode, "Should successfully retrieve collections when KB is initialized")
|
||||
|
||||
var collections []interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&collections)
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Logf("Successfully retrieved %d collections with multiple filters", len(collections))
|
||||
})
|
||||
}
|
||||
|
||||
// TestCollectionEndpointsUnauthorized tests that endpoints return 401 when not authenticated
|
||||
func TestCollectionEndpointsUnauthorized(t *testing.T) {
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
|
||||
baseURL := ""
|
||||
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||
baseURL = openapi.Server.Config.BaseURL
|
||||
}
|
||||
|
||||
endpoints := []struct {
|
||||
method string
|
||||
path string
|
||||
body string
|
||||
}{
|
||||
{"GET", "/kb/collections", ""},
|
||||
{"POST", "/kb/collections", `{"id":"test","config":{}}`},
|
||||
{"DELETE", "/kb/collections/test", ""},
|
||||
{"GET", "/kb/collections/test/exists", ""},
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
t.Run(fmt.Sprintf("Unauthorized_%s_%s", endpoint.method, endpoint.path), func(t *testing.T) {
|
||||
var req *http.Request
|
||||
var err error
|
||||
|
||||
if endpoint.body != "" {
|
||||
req, err = http.NewRequest(endpoint.method, serverURL+baseURL+endpoint.path, bytes.NewBufferString(endpoint.body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
} else {
|
||||
req, err = http.NewRequest(endpoint.method, serverURL+baseURL+endpoint.path, nil)
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
// No Authorization header
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
|
||||
t.Logf("Correctly rejected unauthorized request to %s %s", endpoint.method, endpoint.path)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCollectionIntegration tests the full collection lifecycle
|
||||
func TestCollectionIntegration(t *testing.T) {
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
|
||||
baseURL := ""
|
||||
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||
baseURL = openapi.Server.Config.BaseURL
|
||||
}
|
||||
|
||||
client := testutils.RegisterTestClient(t, "KB Collection Integration Test Client", []string{"https://localhost/callback"})
|
||||
defer testutils.CleanupTestClient(t, client.ClientID)
|
||||
tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
|
||||
testCollectionID := fmt.Sprintf("test_integration_%d", time.Now().UnixNano())
|
||||
|
||||
t.Run("FullCollectionLifecycle", func(t *testing.T) {
|
||||
// Register collection for cleanup (in case lifecycle test fails)
|
||||
testutils.RegisterTestCollection(testCollectionID)
|
||||
|
||||
// Step 1: Check that collection doesn't exist initially
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/kb/collections/"+testCollectionID+"/exists", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Step 2: Create the collection
|
||||
createData := map[string]interface{}{
|
||||
"id": testCollectionID,
|
||||
"metadata": map[string]interface{}{
|
||||
"category": "integration_test",
|
||||
"purpose": "full_lifecycle_test",
|
||||
},
|
||||
"config": map[string]interface{}{
|
||||
"collection_name": testCollectionID + "_vector",
|
||||
"embedding_model": "text-embedding-ada-002",
|
||||
"chunk_size": 1000,
|
||||
"chunk_overlap": 200,
|
||||
"index_type": "hnsw",
|
||||
"distance": "cosine",
|
||||
"dimension": 1536,
|
||||
},
|
||||
}
|
||||
|
||||
body, err := json.Marshal(createData)
|
||||
assert.NoError(t, err)
|
||||
|
||||
createReq, err := http.NewRequest("POST", serverURL+baseURL+"/kb/collections", bytes.NewBuffer(body))
|
||||
assert.NoError(t, err)
|
||||
createReq.Header.Set("Content-Type", "application/json")
|
||||
createReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
createResp, err := http.DefaultClient.Do(createReq)
|
||||
assert.NoError(t, err)
|
||||
defer createResp.Body.Close()
|
||||
|
||||
// Step 3: Verify collection appears in listings (with filter)
|
||||
listReq, err := http.NewRequest("GET", serverURL+baseURL+"/kb/collections?category=integration_test", nil)
|
||||
assert.NoError(t, err)
|
||||
listReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
listResp, err := http.DefaultClient.Do(listReq)
|
||||
assert.NoError(t, err)
|
||||
defer listResp.Body.Close()
|
||||
|
||||
// Step 4: Check that collection now exists
|
||||
existsReq, err := http.NewRequest("GET", serverURL+baseURL+"/kb/collections/"+testCollectionID+"/exists", nil)
|
||||
assert.NoError(t, err)
|
||||
existsReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
existsResp, err := http.DefaultClient.Do(existsReq)
|
||||
assert.NoError(t, err)
|
||||
defer existsResp.Body.Close()
|
||||
|
||||
// Step 5: Remove the collection
|
||||
deleteReq, err := http.NewRequest("DELETE", serverURL+baseURL+"/kb/collections/"+testCollectionID, nil)
|
||||
assert.NoError(t, err)
|
||||
deleteReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
deleteResp, err := http.DefaultClient.Do(deleteReq)
|
||||
assert.NoError(t, err)
|
||||
defer deleteResp.Body.Close()
|
||||
|
||||
// Step 6: Verify collection no longer exists
|
||||
finalExistsReq, err := http.NewRequest("GET", serverURL+baseURL+"/kb/collections/"+testCollectionID+"/exists", nil)
|
||||
assert.NoError(t, err)
|
||||
finalExistsReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
finalExistsResp, err := http.DefaultClient.Do(finalExistsReq)
|
||||
assert.NoError(t, err)
|
||||
defer finalExistsResp.Body.Close()
|
||||
|
||||
t.Logf("Completed full collection lifecycle test for: %s", testCollectionID)
|
||||
})
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ import (
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/kb"
|
||||
"github.com/yaoapp/yao/openapi"
|
||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||
"github.com/yaoapp/yao/test"
|
||||
|
|
@ -28,6 +29,52 @@ var testMutex sync.RWMutex
|
|||
// activeTestCount tracks the number of active tests using the global server
|
||||
var activeTestCount int
|
||||
|
||||
// testCollections tracks collections created during tests for cleanup
|
||||
var testCollections []string
|
||||
var testCollectionsMutex sync.Mutex
|
||||
|
||||
// RegisterTestCollection adds a collection ID to the cleanup list
|
||||
func RegisterTestCollection(collectionID string) {
|
||||
testCollectionsMutex.Lock()
|
||||
defer testCollectionsMutex.Unlock()
|
||||
testCollections = append(testCollections, collectionID)
|
||||
}
|
||||
|
||||
// CleanupTestCollections removes all registered test collections
|
||||
func CleanupTestCollections(t *testing.T) {
|
||||
testCollectionsMutex.Lock()
|
||||
collectionsToClean := make([]string, len(testCollections))
|
||||
copy(collectionsToClean, testCollections)
|
||||
testCollections = nil // Clear the list
|
||||
testCollectionsMutex.Unlock()
|
||||
|
||||
if len(collectionsToClean) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if KB instance is available
|
||||
if kb.Instance == nil {
|
||||
t.Logf("Warning: KB instance not available, cannot cleanup %d test collections", len(collectionsToClean))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
cleanedCount := 0
|
||||
for _, collectionID := range collectionsToClean {
|
||||
removed, err := kb.Instance.RemoveCollection(ctx, collectionID)
|
||||
if err != nil {
|
||||
t.Logf("Warning: Failed to cleanup test collection %s: %v", collectionID, err)
|
||||
} else if removed {
|
||||
cleanedCount++
|
||||
t.Logf("Cleaned up test collection: %s", collectionID)
|
||||
}
|
||||
}
|
||||
|
||||
if cleanedCount > 0 {
|
||||
t.Logf("Successfully cleaned up %d/%d test collections", cleanedCount, len(collectionsToClean))
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare initializes the OpenAPI test environment and starts a mock HTTP server.
|
||||
//
|
||||
// AI ASSISTANT INSTRUCTIONS:
|
||||
|
|
@ -102,6 +149,14 @@ func Prepare(t *testing.T) string {
|
|||
// Step 1: Initialize base test environment with all Yao dependencies
|
||||
test.Prepare(t, config.Conf)
|
||||
|
||||
// Step 1.5: Initialize Knowledge Base (must be done before OpenAPI load)
|
||||
_, err := kb.Load(config.Conf)
|
||||
if err != nil {
|
||||
// KB loading failure is not fatal for tests, just log it
|
||||
t.Logf("Warning: Failed to load Knowledge Base: %v", err)
|
||||
t.Logf("Some KB-related tests may not work properly")
|
||||
}
|
||||
|
||||
// Step 2: Initialize OpenAPI server and make it available globally (only if not already initialized)
|
||||
if openapi.Server == nil {
|
||||
_, err := openapi.Load(config.Conf)
|
||||
|
|
@ -163,7 +218,11 @@ func Prepare(t *testing.T) string {
|
|||
//
|
||||
// This ensures no state leakage between tests and prevents memory leaks
|
||||
//
|
||||
// Step 3: Calls test.Clean() to clean up the base test environment
|
||||
// Step 3: Clean up GraphRag test collections
|
||||
//
|
||||
// This removes any test collections that were created during the test
|
||||
//
|
||||
// Step 4: Calls test.Clean() to clean up the base test environment
|
||||
//
|
||||
// This closes database connections, cleans up temporary files, and resets global state
|
||||
//
|
||||
|
|
@ -196,7 +255,29 @@ func Clean() {
|
|||
}
|
||||
testMutex.Unlock()
|
||||
|
||||
// Step 3: Clean up base test environment
|
||||
// Step 3: Clean up GraphRag test collections (only when cleaning global state)
|
||||
if shouldCleanGlobalState {
|
||||
// Create a dummy test object for logging (since Clean doesn't receive *testing.T)
|
||||
// Note: This is a limitation, but we can still attempt cleanup
|
||||
ctx := context.Background()
|
||||
testCollectionsMutex.Lock()
|
||||
collectionsToClean := make([]string, len(testCollections))
|
||||
copy(collectionsToClean, testCollections)
|
||||
testCollections = nil // Clear the list
|
||||
testCollectionsMutex.Unlock()
|
||||
|
||||
if len(collectionsToClean) > 0 && kb.Instance != nil {
|
||||
for _, collectionID := range collectionsToClean {
|
||||
_, err := kb.Instance.RemoveCollection(ctx, collectionID)
|
||||
if err != nil {
|
||||
// Can't use t.Logf here, but errors will be visible in test output
|
||||
_ = err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Clean up base test environment
|
||||
if shouldCleanGlobalState {
|
||||
test.Clean()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue