package assistant import ( "fmt" "testing" "github.com/stretchr/testify/assert" "github.com/yaoapp/yao/config" "github.com/yaoapp/yao/neo/store" "github.com/yaoapp/yao/test" ) func prepare(t *testing.T) { test.Prepare(t, config.Conf) } func TestLoad_LoadPath(t *testing.T) { prepare(t) defer test.Clean() assistant, err := LoadPath("/assistants/modi") if err != nil { t.Fatal(err) } // Validate basic properties assert.NotNil(t, assistant) assert.Equal(t, "modi", assistant.ID) assert.Equal(t, "Modi", assistant.Name) assert.Equal(t, "https://api.dicebear.com/7.x/bottts/svg?seed=Modi", assistant.Avatar) assert.Equal(t, "deepseek", assistant.Connector) assert.NotNil(t, assistant.Prompts) assert.NotNil(t, assistant.Script) // Test non-existent assistant _, err = LoadPath("/assistants/non-existent") assert.Error(t, err) } func TestLoad_LoadStore(t *testing.T) { prepare(t) defer test.Clean() // Test with nil storage _, err := LoadStore("test-id") assert.Error(t, err) assert.Contains(t, err.Error(), "storage is not set") // Setup mock storage mockStore := &mockStore{ data: map[string]map[string]interface{}{ "test-id": { "assistant_id": "test-id", "name": "Test Assistant", "avatar": "test-avatar", "connector": "gpt-3_5-turbo", }, }, } SetStorage(mockStore) defer SetStorage(nil) // Test loading from store assistant, err := LoadStore("test-id") assert.NoError(t, err) assert.NotNil(t, assistant) assert.Equal(t, "test-id", assistant.ID) assert.Equal(t, "Test Assistant", assistant.Name) assert.Equal(t, "test-avatar", assistant.Avatar) assert.Equal(t, "gpt-3_5-turbo", assistant.Connector) // Test cache functionality assistant2, err := LoadStore("test-id") assert.NoError(t, err) assert.Equal(t, assistant, assistant2) // Should be the same instance from cache // Test non-existent assistant _, err = LoadStore("non-existent") assert.Error(t, err) } func TestLoad_Cache(t *testing.T) { prepare(t) defer test.Clean() // Clear any existing cache first ClearCache() // Test cache operations SetCache(2) // Set small cache size for testing assert.Equal(t, 2, loaded.capacity, "Cache capacity should be 2") // Create test assistants assistant1 := &Assistant{ID: "id1", Name: "Assistant 1"} assistant2 := &Assistant{ID: "id2", Name: "Assistant 2"} assistant3 := &Assistant{ID: "id3", Name: "Assistant 3"} // Test Put and Get loaded.Put(assistant1) assert.Equal(t, 1, loaded.Len(), "Cache should have 1 item") loaded.Put(assistant2) assert.Equal(t, 2, loaded.Len(), "Cache should have 2 items") // Test cache hit cached, exists := loaded.Get("id1") assert.True(t, exists) assert.Equal(t, assistant1, cached) // Test cache eviction (LRU) // At this point: assistant1 is most recently used (due to Get), then assistant2 loaded.Put(assistant3) // This should evict assistant2 since it's least recently used assert.Equal(t, 2, loaded.Len(), "Cache should still have 2 items") _, exists = loaded.Get("id2") assert.False(t, exists, "assistant2 should have been evicted (least recently used)") _, exists = loaded.Get("id1") assert.True(t, exists, "assistant1 should still be in cache (was accessed recently)") _, exists = loaded.Get("id3") assert.True(t, exists, "assistant3 should be in cache (most recently added)") // Test clear cache ClearCache() assert.Nil(t, loaded) // Test setting new cache capacity SetCache(100) assert.NotNil(t, loaded) } func TestLoad_Validate(t *testing.T) { tests := []struct { name string ast *Assistant wantErr bool }{ { name: "valid assistant", ast: &Assistant{ ID: "test-id", Name: "Test Assistant", Connector: "test-connector", }, wantErr: false, }, { name: "missing id", ast: &Assistant{ Name: "Test Assistant", Connector: "test-connector", }, wantErr: true, }, { name: "missing name", ast: &Assistant{ ID: "test-id", Connector: "test-connector", }, wantErr: true, }, { name: "missing connector", ast: &Assistant{ ID: "test-id", Name: "Test Assistant", }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.ast.Validate() if (err != nil) != tt.wantErr { t.Errorf("Assistant.Validate() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestLoad_Clone(t *testing.T) { // Create a test assistant with all fields populated original := &Assistant{ ID: "test-id", Type: "test-type", Name: "Test Assistant", Avatar: "test-avatar", Connector: "test-connector", Path: "test-path", BuiltIn: true, Sort: 1, Description: "test description", Tags: []string{"tag1", "tag2"}, Readonly: true, Mentionable: true, Automated: true, Options: map[string]interface{}{"key": "value"}, Prompts: []Prompt{{Role: "system", Content: "test"}}, Workflow: map[string]interface{}{"step": "test"}, } // Clone the assistant clone := original.Clone() // Verify all fields are correctly cloned assert.Equal(t, original.ID, clone.ID) assert.Equal(t, original.Type, clone.Type) assert.Equal(t, original.Name, clone.Name) assert.Equal(t, original.Avatar, clone.Avatar) assert.Equal(t, original.Connector, clone.Connector) assert.Equal(t, original.Path, clone.Path) assert.Equal(t, original.BuiltIn, clone.BuiltIn) assert.Equal(t, original.Sort, clone.Sort) assert.Equal(t, original.Description, clone.Description) assert.Equal(t, original.Tags, clone.Tags) assert.Equal(t, original.Readonly, clone.Readonly) assert.Equal(t, original.Mentionable, clone.Mentionable) assert.Equal(t, original.Automated, clone.Automated) assert.Equal(t, original.Options, clone.Options) assert.Equal(t, original.Prompts, clone.Prompts) assert.Equal(t, original.Workflow, clone.Workflow) // Verify deep copy by modifying original original.Tags[0] = "modified" original.Options["key"] = "modified" original.Workflow["step"] = "modified" assert.NotEqual(t, original.Tags[0], clone.Tags[0]) assert.NotEqual(t, original.Options["key"], clone.Options["key"]) assert.NotEqual(t, original.Workflow["step"], clone.Workflow["step"]) // Test nil case var nilAssistant *Assistant assert.Nil(t, nilAssistant.Clone()) } func TestLoad_Update(t *testing.T) { // Create a test assistant ast := &Assistant{ ID: "test-id", Name: "Original Name", Connector: "original-connector", } // Test updating various fields updates := map[string]interface{}{ "name": "Updated Name", "avatar": "updated-avatar", "description": "Updated description", "connector": "updated-connector", "type": "updated-type", "sort": 2, "mentionable": true, "automated": true, "tags": []string{"new-tag"}, "options": map[string]interface{}{"new": "value"}, } err := ast.Update(updates) assert.NoError(t, err) // Verify updates assert.Equal(t, "Updated Name", ast.Name) assert.Equal(t, "updated-avatar", ast.Avatar) assert.Equal(t, "Updated description", ast.Description) assert.Equal(t, "updated-connector", ast.Connector) assert.Equal(t, "updated-type", ast.Type) assert.Equal(t, 2, ast.Sort) assert.True(t, ast.Mentionable) assert.True(t, ast.Automated) assert.Equal(t, []string{"new-tag"}, ast.Tags) assert.Equal(t, map[string]interface{}{"new": "value"}, ast.Options) // Test nil assistant var nilAssistant *Assistant err = nilAssistant.Update(updates) assert.Error(t, err) // Test invalid update that would make the assistant invalid invalidUpdates := map[string]interface{}{ "name": "", } err = ast.Update(invalidUpdates) assert.Error(t, err) } func TestLoadBuiltIn(t *testing.T) { prepare(t) defer test.Clean() // Clear any existing cache and storage ClearCache() SetStorage(nil) // Create a mock store to verify built-in assistants are saved mockStore := &mockStore{ data: make(map[string]map[string]interface{}), } SetStorage(mockStore) SetCache(100) // Test loading built-in assistants err := LoadBuiltIn() assert.NoError(t, err) // Verify Modi assistant was loaded assistant, exists := loaded.Get("modi") assert.True(t, exists, "Modi assistant should be loaded in cache") if exists { assert.Equal(t, "modi", assistant.ID) assert.Equal(t, "Modi", assistant.Name) assert.Equal(t, "deepseek", assistant.Connector) assert.True(t, assistant.BuiltIn) assert.True(t, assistant.Readonly) assert.NotNil(t, assistant.Prompts) assert.NotNil(t, assistant.Script) } } // mockStore implements store.Store interface for testing type mockStore struct { data map[string]map[string]interface{} } func (m *mockStore) GetAssistant(id string, locale ...string) (map[string]interface{}, error) { if data, ok := m.data[id]; ok { return data, nil } return nil, fmt.Errorf("assistant not found: %s", id) } // Add other required interface methods with empty implementations func (m *mockStore) GetThread(id string) (map[string]interface{}, error) { return nil, nil } func (m *mockStore) GetMessage(id string) (map[string]interface{}, error) { return nil, nil } func (m *mockStore) GetFile(id string) (map[string]interface{}, error) { return nil, nil } func (m *mockStore) CreateAssistant(data map[string]interface{}) (map[string]interface{}, error) { return nil, nil } func (m *mockStore) CreateThread(data map[string]interface{}) (map[string]interface{}, error) { return nil, nil } func (m *mockStore) CreateMessage(data map[string]interface{}) (map[string]interface{}, error) { return nil, nil } func (m *mockStore) CreateFile(data map[string]interface{}) (map[string]interface{}, error) { return nil, nil } func (m *mockStore) UpdateAssistant(id string, data map[string]interface{}) error { return nil } func (m *mockStore) UpdateThread(id string, data map[string]interface{}) error { return nil } func (m *mockStore) UpdateMessage(id string, data map[string]interface{}) error { return nil } func (m *mockStore) UpdateFile(id string, data map[string]interface{}) error { return nil } func (m *mockStore) DeleteAssistant(id string) error { return nil } func (m *mockStore) DeleteThread(id string) error { return nil } func (m *mockStore) DeleteMessage(id string) error { return nil } func (m *mockStore) DeleteFile(id string) error { return nil } func (m *mockStore) ListAssistants(query map[string]interface{}) ([]map[string]interface{}, error) { return nil, nil } func (m *mockStore) ListThreads(query map[string]interface{}) ([]map[string]interface{}, error) { return nil, nil } func (m *mockStore) ListMessages(query map[string]interface{}) ([]map[string]interface{}, error) { return nil, nil } func (m *mockStore) ListFiles(query map[string]interface{}) ([]map[string]interface{}, error) { return nil, nil } func (m *mockStore) DeleteAllChats(id string) error { return nil } func (m *mockStore) DeleteChat(id string, chatID string) error { return nil } func (m *mockStore) GetAssistants(filter store.AssistantFilter, locale ...string) (*store.AssistantResponse, error) { return nil, nil } func (m *mockStore) GetChat(id string, chatID string, locale ...string) (*store.ChatInfo, error) { return nil, nil } func (m *mockStore) GetChatWithFilter(id string, chatID string, filter store.ChatFilter, locale ...string) (*store.ChatInfo, error) { return nil, nil } func (m *mockStore) GetChats(id string, filter store.ChatFilter, locale ...string) (*store.ChatGroupResponse, error) { return nil, nil } func (m *mockStore) GetHistory(id string, chatID string, locale ...string) ([]map[string]interface{}, error) { return nil, nil } func (m *mockStore) GetHistoryWithFilter(id string, chatID string, filter store.ChatFilter, locale ...string) ([]map[string]interface{}, error) { return nil, nil } func (m *mockStore) SaveAssistant(assistant map[string]interface{}) (interface{}, error) { return nil, nil } func (m *mockStore) SaveHistory(sid string, messages []map[string]interface{}, cid string, context map[string]interface{}) error { return nil } func (m *mockStore) UpdateChatTitle(sid string, cid string, title string) error { return nil } func (m *mockStore) DeleteAssistants(filter store.AssistantFilter) (int64, error) { return 0, nil } func (m *mockStore) GetAssistantTags(locale ...string) ([]store.Tag, error) { return []store.Tag{}, nil } // Attachment related methods func (m *mockStore) SaveAttachment(attachment map[string]interface{}) (interface{}, error) { return attachment["file_id"], nil } func (m *mockStore) DeleteAttachment(fileID string) error { return nil } func (m *mockStore) GetAttachments(filter store.AttachmentFilter, locale ...string) (*store.AttachmentResponse, error) { return &store.AttachmentResponse{}, nil } func (m *mockStore) GetAttachment(fileID string, locale ...string) (map[string]interface{}, error) { return nil, nil } func (m *mockStore) DeleteAttachments(filter store.AttachmentFilter) (int64, error) { return 0, nil } // Knowledge related methods func (m *mockStore) SaveKnowledge(knowledge map[string]interface{}) (interface{}, error) { return knowledge["collection_id"], nil } func (m *mockStore) DeleteKnowledge(collectionID string) error { return nil } func (m *mockStore) GetKnowledges(filter store.KnowledgeFilter, locale ...string) (*store.KnowledgeResponse, error) { return &store.KnowledgeResponse{}, nil } func (m *mockStore) GetKnowledge(collectionID string, locale ...string) (map[string]interface{}, error) { return nil, nil } func (m *mockStore) DeleteKnowledges(filter store.KnowledgeFilter) (int64, error) { return 0, nil } // Close closes the store and releases any resources func (m *mockStore) Close() error { return nil }