From c1e2575e0ae97f1c19cfe8e918c9595297fbbd27 Mon Sep 17 00:00:00 2001 From: WeiCHEN Date: Tue, 8 Sep 2026 13:07:31 +0800 Subject: [PATCH] test(api): add e2e integration tests for claimed document SSE and docgov RESTful APIs Co-authored-by: Cursor --- internal/server/e2e_test.go | 110 ++++++++++++++++++ internal/server/server_test.go | 201 +++++++++++++++++++++++++++++++++ 2 files changed, 311 insertions(+) diff --git a/internal/server/e2e_test.go b/internal/server/e2e_test.go index 0ff77920..389297b6 100644 --- a/internal/server/e2e_test.go +++ b/internal/server/e2e_test.go @@ -183,3 +183,113 @@ func TestE2E_TasksRunStreamPipeline(t *testing.T) { t.Errorf("expected EventDone in task stream") } } + +func TestE2E_ClaimedDocument_SSE_Interception(t *testing.T) { + cfg := &config.Config{ + Env: "test", + AgentBackend: "stub", + DataDir: "./data/test", + KbDbPort: 5432, + } + svc := bizdev.NewService(cfg) + srv := NewServer(cfg, svc) + + ts := httptest.NewServer(srv.Routes()) + defer ts.Close() + + docID := "doc_sse_claim_intercept_001" + projectID := int64(999) + authorID := int64(101) + + // 1. 首次触发任务执行 (生成正式产物) + taskReq1 := TaskRunRequestDTO{ + TaskKey: "feasibility_reg", + ProjectName: "智能微创手术机器人", + CustomerName: "北京某三甲医院", + DeviceType: "腔镜手术机器人", + Backend: "stub", + DocID: docID, + ProjectID: projectID, + AuthorID: authorID, + AuthorName: "张工", + } + reqBytes1, _ := json.Marshal(taskReq1) + res1, err := http.Post(ts.URL+"/api/v1/tasks/run", "application/json", bytes.NewReader(reqBytes1)) + if err != nil || res1.StatusCode != http.StatusOK { + t.Fatalf("first task run failed: %v", err) + } + res1.Body.Close() + + // 2. 编制人查阅正文触发认领 + claimReq := DocGovClaimRequest{ + DocID: docID, + UserID: authorID, + UserName: "张工", + UserRole: "author", + } + claimBytes, _ := json.Marshal(claimReq) + resClaim, err := http.Post(ts.URL+"/api/v1/docgov/claim", "application/json", bytes.NewReader(claimBytes)) + if err != nil || resClaim.StatusCode != http.StatusOK { + t.Fatalf("docgov claim failed: %v", err) + } + resClaim.Body.Close() + + // 3. 已认领文档通过 SSE 流式触发再次执行 -> 验证拦截并推送 governance 与 summary 关键节点事件 + taskReq2 := taskReq1 + taskReq2.TargetMarket = "欧盟CE+美国FDA" + reqBytes2, _ := json.Marshal(taskReq2) + + respStream, err := http.Post(ts.URL+"/api/v1/tasks/run/stream", "application/json", bytes.NewReader(reqBytes2)) + if err != nil { + t.Fatalf("post tasks run stream failed: %v", err) + } + defer respStream.Body.Close() + + if respStream.StatusCode != http.StatusOK { + t.Fatalf("expected status 200, got %d", respStream.StatusCode) + } + + scanner := bufio.NewScanner(respStream.Body) + var events []stream.StreamEvent + + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "data: ") { + var ev stream.StreamEvent + dataStr := strings.TrimPrefix(line, "data: ") + if err := json.Unmarshal([]byte(dataStr), &ev); err == nil { + events = append(events, ev) + } + } + } + + foundGovernance := false + foundSummary := false + var propID string + + for _, ev := range events { + if ev.Type == stream.EventGovernance && ev.Governance != nil { + foundGovernance = true + if ev.Governance.Action == "proposal_required" && ev.Governance.AuthorClaimed { + propID = ev.Governance.ProposalID + t.Logf("✓ SSE 流中成功捕获文档治理拦截事件: proposal_id=%s, 简报=%s", + propID, ev.Governance.NotificationTitle) + } + } + if ev.Type == stream.EventSummary && ev.Summary != nil { + foundSummary = true + t.Logf("✓ SSE 流中成功捕获轻量级摘要事件: 标题=%s, 内容=%s", + ev.Summary.Title, ev.Summary.Content) + } + } + + if !foundGovernance { + t.Errorf("expected EventGovernance in SSE stream for claimed document") + } + if !foundSummary { + t.Errorf("expected EventSummary in SSE stream for claimed document") + } + if propID == "" { + t.Errorf("expected valid proposal_id in governance event") + } +} diff --git a/internal/server/server_test.go b/internal/server/server_test.go index f0bdf20f..75ab2287 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -148,3 +148,204 @@ func TestServer_ChatSSEStream(t *testing.T) { t.Errorf("expected done event in SSE stream") } } + +func TestServer_ContextAndDocGovAPIs(t *testing.T) { + srv := newTestServer() + + // 1. 测试更新并获取用户偏好 + prefReq := UpdateUserPreferenceRequest{ + UserID: 101, + CommunicationStyle: "concise_structured", + FocusAreas: []string{"cost_budget", "reg_compliance"}, + OutputFormatPref: "standard_report", + CustomInstructions: "结论先行,重点突出NMPA二类申报风险", + } + prefBytes, _ := json.Marshal(prefReq) + req := httptest.NewRequest(http.MethodPut, "/api/v1/context/user/preference", bytes.NewReader(prefBytes)) + w := httptest.NewRecorder() + srv.Routes().ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("expected 200 from update user preference, got %d", w.Code) + } + + // 2. 测试获取单用户上下文 + req = httptest.NewRequest(http.MethodGet, "/api/v1/context/user?user_id=101&project_id=1", nil) + w = httptest.NewRecorder() + srv.Routes().ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("expected 200 from get user context, got %d", w.Code) + } + + // 3. 测试获取项目事实黑板上下文 + req = httptest.NewRequest(http.MethodGet, "/api/v1/context/project?project_id=1", nil) + w = httptest.NewRecorder() + srv.Routes().ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("expected 200 from get project context, got %d", w.Code) + } + + // 4. 测试任务运行与文档生成 + taskReq := TaskRunRequestDTO{ + TaskKey: "feasibility_reg", + ProjectName: "超声骨刀系统", + CustomerName: "北京水木东方", + DeviceType: "超声手术设备", + DocID: "doc_api_test_001", + UserID: 101, + AuthorID: 101, + AuthorName: "张工", + ReviewerID: 102, + ReviewerName: "李主管", + ApproverID: 103, + ApproverName: "王总监", + } + taskBytes, _ := json.Marshal(taskReq) + req = httptest.NewRequest(http.MethodPost, "/api/v1/tasks/run", bytes.NewReader(taskBytes)) + w = httptest.NewRecorder() + srv.Routes().ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("expected 200 from tasks/run, got %d", w.Code) + } + + // 5. 查询生成的受控文档 + req = httptest.NewRequest(http.MethodGet, "/api/v1/docgov/document?doc_id=doc_api_test_001", nil) + w = httptest.NewRecorder() + srv.Routes().ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("expected 200 from get docgov document, got %d", w.Code) + } + + // 6. 查阅并认领文档 + claimReq := DocGovClaimRequest{ + DocID: "doc_api_test_001", + UserID: 101, + UserName: "张工", + UserRole: "author", + } + claimBytes, _ := json.Marshal(claimReq) + req = httptest.NewRequest(http.MethodPost, "/api/v1/docgov/claim", bytes.NewReader(claimBytes)) + w = httptest.NewRecorder() + srv.Routes().ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("expected 200 from claim doc, got %d", w.Code) + } + + // 7. 已认领文档再次运行被拦截,生成变更提案 + taskReq2 := taskReq + taskReq2.TargetMarket = "欧盟CE" + taskBytes2, _ := json.Marshal(taskReq2) + req = httptest.NewRequest(http.MethodPost, "/api/v1/tasks/run", bytes.NewReader(taskBytes2)) + w = httptest.NewRecorder() + srv.Routes().ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("expected 200 with proposal from tasks/run, got %d", w.Code) + } + var interceptedRes bizdev.TaskRunResult + _ = json.NewDecoder(w.Body).Decode(&interceptedRes) + if !interceptedRes.RequiresAuthorApproval || interceptedRes.PendingProposal == nil { + t.Fatalf("expected RequiresAuthorApproval=true and non-nil PendingProposal") + } + + // 8. 编制人拍板同意变更提案 + decideReq := DecideProposalRequest{ + ProposalID: interceptedRes.PendingProposal.ProposalID, + AuthorID: 101, + Approve: true, + Reason: "经评估符合出海战略,同意增补CE条款", + } + decideBytes, _ := json.Marshal(decideReq) + req = httptest.NewRequest(http.MethodPost, "/api/v1/docgov/proposal/decide", bytes.NewReader(decideBytes)) + w = httptest.NewRecorder() + srv.Routes().ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("expected 200 from proposal/decide, got %d", w.Code) + } + + // 9. 添加批注 + commReq := AddCommentRequestDTO{ + DocID: "doc_api_test_001", + UserID: 102, + UserName: "李主管", + UserRole: "reviewer", + TargetSection: "## 法规结论", + CommentType: "suggestion", + Content: "建议补充针对大血管闭合的YY/T 0644条款", + } + commBytes, _ := json.Marshal(commReq) + req = httptest.NewRequest(http.MethodPost, "/api/v1/docgov/comments", bytes.NewReader(commBytes)) + w = httptest.NewRecorder() + srv.Routes().ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("expected 200 from add comment, got %d", w.Code) + } + + // 10. 送审 + submitReq := DocGovSubmitReviewRequest{ + DocID: "doc_api_test_001", + UserID: 101, + UserName: "张工", + } + submitBytes, _ := json.Marshal(submitReq) + req = httptest.NewRequest(http.MethodPost, "/api/v1/docgov/submit-review", bytes.NewReader(submitBytes)) + w = httptest.NewRecorder() + srv.Routes().ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("expected 200 from submit review, got %d", w.Code) + } + + // 11. 审核驳回校验 (空原因应被拦截) + rejReq := DocGovDecisionRequest{ + DocID: "doc_api_test_001", + UserID: 102, + UserName: "李主管", + Action: "reject", + Reason: "", + } + rejBytes, _ := json.Marshal(rejReq) + req = httptest.NewRequest(http.MethodPost, "/api/v1/docgov/review-decision", bytes.NewReader(rejBytes)) + w = httptest.NewRecorder() + srv.Routes().ServeHTTP(w, req) + if w.Code != http.StatusBadRequest { + t.Fatalf("expected 400 for empty reject reason, got %d", w.Code) + } + + // 12. 审核通过 + appReq := DocGovDecisionRequest{ + DocID: "doc_api_test_001", + UserID: 102, + UserName: "李主管", + Action: "approve", + Comment: "初审符合注册法规要求,同意转交批准", + } + appBytes, _ := json.Marshal(appReq) + req = httptest.NewRequest(http.MethodPost, "/api/v1/docgov/review-decision", bytes.NewReader(appBytes)) + w = httptest.NewRecorder() + srv.Routes().ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("expected 200 for review approve, got %d", w.Code) + } + + // 13. 批准人批准 + apprReq := DocGovDecisionRequest{ + DocID: "doc_api_test_001", + UserID: 103, + UserName: "王总监", + Action: "approve", + Comment: "准予受控生效", + } + apprBytes, _ := json.Marshal(apprReq) + req = httptest.NewRequest(http.MethodPost, "/api/v1/docgov/approver-decision", bytes.NewReader(apprBytes)) + w = httptest.NewRecorder() + srv.Routes().ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("expected 200 for approver approve, got %d", w.Code) + } + + // 14. 查询审计日志 + req = httptest.NewRequest(http.MethodGet, "/api/v1/docgov/audit-logs?doc_id=doc_api_test_001", nil) + w = httptest.NewRecorder() + srv.Routes().ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("expected 200 from audit logs, got %d", w.Code) + } +}