package usecase import ( "context" "errors" "testing" "apps/backend/internal/module/scout/domain" "apps/backend/internal/module/scout/repository" ) func TestRunPublishVisibilityBarrierAndCounters(t *testing.T) { ctx := context.Background() store := repository.NewMemory() run := domain.NewRun("run-publish", "job-publish", 7, domain.RunBrief{ Intent: "市集", Mode: domain.ModeActivity, TargetCount: 3, }, 100) if err := store.CreateRun(ctx, run); err != nil { t.Fatal(err) } if err := run.Transition(domain.RunRunning, 200); err != nil { t.Fatal(err) } if err := store.ReplaceRunGuarded(ctx, 7, run.ID, []string{domain.RunQueued}, run); err != nil { t.Fatal(err) } svc := New(store) posts := []*domain.Post{ {ID: "publish-new", RunID: run.ID, OwnerUID: 7, OutreachStatus: domain.OutreachNew, PostedAt: 200, CreatedAt: 20}, {ID: "publish-old", RunID: run.ID, OwnerUID: 7, OutreachStatus: domain.OutreachDrafted, PostedAt: 100, CreatedAt: 10}, } if err := svc.StageRunPosts(ctx, 7, run.ID, posts); err != nil { t.Fatal(err) } staged, err := store.ListRunPosts(ctx, 7, run.ID, 1, 10) if err != nil || staged.Pagination.Total != 0 || len(staged.Items) != 0 { t.Fatalf("staged posts leaked: %+v %v", staged, err) } if err := svc.PublishRun(ctx, 7, run.ID, posts); err != nil { t.Fatal(err) } visible, err := store.ListRunPosts(ctx, 7, run.ID, 1, 10) if err != nil || visible.Pagination.Total != 2 || len(visible.Items) != 2 || visible.Items[0].ID != "publish-new" { t.Fatalf("published results=%+v %v", visible, err) } got, err := store.GetRun(ctx, 7, run.ID) if err != nil || got.Status != domain.RunSucceeded || got.EligibleCount != 2 || got.PendingCount != 2 || got.ShortfallCount != 1 { t.Fatalf("published run=%+v %v", got, err) } seen, err := store.HasSeenIdentity(ctx, 7, "id:publish-new") if err != nil || !seen { t.Fatalf("published identity not marked: %v %v", seen, err) } } func TestRunPublishFailureLeavesResultsHiddenAndTerminalGuard(t *testing.T) { ctx := context.Background() store := repository.NewMemory() run := domain.NewRun("run-fail", "job-fail", 7, domain.RunBrief{Intent: "市集", Mode: domain.ModeActivity}, 100) if err := store.CreateRun(ctx, run); err != nil { t.Fatal(err) } if err := run.Transition(domain.RunRunning, 200); err != nil { t.Fatal(err) } if err := store.ReplaceRunGuarded(ctx, 7, run.ID, []string{domain.RunQueued}, run); err != nil { t.Fatal(err) } svc := New(store) posts := []*domain.Post{{ID: "hidden", RunID: run.ID, OwnerUID: 7}} if err := svc.StageRunPosts(ctx, 7, run.ID, posts); err != nil { t.Fatal(err) } if err := svc.FailRun(ctx, 7, run.ID, "provider timeout"); err != nil { t.Fatal(err) } page, err := store.ListRunPosts(ctx, 7, run.ID, 1, 10) if err != nil || page.Pagination.Total != 0 || len(page.Items) != 0 { t.Fatalf("failed run exposed posts: %+v %v", page, err) } failed, err := store.GetRun(ctx, 7, run.ID) if err != nil || failed.Status != domain.RunFailed || failed.Error != "provider timeout" { t.Fatalf("failed run=%+v %v", failed, err) } if err := svc.FailRun(ctx, 7, run.ID, "retry"); err != nil { t.Fatal(err) } if err := svc.StageRunPosts(ctx, 7, run.ID, posts); !errors.Is(err, domain.ErrIllegalRunStatus) { t.Fatalf("terminal stage error=%v", err) } }