package database import ( "path/filepath" "testing" "go.uber.org/zap" ) func TestProcessDetailsSummaryPairsMixedIdentifiedAndIDLessResults(t *testing.T) { db, conversationID, messageID := setupProcessDetailsSummaryTest(t) for _, id := range []string{"call-1", "call-2", "call-3", "call-4"} { if err := db.AddProcessDetail(messageID, conversationID, "tool_call", "call", map[string]interface{}{ "toolName": "http-framework-test", "toolCallId": id, }); err != nil { t.Fatalf("AddProcessDetail(tool_call): %v", err) } } results := []map[string]interface{}{ {"toolName": "http-framework-test", "toolCallId": "call-1", "success": true}, {"toolName": "http-framework-test", "toolCallId": "call-2", "success": true}, {"toolName": "http-framework-test", "success": true}, {"toolName": "http-framework-test", "success": true}, } for _, result := range results { if err := db.AddProcessDetail(messageID, conversationID, "tool_result", "result", result); err != nil { t.Fatalf("AddProcessDetail(tool_result): %v", err) } } summary, err := db.GetProcessDetailsSummary(messageID) if err != nil { t.Fatalf("GetProcessDetailsSummary: %v", err) } if len(summary.ToolExecutions) != 4 { t.Fatalf("tool executions = %d, want 4", len(summary.ToolExecutions)) } for i, execution := range summary.ToolExecutions { if execution.Status != "completed" { t.Fatalf("execution %d status = %q, want completed", i, execution.Status) } } } func TestProcessDetailsSummaryPairsRepeatedToolCallIDsFIFO(t *testing.T) { db, conversationID, messageID := setupProcessDetailsSummaryTest(t) for i := 0; i < 2; i++ { if err := db.AddProcessDetail(messageID, conversationID, "tool_call", "call", map[string]interface{}{ "toolName": "execute", "toolCallId": "legacy-reused-id", }); err != nil { t.Fatalf("AddProcessDetail(tool_call): %v", err) } } for i := 0; i < 2; i++ { if err := db.AddProcessDetail(messageID, conversationID, "tool_result", "result", map[string]interface{}{ "toolName": "execute", "toolCallId": "legacy-reused-id", "success": true, }); err != nil { t.Fatalf("AddProcessDetail(tool_result): %v", err) } } summary, err := db.GetProcessDetailsSummary(messageID) if err != nil { t.Fatalf("GetProcessDetailsSummary: %v", err) } if len(summary.ToolExecutions) != 2 { t.Fatalf("tool executions = %d, want 2", len(summary.ToolExecutions)) } for i, execution := range summary.ToolExecutions { if execution.Status != "completed" { t.Fatalf("execution %d status = %q, want completed", i, execution.Status) } } } func TestProcessDetailsSummaryDoesNotReportPersistedOrphanAsRunning(t *testing.T) { db, conversationID, messageID := setupProcessDetailsSummaryTest(t) if err := db.AddProcessDetail(messageID, conversationID, "tool_call", "call", map[string]interface{}{ "toolName": "execute", "toolCallId": "orphan", }); err != nil { t.Fatalf("AddProcessDetail(tool_call): %v", err) } summary, err := db.GetProcessDetailsSummary(messageID) if err != nil { t.Fatalf("GetProcessDetailsSummary: %v", err) } if len(summary.ToolExecutions) != 1 || summary.ToolExecutions[0].Status != "result_missing" { t.Fatalf("tool executions = %#v, want result_missing", summary.ToolExecutions) } } func setupProcessDetailsSummaryTest(t *testing.T) (*DB, string, string) { t.Helper() db, err := NewDB(filepath.Join(t.TempDir(), "process-details.db"), zap.NewNop()) if err != nil { t.Fatalf("NewDB: %v", err) } t.Cleanup(func() { _ = db.Close() }) conversation, err := db.CreateConversation("process details", ConversationCreateMeta{}) if err != nil { t.Fatalf("CreateConversation: %v", err) } message, err := db.AddMessage(conversation.ID, "assistant", "done", nil) if err != nil { t.Fatalf("AddMessage: %v", err) } return db, conversation.ID, message.ID }