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

328 lines
9.7 KiB
Go
Raw 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 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, "【產文】")
}