Files
weichenandCursor dba8139d56 feat(workflow): namespace task keys without breaking old instances
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>
2026-09-19 10:50:25 +00:00

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("================================================================================")
}