package usecase_test import ( "context" "testing" "apps/backend/internal/module/appnotif/repository" appUC "apps/backend/internal/module/appnotif/usecase" "apps/backend/internal/module/job/domain" jobRepo "apps/backend/internal/module/job/repository" "apps/backend/internal/module/job/usecase" "github.com/stretchr/testify/require" ) func newJobSvc() (*usecase.Service, *appUC.Service) { jr := jobRepo.NewMemory() nr := repository.NewMemory() notif := appUC.New(nr) svc := usecase.New(jr) svc.Notifier = notif return svc, notif } func TestJB_01_CreateDemo(t *testing.T) { svc, _ := newJobSvc() j, err := svc.StartDemo(context.Background(), 1_000_100) require.NoError(t, err) require.Equal(t, domain.StatusQueued, j.Status) require.Equal(t, domain.TemplateDemo, j.TemplateType) list, err := svc.List(context.Background(), 1_000_100) require.NoError(t, err) require.NotEmpty(t, list) } func TestJB_02_ProgressMonotonic(t *testing.T) { svc, _ := newJobSvc() j, err := svc.StartDemo(context.Background(), 1_000_101) require.NoError(t, err) claimed, err := svc.ClaimNext(context.Background(), "worker-test") require.NoError(t, err) require.Equal(t, j.ID, claimed.ID) require.Equal(t, domain.StatusRunning, claimed.Status) out, err := svc.RunDemoToSuccess(context.Background(), claimed.ID) require.NoError(t, err) require.Equal(t, 100, out.ProgressPercent) require.Equal(t, domain.StatusSucceeded, out.Status) // cannot go backwards err = domain.ApplyProgress(out, 50, "nope") require.ErrorIs(t, err, domain.ErrIllegalStatus) } func TestJB_03_JobSucceeded(t *testing.T) { svc, _ := newJobSvc() j, _ := svc.StartDemo(context.Background(), 1_000_102) _, _ = svc.ClaimNext(context.Background(), "w1") out, err := svc.RunDemoToSuccess(context.Background(), j.ID) require.NoError(t, err) require.Equal(t, domain.StatusSucceeded, out.Status) require.True(t, out.CompletedAt > 0) } func TestJB_04_JobFailed(t *testing.T) { svc, _ := newJobSvc() j, _ := svc.StartDemo(context.Background(), 1_000_103) _, _ = svc.ClaimNext(context.Background(), "w1") out, err := svc.FailJob(context.Background(), j.ID, "boom") require.NoError(t, err) require.Equal(t, domain.StatusFailed, out.Status) require.NotEmpty(t, out.Error) require.True(t, out.CompletedAt > 0) } func TestJB_05_JobTerminalCreatesNotification(t *testing.T) { svc, notif := newJobSvc() uid := int64(1_000_104) j, _ := svc.StartDemo(context.Background(), uid) _, _ = svc.ClaimNext(context.Background(), "w1") _, err := svc.RunDemoToSuccess(context.Background(), j.ID) require.NoError(t, err) list, err := notif.List(context.Background(), uid) require.NoError(t, err) require.NotEmpty(t, list) } func TestJB_06_UnreadCount(t *testing.T) { svc, notif := newJobSvc() uid := int64(1_000_105) j, _ := svc.StartDemo(context.Background(), uid) _, _ = svc.ClaimNext(context.Background(), "w1") _, _ = svc.FailJob(context.Background(), j.ID, "x") c, err := notif.UnreadCount(context.Background(), uid) require.NoError(t, err) require.Equal(t, int64(1), c) } func TestJB_07_MarkRead(t *testing.T) { svc, notif := newJobSvc() uid := int64(1_000_106) j, _ := svc.StartDemo(context.Background(), uid) _, _ = svc.ClaimNext(context.Background(), "w1") _, _ = svc.RunDemoToSuccess(context.Background(), j.ID) list, _ := notif.List(context.Background(), uid) require.NotEmpty(t, list) require.NoError(t, notif.MarkRead(context.Background(), uid, list[0].ID)) c, _ := notif.UnreadCount(context.Background(), uid) require.Equal(t, int64(0), c) require.True(t, list[0].ReadAt == 0) // old snapshot } func TestJB_08_MarkAllRead(t *testing.T) { svc, notif := newJobSvc() uid := int64(1_000_107) for i := 0; i < 2; i++ { j, _ := svc.StartDemo(context.Background(), uid) _, _ = svc.ClaimNext(context.Background(), "w1") _, _ = svc.RunDemoToSuccess(context.Background(), j.ID) } c, _ := notif.UnreadCount(context.Background(), uid) require.GreaterOrEqual(t, c, int64(2)) require.NoError(t, notif.MarkAllRead(context.Background(), uid)) c2, _ := notif.UnreadCount(context.Background(), uid) require.Equal(t, int64(0), c2) } func TestJB_09_GetOtherUIDDenied(t *testing.T) { svc, _ := newJobSvc() j, _ := svc.StartDemo(context.Background(), 1_000_108) _, err := svc.Get(context.Background(), 1_000_109, j.ID) require.ErrorIs(t, err, domain.ErrForbidden) } func TestJB_10_GuardedReject(t *testing.T) { svc, _ := newJobSvc() j, _ := svc.StartDemo(context.Background(), 1_000_110) _, _ = svc.ClaimNext(context.Background(), "w1") _, _ = svc.RunDemoToSuccess(context.Background(), j.ID) _, err := svc.Transition(context.Background(), j.ID, domain.StatusRunning, "nope", 10) require.ErrorIs(t, err, domain.ErrIllegalStatus) } func TestJB_11_ManualDeleteTerminal(t *testing.T) { svc, _ := newJobSvc() uid := int64(1_000_111) j, _ := svc.StartDemo(context.Background(), uid) _, _ = svc.ClaimNext(context.Background(), "w1") out, err := svc.RunDemoToSuccess(context.Background(), j.ID) require.NoError(t, err) require.Equal(t, domain.StatusSucceeded, out.Status) require.NoError(t, svc.Delete(context.Background(), uid, j.ID)) _, err = svc.Get(context.Background(), uid, j.ID) require.ErrorIs(t, err, domain.ErrNotFound) } func TestJB_12_DeleteRunningRefused(t *testing.T) { svc, _ := newJobSvc() uid := int64(1_000_112) j, _ := svc.StartDemo(context.Background(), uid) claimed, err := svc.ClaimNext(context.Background(), "w1") require.NoError(t, err) require.Equal(t, domain.StatusRunning, claimed.Status) err = svc.Delete(context.Background(), uid, j.ID) require.ErrorIs(t, err, domain.ErrIllegalStatus) } func TestJB_13_PurgeExpiredTerminal(t *testing.T) { svc, _ := newJobSvc() uid := int64(1_000_113) j, _ := svc.StartDemo(context.Background(), uid) _, _ = svc.ClaimNext(context.Background(), "w1") out, err := svc.RunDemoToSuccess(context.Background(), j.ID) require.NoError(t, err) // 模擬完成時間在 2 天前 out.CompletedAt = domain.NowNano() - domain.TerminalRetentionNs - 1 out.UpdatedAt = out.CompletedAt require.NoError(t, svc.Repo.Update(context.Background(), out)) n, err := svc.PurgeExpiredTerminal(context.Background()) require.NoError(t, err) require.Equal(t, int64(1), n) _, err = svc.Get(context.Background(), uid, j.ID) require.ErrorIs(t, err, domain.ErrNotFound) } func TestJB_14_PurgeKeepsRecentTerminal(t *testing.T) { svc, _ := newJobSvc() uid := int64(1_000_114) j, _ := svc.StartDemo(context.Background(), uid) _, _ = svc.ClaimNext(context.Background(), "w1") _, err := svc.RunDemoToSuccess(context.Background(), j.ID) require.NoError(t, err) n, err := svc.PurgeExpiredTerminal(context.Background()) require.NoError(t, err) require.Equal(t, int64(0), n) got, err := svc.Get(context.Background(), uid, j.ID) require.NoError(t, err) require.Equal(t, j.ID, got.ID) } func TestJB_15_SchedulePersonaAnalyzeAccount(t *testing.T) { svc, _ := newJobSvc() uid := int64(1_000_115) j, err := svc.SchedulePersonaAnalyzeAccount(context.Background(), uid, "pe_abc", "ultralab_tw", "zh-TW") require.NoError(t, err) require.Equal(t, domain.TemplatePersonaAnalyzeAccount, j.TemplateType) require.Equal(t, domain.StatusQueued, j.Status) require.Equal(t, "pe_abc", j.RefID) require.Contains(t, j.Payload, "ultralab_tw") require.Contains(t, j.ProgressSummary, "@ultralab_tw") // re-schedule cancels previous pending j2, err := svc.SchedulePersonaAnalyzeAccount(context.Background(), uid, "pe_abc", "other", "en") require.NoError(t, err) require.NotEqual(t, j.ID, j2.ID) old, err := svc.Get(context.Background(), uid, j.ID) require.NoError(t, err) require.Equal(t, domain.StatusCancelled, old.Status) claimed, err := svc.ClaimNext(context.Background(), "w1") require.NoError(t, err) require.Equal(t, j2.ID, claimed.ID) require.Equal(t, domain.StatusRunning, claimed.Status) } func TestJB_16_SchedulePersonaAnalyzeText(t *testing.T) { svc, _ := newJobSvc() uid := int64(1_000_116) j, err := svc.SchedulePersonaAnalyzeText(context.Background(), uid, "pe_txt", "hello world\n---\nsecond sample text here", "手動", "zh-TW") require.NoError(t, err) require.Equal(t, domain.TemplatePersonaAnalyzeText, j.TemplateType) require.Equal(t, "pe_txt", j.RefID) require.Contains(t, j.Payload, "raw_text") }