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 := make(map[string]interface{})
data["assistant_id"] = assistant.ID data["assistant_id"] = assistant.ID
data["type"] = assistant.Type data["type"] = assistant.Type
data["name"] = assistant.Name
data["avatar"] = assistant.Avatar
data["connector"] = assistant.Connector data["connector"] = assistant.Connector
data["path"] = assistant.Path
data["built_in"] = assistant.BuiltIn data["built_in"] = assistant.BuiltIn
data["sort"] = assistant.Sort data["sort"] = assistant.Sort
data["description"] = assistant.Description
data["readonly"] = assistant.Readonly data["readonly"] = assistant.Readonly
data["public"] = assistant.Public data["public"] = assistant.Public
data["share"] = assistant.Share
data["mentionable"] = assistant.Mentionable data["mentionable"] = assistant.Mentionable
data["automated"] = assistant.Automated data["automated"] = assistant.Automated
data["created_at"] = assistant.CreatedAt data["created_at"] = assistant.CreatedAt
data["updated_at"] = assistant.UpdatedAt 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 != "" { if assistant.YaoCreatedBy != "" {
data["__yao_created_by"] = assistant.YaoCreatedBy data["__yao_created_by"] = assistant.YaoCreatedBy
} else {
data["__yao_created_by"] = nil
} }
if assistant.YaoUpdatedBy != "" { if assistant.YaoUpdatedBy != "" {
data["__yao_updated_by"] = assistant.YaoUpdatedBy data["__yao_updated_by"] = assistant.YaoUpdatedBy
} else {
data["__yao_updated_by"] = nil
} }
if assistant.YaoTeamID != "" { if assistant.YaoTeamID != "" {
data["__yao_team_id"] = assistant.YaoTeamID data["__yao_team_id"] = assistant.YaoTeamID
} else {
data["__yao_team_id"] = nil
} }
if assistant.YaoTenantID != "" { if assistant.YaoTenantID != "" {
data["__yao_tenant_id"] = assistant.YaoTenantID data["__yao_tenant_id"] = assistant.YaoTenantID
} else {
data["__yao_tenant_id"] = nil
} }
// Handle simple types // Handle simple types

View file

@ -43,6 +43,8 @@ func TestSaveAssistant(t *testing.T) {
Sort: 100, Sort: 100,
BuiltIn: false, BuiltIn: false,
Readonly: false, Readonly: false,
Public: false,
Share: "private",
Mentionable: true, Mentionable: true,
Automated: true, Automated: true,
} }
@ -71,6 +73,7 @@ func TestSaveAssistant(t *testing.T) {
Connector: "openai", Connector: "openai",
Description: "Original description", Description: "Original description",
Tags: []string{"original"}, Tags: []string{"original"},
Share: "private",
} }
id, err := store.SaveAssistant(assistant) id, err := store.SaveAssistant(assistant)
@ -150,6 +153,7 @@ func TestSaveAssistant(t *testing.T) {
Name: "Complex Assistant", Name: "Complex Assistant",
Type: "assistant", Type: "assistant",
Connector: "openai", Connector: "openai",
Share: "private",
Prompts: []Prompt{ Prompts: []Prompt{
{Role: "system", Content: "You are a helpful assistant"}, {Role: "system", Content: "You are a helpful assistant"},
{Role: "user", Content: "Hello"}, {Role: "user", Content: "Hello"},
@ -210,6 +214,7 @@ func TestDeleteAssistant(t *testing.T) {
Name: "Delete Test", Name: "Delete Test",
Type: "assistant", Type: "assistant",
Connector: "openai", Connector: "openai",
Share: "private",
} }
id, err := store.SaveAssistant(assistant) id, err := store.SaveAssistant(assistant)
@ -262,6 +267,7 @@ func TestGetAssistant(t *testing.T) {
Tags: []string{"tag1", "tag2"}, Tags: []string{"tag1", "tag2"},
Sort: 150, Sort: 150,
BuiltIn: false, BuiltIn: false,
Share: "private",
Mentionable: true, Mentionable: true,
} }
@ -335,6 +341,7 @@ func TestGetAssistants(t *testing.T) {
Description: "First test assistant", Description: "First test assistant",
Tags: []string{"test", "automation"}, Tags: []string{"test", "automation"},
Sort: 100, Sort: 100,
Share: "private",
Mentionable: true, Mentionable: true,
Automated: true, Automated: true,
}, },
@ -345,6 +352,7 @@ func TestGetAssistants(t *testing.T) {
Description: "Second test assistant", Description: "Second test assistant",
Tags: []string{"test", "manual"}, Tags: []string{"test", "manual"},
Sort: 200, Sort: 200,
Share: "private",
Mentionable: false, Mentionable: false,
Automated: false, Automated: false,
}, },
@ -355,6 +363,7 @@ func TestGetAssistants(t *testing.T) {
Description: "Third test bot", Description: "Third test bot",
Tags: []string{"bot", "automation"}, Tags: []string{"bot", "automation"},
Sort: 50, Sort: 50,
Share: "private",
Mentionable: true, Mentionable: true,
Automated: true, Automated: true,
}, },
@ -638,6 +647,7 @@ func TestDeleteAssistants(t *testing.T) {
Type: "assistant", Type: "assistant",
Connector: "openai", Connector: "openai",
Tags: []string{tag}, Tags: []string{tag},
Share: "private",
} }
_, err := store.SaveAssistant(assistant) _, err := store.SaveAssistant(assistant)
if err != nil { if err != nil {
@ -666,6 +676,7 @@ func TestDeleteAssistants(t *testing.T) {
Name: fmt.Sprintf("Connector Test %d", i), Name: fmt.Sprintf("Connector Test %d", i),
Type: "assistant", Type: "assistant",
Connector: connector, Connector: connector,
Share: "private",
} }
_, err := store.SaveAssistant(assistant) _, err := store.SaveAssistant(assistant)
if err != nil { if err != nil {
@ -694,6 +705,7 @@ func TestDeleteAssistants(t *testing.T) {
Type: "assistant", Type: "assistant",
Connector: "openai", Connector: "openai",
Description: "Test description", Description: "Test description",
Share: "private",
} }
_, err := store.SaveAssistant(assistant) _, err := store.SaveAssistant(assistant)
if err != nil { if err != nil {
@ -719,6 +731,7 @@ func TestDeleteAssistants(t *testing.T) {
Name: "Single Delete Test", Name: "Single Delete Test",
Type: "assistant", Type: "assistant",
Connector: "openai", Connector: "openai",
Share: "private",
} }
id, err := store.SaveAssistant(assistant) id, err := store.SaveAssistant(assistant)
if err != nil { if err != nil {
@ -761,18 +774,21 @@ func TestGetAssistantTags(t *testing.T) {
Type: "assistant", Type: "assistant",
Connector: "openai", Connector: "openai",
Tags: []string{uniqueTag, "common"}, Tags: []string{uniqueTag, "common"},
Share: "private",
}, },
{ {
Name: "Tags Test 2", Name: "Tags Test 2",
Type: "assistant", Type: "assistant",
Connector: "openai", Connector: "openai",
Tags: []string{uniqueTag, "different"}, Tags: []string{uniqueTag, "different"},
Share: "private",
}, },
{ {
Name: "Tags Test 3", Name: "Tags Test 3",
Type: "assistant", Type: "assistant",
Connector: "openai", Connector: "openai",
Tags: []string{"common", "another"}, Tags: []string{"common", "another"},
Share: "private",
}, },
} }
@ -866,6 +882,7 @@ func TestAssistantPermissionFields(t *testing.T) {
Type: "assistant", Type: "assistant",
Connector: "openai", Connector: "openai",
Description: "Testing permission fields", Description: "Testing permission fields",
Share: "private",
YaoCreatedBy: "user-123", YaoCreatedBy: "user-123",
YaoUpdatedBy: "user-123", YaoUpdatedBy: "user-123",
YaoTeamID: "team-456", YaoTeamID: "team-456",
@ -905,6 +922,7 @@ func TestAssistantPermissionFields(t *testing.T) {
Name: "Update Permission Test", Name: "Update Permission Test",
Type: "assistant", Type: "assistant",
Connector: "openai", Connector: "openai",
Share: "private",
YaoCreatedBy: "user-original", YaoCreatedBy: "user-original",
YaoTeamID: "team-original", YaoTeamID: "team-original",
} }
@ -947,6 +965,7 @@ func TestAssistantPermissionFields(t *testing.T) {
Name: "No Permission Fields", Name: "No Permission Fields",
Type: "assistant", Type: "assistant",
Connector: "openai", Connector: "openai",
Share: "private",
} }
id, err := store.SaveAssistant(assistant) 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 // TestAssistantCompleteWorkflow tests a complete workflow
func TestAssistantCompleteWorkflow(t *testing.T) { func TestAssistantCompleteWorkflow(t *testing.T) {
test.Prepare(t, config.Conf) test.Prepare(t, config.Conf)
@ -999,6 +1121,7 @@ func TestAssistantCompleteWorkflow(t *testing.T) {
Description: fmt.Sprintf("Workflow test assistant %d", i), Description: fmt.Sprintf("Workflow test assistant %d", i),
Tags: []string{"workflow", fmt.Sprintf("test-%d", i)}, Tags: []string{"workflow", fmt.Sprintf("test-%d", i)},
Sort: i * 100, Sort: i * 100,
Share: "private",
} }
id, err := store.SaveAssistant(assistant) id, err := store.SaveAssistant(assistant)