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.
This commit is contained in:
Max 2025-11-07 14:43:01 +08:00
parent 1d5f27cd98
commit 52f74e5edd
2 changed files with 163 additions and 6 deletions

View file

@ -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

View file

@ -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)