yao/agent/assistant/history_test.go
Max 4d7e8e8df6 Enhance history management in assistant with improved message filtering and conversion
- Introduce a new method to control the maximum number of history messages loaded, prioritizing user-defined options over store settings.
- Update the history loading logic to filter out non-semantic message types and convert tool call and action messages into historical summaries for better context.
- Refactor the message conversion functions to handle different formats and ensure clarity in the historical context provided to the LLM.
- Add comprehensive tests to validate the new behavior and ensure accurate message handling in various scenarios.
2026-03-02 12:00:20 +08:00

994 lines
29 KiB
Go

package assistant_test
import (
"context"
"fmt"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/yaoapp/yao/agent/assistant"
agentcontext "github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/agent/output/message"
storetypes "github.com/yaoapp/yao/agent/store/types"
"github.com/yaoapp/yao/agent/testutils"
oauthtypes "github.com/yaoapp/yao/openapi/oauth/types"
)
// =============================================================================
// Helper Functions
// =============================================================================
// newHistoryTestContext creates a test context for history tests
func newHistoryTestContext(chatID string) *agentcontext.Context {
authorized := &oauthtypes.AuthorizedInfo{
Subject: "test-user",
UserID: "history-test-user",
TeamID: "history-test-team",
TenantID: "history-test-tenant",
}
ctx := agentcontext.New(context.Background(), authorized, chatID)
ctx.AssistantID = "tests.history"
ctx.Locale = "en-us"
ctx.Client = agentcontext.Client{
Type: "web",
IP: "127.0.0.1",
}
ctx.Referer = agentcontext.RefererAPI
ctx.Accept = agentcontext.AcceptWebCUI
ctx.IDGenerator = message.NewIDGenerator()
ctx.Metadata = make(map[string]interface{})
return ctx
}
// =============================================================================
// WithHistory Tests
// =============================================================================
func TestWithHistory(t *testing.T) {
testutils.Prepare(t)
defer testutils.Clean(t)
// Get assistant
ast, err := assistant.Get("tests.history")
require.NoError(t, err)
require.NotNil(t, ast)
// Get chat store for setup/cleanup
chatStore := assistant.GetChatStore()
if chatStore == nil {
t.Skip("Chat store not configured, skipping history tests")
}
t.Run("NoHistory", func(t *testing.T) {
chatID := fmt.Sprintf("test_history_none_%s", uuid.New().String()[:8])
ctx := newHistoryTestContext(chatID)
// Create chat without any messages
err := chatStore.CreateChat(&storetypes.Chat{
ChatID: chatID,
AssistantID: ast.ID,
Status: "active",
Share: "private",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
require.NoError(t, err)
defer chatStore.DeleteChat(chatID)
input := []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: "Hello, this is my first message"},
}
result, err := ast.WithHistory(ctx, input, nil)
require.NoError(t, err)
require.NotNil(t, result)
// With no history, InputMessages and FullMessages should be the same as input
assert.Equal(t, input, result.InputMessages)
assert.Equal(t, input, result.FullMessages)
t.Log("✓ No history: input returned as is")
})
t.Run("WithExistingHistory", func(t *testing.T) {
chatID := fmt.Sprintf("test_history_exist_%s", uuid.New().String()[:8])
ctx := newHistoryTestContext(chatID)
reqID := uuid.New().String()[:8]
// Create chat
err := chatStore.CreateChat(&storetypes.Chat{
ChatID: chatID,
AssistantID: ast.ID,
Status: "active",
Share: "private",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
require.NoError(t, err)
defer func() {
chatStore.DeleteMessages(chatID, nil)
chatStore.DeleteChat(chatID)
}()
// Add history messages
historyMessages := []*storetypes.Message{
{
MessageID: fmt.Sprintf("hist_msg_1_%s", reqID),
ChatID: chatID,
RequestID: fmt.Sprintf("req_%s", reqID),
Role: "user",
Type: "user_input",
Props: map[string]interface{}{"content": "Previous question"},
Sequence: 1,
AssistantID: ast.ID,
CreatedAt: time.Now().Add(-2 * time.Minute),
},
{
MessageID: fmt.Sprintf("hist_msg_2_%s", reqID),
ChatID: chatID,
RequestID: fmt.Sprintf("req_%s", reqID),
Role: "assistant",
Type: "text",
Props: map[string]interface{}{"text": "Previous answer"},
Sequence: 2,
AssistantID: ast.ID,
CreatedAt: time.Now().Add(-1 * time.Minute),
},
}
err = chatStore.SaveMessages(chatID, historyMessages)
require.NoError(t, err)
// New input message
input := []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: "New question"},
}
result, err := ast.WithHistory(ctx, input, nil)
require.NoError(t, err)
require.NotNil(t, result)
// InputMessages should be unchanged (no overlap)
assert.Equal(t, input, result.InputMessages)
// FullMessages should have history + input
assert.Len(t, result.FullMessages, 3) // 2 history + 1 new
// Verify order: history first, then input
assert.Equal(t, agentcontext.RoleUser, result.FullMessages[0].Role)
assert.Equal(t, "Previous question", result.FullMessages[0].Content)
assert.Equal(t, agentcontext.RoleAssistant, result.FullMessages[1].Role)
assert.Equal(t, "Previous answer", result.FullMessages[1].Content)
assert.Equal(t, agentcontext.RoleUser, result.FullMessages[2].Role)
assert.Equal(t, "New question", result.FullMessages[2].Content)
t.Log("✓ History merged correctly with new input")
})
t.Run("SkipHistoryOption", func(t *testing.T) {
chatID := fmt.Sprintf("test_history_skip_%s", uuid.New().String()[:8])
ctx := newHistoryTestContext(chatID)
reqID := uuid.New().String()[:8]
// Create chat with history
err := chatStore.CreateChat(&storetypes.Chat{
ChatID: chatID,
AssistantID: ast.ID,
Status: "active",
Share: "private",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
require.NoError(t, err)
defer func() {
chatStore.DeleteMessages(chatID, nil)
chatStore.DeleteChat(chatID)
}()
// Add history message
err = chatStore.SaveMessages(chatID, []*storetypes.Message{
{
MessageID: fmt.Sprintf("skip_hist_1_%s", reqID),
ChatID: chatID,
RequestID: fmt.Sprintf("req_skip_%s", reqID),
Role: "user",
Type: "user_input",
Props: map[string]interface{}{"content": "Should be skipped"},
Sequence: 1,
AssistantID: ast.ID,
CreatedAt: time.Now(),
},
})
require.NoError(t, err)
input := []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: "Only this should appear"},
}
// Use Skip.History option
opts := &agentcontext.Options{
Skip: &agentcontext.Skip{
History: true,
},
}
result, err := ast.WithHistory(ctx, input, nil, opts)
require.NoError(t, err)
require.NotNil(t, result)
// Both should be same as input (history skipped)
assert.Equal(t, input, result.InputMessages)
assert.Equal(t, input, result.FullMessages)
assert.Len(t, result.FullMessages, 1)
t.Log("✓ History skipped when Skip.History=true")
})
t.Run("OverlapDetection", func(t *testing.T) {
chatID := fmt.Sprintf("test_history_overlap_%s", uuid.New().String()[:8])
ctx := newHistoryTestContext(chatID)
reqID := uuid.New().String()[:8]
// Create chat
err := chatStore.CreateChat(&storetypes.Chat{
ChatID: chatID,
AssistantID: ast.ID,
Status: "active",
Share: "private",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
require.NoError(t, err)
defer func() {
chatStore.DeleteMessages(chatID, nil)
chatStore.DeleteChat(chatID)
}()
// Add history messages
err = chatStore.SaveMessages(chatID, []*storetypes.Message{
{
MessageID: fmt.Sprintf("overlap_1_%s", reqID),
ChatID: chatID,
RequestID: fmt.Sprintf("req_overlap_%s", reqID),
Role: "user",
Type: "user_input",
Props: map[string]interface{}{"content": "Message one"},
Sequence: 1,
AssistantID: ast.ID,
CreatedAt: time.Now().Add(-3 * time.Minute),
},
{
MessageID: fmt.Sprintf("overlap_2_%s", reqID),
ChatID: chatID,
RequestID: fmt.Sprintf("req_overlap_%s", reqID),
Role: "assistant",
Type: "text",
Props: map[string]interface{}{"text": "Response one"},
Sequence: 2,
AssistantID: ast.ID,
CreatedAt: time.Now().Add(-2 * time.Minute),
},
{
MessageID: fmt.Sprintf("overlap_3_%s", reqID),
ChatID: chatID,
RequestID: fmt.Sprintf("req_overlap_2_%s", reqID),
Role: "user",
Type: "user_input",
Props: map[string]interface{}{"content": "Message two"},
Sequence: 3,
AssistantID: ast.ID,
CreatedAt: time.Now().Add(-1 * time.Minute),
},
})
require.NoError(t, err)
// Input that overlaps with history (includes last messages)
// Some clients send full history + new message
input := []agentcontext.Message{
{Role: agentcontext.RoleAssistant, Content: "Response one"}, // Overlap
{Role: agentcontext.RoleUser, Content: "Message two"}, // Overlap
{Role: agentcontext.RoleUser, Content: "New message"}, // New
}
result, err := ast.WithHistory(ctx, input, nil)
require.NoError(t, err)
require.NotNil(t, result)
// InputMessages should have overlap removed
assert.Len(t, result.InputMessages, 1, "Should remove 2 overlapping messages")
assert.Equal(t, "New message", result.InputMessages[0].Content)
// FullMessages should be history + clean input
assert.Len(t, result.FullMessages, 4) // 3 history + 1 new
t.Log("✓ Overlap detected and removed from input")
})
t.Run("EmptyChatID", func(t *testing.T) {
ctx := newHistoryTestContext("")
input := []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: "No chat ID"},
}
result, err := ast.WithHistory(ctx, input, nil)
require.NoError(t, err)
require.NotNil(t, result)
// With empty chat ID, should return input as is
assert.Equal(t, input, result.InputMessages)
assert.Equal(t, input, result.FullMessages)
t.Log("✓ Empty chat ID handled gracefully")
})
t.Run("MultipleUserMessages", func(t *testing.T) {
chatID := fmt.Sprintf("test_history_multi_%s", uuid.New().String()[:8])
ctx := newHistoryTestContext(chatID)
reqID := uuid.New().String()[:8]
// Create chat with history
err := chatStore.CreateChat(&storetypes.Chat{
ChatID: chatID,
AssistantID: ast.ID,
Status: "active",
Share: "private",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
require.NoError(t, err)
defer func() {
chatStore.DeleteMessages(chatID, nil)
chatStore.DeleteChat(chatID)
}()
// Add history
err = chatStore.SaveMessages(chatID, []*storetypes.Message{
{
MessageID: fmt.Sprintf("multi_1_%s", reqID),
ChatID: chatID,
RequestID: fmt.Sprintf("req_multi_%s", reqID),
Role: "user",
Type: "user_input",
Props: map[string]interface{}{"content": "First"},
Sequence: 1,
AssistantID: ast.ID,
CreatedAt: time.Now().Add(-1 * time.Minute),
},
})
require.NoError(t, err)
// Multiple input messages
input := []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: "Second"},
{Role: agentcontext.RoleUser, Content: "Third"},
}
result, err := ast.WithHistory(ctx, input, nil)
require.NoError(t, err)
require.NotNil(t, result)
assert.Len(t, result.InputMessages, 2)
assert.Len(t, result.FullMessages, 3) // 1 history + 2 new
t.Log("✓ Multiple input messages handled correctly")
})
}
// =============================================================================
// History Load Tests
// =============================================================================
func TestHistoryLoading(t *testing.T) {
testutils.Prepare(t)
defer testutils.Clean(t)
ast, err := assistant.Get("tests.history")
require.NoError(t, err)
chatStore := assistant.GetChatStore()
if chatStore == nil {
t.Skip("Chat store not configured")
}
t.Run("FilterNonConversationTypes", func(t *testing.T) {
chatID := fmt.Sprintf("test_filter_%s", uuid.New().String()[:8])
ctx := newHistoryTestContext(chatID)
reqID := uuid.New().String()[:8]
// Create chat
err := chatStore.CreateChat(&storetypes.Chat{
ChatID: chatID,
AssistantID: ast.ID,
Status: "active",
Share: "private",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
require.NoError(t, err)
defer func() {
chatStore.DeleteMessages(chatID, nil)
chatStore.DeleteChat(chatID)
}()
// Add various message types (only user/assistant roles allowed by DB constraint)
// loadHistory filters by role (user/assistant only) and converts based on type:
// - loading/event: skipped (no semantic value)
// - tool_call/action: converted to historical summary text
// - text/user_input/error: kept as-is
err = chatStore.SaveMessages(chatID, []*storetypes.Message{
{
MessageID: fmt.Sprintf("filter_1_%s", reqID),
ChatID: chatID,
RequestID: fmt.Sprintf("req_filter_%s", reqID),
Role: "user",
Type: "user_input",
Props: map[string]interface{}{"content": "User message"},
Sequence: 1,
AssistantID: ast.ID,
CreatedAt: time.Now().Add(-3 * time.Minute),
},
{
MessageID: fmt.Sprintf("filter_2_%s", reqID),
ChatID: chatID,
RequestID: fmt.Sprintf("req_filter_%s", reqID),
Role: "assistant",
Type: "loading",
Props: map[string]interface{}{"text": "Loading..."},
Sequence: 2,
AssistantID: ast.ID,
CreatedAt: time.Now().Add(-2 * time.Minute),
},
{
MessageID: fmt.Sprintf("filter_3_%s", reqID),
ChatID: chatID,
RequestID: fmt.Sprintf("req_filter_%s", reqID),
Role: "assistant",
Type: "text",
Props: map[string]interface{}{"text": "Assistant response"},
Sequence: 3,
AssistantID: ast.ID,
CreatedAt: time.Now().Add(-1 * time.Minute),
},
})
require.NoError(t, err)
input := []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: "New input"},
}
result, err := ast.WithHistory(ctx, input, nil)
require.NoError(t, err)
require.NotNil(t, result)
// loading type is skipped (no semantic value)
// History: user_input + text = 2 messages; plus 1 new input = 3 total
assert.Len(t, result.FullMessages, 3)
// Verify only user and assistant roles
for _, msg := range result.FullMessages {
assert.True(t, msg.Role == agentcontext.RoleUser || msg.Role == agentcontext.RoleAssistant,
"Expected user or assistant role, got: %s", msg.Role)
}
t.Log("✓ Loading type filtered, user/assistant roles kept")
})
t.Run("ToolCallConvertedToSummary", func(t *testing.T) {
chatID := fmt.Sprintf("test_toolcall_%s", uuid.New().String()[:8])
ctx := newHistoryTestContext(chatID)
reqID := uuid.New().String()[:8]
err := chatStore.CreateChat(&storetypes.Chat{
ChatID: chatID,
AssistantID: ast.ID,
Status: "active",
Share: "private",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
require.NoError(t, err)
defer func() {
chatStore.DeleteMessages(chatID, nil)
chatStore.DeleteChat(chatID)
}()
// Add tool_call messages in both formats
err = chatStore.SaveMessages(chatID, []*storetypes.Message{
{
MessageID: fmt.Sprintf("tc_1_%s", reqID),
ChatID: chatID,
RequestID: fmt.Sprintf("req_tc_%s", reqID),
Role: "user",
Type: "user_input",
Props: map[string]interface{}{"content": "echo 3 ping 4"},
Sequence: 1,
AssistantID: ast.ID,
CreatedAt: time.Now().Add(-3 * time.Minute),
},
// Raw stream chunk format (actual DB format)
{
MessageID: fmt.Sprintf("tc_2_%s", reqID),
ChatID: chatID,
RequestID: fmt.Sprintf("req_tc_%s", reqID),
Role: "assistant",
Type: "tool_call",
Props: map[string]interface{}{"content": `[{"index":0,"id":"call_abc","type":"function","function":{"name":"echo__ping"}}][{"index":0,"function":{"arguments":"{\"count\":3}"}}]`},
Sequence: 2,
AssistantID: ast.ID,
CreatedAt: time.Now().Add(-2 * time.Minute),
},
// Standard ToolCallProps format
{
MessageID: fmt.Sprintf("tc_3_%s", reqID),
ChatID: chatID,
RequestID: fmt.Sprintf("req_tc_%s", reqID),
Role: "assistant",
Type: "tool_call",
Props: map[string]interface{}{"name": "echo__echo", "arguments": `{"message":"hello"}`},
Sequence: 3,
AssistantID: ast.ID,
CreatedAt: time.Now().Add(-1 * time.Minute),
},
})
require.NoError(t, err)
input := []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: "echo 5 ping 6"},
}
result, err := ast.WithHistory(ctx, input, nil)
require.NoError(t, err)
require.NotNil(t, result)
// 1 user_input + 2 tool_call summaries + 1 new input = 4
assert.Len(t, result.FullMessages, 4)
// Verify tool_call messages are converted to summary text
tcMsg1 := result.FullMessages[1]
assert.Equal(t, agentcontext.RoleAssistant, tcMsg1.Role)
assert.Contains(t, tcMsg1.Content, "[Historical Tool Call Summary]")
assert.Contains(t, tcMsg1.Content, "echo__ping")
assert.Contains(t, tcMsg1.Content, `{"count":3}`)
tcMsg2 := result.FullMessages[2]
assert.Equal(t, agentcontext.RoleAssistant, tcMsg2.Role)
assert.Contains(t, tcMsg2.Content, "[Historical Tool Call Summary]")
assert.Contains(t, tcMsg2.Content, "echo__echo")
assert.Contains(t, tcMsg2.Content, `{"message":"hello"}`)
t.Log("✓ Tool call messages converted to historical summaries")
})
t.Run("ActionConvertedToSummary", func(t *testing.T) {
chatID := fmt.Sprintf("test_action_%s", uuid.New().String()[:8])
ctx := newHistoryTestContext(chatID)
reqID := uuid.New().String()[:8]
err := chatStore.CreateChat(&storetypes.Chat{
ChatID: chatID,
AssistantID: ast.ID,
Status: "active",
Share: "private",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
require.NoError(t, err)
defer func() {
chatStore.DeleteMessages(chatID, nil)
chatStore.DeleteChat(chatID)
}()
err = chatStore.SaveMessages(chatID, []*storetypes.Message{
{
MessageID: fmt.Sprintf("act_1_%s", reqID),
ChatID: chatID,
RequestID: fmt.Sprintf("req_act_%s", reqID),
Role: "user",
Type: "user_input",
Props: map[string]interface{}{"content": "Do something"},
Sequence: 1,
AssistantID: ast.ID,
CreatedAt: time.Now().Add(-2 * time.Minute),
},
// Action with payload
{
MessageID: fmt.Sprintf("act_2_%s", reqID),
ChatID: chatID,
RequestID: fmt.Sprintf("req_act_%s", reqID),
Role: "assistant",
Type: "action",
Props: map[string]interface{}{
"name": "robot.execute",
"payload": map[string]interface{}{"goals": "test goal", "robot_id": "12345"},
},
Sequence: 2,
AssistantID: ast.ID,
CreatedAt: time.Now().Add(-1 * time.Minute),
},
})
require.NoError(t, err)
input := []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: "What happened?"},
}
result, err := ast.WithHistory(ctx, input, nil)
require.NoError(t, err)
require.NotNil(t, result)
// 1 user_input + 1 action summary + 1 new input = 3
assert.Len(t, result.FullMessages, 3)
actMsg := result.FullMessages[1]
assert.Equal(t, agentcontext.RoleAssistant, actMsg.Role)
assert.Contains(t, actMsg.Content, "[Historical Action Summary]")
assert.Contains(t, actMsg.Content, "robot.execute")
assert.Contains(t, actMsg.Content, "test goal")
assert.Contains(t, actMsg.Content, "12345")
t.Log("✓ Action messages converted to historical summaries with payload")
})
t.Run("ActionWithoutPayload", func(t *testing.T) {
chatID := fmt.Sprintf("test_action_nopay_%s", uuid.New().String()[:8])
ctx := newHistoryTestContext(chatID)
reqID := uuid.New().String()[:8]
err := chatStore.CreateChat(&storetypes.Chat{
ChatID: chatID,
AssistantID: ast.ID,
Status: "active",
Share: "private",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
require.NoError(t, err)
defer func() {
chatStore.DeleteMessages(chatID, nil)
chatStore.DeleteChat(chatID)
}()
err = chatStore.SaveMessages(chatID, []*storetypes.Message{
{
MessageID: fmt.Sprintf("actnp_1_%s", reqID),
ChatID: chatID,
RequestID: fmt.Sprintf("req_actnp_%s", reqID),
Role: "assistant",
Type: "action",
Props: map[string]interface{}{"name": "navigate"},
Sequence: 1,
AssistantID: ast.ID,
CreatedAt: time.Now(),
},
})
require.NoError(t, err)
input := []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: "What happened?"},
}
result, err := ast.WithHistory(ctx, input, nil)
require.NoError(t, err)
require.NotNil(t, result)
// 1 action summary + 1 new input = 2
assert.Len(t, result.FullMessages, 2)
actMsg := result.FullMessages[0]
assert.Equal(t, agentcontext.RoleAssistant, actMsg.Role)
assert.Contains(t, actMsg.Content, "[Historical Action Summary]")
assert.Contains(t, actMsg.Content, "navigate")
assert.NotContains(t, actMsg.Content, "payload")
t.Log("✓ Action without payload handled correctly")
})
t.Run("ContentExtraction", func(t *testing.T) {
chatID := fmt.Sprintf("test_extract_%s", uuid.New().String()[:8])
ctx := newHistoryTestContext(chatID)
reqID := uuid.New().String()[:8]
// Create chat
err := chatStore.CreateChat(&storetypes.Chat{
ChatID: chatID,
AssistantID: ast.ID,
Status: "active",
Share: "private",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
require.NoError(t, err)
defer func() {
chatStore.DeleteMessages(chatID, nil)
chatStore.DeleteChat(chatID)
}()
// Add messages with different content formats
err = chatStore.SaveMessages(chatID, []*storetypes.Message{
{
MessageID: fmt.Sprintf("extract_1_%s", reqID),
ChatID: chatID,
RequestID: fmt.Sprintf("req_extract_%s", reqID),
Role: "user",
Type: "user_input",
Props: map[string]interface{}{"content": "User content from props.content"},
Sequence: 1,
AssistantID: ast.ID,
CreatedAt: time.Now().Add(-2 * time.Minute),
},
{
MessageID: fmt.Sprintf("extract_2_%s", reqID),
ChatID: chatID,
RequestID: fmt.Sprintf("req_extract_%s", reqID),
Role: "assistant",
Type: "text",
Props: map[string]interface{}{"text": "Assistant content from props.text"},
Sequence: 2,
AssistantID: ast.ID,
CreatedAt: time.Now().Add(-1 * time.Minute),
},
})
require.NoError(t, err)
input := []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: "New message"},
}
result, err := ast.WithHistory(ctx, input, nil)
require.NoError(t, err)
require.NotNil(t, result)
// Verify content was extracted correctly
assert.Len(t, result.FullMessages, 3)
assert.Equal(t, "User content from props.content", result.FullMessages[0].Content)
assert.Equal(t, "Assistant content from props.text", result.FullMessages[1].Content)
t.Log("✓ Content extracted correctly from different formats")
})
}
// =============================================================================
// Edge Cases Tests
// =============================================================================
func TestHistoryEdgeCases(t *testing.T) {
testutils.Prepare(t)
defer testutils.Clean(t)
ast, err := assistant.Get("tests.history")
require.NoError(t, err)
chatStore := assistant.GetChatStore()
if chatStore == nil {
t.Skip("Chat store not configured")
}
t.Run("EmptyInput", func(t *testing.T) {
chatID := fmt.Sprintf("test_empty_input_%s", uuid.New().String()[:8])
ctx := newHistoryTestContext(chatID)
reqID := uuid.New().String()[:8]
// Create chat with history
err := chatStore.CreateChat(&storetypes.Chat{
ChatID: chatID,
AssistantID: ast.ID,
Status: "active",
Share: "private",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
require.NoError(t, err)
defer func() {
chatStore.DeleteMessages(chatID, nil)
chatStore.DeleteChat(chatID)
}()
err = chatStore.SaveMessages(chatID, []*storetypes.Message{
{
MessageID: fmt.Sprintf("empty_input_1_%s", reqID),
ChatID: chatID,
RequestID: fmt.Sprintf("req_empty_%s", reqID),
Role: "user",
Type: "user_input",
Props: map[string]interface{}{"content": "Previous"},
Sequence: 1,
AssistantID: ast.ID,
CreatedAt: time.Now(),
},
})
require.NoError(t, err)
// Empty input
input := []agentcontext.Message{}
result, err := ast.WithHistory(ctx, input, nil)
require.NoError(t, err)
require.NotNil(t, result)
// Should return history only
assert.Empty(t, result.InputMessages)
assert.Len(t, result.FullMessages, 1)
t.Log("✓ Empty input handled correctly")
})
t.Run("FullOverlap", func(t *testing.T) {
chatID := fmt.Sprintf("test_full_overlap_%s", uuid.New().String()[:8])
ctx := newHistoryTestContext(chatID)
reqID := uuid.New().String()[:8]
// Create chat
err := chatStore.CreateChat(&storetypes.Chat{
ChatID: chatID,
AssistantID: ast.ID,
Status: "active",
Share: "private",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
require.NoError(t, err)
defer func() {
chatStore.DeleteMessages(chatID, nil)
chatStore.DeleteChat(chatID)
}()
// Add history
err = chatStore.SaveMessages(chatID, []*storetypes.Message{
{
MessageID: fmt.Sprintf("full_overlap_1_%s", reqID),
ChatID: chatID,
RequestID: fmt.Sprintf("req_full_%s", reqID),
Role: "user",
Type: "user_input",
Props: map[string]interface{}{"content": "Exact same message"},
Sequence: 1,
AssistantID: ast.ID,
CreatedAt: time.Now(),
},
})
require.NoError(t, err)
// Input is exactly the same as history
input := []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: "Exact same message"},
}
result, err := ast.WithHistory(ctx, input, nil)
require.NoError(t, err)
require.NotNil(t, result)
// Full overlap: clean input should be empty
assert.Empty(t, result.InputMessages)
// FullMessages should be just history (no duplicates)
assert.Len(t, result.FullMessages, 1)
t.Log("✓ Full overlap handled correctly")
})
t.Run("NonExistentChat", func(t *testing.T) {
chatID := "non_existent_chat_12345"
ctx := newHistoryTestContext(chatID)
input := []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: "Message to non-existent chat"},
}
result, err := ast.WithHistory(ctx, input, nil)
require.NoError(t, err)
require.NotNil(t, result)
// Should return input as is (no history found)
assert.Equal(t, input, result.InputMessages)
assert.Equal(t, input, result.FullMessages)
t.Log("✓ Non-existent chat handled gracefully")
})
t.Run("MessageWithName", func(t *testing.T) {
chatID := fmt.Sprintf("test_name_%s", uuid.New().String()[:8])
ctx := newHistoryTestContext(chatID)
reqID := uuid.New().String()[:8]
// Create chat
err := chatStore.CreateChat(&storetypes.Chat{
ChatID: chatID,
AssistantID: ast.ID,
Status: "active",
Share: "private",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
require.NoError(t, err)
defer func() {
chatStore.DeleteMessages(chatID, nil)
chatStore.DeleteChat(chatID)
}()
// Add message with name
err = chatStore.SaveMessages(chatID, []*storetypes.Message{
{
MessageID: fmt.Sprintf("name_msg_1_%s", reqID),
ChatID: chatID,
RequestID: fmt.Sprintf("req_name_%s", reqID),
Role: "user",
Type: "user_input",
Props: map[string]interface{}{"content": "Message with name", "name": "John"},
Sequence: 1,
AssistantID: ast.ID,
CreatedAt: time.Now(),
},
})
require.NoError(t, err)
input := []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: "New message"},
}
result, err := ast.WithHistory(ctx, input, nil)
require.NoError(t, err)
require.NotNil(t, result)
// First message should have name
assert.Len(t, result.FullMessages, 2)
assert.NotNil(t, result.FullMessages[0].Name)
assert.Equal(t, "John", *result.FullMessages[0].Name)
t.Log("✓ Message name field preserved")
})
t.Run("EmptyContent", func(t *testing.T) {
chatID := fmt.Sprintf("test_empty_content_%s", uuid.New().String()[:8])
ctx := newHistoryTestContext(chatID)
reqID := uuid.New().String()[:8]
// Create chat
err := chatStore.CreateChat(&storetypes.Chat{
ChatID: chatID,
AssistantID: ast.ID,
Status: "active",
Share: "private",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
require.NoError(t, err)
defer func() {
chatStore.DeleteMessages(chatID, nil)
chatStore.DeleteChat(chatID)
}()
// Add message with empty content in props
err = chatStore.SaveMessages(chatID, []*storetypes.Message{
{
MessageID: fmt.Sprintf("empty_content_1_%s", reqID),
ChatID: chatID,
RequestID: fmt.Sprintf("req_empty_content_%s", reqID),
Role: "user",
Type: "user_input",
Props: map[string]interface{}{}, // empty props (no content)
Sequence: 1,
AssistantID: ast.ID,
CreatedAt: time.Now(),
},
})
require.NoError(t, err)
input := []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: "New message"},
}
result, err := ast.WithHistory(ctx, input, nil)
require.NoError(t, err)
require.NotNil(t, result)
// Message with empty props should be skipped (no content extractable)
// Only new input should be present
assert.Len(t, result.FullMessages, 1)
assert.Equal(t, "New message", result.FullMessages[0].Content)
t.Log("✓ Empty content handled gracefully (message skipped)")
})
}