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.
331 lines
12 KiB
Go
331 lines
12 KiB
Go
// 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])
|
||
}
|
||
}
|
||
}
|