package server import ( "bufio" "bytes" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "chat-agent-system/internal/bizdev" "chat-agent-system/internal/config" "chat-agent-system/internal/stream" ) func TestE2E_ChatSSEStreamPipeline(t *testing.T) { cfg := &config.Config{ Env: "test", AgentBackend: "stub", DataDir: "./data/test", KbDbPort: 5432, } svc := bizdev.MustNewService(t, cfg) srv := NewServer(cfg, svc) ts := httptest.NewServer(srv.Routes()) defer ts.Close() // 1. 发起 SSE 流式对话请求 reqBody := StreamChatRequest{ SessionID: "e2e_sse_session_01", Message: "查一下当前进展", IncludeDetails: true, ProjectName: "医用高清内窥镜系统", CustomerName: "某三甲医院联合研发中心", DeviceType: "内窥镜", TargetMarket: "国内", } jsonBytes, _ := json.Marshal(reqBody) req, err := http.NewRequest(http.MethodPost, ts.URL+"/api/v1/chat/stream", bytes.NewReader(jsonBytes)) if err != nil { t.Fatalf("create request failed: %v", err) } req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { t.Fatalf("do request failed: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected status 200, got %d", resp.StatusCode) } contentType := resp.Header.Get("Content-Type") if !strings.Contains(contentType, "text/event-stream") { t.Fatalf("expected Content-Type text/event-stream, got %s", contentType) } // 2. 流式解析读取 scanner := bufio.NewScanner(resp.Body) var events []stream.StreamEvent var currentEventType string for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, "event: ") { currentEventType = strings.TrimPrefix(line, "event: ") } else if strings.HasPrefix(line, "data: ") { dataStr := strings.TrimPrefix(line, "data: ") var ev stream.StreamEvent if err := json.Unmarshal([]byte(dataStr), &ev); err == nil { if ev.Type == "" { ev.Type = stream.EventType(currentEventType) } events = append(events, ev) } } } if len(events) == 0 { t.Fatalf("expected streamed events, got 0") } // 3. 验证关键事件存在性与顺序 foundPhase := false foundStatus := false foundDone := false for _, ev := range events { switch ev.Type { case stream.EventPhase: foundPhase = true if ev.Phase == nil || ev.Phase.Message == "" { t.Errorf("expected non-empty phase message") } case stream.EventStatus: foundStatus = true if ev.Status == nil || ev.Status.Intent == "" { t.Errorf("expected non-empty status payload") } case stream.EventDone: foundDone = true } } if !foundPhase { t.Errorf("expected EventPhase in stream") } if !foundStatus { t.Errorf("expected EventStatus in stream") } if !foundDone { t.Errorf("expected EventDone in stream") } } func TestE2E_TasksRunStreamPipeline(t *testing.T) { cfg := &config.Config{ Env: "test", AgentBackend: "stub", DataDir: "./data/test", KbDbPort: 5432, } svc := bizdev.MustNewService(t, cfg) srv := NewServer(cfg, svc) ts := httptest.NewServer(srv.Routes()) defer ts.Close() reqBody := TaskRunRequestDTO{ TaskKey: "intake", ProjectID: 1, ProjectName: "超声内镜系统", CustomerName: "北京水木东方", DeviceType: "超声主机", Backend: "stub", } jsonBytes, _ := json.Marshal(reqBody) resp, err := http.Post(ts.URL+"/api/v1/tasks/run/stream", "application/json", bytes.NewReader(jsonBytes)) if err != nil { t.Fatalf("post tasks run stream failed: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected status 200, got %d", resp.StatusCode) } scanner := bufio.NewScanner(resp.Body) eventCount := 0 hasDeliverable := false hasDone := false for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, "data: ") { eventCount++ var ev stream.StreamEvent dataStr := strings.TrimPrefix(line, "data: ") if err := json.Unmarshal([]byte(dataStr), &ev); err == nil { if ev.Type == stream.EventDeliverable { hasDeliverable = true } if ev.Type == stream.EventDone { hasDone = true } } } } if eventCount == 0 { t.Fatalf("expected task run events, got 0") } if !hasDeliverable { t.Errorf("expected EventDeliverable in task stream") } if !hasDone { t.Errorf("expected EventDone in task stream") } } func TestE2E_ClaimedDocument_SSE_Interception(t *testing.T) { cfg := &config.Config{ Env: "test", AgentBackend: "stub", DataDir: t.TempDir(), KbDbPort: 5432, AdminUsername: "admin", AdminPassword: "admin-secret-password-123", } svc := bizdev.MustNewService(t, cfg) srv := NewServer(cfg, svc) ts := httptest.NewServer(srv.Routes()) defer ts.Close() projectID := int64(999) authorID := int64(101) // 1. 首次触发任务执行 (生成正式产物) taskReq1 := TaskRunRequestDTO{ TaskKey: "feasibility_reg", ProjectName: "智能微创手术机器人", CustomerName: "北京某三甲医院", DeviceType: "腔镜手术机器人", Backend: "stub", 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) } var taskRes1 bizdev.TaskRunResult if err := json.NewDecoder(res1.Body).Decode(&taskRes1); err != nil { t.Fatalf("decode first task run: %v", err) } res1.Body.Close() docID := "doc_proj999_feasibility_reg" if taskRes1.DocRecord != nil && taskRes1.DocRecord.DocID != "" { docID = taskRes1.DocRecord.DocID } // 2. 编制人查阅正文触发认领 claimReq := DocGovClaimRequest{ DocID: docID, UserID: authorID, UserName: "张工", UserRole: "author", } claimBytes, _ := json.Marshal(claimReq) claimHTTP, _ := http.NewRequest(http.MethodPost, ts.URL+"/api/v1/docgov/claim", bytes.NewReader(claimBytes)) claimHTTP.Header.Set("Content-Type", "application/json") withBearerAuth(claimHTTP, loginTestAdmin(t, srv)) resClaim, err := http.DefaultClient.Do(claimHTTP) if err != nil || resClaim.StatusCode != http.StatusOK { t.Fatalf("docgov claim failed: status=%v err=%v", func() int { if resClaim != nil { return resClaim.StatusCode } return 0 }(), 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") } }