package bizdev_test import ( "context" "strings" "sync" "testing" "chat-agent-system/internal/bizdev" "chat-agent-system/internal/config" "chat-agent-system/internal/docgov" "chat-agent-system/internal/docrevision" "chat-agent-system/internal/stream" "chat-agent-system/internal/workflowdef" ) // executeStageDrainingPending runs ExecuteStage, confirming high-risk agent candidates // (and projecting fact source) until the stage succeeds or stops on a DocGov proposal intercept. // This mirrors the product path: confirm on the page, then re-run the stage. func executeStageDrainingPending( t *testing.T, ctx context.Context, svc *bizdev.Service, req bizdev.StageRunRequest, authorID int64, dataRoot string, ) *bizdev.StageRunResult { t.Helper() for attempt := 0; attempt < 16; attempt++ { res, err := svc.ExecuteStage(ctx, req) if err != nil { t.Fatalf("ExecuteStage attempt %d: %v", attempt, err) } if res.Success { return res } for _, sub := range res.SubArtifacts { if sub != nil && sub.IsProposalIntercept() { return res } } if res.PendingAgentCandidateID <= 0 { t.Fatalf("stage failed without pending/proposal: %s", res.ErrorReason) } var pending *bizdev.TaskRunResult for _, sub := range res.SubArtifacts { if sub != nil && sub.AwaitsAgentConfirmation() { pending = sub break } } if pending == nil || pending.DocRecord == nil || pending.DocRecord.DocID == "" { t.Fatalf("pending stage missing DocRecord: %+v", res) } docID := pending.DocRecord.DocID st, err := bizdev.Snapshot(svc).Rev.GetRevisionState(ctx, docID) if err != nil { t.Fatalf("GetRevisionState(%s): %v", docID, err) } st2, err := bizdev.Snapshot(svc).Rev.CommitAgentVersion(ctx, docID, pending.PendingAgentCandidateID, docrevision.CommitAgentDecision{ Decision: "commit", Confirmed: true, ActorUserID: authorID, ExpectedInputSHA: st.HeadSHA256, }) if err != nil { t.Fatalf("CommitAgentVersion(%s,#%d): %v", docID, pending.PendingAgentCandidateID, err) } if err := bizdev.Snapshot(svc).Rev.ProjectOutputMarkdown(docID, dataRoot); err != nil { t.Fatalf("ProjectOutputMarkdown(%s): %v", docID, err) } if st2 != nil && st2.HeadSHA256 != "" { if err := bizdev.Snapshot(svc).Gov.MarkDocumentContentUpdated(ctx, docID, st2.HeadSHA256, false); err != nil { t.Fatalf("MarkDocumentContentUpdated: %v", err) } } t.Logf("confirmed pending candidate #%d on %s; re-running stage (attempt %d)", pending.PendingAgentCandidateID, docID, attempt+1) } t.Fatal("stage still unresolved after max pending confirm/retry loops") return nil } func TestBizDev_FullLink_V01_V05_V10_E2E(t *testing.T) { cfg := &config.Config{ DataDir: t.TempDir(), } svc := bizdev.MustNewService(t, cfg) // 收集全流程发射的流式事件 var eventsMu sync.Mutex var collectedEvents []stream.StreamEvent streamListener := func(ev stream.StreamEvent) { eventsMu.Lock() defer eventsMu.Unlock() collectedEvents = append(collectedEvents, ev) } ctx, _ := stream.WithListener(context.Background(), false, streamListener) projectID := int64(1001) projectName := "4K超高清荧光硬管内窥镜摄像系统" customerName := "水木东方创新医疗转化中心" deviceType := "硬管内窥镜摄像系统" intendedUse := "微创外科手术中解剖结构观察与吲哚菁绿(ICG)组织血流荧光实时显影" targetMarket := "中国NMPA二类" authorID := int64(201) authorName := "张工(系统编制人)" reviewerID := int64(202) reviewerName := "李主管(合规审核人)" approverID := int64(203) approverName := "王总监(项目批准人)" t.Log("================================================================================") t.Log(">>> 阶段 1: 执行成熟度阶段 V0.1 (闪电摸底与盲区探测) <<<") t.Log("================================================================================") stageReqV01 := bizdev.StageRunRequest{ Stage: bizdev.StageV01_Triage, ProjectID: projectID, ProjectName: projectName, CustomerName: customerName, DeviceType: deviceType, IntendedUse: intendedUse, TargetMarket: targetMarket, Backend: "stub", UserID: authorID, AuthorID: authorID, AuthorName: authorName, ReviewerID: reviewerID, ReviewerName: reviewerName, ApproverID: approverID, ApproverName: approverName, } resV01, err := svc.ExecuteStage(ctx, stageReqV01) if err != nil { t.Fatalf("Stage V0.1 execution failed: %v", err) } if !resV01.Success { t.Fatalf("Stage V0.1 should succeed, got error: %s", resV01.ErrorReason) } if resV01.PrimaryArtifact == nil || !workflowdef.SameTaskKey(resV01.PrimaryArtifact.TaskKey, "intake") { t.Fatalf("Stage V0.1 primary artifact should be intake, got: %+v", resV01.PrimaryArtifact) } if resV01.AdvisorBrief == nil || resV01.AdvisorBrief.MaturityLevel != "V0.1" { t.Fatalf("Stage V0.1 AdvisorBrief maturity level should be V0.1, got: %+v", resV01.AdvisorBrief) } t.Logf("✓ Stage V0.1 成功完成: 标题=%s, 诊断=%s, 成熟度得分=%d", resV01.StageTitle, resV01.AdvisorBrief.MaturityHeadline, resV01.AdvisorBrief.MaturityScore) // 验证项目事实黑板已沉淀 V0.1 提取事实 pCtx, err := bizdev.Snapshot(svc).Ctx.GetProjectContext(ctx, projectID) if err != nil || pCtx == nil { t.Fatalf("get project context failed: %v", err) } if pCtx.CurrentStage != string(bizdev.StageV01_Triage) { t.Errorf("project current stage should be V0.1_triage, got %s", pCtx.CurrentStage) } t.Logf("✓ 项目事实黑板成功同步: 当前阶段=%s, 黑板字段数=%d", pCtx.CurrentStage, len(pCtx.BlackboardFacts)) t.Log("================================================================================") t.Log(">>> 阶段 2: 执行成熟度阶段 V0.5 (方案成型与法规对齐) <<<") t.Log("================================================================================") stageReqV05 := bizdev.StageRunRequest{ Stage: bizdev.StageV05_Feasibility, ProjectID: projectID, ProjectName: projectName, CustomerName: customerName, DeviceType: deviceType, IntendedUse: intendedUse, TargetMarket: targetMarket, Backend: "stub", UserID: authorID, AuthorID: authorID, AuthorName: authorName, ReviewerID: reviewerID, ReviewerName: reviewerName, ApproverID: approverID, ApproverName: approverName, } resV05 := executeStageDrainingPending(t, ctx, svc, stageReqV05, authorID, cfg.DataDir) if !resV05.Success { t.Fatalf("Stage V0.5 should succeed after confirming pendings, got error: %s", resV05.ErrorReason) } if len(resV05.SubArtifacts) != 5 { t.Fatalf("Stage V0.5 should produce 5 sub-artifacts, got %d", len(resV05.SubArtifacts)) } keys := make([]string, len(resV05.SubArtifacts)) for i, sub := range resV05.SubArtifacts { keys[i] = sub.TaskKey } t.Logf("✓ Stage V0.5 成功完成: 包含任务=%v, 成熟度评分=%d (%s)", keys, resV05.AdvisorBrief.MaturityScore, resV05.AdvisorBrief.MaturityLevel) // 验证 feasibility_reg 受控文档已建立 docID := "doc_proj1001_bd_feasibility_reg" docRec, err := bizdev.Snapshot(svc).Gov.GetDocument(ctx, docID) if err != nil || docRec == nil { t.Fatalf("get docgov record failed: %v", err) } if docRec.AuthorClaimed { t.Fatalf("document should NOT be claimed yet (unclaimed initially)") } if docRec.IsLocked { t.Fatalf("document in drafting state should NOT be locked") } t.Logf("✓ 受控文档台账已成功生成: doc_id=%s, state=%s, author=%s, claimed=%v", docRec.DocID, docRec.CurrentState, docRec.AuthorName, docRec.AuthorClaimed) t.Log("================================================================================") t.Log(">>> 阶段 3: 编制负责人查阅正文触发认领 (Author Claims Document) <<<") t.Log("================================================================================") docClaimed, err := bizdev.Snapshot(svc).Gov.RecordDocumentViewed(ctx, docID, authorID, docgov.RoleAuthor, authorName) if err != nil { t.Fatalf("RecordDocumentViewed failed: %v", err) } if !docClaimed.AuthorClaimed { t.Fatalf("document should be marked as AuthorClaimed=true after author viewed non-template") } t.Logf("✓ 编制负责人完成查阅认领: doc_id=%s, author_claimed=%v, claimed_at=%v", docClaimed.DocID, docClaimed.AuthorClaimed, docClaimed.AuthorClaimedAt) t.Log("================================================================================") t.Log(">>> 阶段 4: 上游客户输入变更触发阶段重新执行 -> 触发文档治理拦截并生成提案 <<<") t.Log("================================================================================") stageReqV05Modified := stageReqV05 stageReqV05Modified.TargetMarket = "中国NMPA二类 + 欧盟CE MDR" resV05Intercepted := executeStageDrainingPending(t, ctx, svc, stageReqV05Modified, authorID, cfg.DataDir) if resV05Intercepted.Success { t.Fatalf("Stage V0.5 re-run on claimed document should be intercepted (Success=false)") } // 寻找被拦截的子任务 var interceptedSub *bizdev.TaskRunResult for _, sub := range resV05Intercepted.SubArtifacts { if sub.RequiresAuthorApproval && sub.PendingProposal != nil { interceptedSub = sub break } } if interceptedSub == nil { t.Fatalf("expected one sub-artifact with RequiresAuthorApproval=true and non-nil PendingProposal") } propID := interceptedSub.PendingProposal.ProposalID t.Logf("✓ 成功拦截阶段执行: 任务=[%s], 提案ID=[%s], 拟修改提要=[%s]", interceptedSub.TaskKey, propID, interceptedSub.PendingProposal.ProposedChanges) t.Log("================================================================================") t.Log(">>> 阶段 5: 编制负责人拍板同意变更提案 (Author Approves Proposal) <<<") t.Log("================================================================================") proposalDecided, err := bizdev.Snapshot(svc).Gov.DecideModificationProposal( ctx, propID, authorID, true, "经评估产品出海战略,同意增补欧盟CE MDR合规条款与多语言标签评估", ) if err != nil { t.Fatalf("DecideModificationProposal failed: %v", err) } if proposalDecided.Status != docgov.ProposalApprovedByAuthor { t.Fatalf("proposal status should be approved_by_author, got: %s", proposalDecided.Status) } t.Logf("✓ 编制负责人成功拍板提案: status=%s, reason=%s", proposalDecided.Status, proposalDecided.AuthorDecisionReason) t.Log("================================================================================") t.Log(">>> 阶段 6: 携带授权强制重新执行阶段 (ForceDirectEdit=true) <<<") t.Log("================================================================================") stageReqV05Force := stageReqV05Modified stageReqV05Force.ForceDirectEdit = true resV05Updated := executeStageDrainingPending(t, ctx, svc, stageReqV05Force, authorID, cfg.DataDir) if !resV05Updated.Success { t.Fatalf("Stage V0.5 force re-run should succeed, error: %s", resV05Updated.ErrorReason) } for _, art := range resV05Updated.SubArtifacts { if workflowdef.SameTaskKey(art.TaskKey, "feasibility_reg") { _ = bizdev.Snapshot(svc).Gov.SetDocumentWorkDir(ctx, docID, art.WorkDir) break } } t.Log("================================================================================") t.Log(">>> 阶段 7: 受控文档三级责任人闭环流转 (送审 -> 批注 -> 审核通过 -> 批准生效) <<<") t.Log("================================================================================") // 1. 编制人送审 docReviewing, err := bizdev.Snapshot(svc).Gov.SubmitForReview(ctx, docID, authorID, docgov.RoleAuthor, authorName) if err != nil { t.Fatalf("SubmitForReview failed: %v", err) } if docReviewing.CurrentState != docgov.StateReviewing || !docReviewing.IsLocked { t.Fatalf("doc state should be reviewing and locked") } t.Logf("✓ 编制人提交送审: doc_state=%s, is_locked=%v", docReviewing.CurrentState, docReviewing.IsLocked) // 2. 审核人添加批注意见 comm, err := bizdev.Snapshot(svc).Gov.AddComment( ctx, docID, reviewerID, reviewerName, docgov.RoleReviewer, "## 关键性能与型检预估", docgov.CommentSuggestion, "针对荧光双波长光学滤光衰减特性,建议在送检前增加YY 0068.1硬管内窥镜机械强度摸底", ) if err != nil { t.Fatalf("AddComment failed: %v", err) } t.Logf("✓ 审核人添加章节批注: comment_id=%d, type=%s, section=%s", comm.ID, comm.CommentType, comm.TargetSection) // 3. 审核人审核通过 docApproving, err := bizdev.Snapshot(svc).Gov.ReviewApprove(ctx, docID, reviewerID, docgov.RoleReviewer, reviewerName, "经审阅合规策略严密,准予进入总监批准") if err != nil { t.Fatalf("ReviewApprove failed: %v", err) } if docApproving.CurrentState != docgov.StateApproving || !docApproving.IsLocked { t.Fatalf("doc state should be approving and locked") } t.Logf("✓ 审核人审核通过: doc_state=%s, is_locked=%v", docApproving.CurrentState, docApproving.IsLocked) // 4. 批准人终态批准归档 (自动触发 onApprovedHook 切片向量化沉淀为标杆案例) docApproved, err := bizdev.Snapshot(svc).Gov.ApproverApprove(ctx, docID, approverID, docgov.RoleApprover, approverName, "符合转化中心立项及CDO报价基线标准,正式批准生效") if err != nil { t.Fatalf("ApproverApprove failed: %v", err) } if docApproved.CurrentState != docgov.StateApproved || !docApproved.IsLocked || docApproved.ApprovedAt == nil { t.Fatalf("doc state should be approved, locked with non-nil ApprovedAt") } t.Logf("✓ 批准人最终批准生效: doc_state=%s, approved_at=%v", docApproved.CurrentState, docApproved.ApprovedAt) t.Log("================================================================================") t.Log(">>> 阶段 8: 执行成熟度阶段 V1.0 (工程拆解、Excel测算与CDO商业报价) <<<") t.Log("================================================================================") stageReqV10 := bizdev.StageRunRequest{ Stage: bizdev.StageV10_Quoting, ProjectID: projectID, ProjectName: projectName, CustomerName: customerName, DeviceType: deviceType, IntendedUse: intendedUse, TargetMarket: targetMarket, Backend: "stub", UserID: authorID, AuthorID: authorID, AuthorName: authorName, ReviewerID: reviewerID, ReviewerName: reviewerName, ApproverID: approverID, ApproverName: approverName, } resV10, err := svc.ExecuteStage(ctx, stageReqV10) if err != nil { t.Fatalf("Stage V1.0 execution failed: %v", err) } if !resV10.Success { t.Fatalf("Stage V1.0 should succeed, error: %s", resV10.ErrorReason) } if resV10.PrimaryArtifact == nil || !workflowdef.SameTaskKey(resV10.PrimaryArtifact.TaskKey, "cdo_quote") { t.Fatalf("Stage V1.0 primary artifact should be cdo_quote") } // 验证已生成 Excel 定价核算附件 hasExcel := false for _, att := range resV10.PrimaryArtifact.Attachments { if strings.HasSuffix(att.Filename, ".xlsx") || strings.Contains(att.Filename, "cdo_quote") { hasExcel = true t.Logf("✓ 成功产出商业报价 Excel 附件: %s (大小: %d 字节, 类型: %s)", att.Filename, len(att.Data), att.ContentType) break } } if !hasExcel { t.Errorf("expected Excel attachment in cdo_quote primary artifact") } t.Logf("✓ Stage V1.0 商业锁定成功: 交付物=[%s], 成熟度评分=%d (%s), 研判=[%s]", resV10.PrimaryArtifact.ArtifactTitle, resV10.AdvisorBrief.MaturityScore, resV10.AdvisorBrief.MaturityLevel, resV10.AdvisorBrief.MaturityHeadline) t.Log("================================================================================") t.Log(">>> 阶段 9: 全流程 SSE 事件时序与轻量化事件校验 <<<") t.Log("================================================================================") eventsMu.Lock() totalEvents := len(collectedEvents) var hasPhase, hasSummary, hasGovernance, hasDeliverable bool for _, ev := range collectedEvents { switch ev.Type { case stream.EventPhase: hasPhase = true case stream.EventSummary: hasSummary = true case stream.EventGovernance: hasGovernance = true case stream.EventDeliverable: hasDeliverable = true } } eventsMu.Unlock() if totalEvents == 0 { t.Fatalf("expected streamed events, got 0") } if !hasPhase { t.Errorf("expected EventPhase in stream") } if !hasSummary { t.Errorf("expected EventSummary in stream") } if !hasGovernance { t.Errorf("expected EventGovernance in stream") } if !hasDeliverable { t.Errorf("expected EventDeliverable in stream") } t.Logf("✓ 全流程 SSE 流式事件校验完美通过! 捕获事件总数=%d, 包含 phase=%v, summary=%v, governance=%v, deliverable=%v", totalEvents, hasPhase, hasSummary, hasGovernance, hasDeliverable) t.Log("================================================================================") t.Log(">>> 阶段 10: 验证受控批准交付物沉淀为标杆案例,并在后续跨项目咨询中自动召回 <<<") t.Log("================================================================================") recCases, err := bizdev.Snapshot(svc).Ctx.SearchSimilarCases(ctx, bizdev.CaseSearchOptions{ Query: "4K超高清荧光硬管内窥镜摄像系统", Limit: 3, MinScore: 0.05, }) if err != nil { t.Fatalf("SearchSimilarCases in Stage 10 failed: %v", err) } if len(recCases) == 0 { t.Fatalf("expected approved document from Stage 7 to be indexed and recommended, got 0") } t.Logf("✓ 成功自推荐历史批准标杆案例: 命中数=%d, 首个案例=[%s] %s, 相似度=%.2f", len(recCases), recCases[0].ArtifactTitle, recCases[0].SectionTitle, recCases[0].SimilarityScore) t.Log("================================================================================") t.Log("🎉 全业务链 (V0.1 -> V0.5 -> 文档认领/拦截/批准 -> V1.0 -> 标杆案例自推荐) 端到端大贯通圆满成功!") t.Log("================================================================================") }