package agent import ( "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/yaoapp/yao/agent/assistant" "github.com/yaoapp/yao/agent/types" "github.com/yaoapp/yao/config" "github.com/yaoapp/yao/test" ) func prepare(t *testing.T) { test.Prepare(t, config.Conf) err := Load(config.Conf) require.NoError(t, err) } func TestLoad(t *testing.T) { prepare(t) defer test.Clean() agent := GetAgent() require.NotNil(t, agent) t.Run("LoadAgentSettings", func(t *testing.T) { // Cache setting assert.NotEmpty(t, agent.Cache) // Store setting assert.NotNil(t, agent.Store) assert.Greater(t, agent.StoreSetting.MaxSize, 0) // Uses setting assert.NotNil(t, agent.Uses) assert.NotEmpty(t, agent.Uses.Default) }) t.Run("LoadDefaultAssistant", func(t *testing.T) { assert.NotNil(t, agent.Assistant) }) t.Run("LoadGlobalPrompts", func(t *testing.T) { // Global prompts should be loaded from agent/prompts.yml assert.NotNil(t, agent.GlobalPrompts) assert.Greater(t, len(agent.GlobalPrompts), 0) // First prompt should be system role assert.Equal(t, "system", agent.GlobalPrompts[0].Role) // Content should contain system context info (with variables not yet parsed) assert.Contains(t, agent.GlobalPrompts[0].Content, "$SYS.") }) t.Run("LoadKBConfig", func(t *testing.T) { // KB configuration should be loaded from agent/kb.yml assert.NotNil(t, agent.KB) assert.NotNil(t, agent.KB.Chat) // Verify chat KB settings assert.Equal(t, "__yao.openai", agent.KB.Chat.EmbeddingProviderID) assert.Equal(t, "text-embedding-3-small", agent.KB.Chat.EmbeddingOptionID) assert.Equal(t, "zh-CN", agent.KB.Chat.Locale) // Verify config assert.NotNil(t, agent.KB.Chat.Config) assert.Equal(t, "hnsw", agent.KB.Chat.Config.IndexType.String()) assert.Equal(t, "cosine", agent.KB.Chat.Config.Distance.String()) // Verify metadata assert.NotNil(t, agent.KB.Chat.Metadata) assert.Equal(t, "chat_session", agent.KB.Chat.Metadata["category"]) assert.Equal(t, true, agent.KB.Chat.Metadata["auto_created"]) // Verify document defaults assert.NotNil(t, agent.KB.Chat.DocumentDefaults) assert.NotNil(t, agent.KB.Chat.DocumentDefaults.Chunking) assert.Equal(t, "__yao.structured", agent.KB.Chat.DocumentDefaults.Chunking.ProviderID) assert.Equal(t, "standard", agent.KB.Chat.DocumentDefaults.Chunking.OptionID) assert.NotNil(t, agent.KB.Chat.DocumentDefaults.Extraction) assert.Equal(t, "__yao.openai", agent.KB.Chat.DocumentDefaults.Extraction.ProviderID) assert.Equal(t, "gpt-4o-mini", agent.KB.Chat.DocumentDefaults.Extraction.OptionID) assert.NotNil(t, agent.KB.Chat.DocumentDefaults.Converter) assert.Equal(t, "__yao.utf8", agent.KB.Chat.DocumentDefaults.Converter.ProviderID) assert.Equal(t, "standard-text", agent.KB.Chat.DocumentDefaults.Converter.OptionID) }) t.Run("LoadSearchConfig", func(t *testing.T) { // Search configuration should be loaded from agent/search.yml assert.NotNil(t, agent.Search) // Verify web config assert.NotNil(t, agent.Search.Web) assert.Equal(t, "tavily", agent.Search.Web.Provider) assert.Equal(t, 10, agent.Search.Web.MaxResults) // Verify KB config assert.NotNil(t, agent.Search.KB) assert.Equal(t, 0.7, agent.Search.KB.Threshold) assert.False(t, agent.Search.KB.Graph) // Verify DB config assert.NotNil(t, agent.Search.DB) assert.Equal(t, 20, agent.Search.DB.MaxResults) // Verify keyword config assert.NotNil(t, agent.Search.Keyword) assert.Equal(t, 10, agent.Search.Keyword.MaxKeywords) assert.Equal(t, "auto", agent.Search.Keyword.Language) // Verify rerank config assert.NotNil(t, agent.Search.Rerank) assert.Equal(t, 10, agent.Search.Rerank.TopN) // Verify citation config assert.NotNil(t, agent.Search.Citation) assert.Equal(t, "#ref:{id}", agent.Search.Citation.Format) assert.True(t, agent.Search.Citation.AutoInjectPrompt) // Verify weights config assert.NotNil(t, agent.Search.Weights) assert.Equal(t, 1.0, agent.Search.Weights.User) assert.Equal(t, 0.8, agent.Search.Weights.Hook) assert.Equal(t, 0.6, agent.Search.Weights.Auto) // Verify options config assert.NotNil(t, agent.Search.Options) assert.Equal(t, 5, agent.Search.Options.SkipThreshold) }) } func TestGetGlobalPrompts(t *testing.T) { prepare(t) defer test.Clean() t.Run("ParseWithoutContext", func(t *testing.T) { prompts := GetGlobalPrompts(nil) require.NotNil(t, prompts) require.Greater(t, len(prompts), 0) // $SYS.* variables should be replaced content := prompts[0].Content assert.NotContains(t, content, "$SYS.DATETIME") assert.NotContains(t, content, "$SYS.TIMEZONE") assert.NotContains(t, content, "$SYS.WEEKDAY") // Should contain actual time values now := time.Now() assert.Contains(t, content, now.Format("2006-01-02")) }) t.Run("ParseWithContext", func(t *testing.T) { ctx := map[string]string{ "USER_ID": "test-user-123", "LOCALE": "zh-CN", } prompts := GetGlobalPrompts(ctx) require.NotNil(t, prompts) require.Greater(t, len(prompts), 0) // $SYS.* variables should be replaced content := prompts[0].Content assert.NotContains(t, content, "$SYS.DATETIME") }) t.Run("ParseSystemTimeVariables", func(t *testing.T) { prompts := GetGlobalPrompts(nil) require.NotNil(t, prompts) content := prompts[0].Content now := time.Now() // Should contain current date assert.Contains(t, content, now.Format("2006-01-02")) // Should contain timezone assert.Contains(t, content, now.Location().String()) // Should contain weekday assert.Contains(t, content, now.Weekday().String()) }) } func TestGetGlobalPromptsWithDisableFlag(t *testing.T) { prepare(t) defer test.Clean() agent := GetAgent() require.NotNil(t, agent) t.Run("GlobalPromptsExist", func(t *testing.T) { // Verify global prompts are loaded assert.NotNil(t, agent.GlobalPrompts) assert.Greater(t, len(agent.GlobalPrompts), 0) }) t.Run("AssistantCanDisableGlobalPrompts", func(t *testing.T) { // The fullfields test assistant has disable_global_prompts: true // This test verifies the flag is properly loaded // The actual merging logic is in the assistant module prompts := GetGlobalPrompts(nil) assert.NotNil(t, prompts) // Global prompts should still be available // The assistant decides whether to use them based on DisableGlobalPrompts flag }) } func TestResolveEnvStrings(t *testing.T) { t.Setenv("TEST_CONNECTOR", "openai.gpt-5") t.Setenv("TEST_ASSISTANT", "my-assistant") t.Setenv("TEST_CACHE", "my-cache") t.Run("SystemFields", func(t *testing.T) { setting := &types.DSL{ System: &types.System{ Default: "$ENV.TEST_CONNECTOR", Keyword: "$ENV.TEST_CONNECTOR", QueryDSL: "$ENV.TEST_CONNECTOR", Title: "$ENV.TEST_CONNECTOR", Prompt: "$ENV.TEST_CONNECTOR", RobotPrompt: "$ENV.TEST_CONNECTOR", NeedSearch: "$ENV.TEST_CONNECTOR", Entity: "$ENV.TEST_CONNECTOR", Vision: "$ENV.TEST_CONNECTOR", Voice: "$ENV.TEST_CONNECTOR", }, } resolveEnvStrings(setting) assert.Equal(t, "openai.gpt-5", setting.System.Default) assert.Equal(t, "openai.gpt-5", setting.System.Keyword) assert.Equal(t, "openai.gpt-5", setting.System.QueryDSL) assert.Equal(t, "openai.gpt-5", setting.System.Title) assert.Equal(t, "openai.gpt-5", setting.System.Prompt) assert.Equal(t, "openai.gpt-5", setting.System.RobotPrompt) assert.Equal(t, "openai.gpt-5", setting.System.NeedSearch) assert.Equal(t, "openai.gpt-5", setting.System.Entity) assert.Equal(t, "openai.gpt-5", setting.System.Vision) assert.Equal(t, "openai.gpt-5", setting.System.Voice) }) t.Run("SystemVisionVoiceSeparateEnv", func(t *testing.T) { t.Setenv("TEST_VISION_CONN", "openai.gpt-4o") t.Setenv("TEST_VOICE_CONN", "whisper-1") setting := &types.DSL{ System: &types.System{ Default: "$ENV.TEST_CONNECTOR", Vision: "$ENV.TEST_VISION_CONN", Voice: "$ENV.TEST_VOICE_CONN", }, } resolveEnvStrings(setting) assert.Equal(t, "openai.gpt-5", setting.System.Default) assert.Equal(t, "openai.gpt-4o", setting.System.Vision) assert.Equal(t, "whisper-1", setting.System.Voice) }) t.Run("UsesFields", func(t *testing.T) { setting := &types.DSL{ Uses: &types.Uses{ Default: "$ENV.TEST_ASSISTANT", Title: "$ENV.TEST_ASSISTANT", Prompt: "$ENV.TEST_ASSISTANT", RobotPrompt: "$ENV.TEST_ASSISTANT", Vision: "$ENV.TEST_ASSISTANT", Audio: "$ENV.TEST_ASSISTANT", Search: "$ENV.TEST_ASSISTANT", Fetch: "$ENV.TEST_ASSISTANT", Web: "$ENV.TEST_ASSISTANT", Keyword: "$ENV.TEST_ASSISTANT", QueryDSL: "$ENV.TEST_ASSISTANT", Rerank: "$ENV.TEST_ASSISTANT", }, } resolveEnvStrings(setting) assert.Equal(t, "my-assistant", setting.Uses.Default) assert.Equal(t, "my-assistant", setting.Uses.Title) assert.Equal(t, "my-assistant", setting.Uses.Prompt) assert.Equal(t, "my-assistant", setting.Uses.RobotPrompt) assert.Equal(t, "my-assistant", setting.Uses.Vision) assert.Equal(t, "my-assistant", setting.Uses.Audio) assert.Equal(t, "my-assistant", setting.Uses.Search) assert.Equal(t, "my-assistant", setting.Uses.Fetch) assert.Equal(t, "my-assistant", setting.Uses.Web) assert.Equal(t, "my-assistant", setting.Uses.Keyword) assert.Equal(t, "my-assistant", setting.Uses.QueryDSL) assert.Equal(t, "my-assistant", setting.Uses.Rerank) }) t.Run("CacheField", func(t *testing.T) { setting := &types.DSL{Cache: "$ENV.TEST_CACHE"} resolveEnvStrings(setting) assert.Equal(t, "my-cache", setting.Cache) }) t.Run("PlainStringsUnchanged", func(t *testing.T) { setting := &types.DSL{ Cache: "plain-cache", System: &types.System{ Default: "openai.gpt-5", }, Uses: &types.Uses{ Default: "mohe", Title: "__yao.title", }, } resolveEnvStrings(setting) assert.Equal(t, "plain-cache", setting.Cache) assert.Equal(t, "openai.gpt-5", setting.System.Default) assert.Equal(t, "mohe", setting.Uses.Default) assert.Equal(t, "__yao.title", setting.Uses.Title) }) t.Run("NilSystemAndUses", func(t *testing.T) { setting := &types.DSL{Cache: "test"} assert.NotPanics(t, func() { resolveEnvStrings(setting) }) }) t.Run("UndefinedEnvReturnsEmpty", func(t *testing.T) { setting := &types.DSL{ System: &types.System{ Default: "$ENV.UNDEFINED_VAR_12345", }, } resolveEnvStrings(setting) assert.Equal(t, "", setting.System.Default) }) } func TestGlobalPromptsContent(t *testing.T) { prepare(t) defer test.Clean() agent := GetAgent() require.NotNil(t, agent) require.NotNil(t, agent.GlobalPrompts) require.Greater(t, len(agent.GlobalPrompts), 0) t.Run("SystemContextPrompt", func(t *testing.T) { // Find system prompt var systemPrompt string for _, p := range agent.GlobalPrompts { if p.Role == "system" { systemPrompt = p.Content break } } assert.NotEmpty(t, systemPrompt) assert.Contains(t, systemPrompt, "System Context") }) t.Run("VariablesInRawPrompts", func(t *testing.T) { // Raw prompts should contain unparsed variables content := agent.GlobalPrompts[0].Content assert.True(t, strings.Contains(content, "$SYS.") || strings.Contains(content, "$ENV.") || strings.Contains(content, "$CTX."), "Raw prompts should contain variable placeholders") }) } func TestAssistantGlobalPrompts(t *testing.T) { prepare(t) defer test.Clean() t.Run("AssistantModuleReceivesGlobalPrompts", func(t *testing.T) { // Verify assistant module has global prompts prompts := assistant.GetGlobalPrompts(nil) require.NotNil(t, prompts) require.Greater(t, len(prompts), 0) // Should be parsed (no $SYS.* variables) content := prompts[0].Content assert.NotContains(t, content, "$SYS.DATETIME") }) t.Run("AssistantModuleParsesWithContext", func(t *testing.T) { ctx := map[string]string{ "USER_ID": "assistant-test-user", "LOCALE": "en-US", } prompts := assistant.GetGlobalPrompts(ctx) require.NotNil(t, prompts) // $SYS.* should be replaced content := prompts[0].Content assert.NotContains(t, content, "$SYS.") // Should contain current time info now := time.Now() assert.Contains(t, content, now.Format("2006-01-02")) }) }