Files
Agent-System/internal/server/piagent_e2e_test.go
T
weichen 3145550a59 feat(security): complete P0 release blocking remediation
Unify auth middleware and remove X-User-ID trust, add project_members
authorization, sandbox workspaces via resolver, and admin-only reconcile
by run_id. Introduce goose migrations with startup schema gate, fail closed
when DB unavailable outside explicit ALLOW_IN_MEMORY, bundle web/dist in
app Dockerfile, and remove frontend pseudo-login fallback.
2026-09-13 02:50:22 +00:00

459 lines
18 KiB
Go

package server_test
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"time"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/config"
"chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/server"
"chat-agent-system/internal/squad"
)
// TestPiAgent_FullLifecycle_MultiRole_RealWorld_E2E
// 贯穿:新建项目 -> 角色权限配置 -> 分角色材料补充与真实对话 -> 自由非标任务执行 -> 工作流推进 -> 审核驳回/再批 -> 数据库落盘与全量审计追溯
func TestPiAgent_FullLifecycle_MultiRole_RealWorld_E2E(t *testing.T) {
if testing.Short() {
t.Skip("skipping slow real-world E2E test in short mode")
}
// 1. 初始化配置并强制指定 pi-agent 后端与真实 Runner
cfg := &config.Config{
Env: "dev",
AgentBackend: "pi-agent",
PiAgentRunnerURL: "http://127.0.0.1:18865",
PiAgentModel: "MiniMax-M3",
PiAgentProvider: "minimax",
DataDir: t.TempDir(),
AllowInMemory: true,
}
svc := bizdev.MustNewService(t, cfg)
srv := server.NewServer(cfg, svc)
ts := httptest.NewServer(srv.Routes())
defer ts.Close()
client := &http.Client{Timeout: 10 * time.Minute}
ctx := context.Background()
// 2. 验证 Health 确认 AgentBackend 为 pi-agent
respHealth, err := client.Get(ts.URL + "/api/v1/health")
if err != nil {
t.Fatalf("health check failed: %v", err)
}
var healthData server.HealthResponse
_ = json.NewDecoder(respHealth.Body).Decode(&healthData)
respHealth.Body.Close()
t.Logf("✓ [0. 环境校验] Health检查正常: Env=%s, AgentBackend=%s, SharedKBPort=%d",
healthData.Env, healthData.AgentBackend, healthData.SharedKBPort)
// 3. 项目基本信息与三级角色定义
projectID := int64(time.Now().UnixNano()%900000 + 100000)
projectName := "数字化高频超声手术吸引系统 (CUSA-Ultra)"
customerName := "北京协和神外微创转化中心"
deviceType := "高频超声手术吸引系统"
intendedUse := "神经外科与肝胆外科精密手术中利用 24kHz-35kHz 超声微幅振动选择性粉碎并吸引病灶组织,保留邻近大血管与神经"
targetMarket := "中国NMPA三类创新医疗器械"
authorID := int64(1)
authorName := "张工"
reviewerID := int64(2)
reviewerName := "李主管"
approverID := int64(3)
approverName := "王总监"
sessionID := fmt.Sprintf("sess_cusa_%d", time.Now().UnixNano())
// 4. 【编制人·张工】登录与立项阶段对话 (穿插非工作流问答 + 行业标准咨询)
t.Log("--------------------------------------------------------------------------------")
t.Log(">>> 步骤 1: 【编制人·张工】开展立项对接对话与标准咨询 (含非工作流穿插交互)")
t.Log("--------------------------------------------------------------------------------")
postChat := func(msg string, uID int64, uName, uRole string) string {
chatReq := server.ChatRequest{
SessionID: sessionID,
Message: msg,
ProjectName: projectName,
CustomerName: customerName,
DeviceType: deviceType,
IntendedUse: intendedUse,
TargetMarket: targetMarket,
UserName: uName,
UserRole: uRole,
UserID: uID,
ProjectID: projectID,
}
b, _ := json.Marshal(chatReq)
res, err := client.Post(ts.URL+"/api/v1/chat", "application/json", bytes.NewReader(b))
if err != nil {
t.Fatalf("post chat failed: %v", err)
}
defer res.Body.Close()
var cr struct {
ReplyText string `json:"reply_text"`
Intent string `json:"intent"`
Branch string `json:"branch"`
}
_ = json.NewDecoder(res.Body).Decode(&cr)
return cr.ReplyText
}
// 4.1 穿插闲聊 / 非工作流技术探讨论述
casualReply := postChat("参谋,你觉得明天的天气怎么样?另外帮我解释一下什么叫压电换能器的逆压电效应?", authorID, authorName, "author")
t.Logf("✓ [张工-闲聊与技术咨询] 协同参谋答复:\n%s", casualReply)
// 4.2 业务补充与立项需求对话
intakeReply := postChat("我们准备正式立项 CUSA-Ultra 项目,刀头频率设定为 24kHz 与 35kHz 双频,具备实时灌注与负压吸引,满足神经外科浅脑肿瘤切除,请问法规应关注哪些强标?", authorID, authorName, "author")
t.Logf("✓ [张工-立项需求对话] 协同参谋答复:\n%s", intakeReply)
// 5. 推进成熟度工作流 V0.1: 立项摸底 (intake)
t.Log("--------------------------------------------------------------------------------")
t.Log(">>> 步骤 2: 推进工作流成熟度阶段 V0.1 (立项需求摸底与盲区探测)")
t.Log("--------------------------------------------------------------------------------")
stageReqV01 := bizdev.StageRunRequest{
Stage: bizdev.StageV01_Triage,
ProjectID: projectID,
ProjectName: projectName,
CustomerName: customerName,
DeviceType: deviceType,
IntendedUse: intendedUse,
TargetMarket: targetMarket,
Backend: "pi-agent",
UserID: authorID,
AuthorID: authorID,
AuthorName: authorName,
ReviewerID: reviewerID,
ReviewerName: reviewerName,
ApproverID: approverID,
ApproverName: approverName,
Fields: map[string]string{
"operating_frequency": "24kHz / 35kHz",
"transducer_type": "PZT-8 压电陶瓷复合换能器",
"cooling_flow_rate": "10-50 mL/min 生理盐水冲洗",
},
}
resV01, err := svc.ExecuteStage(ctx, stageReqV01)
if err != nil {
t.Fatalf("ExecuteStage V0.1 failed: %v", err)
}
if !resV01.Success {
t.Fatalf("Stage V0.1 failed: %s", resV01.ErrorReason)
}
t.Logf("✓ [V0.1 阶段完成] 交付物: %s, 成熟度评分: %d, 标题: %s",
resV01.PrimaryArtifact.ArtifactTitle, resV01.AdvisorBrief.MaturityScore, resV01.AdvisorBrief.MaturityHeadline)
targetDocID := ""
if resV01.PrimaryArtifact != nil && resV01.PrimaryArtifact.DocRecord != nil {
targetDocID = resV01.PrimaryArtifact.DocRecord.DocID
}
if targetDocID == "" {
targetDocID = fmt.Sprintf("doc_proj%d_intake_v1", projectID)
}
// 6. 推进自由非标任务 (Free Ad-Hoc Task): 24kHz 高频空化钛合金刀头微裂纹与疲劳剥落风险专项分析报告
t.Log("--------------------------------------------------------------------------------")
t.Log(">>> 步骤 3: 触发自由非标任务 (Free Ad-Hoc Task) - 超声空化微射流与钛合金疲劳断裂专项研判")
t.Log("--------------------------------------------------------------------------------")
freeTaskReq := server.TaskRunRequestDTO{
TaskKey: "free_cusa_cavitation_risk",
ProjectName: projectName,
CustomerName: customerName,
DeviceType: deviceType,
IntendedUse: intendedUse,
TargetMarket: targetMarket,
Backend: "pi-agent",
ProjectID: projectID,
AuthorID: authorID,
AuthorName: authorName,
ReviewerID: reviewerID,
ReviewerName: reviewerName,
ApproverID: approverID,
ApproverName: approverName,
Fields: map[string]string{
"material_grade": "Ti-6Al-4V ELI (超低间隙五级钛合金)",
"tip_amplitude_um": "300 微米纵向振幅峰峰值",
"cavitation_cycles": "10^8 次高周疲劳循环",
},
}
// 为自由非标任务定制快速精简 WBS 步骤,确保单测试验证效率
if os.Getenv("RUN_FULL_ADHOC_WBS") != "1" {
freeTaskReq.CustomSteps = []squad.StepSpec{
{
ID: "step_01_cavitation_analysis",
Role: "writer",
Title: "钛合金刀头高周超声疲劳与空化剥落风险专项研判",
SubWorkspace: "tasks/01_analysis",
Mode: "execute",
Accept: []string{
"分析 Ti-6Al-4V ELI 在 24kHz/35kHz 空化微射流下的疲劳断裂机理",
"给出刀头表面喷丸强化与声学阻抗匹配方案",
"交付正文 output.md",
},
},
}
}
freeBytes, _ := json.Marshal(freeTaskReq)
freeResp, err := client.Post(ts.URL+"/api/v1/tasks/run", "application/json", bytes.NewReader(freeBytes))
if err != nil {
t.Fatalf("run free task failed: %v", err)
}
var freeRes struct {
Success bool `json:"success"`
ArtifactTitle string `json:"artifact_title"`
WorkDir string `json:"work_dir"`
}
_ = json.NewDecoder(freeResp.Body).Decode(&freeRes)
freeResp.Body.Close()
t.Logf("✓ [自由非标任务完成] 任务成功=%v, 交付物=[%s]", freeRes.Success, freeRes.ArtifactTitle)
// 7. 若开启全量阶段环境变量 (RUN_HEAVY_STAGES=1),则继续演练 V0.5 与 V1.0
if os.Getenv("RUN_HEAVY_STAGES") == "1" {
t.Log("--------------------------------------------------------------------------------")
t.Log(">>> 步骤 4: 推进工作流成熟度阶段 V0.5 (法规合规与注册可行性论证)")
t.Log("--------------------------------------------------------------------------------")
stageReqV05 := bizdev.StageRunRequest{
Stage: bizdev.StageV05_Feasibility,
ProjectID: projectID,
ProjectName: projectName,
CustomerName: customerName,
DeviceType: deviceType,
IntendedUse: intendedUse,
TargetMarket: targetMarket,
Backend: "pi-agent",
UserID: authorID,
AuthorID: authorID,
AuthorName: authorName,
ReviewerID: reviewerID,
ReviewerName: reviewerName,
ApproverID: approverID,
ApproverName: approverName,
}
resV05, err := svc.ExecuteStage(ctx, stageReqV05)
if err != nil {
t.Fatalf("Stage V0.5 execution failed: %v", err)
}
if !resV05.Success {
t.Fatalf("Stage V0.5 should succeed, got error: %s", resV05.ErrorReason)
}
if resV05.PrimaryArtifact != nil && resV05.PrimaryArtifact.DocRecord != nil {
targetDocID = resV05.PrimaryArtifact.DocRecord.DocID
t.Logf("✓ [V0.5 阶段完成] 法规可行性交付物: %s, DocID=%s, 成熟度评分=%d",
resV05.PrimaryArtifact.ArtifactTitle, targetDocID, resV05.AdvisorBrief.MaturityScore)
}
t.Log("--------------------------------------------------------------------------------")
t.Log(">>> 步骤 4.2: 推进成熟度阶段 V1.0 (商业锁定与 CDO 报价 Excel 自动测算)")
t.Log("--------------------------------------------------------------------------------")
stageReqV10 := bizdev.StageRunRequest{
Stage: bizdev.StageV10_Quoting,
ProjectID: projectID,
ProjectName: projectName,
CustomerName: customerName,
DeviceType: deviceType,
IntendedUse: intendedUse,
TargetMarket: targetMarket,
Backend: "pi-agent",
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, got error: %s", resV10.ErrorReason)
}
t.Logf("✓ [V1.0 阶段完成] 商业报价交付物: %s, 成熟度评分=%d (%s)",
resV10.PrimaryArtifact.ArtifactTitle, resV10.AdvisorBrief.MaturityScore, resV10.AdvisorBrief.MaturityLevel)
}
// 8. 受控文档生命周期演练: 编制人认领 -> 提交审核 -> 审核人批注 -> 审核驳回 -> 编制人修改并再提交 -> 审核人通过 -> 批准人最终批准
t.Log("--------------------------------------------------------------------------------")
t.Log(">>> 步骤 5: 受控文档闭环治理演练 (认领 -> 批注 -> 审核驳回 -> 修正再送审 -> 批准)")
t.Log("--------------------------------------------------------------------------------")
// 8.1 编制人【张工】查阅并显式认领
claimReq := map[string]any{
"doc_id": targetDocID,
"user_id": authorID,
"user_name": authorName,
}
cb, _ := json.Marshal(claimReq)
respClaim, err := client.Post(ts.URL+"/api/v1/docgov/claim", "application/json", bytes.NewReader(cb))
if err != nil || respClaim.StatusCode != http.StatusOK {
t.Fatalf("claim failed: %v", err)
}
respClaim.Body.Close()
t.Logf("✓ [张工] 成功认领受控文档: DocID=%s", targetDocID)
// 8.2 编制人【张工】送审 (进入 StateReviewing)
submitReq := map[string]any{
"doc_id": targetDocID,
"author_id": authorID,
"note": "初稿编制完成,已包含 24kHz/35kHz 双频超声与灌注吸引合规说明,请主管审核。",
}
sb, _ := json.Marshal(submitReq)
respSub, err := client.Post(ts.URL+"/api/v1/docgov/submit-review", "application/json", bytes.NewReader(sb))
if err != nil || respSub.StatusCode != http.StatusOK {
t.Fatalf("submit-review failed: %v", err)
}
respSub.Body.Close()
t.Logf("✓ [张工] 提交审核成功,文档进入锁定审核态")
// 8.3 【审核人·李主管】介入并添加批注
commReq := map[string]any{
"doc_id": targetDocID,
"user_id": reviewerID,
"user_name": reviewerName,
"user_role": "reviewer",
"target_section": "第三章:注册检验与电气安全合规路径",
"comment_type": "risk",
"content": "请补充说明超声刀头与高频手术设备(高频电刀)联合使用时,GB 9706.202 及 YY 0644 隔离抗扰度测试要求。",
}
cmb, _ := json.Marshal(commReq)
respComm, err := client.Post(ts.URL+"/api/v1/docgov/comments", "application/json", bytes.NewReader(cmb))
if err != nil || respComm.StatusCode != http.StatusOK {
t.Fatalf("add comment failed: %v", err)
}
respComm.Body.Close()
t.Logf("✓ [李主管] 审核批注已录入")
// 8.4 【审核人·李主管】第一次执行【驳回 (Reject)】以验证受控纠偏流
rejReq := map[string]any{
"doc_id": targetDocID,
"user_id": reviewerID,
"user_name": reviewerName,
"action": "reject",
"reason": "缺少高频电刀共模互扰防范机制论证,打回草稿态由编制人张工补充后重新送审。",
}
rjb, _ := json.Marshal(rejReq)
respRej, err := client.Post(ts.URL+"/api/v1/docgov/review-decision", "application/json", bytes.NewReader(rjb))
if err != nil || respRej.StatusCode != http.StatusOK {
t.Fatalf("review reject failed: %v", err)
}
respRej.Body.Close()
t.Logf("✓ [李主管] 驳回成功,受控文档自动解锁并退回 drafting 状态")
// 8.5 编制人【张工】再次送审
submitReq2 := map[string]any{
"doc_id": targetDocID,
"author_id": authorID,
"note": "已补充 GB 9706.202 绝缘隔离与高频消磁屏蔽方案,重新送审。",
}
sb2, _ := json.Marshal(submitReq2)
respSub2, err := client.Post(ts.URL+"/api/v1/docgov/submit-review", "application/json", bytes.NewReader(sb2))
if err != nil || respSub2.StatusCode != http.StatusOK {
t.Fatalf("resubmit failed: %v", err)
}
respSub2.Body.Close()
t.Logf("✓ [张工] 补充材料后再次送审成功")
// 8.6 【审核人·李主管】第二次审核 -> 审核通过 (Pass)
passReq := map[string]any{
"doc_id": targetDocID,
"user_id": reviewerID,
"user_name": reviewerName,
"action": "approve",
"comment": "方案已完善,GB 9706.202 及 YY 0644 检验项齐备,同意流转至王总监终审。",
}
pb, _ := json.Marshal(passReq)
respPass, err := client.Post(ts.URL+"/api/v1/docgov/review-decision", "application/json", bytes.NewReader(pb))
if err != nil || respPass.StatusCode != http.StatusOK {
t.Fatalf("review approve failed: %v", err)
}
respPass.Body.Close()
t.Logf("✓ [李主管] 审核通过,文档流转至待批准态 (StateApproving)")
// 8.7 【批准人·王总监】最终批准 (Approve)
apprReq := map[string]any{
"doc_id": targetDocID,
"user_id": approverID,
"user_name": approverName,
"action": "approve",
"comment": "符合创新微创手术吸引系统三年规划,同意立项并进入工程实施与核价锁定。",
}
apb, _ := json.Marshal(apprReq)
respAppr, err := client.Post(ts.URL+"/api/v1/docgov/approver-decision", "application/json", bytes.NewReader(apb))
if err != nil || respAppr.StatusCode != http.StatusOK {
t.Fatalf("approver decision failed: %v", err)
}
respAppr.Body.Close()
t.Logf("✓ [王总监] 最终批准成功,文档彻底锁定生效归档 (StateApproved)")
// 9. 验证全生命周期数据落盘与审计追溯
t.Log("--------------------------------------------------------------------------------")
t.Log(">>> 步骤 6: 审计与数据库落盘全面核查 (会话、消息、文档状态、审计日志)")
t.Log("--------------------------------------------------------------------------------")
// 9.1 验证审计日志 API (GET /api/v1/docgov/audit-logs)
respAudit, err := client.Get(fmt.Sprintf("%s/api/v1/docgov/audit-logs?doc_id=%s", ts.URL, targetDocID))
if err != nil || respAudit.StatusCode != http.StatusOK {
t.Fatalf("list audit logs failed: %v", err)
}
var auditLogs []docgov.AuditLog
_ = json.NewDecoder(respAudit.Body).Decode(&auditLogs)
respAudit.Body.Close()
if len(auditLogs) < 5 {
t.Errorf("expected at least 5 audit logs, got %d", len(auditLogs))
}
t.Logf("✓ 成功追溯受控文档全生命周期审计轨迹流 (%d 条记录):", len(auditLogs))
for idx, log := range auditLogs {
t.Logf(" [%d] %s | 操作人: %s (%s) | 动作: %s -> %s",
idx+1, log.CreatedAt.Format("15:04:05"), log.ActorName, log.ActorRole, log.FromState, log.ToState)
}
// 10.2 验证历史对话消息持久化 (GET /api/v1/chat/messages)
respMsgs, err := client.Get(fmt.Sprintf("%s/api/v1/chat/messages?session_id=%s", ts.URL, sessionID))
if err != nil || respMsgs.StatusCode != http.StatusOK {
t.Fatalf("get chat messages failed: %v", err)
}
var msgList []ctxmgr.ChatMessageRecord
_ = json.NewDecoder(respMsgs.Body).Decode(&msgList)
respMsgs.Body.Close()
if len(msgList) < 4 {
t.Errorf("expected at least 4 recorded messages, got %d", len(msgList))
}
t.Logf("✓ 成功查询并追溯全周期真实多轮对话持久化记录 (%d 条):", len(msgList))
for idx, m := range msgList {
roleName := "智能体参谋 (pi-agent/MiniMax)"
if m.Role == "user" {
roleName = "用户 (张工)"
}
summaryContent := m.Content
if len(summaryContent) > 60 {
summaryContent = summaryContent[:60] + "..."
}
t.Logf(" [%d] [%s] %s: %s (意图: %s, 分支: %s)",
idx+1, m.CreatedAt.Format("15:04:05"), roleName, summaryContent, m.Intent, m.Branch)
}
// 10.3 验证工作区物理产物存在
wsPath := filepath.Join(cfg.DataDir, "workspaces")
if fi, err := os.Stat(wsPath); err == nil && fi.IsDir() {
t.Logf("✓ 物理工作区目录正常落盘: %s", wsPath)
}
}