210 lines
6.8 KiB
Go
210 lines
6.8 KiB
Go
package main
|
||
|
||
import (
|
||
"os"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
func TestAfterSalesResolveIssueArchivesKnowledgeCase(t *testing.T) {
|
||
cleanupAfterSalesKnowledgeTestFiles(t)
|
||
rebuildCh := stubAfterSalesKnowledgeRebuild(t)
|
||
|
||
engine := &AfterSalesIssueEngine{issues: []AfterSalesIssue{{
|
||
ID: "i1",
|
||
CreatedAt: "2026-06-04T09:00:00+08:00",
|
||
Status: afterSalesIssueStatusPending,
|
||
RoomName: "售后群",
|
||
CustomerName: "华南客户",
|
||
IssueContent: "镜头无法调焦",
|
||
AISuggestion: "检查调焦机构",
|
||
AssignedEngineerID: "engineer-a",
|
||
ImagePaths: []string{"a.jpg"},
|
||
}}}
|
||
|
||
knowledgeCase, err := engine.resolveIssue("i1", "已确认调焦环松动,重新固定后恢复。")
|
||
if err != nil {
|
||
t.Fatalf("resolve issue: %v", err)
|
||
}
|
||
if engine.issues[0].Status != afterSalesIssueStatusResolved {
|
||
t.Fatalf("expected resolved issue, got %#v", engine.issues[0])
|
||
}
|
||
if engine.issues[0].KnowledgeSourcePath == "" || engine.issues[0].KnowledgeArchivedAt == "" {
|
||
t.Fatalf("expected issue knowledge metadata, got %#v", engine.issues[0])
|
||
}
|
||
if knowledgeCase.IssueID != "i1" || knowledgeCase.ImageCount != 1 {
|
||
t.Fatalf("unexpected case: %#v", knowledgeCase)
|
||
}
|
||
|
||
data, err := os.ReadFile(knowledgeCase.MarkdownPath)
|
||
if err != nil {
|
||
t.Fatalf("read markdown: %v", err)
|
||
}
|
||
text := string(data)
|
||
for _, want := range []string{"问题ID:i1", "镜头无法调焦", "已确认调焦环松动", "检查调焦机构", "售后群", "华南客户"} {
|
||
if !strings.Contains(text, want) {
|
||
t.Fatalf("expected markdown to contain %q, got %s", want, text)
|
||
}
|
||
}
|
||
cases, err := listAfterSalesKnowledgeCases()
|
||
if err != nil {
|
||
t.Fatalf("list cases: %v", err)
|
||
}
|
||
if len(cases) != 1 || cases[0].IssueID != "i1" {
|
||
t.Fatalf("expected one listed case, got %#v", cases)
|
||
}
|
||
select {
|
||
case <-rebuildCh:
|
||
case <-time.After(time.Second):
|
||
t.Fatal("expected knowledge rebuild hook")
|
||
}
|
||
}
|
||
|
||
func TestAfterSalesResolveIssueRequiresResolution(t *testing.T) {
|
||
cleanupAfterSalesKnowledgeTestFiles(t)
|
||
rebuildCh := stubAfterSalesKnowledgeRebuild(t)
|
||
engine := &AfterSalesIssueEngine{issues: []AfterSalesIssue{{
|
||
ID: "i1",
|
||
Status: afterSalesIssueStatusPending,
|
||
RoomName: "售后群",
|
||
}}}
|
||
|
||
if _, err := engine.resolveIssue("i1", " "); err == nil {
|
||
t.Fatal("expected empty resolution error")
|
||
}
|
||
if engine.issues[0].Status != afterSalesIssueStatusPending {
|
||
t.Fatalf("expected status unchanged, got %#v", engine.issues[0])
|
||
}
|
||
select {
|
||
case <-rebuildCh:
|
||
t.Fatal("did not expect rebuild")
|
||
default:
|
||
}
|
||
}
|
||
|
||
func TestAfterSalesKnowledgeCaseOverwrite(t *testing.T) {
|
||
cleanupAfterSalesKnowledgeTestFiles(t)
|
||
stubAfterSalesKnowledgeRebuild(t)
|
||
engine := &AfterSalesIssueEngine{issues: []AfterSalesIssue{{
|
||
ID: "i1",
|
||
CreatedAt: "2026-06-04T09:00:00+08:00",
|
||
Status: afterSalesIssueStatusPending,
|
||
IssueContent: "图像模糊",
|
||
AISuggestion: "检查焦距",
|
||
}}}
|
||
|
||
first, err := engine.resolveIssue("i1", "第一次处理方案")
|
||
if err != nil {
|
||
t.Fatalf("first resolve: %v", err)
|
||
}
|
||
second, err := engine.resolveIssue("i1", "第二次处理方案")
|
||
if err != nil {
|
||
t.Fatalf("second resolve: %v", err)
|
||
}
|
||
if first.MarkdownPath != second.MarkdownPath {
|
||
t.Fatalf("expected same markdown path, got %s and %s", first.MarkdownPath, second.MarkdownPath)
|
||
}
|
||
cases, err := listAfterSalesKnowledgeCases()
|
||
if err != nil {
|
||
t.Fatalf("list cases: %v", err)
|
||
}
|
||
if len(cases) != 1 || cases[0].ResolutionContent != "第二次处理方案" {
|
||
t.Fatalf("expected overwritten single case, got %#v", cases)
|
||
}
|
||
data, err := os.ReadFile(second.MarkdownPath)
|
||
if err != nil {
|
||
t.Fatalf("read markdown: %v", err)
|
||
}
|
||
if strings.Contains(string(data), "第一次处理方案") || !strings.Contains(string(data), "第二次处理方案") {
|
||
t.Fatalf("expected markdown overwrite, got %s", string(data))
|
||
}
|
||
}
|
||
|
||
func TestAfterSalesKnowledgeSyncLegacyResolvedIssue(t *testing.T) {
|
||
cleanupAfterSalesKnowledgeTestFiles(t)
|
||
rebuildCh := stubAfterSalesKnowledgeRebuild(t)
|
||
engine := &AfterSalesIssueEngine{issues: []AfterSalesIssue{{
|
||
ID: "legacy",
|
||
CreatedAt: "2026-06-02T11:10:24+08:00",
|
||
UpdatedAt: "2026-06-04T11:03:34+08:00",
|
||
Status: afterSalesIssueStatusResolved,
|
||
RoomName: "刘羽、JM.、C",
|
||
CustomerName: "JM.",
|
||
IssueContent: "客户询问客服身份",
|
||
AISuggestion: "客户询问客服身份,需确认是否需要进一步解释或提供帮助",
|
||
AssignedEngineerID: "1688855899845302",
|
||
NotifyStatus: afterSalesNotifySent,
|
||
}}}
|
||
|
||
if err := engine.syncResolvedKnowledgeCases(); err != nil {
|
||
t.Fatalf("sync legacy resolved: %v", err)
|
||
}
|
||
cases, err := listAfterSalesKnowledgeCases()
|
||
if err != nil {
|
||
t.Fatalf("list cases: %v", err)
|
||
}
|
||
if len(cases) != 1 || cases[0].IssueID != "legacy" {
|
||
t.Fatalf("expected one synced legacy case, got %#v", cases)
|
||
}
|
||
if cases[0].ResolutionContent != "客户询问客服身份,需确认是否需要进一步解释或提供帮助" {
|
||
t.Fatalf("expected AI suggestion fallback, got %#v", cases[0])
|
||
}
|
||
if engine.issues[0].KnowledgeSourcePath == "" || engine.issues[0].ResolutionContent == "" {
|
||
t.Fatalf("expected issue backfill metadata, got %#v", engine.issues[0])
|
||
}
|
||
if !fileExists(cases[0].MarkdownPath) {
|
||
t.Fatalf("expected markdown file at %s", cases[0].MarkdownPath)
|
||
}
|
||
select {
|
||
case <-rebuildCh:
|
||
case <-time.After(time.Second):
|
||
t.Fatal("expected knowledge rebuild hook")
|
||
}
|
||
}
|
||
|
||
func TestAfterSalesKnowledgeCaseMissingMarkdownFlag(t *testing.T) {
|
||
cleanupAfterSalesKnowledgeTestFiles(t)
|
||
stubAfterSalesKnowledgeRebuild(t)
|
||
engine := &AfterSalesIssueEngine{issues: []AfterSalesIssue{{ID: "i1", Status: afterSalesIssueStatusPending, IssueContent: "报错"}}}
|
||
knowledgeCase, err := engine.resolveIssue("i1", "重启设备恢复")
|
||
if err != nil {
|
||
t.Fatalf("resolve issue: %v", err)
|
||
}
|
||
if err := os.Remove(knowledgeCase.MarkdownPath); err != nil {
|
||
t.Fatalf("remove markdown: %v", err)
|
||
}
|
||
cases, err := listAfterSalesKnowledgeCases()
|
||
if err != nil {
|
||
t.Fatalf("list cases: %v", err)
|
||
}
|
||
if len(cases) != 1 || !cases[0].MissingMarkdown {
|
||
t.Fatalf("expected missing markdown flag, got %#v", cases)
|
||
}
|
||
}
|
||
|
||
func stubAfterSalesKnowledgeRebuild(t *testing.T) chan struct{} {
|
||
t.Helper()
|
||
oldHook := afterSalesKnowledgeRebuildHook
|
||
ch := make(chan struct{}, 8)
|
||
afterSalesKnowledgeRebuildHook = func() {
|
||
ch <- struct{}{}
|
||
}
|
||
t.Cleanup(func() {
|
||
afterSalesKnowledgeRebuildHook = oldHook
|
||
})
|
||
return ch
|
||
}
|
||
|
||
func cleanupAfterSalesKnowledgeTestFiles(t *testing.T) {
|
||
t.Helper()
|
||
t.Cleanup(func() {
|
||
_ = os.Remove(afterSalesKnowledgeCasesPath())
|
||
_ = os.Remove(afterSalesIssuesPath())
|
||
_ = os.RemoveAll(afterSalesKnowledgeMarkdownDir())
|
||
})
|
||
_ = os.Remove(afterSalesKnowledgeCasesPath())
|
||
_ = os.Remove(afterSalesIssuesPath())
|
||
_ = os.RemoveAll(afterSalesKnowledgeMarkdownDir())
|
||
}
|