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) } }