From 52f74e5edd0c49711253e64a818145bec411dfd7 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 7 Nov 2025 14:43:01 +0800 Subject: [PATCH] Enhance assistant model handling and storage logic - Added handling for nullable string fields in the AssistantModel, ensuring empty strings are stored as NULL in the database. - Implemented default value assignment for the Share field, applying "private" when no value is provided. - Introduced a new test case to verify that empty strings are correctly stored as NULL and that default values are applied as expected. - Updated existing tests to include the Share field in various assistant scenarios, ensuring comprehensive coverage. --- agent/store/xun.go | 46 +++++++++-- agent/store/xun_assistant_test.go | 123 ++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 6 deletions(-) diff --git a/agent/store/xun.go b/agent/store/xun.go index 5df1e5a1..2306c2fc 100644 --- a/agent/store/xun.go +++ b/agent/store/xun.go @@ -995,33 +995,67 @@ func (conv *Xun) SaveAssistant(assistant *AssistantModel) (string, error) { data := make(map[string]interface{}) data["assistant_id"] = assistant.ID data["type"] = assistant.Type - data["name"] = assistant.Name - data["avatar"] = assistant.Avatar data["connector"] = assistant.Connector - data["path"] = assistant.Path data["built_in"] = assistant.BuiltIn data["sort"] = assistant.Sort - data["description"] = assistant.Description data["readonly"] = assistant.Readonly data["public"] = assistant.Public - data["share"] = assistant.Share data["mentionable"] = assistant.Mentionable data["automated"] = assistant.Automated data["created_at"] = assistant.CreatedAt data["updated_at"] = assistant.UpdatedAt - // Permission management fields + // Handle nullable string fields from assistant.mod.yao + // Store as nil if empty string (this matches database nullable: true fields) + if assistant.Name != "" { + data["name"] = assistant.Name + } else { + data["name"] = nil + } + if assistant.Avatar != "" { + data["avatar"] = assistant.Avatar + } else { + data["avatar"] = nil + } + if assistant.Description != "" { + data["description"] = assistant.Description + } else { + data["description"] = nil + } + if assistant.Path != "" { + data["path"] = assistant.Path + } else { + data["path"] = nil + } + + // Share field: nullable: false with default "private" + // Apply default if empty + if assistant.Share != "" { + data["share"] = assistant.Share + } else { + data["share"] = "private" // Apply default value + } + + // Permission management fields - store as nil if empty if assistant.YaoCreatedBy != "" { data["__yao_created_by"] = assistant.YaoCreatedBy + } else { + data["__yao_created_by"] = nil } if assistant.YaoUpdatedBy != "" { data["__yao_updated_by"] = assistant.YaoUpdatedBy + } else { + data["__yao_updated_by"] = nil } if assistant.YaoTeamID != "" { data["__yao_team_id"] = assistant.YaoTeamID + } else { + data["__yao_team_id"] = nil } if assistant.YaoTenantID != "" { data["__yao_tenant_id"] = assistant.YaoTenantID + } else { + data["__yao_tenant_id"] = nil } // Handle simple types diff --git a/agent/store/xun_assistant_test.go b/agent/store/xun_assistant_test.go index ac4d4d0f..b4f6f4ce 100644 --- a/agent/store/xun_assistant_test.go +++ b/agent/store/xun_assistant_test.go @@ -43,6 +43,8 @@ func TestSaveAssistant(t *testing.T) { Sort: 100, BuiltIn: false, Readonly: false, + Public: false, + Share: "private", Mentionable: true, Automated: true, } @@ -71,6 +73,7 @@ func TestSaveAssistant(t *testing.T) { Connector: "openai", Description: "Original description", Tags: []string{"original"}, + Share: "private", } id, err := store.SaveAssistant(assistant) @@ -150,6 +153,7 @@ func TestSaveAssistant(t *testing.T) { Name: "Complex Assistant", Type: "assistant", Connector: "openai", + Share: "private", Prompts: []Prompt{ {Role: "system", Content: "You are a helpful assistant"}, {Role: "user", Content: "Hello"}, @@ -210,6 +214,7 @@ func TestDeleteAssistant(t *testing.T) { Name: "Delete Test", Type: "assistant", Connector: "openai", + Share: "private", } id, err := store.SaveAssistant(assistant) @@ -262,6 +267,7 @@ func TestGetAssistant(t *testing.T) { Tags: []string{"tag1", "tag2"}, Sort: 150, BuiltIn: false, + Share: "private", Mentionable: true, } @@ -335,6 +341,7 @@ func TestGetAssistants(t *testing.T) { Description: "First test assistant", Tags: []string{"test", "automation"}, Sort: 100, + Share: "private", Mentionable: true, Automated: true, }, @@ -345,6 +352,7 @@ func TestGetAssistants(t *testing.T) { Description: "Second test assistant", Tags: []string{"test", "manual"}, Sort: 200, + Share: "private", Mentionable: false, Automated: false, }, @@ -355,6 +363,7 @@ func TestGetAssistants(t *testing.T) { Description: "Third test bot", Tags: []string{"bot", "automation"}, Sort: 50, + Share: "private", Mentionable: true, Automated: true, }, @@ -638,6 +647,7 @@ func TestDeleteAssistants(t *testing.T) { Type: "assistant", Connector: "openai", Tags: []string{tag}, + Share: "private", } _, err := store.SaveAssistant(assistant) if err != nil { @@ -666,6 +676,7 @@ func TestDeleteAssistants(t *testing.T) { Name: fmt.Sprintf("Connector Test %d", i), Type: "assistant", Connector: connector, + Share: "private", } _, err := store.SaveAssistant(assistant) if err != nil { @@ -694,6 +705,7 @@ func TestDeleteAssistants(t *testing.T) { Type: "assistant", Connector: "openai", Description: "Test description", + Share: "private", } _, err := store.SaveAssistant(assistant) if err != nil { @@ -719,6 +731,7 @@ func TestDeleteAssistants(t *testing.T) { Name: "Single Delete Test", Type: "assistant", Connector: "openai", + Share: "private", } id, err := store.SaveAssistant(assistant) if err != nil { @@ -761,18 +774,21 @@ func TestGetAssistantTags(t *testing.T) { Type: "assistant", Connector: "openai", Tags: []string{uniqueTag, "common"}, + Share: "private", }, { Name: "Tags Test 2", Type: "assistant", Connector: "openai", Tags: []string{uniqueTag, "different"}, + Share: "private", }, { Name: "Tags Test 3", Type: "assistant", Connector: "openai", Tags: []string{"common", "another"}, + Share: "private", }, } @@ -866,6 +882,7 @@ func TestAssistantPermissionFields(t *testing.T) { Type: "assistant", Connector: "openai", Description: "Testing permission fields", + Share: "private", YaoCreatedBy: "user-123", YaoUpdatedBy: "user-123", YaoTeamID: "team-456", @@ -905,6 +922,7 @@ func TestAssistantPermissionFields(t *testing.T) { Name: "Update Permission Test", Type: "assistant", Connector: "openai", + Share: "private", YaoCreatedBy: "user-original", YaoTeamID: "team-original", } @@ -947,6 +965,7 @@ func TestAssistantPermissionFields(t *testing.T) { Name: "No Permission Fields", Type: "assistant", Connector: "openai", + Share: "private", } id, err := store.SaveAssistant(assistant) @@ -975,6 +994,109 @@ func TestAssistantPermissionFields(t *testing.T) { }) } +// TestEmptyStringAsNull tests that empty strings are stored as NULL in database +func TestEmptyStringAsNull(t *testing.T) { + test.Prepare(t, config.Conf) + defer test.Clean() + + store, err := NewXun(Setting{ + Connector: "default", + }) + if err != nil { + t.Fatalf("Failed to create store: %v", err) + } + defer store.Close() + + t.Run("EmptyStringsStoredAsNull", func(t *testing.T) { + // Create assistant with empty strings for nullable fields + // According to assistant.mod.yao, nullable string fields are: + // - name (nullable: true, but required by validation) + // - avatar, description, path (nullable: true) + // - share (nullable: false, but empty should trigger default) + assistant := &AssistantModel{ + Name: "Test Null Fields", // Required by validation + Type: "assistant", + Connector: "openai", + Avatar: "", // Empty string should become NULL (nullable: true) + Path: "", // Empty string should become NULL (nullable: true) + Description: "", // Empty string should become NULL (nullable: true) + Share: "", // Empty string should become NULL, then default "private" applied + } + + id, err := store.SaveAssistant(assistant) + if err != nil { + t.Fatalf("Failed to save assistant: %v", err) + } + + // Retrieve and verify empty strings are returned (not stored as empty strings) + retrieved, err := store.GetAssistant(id) + if err != nil { + t.Fatalf("Failed to get assistant: %v", err) + } + + // Name should be preserved (required field) + if retrieved.Name != "Test Null Fields" { + t.Errorf("Expected Name 'Test Null Fields', got '%s'", retrieved.Name) + } + + // These nullable fields should be empty strings in Go (converted from NULL) + if retrieved.Avatar != "" { + t.Errorf("Expected empty Avatar, got '%s'", retrieved.Avatar) + } + if retrieved.Path != "" { + t.Errorf("Expected empty Path, got '%s'", retrieved.Path) + } + if retrieved.Description != "" { + t.Errorf("Expected empty Description, got '%s'", retrieved.Description) + } + // Share should have default value "private" applied + if retrieved.Share != "private" { + t.Errorf("Expected Share to be 'private', got '%s'", retrieved.Share) + } + + t.Logf("Successfully verified empty strings are stored as NULL for assistant %s", id) + }) + + t.Run("NonEmptyStringsPreserved", func(t *testing.T) { + // Create assistant with non-empty values + assistant := &AssistantModel{ + Name: "Test Non-Empty Fields", + Type: "assistant", + Connector: "openai", + Avatar: "https://example.com/avatar.png", + Path: "/path/to/assistant", + Description: "This is a description", + Share: "private", + } + + id, err := store.SaveAssistant(assistant) + if err != nil { + t.Fatalf("Failed to save assistant: %v", err) + } + + // Retrieve and verify values are preserved + retrieved, err := store.GetAssistant(id) + if err != nil { + t.Fatalf("Failed to get assistant: %v", err) + } + + if retrieved.Avatar != "https://example.com/avatar.png" { + t.Errorf("Expected Avatar 'https://example.com/avatar.png', got '%s'", retrieved.Avatar) + } + if retrieved.Path != "/path/to/assistant" { + t.Errorf("Expected Path '/path/to/assistant', got '%s'", retrieved.Path) + } + if retrieved.Description != "This is a description" { + t.Errorf("Expected Description 'This is a description', got '%s'", retrieved.Description) + } + if retrieved.Share != "private" { + t.Errorf("Expected Share 'private', got '%s'", retrieved.Share) + } + + t.Logf("Successfully verified non-empty strings are preserved for assistant %s", id) + }) +} + // TestAssistantCompleteWorkflow tests a complete workflow func TestAssistantCompleteWorkflow(t *testing.T) { test.Prepare(t, config.Conf) @@ -999,6 +1121,7 @@ func TestAssistantCompleteWorkflow(t *testing.T) { Description: fmt.Sprintf("Workflow test assistant %d", i), Tags: []string{"workflow", fmt.Sprintf("test-%d", i)}, Sort: i * 100, + Share: "private", } id, err := store.SaveAssistant(assistant)