Files
agent-desk-ng/internal/services/knowledge_faq_excel_test.go
mono 2f7a491666 feat: harden support, localize knowledge and add operator availability [skip ci]
Source-only publication of reviewed commit 64e96a7f423673dc29ad56485be9fc79bce8af1c.

Passed in isolation: focused frontend/backend tests, race checks, TypeScript typecheck, lint, Go compilation, Flowgram build, and deterministic SDK build/tests.

Not accepted as a production release: Next.js export/native assembly remains blocked by environment memory limits; browser interaction and MySQL staging acceptance remain unverified. No production server, database/schema, runtime configuration, deployment, release tag, or workflow execution was performed.

Expected source tree: 0a4a7528e9.
2026-10-16 15:21:42 +00:00

331 lines
12 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Modified in the independent AgentDesk fork: localized knowledge workflows and safe display errors.
package services
import (
"bytes"
"encoding/json"
"reflect"
"testing"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/repositories"
"github.com/glebarez/sqlite"
"github.com/mlogclub/simple/sqls"
"github.com/xuri/excelize/v2"
"gorm.io/gorm"
)
func TestKnowledgeFAQExcelTemplateContainsExpectedHeaders(t *testing.T) {
file, err := KnowledgeFAQService.BuildKnowledgeFAQImportTemplate("zh-CN")
if err != nil {
t.Fatalf("BuildKnowledgeFAQImportTemplate() error = %v", err)
}
workbook, err := excelize.OpenReader(bytes.NewReader(file.Data))
if err != nil {
t.Fatalf("open template workbook: %v", err)
}
defer workbook.Close()
rows, err := workbook.GetRows("FAQ")
if err != nil {
t.Fatalf("get FAQ rows: %v", err)
}
if len(rows) == 0 {
t.Fatal("template has no rows")
}
want := []string{"标准问题", "答案", "相似问", "备注"}
for i, value := range want {
if rows[0][i] != value {
t.Fatalf("header %d = %q, want %q", i, rows[0][i], value)
}
}
}
func TestKnowledgeFAQExcelExportWritesSimilarQuestionsWithLineBreaks(t *testing.T) {
setupKnowledgeFAQExcelTestDB(t)
kb := createKnowledgeFAQExcelTestBase(t, enums.StatusOk)
similarQuestions, _ := json.Marshal([]string{"怎么重置密码", "忘记密码怎么办"})
if err := repositories.KnowledgeFAQRepository.Create(sqls.DB(), &models.KnowledgeFAQ{
KnowledgeBaseID: kb.ID,
Question: "如何重置密码?",
Answer: "进入个人设置后点击重置密码。",
SimilarQuestions: string(similarQuestions),
Status: enums.StatusOk,
IndexStatus: enums.KnowledgeDocumentIndexStatusPending,
Remark: "账号",
}); err != nil {
t.Fatalf("create faq: %v", err)
}
file, err := KnowledgeFAQService.ExportKnowledgeFAQs(kb.ID)
if err != nil {
t.Fatalf("ExportKnowledgeFAQs() error = %v", err)
}
workbook, err := excelize.OpenReader(bytes.NewReader(file.Data))
if err != nil {
t.Fatalf("open export workbook: %v", err)
}
defer workbook.Close()
cell, err := workbook.GetCellValue("FAQ", "C2")
if err != nil {
t.Fatalf("get C2: %v", err)
}
if cell != "怎么重置密码\n忘记密码怎么办" {
t.Fatalf("similar questions cell = %q", cell)
}
}
func TestImportKnowledgeFAQsAppendSkipsExistingQuestion(t *testing.T) {
setupKnowledgeFAQExcelTestDB(t)
restore := stubKnowledgeFAQImportIndex(t)
defer restore()
kb := createKnowledgeFAQExcelTestBase(t, enums.StatusOk)
if err := repositories.KnowledgeFAQRepository.Create(sqls.DB(), &models.KnowledgeFAQ{
KnowledgeBaseID: kb.ID,
Question: "如何重置密码?",
Answer: "旧答案",
Status: enums.StatusOk,
IndexStatus: enums.KnowledgeDocumentIndexStatusIndexed,
}); err != nil {
t.Fatalf("create faq: %v", err)
}
result, err := KnowledgeFAQService.ImportKnowledgeFAQs(request.ImportKnowledgeFAQRequest{
KnowledgeBaseID: kb.ID,
Mode: request.KnowledgeFAQImportModeAppend,
Filename: "faq.xlsx",
Reader: bytes.NewReader(buildKnowledgeFAQExcelWorkbook(t, [][]string{
{"标准问题", "答案", "相似问", "备注"},
{"如何重置密码?", "新答案", "重置密码在哪里\n忘记密码", "账号"},
{"支持哪些渠道?", "支持网页和企业微信。", "", "渠道"},
})),
}, knowledgeFAQExcelTestOperator())
if err != nil {
t.Fatalf("ImportKnowledgeFAQs() error = %v", err)
}
if result.Created != 1 || result.Updated != 0 || result.Skipped != 1 || result.Failed != 0 {
t.Fatalf("result = %+v", result)
}
list := repositories.KnowledgeFAQRepository.Find(sqls.DB(), sqls.NewCnd().Eq("knowledge_base_id", kb.ID))
if len(list) != 2 {
t.Fatalf("faq count = %d, want 2", len(list))
}
existing := repositories.KnowledgeFAQRepository.Find(sqls.DB(), sqls.NewCnd().Eq("question", "如何重置密码?"))
if len(existing) != 1 || existing[0].Answer != "旧答案" {
t.Fatalf("existing faq changed: %+v", existing)
}
}
func TestImportKnowledgeFAQsOverwriteUpdatesExistingQuestion(t *testing.T) {
setupKnowledgeFAQExcelTestDB(t)
restore := stubKnowledgeFAQImportIndex(t)
defer restore()
kb := createKnowledgeFAQExcelTestBase(t, enums.StatusDisabled)
if err := repositories.KnowledgeFAQRepository.Create(sqls.DB(), &models.KnowledgeFAQ{
KnowledgeBaseID: kb.ID,
Question: "如何重置密码?",
Answer: "旧答案",
Status: enums.StatusOk,
IndexStatus: enums.KnowledgeDocumentIndexStatusIndexed,
}); err != nil {
t.Fatalf("create faq: %v", err)
}
result, err := KnowledgeFAQService.ImportKnowledgeFAQs(request.ImportKnowledgeFAQRequest{
KnowledgeBaseID: kb.ID,
Mode: request.KnowledgeFAQImportModeOverwrite,
Filename: "faq.xlsx",
Reader: bytes.NewReader(buildKnowledgeFAQExcelWorkbook(t, [][]string{
{"标准问题", "答案", "相似问", "备注"},
{"如何重置密码?", "新答案", "重置密码在哪里\n重置密码在哪里\n忘记密码", "账号"},
{"支持哪些渠道?", "支持网页和企业微信。", "", "渠道"},
})),
}, knowledgeFAQExcelTestOperator())
if err != nil {
t.Fatalf("ImportKnowledgeFAQs() error = %v", err)
}
if result.Created != 1 || result.Updated != 1 || result.Skipped != 0 || result.Failed != 0 {
t.Fatalf("result = %+v", result)
}
updated := repositories.KnowledgeFAQRepository.Find(sqls.DB(), sqls.NewCnd().Eq("question", "如何重置密码?"))
if len(updated) != 1 {
t.Fatalf("updated faq count = %d", len(updated))
}
if updated[0].Answer != "新答案" {
t.Fatalf("updated answer = %q", updated[0].Answer)
}
if updated[0].IndexStatus != enums.KnowledgeDocumentIndexStatusPending {
t.Fatalf("index status = %q", updated[0].IndexStatus)
}
var similar []string
if err := json.Unmarshal([]byte(updated[0].SimilarQuestions), &similar); err != nil {
t.Fatalf("unmarshal similar questions: %v", err)
}
if len(similar) != 2 || similar[0] != "重置密码在哪里" || similar[1] != "忘记密码" {
t.Fatalf("similar questions = %#v", similar)
}
created := repositories.KnowledgeFAQRepository.Find(sqls.DB(), sqls.NewCnd().Eq("question", "支持哪些渠道?"))
if len(created) != 1 || created[0].Status != enums.StatusDisabled {
t.Fatalf("created faq = %+v", created)
}
}
func setupKnowledgeFAQExcelTestDB(t *testing.T) {
t.Helper()
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatalf("open sqlite db: %v", err)
}
if err := db.AutoMigrate(&models.KnowledgeBase{}, &models.KnowledgeFAQ{}, &models.KnowledgeChunk{}); err != nil {
t.Fatalf("auto migrate: %v", err)
}
sqls.SetDB(db)
}
func createKnowledgeFAQExcelTestBase(t *testing.T, status enums.Status) *models.KnowledgeBase {
t.Helper()
item := &models.KnowledgeBase{
Name: "FAQ",
KnowledgeType: string(enums.KnowledgeBaseTypeFAQ),
Status: status,
}
if err := repositories.KnowledgeBaseRepository.Create(sqls.DB(), item); err != nil {
t.Fatalf("create knowledge base: %v", err)
}
return item
}
func buildKnowledgeFAQExcelWorkbook(t *testing.T, rows [][]string) []byte {
t.Helper()
workbook := excelize.NewFile()
defer workbook.Close()
sheet := workbook.GetSheetName(0)
if err := workbook.SetSheetName(sheet, "FAQ"); err != nil {
t.Fatalf("set sheet name: %v", err)
}
for rowIndex, row := range rows {
for colIndex, value := range row {
cell, err := excelize.CoordinatesToCellName(colIndex+1, rowIndex+1)
if err != nil {
t.Fatalf("cell name: %v", err)
}
if err := workbook.SetCellValue("FAQ", cell, value); err != nil {
t.Fatalf("set cell value: %v", err)
}
}
}
var buf bytes.Buffer
if err := workbook.Write(&buf); err != nil {
t.Fatalf("write workbook: %v", err)
}
return buf.Bytes()
}
func knowledgeFAQExcelTestOperator() *dto.AuthPrincipal {
return &dto.AuthPrincipal{UserID: 1, Username: "admin"}
}
func stubKnowledgeFAQImportIndex(t *testing.T) func() {
t.Helper()
original := indexKnowledgeFAQAfterImport
indexed := make([]int64, 0)
indexKnowledgeFAQAfterImport = func(faqID int64) error {
indexed = append(indexed, faqID)
return nil
}
return func() {
if len(indexed) == 0 {
t.Fatalf("expected imported FAQs to be indexed")
}
indexKnowledgeFAQAfterImport = original
}
}
func TestKnowledgeFAQExcelLocalizedTemplatesRoundTrip(t *testing.T) {
for _, tc := range []struct {
locale string
headers []string
question string
}{
{"en-US", []string{"Primary question", "Answer", "Similar questions", "Notes"}, "How do I reset my password?"},
{"ru-RU", []string{"Основной вопрос", "Ответ", "Похожие вопросы", "Примечания"}, "Как сбросить пароль?"},
{"zh-CN", []string{"标准问题", "答案", "相似问", "备注"}, "如何重置密码?"},
} {
t.Run(tc.locale, func(t *testing.T) {
file, err := KnowledgeFAQService.BuildKnowledgeFAQImportTemplate(tc.locale)
if err != nil {
t.Fatal(err)
}
workbook, err := excelize.OpenReader(bytes.NewReader(file.Data))
if err != nil {
t.Fatal(err)
}
defer workbook.Close()
table, err := workbook.GetRows("FAQ")
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(table[0], tc.headers) || table[1][0] != tc.question {
t.Fatalf("wrong template: %#v", table)
}
rows, result, err := parseKnowledgeFAQImportRows(bytes.NewReader(file.Data), tc.locale)
if err != nil || result.Failed != 0 || len(rows) != 1 || rows[0].Question != tc.question {
t.Fatalf("round trip = %+v, %+v, %v", rows, result, err)
}
})
}
}
func TestKnowledgeFAQExcelImportAcceptsLegacyAndRussianHeaders(t *testing.T) {
for _, headers := range [][]string{
{"question", "answer", "similarQuestions", "remark"},
{"标准问题", "答案", "相似问", "备注"},
{"\ufeffОсновной вопрос", "ОТВЕТ", "Похожие вопросы", "Примечания"},
{"Вопрос", "Ответ"},
} {
question, answer := " Author question 原文 ", "\n Answer 回答:原文\nnext "
data := buildKnowledgeFAQExcelWorkbook(t, [][]string{headers, {question, answer}})
rows, result, err := parseKnowledgeFAQImportRows(bytes.NewReader(data), "ru-RU")
if err != nil || result.Failed != 0 || len(rows) != 1 {
t.Fatalf("parse %v = %+v, %v", headers, result, err)
}
if rows[0].Question != question || rows[0].Answer != answer {
t.Fatalf("authored data modified: %+v", rows[0])
}
if len(rows[0].SimilarQuestions) != 0 || rows[0].Remark != "" {
t.Fatalf("missing optional columns copied question: %+v", rows[0])
}
}
}
func TestKnowledgeFAQExcelLocalizedExportPreservesRows(t *testing.T) {
setupKnowledgeFAQExcelTestDB(t)
kb := createKnowledgeFAQExcelTestBase(t, enums.StatusOk)
item := &models.KnowledgeFAQ{KnowledgeBaseID: kb.ID, Question: " Original 问题 ", Answer: "\nОтвет <tag>原文</tag> ", SimilarQuestions: `[" same question ","相似问"]`, Remark: " Notes 原文 ", Status: enums.StatusOk}
if err := repositories.KnowledgeFAQRepository.Create(sqls.DB(), item); err != nil {
t.Fatal(err)
}
for _, locale := range []string{"en-US", "ru-RU"} {
file, err := KnowledgeFAQService.ExportKnowledgeFAQs(kb.ID, locale)
if err != nil {
t.Fatal(err)
}
rows, result, err := parseKnowledgeFAQImportRows(bytes.NewReader(file.Data), locale)
if err != nil || result.Failed != 0 || len(rows) != 1 {
t.Fatalf("export round trip: %+v, %v", result, err)
}
if rows[0].Question != item.Question || rows[0].Answer != item.Answer || rows[0].Remark != item.Remark || !reflect.DeepEqual(rows[0].SimilarQuestions, []string{" same question ", "相似问"}) {
t.Fatalf("row changed: %+v", rows[0])
}
}
}