package docgov_test import ( "context" "testing" "chat-agent-system/internal/docgov" ) func TestDocGov_AuthorPermissionsAndLocking(t *testing.T) { ctx := context.Background() engine := docgov.NewGovernanceEngine(nil) // 1. 初始化文档:Author=101(张工), Reviewer=102(李主管), Approver=103(王总监) doc, err := engine.CreateOrGetDocument( ctx, "doc_feasibility_001", 1, "feasibility_reg", "4K超高清腹腔镜注册可行性评估报告", "V0.5_feasibility", "data/dev/workspaces/bizdev/feasibility_reg_1", 101, "张工", 102, "李主管", 103, "王总监", ) if err != nil { t.Fatalf("CreateOrGetDocument failed: %v", err) } // 2. 只有编制人有权修改正文 _, err = engine.RecordDocumentEdited(ctx, doc.DocID, 101, docgov.RoleAuthor, "张工") if err != nil { t.Fatalf("Author should be allowed to edit in drafting state, got: %v", err) } // 3. 审核人尝试直接修改正文 -> 应当被拒绝 _, err = engine.RecordDocumentEdited(ctx, doc.DocID, 102, docgov.RoleReviewer, "李主管") if err != docgov.ErrReviewerCannotEdit { t.Fatalf("expected ErrReviewerCannotEdit, got: %v", err) } // 4. 批准人尝试直接修改正文 -> 应当被拒绝 _, err = engine.RecordDocumentEdited(ctx, doc.DocID, 103, docgov.RoleApprover, "王总监") if err != docgov.ErrApproverCannotEdit { t.Fatalf("expected ErrApproverCannotEdit, got: %v", err) } // 5. 非指定的其他人尝试以 Author 角色修改 -> 权限拒绝 _, err = engine.RecordDocumentEdited(ctx, doc.DocID, 999, docgov.RoleAuthor, "非法用户") if err == nil { t.Fatalf("expected unauthorized actor error, got nil") } // 6. 送审后进入锁定状态,此时即使 Author 也不能直接修改正文 _, err = engine.SubmitForReview(ctx, doc.DocID, 101, docgov.RoleAuthor, "张工") if err != nil { t.Fatalf("SubmitForReview failed: %v", err) } _, err = engine.RecordDocumentEdited(ctx, doc.DocID, 101, docgov.RoleAuthor, "张工") if err == nil { t.Fatalf("expected document locked error when editing during reviewing state, got nil") } } func TestDocGov_ClaimingMechanismAndProposal(t *testing.T) { ctx := context.Background() engine := docgov.NewGovernanceEngine(nil) docID := "doc_intake_002" doc, err := engine.CreateOrGetDocument( ctx, docID, 1, "intake", "立项与技术摸底需求表", "V0.1_triage", "workspaces/intake_2", 201, "刘工", 202, "赵主管", 203, "孙总监", ) if err != nil { t.Fatalf("CreateOrGetDocument failed: %v", err) } // 1. 初始状态为模板状态 (is_template_only = true) if !doc.IsTemplateOnly { t.Fatalf("initial state should be template only") } // 2. 编制人在模板状态下查看文档 -> 不算认领! doc, err = engine.RecordDocumentViewed(ctx, docID, 201, docgov.RoleAuthor, "刘工") if err != nil { t.Fatalf("RecordDocumentViewed failed: %v", err) } if doc.AuthorClaimed { t.Fatalf("author should NOT be claimed when viewing a template-only document") } // 3. 此时系统或阶段发起修改 -> 允许直接编辑 (未认领) checkRes, err := engine.CheckModificationPermission(ctx, docID, docgov.RoleSystem) if err != nil { t.Fatalf("CheckModificationPermission failed: %v", err) } if !checkRes.AllowedDirectEdit || checkRes.RequiresProposal { t.Fatalf("expected AllowedDirectEdit=true and RequiresProposal=false for unclaimed document") } // 4. 智能体小队生成了正式产出正文,脱离模板状态 err = engine.MarkDocumentContentUpdated(ctx, docID, "sha256_mock_content_12345", false) if err != nil { t.Fatalf("MarkDocumentContentUpdated failed: %v", err) } // 5. 编制人查看已生成的正式产出文档 -> 触发认领! doc, err = engine.RecordDocumentViewed(ctx, docID, 201, docgov.RoleAuthor, "刘工") if err != nil { t.Fatalf("RecordDocumentViewed failed: %v", err) } if !doc.AuthorClaimed { t.Fatalf("author MUST be claimed after viewing non-template document") } // 6. 再次发起阶段修改 -> 拦截直接编辑,必须生成变更提案 checkRes2, err := engine.CheckModificationPermission(ctx, docID, docgov.RoleSystem) if err != nil { t.Fatalf("CheckModificationPermission failed: %v", err) } if checkRes2.AllowedDirectEdit || !checkRes2.RequiresProposal { t.Fatalf("expected AllowedDirectEdit=false and RequiresProposal=true for claimed document") } // 7. 生成变更提案 proposal, err := engine.ProposeModification( ctx, docID, "agent_squad_upstream_sync", "根据客户最新电话沟通,将目标市场从【国内】追加【欧盟CE】,并增加YY/T1603标准条款", "影响法规注册路径,需要从单纯二类补充MDR Annex VIII分类与CER评估", "可能延展型检周期4周,增加CE技术文档编制预算", ) if err != nil { t.Fatalf("ProposeModification failed: %v", err) } if proposal.Status != docgov.ProposalPendingAuthorApproval { t.Fatalf("proposal status should be pending_author_approval, got %s", proposal.Status) } // 8. 编制人查看并拍板同意该变更 proposal, err = engine.DecideModificationProposal(ctx, proposal.ProposalID, 201, true, "经评估追加CE符合公司Q3出海战略,同意智能体小队自动增补章节") if err != nil { t.Fatalf("DecideModificationProposal failed: %v", err) } if proposal.Status != docgov.ProposalApprovedByAuthor { t.Fatalf("expected status approved_by_author, got %s", proposal.Status) } } func TestDocGov_FullApprovalLifecycleAndRejectRollback(t *testing.T) { ctx := context.Background() engine := docgov.NewGovernanceEngine(nil) docID := "doc_cdo_quote_003" doc, err := engine.CreateOrGetDocument( ctx, docID, 1, "cdo_quote", "4K腔镜系统CDO工程拆解与商务报价", "V1.0_quoting", "workspaces/cdo_quote_3", 301, "钱工", // 编制人 302, "周主管", // 审核人 303, "吴总监", // 批准人 ) if err != nil { t.Fatalf("CreateOrGetDocument failed: %v", err) } // 1. 编制人完成正文并送审 doc, err = engine.SubmitForReview(ctx, docID, 301, docgov.RoleAuthor, "钱工") if err != nil { t.Fatalf("SubmitForReview failed: %v", err) } if doc.CurrentState != docgov.StateReviewing || !doc.IsLocked { t.Fatalf("doc state should be reviewing and locked") } // 2. 审核人查阅并添加批注 comm, err := engine.AddComment(ctx, docID, 302, "周主管", docgov.RoleReviewer, "## BOM预算与工时", docgov.CommentSuggestion, "请复核EMC整改冗余工时是否按照2轮测算") if err != nil { t.Fatalf("AddComment failed: %v", err) } if comm.Status != "open" { t.Fatalf("expected comment status open") } // 3. 审核人驳回测试:原因非空校验 _, err = engine.ReviewReject(ctx, docID, 302, docgov.RoleReviewer, "周主管", " ") if err != docgov.ErrRejectReasonRequired { t.Fatalf("expected ErrRejectReasonRequired for empty rejection reason, got: %v", err) } // 审核人提供明确理由驳回 -> 退回编制草稿并解锁 doc, err = engine.ReviewReject(ctx, docID, 302, docgov.RoleReviewer, "周主管", "型检EMC整改工时偏低,请补齐风险冗余后重提") if err != nil { t.Fatalf("ReviewReject failed: %v", err) } if doc.CurrentState != docgov.StateDrafting || doc.IsLocked { t.Fatalf("doc should rollback to drafting and be unlocked, state=%s locked=%v", doc.CurrentState, doc.IsLocked) } if doc.LastRejectedReason != "型检EMC整改工时偏低,请补齐风险冗余后重提" { t.Fatalf("unexpected last rejected reason: %s", doc.LastRejectedReason) } // 4. 编制人修改后重新送审 _, _ = engine.RecordDocumentEdited(ctx, docID, 301, docgov.RoleAuthor, "钱工") doc, err = engine.SubmitForReview(ctx, docID, 301, docgov.RoleAuthor, "钱工") if err != nil { t.Fatalf("resubmit review failed: %v", err) } // 5. 审核人通过 -> 进入 approving (批准中) doc, err = engine.ReviewApprove(ctx, docID, 302, docgov.RoleReviewer, "周主管", "复核无误,工时冗余合理,同意提交批准") if err != nil { t.Fatalf("ReviewApprove failed: %v", err) } if doc.CurrentState != docgov.StateApproving || !doc.IsLocked { t.Fatalf("expected doc in approving state and locked") } // 6. 批准人查阅批注并最终批准 -> 进入 approved (受控归档) doc, err = engine.ApproverApprove(ctx, docID, 303, docgov.RoleApprover, "吴总监", "战略预算与报价符合要求,批准受控下发") if err != nil { t.Fatalf("ApproverApprove failed: %v", err) } if doc.CurrentState != docgov.StateApproved || !doc.IsLocked || doc.ApprovedAt == nil { t.Fatalf("doc should be approved and locked with approved_at set") } // 7. 验证审计日志完整记录 logs, err := engine.ListAuditLogs(ctx, docID) if err != nil { t.Fatalf("ListAuditLogs failed: %v", err) } if len(logs) < 5 { t.Fatalf("expected at least 5 audit logs, got %d", len(logs)) } } func TestMarkProposalApplied_MissingOrNotApproved(t *testing.T) { ctx := context.Background() engine := docgov.NewGovernanceEngine(nil) if err := engine.MarkProposalApplied(ctx, "prop_missing"); err != docgov.ErrProposalNotFound { t.Fatalf("missing want ErrProposalNotFound got %v", err) } docID := "doc_mark_prop" _, err := engine.CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", "", 1, "a", 2, "r", 3, "p") if err != nil { t.Fatal(err) } prop, err := engine.ProposeModification(ctx, docID, "agent", "改", "影响", "风险") if err != nil { t.Fatal(err) } if err := engine.MarkProposalApplied(ctx, prop.ProposalID); err == nil { t.Fatal("unapproved proposal must not MarkProposalApplied") } // Unapproved failure must leave status unchanged (no false applied in memory). if prop.Status == docgov.ProposalApplied { t.Fatal("memory must not be ProposalApplied after failed mark") } } func TestMarkProposalApplied_IdempotentAfterApprove(t *testing.T) { ctx := context.Background() engine := docgov.NewGovernanceEngine(nil) docID := "doc_mark_prop_ok" _, err := engine.CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", "", 1, "a", 2, "r", 3, "p") if err != nil { t.Fatal(err) } prop, err := engine.ProposeModification(ctx, docID, "agent", "改", "影响", "风险") if err != nil { t.Fatal(err) } if _, err := engine.DecideModificationProposal(ctx, prop.ProposalID, 1, true, "同意"); err != nil { t.Fatal(err) } if err := engine.MarkProposalApplied(ctx, prop.ProposalID); err != nil { t.Fatalf("first apply: %v", err) } if err := engine.MarkProposalApplied(ctx, prop.ProposalID); err != nil { t.Fatalf("second apply must be idempotent: %v", err) } }