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, "【產文】") }