diff --git a/docs b/docs index 18dc676..de2f19d 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit 18dc676a5f6d84a4c8a0497b3205651de7f634e7 +Subproject commit de2f19d644b8f971390a5886ff9a2ad9cb994a41 diff --git a/internal/ai/rag/index_retrieve_helpers_test.go b/internal/ai/rag/index_retrieve_helpers_test.go new file mode 100644 index 0000000..bd2bbe5 --- /dev/null +++ b/internal/ai/rag/index_retrieve_helpers_test.go @@ -0,0 +1,91 @@ +package rag + +import ( + "strings" + "testing" + + "cs-agent/internal/models" +) + +func TestBuildFAQChunkContent(t *testing.T) { + faq := &models.KnowledgeFAQ{ + Question: "如何退款", + SimilarQuestions: `["退款怎么申请","申请售后"]`, + Answer: "在订单页发起退款。", + } + + content := buildFAQChunkContent(faq) + if !strings.Contains(content, "问题:如何退款") { + t.Fatalf("expected question in content, got %q", content) + } + if !strings.Contains(content, "相似问:退款怎么申请;申请售后") { + t.Fatalf("expected similar questions in content, got %q", content) + } + if !strings.Contains(content, "回答:在订单页发起退款。") { + t.Fatalf("expected answer in content, got %q", content) + } +} + +func TestBuildFAQChunkModel(t *testing.T) { + knowledgeBase := &models.KnowledgeBase{ID: 11} + faq := &models.KnowledgeFAQ{ID: 22, Question: "如何退款"} + + chunk, chunkID := buildFAQChunkModel(knowledgeBase, faq, "问题:如何退款\n回答:在订单页发起退款。") + if chunkID == "" { + t.Fatalf("expected chunk id") + } + if chunk.KnowledgeBaseID != 11 || chunk.FaqID != 22 { + t.Fatalf("unexpected chunk identity: %#v", chunk) + } + if chunk.VectorID != chunkID { + t.Fatalf("expected vector id to match chunk id") + } + if chunk.Title != "如何退款" { + t.Fatalf("unexpected title: %q", chunk.Title) + } +} + +func TestNormalizeContextResultsMergesAndDedupes(t *testing.T) { + results := normalizeContextResults([]RetrieveResult{ + {DocumentID: 1, ChunkNo: 1, SectionPath: "A", Content: "第一段", Score: 0.7}, + {DocumentID: 1, ChunkNo: 2, SectionPath: "A", Content: "第二段", Score: 0.9}, + {DocumentID: 1, ChunkNo: 3, SectionPath: "A", Content: "第三段", Score: 0.6}, + {DocumentID: 2, ChunkNo: 1, Title: "标题", Content: "独立段", Score: 0.5}, + {FaqID: 9, FaqQuestion: "FAQ", Content: "FAQ内容", Score: 0.8}, + {FaqID: 9, FaqQuestion: "FAQ", Content: "FAQ重复", Score: 0.7}, + }) + + if len(results) != 3 { + t.Fatalf("expected 3 normalized results, got %d", len(results)) + } + if !strings.Contains(results[0].Content, "第一段\n第二段") { + t.Fatalf("expected adjacent document chunks to merge, got %q", results[0].Content) + } + if results[0].Score != 0.9 { + t.Fatalf("expected merged score to keep max score, got %v", results[0].Score) + } + if results[1].Content != "独立段" { + t.Fatalf("expected section duplicates to be removed after merge, got %q", results[1].Content) + } +} + +func TestBuildContextChunkText(t *testing.T) { + faqText := buildContextChunkText(RetrieveResult{ + FaqID: 1, + FaqQuestion: "如何退款", + Content: "在订单页发起退款。", + }) + if !strings.Contains(faqText, "【FAQ:如何退款】") { + t.Fatalf("unexpected faq context text: %q", faqText) + } + + docText := buildContextChunkText(RetrieveResult{ + DocumentID: 2, + DocumentTitle: "退款文档", + SectionPath: "售后/退款", + Content: "文档内容", + }) + if !strings.Contains(docText, "【文档:退款文档|章节:售后/退款】") { + t.Fatalf("unexpected document context text: %q", docText) + } +} diff --git a/internal/ai/runtime/reply_helpers_test.go b/internal/ai/runtime/reply_helpers_test.go new file mode 100644 index 0000000..803b381 --- /dev/null +++ b/internal/ai/runtime/reply_helpers_test.go @@ -0,0 +1,92 @@ +package runtime + +import ( + "strings" + "testing" + + "cs-agent/internal/pkg/toolx" +) + +func TestSummaryPrimaryToolCodePrefersToolSearchTarget(t *testing.T) { + summary := &Summary{ + InvokedToolCodes: []string{toolx.BuiltinToolSearch.Code}, + TraceData: `{ + "toolSearch": { + "items": [ + {"targetToolCode":"mcp/server/tool_a"} + ] + } + }`, + } + + if got := summaryPrimaryToolCode(summary); got != "mcp/server/tool_a" { + t.Fatalf("unexpected primary tool code: %q", got) + } +} + +func TestToRunLogFinalAction(t *testing.T) { + if got := toRunLogFinalAction(&Summary{PlannedSkillCode: "refund", ReplyText: "ok"}); got != "skill" { + t.Fatalf("expected skill final action, got %q", got) + } + + graphSummary := &Summary{ + ReplyText: "ok", + TraceData: `{ + "graphTools": { + "items": [ + {"toolCode":"` + toolx.GraphAnalyzeConversation.Code + `"} + ] + } + }`, + } + if got := toRunLogFinalAction(graphSummary); got != "graph" { + t.Fatalf("expected graph final action, got %q", got) + } + + if got := toRunLogFinalAction(&Summary{Status: "fallback"}); got != "fallback" { + t.Fatalf("expected fallback final action, got %q", got) + } +} + +func TestExtractInterruptMessageAndCheckpointError(t *testing.T) { + if got := extractInterruptMessage(`{"message":"请补充订单号"}`); got != "请补充订单号" { + t.Fatalf("unexpected interrupt message: %q", got) + } + if got := extractInterruptMessage("not-json"); got != "" { + t.Fatalf("expected empty message for invalid json, got %q", got) + } + + err := fakeErr("Failed to load from checkpoint: record does not exist") + if !isCheckpointMissingError(err) { + t.Fatalf("expected checkpoint missing error to be detected") + } + if isCheckpointMissingError(fakeErr("other error")) { + t.Fatalf("expected unrelated error to be ignored") + } +} + +func TestGraphPlanReason(t *testing.T) { + summary := &Summary{ + TraceData: `{ + "graphTools": { + "items": [ + { + "toolCode":"` + toolx.GraphTriageServiceRequest.Code + `", + "recommendedAction":"create_ticket", + "ticketDraftReady": true + } + ] + } + }`, + } + got := graphPlanReason(summary) + if !strings.Contains(got, "create_ticket") || !strings.Contains(got, "ready ticket draft") { + t.Fatalf("unexpected graph plan reason: %q", got) + } +} + +type fakeErr string + +func (e fakeErr) Error() string { + return string(e) +}