mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-08-14 15:10:20 +02:00
* feat(chat): add project-based conversation sidebar * feat(chat): refine Codex-style conversation UI * feat(chat): add Codex-style conversation workflow * feat(ui): 优化对话框与项目侧边栏交互 * fix(chat): 修复暗色输入框圆角填色 * fix(chat): 恢复输入区分层错位布局 * fix(hitl): isolate reviewer state per conversation * feat(hitl): 增加双入口审批与倒计时进度 * fix(hitl): 汇总项目审批并隔离对话状态 * fix(ui): 修复审批状态与无项目新任务 * fix(ui): 优化审批状态与对话切换性能 * fix(ui): 修复中断任务审批仍计时 * fix(ui): 修复多对话并发切换卡顿 * fix(hitl): 主动同步审批并关闭中断状态 * fix(ui): 固定项目审批汇总为绿色 * feat(ui): 同步系统模型与推理强度 * feat(ui): 完善项目侧栏预览与新建入口 * fix(ui): 防止无项目文件夹误展开 * fix(ui): 防止长历史对话滚动误触审批 * fix(ui): 修复对话操作并补充项目置顶 * fix(ui): 移除对话分组并调整项目置顶排序 * feat(ui): 优化迭代导航与审批交互 * fix(hitl): 将 write_file 加入内置免审批工具 * fix(chat): 支持回车发送与 Shift 回车换行 * fix(chat): 优化对话刷新与 Codex 风格交互 * fix(ui): 显示对话具体更新时间 * fix(ui): 优化对话刷新与项目加载 * fix(ui): 修复 Agent 审查文字裁切 * fix(chat): 修复刷新续流与多标签页同步 * fix(chat): 修复滚动跟随与中断任务终态 * fix(ui): 修复流式滚动跳动与工具状态恢复 * fix(ui): 修复刷新后流式输出停止粘底
183 lines
6.5 KiB
Go
183 lines
6.5 KiB
Go
package database
|
|
|
|
import (
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
func TestProcessDetailsSummaryDoesNotGuessIDLessResultsByOrder(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},
|
|
}
|
|
var resultIDs []string
|
|
for _, result := range results {
|
|
resultID, err := db.AddProcessDetailWithID(messageID, conversationID, "tool_result", "result", result)
|
|
if err != nil {
|
|
t.Fatalf("AddProcessDetail(tool_result): %v", err)
|
|
}
|
|
resultIDs = append(resultIDs, resultID)
|
|
}
|
|
|
|
summary, err := db.GetProcessDetailsSummary(messageID)
|
|
if err != nil {
|
|
t.Fatalf("GetProcessDetailsSummary: %v", err)
|
|
}
|
|
if len(summary.ToolExecutions) != 6 {
|
|
t.Fatalf("tool executions = %d, want 6", len(summary.ToolExecutions))
|
|
}
|
|
for i, execution := range summary.ToolExecutions[:2] {
|
|
if execution.Status != "completed" {
|
|
t.Fatalf("execution %d status = %q, want completed", i, execution.Status)
|
|
}
|
|
if execution.ResultDetailID != resultIDs[i] {
|
|
t.Fatalf("execution %d result detail id = %q, want %q", i, execution.ResultDetailID, resultIDs[i])
|
|
}
|
|
}
|
|
for i, execution := range summary.ToolExecutions[2:4] {
|
|
if execution.Status != "result_missing" {
|
|
t.Fatalf("unmatched call %d status = %q, want result_missing", i, execution.Status)
|
|
}
|
|
}
|
|
for i, execution := range summary.ToolExecutions[4:] {
|
|
if execution.Status != "completed" || execution.ToolCallID != "" {
|
|
t.Fatalf("idless result %d = %#v, want separate completed result without toolCallId", i, execution)
|
|
}
|
|
}
|
|
}
|
|
|
|
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 TestProcessDetailsSummaryIncludesPersistedTurnTiming(t *testing.T) {
|
|
db, _, messageID := setupProcessDetailsSummaryTest(t)
|
|
startedAt := "2026-08-10T08:00:00Z"
|
|
completedAt := "2026-08-10T08:12:59Z"
|
|
if _, err := db.Exec(
|
|
"UPDATE messages SET content = ?, created_at = ?, updated_at = ? WHERE id = ?",
|
|
"done", startedAt, completedAt, messageID,
|
|
); err != nil {
|
|
t.Fatalf("update message timing: %v", err)
|
|
}
|
|
|
|
summary, err := db.GetProcessDetailsSummary(messageID)
|
|
if err != nil {
|
|
t.Fatalf("GetProcessDetailsSummary: %v", err)
|
|
}
|
|
if summary.Status != "completed" {
|
|
t.Fatalf("status = %q, want completed", summary.Status)
|
|
}
|
|
if summary.StartedAt == nil || summary.CompletedAt == nil {
|
|
t.Fatalf("timing missing: %#v", summary)
|
|
}
|
|
if want := int64((12*time.Minute + 59*time.Second) / time.Millisecond); summary.DurationMs != want {
|
|
t.Fatalf("durationMs = %d, want %d", summary.DurationMs, want)
|
|
}
|
|
}
|
|
|
|
func TestProcessDetailsSummaryTreatsCancelledPlaceholderAsTerminal(t *testing.T) {
|
|
db, conversationID, messageID := setupProcessDetailsSummaryTest(t)
|
|
startedAt := "2026-08-10T08:00:00Z"
|
|
if _, err := db.Exec(
|
|
"UPDATE messages SET content = ?, created_at = ?, updated_at = ? WHERE id = ?",
|
|
"处理中...", startedAt, startedAt, messageID,
|
|
); err != nil {
|
|
t.Fatalf("update running placeholder: %v", err)
|
|
}
|
|
if _, err := db.Exec(`
|
|
INSERT INTO process_details (id, message_id, conversation_id, event_type, message, data, created_at)
|
|
VALUES ('cancelled-detail', ?, ?, 'cancelled', 'interrupted', '{}', '2026-08-10T08:02:05Z')`,
|
|
messageID, conversationID); err != nil {
|
|
t.Fatalf("insert cancelled detail: %v", err)
|
|
}
|
|
|
|
summary, err := db.GetProcessDetailsSummary(messageID)
|
|
if err != nil {
|
|
t.Fatalf("GetProcessDetailsSummary: %v", err)
|
|
}
|
|
if summary.Status != "cancelled" {
|
|
t.Fatalf("status = %q, want cancelled", summary.Status)
|
|
}
|
|
if summary.CompletedAt == nil {
|
|
t.Fatal("cancelled summary should expose a fixed completion time")
|
|
}
|
|
if want := int64((2*time.Minute + 5*time.Second) / time.Millisecond); summary.DurationMs != want {
|
|
t.Fatalf("durationMs = %d, want %d", summary.DurationMs, want)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|