167 lines
6.0 KiB
Go
167 lines
6.0 KiB
Go
|
|
package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"errors"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
jobDomain "apps/backend/internal/module/job/domain"
|
||
|
|
jobRepo "apps/backend/internal/module/job/repository"
|
||
|
|
jobUC "apps/backend/internal/module/job/usecase"
|
||
|
|
scoutDomain "apps/backend/internal/module/scout/domain"
|
||
|
|
scoutRepo "apps/backend/internal/module/scout/repository"
|
||
|
|
scoutUC "apps/backend/internal/module/scout/usecase"
|
||
|
|
)
|
||
|
|
|
||
|
|
type workerScoutProvider struct{ err error }
|
||
|
|
|
||
|
|
func (p workerScoutProvider) SearchThreads(_ context.Context, terms []string, limit int) ([]scoutUC.ThreadSearchResult, error) {
|
||
|
|
if p.err != nil {
|
||
|
|
return nil, p.err
|
||
|
|
}
|
||
|
|
term := "市集"
|
||
|
|
if len(terms) > 0 && terms[0] != "" {
|
||
|
|
term = terms[0]
|
||
|
|
}
|
||
|
|
if limit < 1 {
|
||
|
|
limit = 1
|
||
|
|
}
|
||
|
|
return []scoutUC.ThreadSearchResult{{
|
||
|
|
URL: "https://www.threads.net/@worker/post/ok", Snippet: term + "討論內容",
|
||
|
|
}}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
type failingPublishScoutRepo struct{ scoutDomain.Repository }
|
||
|
|
|
||
|
|
func (failingPublishScoutRepo) PublishRunPosts(context.Context, int64, string, []*scoutDomain.Post) error {
|
||
|
|
return errors.New("publish storage unavailable")
|
||
|
|
}
|
||
|
|
|
||
|
|
func workerPayload(runID string, brief scoutDomain.RunBrief) string {
|
||
|
|
raw, _ := json.Marshal(struct {
|
||
|
|
RunID string `json:"run_id"`
|
||
|
|
scoutDomain.RunBrief
|
||
|
|
}{RunID: runID, RunBrief: brief})
|
||
|
|
return string(raw)
|
||
|
|
}
|
||
|
|
|
||
|
|
func workerRunAndJob(t *testing.T, scoutStore scoutDomain.Repository, jobStore jobDomain.Repository, status string) (*scoutUC.Service, *jobUC.Service, *jobDomain.Job, *scoutDomain.Run) {
|
||
|
|
t.Helper()
|
||
|
|
brief := scoutDomain.RunBrief{Intent: "市集", Mode: scoutDomain.ModeActivity, ScanTerms: []string{"市集"}}
|
||
|
|
run := scoutDomain.NewRun("worker-run", "worker-job", 7, brief, 1)
|
||
|
|
if err := scoutStore.CreateRun(context.Background(), run); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
job := &jobDomain.Job{ID: run.JobID, OwnerUID: 7, TemplateType: jobDomain.TemplateScoutScan,
|
||
|
|
Status: jobDomain.StatusRunning, RefID: run.ID, Payload: workerPayload(run.ID, brief), CreatedAt: 1, UpdatedAt: 1}
|
||
|
|
if err := jobStore.Insert(context.Background(), job); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if status != scoutDomain.RunQueued {
|
||
|
|
if status == scoutDomain.RunCancelled {
|
||
|
|
run.Status = scoutDomain.RunCancelled
|
||
|
|
} else {
|
||
|
|
run.Status = status
|
||
|
|
}
|
||
|
|
if err := scoutStore.ReplaceRunGuarded(context.Background(), 7, run.ID, []string{scoutDomain.RunQueued}, run); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
scout := scoutUC.New(scoutStore)
|
||
|
|
scout.Provider = workerScoutProvider{}
|
||
|
|
return scout, jobUC.New(jobStore), job, run
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestWorkerScoutScanSuccessPublishesExactlyOnce(t *testing.T) {
|
||
|
|
ctx := context.Background()
|
||
|
|
scoutStore := scoutRepo.NewMemory()
|
||
|
|
jobStore := jobRepo.NewMemory()
|
||
|
|
scout, jobs, job, run := workerRunAndJob(t, scoutStore, jobStore, scoutDomain.RunQueued)
|
||
|
|
if err := runScoutScan(ctx, jobs, scout, job); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
gotRun, err := scoutStore.GetRun(ctx, 7, run.ID)
|
||
|
|
if err != nil || gotRun.Status != scoutDomain.RunSucceeded || gotRun.EligibleCount != 1 {
|
||
|
|
t.Fatalf("run=%+v err=%v", gotRun, err)
|
||
|
|
}
|
||
|
|
posts, err := scoutStore.ListRunPosts(ctx, 7, run.ID, 1, 10)
|
||
|
|
if err != nil || len(posts.Items) != 1 {
|
||
|
|
t.Fatalf("published posts=%+v err=%v", posts, err)
|
||
|
|
}
|
||
|
|
jobAfter, err := jobStore.FindByID(ctx, job.ID)
|
||
|
|
if err != nil || jobAfter.Status != jobDomain.StatusSucceeded {
|
||
|
|
t.Fatalf("job=%+v err=%v", jobAfter, err)
|
||
|
|
}
|
||
|
|
if err := runScoutScan(ctx, jobs, scout, job); err == nil {
|
||
|
|
t.Fatal("terminal run retry should not search/publish again")
|
||
|
|
}
|
||
|
|
postsAgain, err := scoutStore.ListRunPosts(ctx, 7, run.ID, 1, 10)
|
||
|
|
if err != nil || len(postsAgain.Items) != 1 {
|
||
|
|
t.Fatalf("retry changed result membership: %+v %v", postsAgain, err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestWorkerScoutScanMismatchFailsBeforeSearch(t *testing.T) {
|
||
|
|
ctx := context.Background()
|
||
|
|
scoutStore := scoutRepo.NewMemory()
|
||
|
|
jobStore := jobRepo.NewMemory()
|
||
|
|
scout, jobs, job, run := workerRunAndJob(t, scoutStore, jobStore, scoutDomain.RunQueued)
|
||
|
|
job.RefID = "wrong-run"
|
||
|
|
if err := runScoutScan(ctx, jobs, scout, job); err == nil {
|
||
|
|
t.Fatal("reference mismatch should fail")
|
||
|
|
}
|
||
|
|
got, err := scoutStore.GetRun(ctx, 7, run.ID)
|
||
|
|
if err != nil || got.Status != scoutDomain.RunQueued {
|
||
|
|
t.Fatalf("mismatch changed run=%+v err=%v", got, err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestWorkerScoutScanProviderFailureFailsRunAndHidesResults(t *testing.T) {
|
||
|
|
ctx := context.Background()
|
||
|
|
scoutStore := scoutRepo.NewMemory()
|
||
|
|
jobStore := jobRepo.NewMemory()
|
||
|
|
scout, jobs, job, run := workerRunAndJob(t, scoutStore, jobStore, scoutDomain.RunQueued)
|
||
|
|
scout.Provider = workerScoutProvider{err: errors.New("provider timeout: token redacted")}
|
||
|
|
if err := runScoutScan(ctx, jobs, scout, job); err == nil {
|
||
|
|
t.Fatal("provider failure should fail")
|
||
|
|
}
|
||
|
|
got, err := scoutStore.GetRun(ctx, 7, run.ID)
|
||
|
|
if err != nil || got.Status != scoutDomain.RunFailed || got.Error == "" {
|
||
|
|
t.Fatalf("failed run=%+v err=%v", got, err)
|
||
|
|
}
|
||
|
|
posts, err := scoutStore.ListRunPosts(ctx, 7, run.ID, 1, 10)
|
||
|
|
if err != nil || len(posts.Items) != 0 {
|
||
|
|
t.Fatalf("failed run exposed posts=%+v err=%v", posts, err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestWorkerScoutScanPublishFailureFailsRun(t *testing.T) {
|
||
|
|
ctx := context.Background()
|
||
|
|
base := scoutRepo.NewMemory()
|
||
|
|
scoutStore := failingPublishScoutRepo{Repository: base}
|
||
|
|
jobStore := jobRepo.NewMemory()
|
||
|
|
scout, jobs, job, run := workerRunAndJob(t, scoutStore, jobStore, scoutDomain.RunQueued)
|
||
|
|
if err := runScoutScan(ctx, jobs, scout, job); err == nil {
|
||
|
|
t.Fatal("publish failure should fail")
|
||
|
|
}
|
||
|
|
got, err := base.GetRun(ctx, 7, run.ID)
|
||
|
|
if err != nil || got.Status != scoutDomain.RunFailed {
|
||
|
|
t.Fatalf("publish failed run=%+v err=%v", got, err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestWorkerScoutScanCancelledRunDoesNotRestart(t *testing.T) {
|
||
|
|
ctx := context.Background()
|
||
|
|
scoutStore := scoutRepo.NewMemory()
|
||
|
|
jobStore := jobRepo.NewMemory()
|
||
|
|
scout, jobs, job, run := workerRunAndJob(t, scoutStore, jobStore, scoutDomain.RunCancelled)
|
||
|
|
if err := runScoutScan(ctx, jobs, scout, job); err == nil {
|
||
|
|
t.Fatal("cancelled run should not restart")
|
||
|
|
}
|
||
|
|
got, err := scoutStore.GetRun(ctx, 7, run.ID)
|
||
|
|
if err != nil || got.Status != scoutDomain.RunCancelled {
|
||
|
|
t.Fatalf("cancelled run changed=%+v err=%v", got, err)
|
||
|
|
}
|
||
|
|
}
|