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

345 lines
10 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{}
svc.Personas = fakePersonaSource{}
return svc
}
type fakePersonaSource struct{}
func (fakePersonaSource) ResolvePersona(context.Context, int64, string) (*usecase.PersonaSnapshot, error) {
return &usecase.PersonaSnapshot{
ID: "persona-ready", Name: "測試人設", Status: "ready",
DraftText: "語氣自然、有具體情緒;句子長短交錯,不用固定開頭。",
}, nil
}
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, false)
}
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_GenerateUsesConversationAndPersona(t *testing.T) {
svc := newInspire()
uid := int64(5_001_005)
setupInspireUID(svc, uid)
first, err := chat(svc, uid, "遠端上班第三年,會議永遠開不完,我想談這種疲累感。", "chat", "", "")
require.NoError(t, err)
out, err := chat(svc, uid, "", "generate", first.Session.ID, "")
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, "測試人設")
require.Contains(t, out.Prompt, "高互動貼文原則")
user := out.Session.Messages[len(out.Session.Messages)-2]
require.True(t, strings.HasPrefix(user.Text, "【產文】"))
}
func TestIN_05_ChatCanInjectExaResearch(t *testing.T) {
svc := newInspire()
uid := int64(5_001_015)
setupInspireUID(svc, uid)
out, err := svc.Chat(context.Background(), uid, "最近大家怎麼討論遠端工作?", nil, "chat", "", "", "", true)
require.NoError(t, err)
require.Contains(t, out.Prompt, "Exa 查詢資料")
require.Contains(t, out.Prompt, "來源:")
}
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, "【產文】")
}