yao/agent/assistant/load_store_test.go
Max e8720f0421 Add dependencies management to Assistant model and related functionalities
- Introduce a new field for dependencies in the Assistant model to manage external MCP client dependencies with version constraints.
- Implement deep copy functionality for dependencies in the Clone method to ensure integrity during assistant cloning.
- Enhance the Update method to handle dependencies input from data maps, supporting both string and interface types.
- Update the Map method to include dependencies in the serialized output.
- Add comprehensive tests to validate loading, cloning, and mapping of dependencies, ensuring correct behavior across various scenarios.
2026-03-02 14:42:59 +08:00

1189 lines
32 KiB
Go

package assistant_test
import (
stdContext "context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/yaoapp/yao/agent/assistant"
"github.com/yaoapp/yao/agent/context"
searchTypes "github.com/yaoapp/yao/agent/search/types"
store "github.com/yaoapp/yao/agent/store/types"
"github.com/yaoapp/yao/agent/testutils"
"github.com/yaoapp/yao/openapi/oauth/types"
)
// TestLoadStoreWithSource tests loading assistant from database with Source field
func TestLoadStoreWithSource(t *testing.T) {
testutils.Prepare(t)
defer testutils.Clean(t)
// Create assistant with Source
assistantID := "test.store-with-source"
now := time.Now().UnixNano()
ast := &assistant.Assistant{
AssistantModel: store.AssistantModel{
ID: assistantID,
Name: "Test Assistant With Source",
Type: "assistant",
Connector: "gpt-4o",
Description: "Test assistant loaded from store with source code",
Prompts: []store.Prompt{
{Role: "system", Content: "You are a helpful assistant."},
},
Options: map[string]interface{}{
"temperature": 0.7,
},
Tags: []string{"Test", "Source"},
// Simple Create hook that returns null
Source: `
// @ts-nocheck
function Create(ctx, messages) {
return null;
}
`,
CreatedAt: now,
UpdatedAt: now,
},
}
// Save to database
err := ast.Save()
require.NoError(t, err)
// Cleanup after test
defer func() {
storage := assistant.GetStorage()
if storage != nil {
storage.DeleteAssistant(assistantID)
}
assistant.GetCache().Clear()
}()
// Clear cache to ensure fresh load from database
assistant.GetCache().Clear()
// Load from store
loaded, err := assistant.Get(assistantID)
require.NoError(t, err)
require.NotNil(t, loaded)
// Verify basic fields
assert.Equal(t, assistantID, loaded.ID)
assert.Equal(t, "Test Assistant With Source", loaded.Name)
assert.Equal(t, "assistant", loaded.Type)
assert.Equal(t, "Test assistant loaded from store with source code", loaded.Description)
// Verify prompts
require.NotNil(t, loaded.Prompts)
assert.Len(t, loaded.Prompts, 1)
assert.Equal(t, "system", loaded.Prompts[0].Role)
assert.Equal(t, "You are a helpful assistant.", loaded.Prompts[0].Content)
// Verify options
assert.NotNil(t, loaded.Options)
assert.Equal(t, 0.7, loaded.Options["temperature"])
// Verify tags
assert.NotNil(t, loaded.Tags)
assert.Contains(t, loaded.Tags, "Test")
assert.Contains(t, loaded.Tags, "Source")
// Verify script was compiled from source
assert.NotNil(t, loaded.HookScript, "HookScript should be compiled from Source field")
// Verify source is stored
assert.NotEmpty(t, loaded.Source)
}
// TestLoadStoreWithoutSource tests loading assistant from database without Source field
func TestLoadStoreWithoutSource(t *testing.T) {
testutils.Prepare(t)
defer testutils.Clean(t)
// Create assistant without Source
assistantID := "test.store-without-source"
now := time.Now().UnixNano()
ast := &assistant.Assistant{
AssistantModel: store.AssistantModel{
ID: assistantID,
Name: "Test Assistant Without Source",
Type: "assistant",
Connector: "gpt-4o",
Description: "Test assistant loaded from store without source code",
Prompts: []store.Prompt{
{Role: "system", Content: "You are a helpful assistant without hooks."},
},
Options: map[string]interface{}{
"temperature": 0.5,
"max_tokens": 1000,
},
Tags: []string{"Test", "NoSource"},
CreatedAt: now,
UpdatedAt: now,
},
}
// Save to database
err := ast.Save()
require.NoError(t, err)
// Cleanup after test
defer func() {
storage := assistant.GetStorage()
if storage != nil {
storage.DeleteAssistant(assistantID)
}
assistant.GetCache().Clear()
}()
// Clear cache to ensure fresh load from database
assistant.GetCache().Clear()
// Load from store
loaded, err := assistant.Get(assistantID)
require.NoError(t, err)
require.NotNil(t, loaded)
// Verify basic fields
assert.Equal(t, assistantID, loaded.ID)
assert.Equal(t, "Test Assistant Without Source", loaded.Name)
assert.Equal(t, "assistant", loaded.Type)
assert.Equal(t, "Test assistant loaded from store without source code", loaded.Description)
// Verify prompts
require.NotNil(t, loaded.Prompts)
assert.Len(t, loaded.Prompts, 1)
assert.Equal(t, "system", loaded.Prompts[0].Role)
// Verify options
assert.NotNil(t, loaded.Options)
assert.Equal(t, 0.5, loaded.Options["temperature"])
assert.Equal(t, float64(1000), loaded.Options["max_tokens"])
// Verify tags
assert.NotNil(t, loaded.Tags)
assert.Contains(t, loaded.Tags, "Test")
assert.Contains(t, loaded.Tags, "NoSource")
// Verify script is nil (no source)
assert.Nil(t, loaded.HookScript, "HookScript should be nil when no Source field")
assert.Empty(t, loaded.Source)
}
// newStoreTestContext creates a Context for testing with commonly used fields pre-populated.
func newStoreTestContext(chatID, assistantID string) *context.Context {
authorized := &types.AuthorizedInfo{
Subject: "test-user",
ClientID: "test-client-id",
Scope: "openid profile email",
SessionID: "test-session-id",
UserID: "test-user-123",
TeamID: "test-team-456",
TenantID: "test-tenant-789",
}
ctx := context.New(stdContext.Background(), authorized, chatID)
ctx.AssistantID = assistantID
ctx.Locale = "en-us"
ctx.Theme = "light"
ctx.Client = context.Client{
Type: "web",
UserAgent: "TestAgent/1.0",
IP: "127.0.0.1",
}
ctx.Referer = context.RefererAPI
ctx.Accept = context.AcceptWebCUI
ctx.Route = ""
ctx.Metadata = make(map[string]interface{})
return ctx
}
// TestLoadStoreWithSourceExecuteHook tests that Source-based script is properly compiled and can execute
func TestLoadStoreWithSourceExecuteHook(t *testing.T) {
testutils.Prepare(t)
defer testutils.Clean(t)
// Create assistant with a working Create hook
assistantID := "test.store-source-hook"
now := time.Now().UnixNano()
ast := &assistant.Assistant{
AssistantModel: store.AssistantModel{
ID: assistantID,
Name: "Test Source Hook",
Type: "assistant",
Connector: "gpt-4o",
Prompts: []store.Prompt{
{Role: "system", Content: "Default prompt"},
},
// Create hook that modifies temperature and adds metadata
Source: `
// @ts-nocheck
function Create(ctx: any, messages: any[]): any {
return {
temperature: 0.9,
metadata: {
hook_executed: true,
chat_id: ctx.chat_id
}
};
}
`,
CreatedAt: now,
UpdatedAt: now,
},
}
// Save to database
err := ast.Save()
require.NoError(t, err)
// Cleanup after test
defer func() {
storage := assistant.GetStorage()
if storage != nil {
storage.DeleteAssistant(assistantID)
}
assistant.GetCache().Clear()
}()
// Clear cache
assistant.GetCache().Clear()
// Load from store
loaded, err := assistant.Get(assistantID)
require.NoError(t, err)
require.NotNil(t, loaded)
require.NotNil(t, loaded.HookScript, "HookScript should be compiled from Source")
// Verify the script object exists and is usable
assert.NotNil(t, loaded.HookScript.Script)
// Execute the Create hook
ctx := newStoreTestContext("test-chat-id", assistantID)
messages := []context.Message{{Role: "user", Content: "Hello"}}
res, _, err := loaded.HookScript.Create(ctx, messages, &context.Options{})
require.NoError(t, err, "Create hook should execute without error")
require.NotNil(t, res, "Create hook should return a response")
// Verify temperature was set
require.NotNil(t, res.Temperature, "Temperature should be set")
assert.Equal(t, 0.9, *res.Temperature, "Temperature should be 0.9")
// Verify metadata was set
require.NotNil(t, res.Metadata, "Metadata should be set")
assert.Equal(t, true, res.Metadata["hook_executed"], "hook_executed should be true")
assert.Equal(t, "test-chat-id", res.Metadata["chat_id"], "chat_id should match context")
}
// TestLoadStoreWithPromptPresets tests loading assistant with prompt presets from database
func TestLoadStoreWithPromptPresets(t *testing.T) {
testutils.Prepare(t)
defer testutils.Clean(t)
assistantID := "test.store-with-presets"
now := time.Now().UnixNano()
ast := &assistant.Assistant{
AssistantModel: store.AssistantModel{
ID: assistantID,
Name: "Test With Presets",
Type: "assistant",
Connector: "gpt-4o",
Prompts: []store.Prompt{
{Role: "system", Content: "Default prompt"},
},
PromptPresets: map[string][]store.Prompt{
"friendly": {
{Role: "system", Content: "You are a friendly assistant."},
},
"professional": {
{Role: "system", Content: "You are a professional assistant."},
},
"mode.casual": {
{Role: "system", Content: "You are a casual assistant."},
},
},
CreatedAt: now,
UpdatedAt: now,
},
}
err := ast.Save()
require.NoError(t, err)
defer func() {
storage := assistant.GetStorage()
if storage != nil {
storage.DeleteAssistant(assistantID)
}
assistant.GetCache().Clear()
}()
assistant.GetCache().Clear()
loaded, err := assistant.Get(assistantID)
require.NoError(t, err)
require.NotNil(t, loaded)
// Verify prompt presets
require.NotNil(t, loaded.PromptPresets)
assert.Len(t, loaded.PromptPresets, 3)
friendlyPreset, ok := loaded.PromptPresets["friendly"]
assert.True(t, ok)
assert.Len(t, friendlyPreset, 1)
assert.Equal(t, "You are a friendly assistant.", friendlyPreset[0].Content)
professionalPreset, ok := loaded.PromptPresets["professional"]
assert.True(t, ok)
assert.Len(t, professionalPreset, 1)
assert.Equal(t, "You are a professional assistant.", professionalPreset[0].Content)
casualPreset, ok := loaded.PromptPresets["mode.casual"]
assert.True(t, ok)
assert.Len(t, casualPreset, 1)
assert.Equal(t, "You are a casual assistant.", casualPreset[0].Content)
}
// TestLoadStoreWithDisableGlobalPrompts tests loading assistant with disable_global_prompts flag
func TestLoadStoreWithDisableGlobalPrompts(t *testing.T) {
testutils.Prepare(t)
defer testutils.Clean(t)
assistantID := "test.store-disable-global"
now := time.Now().UnixNano()
ast := &assistant.Assistant{
AssistantModel: store.AssistantModel{
ID: assistantID,
Name: "Test Disable Global Prompts",
Type: "assistant",
Connector: "gpt-4o",
DisableGlobalPrompts: true,
Prompts: []store.Prompt{
{Role: "system", Content: "Only this prompt should be used."},
},
CreatedAt: now,
UpdatedAt: now,
},
}
err := ast.Save()
require.NoError(t, err)
defer func() {
storage := assistant.GetStorage()
if storage != nil {
storage.DeleteAssistant(assistantID)
}
assistant.GetCache().Clear()
}()
assistant.GetCache().Clear()
loaded, err := assistant.Get(assistantID)
require.NoError(t, err)
require.NotNil(t, loaded)
assert.True(t, loaded.DisableGlobalPrompts)
}
// TestLoadStoreCaching tests that loaded assistants are cached
func TestLoadStoreCaching(t *testing.T) {
testutils.Prepare(t)
defer testutils.Clean(t)
assistantID := "test.store-caching"
now := time.Now().UnixNano()
ast := &assistant.Assistant{
AssistantModel: store.AssistantModel{
ID: assistantID,
Name: "Test Caching",
Type: "assistant",
Connector: "gpt-4o",
CreatedAt: now,
UpdatedAt: now,
},
}
err := ast.Save()
require.NoError(t, err)
defer func() {
storage := assistant.GetStorage()
if storage != nil {
storage.DeleteAssistant(assistantID)
}
assistant.GetCache().Clear()
}()
assistant.GetCache().Clear()
// First load
ast1, err := assistant.Get(assistantID)
require.NoError(t, err)
require.NotNil(t, ast1)
// Second load - should be from cache
ast2, err := assistant.Get(assistantID)
require.NoError(t, err)
require.NotNil(t, ast2)
// Should be the same instance (from cache)
assert.Same(t, ast1, ast2)
}
// TestLoadStoreNotFound tests loading non-existent assistant
func TestLoadStoreNotFound(t *testing.T) {
testutils.Prepare(t)
defer testutils.Clean(t)
assistant.GetCache().Clear()
_, err := assistant.Get("non-existent-assistant-id-12345")
assert.Error(t, err)
}
// TestLoadStoreWithAllFields tests loading assistant with comprehensive fields
func TestLoadStoreWithAllFields(t *testing.T) {
testutils.Prepare(t)
defer testutils.Clean(t)
assistantID := "test.store-all-fields"
now := time.Now().UnixNano()
ast := &assistant.Assistant{
AssistantModel: store.AssistantModel{
ID: assistantID,
Name: "Test All Fields",
Type: "assistant",
Avatar: "/api/icons/test.png",
Connector: "gpt-4o",
Description: "Test assistant with all fields",
Tags: []string{"Test", "AllFields", "Complete"},
Readonly: true,
Public: true,
Share: "team",
Mentionable: true,
Automated: false,
Sort: 100,
Options: map[string]interface{}{
"temperature": 0.8,
"max_tokens": 2000,
},
Prompts: []store.Prompt{
{Role: "system", Content: "You are a test assistant."},
{Role: "system", Content: "Follow all instructions carefully."},
},
PromptPresets: map[string][]store.Prompt{
"default": {
{Role: "system", Content: "Default mode prompt."},
},
},
DisableGlobalPrompts: true,
Placeholder: &store.Placeholder{
Title: "Test Placeholder",
Description: "This is a test placeholder",
Prompts: []string{"Test prompt 1", "Test prompt 2"},
},
Dependencies: map[string]string{
"echo": "^1.0.0",
"customer": ">=2.0.0",
},
Source: `
// @ts-nocheck
function Create(ctx: any, messages: any[]): any {
return {
temperature: 0.5,
metadata: {
assistant_name: "Test All Fields",
executed: true
}
};
}
`,
CreatedAt: now,
UpdatedAt: now,
},
}
err := ast.Save()
require.NoError(t, err)
defer func() {
storage := assistant.GetStorage()
if storage != nil {
storage.DeleteAssistant(assistantID)
}
assistant.GetCache().Clear()
}()
assistant.GetCache().Clear()
loaded, err := assistant.Get(assistantID)
require.NoError(t, err)
require.NotNil(t, loaded)
// Verify all fields
assert.Equal(t, assistantID, loaded.ID)
assert.Equal(t, "Test All Fields", loaded.Name)
assert.Equal(t, "assistant", loaded.Type)
assert.Equal(t, "/api/icons/test.png", loaded.Avatar)
assert.Equal(t, "Test assistant with all fields", loaded.Description)
// Boolean fields
assert.True(t, loaded.Readonly)
assert.True(t, loaded.Public)
assert.Equal(t, "team", loaded.Share)
assert.True(t, loaded.Mentionable)
assert.False(t, loaded.Automated)
assert.True(t, loaded.DisableGlobalPrompts)
assert.Equal(t, 100, loaded.Sort)
// Tags
assert.Len(t, loaded.Tags, 3)
assert.Contains(t, loaded.Tags, "Test")
assert.Contains(t, loaded.Tags, "AllFields")
assert.Contains(t, loaded.Tags, "Complete")
// Options
assert.Equal(t, 0.8, loaded.Options["temperature"])
assert.Equal(t, float64(2000), loaded.Options["max_tokens"])
// Prompts
assert.Len(t, loaded.Prompts, 2)
// Prompt presets
assert.NotNil(t, loaded.PromptPresets)
assert.Contains(t, loaded.PromptPresets, "default")
// Placeholder
assert.NotNil(t, loaded.Placeholder)
assert.Equal(t, "Test Placeholder", loaded.Placeholder.Title)
assert.Equal(t, "This is a test placeholder", loaded.Placeholder.Description)
assert.Len(t, loaded.Placeholder.Prompts, 2)
// Script from source
assert.NotNil(t, loaded.HookScript)
assert.NotEmpty(t, loaded.Source)
// Dependencies
require.NotNil(t, loaded.Dependencies)
assert.Len(t, loaded.Dependencies, 2)
assert.Equal(t, "^1.0.0", loaded.Dependencies["echo"])
assert.Equal(t, ">=2.0.0", loaded.Dependencies["customer"])
// Execute the Create hook to verify it works
ctx := newStoreTestContext("test-chat-all-fields", assistantID)
messages := []context.Message{{Role: "user", Content: "Test message"}}
res, _, err := loaded.HookScript.Create(ctx, messages, &context.Options{})
require.NoError(t, err, "Create hook should execute without error")
require.NotNil(t, res, "Create hook should return a response")
// Verify hook returned expected values
require.NotNil(t, res.Temperature, "Temperature should be set")
assert.Equal(t, 0.5, *res.Temperature, "Temperature should be 0.5")
require.NotNil(t, res.Metadata, "Metadata should be set")
assert.Equal(t, "Test All Fields", res.Metadata["assistant_name"], "assistant_name should match")
assert.Equal(t, true, res.Metadata["executed"], "executed should be true")
}
// TestLoadStoreHookWithTypeScript tests that TypeScript features work in Source field
func TestLoadStoreHookWithTypeScript(t *testing.T) {
testutils.Prepare(t)
defer testutils.Clean(t)
assistantID := "test.store-typescript-hook"
now := time.Now().UnixNano()
ast := &assistant.Assistant{
AssistantModel: store.AssistantModel{
ID: assistantID,
Name: "Test TypeScript Hook",
Type: "assistant",
Connector: "gpt-4o",
Prompts: []store.Prompt{
{Role: "system", Content: "Default prompt"},
},
// TypeScript code with type annotations and interfaces
Source: `
// TypeScript interfaces
interface CreateContext {
chat_id: string;
assistant_id: string;
locale: string;
authorized?: {
user_id: string;
team_id: string;
};
}
interface Message {
role: string;
content: string | object;
}
interface CreateResponse {
temperature?: number;
messages?: Message[];
metadata?: Record<string, any>;
}
// Create hook with full TypeScript syntax
function Create(ctx: CreateContext, messages: Message[]): CreateResponse | null {
// Type-safe access to context
const chatId: string = ctx.chat_id || "unknown";
const locale: string = ctx.locale || "en-us";
const userId: string = ctx.authorized?.user_id || "anonymous";
// Process messages
const userMessages: Message[] = messages.filter((m: Message) => m.role === "user");
const messageCount: number = userMessages.length;
// Return typed response
return {
temperature: 0.7,
messages: [
{
role: "system",
content: "TypeScript hook executed successfully"
}
],
metadata: {
chat_id: chatId,
locale: locale,
user_id: userId,
message_count: messageCount,
typescript_features: true
}
};
}
`,
CreatedAt: now,
UpdatedAt: now,
},
}
err := ast.Save()
require.NoError(t, err)
defer func() {
storage := assistant.GetStorage()
if storage != nil {
storage.DeleteAssistant(assistantID)
}
assistant.GetCache().Clear()
}()
assistant.GetCache().Clear()
loaded, err := assistant.Get(assistantID)
require.NoError(t, err)
require.NotNil(t, loaded)
require.NotNil(t, loaded.HookScript, "HookScript should be compiled from TypeScript Source")
// Execute the Create hook
ctx := newStoreTestContext("ts-test-chat", assistantID)
messages := []context.Message{
{Role: "user", Content: "Hello"},
{Role: "assistant", Content: "Hi there"},
{Role: "user", Content: "How are you?"},
}
res, _, err := loaded.HookScript.Create(ctx, messages, &context.Options{})
require.NoError(t, err, "TypeScript Create hook should execute without error")
require.NotNil(t, res, "Create hook should return a response")
// Verify temperature
require.NotNil(t, res.Temperature)
assert.Equal(t, 0.7, *res.Temperature)
// Verify messages
require.Len(t, res.Messages, 1)
assert.Equal(t, context.RoleSystem, res.Messages[0].Role)
assert.Equal(t, "TypeScript hook executed successfully", res.Messages[0].Content)
// Verify metadata
require.NotNil(t, res.Metadata)
assert.Equal(t, "ts-test-chat", res.Metadata["chat_id"])
assert.Equal(t, "en-us", res.Metadata["locale"])
assert.Equal(t, "test-user-123", res.Metadata["user_id"])
assert.Equal(t, float64(2), res.Metadata["message_count"]) // 2 user messages
assert.Equal(t, true, res.Metadata["typescript_features"])
}
// TestLoadStoreHookReturnNull tests that hook returning null works correctly
func TestLoadStoreHookReturnNull(t *testing.T) {
testutils.Prepare(t)
defer testutils.Clean(t)
assistantID := "test.store-hook-null"
now := time.Now().UnixNano()
ast := &assistant.Assistant{
AssistantModel: store.AssistantModel{
ID: assistantID,
Name: "Test Hook Return Null",
Type: "assistant",
Connector: "gpt-4o",
Source: `
function Create(ctx: any, messages: any[]): any {
// Return null to indicate no modifications
return null;
}
`,
CreatedAt: now,
UpdatedAt: now,
},
}
err := ast.Save()
require.NoError(t, err)
defer func() {
storage := assistant.GetStorage()
if storage != nil {
storage.DeleteAssistant(assistantID)
}
assistant.GetCache().Clear()
}()
assistant.GetCache().Clear()
loaded, err := assistant.Get(assistantID)
require.NoError(t, err)
require.NotNil(t, loaded)
require.NotNil(t, loaded.HookScript)
ctx := newStoreTestContext("null-test-chat", assistantID)
messages := []context.Message{{Role: "user", Content: "Hello"}}
res, _, err := loaded.HookScript.Create(ctx, messages, &context.Options{})
require.NoError(t, err, "Hook returning null should not error")
assert.Nil(t, res, "Hook returning null should return nil response")
}
// TestLoadStoreHookWithPromptPreset tests that hook can return prompt_preset
func TestLoadStoreHookWithPromptPreset(t *testing.T) {
testutils.Prepare(t)
defer testutils.Clean(t)
assistantID := "test.store-hook-preset"
now := time.Now().UnixNano()
ast := &assistant.Assistant{
AssistantModel: store.AssistantModel{
ID: assistantID,
Name: "Test Hook Prompt Preset",
Type: "assistant",
Connector: "gpt-4o",
Prompts: []store.Prompt{
{Role: "system", Content: "Default prompt"},
},
PromptPresets: map[string][]store.Prompt{
"friendly": {
{Role: "system", Content: "You are a friendly assistant."},
},
"professional": {
{Role: "system", Content: "You are a professional assistant."},
},
},
Source: `
function Create(ctx: any, messages: any[]): any {
// Check first message to determine preset
const firstMsg = messages[0];
if (firstMsg && typeof firstMsg.content === "string") {
if (firstMsg.content.includes("friendly")) {
return { prompt_preset: "friendly" };
}
if (firstMsg.content.includes("professional")) {
return { prompt_preset: "professional" };
}
}
return null;
}
`,
CreatedAt: now,
UpdatedAt: now,
},
}
err := ast.Save()
require.NoError(t, err)
defer func() {
storage := assistant.GetStorage()
if storage != nil {
storage.DeleteAssistant(assistantID)
}
assistant.GetCache().Clear()
}()
assistant.GetCache().Clear()
loaded, err := assistant.Get(assistantID)
require.NoError(t, err)
require.NotNil(t, loaded)
require.NotNil(t, loaded.HookScript)
// Test friendly preset selection
t.Run("SelectFriendlyPreset", func(t *testing.T) {
ctx := newStoreTestContext("preset-test-1", assistantID)
messages := []context.Message{{Role: "user", Content: "Be friendly please"}}
res, _, err := loaded.HookScript.Create(ctx, messages, &context.Options{})
require.NoError(t, err)
require.NotNil(t, res)
assert.Equal(t, "friendly", res.PromptPreset)
})
// Test professional preset selection
t.Run("SelectProfessionalPreset", func(t *testing.T) {
ctx := newStoreTestContext("preset-test-2", assistantID)
messages := []context.Message{{Role: "user", Content: "Be professional"}}
res, _, err := loaded.HookScript.Create(ctx, messages, &context.Options{})
require.NoError(t, err)
require.NotNil(t, res)
assert.Equal(t, "professional", res.PromptPreset)
})
// Test no preset (returns null)
t.Run("NoPreset", func(t *testing.T) {
ctx := newStoreTestContext("preset-test-3", assistantID)
messages := []context.Message{{Role: "user", Content: "Hello"}}
res, _, err := loaded.HookScript.Create(ctx, messages, &context.Options{})
require.NoError(t, err)
assert.Nil(t, res)
})
}
// TestLoadStoreHookDisableGlobalPrompts tests that hook can disable global prompts
func TestLoadStoreHookDisableGlobalPrompts(t *testing.T) {
testutils.Prepare(t)
defer testutils.Clean(t)
assistantID := "test.store-hook-disable-global"
now := time.Now().UnixNano()
ast := &assistant.Assistant{
AssistantModel: store.AssistantModel{
ID: assistantID,
Name: "Test Hook Disable Global",
Type: "assistant",
Connector: "gpt-4o",
Source: `
function Create(ctx: any, messages: any[]): any {
const firstMsg = messages[0];
if (firstMsg && typeof firstMsg.content === "string") {
if (firstMsg.content.includes("disable_global")) {
return { disable_global_prompts: true };
}
if (firstMsg.content.includes("enable_global")) {
return { disable_global_prompts: false };
}
}
return null;
}
`,
CreatedAt: now,
UpdatedAt: now,
},
}
err := ast.Save()
require.NoError(t, err)
defer func() {
storage := assistant.GetStorage()
if storage != nil {
storage.DeleteAssistant(assistantID)
}
assistant.GetCache().Clear()
}()
assistant.GetCache().Clear()
loaded, err := assistant.Get(assistantID)
require.NoError(t, err)
require.NotNil(t, loaded)
require.NotNil(t, loaded.HookScript)
// Test disable global prompts
t.Run("DisableGlobalPrompts", func(t *testing.T) {
ctx := newStoreTestContext("disable-test-1", assistantID)
messages := []context.Message{{Role: "user", Content: "disable_global prompts"}}
res, _, err := loaded.HookScript.Create(ctx, messages, &context.Options{})
require.NoError(t, err)
require.NotNil(t, res)
require.NotNil(t, res.DisableGlobalPrompts)
assert.True(t, *res.DisableGlobalPrompts)
})
// Test enable global prompts
t.Run("EnableGlobalPrompts", func(t *testing.T) {
ctx := newStoreTestContext("disable-test-2", assistantID)
messages := []context.Message{{Role: "user", Content: "enable_global prompts"}}
res, _, err := loaded.HookScript.Create(ctx, messages, &context.Options{})
require.NoError(t, err)
require.NotNil(t, res)
require.NotNil(t, res.DisableGlobalPrompts)
assert.False(t, *res.DisableGlobalPrompts)
})
}
// TestLoadStoreWithSearchConfig tests loading assistant with search configuration from database
func TestLoadStoreWithSearchConfig(t *testing.T) {
testutils.Prepare(t)
defer testutils.Clean(t)
assistantID := "test.store-with-search"
now := time.Now().UnixNano()
ast := &assistant.Assistant{
AssistantModel: store.AssistantModel{
ID: assistantID,
Name: "Test With Search Config",
Type: "assistant",
Connector: "gpt-4o",
Uses: &context.Uses{
Vision: "agent",
Audio: "mcp:audio-server",
Fetch: "agent",
Web: "builtin",
Keyword: "builtin",
QueryDSL: "builtin",
Rerank: "builtin",
},
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,
},
Keyword: &searchTypes.KeywordConfig{
MaxKeywords: 8,
Language: "auto",
},
QueryDSL: &searchTypes.QueryDSLConfig{
Strict: true,
},
Rerank: &searchTypes.RerankConfig{
TopN: 5,
},
Citation: &searchTypes.CitationConfig{
Format: "#cite:{id}",
AutoInjectPrompt: false,
CustomPrompt: "Please cite sources.",
},
Weights: &searchTypes.WeightsConfig{
User: 1.0,
Hook: 0.85,
Auto: 0.65,
},
Options: &searchTypes.OptionsConfig{
SkipThreshold: 3,
},
},
CreatedAt: now,
UpdatedAt: now,
},
}
err := ast.Save()
require.NoError(t, err)
defer func() {
storage := assistant.GetStorage()
if storage != nil {
storage.DeleteAssistant(assistantID)
}
assistant.GetCache().Clear()
}()
assistant.GetCache().Clear()
loaded, err := assistant.Get(assistantID)
require.NoError(t, err)
require.NotNil(t, loaded)
// Verify Uses
require.NotNil(t, loaded.Uses)
assert.Equal(t, "agent", loaded.Uses.Vision)
assert.Equal(t, "mcp:audio-server", loaded.Uses.Audio)
assert.Equal(t, "agent", loaded.Uses.Fetch)
assert.Equal(t, "builtin", loaded.Uses.Web)
assert.Equal(t, "builtin", loaded.Uses.Keyword)
assert.Equal(t, "builtin", loaded.Uses.QueryDSL)
assert.Equal(t, "builtin", loaded.Uses.Rerank)
// Verify Search config
require.NotNil(t, loaded.Search)
// Web config
require.NotNil(t, loaded.Search.Web)
assert.Equal(t, "tavily", loaded.Search.Web.Provider)
assert.Equal(t, 15, loaded.Search.Web.MaxResults)
// KB config
require.NotNil(t, loaded.Search.KB)
assert.Equal(t, []string{"docs", "faq"}, loaded.Search.KB.Collections)
assert.Equal(t, 0.8, loaded.Search.KB.Threshold)
assert.True(t, loaded.Search.KB.Graph)
// DB config
require.NotNil(t, loaded.Search.DB)
assert.Equal(t, []string{"user", "product"}, loaded.Search.DB.Models)
assert.Equal(t, 50, loaded.Search.DB.MaxResults)
// Keyword config
require.NotNil(t, loaded.Search.Keyword)
assert.Equal(t, 8, loaded.Search.Keyword.MaxKeywords)
assert.Equal(t, "auto", loaded.Search.Keyword.Language)
// QueryDSL config
require.NotNil(t, loaded.Search.QueryDSL)
assert.True(t, loaded.Search.QueryDSL.Strict)
// Rerank config
require.NotNil(t, loaded.Search.Rerank)
assert.Equal(t, 5, loaded.Search.Rerank.TopN)
// Citation config
require.NotNil(t, loaded.Search.Citation)
assert.Equal(t, "#cite:{id}", loaded.Search.Citation.Format)
assert.False(t, loaded.Search.Citation.AutoInjectPrompt)
assert.Equal(t, "Please cite sources.", loaded.Search.Citation.CustomPrompt)
// Weights config
require.NotNil(t, loaded.Search.Weights)
assert.Equal(t, 1.0, loaded.Search.Weights.User)
assert.Equal(t, 0.85, loaded.Search.Weights.Hook)
assert.Equal(t, 0.65, loaded.Search.Weights.Auto)
// Options config
require.NotNil(t, loaded.Search.Options)
assert.Equal(t, 3, loaded.Search.Options.SkipThreshold)
}
// TestLoadStoreWithPartialSearchConfig tests loading assistant with partial search configuration
func TestLoadStoreWithPartialSearchConfig(t *testing.T) {
testutils.Prepare(t)
defer testutils.Clean(t)
assistantID := "test.store-partial-search"
now := time.Now().UnixNano()
ast := &assistant.Assistant{
AssistantModel: store.AssistantModel{
ID: assistantID,
Name: "Test Partial Search Config",
Type: "assistant",
Connector: "gpt-4o",
Search: &searchTypes.Config{
Web: &searchTypes.WebConfig{
Provider: "serper",
},
// Only web config, others are nil
},
CreatedAt: now,
UpdatedAt: now,
},
}
err := ast.Save()
require.NoError(t, err)
defer func() {
storage := assistant.GetStorage()
if storage != nil {
storage.DeleteAssistant(assistantID)
}
assistant.GetCache().Clear()
}()
assistant.GetCache().Clear()
loaded, err := assistant.Get(assistantID)
require.NoError(t, err)
require.NotNil(t, loaded)
// Verify Search config
require.NotNil(t, loaded.Search)
// Web config should be set
require.NotNil(t, loaded.Search.Web)
assert.Equal(t, "serper", loaded.Search.Web.Provider)
// Other configs should be nil
assert.Nil(t, loaded.Search.KB)
assert.Nil(t, loaded.Search.DB)
assert.Nil(t, loaded.Search.Keyword)
assert.Nil(t, loaded.Search.QueryDSL)
assert.Nil(t, loaded.Search.Rerank)
assert.Nil(t, loaded.Search.Citation)
assert.Nil(t, loaded.Search.Weights)
assert.Nil(t, loaded.Search.Options)
}
// TestLoadStoreWithoutSearchConfig tests loading assistant without search configuration
func TestLoadStoreWithoutSearchConfig(t *testing.T) {
testutils.Prepare(t)
defer testutils.Clean(t)
assistantID := "test.store-no-search"
now := time.Now().UnixNano()
ast := &assistant.Assistant{
AssistantModel: store.AssistantModel{
ID: assistantID,
Name: "Test No Search Config",
Type: "assistant",
Connector: "gpt-4o",
// No Search config
CreatedAt: now,
UpdatedAt: now,
},
}
err := ast.Save()
require.NoError(t, err)
defer func() {
storage := assistant.GetStorage()
if storage != nil {
storage.DeleteAssistant(assistantID)
}
assistant.GetCache().Clear()
}()
assistant.GetCache().Clear()
loaded, err := assistant.Get(assistantID)
require.NoError(t, err)
require.NotNil(t, loaded)
// Search config should be nil
assert.Nil(t, loaded.Search)
}