Published manifests now use bd_/pm_/tm_/sw_ prefixes so workflows cannot share a doc id. Frozen instances keep the old keys; lookup, templates, and duty checks treat the two names as the same task. Co-authored-by: Cursor <cursoragent@cursor.com>
447 lines
18 KiB
Go
447 lines
18 KiB
Go
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("================================================================================")
|
|
}
|