thread-master/apps/backend/internal/logic/scout/run_scan_logic_test.go

105 lines
4.1 KiB
Go
Raw Normal View History

2026-08-13 02:22:24 +00:00
package scout
import (
"context"
"encoding/json"
"errors"
"testing"
"apps/backend/internal/middleware"
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"
"apps/backend/internal/svc"
"apps/backend/internal/types"
)
func TestRunScanCreatesIndependentRunAndJobLink(t *testing.T) {
ctx := middleware.WithUID(context.Background(), 7)
scoutStore := scoutRepo.NewMemory()
jobs := jobUC.New(jobRepo.NewMemory())
svcCtx := &svc.ServiceContext{Scout: scoutUC.New(scoutStore), Jobs: jobs}
data, err := NewRunScanLogic(ctx, svcCtx).RunScan(&types.ScoutScanReq{Brief: types.ScoutBriefPublic{
Intent: "市集", Mode: domainModeActivity, ThemeKey: "same-theme", ThemeLabel: "週末市集",
ScanTerms: []string{"市集"}, TargetCount: 12,
}})
if err != nil {
t.Fatal(err)
}
if data.Run.Id == "" || data.Job.Id == "" || data.Run.Id == data.Job.Id {
t.Fatalf("missing independent ids: %+v", data)
}
if data.Job.RefId != data.Run.Id || data.Run.JobId != data.Job.Id || data.Run.Status != scoutDomain.RunQueued {
t.Fatalf("job/run link mismatch: job=%+v run=%+v", data.Job, data.Run)
}
var raw map[string]json.RawMessage
if err := json.Unmarshal([]byte(data.Job.Payload), &raw); err != nil {
t.Fatal(err)
}
var payloadRunID string
if err := json.Unmarshal(raw["run_id"], &payloadRunID); err != nil || payloadRunID != data.Run.Id {
t.Fatalf("payload run_id=%q err=%v payload=%s", payloadRunID, err, data.Job.Payload)
}
var brief scoutDomain.RunBrief
if err := json.Unmarshal([]byte(data.Job.Payload), &brief); err != nil || brief.Intent != "市集" || len(brief.ScanTerms) != 1 {
t.Fatalf("flat worker brief=%+v err=%v", brief, err)
}
if data.Run.TargetCount != 12 {
t.Fatalf("target count=%d", data.Run.TargetCount)
}
runs, err := scoutStore.ListRuns(context.Background(), scoutDomain.RunFilter{OwnerUID: 7, Page: 1, PageSize: 10})
if err != nil || len(runs.Items) != 1 || runs.Items[0].ID != data.Run.Id {
t.Fatalf("stored runs=%+v err=%v", runs, err)
}
}
func TestRunScanCreatesFreshRunForSameTheme(t *testing.T) {
ctx := middleware.WithUID(context.Background(), 7)
scoutStore := scoutRepo.NewMemory()
svcCtx := &svc.ServiceContext{Scout: scoutUC.New(scoutStore), Jobs: jobUC.New(jobRepo.NewMemory())}
logic := NewRunScanLogic(ctx, svcCtx)
request := &types.ScoutScanReq{Brief: types.ScoutBriefPublic{Intent: "市集", Mode: domainModeActivity, ThemeKey: "repeat", ScanTerms: []string{"市集"}}}
first, err := logic.RunScan(request)
if err != nil {
t.Fatal(err)
}
second, err := logic.RunScan(request)
if err != nil {
t.Fatal(err)
}
if first.Run.Id == second.Run.Id || first.Job.Id == second.Job.Id {
t.Fatalf("same theme reused id: first=%+v second=%+v", first, second)
}
}
type failingInsertJobRepo struct{ jobDomain.Repository }
func (failingInsertJobRepo) Insert(context.Context, *jobDomain.Job) error {
return errors.New("insert unavailable")
}
func TestRunScanSchedulingFailureFailsQueuedRun(t *testing.T) {
ctx := middleware.WithUID(context.Background(), 7)
scoutStore := scoutRepo.NewMemory()
failingJobs := jobUC.New(failingInsertJobRepo{Repository: jobRepo.NewMemory()})
svcCtx := &svc.ServiceContext{Scout: scoutUC.New(scoutStore), Jobs: failingJobs}
_, err := NewRunScanLogic(ctx, svcCtx).RunScan(&types.ScoutScanReq{Brief: types.ScoutBriefPublic{
Intent: "市集", Mode: domainModeActivity, ScanTerms: []string{"市集"},
}})
if err == nil {
t.Fatal("expected scheduling failure")
}
runs, listErr := scoutStore.ListRuns(context.Background(), scoutDomain.RunFilter{OwnerUID: 7, Page: 1, PageSize: 10})
if listErr != nil || len(runs.Items) != 1 || runs.Items[0].Status != scoutDomain.RunFailed {
t.Fatalf("failed run convergence: runs=%+v err=%v listErr=%v", runs, err, listErr)
}
if runs.Items[0].Error == "" {
t.Fatal("failed run missing safe error")
}
}
const domainModeActivity = "activity"