Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
110 lines
4.8 KiB
Go
110 lines
4.8 KiB
Go
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)
|
|
}
|
|
}
|