package workbench import ( "errors" "strings" "testing" ) func TestTaskDurationContractRejectsInvalidValues(t *testing.T) { for _, value := range []any{-1, 1441, 1.5, "60"} { r := &Resource{Kind: "content-tasks", Name: "Task", Data: JSON{"duration_minutes": value}} if err := NewService(nil).validate(nil, r); !errors.Is(err, ErrInvalid) { t.Fatalf("invalid duration accepted: %v: %v", value, err) } } for _, value := range []int{0, 1, 60, 1440} { if err := validateTaskDuration(value); err != nil { t.Fatalf("valid duration rejected: %d: %v", value, err) } } d, err := decode[TaskData](JSON{}) if err != nil || d.DurationMinutes != 0 || taskDurationInstructions(d, -1) != "" || taskDurationReviewInstructions(d) != "" { t.Fatal("legacy tasks must retain unspecified duration") } } func TestDecompositionUsesAndPreservesTaskDuration(t *testing.T) { s, ctx, job, model := generationHarness(t) flow := &Resource{ID: "flow", TenantID: 1, OwnerID: "writer", Kind: "flows", Name: "Flow", Version: 1, Data: asJSON(FlowData{Nodes: []FlowNode{{ID: "n1", AgentID: "author"}}})} if err := s.db.Create(flow).Error; err != nil { t.Fatal(err) } r := &Resource{Kind: "content-tasks", Name: "Task", Data: asJSON(TaskData{DurationMinutes: 120, SourceText: "Framework", FlowID: flow.ID})} job.Action = "decompose" model.output = `{"segments":[{"id":"s1","title":"Foundation","agent_id":"author","node_id":"n1"}]}` if err := s.executeTask(ctx, job, r, ActionRequest{}); err != nil { t.Fatal(err) } d, err := decode[TaskData](r.Data) if err != nil || d.DurationMinutes != 120 || len(d.Segments) != 1 || !strings.Contains(model.messages[1].Content, "直播目标总时长:120 分钟") { t.Fatalf("duration lost during decomposition: %+v %v", d, err) } } func TestSegmentGenerationAndRevisionUseTotalDurationWithoutSkippedContent(t *testing.T) { for _, action := range []string{"generate", "revise"} { t.Run(action, func(t *testing.T) { s, ctx, job, model := generationHarness(t) job.Action = action d := TaskData{DurationMinutes: 120, AgentID: "author", Segments: []Segment{ {ID: "a", Title: "Foundation", Status: "accepted", Content: "Accepted context"}, {ID: "b", Title: "Excluded", Status: "skipped", Content: "Skipped private draft"}, {ID: "c", Title: "Application", AgentID: "author", Status: "generated", Content: "Current draft"}, }} r := &Resource{Kind: "content-tasks", Data: asJSON(d)} if err := s.executeTask(ctx, job, r, ActionRequest{SegmentID: "c", Advice: "Clearer"}); err != nil { t.Fatal(err) } prompt := model.messages[1].Content for _, expected := range []string{"直播目标总时长:120 分钟", "2 个未跳过", "参考时长约 60.0 分钟", "Accepted context"} { if !strings.Contains(prompt, expected) { t.Fatalf("generation prompt missing %q", expected) } } if strings.Contains(prompt, "Skipped private draft") { t.Fatal("skipped content returned to generation") } stored, _ := decode[TaskData](r.Data) if stored.DurationMinutes != 120 { t.Fatal("generation dropped duration") } }) } } func TestDurationChangesPreserveTextAndRequireFreshQualityReview(t *testing.T) { s, ctx, job, model := generationHarness(t) job.Action = "review" d := TaskData{DurationMinutes: 75, AgentID: "author", Segments: []Segment{{ID: "s1", AgentID: "author", Title: "Foundation", Content: "Accepted manuscript", Status: "accepted"}}} r := &Resource{Kind: "content-tasks", Data: asJSON(d)} model.output = `{"passed":true,"summary":"Reviewed","issues":[]}` if err := s.executeTask(ctx, job, r, ActionRequest{}); err != nil { t.Fatal(err) } d, _ = decode[TaskData](r.Data) accepted, _ := acceptedManuscript(d) if d.Quality.DurationMinutes != 75 || requireQuality(d, accepted) != nil || !strings.Contains(model.messages[1].Content, "75 分钟") || !strings.Contains(model.messages[1].Content, "不得把局部窗口视作完整任务") { t.Fatal("quality review omitted duration context") } job.Action = "compose" if err := s.executeTask(ctx, job, r, ActionRequest{}); err != nil { t.Fatal(err) } d, _ = decode[TaskData](r.Data) if d.Composed != "Accepted manuscript" || d.Review == "" { t.Fatal("fixture must contain both a composition and a quality review") } d.DurationMinutes = 90 if requireQuality(d, accepted) == nil { t.Fatal("old duration approval reused") } if err := applyManualUpdate(r, resourceRequest{Name: "Task", Data: asJSON(d)}); err != nil { t.Fatal(err) } updated, _ := decode[TaskData](r.Data) if updated.DurationMinutes != 90 || updated.Segments[0].Content != "Accepted manuscript" || updated.Segments[0].Status != "needs_review" || updated.Quality != nil || updated.Review != "" || updated.Composed != "" || updated.Segments[0].Review != "" || r.Status != "needs_review" { t.Fatalf("duration change did not preserve and invalidate content correctly: %+v", updated) } }