yao/kb/api/addfile_test.go
Max 01820d9fe2 Add function execution support to Job system
- Introduced `AddFunc` method to the `Job` struct for adding Go functions as job executions, allowing for dynamic execution of functions with specified arguments.
- Enhanced internal execution handling to register functions in a global registry, ensuring proper cleanup after execution.
- Implemented `ExecuteFunc` method in the `Goroutine` struct to handle the execution of registered functions, including error handling and context management.
- Added comprehensive unit tests for `AddFunc`, verifying function registration, execution, and memory cleanup post-execution.
- Updated related documentation to reflect the new functionality and usage patterns for adding and executing Go functions within the job system.
2025-12-19 14:50:53 +08:00

585 lines
16 KiB
Go

package api_test
import (
"context"
"fmt"
"mime/multipart"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
graphragtypes "github.com/yaoapp/gou/graphrag/types"
"github.com/yaoapp/yao/attachment"
"github.com/yaoapp/yao/kb"
"github.com/yaoapp/yao/kb/api"
)
// Note: TestMain is defined in collection_test.go, which handles environment setup
// Run tests with: source env.local.sh && go test -v ./kb/api/...
// createTestCollectionForFile is a helper to create a test collection for file tests
func createTestCollectionForFile(t *testing.T, ctx context.Context) string {
if kb.API == nil {
t.Skip("KB API not initialized")
}
collectionID := fmt.Sprintf("test_file_%d", time.Now().UnixNano())
params := &api.CreateCollectionParams{
ID: collectionID,
Metadata: map[string]interface{}{
"name": "Test File Collection",
"description": "Collection for AddFile tests",
},
EmbeddingProviderID: "__yao.openai",
EmbeddingOptionID: "text-embedding-3-small",
Locale: "en",
Config: &graphragtypes.CreateCollectionOptions{
Distance: "cosine",
IndexType: "hnsw",
},
}
_, err := kb.API.CreateCollection(ctx, params)
if err != nil {
t.Fatalf("Failed to create test collection: %v", err)
}
return collectionID
}
// cleanupTestCollectionForFile removes a test collection
func cleanupTestCollectionForFile(ctx context.Context, collectionID string) {
if kb.API != nil {
_, _ = kb.API.RemoveCollection(ctx, collectionID)
}
}
// ========== AddFile Tests ==========
// Note: Full AddFile tests require actual files to be uploaded via attachment manager
// These tests verify parameter validation and error handling
func TestAddFile(t *testing.T) {
if kb.API == nil {
t.Skip("KB API not initialized")
}
ctx := context.Background()
collectionID := createTestCollectionForFile(t, ctx)
defer cleanupTestCollectionForFile(ctx, collectionID)
t.Run("AddFileMissingCollectionID", func(t *testing.T) {
params := &api.AddFileParams{
FileID: "some_file_id",
Chunking: &api.ProviderConfigParams{
ProviderID: "__yao.structured",
},
Embedding: &api.ProviderConfigParams{
ProviderID: "__yao.openai",
},
}
result, err := kb.API.AddFile(ctx, params)
assert.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "collection_id is required")
})
t.Run("AddFileMissingFileID", func(t *testing.T) {
params := &api.AddFileParams{
CollectionID: collectionID,
Chunking: &api.ProviderConfigParams{
ProviderID: "__yao.structured",
},
Embedding: &api.ProviderConfigParams{
ProviderID: "__yao.openai",
},
}
result, err := kb.API.AddFile(ctx, params)
assert.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "file_id is required")
})
t.Run("AddFileMissingChunking", func(t *testing.T) {
params := &api.AddFileParams{
CollectionID: collectionID,
FileID: "some_file_id",
Embedding: &api.ProviderConfigParams{
ProviderID: "__yao.openai",
},
}
result, err := kb.API.AddFile(ctx, params)
assert.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "chunking configuration is required")
})
t.Run("AddFileMissingEmbedding", func(t *testing.T) {
params := &api.AddFileParams{
CollectionID: collectionID,
FileID: "some_file_id",
Chunking: &api.ProviderConfigParams{
ProviderID: "__yao.structured",
},
}
result, err := kb.API.AddFile(ctx, params)
assert.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "embedding configuration is required")
})
t.Run("AddFileInvalidUploader", func(t *testing.T) {
params := &api.AddFileParams{
CollectionID: collectionID,
FileID: "some_file_id",
Uploader: "invalid_uploader",
Chunking: &api.ProviderConfigParams{
ProviderID: "__yao.structured",
},
Embedding: &api.ProviderConfigParams{
ProviderID: "__yao.openai",
},
}
result, err := kb.API.AddFile(ctx, params)
assert.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "invalid uploader")
})
t.Run("AddFileNotFound", func(t *testing.T) {
params := &api.AddFileParams{
CollectionID: collectionID,
FileID: "nonexistent_file_id",
Chunking: &api.ProviderConfigParams{
ProviderID: "__yao.structured",
},
Embedding: &api.ProviderConfigParams{
ProviderID: "__yao.openai",
},
}
result, err := kb.API.AddFile(ctx, params)
assert.Error(t, err)
assert.Nil(t, result)
// Error could be "file not found" or "invalid uploader" depending on environment
assert.True(t, err != nil, "Expected an error")
})
}
// ========== AddFileAsync Tests ==========
func TestAddFileAsync(t *testing.T) {
if kb.API == nil {
t.Skip("KB API not initialized")
}
ctx := context.Background()
collectionID := createTestCollectionForFile(t, ctx)
defer cleanupTestCollectionForFile(ctx, collectionID)
t.Run("AddFileAsyncMissingCollectionID", func(t *testing.T) {
params := &api.AddFileParams{
FileID: "some_file_id",
Chunking: &api.ProviderConfigParams{
ProviderID: "__yao.structured",
},
Embedding: &api.ProviderConfigParams{
ProviderID: "__yao.openai",
},
}
result, err := kb.API.AddFileAsync(ctx, params)
assert.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "collection_id is required")
})
t.Run("AddFileAsyncMissingFileID", func(t *testing.T) {
params := &api.AddFileParams{
CollectionID: collectionID,
Chunking: &api.ProviderConfigParams{
ProviderID: "__yao.structured",
},
Embedding: &api.ProviderConfigParams{
ProviderID: "__yao.openai",
},
}
result, err := kb.API.AddFileAsync(ctx, params)
assert.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "file_id is required")
})
t.Run("AddFileAsyncMissingChunking", func(t *testing.T) {
params := &api.AddFileParams{
CollectionID: collectionID,
FileID: "some_file_id",
Embedding: &api.ProviderConfigParams{
ProviderID: "__yao.openai",
},
}
result, err := kb.API.AddFileAsync(ctx, params)
assert.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "chunking configuration is required")
})
t.Run("AddFileAsyncMissingEmbedding", func(t *testing.T) {
params := &api.AddFileParams{
CollectionID: collectionID,
FileID: "some_file_id",
Chunking: &api.ProviderConfigParams{
ProviderID: "__yao.structured",
},
}
result, err := kb.API.AddFileAsync(ctx, params)
assert.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "embedding configuration is required")
})
t.Run("AddFileAsyncInvalidUploader", func(t *testing.T) {
params := &api.AddFileParams{
CollectionID: collectionID,
FileID: "some_file_id",
Uploader: "invalid_uploader",
Chunking: &api.ProviderConfigParams{
ProviderID: "__yao.structured",
},
Embedding: &api.ProviderConfigParams{
ProviderID: "__yao.openai",
},
}
result, err := kb.API.AddFileAsync(ctx, params)
assert.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "invalid uploader")
})
t.Run("AddFileAsyncNotFound", func(t *testing.T) {
params := &api.AddFileParams{
CollectionID: collectionID,
FileID: "nonexistent_file_id",
Chunking: &api.ProviderConfigParams{
ProviderID: "__yao.structured",
},
Embedding: &api.ProviderConfigParams{
ProviderID: "__yao.openai",
},
}
result, err := kb.API.AddFileAsync(ctx, params)
assert.Error(t, err)
assert.Nil(t, result)
// Error could be "file not found" or "invalid uploader" depending on environment
assert.True(t, err != nil, "Expected an error")
})
}
// ========== AddFile with Real File Tests ==========
// getTestUploader returns the uploader name and manager for testing
func getTestUploader(t *testing.T) (string, *attachment.Manager) {
// Try __yao.attachment first (system uploader)
if manager, ok := attachment.Managers["__yao.attachment"]; ok {
return "__yao.attachment", manager
}
// Try local manager
if manager, ok := attachment.Managers["local"]; ok {
return "local", manager
}
// List available managers for debugging
var available []string
for name := range attachment.Managers {
available = append(available, name)
}
t.Fatalf("No attachment manager available. Available managers: %v", available)
return "", nil
}
// uploadTestFile uploads a test file using the attachment manager and returns the file ID
func uploadTestFile(t *testing.T, ctx context.Context, filename, content string) string {
_, manager := getTestUploader(t)
// Create file header
fileHeader := &attachment.FileHeader{
FileHeader: &multipart.FileHeader{
Filename: filename,
Size: int64(len(content)),
Header: make(map[string][]string),
},
}
fileHeader.Header.Set("Content-Type", "text/plain")
// Upload the file
reader := strings.NewReader(content)
file, err := manager.Upload(ctx, fileHeader, reader, attachment.UploadOption{})
if err != nil {
t.Fatalf("Failed to upload test file: %v", err)
}
t.Logf("Uploaded test file: %s (ID: %s)", filename, file.ID)
return file.ID
}
// cleanupTestFile removes a test file
func cleanupTestFile(ctx context.Context, t *testing.T, fileID string) {
_, manager := getTestUploader(t)
_ = manager.Delete(ctx, fileID)
}
func TestAddFileWithRealFile(t *testing.T) {
if kb.API == nil {
t.Skip("KB API not initialized")
}
ctx := context.Background()
collectionID := createTestCollectionForFile(t, ctx)
defer cleanupTestCollectionForFile(ctx, collectionID)
// Get the uploader name
uploaderName, _ := getTestUploader(t)
// Upload a test file
testContent := `This is a test document for the knowledge base.
It contains content to test the file processing functionality.`
fileID := uploadTestFile(t, ctx, "test_document.txt", testContent)
defer cleanupTestFile(ctx, t, fileID)
t.Run("AddFileSuccess", func(t *testing.T) {
params := &api.AddFileParams{
CollectionID: collectionID,
FileID: fileID,
Uploader: uploaderName,
Locale: "en",
Metadata: map[string]interface{}{
"description": "A test file document",
},
Chunking: &api.ProviderConfigParams{
ProviderID: "__yao.structured",
OptionID: "standard",
},
Embedding: &api.ProviderConfigParams{
ProviderID: "__yao.openai",
OptionID: "text-embedding-3-small",
},
AuthScope: map[string]interface{}{
"__yao_created_by": "test_user",
},
}
result, err := kb.API.AddFile(ctx, params)
assert.NoError(t, err)
assert.NotNil(t, result)
if result != nil {
assert.Equal(t, collectionID, result.CollectionID)
assert.NotEmpty(t, result.DocID)
assert.Equal(t, fileID, result.FileID)
assert.Contains(t, result.Message, "successfully")
t.Logf("Added file document: %s", result.DocID)
}
// Verify document was created
if result != nil {
doc, err := kb.API.GetDocument(ctx, result.DocID, nil)
assert.NoError(t, err)
assert.NotNil(t, doc)
assert.Equal(t, "file", doc["type"])
assert.Equal(t, "completed", doc["status"])
t.Logf("✅ File Document verified: type=%v, status=%v", doc["type"], doc["status"])
}
})
}
func TestAddFileAsyncWithRealFile(t *testing.T) {
if kb.API == nil {
t.Skip("KB API not initialized")
}
ctx := context.Background()
collectionID := createTestCollectionForFile(t, ctx)
defer cleanupTestCollectionForFile(ctx, collectionID)
// Get the uploader name
uploaderName, _ := getTestUploader(t)
// Upload a test file for async processing
testContent := `Async test document content.
This document will be processed asynchronously.
The job system should handle the processing in the background.`
fileID := uploadTestFile(t, ctx, "async_test_document.txt", testContent)
defer cleanupTestFile(ctx, t, fileID)
t.Run("AddFileAsyncSuccess", func(t *testing.T) {
params := &api.AddFileParams{
CollectionID: collectionID,
FileID: fileID,
Uploader: uploaderName,
Locale: "en",
Chunking: &api.ProviderConfigParams{
ProviderID: "__yao.structured",
OptionID: "standard",
},
Embedding: &api.ProviderConfigParams{
ProviderID: "__yao.openai",
OptionID: "text-embedding-3-small",
},
Job: &api.JobOptionsParams{
Name: "Test Async File Job",
Description: "Testing async file processing",
Category: "Test",
},
}
result, err := kb.API.AddFileAsync(ctx, params)
assert.NoError(t, err)
assert.NotNil(t, result)
if result != nil {
assert.NotEmpty(t, result.JobID)
assert.NotEmpty(t, result.DocID)
t.Logf("Created async file job: %s for document: %s", result.JobID, result.DocID)
}
// Verify document was created with pending status
if result != nil {
doc, err := kb.API.GetDocument(ctx, result.DocID, nil)
assert.NoError(t, err)
assert.NotNil(t, doc)
assert.Equal(t, "file", doc["type"])
assert.Equal(t, result.JobID, doc["job_id"])
t.Logf("✅ Async file document created: status=%v, job_id=%v", doc["status"], doc["job_id"])
// Wait for job to complete (max 30 seconds)
maxWait := 30 * time.Second
pollInterval := 500 * time.Millisecond
startTime := time.Now()
var finalStatus string
for time.Since(startTime) < maxWait {
doc, err = kb.API.GetDocument(ctx, result.DocID, nil)
if err != nil {
t.Logf("Error getting document: %v", err)
break
}
finalStatus, _ = doc["status"].(string)
if finalStatus == "completed" || finalStatus == "error" {
break
}
time.Sleep(pollInterval)
}
t.Logf("✅ Job completed: final status=%s, elapsed=%v", finalStatus, time.Since(startTime))
assert.Equal(t, "completed", finalStatus, "Job should complete successfully")
}
})
}
func TestAddFileIntegration(t *testing.T) {
if kb.API == nil {
t.Skip("KB API not initialized")
}
ctx := context.Background()
collectionID := createTestCollectionForFile(t, ctx)
defer cleanupTestCollectionForFile(ctx, collectionID)
// Get the uploader name
uploaderName, _ := getTestUploader(t)
t.Run("FullFileLifecycle", func(t *testing.T) {
// Upload a test file
testContent := `Integration test document.
This document tests the full lifecycle of file processing:
1. Upload file
2. Add to knowledge base
3. Verify document creation
4. List documents
5. Remove document
End of test content.`
fileID := uploadTestFile(t, ctx, "lifecycle_test.txt", testContent)
defer cleanupTestFile(ctx, t, fileID)
// 1. Add File Document
addParams := &api.AddFileParams{
CollectionID: collectionID,
FileID: fileID,
Uploader: uploaderName,
Locale: "en",
Metadata: map[string]interface{}{
"title": "File Lifecycle Test",
"description": "Full lifecycle integration test",
},
Chunking: &api.ProviderConfigParams{
ProviderID: "__yao.structured",
OptionID: "standard",
},
Embedding: &api.ProviderConfigParams{
ProviderID: "__yao.openai",
OptionID: "text-embedding-3-small",
},
AuthScope: map[string]interface{}{
"__yao_created_by": "integration_test",
},
}
result, err := kb.API.AddFile(ctx, addParams)
assert.NoError(t, err)
assert.NotNil(t, result)
if result == nil {
t.Fatalf("Failed to create file document: result is nil")
}
t.Logf("1. Created file document: %s", result.DocID)
// 2. Get Document
doc, err := kb.API.GetDocument(ctx, result.DocID, nil)
assert.NoError(t, err)
assert.NotNil(t, doc)
assert.Equal(t, "file", doc["type"])
assert.Equal(t, "completed", doc["status"])
t.Logf("2. Retrieved document: name=%v, type=%v, status=%v", doc["name"], doc["type"], doc["status"])
// 3. List Documents
listFilter := &api.ListDocumentsFilter{
Page: 1,
PageSize: 20,
CollectionID: collectionID,
}
listResult, err := kb.API.ListDocuments(ctx, listFilter)
assert.NoError(t, err)
assert.GreaterOrEqual(t, len(listResult.Data), 1)
t.Logf("3. Found document in list: %d documents", len(listResult.Data))
// 4. Remove Document
removeParams := &api.RemoveDocumentsParams{
DocumentIDs: []string{result.DocID},
}
removeResult, err := kb.API.RemoveDocuments(ctx, removeParams)
assert.NoError(t, err)
assert.NotNil(t, removeResult)
t.Logf("4. Removed document: %d deleted", removeResult.DeletedCount)
// 5. Verify Removal
_, err = kb.API.GetDocument(ctx, result.DocID, nil)
assert.Error(t, err)
t.Logf("5. Verified document removal")
t.Logf("✅ Full file lifecycle test completed successfully")
})
}