Files
qiweimanager-master/helper/after_sales_test.go

676 lines
23 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.
package main
import (
"errors"
"os"
"path/filepath"
"strings"
"testing"
"time"
"qiweimanager/config"
)
func TestAfterSalesAtomicJSONReadWrite(t *testing.T) {
path := filepath.Join(t.TempDir(), "issues.json")
want := []AfterSalesIssue{{
ID: "issue-1",
RoomName: "VIP客户售后群-001",
CustomerName: "张三",
Status: afterSalesIssueStatusPending,
}}
if err := atomicWriteJSON(path, want); err != nil {
t.Fatalf("atomicWriteJSON failed: %v", err)
}
var got []AfterSalesIssue
if err := readJSONFile(path, &got); err != nil {
t.Fatalf("readJSONFile failed: %v", err)
}
if len(got) != 1 || got[0].ID != want[0].ID || got[0].CustomerName != want[0].CustomerName {
t.Fatalf("unexpected issues: %#v", got)
}
badPath := filepath.Join(t.TempDir(), "bad.json")
if err := os.WriteFile(badPath, []byte("{bad json"), 0644); err != nil {
t.Fatalf("write bad json: %v", err)
}
if err := readJSONFile(badPath, &got); err == nil {
t.Fatal("expected bad JSON to return an error")
}
}
func TestParseAfterSalesAIResponseFromMarkdownFence(t *testing.T) {
text := "```json\n" +
`[{"room_name":"售后群","customer_user_id":"wm_1","customer_name":"李四","issue_content":"软件无法登录","image_paths":["C:\\tmp\\a.png",""],"image_refs":["cdn-1"],"ai_suggestion":"建议先检查网络。","source_message_ids":["m1"],"confidence":0.82}]` +
"\n```"
got, err := parseAfterSalesAIResponse(text)
if err != nil {
t.Fatalf("parseAfterSalesAIResponse failed: %v", err)
}
if len(got) != 1 {
t.Fatalf("expected 1 candidate, got %d", len(got))
}
if got[0].CustomerName != "李四" || got[0].IssueContent != "软件无法登录" {
t.Fatalf("unexpected candidate: %#v", got[0])
}
if len(got[0].ImagePaths) != 1 || got[0].ImagePaths[0] != `C:\tmp\a.png` {
t.Fatalf("image paths were not normalized: %#v", got[0].ImagePaths)
}
}
func TestAfterSalesCollectUsesFirstHourWindowAndAdvancesOnSuccess(t *testing.T) {
restoreAutoReply, restoreCollector := installAfterSalesCollectTestHooks(t)
defer restoreAutoReply()
defer restoreCollector()
now := time.Now()
engine := &AfterSalesIssueEngine{
messages: []AfterSalesMessage{
{
MessageID: "old",
ConversationID: "room-1",
RoomName: "售后群",
SenderUserID: "wm_1",
SenderName: "张三",
SenderIdentity: senderIdentityExternal,
Content: "两小时前的问题",
MessageType: "text",
SendTime: now.Add(-2 * time.Hour).Unix(),
ReceivedAt: now.Add(-2 * time.Hour).Unix(),
},
{
MessageID: "new",
ConversationID: "room-1",
RoomName: "售后群",
SenderUserID: "wm_1",
SenderName: "张三",
SenderIdentity: senderIdentityExternal,
Content: "刚才登录报错",
MessageType: "text",
SendTime: now.Add(-30 * time.Minute).Unix(),
ReceivedAt: now.Add(-30 * time.Minute).Unix(),
},
},
}
var seen []AfterSalesMessage
afterSalesAICollector = func(aiCfg config.AIConfig, messages []AfterSalesMessage) ([]afterSalesAIIssueCandidate, error) {
seen = append([]AfterSalesMessage(nil), messages...)
return []afterSalesAIIssueCandidate{{
RoomName: "售后群",
CustomerUserID: "wm_1",
CustomerName: "张三",
IssueContent: "登录报错",
AISuggestion: "建议先核对网络和账号状态。",
SourceMessageIDs: []string{"new"},
Confidence: 0.9,
}}, nil
}
engine.collectLockedAsync("", false)
if engine.state.LastCollectAt == 0 {
t.Fatal("expected successful collect to advance LastCollectAt")
}
if engine.state.LastAddedCount != 1 || len(engine.issues) != 1 {
t.Fatalf("expected one added issue, state=%#v issues=%#v", engine.state, engine.issues)
}
if len(seen) != 1 || seen[0].MessageID != "new" {
t.Fatalf("first collection should only analyze the latest hour, saw %#v", seen)
}
if engine.state.LastError != "" {
t.Fatalf("unexpected LastError: %s", engine.state.LastError)
}
}
func TestAfterSalesCollectDoesNotAdvanceOnFailure(t *testing.T) {
restoreAutoReply, restoreCollector := installAfterSalesCollectTestHooks(t)
defer restoreAutoReply()
defer restoreCollector()
now := time.Now()
engine := &AfterSalesIssueEngine{
messages: []AfterSalesMessage{{
MessageID: "m1",
ConversationID: "room-1",
RoomName: "售后群",
SenderUserID: "wm_1",
SenderName: "张三",
SenderIdentity: senderIdentityExternal,
Content: "登录报错",
MessageType: "text",
SendTime: now.Add(-10 * time.Minute).Unix(),
ReceivedAt: now.Add(-10 * time.Minute).Unix(),
}},
}
afterSalesAICollector = func(aiCfg config.AIConfig, messages []AfterSalesMessage) ([]afterSalesAIIssueCandidate, error) {
return nil, errors.New("AI failed")
}
engine.collectLockedAsync("", false)
if engine.state.LastCollectAt != 0 {
t.Fatalf("failed collect must not advance LastCollectAt, got %d", engine.state.LastCollectAt)
}
if engine.state.LastError == "" {
t.Fatal("expected LastError after failed collect")
}
}
func TestAfterSalesManualCollectScansAllCachedMessages(t *testing.T) {
restoreAutoReply, restoreCollector := installAfterSalesCollectTestHooks(t)
defer restoreAutoReply()
defer restoreCollector()
now := time.Now()
engine := &AfterSalesIssueEngine{
state: AfterSalesCollectState{LastCollectAt: now.Add(-10 * time.Minute).Unix()},
messages: []AfterSalesMessage{
{
MessageID: "old-room-1",
ConversationID: "room-1",
RoomName: "Room 1",
SenderUserID: "wm_1",
SenderName: "Customer 1",
SenderIdentity: senderIdentityExternal,
Content: "old issue",
MessageType: "text",
SendTime: now.Add(-3 * time.Hour).Unix(),
ReceivedAt: now.Add(-3 * time.Hour).Unix(),
},
{
MessageID: "new-room-2",
ConversationID: "room-2",
RoomName: "Room 2",
SenderUserID: "wm_2",
SenderName: "Customer 2",
SenderIdentity: senderIdentityExternal,
Content: "new issue",
MessageType: "text",
SendTime: now.Add(-5 * time.Minute).Unix(),
ReceivedAt: now.Add(-5 * time.Minute).Unix(),
},
},
}
var seen []AfterSalesMessage
afterSalesAICollector = func(aiCfg config.AIConfig, messages []AfterSalesMessage) ([]afterSalesAIIssueCandidate, error) {
seen = append(seen, messages...)
return nil, nil
}
added, scanned, err := engine.collectNow("", true)
if err != nil {
t.Fatalf("manual collect failed: %v", err)
}
if added != 0 || scanned != 2 || len(seen) != 2 {
t.Fatalf("expected manual collect to scan all cached messages, added=%d scanned=%d seen=%#v", added, scanned, seen)
}
}
func TestAfterSalesManualCollectFiltersSelectedGroup(t *testing.T) {
restoreAutoReply, restoreCollector := installAfterSalesCollectTestHooks(t)
defer restoreAutoReply()
defer restoreCollector()
now := time.Now()
engine := &AfterSalesIssueEngine{
messages: []AfterSalesMessage{
{
MessageID: "room-1-msg",
ConversationID: "room-1",
RoomName: "Room 1",
SenderUserID: "wm_1",
SenderName: "Customer 1",
SenderIdentity: senderIdentityExternal,
Content: "room 1 issue",
MessageType: "text",
SendTime: now.Add(-30 * time.Minute).Unix(),
ReceivedAt: now.Add(-30 * time.Minute).Unix(),
},
{
MessageID: "room-2-msg",
ConversationID: "room-2",
RoomName: "Room 2",
SenderUserID: "wm_2",
SenderName: "Customer 2",
SenderIdentity: senderIdentityExternal,
Content: "room 2 issue",
MessageType: "text",
SendTime: now.Add(-20 * time.Minute).Unix(),
ReceivedAt: now.Add(-20 * time.Minute).Unix(),
},
},
}
var seen []AfterSalesMessage
afterSalesAICollector = func(aiCfg config.AIConfig, messages []AfterSalesMessage) ([]afterSalesAIIssueCandidate, error) {
seen = append([]AfterSalesMessage(nil), messages...)
return nil, nil
}
_, scanned, err := engine.collectNow("room-2", true)
if err != nil {
t.Fatalf("selected manual collect failed: %v", err)
}
if scanned != 1 || len(seen) != 1 || seen[0].MessageID != "room-2-msg" {
t.Fatalf("expected only selected group message, scanned=%d seen=%#v", scanned, seen)
}
}
func TestAfterSalesManualCollectDoesNotAdvanceAutoCursor(t *testing.T) {
restoreAutoReply, restoreCollector := installAfterSalesCollectTestHooks(t)
defer restoreAutoReply()
defer restoreCollector()
now := time.Now()
lastCollectAt := now.Add(-15 * time.Minute).Unix()
engine := &AfterSalesIssueEngine{
state: AfterSalesCollectState{Collecting: true, LastCollectAt: lastCollectAt},
messages: []AfterSalesMessage{{
MessageID: "m1",
ConversationID: "room-1",
RoomName: "Room 1",
SenderUserID: "wm_1",
SenderName: "Customer",
SenderIdentity: senderIdentityExternal,
Content: "old cached issue",
MessageType: "text",
SendTime: now.Add(-2 * time.Hour).Unix(),
ReceivedAt: now.Add(-2 * time.Hour).Unix(),
}},
}
afterSalesAICollector = func(aiCfg config.AIConfig, messages []AfterSalesMessage) ([]afterSalesAIIssueCandidate, error) {
return nil, nil
}
engine.collectLockedAsync("", true)
if engine.state.LastCollectAt != lastCollectAt {
t.Fatalf("manual collect should not advance automatic cursor, got %d want %d", engine.state.LastCollectAt, lastCollectAt)
}
if engine.state.Collecting {
t.Fatal("manual collect should clear collecting flag")
}
}
func TestAfterSalesManualCollectSelectedGroupNoCachedMessages(t *testing.T) {
engine := &AfterSalesIssueEngine{}
added, scanned, err := engine.collectNow("room-missing", true)
if err != nil {
t.Fatalf("empty selected collect failed: %v", err)
}
if added != 0 || scanned != 0 {
t.Fatalf("expected no additions for empty selected group, added=%d scanned=%d", added, scanned)
}
if msg := afterSalesCollectEmptyMessage("room-missing", true, scanned); msg == "" {
t.Fatal("expected empty selected group message")
}
}
func TestAfterSalesHistoryImportParsesCopiedText(t *testing.T) {
base := time.Date(2026, 5, 28, 15, 30, 0, 0, time.Local)
raw := "郑悦滨 2026-05-28 15:13 那玩意儿没电了,存不住数据。\n你量一下电压肯定低于3V了。\n梁锦明 15:14 郑工\n郑悦滨梁师傅好"
messages := parseAfterSalesHistoryMessages("R:room-1", "设备售后问题交流群", raw, base)
if len(messages) != 3 {
t.Fatalf("expected 3 parsed messages, got %d: %#v", len(messages), messages)
}
if messages[0].SenderName != "郑悦滨" || !strings.Contains(messages[0].Content, "存不住数据") || !strings.Contains(messages[0].Content, "低于3V") {
t.Fatalf("unexpected first message: %#v", messages[0])
}
if messages[2].SenderName != "郑悦滨" || messages[2].Content != "梁师傅好" {
t.Fatalf("colon format was not parsed: %#v", messages[2])
}
}
func TestAfterSalesHistoryImportDedupesMessages(t *testing.T) {
engine := &AfterSalesIssueEngine{}
raw := "Customer " + time.Now().Format("2006-01-02") + " 15:13 device broken"
messages := parseAfterSalesHistoryMessages("room-1", "Room 1", raw, time.Now())
first := engine.mergeHistoryMessages(messages)
second := engine.mergeHistoryMessages(messages)
if first != 1 || second != 0 {
t.Fatalf("expected first import only, first=%d second=%d messages=%#v", first, second, engine.messages)
}
if engine.state.MessageBufferCount != 1 {
t.Fatalf("expected message buffer count 1, got %d", engine.state.MessageBufferCount)
}
}
func TestAfterSalesHistoryImportCollectsIssue(t *testing.T) {
restoreAutoReply, restoreCollector := installAfterSalesCollectTestHooks(t)
defer restoreAutoReply()
defer restoreCollector()
engine := &AfterSalesIssueEngine{}
afterSalesAICollector = func(aiCfg config.AIConfig, messages []AfterSalesMessage) ([]afterSalesAIIssueCandidate, error) {
if len(messages) != 1 || messages[0].ConversationID != "room-1" {
t.Fatalf("unexpected imported messages sent to collector: %#v", messages)
}
return []afterSalesAIIssueCandidate{{
CustomerUserID: messages[0].SenderUserID,
CustomerName: messages[0].SenderName,
IssueContent: "device cannot store data",
SourceMessageIDs: []string{messages[0].MessageID},
Confidence: 0.9,
}}, nil
}
imported, added, err := engine.importHistoryAndCollect(AfterSalesHistoryImportRequest{
ConversationID: "room-1",
RoomName: "Room 1",
RawText: "Customer 2026-05-28 15:13 device cannot store data",
})
if err != nil {
t.Fatalf("history import failed: %v", err)
}
if imported != 1 || added != 1 || len(engine.issues) != 1 {
t.Fatalf("expected imported issue, imported=%d added=%d issues=%#v", imported, added, engine.issues)
}
}
func TestAfterSalesHistoryImportSegmentsMultipleIssues(t *testing.T) {
restoreAutoReply, restoreCollector := installAfterSalesCollectTestHooks(t)
defer restoreAutoReply()
defer restoreCollector()
engine := &AfterSalesIssueEngine{}
calls := 0
afterSalesAICollector = func(aiCfg config.AIConfig, messages []AfterSalesMessage) ([]afterSalesAIIssueCandidate, error) {
calls++
if len(messages) != 1 {
t.Fatalf("expected one imported issue segment per AI call, got %#v", messages)
}
msg := messages[0]
return []afterSalesAIIssueCandidate{{
CustomerUserID: msg.SenderUserID,
CustomerName: msg.SenderName,
IssueContent: msg.Content,
SourceMessageIDs: []string{msg.MessageID},
Confidence: 0.9,
}}, nil
}
result, err := engine.importHistoryAndCollectDetailed(AfterSalesHistoryImportRequest{
ConversationID: "room-1",
RoomName: "Room 1",
RawText: strings.Join([]string{
"Customer 2026-05-28 15:13 first device cannot store data and needs repair",
"Customer 2026-05-28 15:20 second device reports voltage error and cannot start",
}, "\n"),
})
if err != nil {
t.Fatalf("history import failed: %v", err)
}
if result.Imported != 2 || result.Segments != 2 || result.Added != 2 || calls != 2 || len(engine.issues) != 2 {
t.Fatalf("expected two segmented issues, result=%#v calls=%d issues=%#v", result, calls, engine.issues)
}
}
func TestAfterSalesMergeSkipsExistingResolvedFingerprint(t *testing.T) {
content := "软件无法登录"
fingerprint := afterSalesFingerprint("room-1", "wm_1", content)
existingIssue := AfterSalesIssue{
ID: "existing",
ConversationID: "room-1",
CustomerUserID: "wm_1",
IssueContent: content,
Status: afterSalesIssueStatusResolved,
Fingerprint: fingerprint,
}
engine := &AfterSalesIssueEngine{issues: []AfterSalesIssue{existingIssue}}
batch := []AfterSalesMessage{{
MessageID: "m1",
ConversationID: "room-1",
RoomName: "售后群",
SenderUserID: "wm_1",
SenderName: "张三",
SenderIdentity: senderIdentityExternal,
Content: content,
MessageType: "text",
SendTime: time.Now().Unix(),
}}
added := engine.mergeAIIssueCandidates([]afterSalesAIIssueCandidate{{
RoomName: "售后群",
CustomerUserID: "wm_1",
CustomerName: "张三",
IssueContent: content,
AISuggestion: "建议检查账号状态。",
SourceMessageIDs: []string{"m1"},
}}, batch, map[string]AfterSalesIssue{fingerprint: existingIssue})
if added != 0 {
t.Fatalf("expected duplicate resolved issue to be skipped, added=%d", added)
}
if len(engine.issues) != 1 || engine.issues[0].Status != afterSalesIssueStatusResolved {
t.Fatalf("existing resolved issue was changed: %#v", engine.issues)
}
}
func TestAfterSalesCollectResolvesMissingRoomNameFromCache(t *testing.T) {
restoreAutoReply, restoreCollector := installAfterSalesCollectTestHooks(t)
defer restoreAutoReply()
defer restoreCollector()
autoReplyEngine.groupNames = map[string]string{"R:room-1": "Resolved Group"}
engine := &AfterSalesIssueEngine{}
batch := []AfterSalesMessage{{
MessageID: "m1",
ConversationID: "R:room-1",
RoomName: "R:room-1",
SenderUserID: "wm_1",
SenderName: "Customer",
SenderIdentity: senderIdentityExternal,
Content: "error",
MessageType: "text",
SendTime: time.Now().Unix(),
}}
added := engine.mergeAIIssueCandidates([]afterSalesAIIssueCandidate{{
CustomerUserID: "wm_1",
CustomerName: "Customer",
IssueContent: "error",
SourceMessageIDs: []string{"m1"},
}}, batch, map[string]AfterSalesIssue{})
if added != 1 || len(engine.issues) != 1 {
t.Fatalf("expected one issue, added=%d issues=%#v", added, engine.issues)
}
if engine.issues[0].RoomName != "Resolved Group" {
t.Fatalf("expected resolved group name, got %q", engine.issues[0].RoomName)
}
}
func TestAfterSalesCollectRecordsSourceAccount(t *testing.T) {
restoreAutoReply, restoreCollector := installAfterSalesCollectTestHooks(t)
defer restoreAutoReply()
defer restoreCollector()
restoreClients := setTestIdentifiedClients(t, map[uint32]string{7: "robot-a"})
defer restoreClients()
autoReplyEngine.rememberCurrentAccountNames(7, "robot-a", "售后A")
engine := &AfterSalesIssueEngine{}
batch := []AfterSalesMessage{{
MessageID: "m1",
ClientID: 7,
ConversationID: "R:room-1",
RoomName: "售后群",
SenderUserID: "wm_1",
SenderName: "Customer",
SenderIdentity: senderIdentityExternal,
Content: "error",
MessageType: "text",
SendTime: time.Now().Unix(),
}}
added := engine.mergeAIIssueCandidates([]afterSalesAIIssueCandidate{{
CustomerUserID: "wm_1",
CustomerName: "Customer",
IssueContent: "error",
SourceMessageIDs: []string{"m1"},
}}, batch, map[string]AfterSalesIssue{})
if added != 1 || len(engine.issues) != 1 {
t.Fatalf("expected one issue, added=%d issues=%#v", added, engine.issues)
}
issue := engine.issues[0]
if issue.SourceClientID != 7 || issue.SourceAccountUserID != "robot-a" || issue.SourceAccountName != "售后A" {
t.Fatalf("expected source account fields, got %#v", issue)
}
}
func TestAfterSalesImageExtractionPrefersLocalPath(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "image.jpg")
if err := os.WriteFile(path, []byte{0xff, 0xd8, 0xff, 0xd9}, 0644); err != nil {
t.Fatalf("write image: %v", err)
}
gotPath, gotRef := extractAfterSalesImageFromMessage(autoReplyMessage{MediaLocalPath: path, MediaKind: "image"}, nil)
if gotPath != path || gotRef != "" {
t.Fatalf("expected local image path, got path=%q ref=%q", gotPath, gotRef)
}
}
func TestAfterSalesFileContentExtractionReadsTextFile(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "issue.txt")
if err := os.WriteFile(path, []byte("motor alarm E42\nneeds service"), 0644); err != nil {
t.Fatalf("write text file: %v", err)
}
content, status := extractAfterSalesFileContent(path)
if status != afterSalesFileStatusReady {
t.Fatalf("expected parsed status, got %q content=%q", status, content)
}
if !strings.Contains(content, "motor alarm E42") || !strings.Contains(content, "needs service") {
t.Fatalf("expected extracted file content, got %q", content)
}
}
func TestAfterSalesCandidateFileAttachmentsFollowSourceMessageIDs(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "issue.csv")
if err := os.WriteFile(path, []byte("part,status\nlens,error"), 0644); err != nil {
t.Fatalf("write csv file: %v", err)
}
content, status := extractAfterSalesFileContent(path)
batch := []AfterSalesMessage{
{
MessageID: "text-1",
ConversationID: "room-1",
Content: "please check attached file",
MessageType: "text",
},
{
MessageID: "file-1",
ConversationID: "room-1",
Content: "file: issue.csv",
MessageType: "file",
FilePath: path,
FileName: "issue.csv",
FileContent: content,
FileExtractStatus: status,
},
}
files := collectCandidateFileAttachments([]string{"file-1"}, batch, map[string]AfterSalesMessage{"file-1": batch[1]})
if len(files) != 1 {
t.Fatalf("expected one file attachment, got %#v", files)
}
if files[0].Name != "issue.csv" || files[0].Path != path || files[0].SourceMessageID != "file-1" {
t.Fatalf("unexpected file attachment metadata: %#v", files[0])
}
if files[0].ExtractStatus != afterSalesFileStatusReady || !strings.Contains(files[0].Content, "lens") {
t.Fatalf("unexpected file attachment content/status: %#v", files[0])
}
}
func TestAfterSalesRepairKeepsEditedRoomNameAndDedupesImages(t *testing.T) {
restoreAutoReply, _ := installAfterSalesCollectTestHooks(t)
defer restoreAutoReply()
dir := t.TempDir()
path := filepath.Join(dir, "image.jpg")
if err := os.WriteFile(path, []byte{0xff, 0xd8, 0xff, 0xd9}, 0644); err != nil {
t.Fatalf("write image: %v", err)
}
autoReplyEngine.groupNames = map[string]string{"R:room-1": "Resolved Group"}
engine := &AfterSalesIssueEngine{
issues: []AfterSalesIssue{{
ID: "issue-1",
ConversationID: "R:room-1",
RoomName: "Edited Group",
SourceMessageIDs: []string{"m1"},
}},
messages: []AfterSalesMessage{{
MessageID: "m1",
ImagePath: path,
}},
}
if !engine.repairIssuesLocked() {
t.Fatal("expected image dedupe repair to report a change")
}
if engine.issues[0].RoomName != "Edited Group" {
t.Fatalf("edited room name was overwritten: %q", engine.issues[0].RoomName)
}
if len(engine.issues[0].ImagePaths) != 1 || engine.issues[0].ImagePaths[0] != path {
t.Fatalf("expected one deduped image path, got %#v", engine.issues[0].ImagePaths)
}
}
func TestAutoReplyGroupNamePersistsAcrossReload(t *testing.T) {
old := identityCachePathOverride
identityCachePathOverride = filepath.Join(t.TempDir(), "identity.json")
defer func() { identityCachePathOverride = old }()
engine := &AutoReplyEngine{
groupNames: make(map[string]string),
identityGroups: make(map[int32]map[string]autoReplyGroupOption),
identityCaches: make(map[int32]*autoReplyIdentityCache),
status: AutoReplyStatus{ReasonCounts: map[string]int{}},
}
engine.rememberGroupName(7, "R:all-hands", "All Hands", 26)
reloaded := &AutoReplyEngine{
groupNames: make(map[string]string),
identityGroups: make(map[int32]map[string]autoReplyGroupOption),
identityCaches: make(map[int32]*autoReplyIdentityCache),
status: AutoReplyStatus{ReasonCounts: map[string]int{}},
}
if err := reloaded.loadIdentityCache(); err != nil {
t.Fatalf("load identity cache failed: %v", err)
}
if got := reloaded.ResolveGroupName("R:all-hands"); got != "All Hands" {
t.Fatalf("expected persisted group name, got %q", got)
}
}
func installAfterSalesCollectTestHooks(t *testing.T) (func(), func()) {
t.Helper()
originalAutoReplyEngine := autoReplyEngine
autoReplyEngine = &AutoReplyEngine{
config: config.AutoReplyConfig{
AI: config.AIConfig{
Provider: "openai_compatible",
BaseURL: "http://127.0.0.1",
Model: "test-model",
TimeoutSeconds: 1,
MaxTokens: 128,
},
},
}
originalCollector := afterSalesAICollector
return func() {
autoReplyEngine = originalAutoReplyEngine
}, func() {
afterSalesAICollector = originalCollector
}
}