yao/agent/store/xun/resume_test.go
Max 76bbfa0927 Enhance chat filtering and management in Xun store
- Updated the ChatFilter structure to include advanced permission filters for UserID and TeamID, allowing for more granular chat retrieval.
- Added examples in the documentation to demonstrate new filtering capabilities, including combinations of user and team filters, as well as complex conditions using QueryFilter.
- Implemented batch saving and retrieval functionalities for messages and resumes, improving data management efficiency.
- Revised related tests to validate the new filtering features and ensure robust functionality across chat management operations.
2025-12-09 11:18:55 +08:00

839 lines
24 KiB
Go

package xun_test
import (
"fmt"
"testing"
"time"
"github.com/yaoapp/yao/agent/store/types"
"github.com/yaoapp/yao/agent/store/xun"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/test"
)
// TestSaveResume tests batch saving resume records
func TestSaveResume(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 a chat first
chat := &types.Chat{
AssistantID: "test_assistant",
Title: "Resume Test Chat",
}
err = store.CreateChat(chat)
if err != nil {
t.Fatalf("Failed to create chat: %v", err)
}
defer store.DeleteChat(chat.ChatID)
t.Run("SaveSingleRecord", func(t *testing.T) {
requestID := fmt.Sprintf("req_%d", time.Now().UnixNano())
records := []*types.Resume{
{
ChatID: chat.ChatID,
RequestID: requestID,
AssistantID: "test_assistant",
StackID: "stack_001",
StackDepth: 0,
Type: types.ResumeTypeLLM,
Status: types.ResumeStatusInterrupted,
Sequence: 1,
},
}
err := store.SaveResume(records)
if err != nil {
t.Fatalf("Failed to save resume record: %v", err)
}
// Verify
retrieved, err := store.GetResume(chat.ChatID)
if err != nil {
t.Fatalf("Failed to get resume records: %v", err)
}
found := false
for _, r := range retrieved {
if r.RequestID == requestID {
found = true
if r.Type != types.ResumeTypeLLM {
t.Errorf("Expected type '%s', got '%s'", types.ResumeTypeLLM, r.Type)
}
if r.Status != types.ResumeStatusInterrupted {
t.Errorf("Expected status '%s', got '%s'", types.ResumeStatusInterrupted, r.Status)
}
break
}
}
if !found {
t.Error("Could not find saved resume record")
}
// Clean up
store.DeleteResume(chat.ChatID)
})
t.Run("SaveBatchRecords", func(t *testing.T) {
// Create a new chat for this test
batchChat := &types.Chat{
AssistantID: "test_assistant",
}
err := store.CreateChat(batchChat)
if err != nil {
t.Fatalf("Failed to create chat: %v", err)
}
defer store.DeleteChat(batchChat.ChatID)
requestID := fmt.Sprintf("req_%d", time.Now().UnixNano())
records := []*types.Resume{
{
ChatID: batchChat.ChatID,
RequestID: requestID,
AssistantID: "test_assistant",
StackID: "stack_001",
StackDepth: 0,
Type: types.ResumeTypeInput,
Status: types.ResumeStatusInterrupted,
Sequence: 1,
},
{
ChatID: batchChat.ChatID,
RequestID: requestID,
AssistantID: "test_assistant",
StackID: "stack_001",
StackDepth: 0,
Type: types.ResumeTypeHookCreate,
Status: types.ResumeStatusInterrupted,
Sequence: 2,
},
{
ChatID: batchChat.ChatID,
RequestID: requestID,
AssistantID: "test_assistant",
StackID: "stack_001",
StackDepth: 0,
Type: types.ResumeTypeLLM,
Status: types.ResumeStatusFailed,
Sequence: 3,
Error: "Connection timeout",
},
}
err = store.SaveResume(records)
if err != nil {
t.Fatalf("Failed to save batch resume records: %v", err)
}
// Verify all records saved
retrieved, err := store.GetResume(batchChat.ChatID)
if err != nil {
t.Fatalf("Failed to get resume records: %v", err)
}
if len(retrieved) != 3 {
t.Errorf("Expected 3 records, got %d", len(retrieved))
}
// Verify order (should be by sequence)
if len(retrieved) >= 3 {
if retrieved[0].Sequence != 1 {
t.Errorf("Expected first record sequence 1, got %d", retrieved[0].Sequence)
}
if retrieved[2].Sequence != 3 {
t.Errorf("Expected last record sequence 3, got %d", retrieved[2].Sequence)
}
if retrieved[2].Error != "Connection timeout" {
t.Errorf("Expected error 'Connection timeout', got '%s'", retrieved[2].Error)
}
}
t.Logf("Saved %d resume records in single batch call", len(records))
})
t.Run("SaveRecordWithAllFields", func(t *testing.T) {
fullChat := &types.Chat{
AssistantID: "test_assistant",
}
err := store.CreateChat(fullChat)
if err != nil {
t.Fatalf("Failed to create chat: %v", err)
}
defer store.DeleteChat(fullChat.ChatID)
requestID := fmt.Sprintf("req_%d", time.Now().UnixNano())
records := []*types.Resume{
{
ChatID: fullChat.ChatID,
RequestID: requestID,
AssistantID: "test_assistant",
StackID: "stack_001",
StackParentID: "stack_000",
StackDepth: 1,
Type: types.ResumeTypeDelegate,
Status: types.ResumeStatusInterrupted,
Input: map[string]interface{}{"agent_id": "sub_agent", "messages": []interface{}{}},
Output: map[string]interface{}{"partial": true},
SpaceSnapshot: map[string]interface{}{"key1": "value1", "key2": 123},
Error: "User cancelled",
Sequence: 1,
Metadata: map[string]interface{}{"retry_count": 0},
},
}
err = store.SaveResume(records)
if err != nil {
t.Fatalf("Failed to save record: %v", err)
}
retrieved, err := store.GetResume(fullChat.ChatID)
if err != nil {
t.Fatalf("Failed to get records: %v", err)
}
if len(retrieved) != 1 {
t.Fatalf("Expected 1 record, got %d", len(retrieved))
}
r := retrieved[0]
if r.StackParentID != "stack_000" {
t.Errorf("Expected stack_parent_id 'stack_000', got '%s'", r.StackParentID)
}
if r.StackDepth != 1 {
t.Errorf("Expected stack_depth 1, got %d", r.StackDepth)
}
if r.Input == nil {
t.Error("Expected input to be set")
}
if r.Output == nil {
t.Error("Expected output to be set")
}
if r.SpaceSnapshot == nil {
t.Error("Expected space_snapshot to be set")
} else if r.SpaceSnapshot["key1"] != "value1" {
t.Errorf("Expected space_snapshot key1='value1', got '%v'", r.SpaceSnapshot["key1"])
}
if r.Metadata == nil {
t.Error("Expected metadata to be set")
}
})
t.Run("SaveEmptyRecords", func(t *testing.T) {
err := store.SaveResume([]*types.Resume{})
if err != nil {
t.Errorf("Expected no error for empty records, got: %v", err)
}
})
t.Run("SaveRecordWithoutChatID", func(t *testing.T) {
records := []*types.Resume{{RequestID: "req", AssistantID: "ast", StackID: "stk", Type: "llm", Status: "failed", Sequence: 1}}
err := store.SaveResume(records)
if err == nil {
t.Error("Expected error when saving without chat_id")
}
})
t.Run("SaveRecordWithoutRequestID", func(t *testing.T) {
records := []*types.Resume{{ChatID: chat.ChatID, AssistantID: "ast", StackID: "stk", Type: "llm", Status: "failed", Sequence: 1}}
err := store.SaveResume(records)
if err == nil {
t.Error("Expected error when saving without request_id")
}
})
t.Run("SaveRecordWithoutAssistantID", func(t *testing.T) {
records := []*types.Resume{{ChatID: chat.ChatID, RequestID: "req", StackID: "stk", Type: "llm", Status: "failed", Sequence: 1}}
err := store.SaveResume(records)
if err == nil {
t.Error("Expected error when saving without assistant_id")
}
})
t.Run("SaveRecordWithoutStackID", func(t *testing.T) {
records := []*types.Resume{{ChatID: chat.ChatID, RequestID: "req", AssistantID: "ast", Type: "llm", Status: "failed", Sequence: 1}}
err := store.SaveResume(records)
if err == nil {
t.Error("Expected error when saving without stack_id")
}
})
t.Run("SaveRecordWithoutType", func(t *testing.T) {
records := []*types.Resume{{ChatID: chat.ChatID, RequestID: "req", AssistantID: "ast", StackID: "stk", Status: "failed", Sequence: 1}}
err := store.SaveResume(records)
if err == nil {
t.Error("Expected error when saving without type")
}
})
t.Run("SaveRecordWithoutStatus", func(t *testing.T) {
records := []*types.Resume{{ChatID: chat.ChatID, RequestID: "req", AssistantID: "ast", StackID: "stk", Type: "llm", Sequence: 1}}
err := store.SaveResume(records)
if err == nil {
t.Error("Expected error when saving without status")
}
})
}
// TestGetResume tests retrieving resume records
func TestGetResume(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 chat and resume records
chat := &types.Chat{
AssistantID: "test_assistant",
}
err = store.CreateChat(chat)
if err != nil {
t.Fatalf("Failed to create chat: %v", err)
}
defer store.DeleteChat(chat.ChatID)
requestID := fmt.Sprintf("req_%d", time.Now().UnixNano())
records := []*types.Resume{
{ChatID: chat.ChatID, RequestID: requestID, AssistantID: "ast1", StackID: "stk1", Type: types.ResumeTypeInput, Status: types.ResumeStatusInterrupted, Sequence: 1},
{ChatID: chat.ChatID, RequestID: requestID, AssistantID: "ast1", StackID: "stk1", Type: types.ResumeTypeHookCreate, Status: types.ResumeStatusInterrupted, Sequence: 2},
{ChatID: chat.ChatID, RequestID: requestID, AssistantID: "ast1", StackID: "stk1", Type: types.ResumeTypeLLM, Status: types.ResumeStatusFailed, Sequence: 3},
}
err = store.SaveResume(records)
if err != nil {
t.Fatalf("Failed to save records: %v", err)
}
defer store.DeleteResume(chat.ChatID)
t.Run("GetAllRecords", func(t *testing.T) {
retrieved, err := store.GetResume(chat.ChatID)
if err != nil {
t.Fatalf("Failed to get records: %v", err)
}
if len(retrieved) != 3 {
t.Errorf("Expected 3 records, got %d", len(retrieved))
}
// Verify order by sequence
for i := 1; i < len(retrieved); i++ {
if retrieved[i].Sequence < retrieved[i-1].Sequence {
t.Error("Records not ordered by sequence")
}
}
})
t.Run("GetRecordsWithEmptyChatID", func(t *testing.T) {
_, err := store.GetResume("")
if err == nil {
t.Error("Expected error when getting records without chat_id")
}
})
t.Run("GetRecordsFromNonExistentChat", func(t *testing.T) {
retrieved, err := store.GetResume("nonexistent_chat")
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if len(retrieved) != 0 {
t.Errorf("Expected 0 records from non-existent chat, got %d", len(retrieved))
}
})
}
// TestGetLastResume tests retrieving the last resume record
func TestGetLastResume(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)
}
chat := &types.Chat{
AssistantID: "test_assistant",
}
err = store.CreateChat(chat)
if err != nil {
t.Fatalf("Failed to create chat: %v", err)
}
defer store.DeleteChat(chat.ChatID)
t.Run("GetLastRecordFromMultiple", func(t *testing.T) {
requestID := fmt.Sprintf("req_%d", time.Now().UnixNano())
records := []*types.Resume{
{ChatID: chat.ChatID, RequestID: requestID, AssistantID: "ast", StackID: "stk", Type: types.ResumeTypeInput, Status: types.ResumeStatusInterrupted, Sequence: 1},
{ChatID: chat.ChatID, RequestID: requestID, AssistantID: "ast", StackID: "stk", Type: types.ResumeTypeHookCreate, Status: types.ResumeStatusInterrupted, Sequence: 2},
{ChatID: chat.ChatID, RequestID: requestID, AssistantID: "ast", StackID: "stk", Type: types.ResumeTypeLLM, Status: types.ResumeStatusFailed, Sequence: 3, Error: "Last error"},
}
err := store.SaveResume(records)
if err != nil {
t.Fatalf("Failed to save records: %v", err)
}
defer store.DeleteResume(chat.ChatID)
last, err := store.GetLastResume(chat.ChatID)
if err != nil {
t.Fatalf("Failed to get last record: %v", err)
}
if last == nil {
t.Fatal("Expected last record, got nil")
}
if last.Sequence != 3 {
t.Errorf("Expected sequence 3, got %d", last.Sequence)
}
if last.Type != types.ResumeTypeLLM {
t.Errorf("Expected type '%s', got '%s'", types.ResumeTypeLLM, last.Type)
}
if last.Error != "Last error" {
t.Errorf("Expected error 'Last error', got '%s'", last.Error)
}
})
t.Run("GetLastRecordFromEmpty", func(t *testing.T) {
emptyChat := &types.Chat{AssistantID: "test_assistant"}
err := store.CreateChat(emptyChat)
if err != nil {
t.Fatalf("Failed to create chat: %v", err)
}
defer store.DeleteChat(emptyChat.ChatID)
last, err := store.GetLastResume(emptyChat.ChatID)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if last != nil {
t.Error("Expected nil for empty chat, got record")
}
})
t.Run("GetLastRecordWithEmptyChatID", func(t *testing.T) {
_, err := store.GetLastResume("")
if err == nil {
t.Error("Expected error when getting last record without chat_id")
}
})
}
// TestGetResumeByStackID tests retrieving records by stack ID
func TestGetResumeByStackID(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)
}
chat := &types.Chat{
AssistantID: "test_assistant",
}
err = store.CreateChat(chat)
if err != nil {
t.Fatalf("Failed to create chat: %v", err)
}
defer store.DeleteChat(chat.ChatID)
requestID := fmt.Sprintf("req_%d", time.Now().UnixNano())
records := []*types.Resume{
{ChatID: chat.ChatID, RequestID: requestID, AssistantID: "ast1", StackID: "stack_A", Type: types.ResumeTypeInput, Status: types.ResumeStatusInterrupted, Sequence: 1},
{ChatID: chat.ChatID, RequestID: requestID, AssistantID: "ast1", StackID: "stack_A", Type: types.ResumeTypeLLM, Status: types.ResumeStatusInterrupted, Sequence: 2},
{ChatID: chat.ChatID, RequestID: requestID, AssistantID: "ast2", StackID: "stack_B", StackParentID: "stack_A", StackDepth: 1, Type: types.ResumeTypeDelegate, Status: types.ResumeStatusFailed, Sequence: 3},
}
err = store.SaveResume(records)
if err != nil {
t.Fatalf("Failed to save records: %v", err)
}
defer store.DeleteResume(chat.ChatID)
t.Run("GetRecordsByStackA", func(t *testing.T) {
retrieved, err := store.GetResumeByStackID("stack_A")
if err != nil {
t.Fatalf("Failed to get records: %v", err)
}
if len(retrieved) != 2 {
t.Errorf("Expected 2 records for stack_A, got %d", len(retrieved))
}
})
t.Run("GetRecordsByStackB", func(t *testing.T) {
retrieved, err := store.GetResumeByStackID("stack_B")
if err != nil {
t.Fatalf("Failed to get records: %v", err)
}
if len(retrieved) != 1 {
t.Errorf("Expected 1 record for stack_B, got %d", len(retrieved))
}
if len(retrieved) > 0 {
if retrieved[0].StackParentID != "stack_A" {
t.Errorf("Expected stack_parent_id 'stack_A', got '%s'", retrieved[0].StackParentID)
}
if retrieved[0].StackDepth != 1 {
t.Errorf("Expected stack_depth 1, got %d", retrieved[0].StackDepth)
}
}
})
t.Run("GetRecordsByNonExistentStack", func(t *testing.T) {
retrieved, err := store.GetResumeByStackID("nonexistent_stack")
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if len(retrieved) != 0 {
t.Errorf("Expected 0 records, got %d", len(retrieved))
}
})
t.Run("GetRecordsByEmptyStackID", func(t *testing.T) {
_, err := store.GetResumeByStackID("")
if err == nil {
t.Error("Expected error when getting records without stack_id")
}
})
}
// TestGetStackPath tests retrieving the stack path
func TestGetStackPath(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)
}
chat := &types.Chat{
AssistantID: "test_assistant",
}
err = store.CreateChat(chat)
if err != nil {
t.Fatalf("Failed to create chat: %v", err)
}
defer store.DeleteChat(chat.ChatID)
// Create a nested stack structure: root -> child -> grandchild
requestID := fmt.Sprintf("req_%d", time.Now().UnixNano())
records := []*types.Resume{
{ChatID: chat.ChatID, RequestID: requestID, AssistantID: "ast1", StackID: "root_stack", Type: types.ResumeTypeInput, Status: types.ResumeStatusInterrupted, Sequence: 1},
{ChatID: chat.ChatID, RequestID: requestID, AssistantID: "ast2", StackID: "child_stack", StackParentID: "root_stack", StackDepth: 1, Type: types.ResumeTypeDelegate, Status: types.ResumeStatusInterrupted, Sequence: 2},
{ChatID: chat.ChatID, RequestID: requestID, AssistantID: "ast3", StackID: "grandchild_stack", StackParentID: "child_stack", StackDepth: 2, Type: types.ResumeTypeLLM, Status: types.ResumeStatusFailed, Sequence: 3},
}
err = store.SaveResume(records)
if err != nil {
t.Fatalf("Failed to save records: %v", err)
}
defer store.DeleteResume(chat.ChatID)
t.Run("GetPathFromGrandchild", func(t *testing.T) {
path, err := store.GetStackPath("grandchild_stack")
if err != nil {
t.Fatalf("Failed to get stack path: %v", err)
}
if len(path) != 3 {
t.Errorf("Expected path length 3, got %d", len(path))
}
if len(path) >= 3 {
if path[0] != "root_stack" {
t.Errorf("Expected first element 'root_stack', got '%s'", path[0])
}
if path[1] != "child_stack" {
t.Errorf("Expected second element 'child_stack', got '%s'", path[1])
}
if path[2] != "grandchild_stack" {
t.Errorf("Expected third element 'grandchild_stack', got '%s'", path[2])
}
}
t.Logf("Stack path: %v", path)
})
t.Run("GetPathFromChild", func(t *testing.T) {
path, err := store.GetStackPath("child_stack")
if err != nil {
t.Fatalf("Failed to get stack path: %v", err)
}
if len(path) != 2 {
t.Errorf("Expected path length 2, got %d", len(path))
}
if len(path) >= 2 {
if path[0] != "root_stack" {
t.Errorf("Expected first element 'root_stack', got '%s'", path[0])
}
if path[1] != "child_stack" {
t.Errorf("Expected second element 'child_stack', got '%s'", path[1])
}
}
})
t.Run("GetPathFromRoot", func(t *testing.T) {
path, err := store.GetStackPath("root_stack")
if err != nil {
t.Fatalf("Failed to get stack path: %v", err)
}
if len(path) != 1 {
t.Errorf("Expected path length 1, got %d", len(path))
}
if len(path) >= 1 && path[0] != "root_stack" {
t.Errorf("Expected 'root_stack', got '%s'", path[0])
}
})
t.Run("GetPathWithEmptyStackID", func(t *testing.T) {
_, err := store.GetStackPath("")
if err == nil {
t.Error("Expected error when getting path without stack_id")
}
})
}
// TestDeleteResume tests deleting resume records
func TestDeleteResume(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("DeleteExistingRecords", func(t *testing.T) {
chat := &types.Chat{AssistantID: "test_assistant"}
err := store.CreateChat(chat)
if err != nil {
t.Fatalf("Failed to create chat: %v", err)
}
defer store.DeleteChat(chat.ChatID)
requestID := fmt.Sprintf("req_%d", time.Now().UnixNano())
records := []*types.Resume{
{ChatID: chat.ChatID, RequestID: requestID, AssistantID: "ast", StackID: "stk", Type: types.ResumeTypeLLM, Status: types.ResumeStatusFailed, Sequence: 1},
{ChatID: chat.ChatID, RequestID: requestID, AssistantID: "ast", StackID: "stk", Type: types.ResumeTypeTool, Status: types.ResumeStatusFailed, Sequence: 2},
}
err = store.SaveResume(records)
if err != nil {
t.Fatalf("Failed to save records: %v", err)
}
// Delete
err = store.DeleteResume(chat.ChatID)
if err != nil {
t.Fatalf("Failed to delete records: %v", err)
}
// Verify deleted
retrieved, err := store.GetResume(chat.ChatID)
if err != nil {
t.Fatalf("Failed to get records: %v", err)
}
if len(retrieved) != 0 {
t.Errorf("Expected 0 records after delete, got %d", len(retrieved))
}
})
t.Run("DeleteFromEmptyChat", func(t *testing.T) {
chat := &types.Chat{AssistantID: "test_assistant"}
err := store.CreateChat(chat)
if err != nil {
t.Fatalf("Failed to create chat: %v", err)
}
defer store.DeleteChat(chat.ChatID)
// Delete from chat with no records - should not error
err = store.DeleteResume(chat.ChatID)
if err != nil {
t.Errorf("Expected no error when deleting from empty chat, got: %v", err)
}
})
t.Run("DeleteWithEmptyChatID", func(t *testing.T) {
err := store.DeleteResume("")
if err == nil {
t.Error("Expected error when deleting with empty chat_id")
}
})
}
// TestResumeCompleteWorkflow tests a complete resume/retry workflow
func TestResumeCompleteWorkflow(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("CompleteA2AWorkflow", func(t *testing.T) {
// Create chat
chat := &types.Chat{
AssistantID: "main_assistant",
Title: "A2A Workflow Test",
}
err := store.CreateChat(chat)
if err != nil {
t.Fatalf("Failed to create chat: %v", err)
}
defer store.DeleteChat(chat.ChatID)
// Simulate A2A call that gets interrupted
// Main assistant -> Sub assistant (interrupted during LLM call)
requestID := fmt.Sprintf("req_%d", time.Now().UnixNano())
records := []*types.Resume{
// Main assistant steps
{
ChatID: chat.ChatID,
RequestID: requestID,
AssistantID: "main_assistant",
StackID: "main_stack",
StackDepth: 0,
Type: types.ResumeTypeInput,
Status: types.ResumeStatusInterrupted,
Input: map[string]interface{}{"messages": []interface{}{map[string]interface{}{"role": "user", "content": "Analyze this"}}},
Sequence: 1,
},
{
ChatID: chat.ChatID,
RequestID: requestID,
AssistantID: "main_assistant",
StackID: "main_stack",
StackDepth: 0,
Type: types.ResumeTypeDelegate,
Status: types.ResumeStatusInterrupted,
SpaceSnapshot: map[string]interface{}{"task": "analyze", "data_id": "123"},
Sequence: 2,
},
// Sub assistant steps
{
ChatID: chat.ChatID,
RequestID: requestID,
AssistantID: "sub_assistant",
StackID: "sub_stack",
StackParentID: "main_stack",
StackDepth: 1,
Type: types.ResumeTypeInput,
Status: types.ResumeStatusInterrupted,
Sequence: 3,
},
{
ChatID: chat.ChatID,
RequestID: requestID,
AssistantID: "sub_assistant",
StackID: "sub_stack",
StackParentID: "main_stack",
StackDepth: 1,
Type: types.ResumeTypeLLM,
Status: types.ResumeStatusInterrupted,
Input: map[string]interface{}{"messages": []interface{}{}},
Output: map[string]interface{}{"partial_content": "The analysis shows..."},
SpaceSnapshot: map[string]interface{}{"task": "analyze", "data_id": "123"},
Sequence: 4,
},
}
err = store.SaveResume(records)
if err != nil {
t.Fatalf("Failed to save resume records: %v", err)
}
t.Logf("Saved %d resume records for A2A workflow", len(records))
// 1. Get last resume record (should be the interrupted LLM call)
last, err := store.GetLastResume(chat.ChatID)
if err != nil {
t.Fatalf("Failed to get last resume: %v", err)
}
if last == nil {
t.Fatal("Expected last resume record")
}
if last.Type != types.ResumeTypeLLM {
t.Errorf("Expected type '%s', got '%s'", types.ResumeTypeLLM, last.Type)
}
if last.StackDepth != 1 {
t.Errorf("Expected stack_depth 1, got %d", last.StackDepth)
}
// 2. Get stack path to understand the call hierarchy
path, err := store.GetStackPath(last.StackID)
if err != nil {
t.Fatalf("Failed to get stack path: %v", err)
}
if len(path) != 2 {
t.Errorf("Expected path length 2, got %d", len(path))
}
t.Logf("Stack path: %v", path)
// 3. Get all records for the sub stack
subRecords, err := store.GetResumeByStackID("sub_stack")
if err != nil {
t.Fatalf("Failed to get sub stack records: %v", err)
}
if len(subRecords) != 2 {
t.Errorf("Expected 2 records for sub_stack, got %d", len(subRecords))
}
// 4. Verify space snapshot is preserved
if last.SpaceSnapshot == nil {
t.Error("Expected space_snapshot to be set")
} else {
if last.SpaceSnapshot["task"] != "analyze" {
t.Errorf("Expected task='analyze', got '%v'", last.SpaceSnapshot["task"])
}
}
// 5. Clean up after successful resume
err = store.DeleteResume(chat.ChatID)
if err != nil {
t.Fatalf("Failed to delete resume records: %v", err)
}
// 6. Verify cleanup
remaining, err := store.GetResume(chat.ChatID)
if err != nil {
t.Fatalf("Failed to get remaining records: %v", err)
}
if len(remaining) != 0 {
t.Errorf("Expected 0 records after cleanup, got %d", len(remaining))
}
t.Log("Complete A2A workflow test passed!")
})
}