package xun_test import ( "fmt" "os" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/yaoapp/xun/dbal/query" "github.com/yaoapp/yao/agent/context" "github.com/yaoapp/yao/agent/i18n" searchTypes "github.com/yaoapp/yao/agent/search/types" "github.com/yaoapp/yao/agent/store/types" "github.com/yaoapp/yao/agent/store/xun" "github.com/yaoapp/yao/config" "github.com/yaoapp/yao/test" ) func TestMain(m *testing.M) { // Setup will be done in each test via test.Prepare test.Prepare(nil, config.Conf) defer test.Clean() // Run tests and exit with appropriate exit code code := m.Run() os.Exit(code) } // TestSaveAssistant tests creating and updating assistants func TestSaveAssistant(t *testing.T) { test.Prepare(t, config.Conf) defer test.Clean() // Create a new xun store store, err := xun.NewXun(types.Setting{ Connector: "default", }) if err != nil { t.Fatalf("Failed to create store: %v", err) } t.Run("CreateNewAssistant", func(t *testing.T) { assistant := &types.AssistantModel{ Name: "Test Assistant", Type: "assistant", Connector: "openai", Description: "A test assistant for unit testing", Avatar: "https://example.com/avatar.png", Tags: []string{"test", "automation"}, Options: map[string]interface{}{"temperature": 0.7}, Sort: 100, BuiltIn: false, Readonly: false, Public: false, Share: "private", Mentionable: true, Automated: true, } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to save assistant: %v", err) } if id == "" { t.Error("Expected non-empty assistant ID") } if assistant.ID == "" { t.Error("Expected assistant.ID to be set") } t.Logf("Created assistant with ID: %s", id) }) t.Run("UpdateExistingAssistant", func(t *testing.T) { // Create initial assistant assistant := &types.AssistantModel{ Name: "Update Test Assistant", Type: "assistant", Connector: "openai", Description: "Original description", Tags: []string{"original"}, Share: "private", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } // Update the assistant assistant.Description = "Updated description" assistant.Tags = []string{"updated", "modified"} assistant.Sort = 200 updatedID, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to update assistant: %v", err) } if updatedID != id { t.Errorf("Expected ID %s, got %s", id, updatedID) } // Verify update - request all fields to see the update retrieved, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve updated assistant: %v", err) } if retrieved.Description != "Updated description" { t.Errorf("Expected description 'Updated description', got '%s'", retrieved.Description) } if len(retrieved.Tags) != 2 || retrieved.Tags[0] != "updated" { t.Errorf("Expected tags [updated, modified], got %v", retrieved.Tags) } }) t.Run("ValidationErrors", func(t *testing.T) { // Test nil assistant _, err := store.SaveAssistant(nil) if err == nil { t.Error("Expected error for nil assistant") } // Test missing name assistant := &types.AssistantModel{ Type: "assistant", Connector: "openai", } _, err = store.SaveAssistant(assistant) if err == nil { t.Error("Expected error for missing name") } // Test missing type assistant = &types.AssistantModel{ Name: "Test", Connector: "openai", } _, err = store.SaveAssistant(assistant) if err == nil { t.Error("Expected error for missing type") } // Test missing connector assistant = &types.AssistantModel{ Name: "Test", Type: "assistant", } _, err = store.SaveAssistant(assistant) if err == nil { t.Error("Expected error for missing connector") } }) t.Run("ComplexDataTypes", func(t *testing.T) { assistant := &types.AssistantModel{ Name: "Complex Assistant", Type: "assistant", Connector: "openai", Share: "private", Prompts: []types.Prompt{ {Role: "system", Content: "You are a helpful assistant"}, {Role: "user", Content: "Hello"}, }, Options: map[string]interface{}{ "temperature": 0.8, "max_tokens": 2000, }, Tags: []string{"complex", "testing", "data"}, Placeholder: &types.Placeholder{ Title: "Type your message", Description: "Enter your message here...", Prompts: []string{"What can I help you with?"}, }, } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to save complex assistant: %v", err) } // Retrieve and verify - request all fields for complex data retrieved, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve complex assistant: %v", err) } if len(retrieved.Prompts) != 2 { t.Errorf("Expected 2 prompts, got %d", len(retrieved.Prompts)) } if retrieved.Placeholder == nil { t.Error("Expected placeholder to be set") } if len(retrieved.Tags) != 3 { t.Errorf("Expected 3 tags, got %d", len(retrieved.Tags)) } }) t.Run("SaveWithMCPServers", func(t *testing.T) { // Test creating assistant with MCP servers directly // This will test that: // - server1 (no tools/resources) serializes as "server1" // - server2 (with tools) serializes as {"server_id":"server2","tools":[...]} // - server3 (with both) serializes as {"server_id":"server3","resources":[...],"tools":[...]} assistant := &types.AssistantModel{ Name: "MCP Save Test", Type: "assistant", Connector: "openai", Share: "private", MCP: &types.MCPServers{ Servers: []types.MCPServerConfig{ {ServerID: "server1"}, { ServerID: "server2", Tools: []string{"tool1", "tool2"}, }, { ServerID: "server3", Resources: []string{"res1", "res2"}, Tools: []string{"tool3", "tool4"}, }, }, Options: map[string]interface{}{ "timeout": 30, }, }, } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to save assistant with MCP: %v", err) } // Retrieve and verify MCP configuration - mcp is in default fields retrieved, err := store.GetAssistant(id, []string{}) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.MCP == nil { t.Fatal("Expected MCP to be set") } if len(retrieved.MCP.Servers) != 3 { t.Errorf("Expected 3 MCP servers, got %d", len(retrieved.MCP.Servers)) } // Verify server1 (simple format) if retrieved.MCP.Servers[0].ServerID != "server1" { t.Errorf("Expected server1, got '%s'", retrieved.MCP.Servers[0].ServerID) } // Verify server2 (with tools) if retrieved.MCP.Servers[1].ServerID != "server2" { t.Errorf("Expected server2, got '%s'", retrieved.MCP.Servers[1].ServerID) } if len(retrieved.MCP.Servers[1].Tools) != 2 { t.Errorf("Expected 2 tools for server2, got %d", len(retrieved.MCP.Servers[1].Tools)) } // Verify server3 (with resources and tools) if retrieved.MCP.Servers[2].ServerID != "server3" { t.Errorf("Expected server3, got '%s'", retrieved.MCP.Servers[2].ServerID) } if len(retrieved.MCP.Servers[2].Resources) != 2 { t.Errorf("Expected 2 resources for server3, got %d", len(retrieved.MCP.Servers[2].Resources)) } if len(retrieved.MCP.Servers[2].Tools) != 2 { t.Errorf("Expected 2 tools for server3, got %d", len(retrieved.MCP.Servers[2].Tools)) } // Verify options if retrieved.MCP.Options == nil { t.Error("Expected MCP options to be set") } if timeout, ok := retrieved.MCP.Options["timeout"].(float64); !ok || timeout != 30 { t.Errorf("Expected timeout 30, got %v", retrieved.MCP.Options["timeout"]) } t.Logf("Successfully verified MCP configuration for assistant %s", id) }) t.Run("UpdateWithMCPServers", func(t *testing.T) { // Create assistant without MCP assistant := &types.AssistantModel{ Name: "MCP Update Test", Type: "assistant", Connector: "openai", Share: "private", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } // Update assistant with MCP assistant.MCP = &types.MCPServers{ Servers: []types.MCPServerConfig{ {ServerID: "new-server1"}, { ServerID: "new-server2", Tools: []string{"newtool1"}, }, }, } _, err = store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to update assistant with MCP: %v", err) } // Retrieve and verify - mcp is in default fields retrieved, err := store.GetAssistant(id, []string{}) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.MCP == nil || len(retrieved.MCP.Servers) != 2 { t.Errorf("Expected 2 MCP servers, got %v", retrieved.MCP) } if retrieved.MCP.Servers[0].ServerID != "new-server1" { t.Errorf("Expected new-server1, got '%s'", retrieved.MCP.Servers[0].ServerID) } t.Logf("Successfully updated and verified MCP for assistant %s", id) }) t.Run("UsesConfiguration", func(t *testing.T) { // Test assistant with Uses configuration assistant := &types.AssistantModel{ Name: "Uses Test Assistant", Type: "assistant", Connector: "openai", Share: "private", Uses: &context.Uses{ Vision: "mcp:vision-server", Audio: "agent", Search: "mcp:search-server", Fetch: "agent", }, } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to save assistant with uses: %v", err) } // Retrieve and verify uses configuration - uses is NOT in default fields, need to request all retrieved, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.Uses == nil { t.Fatal("Expected uses to be set") } if retrieved.Uses.Vision != "mcp:vision-server" { t.Errorf("Expected vision 'mcp:vision-server', got '%s'", retrieved.Uses.Vision) } if retrieved.Uses.Audio != "agent" { t.Errorf("Expected audio 'agent', got '%s'", retrieved.Uses.Audio) } if retrieved.Uses.Search != "mcp:search-server" { t.Errorf("Expected search 'mcp:search-server', got '%s'", retrieved.Uses.Search) } if retrieved.Uses.Fetch != "agent" { t.Errorf("Expected fetch 'agent', got '%s'", retrieved.Uses.Fetch) } t.Logf("Successfully saved and retrieved assistant with uses configuration") }) t.Run("NilUses", func(t *testing.T) { // Test assistant without Uses configuration assistant := &types.AssistantModel{ Name: "No Uses Assistant", Type: "assistant", Connector: "openai", Share: "private", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to save assistant without uses: %v", err) } // Retrieve and verify uses is nil - request all fields to check uses retrieved, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.Uses != nil { t.Errorf("Expected uses to be nil, got %+v", retrieved.Uses) } }) t.Run("PartialUsesConfiguration", func(t *testing.T) { // Test assistant with partial Uses configuration assistant := &types.AssistantModel{ Name: "Partial Uses Assistant", Type: "assistant", Connector: "openai", Share: "private", Uses: &context.Uses{ Vision: "mcp:vision-only", // Audio, Search, Fetch not set }, } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to save assistant with partial uses: %v", err) } // Retrieve and verify - request all fields for uses retrieved, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.Uses == nil { t.Fatal("Expected uses to be set") } if retrieved.Uses.Vision != "mcp:vision-only" { t.Errorf("Expected vision 'mcp:vision-only', got '%s'", retrieved.Uses.Vision) } if retrieved.Uses.Audio != "" { t.Errorf("Expected audio to be empty, got '%s'", retrieved.Uses.Audio) } if retrieved.Uses.Search != "" { t.Errorf("Expected search to be empty, got '%s'", retrieved.Uses.Search) } if retrieved.Uses.Fetch != "" { t.Errorf("Expected fetch to be empty, got '%s'", retrieved.Uses.Fetch) } }) t.Run("SearchConfiguration", func(t *testing.T) { // Test assistant with Search configuration assistant := &types.AssistantModel{ Name: "Search Config Test Assistant", Type: "assistant", Connector: "openai", Share: "private", Search: &searchTypes.Config{ Web: &searchTypes.WebConfig{ Provider: "tavily", MaxResults: 15, }, KB: &searchTypes.KBConfig{ Collections: []string{"docs", "faq"}, Threshold: 0.8, Graph: true, }, DB: &searchTypes.DBConfig{ Models: []string{"user", "product"}, MaxResults: 50, }, Citation: &searchTypes.CitationConfig{ Format: "#ref:{id}", AutoInjectPrompt: true, }, Weights: &searchTypes.WeightsConfig{ User: 1.0, Hook: 0.9, Auto: 0.7, }, }, } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to save assistant with search config: %v", err) } // Retrieve and verify search configuration - search is NOT in default fields retrieved, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.Search == nil { t.Fatal("Expected search to be set") } // Verify Web config if retrieved.Search.Web == nil { t.Fatal("Expected search.web to be set") } if retrieved.Search.Web.Provider != "tavily" { t.Errorf("Expected web provider 'tavily', got '%s'", retrieved.Search.Web.Provider) } if retrieved.Search.Web.MaxResults != 15 { t.Errorf("Expected web max_results 15, got %d", retrieved.Search.Web.MaxResults) } // Verify KB config if retrieved.Search.KB == nil { t.Fatal("Expected search.kb to be set") } if len(retrieved.Search.KB.Collections) != 2 { t.Errorf("Expected 2 KB collections, got %d", len(retrieved.Search.KB.Collections)) } if retrieved.Search.KB.Collections[0] != "docs" { t.Errorf("Expected first collection 'docs', got '%s'", retrieved.Search.KB.Collections[0]) } if retrieved.Search.KB.Threshold != 0.8 { t.Errorf("Expected KB threshold 0.8, got %f", retrieved.Search.KB.Threshold) } if !retrieved.Search.KB.Graph { t.Error("Expected KB graph to be true") } // Verify DB config if retrieved.Search.DB == nil { t.Fatal("Expected search.db to be set") } if len(retrieved.Search.DB.Models) != 2 { t.Errorf("Expected 2 DB models, got %d", len(retrieved.Search.DB.Models)) } if retrieved.Search.DB.MaxResults != 50 { t.Errorf("Expected DB max_results 50, got %d", retrieved.Search.DB.MaxResults) } // Verify Citation config if retrieved.Search.Citation == nil { t.Fatal("Expected search.citation to be set") } if retrieved.Search.Citation.Format != "#ref:{id}" { t.Errorf("Expected citation format '#ref:{id}', got '%s'", retrieved.Search.Citation.Format) } if !retrieved.Search.Citation.AutoInjectPrompt { t.Error("Expected citation auto_inject_prompt to be true") } // Verify Weights config if retrieved.Search.Weights == nil { t.Fatal("Expected search.weights to be set") } if retrieved.Search.Weights.User != 1.0 { t.Errorf("Expected weights.user 1.0, got %f", retrieved.Search.Weights.User) } if retrieved.Search.Weights.Hook != 0.9 { t.Errorf("Expected weights.hook 0.9, got %f", retrieved.Search.Weights.Hook) } if retrieved.Search.Weights.Auto != 0.7 { t.Errorf("Expected weights.auto 0.7, got %f", retrieved.Search.Weights.Auto) } t.Logf("Successfully saved and retrieved assistant with search configuration") }) t.Run("NilSearchConfiguration", func(t *testing.T) { // Test assistant without Search configuration assistant := &types.AssistantModel{ Name: "No Search Config Assistant", Type: "assistant", Connector: "openai", Share: "private", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to save assistant without search: %v", err) } // Retrieve and verify search is nil - request all fields to check search retrieved, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.Search != nil { t.Errorf("Expected search to be nil, got %+v", retrieved.Search) } }) t.Run("PartialSearchConfiguration", func(t *testing.T) { // Test assistant with partial Search configuration assistant := &types.AssistantModel{ Name: "Partial Search Config Assistant", Type: "assistant", Connector: "openai", Share: "private", Search: &searchTypes.Config{ Web: &searchTypes.WebConfig{ Provider: "serper", }, // KB, DB, Citation, Weights not set }, } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to save assistant with partial search: %v", err) } // Retrieve and verify - request all fields for search retrieved, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.Search == nil { t.Fatal("Expected search to be set") } if retrieved.Search.Web == nil { t.Fatal("Expected search.web to be set") } if retrieved.Search.Web.Provider != "serper" { t.Errorf("Expected web provider 'serper', got '%s'", retrieved.Search.Web.Provider) } // Other fields should be nil if retrieved.Search.KB != nil { t.Errorf("Expected search.kb to be nil, got %+v", retrieved.Search.KB) } if retrieved.Search.DB != nil { t.Errorf("Expected search.db to be nil, got %+v", retrieved.Search.DB) } if retrieved.Search.Citation != nil { t.Errorf("Expected search.citation to be nil, got %+v", retrieved.Search.Citation) } if retrieved.Search.Weights != nil { t.Errorf("Expected search.weights to be nil, got %+v", retrieved.Search.Weights) } }) t.Run("ConnectorOptions", func(t *testing.T) { // Test assistant with connector options optionalTrue := true assistant := &types.AssistantModel{ Name: "Connector Options Test", Type: "assistant", Connector: "openai", Share: "private", ConnectorOptions: &types.ConnectorOptions{ Optional: &optionalTrue, Connectors: []string{"openai", "anthropic"}, Filters: []types.ModelCapability{types.CapVision, types.CapToolCalls}, }, } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to save assistant with connector options: %v", err) } // Retrieve and verify - connector_options is NOT in default fields retrieved, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.ConnectorOptions == nil { t.Fatal("Expected connector options to be set") } if retrieved.ConnectorOptions.Optional == nil || !*retrieved.ConnectorOptions.Optional { t.Error("Expected optional to be true") } if len(retrieved.ConnectorOptions.Connectors) != 2 { t.Errorf("Expected 2 connectors, got %d", len(retrieved.ConnectorOptions.Connectors)) } if len(retrieved.ConnectorOptions.Filters) != 2 { t.Errorf("Expected 2 filters, got %d", len(retrieved.ConnectorOptions.Filters)) } if retrieved.ConnectorOptions.Filters[0] != types.CapVision { t.Errorf("Expected first filter to be vision, got '%s'", retrieved.ConnectorOptions.Filters[0]) } t.Logf("Successfully saved and retrieved connector options for assistant %s", id) }) t.Run("PromptPresets", func(t *testing.T) { // Test assistant with prompt presets assistant := &types.AssistantModel{ Name: "Prompt Presets Test", Type: "assistant", Connector: "openai", Share: "private", PromptPresets: map[string][]types.Prompt{ "chat": { {Role: "system", Content: "You are a friendly chatbot"}, {Role: "user", Content: "Hello!"}, }, "task": { {Role: "system", Content: "You are a task executor"}, }, }, } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to save assistant with prompt presets: %v", err) } // Retrieve and verify - prompt_presets is NOT in default fields retrieved, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.PromptPresets == nil { t.Fatal("Expected prompt presets to be set") } if len(retrieved.PromptPresets) != 2 { t.Errorf("Expected 2 preset groups, got %d", len(retrieved.PromptPresets)) } chatPrompts, ok := retrieved.PromptPresets["chat"] if !ok { t.Fatal("Expected 'chat' preset to exist") } if len(chatPrompts) != 2 { t.Errorf("Expected 2 chat prompts, got %d", len(chatPrompts)) } if chatPrompts[0].Role != "system" { t.Errorf("Expected system role, got '%s'", chatPrompts[0].Role) } taskPrompts, ok := retrieved.PromptPresets["task"] if !ok { t.Fatal("Expected 'task' preset to exist") } if len(taskPrompts) != 1 { t.Errorf("Expected 1 task prompt, got %d", len(taskPrompts)) } t.Logf("Successfully saved and retrieved prompt presets for assistant %s", id) }) t.Run("SourceField", func(t *testing.T) { // Test assistant with source code sourceCode := `function onMessage(msg) { console.log("Received:", msg); return { status: "ok" }; }` assistant := &types.AssistantModel{ Name: "Source Field Test", Type: "assistant", Connector: "openai", Share: "private", Source: sourceCode, } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to save assistant with source: %v", err) } // Retrieve and verify - source is NOT in default fields retrieved, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.Source != sourceCode { t.Errorf("Expected source code to match, got '%s'", retrieved.Source) } t.Logf("Successfully saved and retrieved source code for assistant %s", id) }) t.Run("SandboxConfiguration", func(t *testing.T) { assistant := &types.AssistantModel{ Name: "Sandbox Test Assistant", Type: "assistant", Connector: "openai", Share: "private", Sandbox: &types.Sandbox{ Command: "claude", Timeout: "5m", Arguments: map[string]interface{}{ "max_turns": 10, "permission_mode": "bypassPermissions", }, }, } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to save assistant with sandbox: %v", err) } retrieved, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.Sandbox == nil { t.Fatal("Expected sandbox to be set") } if retrieved.Sandbox.Command != "claude" { t.Errorf("Expected command 'claude', got '%s'", retrieved.Sandbox.Command) } if retrieved.Sandbox.Timeout != "5m" { t.Errorf("Expected timeout '5m', got '%s'", retrieved.Sandbox.Timeout) } if retrieved.Sandbox.Arguments == nil { t.Fatal("Expected sandbox arguments to be set") } if maxTurns, ok := retrieved.Sandbox.Arguments["max_turns"].(float64); !ok || maxTurns != 10 { t.Errorf("Expected max_turns 10, got %v", retrieved.Sandbox.Arguments["max_turns"]) } t.Logf("Successfully saved and retrieved sandbox configuration for assistant %s", id) }) t.Run("SandboxWithImage", func(t *testing.T) { assistant := &types.AssistantModel{ Name: "Sandbox Image Test", Type: "assistant", Connector: "openai", Share: "private", Sandbox: &types.Sandbox{ Command: "claude", Image: "yaoapp/sandbox-claude-desktop:latest", Timeout: "20m", MaxMemory: "4g", MaxCPU: 2.0, Arguments: map[string]interface{}{ "max_turns": 500, "permission_mode": "bypassPermissions", "disallowed_tools": "WebSearch", }, Secrets: map[string]string{ "GITHUB_TOKEN": "$ENV.GITHUB_TOKEN", }, }, } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to save assistant with sandbox image: %v", err) } retrieved, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.Sandbox == nil { t.Fatal("Expected sandbox to be set") } if retrieved.Sandbox.Image != "yaoapp/sandbox-claude-desktop:latest" { t.Errorf("Expected image 'yaoapp/sandbox-claude-desktop:latest', got '%s'", retrieved.Sandbox.Image) } if retrieved.Sandbox.MaxMemory != "4g" { t.Errorf("Expected max_memory '4g', got '%s'", retrieved.Sandbox.MaxMemory) } if retrieved.Sandbox.MaxCPU != 2.0 { t.Errorf("Expected max_cpu 2.0, got %f", retrieved.Sandbox.MaxCPU) } if retrieved.Sandbox.Secrets == nil || retrieved.Sandbox.Secrets["GITHUB_TOKEN"] != "$ENV.GITHUB_TOKEN" { t.Errorf("Expected secrets to contain GITHUB_TOKEN, got %v", retrieved.Sandbox.Secrets) } t.Logf("Successfully saved and retrieved sandbox with image for assistant %s", id) }) t.Run("NilSandbox", func(t *testing.T) { assistant := &types.AssistantModel{ Name: "No Sandbox Assistant", Type: "assistant", Connector: "openai", Share: "private", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to save assistant without sandbox: %v", err) } retrieved, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.Sandbox != nil { t.Errorf("Expected sandbox to be nil, got %+v", retrieved.Sandbox) } }) t.Run("CapabilitiesField", func(t *testing.T) { assistant := &types.AssistantModel{ Name: "Capabilities Test", Type: "assistant", Connector: "openai", Share: "private", Description: "A test assistant", Capabilities: "Can search the web, analyze data, write code, and summarize documents.", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to save assistant with capabilities: %v", err) } retrieved, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.Capabilities != "Can search the web, analyze data, write code, and summarize documents." { t.Errorf("Expected capabilities to match, got '%s'", retrieved.Capabilities) } if retrieved.Description != "A test assistant" { t.Errorf("Expected description 'A test assistant', got '%s'", retrieved.Description) } t.Logf("Successfully saved and retrieved capabilities for assistant %s", id) }) t.Run("EmptyCapabilities", func(t *testing.T) { assistant := &types.AssistantModel{ Name: "No Capabilities Assistant", Type: "assistant", Connector: "openai", Share: "private", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to save assistant without capabilities: %v", err) } retrieved, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.Capabilities != "" { t.Errorf("Expected empty capabilities, got '%s'", retrieved.Capabilities) } }) t.Run("CapabilitiesWithI18n", func(t *testing.T) { assistant := &types.AssistantModel{ Name: "{{name}}", Type: "assistant", Connector: "openai", Share: "private", Description: "{{description}}", Capabilities: "{{capabilities}}", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to save assistant with i18n capabilities: %v", err) } // Setup i18n i18n.Locales[id] = map[string]i18n.I18n{ "en": { Locale: "en", Messages: map[string]any{ "name": "i18n Test", "description": "Description in English", "capabilities": "Can do X, Y, and Z", }, }, "zh-cn": { Locale: "zh-cn", Messages: map[string]any{ "name": "国际化测试", "description": "中文描述", "capabilities": "可以做X、Y和Z", }, }, } retrievedEN, err := store.GetAssistant(id, types.AssistantFullFields, "en") if err != nil { t.Fatalf("Failed to get assistant with EN locale: %v", err) } if retrievedEN.Capabilities != "Can do X, Y, and Z" { t.Errorf("Expected capabilities 'Can do X, Y, and Z', got '%s'", retrievedEN.Capabilities) } retrievedZH, err := store.GetAssistant(id, types.AssistantFullFields, "zh-cn") if err != nil { t.Fatalf("Failed to get assistant with ZH locale: %v", err) } if retrievedZH.Capabilities != "可以做X、Y和Z" { t.Errorf("Expected capabilities '可以做X、Y和Z', got '%s'", retrievedZH.Capabilities) } // Cleanup delete(i18n.Locales, id) t.Logf("Successfully tested capabilities i18n for assistant %s", id) }) t.Run("CapabilitiesInKeywordSearch", func(t *testing.T) { uniqueCapability := fmt.Sprintf("unique-cap-%d", time.Now().UnixNano()) assistant := &types.AssistantModel{ Name: "Capabilities Search Test", Type: "assistant", Connector: "openai", Share: "private", Capabilities: uniqueCapability, } _, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to save assistant: %v", err) } response, err := store.GetAssistants(types.AssistantFilter{ Keywords: uniqueCapability, Page: 1, PageSize: 20, }) if err != nil { t.Fatalf("Failed to search by capabilities keyword: %v", err) } if len(response.Data) < 1 { t.Error("Expected to find assistant by capabilities keyword search") } found := false for _, a := range response.Data { if a.Capabilities == uniqueCapability { found = true break } } if !found { t.Error("Expected to find assistant with matching capabilities") } }) t.Run("KeywordsSearchLocales", func(t *testing.T) { uniqueLocaleText := fmt.Sprintf("localekw-%d", time.Now().UnixNano()) assistant := &types.AssistantModel{ Name: "Locale Search Test", Type: "assistant", Connector: "openai", Share: "private", Locales: i18n.Map{ "zh-cn": i18n.I18n{ Locale: "zh-cn", Messages: map[string]any{"name": uniqueLocaleText}, }, }, } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to save assistant with locales: %v", err) } defer store.DeleteAssistant(id) response, err := store.GetAssistants(types.AssistantFilter{ Keywords: uniqueLocaleText, Page: 1, PageSize: 20, }) if err != nil { t.Fatalf("Keywords search with locales should not error: %v", err) } found := false for _, a := range response.Data { if a.ID == id { found = true break } } if !found { t.Error("Expected to find assistant by locale text keyword search") } // Partial match should also work partial := uniqueLocaleText[:10] response2, err := store.GetAssistants(types.AssistantFilter{ Keywords: partial, Page: 1, PageSize: 20, }) if err != nil { t.Fatalf("Partial keywords search should not error: %v", err) } found = false for _, a := range response2.Data { if a.ID == id { found = true break } } if !found { t.Error("Expected to find assistant by partial locale text keyword search") } }) t.Run("UpdateSandbox", func(t *testing.T) { assistant := &types.AssistantModel{ Name: "Update Sandbox Test", Type: "assistant", Connector: "openai", Share: "private", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } // Update with sandbox updates := map[string]interface{}{ "sandbox": &types.Sandbox{ Command: "claude", Timeout: "10m", }, } err = store.UpdateAssistant(id, updates) if err != nil { t.Fatalf("Failed to update sandbox: %v", err) } retrieved, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.Sandbox == nil { t.Fatal("Expected sandbox to be set") } if retrieved.Sandbox.Command != "claude" { t.Errorf("Expected command 'claude', got '%s'", retrieved.Sandbox.Command) } // Update to remove sandbox updates2 := map[string]interface{}{ "sandbox": nil, } err = store.UpdateAssistant(id, updates2) if err != nil { t.Fatalf("Failed to remove sandbox: %v", err) } retrieved2, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved2.Sandbox != nil { t.Errorf("Expected sandbox to be nil, got %+v", retrieved2.Sandbox) } }) t.Run("UpdateCapabilities", func(t *testing.T) { assistant := &types.AssistantModel{ Name: "Update Capabilities Test", Type: "assistant", Connector: "openai", Share: "private", Capabilities: "Original capabilities", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } // Update capabilities updates := map[string]interface{}{ "capabilities": "Updated capabilities: can search, analyze, and write code", } err = store.UpdateAssistant(id, updates) if err != nil { t.Fatalf("Failed to update capabilities: %v", err) } retrieved, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.Capabilities != "Updated capabilities: can search, analyze, and write code" { t.Errorf("Expected updated capabilities, got '%s'", retrieved.Capabilities) } // Update to clear capabilities updates2 := map[string]interface{}{ "capabilities": "", } err = store.UpdateAssistant(id, updates2) if err != nil { t.Fatalf("Failed to clear capabilities: %v", err) } retrieved2, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved2.Capabilities != "" { t.Errorf("Expected empty capabilities, got '%s'", retrieved2.Capabilities) } }) t.Run("FilterBySandbox", func(t *testing.T) { // Create one assistant with sandbox withSandbox := &types.AssistantModel{ Name: "Filter Sandbox Yes", Type: "assistant", Connector: "openai", Share: "private", Sandbox: &types.Sandbox{ Command: "claude", Timeout: "5m", }, } idWith, err := store.SaveAssistant(withSandbox) if err != nil { t.Fatalf("Failed to save assistant with sandbox: %v", err) } // Create one assistant without sandbox withoutSandbox := &types.AssistantModel{ Name: "Filter Sandbox No", Type: "assistant", Connector: "openai", Share: "private", } idWithout, err := store.SaveAssistant(withoutSandbox) if err != nil { t.Fatalf("Failed to save assistant without sandbox: %v", err) } testIDs := []string{idWith, idWithout} // Filter: sandbox=true, scoped to test IDs trueVal := true result, err := store.GetAssistants(types.AssistantFilter{ Page: 1, PageSize: 100, Sandbox: &trueVal, AssistantIDs: testIDs, }) if err != nil { t.Fatalf("Failed to filter with sandbox=true: %v", err) } if len(result.Data) != 1 { t.Errorf("Expected 1 result for sandbox=true, got %d", len(result.Data)) } else if result.Data[0].ID != idWith { t.Errorf("Expected assistant %s, got %s", idWith, result.Data[0].ID) } // Filter: sandbox=false, scoped to test IDs falseVal := false result2, err := store.GetAssistants(types.AssistantFilter{ Page: 1, PageSize: 100, Sandbox: &falseVal, AssistantIDs: testIDs, }) if err != nil { t.Fatalf("Failed to filter with sandbox=false: %v", err) } if len(result2.Data) != 1 { t.Errorf("Expected 1 result for sandbox=false, got %d", len(result2.Data)) } else if result2.Data[0].ID != idWithout { t.Errorf("Expected assistant %s, got %s", idWithout, result2.Data[0].ID) } t.Logf("Sandbox filter test passed: sandbox=true returned %d, sandbox=false returned %d", len(result.Data), len(result2.Data)) }) t.Run("AllNewFieldsTogether", func(t *testing.T) { // Test assistant with all new fields together optionalFalse := false assistant := &types.AssistantModel{ Name: "All New Fields Test", Type: "assistant", Connector: "openai", Share: "private", ConnectorOptions: &types.ConnectorOptions{ Optional: &optionalFalse, Connectors: []string{"openai"}, Filters: []types.ModelCapability{types.CapVision}, }, PromptPresets: map[string][]types.Prompt{ "default": { {Role: "system", Content: "Default system prompt"}, }, }, Source: "// Hook code here", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to save assistant with all new fields: %v", err) } // Retrieve and verify all new fields retrieved, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.ConnectorOptions == nil { t.Error("Expected connector options to be set") } if retrieved.PromptPresets == nil { t.Error("Expected prompt presets to be set") } if retrieved.Source == "" { t.Error("Expected source to be set") } t.Logf("Successfully saved and retrieved all new fields for assistant %s", id) }) } // TestDeleteAssistant tests deleting a single assistant func TestDeleteAssistant(t *testing.T) { test.Prepare(t, config.Conf) defer test.Clean() store, err := xun.NewXun(types.Setting{ Connector: "default", }) if err != nil { t.Fatalf("Failed to create store: %v", err) } t.Run("DeleteExistingAssistant", func(t *testing.T) { // Create assistant assistant := &types.AssistantModel{ Name: "Delete Test", Type: "assistant", Connector: "openai", Share: "private", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } // Delete it err = store.DeleteAssistant(id) if err != nil { t.Fatalf("Failed to delete assistant: %v", err) } // Verify deletion _, err = store.GetAssistant(id, nil) if err == nil { t.Error("Expected error when getting deleted assistant") } }) t.Run("DeleteNonExistentAssistant", func(t *testing.T) { err := store.DeleteAssistant("nonexistent-id") if err == nil { t.Error("Expected error when deleting non-existent assistant") } }) } // TestGetAssistant tests retrieving a single assistant func TestGetAssistant(t *testing.T) { test.Prepare(t, config.Conf) defer test.Clean() store, err := xun.NewXun(types.Setting{ Connector: "default", }) if err != nil { t.Fatalf("Failed to create store: %v", err) } t.Run("GetExistingAssistant", func(t *testing.T) { // Create assistant assistant := &types.AssistantModel{ Name: "Get Test", Type: "assistant", Connector: "openai", Description: "Test description", Avatar: "https://example.com/avatar.png", Tags: []string{"tag1", "tag2"}, Sort: 150, BuiltIn: false, Share: "private", Mentionable: true, } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } // Retrieve it with default fields (tags are now in default fields) retrieved, err := store.GetAssistant(id, nil) if err != nil { t.Fatalf("Failed to get assistant: %v", err) } if retrieved.ID != id { t.Errorf("Expected ID %s, got %s", id, retrieved.ID) } if retrieved.Name != "Get Test" { t.Errorf("Expected name 'Get Test', got '%s'", retrieved.Name) } if retrieved.Description != "Test description" { t.Errorf("Expected description 'Test description', got '%s'", retrieved.Description) } if len(retrieved.Tags) != 2 { t.Errorf("Expected 2 tags, got %d", len(retrieved.Tags)) } if retrieved.Sort != 150 { t.Errorf("Expected sort 150, got %d", retrieved.Sort) } }) t.Run("GetNonExistentAssistant", func(t *testing.T) { _, err := store.GetAssistant("nonexistent-id", nil) if err == nil { t.Error("Expected error when getting non-existent assistant") } }) } // TestGetAssistants tests retrieving multiple assistants with filtering and pagination func TestGetAssistants(t *testing.T) { test.Prepare(t, config.Conf) defer test.Clean() store, err := xun.NewXun(types.Setting{ Connector: "default", }) if err != nil { t.Fatalf("Failed to create store: %v", err) } // Clean up existing data before creating test assistants deleted, err := store.DeleteAssistants(types.AssistantFilter{}) if err != nil { t.Logf("Warning: Failed to clean up existing assistants: %v", err) } else if deleted > 0 { t.Logf("Cleaned up %d existing assistants", deleted) } // Create test assistants assistants := []types.AssistantModel{ { Name: "Assistant 1", Type: "assistant", Connector: "openai", Description: "First test assistant", Tags: []string{"test", "automation"}, Sort: 100, Share: "private", Mentionable: true, Automated: true, }, { Name: "Assistant 2", Type: "assistant", Connector: "anthropic", Description: "Second test assistant", Tags: []string{"test", "manual"}, Sort: 200, Share: "private", Mentionable: false, Automated: false, }, { Name: "Assistant 3", Type: "bot", Connector: "openai", Description: "Third test bot", Tags: []string{"bot", "automation"}, Sort: 50, Share: "private", Mentionable: true, Automated: true, }, } createdIDs := []string{} for _, asst := range assistants { id, err := store.SaveAssistant(&asst) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } createdIDs = append(createdIDs, id) } t.Run("GetAllAssistants", func(t *testing.T) { response, err := store.GetAssistants(types.AssistantFilter{ Page: 1, PageSize: 20, }) if err != nil { t.Fatalf("Failed to get assistants: %v", err) } if len(response.Data) < 3 { t.Errorf("Expected at least 3 assistants, got %d", len(response.Data)) } if response.Total < 3 { t.Errorf("Expected total >= 3, got %d", response.Total) } }) t.Run("FilterByType", func(t *testing.T) { response, err := store.GetAssistants(types.AssistantFilter{ Type: "assistant", Page: 1, PageSize: 20, }) if err != nil { t.Fatalf("Failed to get assistants by type: %v", err) } for _, assistant := range response.Data { if assistant.Type != "assistant" { t.Errorf("Expected type 'assistant', got '%s'", assistant.Type) } } }) t.Run("FilterByConnector", func(t *testing.T) { response, err := store.GetAssistants(types.AssistantFilter{ Connector: "openai", Page: 1, PageSize: 20, }) if err != nil { t.Fatalf("Failed to get assistants by connector: %v", err) } for _, assistant := range response.Data { if assistant.Connector != "openai" { t.Errorf("Expected connector 'openai', got '%s'", assistant.Connector) } } }) t.Run("FilterByTags", func(t *testing.T) { response, err := store.GetAssistants(types.AssistantFilter{ Tags: []string{"automation"}, Page: 1, PageSize: 20, }) if err != nil { t.Fatalf("Failed to get assistants by tags: %v", err) } // Should find assistants with "automation" tag found := false for _, assistant := range response.Data { for _, tag := range assistant.Tags { if tag == "automation" { found = true break } } if found { break } } if !found && len(response.Data) > 0 { t.Error("Expected to find assistants with 'automation' tag") } }) t.Run("FilterByKeywords", func(t *testing.T) { response, err := store.GetAssistants(types.AssistantFilter{ Keywords: "Second", Page: 1, PageSize: 20, }) if err != nil { t.Fatalf("Failed to get assistants by keywords: %v", err) } // Should find "Assistant 2" found := false for _, assistant := range response.Data { if assistant.Name == "Assistant 2" { found = true break } } if !found { t.Error("Expected to find assistant with keyword 'Second'") } }) t.Run("FilterByMentionable", func(t *testing.T) { mentionableTrue := true response, err := store.GetAssistants(types.AssistantFilter{ Mentionable: &mentionableTrue, Page: 1, PageSize: 20, }) if err != nil { t.Fatalf("Failed to get mentionable assistants: %v", err) } if len(response.Data) != 2 { t.Errorf("Expected 2 mentionable assistants, got %d", len(response.Data)) } for _, assistant := range response.Data { if !assistant.Mentionable { t.Errorf("Expected assistant %s (%s) to be mentionable, but it's not", assistant.ID, assistant.Name) } } }) t.Run("FilterByAutomated", func(t *testing.T) { automatedFalse := false response, err := store.GetAssistants(types.AssistantFilter{ Automated: &automatedFalse, Page: 1, PageSize: 20, }) if err != nil { t.Fatalf("Failed to get non-automated assistants: %v", err) } for _, assistant := range response.Data { if assistant.Automated { t.Error("Expected all assistants to be non-automated") } } }) t.Run("Pagination", func(t *testing.T) { // Test first page response1, err := store.GetAssistants(types.AssistantFilter{ Page: 1, PageSize: 2, }) if err != nil { t.Fatalf("Failed to get first page: %v", err) } if len(response1.Data) > 2 { t.Errorf("Expected max 2 results, got %d", len(response1.Data)) } if response1.Page != 1 { t.Errorf("Expected page 1, got %d", response1.Page) } if response1.PageSize != 2 { t.Errorf("Expected page size 2, got %d", response1.PageSize) } // Test second page if there are enough records if response1.Total > 2 { response2, err := store.GetAssistants(types.AssistantFilter{ Page: 2, PageSize: 2, }) if err != nil { t.Fatalf("Failed to get second page: %v", err) } if response2.Page != 2 { t.Errorf("Expected page 2, got %d", response2.Page) } } }) t.Run("FieldSelection", func(t *testing.T) { response, err := store.GetAssistants(types.AssistantFilter{ Select: []string{"assistant_id", "name", "type"}, Page: 1, PageSize: 20, }) if err != nil { t.Fatalf("Failed to get assistants with field selection: %v", err) } if len(response.Data) > 0 { assistant := response.Data[0] if assistant.ID == "" { t.Error("Expected assistant_id field") } if assistant.Name == "" { t.Error("Expected name field") } if assistant.Type == "" { t.Error("Expected type field") } } }) t.Run("FilterByAssistantID", func(t *testing.T) { if len(createdIDs) > 0 { response, err := store.GetAssistants(types.AssistantFilter{ AssistantID: createdIDs[0], Page: 1, PageSize: 20, }) if err != nil { t.Fatalf("Failed to get assistant by ID: %v", err) } if len(response.Data) != 1 { t.Errorf("Expected 1 result, got %d", len(response.Data)) } if response.Data[0].ID != createdIDs[0] { t.Errorf("Expected assistant_id %s, got %s", createdIDs[0], response.Data[0].ID) } } }) t.Run("FilterByAssistantIDs", func(t *testing.T) { if len(createdIDs) >= 2 { filterIDs := []string{createdIDs[0], createdIDs[1]} response, err := store.GetAssistants(types.AssistantFilter{ AssistantIDs: filterIDs, Page: 1, PageSize: 20, }) if err != nil { t.Fatalf("Failed to get assistants by IDs: %v", err) } if len(response.Data) < 2 { t.Errorf("Expected at least 2 results, got %d", len(response.Data)) } } }) } // TestDeleteAssistants tests bulk deletion with filters func TestDeleteAssistants(t *testing.T) { test.Prepare(t, config.Conf) defer test.Clean() store, err := xun.NewXun(types.Setting{ Connector: "default", }) if err != nil { t.Fatalf("Failed to create store: %v", err) } t.Run("DeleteByTag", func(t *testing.T) { // Create assistants with specific tag tag := fmt.Sprintf("delete-test-%d", time.Now().UnixNano()) for i := 0; i < 3; i++ { assistant := &types.AssistantModel{ Name: fmt.Sprintf("Delete Test %d", i), Type: "assistant", Connector: "openai", Tags: []string{tag}, Share: "private", } _, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } } // Delete by tag count, err := store.DeleteAssistants(types.AssistantFilter{ Tags: []string{tag}, }) if err != nil { t.Fatalf("Failed to delete assistants: %v", err) } if count < 3 { t.Errorf("Expected at least 3 deletions, got %d", count) } }) t.Run("DeleteByConnector", func(t *testing.T) { // Create assistants with specific connector connector := fmt.Sprintf("test-connector-%d", time.Now().UnixNano()) for i := 0; i < 2; i++ { assistant := &types.AssistantModel{ Name: fmt.Sprintf("Connector Test %d", i), Type: "assistant", Connector: connector, Share: "private", } _, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } } // Delete by connector count, err := store.DeleteAssistants(types.AssistantFilter{ Connector: connector, }) if err != nil { t.Fatalf("Failed to delete assistants: %v", err) } if count < 2 { t.Errorf("Expected at least 2 deletions, got %d", count) } }) t.Run("DeleteByKeywords", func(t *testing.T) { // Create assistants with specific keyword keyword := fmt.Sprintf("unique-keyword-%d", time.Now().UnixNano()) assistant := &types.AssistantModel{ Name: fmt.Sprintf("Assistant with %s", keyword), Type: "assistant", Connector: "openai", Description: "Test description", Share: "private", } _, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } // Delete by keyword count, err := store.DeleteAssistants(types.AssistantFilter{ Keywords: keyword, }) if err != nil { t.Fatalf("Failed to delete assistants: %v", err) } if count < 1 { t.Errorf("Expected at least 1 deletion, got %d", count) } }) t.Run("DeleteByAssistantID", func(t *testing.T) { // Create an assistant assistant := &types.AssistantModel{ Name: "Single Delete Test", Type: "assistant", Connector: "openai", Share: "private", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } // Delete by ID count, err := store.DeleteAssistants(types.AssistantFilter{ AssistantID: id, }) if err != nil { t.Fatalf("Failed to delete assistant: %v", err) } if count != 1 { t.Errorf("Expected 1 deletion, got %d", count) } }) } // TestGetAssistantTags tests retrieving unique tags func TestGetAssistantTags(t *testing.T) { test.Prepare(t, config.Conf) defer test.Clean() store, err := xun.NewXun(types.Setting{ Connector: "default", }) if err != nil { t.Fatalf("Failed to create store: %v", err) } t.Run("GetUniqueTags", func(t *testing.T) { // Create assistants with various tags uniqueTag := fmt.Sprintf("tag-test-%d", time.Now().UnixNano()) assistants := []types.AssistantModel{ { Name: "Tags Test 1", 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", }, } for _, asst := range assistants { _, err := store.SaveAssistant(&asst) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } } // Get all tags tags, err := store.GetAssistantTags(types.AssistantFilter{}) if err != nil { t.Fatalf("Failed to get tags: %v", err) } // Verify we have some tags if len(tags) == 0 { t.Error("Expected at least some tags") } // Verify tag structure for _, tag := range tags { if tag.Value == "" { t.Error("Expected tag to have non-empty value") } if tag.Label == "" { t.Error("Expected tag to have non-empty label") } } t.Logf("Found %d unique tags", len(tags)) }) t.Run("GetTagsWithFilter", func(t *testing.T) { // Create test assistants with specific tags and attributes uniqueTag := fmt.Sprintf("filter-tag-%d", time.Now().UnixNano()) assistants := []types.AssistantModel{ { Name: "Filtered Tags Test 1", Type: "assistant", Connector: "openai", Tags: []string{uniqueTag, "ai"}, Share: "private", BuiltIn: false, Mentionable: true, }, { Name: "Filtered Tags Test 2", Type: "assistant", Connector: "anthropic", Tags: []string{uniqueTag, "coding"}, Share: "private", BuiltIn: true, Mentionable: false, }, { Name: "Filtered Tags Test 3", Type: "assistant", Connector: "openai", Tags: []string{uniqueTag, "search"}, Share: "private", BuiltIn: false, Automated: true, }, } for _, asst := range assistants { _, err := store.SaveAssistant(&asst) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } } // Test: Get tags filtered by connector tagsOpenAI, err := store.GetAssistantTags(types.AssistantFilter{ Connector: "openai", }) if err != nil { t.Fatalf("Failed to get tags with connector filter: %v", err) } t.Logf("Found %d tags for openai connector", len(tagsOpenAI)) // Test: Get tags filtered by built_in builtInFalse := false tagsNonBuiltIn, err := store.GetAssistantTags(types.AssistantFilter{ BuiltIn: &builtInFalse, }) if err != nil { t.Fatalf("Failed to get tags with built_in filter: %v", err) } t.Logf("Found %d tags for non-built-in assistants", len(tagsNonBuiltIn)) // Test: Get tags filtered by mentionable mentionableTrue := true tagsMentionable, err := store.GetAssistantTags(types.AssistantFilter{ Mentionable: &mentionableTrue, }) if err != nil { t.Fatalf("Failed to get tags with mentionable filter: %v", err) } t.Logf("Found %d tags for mentionable assistants", len(tagsMentionable)) // Test: Get tags filtered by keywords tagsWithKeywords, err := store.GetAssistantTags(types.AssistantFilter{ Keywords: "Filtered Tags Test", }) if err != nil { t.Fatalf("Failed to get tags with keywords filter: %v", err) } t.Logf("Found %d tags with keywords filter", len(tagsWithKeywords)) }) t.Run("GetTagsWithQueryFilter", func(t *testing.T) { // Create test assistants with permission fields permTag := fmt.Sprintf("perm-tag-%d", time.Now().UnixNano()) assistants := []types.AssistantModel{ { Name: "Permission Tags Test 1", Type: "assistant", Connector: "openai", Tags: []string{permTag, "public-tag"}, Share: "private", Public: true, YaoCreatedBy: "user-1", YaoTeamID: "team-1", }, { Name: "Permission Tags Test 2", Type: "assistant", Connector: "openai", Tags: []string{permTag, "team-tag"}, Share: "team", Public: false, YaoCreatedBy: "user-2", YaoTeamID: "team-1", }, { Name: "Permission Tags Test 3", Type: "assistant", Connector: "openai", Tags: []string{permTag, "private-tag"}, Share: "private", Public: false, YaoCreatedBy: "user-3", YaoTeamID: "team-2", }, } for _, asst := range assistants { _, err := store.SaveAssistant(&asst) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } } // Test: Get tags for public assistants only tagsPublic, err := store.GetAssistantTags(types.AssistantFilter{ QueryFilter: func(qb query.Query) { qb.Where("public", true) }, }) if err != nil { t.Fatalf("Failed to get tags for public assistants: %v", err) } t.Logf("Found %d tags for public assistants", len(tagsPublic)) // Test: Get tags for team-1 assistants tagsTeam1, err := store.GetAssistantTags(types.AssistantFilter{ QueryFilter: func(qb query.Query) { qb.Where("__yao_team_id", "team-1") }, }) if err != nil { t.Fatalf("Failed to get tags for team-1: %v", err) } t.Logf("Found %d tags for team-1 assistants", len(tagsTeam1)) // Test: Complex permission filter (public OR team-1 with share=team) tagsComplex, err := store.GetAssistantTags(types.AssistantFilter{ QueryFilter: func(qb query.Query) { qb.Where(func(qb query.Query) { qb.Where("public", true) }).OrWhere(func(qb query.Query) { qb.Where("__yao_team_id", "team-1"). Where("share", "team") }) }, }) if err != nil { t.Fatalf("Failed to get tags with complex filter: %v", err) } t.Logf("Found %d tags with complex permission filter", len(tagsComplex)) }) t.Run("GetTagsWithTypesFilter", func(t *testing.T) { uniqueTag := fmt.Sprintf("types-tag-%d", time.Now().UnixNano()) robotTag := fmt.Sprintf("robot-tag-%d", time.Now().UnixNano()) assistants := []types.AssistantModel{ { Name: "Types Tag Assistant", Type: "assistant", Connector: "openai", Tags: []string{uniqueTag, "shared"}, Share: "private", }, { Name: "Types Tag Robot", Type: "robot", Connector: "openai", Tags: []string{robotTag, "shared"}, Share: "private", }, } for _, asst := range assistants { _, err := store.SaveAssistant(&asst) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } } tags, err := store.GetAssistantTags(types.AssistantFilter{ Types: []string{"assistant", "robot"}, }) if err != nil { t.Fatalf("Failed to get tags with types filter: %v", err) } tagValues := make(map[string]bool) for _, tag := range tags { tagValues[tag.Value] = true } assert.True(t, tagValues[uniqueTag], "Should contain assistant-type tag %s", uniqueTag) assert.True(t, tagValues[robotTag], "Should contain robot-type tag %s", robotTag) assert.True(t, tagValues["shared"], "Should contain shared tag") t.Logf("Found %d tags for types=[assistant,robot]", len(tags)) }) t.Run("GetTagsWithSingleTypeVsTypes", func(t *testing.T) { tagsSingleType, err := store.GetAssistantTags(types.AssistantFilter{ Type: "assistant", }) if err != nil { t.Fatalf("Failed to get tags with single type: %v", err) } tagsSliceType, err := store.GetAssistantTags(types.AssistantFilter{ Types: []string{"assistant"}, }) if err != nil { t.Fatalf("Failed to get tags with types slice: %v", err) } assert.Equal(t, len(tagsSingleType), len(tagsSliceType), "Type='assistant' and Types=['assistant'] should return same number of tags") singleSet := make(map[string]bool) for _, tag := range tagsSingleType { singleSet[tag.Value] = true } for _, tag := range tagsSliceType { assert.True(t, singleSet[tag.Value], "Tag %q from Types query should also appear in Type query", tag.Value) } }) } // TestAssistantPermissionFields tests permission management fields func TestAssistantPermissionFields(t *testing.T) { test.Prepare(t, config.Conf) defer test.Clean() store, err := xun.NewXun(types.Setting{ Connector: "default", }) if err != nil { t.Fatalf("Failed to create store: %v", err) } t.Run("SaveWithPermissionFields", func(t *testing.T) { assistant := &types.AssistantModel{ Name: "Permission Test Assistant", Type: "assistant", Connector: "openai", Description: "Testing permission fields", Share: "private", YaoCreatedBy: "user-123", YaoUpdatedBy: "user-123", YaoTeamID: "team-456", YaoTenantID: "tenant-789", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to save assistant with permission fields: %v", err) } // Retrieve and verify - default fields include permission fields retrieved, err := store.GetAssistant(id, nil) if err != nil { t.Fatalf("Failed to get assistant: %v", err) } if retrieved.YaoCreatedBy != "user-123" { t.Errorf("Expected YaoCreatedBy 'user-123', got '%s'", retrieved.YaoCreatedBy) } if retrieved.YaoUpdatedBy != "user-123" { t.Errorf("Expected YaoUpdatedBy 'user-123', got '%s'", retrieved.YaoUpdatedBy) } if retrieved.YaoTeamID != "team-456" { t.Errorf("Expected YaoTeamID 'team-456', got '%s'", retrieved.YaoTeamID) } if retrieved.YaoTenantID != "tenant-789" { t.Errorf("Expected YaoTenantID 'tenant-789', got '%s'", retrieved.YaoTenantID) } t.Logf("Permission fields saved and retrieved successfully for assistant %s", id) }) t.Run("UpdatePermissionFields", func(t *testing.T) { // Create assistant assistant := &types.AssistantModel{ Name: "Update Permission Test", Type: "assistant", Connector: "openai", Share: "private", YaoCreatedBy: "user-original", YaoTeamID: "team-original", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } // Update with new permission fields assistant.ID = id assistant.YaoUpdatedBy = "user-updater" assistant.YaoTenantID = "tenant-new" _, err = store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to update assistant: %v", err) } // Verify update - default fields include permission fields retrieved, err := store.GetAssistant(id, nil) if err != nil { t.Fatalf("Failed to get updated assistant: %v", err) } if retrieved.YaoCreatedBy != "user-original" { t.Errorf("Expected YaoCreatedBy to remain 'user-original', got '%s'", retrieved.YaoCreatedBy) } if retrieved.YaoUpdatedBy != "user-updater" { t.Errorf("Expected YaoUpdatedBy 'user-updater', got '%s'", retrieved.YaoUpdatedBy) } if retrieved.YaoTenantID != "tenant-new" { t.Errorf("Expected YaoTenantID 'tenant-new', got '%s'", retrieved.YaoTenantID) } }) t.Run("EmptyPermissionFields", func(t *testing.T) { // Create assistant without permission fields assistant := &types.AssistantModel{ Name: "No Permission Fields", Type: "assistant", Connector: "openai", Share: "private", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to save assistant: %v", err) } // Retrieve and verify fields are empty retrieved, err := store.GetAssistant(id, nil) if err != nil { t.Fatalf("Failed to get assistant: %v", err) } if retrieved.YaoCreatedBy != "" { t.Errorf("Expected empty YaoCreatedBy, got '%s'", retrieved.YaoCreatedBy) } if retrieved.YaoUpdatedBy != "" { t.Errorf("Expected empty YaoUpdatedBy, got '%s'", retrieved.YaoUpdatedBy) } if retrieved.YaoTeamID != "" { t.Errorf("Expected empty YaoTeamID, got '%s'", retrieved.YaoTeamID) } if retrieved.YaoTenantID != "" { t.Errorf("Expected empty YaoTenantID, got '%s'", retrieved.YaoTenantID) } }) } // 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 := xun.NewXun(types.Setting{ Connector: "default", }) if err != nil { t.Fatalf("Failed to create store: %v", err) } 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 := &types.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, nil) 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 := &types.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 - path is sensitive, need full fields retrieved, err := store.GetAssistant(id, types.AssistantFullFields) 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) }) } // TestGetAssistantWithLocale tests retrieving assistant with locale translation func TestGetAssistantWithLocale(t *testing.T) { test.Prepare(t, config.Conf) defer test.Clean() store, err := xun.NewXun(types.Setting{ Connector: "default", }) if err != nil { t.Fatalf("Failed to create store: %v", err) } t.Run("GetAssistantWithLocaleTranslation", func(t *testing.T) { // Create assistant with i18n locales assistant := &types.AssistantModel{ Name: "{{name}}", Type: "assistant", Connector: "openai", Description: "{{description}}", Tags: []string{"test"}, Share: "private", Placeholder: &types.Placeholder{ Title: "{{chat.title}}", Description: "{{chat.description}}", Prompts: []string{"{{chat.prompts.0}}", "{{chat.prompts.1}}"}, }, } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } // Setup i18n for testing i18n.Locales[id] = map[string]i18n.I18n{ "en": { Locale: "en", Messages: map[string]any{ "name": "Test Assistant", "description": "This is a test assistant", "chat.title": "Chat with me", "chat.description": "Start a conversation", "chat.prompts.0": "How can I help you?", "chat.prompts.1": "What would you like to know?", }, }, "zh-cn": { Locale: "zh-cn", Messages: map[string]any{ "name": "测试助手", "description": "这是一个测试助手", "chat.title": "与我聊天", "chat.description": "开始对话", "chat.prompts.0": "我能帮你什么?", "chat.prompts.1": "你想了解什么?", }, }, } // Test English locale - request all fields for placeholder retrievedEN, err := store.GetAssistant(id, types.AssistantFullFields, "en") if err != nil { t.Fatalf("Failed to get assistant with EN locale: %v", err) } if retrievedEN.Name != "Test Assistant" { t.Errorf("Expected name 'Test Assistant', got '%s'", retrievedEN.Name) } if retrievedEN.Description != "This is a test assistant" { t.Errorf("Expected description 'This is a test assistant', got '%s'", retrievedEN.Description) } if retrievedEN.Placeholder == nil { t.Fatal("Expected placeholder to be set") } if retrievedEN.Placeholder.Title != "Chat with me" { t.Errorf("Expected placeholder title 'Chat with me', got '%s'", retrievedEN.Placeholder.Title) } if retrievedEN.Placeholder.Description != "Start a conversation" { t.Errorf("Expected placeholder description 'Start a conversation', got '%s'", retrievedEN.Placeholder.Description) } if len(retrievedEN.Placeholder.Prompts) != 2 { t.Errorf("Expected 2 placeholder prompts, got %d", len(retrievedEN.Placeholder.Prompts)) } if retrievedEN.Placeholder.Prompts[0] != "How can I help you?" { t.Errorf("Expected first prompt 'How can I help you?', got '%s'", retrievedEN.Placeholder.Prompts[0]) } // Test Chinese locale - request all fields for placeholder retrievedZH, err := store.GetAssistant(id, types.AssistantFullFields, "zh-cn") if err != nil { t.Fatalf("Failed to get assistant with ZH locale: %v", err) } if retrievedZH.Name != "测试助手" { t.Errorf("Expected name '测试助手', got '%s'", retrievedZH.Name) } if retrievedZH.Description != "这是一个测试助手" { t.Errorf("Expected description '这是一个测试助手', got '%s'", retrievedZH.Description) } if retrievedZH.Placeholder == nil { t.Fatal("Expected placeholder to be set") } if retrievedZH.Placeholder.Title != "与我聊天" { t.Errorf("Expected placeholder title '与我聊天', got '%s'", retrievedZH.Placeholder.Title) } // Test without locale (should return original {{...}} values) - request all fields for placeholder retrievedNoLocale, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to get assistant without locale: %v", err) } if retrievedNoLocale.Name != "{{name}}" { t.Errorf("Expected original name '{{name}}', got '%s'", retrievedNoLocale.Name) } if retrievedNoLocale.Description != "{{description}}" { t.Errorf("Expected original description '{{description}}', got '%s'", retrievedNoLocale.Description) } // Cleanup delete(i18n.Locales, id) t.Logf("Successfully tested locale translation for assistant %s", id) }) } // TestGetAssistantsWithLocale tests retrieving multiple assistants with locale translation func TestGetAssistantsWithLocale(t *testing.T) { test.Prepare(t, config.Conf) defer test.Clean() store, err := xun.NewXun(types.Setting{ Connector: "default", }) if err != nil { t.Fatalf("Failed to create store: %v", err) } t.Run("GetAssistantsWithLocaleTranslation", func(t *testing.T) { // Create assistant with i18n locales assistant := &types.AssistantModel{ Name: "{{name}}", Type: "assistant", Connector: "openai", Description: "{{description}}", Tags: []string{"locale-test"}, Share: "private", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } // Setup i18n for testing i18n.Locales[id] = map[string]i18n.I18n{ "en": { Locale: "en", Messages: map[string]any{ "name": "List Test Assistant", "description": "This appears in the list", }, }, "zh-cn": { Locale: "zh-cn", Messages: map[string]any{ "name": "列表测试助手", "description": "这出现在列表中", }, }, } // Test GetAssistants with English locale responseEN, err := store.GetAssistants(types.AssistantFilter{ Tags: []string{"locale-test"}, Page: 1, PageSize: 20, }, "en") if err != nil { t.Fatalf("Failed to get assistants with EN locale: %v", err) } if len(responseEN.Data) < 1 { t.Fatal("Expected at least 1 assistant in response") } found := false for _, asst := range responseEN.Data { if asst.ID == id { found = true if asst.Name != "List Test Assistant" { t.Errorf("Expected name 'List Test Assistant', got '%s'", asst.Name) } if asst.Description != "This appears in the list" { t.Errorf("Expected description 'This appears in the list', got '%s'", asst.Description) } break } } if !found { t.Error("Expected to find the test assistant in the list") } // Test GetAssistants with Chinese locale responseZH, err := store.GetAssistants(types.AssistantFilter{ Tags: []string{"locale-test"}, Page: 1, PageSize: 20, }, "zh-cn") if err != nil { t.Fatalf("Failed to get assistants with ZH locale: %v", err) } found = false for _, asst := range responseZH.Data { if asst.ID == id { found = true if asst.Name != "列表测试助手" { t.Errorf("Expected name '列表测试助手', got '%s'", asst.Name) } if asst.Description != "这出现在列表中" { t.Errorf("Expected description '这出现在列表中', got '%s'", asst.Description) } break } } if !found { t.Error("Expected to find the test assistant in the list") } // Cleanup delete(i18n.Locales, id) t.Logf("Successfully tested locale translation for assistants list") }) } // TestGetAssistantsWithQueryFilter tests using QueryFilter for permission filtering func TestGetAssistantsWithQueryFilter(t *testing.T) { test.Prepare(t, config.Conf) defer test.Clean() store, err := xun.NewXun(types.Setting{ Connector: "default", }) if err != nil { t.Fatalf("Failed to create store: %v", err) } // Create test assistants with different permission settings assistants := []types.AssistantModel{ { Name: "Public Assistant", Type: "assistant", Connector: "openai", Description: "Public assistant visible to all", Tags: []string{"query-filter-test"}, Public: true, Share: "private", YaoCreatedBy: "user-1", YaoTeamID: "team-1", }, { Name: "Team Shared Assistant", Type: "assistant", Connector: "openai", Description: "Team shared assistant", Tags: []string{"query-filter-test"}, Public: false, Share: "team", YaoCreatedBy: "user-2", YaoTeamID: "team-1", }, { Name: "Private Assistant Owner", Type: "assistant", Connector: "openai", Description: "Private assistant owned by user-1", Tags: []string{"query-filter-test"}, Public: false, Share: "private", YaoCreatedBy: "user-1", YaoTeamID: "team-1", }, { Name: "Private Assistant Other", Type: "assistant", Connector: "openai", Description: "Private assistant owned by user-3", Tags: []string{"query-filter-test"}, Public: false, Share: "private", YaoCreatedBy: "user-3", YaoTeamID: "team-2", }, } createdIDs := []string{} for _, asst := range assistants { id, err := store.SaveAssistant(&asst) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } createdIDs = append(createdIDs, id) } t.Run("FilterByPublic", func(t *testing.T) { // QueryFilter: only public assistants response, err := store.GetAssistants(types.AssistantFilter{ Tags: []string{"query-filter-test"}, Page: 1, PageSize: 20, QueryFilter: func(qb query.Query) { qb.Where("public", true) }, }) if err != nil { t.Fatalf("Failed to get public assistants: %v", err) } if len(response.Data) != 1 { t.Errorf("Expected 1 public assistant, got %d", len(response.Data)) } if len(response.Data) > 0 && response.Data[0].Name != "Public Assistant" { t.Errorf("Expected 'Public Assistant', got '%s'", response.Data[0].Name) } }) t.Run("FilterByTeamAndShare", func(t *testing.T) { // QueryFilter: team-1 assistants that are shared with team response, err := store.GetAssistants(types.AssistantFilter{ Tags: []string{"query-filter-test"}, Page: 1, PageSize: 20, QueryFilter: func(qb query.Query) { qb.Where("__yao_team_id", "team-1"). Where("share", "team") }, }) if err != nil { t.Fatalf("Failed to get team shared assistants: %v", err) } if len(response.Data) != 1 { t.Errorf("Expected 1 team shared assistant, got %d", len(response.Data)) } if len(response.Data) > 0 && response.Data[0].Name != "Team Shared Assistant" { t.Errorf("Expected 'Team Shared Assistant', got '%s'", response.Data[0].Name) } }) t.Run("FilterByOwner", func(t *testing.T) { // QueryFilter: assistants created by user-1 response, err := store.GetAssistants(types.AssistantFilter{ Tags: []string{"query-filter-test"}, Page: 1, PageSize: 20, QueryFilter: func(qb query.Query) { qb.Where("__yao_created_by", "user-1") }, }) if err != nil { t.Fatalf("Failed to get user-1 assistants: %v", err) } if len(response.Data) != 2 { t.Errorf("Expected 2 assistants for user-1, got %d", len(response.Data)) } for _, asst := range response.Data { if asst.YaoCreatedBy != "user-1" { t.Errorf("Expected creator 'user-1', got '%s'", asst.YaoCreatedBy) } } }) t.Run("ComplexQueryFilter", func(t *testing.T) { // Complex QueryFilter: (public = true) OR (team_id = team-1 AND (created_by = user-1 OR share = team)) response, err := store.GetAssistants(types.AssistantFilter{ Tags: []string{"query-filter-test"}, Page: 1, PageSize: 20, QueryFilter: func(qb query.Query) { qb.Where(func(qb query.Query) { // Public assistants qb.Where("public", true) }).OrWhere(func(qb query.Query) { // Team assistants where user is creator or shared with team qb.Where("__yao_team_id", "team-1").Where(func(qb query.Query) { qb.Where("__yao_created_by", "user-1"). OrWhere("share", "team") }) }) }, }) if err != nil { t.Fatalf("Failed to get filtered assistants: %v", err) } // Should find: Public Assistant, Team Shared Assistant, Private Assistant Owner if len(response.Data) != 3 { t.Errorf("Expected 3 assistants, got %d", len(response.Data)) } // Verify we got the right assistants names := make(map[string]bool) for _, asst := range response.Data { names[asst.Name] = true } expectedNames := []string{"Public Assistant", "Team Shared Assistant", "Private Assistant Owner"} for _, name := range expectedNames { if !names[name] { t.Errorf("Expected to find '%s' in results", name) } } // Should NOT find Private Assistant Other if names["Private Assistant Other"] { t.Error("Should not find 'Private Assistant Other' in results") } }) t.Run("QueryFilterWithNullCheck", func(t *testing.T) { // QueryFilter: assistants where team_id is null response, err := store.GetAssistants(types.AssistantFilter{ Tags: []string{"query-filter-test"}, Page: 1, PageSize: 20, QueryFilter: func(qb query.Query) { qb.WhereNull("__yao_team_id") }, }) if err != nil { t.Fatalf("Failed to get assistants with null team_id: %v", err) } // All test assistants have team_id, so should find 0 if len(response.Data) != 0 { t.Errorf("Expected 0 assistants with null team_id, got %d", len(response.Data)) } }) t.Run("QueryFilterCombinedWithOtherFilters", func(t *testing.T) { // Combine QueryFilter with other filters response, err := store.GetAssistants(types.AssistantFilter{ Tags: []string{"query-filter-test"}, Connector: "openai", Page: 1, PageSize: 20, QueryFilter: func(qb query.Query) { qb.Where("public", true) }, }) if err != nil { t.Fatalf("Failed to get combined filtered assistants: %v", err) } // Should only find public openai assistants if len(response.Data) != 1 { t.Errorf("Expected 1 assistant, got %d", len(response.Data)) } if len(response.Data) > 0 { if response.Data[0].Connector != "openai" { t.Errorf("Expected connector 'openai', got '%s'", response.Data[0].Connector) } if !response.Data[0].Public { t.Error("Expected public assistant") } } }) // Cleanup for _, id := range createdIDs { _ = store.DeleteAssistant(id) } } // TestUpdateAssistant tests the UpdateAssistant method for incremental updates func TestUpdateAssistant(t *testing.T) { test.Prepare(t, config.Conf) defer test.Clean() store, err := xun.NewXun(types.Setting{ Connector: "default", }) if err != nil { t.Fatalf("Failed to create store: %v", err) } t.Run("UpdateSingleField", func(t *testing.T) { // Create assistant assistant := &types.AssistantModel{ Name: "Original Name", Type: "assistant", Connector: "openai", Description: "Original description", Tags: []string{"original"}, Share: "private", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } // Update only description updates := map[string]interface{}{ "description": "Updated description", } err = store.UpdateAssistant(id, updates) if err != nil { t.Fatalf("Failed to update assistant: %v", err) } // Verify update - need full fields to see tags retrieved, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.Description != "Updated description" { t.Errorf("Expected description 'Updated description', got '%s'", retrieved.Description) } // Other fields should remain unchanged if retrieved.Name != "Original Name" { t.Errorf("Expected name 'Original Name', got '%s'", retrieved.Name) } if len(retrieved.Tags) != 1 || retrieved.Tags[0] != "original" { t.Errorf("Expected tags [original], got %v", retrieved.Tags) } }) t.Run("UpdateMultipleFields", func(t *testing.T) { // Create assistant assistant := &types.AssistantModel{ Name: "Test Assistant", Type: "assistant", Connector: "openai", Description: "Test description", Sort: 100, Mentionable: false, Share: "private", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } // Update multiple fields updates := map[string]interface{}{ "name": "Updated Name", "description": "Updated description", "sort": 200, "mentionable": true, } err = store.UpdateAssistant(id, updates) if err != nil { t.Fatalf("Failed to update assistant: %v", err) } // Verify all updates - use default fields (includes name, description, sort, mentionable) retrieved, err := store.GetAssistant(id, nil) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.Name != "Updated Name" { t.Errorf("Expected name 'Updated Name', got '%s'", retrieved.Name) } if retrieved.Description != "Updated description" { t.Errorf("Expected description 'Updated description', got '%s'", retrieved.Description) } if retrieved.Sort != 200 { t.Errorf("Expected sort 200, got %d", retrieved.Sort) } if !retrieved.Mentionable { t.Error("Expected mentionable to be true") } }) t.Run("UpdateJSONFields", func(t *testing.T) { // Create assistant with complex fields assistant := &types.AssistantModel{ Name: "JSON Test", Type: "assistant", Connector: "openai", Tags: []string{"tag1", "tag2"}, Options: map[string]interface{}{"temperature": 0.7}, Prompts: []types.Prompt{ {Role: "system", Content: "Original system prompt"}, }, Share: "private", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } // Update JSON fields updates := map[string]interface{}{ "tags": []string{"updated", "new-tags"}, "options": map[string]interface{}{ "temperature": 0.9, "max_tokens": 2000, }, "prompts": []types.Prompt{ {Role: "system", Content: "Updated system prompt"}, {Role: "user", Content: "New user prompt"}, }, } err = store.UpdateAssistant(id, updates) if err != nil { t.Fatalf("Failed to update JSON fields: %v", err) } // Verify updates - need full fields for tags, options, prompts retrieved, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if len(retrieved.Tags) != 2 || retrieved.Tags[0] != "updated" { t.Errorf("Expected tags [updated, new-tags], got %v", retrieved.Tags) } if temp, ok := retrieved.Options["temperature"].(float64); !ok || temp != 0.9 { t.Errorf("Expected temperature 0.9, got %v", retrieved.Options["temperature"]) } if len(retrieved.Prompts) != 2 { t.Errorf("Expected 2 prompts, got %d", len(retrieved.Prompts)) } if retrieved.Prompts[0].Content != "Updated system prompt" { t.Errorf("Expected updated system prompt, got '%s'", retrieved.Prompts[0].Content) } }) t.Run("UpdateKBAndMCP", func(t *testing.T) { // Create assistant assistant := &types.AssistantModel{ Name: "KB MCP Test", Type: "assistant", Connector: "openai", Share: "private", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } // Update KB and MCP updates := map[string]interface{}{ "kb": map[string]interface{}{ "collections": []string{"collection1", "collection2"}, }, "mcp": map[string]interface{}{ "servers": []string{"server1", "server2"}, }, } err = store.UpdateAssistant(id, updates) if err != nil { t.Fatalf("Failed to update KB and MCP: %v", err) } // Verify updates - KB and MCP are in default fields retrieved, err := store.GetAssistant(id, nil) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.KB == nil || len(retrieved.KB.Collections) != 2 { t.Errorf("Expected 2 KB collections, got %v", retrieved.KB) } if retrieved.MCP == nil || len(retrieved.MCP.Servers) != 2 { t.Errorf("Expected 2 MCP servers, got %v", retrieved.MCP) } if retrieved.MCP.Servers[0].ServerID != "server1" { t.Errorf("Expected first server 'server1', got '%s'", retrieved.MCP.Servers[0].ServerID) } }) t.Run("UpdateKBDBAndMCP", func(t *testing.T) { // Create assistant assistant := &types.AssistantModel{ Name: "KB DB MCP Test", Type: "assistant", Connector: "openai", Share: "private", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } // Update KB, DB and MCP updates := map[string]interface{}{ "kb": map[string]interface{}{ "collections": []string{"collection1", "collection2"}, }, "db": map[string]interface{}{ "models": []string{"model1", "model2"}, }, "mcp": map[string]interface{}{ "servers": []string{"server1", "server2"}, }, } err = store.UpdateAssistant(id, updates) if err != nil { t.Fatalf("Failed to update KB, DB and MCP: %v", err) } // Verify updates - KB, DB and MCP are in default fields retrieved, err := store.GetAssistant(id, nil) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.KB == nil || len(retrieved.KB.Collections) != 2 { t.Errorf("Expected 2 KB collections, got %v", retrieved.KB) } if retrieved.DB == nil || len(retrieved.DB.Models) != 2 { t.Errorf("Expected 2 DB models, got %v", retrieved.DB) } if retrieved.DB.Models[0] != "model1" { t.Errorf("Expected first model 'model1', got '%s'", retrieved.DB.Models[0]) } if retrieved.MCP == nil || len(retrieved.MCP.Servers) != 2 { t.Errorf("Expected 2 MCP servers, got %v", retrieved.MCP) } if retrieved.MCP.Servers[0].ServerID != "server1" { t.Errorf("Expected first server 'server1', got '%s'", retrieved.MCP.Servers[0].ServerID) } }) t.Run("UpdateDBWithOptions", func(t *testing.T) { // Create assistant assistant := &types.AssistantModel{ Name: "DB Advanced Test", Type: "assistant", Connector: "openai", Share: "private", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } // Update with DB using advanced configuration updates := map[string]interface{}{ "db": map[string]interface{}{ "models": []string{"user", "product", "order"}, "options": map[string]interface{}{ "limit": 100, "offset": 0, }, }, } err = store.UpdateAssistant(id, updates) if err != nil { t.Fatalf("Failed to update DB: %v", err) } // Verify updates - DB is in default fields retrieved, err := store.GetAssistant(id, nil) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.DB == nil { t.Fatal("Expected DB to be set") } if len(retrieved.DB.Models) != 3 { t.Errorf("Expected 3 DB models, got %d", len(retrieved.DB.Models)) } if retrieved.DB.Models[0] != "user" { t.Errorf("Expected first model 'user', got '%s'", retrieved.DB.Models[0]) } if retrieved.DB.Options == nil { t.Error("Expected DB options to be set") } else { if limit, ok := retrieved.DB.Options["limit"].(float64); !ok || limit != 100 { t.Errorf("Expected DB limit 100, got %v", retrieved.DB.Options["limit"]) } } }) t.Run("UpdateModesAndDefaultMode", func(t *testing.T) { // Create assistant assistant := &types.AssistantModel{ Name: "Modes Test", Type: "assistant", Connector: "openai", Share: "private", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } // Update with modes and default_mode updates := map[string]interface{}{ "modes": []string{"chat", "task", "analyze"}, "default_mode": "chat", } err = store.UpdateAssistant(id, updates) if err != nil { t.Fatalf("Failed to update modes: %v", err) } // Verify updates - modes and default_mode are in default fields retrieved, err := store.GetAssistant(id, nil) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.Modes == nil || len(retrieved.Modes) != 3 { t.Errorf("Expected 3 modes, got %v", retrieved.Modes) } if retrieved.Modes[0] != "chat" { t.Errorf("Expected first mode 'chat', got '%s'", retrieved.Modes[0]) } if retrieved.DefaultMode != "chat" { t.Errorf("Expected default_mode 'chat', got '%s'", retrieved.DefaultMode) } }) t.Run("UpdateModesOnly", func(t *testing.T) { // Create assistant with default_mode assistant := &types.AssistantModel{ Name: "Modes Only Test", Type: "assistant", Connector: "openai", Share: "private", DefaultMode: "task", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } // Update only modes updates := map[string]interface{}{ "modes": []string{"chat", "task"}, } err = store.UpdateAssistant(id, updates) if err != nil { t.Fatalf("Failed to update modes: %v", err) } // Verify updates - default_mode should remain unchanged retrieved, err := store.GetAssistant(id, nil) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if len(retrieved.Modes) != 2 { t.Errorf("Expected 2 modes, got %d", len(retrieved.Modes)) } if retrieved.DefaultMode != "task" { t.Errorf("Expected default_mode to remain 'task', got '%s'", retrieved.DefaultMode) } }) t.Run("UpdateMCPWithToolsAndResources", func(t *testing.T) { // Create assistant assistant := &types.AssistantModel{ Name: "MCP Advanced Test", Type: "assistant", Connector: "openai", Share: "private", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } // Update with MCP servers using advanced configuration updates := map[string]interface{}{ "mcp": map[string]interface{}{ "servers": []interface{}{ "server1", // Simple format map[string]interface{}{ "server2": []string{"tool1", "tool2"}, // Tools only }, map[string]interface{}{ "server3": map[string]interface{}{ "resources": []string{"res1", "res2"}, "tools": []string{"tool3", "tool4"}, }, }, }, }, } err = store.UpdateAssistant(id, updates) if err != nil { t.Fatalf("Failed to update MCP: %v", err) } // Verify updates - MCP is in default fields retrieved, err := store.GetAssistant(id, nil) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.MCP == nil || len(retrieved.MCP.Servers) != 3 { t.Fatalf("Expected 3 MCP servers, got %d", len(retrieved.MCP.Servers)) } // Verify server1 (simple format) if retrieved.MCP.Servers[0].ServerID != "server1" { t.Errorf("Expected server1, got '%s'", retrieved.MCP.Servers[0].ServerID) } if len(retrieved.MCP.Servers[0].Tools) != 0 { t.Errorf("Expected no tools for server1, got %v", retrieved.MCP.Servers[0].Tools) } // Verify server2 (tools only) if retrieved.MCP.Servers[1].ServerID != "server2" { t.Errorf("Expected server2, got '%s'", retrieved.MCP.Servers[1].ServerID) } if len(retrieved.MCP.Servers[1].Tools) != 2 { t.Errorf("Expected 2 tools for server2, got %d", len(retrieved.MCP.Servers[1].Tools)) } if retrieved.MCP.Servers[1].Tools[0] != "tool1" { t.Errorf("Expected tool1, got '%s'", retrieved.MCP.Servers[1].Tools[0]) } // Verify server3 (full config) if retrieved.MCP.Servers[2].ServerID != "server3" { t.Errorf("Expected server3, got '%s'", retrieved.MCP.Servers[2].ServerID) } if len(retrieved.MCP.Servers[2].Resources) != 2 { t.Errorf("Expected 2 resources for server3, got %d", len(retrieved.MCP.Servers[2].Resources)) } if len(retrieved.MCP.Servers[2].Tools) != 2 { t.Errorf("Expected 2 tools for server3, got %d", len(retrieved.MCP.Servers[2].Tools)) } if retrieved.MCP.Servers[2].Resources[0] != "res1" { t.Errorf("Expected res1, got '%s'", retrieved.MCP.Servers[2].Resources[0]) } if retrieved.MCP.Servers[2].Tools[0] != "tool3" { t.Errorf("Expected tool3, got '%s'", retrieved.MCP.Servers[2].Tools[0]) } t.Logf("Successfully verified MCP advanced configuration for assistant %s", id) }) t.Run("UpdateUses", func(t *testing.T) { // Create assistant without uses assistant := &types.AssistantModel{ Name: "Uses Update Test", Type: "assistant", Connector: "openai", Share: "private", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } // Update with uses configuration updates := map[string]interface{}{ "uses": &context.Uses{ Vision: "mcp:new-vision", Audio: "mcp:new-audio", Search: "agent", Fetch: "mcp:fetch-server", }, } err = store.UpdateAssistant(id, updates) if err != nil { t.Fatalf("Failed to update uses: %v", err) } // Verify updates - uses is NOT in default fields retrieved, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.Uses == nil { t.Fatal("Expected uses to be set") } if retrieved.Uses.Vision != "mcp:new-vision" { t.Errorf("Expected vision 'mcp:new-vision', got '%s'", retrieved.Uses.Vision) } if retrieved.Uses.Audio != "mcp:new-audio" { t.Errorf("Expected audio 'mcp:new-audio', got '%s'", retrieved.Uses.Audio) } if retrieved.Uses.Search != "agent" { t.Errorf("Expected search 'agent', got '%s'", retrieved.Uses.Search) } if retrieved.Uses.Fetch != "mcp:fetch-server" { t.Errorf("Expected fetch 'mcp:fetch-server', got '%s'", retrieved.Uses.Fetch) } // Update to change uses updates2 := map[string]interface{}{ "uses": &context.Uses{ Vision: "agent", Audio: "agent", }, } err = store.UpdateAssistant(id, updates2) if err != nil { t.Fatalf("Failed to update uses again: %v", err) } // Verify second update - uses is NOT in default fields retrieved2, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved2.Uses.Vision != "agent" { t.Errorf("Expected vision 'agent', got '%s'", retrieved2.Uses.Vision) } if retrieved2.Uses.Audio != "agent" { t.Errorf("Expected audio 'agent', got '%s'", retrieved2.Uses.Audio) } // Update to remove uses (set to nil) updates3 := map[string]interface{}{ "uses": nil, } err = store.UpdateAssistant(id, updates3) if err != nil { t.Fatalf("Failed to set uses to nil: %v", err) } // Verify uses is nil - uses is NOT in default fields retrieved3, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved3.Uses != nil { t.Errorf("Expected uses to be nil, got %+v", retrieved3.Uses) } }) t.Run("UpdateSearch", func(t *testing.T) { // Create assistant without search assistant := &types.AssistantModel{ Name: "Search Update Test", Type: "assistant", Connector: "openai", Share: "private", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } // Update with search configuration updates := map[string]interface{}{ "search": &searchTypes.Config{ Web: &searchTypes.WebConfig{ Provider: "tavily", MaxResults: 20, }, KB: &searchTypes.KBConfig{ Collections: []string{"knowledge"}, Threshold: 0.75, }, }, } err = store.UpdateAssistant(id, updates) if err != nil { t.Fatalf("Failed to update search: %v", err) } // Verify updates - search is NOT in default fields retrieved, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.Search == nil { t.Fatal("Expected search to be set") } if retrieved.Search.Web == nil { t.Fatal("Expected search.web to be set") } if retrieved.Search.Web.Provider != "tavily" { t.Errorf("Expected web provider 'tavily', got '%s'", retrieved.Search.Web.Provider) } if retrieved.Search.Web.MaxResults != 20 { t.Errorf("Expected web max_results 20, got %d", retrieved.Search.Web.MaxResults) } if retrieved.Search.KB == nil { t.Fatal("Expected search.kb to be set") } if len(retrieved.Search.KB.Collections) != 1 { t.Errorf("Expected 1 KB collection, got %d", len(retrieved.Search.KB.Collections)) } if retrieved.Search.KB.Threshold != 0.75 { t.Errorf("Expected KB threshold 0.75, got %f", retrieved.Search.KB.Threshold) } // Update to change search configuration updates2 := map[string]interface{}{ "search": &searchTypes.Config{ Web: &searchTypes.WebConfig{ Provider: "serper", MaxResults: 30, }, Citation: &searchTypes.CitationConfig{ Format: "#cite:{id}", AutoInjectPrompt: false, }, }, } err = store.UpdateAssistant(id, updates2) if err != nil { t.Fatalf("Failed to update search again: %v", err) } // Verify second update - search is NOT in default fields retrieved2, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved2.Search.Web.Provider != "serper" { t.Errorf("Expected web provider 'serper', got '%s'", retrieved2.Search.Web.Provider) } if retrieved2.Search.Web.MaxResults != 30 { t.Errorf("Expected web max_results 30, got %d", retrieved2.Search.Web.MaxResults) } if retrieved2.Search.Citation == nil { t.Fatal("Expected search.citation to be set") } if retrieved2.Search.Citation.Format != "#cite:{id}" { t.Errorf("Expected citation format '#cite:{id}', got '%s'", retrieved2.Search.Citation.Format) } // Update to remove search (set to nil) updates3 := map[string]interface{}{ "search": nil, } err = store.UpdateAssistant(id, updates3) if err != nil { t.Fatalf("Failed to set search to nil: %v", err) } // Verify search is nil - search is NOT in default fields retrieved3, err := store.GetAssistant(id, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved3.Search != nil { t.Errorf("Expected search to be nil, got %+v", retrieved3.Search) } }) t.Run("UpdatePermissionFields", func(t *testing.T) { // Create assistant with permission fields assistant := &types.AssistantModel{ Name: "Permission Test", Type: "assistant", Connector: "openai", Share: "private", YaoCreatedBy: "user-1", YaoTeamID: "team-1", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } // Update permission fields updates := map[string]interface{}{ "__yao_updated_by": "user-2", "__yao_tenant_id": "tenant-1", } err = store.UpdateAssistant(id, updates) if err != nil { t.Fatalf("Failed to update permission fields: %v", err) } // Verify updates - permission fields are in default fields retrieved, err := store.GetAssistant(id, nil) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.YaoUpdatedBy != "user-2" { t.Errorf("Expected YaoUpdatedBy 'user-2', got '%s'", retrieved.YaoUpdatedBy) } if retrieved.YaoTenantID != "tenant-1" { t.Errorf("Expected YaoTenantID 'tenant-1', got '%s'", retrieved.YaoTenantID) } // Created by should remain unchanged if retrieved.YaoCreatedBy != "user-1" { t.Errorf("Expected YaoCreatedBy 'user-1', got '%s'", retrieved.YaoCreatedBy) } }) t.Run("UpdateWithEmptyStrings", func(t *testing.T) { // Create assistant with values assistant := &types.AssistantModel{ Name: "Empty String Test", Type: "assistant", Connector: "openai", Avatar: "https://example.com/avatar.png", Description: "Some description", Share: "private", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } // Update with empty strings (should become NULL) updates := map[string]interface{}{ "avatar": "", "description": "", } err = store.UpdateAssistant(id, updates) if err != nil { t.Fatalf("Failed to update with empty strings: %v", err) } // Verify empty strings are stored as NULL - default fields include avatar, description retrieved, err := store.GetAssistant(id, nil) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.Avatar != "" { t.Errorf("Expected empty avatar, got '%s'", retrieved.Avatar) } if retrieved.Description != "" { t.Errorf("Expected empty description, got '%s'", retrieved.Description) } // Name should remain unchanged if retrieved.Name != "Empty String Test" { t.Errorf("Expected name 'Empty String Test', got '%s'", retrieved.Name) } }) t.Run("UpdateNonExistentAssistant", func(t *testing.T) { updates := map[string]interface{}{ "name": "Updated Name", } err := store.UpdateAssistant("nonexistent-id", updates) if err == nil { t.Error("Expected error when updating non-existent assistant") } if !strings.Contains(err.Error(), "not found") { t.Errorf("Expected 'not found' error, got: %v", err) } }) t.Run("UpdateWithEmptyID", func(t *testing.T) { updates := map[string]interface{}{ "name": "Updated Name", } err := store.UpdateAssistant("", updates) if err == nil { t.Error("Expected error when updating with empty ID") } if !strings.Contains(err.Error(), "required") { t.Errorf("Expected 'required' error, got: %v", err) } }) t.Run("UpdateWithEmptyUpdates", func(t *testing.T) { // Create assistant assistant := &types.AssistantModel{ Name: "Empty Updates Test", Type: "assistant", Connector: "openai", Share: "private", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } // Try to update with empty map updates := map[string]interface{}{} err = store.UpdateAssistant(id, updates) if err == nil { t.Error("Expected error when updating with no fields") } if !strings.Contains(err.Error(), "no fields to update") { t.Errorf("Expected 'no fields to update' error, got: %v", err) } }) t.Run("UpdateTimestampAutomaticallySet", func(t *testing.T) { // Create assistant assistant := &types.AssistantModel{ Name: "Timestamp Test", Type: "assistant", Connector: "openai", Share: "private", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } // Get original updated_at - default fields include updated_at original, err := store.GetAssistant(id, nil) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } // Wait a bit to ensure timestamp difference time.Sleep(100 * time.Millisecond) // Update assistant updates := map[string]interface{}{ "description": "Updated to test timestamp", } err = store.UpdateAssistant(id, updates) if err != nil { t.Fatalf("Failed to update assistant: %v", err) } // Get updated assistant - default fields include description, updated_at updated, err := store.GetAssistant(id, nil) if err != nil { t.Fatalf("Failed to retrieve updated assistant: %v", err) } // Verify description was updated (main test objective) if updated.Description != "Updated to test timestamp" { t.Errorf("Expected description 'Updated to test timestamp', got '%s'", updated.Description) } // Only check timestamp if both are set (some stores may not return timestamps) if original.UpdatedAt > 0 && updated.UpdatedAt > 0 { if updated.UpdatedAt <= original.UpdatedAt { t.Errorf("Expected updated_at to increase, original=%d, updated=%d", original.UpdatedAt, updated.UpdatedAt) } } else { t.Logf("Skipping timestamp comparison (original=%d, updated=%d)", original.UpdatedAt, updated.UpdatedAt) } }) t.Run("UpdateSkipsSystemFields", func(t *testing.T) { // Create assistant assistant := &types.AssistantModel{ Name: "System Fields Test", Type: "assistant", Connector: "openai", Share: "private", } id, err := store.SaveAssistant(assistant) if err != nil { t.Fatalf("Failed to create assistant: %v", err) } // Get original - default fields original, err := store.GetAssistant(id, nil) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } // Try to update system fields (should be ignored) updates := map[string]interface{}{ "assistant_id": "new-id-123", // Should be ignored "created_at": int64(123456789), // Should be ignored "name": "Valid Update", // Should be applied } err = store.UpdateAssistant(id, updates) if err != nil { t.Fatalf("Failed to update assistant: %v", err) } // Verify system fields unchanged, but name updated - default fields retrieved, err := store.GetAssistant(id, nil) if err != nil { t.Fatalf("Failed to retrieve assistant: %v", err) } if retrieved.ID != id { t.Errorf("Expected ID to remain %s, got %s", id, retrieved.ID) } if retrieved.CreatedAt != original.CreatedAt { t.Errorf("Expected created_at to remain unchanged") } if retrieved.Name != "Valid Update" { t.Errorf("Expected name 'Valid Update', got '%s'", retrieved.Name) } }) } // TestAssistantCompleteWorkflow tests a complete workflow func TestAssistantCompleteWorkflow(t *testing.T) { test.Prepare(t, config.Conf) defer test.Clean() store, err := xun.NewXun(types.Setting{ Connector: "default", }) if err != nil { t.Fatalf("Failed to create store: %v", err) } t.Run("CompleteWorkflow", func(t *testing.T) { // Step 1: Create multiple assistants assistantIDs := []string{} for i := 0; i < 3; i++ { assistant := &types.AssistantModel{ Name: fmt.Sprintf("Workflow Assistant %d", i), Type: "assistant", Connector: "openai", 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) if err != nil { t.Fatalf("Failed to create assistant %d: %v", i, err) } assistantIDs = append(assistantIDs, id) } t.Logf("Created %d assistants", len(assistantIDs)) // Step 2: Retrieve all assistants response, err := store.GetAssistants(types.AssistantFilter{ Tags: []string{"workflow"}, Page: 1, PageSize: 20, }) if err != nil { t.Fatalf("Failed to get assistants: %v", err) } if len(response.Data) < 3 { t.Errorf("Expected at least 3 assistants, got %d", len(response.Data)) } // Step 3: Update one assistant - need full fields for tags updatedID := assistantIDs[1] updatedAssistant, err := store.GetAssistant(updatedID, types.AssistantFullFields) if err != nil { t.Fatalf("Failed to get assistant for update: %v", err) } updatedAssistant.Description = "Updated workflow description" updatedAssistant.Tags = append(updatedAssistant.Tags, "updated") _, err = store.SaveAssistant(updatedAssistant) if err != nil { t.Fatalf("Failed to update assistant: %v", err) } // Verify update - default fields include description verifyAssistant, err := store.GetAssistant(updatedID, nil) if err != nil { t.Fatalf("Failed to verify update: %v", err) } if verifyAssistant.Description != "Updated workflow description" { t.Errorf("Update not applied correctly") } // Step 4: Delete one assistant err = store.DeleteAssistant(assistantIDs[0]) if err != nil { t.Fatalf("Failed to delete assistant: %v", err) } // Verify deletion _, err = store.GetAssistant(assistantIDs[0], nil) if err == nil { t.Error("Expected error when getting deleted assistant") } // Step 5: Get tags tags, err := store.GetAssistantTags(types.AssistantFilter{}) if err != nil { t.Fatalf("Failed to get tags: %v", err) } // Should find "workflow" tag found := false for _, tag := range tags { if tag.Value == "workflow" { found = true break } } if !found { t.Error("Expected to find 'workflow' tag") } // Step 6: Bulk delete remaining assistants count, err := store.DeleteAssistants(types.AssistantFilter{ Tags: []string{"workflow"}, }) if err != nil { t.Fatalf("Failed to bulk delete: %v", err) } t.Logf("Bulk deleted %d assistants", count) // Verify bulk deletion finalResponse, err := store.GetAssistants(types.AssistantFilter{ Tags: []string{"workflow"}, Page: 1, PageSize: 20, }) if err != nil { t.Fatalf("Failed to verify bulk deletion: %v", err) } if len(finalResponse.Data) > 0 { t.Logf("Warning: Still found %d assistants after bulk delete", len(finalResponse.Data)) } }) }