thread-master/apps/backend/internal/module/inspire/usecase/m5_inspire_test.go

328 lines
9.7 KiB
Go
Raw Normal View History

2026-07-13 01:15:30 +00:00
package usecase_test
import (
"context"
"strings"
"testing"
"apps/backend/internal/module/ai"
"apps/backend/internal/module/inspire/domain"
"apps/backend/internal/module/inspire/repository"
"apps/backend/internal/module/inspire/usecase"
"apps/backend/internal/module/search"
usageDomain "apps/backend/internal/module/usage/domain"
usageRepo "apps/backend/internal/module/usage/repository"
usageUC "apps/backend/internal/module/usage/usecase"
"github.com/stretchr/testify/require"
)
func newInspire() *usecase.Service {
svc := usecase.New(repository.NewMemory())
ures := usageRepo.NewMemory()
sr := &usageUC.StaticResolver{Map: map[string]string{}}
us := usageUC.New(ures, sr)
svc.Usage = us
svc.AI = &ai.FakeClient{}
svc.Search = &search.FakeClient{}
return svc
}
func setupInspireUID(svc *usecase.Service, uid int64) {
_ = svc.Usage.Repo.SavePrefs(context.Background(), &usageDomain.MemberPrefs{
UID: uid, PlanID: usageDomain.PlanPro, Unlimited: true, UpdatedAt: domain.NowNano(),
})
if sr, ok := svc.Usage.Resolver.(*usageUC.StaticResolver); ok {
if sr.Map == nil {
sr.Map = map[string]string{}
}
for _, m := range []string{usageDomain.MeterAICopy, usageDomain.MeterWebSearch, usageDomain.MeterAIImage} {
sr.Map[itoa(uid)+":"+m] = usageDomain.KeyModePlatform
}
}
}
func itoa(n int64) string {
if n == 0 {
return "0"
}
neg := n < 0
if neg {
n = -n
}
var b [32]byte
i := len(b)
for n > 0 {
i--
b[i] = byte('0' + n%10)
n /= 10
}
if neg {
i--
b[i] = '-'
}
return string(b[i:])
}
// Chat(ctx, uid, message, pinned, mode, persona, sessionID, material)
func chat(svc *usecase.Service, uid int64, msg, mode, sessionID, material string) (*usecase.ChatOutcome, error) {
return svc.Chat(context.Background(), uid, msg, nil, mode, "", sessionID, material)
}
func TestIN_01_ListTrends(t *testing.T) {
svc := newInspire()
uid := int64(5_001_001)
setupInspireUID(svc, uid)
list, err := svc.ListTrends(context.Background(), uid)
require.NoError(t, err)
require.NotEmpty(t, list)
require.NotEmpty(t, list[0].Label)
}
func TestIN_02_RefreshTrends(t *testing.T) {
svc := newInspire()
uid := int64(5_001_002)
setupInspireUID(svc, uid)
_, _ = svc.ListTrends(context.Background(), uid)
list, err := svc.RefreshTrends(context.Background(), uid)
require.NoError(t, err)
require.NotEmpty(t, list)
}
func TestIN_03_ElementCRUD(t *testing.T) {
svc := newInspire()
uid := int64(5_001_003)
setupInspireUID(svc, uid)
el, err := svc.SaveElement(context.Background(), uid, &domain.Element{
Kind: "snippet", Title: "鉤子", Body: "先講痛點", Reusable: true,
})
require.NoError(t, err)
list, err := svc.ListElements(context.Background(), uid)
require.NoError(t, err)
require.Len(t, list, 1)
require.NoError(t, svc.RemoveElement(context.Background(), uid, el.ID))
list, _ = svc.ListElements(context.Background(), uid)
require.Empty(t, list)
}
func TestIN_04_ChatMode(t *testing.T) {
svc := newInspire()
uid := int64(5_001_004)
setupInspireUID(svc, uid)
out, err := chat(svc, uid, "幫我想開場", "chat", "", "")
require.NoError(t, err)
require.NotNil(t, out)
sess := out.Session
require.GreaterOrEqual(t, len(sess.Messages), 2)
require.Equal(t, "assistant", sess.Messages[len(sess.Messages)-1].Role)
require.Nil(t, sess.Messages[len(sess.Messages)-1].Draft)
require.NotEmpty(t, out.Fingerprint)
require.Contains(t, out.Prompt, "發想")
}
func TestIN_05_GenerateRequiresMaterialAndRewrites(t *testing.T) {
svc := newInspire()
uid := int64(5_001_005)
setupInspireUID(svc, uid)
// 無素材 → 拒絕
_, err := chat(svc, uid, "產文", "generate", "", "")
require.Error(t, err)
mat := "遠端上班第三年,會議永遠開不完,想找一種不裝的吐槽角度。"
out, err := chat(svc, uid, "短一點、口語", "generate", "", mat)
require.NoError(t, err)
last := out.Session.Messages[len(out.Session.Messages)-1]
require.NotNil(t, last.Draft)
require.NotEmpty(t, last.Draft.Body)
require.Contains(t, out.Prompt, "待改寫內容")
require.Contains(t, out.Prompt, "改寫者")
require.Contains(t, out.Prompt, mat[:8])
// session 泡泡只留短紀錄,不是整包素材正文
user := out.Session.Messages[len(out.Session.Messages)-2]
require.True(t, strings.HasPrefix(user.Text, "【產文】"))
require.Less(t, len([]rune(user.Text)), len([]rune(mat))+20)
}
func TestIN_07_PreviewMatchesChatFingerprint(t *testing.T) {
svc := newInspire()
uid := int64(5_001_007)
setupInspireUID(svc, uid)
msg := "寫一段開場鉤子"
prev, err := svc.PreviewPrompt(context.Background(), uid, msg, nil, "chat", "", "", "")
require.NoError(t, err)
require.NotEmpty(t, prev.Fingerprint)
out, err := chat(svc, uid, msg, "chat", "", "")
require.NoError(t, err)
require.Equal(t, prev.Fingerprint, out.Fingerprint, "preview and first chat must share fingerprint")
require.Equal(t, prev.Prompt, out.Prompt)
}
func TestIN_06_ResearchSearch(t *testing.T) {
svc := newInspire()
uid := int64(5_001_006)
setupInspireUID(svc, uid)
hits, err := svc.ResearchSearch(context.Background(), uid, "敏感肌保養")
require.NoError(t, err)
require.NotEmpty(t, hits)
require.NotEmpty(t, hits[0].URL)
}
func TestIN_07_GenerateImage(t *testing.T) {
svc := newInspire()
uid := int64(5_001_017)
setupInspireUID(svc, uid)
img, err := svc.GenerateImage(context.Background(), uid, "溫暖海邊插畫")
require.NoError(t, err)
require.NotEmpty(t, img.URL)
require.NotEmpty(t, img.ID)
}
func TestIN_08_UploadSeparate(t *testing.T) {
svc := newInspire()
uid := int64(5_001_008)
setupInspireUID(svc, uid)
img, err := svc.GenerateImage(context.Background(), uid, "x")
require.NoError(t, err)
require.True(t, len(img.URL) > 8)
}
func TestIN_09_LegacyRemoved(t *testing.T) {
svc := newInspire()
require.ErrorIs(t, svc.LegacyRemoved(), domain.ErrRemoved)
}
func TestIN_10_ClearSessionCreatesNewKeepsOld(t *testing.T) {
svc := newInspire()
uid := int64(5_001_010)
setupInspireUID(svc, uid)
out, err := chat(svc, uid, "先聊一句", "chat", "", "")
require.NoError(t, err)
oldID := out.Session.ID
require.NotEmpty(t, oldID)
require.NotEmpty(t, out.Session.Messages)
next, err := svc.ClearSession(context.Background(), uid)
require.NoError(t, err)
require.NotEqual(t, oldID, next.ID)
require.Empty(t, next.Messages)
list, err := svc.ListSessionSummaries(context.Background(), uid)
require.NoError(t, err)
require.GreaterOrEqual(t, len(list), 2)
ids := map[string]bool{}
for _, s := range list {
ids[s.ID] = true
}
require.True(t, ids[oldID])
require.True(t, ids[next.ID])
old, err := svc.ActivateSession(context.Background(), uid, oldID)
require.NoError(t, err)
require.Equal(t, oldID, old.ID)
require.NotEmpty(t, old.Messages)
}
func TestIN_11_MultiSessionSwitchAndDelete(t *testing.T) {
svc := newInspire()
uid := int64(5_001_011)
setupInspireUID(svc, uid)
a, err := chat(svc, uid, "話題A", "chat", "", "")
require.NoError(t, err)
b, err := svc.CreateSession(context.Background(), uid, "")
require.NoError(t, err)
require.NotEqual(t, a.Session.ID, b.ID)
outB, err := chat(svc, uid, "話題B", "chat", b.ID, "")
require.NoError(t, err)
require.Equal(t, b.ID, outB.Session.ID)
after, err := svc.DeleteSession(context.Background(), uid, b.ID)
require.NoError(t, err)
require.NotEqual(t, b.ID, after.ID)
list, err := svc.ListSessionSummaries(context.Background(), uid)
require.NoError(t, err)
for _, s := range list {
require.NotEqual(t, b.ID, s.ID)
}
}
func TestIN_12_SessionIsolationAndHistoryKept(t *testing.T) {
svc := newInspire()
uidA := int64(5_001_012)
uidB := int64(5_001_013)
setupInspireUID(svc, uidA)
setupInspireUID(svc, uidB)
outA1, err := chat(svc, uidA, "A的第一則", "chat", "", "")
require.NoError(t, err)
sessA1 := outA1.Session.ID
sA2, err := svc.CreateSession(context.Background(), uidA, "")
require.NoError(t, err)
outA2, err := chat(svc, uidA, "A的第二則對話", "chat", sA2.ID, "")
require.NoError(t, err)
require.Equal(t, sA2.ID, outA2.Session.ID)
outB, err := chat(svc, uidB, "B的秘密話題", "chat", "", "")
require.NoError(t, err)
_, err = svc.GetSessionByID(context.Background(), uidB, sessA1)
require.ErrorIs(t, err, domain.ErrNotFound)
listA, err := svc.ListSessionSummaries(context.Background(), uidA)
require.NoError(t, err)
for _, s := range listA {
require.NotEqual(t, outB.Session.ID, s.ID)
}
require.GreaterOrEqual(t, len(listA), 2)
back, err := svc.ActivateSession(context.Background(), uidA, sessA1)
require.NoError(t, err)
require.Equal(t, sessA1, back.ID)
found := false
for _, m := range back.Messages {
if m.Role == "user" && m.Text == "A的第一則" {
found = true
}
require.NotEqual(t, "B的秘密話題", m.Text)
require.NotEqual(t, "A的第二則對話", m.Text)
}
require.True(t, found)
back2, err := svc.ActivateSession(context.Background(), uidA, sA2.ID)
require.NoError(t, err)
found2 := false
for _, m := range back2.Messages {
if m.Text == "A的第二則對話" {
found2 = true
}
require.NotEqual(t, "A的第一則", m.Text)
}
require.True(t, found2)
}
func TestIN_13_ListTrendsDoesNotHitLiveSearch(t *testing.T) {
svc := usecase.New(repository.NewMemory())
uid := int64(5_001_014)
list, err := svc.ListTrends(context.Background(), uid)
require.NoError(t, err)
require.NotEmpty(t, list)
for _, titem := range list {
require.Equal(t, "seed", titem.SourceLabel)
}
}
func TestComposeDraftMaterial(t *testing.T) {
sess := &domain.Session{
Title: "周末咖啡",
Messages: []domain.ChatMessage{
{Role: "user", Text: "想寫周末放空"},
{Role: "assistant", Text: "目前共識:慢節奏、不裝"},
{Role: "user", Text: "【產文】短一點素材xxx"},
},
}
mat := usecase.ComposeDraftMaterial(sess, "")
require.Contains(t, mat, "周末咖啡")
require.Contains(t, mat, "想寫周末放空")
require.NotContains(t, mat, "【產文】")
}