Composition roots already hold Snapshot ports; the interface and archtest now forbid putting Pool/DocGov/DocRevision/ContextManager/CAS/ProjectWorkflow/FreeformDoc/KB back on Service. Co-authored-by: Cursor <cursoragent@cursor.com>
353 lines
13 KiB
Go
353 lines
13 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"chat-agent-system/internal/bizdev"
|
|
"chat-agent-system/internal/config"
|
|
"chat-agent-system/internal/docgov"
|
|
"chat-agent-system/internal/projectworkflow"
|
|
"chat-agent-system/internal/squad"
|
|
"chat-agent-system/internal/stream"
|
|
"chat-agent-system/internal/taskrun"
|
|
"chat-agent-system/internal/workspace"
|
|
)
|
|
|
|
func TestFinalizeTaskRun_ReplacesPlaceholderContent(t *testing.T) {
|
|
cfg := &config.Config{Env: "test", DataDir: t.TempDir(), AgentBackend: "stub"}
|
|
svc := bizdev.MustNewService(t, cfg)
|
|
srv := NewServer(cfg, svc)
|
|
|
|
workDir := t.TempDir()
|
|
_ = os.WriteFile(filepath.Join(workDir, "output.md"), []byte("# 最终交付\n\n正文内容。"), 0o644)
|
|
|
|
ctxMgr := bizdev.Snapshot(svc).Ctx
|
|
asstID, err := ctxMgr.RecordUserMessageWithID(context.Background(), "sess-finalize", 0, "assistant", "任务执行中…", "TRIGGER_TASK", "", map[string]any{
|
|
"task_status": "running",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("RecordUserMessageWithID: %v", err)
|
|
}
|
|
|
|
pwSvc := projectworkflow.NewService(projectworkflow.NewStore(nil), nil)
|
|
_, _ = pwSvc.CreateWorkflowRun(context.Background(), &projectworkflow.WorkflowRun{
|
|
RunID: "run_finalize_test",
|
|
ProjectID: 56,
|
|
TaskKey: "intake",
|
|
Status: "running",
|
|
})
|
|
|
|
srv.pwSvc = pwSvc
|
|
srv.taskRuns = taskrun.NewManager(pwSvc, ctxMgr)
|
|
srv.initTaskRunExecutors()
|
|
|
|
record := &taskrun.RunRecord{
|
|
RunID: "run_finalize_test",
|
|
SessionID: "sess-finalize",
|
|
ProjectID: 56,
|
|
TaskKey: "intake",
|
|
AssistantMsgID: asstID,
|
|
}
|
|
|
|
status, finErr := srv.finalizeTaskRun(context.Background(), record, &bizdev.TaskRunResult{
|
|
Success: true,
|
|
ArtifactTitle: "用户初始需求收集表",
|
|
WorkDir: workDir,
|
|
TaskKey: "intake",
|
|
QualityReport: &squad.QualityReport{Passed: false, Score: 40, Violations: []string{"不做展开"}},
|
|
DocRecord: &docgov.DocumentRecord{DocID: "doc_proj56_intake", ContentSHA256: "abc"},
|
|
})
|
|
if finErr != nil {
|
|
t.Fatalf("finalizeTaskRun: %v", finErr)
|
|
}
|
|
if status != "quality_failed" {
|
|
t.Fatalf("status=%q want quality_failed", status)
|
|
}
|
|
|
|
msg, err := ctxMgr.GetMessageByID(context.Background(), asstID)
|
|
if err != nil {
|
|
t.Fatalf("GetMessageByID: %v", err)
|
|
}
|
|
if msg.Content == "任务执行中…" {
|
|
t.Fatalf("content still placeholder: %q", msg.Content)
|
|
}
|
|
if msg.Metadata["task_status"] != "quality_failed" {
|
|
t.Fatalf("metadata task_status=%v", msg.Metadata["task_status"])
|
|
}
|
|
artifact, ok := msg.Metadata["artifact"].(map[string]any)
|
|
if !ok || artifact["doc_id"] != "doc_proj56_intake" {
|
|
t.Fatalf("missing artifact doc_id in metadata: %#v", msg.Metadata["artifact"])
|
|
}
|
|
|
|
run, err := pwSvc.GetWorkflowRun(context.Background(), "run_finalize_test")
|
|
if err != nil {
|
|
t.Fatalf("GetWorkflowRun: %v", err)
|
|
}
|
|
if run.OutputDocID != "doc_proj56_intake" {
|
|
t.Fatalf("OutputDocID=%q", run.OutputDocID)
|
|
}
|
|
if run.OutputSummary == "" {
|
|
t.Fatal("OutputSummary empty")
|
|
}
|
|
}
|
|
|
|
func TestFinalizeTaskRun_PendingAuthorConfirmation_NotCompleted(t *testing.T) {
|
|
cfg := &config.Config{Env: "test", DataDir: t.TempDir(), AgentBackend: "stub"}
|
|
svc := bizdev.MustNewService(t, cfg)
|
|
srv := NewServer(cfg, svc)
|
|
|
|
workDir := t.TempDir()
|
|
_ = os.WriteFile(filepath.Join(workDir, "output.md"), []byte("# base head\n"), 0o644)
|
|
|
|
ctxMgr := bizdev.Snapshot(svc).Ctx
|
|
asstID, err := ctxMgr.RecordUserMessageWithID(context.Background(), "sess-pending", 0, "assistant", "任务执行中…", "TRIGGER_TASK", "", map[string]any{
|
|
"task_status": "running",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("RecordUserMessageWithID: %v", err)
|
|
}
|
|
|
|
pwSvc := projectworkflow.NewService(projectworkflow.NewStore(nil), nil)
|
|
_, _ = pwSvc.CreateWorkflowRun(context.Background(), &projectworkflow.WorkflowRun{
|
|
RunID: "run_pending_confirm", ProjectID: 56, TaskKey: "intake", Status: "running",
|
|
})
|
|
srv.pwSvc = pwSvc
|
|
srv.taskRuns = taskrun.NewManager(pwSvc, ctxMgr)
|
|
srv.initTaskRunExecutors()
|
|
|
|
record := &taskrun.RunRecord{
|
|
RunID: "run_pending_confirm", SessionID: "sess-pending", ProjectID: 56, TaskKey: "intake", AssistantMsgID: asstID,
|
|
}
|
|
status, finErr := srv.finalizeTaskRun(context.Background(), record, &bizdev.TaskRunResult{
|
|
Success: true,
|
|
ArtifactTitle: "用户初始需求收集表",
|
|
WorkDir: workDir,
|
|
TaskKey: "intake",
|
|
RequiresAuthorApproval: true,
|
|
PendingAgentCandidateID: 55,
|
|
PendingChangeReportID: 8,
|
|
PendingRiskLevel: "high",
|
|
DocRecord: &docgov.DocumentRecord{DocID: "doc_proj56_intake", ContentSHA256: "oldhead"},
|
|
})
|
|
if finErr != nil {
|
|
t.Fatalf("finalizeTaskRun: %v", finErr)
|
|
}
|
|
if status != bizdev.TaskStatusPendingAuthorConfirmation {
|
|
t.Fatalf("status=%q", status)
|
|
}
|
|
|
|
msg, err := ctxMgr.GetMessageByID(context.Background(), asstID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(msg.Content, "已完成") || strings.Contains(msg.Content, "正文已保存") {
|
|
t.Fatalf("async summary must not claim completed: %q", msg.Content)
|
|
}
|
|
if !strings.Contains(msg.Content, "文档候选已生成") || !strings.Contains(msg.Content, "待编制人确认") {
|
|
t.Fatalf("async summary missing pending wording: %q", msg.Content)
|
|
}
|
|
if msg.Metadata["task_status"] != bizdev.TaskStatusPendingAuthorConfirmation {
|
|
t.Fatalf("metadata task_status=%v", msg.Metadata["task_status"])
|
|
}
|
|
artifact, ok := msg.Metadata["artifact"].(map[string]any)
|
|
if !ok || artifact["doc_id"] != "doc_proj56_intake" {
|
|
t.Fatalf("artifact=%#v", msg.Metadata["artifact"])
|
|
}
|
|
if artifact["pending_agent_candidate_id"] != int64(55) {
|
|
t.Fatalf("pending candidate meta=%v", artifact["pending_agent_candidate_id"])
|
|
}
|
|
if artifact["awaiting_author_confirmation"] != true {
|
|
t.Fatal("expected awaiting_author_confirmation on artifact")
|
|
}
|
|
|
|
run, err := pwSvc.GetWorkflowRun(context.Background(), "run_pending_confirm")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if run.Status != bizdev.TaskStatusPendingAuthorConfirmation {
|
|
t.Fatalf("workflow status=%q", run.Status)
|
|
}
|
|
}
|
|
|
|
func TestAsyncTaskRun_FinalizesAfterWorkerCompletes(t *testing.T) {
|
|
cfg := &config.Config{Env: "test", DataDir: t.TempDir(), AgentBackend: "stub"}
|
|
svc := bizdev.MustNewService(t, cfg)
|
|
pwSvc := projectworkflow.NewService(projectworkflow.NewStore(nil), nil)
|
|
ctxMgr := bizdev.Snapshot(svc).Ctx
|
|
|
|
workDir := t.TempDir()
|
|
_ = os.WriteFile(filepath.Join(workDir, "output.md"), []byte("# async finalize\n"), 0o644)
|
|
|
|
srv := &Server{cfg: cfg, svc: svc, pwSvc: pwSvc, ctxMgr: ctxMgr, taskRuns: taskrun.NewManager(pwSvc, ctxMgr)}
|
|
srv.taskRuns.SetExecutor(
|
|
func(_ context.Context, _ string, _ bizdev.TaskRunRequest, _ *stream.Streamer) (*bizdev.TaskRunResult, error) {
|
|
return &bizdev.TaskRunResult{
|
|
Success: true,
|
|
ArtifactTitle: "用户初始需求收集表",
|
|
WorkDir: workDir,
|
|
TaskKey: "intake",
|
|
QualityReport: &squad.QualityReport{Passed: true, Score: 95},
|
|
DocRecord: &docgov.DocumentRecord{DocID: "doc_proj1_intake"},
|
|
}, nil
|
|
},
|
|
nil,
|
|
nil,
|
|
)
|
|
srv.taskRuns.SetFinalizer(func(ctx context.Context, record *taskrun.RunRecord, result *bizdev.TaskRunResult) (string, error) {
|
|
return srv.finalizeTaskRun(ctx, record, result)
|
|
})
|
|
|
|
asstID, _ := ctxMgr.RecordUserMessageWithID(context.Background(), "sess-async", 0, "assistant", "任务执行中…", "TRIGGER_TASK", "", nil)
|
|
runID := "run_async_finalize"
|
|
_, _ = pwSvc.CreateWorkflowRun(context.Background(), &projectworkflow.WorkflowRun{
|
|
RunID: runID, ProjectID: 1, TaskKey: "intake", Status: "running",
|
|
})
|
|
|
|
record := &taskrun.RunRecord{
|
|
RunID: runID, SessionID: "sess-async", ProjectID: 1, TaskKey: "intake", AssistantMsgID: asstID,
|
|
}
|
|
_, err := srv.taskRuns.CreateAndStart(context.Background(), runID, record, bizdev.TaskRunRequest{TaskKey: "intake", ProjectID: 1}, bizdev.StageRunRequest{}, "")
|
|
if err != nil {
|
|
t.Fatalf("CreateAndStart: %v", err)
|
|
}
|
|
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
rec, getErr := srv.taskRuns.GetRun(runID)
|
|
if getErr == nil && rec.Status != "running" {
|
|
msg, _ := ctxMgr.GetMessageByID(context.Background(), asstID)
|
|
if msg != nil && msg.Content != "任务执行中…" {
|
|
return
|
|
}
|
|
t.Fatalf("run finished with status=%s but content=%q", rec.Status, msg.Content)
|
|
}
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
t.Fatal("worker did not finish")
|
|
}
|
|
|
|
func TestAsyncTaskRun_PendingConfirmation_FinalizesAsPending(t *testing.T) {
|
|
cfg := &config.Config{Env: "test", DataDir: t.TempDir(), AgentBackend: "stub"}
|
|
svc := bizdev.MustNewService(t, cfg)
|
|
pwSvc := projectworkflow.NewService(projectworkflow.NewStore(nil), nil)
|
|
ctxMgr := bizdev.Snapshot(svc).Ctx
|
|
|
|
workDir := t.TempDir()
|
|
_ = os.WriteFile(filepath.Join(workDir, "output.md"), []byte("# head\n"), 0o644)
|
|
|
|
srv := &Server{cfg: cfg, svc: svc, pwSvc: pwSvc, ctxMgr: ctxMgr, taskRuns: taskrun.NewManager(pwSvc, ctxMgr)}
|
|
srv.taskRuns.SetExecutor(
|
|
func(_ context.Context, _ string, _ bizdev.TaskRunRequest, _ *stream.Streamer) (*bizdev.TaskRunResult, error) {
|
|
return &bizdev.TaskRunResult{
|
|
Success: true,
|
|
ArtifactTitle: "用户初始需求收集表",
|
|
WorkDir: workDir,
|
|
TaskKey: "intake",
|
|
RequiresAuthorApproval: true,
|
|
PendingAgentCandidateID: 3,
|
|
PendingChangeReportID: 4,
|
|
PendingRiskLevel: "high",
|
|
DocRecord: &docgov.DocumentRecord{DocID: "doc_proj1_intake"},
|
|
}, nil
|
|
},
|
|
nil,
|
|
nil,
|
|
)
|
|
srv.taskRuns.SetFinalizer(func(ctx context.Context, record *taskrun.RunRecord, result *bizdev.TaskRunResult) (string, error) {
|
|
return srv.finalizeTaskRun(ctx, record, result)
|
|
})
|
|
|
|
asstID, _ := ctxMgr.RecordUserMessageWithID(context.Background(), "sess-async-pending", 0, "assistant", "任务执行中…", "TRIGGER_TASK", "", nil)
|
|
runID := "run_async_pending"
|
|
_, _ = pwSvc.CreateWorkflowRun(context.Background(), &projectworkflow.WorkflowRun{
|
|
RunID: runID, ProjectID: 1, TaskKey: "intake", Status: "running",
|
|
})
|
|
|
|
record := &taskrun.RunRecord{
|
|
RunID: runID, SessionID: "sess-async-pending", ProjectID: 1, TaskKey: "intake", AssistantMsgID: asstID,
|
|
}
|
|
_, err := srv.taskRuns.CreateAndStart(context.Background(), runID, record, bizdev.TaskRunRequest{TaskKey: "intake", ProjectID: 1}, bizdev.StageRunRequest{}, "")
|
|
if err != nil {
|
|
t.Fatalf("CreateAndStart: %v", err)
|
|
}
|
|
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
rec, getErr := srv.taskRuns.GetRun(runID)
|
|
if getErr == nil && rec.Status != "running" {
|
|
msg, _ := ctxMgr.GetMessageByID(context.Background(), asstID)
|
|
if msg == nil {
|
|
t.Fatal("nil message")
|
|
}
|
|
if msg.Metadata["task_status"] != bizdev.TaskStatusPendingAuthorConfirmation {
|
|
t.Fatalf("task_status=%v content=%q status=%s", msg.Metadata["task_status"], msg.Content, rec.Status)
|
|
}
|
|
if strings.Contains(msg.Content, "**已完成**") {
|
|
t.Fatalf("must not show completed: %q", msg.Content)
|
|
}
|
|
if !strings.Contains(msg.Content, "待编制人确认") {
|
|
t.Fatalf("content=%q", msg.Content)
|
|
}
|
|
return
|
|
}
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
t.Fatal("worker did not finish")
|
|
}
|
|
|
|
func TestFinalizeTaskRun_HashesViaWorkspaceCapability(t *testing.T) {
|
|
cfg := &config.Config{Env: "test", DataDir: t.TempDir(), AgentBackend: "stub"}
|
|
svc := bizdev.MustNewService(t, cfg)
|
|
srv := NewServer(cfg, svc)
|
|
ctx := context.Background()
|
|
ref, err := srv.wsCap.Issue(workspace.WorkspaceRef{ID: "ws_hash", ProjectScope: 1, RunID: "run_hash"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body := []byte("# hashed via capability\n")
|
|
if err := srv.wsCap.WriteArtifact(ctx, ref, workspace.DeliverableKey, body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctxMgr := bizdev.Snapshot(svc).Ctx
|
|
asstID, err := ctxMgr.RecordUserMessageWithID(ctx, "sess-hash", 0, "assistant", "任务执行中…", "TRIGGER_TASK", "", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pwSvc := projectworkflow.NewService(projectworkflow.NewStore(nil), nil)
|
|
_, _ = pwSvc.CreateWorkflowRun(ctx, &projectworkflow.WorkflowRun{
|
|
RunID: "run_hash", ProjectID: 1, TaskKey: "intake", Status: "running",
|
|
})
|
|
srv.pwSvc = pwSvc
|
|
record := &taskrun.RunRecord{RunID: "run_hash", SessionID: "sess-hash", ProjectID: 1, TaskKey: "intake", AssistantMsgID: asstID, WorkspaceID: ref.ID}
|
|
status, err := srv.finalizeTaskRun(ctx, record, &bizdev.TaskRunResult{
|
|
Success: true, TaskKey: "intake", WorkDir: ref.ID,
|
|
DocRecord: &docgov.DocumentRecord{DocID: "doc_hash"},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if status != "completed" {
|
|
t.Fatalf("status=%s", status)
|
|
}
|
|
msg, err := ctxMgr.GetMessageByID(ctx, asstID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
art, _ := msg.Metadata["artifact"].(map[string]any)
|
|
if art == nil {
|
|
t.Fatalf("artifact missing: %#v", msg.Metadata)
|
|
}
|
|
sum := sha256.Sum256(body)
|
|
want := hex.EncodeToString(sum[:])
|
|
if art["sha256"] != want {
|
|
t.Fatalf("sha256=%v want %s", art["sha256"], want)
|
|
}
|
|
}
|