Initial qiwei secondary development handoff

This commit is contained in:
2026-06-23 21:11:20 +08:00
commit 858cb68f4f
207 changed files with 52782 additions and 0 deletions

View File

@@ -0,0 +1,209 @@
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{"问题IDi1", "镜头无法调焦", "已确认调焦环松动", "检查调焦机构", "售后群", "华南客户"} {
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())
}